updates on the astryx desing

This commit is contained in:
2026-08-14 16:09:14 +05:30
parent d3dd687a8d
commit a46212f291
106 changed files with 10244 additions and 24738 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,28 @@
import { useEffect, useState } from 'react';
// material-ui
import {
Autocomplete,
Avatar,
Button,
Grid,
InputLabel,
MenuItem,
Paper,
Select,
Stack,
TextField,
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import { MdPersonAddAlt1 } from 'react-icons/md';
import { useNavigate } from 'react-router';
import { useMutation, useQuery } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import { MdPersonAddAlt1 } from 'react-icons/md';
import { Grid } from '@astryxdesign/core/Grid';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { Card } from '@astryxdesign/core/Card';
import { Text } from '@astryxdesign/core/Text';
import { Button } from '@astryxdesign/core/Button';
import { TextInput } from '@astryxdesign/core/TextInput';
import { Selector } from '@astryxdesign/core/Selector';
import { Divider } from '@astryxdesign/core/Divider';
// project import
import MainCard from 'components/MainCard';
import { createTenantCustomer, getAdminTenants } from 'pages/api/doormileApi';
import Loader from 'components/Loader';
import PageHeader from 'components/nearle_components/PageHeader';
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router';
import { DT, tint } from 'themes/dt/tokens';
import { createTenantCustomer, getAdminTenants } from 'pages/api/doormileApi';
import { DT } from 'themes/dt/tokens';
import { AccentAvatar } from 'themes/dt/primitives';
const BRAND = DT.brand;
// Same field set as nearle_console_express's createCustomer.js (Location,
// Choose Client, Name, Phone, Email, Door No, Address, Location/City/State/
@@ -43,8 +38,6 @@ import { DT, tint } from 'themes/dt/tokens';
// ignored server-side — unconfirmed, this endpoint isn't in the "confirmed
// end-to-end" list.
const Createcustomer = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const navigate = useNavigate();
const loggedInTenantId = localStorage.getItem('tenantid');
@@ -66,11 +59,12 @@ const Createcustomer = () => {
const [appLocaLat, setAppLocaLat] = useState();
const [appLocaLng, setAppLocaLng] = useState();
const { data: tenantList = [], isLoading: tenantListLoading } = useQuery({
const { data: tenantList = [] } = useQuery({
queryKey: ['admin-tenants-picker'],
queryFn: getAdminTenants,
enabled: isStaffLogin
});
const tenantOptions = (tenantList || []).map((t) => ({ value: String(t.tenantid), label: t.tenantname || `Tenant #${t.tenantid}` }));
// LocationAutocomplete only reports back applocationid/locationname — it
// caches the full hub list (with lat/lng) in localStorage under a
@@ -125,13 +119,8 @@ const Createcustomer = () => {
});
};
const opentoast = (message) => {
enqueueSnackbar(message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
};
const opentoast = (message) =>
enqueueSnackbar(message, { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 });
const createCustomerMutation = useMutation({
mutationFn: (payload) => createTenantCustomer(payload),
@@ -147,9 +136,7 @@ const Createcustomer = () => {
opentoast(res.message || 'Could not create customer');
}
},
onError: (err) => {
opentoast(err.response?.data?.message || err.message || 'Failed to create customer');
}
onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to create customer')
});
const createprofile = () => {
@@ -174,238 +161,86 @@ const Createcustomer = () => {
<>
{createCustomerMutation.isPending && <Loader />}
<Grid item xs={12} sx={{ mb: 2 }}>
<Paper
sx={{
p: 2.5,
borderRadius: DT.radiusCard + 'px',
boxShadow: DT.shadowSoft,
border: '1px solid',
borderColor: DT.borderSubtle,
background: `linear-gradient(135deg, ${tint('#000000')} 0%, ${tint('#3A3A3A')} 100%)`
}}
>
<Stack direction="row" spacing={1.5} alignItems="center">
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
<MdPersonAddAlt1 size={22} />
</Avatar>
<Typography variant="h3">Create Customer</Typography>
</Stack>
</Paper>
</Grid>
<MainCard
padding={6}
>
<Grid container spacing={isMobile ? 2 : 3}>
<Grid item xs={12}>
<MainCard height="100%" padding={5}>
<Grid container spacing={isMobile ? 2 : 3}>
{/* ===================================================== || Location & Client || ===================================================== */}
<Grid item xs={12} md={6}>
<Stack spacing={1.25}>
<InputLabel>Location</InputLabel>
<LocationAutocomplete locaName={locaName} setAppId={setAppId} setLocoName={setLocoName} />
</Stack>
</Grid>
{isStaffLogin && (
<Grid item xs={12} md={6}>
<Stack spacing={1.25}>
<InputLabel>Choose Client</InputLabel>
<Autocomplete
fullWidth
loading={tenantListLoading}
options={tenantList || []}
getOptionLabel={(option) => option?.tenantname || ''}
renderInput={(params) => <TextField {...params} placeholder="Choose Client" />}
onChange={(e, val) => setTid(val ? val.tenantid : '')}
/>
</Stack>
</Grid>
)}
<PageHeader
title="Create Customer"
subtitle="Add a customer record"
action={
<AccentAvatar color={BRAND} selected size={40}>
<MdPersonAddAlt1 size={20} />
</AccentAvatar>
}
/>
{/* ===================================================== || Name / Phone / Email || ===================================================== */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="customer-firstname">Name</InputLabel>
<TextField
fullWidth
id="customer-firstname"
placeholder="First Name"
onChange={(e) => setFirstname(e.target.value)}
value={firstname}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="customer-lastname">Last Name</InputLabel>
<TextField
fullWidth
id="customer-lastname"
placeholder="Last Name"
onChange={(e) => setLastname(e.target.value)}
value={lastname}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="customer-phone">Phone Number</InputLabel>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
<MenuItem value="+1">+91</MenuItem>
</Select>
<TextField
type="number"
id="customer-phone"
fullWidth
placeholder="Phone Number"
onChange={(e) => {
if (e.target.value.toString().length <= 10) {
setPhone(e.target.value);
}
}}
value={phone}
autoComplete="off"
/>
</Stack>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="customer-email">Email</InputLabel>
<TextField
type="email"
fullWidth
id="customer-email"
placeholder="Email Address"
onChange={(e) => setEmail(e.target.value)}
value={email}
autoComplete="off"
/>
</Stack>
</Grid>
{/* ===================================================== || Door No || ===================================================== */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="customer-doorno">Door No</InputLabel>
<TextField
fullWidth
id="customer-doorno"
placeholder="Door No"
onChange={(e) => setDoorno(e.target.value)}
value={doorno}
autoComplete="off"
/>
</Stack>
</Grid>
{/* ===================================================== || Address section — collected, not persisted || ===================================================== */}
<Grid item xs={12}>
<Typography variant="caption" sx={{ color: DT.textMuted, fontStyle: 'italic' }}>
The backend doesn&apos;t store an address against a customer yet — these fields aren&apos;t required and won&apos;t be saved on submit.
</Typography>
</Grid>
<Grid item xs={12}>
<Stack spacing={1.25}>
<InputLabel>Address</InputLabel>
<AddressAutocomplete
id="customer-address"
fullWidth
value={inputValue2}
onChange={(text) => setInputValue2(text)}
onPlaceSelected={handlePlaceSelected}
bias={{ lat: appLocaLat, lng: appLocaLng }}
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel>Location</InputLabel>
<TextField
fullWidth
placeholder="Location"
onChange={(e) => setPickCust((p) => ({ ...p, suburb: e.target.value }))}
value={pickCust.suburb || ''}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel>City</InputLabel>
<TextField
fullWidth
placeholder="City"
onChange={(e) => setPickCust((p) => ({ ...p, city: e.target.value }))}
value={pickCust.city || ''}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel>State</InputLabel>
<TextField
fullWidth
placeholder="State"
onChange={(e) => setPickCust((p) => ({ ...p, state: e.target.value }))}
value={pickCust.state || ''}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel>Post Code</InputLabel>
<TextField
fullWidth
type="number"
placeholder="Postcode"
onChange={(e) => setPickCust((p) => ({ ...p, postcode: e.target.value }))}
value={pickCust.postcode || ''}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1.25}>
<InputLabel>Landmark</InputLabel>
<TextField fullWidth placeholder="Landmark" onChange={(e) => setLandmark(e.target.value)} value={landmark} autoComplete="off" />
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel>Latitude</InputLabel>
<TextField fullWidth disabled value={startPoint.latitude || ''} sx={{ cursor: 'not-allowed' }} />
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel>Longitude</InputLabel>
<TextField fullWidth disabled value={startPoint.longitude || ''} sx={{ cursor: 'not-allowed' }} />
</Stack>
</Grid>
</Grid>
</MainCard>
<Card padding={4} elevation="none" style={{ marginTop: 16 }}>
<VStack gap={4} padding={0}>
<Grid columns={{ minWidth: 260 }} gap={3}>
<VStack gap={1.25} padding={0}>
<Text type="label" weight="semibold" color="secondary">
Location
</Text>
<LocationAutocomplete locaName={locaName} setAppId={setAppId} setLocoName={setLocoName} />
</VStack>
{isStaffLogin && (
<Selector
label="Choose Client"
options={tenantOptions}
value={tid ? String(tid) : ''}
onChange={(v) => setTid(v || '')}
placeholder="Choose Client"
hasSearch
/>
)}
<TextInput label="Name" placeholder="First Name" value={firstname} onChange={setFirstname} isRequired />
<TextInput label="Last Name" placeholder="Last Name" value={lastname} onChange={setLastname} isOptional />
<TextInput
label="Phone Number"
placeholder="Phone Number"
value={phone}
onChange={(v) => {
const digits = v.replace(/\D/g, '');
if (digits.length <= 10) setPhone(digits);
}}
isRequired
/>
<TextInput label="Email" type="email" placeholder="Email Address" value={email} onChange={setEmail} isOptional />
<TextInput label="Door No" placeholder="Door No" value={doorno} onChange={setDoorno} isOptional />
</Grid>
<Grid item xs={12}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="flex-end"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={2}
>
<Button variant="contained" onClick={createprofile} fullWidth={isMobile}>
Create
</Button>
</Stack>
<Divider />
<Text type="supporting" color="secondary" style={{ fontStyle: 'italic' }}>
The backend doesn&apos;t store an address against a customer yet — these fields aren&apos;t required and won&apos;t be saved on submit.
</Text>
<AddressAutocomplete
id="customer-address"
label="Address"
value={inputValue2}
onChange={setInputValue2}
onPlaceSelected={handlePlaceSelected}
bias={{ lat: appLocaLat, lng: appLocaLng }}
/>
<Grid columns={{ minWidth: 220 }} gap={3}>
<TextInput label="Location" placeholder="Location" value={pickCust.suburb || ''} onChange={(v) => setPickCust((p) => ({ ...p, suburb: v }))} />
<TextInput label="City" placeholder="City" value={pickCust.city || ''} onChange={(v) => setPickCust((p) => ({ ...p, city: v }))} />
<TextInput label="State" placeholder="State" value={pickCust.state || ''} onChange={(v) => setPickCust((p) => ({ ...p, state: v }))} />
<TextInput
label="Post Code"
placeholder="Postcode"
value={pickCust.postcode || ''}
onChange={(v) => setPickCust((p) => ({ ...p, postcode: v.replace(/\D/g, '') }))}
/>
<TextInput label="Landmark" placeholder="Landmark" value={landmark} onChange={setLandmark} />
<TextInput label="Latitude" value={startPoint.latitude || ''} isDisabled />
<TextInput label="Longitude" value={startPoint.longitude || ''} isDisabled />
</Grid>
</Grid>
</MainCard>
<HStack justify="end">
<Button label="Create" variant="primary" onClick={createprofile} isLoading={createCustomerMutation.isPending} />
</HStack>
</VStack>
</Card>
</>
);
};

View File

@@ -1,36 +1,31 @@
import { useState } from 'react';
// material-ui
import {
Avatar,
Button,
FormControlLabel,
Grid,
InputLabel,
MenuItem,
Paper,
Select,
Stack,
Switch,
TextField,
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import { MdPersonAddAlt1 } from 'react-icons/md';
import { useMutation } from '@tanstack/react-query';
// project import
import MainCard from 'components/MainCard';
import { createAdminTenant } from 'pages/api/doormileApi';
import Loader from 'components/Loader';
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router';
import { DT, tint } from 'themes/dt/tokens';
import { useMutation } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import { MdPersonAddAlt1 } from 'react-icons/md';
import { Grid } from '@astryxdesign/core/Grid';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { Card } from '@astryxdesign/core/Card';
import { Button } from '@astryxdesign/core/Button';
import { TextInput } from '@astryxdesign/core/TextInput';
import { Selector } from '@astryxdesign/core/Selector';
import { Switch } from '@astryxdesign/core/Switch';
import Loader from 'components/Loader';
import PageHeader from 'components/nearle_components/PageHeader';
import { createAdminTenant } from 'pages/api/doormileApi';
import { DT } from 'themes/dt/tokens';
import { AccentAvatar } from 'themes/dt/primitives';
const BRAND = DT.brand;
const STATUS_OPTIONS = [
{ value: 'Active', label: 'Active' },
{ value: 'Inactive', label: 'Inactive' }
];
const Createclient = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const navigate = useNavigate();
const [tenantname, setTenantname] = useState('');
@@ -43,13 +38,8 @@ const Createclient = () => {
const [status, setStatus] = useState('Active');
const [requiredeliveryotp, setRequiredeliveryotp] = useState(false);
const opentoast = (message) => {
enqueueSnackbar(message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
};
const opentoast = (message) =>
enqueueSnackbar(message, { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 });
const createTenantMutation = useMutation({
mutationFn: (payload) => createAdminTenant(payload),
@@ -65,9 +55,7 @@ const Createclient = () => {
opentoast(res.message || 'Client already exists');
}
},
onError: (err) => {
opentoast(err.response?.data?.message || err.message || 'Failed to create client');
}
onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to create client')
});
const createprofile = () => {
@@ -97,114 +85,45 @@ const Createclient = () => {
<>
{createTenantMutation.isPending && <Loader />}
<Grid item xs={12} sx={{ mb: 2 }}>
<Paper
sx={{
p: 2.5,
borderRadius: DT.radiusCard + 'px',
boxShadow: DT.shadowSoft,
border: '1px solid',
borderColor: DT.borderSubtle,
background: `linear-gradient(135deg, ${tint('#000000')} 0%, ${tint('#3A3A3A')} 100%)`
}}
>
<Stack direction="row" spacing={1.5} alignItems="center">
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
<MdPersonAddAlt1 size={22} />
</Avatar>
<Typography variant="h3">Create Client</Typography>
</Stack>
</Paper>
</Grid>
<MainCard
padding={6}
>
<Grid container spacing={isMobile ? 2 : 3}>
<Grid item xs={12}>
<MainCard height="100%" padding={5}>
<Grid container spacing={isMobile ? 2 : 3}>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="tenant-name">Client / Business Name</InputLabel>
<TextField
fullWidth
id="tenant-name"
placeholder="e.g. DailyGrubs"
onChange={(e) => setTenantname(e.target.value)}
value={tenantname}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="tenant-status">Status</InputLabel>
<Select id="tenant-status" fullWidth value={status} onChange={(e) => setStatus(e.target.value)}>
<MenuItem value="Active">Active</MenuItem>
<MenuItem value="Inactive">Inactive</MenuItem>
</Select>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="tenant-contact">Primary Contact Number</InputLabel>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
<MenuItem value="+1">+91</MenuItem>
</Select>
<TextField
type="number"
id="tenant-contact"
fullWidth
placeholder="Contact Number"
onChange={(e) => {
if (e.target.value.toString().length <= 10) {
setPrimarycontact(e.target.value);
}
}}
value={primarycontact}
autoComplete="off"
/>
</Stack>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="tenant-email">Primary Email</InputLabel>
<TextField
type="email"
fullWidth
id="tenant-email"
placeholder="Primary Email Address"
onChange={(e) => setPrimaryemail(e.target.value)}
value={primaryemail}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={<Switch checked={requiredeliveryotp} onChange={(e) => setRequiredeliveryotp(e.target.checked)} />}
label="Require delivery OTP for this client (off by default)"
/>
</Grid>
</Grid>
</MainCard>
<PageHeader
title="Create Client"
subtitle="Add a new tenant"
action={
<AccentAvatar color={BRAND} selected size={40}>
<MdPersonAddAlt1 size={20} />
</AccentAvatar>
}
/>
<Card padding={4} elevation="none" style={{ marginTop: 16 }}>
<VStack gap={4} padding={0}>
<Grid columns={{ minWidth: 260 }} gap={3}>
<TextInput label="Client / Business Name" placeholder="e.g. DailyGrubs" value={tenantname} onChange={setTenantname} isRequired />
<Selector label="Status" options={STATUS_OPTIONS} value={status} onChange={setStatus} />
<TextInput
label="Primary Contact Number"
placeholder="Contact Number"
value={primarycontact}
onChange={(v) => {
const digits = v.replace(/\D/g, '');
if (digits.length <= 10) setPrimarycontact(digits);
}}
isRequired
/>
<TextInput label="Primary Email" type="email" placeholder="Primary Email Address" value={primaryemail} onChange={setPrimaryemail} isRequired />
</Grid>
<Grid item xs={12}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="flex-end"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={2}
>
<Button variant="contained" onClick={createprofile} fullWidth={isMobile}>
Create
</Button>
</Stack>
</Grid>
</Grid>
</MainCard>
<Switch
label="Require delivery OTP for this client (off by default)"
value={requiredeliveryotp}
onChange={setRequiredeliveryotp}
/>
<HStack justify="end">
<Button label="Create" variant="primary" onClick={createprofile} isLoading={createTenantMutation.isPending} />
</HStack>
</VStack>
</Card>
</>
);
};