chore: format project automatically before starting dev server

This commit is contained in:
2026-09-16 14:14:04 +03:30
parent 7c55b3916c
commit 812029fe84
66 changed files with 2088 additions and 1125 deletions
+5 -1
View File
@@ -3,5 +3,9 @@ import ProtectedPage from '@/components/auth/ProtectedPage';
import DashboardShell from '@/components/dashboard/DashboardShell';
export default function Layout({children}: {children: ReactNode}) {
return <ProtectedPage><DashboardShell>{children}</DashboardShell></ProtectedPage>;
return (
<ProtectedPage>
<DashboardShell>{children}</DashboardShell>
</ProtectedPage>
);
}
+131 -45
View File
@@ -23,57 +23,143 @@ export default function OrderDetailsPage({params}: {params: {id: string}}) {
const invoiceRef = useRef<HTMLDivElement>(null);
const numberLocale = locale === 'fa' ? 'fa-IR' : 'en-US';
const money = (value: number) => `${value.toLocaleString(numberLocale)} ${locale === 'fa' ? 'ریال' : 'Rials'}`;
useEffect(() => { if (auth.session) getOrder(params.id, auth.session.profile).then(setOrder); }, [auth.session, params.id]);
if (order === null) return <Skeleton active/>;
if (!order) return <div><Empty description={copy.notFound}/><Link href="/dashboard/orders" className="text-brand-400 block text-center mt-5">{copy.backToOrders}</Link></div>;
useEffect(() => {
if (auth.session) getOrder(params.id, auth.session.profile).then(setOrder);
}, [auth.session, params.id]);
if (order === null) return <Skeleton active />;
if (!order)
return (
<div>
<Empty description={copy.notFound} />
<Link href="/dashboard/orders" className="text-brand-400 block text-center mt-5">
{copy.backToOrders}
</Link>
</div>
);
const exportPdf = async () => {
if (!invoiceRef.current) return;
setExporting('pdf');
try { await downloadInvoicePdf(invoiceRef.current, order.invoice!.number); }
catch { await message.error('ساخت فایل PDF انجام نشد. دوباره تلاش کنید.'); }
finally { setExporting(null); }
try {
await downloadInvoicePdf(invoiceRef.current, order.invoice!.number);
} catch {
await message.error('ساخت فایل PDF انجام نشد. دوباره تلاش کنید.');
} finally {
setExporting(null);
}
};
const exportExcel = async () => {
setExporting('excel');
try { await downloadInvoiceExcel(order, order.invoice!.number); }
catch { await message.error('ساخت فایل Excel انجام نشد. دوباره تلاش کنید.'); }
finally { setExporting(null); }
try {
await downloadInvoiceExcel(order, order.invoice!.number);
} catch {
await message.error('ساخت فایل Excel انجام نشد. دوباره تلاش کنید.');
} finally {
setExporting(null);
}
};
return <>
<Link href="/dashboard/orders" className="text-brand-400 hover:underline">{copy.backToOrders}</Link>
<div className="flex flex-wrap justify-between gap-4 items-center mt-5">
<div><p className="text-secondary">{copy.orderNumber}</p><h1 className="text-3xl font-extrabold text-primary" dir="ltr">{order.number}</h1></div>
<OrderStatusTag status={order.status}/>
</div>
<div className="grid sm:grid-cols-3 gap-4 mt-7">
<div className="rounded-xl border border-subtle bg-elevated p-4"><span className="text-sm text-secondary">{copy.orderDate}</span><strong className="block text-primary mt-1">{order.createdAt}</strong></div>
<div className="rounded-xl border border-subtle bg-elevated p-4"><span className="text-sm text-secondary">{copy.lastUpdate}</span><strong className="block text-primary mt-1">{order.updatedAt}</strong></div>
<div className="rounded-xl border border-subtle bg-elevated p-4"><span className="text-sm text-secondary">{copy.paymentStatus}</span><strong className="block text-primary mt-1">{paymentStatusLabels[locale][order.paymentStatus]}</strong></div>
</div>
<section className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7 mt-7">
<h2 className="text-xl font-bold text-primary mb-6">{copy.timeline}</h2>
<Steps direction="vertical" current={order.events.length - 1} items={order.events.map(event => ({title: orderStatusLabels[locale][event.status], description: `${event.date} — ${event.note}`, status: event.completed ? 'finish' : 'wait'}))}/>
</section>
<section className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7 mt-7">
<h2 className="text-xl font-bold text-primary mb-5">{copy.orderItems}</h2>
<div className="space-y-5">{order.items.map(item => <article key={item.id} className="flex flex-wrap sm:flex-nowrap gap-5 border-b border-subtle last:border-0 pb-5 last:pb-0">
<img src={item.image} alt={item.name} className="w-24 h-28 object-contain rounded-xl bg-surface"/>
<div className="flex-1"><h3 className="font-bold text-primary">{item.name}</h3><p className="text-sm text-secondary mt-1" dir="ltr">{item.model}</p><p className="text-secondary mt-3">{copy.modules}: {item.modules.join('، ')}</p><p className="text-primary mt-3">{copy.quantity}: {item.quantity.toLocaleString(numberLocale)} · {copy.total}: {money(item.quantity * item.unitPrice)}</p></div>
</article>)}</div>
</section>
<section className="mt-7">
<h2 className="text-xl font-bold text-primary mb-5">{copy.invoice}</h2>
{!order.invoice ? <Alert type="info" showIcon message={copy.invoicePending}/> : <>
<div className="flex flex-wrap gap-3 mb-5 invoice-actions">
<Button type="primary" icon={<DownloadOutlined/>} loading={exporting === 'pdf'} onClick={exportPdf}>{copy.downloadPdf}</Button>
<Button icon={<FileExcelOutlined/>} loading={exporting === 'excel'} onClick={exportExcel}>{copy.downloadExcel}</Button>
<Button icon={<PrinterOutlined/>} onClick={() => window.print()}>{copy.print}</Button>
return (
<>
<Link href="/dashboard/orders" className="text-brand-400 hover:underline">
{copy.backToOrders}
</Link>
<div className="flex flex-wrap justify-between gap-4 items-center mt-5">
<div>
<p className="text-secondary">{copy.orderNumber}</p>
<h1 className="text-3xl font-extrabold text-primary" dir="ltr">
{order.number}
</h1>
</div>
<div className="overflow-x-auto rounded-2xl border border-subtle"><InvoiceDocument ref={invoiceRef} order={order}/></div>
</>}
</section>
</>;
<OrderStatusTag status={order.status} />
</div>
<div className="grid sm:grid-cols-3 gap-4 mt-7">
<div className="rounded-xl border border-subtle bg-elevated p-4">
<span className="text-sm text-secondary">{copy.orderDate}</span>
<strong className="block text-primary mt-1">{order.createdAt}</strong>
</div>
<div className="rounded-xl border border-subtle bg-elevated p-4">
<span className="text-sm text-secondary">{copy.lastUpdate}</span>
<strong className="block text-primary mt-1">{order.updatedAt}</strong>
</div>
<div className="rounded-xl border border-subtle bg-elevated p-4">
<span className="text-sm text-secondary">{copy.paymentStatus}</span>
<strong className="block text-primary mt-1">
{paymentStatusLabels[locale][order.paymentStatus]}
</strong>
</div>
</div>
<section className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7 mt-7">
<h2 className="text-xl font-bold text-primary mb-6">{copy.timeline}</h2>
<Steps
direction="vertical"
current={order.events.length - 1}
items={order.events.map(event => ({
title: orderStatusLabels[locale][event.status],
description: `${event.date} — ${event.note}`,
status: event.completed ? 'finish' : 'wait',
}))}
/>
</section>
<section className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7 mt-7">
<h2 className="text-xl font-bold text-primary mb-5">{copy.orderItems}</h2>
<div className="space-y-5">
{order.items.map(item => (
<article
key={item.id}
className="flex flex-wrap sm:flex-nowrap gap-5 border-b border-subtle last:border-0 pb-5 last:pb-0"
>
<img
src={item.image}
alt={item.name}
className="w-24 h-28 object-contain rounded-xl bg-surface"
/>
<div className="flex-1">
<h3 className="font-bold text-primary">{item.name}</h3>
<p className="text-sm text-secondary mt-1" dir="ltr">
{item.model}
</p>
<p className="text-secondary mt-3">
{copy.modules}: {item.modules.join('، ')}
</p>
<p className="text-primary mt-3">
{copy.quantity}: {item.quantity.toLocaleString(numberLocale)} · {copy.total}:{' '}
{money(item.quantity * item.unitPrice)}
</p>
</div>
</article>
))}
</div>
</section>
<section className="mt-7">
<h2 className="text-xl font-bold text-primary mb-5">{copy.invoice}</h2>
{!order.invoice ? (
<Alert type="info" showIcon message={copy.invoicePending} />
) : (
<>
<div className="flex flex-wrap gap-3 mb-5 invoice-actions">
<Button
type="primary"
icon={<DownloadOutlined />}
loading={exporting === 'pdf'}
onClick={exportPdf}
>
{copy.downloadPdf}
</Button>
<Button icon={<FileExcelOutlined />} loading={exporting === 'excel'} onClick={exportExcel}>
{copy.downloadExcel}
</Button>
<Button icon={<PrinterOutlined />} onClick={() => window.print()}>
{copy.print}
</Button>
</div>
<div className="overflow-x-auto rounded-2xl border border-subtle">
<InvoiceDocument ref={invoiceRef} order={order} />
</div>
</>
)}
</section>
</>
);
}
+20 -6
View File
@@ -14,10 +14,24 @@ export default function OrdersPage() {
const {locale} = useLanguage();
const copy = accountCopy[locale];
const [orders, setOrders] = useState<CustomerOrder[] | null>(null);
useEffect(() => { if (auth.session) getOrders(auth.session.profile).then(setOrders); }, [auth.session]);
return <>
<h1 className="text-3xl font-extrabold text-primary">{copy.orders}</h1>
<p className="text-secondary mt-3 mb-7">{copy.dashboardDescription}</p>
{!orders ? <Skeleton active/> : orders.length ? <div className="space-y-4">{orders.map(order => <OrderCard key={order.id} order={order}/>)}</div> : <Empty description={copy.emptyOrders}/>}
</>;
useEffect(() => {
if (auth.session) getOrders(auth.session.profile).then(setOrders);
}, [auth.session]);
return (
<>
<h1 className="text-3xl font-extrabold text-primary">{copy.orders}</h1>
<p className="text-secondary mt-3 mb-7">{copy.dashboardDescription}</p>
{!orders ? (
<Skeleton active />
) : orders.length ? (
<div className="space-y-4">
{orders.map(order => (
<OrderCard key={order.id} order={order} />
))}
</div>
) : (
<Empty description={copy.emptyOrders} />
)}
</>
);
}
+58 -16
View File
@@ -16,27 +16,69 @@ export default function DashboardPage() {
const {locale} = useLanguage();
const copy = accountCopy[locale];
const [orders, setOrders] = useState<CustomerOrder[] | null>(null);
useEffect(() => { if (auth.session) getOrders(auth.session.profile).then(setOrders); }, [auth.session]);
useEffect(() => {
if (auth.session) getOrders(auth.session.profile).then(setOrders);
}, [auth.session]);
const completion = useMemo(() => {
if (!auth.session) return 0;
const profile = auth.session.profile;
const required = [profile.firstName, profile.lastName, profile.province, profile.city, profile.address, profile.postalCode];
return Math.round(required.filter(Boolean).length / required.length * 100);
const required = [
profile.firstName,
profile.lastName,
profile.province,
profile.city,
profile.address,
profile.postalCode,
];
return Math.round((required.filter(Boolean).length / required.length) * 100);
}, [auth.session]);
if (!orders) return <Skeleton active/>;
if (!orders) return <Skeleton active />;
const activeOrders = orders.filter(order => !['delivered', 'cancelled'].includes(order.status)).length;
const cards = [
{label: copy.profileCompletion, value: `${completion.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US')}٪`, icon: <CheckCircleOutlined/>, extra: <Progress percent={completion} showInfo={false} size="small"/>},
{label: copy.activeOrders, value: activeOrders.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US'), icon: <ClockCircleOutlined/>},
{label: copy.totalOrders, value: orders.length.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US'), icon: <FileTextOutlined/>},
{
label: copy.profileCompletion,
value: `${completion.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US')}٪`,
icon: <CheckCircleOutlined />,
extra: <Progress percent={completion} showInfo={false} size="small" />,
},
{
label: copy.activeOrders,
value: activeOrders.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US'),
icon: <ClockCircleOutlined />,
},
{
label: copy.totalOrders,
value: orders.length.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US'),
icon: <FileTextOutlined />,
},
];
return <>
<h1 className="text-3xl font-extrabold text-primary">{copy.welcome} {auth.session?.profile.firstName}</h1>
<p className="text-secondary mt-3">{copy.dashboardDescription}</p>
<div className="grid sm:grid-cols-3 gap-4 mt-8">{cards.map(card => <div key={card.label} className="rounded-2xl border border-subtle bg-elevated p-5">
<div className="text-brand-400 text-2xl">{card.icon}</div><p className="text-secondary text-sm mt-4">{card.label}</p><strong className="text-2xl text-primary block mt-1">{card.value}</strong>{card.extra && <div className="mt-3">{card.extra}</div>}
</div>)}</div>
<div className="flex items-center justify-between gap-4 mt-10 mb-5"><h2 className="text-xl font-bold text-primary">{copy.recentOrders}</h2><Link href="/dashboard/orders" className="text-brand-400">{copy.viewAll}</Link></div>
<div className="space-y-4">{orders.slice(0, 2).map(order => <OrderCard key={order.id} order={order}/>)}</div>
</>;
return (
<>
<h1 className="text-3xl font-extrabold text-primary">
{copy.welcome} {auth.session?.profile.firstName}
</h1>
<p className="text-secondary mt-3">{copy.dashboardDescription}</p>
<div className="grid sm:grid-cols-3 gap-4 mt-8">
{cards.map(card => (
<div key={card.label} className="rounded-2xl border border-subtle bg-elevated p-5">
<div className="text-brand-400 text-2xl">{card.icon}</div>
<p className="text-secondary text-sm mt-4">{card.label}</p>
<strong className="text-2xl text-primary block mt-1">{card.value}</strong>
{card.extra && <div className="mt-3">{card.extra}</div>}
</div>
))}
</div>
<div className="flex items-center justify-between gap-4 mt-10 mb-5">
<h2 className="text-xl font-bold text-primary">{copy.recentOrders}</h2>
<Link href="/dashboard/orders" className="text-brand-400">
{copy.viewAll}
</Link>
</div>
<div className="space-y-4">
{orders.slice(0, 2).map(order => (
<OrderCard key={order.id} order={order} />
))}
</div>
</>
);
}
+84 -27
View File
@@ -15,34 +15,91 @@ export default function ProfilePage() {
const [form] = Form.useForm<CustomerProfile>();
const [saving, setSaving] = useState(false);
const customerType = Form.useWatch('type', form);
useEffect(() => { if (auth.session) form.setFieldsValue(auth.session.profile); }, [auth.session, form]);
useEffect(() => {
if (auth.session) form.setFieldsValue(auth.session.profile);
}, [auth.session, form]);
const submit = async (values: CustomerProfile) => {
setSaving(true);
try { await auth.updateProfile(values); message.success(copy.saved); } finally { setSaving(false); }
try {
await auth.updateProfile(values);
message.success(copy.saved);
} finally {
setSaving(false);
}
};
return <>
<h1 className="text-3xl font-extrabold text-primary">{copy.profile}</h1>
<p className="text-secondary mt-3 mb-7">{copy.invoicePending}</p>
<Form form={form} layout="vertical" onFinish={submit} className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7">
<Form.Item name="type" label={copy.customerType} rules={[{required: true}]}><Radio.Group optionType="button" buttonStyle="solid" options={[{label: copy.individual, value: 'individual'}, {label: copy.corporate, value: 'corporate'}]}/></Form.Item>
<div className="grid md:grid-cols-2 gap-x-5">
<Form.Item name="firstName" label={copy.firstName} rules={[{required: true}]}><Input size="large"/></Form.Item>
<Form.Item name="lastName" label={copy.lastName} rules={[{required: true}]}><Input size="large"/></Form.Item>
<Form.Item name="phone" label={copy.phone}><Input size="large" dir="ltr" disabled/></Form.Item>
<Form.Item name="email" label={copy.email} rules={[{type: 'email'}]}><Input size="large" dir="ltr"/></Form.Item>
{customerType === 'corporate' && <>
<Form.Item name="companyName" label={copy.companyName} rules={[{required: true}]}><Input size="large"/></Form.Item>
<Form.Item name="nationalId" label={copy.nationalId} rules={[{required: true}]}><Input size="large" dir="ltr"/></Form.Item>
<Form.Item name="economicCode" label={copy.economicCode}><Input size="large" dir="ltr"/></Form.Item>
</>}
<Form.Item name="province" label={copy.province} rules={[{required: true}]}><Input size="large"/></Form.Item>
<Form.Item name="city" label={copy.city} rules={[{required: true}]}><Input size="large"/></Form.Item>
<Form.Item name="postalCode" label={copy.postalCode} rules={[{required: true}]}><Input size="large" dir="ltr" maxLength={10}/></Form.Item>
<Form.Item name="preferredCallTime" label={copy.preferredCallTime}><Input size="large"/></Form.Item>
</div>
<Form.Item name="address" label={copy.address} rules={[{required: true}]}><Input.TextArea rows={4}/></Form.Item>
<Alert type="info" showIcon message={copy.verifiedPhone} className="mb-6"/>
<Button type="primary" htmlType="submit" size="large" loading={saving}>{copy.save}</Button>
</Form>
</>;
return (
<>
<h1 className="text-3xl font-extrabold text-primary">{copy.profile}</h1>
<p className="text-secondary mt-3 mb-7">{copy.invoicePending}</p>
<Form
form={form}
layout="vertical"
onFinish={submit}
className="rounded-2xl border border-subtle bg-elevated p-5 md:p-7"
>
<Form.Item name="type" label={copy.customerType} rules={[{required: true}]}>
<Radio.Group
optionType="button"
buttonStyle="solid"
options={[
{
label: copy.individual,
value: 'individual',
},
{
label: copy.corporate,
value: 'corporate',
},
]}
/>
</Form.Item>
<div className="grid md:grid-cols-2 gap-x-5">
<Form.Item name="firstName" label={copy.firstName} rules={[{required: true}]}>
<Input size="large" />
</Form.Item>
<Form.Item name="lastName" label={copy.lastName} rules={[{required: true}]}>
<Input size="large" />
</Form.Item>
<Form.Item name="phone" label={copy.phone}>
<Input size="large" dir="ltr" disabled />
</Form.Item>
<Form.Item name="email" label={copy.email} rules={[{type: 'email'}]}>
<Input size="large" dir="ltr" />
</Form.Item>
{customerType === 'corporate' && (
<>
<Form.Item name="companyName" label={copy.companyName} rules={[{required: true}]}>
<Input size="large" />
</Form.Item>
<Form.Item name="nationalId" label={copy.nationalId} rules={[{required: true}]}>
<Input size="large" dir="ltr" />
</Form.Item>
<Form.Item name="economicCode" label={copy.economicCode}>
<Input size="large" dir="ltr" />
</Form.Item>
</>
)}
<Form.Item name="province" label={copy.province} rules={[{required: true}]}>
<Input size="large" />
</Form.Item>
<Form.Item name="city" label={copy.city} rules={[{required: true}]}>
<Input size="large" />
</Form.Item>
<Form.Item name="postalCode" label={copy.postalCode} rules={[{required: true}]}>
<Input size="large" dir="ltr" maxLength={10} />
</Form.Item>
<Form.Item name="preferredCallTime" label={copy.preferredCallTime}>
<Input size="large" />
</Form.Item>
</div>
<Form.Item name="address" label={copy.address} rules={[{required: true}]}>
<Input.TextArea rows={4} />
</Form.Item>
<Alert type="info" showIcon message={copy.verifiedPhone} className="mb-6" />
<Button type="primary" htmlType="submit" size="large" loading={saving}>
{copy.save}
</Button>
</Form>
</>
);
}