import React, { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { ArrowLeft, Building2, Copy, Eye, EyeOff, KeyRound, MapPin, Pencil, ShieldCheck, Trash2, UserPlus } from 'lucide-react'; import { Alert, Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, DataTable, EmptyState, Field, Input, PageHeader, Stack, StatusBadge, Surface, Switch, } from '@/components/ds'; import { inputVariants } from '@/components/ui/input'; import { useAuth } from '@/lib/AuthContext'; import { useOnboardClient, useOnboardedClients, useOnboardingCities } from '@/lib/doormileHooks'; import AddressAutocomplete from '@/components/doormile/AddressAutocomplete'; import { EditClientModal, RemoveClientModal } from './ClientLoginDialogs'; import { addressFromPlace, addressPayload, canOnboardClients, generateClientPassword, hasMapLocation, onboardingUnavailableMessage, validateOnboarding, } from '@/lib/clientOnboarding'; /** * Client onboarding — register a new client and give them a console login. * * One submit creates, in a single server transaction, the tenant (the client * company), its console login (doormile_auth, scoped to that tenant) and its * appusers row. The client then signs in at the normal console login and sees * only their own data. * * Restricted to the onboarding owner login (admin@doormile.com). The server * enforces that; this page only hides itself from everyone else. */ const EMPTY = { companyname: '', contactname: '', email: '', phone: '', applocationid: '', password: '', confirm: '', requiredeliveryotp: false, // Main address, saved as the client's primary location. Coordinates come // only from picking a suggestion. address: '', city: '', state: '', pincode: '', latitude: '', longitude: '', }; const SERVER_FIELD = [ ['company name', 'companyname'], ['contact person', 'contactname'], ['email', 'email'], ['mobile', 'phone'], ['password', 'password'], ['operating city', 'applocationid'], ['pincode', 'pincode'], ['address', 'address'], ['map location', 'address'], ]; /** Put a server message under the field it is about, when it names one. */ function serverFieldOf(message) { const m = String(message || '').toLowerCase(); const hit = SERVER_FIELD.find(([needle]) => m.includes(needle)); return hit ? hit[1] : null; } function OnboardedList() { const { data = [], isLoading } = useOnboardedClients(); const [editing, setEditing] = useState(null); const [removing, setRemoving] = useState(null); // Rows without an authid come from a backend older than Edit/Delete (it has // neither the field nor the PUT/DELETE routes). Never send /clients/undefined: // disable the actions and say what to do instead. const backendTooOld = data.length > 0 && data.some((r) => r.authid == null); const columns = [ { key: 'tenantname', header: 'Client', accessor: (r) => r.tenantname }, { key: 'loginemail', header: 'Console login', accessor: (r) => r.loginemail }, { key: 'primarycontact', header: 'Contact', accessor: (r) => r.primarycontact || '—' }, { key: 'address', header: 'Address', accessor: (r) => (r.address ? [r.address, r.pincode].filter(Boolean).join(' · ') : '—'), }, { key: 'status', header: 'Status', cell: (r) => }, { key: 'logincreatedat', header: 'Login created', accessor: (r) => (r.logincreatedat ? new Date(r.logincreatedat).toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }) : '—'), }, { key: 'actions', header: 'Actions', align: 'right', cell: (r) => (
), }, ]; return (

Clients with a console login

{backendTooOld && ( The API server this console talks to is running an older backend build without the edit and delete endpoints. Rebuild and redeploy doormile_backend from the current code, then reload this page. )} row.authid ?? `${row.tenantid}-${row.loginemail}-${i}`} loading={isLoading} emptyState={} /> setEditing(null)} /> setRemoving(null)} />
); } export default function ClientOnboarding() { const navigate = useNavigate(); const { user, isClient } = useAuth(); const allowed = canOnboardClients(user, isClient); const onboard = useOnboardClient(); const citiesQ = useOnboardingCities({ enabled: allowed, retry: false }); const locations = citiesQ.data || []; // When the city list cannot be read, onboarding cannot work on this server // either (same backend, same gate) — say why instead of an empty dropdown. const unavailable = citiesQ.isError ? onboardingUnavailableMessage(citiesQ.error) : ''; const cities = useMemo(() => locations.filter((l) => Number(l.applocationid) > 0), [locations]); const [form, setForm] = useState(EMPTY); const [errors, setErrors] = useState({}); const [formError, setFormError] = useState(''); const [showPassword, setShowPassword] = useState(false); const [created, setCreated] = useState(null); // { tenant, login, password } const [copied, setCopied] = useState(false); const set = (key) => (value) => { setForm((f) => ({ ...f, [key]: value })); setErrors((e) => ({ ...e, [key]: undefined })); }; if (!allowed) { return ( navigate('/doormile/home') } : { label: 'Back to clients', onClick: () => navigate('/doormile/tenants') } } /> ); } const submit = (event) => { event.preventDefault(); setFormError(''); const nextErrors = validateOnboarding(form); setErrors(nextErrors); if (Object.keys(nextErrors).length) return; const password = form.password; onboard.mutate( { companyname: form.companyname.trim(), contactname: form.contactname.trim(), email: form.email.trim().toLowerCase(), phone: form.phone.replace(/\D/g, ''), password, applocationid: Number(form.applocationid), requiredeliveryotp: form.requiredeliveryotp, ...addressPayload(form), }, { onSuccess: (res) => { if (res?.success === false) { setFormError(res.message || 'The client was not created.'); return; } setCreated({ ...res.data, password }); setForm(EMPTY); setCopied(false); }, onError: (err) => { const status = err?.response?.status; const message = err?.response?.data?.message; if (status === 403) { setFormError('Your login is not allowed to onboard clients.'); return; } const field = serverFieldOf(message); if (field && message) setErrors((e) => ({ ...e, [field]: message })); else setFormError(message || 'The client could not be created. Nothing was saved.'); }, } ); }; const copyLogin = async () => { if (!created) return; const text = [ `Doormile console: ${window.location.origin}/login`, `Email: ${created.login.email}`, `Password: ${created.password}`, ].join('\n'); try { await navigator.clipboard.writeText(text); setCopied(true); } catch { setCopied(false); } }; return ( Clients Onboard client } actions={ } /> {created && (

Console login {created.login.email} is ready. They sign in at the usual console login and see only their own orders.

This is the only time the password is shown — share it with the client securely.

)}

Onboard a new client

Creates the client and their console login together. If any part fails, nothing is saved.

{unavailable && ( citiesQ.refetch()}> Try again } > {unavailable} )} {formError && ( {formError} )}
set('companyname')(e.target.value)} placeholder="Peelamedu Provisions Pvt Ltd" maxLength={120} /> set('contactname')(e.target.value)} placeholder="Priya Raman" maxLength={80} /> set('phone')(e.target.value.replace(/\D/g, '').slice(0, 10))} placeholder="9876543210" inputMode="numeric" /> set('email')(e.target.value)} placeholder="ops@client.com" autoComplete="off" />
set('address')(text)} onPlaceSelected={(place) => { setForm((f) => ({ ...f, ...addressFromPlace(place) })); setErrors((e) => ({ ...e, address: undefined, pincode: undefined })); }} onPlaceCleared={() => setForm((f) => ({ ...f, latitude: '', longitude: '' }))} /> {errors.address ? (

{errors.address}

) : (

{hasMapLocation(form) ? "Location pinned. This becomes the client's pickup point and zone." : 'Pick a suggestion so the address gets a map location.'}

)}
set('pincode')(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="641002" inputMode="numeric" /> set('city')(e.target.value)} placeholder="Coimbatore" maxLength={80} /> set('password')(e.target.value)} autoComplete="new-password" maxLength={72} /> set('confirm')(e.target.value)} autoComplete="new-password" maxLength={72} />
); }