auto suggest fix
This commit is contained in:
@@ -20,7 +20,7 @@ This repository implements the platform foundation milestone:
|
||||
3. Add Prisma models and migrations for all persisted schema changes.
|
||||
4. Keep uploaded files on disk under `/app/data/uploads`; never store blobs in SQLite.
|
||||
5. Reuse shared DTOs and permission keys from the `shared` package.
|
||||
6. Any UI that looks up items by SKU or item name must use a searchable picker/autocomplete, not a long dropdown.
|
||||
6. Any non-filter UI that looks up records or items must use a searchable picker/autocomplete, not a long static dropdown.
|
||||
7. Maintain the denser UI baseline on active screens; avoid reintroducing oversized `px-4 py-3` style controls, tall action bars, or overly loose card spacing without a specific reason.
|
||||
|
||||
## Operational notes
|
||||
@@ -31,7 +31,7 @@ This repository implements the platform foundation milestone:
|
||||
- Prefer Node 22 locally when running Prisma migration commands to match the Docker runtime.
|
||||
- Branding defaults live in the frontend theme token layer and are overridden by the persisted company profile.
|
||||
- Back up the whole `/app/data` volume to capture both the database and attachments.
|
||||
- Treat searchable SKU lookup as a standing UX requirement for inventory, BOM, sales, purchasing, and manufacturing flows.
|
||||
- Treat searchable lookup as a standing UX requirement for inventory, BOM, sales, purchasing, manufacturing, customer, vendor, and other operational record-picking flows. Filter-only controls can still use dropdowns.
|
||||
|
||||
## Next roadmap candidates
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ The current inventory foundation supports:
|
||||
|
||||
This module introduces `inventory.read` and `inventory.write` permissions. After updating the code, restart the server against the migrated database so bootstrap can upsert the new permissions onto the default administrator role.
|
||||
|
||||
Moving forward, any UI that requires searching for an item by SKU or item name should use a searchable picker/autocomplete rather than a static dropdown.
|
||||
Moving forward, any UI that requires searching for records or items should use a searchable picker/autocomplete rather than a static dropdown. Filter controls can remain dropdowns, but non-filter lookup fields such as SKU pickers, customer selectors, vendor selectors, and similar operational search inputs should not be implemented as long static selects.
|
||||
|
||||
## UI Density
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- Keep reusable UI primitives in `src/components`.
|
||||
- Theme state and brand tokens belong in `src/theme`.
|
||||
- PDF screen components must remain separate from API-rendered document templates.
|
||||
- Any item/SKU lookup UI must be implemented as a searchable picker or autocomplete; do not use long static dropdowns for inventory-scale datasets.
|
||||
- Any non-filter lookup UI must be implemented as a searchable picker or autocomplete; do not use long static dropdowns for operational datasets such as items, customers, vendors, or document-linked records.
|
||||
- Preserve the current dense operations UI style on active module pages: compact controls, tighter card padding, and shorter empty states unless a screen has a clear reason to be more spacious.
|
||||
|
||||
## Backend rules
|
||||
|
||||
@@ -17,6 +17,8 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
|
||||
const [form, setForm] = useState<SalesDocumentInput>(emptySalesDocumentInput);
|
||||
const [status, setStatus] = useState(mode === "create" ? `Create a new ${config.singularLabel.toLowerCase()}.` : `Loading ${config.singularLabel.toLowerCase()}...`);
|
||||
const [customers, setCustomers] = useState<SalesCustomerOptionDto[]>([]);
|
||||
const [customerSearchTerm, setCustomerSearchTerm] = useState("");
|
||||
const [customerPickerOpen, setCustomerPickerOpen] = useState(false);
|
||||
const [itemOptions, setItemOptions] = useState<InventoryItemOptionDto[]>([]);
|
||||
const [lineSearchTerms, setLineSearchTerms] = useState<string[]>([]);
|
||||
const [activeLinePicker, setActiveLinePicker] = useState<number | null>(null);
|
||||
@@ -54,6 +56,7 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
|
||||
position: line.position,
|
||||
})),
|
||||
});
|
||||
setCustomerSearchTerm(document.customerName);
|
||||
setLineSearchTerms(document.lines.map((line: SalesDocumentDetailDto["lines"][number]) => line.itemSku));
|
||||
setStatus(`${config.singularLabel} loaded.`);
|
||||
})
|
||||
@@ -67,6 +70,10 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
|
||||
setForm((current: SalesDocumentInput) => ({ ...current, [key]: value }));
|
||||
}
|
||||
|
||||
function getSelectedCustomerName(customerId: string) {
|
||||
return customers.find((customer) => customer.id === customerId)?.name ?? "";
|
||||
}
|
||||
|
||||
function updateLine(index: number, nextLine: SalesLineInput) {
|
||||
setForm((current: SalesDocumentInput) => ({
|
||||
...current,
|
||||
@@ -152,18 +159,73 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
|
||||
<div className="grid gap-3 xl:grid-cols-4">
|
||||
<label className="block xl:col-span-2">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Customer</span>
|
||||
<select
|
||||
value={form.customerId}
|
||||
onChange={(event) => updateField("customerId", event.target.value)}
|
||||
className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
|
||||
>
|
||||
<option value="">Select customer</option>
|
||||
{customers.map((customer) => (
|
||||
<option key={customer.id} value={customer.id}>
|
||||
{customer.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="relative">
|
||||
<input
|
||||
value={customerSearchTerm}
|
||||
onChange={(event) => {
|
||||
setCustomerSearchTerm(event.target.value);
|
||||
updateField("customerId", "");
|
||||
setCustomerPickerOpen(true);
|
||||
}}
|
||||
onFocus={() => setCustomerPickerOpen(true)}
|
||||
onBlur={() => {
|
||||
window.setTimeout(() => {
|
||||
setCustomerPickerOpen(false);
|
||||
if (form.customerId) {
|
||||
setCustomerSearchTerm(getSelectedCustomerName(form.customerId));
|
||||
}
|
||||
}, 120);
|
||||
}}
|
||||
placeholder="Search customer"
|
||||
className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
|
||||
/>
|
||||
{customerPickerOpen ? (
|
||||
<div className="absolute z-20 mt-2 max-h-64 w-full overflow-y-auto rounded-2xl border border-line/70 bg-surface shadow-panel">
|
||||
{customers
|
||||
.filter((customer) => {
|
||||
const query = customerSearchTerm.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
customer.name.toLowerCase().includes(query) ||
|
||||
customer.email.toLowerCase().includes(query)
|
||||
);
|
||||
})
|
||||
.slice(0, 12)
|
||||
.map((customer) => (
|
||||
<button
|
||||
key={customer.id}
|
||||
type="button"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
updateField("customerId", customer.id);
|
||||
setCustomerSearchTerm(customer.name);
|
||||
setCustomerPickerOpen(false);
|
||||
}}
|
||||
className="block w-full border-b border-line/50 px-4 py-2 text-left text-sm transition last:border-b-0 hover:bg-page/70"
|
||||
>
|
||||
<div className="font-semibold text-text">{customer.name}</div>
|
||||
<div className="mt-1 text-xs text-muted">{customer.email}</div>
|
||||
</button>
|
||||
))}
|
||||
{customers.filter((customer) => {
|
||||
const query = customerSearchTerm.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return customer.name.toLowerCase().includes(query) || customer.email.toLowerCase().includes(query);
|
||||
}).length === 0 ? (
|
||||
<div className="px-2 py-2 text-sm text-muted">No matching customers found.</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 min-h-5 text-xs text-muted">
|
||||
{form.customerId ? getSelectedCustomerName(form.customerId) : "No customer selected"}
|
||||
</div>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Status</span>
|
||||
|
||||
Reference in New Issue
Block a user