From 1777189431ebf614b5ed0b878345b520dacc9bb6 Mon Sep 17 00:00:00 2001 From: zichun Date: Fri, 10 Apr 2026 10:56:48 +0800 Subject: [PATCH] feat(web): create-location + adjust-stock forms, close SPA CRUD gap --- web/src/App.tsx | 4 ++++ web/src/api/client.ts | 4 ++++ web/src/pages/AdjustStockPage.tsx | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ web/src/pages/BalancesPage.tsx | 2 ++ web/src/pages/CreateLocationPage.tsx | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ web/src/pages/LocationsPage.tsx | 7 ++++++- 6 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 web/src/pages/AdjustStockPage.tsx create mode 100644 web/src/pages/CreateLocationPage.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index 4a33548..2d671ea 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -23,7 +23,9 @@ import { UomsPage } from '@/pages/UomsPage' import { PartnersPage } from '@/pages/PartnersPage' import { CreatePartnerPage } from '@/pages/CreatePartnerPage' import { LocationsPage } from '@/pages/LocationsPage' +import { CreateLocationPage } from '@/pages/CreateLocationPage' import { BalancesPage } from '@/pages/BalancesPage' +import { AdjustStockPage } from '@/pages/AdjustStockPage' import { MovementsPage } from '@/pages/MovementsPage' import { SalesOrdersPage } from '@/pages/SalesOrdersPage' import { CreateSalesOrderPage } from '@/pages/CreateSalesOrderPage' @@ -60,7 +62,9 @@ export default function App() { } /> } /> } /> + } /> } /> + } /> } /> } /> } /> diff --git a/web/src/api/client.ts b/web/src/api/client.ts index ce79197..6c5272b 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -180,7 +180,11 @@ export const partners = { export const inventory = { listLocations: () => apiFetch('/api/v1/inventory/locations'), + createLocation: (body: { code: string; name: string; type: string }) => + apiFetch('/api/v1/inventory/locations', { method: 'POST', body: JSON.stringify(body) }), listBalances: () => apiFetch('/api/v1/inventory/balances'), + adjustBalance: (body: { itemCode: string; locationId: string; quantity: number }) => + apiFetch('/api/v1/inventory/balances/adjust', { method: 'POST', body: JSON.stringify(body) }), listMovements: () => apiFetch('/api/v1/inventory/movements'), } diff --git a/web/src/pages/AdjustStockPage.tsx b/web/src/pages/AdjustStockPage.tsx new file mode 100644 index 0000000..4294efa --- /dev/null +++ b/web/src/pages/AdjustStockPage.tsx @@ -0,0 +1,89 @@ +import { useEffect, useState, type FormEvent } from 'react' +import { useNavigate } from 'react-router-dom' +import { catalog, inventory } from '@/api/client' +import type { Item, Location } from '@/types/api' +import { PageHeader } from '@/components/PageHeader' +import { ErrorBox } from '@/components/ErrorBox' + +export function AdjustStockPage() { + const navigate = useNavigate() + const [items, setItems] = useState([]) + const [locations, setLocations] = useState([]) + const [itemCode, setItemCode] = useState('') + const [locationId, setLocationId] = useState('') + const [quantity, setQuantity] = useState('') + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) + const [result, setResult] = useState(null) + + useEffect(() => { + Promise.all([catalog.listItems(), inventory.listLocations()]).then(([i, l]) => { + setItems(i) + setLocations(l.filter((x) => x.active)) + if (i.length > 0) setItemCode(i[0].code) + if (l.length > 0) setLocationId(l[0].id) + }) + }, []) + + const onSubmit = async (e: FormEvent) => { + e.preventDefault() + setError(null) + setResult(null) + setSubmitting(true) + try { + const bal = await inventory.adjustBalance({ + itemCode, + locationId, + quantity: Number(quantity), + }) + setResult( + `Balance set: ${bal.itemCode} @ location = ${bal.quantity}`, + ) + } catch (err: unknown) { + setError(err instanceof Error ? err : new Error(String(err))) + } finally { + setSubmitting(false) + } + } + + return ( +
+ navigate('/balances')}>← Balances} + /> +
+
+ + +
+
+ + +
+
+ + setQuantity(e.target.value)} + className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm text-right" /> +
+ {error && } + {result && ( +
+ {result} +
+ )} + + +
+ ) +} diff --git a/web/src/pages/BalancesPage.tsx b/web/src/pages/BalancesPage.tsx index 8dc56f8..3efe48d 100644 --- a/web/src/pages/BalancesPage.tsx +++ b/web/src/pages/BalancesPage.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' import { inventory } from '@/api/client' import type { Location, StockBalance } from '@/types/api' import { PageHeader } from '@/components/PageHeader' @@ -54,6 +55,7 @@ export function BalancesPage() { Adjust Stock} /> {loading && } {error && } diff --git a/web/src/pages/CreateLocationPage.tsx b/web/src/pages/CreateLocationPage.tsx new file mode 100644 index 0000000..e6a4748 --- /dev/null +++ b/web/src/pages/CreateLocationPage.tsx @@ -0,0 +1,63 @@ +import { useState, type FormEvent } from 'react' +import { useNavigate } from 'react-router-dom' +import { inventory } from '@/api/client' +import { PageHeader } from '@/components/PageHeader' +import { ErrorBox } from '@/components/ErrorBox' + +const LOCATION_TYPES = ['WAREHOUSE', 'BIN', 'VIRTUAL'] as const + +export function CreateLocationPage() { + const navigate = useNavigate() + const [code, setCode] = useState('') + const [name, setName] = useState('') + const [type, setType] = useState('WAREHOUSE') + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) + + const onSubmit = async (e: FormEvent) => { + e.preventDefault() + setError(null) + setSubmitting(true) + try { + await inventory.createLocation({ code, name, type }) + navigate('/locations') + } catch (err: unknown) { + setError(err instanceof Error ? err : new Error(String(err))) + } finally { + setSubmitting(false) + } + } + + return ( +
+ navigate('/locations')}>Cancel} + /> +
+
+ + setCode(e.target.value)} + placeholder="WH-NEW" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> +
+
+ + setName(e.target.value)} + placeholder="New Warehouse" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> +
+
+ + +
+ {error && } + + +
+ ) +} diff --git a/web/src/pages/LocationsPage.tsx b/web/src/pages/LocationsPage.tsx index 970c424..9422592 100644 --- a/web/src/pages/LocationsPage.tsx +++ b/web/src/pages/LocationsPage.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' import { inventory } from '@/api/client' import type { Location } from '@/types/api' import { PageHeader } from '@/components/PageHeader' @@ -32,7 +33,11 @@ export function LocationsPage() { return (
- + + New Location} + /> {loading && } {error && } {!loading && !error && } -- libgit2 0.22.2