diff --git a/src/app/asset-categories/page.tsx b/src/app/asset-categories/page.tsx new file mode 100644 index 0000000..f2e8860 --- /dev/null +++ b/src/app/asset-categories/page.tsx @@ -0,0 +1,168 @@ +"use client"; + +import { useState, type FormEvent } from "react"; +import { useApiList } from "@/hooks/use-api-list"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +// Shape returned by GET /api/asset-categories — snake_case per this repo's +// API-boundary convention (see src/lib/asset-categories.ts's toRecord()). +interface AssetCategoryRecord { + id: string; + name: string; + value_group: "high_value" | "low_value"; + created_at: string; +} + +const VALUE_GROUP_OPTIONS = ["high_value", "low_value"] as const; + +export default function AssetCategoriesPage() { + const { + data: assetCategories, + loading, + error, + refetch, + } = useApiList("/api/asset-categories"); + + const [name, setName] = useState(""); + const [valueGroup, setValueGroup] = useState<(typeof VALUE_GROUP_OPTIONS)[number]>( + "high_value" + ); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + setSubmitting(true); + setSubmitError(null); + + try { + const response = await fetch("/api/asset-categories", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, valueGroup }), + }); + + let body: unknown; + try { + body = await response.json(); + } catch { + body = null; + } + + if (!response.ok) { + const message = + body !== null && + typeof body === "object" && + "error" in body && + typeof (body as { error: unknown }).error === "string" + ? (body as { error: string }).error + : "Request failed"; + setSubmitError(message); + return; + } + + setName(""); + setValueGroup("high_value"); + refetch(); + } catch { + setSubmitError("Request failed"); + } finally { + setSubmitting(false); + } + } + + return ( +
+

Asset Categories

+ +
+ {loading ? ( +

Loading...

+ ) : error ? ( +

{error}

+ ) : ( + + + + ID + Name + Value Group + Created At + + + + {assetCategories.map((assetCategory) => ( + + {assetCategory.id} + {assetCategory.name} + {assetCategory.value_group} + {assetCategory.created_at} + + ))} + +
+ )} +
+ +
+

New Asset Category

+ +
+
+ + setName(event.target.value)} + required + /> +
+ +
+ + +
+ + {submitError ?

{submitError}

: null} + + +
+
+
+ ); +} diff --git a/src/app/employees/page.tsx b/src/app/employees/page.tsx new file mode 100644 index 0000000..8b41c74 --- /dev/null +++ b/src/app/employees/page.tsx @@ -0,0 +1,183 @@ +"use client"; + +import { useState, type FormEvent } from "react"; +import { useApiList } from "@/hooks/use-api-list"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +// Shape returned by GET /api/employees — snake_case per this repo's +// API-boundary convention (see src/lib/employees.ts's toRecord()). +interface EmployeeRecord { + id: string; + name: string; + department: string; + manager_id: string | null; + created_at: string; +} + +// Radix can't take an empty-string value, so "no manager" is +// represented by this sentinel in the form state and translated to +// `undefined` (omitted from the request body) on submit. +const NO_MANAGER_VALUE = "none"; + +export default function EmployeesPage() { + const { data: employees, loading, error, refetch } = useApiList( + "/api/employees" + ); + + const [name, setName] = useState(""); + const [department, setDepartment] = useState(""); + const [managerId, setManagerId] = useState(NO_MANAGER_VALUE); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + setSubmitting(true); + setSubmitError(null); + + try { + const response = await fetch("/api/employees", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name, + department, + ...(managerId !== NO_MANAGER_VALUE ? { managerId } : {}), + }), + }); + + let body: unknown; + try { + body = await response.json(); + } catch { + body = null; + } + + if (!response.ok) { + const message = + body !== null && + typeof body === "object" && + "error" in body && + typeof (body as { error: unknown }).error === "string" + ? (body as { error: string }).error + : "Request failed"; + setSubmitError(message); + return; + } + + setName(""); + setDepartment(""); + setManagerId(NO_MANAGER_VALUE); + refetch(); + } catch { + setSubmitError("Request failed"); + } finally { + setSubmitting(false); + } + } + + return ( +
+

Employees

+ +
+ {loading ? ( +

Loading...

+ ) : error ? ( +

{error}

+ ) : ( + + + + ID + Name + Department + Manager ID + Created At + + + + {employees.map((employee) => ( + + {employee.id} + {employee.name} + {employee.department} + {employee.manager_id ?? "-"} + {employee.created_at} + + ))} + +
+ )} +
+ +
+

New Employee

+ +
+
+ + setName(event.target.value)} + required + /> +
+ +
+ + setDepartment(event.target.value)} + required + /> +
+ +
+ + +
+ + {submitError ? ( +

{submitError}

+ ) : null} + + +
+
+
+ ); +} diff --git a/src/app/vendors/page.tsx b/src/app/vendors/page.tsx new file mode 100644 index 0000000..a7970ce --- /dev/null +++ b/src/app/vendors/page.tsx @@ -0,0 +1,216 @@ +"use client"; + +import { useState, type FormEvent } from "react"; +import { useApiList } from "@/hooks/use-api-list"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +// Shape returned by GET /api/vendors — snake_case per this repo's +// API-boundary convention (see src/lib/vendors.ts's toRecord()). +interface VendorRecord { + id: string; + name: string; + contact_email: string; + contact_phone: string; + address: string; + tax_code: string | null; + notes: string | null; + created_at: string; +} + +export default function VendorsPage() { + const { data: vendors, loading, error, refetch } = useApiList( + "/api/vendors" + ); + + const [name, setName] = useState(""); + const [contactEmail, setContactEmail] = useState(""); + const [contactPhone, setContactPhone] = useState(""); + const [address, setAddress] = useState(""); + const [taxCode, setTaxCode] = useState(""); + const [notes, setNotes] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(null); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + setSubmitting(true); + setSubmitError(null); + + try { + const response = await fetch("/api/vendors", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name, + contactEmail, + contactPhone, + address, + // taxCode/notes are optional+nullable and must be non-empty + // strings if present (src/lib/vendors.ts's createVendorSchema) — + // omit the key entirely when the field is left blank, same + // pattern as employees page's managerId. + ...(taxCode.trim() !== "" ? { taxCode } : {}), + ...(notes.trim() !== "" ? { notes } : {}), + }), + }); + + let body: unknown; + try { + body = await response.json(); + } catch { + body = null; + } + + if (!response.ok) { + const message = + body !== null && + typeof body === "object" && + "error" in body && + typeof (body as { error: unknown }).error === "string" + ? (body as { error: string }).error + : "Request failed"; + setSubmitError(message); + return; + } + + setName(""); + setContactEmail(""); + setContactPhone(""); + setAddress(""); + setTaxCode(""); + setNotes(""); + refetch(); + } catch { + setSubmitError("Request failed"); + } finally { + setSubmitting(false); + } + } + + return ( +
+

Vendors

+ +
+ {loading ? ( +

Loading...

+ ) : error ? ( +

{error}

+ ) : ( + + + + ID + Name + Contact Email + Contact Phone + Address + Tax Code + Notes + Created At + + + + {vendors.map((vendor) => ( + + {vendor.id} + {vendor.name} + {vendor.contact_email} + {vendor.contact_phone} + {vendor.address} + {vendor.tax_code ?? "-"} + {vendor.notes ?? "-"} + {vendor.created_at} + + ))} + +
+ )} +
+ +
+

New Vendor

+ +
+
+ + setName(event.target.value)} + required + /> +
+ +
+ + setContactEmail(event.target.value)} + required + /> +
+ +
+ + setContactPhone(event.target.value)} + required + /> +
+ +
+ + setAddress(event.target.value)} + required + /> +
+ +
+ + setTaxCode(event.target.value)} + /> +
+ +
+ +