Trips
model.js 203 lines
Files · 9
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203import manifest from "./manifest.json";
import { DDL } from "./schema.js";
import {
runtime,
localDate,
validDate,
addDays,
nowISO,
uid,
nonblank,
} from "./runtime.js";
export function createModel(L) {
const rt = runtime(L, manifest, DDL);
async function trip(id) {
const row = (await L.db.query("SELECT * FROM trips WHERE id=?", [id]))[0];
if (!row) throw new Error("Choose an existing trip.");
return row;
}
async function addTrip(data) {
const title = nonblank(data.title, "Trip title"),
destination = nonblank(data.destination, "Destination"),
start_date = validDate(data.start_date),
end_date = validDate(data.end_date);
if (end_date < start_date)
throw new Error("End date must be on or after the start date.");
const row = {
id: uid(),
title,
destination,
start_date,
end_date,
created_at: nowISO(),
};
await L.db.exec(
"INSERT INTO trips VALUES (?,?,?,?,?,?)",
Object.values(row),
);
const { id, ...rest } = row;
await rt.publish("trip-created", { trip_id: id, ...rest });
return row;
}
async function addItem(data) {
rt.validate("listeners", "add-itinerary-item", data);
const parent = await trip(data.trip_id),
date = validDate(data.date),
title = nonblank(data.title, "Item title");
if (date < parent.start_date || date > parent.end_date)
throw new Error("Itinerary date must be within the trip dates.");
const row = {
id: uid(),
trip_id: parent.id,
kind: data.kind,
title,
date,
time: data.time ?? "",
location: data.location ?? "",
confirmation_code: data.confirmation_code ?? "",
created_at: nowISO(),
};
await L.db.exec(
"INSERT INTO itinerary_items VALUES (?,?,?,?,?,?,?,?,?)",
Object.values(row),
);
const { id, ...rest } = row;
await rt.publish("item-added", { item_id: id, ...rest });
}
async function addPacking(data) {
rt.validate("listeners", "add-packing-item", data);
await trip(data.trip_id);
await L.db.exec(
"INSERT INTO packing_items (id,trip_id,label,quantity,created_at) VALUES (?,?,?,?,?)",
[
uid(),
data.trip_id,
nonblank(data.label, "Packing item"),
data.quantity ?? 1,
nowISO(),
],
);
}
const ready = rt.ready.then(() => {
rt.listen("add-itinerary-item", addItem);
rt.listen("add-packing-item", addPacking);
return rt.run(() => rt.flush());
});
return {
ready,
subscribe: rt.subscribe,
load: async () => {
await ready;
return {
trips: await L.db.query("SELECT * FROM trips ORDER BY start_date,id"),
items: await L.db.query(
"SELECT * FROM itinerary_items ORDER BY date, CASE WHEN time='' THEN 1 ELSE 0 END, time, created_at",
),
packing: await L.db.query(
"SELECT * FROM packing_items ORDER BY created_at,id",
),
today: localDate(),
pending: rt.deliveryError,
};
},
addTrip: (data) =>
rt.run(async () => {
const row = await addTrip(data);
await rt.flush();
return row;
}),
addItem: (data) =>
rt.run(async () => {
await addItem(data);
await rt.flush();
}),
addPacking: (data) => rt.run(() => addPacking(data)),
toggle: (item) =>
rt.run(async () => {
const current = (
await L.db.query("SELECT * FROM packing_items WHERE id=?", [item.id])
)[0];
if (!current) throw new Error("Packing item no longer exists.");
const checked = !current.checked,
stamp = checked ? nowISO() : null;
await L.db.exec(
"UPDATE packing_items SET checked=?,checked_at=? WHERE id=?",
[checked ? 1 : 0, stamp, current.id],
);
if (checked)
await rt.publish("packing-item-checked", {
packing_item_id: current.id,
trip_id: current.trip_id,
label: current.label,
quantity: current.quantity,
checked: true,
checked_at: stamp,
});
await rt.flush();
}),
removeItem: (id) =>
rt.run(() => L.db.exec("DELETE FROM itinerary_items WHERE id=?", [id])),
removePacking: (id) =>
rt.run(() => L.db.exec("DELETE FROM packing_items WHERE id=?", [id])),
tick: () => rt.run(() => rt.flush()),
sample: () =>
rt.run(async () => {
if ((await L.db.query("SELECT id FROM trips")).length)
throw new Error(
"Sample data can only be added to an empty trip list.",
);
const start = addDays(localDate(), 10),
end = addDays(start, 4);
const t = await addTrip({
title: "A few days in Lisbon",
destination: "Lisbon, Portugal",
start_date: start,
end_date: end,
});
await addItem({
trip_id: t.id,
kind: "flight",
title: "Flight to Lisbon",
date: start,
time: "09:40",
location: "Departure terminal 2",
confirmation_code: "SAMPLE-4K2",
});
await addItem({
trip_id: t.id,
kind: "lodging",
title: "Check in · Alfama apartment",
date: start,
time: "15:00",
location: "Alfama, Lisbon",
confirmation_code: "SAMPLE-891",
});
await addItem({
trip_id: t.id,
kind: "activity",
title: "Walk along the waterfront",
date: addDays(start, 1),
time: "10:00",
location: "Praça do Comércio",
});
await addItem({
trip_id: t.id,
kind: "activity",
title: "Day trip to Sintra",
date: addDays(start, 2),
time: "08:30",
location: "Rossio station",
});
for (const [label, quantity] of [
["Passport", 1],
["Walking shoes", 1],
["Phone charger", 1],
["T-shirts", 4],
["Light jacket", 1],
])
await addPacking({ trip_id: t.id, label, quantity });
await rt.flush();
return t;
}),
};
}