Add trip checklists with rule-based packing advice

Each trip gets a checklist whose items group under free-text categories
(Documents, Clothing, Toiletries, Health, Electronics, Extras first, then
any custom ones alphabetically). Items are either shared — every member
sees and can tick them, and checked_by records who — or personal to one
member, which nobody else can see or touch. Items carry an optional
quantity, drag-reorder within their category, and "Uncheck all" resets the
list for the trip home.

The "Suggestions" modal is deterministic, offline advice derived from the
trip itself (src/server/util/packing.js) — no LLM and no external calls, so
it stays unit-testable and works on a self-hosted box. Nights scale
clothing quantities, flights add liquids/power-bank/check-in, rentals add
licence + IDP, ferries add motion-sickness tablets, tropical stops add sun
cream and repellent, and the destination country picks the plug type from a
bundled ~50-country table. Every suggestion carries a short reason, and
already-added ones are keyed by suggestion_key so they can't be duplicated.

Two rules deliberately differ from the naive reading, both regression-tested:
a latitude floor stops a December trip to Bangkok being tagged cold as well
as tropical, and only a flight segment's arrival airport counts, since the
first segment's departure airport is home rather than a destination.

checklist_items is a new table, so the existing CREATE TABLE IF NOT EXISTS
path creates it on upgrade; no MIGRATIONS entry is needed and existing data
is untouched.

docs/API.md documents the full contract. 113/113 tests pass.
This commit is contained in:
2026-08-03 18:18:25 +07:00
parent e2c3089c25
commit e342cd9a91
16 changed files with 1913 additions and 2 deletions
+10
View File
@@ -84,6 +84,16 @@ export const api = {
},
geocode: (q) => get(`/api/geocode?q=${encodeURIComponent(q)}`),
airports: (q) => get(`/api/airports?q=${encodeURIComponent(q)}`),
checklist: {
list: (tripId) => get(`/api/trips/${tripId}/checklist`),
create: (tripId, payload) => post(`/api/trips/${tripId}/checklist`, payload),
update: (itemId, patchBody) => patch(`/api/checklist/${itemId}`, patchBody),
remove: (itemId) => del(`/api/checklist/${itemId}`),
reset: (tripId) => post(`/api/trips/${tripId}/checklist/reset`, {}),
suggestions: (tripId) => get(`/api/trips/${tripId}/checklist/suggestions`),
addSuggestions: (tripId, keys, personal) =>
post(`/api/trips/${tripId}/checklist/suggestions`, personal === undefined ? { keys } : { keys, personal }),
},
};
export default api;