Compare commits
5 Commits
Feature_HRM
...
ui-fixers
| Author | SHA1 | Date | |
|---|---|---|---|
| d35b076435 | |||
| c564916c60 | |||
| 86bb4d4908 | |||
| 816ffbbfb6 | |||
| ae20bc4e34 |
@@ -112,7 +112,7 @@ export default function AttendanceBatchDetailPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
@@ -132,9 +132,9 @@ export default function AttendanceBatchDetailPage() {
|
||||
<FieldGroup>
|
||||
<Field><FieldLabel htmlFor="u-reason">Reason</FieldLabel><Input id="u-reason" value={unlockReason} onChange={(e) => setUnlockReason(e.target.value)} /></Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={unlock} disabled={busy}>Unlock</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={unlock} disabled={busy}>Unlock</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function AttendanceBatchesPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
@@ -86,7 +86,7 @@ export default function AttendanceBatchesPage() {
|
||||
<DialogDescription>Columns: Employee Code, Date, Check In, Check Out.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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-end">Period end</FieldLabel><Input id="a-end" type="date" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} /></Field>
|
||||
</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)} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="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>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</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>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -208,7 +208,7 @@ export default function EmployeeDetailPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold text-foreground">{employee.fullName}</h1>
|
||||
<p className="text-base text-muted-foreground">
|
||||
@@ -237,7 +237,7 @@ export default function EmployeeDetailPage() {
|
||||
|
||||
{tab === "Overview" && (
|
||||
<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>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>
|
||||
@@ -363,7 +363,7 @@ export default function EmployeeDetailPage() {
|
||||
<DialogDescription>Supersedes the current open-ended structure from this date.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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>Basic salary</FieldLabel><Input type="number" min={0} value={structureBasic} onChange={(e) => setStructureBasic(Number(e.target.value))} /></Field>
|
||||
</div>
|
||||
@@ -383,9 +383,9 @@ export default function EmployeeDetailPage() {
|
||||
</div>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setStructureOpen(false)} disabled={busy}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={createStructure} disabled={busy}>{busy ? "Saving…" : "Save"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setStructureOpen(false)} disabled={busy}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={createStructure} disabled={busy}>{busy ? "Saving…" : "Save"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -433,7 +433,7 @@ export default function EmployeeDetailPage() {
|
||||
<SelectContent><SelectItem value="Loan">Loan</SelectItem><SelectItem value="Advance">Advance</SelectItem></SelectContent>
|
||||
</Select>
|
||||
</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>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>
|
||||
@@ -445,9 +445,9 @@ export default function EmployeeDetailPage() {
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setLoanOpen(false)} disabled={busy}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={createLoan} disabled={busy}>{busy ? "Creating…" : "Create"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setLoanOpen(false)} disabled={busy}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={createLoan} disabled={busy}>{busy ? "Creating…" : "Create"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -121,7 +121,7 @@ export default function EmployeesPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
@@ -134,7 +134,7 @@ export default function EmployeesPage() {
|
||||
<DialogDescription>Not every employee needs a login — a system user account is optional and separate.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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}>
|
||||
<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} />
|
||||
@@ -170,7 +170,7 @@ export default function EmployeesPage() {
|
||||
</div>
|
||||
)}
|
||||
</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}>
|
||||
<FieldLabel htmlFor="e-dept">Department</FieldLabel>
|
||||
<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>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function LeaveRequestsPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
@@ -129,15 +129,15 @@ export default function LeaveRequestsPage() {
|
||||
<SelectContent>{leaveTypes.map((t) => <SelectItem key={t.leaveTypeId} value={String(t.leaveTypeId)}>{t.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</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>End date</FieldLabel><Input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} /></Field>
|
||||
</div>
|
||||
<Field><FieldLabel>Reason (optional)</FieldLabel><Input value={reason} onChange={(e) => setReason(e.target.value)} /></Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Submitting…" : "Submit"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Submitting…" : "Submit"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function PayrollLineDetailPage() {
|
||||
</table>
|
||||
</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>Absent days: {line.absentDays}</div>
|
||||
<div>Leave days: {line.leaveDays}</div>
|
||||
|
||||
@@ -88,12 +88,12 @@ export default function PayrollRunDetailPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
{run.status === "Draft" && <Button onClick={approve} disabled={busy}>Approve</Button>}
|
||||
{run.status === "Approved" && <Button onClick={lock} disabled={busy}>Lock Payroll</Button>}
|
||||
@@ -110,9 +110,9 @@ export default function PayrollRunDetailPage() {
|
||||
<FieldGroup>
|
||||
<Field><FieldLabel htmlFor="p-reason">Reason</FieldLabel><Input id="p-reason" value={unlockReason} onChange={(e) => setUnlockReason(e.target.value)} /></Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={unlock} disabled={busy}>Unlock</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={unlock} disabled={busy}>Unlock</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function PayrollRunsPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold text-foreground">Payroll</h1>
|
||||
<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>
|
||||
</DialogHeader>
|
||||
<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>Month</FieldLabel><Input type="number" min={1} max={12} value={periodMonth} onChange={(e) => setPeriodMonth(Number(e.target.value))} /></Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={handleGenerate} disabled={submitting}>{submitting ? "Generating…" : "Generate"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleGenerate} disabled={submitting}>{submitting ? "Generating…" : "Generate"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function DepartmentsPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
@@ -140,9 +140,9 @@ export default function DepartmentsPage() {
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function DocumentTypesPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
@@ -120,9 +120,9 @@ export default function DocumentTypesPage() {
|
||||
<FieldLabel htmlFor="dt-expiry">Track expiry date (e.g. passport, visa)</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function LeaveTypesPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
@@ -121,9 +121,9 @@ export default function LeaveTypesPage() {
|
||||
<FieldLabel htmlFor="lt-carry">Carry-forward allowed</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function SalaryComponentsPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
@@ -120,9 +120,9 @@ export default function SalaryComponentsPage() {
|
||||
<FieldLabel htmlFor="sc-epfetf">EPF/ETF applicable</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</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>}
|
||||
|
||||
<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>
|
||||
<Dialog open={settingOpen} onOpenChange={setSettingOpen}>
|
||||
<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>Default OT multiplier</FieldLabel><Input type="number" step="0.1" value={otMultiplierDefault} onChange={(e) => setOtMultiplierDefault(Number(e.target.value))} /></Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setSettingOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={createSetting} disabled={submitting}>{submitting ? "Saving…" : "Save"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setSettingOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={createSetting} disabled={submitting}>{submitting ? "Saving…" : "Save"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -133,7 +133,7 @@ export default function StatutorySettingsPage() {
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<Dialog open={slabOpen} onOpenChange={setSlabOpen}>
|
||||
<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>Rate (0-1)</FieldLabel><Input type="number" step="0.01" value={rate} onChange={(e) => setRate(Number(e.target.value))} /></Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setSlabOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={createSlab} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setSlabOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={createSlab} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function WorkShiftsPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
@@ -122,7 +122,7 @@ export default function WorkShiftsPage() {
|
||||
<Input id="w-name" value={name} onChange={(e) => setName(e.target.value)} aria-invalid={!!errors.name} />
|
||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="w-start">Start time</FieldLabel>
|
||||
<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)} />
|
||||
<FieldLabel htmlFor="w-overnight">Overnight shift (end time rolls past midnight)</FieldLabel>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="w-grace">Grace (minutes)</FieldLabel>
|
||||
<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>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -13,10 +13,10 @@ export default function DashboardLayout({
|
||||
<AuthProvider>
|
||||
<div className="flex h-screen overflow-hidden bg-background">
|
||||
<AppSidebar />
|
||||
<main className="flex flex-1 flex-col">
|
||||
<main className="flex min-w-0 flex-1 flex-col">
|
||||
<Header />
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-6 lg:p-8">
|
||||
<div className="min-w-0 flex-1 overflow-auto">
|
||||
<div className="min-w-0 px-6 pt-3 pb-6 lg:px-8 lg:pt-4 lg:pb-8">
|
||||
<Breadcrumbs />
|
||||
<div className="rounded-xl bg-card border border-border shadow-sm">
|
||||
<div className="p-6">
|
||||
|
||||
@@ -267,7 +267,7 @@ export default function PurchaseOrderDetailPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/procurement/purchase-orders" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
@@ -283,7 +283,7 @@ export default function PurchaseOrderDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{po.status === "Draft" && (
|
||||
<>
|
||||
<Button variant="outline" size="lg" onClick={handleSubmitPo} disabled={submitting || deleting}>
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function PurchaseOrdersListPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function PurchaseReturnsListPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function RequisitionDetailPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/procurement/requisitions" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
@@ -86,7 +86,7 @@ export default function RequisitionDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{requisition.status === "Draft" && (
|
||||
<Button size="lg" onClick={handleSubmit} disabled={submitting}>
|
||||
<Send className="size-5" />
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function RequisitionsListPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function RfqsListPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -181,7 +181,7 @@ export default function ItemDetailPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
|
||||
@@ -155,7 +155,7 @@ export default function BrandsPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
@@ -180,11 +180,11 @@ export default function BrandsPage() {
|
||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
Cancel
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -111,7 +111,7 @@ export default function CategorySubCategoriesPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/products/categories" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
@@ -138,11 +138,11 @@ export default function CategorySubCategoriesPage() {
|
||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
Cancel
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -154,7 +154,7 @@ export default function CategoriesPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
@@ -179,11 +179,11 @@ export default function CategoriesPage() {
|
||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
Cancel
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function ItemTypesPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
@@ -141,11 +141,11 @@ export default function ItemTypesPage() {
|
||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
Cancel
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function ItemsPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function UomsPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
@@ -82,11 +82,11 @@ export default function UomsPage() {
|
||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
Cancel
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -116,7 +116,7 @@ export default function GrnDetailPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/receiving/grn" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
|
||||
@@ -400,20 +400,7 @@ export default function NewGrnPage() {
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center 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 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" && (
|
||||
@@ -422,7 +409,7 @@ export default function NewGrnPage() {
|
||||
</p>
|
||||
)}
|
||||
</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
|
||||
no poLineId as a direct receipt (docs/10 FR-GRN-01, revised). */}
|
||||
<Button type="button" variant="outline" onClick={() => setLines((prev) => [...prev, emptyLine()])}>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function GrnListPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold text-foreground">Goods Receipt Notes</h1>
|
||||
<p className="text-base text-muted-foreground">
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import Link from "next/link"
|
||||
import { ShieldCheck, SlidersHorizontal, Users } from "lucide-react"
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: "Roles",
|
||||
href: "/dashboard/settings/roles",
|
||||
icon: ShieldCheck,
|
||||
description: "Manage roles and navigation permissions",
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
href: "/dashboard/settings/users",
|
||||
icon: Users,
|
||||
description: "Create users and assign their roles",
|
||||
},
|
||||
{
|
||||
title: "Product Configuration",
|
||||
href: "/dashboard/products/settings",
|
||||
icon: SlidersHorizontal,
|
||||
description: "Configure product and master-data options",
|
||||
},
|
||||
]
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">ERP Settings</h1>
|
||||
<p className="text-base text-muted-foreground">
|
||||
Manage ERP access, users, and system configuration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{cards.map((card) => (
|
||||
<Link
|
||||
key={card.href}
|
||||
href={card.href}
|
||||
className="flex items-center gap-4 rounded-2xl border bg-card p-5 shadow-sm ring-1 ring-foreground/5 transition-colors hover:bg-muted"
|
||||
>
|
||||
<div className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-primary/10">
|
||||
<card.icon className="size-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-foreground">{card.title}</p>
|
||||
<p className="text-sm text-muted-foreground">{card.description}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -123,7 +123,7 @@ export default function RolesPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold text-foreground">Roles</h1>
|
||||
<p className="text-base text-muted-foreground">
|
||||
@@ -182,11 +182,11 @@ export default function RolesPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
Cancel
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function UsersPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold text-foreground">Users</h1>
|
||||
<p className="text-base text-muted-foreground">
|
||||
@@ -242,11 +242,11 @@ export default function UsersPage() {
|
||||
<FieldError errors={[errors.userTypeId ? { message: errors.userTypeId } : undefined]} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
Cancel
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function AdjustmentsListPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function CountDetailPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/stock/counts" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function CountsListPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function TransferDetailPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/stock/transfers" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function TransfersListPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function WastagePage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ export default function VendorDetailPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/vendors" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
|
||||
+5
-5
@@ -127,7 +127,7 @@ export default function VendorsPage() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
@@ -172,11 +172,11 @@ export default function VendorsPage() {
|
||||
<FieldError errors={[errors.currency ? { message: errors.currency } : undefined]} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
Cancel
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -303,7 +303,7 @@ export default function VendorsPage() {
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function WarehouseDetailPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard/warehouse" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<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, …" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
Cancel
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function WarehousesPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Link href="/dashboard" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<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" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
Cancel
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@custom-variant dark (&:is(.dark *, .vibrant *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
@@ -129,6 +129,69 @@
|
||||
--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 {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
@@ -154,4 +217,5 @@
|
||||
h4 {
|
||||
@apply text-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,12 @@ export default function RootLayout({
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<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>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
|
||||
@@ -25,10 +25,10 @@ export default function ForgotOtpPage() {
|
||||
|
||||
return (
|
||||
<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="bg-black/10 rounded-full p-4 mb-4">
|
||||
<Mail className="text-black size-6" />
|
||||
<div className="bg-foreground/10 rounded-full p-4 mb-4">
|
||||
<Mail className="text-foreground size-6" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground text-center">Verify your email</h1>
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@ export default function ForgotOtpPage() {
|
||||
e.preventDefault()
|
||||
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
|
||||
</button>
|
||||
@@ -57,10 +57,10 @@ export default function ForgotOtpPage() {
|
||||
|
||||
<div className="flex gap-3">
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function ForgotEmailPage() {
|
||||
|
||||
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="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">
|
||||
<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>
|
||||
@@ -40,19 +40,28 @@ export default function ForgotEmailPage() {
|
||||
{emailError !== "" && <p className="text-sm text-error mt-2 font-medium">{emailError}</p>}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full h-12 text-base font-medium rounded-full bg-black text-white hover:bg-black/90"
|
||||
disabled={emailError !== ""}
|
||||
onClick={() => {
|
||||
if (email.length === 0) {
|
||||
setEmailError('Email is required')
|
||||
return
|
||||
}
|
||||
if (emailError === "") router.push('/login/forgot/otp')
|
||||
}}
|
||||
>
|
||||
Send verification code
|
||||
</Button>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.push('/login')}
|
||||
className="flex-1 h-12 rounded-full border-input text-foreground hover:bg-foreground/5"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1 h-12 text-base font-medium rounded-full bg-foreground text-background hover:bg-foreground/90"
|
||||
disabled={emailError !== ""}
|
||||
onClick={() => {
|
||||
if (email.length === 0) {
|
||||
setEmailError('Email is required')
|
||||
return
|
||||
}
|
||||
if (emailError === "") router.push('/login/forgot/otp')
|
||||
}}
|
||||
>
|
||||
Send verification code
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function ForgotResetPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
@@ -103,15 +103,15 @@ export default function ForgotResetPage() {
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.back()}
|
||||
className="flex-1 h-12 rounded-full border-gray-300 text-black hover:bg-black/5"
|
||||
onClick={() => router.push('/login')}
|
||||
className="flex-1 h-12 rounded-full border-input text-foreground hover:bg-foreground/5"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!allValid}
|
||||
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
|
||||
</Button>
|
||||
@@ -131,7 +131,7 @@ export default function ForgotResetPage() {
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
</Button>
|
||||
</DialogContent>
|
||||
|
||||
@@ -17,29 +17,6 @@ import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
|
||||
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() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
@@ -77,19 +54,28 @@ function LoginForm() {
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden">
|
||||
{/* Full-page background photo — right side only */}
|
||||
<div className="absolute inset-y-0 left-[45%] right-0">
|
||||
{/* Full-page background photo — right side only. Dark mode swaps to the backdrop-free
|
||||
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
|
||||
src="/images/loginerps.png"
|
||||
alt="ERP Illustration"
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<Link href="/" className="inline-flex items-center">
|
||||
<Image src="/images/logo%20(2).png" alt="Hexa ERP" width={300} height={150} />
|
||||
@@ -153,7 +139,7 @@ function LoginForm() {
|
||||
</div>
|
||||
<FieldError errors={[form.formState.errors.password]} />
|
||||
<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?
|
||||
</Link>
|
||||
</div>
|
||||
@@ -168,7 +154,7 @@ function LoginForm() {
|
||||
|
||||
<Button
|
||||
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}
|
||||
>
|
||||
{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."}
|
||||
</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">
|
||||
<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>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -47,6 +47,9 @@ const navItems: {
|
||||
/** Where clicking the row actually navigates, if different from `href`. `href` itself
|
||||
* stays the section prefix used to decide whether this row is "active". */
|
||||
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
|
||||
chevron?: boolean
|
||||
children?: { title: string; code: string; href: string; icon: LucideIcon }[]
|
||||
@@ -106,6 +109,7 @@ const navItems: {
|
||||
title: "Settings",
|
||||
code: "settings",
|
||||
href: "/dashboard/settings",
|
||||
expandOnly: true,
|
||||
icon: Settings,
|
||||
chevron: true,
|
||||
children: [
|
||||
@@ -139,9 +143,13 @@ function SidebarContent({
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
|
||||
|
||||
useEffect(() => {
|
||||
const parent = items.find((i) =>
|
||||
i.children?.some((c) => pathname === c.href || pathname.startsWith(`${c.href}/`))
|
||||
)
|
||||
const parent = items.find((i) => {
|
||||
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) {
|
||||
setExpanded((prev) => (prev[parent.code] ? prev : { ...prev, [parent.code]: true }))
|
||||
}
|
||||
@@ -153,7 +161,7 @@ function SidebarContent({
|
||||
return (
|
||||
<nav
|
||||
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")
|
||||
)}
|
||||
>
|
||||
@@ -187,7 +195,7 @@ function SidebarContent({
|
||||
|
||||
{/* Nav items — scrolls internally when it overflows, without a visible
|
||||
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) => {
|
||||
const isActive =
|
||||
item.href === "/dashboard" ? pathname === item.href : pathname.startsWith(item.href)
|
||||
@@ -205,15 +213,18 @@ function SidebarContent({
|
||||
<Link
|
||||
href={item.landingHref ?? item.href}
|
||||
title={iconOnly ? item.title : undefined}
|
||||
onClick={onClose}
|
||||
onClick={() => {
|
||||
onClose?.()
|
||||
if (hasChildren) setExpanded((prev) => (prev[item.code] ? prev : { ...prev, [item.code]: true }))
|
||||
}}
|
||||
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",
|
||||
isActive ? "text-primary" : "text-foreground"
|
||||
)}
|
||||
>
|
||||
<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 && (
|
||||
<>
|
||||
@@ -270,14 +281,14 @@ function SidebarContent({
|
||||
onClick={onClose}
|
||||
tabIndex={isOpen ? undefined : -1}
|
||||
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
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
</Link>
|
||||
@@ -362,9 +373,9 @@ export function AppSidebar() {
|
||||
type="button"
|
||||
onClick={() => setMobileOpen(true)}
|
||||
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>
|
||||
|
||||
{/* ── Mobile overlay backdrop ──────────────────────── */}
|
||||
|
||||
@@ -164,8 +164,8 @@ export function Header() {
|
||||
}
|
||||
|
||||
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">
|
||||
<div className="flex items-center gap-3">
|
||||
<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 min-w-0 flex-1 items-center gap-2 sm:gap-3">
|
||||
{showBackButton && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -176,10 +176,10 @@ export function Header() {
|
||||
<ArrowLeft className="size-5" />
|
||||
</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 className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="flex shrink-0 items-center gap-1.5 sm:gap-3">
|
||||
<ThemeToggle />
|
||||
|
||||
<Popover>
|
||||
|
||||
@@ -1,38 +1,62 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { Moon, Sun, Monitor } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
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
|
||||
* the client's first paint, before next-themes reads localStorage), so rendering an icon
|
||||
* before that would either be wrong or cause a hydration mismatch.
|
||||
* Renders an empty slot until mounted: `theme` is unknown on the server (and on the
|
||||
* client's first paint, before next-themes reads localStorage), so rendering the active
|
||||
* segment before that would either be wrong or cause a hydration mismatch.
|
||||
*/
|
||||
export function ThemeToggle({ className }: { className?: string }) {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const { theme, setTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
||||
className={cn(
|
||||
"flex size-10 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Theme"
|
||||
className={cn("flex shrink-0 items-center gap-0.5 rounded-full bg-muted p-1", className)}
|
||||
>
|
||||
{isDark ? <Sun className="size-5" /> : <Moon className="size-5" />}
|
||||
</button>
|
||||
{options.map(({ value, label, icon: Icon }) => {
|
||||
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>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-indigo-50 hover:bg-indigo-50">
|
||||
<TableRow className="border-b-0 hover:bg-primary/5">
|
||||
{columns.map((column, index) => (
|
||||
<TableHead
|
||||
key={column.key}
|
||||
className={cn(
|
||||
"text-indigo-700",
|
||||
index === 0 && "rounded-l-lg pl-3",
|
||||
index === columns.length - 1 && !actions && "rounded-r-lg pr-3",
|
||||
column.headerClassName
|
||||
@@ -158,7 +157,7 @@ export function DataTable<T>({
|
||||
</TableHead>
|
||||
))}
|
||||
{actions && (
|
||||
<TableHead className="rounded-r-lg pr-3 text-right text-indigo-700">
|
||||
<TableHead className="rounded-r-lg pr-3 text-right">
|
||||
Actions
|
||||
</TableHead>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
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}
|
||||
/>
|
||||
)
|
||||
@@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
<th
|
||||
data-slot="table-head"
|
||||
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
|
||||
)}
|
||||
{...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