feat: enhance UI components and add vibrant theme support
- Updated layout and styling for various dashboard pages including stock adjustments, counts, transfers, wastage, and vendors to improve responsiveness and visual consistency. - Introduced a vibrant theme option in the theme toggle component, allowing users to switch between light, dark, and vibrant themes. - Refactored button styles across login and forgot password pages for better accessibility and visual feedback. - Improved sidebar navigation with expanded functionality for items without dedicated pages. - Enhanced data table styling for better readability and user interaction. - Added new background images to support the vibrant theme.
This commit is contained in:
@@ -112,7 +112,7 @@ export default function AttendanceBatchDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">{batch.docNo}</h1>
|
<h1 className="text-2xl font-bold text-foreground">{batch.docNo}</h1>
|
||||||
<p className="text-base text-muted-foreground">{new Date(batch.periodStart).toLocaleDateString()} – {new Date(batch.periodEnd).toLocaleDateString()} · {batch.rowCountTotal} rows</p>
|
<p className="text-base text-muted-foreground">{new Date(batch.periodStart).toLocaleDateString()} – {new Date(batch.periodEnd).toLocaleDateString()} · {batch.rowCountTotal} rows</p>
|
||||||
@@ -132,9 +132,9 @@ export default function AttendanceBatchDetailPage() {
|
|||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<Field><FieldLabel htmlFor="u-reason">Reason</FieldLabel><Input id="u-reason" value={unlockReason} onChange={(e) => setUnlockReason(e.target.value)} /></Field>
|
<Field><FieldLabel htmlFor="u-reason">Reason</FieldLabel><Input id="u-reason" value={unlockReason} onChange={(e) => setUnlockReason(e.target.value)} /></Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={unlock} disabled={busy}>Unlock</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={unlock} disabled={busy}>Unlock</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default function AttendanceBatchesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Attendance</h1>
|
<h1 className="text-2xl font-bold text-foreground">Attendance</h1>
|
||||||
<p className="text-base text-muted-foreground">Upload → Preview → Confirm. Once Confirmed, a batch becomes payroll's source of truth.</p>
|
<p className="text-base text-muted-foreground">Upload → Preview → Confirm. Once Confirmed, a batch becomes payroll's source of truth.</p>
|
||||||
@@ -86,7 +86,7 @@ export default function AttendanceBatchesPage() {
|
|||||||
<DialogDescription>Columns: Employee Code, Date, Check In, Check Out.</DialogDescription>
|
<DialogDescription>Columns: Employee Code, Date, Check In, Check Out.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<Field><FieldLabel htmlFor="a-start">Period start</FieldLabel><Input id="a-start" type="date" value={periodStart} onChange={(e) => setPeriodStart(e.target.value)} /></Field>
|
<Field><FieldLabel htmlFor="a-start">Period start</FieldLabel><Input id="a-start" type="date" value={periodStart} onChange={(e) => setPeriodStart(e.target.value)} /></Field>
|
||||||
<Field><FieldLabel htmlFor="a-end">Period end</FieldLabel><Input id="a-end" type="date" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} /></Field>
|
<Field><FieldLabel htmlFor="a-end">Period end</FieldLabel><Input id="a-end" type="date" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} /></Field>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,9 +95,9 @@ export default function AttendanceBatchesPage() {
|
|||||||
<Input id="a-file" type="file" accept=".xlsx,.csv" onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
|
<Input id="a-file" type="file" accept=".xlsx,.csv" onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={handleUpload} disabled={submitting}><Upload className="size-4" />{submitting ? "Uploading…" : "Upload"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleUpload} disabled={submitting}><Upload className="size-4" />{submitting ? "Uploading…" : "Upload"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export default function EmployeeDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">{employee.fullName}</h1>
|
<h1 className="text-2xl font-bold text-foreground">{employee.fullName}</h1>
|
||||||
<p className="text-base text-muted-foreground">
|
<p className="text-base text-muted-foreground">
|
||||||
@@ -237,7 +237,7 @@ export default function EmployeeDetailPage() {
|
|||||||
|
|
||||||
{tab === "Overview" && (
|
{tab === "Overview" && (
|
||||||
<FieldGroup className="max-w-2xl">
|
<FieldGroup className="max-w-2xl">
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<Field><FieldLabel>Full name</FieldLabel><Input value={employee.fullName} onChange={(e) => setEmployee({ ...employee, fullName: e.target.value })} /></Field>
|
<Field><FieldLabel>Full name</FieldLabel><Input value={employee.fullName} onChange={(e) => setEmployee({ ...employee, fullName: e.target.value })} /></Field>
|
||||||
<Field><FieldLabel>Email</FieldLabel><Input type="email" value={employee.email ?? ""} onChange={(e) => setEmployee({ ...employee, email: e.target.value })} /></Field>
|
<Field><FieldLabel>Email</FieldLabel><Input type="email" value={employee.email ?? ""} onChange={(e) => setEmployee({ ...employee, email: e.target.value })} /></Field>
|
||||||
<Field><FieldLabel>NIC</FieldLabel><Input value={employee.nic ?? ""} onChange={(e) => setEmployee({ ...employee, nic: e.target.value })} /></Field>
|
<Field><FieldLabel>NIC</FieldLabel><Input value={employee.nic ?? ""} onChange={(e) => setEmployee({ ...employee, nic: e.target.value })} /></Field>
|
||||||
@@ -363,7 +363,7 @@ export default function EmployeeDetailPage() {
|
|||||||
<DialogDescription>Supersedes the current open-ended structure from this date.</DialogDescription>
|
<DialogDescription>Supersedes the current open-ended structure from this date.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<Field><FieldLabel>Effective from</FieldLabel><Input type="date" value={structureEffectiveFrom} onChange={(e) => setStructureEffectiveFrom(e.target.value)} /></Field>
|
<Field><FieldLabel>Effective from</FieldLabel><Input type="date" value={structureEffectiveFrom} onChange={(e) => setStructureEffectiveFrom(e.target.value)} /></Field>
|
||||||
<Field><FieldLabel>Basic salary</FieldLabel><Input type="number" min={0} value={structureBasic} onChange={(e) => setStructureBasic(Number(e.target.value))} /></Field>
|
<Field><FieldLabel>Basic salary</FieldLabel><Input type="number" min={0} value={structureBasic} onChange={(e) => setStructureBasic(Number(e.target.value))} /></Field>
|
||||||
</div>
|
</div>
|
||||||
@@ -383,9 +383,9 @@ export default function EmployeeDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setStructureOpen(false)} disabled={busy}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setStructureOpen(false)} disabled={busy}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={createStructure} disabled={busy}>{busy ? "Saving…" : "Save"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={createStructure} disabled={busy}>{busy ? "Saving…" : "Save"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -433,7 +433,7 @@ export default function EmployeeDetailPage() {
|
|||||||
<SelectContent><SelectItem value="Loan">Loan</SelectItem><SelectItem value="Advance">Advance</SelectItem></SelectContent>
|
<SelectContent><SelectItem value="Loan">Loan</SelectItem><SelectItem value="Advance">Advance</SelectItem></SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<Field><FieldLabel>Principal</FieldLabel><Input type="number" min={0} value={loanPrincipal} onChange={(e) => setLoanPrincipal(Number(e.target.value))} /></Field>
|
<Field><FieldLabel>Principal</FieldLabel><Input type="number" min={0} value={loanPrincipal} onChange={(e) => setLoanPrincipal(Number(e.target.value))} /></Field>
|
||||||
<Field><FieldLabel>Installment amount</FieldLabel><Input type="number" min={0} value={loanInstallmentAmount} onChange={(e) => setLoanInstallmentAmount(Number(e.target.value))} /></Field>
|
<Field><FieldLabel>Installment amount</FieldLabel><Input type="number" min={0} value={loanInstallmentAmount} onChange={(e) => setLoanInstallmentAmount(Number(e.target.value))} /></Field>
|
||||||
<Field><FieldLabel># installments</FieldLabel><Input type="number" min={1} value={loanCount} onChange={(e) => setLoanCount(Number(e.target.value))} /></Field>
|
<Field><FieldLabel># installments</FieldLabel><Input type="number" min={1} value={loanCount} onChange={(e) => setLoanCount(Number(e.target.value))} /></Field>
|
||||||
@@ -445,9 +445,9 @@ export default function EmployeeDetailPage() {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setLoanOpen(false)} disabled={busy}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setLoanOpen(false)} disabled={busy}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={createLoan} disabled={busy}>{busy ? "Creating…" : "Create"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={createLoan} disabled={busy}>{busy ? "Creating…" : "Create"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default function EmployeesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Employees</h1>
|
<h1 className="text-2xl font-bold text-foreground">Employees</h1>
|
||||||
<p className="text-base text-muted-foreground">Staff records — separate from system login accounts (see the link chip below).</p>
|
<p className="text-base text-muted-foreground">Staff records — separate from system login accounts (see the link chip below).</p>
|
||||||
@@ -134,7 +134,7 @@ export default function EmployeesPage() {
|
|||||||
<DialogDescription>Not every employee needs a login — a system user account is optional and separate.</DialogDescription>
|
<DialogDescription>Not every employee needs a login — a system user account is optional and separate.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<Field data-invalid={!!errors.employeeCode}>
|
<Field data-invalid={!!errors.employeeCode}>
|
||||||
<FieldLabel htmlFor="e-code">Employee code</FieldLabel>
|
<FieldLabel htmlFor="e-code">Employee code</FieldLabel>
|
||||||
<Input id="e-code" value={form.employeeCode} onChange={(e) => setForm((f) => ({ ...f, employeeCode: e.target.value }))} aria-invalid={!!errors.employeeCode} />
|
<Input id="e-code" value={form.employeeCode} onChange={(e) => setForm((f) => ({ ...f, employeeCode: e.target.value }))} aria-invalid={!!errors.employeeCode} />
|
||||||
@@ -170,7 +170,7 @@ export default function EmployeesPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<Field data-invalid={!!errors.departmentId}>
|
<Field data-invalid={!!errors.departmentId}>
|
||||||
<FieldLabel htmlFor="e-dept">Department</FieldLabel>
|
<FieldLabel htmlFor="e-dept">Department</FieldLabel>
|
||||||
<Select value={form.departmentId ? String(form.departmentId) : undefined} onValueChange={(v) => setForm((f) => ({ ...f, departmentId: Number(v) }))}>
|
<Select value={form.departmentId ? String(form.departmentId) : undefined} onValueChange={(v) => setForm((f) => ({ ...f, departmentId: Number(v) }))}>
|
||||||
@@ -205,9 +205,9 @@ export default function EmployeesPage() {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default function LeaveRequestsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Leave</h1>
|
<h1 className="text-2xl font-bold text-foreground">Leave</h1>
|
||||||
<p className="text-base text-muted-foreground">Approved leave feeds Attendance's OnLeave status and Payroll's No-Pay calculation.</p>
|
<p className="text-base text-muted-foreground">Approved leave feeds Attendance's OnLeave status and Payroll's No-Pay calculation.</p>
|
||||||
@@ -129,15 +129,15 @@ export default function LeaveRequestsPage() {
|
|||||||
<SelectContent>{leaveTypes.map((t) => <SelectItem key={t.leaveTypeId} value={String(t.leaveTypeId)}>{t.name}</SelectItem>)}</SelectContent>
|
<SelectContent>{leaveTypes.map((t) => <SelectItem key={t.leaveTypeId} value={String(t.leaveTypeId)}>{t.name}</SelectItem>)}</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<Field><FieldLabel>Start date</FieldLabel><Input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} /></Field>
|
<Field><FieldLabel>Start date</FieldLabel><Input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} /></Field>
|
||||||
<Field><FieldLabel>End date</FieldLabel><Input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} /></Field>
|
<Field><FieldLabel>End date</FieldLabel><Input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} /></Field>
|
||||||
</div>
|
</div>
|
||||||
<Field><FieldLabel>Reason (optional)</FieldLabel><Input value={reason} onChange={(e) => setReason(e.target.value)} /></Field>
|
<Field><FieldLabel>Reason (optional)</FieldLabel><Input value={reason} onChange={(e) => setReason(e.target.value)} /></Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Submitting…" : "Submit"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Submitting…" : "Submit"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default function PayrollLineDetailPage() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-3 text-sm text-muted-foreground">
|
<div className="grid grid-cols-2 gap-3 text-sm text-muted-foreground sm:grid-cols-3">
|
||||||
<div>Present days: {line.presentDays}</div>
|
<div>Present days: {line.presentDays}</div>
|
||||||
<div>Absent days: {line.absentDays}</div>
|
<div>Absent days: {line.absentDays}</div>
|
||||||
<div>Leave days: {line.leaveDays}</div>
|
<div>Leave days: {line.leaveDays}</div>
|
||||||
|
|||||||
@@ -88,12 +88,12 @@ export default function PayrollRunDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">{run.docNo}</h1>
|
<h1 className="text-2xl font-bold text-foreground">{run.docNo}</h1>
|
||||||
<p className="text-base text-muted-foreground">{run.periodMonth.toString().padStart(2, "0")}/{run.periodYear} · {run.employeeCount} employees</p>
|
<p className="text-base text-muted-foreground">{run.periodMonth.toString().padStart(2, "0")}/{run.periodYear} · {run.employeeCount} employees</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<Badge variant="outline" className="h-7 px-3 text-sm">{run.status}</Badge>
|
<Badge variant="outline" className="h-7 px-3 text-sm">{run.status}</Badge>
|
||||||
{run.status === "Draft" && <Button onClick={approve} disabled={busy}>Approve</Button>}
|
{run.status === "Draft" && <Button onClick={approve} disabled={busy}>Approve</Button>}
|
||||||
{run.status === "Approved" && <Button onClick={lock} disabled={busy}>Lock Payroll</Button>}
|
{run.status === "Approved" && <Button onClick={lock} disabled={busy}>Lock Payroll</Button>}
|
||||||
@@ -110,9 +110,9 @@ export default function PayrollRunDetailPage() {
|
|||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<Field><FieldLabel htmlFor="p-reason">Reason</FieldLabel><Input id="p-reason" value={unlockReason} onChange={(e) => setUnlockReason(e.target.value)} /></Field>
|
<Field><FieldLabel htmlFor="p-reason">Reason</FieldLabel><Input id="p-reason" value={unlockReason} onChange={(e) => setUnlockReason(e.target.value)} /></Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={unlock} disabled={busy}>Unlock</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={unlock} disabled={busy}>Unlock</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default function PayrollRunsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Payroll</h1>
|
<h1 className="text-2xl font-bold text-foreground">Payroll</h1>
|
||||||
<p className="text-base text-muted-foreground">Generate → Review → Approve → Lock → Payslips.</p>
|
<p className="text-base text-muted-foreground">Generate → Review → Approve → Lock → Payslips.</p>
|
||||||
@@ -76,14 +76,14 @@ export default function PayrollRunsPage() {
|
|||||||
<DialogDescription>Blocked if attendance for this period isn't fully Confirmed yet.</DialogDescription>
|
<DialogDescription>Blocked if attendance for this period isn't fully Confirmed yet.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<Field><FieldLabel>Year</FieldLabel><Input type="number" value={periodYear} onChange={(e) => setPeriodYear(Number(e.target.value))} /></Field>
|
<Field><FieldLabel>Year</FieldLabel><Input type="number" value={periodYear} onChange={(e) => setPeriodYear(Number(e.target.value))} /></Field>
|
||||||
<Field><FieldLabel>Month</FieldLabel><Input type="number" min={1} max={12} value={periodMonth} onChange={(e) => setPeriodMonth(Number(e.target.value))} /></Field>
|
<Field><FieldLabel>Month</FieldLabel><Input type="number" min={1} max={12} value={periodMonth} onChange={(e) => setPeriodMonth(Number(e.target.value))} /></Field>
|
||||||
</div>
|
</div>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={handleGenerate} disabled={submitting}>{submitting ? "Generating…" : "Generate"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleGenerate} disabled={submitting}>{submitting ? "Generating…" : "Generate"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default function DepartmentsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Departments</h1>
|
<h1 className="text-2xl font-bold text-foreground">Departments</h1>
|
||||||
<p className="text-base text-muted-foreground">Org structure — unlimited nesting, optionally scoped to a branch.</p>
|
<p className="text-base text-muted-foreground">Org structure — unlimited nesting, optionally scoped to a branch.</p>
|
||||||
@@ -140,9 +140,9 @@ export default function DepartmentsPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function DocumentTypesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Document Types</h1>
|
<h1 className="text-2xl font-bold text-foreground">Document Types</h1>
|
||||||
<p className="text-base text-muted-foreground">The staff document catalog — NIC, contracts, certificates, etc.</p>
|
<p className="text-base text-muted-foreground">The staff document catalog — NIC, contracts, certificates, etc.</p>
|
||||||
@@ -120,9 +120,9 @@ export default function DocumentTypesPage() {
|
|||||||
<FieldLabel htmlFor="dt-expiry">Track expiry date (e.g. passport, visa)</FieldLabel>
|
<FieldLabel htmlFor="dt-expiry">Track expiry date (e.g. passport, visa)</FieldLabel>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default function LeaveTypesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Leave Types</h1>
|
<h1 className="text-2xl font-bold text-foreground">Leave Types</h1>
|
||||||
<p className="text-base text-muted-foreground">Annual, Casual, Medical, Unpaid, etc. — drives Attendance's OnLeave classification and Payroll's No-Pay calc.</p>
|
<p className="text-base text-muted-foreground">Annual, Casual, Medical, Unpaid, etc. — drives Attendance's OnLeave classification and Payroll's No-Pay calc.</p>
|
||||||
@@ -121,9 +121,9 @@ export default function LeaveTypesPage() {
|
|||||||
<FieldLabel htmlFor="lt-carry">Carry-forward allowed</FieldLabel>
|
<FieldLabel htmlFor="lt-carry">Carry-forward allowed</FieldLabel>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default function SalaryComponentsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Salary Components</h1>
|
<h1 className="text-2xl font-bold text-foreground">Salary Components</h1>
|
||||||
<p className="text-base text-muted-foreground">Allowances and ad hoc other deductions — OT/Late/No-Pay/Loan/EPF/ETF/Tax are computed automatically, not components.</p>
|
<p className="text-base text-muted-foreground">Allowances and ad hoc other deductions — OT/Late/No-Pay/Loan/EPF/ETF/Tax are computed automatically, not components.</p>
|
||||||
@@ -120,9 +120,9 @@ export default function SalaryComponentsPage() {
|
|||||||
<FieldLabel htmlFor="sc-epfetf">EPF/ETF applicable</FieldLabel>
|
<FieldLabel htmlFor="sc-epfetf">EPF/ETF applicable</FieldLabel>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default function StatutorySettingsPage() {
|
|||||||
{error && <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-5 text-base text-destructive">{error}</div>}
|
{error && <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-5 text-base text-destructive">{error}</div>}
|
||||||
|
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<h2 className="text-lg font-semibold text-foreground">EPF / ETF Rates</h2>
|
<h2 className="text-lg font-semibold text-foreground">EPF / ETF Rates</h2>
|
||||||
<Dialog open={settingOpen} onOpenChange={setSettingOpen}>
|
<Dialog open={settingOpen} onOpenChange={setSettingOpen}>
|
||||||
<DialogTrigger render={<Button size="sm"><Plus className="size-4" />New Setting</Button>} />
|
<DialogTrigger render={<Button size="sm"><Plus className="size-4" />New Setting</Button>} />
|
||||||
@@ -101,9 +101,9 @@ export default function StatutorySettingsPage() {
|
|||||||
<Field><FieldLabel>ETF employer rate (0-1)</FieldLabel><Input type="number" step="0.001" value={etfEmployerRate} onChange={(e) => setEtfEmployerRate(Number(e.target.value))} /></Field>
|
<Field><FieldLabel>ETF employer rate (0-1)</FieldLabel><Input type="number" step="0.001" value={etfEmployerRate} onChange={(e) => setEtfEmployerRate(Number(e.target.value))} /></Field>
|
||||||
<Field><FieldLabel>Default OT multiplier</FieldLabel><Input type="number" step="0.1" value={otMultiplierDefault} onChange={(e) => setOtMultiplierDefault(Number(e.target.value))} /></Field>
|
<Field><FieldLabel>Default OT multiplier</FieldLabel><Input type="number" step="0.1" value={otMultiplierDefault} onChange={(e) => setOtMultiplierDefault(Number(e.target.value))} /></Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setSettingOpen(false)} disabled={submitting}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setSettingOpen(false)} disabled={submitting}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={createSetting} disabled={submitting}>{submitting ? "Saving…" : "Save"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={createSetting} disabled={submitting}>{submitting ? "Saving…" : "Save"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -133,7 +133,7 @@ export default function StatutorySettingsPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<h2 className="text-lg font-semibold text-foreground">Tax Slabs</h2>
|
<h2 className="text-lg font-semibold text-foreground">Tax Slabs</h2>
|
||||||
<Dialog open={slabOpen} onOpenChange={setSlabOpen}>
|
<Dialog open={slabOpen} onOpenChange={setSlabOpen}>
|
||||||
<DialogTrigger render={<Button size="sm"><Plus className="size-4" />New Slab</Button>} />
|
<DialogTrigger render={<Button size="sm"><Plus className="size-4" />New Slab</Button>} />
|
||||||
@@ -148,9 +148,9 @@ export default function StatutorySettingsPage() {
|
|||||||
<Field><FieldLabel>Upper bound (optional)</FieldLabel><Input type="number" value={upperBound} onChange={(e) => setUpperBound(e.target.value)} placeholder="And above" /></Field>
|
<Field><FieldLabel>Upper bound (optional)</FieldLabel><Input type="number" value={upperBound} onChange={(e) => setUpperBound(e.target.value)} placeholder="And above" /></Field>
|
||||||
<Field><FieldLabel>Rate (0-1)</FieldLabel><Input type="number" step="0.01" value={rate} onChange={(e) => setRate(Number(e.target.value))} /></Field>
|
<Field><FieldLabel>Rate (0-1)</FieldLabel><Input type="number" step="0.01" value={rate} onChange={(e) => setRate(Number(e.target.value))} /></Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setSlabOpen(false)} disabled={submitting}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setSlabOpen(false)} disabled={submitting}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={createSlab} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={createSlab} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default function WorkShiftsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Work Shifts</h1>
|
<h1 className="text-2xl font-bold text-foreground">Work Shifts</h1>
|
||||||
<p className="text-base text-muted-foreground">The baseline Attendance computes Late/Early/OT against.</p>
|
<p className="text-base text-muted-foreground">The baseline Attendance computes Late/Early/OT against.</p>
|
||||||
@@ -122,7 +122,7 @@ export default function WorkShiftsPage() {
|
|||||||
<Input id="w-name" value={name} onChange={(e) => setName(e.target.value)} aria-invalid={!!errors.name} />
|
<Input id="w-name" value={name} onChange={(e) => setName(e.target.value)} aria-invalid={!!errors.name} />
|
||||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||||
</Field>
|
</Field>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel htmlFor="w-start">Start time</FieldLabel>
|
<FieldLabel htmlFor="w-start">Start time</FieldLabel>
|
||||||
<Input id="w-start" type="time" value={startTime} onChange={(e) => setStartTime(e.target.value)} />
|
<Input id="w-start" type="time" value={startTime} onChange={(e) => setStartTime(e.target.value)} />
|
||||||
@@ -136,7 +136,7 @@ export default function WorkShiftsPage() {
|
|||||||
<Checkbox id="w-overnight" checked={isOvernight} onCheckedChange={(v) => setIsOvernight(v === true)} />
|
<Checkbox id="w-overnight" checked={isOvernight} onCheckedChange={(v) => setIsOvernight(v === true)} />
|
||||||
<FieldLabel htmlFor="w-overnight">Overnight shift (end time rolls past midnight)</FieldLabel>
|
<FieldLabel htmlFor="w-overnight">Overnight shift (end time rolls past midnight)</FieldLabel>
|
||||||
</Field>
|
</Field>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel htmlFor="w-grace">Grace (minutes)</FieldLabel>
|
<FieldLabel htmlFor="w-grace">Grace (minutes)</FieldLabel>
|
||||||
<Input id="w-grace" type="number" min={0} value={graceMinutes} onChange={(e) => setGraceMinutes(Number(e.target.value))} />
|
<Input id="w-grace" type="number" min={0} value={graceMinutes} onChange={(e) => setGraceMinutes(Number(e.target.value))} />
|
||||||
@@ -166,9 +166,9 @@ export default function WorkShiftsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ export default function DashboardLayout({
|
|||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<div className="flex h-screen overflow-hidden bg-background">
|
<div className="flex h-screen overflow-hidden bg-background">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<main className="flex flex-1 flex-col">
|
<main className="flex min-w-0 flex-1 flex-col">
|
||||||
<Header />
|
<Header />
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="min-w-0 flex-1 overflow-auto">
|
||||||
<div className="p-6 lg:p-8">
|
<div className="min-w-0 px-6 pt-3 pb-6 lg:px-8 lg:pt-4 lg:pb-8">
|
||||||
<Breadcrumbs />
|
<Breadcrumbs />
|
||||||
<div className="rounded-xl bg-card border border-border shadow-sm">
|
<div className="rounded-xl bg-card border border-border shadow-sm">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ export default function PurchaseOrderDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/procurement/purchase-orders" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/procurement/purchase-orders" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
@@ -283,7 +283,7 @@ export default function PurchaseOrderDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{po.status === "Draft" && (
|
{po.status === "Draft" && (
|
||||||
<>
|
<>
|
||||||
<Button variant="outline" size="lg" onClick={handleSubmitPo} disabled={submitting || deleting}>
|
<Button variant="outline" size="lg" onClick={handleSubmitPo} disabled={submitting || deleting}>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default function PurchaseOrdersListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Purchase Orders</h1>
|
<h1 className="text-2xl font-bold text-foreground">Purchase Orders</h1>
|
||||||
<p className="text-base text-muted-foreground">Auto-approved on creation and freely editable while open (FR-PROC-03..05).</p>
|
<p className="text-base text-muted-foreground">Auto-approved on creation and freely editable while open (FR-PROC-03..05).</p>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function PurchaseReturnsListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Purchase Returns</h1>
|
<h1 className="text-2xl font-bold text-foreground">Purchase Returns</h1>
|
||||||
<p className="text-base text-muted-foreground">Return received goods to a vendor, referencing the original GRN line (FR-PROC-08).</p>
|
<p className="text-base text-muted-foreground">Return received goods to a vendor, referencing the original GRN line (FR-PROC-08).</p>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function RequisitionDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/procurement/requisitions" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/procurement/requisitions" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
@@ -86,7 +86,7 @@ export default function RequisitionDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{requisition.status === "Draft" && (
|
{requisition.status === "Draft" && (
|
||||||
<Button size="lg" onClick={handleSubmit} disabled={submitting}>
|
<Button size="lg" onClick={handleSubmit} disabled={submitting}>
|
||||||
<Send className="size-5" />
|
<Send className="size-5" />
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function RequisitionsListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Requisitions</h1>
|
<h1 className="text-2xl font-bold text-foreground">Requisitions</h1>
|
||||||
<p className="text-base text-muted-foreground">Raise a purchase requisition and submit it into procurement (FR-PROC-01).</p>
|
<p className="text-base text-muted-foreground">Raise a purchase requisition and submit it into procurement (FR-PROC-01).</p>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function RfqsListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">RFQs</h1>
|
<h1 className="text-2xl font-bold text-foreground">RFQs</h1>
|
||||||
<p className="text-base text-muted-foreground">Request quotations from vendors and compare pricing (FR-PROC-02).</p>
|
<p className="text-base text-muted-foreground">Request quotations from vendors and compare pricing (FR-PROC-02).</p>
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export default function ItemDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export default function BrandsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
@@ -180,11 +180,11 @@ export default function BrandsPage() {
|
|||||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="min-w-36" onClick={handleSubmit} disabled={submitting}>
|
<Button className="w-full sm:w-auto sm:min-w-36" onClick={handleSubmit} disabled={submitting}>
|
||||||
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
|
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export default function CategorySubCategoriesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/products/categories" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/products/categories" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
@@ -138,11 +138,11 @@ export default function CategorySubCategoriesPage() {
|
|||||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="min-w-36" onClick={handleSubmit} disabled={submitting}>
|
<Button className="w-full sm:w-auto sm:min-w-36" onClick={handleSubmit} disabled={submitting}>
|
||||||
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
|
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default function CategoriesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
@@ -179,11 +179,11 @@ export default function CategoriesPage() {
|
|||||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="min-w-36" onClick={handleSubmit} disabled={submitting}>
|
<Button className="w-full sm:w-auto sm:min-w-36" onClick={handleSubmit} disabled={submitting}>
|
||||||
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
|
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export default function ItemTypesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
@@ -141,11 +141,11 @@ export default function ItemTypesPage() {
|
|||||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="min-w-36" onClick={handleSubmit} disabled={submitting}>
|
<Button className="w-full sm:w-auto sm:min-w-36" onClick={handleSubmit} disabled={submitting}>
|
||||||
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
|
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export default function ItemsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Items</h1>
|
<h1 className="text-2xl font-bold text-foreground">Items</h1>
|
||||||
<p className="text-base text-muted-foreground">Item master — SKU, tracking mode, category, default vendor (FR-MD-01).</p>
|
<p className="text-base text-muted-foreground">Item master — SKU, tracking mode, category, default vendor (FR-MD-01).</p>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function UomsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
@@ -82,11 +82,11 @@ export default function UomsPage() {
|
|||||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}>
|
<Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
|
||||||
{submitting ? "Creating…" : "Create"}
|
{submitting ? "Creating…" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export default function GrnDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/receiving/grn" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/receiving/grn" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
|
|||||||
@@ -400,20 +400,7 @@ export default function NewGrnPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
|
||||||
<h2 className="text-base font-semibold text-foreground">Lines</h2>
|
|
||||||
{mode === "po" && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Lines default from the PO's open quantities — add a row for anything received that wasn't ordered.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button type="button" variant="outline" onClick={() => setLines((prev) => [...prev, emptyLine()])}>
|
|
||||||
<Plus className="size-5" />
|
|
||||||
Add line
|
|
||||||
</Button>
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-semibold text-foreground">Lines</h2>
|
<h2 className="text-base font-semibold text-foreground">Lines</h2>
|
||||||
{mode === "po" && (
|
{mode === "po" && (
|
||||||
@@ -422,7 +409,7 @@ export default function NewGrnPage() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{/* Off-PO items are allowed on a PO-based GRN — the server treats a line with
|
{/* Off-PO items are allowed on a PO-based GRN — the server treats a line with
|
||||||
no poLineId as a direct receipt (docs/10 FR-GRN-01, revised). */}
|
no poLineId as a direct receipt (docs/10 FR-GRN-01, revised). */}
|
||||||
<Button type="button" variant="outline" onClick={() => setLines((prev) => [...prev, emptyLine()])}>
|
<Button type="button" variant="outline" onClick={() => setLines((prev) => [...prev, emptyLine()])}>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function GrnListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Goods Receipt Notes</h1>
|
<h1 className="text-2xl font-bold text-foreground">Goods Receipt Notes</h1>
|
||||||
<p className="text-base text-muted-foreground">
|
<p className="text-base text-muted-foreground">
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export default function RolesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Roles</h1>
|
<h1 className="text-2xl font-bold text-foreground">Roles</h1>
|
||||||
<p className="text-base text-muted-foreground">
|
<p className="text-base text-muted-foreground">
|
||||||
@@ -182,11 +182,11 @@ export default function RolesPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}>
|
<Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
|
||||||
{submitting ? "Creating…" : "Create"}
|
{submitting ? "Creating…" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export default function UsersPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Users</h1>
|
<h1 className="text-2xl font-bold text-foreground">Users</h1>
|
||||||
<p className="text-base text-muted-foreground">
|
<p className="text-base text-muted-foreground">
|
||||||
@@ -242,11 +242,11 @@ export default function UsersPage() {
|
|||||||
<FieldError errors={[errors.userTypeId ? { message: errors.userTypeId } : undefined]} />
|
<FieldError errors={[errors.userTypeId ? { message: errors.userTypeId } : undefined]} />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}>
|
<Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
|
||||||
{submitting ? "Creating…" : "Create"}
|
{submitting ? "Creating…" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function AdjustmentsListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function CountDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/stock/counts" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/stock/counts" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function CountsListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export default function TransferDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/stock/transfers" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/stock/transfers" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function TransfersListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function WastagePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
|
|||||||
+1
-1
@@ -130,7 +130,7 @@ export default function VendorDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/vendors" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/vendors" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
|
|||||||
+5
-5
@@ -127,7 +127,7 @@ export default function VendorsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Vendors</h1>
|
<h1 className="text-2xl font-bold text-foreground">Vendors</h1>
|
||||||
<p className="text-base text-muted-foreground">Supplier master data — code, terms, tax registration, currency (FR-MD-06).</p>
|
<p className="text-base text-muted-foreground">Supplier master data — code, terms, tax registration, currency (FR-MD-06).</p>
|
||||||
@@ -172,11 +172,11 @@ export default function VendorsPage() {
|
|||||||
<FieldError errors={[errors.currency ? { message: errors.currency } : undefined]} />
|
<FieldError errors={[errors.currency ? { message: errors.currency } : undefined]} />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}>
|
<Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
|
||||||
{submitting ? "Creating…" : "Create"}
|
{submitting ? "Creating…" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -303,7 +303,7 @@ export default function VendorsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center pt-2">
|
<div className="flex justify-center pt-2">
|
||||||
<DialogClose render={<Button variant="outline" className="min-w-36" />}>Close</DialogClose>
|
<DialogClose render={<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" />}>Close</DialogClose>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default function WarehouseDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard/warehouse" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard/warehouse" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
@@ -124,11 +124,11 @@ export default function WarehouseDetailPage() {
|
|||||||
<Input id="bin-type" value={binType} onChange={(e) => setBinType(e.target.value)} placeholder="Shelf, Pallet, …" />
|
<Input id="bin-type" value={binType} onChange={(e) => setBinType(e.target.value)} placeholder="Shelf, Pallet, …" />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}>
|
<Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
|
||||||
{submitting ? "Creating…" : "Create"}
|
{submitting ? "Creating…" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export default function WarehousesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
<Link href="/dashboard" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
@@ -134,11 +134,11 @@ export default function WarehousesPage() {
|
|||||||
<Input id="wh-code" value={generatedCode} readOnly disabled placeholder="Enter a name to generate a code" className="text-muted-foreground" />
|
<Input id="wh-code" value={generatedCode} readOnly disabled placeholder="Enter a name to generate a code" className="text-muted-foreground" />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
<div className="flex justify-center gap-3 pt-2">
|
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}>
|
<Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
|
||||||
{submitting ? "Creating…" : "Create"}
|
{submitting ? "Creating…" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@import "shadcn/tailwind.css";
|
@import "shadcn/tailwind.css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *, .vibrant *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
@@ -129,6 +129,69 @@
|
|||||||
--sidebar-ring: oklch(0.68 0.186 265.215);
|
--sidebar-ring: oklch(0.68 0.186 265.215);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Vibrant — the "System" toggle option. Light content area (background,
|
||||||
|
cards, header, table panels) paired with a dark sidebar — the same split
|
||||||
|
Linear/Vercel/Notion use in their light themes. One violet accent drives
|
||||||
|
every interactive state; the sidebar keeps its own dark token family
|
||||||
|
(applied via .sidebar-surface below) so it stays dark regardless. */
|
||||||
|
.vibrant {
|
||||||
|
--background: oklch(0.97 0.004 265);
|
||||||
|
--foreground: oklch(0.2 0.02 265);
|
||||||
|
--card: oklch(0.995 0.002 265);
|
||||||
|
--card-foreground: oklch(0.2 0.02 265);
|
||||||
|
--popover: oklch(0.995 0.002 265);
|
||||||
|
--popover-foreground: oklch(0.2 0.02 265);
|
||||||
|
--primary: oklch(0.55 0.2 275);
|
||||||
|
--primary-foreground: oklch(0.98 0 0);
|
||||||
|
--secondary: oklch(0.93 0.02 275);
|
||||||
|
--secondary-foreground: oklch(0.32 0.15 275);
|
||||||
|
--muted: oklch(0.94 0.006 265);
|
||||||
|
--muted-foreground: oklch(0.48 0.02 265);
|
||||||
|
--accent: oklch(0.55 0.14 210);
|
||||||
|
--accent-foreground: oklch(0.98 0 0);
|
||||||
|
--destructive: oklch(0.58 0.22 25);
|
||||||
|
--border: oklch(0.88 0.012 265);
|
||||||
|
--input: oklch(0.92 0.01 265);
|
||||||
|
--ring: oklch(0.55 0.2 275);
|
||||||
|
--success: oklch(0.55 0.15 150);
|
||||||
|
--warning: oklch(0.72 0.15 80);
|
||||||
|
--error: oklch(0.58 0.22 25);
|
||||||
|
--info: oklch(0.55 0.14 210);
|
||||||
|
--chart-1: oklch(0.55 0.2 275);
|
||||||
|
--chart-2: oklch(0.55 0.14 210);
|
||||||
|
--chart-3: oklch(0.55 0.15 150);
|
||||||
|
--chart-4: oklch(0.72 0.15 80);
|
||||||
|
--chart-5: oklch(0.58 0.22 25);
|
||||||
|
--sidebar: oklch(0.18 0.02 265);
|
||||||
|
--sidebar-foreground: oklch(0.96 0.005 265);
|
||||||
|
--sidebar-primary: oklch(0.64 0.19 275);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0 0);
|
||||||
|
--sidebar-accent: oklch(0.28 0.03 265);
|
||||||
|
--sidebar-accent-foreground: oklch(0.96 0.005 265);
|
||||||
|
--sidebar-border: oklch(0.26 0.025 265);
|
||||||
|
--sidebar-ring: oklch(0.64 0.19 275);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Re-points the shared tokens (--card, --foreground, --muted*, --primary...)
|
||||||
|
at the sidebar's own dark family for anything inside this scope, so
|
||||||
|
AppSidebar's existing bg-card/text-foreground/text-muted-foreground
|
||||||
|
classes render dark without AppSidebar.tsx needing sidebar-specific
|
||||||
|
classes. CSS custom properties resolve against the cascade at point of
|
||||||
|
use, so this indirection (the same trick .dark/.vibrant use at the root)
|
||||||
|
works scoped to just this subtree. */
|
||||||
|
.vibrant .sidebar-surface {
|
||||||
|
--card: var(--sidebar);
|
||||||
|
--card-foreground: var(--sidebar-foreground);
|
||||||
|
--popover: var(--sidebar);
|
||||||
|
--popover-foreground: var(--sidebar-foreground);
|
||||||
|
--foreground: var(--sidebar-foreground);
|
||||||
|
--muted: var(--sidebar-accent);
|
||||||
|
--muted-foreground: oklch(0.72 0.015 265);
|
||||||
|
--primary: var(--sidebar-primary);
|
||||||
|
--primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--border: var(--sidebar-border);
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
@@ -155,3 +218,4 @@
|
|||||||
@apply text-lg;
|
@apply text-lg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ export default function RootLayout({
|
|||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col">
|
<body className="min-h-full flex flex-col">
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="light"
|
||||||
|
themes={["light", "dark", "vibrant"]}
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
<TooltipProvider>{children}</TooltipProvider>
|
<TooltipProvider>{children}</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ export default function ForgotOtpPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 p-4">
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 p-4">
|
||||||
<div className="w-full max-w-2xl rounded-xl bg-card p-8 shadow-lg border border-gray-200">
|
<div className="w-full max-w-2xl rounded-xl bg-card p-8 shadow-lg border border-border">
|
||||||
<div className="flex flex-col items-center mb-8">
|
<div className="flex flex-col items-center mb-8">
|
||||||
<div className="bg-black/10 rounded-full p-4 mb-4">
|
<div className="bg-foreground/10 rounded-full p-4 mb-4">
|
||||||
<Mail className="text-black size-6" />
|
<Mail className="text-foreground size-6" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-foreground text-center">Verify your email</h1>
|
<h1 className="text-2xl font-bold text-foreground text-center">Verify your email</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@ export default function ForgotOtpPage() {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleResend()
|
handleResend()
|
||||||
}}
|
}}
|
||||||
className="text-sm font-medium text-black hover:text-black/80 transition-colors"
|
className="text-sm font-medium text-foreground hover:text-foreground/80 transition-colors"
|
||||||
>
|
>
|
||||||
Didn't receive the code? Resend
|
Didn't receive the code? Resend
|
||||||
</button>
|
</button>
|
||||||
@@ -57,10 +57,10 @@ export default function ForgotOtpPage() {
|
|||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Link href="/login" className="flex-1">
|
<Link href="/login" className="flex-1">
|
||||||
<Button variant="outline" className="w-full h-12 rounded-full border-gray-300 text-black hover:bg-black/5">Back</Button>
|
<Button variant="outline" className="w-full h-12 rounded-full border-input text-foreground hover:bg-foreground/5">Back</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/login/forgot/reset" className="flex-1">
|
<Link href="/login/forgot/reset" className="flex-1">
|
||||||
<Button className="w-full h-12 rounded-full bg-black text-white hover:bg-black/90">Verify</Button>
|
<Button className="w-full h-12 rounded-full bg-foreground text-background hover:bg-foreground/90">Verify</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function ForgotEmailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 px-4 py-8">
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 px-4 py-8">
|
||||||
<div className="w-full max-w-2xl rounded-xl bg-card p-8 pl-10 shadow-lg border border-gray-200">
|
<div className="w-full max-w-2xl rounded-xl bg-card p-8 pl-10 shadow-lg border border-border">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<h1 className="text-2xl font-bold text-foreground">Reset your password</h1>
|
<h1 className="text-2xl font-bold text-foreground">Reset your password</h1>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">Enter the email address associated with your account and we'll send you a one-time code to verify your identity.</p>
|
<p className="mt-2 text-sm text-muted-foreground">Enter the email address associated with your account and we'll send you a one-time code to verify your identity.</p>
|
||||||
@@ -40,19 +40,28 @@ export default function ForgotEmailPage() {
|
|||||||
{emailError !== "" && <p className="text-sm text-error mt-2 font-medium">{emailError}</p>}
|
{emailError !== "" && <p className="text-sm text-error mt-2 font-medium">{emailError}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<div className="flex gap-3">
|
||||||
className="w-full h-12 text-base font-medium rounded-full bg-black text-white hover:bg-black/90"
|
<Button
|
||||||
disabled={emailError !== ""}
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => router.push('/login')}
|
||||||
if (email.length === 0) {
|
className="flex-1 h-12 rounded-full border-input text-foreground hover:bg-foreground/5"
|
||||||
setEmailError('Email is required')
|
>
|
||||||
return
|
Back
|
||||||
}
|
</Button>
|
||||||
if (emailError === "") router.push('/login/forgot/otp')
|
<Button
|
||||||
}}
|
className="flex-1 h-12 text-base font-medium rounded-full bg-foreground text-background hover:bg-foreground/90"
|
||||||
>
|
disabled={emailError !== ""}
|
||||||
Send verification code
|
onClick={() => {
|
||||||
</Button>
|
if (email.length === 0) {
|
||||||
|
setEmailError('Email is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (emailError === "") router.push('/login/forgot/otp')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Send verification code
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default function ForgotResetPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 p-4">
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 p-4">
|
||||||
<div className="w-full max-w-xl rounded-xl bg-card p-8 shadow-lg border border-gray-200">
|
<div className="w-full max-w-xl rounded-xl bg-card p-8 shadow-lg border border-border">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<h1 className="text-2xl font-bold text-foreground">Create a new password</h1>
|
<h1 className="text-2xl font-bold text-foreground">Create a new password</h1>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">Choose a strong password to secure your account.</p>
|
<p className="mt-2 text-sm text-muted-foreground">Choose a strong password to secure your account.</p>
|
||||||
@@ -103,15 +103,15 @@ export default function ForgotResetPage() {
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.push('/login')}
|
||||||
className="flex-1 h-12 rounded-full border-gray-300 text-black hover:bg-black/5"
|
className="flex-1 h-12 rounded-full border-input text-foreground hover:bg-foreground/5"
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={!allValid}
|
disabled={!allValid}
|
||||||
onClick={() => setShowSuccess(true)}
|
onClick={() => setShowSuccess(true)}
|
||||||
className="flex-1 h-12 rounded-full bg-black text-white hover:bg-black/90"
|
className="flex-1 h-12 rounded-full bg-foreground text-background hover:bg-foreground/90"
|
||||||
>
|
>
|
||||||
Change password
|
Change password
|
||||||
</Button>
|
</Button>
|
||||||
@@ -131,7 +131,7 @@ export default function ForgotResetPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Button onClick={() => router.push('/login')} className="w-4/5 mx-auto h-12 mt-6 rounded-full bg-black text-white hover:bg-black/90">
|
<Button onClick={() => router.push('/login')} className="w-4/5 mx-auto h-12 mt-6 rounded-full bg-foreground text-background hover:bg-foreground/90">
|
||||||
Return to sign in
|
Return to sign in
|
||||||
</Button>
|
</Button>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -17,29 +17,6 @@ import { Checkbox } from "@/components/ui/checkbox"
|
|||||||
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
|
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
|
|
||||||
function GoogleIcon() {
|
|
||||||
return (
|
|
||||||
<svg viewBox="0 0 24 24" className="size-4">
|
|
||||||
<path
|
|
||||||
fill="#4285F4"
|
|
||||||
d="M23.52 12.27c0-.85-.08-1.67-.22-2.45H12v4.64h6.47a5.53 5.53 0 0 1-2.4 3.63v3h3.89c2.27-2.09 3.56-5.17 3.56-8.82Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#34A853"
|
|
||||||
d="M12 24c3.24 0 5.95-1.07 7.93-2.91l-3.89-3c-1.08.73-2.46 1.15-4.04 1.15-3.1 0-5.73-2.09-6.67-4.9H1.32v3.09A12 12 0 0 0 12 24Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#FBBC05"
|
|
||||||
d="M5.33 14.34a7.2 7.2 0 0 1 0-4.68V6.57H1.32a12 12 0 0 0 0 10.86l4.01-3.09Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#EA4335"
|
|
||||||
d="M12 4.77c1.76 0 3.35.6 4.6 1.79l3.45-3.45C17.94 1.19 15.23 0 12 0A12 12 0 0 0 1.32 6.57l4.01 3.09C6.27 6.86 8.9 4.77 12 4.77Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
@@ -77,19 +54,28 @@ function LoginForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen overflow-hidden">
|
<div className="relative min-h-screen overflow-hidden">
|
||||||
{/* Full-page background photo — right side only */}
|
{/* Full-page background photo — right side only. Dark mode swaps to the backdrop-free
|
||||||
<div className="absolute inset-y-0 left-[45%] right-0">
|
cutout (whithoutbackground.png) so there's no white scene box floating on the
|
||||||
|
dark panel. */}
|
||||||
|
<div className="absolute inset-y-0 left-[45%] right-0 bg-white dark:bg-background">
|
||||||
<Image
|
<Image
|
||||||
src="/images/loginerps.png"
|
src="/images/loginerps.png"
|
||||||
alt="ERP Illustration"
|
alt="ERP Illustration"
|
||||||
fill
|
fill
|
||||||
className="object-contain object-center bg-white"
|
className="object-contain object-center dark:hidden"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src="/images/whithoutbackground.png"
|
||||||
|
alt="ERP Illustration"
|
||||||
|
fill
|
||||||
|
className="hidden object-contain object-center dark:block"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Login panel — overlays left side, no hard border with the photo */}
|
{/* Login panel — overlays left side, no hard border with the photo */}
|
||||||
<main className="relative z-10 flex min-h-screen w-full flex-col justify-center bg-white px-6 py-12 sm:px-12 md:w-[45%]">
|
<main className="relative z-10 flex min-h-screen w-full flex-col justify-center bg-white dark:bg-background px-6 py-12 sm:px-12 md:w-[45%]">
|
||||||
<div className="mb-8 flex justify-center">
|
<div className="mb-8 flex justify-center">
|
||||||
<Link href="/" className="inline-flex items-center">
|
<Link href="/" className="inline-flex items-center">
|
||||||
<Image src="/images/logo%20(2).png" alt="Hexa ERP" width={300} height={150} />
|
<Image src="/images/logo%20(2).png" alt="Hexa ERP" width={300} height={150} />
|
||||||
@@ -153,7 +139,7 @@ function LoginForm() {
|
|||||||
</div>
|
</div>
|
||||||
<FieldError errors={[form.formState.errors.password]} />
|
<FieldError errors={[form.formState.errors.password]} />
|
||||||
<div className="flex justify-end mt-1">
|
<div className="flex justify-end mt-1">
|
||||||
<Link href="/login/forgot" className="text-sm font-medium text-black hover:text-black/70 transition-colors">
|
<Link href="/login/forgot" className="text-sm font-medium text-foreground hover:text-foreground/70 transition-colors">
|
||||||
Forgot password?
|
Forgot password?
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +154,7 @@ function LoginForm() {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-1/2 mx-auto mt-8 h-12 text-sm font-medium rounded-full bg-black text-white hover:bg-black/90"
|
className="w-1/2 mx-auto mt-8 h-12 text-sm font-medium rounded-full bg-foreground text-background hover:bg-foreground/90"
|
||||||
disabled={form.formState.isSubmitting}
|
disabled={form.formState.isSubmitting}
|
||||||
>
|
>
|
||||||
{form.formState.isSubmitting ? "Signing in…" : "Sign in"}
|
{form.formState.isSubmitting ? "Signing in…" : "Sign in"}
|
||||||
@@ -181,27 +167,9 @@ function LoginForm() {
|
|||||||
{Object.values(form.formState.errors).length > 0 && "There are errors in the form."}
|
{Object.values(form.formState.errors).length > 0 && "There are errors in the form."}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="my-8 flex items-center gap-4">
|
|
||||||
<div className="flex-1 h-px bg-border" />
|
|
||||||
<span className="text-sm text-muted-foreground font-medium">Or continue with</span>
|
|
||||||
<div className="flex-1 h-px bg-border" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="w-full h-11 text-base font-medium inline-flex items-center justify-center gap-3 rounded-full bg-transparent text-foreground border border-gray-300 hover:bg-black/5"
|
|
||||||
onClick={() => {
|
|
||||||
alert('Google OAuth flow placeholder')
|
|
||||||
}}
|
|
||||||
aria-label="Continue with Google"
|
|
||||||
>
|
|
||||||
<GoogleIcon />
|
|
||||||
Continue with Google
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="mt-12 text-center">
|
<div className="mt-12 text-center">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Don't have an account? <Link href="#" className="font-semibold text-black hover:text-black/80 transition-colors">Create an account</Link>
|
Don't have an account? <Link href="#" className="font-semibold text-foreground hover:text-foreground/80 transition-colors">Create an account</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ const navItems: {
|
|||||||
/** Where clicking the row actually navigates, if different from `href`. `href` itself
|
/** Where clicking the row actually navigates, if different from `href`. `href` itself
|
||||||
* stays the section prefix used to decide whether this row is "active". */
|
* stays the section prefix used to decide whether this row is "active". */
|
||||||
landingHref?: string
|
landingHref?: string
|
||||||
|
/** This item's own href has no page of its own (no landingHref either) — clicking the
|
||||||
|
* row should only expand/collapse its children, never navigate. */
|
||||||
|
expandOnly?: boolean
|
||||||
icon: LucideIcon
|
icon: LucideIcon
|
||||||
chevron?: boolean
|
chevron?: boolean
|
||||||
children?: { title: string; code: string; href: string; icon: LucideIcon }[]
|
children?: { title: string; code: string; href: string; icon: LucideIcon }[]
|
||||||
@@ -106,6 +109,7 @@ const navItems: {
|
|||||||
title: "Settings",
|
title: "Settings",
|
||||||
code: "settings",
|
code: "settings",
|
||||||
href: "/dashboard/settings",
|
href: "/dashboard/settings",
|
||||||
|
expandOnly: true,
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
chevron: true,
|
chevron: true,
|
||||||
children: [
|
children: [
|
||||||
@@ -139,9 +143,13 @@ function SidebarContent({
|
|||||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parent = items.find((i) =>
|
const parent = items.find((i) => {
|
||||||
i.children?.some((c) => pathname === c.href || pathname.startsWith(`${c.href}/`))
|
if (!i.children?.length) return false
|
||||||
)
|
if (i.children.some((c) => pathname === c.href || pathname.startsWith(`${c.href}/`))) return true
|
||||||
|
// Landing on the parent's own hub page (e.g. /dashboard/settings itself,
|
||||||
|
// not one of its children) should also reveal its sub-items.
|
||||||
|
return pathname === i.href || pathname.startsWith(`${i.href}/`)
|
||||||
|
})
|
||||||
if (parent) {
|
if (parent) {
|
||||||
setExpanded((prev) => (prev[parent.code] ? prev : { ...prev, [parent.code]: true }))
|
setExpanded((prev) => (prev[parent.code] ? prev : { ...prev, [parent.code]: true }))
|
||||||
}
|
}
|
||||||
@@ -153,7 +161,7 @@ function SidebarContent({
|
|||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full flex-col rounded-3xl bg-card p-3 shadow-sm ring-1 ring-foreground/10 transition-[width] duration-300 ease-in-out",
|
"sidebar-surface flex h-full flex-col rounded-3xl bg-card p-3 shadow-sm ring-1 ring-foreground/10 transition-[width] duration-300 ease-in-out",
|
||||||
!isMobile && (collapsed ? "w-20" : "w-64")
|
!isMobile && (collapsed ? "w-20" : "w-64")
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -187,7 +195,7 @@ function SidebarContent({
|
|||||||
|
|
||||||
{/* Nav items — scrolls internally when it overflows, without a visible
|
{/* Nav items — scrolls internally when it overflows, without a visible
|
||||||
scrollbar so the rounded panel stays clean. */}
|
scrollbar so the rounded panel stays clean. */}
|
||||||
<ul className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
<ul className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
item.href === "/dashboard" ? pathname === item.href : pathname.startsWith(item.href)
|
item.href === "/dashboard" ? pathname === item.href : pathname.startsWith(item.href)
|
||||||
@@ -205,15 +213,18 @@ function SidebarContent({
|
|||||||
<Link
|
<Link
|
||||||
href={item.landingHref ?? item.href}
|
href={item.landingHref ?? item.href}
|
||||||
title={iconOnly ? item.title : undefined}
|
title={iconOnly ? item.title : undefined}
|
||||||
onClick={onClose}
|
onClick={() => {
|
||||||
|
onClose?.()
|
||||||
|
if (hasChildren) setExpanded((prev) => (prev[item.code] ? prev : { ...prev, [item.code]: true }))
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 items-center gap-3 rounded-2xl px-4 py-3 text-base font-semibold",
|
"flex flex-1 items-center gap-3 rounded-2xl px-4 py-1.5 text-base font-normal",
|
||||||
iconOnly && "justify-center px-0",
|
iconOnly && "justify-center px-0",
|
||||||
isActive ? "text-primary" : "text-foreground"
|
isActive ? "text-primary" : "text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<item.icon
|
<item.icon
|
||||||
className={cn("size-5 shrink-0", isActive ? "text-primary" : "text-muted-foreground")}
|
className={cn("size-3.5 shrink-0", isActive ? "text-primary" : "text-muted-foreground")}
|
||||||
/>
|
/>
|
||||||
{!iconOnly && (
|
{!iconOnly && (
|
||||||
<>
|
<>
|
||||||
@@ -270,14 +281,14 @@ function SidebarContent({
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
tabIndex={isOpen ? undefined : -1}
|
tabIndex={isOpen ? undefined : -1}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium transition-colors",
|
"flex items-center gap-2.5 rounded-xl px-3 py-1 text-sm font-medium transition-colors",
|
||||||
childActive
|
childActive
|
||||||
? "bg-primary/10 text-primary"
|
? "bg-primary/10 text-primary"
|
||||||
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<child.icon
|
<child.icon
|
||||||
className={cn("size-4 shrink-0", childActive ? "text-primary" : "text-muted-foreground")}
|
className={cn("size-3 shrink-0", childActive ? "text-primary" : "text-muted-foreground")}
|
||||||
/>
|
/>
|
||||||
{child.title}
|
{child.title}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -362,9 +373,9 @@ export function AppSidebar() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMobileOpen(true)}
|
onClick={() => setMobileOpen(true)}
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
className="fixed top-5 left-5 z-40 flex size-10 items-center justify-center rounded-2xl bg-card shadow-sm ring-1 ring-foreground/10 text-muted-foreground hover:bg-muted lg:hidden"
|
className="fixed top-8 left-5 z-40 flex size-8 items-center justify-center rounded-xl bg-card shadow-sm ring-1 ring-foreground/10 text-muted-foreground hover:bg-muted lg:hidden"
|
||||||
>
|
>
|
||||||
<Menu className="size-5" />
|
<Menu className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* ── Mobile overlay backdrop ──────────────────────── */}
|
{/* ── Mobile overlay backdrop ──────────────────────── */}
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ export function Header() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="mx-6 mt-3 mb-6 flex items-center justify-between gap-2 rounded-3xl bg-card py-4 pr-4 pl-14 shadow-sm ring-1 ring-foreground/10 lg:mx-8 lg:mt-4 lg:gap-4 lg:p-4">
|
<header className="mx-3 mt-3 mb-3 flex items-center justify-between gap-2 rounded-3xl bg-card py-4 pr-3 pl-12 shadow-sm ring-1 ring-foreground/10 sm:mx-6 sm:pr-4 sm:pl-14 lg:mx-8 lg:mt-4 lg:mb-4 lg:gap-4 lg:p-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
|
||||||
{showBackButton && (
|
{showBackButton && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -176,10 +176,10 @@ export function Header() {
|
|||||||
<ArrowLeft className="size-5" />
|
<ArrowLeft className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<h1 className="text-base font-bold tracking-tight text-foreground sm:text-xl">{title}</h1>
|
<h1 className="min-w-0 truncate text-base font-bold tracking-tight text-foreground sm:text-xl">{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 sm:gap-3">
|
<div className="flex shrink-0 items-center gap-1.5 sm:gap-3">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
|
|
||||||
<Popover>
|
<Popover>
|
||||||
|
|||||||
@@ -1,38 +1,62 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Moon, Sun } from "lucide-react"
|
import { Moon, Sun, Monitor } from "lucide-react"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ value: "light", label: "Light mode", icon: Sun },
|
||||||
|
{ value: "vibrant", label: "Vibrant theme", icon: Monitor },
|
||||||
|
{ value: "dark", label: "Dark mode", icon: Moon },
|
||||||
|
] as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an empty slot until mounted: `resolvedTheme` is unknown on the server (and on
|
* Renders an empty slot until mounted: `theme` is unknown on the server (and on the
|
||||||
* the client's first paint, before next-themes reads localStorage), so rendering an icon
|
* client's first paint, before next-themes reads localStorage), so rendering the active
|
||||||
* before that would either be wrong or cause a hydration mismatch.
|
* segment before that would either be wrong or cause a hydration mismatch.
|
||||||
*/
|
*/
|
||||||
export function ThemeToggle({ className }: { className?: string }) {
|
export function ThemeToggle({ className }: { className?: string }) {
|
||||||
const { resolvedTheme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
useEffect(() => setMounted(true), [])
|
useEffect(() => setMounted(true), [])
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return <div className={cn("size-10 shrink-0", className)} aria-hidden="true" />
|
return <div className={cn("h-10 w-27 shrink-0", className)} aria-hidden="true" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDark = resolvedTheme === "dark"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
type="button"
|
role="radiogroup"
|
||||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
aria-label="Theme"
|
||||||
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
className={cn("flex shrink-0 items-center gap-0.5 rounded-full bg-muted p-1", className)}
|
||||||
className={cn(
|
|
||||||
"flex size-10 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{isDark ? <Sun className="size-5" /> : <Moon className="size-5" />}
|
{options.map(({ value, label, icon: Icon }) => {
|
||||||
</button>
|
const isActive = theme === value
|
||||||
|
const isVibrant = value === "vibrant"
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={isActive}
|
||||||
|
aria-label={label}
|
||||||
|
title={label}
|
||||||
|
onClick={() => setTheme(value)}
|
||||||
|
className={cn(
|
||||||
|
"flex size-8 shrink-0 items-center justify-center rounded-full transition-colors",
|
||||||
|
isActive
|
||||||
|
? isVibrant
|
||||||
|
? "bg-linear-to-br from-indigo-500 to-violet-600 text-white shadow-sm"
|
||||||
|
: "bg-card text-primary shadow-sm ring-1 ring-foreground/10"
|
||||||
|
: "text-muted-foreground hover:text-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="size-4" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,12 +143,11 @@ export function DataTable<T>({
|
|||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-indigo-50 hover:bg-indigo-50">
|
<TableRow className="border-b-0 hover:bg-primary/5">
|
||||||
{columns.map((column, index) => (
|
{columns.map((column, index) => (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={column.key}
|
key={column.key}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-indigo-700",
|
|
||||||
index === 0 && "rounded-l-lg pl-3",
|
index === 0 && "rounded-l-lg pl-3",
|
||||||
index === columns.length - 1 && !actions && "rounded-r-lg pr-3",
|
index === columns.length - 1 && !actions && "rounded-r-lg pr-3",
|
||||||
column.headerClassName
|
column.headerClassName
|
||||||
@@ -158,7 +157,7 @@ export function DataTable<T>({
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
{actions && (
|
{actions && (
|
||||||
<TableHead className="rounded-r-lg pr-3 text-right text-indigo-700">
|
<TableHead className="rounded-r-lg pr-3 text-right">
|
||||||
Actions
|
Actions
|
||||||
</TableHead>
|
</TableHead>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|||||||
return (
|
return (
|
||||||
<thead
|
<thead
|
||||||
data-slot="table-header"
|
data-slot="table-header"
|
||||||
className={cn("[&_tr]:border-b", className)}
|
className={cn("bg-primary/5 [&_tr]:border-b [&_tr]:hover:bg-primary/5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|||||||
<th
|
<th
|
||||||
data-slot="table-head"
|
data-slot="table-head"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
"h-10 px-2 text-left align-middle font-semibold whitespace-nowrap text-primary [&:has([role=checkbox])]:pr-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
Reference in New Issue
Block a user