ui bug fixers

This commit is contained in:
2026-10-07 10:28:29 +05:30
committed by ImanThiyanga
parent 0d3f2fba14
commit 3abcfa6e6f
37 changed files with 1852 additions and 542 deletions
@@ -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&apos;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>
)
}