updates on the design and the onboarding things

This commit is contained in:
2026-09-30 14:45:49 +05:30
parent 53199127ba
commit ede1731fa5
11 changed files with 907 additions and 336 deletions

View File

@@ -0,0 +1,242 @@
import React, { useEffect, useState } from 'react';
import { KeyRound, Pencil, Trash2 } from 'lucide-react';
import { Alert, Button, Field, Input, Modal, Switch } from '@/components/ds';
import { inputVariants } from '@/components/ui/input';
import { useDeleteOnboardedClient, useUpdateOnboardedClient } from '@/lib/doormileHooks';
import { generateClientPassword } from '@/lib/clientOnboarding';
/**
* Edit and Remove for one onboarded client login (a row of "Clients with a
* console login"). Both call owner-only endpoints: PUT and DELETE
* /admin/clients/:authid.
*/
const STATUSES = ['Active', 'Pending', 'Inactive'];
/** Only the fields that actually changed, so an edit never rewrites the rest. */
export function changedFields(row, form) {
const out = {};
const trim = (v) => String(v ?? '').trim();
if (trim(form.companyname) !== trim(row.tenantname)) out.companyname = trim(form.companyname);
if (trim(form.contactname) !== trim(row.contactname)) out.contactname = trim(form.contactname);
if (trim(form.email).toLowerCase() !== trim(row.loginemail).toLowerCase()) out.email = trim(form.email).toLowerCase();
if (form.phone.replace(/\D/g, '') !== String(row.primarycontact || '').replace(/\D/g, '')) out.phone = form.phone.replace(/\D/g, '');
if (form.status !== row.status) out.status = form.status;
if (Boolean(form.requiredeliveryotp) !== Boolean(row.requiredeliveryotp)) out.requiredeliveryotp = Boolean(form.requiredeliveryotp);
if (form.password) out.password = form.password;
return out;
}
export function validateEdit(form, changes) {
const errors = {};
if ('companyname' in changes && changes.companyname.length < 2) errors.companyname = 'Enter the company name';
if ('contactname' in changes && changes.contactname.length < 2) errors.contactname = "Enter the contact person's name";
if ('email' in changes && !/^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(changes.email)) errors.email = 'Enter a valid email address';
if ('phone' in changes && !/^[6-9]\d{9}$/.test(changes.phone)) errors.phone = 'Enter a 10-digit mobile number';
if (form.password && form.password.length < 8) errors.password = 'At least 8 characters';
if (form.password && form.password.length > 72) errors.password = 'At most 72 characters';
return errors;
}
function formFrom(row) {
return {
companyname: row?.tenantname || '',
contactname: row?.contactname || '',
email: row?.loginemail || '',
phone: String(row?.primarycontact || '').replace(/\D/g, ''),
status: STATUSES.includes(row?.status) ? row.status : 'Active',
requiredeliveryotp: Boolean(row?.requiredeliveryotp),
password: '',
};
}
export function EditClientModal({ row, onClose }) {
const update = useUpdateOnboardedClient();
const [form, setForm] = useState(formFrom(row));
const [errors, setErrors] = useState({});
const [serverError, setServerError] = useState('');
const [showPassword, setShowPassword] = useState(false);
useEffect(() => {
setForm(formFrom(row));
setErrors({});
setServerError('');
setShowPassword(false);
}, [row]);
const set = (key) => (value) => {
setForm((f) => ({ ...f, [key]: value }));
setErrors((e) => ({ ...e, [key]: undefined }));
};
const changes = row ? changedFields(row, form) : {};
const nothingChanged = Object.keys(changes).length === 0;
const save = () => {
const nextErrors = validateEdit(form, changes);
setErrors(nextErrors);
setServerError('');
if (Object.keys(nextErrors).length || nothingChanged) return;
update.mutate(
{ authid: row.authid, data: changes },
{
onSuccess: (res) => {
if (res?.success !== false) onClose();
},
onError: (err) => setServerError(err?.response?.data?.message || 'The change was not saved.'),
}
);
};
return (
<Modal
open={Boolean(row)}
onOpenChange={(open) => !open && onClose()}
title={`Edit ${row?.tenantname || 'client'}`}
description="Changes the client and its console login together."
icon={Pencil}
busy={update.isPending}
footer={
<>
<Button variant="ghost" onClick={onClose} disabled={update.isPending}>
Cancel
</Button>
<Button onClick={save} loading={update.isPending} disabled={nothingChanged}>
Save changes
</Button>
</>
}
>
{serverError && (
<Alert tone="destructive" className="mb-4" role="alert">
{serverError}
</Alert>
)}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Field label="Company name" error={errors.companyname} className="sm:col-span-2">
<Input value={form.companyname} maxLength={120} onChange={(e) => set('companyname')(e.target.value)} />
</Field>
<Field label="Contact person" error={errors.contactname}>
<Input value={form.contactname} maxLength={80} onChange={(e) => set('contactname')(e.target.value)} />
</Field>
<Field label="Mobile number" error={errors.phone}>
<Input
value={form.phone}
inputMode="numeric"
onChange={(e) => set('phone')(e.target.value.replace(/\D/g, '').slice(0, 10))}
/>
</Field>
<Field label="Login email" error={errors.email} hint="Changing it changes what they sign in with">
<Input type="email" value={form.email} autoComplete="off" onChange={(e) => set('email')(e.target.value)} />
</Field>
<Field label="Status">
<select value={form.status} onChange={(e) => set('status')(e.target.value)} className={inputVariants()}>
{STATUSES.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
</Field>
<Field
label="New password"
error={errors.password}
hint="Leave empty to keep the current password"
htmlFor="edit-client-password"
className="sm:col-span-2"
>
<Input
id="edit-client-password"
type={showPassword ? 'text' : 'password'}
value={form.password}
maxLength={72}
autoComplete="new-password"
onChange={(e) => set('password')(e.target.value)}
/>
</Field>
<div className="flex flex-wrap gap-2 sm:col-span-2">
<Button
type="button"
size="sm"
variant="outline"
onClick={() => {
set('password')(generateClientPassword());
setShowPassword(true);
}}
>
<KeyRound className="mr-1.5 h-4 w-4" /> Generate new password
</Button>
{form.password && (
<Button type="button" size="sm" variant="ghost" onClick={() => setShowPassword((s) => !s)}>
{showPassword ? 'Hide' : 'Show'} password
</Button>
)}
</div>
<Field label="Require delivery OTP" inline className="sm:col-span-2">
<Switch checked={form.requiredeliveryotp} onCheckedChange={set('requiredeliveryotp')} />
</Field>
</div>
</Modal>
);
}
export function RemoveClientModal({ row, onClose }) {
const remove = useDeleteOnboardedClient();
const [typed, setTyped] = useState('');
const [serverError, setServerError] = useState('');
useEffect(() => {
setTyped('');
setServerError('');
}, [row]);
const confirmed = row && typed.trim().toLowerCase() === String(row.tenantname).trim().toLowerCase();
const submit = () => {
if (!confirmed) return;
remove.mutate(row.authid, {
onSuccess: (res) => {
if (res?.success !== false) onClose();
},
onError: (err) => setServerError(err?.response?.data?.message || 'The login was not removed.'),
});
};
return (
<Modal
open={Boolean(row)}
onOpenChange={(open) => !open && onClose()}
title={`Remove ${row?.tenantname || 'client'}'s login`}
icon={Trash2}
busy={remove.isPending}
footer={
<>
<Button variant="ghost" onClick={onClose} disabled={remove.isPending}>
Cancel
</Button>
<Button variant="destructive" onClick={submit} loading={remove.isPending} disabled={!confirmed}>
Remove login
</Button>
</>
}
>
{serverError && (
<Alert tone="destructive" className="mb-4" role="alert">
{serverError}
</Alert>
)}
<div className="space-y-3 text-body-sm text-ink-2">
<p>
<span className="font-semibold">{row?.loginemail}</span> will no longer be able to sign in to the console.
</p>
<p>
The client&apos;s orders, customers and pricing are kept. If this is its last login, the client is marked{' '}
<span className="font-semibold">Inactive</span>. A session already signed in ends when its token expires.
</p>
<Field label={`Type "${row?.tenantname || ''}" to confirm`}>
<Input value={typed} onChange={(e) => setTyped(e.target.value)} autoComplete="off" />
</Field>
</div>
</Modal>
);
}