The Purchase Order form never reached the server. Creating a PO with two
or more "Manual Entry" rows fired N parallel POSTs to
/api/collections/stock_items/records via Promise.all. The PocketBase JS
SDK auto-cancels any in-flight request whose method + path match a newer
one, so the first POST was aborted with:
ClientResponseError 0: The request was autocancelled.
Promise.all then rejected before the PO create ever ran, so the PO was
never created - only an orphan stock item per attempt. Confirmed against
the production PB logs: two stock_items POSTs today, zero
purchase_orders POSTs, and two orphan stock_items rows with no PO.
Fixes:
- src/lib/pocketbase.ts: pb.autoCancellation(false) on the browser
client. Every server-side client in this repo already did this
(scripts/*.mjs, server/po-api.mjs) - the browser client was the one
place it was missing.
- PurchaseOrderForm: resolve rows sequentially instead of Promise.all,
and build the snapshot in the same pass so manual rows carry the real
stock item id instead of "".
- PurchaseOrderForm: disable the submit button while submitting, so a
double click cannot create two POs.
- PurchaseOrderForm: compare SKUs trimmed, so a stray trailing space
does not create a duplicate stock item.
- po-photo-ingest.mjs, jei_skill.ts, setup-users.js: same
autoCancellation(false) guard.
Item descriptions were cut off with an ellipsis. Long names like
"PRO132T-C24 - CARBON BRUSH EACH, PRO152T" lost their tail, which on a
quotation reads as a mistake.
Descriptions now wrap onto as many lines as they need and the row grows
to fit, so the full name is always printed. SKU, quantity, price and
total stay on the first line of the row.
While wrapping, the quote PDF gained the page handling the purchase
order PDF already had: a row that no longer fits moves to a new page
with the column headers repeated, and the totals and notes blocks
break to a new page rather than running off the bottom. Before this
the quote PDF had no page breaks at all, so a long quote simply ran
off the page.
Addresses also keep their line breaks now instead of being flattened
into one line.
The two quote PDF builders - "Download PDF" and the copy stored in
PocketBase - were near-identical copies of ~275 lines. They now share
one builder (quotePdf.ts), so a layout fix can no longer land in one
and miss the other.
Two things that meant going into PocketBase by hand or jumping between
screens.
Deleting a quote
- Quotes had no delete action - a test quote could only be removed
through the PocketBase admin UI. Purchase orders already soft delete
by flipping a `deleted` flag, so quotes now do the same: the Delete
action confirms first, then hides the quote from the list while the
record and its PDF stay in PocketBase for audit.
- The quotes list filters out soft-deleted quotes, so a deleted quote
is out of the way but recoverable by clearing the flag.
Adding a customer while writing a quote
- Typing a customer name that was not in the list used to create a
name-only stub, which then had to be edited from the Customers screen
before the quote could show an address or VAT number.
- The picker now opens the full customer form right there - address,
VAT number, email and phone - with the name pre-filled. The new
customer is created, added to the list and selected, so the quote
carries on and the PDF has the details.
A quote should look like a document from a real company, so this adds
the customer details a quote needs, gives every quote a valid-until
date, and makes the customer picker usable once there are 20+ of them.
Customer details on quotes
- The `customers` collection only had a name, so the quote PDF's
"Customer Information" block could only ever print a name. Added
optional address, VAT number, email and phone fields.
- The PDF block now grows to fit whatever is on file and prints the
VAT number and contact line, so a filled-in customer produces a
professional quote.
- The quote details dialog shows the same details.
- The customer form gained the new fields, and "Edit customer" now
works - it was a menu item that did nothing, so there was no way
to add a VAT number to an existing customer.
- The customers table shows Address and VAT Number columns.
Valid until
- Quotes were created with an empty validUntil, so the PDF printed
"Valid Until: N/A". The form now defaults it to 30 days after the
quote date and keeps it in step when the date moves. The three
existing quotes without one were backfilled the same way.
Searchable customer picker
- The customer dropdown listed every customer in a plain select with
no way to add one that was missing. It is now a searchable combobox
(by name, address or VAT number) that offers to create the customer
when nothing matches, and creates it inline.
Sidebar
- Settings moved to a gear in the corner next to the app name, and
Logs moved into Settings as a tab. The nav is now eight items with
no grouping - grouping them would add a layer for no gain at this
size. The old /logs link still lands on the logs tab.
Also fixes a real PDF bug: item descriptions were cut to 40 characters,
which is wider than the description column, so long names printed on
top of the SKU column. Descriptions and SKUs are now measured against
the real column width and truncated with an ellipsis.
Live PocketBase changes (see scripts/add-customer-fields.mjs):
- customers update and delete rules were null (superusers only), so a
member could not edit the customer they had just added. They now
match the other collections.
Adds a Settings > Users screen where members can be added, turned off
and removed, and fixes three defects found while auditing the app on
a phone.
User management
- Settings is a real page with a Users section: add a member (name,
email, password), flip "Can sign in" to block or restore access
without deleting their history, or remove them outright.
- The app has no roles - membership is just the active flag - so the
Settings route no longer requires the "admin" role, which left it
unreachable for every real member. Settings is now in the sidebar.
- New members are created with active: true. AuthContext refuses to
sign in a user whose active flag is false, and the field had no
default, so previously created users were locked out of the app.
Live PocketBase rules for the `users` collection (see
scripts/fix-users-collection.mjs):
- createRule was "" which means PUBLIC - any anonymous visitor could
create an account on the instance. It now requires an authenticated
member.
- listRule was "id = @request.auth.id", so a member could only ever
see their own record and no member list could be rendered. It now
matches every other collection in the app: @request.auth.id != "".
Totals always showed R0.00
- form.watch("items") returns the same array instance that
react-hook-form mutates in place, so the useMemo keyed on it never
recomputed. Both the quote and purchase order totals stayed at zero
however many items were added. Switched to useWatch with compute,
which re-runs whenever the watched field really changes.
Mobile layout
- The page was zoomed out on phones: flex items default to
min-width:auto, so a wide table stretched the layout past the
viewport. Added min-w-0 to the layout chain.
- The sr-only labels inside the tables are position:absolute with no
positioned ancestor, so they escaped the table's overflow-x-auto
clip and stretched the document. Made the scroll containers
relative.
- DialogContent was w-full plus an mx-4 gutter, which overflowed the
viewport by 16px and clipped the right edge of every form. The
component now owns the gutter itself.
Verified at 390x844: all pages and the quote, purchase order and add
user dialogs fit the viewport with no horizontal overflow.
Quotes and purchase orders were silently losing their line items.
Quote items field was a multi-relation to stock_items, but PocketBase
stored only the last id as a plain string no matter how it was sent
(JSON string via FormData, raw array, or repeated FormData entries).
Every quote created through the UI had `items: ""` or `items: "<id>"`.
Recreated the field as JSON so an array of stock_item ids round-trips
(companion fields quantities/unitPrices already line up with it).
PO snapshot_items had the same class of bug from the other direction:
String(obj) coerced the snapshot array to "[object Object]", which
PocketBase stored verbatim, rendering the PO as N rows of
"Unknown / £0.00". The form now sends JSON-stringified arrays, and
the reader only trusts a snapshot whose rows actually carry a name.
Also fixes:
- Quote item lines are hydrated from stock_items; the parallel
quantities/unitPrices arrays are the source of truth, so quotes
written before the schema fix still render and total correctly.
- Quote form rejects stock rows where no item was actually picked
from the dropdown, instead of saving an empty stockItemId.
- PO PDF filename is now the plain order number (PO-20261015-001.pdf).
Adds the quotes.items schema migration plus three dev scripts for
creating a quote, re-applying the field fix and rendering a test PDF.
- Introduced `generate-icons.mjs` script to automate the creation of PWA icons and favicons from the provided SVG logo.
- Generated icons include:
- PWA icons: 192x192 and 512x512
- Maskable icon with 12% padding: 512x512
- Favicons: 32x32, 16x16
- Apple-touch icon: 180x180
- Added a new PNG file for the maskable icon.