Files
pos/client/src/pages/VendorPage.tsx
jason d53c772dd6 Add Milestones 1 & 2: full-stack POS foundation with admin UI
- Node/Express/TypeScript API under /api/v1 with JWT auth (login, refresh, logout, /me)
- Prisma schema: vendors, users, roles, products, categories, taxes, transactions
- SQLite for local dev; Postgres via docker-compose for production
- Full CRUD routes for vendors, users, categories, taxes, products with Zod validation and RBAC
- Paginated list endpoints scoped per vendor; refresh token rotation
- React/TypeScript admin SPA (Vite): login, protected routing, sidebar layout
- Pages: Dashboard, Catalog (tabbed Products/Categories/Taxes), Users, Vendor Settings
- Shared UI: Table, Modal, FormField, Btn, PageHeader components
- Multi-stage Dockerfile; docker-compose with Postgres healthcheck
- Seed script with demo vendor and owner account
- INSTRUCTIONS.md, ROADMAP.md, .claude/launch.json for dev server config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 23:18:04 -05:00

130 lines
4.0 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { api } from "../api/client";
import { PageHeader } from "../components/PageHeader";
import { FormField, inputStyle, Btn } from "../components/FormField";
interface Vendor {
id: string;
name: string;
businessNum: string | null;
taxSettings: string | null;
createdAt: string;
updatedAt: string;
}
export default function VendorPage() {
const [vendor, setVendor] = useState<Vendor | null>(null);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const [form, setForm] = useState({ name: "", businessNum: "" });
useEffect(() => {
api
.get<{ data: Vendor[] }>("/vendors")
.then((res) => {
const v = res.data[0] ?? null;
setVendor(v);
if (v) setForm({ name: v.name, businessNum: v.businessNum ?? "" });
})
.catch(console.error)
.finally(() => setLoading(false));
}, []);
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
if (!vendor) return;
setSaving(true);
setError("");
try {
const updated = await api.put<Vendor>(`/vendors/${vendor.id}`, form);
setVendor(updated);
setEditing(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Save failed");
} finally {
setSaving(false);
}
};
if (loading) return <div style={{ padding: 32 }}>Loading</div>;
if (!vendor) return <div style={{ padding: 32 }}>No vendor found.</div>;
return (
<div style={{ padding: "32px 28px", maxWidth: 600 }}>
<PageHeader
title="Vendor Settings"
subtitle="Business details and configuration"
action={
!editing && (
<Btn onClick={() => setEditing(true)}>Edit</Btn>
)
}
/>
{editing ? (
<form onSubmit={handleSave} style={card}>
{error && <div style={errStyle}>{error}</div>}
<FormField label="Business Name" required>
<input
style={inputStyle}
value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
required
/>
</FormField>
<FormField label="Business Number / ABN">
<input
style={inputStyle}
value={form.businessNum}
onChange={(e) => setForm((f) => ({ ...f, businessNum: e.target.value }))}
/>
</FormField>
<div style={{ display: "flex", gap: 8, marginTop: 8 }}>
<Btn type="submit" disabled={saving}>
{saving ? "Saving…" : "Save changes"}
</Btn>
<Btn variant="ghost" onClick={() => setEditing(false)}>
Cancel
</Btn>
</div>
</form>
) : (
<div style={card}>
<Row label="Business Name" value={vendor.name} />
<Row label="Business Number" value={vendor.businessNum ?? "—"} />
<Row label="Created" value={new Date(vendor.createdAt).toLocaleDateString()} />
<Row label="Last Updated" value={new Date(vendor.updatedAt).toLocaleDateString()} />
</div>
)}
</div>
);
}
function Row({ label, value }: { label: string; value: string }) {
return (
<div style={{ display: "flex", gap: 16, padding: "10px 0", borderBottom: "1px solid var(--color-border)" }}>
<div style={{ width: 160, fontWeight: 500, fontSize: 13, color: "var(--color-text-muted)" }}>{label}</div>
<div style={{ flex: 1, fontSize: 14 }}>{value}</div>
</div>
);
}
const card: React.CSSProperties = {
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius)",
padding: "20px",
};
const errStyle: React.CSSProperties = {
background: "#fef2f2",
border: "1px solid #fecaca",
color: "var(--color-danger)",
borderRadius: "var(--radius)",
padding: "10px 12px",
fontSize: 13,
marginBottom: 16,
};