From 049c98298a423efa69b448ff6299fe4e3094811b Mon Sep 17 00:00:00 2001 From: Sasanka20 Date: Thu, 24 Sep 2026 12:30:21 +0530 Subject: [PATCH] Ui fixings --- .../erp-system/app/dashboard/help/page.tsx | 56 +--- .../app/dashboard/hrm/employees/page.tsx | 70 ++--- .../app/dashboard/hrm/leave/page.tsx | 57 +--- .../app/dashboard/hrm/payroll/[id]/page.tsx | 56 +--- .../app/dashboard/hrm/payroll/page.tsx | 58 +--- .../hrm/settings/departments/page.tsx | 57 +--- .../hrm/settings/document-types/page.tsx | 57 +--- .../hrm/settings/leave-types/page.tsx | 57 +--- .../hrm/settings/salary-components/page.tsx | 57 +--- .../hrm/settings/work-shifts/page.tsx | 57 +--- .../dashboard/production/produced/page.tsx | 256 ++++++++++++------ .../production/runs/[id]/RunActions.tsx | 2 +- .../dashboard/production/runs/[id]/page.tsx | 146 +++++++--- .../app/dashboard/production/runs/page.tsx | 118 ++++---- .../templates/[id]/StageEditorPanel.tsx | 10 +- .../production/templates/[id]/StageNode.tsx | 7 +- .../production/templates/[id]/page.tsx | 36 ++- .../dashboard/production/templates/page.tsx | 88 +++++- .../app/dashboard/products/page.tsx | 61 ++++- .../app/dashboard/products/tax-rates/page.tsx | 37 +-- .../app/dashboard/settings/roles/page.tsx | 40 +-- .../app/dashboard/settings/users/page.tsx | 73 ++--- .../app/dashboard/warehouse/[id]/page.tsx | 50 +--- .../app/dashboard/warehouse/page.tsx | 58 +--- .../erp-system/app/login/forgot/otp/page.tsx | 90 +++--- Frontend/erp-system/app/login/forgot/page.tsx | 94 +++---- .../app/login/forgot/reset/page.tsx | 223 +++++++-------- Frontend/erp-system/app/login/page.tsx | 41 +-- .../erp-system/components/Layouts/Header.tsx | 62 ++++- .../erp-system/components/auth/AuthShell.tsx | 40 +++ .../components/hrm/CodeNameMasterPage.tsx | 58 +--- .../components/production/RunStageNode.tsx | 36 ++- .../components/settings/BranchMasterPage.tsx | 58 +--- .../erp-system/components/ui/otp-input.tsx | 2 +- 34 files changed, 1021 insertions(+), 1247 deletions(-) create mode 100644 Frontend/erp-system/components/auth/AuthShell.tsx diff --git a/Frontend/erp-system/app/dashboard/help/page.tsx b/Frontend/erp-system/app/dashboard/help/page.tsx index 6c69834..7145e5e 100644 --- a/Frontend/erp-system/app/dashboard/help/page.tsx +++ b/Frontend/erp-system/app/dashboard/help/page.tsx @@ -4,8 +4,6 @@ import { useEffect, useMemo, useState } from "react" import Link from "next/link" import { CheckCircle2, - ChevronLeft, - ChevronRight, Clock, HelpCircle, Inbox, @@ -33,6 +31,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog" +import { PaginationFooter } from "@/components/ui/pagination-footer" import { Skeleton } from "@/components/ui/skeleton" import { StatCard } from "@/components/ui/stat-card" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" @@ -382,52 +381,13 @@ export default function HelpPage() { )} {totalItems > 0 && ( -
-
-

- Showing {(page - 1) * pageSize + 1}– - {Math.min(page * pageSize, totalItems)}{" "} - of {totalItems} tickets -

- -
-
- - - Page {page} of {totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/app/dashboard/hrm/employees/page.tsx b/Frontend/erp-system/app/dashboard/hrm/employees/page.tsx index 4c1ea22..f6df5b2 100644 --- a/Frontend/erp-system/app/dashboard/hrm/employees/page.tsx +++ b/Frontend/erp-system/app/dashboard/hrm/employees/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react" import Link from "next/link" import { useRouter } from "next/navigation" -import { ChevronLeft, ChevronRight, Link2, Pencil, Plus, Search, Trash2, UserCheck, UserMinus, UserX, Users as UsersIcon } from "lucide-react" +import { Eye, Link2, Pencil, Plus, Search, Trash2, UserCheck, UserMinus, UserX, Users as UsersIcon } from "lucide-react" import { employeesApi } from "@/lib/api/employees" import { departmentsApi, designationsApi } from "@/lib/api/hrm-masters" @@ -337,6 +337,14 @@ export default function EmployeesPage() {
+ + + Hire Date Login Status - Actions + Actions @@ -452,6 +460,9 @@ export default function EmployeesPage() {
+ + + @@ -490,54 +501,13 @@ export default function EmployeesPage() { )} {pagination && pagination.totalItems > 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} employees -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)} diff --git a/Frontend/erp-system/app/dashboard/hrm/leave/page.tsx b/Frontend/erp-system/app/dashboard/hrm/leave/page.tsx index 4976ab3..261265c 100644 --- a/Frontend/erp-system/app/dashboard/hrm/leave/page.tsx +++ b/Frontend/erp-system/app/dashboard/hrm/leave/page.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { CheckCircle2, ChevronLeft, ChevronRight, Clock, Plus, Search, X, XCircle } from "lucide-react" +import { CheckCircle2, Clock, Plus, Search, X, XCircle } from "lucide-react" import { employeesApi } from "@/lib/api/employees" import { leaveTypesApi } from "@/lib/api/hrm-masters" @@ -456,54 +456,13 @@ export default function LeaveRequestsPage() { )} {pagination && pagination.totalItems > 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} leave requests -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/app/dashboard/hrm/payroll/[id]/page.tsx b/Frontend/erp-system/app/dashboard/hrm/payroll/[id]/page.tsx index 1700b74..291e9da 100644 --- a/Frontend/erp-system/app/dashboard/hrm/payroll/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/hrm/payroll/[id]/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react" import { useParams, useRouter } from "next/navigation" -import { BadgeDollarSign, ChevronLeft, ChevronRight, FileText, Receipt, Search, Users, Wallet, X } from "lucide-react" +import { BadgeDollarSign, FileText, Receipt, Search, Users, Wallet, X } from "lucide-react" import { payrollRunsApi, payslipViewUrl } from "@/lib/api/payroll" import { errorMessage } from "@/lib/error-map" @@ -14,6 +14,7 @@ import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { Field, FieldGroup, FieldLabel } from "@/components/ui/field" import { Input } from "@/components/ui/input" +import { PaginationFooter } from "@/components/ui/pagination-footer" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "@/components/ui/toast" @@ -328,52 +329,13 @@ export default function PayrollRunDetailPage() { )} {filteredLines.length > 0 && ( -
-
-

- Showing {(currentPage - 1) * pageSize + 1}– - {Math.min(currentPage * pageSize, filteredLines.length)}{" "} - of {filteredLines.length} lines -

- -
-
- - - Page {currentPage} of {totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/app/dashboard/hrm/payroll/page.tsx b/Frontend/erp-system/app/dashboard/hrm/payroll/page.tsx index 94a73ed..0e9d450 100644 --- a/Frontend/erp-system/app/dashboard/hrm/payroll/page.tsx +++ b/Frontend/erp-system/app/dashboard/hrm/payroll/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react" import { useRouter } from "next/navigation" -import { ChevronLeft, ChevronRight, Plus } from "lucide-react" +import { Plus } from "lucide-react" import { payrollRunsApi } from "@/lib/api/payroll" import { errorMessage } from "@/lib/error-map" @@ -16,6 +16,7 @@ import { Badge } from "@/components/ui/badge" import { Field, FieldGroup, FieldLabel } from "@/components/ui/field" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { PaginationFooter } from "@/components/ui/pagination-footer" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "@/components/ui/toast" @@ -238,54 +239,13 @@ export default function PayrollRunsPage() { )} {pagination && pagination.totalItems > 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} payroll runs -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/app/dashboard/hrm/settings/departments/page.tsx b/Frontend/erp-system/app/dashboard/hrm/settings/departments/page.tsx index 42503a0..f5f6a13 100644 --- a/Frontend/erp-system/app/dashboard/hrm/settings/departments/page.tsx +++ b/Frontend/erp-system/app/dashboard/hrm/settings/departments/page.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { CheckCircle2, ChevronLeft, ChevronRight, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" +import { CheckCircle2, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" import { departmentsApi, branchesHrmApi } from "@/lib/api/hrm-masters" import { errorMessage } from "@/lib/error-map" @@ -562,54 +562,13 @@ export default function DepartmentsPage() { )} {pagination && pagination.totalItems > 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} departments -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/app/dashboard/hrm/settings/document-types/page.tsx b/Frontend/erp-system/app/dashboard/hrm/settings/document-types/page.tsx index 2380de8..01ecf2a 100644 --- a/Frontend/erp-system/app/dashboard/hrm/settings/document-types/page.tsx +++ b/Frontend/erp-system/app/dashboard/hrm/settings/document-types/page.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { CheckCircle2, ChevronLeft, ChevronRight, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" +import { CheckCircle2, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" import { hrDocumentTypesApi } from "@/lib/api/hrm-masters" import { errorMessage } from "@/lib/error-map" @@ -534,54 +534,13 @@ export default function DocumentTypesPage() { )} {pagination && pagination.totalItems > 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} document types -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/app/dashboard/hrm/settings/leave-types/page.tsx b/Frontend/erp-system/app/dashboard/hrm/settings/leave-types/page.tsx index 50d1240..a14f4a9 100644 --- a/Frontend/erp-system/app/dashboard/hrm/settings/leave-types/page.tsx +++ b/Frontend/erp-system/app/dashboard/hrm/settings/leave-types/page.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { CheckCircle2, ChevronLeft, ChevronRight, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" +import { CheckCircle2, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" import { leaveTypesApi } from "@/lib/api/hrm-masters" import { errorMessage } from "@/lib/error-map" @@ -559,54 +559,13 @@ export default function LeaveTypesPage() { )} {pagination && pagination.totalItems > 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} leave types -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/app/dashboard/hrm/settings/salary-components/page.tsx b/Frontend/erp-system/app/dashboard/hrm/settings/salary-components/page.tsx index 34a47cf..5ad144b 100644 --- a/Frontend/erp-system/app/dashboard/hrm/settings/salary-components/page.tsx +++ b/Frontend/erp-system/app/dashboard/hrm/settings/salary-components/page.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { CheckCircle2, ChevronLeft, ChevronRight, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" +import { CheckCircle2, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" import { salaryComponentsApi } from "@/lib/api/hrm-masters" import { errorMessage } from "@/lib/error-map" @@ -527,54 +527,13 @@ export default function SalaryComponentsPage() { )} {pagination && pagination.totalItems > 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} salary components -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/app/dashboard/hrm/settings/work-shifts/page.tsx b/Frontend/erp-system/app/dashboard/hrm/settings/work-shifts/page.tsx index 4005411..c3c944c 100644 --- a/Frontend/erp-system/app/dashboard/hrm/settings/work-shifts/page.tsx +++ b/Frontend/erp-system/app/dashboard/hrm/settings/work-shifts/page.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { CheckCircle2, ChevronLeft, ChevronRight, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" +import { CheckCircle2, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" import { workShiftsApi } from "@/lib/api/hrm-masters" import { errorMessage } from "@/lib/error-map" @@ -629,54 +629,13 @@ export default function WorkShiftsPage() { )} {pagination && pagination.totalItems > 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} work shifts -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/app/dashboard/production/produced/page.tsx b/Frontend/erp-system/app/dashboard/production/produced/page.tsx index ddf4d15..7123347 100644 --- a/Frontend/erp-system/app/dashboard/production/produced/page.tsx +++ b/Frontend/erp-system/app/dashboard/production/produced/page.tsx @@ -9,6 +9,7 @@ import { productionRunsApi } from "@/lib/api/production-runs" import { productionTemplatesApi } from "@/lib/api/production-templates" import { warehousesApi } from "@/lib/api/warehouses" import { errorMessage } from "@/lib/error-map" +import { PaginationMeta } from "@/types/common" import { ProducedListResponse, ProductionRunStatus, ProductionTemplateSummary } from "@/types/production" import { Warehouse } from "@/types/master-data" @@ -16,6 +17,7 @@ import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" +import { PaginationFooter } from "@/components/ui/pagination-footer" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table" @@ -34,6 +36,10 @@ function formatCost(value: number) { return value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 }) } +// The produced list comes back whole (no server paging), so pages are sliced client-side from +// the already-loaded lines. The summary totals still cover the whole filtered list. +const PAGE_SIZE_OPTIONS = [5, 10, 15, 20] + export default function ProducedItemsPage() { const router = useRouter() @@ -49,6 +55,8 @@ export default function ProducedItemsPage() { const [templateId, setTemplateId] = useState(null) const [warehouseId, setWarehouseId] = useState(null) const [status, setStatus] = useState("All") + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(PAGE_SIZE_OPTIONS[1]) const load = useCallback(() => { setLoadError(null) @@ -60,7 +68,11 @@ export default function ProducedItemsPage() { warehouseId: warehouseId ?? undefined, status: status === "All" ? undefined : status, }) - .then(setData) + .then((res) => { + setData(res) + // A new filter is a new list — start back at the first page. + setPage(1) + }) .catch((err) => { setData({ items: [], summary: { runCount: 0, totalProducedQty: 0, totalScrappedQty: 0, totalGoodQty: 0, totalCost: 0 } }) setLoadError(errorMessage(err)) @@ -80,26 +92,37 @@ export default function ProducedItemsPage() { const hasFilters = from !== "" || to !== "" || templateId !== null || warehouseId !== null || status !== "All" + const totalItems = data?.items.length ?? 0 + const pagination: PaginationMeta = { + page, + pageSize, + totalItems, + totalPages: Math.max(1, Math.ceil(totalItems / pageSize)), + } + const pageItems = data?.items.slice((page - 1) * pageSize, page * pageSize) ?? [] + return (
-

Produced Items

-

+

Produced Items

+

Finished-goods lines produced by runs, with the roll-up cost of production (FR-MFG-13).

-
-
- - setFrom(e.target.value)} className="h-11 text-base" /> + {/* Two-up on phones (date range side by side, then template/warehouse), status full width — + five stacked labelled controls would push the results well below the fold. */} +
+
+ + setFrom(e.target.value)} className="h-11 min-w-0 text-base" />
-
- - setTo(e.target.value)} className="h-11 text-base" /> +
+ + setTo(e.target.value)} className="h-11 min-w-0 text-base" />
-
- +
+ value={templateId} onValueChange={setTemplateId}> @@ -111,8 +134,8 @@ export default function ProducedItemsPage() {
-
- +
+ value={warehouseId} onValueChange={setWarehouseId}> @@ -124,8 +147,8 @@ export default function ProducedItemsPage() {
-
- +
+ value={status} onValueChange={(v) => setStatus(v ?? "All")}> @@ -175,67 +198,148 @@ export default function ProducedItemsPage() {

) : ( -
- - - - Doc No - Status - Template - Warehouse - Item - Produced - Scrapped - Good Qty - Unit Cost - Cost of Production - Completed - - - - {data.items.map((line) => ( - router.push(`/dashboard/production/runs/${line.runId}`)} - > - {line.docNo} - - - {line.status === "InProgress" ? "In Progress" : line.status} - - - {line.templateName} - {line.warehouseName} - {line.itemName} - {line.producedQty.toLocaleString()} - {line.scrappedQty.toLocaleString()} - {line.goodQty.toLocaleString()} - {formatCost(line.unitCost)} - {formatCost(line.totalCost)} - - {line.completedAt ? new Date(line.completedAt).toLocaleDateString() : "—"} - + <> + {/* Mobile: totals first (the figure people come here for), then one card per line. */} +
+
+

+ Total · {data.summary.runCount} run{data.summary.runCount === 1 ? "" : "s"} +

+

Cost of production

+

{formatCost(data.summary.totalCost)}

+
+
+

Produced

+

{data.summary.totalProducedQty.toLocaleString()}

+
+
+

Scrapped

+

{data.summary.totalScrappedQty.toLocaleString()}

+
+
+

Good

+

{data.summary.totalGoodQty.toLocaleString()}

+
+
+
+ + {pageItems.map((line) => ( + + ))} +
+ +
+
+ + + Doc No + Status + Template + Warehouse + Item + Produced + Scrapped + Good Qty + Unit Cost + Cost of Production + Completed - ))} - - - - - Total ({data.summary.runCount} run{data.summary.runCount === 1 ? "" : "s"}) - - {data.summary.totalProducedQty.toLocaleString()} - {data.summary.totalScrappedQty.toLocaleString()} - {data.summary.totalGoodQty.toLocaleString()} - - - Cost of Production: {formatCost(data.summary.totalCost)} - - - - -
-
+ + + {pageItems.map((line) => ( + router.push(`/dashboard/production/runs/${line.runId}`)} + > + {line.docNo} + + + {line.status === "InProgress" ? "In Progress" : line.status} + + + {line.templateName} + {line.warehouseName} + {line.itemName} + {line.producedQty.toLocaleString()} + {line.scrappedQty.toLocaleString()} + {line.goodQty.toLocaleString()} + {formatCost(line.unitCost)} + {formatCost(line.totalCost)} + + {line.completedAt ? new Date(line.completedAt).toLocaleDateString() : "—"} + + + ))} + + + + + Total ({data.summary.runCount} run{data.summary.runCount === 1 ? "" : "s"}) + + {data.summary.totalProducedQty.toLocaleString()} + {data.summary.totalScrappedQty.toLocaleString()} + {data.summary.totalGoodQty.toLocaleString()} + + + Cost of Production: {formatCost(data.summary.totalCost)} + + + + + +
+ + { setPage(1); setPageSize(size) }} + /> + )}
) diff --git a/Frontend/erp-system/app/dashboard/production/runs/[id]/RunActions.tsx b/Frontend/erp-system/app/dashboard/production/runs/[id]/RunActions.tsx index e726a9f..92f2e11 100644 --- a/Frontend/erp-system/app/dashboard/production/runs/[id]/RunActions.tsx +++ b/Frontend/erp-system/app/dashboard/production/runs/[id]/RunActions.tsx @@ -140,7 +140,7 @@ export function RunActions({ return ( <> -
+
-

Click a stage to open it.

+

+ Tap a stage to open it. + Click a stage to open it. +

-
+ {/* Phones get the stages as a list in flow order — the operator on the floor needs to + reach "their" stage in one tap, not pan and pinch around a canvas. */} +
    + {orderedStages.map((stage, i) => { + const data = stageData.get(stage.runStageId)! + return ( +
  1. + + {i + 1} + + +
  2. + ) + })} +
+ +
{mounted ? ( (null) const [warehouseId, setWarehouseId] = useState(null) const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(PAGE_SIZE) + const [showMoreLoading, setShowMoreLoading] = useState(false) const [templates, setTemplates] = useState([]) const [warehouses, setWarehouses] = useState([]) @@ -85,7 +90,7 @@ export default function ProductionRunsPage() { productionRunsApi .list({ page, - pageSize: PAGE_SIZE, + pageSize, q: query || undefined, status: status === "All" ? undefined : status, templateId: templateId ?? undefined, @@ -99,10 +104,16 @@ export default function ProductionRunsPage() { setRuns([]) setLoadError(errorMessage(err)) }) - }, [page, query, status, templateId, warehouseId]) + .finally(() => setShowMoreLoading(false)) + }, [page, pageSize, query, status, templateId, warehouseId]) useEffect(load, [load]) + function handleShowMore() { + setShowMoreLoading(true) + setPageSize((s) => s + MOBILE_PAGE_SIZE_STEP) + } + // Filter and picker fills. Templates are fetched unfiltered so the *filter* can name an // Inactive template that still has historical runs; the start dialog narrows to Active // itself, because FR-MFG-01 only blocks starting new runs (docs/21 §4). @@ -172,11 +183,11 @@ export default function ProductionRunsPage() {
-

Production Runs

-

All manufacturing runs with per-stage progress at a glance.

+

Production Runs

+

All manufacturing runs with per-stage progress at a glance.

- Start Run} /> + Start Run} /> Start a run @@ -266,54 +277,59 @@ export default function ProductionRunsPage() {
-
-
+ {/* Search full width, then the three filters as a compact grid on phones (four stacked + 56px controls pushed the runs below the fold); one row from lg, where `lg:contents` + lifts the selects into the outer flex row. */} +
+
setSearchInput(e.target.value)} placeholder="Search doc no…" - className="h-14 w-full pl-11 text-base" + className="h-12 w-full pl-11 text-base sm:h-14" aria-label="Search runs" />
- - value={templateId} - onValueChange={(v) => { setTemplateId(v); setPage(1) }} - > - - - - - {templates.map((t) => ( - {t.name} - ))} - - - - value={warehouseId} - onValueChange={(v) => { setWarehouseId(v); setPage(1) }} - > - - - - - {warehouses.map((w) => ( - {w.name} - ))} - - - value={status} onValueChange={(v) => { setStatus(v ?? "All"); setPage(1) }}> - - - - - All statuses - In Progress - Completed - Cancelled - - +
+ + value={templateId} + onValueChange={(v) => { setTemplateId(v); setPage(1) }} + > + + + + + {templates.map((t) => ( + {t.name} + ))} + + + + value={warehouseId} + onValueChange={(v) => { setWarehouseId(v); setPage(1) }} + > + + + + + {warehouses.map((w) => ( + {w.name} + ))} + + + value={status} onValueChange={(v) => { setStatus(v ?? "All"); setPage(1) }}> + + + + + All statuses + In Progress + Completed + Cancelled + + +
{hasFilters && ( @@ -426,7 +442,17 @@ export default function ProductionRunsPage() { })}
- {pagination && } + {pagination && runs.length < pagination.totalItems && ( + + )} + + {pagination && ( +
+ +
+ )} )}
diff --git a/Frontend/erp-system/app/dashboard/production/templates/[id]/StageEditorPanel.tsx b/Frontend/erp-system/app/dashboard/production/templates/[id]/StageEditorPanel.tsx index 06947e0..de4d4e8 100644 --- a/Frontend/erp-system/app/dashboard/production/templates/[id]/StageEditorPanel.tsx +++ b/Frontend/erp-system/app/dashboard/production/templates/[id]/StageEditorPanel.tsx @@ -233,7 +233,9 @@ export function StageEditorPanel({ return ( !next && onClose()}> - + {/* No bare `max-w-lg`: it would override the base `max-w-[calc(100%-2rem)]` and run the + dialog edge to edge on a phone. dvh so mobile browser chrome doesn't hide the bottom. */} + Stage editor @@ -301,7 +303,7 @@ export function StageEditorPanel({ {!readOnly && ( - )} @@ -404,7 +406,7 @@ export function StageEditorPanel({ /> )} {!readOnly && ( - )} @@ -462,7 +464,7 @@ export function StageEditorPanel({ className="h-8 flex-1 text-sm" /> {!readOnly && ( - )} diff --git a/Frontend/erp-system/app/dashboard/production/templates/[id]/StageNode.tsx b/Frontend/erp-system/app/dashboard/production/templates/[id]/StageNode.tsx index 4c24214..5ac15f3 100644 --- a/Frontend/erp-system/app/dashboard/production/templates/[id]/StageNode.tsx +++ b/Frontend/erp-system/app/dashboard/production/templates/[id]/StageNode.tsx @@ -24,14 +24,15 @@ function StageNode({ data, selected }: NodeProps & { data: StageNodeData }) { disconnected && "opacity-40" )} > - + {/* Handles are finger-sized below md: dragging a connection starts from them. */} + {selected && data.onDelete && (
) } diff --git a/Frontend/erp-system/app/dashboard/production/templates/[id]/page.tsx b/Frontend/erp-system/app/dashboard/production/templates/[id]/page.tsx index f7c27c3..eeffae2 100644 --- a/Frontend/erp-system/app/dashboard/production/templates/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/production/templates/[id]/page.tsx @@ -643,7 +643,7 @@ function TemplateBuilderContent() { if (!loaded) { return (
- +
) @@ -661,12 +661,15 @@ function TemplateBuilderContent() { } return ( -
-
-
-
-
-

{name || "Untitled template"}

+ // Phones stack naturally and scroll the page; the canvas gets its own fixed height below. + // Only from lg up does the builder fill the viewport with the canvas taking the remainder — + // on a short screen the fields and issue list would otherwise squeeze it to nothing. +
+
+
+
+
+

{name || "Untitled template"}

{graph ? ( )}
-

+

{stageNodes.length} stage{stageNodes.length === 1 ? "" : "s"} · {edges.length} connection {edges.length === 1 ? "" : "s"}

-
+ {/* Two-up grid on phones so every action is a full-width tap target; a single row from sm. */} +
{!locked && ( <> - @@ -710,6 +714,7 @@ function TemplateBuilderContent() { )} {!locked && ( - @@ -725,7 +730,7 @@ function TemplateBuilderContent() {
-
+
Code setCode(e.target.value)} placeholder="PT-CHAIR" className="h-10" /> @@ -734,7 +739,7 @@ function TemplateBuilderContent() { Name setName(e.target.value)} placeholder="Aluminium Frame Assembly" className="h-10" /> - + Description )} -
+
{mounted && ( - + {/* Covers too much of a phone-sized canvas to be worth it. */} + )}
diff --git a/Frontend/erp-system/app/dashboard/production/templates/page.tsx b/Frontend/erp-system/app/dashboard/production/templates/page.tsx index 1084ed4..0db0d02 100644 --- a/Frontend/erp-system/app/dashboard/production/templates/page.tsx +++ b/Frontend/erp-system/app/dashboard/production/templates/page.tsx @@ -1,6 +1,7 @@ "use client" import { useCallback, useEffect, useMemo, useState } from "react" +import Link from "next/link" import { useRouter } from "next/navigation" import { ReactFlow, Background, Controls, type Edge, type Node, type NodeMouseHandler } from "@xyflow/react" import "@xyflow/react/dist/style.css" @@ -9,6 +10,7 @@ import { LayoutTemplate, Plus, Search } from "lucide-react" import { productionTemplatesApi } from "@/lib/api/production-templates" import { errorMessage } from "@/lib/error-map" +import { cn } from "@/lib/utils" import { ProductionTemplateSummary, TemplateStatus } from "@/types/production" import { PaginationMeta } from "@/types/common" import { @@ -29,6 +31,9 @@ import { Skeleton } from "@/components/ui/skeleton" type StatusFilter = TemplateStatus | "All" const PAGE_SIZE = 25 +// Mobile has no pagination — tapping "Show more" just grows the page size and re-fetches +// from page 1, so previously loaded cards stay put and more append below. +const MOBILE_PAGE_SIZE_STEP = 10 const nodeTypes = { lineHeader: LineHeaderNodeComponent, lineStage: LineStageNodeComponent } @@ -92,6 +97,8 @@ export default function ProductionTemplatesPage() { const [query, setQuery] = useState("") const [status, setStatus] = useState("All") const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(PAGE_SIZE) + const [showMoreLoading, setShowMoreLoading] = useState(false) const [open, setOpen] = useState(false) const [code, setCode] = useState("") @@ -119,7 +126,7 @@ export default function ProductionTemplatesPage() { productionTemplatesApi .list({ page, - pageSize: PAGE_SIZE, + pageSize, q: query || undefined, status: status === "All" ? undefined : status, }) @@ -131,10 +138,16 @@ export default function ProductionTemplatesPage() { setTemplates([]) setLoadError(errorMessage(err)) }) - }, [page, query, status]) + .finally(() => setShowMoreLoading(false)) + }, [page, pageSize, query, status]) useEffect(load, [load]) + function handleShowMore() { + setShowMoreLoading(true) + setPageSize((s) => s + MOBILE_PAGE_SIZE_STEP) + } + const hasFilters = query.length > 0 || status !== "All" const { nodes, edges } = useMemo(() => buildLinesGraph(templates ?? []), [templates]) @@ -174,11 +187,11 @@ export default function ProductionTemplatesPage() {
-

Production Templates

-

Every production line, stage by stage. Click a line to open its builder.

+

Production Templates

+

Every production line, stage by stage. Tap a line to open its builder.

- New Template} /> + New Template} /> New template @@ -253,7 +266,7 @@ export default function ProductionTemplatesPage() { )} {templates === null ? ( - + ) : templates.length === 0 ? (
@@ -263,7 +276,62 @@ export default function ProductionTemplatesPage() {
) : ( <> -
+ {/* Phones get a list: a pan/zoom canvas of wide horizontal lines is unusable on a + narrow touch screen, so each line reads top to bottom instead. */} +
+ {templates.map((t) => ( + +
+
+

{t.code}

+

{t.name}

+
+
+ + {t.status} + + {t.activeRunCount > 0 && ( + + {t.activeRunCount} in progress + + )} +
+
+ + {t.stageNames.length > 0 ? ( +
    + {t.stageNames.map((stageName, i) => ( +
  1. + + {i + 1} + + {stageName} +
  2. + ))} +
+ ) : ( +

No stages yet.

+ )} + + ))} + + {pagination && templates.length < pagination.totalItems && ( + + )} +
+ +
{mounted ? ( - {pagination && } + {pagination && ( +
+ +
+ )} )}
diff --git a/Frontend/erp-system/app/dashboard/products/page.tsx b/Frontend/erp-system/app/dashboard/products/page.tsx index 04f5de5..8d74779 100644 --- a/Frontend/erp-system/app/dashboard/products/page.tsx +++ b/Frontend/erp-system/app/dashboard/products/page.tsx @@ -25,6 +25,7 @@ import { productConfig } from "@/lib/api/product-config" import { errorMessage } from "@/lib/error-map" import { downloadCsv } from "@/lib/csv-export" import { cn } from "@/lib/utils" +import { useIsMobile } from "@/hooks/use-mobile" import { EntityStatus, PaginationMeta } from "@/types/common" import { Brand, Category, ItemOverview, ProductConfig } from "@/types/master-data" @@ -71,6 +72,7 @@ function SortableHeader({ label, active, order, onClick }: { label: string; acti export default function ItemsPage() { const router = useRouter() + const isMobile = useIsMobile() const [items, setItems] = useState(null) const [categories, setCategories] = useState([]) const [brands, setBrands] = useState([]) @@ -84,6 +86,7 @@ export default function ItemsPage() { const [categoryId, setCategoryId] = useState("All") const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(PAGE_SIZE_OPTIONS[1]) + const [loadingMore, setLoadingMore] = useState(false) const [selected, setSelected] = useState>(new Set()) const [sortKey, setSortKey] = useState("sku") @@ -95,10 +98,19 @@ export default function ItemsPage() { return () => clearTimeout(timeout) }, [searchInput]) - useEffect(() => setPage(1), [query, status, categoryId]) + // A new filter — or switching between mobile ("Show more" appends) and desktop (numbered + // pages) — restarts at page 1. Reset during render rather than in an effect. + const resetKey = `${query}|${status}|${categoryId}|${isMobile}` + const [prevResetKey, setPrevResetKey] = useState(resetKey) + if (prevResetKey !== resetKey) { + setPrevResetKey(resetKey) + setPage(1) + } - function load() { - setError(null) + useEffect(() => { + let cancelled = false + // Mobile "Show more" appends the next server page to what's already listed. + const append = isMobile && page > 1 itemsApi .overview({ page, @@ -108,14 +120,27 @@ export default function ItemsPage() { categoryId: categoryId === "All" ? undefined : categoryId, }) .then((res) => { - setItems(res.items) + if (cancelled) return + setError(null) + setItems((prev) => (append && prev ? [...prev, ...res.items] : res.items)) setPagination(res.pagination) - setSelected(new Set()) + if (!append) setSelected(new Set()) }) - .catch((err) => setError(errorMessage(err))) - } + .catch((err) => { + if (!cancelled) setError(errorMessage(err)) + }) + .finally(() => { + if (!cancelled) setLoadingMore(false) + }) + return () => { + cancelled = true + } + }, [page, pageSize, query, status, categoryId, isMobile]) - useEffect(load, [page, pageSize, query, status, categoryId]) + function showMore() { + setLoadingMore(true) + setPage((p) => p + 1) + } useEffect(() => { categoriesApi.list().then((res) => setCategories(res.items)).catch(() => {}) brandsApi.list({ pageSize: 200 }).then((res) => setBrands(res.items)).catch(() => {}) @@ -398,6 +423,12 @@ export default function ItemsPage() {
))} + + {pagination && pagination.page < pagination.totalPages && ( + + )}
@@ -516,12 +547,14 @@ export default function ItemsPage() {
{pagination && ( - { setPage(1); setPageSize(size) }} - /> +
+ { setPage(1); setPageSize(size) }} + /> +
)} )} diff --git a/Frontend/erp-system/app/dashboard/products/tax-rates/page.tsx b/Frontend/erp-system/app/dashboard/products/tax-rates/page.tsx index 9149ca2..86abbf9 100644 --- a/Frontend/erp-system/app/dashboard/products/tax-rates/page.tsx +++ b/Frontend/erp-system/app/dashboard/products/tax-rates/page.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { ArrowUp, ArrowDown, ArrowUpDown, Ban, CheckCircle2, ChevronLeft, ChevronRight, Pencil, Percent, Plus, Search } from "lucide-react" +import { ArrowUp, ArrowDown, ArrowUpDown, Ban, CheckCircle2, Pencil, Percent, Plus, Search } from "lucide-react" import { taxRatesApi } from "@/lib/api/tax-rates" import { errorMessage } from "@/lib/error-map" @@ -17,6 +17,7 @@ import { Input } from "@/components/ui/input" import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { PaginationFooter } from "@/components/ui/pagination-footer" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "@/components/ui/toast" @@ -345,39 +346,7 @@ export default function TaxRatesPage() { - {pagination && pagination.totalPages > 1 && ( -
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} of {pagination.totalItems} -

-
- - - Page {pagination.page} of {pagination.totalPages} - - -
-
- )} + {pagination && } )}
diff --git a/Frontend/erp-system/app/dashboard/settings/roles/page.tsx b/Frontend/erp-system/app/dashboard/settings/roles/page.tsx index 42d2875..058f621 100644 --- a/Frontend/erp-system/app/dashboard/settings/roles/page.tsx +++ b/Frontend/erp-system/app/dashboard/settings/roles/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react" import Link from "next/link" import { useRouter } from "next/navigation" -import { ChevronLeft, ChevronRight, Pencil, Plus, Search, ShieldCheck, Trash2, X } from "lucide-react" +import { Pencil, Plus, Search, ShieldCheck, Trash2, X } from "lucide-react" import { rolesApi } from "@/lib/api/roles" import { errorMessage } from "@/lib/error-map" @@ -382,39 +382,11 @@ export default function RolesPage() { )} {pagination && pagination.totalPages > 1 && ( -
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} roles -

-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+
)}
diff --git a/Frontend/erp-system/app/dashboard/settings/users/page.tsx b/Frontend/erp-system/app/dashboard/settings/users/page.tsx index fc057ec..e5221fb 100644 --- a/Frontend/erp-system/app/dashboard/settings/users/page.tsx +++ b/Frontend/erp-system/app/dashboard/settings/users/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react" import Link from "next/link" import { useRouter } from "next/navigation" -import { ChevronLeft, ChevronRight, Pencil, Search, ShieldCheck, UserCheck, UserX, Users as UsersIcon, X } from "lucide-react" +import { Eye, Pencil, Search, ShieldCheck, UserCheck, UserX, Users as UsersIcon, X } from "lucide-react" import { usersApi } from "@/lib/api/users" import { errorMessage } from "@/lib/error-map" @@ -234,6 +234,14 @@ export default function UsersPage() {
{u.roleName ?? "—"}
+ + +
+ + + 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} users -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/app/dashboard/warehouse/[id]/page.tsx b/Frontend/erp-system/app/dashboard/warehouse/[id]/page.tsx index a77faeb..09919bd 100644 --- a/Frontend/erp-system/app/dashboard/warehouse/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/warehouse/[id]/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react" import { useParams } from "next/navigation" -import { ArrowLeftRight, Boxes, ChevronLeft, ChevronRight, MapPinned, Plus } from "lucide-react" +import { ArrowLeftRight, Boxes, MapPinned, Plus } from "lucide-react" import { warehousesApi } from "@/lib/api/warehouses" import { stockApi } from "@/lib/api/stock" @@ -23,6 +23,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog" +import { PaginationFooter } from "@/components/ui/pagination-footer" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "@/components/ui/toast" @@ -36,14 +37,13 @@ const PAGE_SIZE_OPTIONS = [5, 10, 15, 20] /** Reusable pager for a client-side-sliced table — data for these tables is fetched in * full up front, so paging is done in-memory rather than via query params. */ function TablePager({ - page, pageSize, total, onPageChange, onPageSizeChange, itemLabel, + page, pageSize, total, onPageChange, onPageSizeChange, }: { page: number pageSize: number total: number onPageChange: (page: number) => void onPageSizeChange: (pageSize: number) => void - itemLabel: string }) { const totalPages = Math.max(1, Math.ceil(total / pageSize)) if (total === 0) return null @@ -65,40 +65,13 @@ function TablePager({ )} {/* Desktop: full pager. */} -
-
-

- Showing {(page - 1) * pageSize + 1}– - {Math.min(page * pageSize, total)}{" "} - of {total} {itemLabel} -

- -
-
- - - Page {page} of {totalPages} - - -
+
+
) @@ -322,7 +295,6 @@ export default function WarehouseDetailPage() { total={bins.length} onPageChange={setBinsPage} onPageSizeChange={(size) => { setBinsPageSize(size); setBinsPage(1) }} - itemLabel="bins" />
)} @@ -444,7 +416,6 @@ export default function WarehouseDetailPage() { total={stockRows.length} onPageChange={setStockPage} onPageSizeChange={(size) => { setStockPageSize(size); setStockPage(1) }} - itemLabel="items" /> )} @@ -543,7 +514,6 @@ export default function WarehouseDetailPage() { total={movementByItem.length} onPageChange={setMovementPage} onPageSizeChange={(size) => { setMovementPageSize(size); setMovementPage(1) }} - itemLabel="items" /> )} diff --git a/Frontend/erp-system/app/dashboard/warehouse/page.tsx b/Frontend/erp-system/app/dashboard/warehouse/page.tsx index 0560e7f..a9eef3c 100644 --- a/Frontend/erp-system/app/dashboard/warehouse/page.tsx +++ b/Frontend/erp-system/app/dashboard/warehouse/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react" import Link from "next/link" import { useRouter } from "next/navigation" -import { ChevronLeft, ChevronRight, Eye, Pencil, Plus, Search, Trash2, Warehouse as WarehouseIcon } from "lucide-react" +import { Eye, Pencil, Plus, Search, Trash2, Warehouse as WarehouseIcon } from "lucide-react" import { warehousesApi } from "@/lib/api/warehouses" import { branchesHrmApi } from "@/lib/api/hrm-masters" @@ -29,6 +29,7 @@ import { DialogTrigger, } from "@/components/ui/dialog" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { PaginationFooter } from "@/components/ui/pagination-footer" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "@/components/ui/toast" @@ -509,54 +510,13 @@ export default function WarehousesPage() { )} {pagination && pagination.totalItems > 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} warehouses -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)} diff --git a/Frontend/erp-system/app/login/forgot/otp/page.tsx b/Frontend/erp-system/app/login/forgot/otp/page.tsx index 0d22975..1f7e683 100644 --- a/Frontend/erp-system/app/login/forgot/otp/page.tsx +++ b/Frontend/erp-system/app/login/forgot/otp/page.tsx @@ -1,12 +1,15 @@ "use client" import Link from "next/link" +import { useRouter } from "next/navigation" import { useState, useEffect } from "react" +import { AuthShell } from "@/components/auth/AuthShell" import { Button } from "@/components/ui/button" import OtpInput from "@/components/ui/otp-input" import { Mail } from "lucide-react" export default function ForgotOtpPage() { + const router = useRouter() const [otp, setOtp] = useState("") const [secondsLeft, setSecondsLeft] = useState(60) @@ -24,47 +27,54 @@ export default function ForgotOtpPage() { } return ( -
-
-
-
- -
-

Verify your email

-
-

We've sent a verification code to your email address. Enter it below to continue.

- -
-
- setOtp(v)} /> -
- -
- {canResend ? ( - - ) : ( -

Resend code available in {Math.max(0, secondsLeft)}s

- )} -
- -
- - - - - - -
+ +
+
+
+

Forgot password

+

Verify your email

+

+ We've sent a verification code to your email address. Enter it below to continue. +

-
+ +
{ + e.preventDefault() + router.push("/login/forgot/reset") + }} + > + setOtp(v)} /> + +
+ {canResend ? ( + + ) : ( +

+ Resend code available in {Math.max(0, secondsLeft)}s +

+ )} +
+ + + +
+ + Back to sign in + +
+ + ) } diff --git a/Frontend/erp-system/app/login/forgot/page.tsx b/Frontend/erp-system/app/login/forgot/page.tsx index e78493b..b285527 100644 --- a/Frontend/erp-system/app/login/forgot/page.tsx +++ b/Frontend/erp-system/app/login/forgot/page.tsx @@ -1,7 +1,9 @@ "use client" +import Link from "next/link" import { useRouter } from "next/navigation" import { useState } from "react" +import { AuthShell } from "@/components/auth/AuthShell" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { validateEmail } from "@/lib/validations" @@ -18,52 +20,52 @@ export default function ForgotEmailPage() { } return ( -
-
-
-

Reset your password

-

Enter the email address associated with your account and we'll send you a one-time code to verify your identity.

-
- -
-
- - onEmailChange((e.target as HTMLInputElement).value)} - className="h-11 rounded-lg" - placeholder="you@example.com" - aria-invalid={emailError !== ""} - /> - {emailError !== "" &&

{emailError}

} -
- -
- - -
-
+ +
+

Forgot password

+

Reset your password

+

+ Enter the email address associated with your account and we'll send you a one-time code to verify your identity. +

-
+ +
{ + e.preventDefault() + if (email.length === 0) { + setEmailError("Email is required") + return + } + if (emailError === "") router.push("/login/forgot/otp") + }} + > +
+ + onEmailChange(e.target.value)} + className="mt-2 h-11 rounded-lg px-4 text-base" + placeholder="you@example.com" + aria-invalid={emailError !== ""} + /> + {emailError !== "" &&

{emailError}

} +
+ + + +
+ + Back to sign in + +
+
+ ) } diff --git a/Frontend/erp-system/app/login/forgot/reset/page.tsx b/Frontend/erp-system/app/login/forgot/reset/page.tsx index 856958a..10329a0 100644 --- a/Frontend/erp-system/app/login/forgot/reset/page.tsx +++ b/Frontend/erp-system/app/login/forgot/reset/page.tsx @@ -1,7 +1,9 @@ "use client" +import Link from "next/link" import { useRouter } from "next/navigation" import { useState } from "react" +import { AuthShell } from "@/components/auth/AuthShell" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { @@ -13,6 +15,17 @@ import { } from "@/components/ui/dialog" import { Check, Eye, EyeOff } from "lucide-react" +function PasswordRequirement({ met, label }: { met: boolean; label: string }) { + return ( +
+
+ {met ? : ○} +
+ {label} +
+ ) +} + export default function ForgotResetPage() { const router = useRouter() const [password, setPassword] = useState("") @@ -28,115 +41,109 @@ export default function ForgotResetPage() { const passwordsMatch = password === confirm && password.length > 0 const allValid = meetsLength && hasUpper && hasNumber && hasSpecial && passwordsMatch - const PasswordRequirement = ({ met, label }: { met: boolean; label: string }) => ( -
-
- {met ? : ○} -
- {label} -
- ) - return ( -
-
-
-

Create a new password

-

Choose a strong password to secure your account.

-
- -
-
- -
- setPassword((e.target as HTMLInputElement).value)} - className="h-11 pr-11" - placeholder="Enter your new password" - /> - -
-
- -
- -
- setConfirm((e.target as HTMLInputElement).value)} - className="h-11 pr-11" - placeholder="Confirm your new password" - /> - -
-
- -
-

Password requirements:

- - - - -
- - {!passwordsMatch && confirm.length > 0 && ( -

Passwords don't match.

- )} - -
- - -
-
- - - - -
-
- -
-
- Password changed successfully - Your password has been updated. You can now sign in with your new password. -
-
-
- -
-
+ +
+

Forgot password

+

Create a new password

+

Choose a strong password to secure your account.

-
+ +
{ + e.preventDefault() + if (allValid) setShowSuccess(true) + }} + > +
+ +
+ setPassword(e.target.value)} + className="h-11 rounded-lg px-4 pr-11 text-base" + placeholder="Enter your new password" + /> + +
+
+ +
+ +
+ setConfirm(e.target.value)} + className="h-11 rounded-lg px-4 pr-11 text-base" + placeholder="Confirm your new password" + aria-invalid={!passwordsMatch && confirm.length > 0} + /> + +
+ {!passwordsMatch && confirm.length > 0 && ( +

Passwords don't match.

+ )} +
+ +
+

Password requirements:

+ + + + +
+ + + +
+ + Back to sign in + +
+
+ + + + +
+
+ +
+
+ Password changed successfully + Your password has been updated. You can now sign in with your new password. +
+
+
+ +
+
+ ) } diff --git a/Frontend/erp-system/app/login/page.tsx b/Frontend/erp-system/app/login/page.tsx index 3424e9c..4bd142b 100644 --- a/Frontend/erp-system/app/login/page.tsx +++ b/Frontend/erp-system/app/login/page.tsx @@ -1,7 +1,6 @@ "use client" import { Suspense, useState } from "react" -import Image from "next/image" import Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" import { zodResolver } from "@hookform/resolvers/zod" @@ -12,6 +11,7 @@ import { authApi } from "@/lib/api/auth" import { setStoredUser } from "@/lib/auth-session" import { errorMessage } from "@/lib/error-map" +import { AuthShell } from "@/components/auth/AuthShell" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field" @@ -55,35 +55,7 @@ function LoginForm() { }) return ( -
- {/* 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. */} -
- ERP Illustration - ERP Illustration -
- - {/* Login panel — overlays left side, no hard border with the photo */} -
-
- - Hexa ERP - -
- +

Sign in

Welcome back

@@ -174,13 +146,6 @@ function LoginForm() {
{Object.values(form.formState.errors).length > 0 && "There are errors in the form."}
- -
-

- Don't have an account? Create an account -

-
-
-
+ ) } diff --git a/Frontend/erp-system/components/Layouts/Header.tsx b/Frontend/erp-system/components/Layouts/Header.tsx index 93d2da0..8461978 100644 --- a/Frontend/erp-system/components/Layouts/Header.tsx +++ b/Frontend/erp-system/components/Layouts/Header.tsx @@ -22,7 +22,11 @@ import { Badge } from "@/components/ui/badge" import { DropdownMenu, DropdownMenuContent, + DropdownMenuGroup, DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" @@ -86,6 +90,14 @@ const STOCK_TITLES: Record = { "/dashboard/stock/wastage/new": "Record Wastage", } +const PRODUCTION_TITLES: Record = { + "/dashboard/production": "Production", + "/dashboard/production/templates": "Production Templates", + "/dashboard/production/templates/new": "New Template", + "/dashboard/production/runs": "Production Runs", + "/dashboard/production/produced": "Produced Items", +} + function titleFromPath(pathname: string) { if (pathname === "/dashboard/sales/invoices") return "Sales" if (pathname === "/dashboard/sales/invoices/new") return "New Sale" @@ -103,6 +115,10 @@ function titleFromPath(pathname: string) { if (/^\/dashboard\/stock\/transfers\/[^/]+$/.test(pathname)) return "Stock Transfer" if (/^\/dashboard\/stock\/counts\/[^/]+$/.test(pathname)) return "Stock Count" + if (PRODUCTION_TITLES[pathname]) return PRODUCTION_TITLES[pathname] + if (/^\/dashboard\/production\/templates\/[^/]+$/.test(pathname)) return "Template Builder" + if (/^\/dashboard\/production\/runs\/[^/]+$/.test(pathname)) return "Production Run" + if (PROCUREMENT_TITLES[pathname]) return PROCUREMENT_TITLES[pathname] if (/^\/dashboard\/procurement\/requisitions\/[^/]+$/.test(pathname)) return "Requisition" if (/^\/dashboard\/procurement\/rfqs\/[^/]+$/.test(pathname)) return "RFQ" @@ -277,12 +293,16 @@ export function Header() { type="button" onClick={() => router.back()} aria-label="Go back" - className="flex size-9 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground" + className="flex size-8 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground sm:size-9" > )} -

{title}

+ {/* Phones wrap to two lines rather than truncating — at this width a one-line title + is cut to a few letters ("Prod…"). One truncated line from sm, where it fits. */} +

+ {title} +

@@ -377,7 +397,8 @@ export function Header() { )}
- + {/* 108px wide — on phones it moves into the account menu so the title has room. */} + open && loadNotifications()}> - + {user ? initials(displayName(user)) : "?"} @@ -466,6 +487,39 @@ export function Header() {

)}
+
+ Theme + +
+ {/* The header's branch picker is hidden on phones, so admins switch here. Radio + items rather than a nested Select: a Select's portaled popup would read as an + outside press and close this menu. */} + {branchingEnabled && isAdmin && ( +
+ + + + + Branch + +
+ handleBranchChange(value)} + > + + All Branches + + {branches.map((b) => ( + + {b.name} + + ))} + +
+
+
+ )} } diff --git a/Frontend/erp-system/components/auth/AuthShell.tsx b/Frontend/erp-system/components/auth/AuthShell.tsx new file mode 100644 index 0000000..801adac --- /dev/null +++ b/Frontend/erp-system/components/auth/AuthShell.tsx @@ -0,0 +1,40 @@ +import Image from "next/image" +import Link from "next/link" + +/** Sign-in page chrome shared by the auth screens: illustration on the right, the form panel + * (logo + content) overlaying the left 45%. */ +export function AuthShell({ children }: { children: React.ReactNode }) { + return ( +
+ {/* 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. */} +
+ ERP Illustration + ERP Illustration +
+ + {/* Form panel — overlays left side, no hard border with the photo */} +
+
+ + Hexa ERP + +
+ {children} +
+
+ ) +} diff --git a/Frontend/erp-system/components/hrm/CodeNameMasterPage.tsx b/Frontend/erp-system/components/hrm/CodeNameMasterPage.tsx index a13c1e7..e4abaf8 100644 --- a/Frontend/erp-system/components/hrm/CodeNameMasterPage.tsx +++ b/Frontend/erp-system/components/hrm/CodeNameMasterPage.tsx @@ -4,7 +4,7 @@ // (Branch, Designation, EmploymentType) — identical shape to each other, so one // component parameterized by the resource's api/labels replaces 3 near-duplicate pages. import { useEffect, useState } from "react" -import { CheckCircle2, ChevronLeft, ChevronRight, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" +import { CheckCircle2, Pencil, Plus, Search, Trash2, XCircle, X } from "lucide-react" import { errorMessage } from "@/lib/error-map" import { cn } from "@/lib/utils" @@ -16,6 +16,7 @@ import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" +import { PaginationFooter } from "@/components/ui/pagination-footer" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "@/components/ui/toast" @@ -507,54 +508,13 @@ export function CodeNameMasterPage({ )} {pagination && pagination.totalItems > 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} {title.toLowerCase()} -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/components/production/RunStageNode.tsx b/Frontend/erp-system/components/production/RunStageNode.tsx index 952e9b9..25757d8 100644 --- a/Frontend/erp-system/components/production/RunStageNode.tsx +++ b/Frontend/erp-system/components/production/RunStageNode.tsx @@ -52,25 +52,16 @@ function fmt(n: number): string { } /** - * One stage on a run's production line, positioned from the run's own `posX`/`posY` (copied - * from the template at creation) and colored by its live status. Clicking it opens the stage - * drawer — handled by the page via `onNodeClick`, not here. + * The body of a stage card — everything but the frame. Shared by the canvas node below and the + * run page's phone list, so the two views of a stage can't drift apart. */ -function RunStageNode({ data, selected }: NodeProps & { data: RunStageData }) { +export function RunStageCardBody({ data }: { data: RunStageData }) { const color = STAGE_STATUS_COLOR[data.state] const running = data.actualStartAt !== null && data.actualMinutes === null const intakeShort = data.intake !== null && data.intake.delivered < data.intake.planned return ( -
- - + <>

{data.name}

@@ -116,7 +107,26 @@ function RunStageNode({ data, selected }: NodeProps & { data: RunStageData }) { {fmt(data.availableToTransfer)} to transfer
)} + + ) +} +/** + * One stage on a run's production line, positioned from the run's own `posX`/`posY` (copied + * from the template at creation) and colored by its live status. Clicking it opens the stage + * drawer — handled by the page via `onNodeClick`, not here. + */ +function RunStageNode({ data, selected }: NodeProps & { data: RunStageData }) { + return ( +
+ +
) diff --git a/Frontend/erp-system/components/settings/BranchMasterPage.tsx b/Frontend/erp-system/components/settings/BranchMasterPage.tsx index b2781f4..d3876c0 100644 --- a/Frontend/erp-system/components/settings/BranchMasterPage.tsx +++ b/Frontend/erp-system/components/settings/BranchMasterPage.tsx @@ -8,7 +8,7 @@ import { useEffect, useState } from "react" import Link from "next/link" import { usePathname } from "next/navigation" -import { Building2, ChevronLeft, ChevronRight, CheckCircle2, Eye, Pencil, Plus, Search, Trash2, X, XCircle } from "lucide-react" +import { Building2, CheckCircle2, Eye, Pencil, Plus, Search, Trash2, X, XCircle } from "lucide-react" import { branchesHrmApi } from "@/lib/api/hrm-masters" import { errorMessage } from "@/lib/error-map" @@ -22,6 +22,7 @@ import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" +import { PaginationFooter } from "@/components/ui/pagination-footer" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "@/components/ui/toast" @@ -477,54 +478,13 @@ export function BranchMasterPage() { )} {pagination && pagination.totalItems > 0 && ( -
-
-

- Showing {(pagination.page - 1) * pagination.pageSize + 1}– - - {Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} - {" "} - of {pagination.totalItems} branches -

- -
-
- - - Page {pagination.page} of {pagination.totalPages} - - -
+
+ { setPage(1); setPageSize(size) }} + />
)}
diff --git a/Frontend/erp-system/components/ui/otp-input.tsx b/Frontend/erp-system/components/ui/otp-input.tsx index 1b542e3..c674825 100644 --- a/Frontend/erp-system/components/ui/otp-input.tsx +++ b/Frontend/erp-system/components/ui/otp-input.tsx @@ -38,7 +38,7 @@ export default function OtpInput({ length = 6, value = "", onChange }: OtpInputP value={char} onChange={(e) => handleChange(e.target.value, i)} onKeyDown={(e) => handleKeyDown(e, i)} - className="w-14 h-14 mx-1 text-center text-lg font-semibold border-2 border-input rounded-lg bg-background text-foreground transition-all outline-none placeholder:text-muted-foreground focus:border-ring focus:ring-3 focus:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:border-input dark:bg-input/30 dark:focus:ring-ring/30" + className="size-11 sm:size-14 mx-0.5 sm:mx-1 text-center text-lg font-semibold border-2 border-input rounded-lg bg-background text-foreground transition-all outline-none placeholder:text-muted-foreground focus:border-ring focus:ring-3 focus:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:border-input dark:bg-input/30 dark:focus:ring-ring/30" /> ) }