Commit 24bf9acc1330cecdcb366b7b8fa15202458addc5

Authored by zichun
1 parent 7a3ebe64

fix(web): DynamicExtFields on edit pages + user-friendly error messages

web/src/api/client.ts
@@ -110,8 +110,12 @@ export async function apiFetch<T>( @@ -110,8 +110,12 @@ export async function apiFetch<T>(
110 if (text) { 110 if (text) {
111 try { 111 try {
112 body = JSON.parse(text) 112 body = JSON.parse(text)
113 - const m = (body as { message?: unknown }).message  
114 - if (typeof m === 'string') message = m 113 + // Spring ProblemDetail uses "detail"; fallback to "message"
  114 + const parsed = body as { detail?: unknown; message?: unknown }
  115 + const d = parsed.detail
  116 + const m = parsed.message
  117 + if (typeof d === 'string') message = d
  118 + else if (typeof m === 'string') message = m
115 } catch { 119 } catch {
116 body = text 120 body = text
117 message = text 121 message = text
@@ -171,7 +175,8 @@ export const catalog = { @@ -171,7 +175,8 @@ export const catalog = {
171 itemType: string; baseUomCode: string; active?: boolean 175 itemType: string; baseUomCode: string; active?: boolean
172 }) => apiFetch<Item>('/api/v1/catalog/items', { method: 'POST', body: JSON.stringify(body) }), 176 }) => apiFetch<Item>('/api/v1/catalog/items', { method: 'POST', body: JSON.stringify(body) }),
173 updateItem: (id: string, body: { 177 updateItem: (id: string, body: {
174 - name?: string; description?: string | null; itemType?: string; active?: boolean 178 + name?: string; description?: string | null; itemType?: string; active?: boolean;
  179 + ext?: Record<string, unknown>
175 }) => apiFetch<Item>(`/api/v1/catalog/items/${id}`, { method: 'PATCH', body: JSON.stringify(body) }), 180 }) => apiFetch<Item>(`/api/v1/catalog/items/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
176 listUoms: () => apiFetch<Uom[]>('/api/v1/catalog/uoms'), 181 listUoms: () => apiFetch<Uom[]>('/api/v1/catalog/uoms'),
177 } 182 }
@@ -188,7 +193,8 @@ export const partners = { @@ -188,7 +193,8 @@ export const partners = {
188 }) => apiFetch<Partner>('/api/v1/partners/partners', { method: 'POST', body: JSON.stringify(body) }), 193 }) => apiFetch<Partner>('/api/v1/partners/partners', { method: 'POST', body: JSON.stringify(body) }),
189 update: (id: string, body: { 194 update: (id: string, body: {
190 name?: string; type?: string; 195 name?: string; type?: string;
191 - email?: string | null; phone?: string | null 196 + email?: string | null; phone?: string | null;
  197 + ext?: Record<string, unknown>
192 }) => apiFetch<Partner>(`/api/v1/partners/partners/${id}`, { method: 'PATCH', body: JSON.stringify(body) }), 198 }) => apiFetch<Partner>(`/api/v1/partners/partners/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
193 } 199 }
194 200
web/src/pages/EditItemPage.tsx
@@ -5,6 +5,7 @@ import type { Item } from &#39;@/types/api&#39; @@ -5,6 +5,7 @@ import type { Item } from &#39;@/types/api&#39;
5 import { PageHeader } from '@/components/PageHeader' 5 import { PageHeader } from '@/components/PageHeader'
6 import { Loading } from '@/components/Loading' 6 import { Loading } from '@/components/Loading'
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
  8 +import { DynamicExtFields } from '@/components/DynamicExtFields'
8 9
9 const ITEM_TYPES = ['GOOD', 'SERVICE', 'DIGITAL'] as const 10 const ITEM_TYPES = ['GOOD', 'SERVICE', 'DIGITAL'] as const
10 11
@@ -17,6 +18,7 @@ export function EditItemPage() { @@ -17,6 +18,7 @@ export function EditItemPage() {
17 const [itemType, setItemType] = useState<string>('GOOD') 18 const [itemType, setItemType] = useState<string>('GOOD')
18 const [active, setActive] = useState(true) 19 const [active, setActive] = useState(true)
19 const [loading, setLoading] = useState(true) 20 const [loading, setLoading] = useState(true)
  21 + const [ext, setExt] = useState<Record<string, unknown>>({})
20 const [submitting, setSubmitting] = useState(false) 22 const [submitting, setSubmitting] = useState(false)
21 const [error, setError] = useState<Error | null>(null) 23 const [error, setError] = useState<Error | null>(null)
22 24
@@ -28,6 +30,7 @@ export function EditItemPage() { @@ -28,6 +30,7 @@ export function EditItemPage() {
28 setDescription(i.description ?? '') 30 setDescription(i.description ?? '')
29 setItemType(i.itemType) 31 setItemType(i.itemType)
30 setActive(i.active) 32 setActive(i.active)
  33 + setExt(i.ext || {})
31 }) 34 })
32 .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) 35 .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e))))
33 .finally(() => setLoading(false)) 36 .finally(() => setLoading(false))
@@ -41,6 +44,7 @@ export function EditItemPage() { @@ -41,6 +44,7 @@ export function EditItemPage() {
41 await catalog.updateItem(id, { 44 await catalog.updateItem(id, {
42 name, itemType, active, 45 name, itemType, active,
43 description: description || null, 46 description: description || null,
  47 + ...(Object.keys(ext).length > 0 ? { ext } : {}),
44 }) 48 })
45 navigate('/items') 49 navigate('/items')
46 } catch (err: unknown) { 50 } catch (err: unknown) {
@@ -85,6 +89,7 @@ export function EditItemPage() { @@ -85,6 +89,7 @@ export function EditItemPage() {
85 className="rounded border-slate-300" id="active" /> 89 className="rounded border-slate-300" id="active" />
86 <label htmlFor="active" className="text-sm text-slate-700">Active</label> 90 <label htmlFor="active" className="text-sm text-slate-700">Active</label>
87 </div> 91 </div>
  92 + <DynamicExtFields entityName="Item" values={ext} onChange={(k, v) => setExt(prev => ({ ...prev, [k]: v }))} />
88 {error && <ErrorBox error={error} />} 93 {error && <ErrorBox error={error} />}
89 <button type="submit" className="btn-primary" disabled={submitting}> 94 <button type="submit" className="btn-primary" disabled={submitting}>
90 {submitting ? 'Saving…' : 'Save Changes'} 95 {submitting ? 'Saving…' : 'Save Changes'}
web/src/pages/EditPartnerPage.tsx
@@ -5,6 +5,7 @@ import type { Partner } from &#39;@/types/api&#39; @@ -5,6 +5,7 @@ import type { Partner } from &#39;@/types/api&#39;
5 import { PageHeader } from '@/components/PageHeader' 5 import { PageHeader } from '@/components/PageHeader'
6 import { Loading } from '@/components/Loading' 6 import { Loading } from '@/components/Loading'
7 import { ErrorBox } from '@/components/ErrorBox' 7 import { ErrorBox } from '@/components/ErrorBox'
  8 +import { DynamicExtFields } from '@/components/DynamicExtFields'
8 9
9 const PARTNER_TYPES = ['CUSTOMER', 'SUPPLIER', 'BOTH'] as const 10 const PARTNER_TYPES = ['CUSTOMER', 'SUPPLIER', 'BOTH'] as const
10 11
@@ -17,6 +18,7 @@ export function EditPartnerPage() { @@ -17,6 +18,7 @@ export function EditPartnerPage() {
17 const [email, setEmail] = useState('') 18 const [email, setEmail] = useState('')
18 const [phone, setPhone] = useState('') 19 const [phone, setPhone] = useState('')
19 const [loading, setLoading] = useState(true) 20 const [loading, setLoading] = useState(true)
  21 + const [ext, setExt] = useState<Record<string, unknown>>({})
20 const [submitting, setSubmitting] = useState(false) 22 const [submitting, setSubmitting] = useState(false)
21 const [error, setError] = useState<Error | null>(null) 23 const [error, setError] = useState<Error | null>(null)
22 24
@@ -28,6 +30,7 @@ export function EditPartnerPage() { @@ -28,6 +30,7 @@ export function EditPartnerPage() {
28 setType(p.type) 30 setType(p.type)
29 setEmail(p.email ?? '') 31 setEmail(p.email ?? '')
30 setPhone(p.phone ?? '') 32 setPhone(p.phone ?? '')
  33 + setExt(p.ext || {})
31 }) 34 })
32 .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) 35 .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e))))
33 .finally(() => setLoading(false)) 36 .finally(() => setLoading(false))
@@ -42,6 +45,7 @@ export function EditPartnerPage() { @@ -42,6 +45,7 @@ export function EditPartnerPage() {
42 name, type, 45 name, type,
43 email: email || null, 46 email: email || null,
44 phone: phone || null, 47 phone: phone || null,
  48 + ...(Object.keys(ext).length > 0 ? { ext } : {}),
45 }) 49 })
46 navigate('/partners') 50 navigate('/partners')
47 } catch (err: unknown) { 51 } catch (err: unknown) {
@@ -86,6 +90,7 @@ export function EditPartnerPage() { @@ -86,6 +90,7 @@ export function EditPartnerPage() {
86 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> 90 className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
87 </div> 91 </div>
88 </div> 92 </div>
  93 + <DynamicExtFields entityName="Partner" values={ext} onChange={(k, v) => setExt(prev => ({ ...prev, [k]: v }))} />
89 {error && <ErrorBox error={error} />} 94 {error && <ErrorBox error={error} />}
90 <button type="submit" className="btn-primary" disabled={submitting}> 95 <button type="submit" className="btn-primary" disabled={submitting}>
91 {submitting ? 'Saving…' : 'Save Changes'} 96 {submitting ? 'Saving…' : 'Save Changes'}