branch crud and intergration
This commit is contained in:
@@ -6,10 +6,12 @@ import { useParams } from "next/navigation"
|
||||
import { ArrowLeft, BookText } from "lucide-react"
|
||||
|
||||
import { bankAccountsApi, chequeBooksApi } from "@/lib/api/general-ledger"
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { formatAmount, formatReportDate } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CashAndBankAccountDto, CashBankAccountType, ChequeBook, ChequePage, ChequePageIssueStatus } from "@/types/general-ledger"
|
||||
import { Branch } from "@/types/hrm"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
@@ -32,10 +34,15 @@ export default function ChequeBookDetailPage() {
|
||||
|
||||
const [book, setBook] = useState<ChequeBook | null>(null)
|
||||
const [bankAccount, setBankAccount] = useState<CashAndBankAccountDto | null>(null)
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectedPage, setSelectedPage] = useState<ChequePage | null>(null)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
chequeBooksApi
|
||||
@@ -94,7 +101,7 @@ export default function ChequeBookDetailPage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Branch</p>
|
||||
<p className="text-base font-medium">#{book.branchId}</p>
|
||||
<p className="text-base font-medium">{branches.find((b) => b.branchId === book.branchId)?.name ?? `#${book.branchId}`}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Range</p>
|
||||
|
||||
@@ -6,11 +6,14 @@ import { useRouter } from "next/navigation"
|
||||
import { ArrowLeft } from "lucide-react"
|
||||
|
||||
import { bankAccountsApi, chequeBooksApi } from "@/lib/api/general-ledger"
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { validateChequeBookForm } from "@/lib/validations/general-ledger"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CashAndBankAccountDto, CashBankAccountType } from "@/types/general-ledger"
|
||||
import { Branch } from "@/types/hrm"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -19,9 +22,11 @@ import { toast } from "@/components/ui/toast"
|
||||
|
||||
export default function NewChequeBookPage() {
|
||||
const router = useRouter()
|
||||
const { isAdmin, branchId: myBranchId, activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [bankAccounts, setBankAccounts] = useState<CashAndBankAccountDto[] | null>(null)
|
||||
const [bankAccountsError, setBankAccountsError] = useState<string | null>(null)
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
|
||||
const [branchId, setBranchId] = useState("")
|
||||
const [bankAccountId, setBankAccountId] = useState("")
|
||||
@@ -44,6 +49,21 @@ export default function NewChequeBookPage() {
|
||||
.catch((err) => setBankAccountsError(errorMessage(err)))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}, [])
|
||||
|
||||
// Pinned to the header's branch the same way as Employee/Warehouse/GRN/Sale: an Admin on a
|
||||
// specific branch (or any non-admin) gets it locked here too — only "All Branches" (or the
|
||||
// Branch Configuration toggle being off) leaves it as a free pick, since GL's own record
|
||||
// still needs *some* branch id regardless of our toggle.
|
||||
const lockedBranchId = branchingEnabled ? (isAdmin ? activeBranchId : myBranchId) : null
|
||||
const showBranchPicker = !lockedBranchId
|
||||
|
||||
useEffect(() => {
|
||||
if (lockedBranchId) setBranchId(String(lockedBranchId))
|
||||
}, [lockedBranchId])
|
||||
|
||||
async function handleSubmit() {
|
||||
const nextErrors = validateChequeBookForm({
|
||||
branchId,
|
||||
@@ -118,15 +138,29 @@ export default function NewChequeBookPage() {
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.branchId}>
|
||||
<FieldLabel htmlFor="cb-branch">Branch ID</FieldLabel>
|
||||
<Input
|
||||
id="cb-branch"
|
||||
type="number"
|
||||
value={branchId}
|
||||
onChange={(e) => setBranchId(e.target.value)}
|
||||
placeholder="1"
|
||||
aria-invalid={!!errors.branchId}
|
||||
/>
|
||||
<FieldLabel htmlFor="cb-branch">Branch</FieldLabel>
|
||||
{showBranchPicker ? (
|
||||
<Select<string> value={branchId} onValueChange={(v) => setBranchId(v ?? "")}>
|
||||
<SelectTrigger id="cb-branch" className="w-full text-base" aria-invalid={!!errors.branchId}>
|
||||
<SelectValue placeholder="Select a branch" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{branches.map((b) => (
|
||||
<SelectItem key={b.branchId} value={String(b.branchId)} label={b.name} className="text-base">
|
||||
{b.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
id="cb-branch"
|
||||
value={branches.find((b) => b.branchId === lockedBranchId)?.name ?? `Branch #${lockedBranchId}`}
|
||||
readOnly
|
||||
disabled
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
<FieldError errors={[errors.branchId ? { message: errors.branchId } : undefined]} />
|
||||
</Field>
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@ import { useRouter } from "next/navigation"
|
||||
import { ArrowLeft, BookText, Plus } from "lucide-react"
|
||||
|
||||
import { bankAccountsApi, chequeBooksApi } from "@/lib/api/general-ledger"
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { formatReportDate } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CashAndBankAccountDto, CashBankAccountType, ChequeBook, ChequeBookStatus } from "@/types/general-ledger"
|
||||
import { Branch } from "@/types/hrm"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
@@ -27,15 +30,24 @@ const STATUS_BADGE: Record<ChequeBookStatus, string> = {
|
||||
|
||||
export default function ChequeBooksPage() {
|
||||
const router = useRouter()
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
const [books, setBooks] = useState<ChequeBook[] | null>(null)
|
||||
const [bankAccounts, setBankAccounts] = useState<CashAndBankAccountDto[] | null>(null)
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [status, setStatus] = useState<StatusFilter>("All")
|
||||
|
||||
useEffect(() => {
|
||||
branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
Promise.all([
|
||||
chequeBooksApi.list(status === "All" ? undefined : { status }),
|
||||
chequeBooksApi.list({
|
||||
status: status === "All" ? undefined : status,
|
||||
branchId: branchingEnabled ? (activeBranchId ?? undefined) : undefined,
|
||||
}),
|
||||
bankAccounts ? Promise.resolve(bankAccounts) : bankAccountsApi.list(CashBankAccountType.Bank),
|
||||
])
|
||||
.then(([result, banks]) => {
|
||||
@@ -51,9 +63,10 @@ export default function ChequeBooksPage() {
|
||||
}
|
||||
// bankAccounts intentionally excluded — fetched once, reused across status re-fetches.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [status])
|
||||
}, [status, activeBranchId, branchingEnabled])
|
||||
|
||||
const bankAccountsById = useMemo(() => new Map((bankAccounts ?? []).map((a) => [a.accountId, a])), [bankAccounts])
|
||||
const branchesById = useMemo(() => new Map(branches.map((b) => [b.branchId, b])), [branches])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
@@ -132,7 +145,7 @@ export default function ChequeBooksPage() {
|
||||
<TableCell className="px-3 py-3.5 text-muted-foreground">
|
||||
{bank ? bank.accountName : `#${b.bankAccountId}`}
|
||||
</TableCell>
|
||||
<TableCell className="px-3 py-3.5 text-muted-foreground">#{b.branchId}</TableCell>
|
||||
<TableCell className="px-3 py-3.5 text-muted-foreground">{branchesById.get(b.branchId)?.name ?? `#${b.branchId}`}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">
|
||||
{b.startChequeNo} – {b.endChequeNo}
|
||||
</TableCell>
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ArrowLeft } from "lucide-react"
|
||||
|
||||
import { receivedChequesApi } from "@/lib/api/general-ledger"
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { validateReceivedChequeForm } from "@/lib/validations/general-ledger"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ReceivedFromType } from "@/types/general-ledger"
|
||||
import { Branch } from "@/types/hrm"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
|
||||
const NONE = "__none__"
|
||||
|
||||
export default function NewReceivedChequePage() {
|
||||
const router = useRouter()
|
||||
const { isAdmin, branchId: myBranchId, activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [companyId, setCompanyId] = useState("")
|
||||
const [branchId, setBranchId] = useState("")
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
const [receivedFromType, setReceivedFromType] = useState<ReceivedFromType>(ReceivedFromType.Customer)
|
||||
const [receivedFromId, setReceivedFromId] = useState("")
|
||||
const [receivedFromName, setReceivedFromName] = useState("")
|
||||
@@ -39,6 +46,19 @@ export default function NewReceivedChequePage() {
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}, [])
|
||||
|
||||
// Same pinning as the New Cheque Book form: locked whenever the header (or the caller's own
|
||||
// branch, for non-admins) resolves to one; a free pick only on "All Branches" or the toggle off.
|
||||
const lockedBranchId = branchingEnabled ? (isAdmin ? activeBranchId : myBranchId) : null
|
||||
const showBranchPicker = !lockedBranchId
|
||||
|
||||
useEffect(() => {
|
||||
if (lockedBranchId) setBranchId(String(lockedBranchId))
|
||||
}, [lockedBranchId])
|
||||
|
||||
async function handleSubmit() {
|
||||
const nextErrors = validateReceivedChequeForm({ companyId, receivedFromName, chequeNo, chequeDate, amount, receivedDate })
|
||||
setErrors(nextErrors)
|
||||
@@ -100,8 +120,30 @@ export default function NewReceivedChequePage() {
|
||||
<FieldError errors={[errors.companyId ? { message: errors.companyId } : undefined]} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rc-branch">Branch ID (optional)</FieldLabel>
|
||||
<Input id="rc-branch" type="number" value={branchId} onChange={(e) => setBranchId(e.target.value)} />
|
||||
<FieldLabel htmlFor="rc-branch">Branch (optional)</FieldLabel>
|
||||
{showBranchPicker ? (
|
||||
<Select<string> value={branchId || NONE} onValueChange={(v) => setBranchId(v && v !== NONE ? v : "")}>
|
||||
<SelectTrigger id="rc-branch" className="w-full text-base">
|
||||
<SelectValue placeholder="Select a branch" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE} label="No branch" className="text-base">No branch</SelectItem>
|
||||
{branches.map((b) => (
|
||||
<SelectItem key={b.branchId} value={String(b.branchId)} label={b.name} className="text-base">
|
||||
{b.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
id="rc-branch"
|
||||
value={branches.find((b) => b.branchId === lockedBranchId)?.name ?? `Branch #${lockedBranchId}`}
|
||||
readOnly
|
||||
disabled
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { ArrowLeft, Inbox, Plus } from "lucide-react"
|
||||
|
||||
import { receivedChequesApi } from "@/lib/api/general-ledger"
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { formatAmount, formatReportDate } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ReceivedCheque, ReceivedChequeStatus } from "@/types/general-ledger"
|
||||
import { Branch } from "@/types/hrm"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
@@ -28,16 +31,25 @@ const STATUS_BADGE: Record<ReceivedChequeStatus, string> = {
|
||||
}
|
||||
|
||||
export default function ReceivedChequesPage() {
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
const [cheques, setCheques] = useState<ReceivedCheque[] | null>(null)
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [status, setStatus] = useState<StatusFilter>("All")
|
||||
const [selected, setSelected] = useState<ReceivedCheque | null>(null)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
receivedChequesApi
|
||||
.list(status === "All" ? undefined : { status })
|
||||
.list({
|
||||
status: status === "All" ? undefined : status,
|
||||
branchId: branchingEnabled ? (activeBranchId ?? undefined) : undefined,
|
||||
})
|
||||
.then((res) => {
|
||||
if (!cancelled) setCheques(res.items)
|
||||
})
|
||||
@@ -47,7 +59,9 @@ export default function ReceivedChequesPage() {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [status])
|
||||
}, [status, activeBranchId, branchingEnabled])
|
||||
|
||||
const branchesById = useMemo(() => new Map(branches.map((b) => [b.branchId, b])), [branches])
|
||||
|
||||
function handleUpdated(updated: ReceivedCheque) {
|
||||
setCheques((prev) => (prev ? prev.map((c) => (c.receivedChequeId === updated.receivedChequeId ? updated : c)) : prev))
|
||||
@@ -114,6 +128,7 @@ export default function ReceivedChequesPage() {
|
||||
<TableHead className="h-12 px-3 text-sm">Cheque no.</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Received from</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Type</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Branch</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm text-right">Amount</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Received</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Status</TableHead>
|
||||
@@ -132,6 +147,9 @@ export default function ReceivedChequesPage() {
|
||||
<TableCell className="px-3 py-3.5 font-medium">{c.chequeNo}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{c.receivedFromName}</TableCell>
|
||||
<TableCell className="px-3 py-3.5 text-muted-foreground">{c.receivedFromType}</TableCell>
|
||||
<TableCell className="px-3 py-3.5 text-muted-foreground">
|
||||
{c.branchId ? (branchesById.get(c.branchId)?.name ?? `#${c.branchId}`) : "—"}
|
||||
</TableCell>
|
||||
<TableCell className="px-3 py-3.5 text-right tabular-nums">{formatAmount(c.amount)}</TableCell>
|
||||
<TableCell className="px-3 py-3.5 text-muted-foreground">{formatReportDate(c.receivedDate)}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">
|
||||
|
||||
@@ -5,10 +5,11 @@ import { useParams } from "next/navigation"
|
||||
import { Link2, Plus, Upload } from "lucide-react"
|
||||
|
||||
import { CreateEmployeeLoanRequest, CreateSalaryStructureRequest, employeesApi } from "@/lib/api/employees"
|
||||
import { departmentsApi, designationsApi, employmentTypesApi, hrDocumentTypesApi, salaryComponentsApi, workShiftsApi } from "@/lib/api/hrm-masters"
|
||||
import { branchesHrmApi, departmentsApi, designationsApi, employmentTypesApi, hrDocumentTypesApi, salaryComponentsApi, workShiftsApi } from "@/lib/api/hrm-masters"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Branch,
|
||||
Department,
|
||||
Designation,
|
||||
EmployeeBankDetail,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
WorkShift,
|
||||
} from "@/types/hrm"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -43,6 +45,7 @@ function money(n: number): string {
|
||||
export default function EmployeeDetailPage() {
|
||||
const params = useParams<{ id: string }>()
|
||||
const employeeId = Number(params.id)
|
||||
const { isAdmin, branchId: myBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [tab, setTab] = useState<Tab>("Overview")
|
||||
const [employee, setEmployee] = useState<EmployeeDetail | null>(null)
|
||||
@@ -53,6 +56,7 @@ export default function EmployeeDetailPage() {
|
||||
const [designations, setDesignations] = useState<Designation[]>([])
|
||||
const [employmentTypes, setEmploymentTypes] = useState<EmploymentType[]>([])
|
||||
const [workShifts, setWorkShifts] = useState<WorkShift[]>([])
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const [bankDetails, setBankDetails] = useState<EmployeeBankDetail[]>([])
|
||||
@@ -92,7 +96,8 @@ export default function EmployeeDetailPage() {
|
||||
employmentTypesApi.list({ pageSize: 200, status: "Active" }).then((r) => setEmploymentTypes(r.items)).catch(() => setEmploymentTypes([]))
|
||||
workShiftsApi.list({ pageSize: 200, status: "Active" }).then((r) => setWorkShifts(r.items)).catch(() => setWorkShifts([]))
|
||||
hrDocumentTypesApi.list({ pageSize: 200, status: "Active" }).then((r) => setDocumentTypes(r.items)).catch(() => setDocumentTypes([]))
|
||||
}, [])
|
||||
if (branchingEnabled) branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}, [branchingEnabled])
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === "Bank Details") employeesApi.listBankDetails(employeeId).then(setBankDetails).catch((err) => toast.error("Could not load bank details", errorMessage(err)))
|
||||
@@ -271,6 +276,15 @@ export default function EmployeeDetailPage() {
|
||||
<SelectContent>{workShifts.map((w) => <SelectItem key={w.workShiftId} value={String(w.workShiftId)}>{w.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
{branchingEnabled && (
|
||||
<Field>
|
||||
<FieldLabel>Branch</FieldLabel>
|
||||
<Select value={String(employee.branchId)} onValueChange={(v) => setEmployee({ ...employee, branchId: Number(v) })}>
|
||||
<SelectTrigger disabled={!isAdmin}><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{(isAdmin ? branches : branches.filter((b) => b.branchId === myBranchId || b.branchId === employee.branchId)).map((b) => <SelectItem key={b.branchId} value={String(b.branchId)}>{b.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)}
|
||||
<Field><FieldLabel>EPF number</FieldLabel><Input value={employee.epfNumber ?? ""} onChange={(e) => setEmployee({ ...employee, epfNumber: e.target.value })} /></Field>
|
||||
<Field><FieldLabel>ETF number</FieldLabel><Input value={employee.etfNumber ?? ""} onChange={(e) => setEmployee({ ...employee, etfNumber: e.target.value })} /></Field>
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,13 @@ import Link from "next/link"
|
||||
import { Link2, Pencil, Plus, Users as UsersIcon } from "lucide-react"
|
||||
|
||||
import { employeesApi } from "@/lib/api/employees"
|
||||
import { departmentsApi, designationsApi, employmentTypesApi, workShiftsApi } from "@/lib/api/hrm-masters"
|
||||
import { branchesHrmApi, departmentsApi, designationsApi, employmentTypesApi, workShiftsApi } from "@/lib/api/hrm-masters"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { PaginationMeta } from "@/types/common"
|
||||
import { CreateEmployeeRequest, Department, Designation, EmployeeListItem, EmploymentType, UserMatch, WorkShift } from "@/types/hrm"
|
||||
import { Branch, CreateEmployeeRequest, Department, Designation, EmployeeListItem, EmploymentType, UserMatch, WorkShift } from "@/types/hrm"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -31,10 +32,19 @@ const emptyForm: CreateEmployeeRequest = {
|
||||
departmentId: 0,
|
||||
designationId: 0,
|
||||
employmentTypeId: 0,
|
||||
branchId: 0,
|
||||
workShiftId: 0,
|
||||
}
|
||||
|
||||
export default function EmployeesPage() {
|
||||
const { isAdmin, branchId: myBranchId, branchName: myBranchName, activeBranchId, activeBranchName, branchingEnabled } = useAuth()
|
||||
// The branch to pin the new hire to without asking: an Admin's currently-selected
|
||||
// header branch, or a non-admin's own branch. Only when an Admin is on "All Branches"
|
||||
// (no single branch implied) do they need the dropdown to pick one explicitly.
|
||||
const lockedBranchId = isAdmin ? activeBranchId : myBranchId
|
||||
const lockedBranchName = isAdmin ? activeBranchName : myBranchName
|
||||
const showBranchPicker = branchingEnabled && isAdmin && !lockedBranchId
|
||||
|
||||
const [employees, setEmployees] = useState<EmployeeListItem[] | null>(null)
|
||||
const [pagination, setPagination] = useState<PaginationMeta | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -45,6 +55,7 @@ export default function EmployeesPage() {
|
||||
const [designations, setDesignations] = useState<Designation[]>([])
|
||||
const [employmentTypes, setEmploymentTypes] = useState<EmploymentType[]>([])
|
||||
const [workShifts, setWorkShifts] = useState<WorkShift[]>([])
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [form, setForm] = useState<CreateEmployeeRequest>(emptyForm)
|
||||
@@ -57,18 +68,22 @@ export default function EmployeesPage() {
|
||||
|
||||
function load() {
|
||||
employeesApi
|
||||
.list({ page, pageSize: PAGE_SIZE, q: q || undefined })
|
||||
.list({ page, pageSize: PAGE_SIZE, q: q || undefined, branchId: branchingEnabled ? (activeBranchId ?? undefined) : undefined })
|
||||
.then((res) => { setEmployees(res.items); setPagination(res.pagination) })
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}
|
||||
useEffect(load, [page, q])
|
||||
useEffect(load, [page, q, activeBranchId, branchingEnabled])
|
||||
// Switching branch changes what "page 1" even means — stay on a page past the
|
||||
// new filtered result set otherwise.
|
||||
useEffect(() => setPage(1), [activeBranchId])
|
||||
|
||||
useEffect(() => {
|
||||
departmentsApi.list({ pageSize: 200, status: "Active" }).then((r) => setDepartments(r.items)).catch(() => setDepartments([]))
|
||||
designationsApi.list({ pageSize: 200, status: "Active" }).then((r) => setDesignations(r.items)).catch(() => setDesignations([]))
|
||||
employmentTypesApi.list({ pageSize: 200, status: "Active" }).then((r) => setEmploymentTypes(r.items)).catch(() => setEmploymentTypes([]))
|
||||
workShiftsApi.list({ pageSize: 200, status: "Active" }).then((r) => setWorkShifts(r.items)).catch(() => setWorkShifts([]))
|
||||
}, [])
|
||||
if (branchingEnabled) branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}, [branchingEnabled])
|
||||
|
||||
async function checkEmail(email: string) {
|
||||
if (!email.trim()) { setUserMatch(null); return }
|
||||
@@ -98,6 +113,7 @@ export default function EmployeesPage() {
|
||||
if (!form.designationId) nextErrors.designationId = "Designation is required"
|
||||
if (!form.employmentTypeId) nextErrors.employmentTypeId = "Employment type is required"
|
||||
if (!form.workShiftId) nextErrors.workShiftId = "Work shift is required"
|
||||
if (branchingEnabled && !form.branchId) nextErrors.branchId = "Branch is required"
|
||||
setErrors(nextErrors)
|
||||
if (Object.keys(nextErrors).length > 0) return
|
||||
|
||||
@@ -126,7 +142,14 @@ export default function EmployeesPage() {
|
||||
<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>
|
||||
</div>
|
||||
<Dialog open={open} onOpenChange={(v) => { setOpen(v); if (!v) resetForm() }}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(v) => {
|
||||
setOpen(v)
|
||||
if (!v) resetForm()
|
||||
else if (lockedBranchId) setForm((f) => ({ ...f, branchId: lockedBranchId }))
|
||||
}}
|
||||
>
|
||||
<DialogTrigger render={<Button size="lg"><Plus className="size-5" />New Employee</Button>} />
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader className="items-center text-center">
|
||||
@@ -173,7 +196,7 @@ export default function EmployeesPage() {
|
||||
<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) }))}>
|
||||
<Select value={String(form.departmentId)} onValueChange={(v) => setForm((f) => ({ ...f, departmentId: Number(v) }))}>
|
||||
<SelectTrigger id="e-dept"><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{departments.map((d) => <SelectItem key={d.departmentId} value={String(d.departmentId)}>{d.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
@@ -181,7 +204,7 @@ export default function EmployeesPage() {
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.designationId}>
|
||||
<FieldLabel htmlFor="e-desig">Designation</FieldLabel>
|
||||
<Select value={form.designationId ? String(form.designationId) : undefined} onValueChange={(v) => setForm((f) => ({ ...f, designationId: Number(v) }))}>
|
||||
<Select value={String(form.designationId)} onValueChange={(v) => setForm((f) => ({ ...f, designationId: Number(v) }))}>
|
||||
<SelectTrigger id="e-desig"><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{designations.map((d) => <SelectItem key={d.designationId} value={String(d.designationId)}>{d.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
@@ -189,7 +212,7 @@ export default function EmployeesPage() {
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.employmentTypeId}>
|
||||
<FieldLabel htmlFor="e-etype">Employment type</FieldLabel>
|
||||
<Select value={form.employmentTypeId ? String(form.employmentTypeId) : undefined} onValueChange={(v) => setForm((f) => ({ ...f, employmentTypeId: Number(v) }))}>
|
||||
<Select value={String(form.employmentTypeId)} onValueChange={(v) => setForm((f) => ({ ...f, employmentTypeId: Number(v) }))}>
|
||||
<SelectTrigger id="e-etype"><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{employmentTypes.map((t) => <SelectItem key={t.employmentTypeId} value={String(t.employmentTypeId)}>{t.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
@@ -197,12 +220,28 @@ export default function EmployeesPage() {
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.workShiftId}>
|
||||
<FieldLabel htmlFor="e-shift">Work shift</FieldLabel>
|
||||
<Select value={form.workShiftId ? String(form.workShiftId) : undefined} onValueChange={(v) => setForm((f) => ({ ...f, workShiftId: Number(v) }))}>
|
||||
<Select value={String(form.workShiftId)} onValueChange={(v) => setForm((f) => ({ ...f, workShiftId: Number(v) }))}>
|
||||
<SelectTrigger id="e-shift"><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{workShifts.map((w) => <SelectItem key={w.workShiftId} value={String(w.workShiftId)}>{w.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[errors.workShiftId ? { message: errors.workShiftId } : undefined]} />
|
||||
</Field>
|
||||
{branchingEnabled && (
|
||||
<Field data-invalid={!!errors.branchId}>
|
||||
<FieldLabel htmlFor="e-branch">Branch</FieldLabel>
|
||||
{showBranchPicker ? (
|
||||
<Select value={String(form.branchId)} onValueChange={(v) => setForm((f) => ({ ...f, branchId: v ? Number(v) : 0 }))}>
|
||||
<SelectTrigger id="e-branch" aria-invalid={!!errors.branchId}><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{branches.map((b) => <SelectItem key={b.branchId} value={String(b.branchId)}>{b.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div id="e-branch" className="flex h-9 items-center rounded-lg border border-input bg-muted px-3 text-sm text-muted-foreground">
|
||||
{lockedBranchName ?? "No branch assigned — contact an admin"}
|
||||
</div>
|
||||
)}
|
||||
<FieldError errors={[errors.branchId ? { message: errors.branchId } : undefined]} />
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
</FieldGroup>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { CodeNameMasterPage } from "@/components/hrm/CodeNameMasterPage"
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { Branch } from "@/types/hrm"
|
||||
import { BranchMasterPage } from "@/components/settings/BranchMasterPage"
|
||||
|
||||
export default function BranchesPage() {
|
||||
return (
|
||||
<CodeNameMasterPage<Branch>
|
||||
title="Branches"
|
||||
description="Company locations/branches — used for multi-branch employee and payroll scoping."
|
||||
idOf={(b) => b.branchId}
|
||||
api={branchesHrmApi}
|
||||
/>
|
||||
)
|
||||
return <BranchMasterPage />
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { cn } from "@/lib/utils"
|
||||
import { DashboardStats } from "@/types/dashboard"
|
||||
import { LedgerEntry } from "@/types/stock"
|
||||
import { Warehouse } from "@/types/master-data"
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { StatCard } from "@/components/ui/stat-card"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -42,6 +43,8 @@ const SAMPLE_TREND_IN = [42, 58, 35, 70, 64, 30, 20, 85, 46, 55, 38, 62, 48, 72]
|
||||
const SAMPLE_TREND_OUT = [30, 40, 45, 38, 50, 22, 15, 60, 33, 47, 28, 44, 36, 58]
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||
const [movements, setMovements] = useState<LedgerEntry[] | null>(null)
|
||||
const [trend, setTrend] = useState<LedgerEntry[] | null>(null)
|
||||
@@ -53,11 +56,12 @@ export default function DashboardPage() {
|
||||
|
||||
const from = new Date()
|
||||
from.setDate(from.getDate() - (TREND_DAYS - 1))
|
||||
const branchId = branchingEnabled ? (activeBranchId ?? undefined) : undefined
|
||||
|
||||
Promise.all([
|
||||
dashboardApi.stats(),
|
||||
stockApi.ledger({ page: 1, pageSize: 5 }),
|
||||
stockApi.ledger({ from: isoDate(from), page: 1, pageSize: 200 }),
|
||||
dashboardApi.stats({ branchId }),
|
||||
stockApi.ledger({ page: 1, pageSize: 5, branchId }),
|
||||
stockApi.ledger({ from: isoDate(from), page: 1, pageSize: 200, branchId }),
|
||||
warehousesApi.list(),
|
||||
])
|
||||
.then(([statsRes, ledger, trendRes, warehousesRes]) => {
|
||||
@@ -74,7 +78,7 @@ export default function DashboardPage() {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
}, [activeBranchId, branchingEnabled])
|
||||
|
||||
const warehousesById = useMemo(() => new Map((warehouses ?? []).map((w) => [w.warehouseId, w])), [warehouses])
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { ArrowLeft, ExternalLink, Plus, RefreshCw, ShieldCheck, Trash2 } from "lucide-react"
|
||||
@@ -9,6 +9,7 @@ import { grnsApi } from "@/lib/api/grns"
|
||||
import { purchaseOrdersApi } from "@/lib/api/purchase-orders"
|
||||
import { vendorsApi } from "@/lib/api/vendors"
|
||||
import { warehousesApi } from "@/lib/api/warehouses"
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { itemsApi } from "@/lib/api/items"
|
||||
import { uomsApi } from "@/lib/api/uoms"
|
||||
import { baseUomLabel } from "@/lib/uom-label"
|
||||
@@ -18,7 +19,9 @@ import { cn } from "@/lib/utils"
|
||||
import { CreateGrnLineInput, HoldStatus } from "@/types/grn"
|
||||
import { PurchaseOrder, PurchaseOrderSummary } from "@/types/procurement"
|
||||
import { Bin, ItemListItem, Uom, Vendor, Warehouse } from "@/types/master-data"
|
||||
import { Branch } from "@/types/hrm"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -174,10 +177,13 @@ function emptyLine(): DraftLine {
|
||||
|
||||
export default function NewGrnPage() {
|
||||
const router = useRouter()
|
||||
const { isAdmin, branchId: myBranchId, activeBranchId, activeBranchName, branchingEnabled } = useAuth()
|
||||
|
||||
const [mode, setMode] = useState<Mode>("po")
|
||||
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[] | null>(null)
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
const [branchFilterId, setBranchFilterId] = useState<number | null>(null)
|
||||
const [items, setItems] = useState<ItemListItem[] | null>(null)
|
||||
const [uoms, setUoms] = useState<Uom[] | null>(null)
|
||||
const [vendors, setVendors] = useState<Vendor[] | null>(null)
|
||||
@@ -246,6 +252,31 @@ export default function NewGrnPage() {
|
||||
warehousesApi.listBins(warehouseId).then(setBins).catch(() => setBins([]))
|
||||
}, [warehouseId])
|
||||
|
||||
// Admin-only branch picker, narrowing which warehouses show up below — non-admins never
|
||||
// see this control, their warehouse choices are narrowed to their own branch directly.
|
||||
useEffect(() => {
|
||||
if (branchingEnabled && isAdmin) {
|
||||
branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}
|
||||
}, [branchingEnabled, isAdmin])
|
||||
|
||||
// An Admin on a specific header branch is pinned to it here too (shown, not editable) —
|
||||
// the dropdown only appears when the header itself is on "All Branches", so there's no
|
||||
// branch to pin to and the Admin must pick one explicitly for this GRN.
|
||||
const lockedBranchId = isAdmin ? activeBranchId : myBranchId
|
||||
const showBranchPicker = branchingEnabled && isAdmin && !lockedBranchId
|
||||
const effectiveBranchId = branchingEnabled ? (isAdmin ? (activeBranchId ?? branchFilterId) : myBranchId) : null
|
||||
const visibleWarehouses = useMemo(
|
||||
() => (effectiveBranchId === null ? (warehouses ?? []) : (warehouses ?? []).filter((w) => w.branchId === effectiveBranchId)),
|
||||
[warehouses, effectiveBranchId]
|
||||
)
|
||||
|
||||
// Switching branch can strip the currently-picked warehouse out of the visible set —
|
||||
// clear it rather than silently submitting a warehouse from the previous branch.
|
||||
useEffect(() => {
|
||||
setWarehouseId((prev) => (prev !== null && !visibleWarehouses.some((w) => w.warehouseId === prev) ? null : prev))
|
||||
}, [visibleWarehouses])
|
||||
|
||||
function switchMode(next: Mode) {
|
||||
setMode(next)
|
||||
setPoId(null)
|
||||
@@ -571,6 +602,30 @@ export default function NewGrnPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{branchingEnabled && isAdmin && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">Branch</Label>
|
||||
{showBranchPicker ? (
|
||||
<Select<number | null> value={branchFilterId} onValueChange={(v) => setBranchFilterId(v)}>
|
||||
<SelectTrigger className="h-12! w-full text-base">
|
||||
<SelectValue placeholder="All branches" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{branches.map((b) => (
|
||||
<SelectItem key={b.branchId} value={b.branchId} className="text-base">
|
||||
{b.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="flex h-12 items-center rounded-lg border border-input bg-muted px-3 text-base text-muted-foreground">
|
||||
{activeBranchName ?? `Branch #${lockedBranchId}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">Warehouse</Label>
|
||||
<Select<number | null> value={warehouseId} onValueChange={(v) => setWarehouseId(v)}>
|
||||
@@ -578,7 +633,7 @@ export default function NewGrnPage() {
|
||||
<SelectValue placeholder="Select warehouse" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(warehouses ?? []).map((w) => (
|
||||
{visibleWarehouses.map((w) => (
|
||||
<SelectItem key={w.warehouseId} value={w.warehouseId} className="text-base">
|
||||
{w.code} — {w.name}
|
||||
</SelectItem>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { errorMessage } from "@/lib/error-map"
|
||||
import { GrnStatus, GrnSummary } from "@/types/grn"
|
||||
import { PaginationMeta } from "@/types/common"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
@@ -29,6 +30,8 @@ type StatusFilter = GrnStatus | "All"
|
||||
const PAGE_SIZE = 5
|
||||
|
||||
export default function GrnListPage() {
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [grns, setGrns] = useState<GrnSummary[] | null>(null)
|
||||
const [pagination, setPagination] = useState<PaginationMeta | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -48,7 +51,7 @@ export default function GrnListPage() {
|
||||
// Any filter change starts back at page 1.
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
}, [query, status])
|
||||
}, [query, status, activeBranchId])
|
||||
|
||||
function load() {
|
||||
setError(null)
|
||||
@@ -58,6 +61,7 @@ export default function GrnListPage() {
|
||||
pageSize: PAGE_SIZE,
|
||||
q: query || undefined,
|
||||
status: status === "All" ? undefined : status,
|
||||
branchId: branchingEnabled ? (activeBranchId ?? undefined) : undefined,
|
||||
})
|
||||
.then((res) => {
|
||||
setGrns(res.items)
|
||||
@@ -66,7 +70,7 @@ export default function GrnListPage() {
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}
|
||||
|
||||
useEffect(load, [page, query, status])
|
||||
useEffect(load, [page, query, status, activeBranchId, branchingEnabled])
|
||||
|
||||
const hasFilters = query.length > 0 || status !== "All"
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter, useSearchParams } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { Plus, Trash2 } from "lucide-react"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
@@ -22,8 +23,10 @@ import { customersApi } from "@/lib/api/customers"
|
||||
import { itemsApi } from "@/lib/api/items"
|
||||
import { uomsApi } from "@/lib/api/uoms"
|
||||
import { warehousesApi } from "@/lib/api/warehouses"
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { Customer } from "@/types/customers"
|
||||
import { ItemListItem, Uom, Warehouse } from "@/types/master-data"
|
||||
import { Branch } from "@/types/hrm"
|
||||
import { CreateSalesInvoiceLineRequest, CreateSalesInvoiceRequest, SalesInvoiceType } from "@/types/sales"
|
||||
import { BundleSaleTemplateSummary } from "@/types/bundles"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
@@ -90,11 +93,14 @@ function NewSalesInvoiceContent() {
|
||||
const bundleTemplateIdFromQuery = searchParams.get("bundleTemplateId")
|
||||
const appliedBundleRef = useRef<string | null>(null)
|
||||
const bundleGroupSeqRef = useRef(0)
|
||||
const { isAdmin, branchId: myBranchId, activeBranchId, activeBranchName, branchingEnabled } = useAuth()
|
||||
|
||||
const [customers, setCustomers] = useState<Customer[]>([])
|
||||
const [items, setItems] = useState<ItemListItem[]>([])
|
||||
const [uoms, setUoms] = useState<Uom[]>([])
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[]>([])
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
const [branchFilterId, setBranchFilterId] = useState<number | null>(null)
|
||||
const [customerId, setCustomerId] = useState<number | null>(null)
|
||||
const [warehouseId, setWarehouseId] = useState<number | null>(null)
|
||||
const [invoiceType, setInvoiceType] = useState<SalesInvoiceType>("B2C")
|
||||
@@ -157,6 +163,32 @@ function NewSalesInvoiceContent() {
|
||||
setLines((prev) => prev.map((line) => ({ ...line, warehouseId: nextWarehouseId ?? line.warehouseId })))
|
||||
}
|
||||
|
||||
// Admin-only branch picker, narrowing which warehouses show up below — non-admins never
|
||||
// see this control, their warehouse choices are narrowed to their own branch directly.
|
||||
useEffect(() => {
|
||||
if (branchingEnabled && isAdmin) {
|
||||
branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}
|
||||
}, [branchingEnabled, isAdmin])
|
||||
|
||||
// An Admin on a specific header branch is pinned to it here too (shown, not editable) —
|
||||
// the dropdown only appears when the header itself is on "All Branches", so there's no
|
||||
// branch to pin to and the Admin must pick one explicitly for this sale.
|
||||
const lockedBranchId = isAdmin ? activeBranchId : myBranchId
|
||||
const showBranchPicker = branchingEnabled && isAdmin && !lockedBranchId
|
||||
const effectiveBranchId = branchingEnabled ? (isAdmin ? (activeBranchId ?? branchFilterId) : myBranchId) : null
|
||||
const visibleWarehouses = useMemo(
|
||||
() => (effectiveBranchId === null ? warehouses : warehouses.filter((w) => w.branchId === effectiveBranchId)),
|
||||
[warehouses, effectiveBranchId]
|
||||
)
|
||||
|
||||
// Switching branch can strip the currently-picked warehouse out of the visible set —
|
||||
// clear it (and every line's warehouse) rather than silently submitting a mismatched one.
|
||||
useEffect(() => {
|
||||
if (warehouseId !== null && !visibleWarehouses.some((w) => w.warehouseId === warehouseId)) updateHeaderWarehouse(null)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visibleWarehouses])
|
||||
|
||||
function freeIssueSchemeFor(itemId: number): FreeIssueScheme | null {
|
||||
return freeIssueSchemes.find((s) => s.itemId === itemId) ?? null
|
||||
}
|
||||
@@ -422,6 +454,30 @@ function NewSalesInvoiceContent() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{branchingEnabled && isAdmin && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">Branch</Label>
|
||||
{showBranchPicker ? (
|
||||
<Select value={branchFilterId ? String(branchFilterId) : ""} onValueChange={(v) => setBranchFilterId(v ? Number(v) : null)}>
|
||||
<SelectTrigger className="h-12! w-full text-base">
|
||||
<SelectValue placeholder="All branches" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{branches.map((b) => (
|
||||
<SelectItem key={b.branchId} value={String(b.branchId)} className="text-base">
|
||||
{b.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="flex h-12 items-center rounded-lg border border-input bg-muted px-3 text-base text-muted-foreground">
|
||||
{activeBranchName ?? `Branch #${lockedBranchId}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">Warehouse</Label>
|
||||
<Select value={warehouseId ? String(warehouseId) : ""} onValueChange={(v) => updateHeaderWarehouse(v ? Number(v) : null)}>
|
||||
@@ -429,7 +485,7 @@ function NewSalesInvoiceContent() {
|
||||
<SelectValue placeholder="Select warehouse" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{warehouses.map((w) => (
|
||||
{visibleWarehouses.map((w) => (
|
||||
<SelectItem key={w.warehouseId} value={String(w.warehouseId)} className="text-base">
|
||||
{w.code} — {w.name}
|
||||
</SelectItem>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CheckCircle2, ChevronLeft, ChevronRight, CircleDollarSign, Eye, Filter,
|
||||
|
||||
import { salesApi } from "@/lib/api/sales"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
@@ -28,6 +29,8 @@ const tabs: { value: SalesInvoiceStatus | "All"; label: string }[] = [
|
||||
]
|
||||
|
||||
export default function SalesInvoicesPage() {
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [rows, setRows] = useState<SalesInvoiceSummary[] | null>(null)
|
||||
const [pagination, setPagination] = useState<PaginationMeta | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -44,18 +47,23 @@ export default function SalesInvoicesPage() {
|
||||
return () => clearTimeout(timeout)
|
||||
}, [searchInput])
|
||||
|
||||
useEffect(() => setPage(1), [query, status])
|
||||
useEffect(() => setPage(1), [query, status, activeBranchId])
|
||||
|
||||
useEffect(() => {
|
||||
setError(null)
|
||||
salesApi
|
||||
.listInvoices({ page, pageSize: PAGE_SIZE, status: status === "All" ? undefined : status })
|
||||
.listInvoices({
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
status: status === "All" ? undefined : status,
|
||||
branchId: branchingEnabled ? (activeBranchId ?? undefined) : undefined,
|
||||
})
|
||||
.then((res) => {
|
||||
setRows(res.items)
|
||||
setPagination(res.pagination)
|
||||
})
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}, [page, status])
|
||||
}, [page, status, activeBranchId, branchingEnabled])
|
||||
|
||||
async function handleConfirm(row: SalesInvoiceSummary) {
|
||||
setConfirmingId(row.salesInvoiceId)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { MapPin } from "lucide-react"
|
||||
|
||||
import { branchConfigApi } from "@/lib/api/branch-config"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { BranchConfig } from "@/types/hrm"
|
||||
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
|
||||
/**
|
||||
* Branch Configuration — the singleton toggle for whether this deployment uses branches
|
||||
* at all. Off hides every branch picker/badge across the app (Employee, Warehouse, the
|
||||
* header switcher, Sales filtering); existing records keep whatever branch they already
|
||||
* have, and new ones are silently assigned to the deployment's one default branch.
|
||||
*/
|
||||
export default function BranchConfigurationPage() {
|
||||
const [config, setConfig] = useState<BranchConfig | null>(null)
|
||||
const [etag, setEtag] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
function load() {
|
||||
setError(null)
|
||||
branchConfigApi
|
||||
.get()
|
||||
.then((res) => {
|
||||
setConfig(res.data)
|
||||
setEtag(res.etag)
|
||||
})
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}
|
||||
|
||||
useEffect(load, [])
|
||||
|
||||
async function toggle(next: boolean) {
|
||||
if (!config) return
|
||||
setSaving(true)
|
||||
try {
|
||||
const res = await branchConfigApi.update({ branchingEnabled: next }, etag ?? "")
|
||||
setConfig(res.data)
|
||||
setEtag(res.etag)
|
||||
toast.success("Configuration saved", `Branches ${next ? "enabled" : "disabled"}.`)
|
||||
} catch (err) {
|
||||
toast.error("Could not save configuration", errorMessage(err))
|
||||
load()
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">Branch Configuration</h1>
|
||||
<p className="text-base text-muted-foreground">
|
||||
Switch multi-branch mode on or off for this deployment.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-5 text-base text-destructive">{error}</div>
|
||||
)}
|
||||
|
||||
{!error && !config && <Skeleton className="h-40 w-full" />}
|
||||
|
||||
{!error && config && (
|
||||
<div className="flex flex-col gap-4 rounded-xl border p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="size-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold text-foreground">Branches</h2>
|
||||
</div>
|
||||
<div className="border-t" />
|
||||
|
||||
<div className="flex items-start gap-4 py-2">
|
||||
<Switch
|
||||
checked={config.branchingEnabled}
|
||||
onCheckedChange={toggle}
|
||||
disabled={saving}
|
||||
aria-label="Require branches"
|
||||
className="mt-1 shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-base font-medium text-foreground">Require branches</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
On: Employee and Warehouse records must pick a branch, and Admins can switch between
|
||||
branches from the header. Off: branch pickers disappear everywhere and every record
|
||||
is kept on the deployment's one default branch behind the scenes.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.updatedAt && (
|
||||
<p className="pt-2 text-sm text-muted-foreground">
|
||||
Last changed {new Date(config.updatedAt).toLocaleString()}
|
||||
{config.updatedBy ? ` by user #${config.updatedBy}` : ""}.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { BranchMasterPage } from "@/components/settings/BranchMasterPage"
|
||||
|
||||
export default function BranchesPage() {
|
||||
return <BranchMasterPage />
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from "next/link"
|
||||
import { Building2, LayoutTemplate, SlidersHorizontal } from "lucide-react"
|
||||
import { Building2, LayoutTemplate, MapPin, SlidersHorizontal } from "lucide-react"
|
||||
|
||||
const cards = [
|
||||
{
|
||||
@@ -8,6 +8,12 @@ const cards = [
|
||||
icon: SlidersHorizontal,
|
||||
description: "Configure product and master-data options",
|
||||
},
|
||||
{
|
||||
title: "Branch Configuration",
|
||||
href: "/dashboard/settings/branch-configuration",
|
||||
icon: MapPin,
|
||||
description: "Turn multi-branch mode on or off for this deployment",
|
||||
},
|
||||
{
|
||||
title: "Invoice Template",
|
||||
href: "/dashboard/settings/invoice-template",
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import Link from "next/link"
|
||||
import { BellRing, ShieldCheck, SlidersHorizontal, Users } from "lucide-react"
|
||||
import { BellRing, MapPin, ShieldCheck, SlidersHorizontal, Users } from "lucide-react"
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: "Branches",
|
||||
href: "/dashboard/settings/branches",
|
||||
icon: MapPin,
|
||||
description: "Manage company branches/locations",
|
||||
},
|
||||
{
|
||||
title: "Roles",
|
||||
href: "/dashboard/settings/roles",
|
||||
|
||||
@@ -7,13 +7,15 @@ import { Link2, Pencil, Plus, Users as UsersIcon } from "lucide-react"
|
||||
import { rolesApi } from "@/lib/api/roles"
|
||||
import { usersApi } from "@/lib/api/users"
|
||||
import { employeeCrossLinkApi } from "@/lib/api/employees"
|
||||
import { branchesHrmApi, departmentsApi, designationsApi, employmentTypesApi, workShiftsApi } from "@/lib/api/hrm-masters"
|
||||
import { errorMessage, fieldErrors } from "@/lib/error-map"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { PaginationMeta } from "@/types/common"
|
||||
import { Role } from "@/types/rbac"
|
||||
import { EmployeeMatch } from "@/types/hrm"
|
||||
import { Branch, Department, Designation, EmployeeMatch, EmploymentType, WorkShift } from "@/types/hrm"
|
||||
import { ManagedUser, UserTypeOption } from "@/types/users"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -34,6 +36,13 @@ import { toast } from "@/components/ui/toast"
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
export default function UsersPage() {
|
||||
const { isAdmin, branchId: myBranchId, branchName: myBranchName, activeBranchId, activeBranchName, branchingEnabled } = useAuth()
|
||||
// Same "don't ask if it's already implied" heuristic as the Employees form: an Admin's
|
||||
// currently-selected header branch, or a non-admin's own branch.
|
||||
const lockedBranchId = isAdmin ? activeBranchId : myBranchId
|
||||
const lockedBranchName = isAdmin ? activeBranchName : myBranchName
|
||||
const showBranchPicker = branchingEnabled && isAdmin && !lockedBranchId
|
||||
|
||||
const [users, setUsers] = useState<ManagedUser[] | null>(null)
|
||||
const [pagination, setPagination] = useState<PaginationMeta | null>(null)
|
||||
const [roles, setRoles] = useState<Role[]>([])
|
||||
@@ -52,6 +61,25 @@ export default function UsersPage() {
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
// Non-admin roles must be scoped to one branch (see UserManagementService.CreateAsync) —
|
||||
// these fields build the Employee record that carries it.
|
||||
const selectedRole = roles.find((r) => String(r.roleId) === roleId)
|
||||
const requiresBranchScope = !!roleId && selectedRole?.code?.toUpperCase() !== "ADMIN"
|
||||
|
||||
const [employeeCode, setEmployeeCode] = useState("")
|
||||
const [hireDate, setHireDate] = useState(() => new Date().toISOString().slice(0, 10))
|
||||
const [departmentId, setDepartmentId] = useState(0)
|
||||
const [designationId, setDesignationId] = useState(0)
|
||||
const [employmentTypeId, setEmploymentTypeId] = useState(0)
|
||||
const [workShiftId, setWorkShiftId] = useState(0)
|
||||
const [branchId, setBranchId] = useState(0)
|
||||
|
||||
const [departments, setDepartments] = useState<Department[]>([])
|
||||
const [designations, setDesignations] = useState<Designation[]>([])
|
||||
const [employmentTypes, setEmploymentTypes] = useState<EmploymentType[]>([])
|
||||
const [workShifts, setWorkShifts] = useState<WorkShift[]>([])
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
|
||||
// Advisory cross-link suggestion: does a Staff record already exist with this email?
|
||||
const [staffMatch, setStaffMatch] = useState<EmployeeMatch | null>(null)
|
||||
const [checkingEmail, setCheckingEmail] = useState(false)
|
||||
@@ -98,6 +126,21 @@ export default function UsersPage() {
|
||||
.catch(() => setUserTypes([]))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
departmentsApi.list({ pageSize: 200, status: "Active" }).then((r) => setDepartments(r.items)).catch(() => setDepartments([]))
|
||||
designationsApi.list({ pageSize: 200, status: "Active" }).then((r) => setDesignations(r.items)).catch(() => setDesignations([]))
|
||||
employmentTypesApi.list({ pageSize: 200, status: "Active" }).then((r) => setEmploymentTypes(r.items)).catch(() => setEmploymentTypes([]))
|
||||
workShiftsApi.list({ pageSize: 200, status: "Active" }).then((r) => setWorkShifts(r.items)).catch(() => setWorkShifts([]))
|
||||
if (branchingEnabled) branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}, [branchingEnabled])
|
||||
|
||||
// Once a non-admin role is picked, default the branch to whatever's already implied
|
||||
// (an Admin's selected header branch, or a non-admin creator's own branch) instead of
|
||||
// making them pick it again.
|
||||
useEffect(() => {
|
||||
if (requiresBranchScope && lockedBranchId && !branchId) setBranchId(lockedBranchId)
|
||||
}, [requiresBranchScope, lockedBranchId, branchId])
|
||||
|
||||
function resetForm() {
|
||||
setUsername("")
|
||||
setFullName("")
|
||||
@@ -106,6 +149,13 @@ export default function UsersPage() {
|
||||
setNic("")
|
||||
setRoleId("")
|
||||
setUserTypeId("")
|
||||
setEmployeeCode("")
|
||||
setHireDate(new Date().toISOString().slice(0, 10))
|
||||
setDepartmentId(0)
|
||||
setDesignationId(0)
|
||||
setEmploymentTypeId(0)
|
||||
setWorkShiftId(0)
|
||||
setBranchId(0)
|
||||
setErrors({})
|
||||
setStaffMatch(null)
|
||||
}
|
||||
@@ -117,6 +167,17 @@ export default function UsersPage() {
|
||||
if (!email.trim()) nextErrors.email = "Email is required"
|
||||
if (!roleId) nextErrors.roleId = "Role is required"
|
||||
if (!userTypeId.trim()) nextErrors.userTypeId = "User type is required"
|
||||
// A matched existing Staff record already carries a branch — only ask for a fresh
|
||||
// one to be created when there's no such link.
|
||||
if (requiresBranchScope && !staffMatch) {
|
||||
if (!employeeCode.trim()) nextErrors.employeeCode = "Employee code is required"
|
||||
if (!hireDate) nextErrors.hireDate = "Hire date is required"
|
||||
if (!departmentId) nextErrors.departmentId = "Department is required"
|
||||
if (!designationId) nextErrors.designationId = "Designation is required"
|
||||
if (!employmentTypeId) nextErrors.employmentTypeId = "Employment type is required"
|
||||
if (!workShiftId) nextErrors.workShiftId = "Work shift is required"
|
||||
if (branchingEnabled && !branchId) nextErrors.branchId = "Branch is required"
|
||||
}
|
||||
setErrors(nextErrors)
|
||||
if (Object.keys(nextErrors).length > 0) return
|
||||
|
||||
@@ -131,6 +192,17 @@ export default function UsersPage() {
|
||||
roleId: Number(roleId),
|
||||
userTypeId: userTypeId.trim(),
|
||||
linkEmployeeId: staffMatch ? staffMatch.employeeId : null,
|
||||
...(requiresBranchScope && !staffMatch
|
||||
? {
|
||||
employeeCode: employeeCode.trim(),
|
||||
hireDate,
|
||||
departmentId,
|
||||
designationId,
|
||||
employmentTypeId,
|
||||
workShiftId,
|
||||
branchId: branchId || null,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
toast.success("User created", `Credentials have been emailed to ${result.username}.`)
|
||||
setOpen(false)
|
||||
@@ -164,7 +236,7 @@ export default function UsersPage() {
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader className="items-center text-center">
|
||||
<DialogTitle>New user</DialogTitle>
|
||||
<DialogDescription>Created in both the auth service and here; password is emailed.</DialogDescription>
|
||||
@@ -225,6 +297,73 @@ export default function UsersPage() {
|
||||
</Select>
|
||||
<FieldError errors={[errors.roleId ? { message: errors.roleId } : undefined]} />
|
||||
</Field>
|
||||
{requiresBranchScope && !staffMatch && (
|
||||
<>
|
||||
<div className="rounded-lg border border-primary/30 bg-primary/5 px-3 py-2 text-sm text-muted-foreground">
|
||||
This role isn't Admin — it must be scoped to one branch. These details create the linked staff record.
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<Field data-invalid={!!errors.employeeCode}>
|
||||
<FieldLabel htmlFor="u-empcode">Employee code</FieldLabel>
|
||||
<Input id="u-empcode" value={employeeCode} onChange={(e) => setEmployeeCode(e.target.value)} aria-invalid={!!errors.employeeCode} />
|
||||
<FieldError errors={[errors.employeeCode ? { message: errors.employeeCode } : undefined]} />
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.hireDate}>
|
||||
<FieldLabel htmlFor="u-hire">Hire date</FieldLabel>
|
||||
<Input id="u-hire" type="date" value={hireDate} onChange={(e) => setHireDate(e.target.value)} aria-invalid={!!errors.hireDate} />
|
||||
<FieldError errors={[errors.hireDate ? { message: errors.hireDate } : undefined]} />
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.departmentId}>
|
||||
<FieldLabel htmlFor="u-dept">Department</FieldLabel>
|
||||
<Select value={departmentId ? String(departmentId) : undefined} onValueChange={(v) => setDepartmentId(v ? Number(v) : 0)}>
|
||||
<SelectTrigger id="u-dept" aria-invalid={!!errors.departmentId}><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{departments.map((d) => <SelectItem key={d.departmentId} value={String(d.departmentId)}>{d.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[errors.departmentId ? { message: errors.departmentId } : undefined]} />
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.designationId}>
|
||||
<FieldLabel htmlFor="u-desig">Designation</FieldLabel>
|
||||
<Select value={designationId ? String(designationId) : undefined} onValueChange={(v) => setDesignationId(v ? Number(v) : 0)}>
|
||||
<SelectTrigger id="u-desig" aria-invalid={!!errors.designationId}><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{designations.map((d) => <SelectItem key={d.designationId} value={String(d.designationId)}>{d.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[errors.designationId ? { message: errors.designationId } : undefined]} />
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.employmentTypeId}>
|
||||
<FieldLabel htmlFor="u-etype">Employment type</FieldLabel>
|
||||
<Select value={employmentTypeId ? String(employmentTypeId) : undefined} onValueChange={(v) => setEmploymentTypeId(v ? Number(v) : 0)}>
|
||||
<SelectTrigger id="u-etype" aria-invalid={!!errors.employmentTypeId}><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{employmentTypes.map((t) => <SelectItem key={t.employmentTypeId} value={String(t.employmentTypeId)}>{t.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[errors.employmentTypeId ? { message: errors.employmentTypeId } : undefined]} />
|
||||
</Field>
|
||||
<Field data-invalid={!!errors.workShiftId}>
|
||||
<FieldLabel htmlFor="u-shift">Work shift</FieldLabel>
|
||||
<Select value={workShiftId ? String(workShiftId) : undefined} onValueChange={(v) => setWorkShiftId(v ? Number(v) : 0)}>
|
||||
<SelectTrigger id="u-shift" aria-invalid={!!errors.workShiftId}><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{workShifts.map((w) => <SelectItem key={w.workShiftId} value={String(w.workShiftId)}>{w.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[errors.workShiftId ? { message: errors.workShiftId } : undefined]} />
|
||||
</Field>
|
||||
{branchingEnabled && (
|
||||
<Field data-invalid={!!errors.branchId}>
|
||||
<FieldLabel htmlFor="u-branch">Branch</FieldLabel>
|
||||
{showBranchPicker ? (
|
||||
<Select value={branchId ? String(branchId) : undefined} onValueChange={(v) => setBranchId(v ? Number(v) : 0)}>
|
||||
<SelectTrigger id="u-branch" aria-invalid={!!errors.branchId}><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{branches.map((b) => <SelectItem key={b.branchId} value={String(b.branchId)}>{b.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div id="u-branch" className="flex h-9 items-center rounded-lg border border-input bg-muted px-3 text-sm text-muted-foreground">
|
||||
{lockedBranchName ?? "No branch assigned — contact an admin"}
|
||||
</div>
|
||||
)}
|
||||
<FieldError errors={[errors.branchId ? { message: errors.branchId } : undefined]} />
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Field data-invalid={!!errors.userTypeId}>
|
||||
<FieldLabel htmlFor="u-usertype">User type</FieldLabel>
|
||||
<Select value={userTypeId || undefined} onValueChange={(v) => setUserTypeId(v ?? "")}>
|
||||
|
||||
@@ -12,26 +12,33 @@ import { cn } from "@/lib/utils"
|
||||
import { StockAdjustmentSummary } from "@/types/stock"
|
||||
import { Warehouse } from "@/types/master-data"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { AdjustmentStatusBadge } from "@/components/stock/status-badges"
|
||||
|
||||
export default function AdjustmentsListPage() {
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [adjustments, setAdjustments] = useState<StockAdjustmentSummary[] | null>(null)
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[] | null>(null)
|
||||
const [reasonLabels, setReasonLabels] = useState<Map<number, string>>(new Map())
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([stockAdjustmentsApi.list({ pageSize: 50 }), warehousesApi.list(), reasonCodesApi.list("Adjustment")])
|
||||
Promise.all([
|
||||
stockAdjustmentsApi.list({ pageSize: 50, branchId: branchingEnabled ? (activeBranchId ?? undefined) : undefined }),
|
||||
warehousesApi.list(),
|
||||
reasonCodesApi.list("Adjustment"),
|
||||
])
|
||||
.then(([a, wh, rc]) => {
|
||||
setAdjustments(a.items)
|
||||
setWarehouses(wh.items)
|
||||
setReasonLabels(new Map(rc.items.map((r) => [r.reasonCodeId, r.description])))
|
||||
})
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}, [])
|
||||
}, [activeBranchId, branchingEnabled])
|
||||
|
||||
const whCode = (id: number) => warehouses?.find((w) => w.warehouseId === id)?.code ?? `#${id}`
|
||||
|
||||
|
||||
@@ -11,24 +11,30 @@ import { cn } from "@/lib/utils"
|
||||
import { StockCountSummary } from "@/types/stock"
|
||||
import { Warehouse } from "@/types/master-data"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { CountStatusBadge } from "@/components/stock/status-badges"
|
||||
|
||||
export default function CountsListPage() {
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [counts, setCounts] = useState<StockCountSummary[] | null>(null)
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[] | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([stockCountsApi.list({ pageSize: 50 }), warehousesApi.list()])
|
||||
Promise.all([
|
||||
stockCountsApi.list({ pageSize: 50, branchId: branchingEnabled ? (activeBranchId ?? undefined) : undefined }),
|
||||
warehousesApi.list(),
|
||||
])
|
||||
.then(([c, wh]) => {
|
||||
setCounts(c.items)
|
||||
setWarehouses(wh.items)
|
||||
})
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}, [])
|
||||
}, [activeBranchId, branchingEnabled])
|
||||
|
||||
const whCode = (id: number) => warehouses?.find((w) => w.warehouseId === id)?.code ?? `#${id}`
|
||||
|
||||
|
||||
@@ -11,12 +11,15 @@ import { errorMessage } from "@/lib/error-map"
|
||||
import { OnHand } from "@/types/stock"
|
||||
import { ItemListItem, Warehouse } from "@/types/master-data"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
|
||||
export default function StockEnquiryPage() {
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [rows, setRows] = useState<OnHand[] | null>(null)
|
||||
const [items, setItems] = useState<ItemListItem[] | null>(null)
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[] | null>(null)
|
||||
@@ -43,6 +46,7 @@ export default function StockEnquiryPage() {
|
||||
const term = search.trim().toLowerCase()
|
||||
return rows
|
||||
.filter((r) => warehouseId === "All" || r.warehouseId === warehouseId)
|
||||
.filter((r) => !branchingEnabled || activeBranchId === null || warehousesById.get(r.warehouseId)?.branchId === activeBranchId)
|
||||
.filter((r) => {
|
||||
if (!term) return true
|
||||
const item = itemsById.get(r.itemId)
|
||||
@@ -50,7 +54,7 @@ export default function StockEnquiryPage() {
|
||||
return haystack.includes(term)
|
||||
})
|
||||
.sort((a, b) => a.itemId - b.itemId || a.warehouseId - b.warehouseId)
|
||||
}, [rows, search, warehouseId, itemsById])
|
||||
}, [rows, search, warehouseId, itemsById, warehousesById, activeBranchId, branchingEnabled])
|
||||
|
||||
const loading = rows === null || items === null || warehouses === null
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { LedgerEntry } from "@/types/stock"
|
||||
import { PaginationMeta } from "@/types/common"
|
||||
import { ItemListItem, Warehouse } from "@/types/master-data"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
@@ -23,6 +24,8 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
export default function StockLedgerPage() {
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [entries, setEntries] = useState<LedgerEntry[] | null>(null)
|
||||
const [pagination, setPagination] = useState<PaginationMeta | null>(null)
|
||||
const [items, setItems] = useState<ItemListItem[] | null>(null)
|
||||
@@ -46,7 +49,7 @@ export default function StockLedgerPage() {
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
}, [itemId, warehouseId, from, to])
|
||||
}, [itemId, warehouseId, from, to, activeBranchId])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -54,6 +57,7 @@ export default function StockLedgerPage() {
|
||||
.ledger({
|
||||
itemId: itemId === "All" ? undefined : itemId,
|
||||
warehouseId: warehouseId === "All" ? undefined : warehouseId,
|
||||
branchId: branchingEnabled ? (activeBranchId ?? undefined) : undefined,
|
||||
from: from || undefined,
|
||||
to: to || undefined,
|
||||
page,
|
||||
@@ -70,7 +74,7 @@ export default function StockLedgerPage() {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [itemId, warehouseId, from, to, page])
|
||||
}, [itemId, warehouseId, from, to, page, activeBranchId, branchingEnabled])
|
||||
|
||||
const itemsById = useMemo(() => new Map((items ?? []).map((i) => [i.itemId, i])), [items])
|
||||
const warehousesById = useMemo(() => new Map((warehouses ?? []).map((w) => [w.warehouseId, w])), [warehouses])
|
||||
|
||||
@@ -10,12 +10,15 @@ import { errorMessage } from "@/lib/error-map"
|
||||
import { ReorderAlert } from "@/types/stock"
|
||||
import { ItemListItem, Warehouse } from "@/types/master-data"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
|
||||
export default function ReorderAlertsPage() {
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [alerts, setAlerts] = useState<ReorderAlert[] | null>(null)
|
||||
const [items, setItems] = useState<ItemListItem[] | null>(null)
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[] | null>(null)
|
||||
@@ -24,14 +27,18 @@ export default function ReorderAlertsPage() {
|
||||
const [requested, setRequested] = useState<Set<string>>(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([stockApi.reorderAlerts(), itemsApi.list({ pageSize: 200 }), warehousesApi.list()])
|
||||
Promise.all([
|
||||
stockApi.reorderAlerts(undefined, { branchId: branchingEnabled ? (activeBranchId ?? undefined) : undefined }),
|
||||
itemsApi.list({ pageSize: 200 }),
|
||||
warehousesApi.list(),
|
||||
])
|
||||
.then(([a, it, wh]) => {
|
||||
setAlerts(a.items)
|
||||
setItems(it.items)
|
||||
setWarehouses(wh.items)
|
||||
})
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}, [])
|
||||
}, [activeBranchId, branchingEnabled])
|
||||
|
||||
const itemsById = useMemo(() => new Map((items ?? []).map((i) => [i.itemId, i])), [items])
|
||||
const warehousesById = useMemo(() => new Map((warehouses ?? []).map((w) => [w.warehouseId, w])), [warehouses])
|
||||
|
||||
@@ -11,24 +11,30 @@ import { cn } from "@/lib/utils"
|
||||
import { StockTransferSummary } from "@/types/stock"
|
||||
import { Warehouse } from "@/types/master-data"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { TransferStatusBadge } from "@/components/stock/status-badges"
|
||||
|
||||
export default function TransfersListPage() {
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [transfers, setTransfers] = useState<StockTransferSummary[] | null>(null)
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[] | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([stockTransfersApi.list({ pageSize: 50 }), warehousesApi.list()])
|
||||
Promise.all([
|
||||
stockTransfersApi.list({ pageSize: 50, branchId: branchingEnabled ? (activeBranchId ?? undefined) : undefined }),
|
||||
warehousesApi.list(),
|
||||
])
|
||||
.then(([t, wh]) => {
|
||||
setTransfers(t.items)
|
||||
setWarehouses(wh.items)
|
||||
})
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}, [])
|
||||
}, [activeBranchId, branchingEnabled])
|
||||
|
||||
const whCode = (id: number) => warehouses?.find((w) => w.warehouseId === id)?.code ?? `#${id}`
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { errorMessage } from "@/lib/error-map"
|
||||
import { Valuation } from "@/types/stock"
|
||||
import { ItemListItem, Warehouse } from "@/types/master-data"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
@@ -19,6 +20,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
function ValuationContent() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [items, setItems] = useState<ItemListItem[] | null>(null)
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[] | null>(null)
|
||||
@@ -50,6 +52,10 @@ function ValuationContent() {
|
||||
}, [itemId, warehouseId])
|
||||
|
||||
const item = useMemo(() => (items ?? []).find((i) => i.itemId === itemId), [items, itemId])
|
||||
const visibleWarehouses = useMemo(
|
||||
() => (branchingEnabled && activeBranchId !== null ? (warehouses ?? []).filter((w) => w.branchId === activeBranchId) : (warehouses ?? [])),
|
||||
[warehouses, activeBranchId, branchingEnabled]
|
||||
)
|
||||
|
||||
function setParam(key: "itemId" | "warehouseId", value: number) {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
@@ -98,7 +104,7 @@ function ValuationContent() {
|
||||
<SelectValue placeholder="Select a warehouse" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(warehouses ?? []).map((w) => (
|
||||
{visibleWarehouses.map((w) => (
|
||||
<SelectItem key={w.warehouseId} value={w.warehouseId} className="text-base">
|
||||
{w.code} — {w.name}
|
||||
</SelectItem>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { cn } from "@/lib/utils"
|
||||
import { ReasonCode } from "@/types/stock"
|
||||
import { ItemListItem, Warehouse } from "@/types/master-data"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -21,6 +22,8 @@ import { Label } from "@/components/ui/label"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
|
||||
export default function WastagePage() {
|
||||
const { activeBranchId, branchingEnabled } = useAuth()
|
||||
|
||||
const [records, setRecords] = useState<WastageRecord[] | null>(null)
|
||||
const [reasonCodes, setReasonCodes] = useState<ReasonCode[] | null>(null)
|
||||
const [items, setItems] = useState<ItemListItem[] | null>(null)
|
||||
@@ -44,11 +47,12 @@ export default function WastagePage() {
|
||||
wastageApi
|
||||
.list({
|
||||
warehouseId: warehouseId === "All" ? undefined : warehouseId,
|
||||
branchId: branchingEnabled ? (activeBranchId ?? undefined) : undefined,
|
||||
reasonCodeId: reasonCodeId === "All" ? undefined : reasonCodeId,
|
||||
})
|
||||
.then(setRecords)
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}, [warehouseId, reasonCodeId])
|
||||
}, [warehouseId, reasonCodeId, activeBranchId, branchingEnabled])
|
||||
|
||||
const itemsById = useMemo(() => new Map((items ?? []).map((i) => [i.itemId, i])), [items])
|
||||
const warehousesById = useMemo(() => new Map((warehouses ?? []).map((w) => [w.warehouseId, w])), [warehouses])
|
||||
|
||||
@@ -90,7 +90,10 @@ export default function WarehouseDetailPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">{warehouse.code}</h1>
|
||||
<p className="text-base text-muted-foreground">{warehouse.name} · Bin/location structure (FR-WH-01, FR-MD-07)</p>
|
||||
<p className="text-base text-muted-foreground">
|
||||
{warehouse.name}
|
||||
{warehouse.branchName && ` · ${warehouse.branchName}`} · Bin/location structure (FR-WH-01, FR-MD-07)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,9 +5,12 @@ import Link from "next/link"
|
||||
import { Plus, Warehouse as WarehouseIcon } from "lucide-react"
|
||||
|
||||
import { warehousesApi } from "@/lib/api/warehouses"
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { Bin, Warehouse } from "@/types/master-data"
|
||||
import { Branch } from "@/types/hrm"
|
||||
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
|
||||
@@ -19,6 +22,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
@@ -42,12 +46,16 @@ function generateWarehouseCode(name: string, existingCodes: string[]): string {
|
||||
}
|
||||
|
||||
export default function WarehousesPage() {
|
||||
const { branchingEnabled } = useAuth()
|
||||
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[] | null>(null)
|
||||
const [bins, setBins] = useState<Bin[] | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [name, setName] = useState("")
|
||||
const [branchId, setBranchId] = useState(0)
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
@@ -65,6 +73,9 @@ export default function WarehousesPage() {
|
||||
}
|
||||
|
||||
useEffect(load, [])
|
||||
useEffect(() => {
|
||||
if (branchingEnabled) branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}, [branchingEnabled])
|
||||
|
||||
function binCount(warehouseId: number) {
|
||||
return (bins ?? []).filter((b) => b.warehouseId === warehouseId).length
|
||||
@@ -73,15 +84,17 @@ export default function WarehousesPage() {
|
||||
async function handleCreate() {
|
||||
const nextErrors: Record<string, string> = {}
|
||||
if (!name.trim()) nextErrors.name = "Warehouse name is required"
|
||||
if (branchingEnabled && !branchId) nextErrors.branchId = "Branch is required"
|
||||
setErrors(nextErrors)
|
||||
if (Object.keys(nextErrors).length > 0) return
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const warehouse = await warehousesApi.create({ code: generatedCode, name })
|
||||
const warehouse = await warehousesApi.create({ code: generatedCode, name, branchId })
|
||||
toast.success("Warehouse created", `${warehouse.code} — ${warehouse.name}`)
|
||||
setOpen(false)
|
||||
setName("")
|
||||
setBranchId(0)
|
||||
setErrors({})
|
||||
load()
|
||||
} catch (err) {
|
||||
@@ -129,6 +142,16 @@ export default function WarehousesPage() {
|
||||
<FieldLabel htmlFor="wh-code">Code (auto-generated)</FieldLabel>
|
||||
<Input id="wh-code" value={generatedCode} readOnly disabled placeholder="Enter a name to generate a code" className="text-muted-foreground" />
|
||||
</Field>
|
||||
{branchingEnabled && (
|
||||
<Field data-invalid={!!errors.branchId}>
|
||||
<FieldLabel htmlFor="wh-branch">Branch</FieldLabel>
|
||||
<Select value={String(branchId)} onValueChange={(v) => setBranchId(v ? Number(v) : 0)}>
|
||||
<SelectTrigger id="wh-branch" aria-invalid={!!errors.branchId}><SelectValue placeholder="Select" /></SelectTrigger>
|
||||
<SelectContent>{branches.map((b) => <SelectItem key={b.branchId} value={String(b.branchId)}>{b.name}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[errors.branchId ? { message: errors.branchId } : undefined]} />
|
||||
</Field>
|
||||
)}
|
||||
</FieldGroup>
|
||||
<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}>
|
||||
@@ -167,6 +190,7 @@ export default function WarehousesPage() {
|
||||
<TableRow>
|
||||
<TableHead className="h-12 px-3 text-sm">Code</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Name</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Branch</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Bins</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -179,6 +203,7 @@ export default function WarehousesPage() {
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{w.name}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{w.branchName ?? "—"}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{binCount(w.warehouseId)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
LineChart,
|
||||
ListTree,
|
||||
Lock,
|
||||
MapPin,
|
||||
Menu,
|
||||
Package,
|
||||
PackageCheck,
|
||||
@@ -212,6 +213,7 @@ const navItems: {
|
||||
icon: Settings,
|
||||
chevron: true,
|
||||
children: [
|
||||
{ title: "Branches", code: "settings.branches", href: "/dashboard/settings/branches", icon: MapPin },
|
||||
{ title: "Roles", code: "settings.roles", href: "/dashboard/settings/roles", icon: ShieldCheck },
|
||||
{ title: "Users", code: "settings.users", href: "/dashboard/settings/users", icon: Users },
|
||||
{ title: "Configuration", code: "products.configuration", href: "/dashboard/settings/configuration", icon: SlidersHorizontal },
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { ArrowLeft, Bell, LogOut, Settings, User } from "lucide-react"
|
||||
import { ArrowLeft, Bell, Building2, LogOut, Settings, User } from "lucide-react"
|
||||
|
||||
import { authApi } from "@/lib/api/auth"
|
||||
import { clearStoredUser, displayName, getStoredUser } from "@/lib/auth-session"
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { AuthUser } from "@/types/auth"
|
||||
import { Branch } from "@/types/hrm"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useAuth } from "@/components/auth/AuthProvider"
|
||||
import { ThemeToggle } from "@/components/theme-toggle"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -20,6 +23,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
|
||||
/** Avatar initials from a display name: "ERP Admin" -> "EA", "erpadmin" -> "ER". */
|
||||
function initials(name: string): string {
|
||||
@@ -166,9 +170,27 @@ export function Header() {
|
||||
const router = useRouter()
|
||||
const title = titleFromPath(pathname)
|
||||
const showBackButton = pathname !== "/dashboard"
|
||||
const { isAdmin, branchName, activeBranchId, activeBranchName, setActiveBranch, branchingEnabled } = useAuth()
|
||||
const branchLabel = isAdmin ? (activeBranchName ?? "All Branches") : branchName
|
||||
const [notifications, setNotifications] = useState(initialNotifications)
|
||||
const unreadCount = notifications.filter((n) => n.unread).length
|
||||
|
||||
const [branches, setBranches] = useState<Branch[]>([])
|
||||
useEffect(() => {
|
||||
if (!isAdmin || !branchingEnabled) return
|
||||
branchesHrmApi.list({ pageSize: 200, status: "Active" }).then((r) => setBranches(r.items)).catch(() => setBranches([]))
|
||||
}, [isAdmin, branchingEnabled])
|
||||
|
||||
const ALL_BRANCHES = "__all__"
|
||||
function handleBranchChange(value: string | null) {
|
||||
if (!value || value === ALL_BRANCHES) {
|
||||
setActiveBranch(null, null)
|
||||
return
|
||||
}
|
||||
const branch = branches.find((b) => String(b.branchId) === value)
|
||||
setActiveBranch(branch?.branchId ?? null, branch?.name ?? null)
|
||||
}
|
||||
|
||||
// Read after mount so the first client render matches the server render.
|
||||
const [user, setUser] = useState<AuthUser | null>(null)
|
||||
|
||||
@@ -213,6 +235,36 @@ export function Header() {
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-1.5 sm:gap-3">
|
||||
{branchingEnabled && (isAdmin ? (
|
||||
<Select value={activeBranchId ? String(activeBranchId) : ALL_BRANCHES} onValueChange={handleBranchChange}>
|
||||
<SelectTrigger
|
||||
className="hidden h-8! shrink-0 gap-1.5 rounded-full border-transparent bg-muted px-2.5 text-xs font-medium text-muted-foreground hover:bg-muted/70 sm:flex"
|
||||
aria-label="Switch branch"
|
||||
>
|
||||
<Building2 className="size-3.5" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_BRANCHES}>All Branches</SelectItem>
|
||||
{branches.map((b) => (
|
||||
<SelectItem key={b.branchId} value={String(b.branchId)}>
|
||||
{b.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
branchLabel && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="hidden shrink-0 items-center gap-1.5 border-transparent bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground sm:flex"
|
||||
>
|
||||
<Building2 className="size-3.5" />
|
||||
{branchLabel}
|
||||
</Badge>
|
||||
)
|
||||
))}
|
||||
|
||||
<ThemeToggle />
|
||||
|
||||
<Popover>
|
||||
@@ -298,6 +350,12 @@ export function Header() {
|
||||
{user ? displayName(user) : "Signed in"}
|
||||
</p>
|
||||
{user?.email && <p className="text-sm font-normal text-muted-foreground">{user.email}</p>}
|
||||
{branchLabel && (
|
||||
<p className="mt-1 flex items-center gap-1.5 text-sm font-normal text-muted-foreground">
|
||||
<Building2 className="size-3.5" />
|
||||
{branchLabel}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
|
||||
@@ -9,10 +9,53 @@ import { createContext, useContext, useEffect, useState } from "react"
|
||||
import { authApi } from "@/lib/api/auth"
|
||||
import { MeResponse } from "@/types/rbac"
|
||||
|
||||
interface ActiveBranch {
|
||||
id: number | null
|
||||
name: string | null
|
||||
}
|
||||
|
||||
/** Admin's last-picked header branch, kept per-browser (docs/BRANCH_ISOLATION_PLAN.md
|
||||
* §Frontend impact: "a current branch selector... persisted per session"). Purely a
|
||||
* display/filter convenience — never trusted server-side. */
|
||||
const ACTIVE_BRANCH_KEY = "erpcore.activeBranch"
|
||||
|
||||
function readStoredActiveBranch(): ActiveBranch | null {
|
||||
if (typeof window === "undefined") return null
|
||||
const raw = window.localStorage.getItem(ACTIVE_BRANCH_KEY)
|
||||
if (!raw) return null
|
||||
try {
|
||||
return JSON.parse(raw) as ActiveBranch
|
||||
} catch {
|
||||
window.localStorage.removeItem(ACTIVE_BRANCH_KEY)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthContextValue {
|
||||
roleCode: string | null
|
||||
roleName: string | null
|
||||
navCodes: string[]
|
||||
/** True for the Admin role — the only role that may pick any branch (others are locked to their own). */
|
||||
isAdmin: boolean
|
||||
/** The caller's own branch, via their linked Employee record — null if unlinked. */
|
||||
branchId: number | null
|
||||
branchName: string | null
|
||||
/**
|
||||
* The branch currently being "worked in": for non-admins this always mirrors their own
|
||||
* branch; for an Admin it's whichever branch they last picked in the header (null = "All
|
||||
* Branches"), persisted per-browser. Screens that want to filter by "the branch the user
|
||||
* is currently viewing" should read this, not `branchId`.
|
||||
*/
|
||||
activeBranchId: number | null
|
||||
activeBranchName: string | null
|
||||
/** Admin-only — switches the header's active branch. A no-op for any other role. */
|
||||
setActiveBranch: (id: number | null, name: string | null) => void
|
||||
/**
|
||||
* The deployment-wide toggle (Settings → Configuration → Branch Configuration). False
|
||||
* means this deployment doesn't use branches at all — every branch picker/badge in the
|
||||
* app should be hidden entirely, not merely restricted by role.
|
||||
*/
|
||||
branchingEnabled: boolean
|
||||
/** True until the first `/auth/me` response lands. */
|
||||
loading: boolean
|
||||
}
|
||||
@@ -21,11 +64,19 @@ const AuthContext = createContext<AuthContextValue>({
|
||||
roleCode: null,
|
||||
roleName: null,
|
||||
navCodes: [],
|
||||
isAdmin: false,
|
||||
branchId: null,
|
||||
branchName: null,
|
||||
activeBranchId: null,
|
||||
activeBranchName: null,
|
||||
setActiveBranch: () => {},
|
||||
branchingEnabled: true,
|
||||
loading: true,
|
||||
})
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [me, setMe] = useState<MeResponse | null>(null)
|
||||
const [activeBranch, setActiveBranchState] = useState<ActiveBranch | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -37,19 +88,39 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
.catch(() => {
|
||||
// Unauthenticated/unreachable — fall back to "no permissions" rather than
|
||||
// crash the shell; proxy.ts already redirects unauthenticated users to /login.
|
||||
if (!cancelled) setMe({ roleCode: null, roleName: null, navCodes: [] })
|
||||
if (!cancelled) setMe({ roleCode: null, roleName: null, navCodes: [], isAdmin: false, branchId: null, branchName: null, branchingEnabled: true })
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (me === null) return
|
||||
// Non-admins are always pinned to their own branch — never their own past picks.
|
||||
setActiveBranchState(me.isAdmin ? (readStoredActiveBranch() ?? { id: null, name: null }) : { id: me.branchId, name: me.branchName })
|
||||
}, [me])
|
||||
|
||||
function setActiveBranch(id: number | null, name: string | null) {
|
||||
if (!me?.isAdmin) return
|
||||
const next = { id, name }
|
||||
setActiveBranchState(next)
|
||||
if (typeof window !== "undefined") window.localStorage.setItem(ACTIVE_BRANCH_KEY, JSON.stringify(next))
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
roleCode: me?.roleCode ?? null,
|
||||
roleName: me?.roleName ?? null,
|
||||
navCodes: me?.navCodes ?? [],
|
||||
isAdmin: me?.isAdmin ?? false,
|
||||
branchId: me?.branchId ?? null,
|
||||
branchName: me?.branchName ?? null,
|
||||
activeBranchId: activeBranch?.id ?? null,
|
||||
activeBranchName: activeBranch?.name ?? null,
|
||||
setActiveBranch,
|
||||
branchingEnabled: me?.branchingEnabled ?? true,
|
||||
loading: me === null,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
"use client"
|
||||
|
||||
// Dedicated Branch list/create/deactivate screen — Branch diverges from the plain
|
||||
// "code + name" HRM masters (CodeNameMasterPage): its code is server-generated
|
||||
// (never entered), and it carries Address + Phone Number too. Shared by both
|
||||
// entry points (Settings → Branches and HRM → Settings → Branches) since Branch
|
||||
// is a cross-cutting concept now, not HRM-specific.
|
||||
import { useEffect, useState } from "react"
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Branch } from "@/types/hrm"
|
||||
import { EntityStatus, PaginationMeta } from "@/types/common"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
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 { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
|
||||
export function BranchMasterPage() {
|
||||
const PAGE_SIZE = 10
|
||||
const [items, setItems] = useState<Branch[] | null>(null)
|
||||
const [pagination, setPagination] = useState<PaginationMeta | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [name, setName] = useState("")
|
||||
const [address, setAddress] = useState("")
|
||||
const [phoneNumber, setPhoneNumber] = useState("")
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
function load() {
|
||||
branchesHrmApi
|
||||
.list({ page, pageSize: PAGE_SIZE })
|
||||
.then((res) => {
|
||||
setItems(res.items)
|
||||
setPagination(res.pagination)
|
||||
})
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}
|
||||
|
||||
useEffect(load, [page])
|
||||
|
||||
function resetForm() {
|
||||
setName("")
|
||||
setAddress("")
|
||||
setPhoneNumber("")
|
||||
setErrors({})
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
const nextErrors: Record<string, string> = {}
|
||||
if (!name.trim()) nextErrors.name = "Name is required"
|
||||
setErrors(nextErrors)
|
||||
if (Object.keys(nextErrors).length > 0) return
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await branchesHrmApi.create({
|
||||
name: name.trim(),
|
||||
address: address.trim() || null,
|
||||
phoneNumber: phoneNumber.trim() || null,
|
||||
})
|
||||
toast.success("Branch created")
|
||||
setOpen(false)
|
||||
resetForm()
|
||||
load()
|
||||
} catch (err) {
|
||||
toast.error("Could not create branch", errorMessage(err))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleStatus(item: Branch) {
|
||||
const next: EntityStatus = item.status === "Active" ? "Inactive" : "Active"
|
||||
try {
|
||||
await branchesHrmApi.updateStatus(item.branchId, next)
|
||||
load()
|
||||
} catch (err) {
|
||||
toast.error("Could not update status", errorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">Branches</h1>
|
||||
<p className="text-base text-muted-foreground">Company branches/locations — used for multi-branch scoping.</p>
|
||||
</div>
|
||||
<Dialog open={open} onOpenChange={(next) => { setOpen(next); if (!next) resetForm() }}>
|
||||
<DialogTrigger render={<Button size="lg"><Plus className="size-5" />New</Button>} />
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader className="items-center text-center">
|
||||
<DialogTitle>New Branch</DialogTitle>
|
||||
<DialogDescription>Code is generated automatically. Deactivate later — branches are never deleted.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<Field data-invalid={!!errors.name}>
|
||||
<FieldLabel htmlFor="branch-name">Name</FieldLabel>
|
||||
<Input id="branch-name" value={name} onChange={(e) => setName(e.target.value)} aria-invalid={!!errors.name} />
|
||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="branch-address">Address</FieldLabel>
|
||||
<Input id="branch-address" value={address} onChange={(e) => setAddress(e.target.value)} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="branch-phone">Phone Number</FieldLabel>
|
||||
<Input id="branch-phone" value={phoneNumber} onChange={(e) => setPhoneNumber(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 ? "Creating…" : "Create"}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{error && <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-5 text-base text-destructive">{error}</div>}
|
||||
|
||||
{!error && items === null && (
|
||||
<div className="flex flex-col gap-3">{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-14 w-full" />)}</div>
|
||||
)}
|
||||
|
||||
{!error && items !== null && items.length === 0 && (
|
||||
<div className="flex flex-col items-center gap-4 rounded-xl border border-dashed py-20 text-center">
|
||||
<p className="text-base text-muted-foreground">No branches yet.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && items !== null && items.length > 0 && (
|
||||
<Table className="text-base">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-12 px-3 text-sm">Code</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Name</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Address</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Phone Number</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Status</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow key={item.branchId}>
|
||||
<TableCell className="px-3 py-3.5 font-medium">{item.code}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{item.name}</TableCell>
|
||||
<TableCell className="px-3 py-3.5 text-muted-foreground">{item.address ?? "—"}</TableCell>
|
||||
<TableCell className="px-3 py-3.5 text-muted-foreground">{item.phoneNumber ?? "—"}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">
|
||||
<Badge variant="outline" className={cn("h-6 w-fit justify-center border-transparent px-2.5 text-sm", item.status === "Active" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground")}>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="px-3 py-3.5">
|
||||
<Button variant="ghost" size="sm" onClick={() => toggleStatus(item)}>
|
||||
{item.status === "Active" ? "Deactivate" : "Activate"}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
{pagination && pagination.totalPages > 1 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing {(pagination.page - 1) * pagination.pageSize + 1}–{Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} of {pagination.totalItems}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" variant="outline" size="sm" disabled={pagination.page <= 1} onClick={() => setPage((p) => Math.max(1, p - 1))}>Previous</Button>
|
||||
<span className="text-sm text-muted-foreground">Page {pagination.page} of {pagination.totalPages}</span>
|
||||
<Button type="button" variant="outline" size="sm" disabled={pagination.page >= pagination.totalPages} onClick={() => setPage((p) => Math.min(pagination.totalPages, p + 1))}>Next</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Branch configuration. Singleton - no id. When `branchingEnabled` is off, the
|
||||
// Employee/Warehouse create forms stop asking for a branch and every new record is
|
||||
// silently assigned to the deployment's one default branch server-side instead.
|
||||
import { apiRequestWithETag } from "@/lib/api-client"
|
||||
import { ApiResult } from "@/types/common"
|
||||
import { BranchConfig, UpdateBranchConfigRequest } from "@/types/hrm"
|
||||
|
||||
export const branchConfigApi = {
|
||||
get(): Promise<ApiResult<BranchConfig>> {
|
||||
return apiRequestWithETag<BranchConfig>("/branch-config")
|
||||
},
|
||||
|
||||
update(request: UpdateBranchConfigRequest, ifMatch: string): Promise<ApiResult<BranchConfig>> {
|
||||
return apiRequestWithETag<BranchConfig>("/branch-config", { method: "PUT", body: request, ifMatch })
|
||||
},
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
// Dashboard overview endpoint (Backend/ERPCore/Controllers/DashboardController.cs).
|
||||
import { apiRequest } from "@/lib/api-client"
|
||||
import { apiRequest, buildQuery } from "@/lib/api-client"
|
||||
import { DashboardStats } from "@/types/dashboard"
|
||||
|
||||
export const dashboardApi = {
|
||||
stats(): Promise<DashboardStats> {
|
||||
return apiRequest<DashboardStats>("/dashboard/stats")
|
||||
stats(params: { branchId?: number } = {}): Promise<DashboardStats> {
|
||||
return apiRequest<DashboardStats>(`/dashboard/stats${buildQuery(params)}`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface ListGrnsParams {
|
||||
poId?: number
|
||||
vendorId?: number
|
||||
warehouseId?: number
|
||||
branchId?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ import {
|
||||
WorkShift,
|
||||
} from "@/types/hrm"
|
||||
|
||||
export interface CreateBranchRequest { code: string; name: string; address?: string | null }
|
||||
export type UpdateBranchRequest = Omit<CreateBranchRequest, "code">
|
||||
// Code is server-generated ("BR-001", ...) — never sent from the client.
|
||||
export interface CreateBranchRequest { name: string; address?: string | null; phoneNumber?: string | null }
|
||||
export type UpdateBranchRequest = CreateBranchRequest
|
||||
export const branchesHrmApi = createMasterApi<Branch, CreateBranchRequest, UpdateBranchRequest>("branches")
|
||||
|
||||
export interface CreateDepartmentRequest {
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface ListAdjustmentsParams {
|
||||
pageSize?: number
|
||||
q?: string
|
||||
warehouseId?: number
|
||||
branchId?: number
|
||||
reasonCodeId?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface ListCountsParams {
|
||||
q?: string
|
||||
status?: CountStatus
|
||||
warehouseId?: number
|
||||
branchId?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface ListTransfersParams {
|
||||
status?: TransferStatus
|
||||
srcWarehouseId?: number
|
||||
destWarehouseId?: number
|
||||
branchId?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { LedgerEntry, OnHand, ReorderAlert, ReorderRequisitionResponse, Valuatio
|
||||
export interface LedgerQuery {
|
||||
itemId?: number
|
||||
warehouseId?: number
|
||||
branchId?: number
|
||||
/** `YYYY-MM-DD`. */
|
||||
from?: string
|
||||
to?: string
|
||||
@@ -23,6 +24,7 @@ export interface LedgerQuery {
|
||||
export interface OnHandListParams {
|
||||
itemId?: number
|
||||
warehouseId?: number
|
||||
branchId?: number
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
@@ -46,7 +48,10 @@ export const stockApi = {
|
||||
},
|
||||
|
||||
/** Items at or below their reorder point — computed on read, no stored entity (FR-STK-10). */
|
||||
reorderAlerts(warehouseId?: number, params: { page?: number; pageSize?: number } = {}): Promise<PagedResponse<ReorderAlert>> {
|
||||
reorderAlerts(
|
||||
warehouseId?: number,
|
||||
params: { branchId?: number; page?: number; pageSize?: number } = {},
|
||||
): Promise<PagedResponse<ReorderAlert>> {
|
||||
return apiRequest<PagedResponse<ReorderAlert>>(`/stock/reorder-alerts${buildQuery({ warehouseId, ...params })}`)
|
||||
},
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface WastageRecord {
|
||||
|
||||
export interface ListWastageParams {
|
||||
warehouseId?: number
|
||||
branchId?: number
|
||||
reasonCodeId?: number
|
||||
page?: number
|
||||
pageSize?: number
|
||||
@@ -73,6 +74,7 @@ export const wastageApi = {
|
||||
// otherwise fetch the page and keep the loss-type rows.
|
||||
const res = await stockAdjustmentsApi.list({
|
||||
warehouseId: params.warehouseId,
|
||||
branchId: params.branchId,
|
||||
reasonCodeId: params.reasonCodeId,
|
||||
page: params.page,
|
||||
pageSize: params.pageSize ?? 50,
|
||||
|
||||
@@ -8,11 +8,23 @@ export interface Branch {
|
||||
code: string
|
||||
name: string
|
||||
address: string | null
|
||||
phoneNumber: string | null
|
||||
status: EntityStatus
|
||||
createdAt: string
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
/** Singleton toggle for whether this deployment requires a branch at all. */
|
||||
export interface BranchConfig {
|
||||
branchingEnabled: boolean
|
||||
updatedAt: string | null
|
||||
updatedBy: number | null
|
||||
}
|
||||
|
||||
export interface UpdateBranchConfigRequest {
|
||||
branchingEnabled: boolean
|
||||
}
|
||||
|
||||
export interface Department {
|
||||
departmentId: number
|
||||
code: string
|
||||
@@ -90,7 +102,7 @@ export interface EmployeeListItem {
|
||||
designationName: string | null
|
||||
employmentTypeId: number
|
||||
employmentTypeName: string | null
|
||||
branchId: number | null
|
||||
branchId: number
|
||||
branchName: string | null
|
||||
status: EmployeeStatus
|
||||
hasUserLink: boolean
|
||||
@@ -122,7 +134,7 @@ export interface EmployeeDetail {
|
||||
departmentId: number
|
||||
designationId: number
|
||||
employmentTypeId: number
|
||||
branchId: number | null
|
||||
branchId: number
|
||||
workShiftId: number
|
||||
reportingManagerId: number | null
|
||||
epfNumber: string | null
|
||||
@@ -155,7 +167,7 @@ export interface CreateEmployeeRequest {
|
||||
departmentId: number
|
||||
designationId: number
|
||||
employmentTypeId: number
|
||||
branchId?: number | null
|
||||
branchId: number
|
||||
workShiftId: number
|
||||
reportingManagerId?: number | null
|
||||
epfNumber?: string | null
|
||||
|
||||
@@ -137,11 +137,14 @@ export interface Warehouse {
|
||||
warehouseId: number
|
||||
code: string
|
||||
name: string
|
||||
branchId: number
|
||||
branchName: string | null
|
||||
}
|
||||
|
||||
export interface CreateWarehouseRequest {
|
||||
code: string
|
||||
name: string
|
||||
branchId: number
|
||||
}
|
||||
|
||||
export interface Bin {
|
||||
|
||||
@@ -55,4 +55,8 @@ export interface MeResponse {
|
||||
roleCode: string | null
|
||||
roleName: string | null
|
||||
navCodes: string[]
|
||||
isAdmin: boolean
|
||||
branchId: number | null
|
||||
branchName: string | null
|
||||
branchingEnabled: boolean
|
||||
}
|
||||
|
||||
@@ -255,6 +255,7 @@ export interface SalesListFilterParams {
|
||||
status?: SalesInvoiceStatus | SalesSlipStatus
|
||||
customerId?: number
|
||||
warehouseId?: number
|
||||
branchId?: number
|
||||
}
|
||||
|
||||
export interface FreeIssueSummaryItem extends SalesSlipSummary {
|
||||
|
||||
@@ -24,6 +24,20 @@ export interface CreateUserRequest {
|
||||
password?: string | null
|
||||
/** Explicit, human-confirmed link to an existing unlinked Employee found via email-lookup. */
|
||||
linkEmployeeId?: number | null
|
||||
|
||||
/**
|
||||
* Required (with the other HR fields below) whenever the assigned role isn't Admin and
|
||||
* linkEmployeeId isn't supplied — creates the linked Employee record that scopes this
|
||||
* account to one branch.
|
||||
*/
|
||||
employeeCode?: string | null
|
||||
branchId?: number | null
|
||||
departmentId?: number | null
|
||||
designationId?: number | null
|
||||
employmentTypeId?: number | null
|
||||
workShiftId?: number | null
|
||||
/** Defaults to today when omitted. */
|
||||
hireDate?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateUserRoleRequest {
|
||||
|
||||
Reference in New Issue
Block a user