trips) adding transportation, checklist, and experience planning
This commit is contained in:
@@ -428,9 +428,15 @@ export function deleteOperatorTourDay(id: number): void {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Operator Tour Day Plans (transport / lodging templates per day) ---
|
||||
// --- Operator Tour Day Plans (template plans per day) ---
|
||||
|
||||
export type OperatorTourDayPlanType = 'transport' | 'lodging';
|
||||
export type OperatorTourDayPlanType =
|
||||
| 'transport'
|
||||
| 'lodging'
|
||||
| 'activity'
|
||||
| 'restaurant'
|
||||
| 'packing'
|
||||
| 'todo';
|
||||
|
||||
export interface OperatorTourDayPlan {
|
||||
id: number;
|
||||
@@ -449,6 +455,23 @@ export interface OperatorTourDayPlan {
|
||||
country?: string | null;
|
||||
country_code?: string | null;
|
||||
postal_code?: string | null;
|
||||
// Transport-only template details (other transport mechanism)
|
||||
transport_kind?: string | null;
|
||||
start_date?: string | null;
|
||||
start_time?: string | null;
|
||||
start_timezone?: string | null;
|
||||
end_date?: string | null;
|
||||
end_time?: string | null;
|
||||
end_timezone?: string | null;
|
||||
// Activity/restaurant fields
|
||||
booking_id?: string | null;
|
||||
total_cost?: number | null;
|
||||
description?: string | null;
|
||||
website?: string | null;
|
||||
address?: string | null;
|
||||
contact_number?: string | null;
|
||||
// Packing/todo fields
|
||||
items_json?: string | null;
|
||||
}
|
||||
|
||||
export function listPlansForOperatorTour(tourId: number): OperatorTourDayPlan[] {
|
||||
@@ -474,7 +497,31 @@ export function createOperatorTourDayPlan(
|
||||
country?: string | null;
|
||||
country_code?: string | null;
|
||||
postal_code?: string | null;
|
||||
}
|
||||
},
|
||||
transportFields?: {
|
||||
transport_kind?: string | null;
|
||||
start_date?: string | null;
|
||||
start_time?: string | null;
|
||||
start_timezone?: string | null;
|
||||
end_date?: string | null;
|
||||
end_time?: string | null;
|
||||
end_timezone?: string | null;
|
||||
},
|
||||
experienceFields?: {
|
||||
booking_id?: string | null;
|
||||
total_cost?: number | null;
|
||||
description?: string | null;
|
||||
website?: string | null;
|
||||
address?: string | null;
|
||||
contact_number?: string | null;
|
||||
start_date?: string | null;
|
||||
start_time?: string | null;
|
||||
start_timezone?: string | null;
|
||||
end_date?: string | null;
|
||||
end_time?: string | null;
|
||||
end_timezone?: string | null;
|
||||
},
|
||||
checklistItemsJson?: string | null
|
||||
): OperatorTourDayPlan {
|
||||
const maxPos = db.get<{ pos: number }>(
|
||||
'SELECT COALESCE(MAX(position), -1) + 1 as pos FROM operator_tour_day_plans WHERE operator_tour_day_id = ?',
|
||||
@@ -483,32 +530,68 @@ export function createOperatorTourDayPlan(
|
||||
const position = maxPos?.pos ?? 0;
|
||||
|
||||
const l = lodgingFields;
|
||||
const cols =
|
||||
type === 'lodging' && l
|
||||
? 'operator_tour_day_id, type, title, notes, position, chain, address_line1, address_line2, city_name, country, country_code, postal_code'
|
||||
: 'operator_tour_day_id, type, title, notes, position';
|
||||
const vals =
|
||||
type === 'lodging' && l
|
||||
? [
|
||||
dayId,
|
||||
type,
|
||||
title.trim(),
|
||||
notes?.trim() || null,
|
||||
position,
|
||||
l.chain?.trim() || null,
|
||||
l.address_line1?.trim() || null,
|
||||
l.address_line2?.trim() || null,
|
||||
l.city_name?.trim() || null,
|
||||
l.country?.trim() || null,
|
||||
l.country_code?.trim() || null,
|
||||
l.postal_code?.trim() || null
|
||||
]
|
||||
: [dayId, type, title.trim(), notes?.trim() || null, position];
|
||||
const t = transportFields;
|
||||
let cols = 'operator_tour_day_id, type, title, notes, position';
|
||||
let vals: Array<string | number | null> = [
|
||||
dayId,
|
||||
type,
|
||||
title.trim(),
|
||||
notes?.trim() || null,
|
||||
position
|
||||
];
|
||||
if (type === 'lodging' && l) {
|
||||
cols += ', chain, address_line1, address_line2, city_name, country, country_code, postal_code';
|
||||
vals = [
|
||||
...vals,
|
||||
l.chain?.trim() || null,
|
||||
l.address_line1?.trim() || null,
|
||||
l.address_line2?.trim() || null,
|
||||
l.city_name?.trim() || null,
|
||||
l.country?.trim() || null,
|
||||
l.country_code?.trim() || null,
|
||||
l.postal_code?.trim() || null
|
||||
];
|
||||
}
|
||||
if (type === 'transport') {
|
||||
cols +=
|
||||
', transport_kind, start_date, start_time, start_timezone, end_date, end_time, end_timezone';
|
||||
vals = [
|
||||
...vals,
|
||||
t?.transport_kind?.trim() || 'other',
|
||||
t?.start_date?.trim() || null,
|
||||
t?.start_time?.trim() || null,
|
||||
t?.start_timezone?.trim() || null,
|
||||
t?.end_date?.trim() || null,
|
||||
t?.end_time?.trim() || null,
|
||||
t?.end_timezone?.trim() || null
|
||||
];
|
||||
}
|
||||
if (type === 'activity' || type === 'restaurant') {
|
||||
const e = experienceFields;
|
||||
cols +=
|
||||
', booking_id, total_cost, description, website, address, contact_number, start_date, start_time, start_timezone, end_date, end_time, end_timezone';
|
||||
vals = [
|
||||
...vals,
|
||||
e?.booking_id?.trim() || null,
|
||||
e?.total_cost ?? null,
|
||||
e?.description?.trim() || null,
|
||||
e?.website?.trim() || null,
|
||||
e?.address?.trim() || null,
|
||||
e?.contact_number?.trim() || null,
|
||||
e?.start_date?.trim() || null,
|
||||
e?.start_time?.trim() || null,
|
||||
e?.start_timezone?.trim() || null,
|
||||
e?.end_date?.trim() || null,
|
||||
e?.end_time?.trim() || null,
|
||||
e?.end_timezone?.trim() || null
|
||||
];
|
||||
}
|
||||
if (type === 'packing' || type === 'todo') {
|
||||
cols += ', items_json';
|
||||
vals = [...vals, checklistItemsJson?.trim() || null];
|
||||
}
|
||||
const placeholders = vals.map(() => '?').join(', ');
|
||||
db.run(
|
||||
`INSERT INTO operator_tour_day_plans (${cols}) VALUES (${placeholders})`,
|
||||
vals
|
||||
);
|
||||
db.run(`INSERT INTO operator_tour_day_plans (${cols}) VALUES (${placeholders})`, vals);
|
||||
return db.get<OperatorTourDayPlan>(
|
||||
'SELECT * FROM operator_tour_day_plans WHERE id = last_insert_rowid()'
|
||||
)!;
|
||||
@@ -526,14 +609,28 @@ export function updateOperatorTourDayPlan(
|
||||
country?: string | null;
|
||||
country_code?: string | null;
|
||||
postal_code?: string | null;
|
||||
transport_kind?: string | null;
|
||||
start_date?: string | null;
|
||||
start_time?: string | null;
|
||||
start_timezone?: string | null;
|
||||
end_date?: string | null;
|
||||
end_time?: string | null;
|
||||
end_timezone?: string | null;
|
||||
booking_id?: string | null;
|
||||
total_cost?: number | null;
|
||||
description?: string | null;
|
||||
website?: string | null;
|
||||
address?: string | null;
|
||||
contact_number?: string | null;
|
||||
items_json?: string | null;
|
||||
}
|
||||
): void {
|
||||
const setClauses: string[] = [];
|
||||
const values: (string | null | number)[] = [];
|
||||
const optional = (key: string, v: string | null | undefined) => {
|
||||
const optional = (key: string, v: string | number | null | undefined) => {
|
||||
if (v !== undefined) {
|
||||
setClauses.push(`${key} = ?`);
|
||||
values.push(v === null ? null : (typeof v === 'string' ? v.trim() : v));
|
||||
values.push(v === null ? null : typeof v === 'string' ? v.trim() : v);
|
||||
}
|
||||
};
|
||||
optional('title', updates.title);
|
||||
@@ -545,13 +642,24 @@ export function updateOperatorTourDayPlan(
|
||||
optional('country', updates.country ?? undefined);
|
||||
optional('country_code', updates.country_code ?? undefined);
|
||||
optional('postal_code', updates.postal_code ?? undefined);
|
||||
optional('transport_kind', updates.transport_kind ?? undefined);
|
||||
optional('start_date', updates.start_date ?? undefined);
|
||||
optional('start_time', updates.start_time ?? undefined);
|
||||
optional('start_timezone', updates.start_timezone ?? undefined);
|
||||
optional('end_date', updates.end_date ?? undefined);
|
||||
optional('end_time', updates.end_time ?? undefined);
|
||||
optional('end_timezone', updates.end_timezone ?? undefined);
|
||||
optional('booking_id', updates.booking_id ?? undefined);
|
||||
optional('total_cost', updates.total_cost ?? undefined);
|
||||
optional('description', updates.description ?? undefined);
|
||||
optional('website', updates.website ?? undefined);
|
||||
optional('address', updates.address ?? undefined);
|
||||
optional('contact_number', updates.contact_number ?? undefined);
|
||||
optional('items_json', updates.items_json ?? undefined);
|
||||
if (setClauses.length === 0) return;
|
||||
setClauses.push("updated_at = datetime('now')");
|
||||
values.push(id);
|
||||
db.run(
|
||||
`UPDATE operator_tour_day_plans SET ${setClauses.join(', ')} WHERE id = ?`,
|
||||
values
|
||||
);
|
||||
db.run(`UPDATE operator_tour_day_plans SET ${setClauses.join(', ')} WHERE id = ?`, values);
|
||||
}
|
||||
|
||||
export function deleteOperatorTourDayPlan(id: number): void {
|
||||
|
||||
161
src/lib/server/checklists.ts
Normal file
161
src/lib/server/checklists.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { db } from './db/index.js';
|
||||
import type { PlanStatus } from './plans.js';
|
||||
import { createPlan } from './plans.js';
|
||||
|
||||
export type ChecklistType = 'packing' | 'todo';
|
||||
|
||||
export interface Checklist {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ChecklistItem {
|
||||
id: string;
|
||||
checklist_id: string;
|
||||
content: string;
|
||||
is_checked: number;
|
||||
position: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ChecklistWithItems extends Checklist {
|
||||
items: ChecklistItem[];
|
||||
}
|
||||
|
||||
export interface CreateChecklistInput {
|
||||
tripId: string;
|
||||
userId: string;
|
||||
type: ChecklistType;
|
||||
name: string;
|
||||
parentId?: string;
|
||||
status?: PlanStatus;
|
||||
items?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateChecklistItemInput {
|
||||
id?: string;
|
||||
content: string;
|
||||
isChecked?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateChecklistInput {
|
||||
checklistId: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
status?: PlanStatus;
|
||||
items: UpdateChecklistItemInput[];
|
||||
}
|
||||
|
||||
export function createChecklist(input: CreateChecklistInput): ChecklistWithItems {
|
||||
const plan = createPlan({
|
||||
tripId: input.tripId,
|
||||
userId: input.userId,
|
||||
type: input.type,
|
||||
title: input.name.trim(),
|
||||
parentId: input.parentId ?? null,
|
||||
status: input.status ?? 'idea'
|
||||
});
|
||||
|
||||
const checklistId = randomUUID();
|
||||
db.run(`INSERT INTO checklists (id, plan_id) VALUES (?, ?)`, [checklistId, plan.id]);
|
||||
|
||||
const cleanItems = (input.items ?? []).map((item) => item.trim()).filter(Boolean);
|
||||
cleanItems.forEach((content, index) => {
|
||||
db.run(
|
||||
`INSERT INTO checklist_items (id, checklist_id, content, is_checked, position)
|
||||
VALUES (?, ?, ?, 0, ?)`,
|
||||
[randomUUID(), checklistId, content, index]
|
||||
);
|
||||
});
|
||||
|
||||
const checklist = db.get<Checklist>('SELECT * FROM checklists WHERE id = ?', [checklistId])!;
|
||||
return {
|
||||
...checklist,
|
||||
items: db.all<ChecklistItem>(
|
||||
'SELECT * FROM checklist_items WHERE checklist_id = ? ORDER BY position ASC, created_at ASC',
|
||||
[checklistId]
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
export function getChecklistsForTrip(
|
||||
tripId: string,
|
||||
userId: string,
|
||||
type?: ChecklistType
|
||||
): ChecklistWithItems[] {
|
||||
const params: unknown[] = [tripId, userId];
|
||||
let typeFilter = '';
|
||||
if (type) {
|
||||
typeFilter = ' AND p.type = ?';
|
||||
params.push(type);
|
||||
}
|
||||
|
||||
const checklists = db.all<Checklist>(
|
||||
`SELECT c.* FROM checklists c
|
||||
JOIN plans p ON p.id = c.plan_id
|
||||
WHERE p.trip_id = ? AND p.user_id = ?${typeFilter}
|
||||
ORDER BY p.position ASC, c.created_at ASC`,
|
||||
params
|
||||
);
|
||||
|
||||
return checklists.map((checklist) => ({
|
||||
...checklist,
|
||||
items: db.all<ChecklistItem>(
|
||||
'SELECT * FROM checklist_items WHERE checklist_id = ? ORDER BY position ASC, created_at ASC',
|
||||
[checklist.id]
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
export function updateChecklist(input: UpdateChecklistInput): void {
|
||||
const existing = db.get<{ checklist_id: string; plan_id: string }>(
|
||||
`SELECT c.id as checklist_id, c.plan_id
|
||||
FROM checklists c
|
||||
JOIN plans p ON p.id = c.plan_id
|
||||
WHERE c.id = ? AND p.user_id = ?`,
|
||||
[input.checklistId, input.userId]
|
||||
);
|
||||
if (!existing) throw new Error('Checklist not found or not authorized');
|
||||
|
||||
db.run(`UPDATE plans SET status = ?, title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [
|
||||
input.status ?? 'idea',
|
||||
input.name.trim(),
|
||||
existing.plan_id
|
||||
]);
|
||||
|
||||
db.run('DELETE FROM checklist_items WHERE checklist_id = ?', [input.checklistId]);
|
||||
const cleanItems = input.items
|
||||
.map((item) => ({ content: item.content.trim(), isChecked: item.isChecked }))
|
||||
.filter((item) => item.content.length > 0);
|
||||
cleanItems.forEach((item, index) => {
|
||||
db.run(
|
||||
`INSERT INTO checklist_items (id, checklist_id, content, is_checked, position)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[randomUUID(), input.checklistId, item.content, item.isChecked ? 1 : 0, index]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleChecklistItem(input: {
|
||||
itemId: string;
|
||||
userId: string;
|
||||
isChecked: boolean;
|
||||
}): void {
|
||||
const row = db.get<{ id: string }>(
|
||||
`SELECT ci.id
|
||||
FROM checklist_items ci
|
||||
JOIN checklists c ON c.id = ci.checklist_id
|
||||
JOIN plans p ON p.id = c.plan_id
|
||||
WHERE ci.id = ? AND p.user_id = ?`,
|
||||
[input.itemId, input.userId]
|
||||
);
|
||||
if (!row) throw new Error('Checklist item not found or not authorized');
|
||||
db.run(`UPDATE checklist_items SET is_checked = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [
|
||||
input.isChecked ? 1 : 0,
|
||||
input.itemId
|
||||
]);
|
||||
}
|
||||
@@ -211,6 +211,100 @@ export function runMigrations(db: Database): void {
|
||||
)
|
||||
`);
|
||||
|
||||
// Private vehicle transportation details - links to a transport plan
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS private_vehicles (
|
||||
id TEXT PRIMARY KEY,
|
||||
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
||||
start_address TEXT NOT NULL,
|
||||
end_address TEXT NOT NULL,
|
||||
departure_date TEXT,
|
||||
departure_time TEXT,
|
||||
departure_timezone TEXT,
|
||||
arrival_date TEXT,
|
||||
arrival_time TEXT,
|
||||
arrival_timezone TEXT,
|
||||
start_plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
||||
end_plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
for (const col of [
|
||||
'departure_date TEXT',
|
||||
'departure_time TEXT',
|
||||
'departure_timezone TEXT',
|
||||
'arrival_date TEXT',
|
||||
'arrival_time TEXT',
|
||||
'arrival_timezone TEXT'
|
||||
]) {
|
||||
try {
|
||||
db.run(`ALTER TABLE private_vehicles ADD COLUMN ${col}`);
|
||||
} catch {
|
||||
/* already exists */
|
||||
}
|
||||
}
|
||||
|
||||
// Other transportation details - links to a transport plan
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS other_transports (
|
||||
id TEXT PRIMARY KEY,
|
||||
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
||||
start_date TEXT,
|
||||
start_time TEXT,
|
||||
start_timezone TEXT,
|
||||
end_date TEXT,
|
||||
end_time TEXT,
|
||||
end_timezone TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Activity / restaurant details - links to a plan
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS experience_plans (
|
||||
id TEXT PRIMARY KEY,
|
||||
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
||||
booking_id TEXT,
|
||||
total_cost REAL,
|
||||
description TEXT,
|
||||
website TEXT,
|
||||
address TEXT,
|
||||
contact_number TEXT,
|
||||
start_date TEXT,
|
||||
start_time TEXT,
|
||||
start_timezone TEXT,
|
||||
end_date TEXT,
|
||||
end_time TEXT,
|
||||
end_timezone TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Checklist master table (packing/todo details come from linked plan.type)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS checklists (
|
||||
id TEXT PRIMARY KEY,
|
||||
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS checklist_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
checklist_id TEXT NOT NULL REFERENCES checklists(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
is_checked INTEGER NOT NULL DEFAULT 0,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Lodgings table - links to a plan
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS lodgings (
|
||||
@@ -330,12 +424,12 @@ export function runMigrations(db: Database): void {
|
||||
)
|
||||
`);
|
||||
|
||||
// Template plans (transport/lodging) attached to operator tour days
|
||||
// Template plans attached to operator tour days
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS operator_tour_day_plans (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
operator_tour_day_id INTEGER NOT NULL REFERENCES operator_tour_days(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL CHECK (type IN ('transport', 'lodging')),
|
||||
type TEXT NOT NULL CHECK (type IN ('transport', 'lodging', 'activity', 'restaurant', 'packing', 'todo')),
|
||||
title TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
@@ -344,6 +438,65 @@ export function runMigrations(db: Database): void {
|
||||
)
|
||||
`);
|
||||
|
||||
// Migration: expand operator_tour_day_plans `type` CHECK constraint if it still only supports transport/lodging.
|
||||
try {
|
||||
const schema = db.get<{ sql: string }>(
|
||||
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'operator_tour_day_plans'`
|
||||
);
|
||||
const sql = schema?.sql?.toLowerCase() ?? '';
|
||||
if (sql && !sql.includes("'activity'")) {
|
||||
db.run(`
|
||||
CREATE TABLE operator_tour_day_plans__new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
operator_tour_day_id INTEGER NOT NULL REFERENCES operator_tour_days(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL CHECK (type IN ('transport', 'lodging', 'activity', 'restaurant', 'packing', 'todo')),
|
||||
title TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
chain TEXT,
|
||||
address_line1 TEXT,
|
||||
address_line2 TEXT,
|
||||
city_name TEXT,
|
||||
country TEXT,
|
||||
country_code TEXT,
|
||||
postal_code TEXT,
|
||||
transport_kind TEXT,
|
||||
start_date TEXT,
|
||||
start_time TEXT,
|
||||
start_timezone TEXT,
|
||||
end_date TEXT,
|
||||
end_time TEXT,
|
||||
end_timezone TEXT,
|
||||
booking_id TEXT,
|
||||
total_cost REAL,
|
||||
description TEXT,
|
||||
website TEXT,
|
||||
address TEXT,
|
||||
contact_number TEXT,
|
||||
items_json TEXT
|
||||
)
|
||||
`);
|
||||
db.run(`
|
||||
INSERT INTO operator_tour_day_plans__new (
|
||||
id, operator_tour_day_id, type, title, notes, position, created_at, updated_at,
|
||||
chain, address_line1, address_line2, city_name, country, country_code, postal_code,
|
||||
transport_kind, start_date, start_time, start_timezone, end_date, end_time, end_timezone
|
||||
)
|
||||
SELECT
|
||||
id, operator_tour_day_id, type, title, notes, position, created_at, updated_at,
|
||||
chain, address_line1, address_line2, city_name, country, country_code, postal_code,
|
||||
transport_kind, start_date, start_time, start_timezone, end_date, end_time, end_timezone
|
||||
FROM operator_tour_day_plans
|
||||
`);
|
||||
db.run('DROP TABLE operator_tour_day_plans');
|
||||
db.run('ALTER TABLE operator_tour_day_plans__new RENAME TO operator_tour_day_plans');
|
||||
}
|
||||
} catch {
|
||||
/* best-effort migration */
|
||||
}
|
||||
|
||||
// Lodging-specific fields for operator_tour_day_plans (type=lodging) — same shape as trip lodgings
|
||||
for (const col of [
|
||||
'chain TEXT',
|
||||
@@ -352,7 +505,21 @@ export function runMigrations(db: Database): void {
|
||||
'city_name TEXT',
|
||||
'country TEXT',
|
||||
'country_code TEXT',
|
||||
'postal_code TEXT'
|
||||
'postal_code TEXT',
|
||||
'transport_kind TEXT',
|
||||
'start_date TEXT',
|
||||
'start_time TEXT',
|
||||
'start_timezone TEXT',
|
||||
'end_date TEXT',
|
||||
'end_time TEXT',
|
||||
'end_timezone TEXT',
|
||||
'booking_id TEXT',
|
||||
'total_cost REAL',
|
||||
'description TEXT',
|
||||
'website TEXT',
|
||||
'address TEXT',
|
||||
'contact_number TEXT',
|
||||
'items_json TEXT'
|
||||
]) {
|
||||
try {
|
||||
db.run(`ALTER TABLE operator_tour_day_plans ADD COLUMN ${col}`);
|
||||
@@ -360,6 +527,15 @@ export function runMigrations(db: Database): void {
|
||||
/* already exists */
|
||||
}
|
||||
}
|
||||
try {
|
||||
db.run(
|
||||
`UPDATE operator_tour_day_plans
|
||||
SET transport_kind = 'other'
|
||||
WHERE type = 'transport' AND (transport_kind IS NULL OR TRIM(transport_kind) = '')`
|
||||
);
|
||||
} catch {
|
||||
/* best-effort migration */
|
||||
}
|
||||
|
||||
// Countries table — editable list for admin (used by country selector)
|
||||
db.run(`
|
||||
|
||||
164
src/lib/server/experiences.ts
Normal file
164
src/lib/server/experiences.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { db } from './db/index.js';
|
||||
import type { PlanStatus } from './plans.js';
|
||||
import { createPlan } from './plans.js';
|
||||
|
||||
export type ExperienceType = 'activity' | 'restaurant';
|
||||
|
||||
export interface ExperiencePlan {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
booking_id: string | null;
|
||||
total_cost: number | null;
|
||||
description: string | null;
|
||||
website: string | null;
|
||||
address: string | null;
|
||||
contact_number: string | null;
|
||||
start_date: string | null;
|
||||
start_time: string | null;
|
||||
start_timezone: string | null;
|
||||
end_date: string | null;
|
||||
end_time: string | null;
|
||||
end_timezone: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateExperienceInput {
|
||||
tripId: string;
|
||||
userId: string;
|
||||
type: ExperienceType;
|
||||
name: string;
|
||||
parentId?: string;
|
||||
status?: PlanStatus;
|
||||
bookingId?: string;
|
||||
totalCost?: number;
|
||||
description?: string;
|
||||
website?: string;
|
||||
address?: string;
|
||||
contactNumber?: string;
|
||||
startDate?: string;
|
||||
startTime?: string;
|
||||
startTimezone?: string;
|
||||
endDate?: string;
|
||||
endTime?: string;
|
||||
endTimezone?: string;
|
||||
}
|
||||
|
||||
export interface UpdateExperienceInput {
|
||||
experienceId: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
status?: PlanStatus;
|
||||
bookingId?: string;
|
||||
totalCost?: number;
|
||||
description?: string;
|
||||
website?: string;
|
||||
address?: string;
|
||||
contactNumber?: string;
|
||||
startDate?: string;
|
||||
startTime?: string;
|
||||
startTimezone?: string;
|
||||
endDate?: string;
|
||||
endTime?: string;
|
||||
endTimezone?: string;
|
||||
}
|
||||
|
||||
export function createExperience(input: CreateExperienceInput): ExperiencePlan {
|
||||
const plan = createPlan({
|
||||
tripId: input.tripId,
|
||||
userId: input.userId,
|
||||
type: input.type,
|
||||
title: input.name.trim(),
|
||||
notes: input.description?.trim() || null,
|
||||
parentId: input.parentId ?? null,
|
||||
status: input.status ?? 'idea'
|
||||
});
|
||||
|
||||
const experienceId = randomUUID();
|
||||
db.run(
|
||||
`INSERT INTO experience_plans (
|
||||
id, plan_id, booking_id, total_cost, description, website, address, contact_number,
|
||||
start_date, start_time, start_timezone, end_date, end_time, end_timezone
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
experienceId,
|
||||
plan.id,
|
||||
input.bookingId?.trim() || null,
|
||||
input.totalCost ?? null,
|
||||
input.description?.trim() || null,
|
||||
input.website?.trim() || null,
|
||||
input.address?.trim() || null,
|
||||
input.contactNumber?.trim() || null,
|
||||
input.startDate?.trim() || null,
|
||||
input.startTime?.trim() || null,
|
||||
input.startTimezone?.trim() || null,
|
||||
input.endDate?.trim() || null,
|
||||
input.endTime?.trim() || null,
|
||||
input.endTimezone?.trim() || null
|
||||
]
|
||||
);
|
||||
|
||||
return db.get<ExperiencePlan>('SELECT * FROM experience_plans WHERE id = ?', [experienceId])!;
|
||||
}
|
||||
|
||||
export function updateExperience(input: UpdateExperienceInput): void {
|
||||
const existing = db.get<{ plan_id: string; user_id: string }>(
|
||||
`SELECT ep.plan_id, p.user_id
|
||||
FROM experience_plans ep
|
||||
JOIN plans p ON p.id = ep.plan_id
|
||||
WHERE ep.id = ? AND p.user_id = ?`,
|
||||
[input.experienceId, input.userId]
|
||||
);
|
||||
if (!existing) throw new Error('Plan not found or not authorized');
|
||||
|
||||
db.run(`UPDATE plans SET status = ?, title = ?, notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [
|
||||
input.status ?? 'idea',
|
||||
input.name.trim(),
|
||||
input.description?.trim() || null,
|
||||
existing.plan_id
|
||||
]);
|
||||
|
||||
db.run(
|
||||
`UPDATE experience_plans
|
||||
SET booking_id = ?, total_cost = ?, description = ?, website = ?, address = ?, contact_number = ?,
|
||||
start_date = ?, start_time = ?, start_timezone = ?, end_date = ?, end_time = ?, end_timezone = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[
|
||||
input.bookingId?.trim() || null,
|
||||
input.totalCost ?? null,
|
||||
input.description?.trim() || null,
|
||||
input.website?.trim() || null,
|
||||
input.address?.trim() || null,
|
||||
input.contactNumber?.trim() || null,
|
||||
input.startDate?.trim() || null,
|
||||
input.startTime?.trim() || null,
|
||||
input.startTimezone?.trim() || null,
|
||||
input.endDate?.trim() || null,
|
||||
input.endTime?.trim() || null,
|
||||
input.endTimezone?.trim() || null,
|
||||
input.experienceId
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
export function getExperiencesForTrip(
|
||||
tripId: string,
|
||||
userId: string,
|
||||
type?: ExperienceType
|
||||
): ExperiencePlan[] {
|
||||
const params: unknown[] = [tripId, userId];
|
||||
let typeFilter = '';
|
||||
if (type) {
|
||||
typeFilter = ' AND p.type = ?';
|
||||
params.push(type);
|
||||
}
|
||||
return db.all<ExperiencePlan>(
|
||||
`SELECT ep.* FROM experience_plans ep
|
||||
JOIN plans p ON p.id = ep.plan_id
|
||||
WHERE p.trip_id = ? AND p.user_id = ?${typeFilter}
|
||||
ORDER BY p.position ASC, ep.created_at ASC`,
|
||||
params
|
||||
);
|
||||
}
|
||||
137
src/lib/server/other-transports.ts
Normal file
137
src/lib/server/other-transports.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { db } from './db/index.js';
|
||||
import type { PlanStatus } from './plans.js';
|
||||
|
||||
export interface OtherTransport {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
start_date: string | null;
|
||||
start_time: string | null;
|
||||
start_timezone: string | null;
|
||||
end_date: string | null;
|
||||
end_time: string | null;
|
||||
end_timezone: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateOtherTransportInput {
|
||||
tripId: string;
|
||||
userId: string;
|
||||
parentId?: string;
|
||||
status?: PlanStatus;
|
||||
title: string;
|
||||
notes?: string;
|
||||
startDate?: string;
|
||||
startTime?: string;
|
||||
startTimezone?: string;
|
||||
endDate?: string;
|
||||
endTime?: string;
|
||||
endTimezone?: string;
|
||||
}
|
||||
|
||||
export interface UpdateOtherTransportInput {
|
||||
otherTransportId: string;
|
||||
userId: string;
|
||||
status?: PlanStatus;
|
||||
title: string;
|
||||
notes?: string;
|
||||
startDate?: string;
|
||||
startTime?: string;
|
||||
startTimezone?: string;
|
||||
endDate?: string;
|
||||
endTime?: string;
|
||||
endTimezone?: string;
|
||||
}
|
||||
|
||||
export function createOtherTransport(input: CreateOtherTransportInput): OtherTransport {
|
||||
const planId = randomUUID();
|
||||
const maxPos = db.get<{ pos: number }>(
|
||||
`SELECT COALESCE(MAX(position), -1) + 1 as pos FROM plans WHERE trip_id = ? AND user_id = ?`,
|
||||
[input.tripId, input.userId]
|
||||
);
|
||||
const position = maxPos?.pos ?? 0;
|
||||
|
||||
db.run(
|
||||
`INSERT INTO plans (id, trip_id, user_id, type, status, title, notes, parent_id, position)
|
||||
VALUES (?, ?, ?, 'transport', ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
planId,
|
||||
input.tripId,
|
||||
input.userId,
|
||||
input.status ?? 'idea',
|
||||
input.title.trim(),
|
||||
input.notes?.trim() || null,
|
||||
input.parentId ?? null,
|
||||
position
|
||||
]
|
||||
);
|
||||
|
||||
const otherTransportId = randomUUID();
|
||||
db.run(
|
||||
`INSERT INTO other_transports (
|
||||
id, plan_id,
|
||||
start_date, start_time, start_timezone,
|
||||
end_date, end_time, end_timezone
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
otherTransportId,
|
||||
planId,
|
||||
input.startDate ?? null,
|
||||
input.startTime ?? null,
|
||||
input.startTimezone ?? null,
|
||||
input.endDate ?? null,
|
||||
input.endTime ?? null,
|
||||
input.endTimezone ?? null
|
||||
]
|
||||
);
|
||||
|
||||
return db.get<OtherTransport>('SELECT * FROM other_transports WHERE id = ?', [otherTransportId])!;
|
||||
}
|
||||
|
||||
export function updateOtherTransport(input: UpdateOtherTransportInput): void {
|
||||
const existing = db.get<OtherTransport & { user_id: string }>(
|
||||
`SELECT ot.*, p.user_id FROM other_transports ot
|
||||
JOIN plans p ON p.id = ot.plan_id
|
||||
WHERE ot.id = ? AND p.user_id = ?`,
|
||||
[input.otherTransportId, input.userId]
|
||||
);
|
||||
if (!existing) throw new Error('Other transportation not found or not authorized');
|
||||
|
||||
db.run(
|
||||
`UPDATE plans SET status = ?, title = ?, notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
||||
[
|
||||
input.status ?? 'idea',
|
||||
input.title.trim(),
|
||||
input.notes?.trim() || null,
|
||||
existing.plan_id
|
||||
]
|
||||
);
|
||||
|
||||
db.run(
|
||||
`UPDATE other_transports
|
||||
SET start_date = ?, start_time = ?, start_timezone = ?,
|
||||
end_date = ?, end_time = ?, end_timezone = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[
|
||||
input.startDate ?? null,
|
||||
input.startTime ?? null,
|
||||
input.startTimezone ?? null,
|
||||
input.endDate ?? null,
|
||||
input.endTime ?? null,
|
||||
input.endTimezone ?? null,
|
||||
input.otherTransportId
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
export function getOtherTransportsForTrip(tripId: string, userId: string): OtherTransport[] {
|
||||
return db.all<OtherTransport>(
|
||||
`SELECT ot.* FROM other_transports ot
|
||||
JOIN plans p ON p.id = ot.plan_id
|
||||
WHERE p.trip_id = ? AND p.user_id = ? AND p.type = 'transport'
|
||||
ORDER BY p.position ASC, ot.created_at ASC`,
|
||||
[tripId, userId]
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,9 @@ import { db } from './db/index.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { PlanStatus } from './plans.js';
|
||||
import { createPlan } from './plans.js';
|
||||
import { createOtherTransport } from './other-transports.js';
|
||||
import { createExperience } from './experiences.js';
|
||||
import { createChecklist } from './checklists.js';
|
||||
import { listDaysForOperatorTour, listPlansForOperatorTour } from './admin/data.js';
|
||||
|
||||
export interface PackageTour {
|
||||
@@ -323,15 +326,81 @@ export function cloneTemplateDaysToTour(input: {
|
||||
});
|
||||
const dayPlans = allDayPlans.filter((p) => p.operator_tour_day_id === day.id);
|
||||
for (const dp of dayPlans) {
|
||||
createPlan({
|
||||
tripId: input.tripId,
|
||||
userId: input.userId,
|
||||
type: dp.type,
|
||||
title: dp.title,
|
||||
notes: dp.notes ?? undefined,
|
||||
parentId: newDay.plan_id,
|
||||
status: 'idea'
|
||||
});
|
||||
if (dp.type === 'transport') {
|
||||
createOtherTransport({
|
||||
tripId: input.tripId,
|
||||
userId: input.userId,
|
||||
parentId: newDay.plan_id,
|
||||
status: 'idea',
|
||||
title: dp.title,
|
||||
notes: dp.notes ?? undefined,
|
||||
startDate: dp.start_date ?? undefined,
|
||||
startTime: dp.start_time ?? undefined,
|
||||
startTimezone: dp.start_timezone ?? undefined,
|
||||
endDate: dp.end_date ?? undefined,
|
||||
endTime: dp.end_time ?? undefined,
|
||||
endTimezone: dp.end_timezone ?? undefined
|
||||
});
|
||||
} else if (dp.type === 'activity' || dp.type === 'restaurant') {
|
||||
createExperience({
|
||||
tripId: input.tripId,
|
||||
userId: input.userId,
|
||||
type: dp.type,
|
||||
name: dp.title,
|
||||
parentId: newDay.plan_id,
|
||||
status: 'idea',
|
||||
bookingId: dp.booking_id ?? undefined,
|
||||
totalCost: dp.total_cost ?? undefined,
|
||||
description: dp.description ?? dp.notes ?? undefined,
|
||||
website: dp.website ?? undefined,
|
||||
address: dp.address ?? undefined,
|
||||
contactNumber: dp.contact_number ?? undefined,
|
||||
startDate: dp.start_date ?? undefined,
|
||||
startTime: dp.start_time ?? undefined,
|
||||
startTimezone: dp.start_timezone ?? undefined,
|
||||
endDate: dp.end_date ?? undefined,
|
||||
endTime: dp.end_time ?? undefined,
|
||||
endTimezone: dp.end_timezone ?? undefined
|
||||
});
|
||||
} else if (dp.type === 'packing' || dp.type === 'todo') {
|
||||
let items: string[] = [];
|
||||
try {
|
||||
const parsed = JSON.parse(dp.items_json ?? '[]');
|
||||
if (Array.isArray(parsed)) {
|
||||
items = parsed
|
||||
.map((item) =>
|
||||
typeof item === 'string'
|
||||
? item
|
||||
: typeof item === 'object' && item && 'content' in item
|
||||
? String(item.content)
|
||||
: ''
|
||||
)
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
} catch {
|
||||
items = [];
|
||||
}
|
||||
createChecklist({
|
||||
tripId: input.tripId,
|
||||
userId: input.userId,
|
||||
type: dp.type,
|
||||
name: dp.title,
|
||||
parentId: newDay.plan_id,
|
||||
status: 'idea',
|
||||
items
|
||||
});
|
||||
} else {
|
||||
createPlan({
|
||||
tripId: input.tripId,
|
||||
userId: input.userId,
|
||||
type: dp.type,
|
||||
title: dp.title,
|
||||
notes: dp.notes ?? undefined,
|
||||
parentId: newDay.plan_id,
|
||||
status: 'idea'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
161
src/lib/server/private-vehicles.ts
Normal file
161
src/lib/server/private-vehicles.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { db } from './db/index.js';
|
||||
import type { PlanStatus } from './plans.js';
|
||||
|
||||
export interface PrivateVehicleTransport {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
start_address: string;
|
||||
end_address: string;
|
||||
departure_date: string | null;
|
||||
departure_time: string | null;
|
||||
departure_timezone: string | null;
|
||||
arrival_date: string | null;
|
||||
arrival_time: string | null;
|
||||
arrival_timezone: string | null;
|
||||
start_plan_id: string | null;
|
||||
end_plan_id: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreatePrivateVehicleInput {
|
||||
tripId: string;
|
||||
userId: string;
|
||||
parentId?: string;
|
||||
status?: PlanStatus;
|
||||
startAddress: string;
|
||||
endAddress: string;
|
||||
departureDate?: string;
|
||||
departureTime?: string;
|
||||
departureTimezone?: string;
|
||||
arrivalDate?: string;
|
||||
arrivalTime?: string;
|
||||
arrivalTimezone?: string;
|
||||
startPlanId?: string;
|
||||
endPlanId?: string;
|
||||
}
|
||||
|
||||
export interface UpdatePrivateVehicleInput {
|
||||
privateVehicleId: string;
|
||||
userId: string;
|
||||
status?: PlanStatus;
|
||||
startAddress: string;
|
||||
endAddress: string;
|
||||
departureDate?: string;
|
||||
departureTime?: string;
|
||||
departureTimezone?: string;
|
||||
arrivalDate?: string;
|
||||
arrivalTime?: string;
|
||||
arrivalTimezone?: string;
|
||||
startPlanId?: string;
|
||||
endPlanId?: string;
|
||||
}
|
||||
|
||||
function buildTitle(startAddress: string, endAddress: string): string {
|
||||
return `Private vehicle: ${startAddress} to ${endAddress}`;
|
||||
}
|
||||
|
||||
export function createPrivateVehicle(input: CreatePrivateVehicleInput): PrivateVehicleTransport {
|
||||
const planId = randomUUID();
|
||||
const maxPos = db.get<{ pos: number }>(
|
||||
`SELECT COALESCE(MAX(position), -1) + 1 as pos FROM plans WHERE trip_id = ? AND user_id = ?`,
|
||||
[input.tripId, input.userId]
|
||||
);
|
||||
const position = maxPos?.pos ?? 0;
|
||||
|
||||
db.run(
|
||||
`INSERT INTO plans (id, trip_id, user_id, type, status, title, parent_id, position)
|
||||
VALUES (?, ?, ?, 'transport', ?, ?, ?, ?)`,
|
||||
[
|
||||
planId,
|
||||
input.tripId,
|
||||
input.userId,
|
||||
input.status ?? 'idea',
|
||||
buildTitle(input.startAddress, input.endAddress),
|
||||
input.parentId ?? null,
|
||||
position
|
||||
]
|
||||
);
|
||||
|
||||
const privateVehicleId = randomUUID();
|
||||
db.run(
|
||||
`INSERT INTO private_vehicles (
|
||||
id, plan_id, start_address, end_address,
|
||||
departure_date, departure_time, departure_timezone,
|
||||
arrival_date, arrival_time, arrival_timezone,
|
||||
start_plan_id, end_plan_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
privateVehicleId,
|
||||
planId,
|
||||
input.startAddress,
|
||||
input.endAddress,
|
||||
input.departureDate ?? null,
|
||||
input.departureTime ?? null,
|
||||
input.departureTimezone ?? null,
|
||||
input.arrivalDate ?? null,
|
||||
input.arrivalTime ?? null,
|
||||
input.arrivalTimezone ?? null,
|
||||
input.startPlanId ?? null,
|
||||
input.endPlanId ?? null
|
||||
]
|
||||
);
|
||||
|
||||
return db.get<PrivateVehicleTransport>('SELECT * FROM private_vehicles WHERE id = ?', [
|
||||
privateVehicleId
|
||||
])!;
|
||||
}
|
||||
|
||||
export function updatePrivateVehicle(input: UpdatePrivateVehicleInput): void {
|
||||
const privateVehicle = db.get<PrivateVehicleTransport & { user_id: string }>(
|
||||
`SELECT pv.*, p.user_id FROM private_vehicles pv
|
||||
JOIN plans p ON p.id = pv.plan_id
|
||||
WHERE pv.id = ? AND p.user_id = ?`,
|
||||
[input.privateVehicleId, input.userId]
|
||||
);
|
||||
if (!privateVehicle)
|
||||
throw new Error('Private vehicle transportation not found or not authorized');
|
||||
|
||||
db.run(`UPDATE plans SET status = ?, title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [
|
||||
input.status ?? 'idea',
|
||||
buildTitle(input.startAddress, input.endAddress),
|
||||
privateVehicle.plan_id
|
||||
]);
|
||||
|
||||
db.run(
|
||||
`UPDATE private_vehicles
|
||||
SET start_address = ?, end_address = ?,
|
||||
departure_date = ?, departure_time = ?, departure_timezone = ?,
|
||||
arrival_date = ?, arrival_time = ?, arrival_timezone = ?,
|
||||
start_plan_id = ?, end_plan_id = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[
|
||||
input.startAddress,
|
||||
input.endAddress,
|
||||
input.departureDate ?? null,
|
||||
input.departureTime ?? null,
|
||||
input.departureTimezone ?? null,
|
||||
input.arrivalDate ?? null,
|
||||
input.arrivalTime ?? null,
|
||||
input.arrivalTimezone ?? null,
|
||||
input.startPlanId ?? null,
|
||||
input.endPlanId ?? null,
|
||||
input.privateVehicleId
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
export function getPrivateVehiclesForTrip(
|
||||
tripId: string,
|
||||
userId: string
|
||||
): PrivateVehicleTransport[] {
|
||||
return db.all<PrivateVehicleTransport>(
|
||||
`SELECT pv.* FROM private_vehicles pv
|
||||
JOIN plans p ON p.id = pv.plan_id
|
||||
WHERE p.trip_id = ? AND p.user_id = ? AND p.type = 'transport'
|
||||
ORDER BY p.position ASC, pv.created_at ASC`,
|
||||
[tripId, userId]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user