/
opt
/
mawid
/
apps
/
dashboard
/
app
/
(app)
/
settings
/
/opt/mawid/apps/dashboard/app/(app)/settings
mkdir
upload
Name
Size
Mode
Actions
page.tsx
14530
0644
edit
dl
rm
Edit:
/opt/mawid/apps/dashboard/app/(app)/settings/page.tsx
(14530B)
'use client'; import { useEffect, useState } from 'react'; import { api, serviceName } from '@/lib/api'; import { useLang, type UiKey } from '@/lib/i18n'; import { Button, Card, Field, Input, Select, Spinner } from '@/components/ui'; type Interval = { start: string; end: string }; type WeekHours = Partial<Record<string, Interval[]>>; interface Clinic { id: string; name: string; phone: string; timezone: string; defaultLanguage: string; workingHours: WeekHours; settings: { reminderOffsetsHours?: number[]; waitlistHoldMinutes?: number; ownerWaPhone?: string; depositEnabled?: boolean; }; } interface ServiceRow { id: string; name: { ar?: string; tr?: string; en?: string }; durationMinutes: number; price: string | number | null; active: boolean; } interface StaffRow { id: string; name: string; role: string; active: boolean; services: { id: string }[]; } const DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const; export default function SettingsPage() { const { lang } = useLang(); const [clinic, setClinic] = useState<Clinic | null>(null); const [services, setServices] = useState<ServiceRow[]>([]); const [staff, setStaff] = useState<StaffRow[]>([]); const [flash, setFlash] = useState<string | null>(null); async function loadAll() { const [c, sv, st] = await Promise.all([ api<Clinic>('/clinic'), api<ServiceRow[]>('/services'), api<StaffRow[]>('/staff'), ]); setClinic(c); setServices(sv); setStaff(st); } useEffect(() => { loadAll(); }, []); function flashSaved(section: string) { setFlash(section); setTimeout(() => setFlash(null), 1500); } if (!clinic) return <Spinner />; async function saveClinic(patch: object, section: string) { const updated = await api<Clinic>('/clinic', { method: 'PATCH', body: JSON.stringify(patch) }); setClinic(updated); flashSaved(section); } return ( <div className="space-y-4"> <ClinicInfoCard clinic={clinic} onSave={saveClinic} saved={flash === 'info'} /> <WorkingHoursCard clinic={clinic} onSave={saveClinic} saved={flash === 'hours'} /> <ServicesCard services={services} lang={lang} onChanged={loadAll} /> <StaffCard staff={staff} services={services} lang={lang} onChanged={loadAll} /> </div> ); } function ClinicInfoCard({ clinic, onSave, saved, }: { clinic: Clinic; onSave: (patch: object, section: string) => Promise<void>; saved: boolean; }) { const { t } = useLang(); const [name, setName] = useState(clinic.name); const [phone, setPhone] = useState(clinic.phone); const [timezone, setTimezone] = useState(clinic.timezone); const [defaultLanguage, setDefaultLanguage] = useState(clinic.defaultLanguage); const [ownerWaPhone, setOwnerWaPhone] = useState(clinic.settings.ownerWaPhone ?? ''); const [offsets, setOffsets] = useState( (clinic.settings.reminderOffsetsHours ?? [24, 2]).join(', '), ); function save() { const reminderOffsetsHours = offsets .split(',') .map((s) => Number(s.trim())) .filter((n) => !Number.isNaN(n) && n > 0); onSave( { name, phone, timezone, defaultLanguage, // settings are replaced wholesale — merge the existing object settings: { ...clinic.settings, ownerWaPhone: ownerWaPhone || undefined, reminderOffsetsHours, }, }, 'info', ); } return ( <Card className="space-y-3"> <h2 className="text-sm font-semibold text-slate-500">{t('clinicInfo')}</h2> <div className="grid gap-3 sm:grid-cols-2"> <Field label={t('clinicName')}> <Input value={name} onChange={(e) => setName(e.target.value)} /> </Field> <Field label={t('phone')}> <Input value={phone} onChange={(e) => setPhone(e.target.value)} dir="ltr" /> </Field> <Field label={t('timezone')}> <Input value={timezone} onChange={(e) => setTimezone(e.target.value)} dir="ltr" /> </Field> <Field label={t('defaultLanguage')}> <Select value={defaultLanguage} onChange={(e) => setDefaultLanguage(e.target.value)}> <option value="tr">Türkçe</option> <option value="ar">العربية</option> <option value="en">English</option> </Select> </Field> <Field label={t('ownerWaPhone')}> <Input value={ownerWaPhone} onChange={(e) => setOwnerWaPhone(e.target.value)} dir="ltr" /> </Field> <Field label={t('reminderOffsets')}> <Input value={offsets} onChange={(e) => setOffsets(e.target.value)} dir="ltr" /> </Field> </div> <Button onClick={save}>{saved ? t('saved') : t('save')}</Button> </Card> ); } function WorkingHoursCard({ clinic, onSave, saved, }: { clinic: Clinic; onSave: (patch: object, section: string) => Promise<void>; saved: boolean; }) { const { t } = useLang(); const [hours, setHours] = useState<WeekHours>(clinic.workingHours ?? {}); function setDay(day: string, intervals: Interval[]) { setHours((prev) => ({ ...prev, [day]: intervals })); } return ( <Card className="space-y-3"> <h2 className="text-sm font-semibold text-slate-500">{t('workingHours')}</h2> <div className="space-y-2"> {DAYS.map((day) => { const intervals = hours[day] ?? []; return ( <div key={day} className="flex flex-wrap items-center gap-2"> <span className="w-16 text-sm font-medium">{t(`day_${day}` as UiKey)}</span> {intervals.map((interval, i) => ( <span key={i} className="flex items-center gap-1"> <Input type="time" value={interval.start} className="!w-28" onChange={(e) => setDay( day, intervals.map((v, j) => (j === i ? { ...v, start: e.target.value } : v)), ) } /> <span>–</span> <Input type="time" value={interval.end} className="!w-28" onChange={(e) => setDay( day, intervals.map((v, j) => (j === i ? { ...v, end: e.target.value } : v)), ) } /> <button onClick={() => setDay(day, intervals.filter((_, j) => j !== i))} className="px-1 text-slate-400 hover:text-red-600" > ✕ </button> </span> ))} <button onClick={() => setDay(day, [...intervals, { start: '09:00', end: '18:00' }])} className="text-sm text-emerald-600 hover:text-emerald-700" > + {t('addInterval')} </button> </div> ); })} </div> <Button onClick={() => onSave({ workingHours: hours }, 'hours')}> {saved ? t('saved') : t('save')} </Button> </Card> ); } function ServicesCard({ services, lang, onChanged, }: { services: ServiceRow[]; lang: string; onChanged: () => void; }) { const { t } = useLang(); const [editing, setEditing] = useState<Record<string, Partial<ServiceRow>>>({}); const [adding, setAdding] = useState(false); const [newService, setNewService] = useState({ tr: '', ar: '', en: '', duration: 30, price: '' }); async function saveRow(row: ServiceRow) { const patch = editing[row.id]; if (!patch) return; await api(`/services/${row.id}`, { method: 'PATCH', body: JSON.stringify({ ...(patch.name ? { name: { ...row.name, ...patch.name } } : {}), ...(patch.durationMinutes !== undefined ? { durationMinutes: patch.durationMinutes } : {}), ...(patch.active !== undefined ? { active: patch.active } : {}), }), }); setEditing((prev) => ({ ...prev, [row.id]: {} })); onChanged(); } async function addService() { await api('/services', { method: 'POST', body: JSON.stringify({ name: { tr: newService.tr, ar: newService.ar, en: newService.en }, durationMinutes: newService.duration, ...(newService.price ? { price: Number(newService.price) } : {}), }), }); setAdding(false); setNewService({ tr: '', ar: '', en: '', duration: 30, price: '' }); onChanged(); } return ( <Card className="space-y-3"> <div className="flex items-center justify-between"> <h2 className="text-sm font-semibold text-slate-500">{t('services')}</h2> <Button variant="secondary" onClick={() => setAdding(!adding)}> + {t('add')} </Button> </div> {adding && ( <div className="grid gap-2 rounded-lg bg-slate-50 p-3 sm:grid-cols-5"> <Input placeholder="Türkçe" value={newService.tr} onChange={(e) => setNewService({ ...newService, tr: e.target.value })} /> <Input placeholder="العربية" value={newService.ar} onChange={(e) => setNewService({ ...newService, ar: e.target.value })} /> <Input placeholder="English" value={newService.en} onChange={(e) => setNewService({ ...newService, en: e.target.value })} /> <Input type="number" placeholder={t('duration')} value={newService.duration} onChange={(e) => setNewService({ ...newService, duration: Number(e.target.value) })} /> <div className="flex gap-2"> <Input type="number" placeholder={t('price')} value={newService.price} onChange={(e) => setNewService({ ...newService, price: e.target.value })} /> <Button onClick={addService}>{t('add')}</Button> </div> </div> )} <div className="space-y-2"> {services.map((row) => { const patch = editing[row.id] ?? {}; const dirty = Object.keys(patch).length > 0; return ( <div key={row.id} className="flex flex-wrap items-center gap-2 text-sm"> <span className="min-w-40 flex-1 font-medium">{serviceName(row.name, lang)}</span> <Input type="number" className="!w-20" value={patch.durationMinutes ?? row.durationMinutes} onChange={(e) => setEditing((prev) => ({ ...prev, [row.id]: { ...patch, durationMinutes: Number(e.target.value) }, })) } /> <label className="flex items-center gap-1 text-xs text-slate-500"> <input type="checkbox" checked={patch.active ?? row.active} onChange={(e) => setEditing((prev) => ({ ...prev, [row.id]: { ...patch, active: e.target.checked }, })) } /> {t('active')} </label> {dirty && ( <Button variant="secondary" onClick={() => saveRow(row)}> {t('save')} </Button> )} </div> ); })} </div> </Card> ); } function StaffCard({ staff, services, lang, onChanged, }: { staff: StaffRow[]; services: ServiceRow[]; lang: string; onChanged: () => void; }) { const { t } = useLang(); const [adding, setAdding] = useState(false); const [newStaff, setNewStaff] = useState({ name: '', role: 'practitioner' }); async function toggleService(row: StaffRow, serviceId: string) { const current = row.services.map((s) => s.id); const next = current.includes(serviceId) ? current.filter((id) => id !== serviceId) : [...current, serviceId]; await api(`/staff/${row.id}`, { method: 'PATCH', body: JSON.stringify({ serviceIds: next }) }); onChanged(); } async function addStaff() { await api('/staff', { method: 'POST', body: JSON.stringify({ name: newStaff.name, role: newStaff.role }), }); setAdding(false); setNewStaff({ name: '', role: 'practitioner' }); onChanged(); } return ( <Card className="space-y-3"> <div className="flex items-center justify-between"> <h2 className="text-sm font-semibold text-slate-500">{t('staffSection')}</h2> <Button variant="secondary" onClick={() => setAdding(!adding)}> + {t('add')} </Button> </div> {adding && ( <div className="flex gap-2 rounded-lg bg-slate-50 p-3"> <Input placeholder={t('name')} value={newStaff.name} onChange={(e) => setNewStaff({ ...newStaff, name: e.target.value })} /> <Input placeholder={t('role')} value={newStaff.role} onChange={(e) => setNewStaff({ ...newStaff, role: e.target.value })} /> <Button onClick={addStaff} disabled={!newStaff.name}> {t('add')} </Button> </div> )} <div className="space-y-3"> {staff.map((row) => ( <div key={row.id} className="rounded-lg border border-slate-100 p-3"> <div className="mb-1 font-medium"> {row.name} <span className="text-xs font-normal text-slate-400">{row.role}</span> </div> <div className="flex flex-wrap gap-2"> {services.map((service) => ( <label key={service.id} className="flex items-center gap-1 text-xs text-slate-600"> <input type="checkbox" checked={row.services.some((s) => s.id === service.id)} onChange={() => toggleService(row, service.id)} /> {serviceName(service.name, lang)} </label> ))} </div> </div> ))} </div> </Card> ); }
Save
cmd:
run