diff --git a/web/src/App.tsx b/web/src/App.tsx index 4393da5..ae02fcc 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -19,9 +19,11 @@ import { UserDetailPage } from '@/pages/UserDetailPage' import { RolesPage } from '@/pages/RolesPage' import { ItemsPage } from '@/pages/ItemsPage' import { CreateItemPage } from '@/pages/CreateItemPage' +import { EditItemPage } from '@/pages/EditItemPage' import { UomsPage } from '@/pages/UomsPage' import { PartnersPage } from '@/pages/PartnersPage' import { CreatePartnerPage } from '@/pages/CreatePartnerPage' +import { EditPartnerPage } from '@/pages/EditPartnerPage' import { LocationsPage } from '@/pages/LocationsPage' import { CreateLocationPage } from '@/pages/CreateLocationPage' import { BalancesPage } from '@/pages/BalancesPage' @@ -59,9 +61,11 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> + } /> } /> } /> } /> diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 4778b9c..2b53941 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -162,6 +162,9 @@ export const catalog = { code: string; name: string; description?: string | null; itemType: string; baseUomCode: string; active?: boolean }) => apiFetch('/api/v1/catalog/items', { method: 'POST', body: JSON.stringify(body) }), + updateItem: (id: string, body: { + name?: string; description?: string | null; itemType?: string; active?: boolean + }) => apiFetch(`/api/v1/catalog/items/${id}`, { method: 'PATCH', body: JSON.stringify(body) }), listUoms: () => apiFetch('/api/v1/catalog/uoms'), } @@ -175,6 +178,10 @@ export const partners = { email?: string | null; phone?: string | null; taxId?: string | null; website?: string | null }) => apiFetch('/api/v1/partners/partners', { method: 'POST', body: JSON.stringify(body) }), + update: (id: string, body: { + name?: string; type?: string; + email?: string | null; phone?: string | null + }) => apiFetch(`/api/v1/partners/partners/${id}`, { method: 'PATCH', body: JSON.stringify(body) }), } // ─── Inventory ─────────────────────────────────────────────────────── diff --git a/web/src/pages/EditItemPage.tsx b/web/src/pages/EditItemPage.tsx new file mode 100644 index 0000000..c8df50b --- /dev/null +++ b/web/src/pages/EditItemPage.tsx @@ -0,0 +1,95 @@ +import { useEffect, useState, type FormEvent } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { catalog } from '@/api/client' +import type { Item } from '@/types/api' +import { PageHeader } from '@/components/PageHeader' +import { Loading } from '@/components/Loading' +import { ErrorBox } from '@/components/ErrorBox' + +const ITEM_TYPES = ['GOOD', 'SERVICE', 'DIGITAL'] as const + +export function EditItemPage() { + const { id = '' } = useParams<{ id: string }>() + const navigate = useNavigate() + const [item, setItem] = useState(null) + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [itemType, setItemType] = useState('GOOD') + const [active, setActive] = useState(true) + const [loading, setLoading] = useState(true) + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + catalog.getItem(id) + .then((i) => { + setItem(i) + setName(i.name) + setDescription(i.description ?? '') + setItemType(i.itemType) + setActive(i.active) + }) + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) + .finally(() => setLoading(false)) + }, [id]) + + const onSubmit = async (e: FormEvent) => { + e.preventDefault() + setError(null) + setSubmitting(true) + try { + await catalog.updateItem(id, { + name, itemType, active, + description: description || null, + }) + navigate('/items') + } catch (err: unknown) { + setError(err instanceof Error ? err : new Error(String(err))) + } finally { + setSubmitting(false) + } + } + + if (loading) return + if (!item) return + + return ( +
+ navigate('/items')}>Cancel} + /> +
+
+
+ + setName(e.target.value)} + className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> +
+
+ + +
+
+
+ + setDescription(e.target.value)} + className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> +
+
+ setActive(e.target.checked)} + className="rounded border-slate-300" id="active" /> + +
+ {error && } + + +
+ ) +} diff --git a/web/src/pages/EditPartnerPage.tsx b/web/src/pages/EditPartnerPage.tsx new file mode 100644 index 0000000..8a890ad --- /dev/null +++ b/web/src/pages/EditPartnerPage.tsx @@ -0,0 +1,96 @@ +import { useEffect, useState, type FormEvent } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { partners } from '@/api/client' +import type { Partner } from '@/types/api' +import { PageHeader } from '@/components/PageHeader' +import { Loading } from '@/components/Loading' +import { ErrorBox } from '@/components/ErrorBox' + +const PARTNER_TYPES = ['CUSTOMER', 'SUPPLIER', 'BOTH'] as const + +export function EditPartnerPage() { + const { id = '' } = useParams<{ id: string }>() + const navigate = useNavigate() + const [partner, setPartner] = useState(null) + const [name, setName] = useState('') + const [type, setType] = useState('CUSTOMER') + const [email, setEmail] = useState('') + const [phone, setPhone] = useState('') + const [loading, setLoading] = useState(true) + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + partners.get(id) + .then((p) => { + setPartner(p) + setName(p.name) + setType(p.type) + setEmail(p.email ?? '') + setPhone(p.phone ?? '') + }) + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) + .finally(() => setLoading(false)) + }, [id]) + + const onSubmit = async (e: FormEvent) => { + e.preventDefault() + setError(null) + setSubmitting(true) + try { + await partners.update(id, { + name, type, + email: email || null, + phone: phone || null, + }) + navigate('/partners') + } catch (err: unknown) { + setError(err instanceof Error ? err : new Error(String(err))) + } finally { + setSubmitting(false) + } + } + + if (loading) return + if (!partner) return + + return ( +
+ navigate('/partners')}>Cancel} + /> +
+
+
+ + setName(e.target.value)} + className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> +
+
+ + +
+
+ + setEmail(e.target.value)} + className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> +
+
+ + setPhone(e.target.value)} + className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> +
+
+ {error && } + + +
+ ) +} diff --git a/web/src/pages/ItemsPage.tsx b/web/src/pages/ItemsPage.tsx index eadf650..4da06bf 100644 --- a/web/src/pages/ItemsPage.tsx +++ b/web/src/pages/ItemsPage.tsx @@ -21,7 +21,13 @@ export function ItemsPage() { }, []) const columns: Column[] = [ - { header: 'Code', key: 'code', render: (r) => {r.code} }, + { + header: 'Code', + key: 'code', + render: (r) => ( + {r.code} + ), + }, { header: 'Name', key: 'name' }, { header: 'Type', key: 'itemType' }, { header: 'UoM', key: 'baseUomCode', render: (r) => {r.baseUomCode} }, diff --git a/web/src/pages/PartnersPage.tsx b/web/src/pages/PartnersPage.tsx index 1044e59..a78721f 100644 --- a/web/src/pages/PartnersPage.tsx +++ b/web/src/pages/PartnersPage.tsx @@ -21,7 +21,13 @@ export function PartnersPage() { }, []) const columns: Column[] = [ - { header: 'Code', key: 'code', render: (r) => {r.code} }, + { + header: 'Code', + key: 'code', + render: (r) => ( + {r.code} + ), + }, { header: 'Name', key: 'name' }, { header: 'Type', key: 'type' }, { header: 'Email', key: 'email', render: (r) => r.email ?? '—' },