feat: add basic list+create UI for Employee, Vendor, AssetCategory

This commit is contained in:
toanhoang
2026-08-03 06:54:41 -04:00
parent 0576d6e61c
commit 4bcd4da11b
3 changed files with 567 additions and 0 deletions
+216
View File
@@ -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<VendorRecord>(
"/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<string | null>(null);
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
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 (
<div className="flex flex-col gap-8 p-6">
<h1 className="text-xl font-semibold">Vendors</h1>
<section className="flex flex-col gap-3">
{loading ? (
<p className="text-sm text-muted-foreground">Loading...</p>
) : error ? (
<p className="text-sm text-destructive">{error}</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Contact Email</TableHead>
<TableHead>Contact Phone</TableHead>
<TableHead>Address</TableHead>
<TableHead>Tax Code</TableHead>
<TableHead>Notes</TableHead>
<TableHead>Created At</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{vendors.map((vendor) => (
<TableRow key={vendor.id}>
<TableCell>{vendor.id}</TableCell>
<TableCell>{vendor.name}</TableCell>
<TableCell>{vendor.contact_email}</TableCell>
<TableCell>{vendor.contact_phone}</TableCell>
<TableCell>{vendor.address}</TableCell>
<TableCell>{vendor.tax_code ?? "-"}</TableCell>
<TableCell>{vendor.notes ?? "-"}</TableCell>
<TableCell>{vendor.created_at}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</section>
<section className="flex max-w-sm flex-col gap-4">
<h2 className="text-lg font-semibold">New Vendor</h2>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(event) => setName(event.target.value)}
required
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="contactEmail">Contact Email</Label>
<Input
id="contactEmail"
type="email"
value={contactEmail}
onChange={(event) => setContactEmail(event.target.value)}
required
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="contactPhone">Contact Phone</Label>
<Input
id="contactPhone"
value={contactPhone}
onChange={(event) => setContactPhone(event.target.value)}
required
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="address">Address</Label>
<Input
id="address"
value={address}
onChange={(event) => setAddress(event.target.value)}
required
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="taxCode">Tax Code</Label>
<Input
id="taxCode"
value={taxCode}
onChange={(event) => setTaxCode(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="notes">Notes</Label>
<Textarea
id="notes"
value={notes}
onChange={(event) => setNotes(event.target.value)}
/>
</div>
{submitError ? (
<p className="text-sm text-destructive">{submitError}</p>
) : null}
<Button type="submit" disabled={submitting}>
{submitting ? "Creating..." : "Create Vendor"}
</Button>
</form>
</section>
</div>
);
}