ui bug fixers
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { customersApi } from "@/lib/api/customers"
|
||||
import { errorMessage, fieldErrors } from "@/lib/error-map"
|
||||
import { Customer, CustomerType } from "@/types/customers"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
|
||||
/**
|
||||
* Edit popup for one customer, opened from the customers list and laid out like its
|
||||
* "New customer" dialog. Mount it with `key={customerId}` so each open starts clean. The
|
||||
* list rows carry no ETag, so the customer is re-read here for a fresh If-Match first.
|
||||
*/
|
||||
export function EditCustomerDialog({
|
||||
customerId,
|
||||
onClose,
|
||||
onSaved,
|
||||
}: {
|
||||
customerId: number
|
||||
onClose: () => void
|
||||
onSaved: (customer: Customer) => void
|
||||
}) {
|
||||
const [customer, setCustomer] = useState<Customer | null>(null)
|
||||
const [etag, setEtag] = useState<string | null>(null)
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
|
||||
const [customerCode, setCustomerCode] = useState("")
|
||||
const [customerType, setCustomerType] = useState<CustomerType>("B2C")
|
||||
const [name, setName] = useState("")
|
||||
const [phone, setPhone] = useState("")
|
||||
const [email, setEmail] = useState("")
|
||||
const [addressLine1, setAddressLine1] = useState("")
|
||||
const [addressLine2, setAddressLine2] = useState("")
|
||||
const [city, setCity] = useState("")
|
||||
const [country, setCountry] = useState("")
|
||||
const [taxRegistrationNo, setTaxRegistrationNo] = useState("")
|
||||
const [creditLimit, setCreditLimit] = useState("")
|
||||
const [creditDays, setCreditDays] = useState("")
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
function applyCustomer(data: Customer, tag: string | null) {
|
||||
setCustomer(data)
|
||||
setEtag(tag)
|
||||
setCustomerCode(data.customerCode)
|
||||
setCustomerType(data.customerType)
|
||||
setName(data.name)
|
||||
setPhone(data.phone ?? "")
|
||||
setEmail(data.email ?? "")
|
||||
setAddressLine1(data.addressLine1 ?? "")
|
||||
setAddressLine2(data.addressLine2 ?? "")
|
||||
setCity(data.city ?? "")
|
||||
setCountry(data.country ?? "")
|
||||
setTaxRegistrationNo(data.taxRegistrationNo ?? "")
|
||||
setCreditLimit(String(data.creditLimit))
|
||||
setCreditDays(String(data.creditDays))
|
||||
}
|
||||
|
||||
function reload() {
|
||||
return customersApi.get(customerId).then(({ data, etag: tag }) => applyCustomer(data, tag))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload().catch((err) => setLoadError(errorMessage(err)))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [customerId])
|
||||
|
||||
/** Drops a field's error once the user starts editing it. */
|
||||
function clearError(key: string) {
|
||||
setErrors((prev) => {
|
||||
if (!prev[key]) return prev
|
||||
const next = { ...prev }
|
||||
delete next[key]
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const nextErrors: Record<string, string> = {}
|
||||
if (!customerCode.trim()) nextErrors.customerCode = "Customer code is required"
|
||||
if (!name.trim()) nextErrors.name = "Customer name is required"
|
||||
setErrors(nextErrors)
|
||||
if (Object.keys(nextErrors).length > 0) {
|
||||
toast.error("Could not save customer", Object.values(nextErrors)[0])
|
||||
return
|
||||
}
|
||||
if (!customer || !etag) return
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
const result = await customersApi.update(
|
||||
customerId,
|
||||
{
|
||||
customerCode,
|
||||
customerType,
|
||||
name,
|
||||
// Not editable in this popup — send the stored value back so the PUT doesn't clear it.
|
||||
displayName: customer.displayName,
|
||||
phone: phone || null,
|
||||
email: email || null,
|
||||
addressLine1: addressLine1 || null,
|
||||
addressLine2: addressLine2 || null,
|
||||
city: city || null,
|
||||
country: country || null,
|
||||
taxRegistrationNo: taxRegistrationNo || null,
|
||||
creditLimit: Number(creditLimit) || 0,
|
||||
creditDays: Number(creditDays) || 0,
|
||||
defaultWarehouseId: customer.defaultWarehouseId,
|
||||
branchId: customer.branchId,
|
||||
},
|
||||
etag
|
||||
)
|
||||
toast.success("Customer saved", `${result.data.customerCode} — ${result.data.name}`)
|
||||
onSaved(result.data)
|
||||
} catch (err) {
|
||||
if ((err as { code?: string })?.code === "CONCURRENCY_CONFLICT") {
|
||||
// Someone else saved first: show their version so the user can redo the edit on top of it.
|
||||
toast.error("Customer was changed by someone else", "The latest details have been loaded — make your edit again.")
|
||||
await reload().catch(() => {})
|
||||
return
|
||||
}
|
||||
const fe = fieldErrors(err)
|
||||
if (fe?.customerCode) setErrors({ customerCode: fe.customerCode })
|
||||
toast.error("Could not save customer", errorMessage(err))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader className="items-center text-center">
|
||||
<DialogTitle>Edit customer</DialogTitle>
|
||||
<DialogDescription>Update this customer's details.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{loadError ? (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-base text-destructive">{loadError}</div>
|
||||
) : !customer ? (
|
||||
<div className="flex flex-col gap-5">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-14 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<FieldGroup>
|
||||
<Field data-invalid={!!errors.name}>
|
||||
<FieldLabel htmlFor="ec-name" required>Name</FieldLabel>
|
||||
<Input
|
||||
id="ec-name"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value)
|
||||
clearError("name")
|
||||
}}
|
||||
placeholder="Nimal Perera"
|
||||
aria-invalid={!!errors.name}
|
||||
/>
|
||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field data-invalid={!!errors.customerCode}>
|
||||
<FieldLabel htmlFor="ec-code" required>Code</FieldLabel>
|
||||
<Input
|
||||
id="ec-code"
|
||||
value={customerCode}
|
||||
onChange={(e) => {
|
||||
setCustomerCode(e.target.value)
|
||||
clearError("customerCode")
|
||||
}}
|
||||
aria-invalid={!!errors.customerCode}
|
||||
/>
|
||||
<FieldError errors={[errors.customerCode ? { message: errors.customerCode } : undefined]} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ec-type">Customer type</FieldLabel>
|
||||
<Select<CustomerType> value={customerType} onValueChange={(v) => v && setCustomerType(v)}>
|
||||
<SelectTrigger id="ec-type" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="B2C">B2C — individual</SelectItem>
|
||||
<SelectItem value="B2B">B2B — business</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ec-phone">Phone (optional)</FieldLabel>
|
||||
<Input id="ec-phone" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="0771234567" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ec-email">Email (optional)</FieldLabel>
|
||||
<Input id="ec-email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="customer@example.com" />
|
||||
</Field>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ec-addr1">Address line 1 (optional)</FieldLabel>
|
||||
<Input id="ec-addr1" value={addressLine1} onChange={(e) => setAddressLine1(e.target.value)} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ec-addr2">Address line 2 (optional)</FieldLabel>
|
||||
<Input id="ec-addr2" value={addressLine2} onChange={(e) => setAddressLine2(e.target.value)} />
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ec-city">City (optional)</FieldLabel>
|
||||
<Input id="ec-city" value={city} onChange={(e) => setCity(e.target.value)} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ec-country">Country (optional)</FieldLabel>
|
||||
<Input id="ec-country" value={country} onChange={(e) => setCountry(e.target.value)} />
|
||||
</Field>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ec-tax">Tax registration (optional)</FieldLabel>
|
||||
<Input id="ec-tax" value={taxRegistrationNo} onChange={(e) => setTaxRegistrationNo(e.target.value)} />
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ec-credit-limit">Credit limit (LKR)</FieldLabel>
|
||||
<Input id="ec-credit-limit" type="number" min="0" step="0.01" placeholder="0" value={creditLimit} onChange={(e) => setCreditLimit(e.target.value)} />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ec-credit-days">Credit days</FieldLabel>
|
||||
<Input id="ec-credit-days" type="number" min="0" step="1" placeholder="0" value={creditDays} onChange={(e) => setCreditDays(e.target.value)} />
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={onClose} disabled={saving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-36" onClick={handleSave} disabled={saving || !customer}>
|
||||
{saving ? "Saving…" : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user