updates on the design and the onboarding things
This commit is contained in:
242
src/pages/doormile/clients/ClientLoginDialogs.jsx
Normal file
242
src/pages/doormile/clients/ClientLoginDialogs.jsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user