package-tours) adding support for pre-defining tours
This commit is contained in:
@@ -311,3 +311,110 @@ export function getTourOperatorByName(name: string): TourOperator | null {
|
||||
]) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
// --- Operator Tours ---
|
||||
|
||||
export interface OperatorTour {
|
||||
id: number;
|
||||
operator_id: number;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export function listToursForOperator(operatorId: number): OperatorTour[] {
|
||||
return db.all<OperatorTour>('SELECT * FROM operator_tours WHERE operator_id = ? ORDER BY name', [
|
||||
operatorId
|
||||
]);
|
||||
}
|
||||
|
||||
export function listToursForOperatorByName(operatorName: string): OperatorTour[] {
|
||||
const operator = getTourOperatorByName(operatorName);
|
||||
if (!operator) return [];
|
||||
return listToursForOperator(operator.id);
|
||||
}
|
||||
|
||||
export function createOperatorTour(operatorId: number, name: string): OperatorTour {
|
||||
db.run('INSERT INTO operator_tours (operator_id, name) VALUES (?, ?)', [operatorId, name.trim()]);
|
||||
return db.get<OperatorTour>('SELECT * FROM operator_tours WHERE id = last_insert_rowid()')!;
|
||||
}
|
||||
|
||||
export function updateOperatorTour(id: number, name: string): void {
|
||||
db.run(`UPDATE operator_tours SET name = ?, updated_at = datetime('now') WHERE id = ?`, [
|
||||
name.trim(),
|
||||
id
|
||||
]);
|
||||
}
|
||||
|
||||
export function deleteOperatorTour(id: number): void {
|
||||
db.run('DELETE FROM operator_tours WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
// --- Operator Tour Days ---
|
||||
|
||||
export interface OperatorTourDay {
|
||||
id: number;
|
||||
operator_tour_id: number;
|
||||
day_number: number;
|
||||
title: string | null;
|
||||
notes: string | null;
|
||||
position: number;
|
||||
}
|
||||
|
||||
export function listDaysForOperatorTour(tourId: number): OperatorTourDay[] {
|
||||
return db.all<OperatorTourDay>(
|
||||
'SELECT * FROM operator_tour_days WHERE operator_tour_id = ? ORDER BY position ASC, day_number ASC',
|
||||
[tourId]
|
||||
);
|
||||
}
|
||||
|
||||
export function createOperatorTourDay(
|
||||
tourId: number,
|
||||
title?: string,
|
||||
notes?: string
|
||||
): OperatorTourDay {
|
||||
const count = db.get<{ count: number }>(
|
||||
'SELECT COUNT(*) as count FROM operator_tour_days WHERE operator_tour_id = ?',
|
||||
[tourId]
|
||||
);
|
||||
const dayNumber = (count?.count ?? 0) + 1;
|
||||
const maxPos = db.get<{ pos: number }>(
|
||||
'SELECT COALESCE(MAX(position), -1) + 1 as pos FROM operator_tour_days WHERE operator_tour_id = ?',
|
||||
[tourId]
|
||||
);
|
||||
const position = maxPos?.pos ?? 0;
|
||||
|
||||
db.run(
|
||||
`INSERT INTO operator_tour_days (operator_tour_id, day_number, title, notes, position)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[tourId, dayNumber, title?.trim() || null, notes?.trim() || null, position]
|
||||
);
|
||||
return db.get<OperatorTourDay>(
|
||||
'SELECT * FROM operator_tour_days WHERE id = last_insert_rowid()'
|
||||
)!;
|
||||
}
|
||||
|
||||
export function updateOperatorTourDay(id: number, title?: string, notes?: string): void {
|
||||
db.run(
|
||||
`UPDATE operator_tour_days SET title = ?, notes = ?, updated_at = datetime('now') WHERE id = ?`,
|
||||
[title?.trim() || null, notes?.trim() || null, id]
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteOperatorTourDay(id: number): void {
|
||||
const day = db.get<{ operator_tour_id: number }>(
|
||||
'SELECT operator_tour_id FROM operator_tour_days WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
db.run('DELETE FROM operator_tour_days WHERE id = ?', [id]);
|
||||
if (day) {
|
||||
// Re-number remaining days by position order
|
||||
const remaining = db.all<{ id: number }>(
|
||||
'SELECT id FROM operator_tour_days WHERE operator_tour_id = ? ORDER BY position ASC, id ASC',
|
||||
[day.operator_tour_id]
|
||||
);
|
||||
remaining.forEach((row, i) => {
|
||||
db.run('UPDATE operator_tour_days SET day_number = ? WHERE id = ?', [i + 1, row.id]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
21
src/lib/server/admin/providers/g-adventures.ts
Normal file
21
src/lib/server/admin/providers/g-adventures.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import type { TourProvider, TourSearchResult } from './types.js';
|
||||
|
||||
export const GAdventuresProvider: TourProvider = {
|
||||
name: 'G Adventures',
|
||||
search: async (query: string): Promise<TourSearchResult[]> => {
|
||||
const params = new URLSearchParams();
|
||||
if (query.trim()) params.set('name', query.trim());
|
||||
const url = `https://rest.gadventures.com/tour_dossiers?${params}`;
|
||||
const res = await fetch(url, {
|
||||
headers: { 'X-Application-Key': env.GADVENTURES_API_KEY }
|
||||
});
|
||||
console.dir(res);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return (data.results ?? []).map((r: { id: string; name: string }) => ({
|
||||
id: r.id,
|
||||
title: r.name
|
||||
}));
|
||||
}
|
||||
};
|
||||
10
src/lib/server/admin/providers/index.ts
Normal file
10
src/lib/server/admin/providers/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { TourProvider } from './types.js';
|
||||
import { GAdventuresProvider } from './g-adventures.js';
|
||||
|
||||
const PROVIDERS: Record<string, TourProvider> = {
|
||||
'g adventures': GAdventuresProvider
|
||||
};
|
||||
|
||||
export function getProviderForOperator(operatorName: string): TourProvider | null {
|
||||
return PROVIDERS[operatorName.toLowerCase()] ?? null;
|
||||
}
|
||||
13
src/lib/server/admin/providers/types.ts
Normal file
13
src/lib/server/admin/providers/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface TourSearchResult {
|
||||
/** Provider-specific identifier for the tour */
|
||||
id: string;
|
||||
/** Display name of the tour */
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface TourProvider {
|
||||
/** Human-readable provider name shown in the import drawer */
|
||||
name: string;
|
||||
/** Search tours by query string. Empty query returns a default set of results. */
|
||||
search(query: string): Promise<TourSearchResult[]>;
|
||||
}
|
||||
@@ -255,6 +255,7 @@ export function runMigrations(db: Database): void {
|
||||
id TEXT PRIMARY KEY,
|
||||
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
||||
operator_name TEXT NOT NULL,
|
||||
tour_name TEXT,
|
||||
confirmation_number TEXT,
|
||||
start_date TEXT,
|
||||
start_time TEXT,
|
||||
@@ -268,6 +269,12 @@ export function runMigrations(db: Database): void {
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
// Migration: add tour_name to existing databases
|
||||
try {
|
||||
db.run(`ALTER TABLE package_tours ADD COLUMN tour_name TEXT`);
|
||||
} catch {
|
||||
/* already exists */
|
||||
}
|
||||
|
||||
// Package tour travellers - links people to package tours
|
||||
db.run(`
|
||||
@@ -290,6 +297,31 @@ export function runMigrations(db: Database): void {
|
||||
)
|
||||
`);
|
||||
|
||||
// Predefined tours per tour operator (admin-managed)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS operator_tours (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
operator_id INTEGER NOT NULL REFERENCES tour_operators(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Template itinerary days for predefined operator tours
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS operator_tour_days (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
operator_tour_id INTEGER NOT NULL REFERENCES operator_tours(id) ON DELETE CASCADE,
|
||||
day_number INTEGER NOT NULL DEFAULT 1,
|
||||
title TEXT,
|
||||
notes TEXT,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Countries table — editable list for admin (used by country selector)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS countries (
|
||||
|
||||
@@ -3,7 +3,13 @@ import { setupTestDb } from '../../tests/helpers.js';
|
||||
import type { Database } from './db/types.js';
|
||||
import { createTrip } from './trips.js';
|
||||
import { createPerson } from './travellers.js';
|
||||
import { createPackageTour, getPackageToursForTrip, updatePackageTour } from './package-tours.js';
|
||||
import {
|
||||
createPackageTour,
|
||||
getPackageToursForTrip,
|
||||
updatePackageTour,
|
||||
createTourDay,
|
||||
updateTourDay
|
||||
} from './package-tours.js';
|
||||
|
||||
let db: Database;
|
||||
beforeEach(() => {
|
||||
@@ -34,6 +40,19 @@ describe('createPackageTour', () => {
|
||||
expect(plan?.title).toBe('Viking');
|
||||
});
|
||||
|
||||
it('sets tour_name and uses it in the plan title', () => {
|
||||
const trip = makeTrip();
|
||||
const tour = createPackageTour({
|
||||
tripId: trip.id,
|
||||
userId: 'u1',
|
||||
operatorName: 'G Adventures',
|
||||
tourName: 'Peru'
|
||||
});
|
||||
expect(tour.tour_name).toBe('Peru');
|
||||
const plan = db.get<{ title: string }>('SELECT title FROM plans WHERE id = ?', [tour.plan_id]);
|
||||
expect(plan?.title).toBe('G Adventures: Peru');
|
||||
});
|
||||
|
||||
it('stores all optional fields', () => {
|
||||
const trip = makeTrip();
|
||||
const tour = createPackageTour({
|
||||
@@ -78,7 +97,7 @@ describe('createPackageTour', () => {
|
||||
});
|
||||
|
||||
describe('getPackageToursForTrip', () => {
|
||||
it('returns tours with planStatus, travellerIds, and childPlans', () => {
|
||||
it('returns tours with planStatus, travellerIds, empty days and ungrouped plans', () => {
|
||||
const trip = makeTrip();
|
||||
const person = createPerson('u1', 'Bob', 'B');
|
||||
const tour = createPackageTour({
|
||||
@@ -89,7 +108,7 @@ describe('getPackageToursForTrip', () => {
|
||||
travellerIds: [person.id]
|
||||
});
|
||||
|
||||
// Insert a child plan manually
|
||||
// Insert an ungrouped child plan directly under the tour
|
||||
db.run(
|
||||
`INSERT INTO plans (id, trip_id, user_id, type, status, title, parent_id, position)
|
||||
VALUES ('child-1', ?, 'u1', 'lodging', 'confirmed', 'Tour Hotel', ?, 99)`,
|
||||
@@ -100,9 +119,9 @@ describe('getPackageToursForTrip', () => {
|
||||
expect(tours).toHaveLength(1);
|
||||
expect(tours[0].planStatus).toBe('tentative');
|
||||
expect(tours[0].travellerIds).toContain(person.id);
|
||||
expect(tours[0].childPlans).toHaveLength(1);
|
||||
expect(tours[0].childPlans[0].title).toBe('Tour Hotel');
|
||||
expect(tours[0].childPlans[0].type).toBe('lodging');
|
||||
expect(tours[0].days).toHaveLength(0);
|
||||
expect(tours[0].ungroupedChildPlans).toHaveLength(1);
|
||||
expect(tours[0].ungroupedChildPlans[0].title).toBe('Tour Hotel');
|
||||
});
|
||||
|
||||
it('returns highlight_color from the tour_operators table', () => {
|
||||
@@ -126,9 +145,51 @@ describe('getPackageToursForTrip', () => {
|
||||
createPackageTour({ tripId: trip.id, userId: 'u1', operatorName: 'Globus' });
|
||||
expect(getPackageToursForTrip(trip.id, 'u2')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns days with child plans nested inside', () => {
|
||||
const trip = makeTrip();
|
||||
const tour = createPackageTour({ tripId: trip.id, userId: 'u1', operatorName: 'G Adventures' });
|
||||
const day = createTourDay({
|
||||
tripId: trip.id,
|
||||
userId: 'u1',
|
||||
tourPlanId: tour.plan_id,
|
||||
title: 'The Rainforest'
|
||||
});
|
||||
|
||||
// Insert a child plan under the day
|
||||
db.run(
|
||||
`INSERT INTO plans (id, trip_id, user_id, type, status, title, parent_id, position)
|
||||
VALUES ('day-child-1', ?, 'u1', 'lodging', 'confirmed', 'Jungle Lodge', ?, 99)`,
|
||||
[trip.id, day.plan_id]
|
||||
);
|
||||
|
||||
const tours = getPackageToursForTrip(trip.id, 'u1');
|
||||
expect(tours[0].days).toHaveLength(1);
|
||||
expect(tours[0].days[0].day_number).toBe(1);
|
||||
expect(tours[0].days[0].title).toBe('The Rainforest');
|
||||
expect(tours[0].days[0].childPlans).toHaveLength(1);
|
||||
expect(tours[0].days[0].childPlans[0].title).toBe('Jungle Lodge');
|
||||
// Day itself does not appear in ungrouped
|
||||
expect(tours[0].ungroupedChildPlans).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePackageTour', () => {
|
||||
it('updates tour_name and plan title', () => {
|
||||
const trip = makeTrip();
|
||||
const tour = createPackageTour({ tripId: trip.id, userId: 'u1', operatorName: 'Contiki' });
|
||||
updatePackageTour({
|
||||
tourId: tour.id,
|
||||
userId: 'u1',
|
||||
operatorName: 'Contiki',
|
||||
tourName: 'Europe'
|
||||
});
|
||||
const updated = getPackageToursForTrip(trip.id, 'u1')[0];
|
||||
expect(updated.tour_name).toBe('Europe');
|
||||
const plan = db.get<{ title: string }>('SELECT title FROM plans WHERE id = ?', [tour.plan_id]);
|
||||
expect(plan?.title).toBe('Contiki: Europe');
|
||||
});
|
||||
|
||||
it('updates both plan and package_tours rows', () => {
|
||||
const trip = makeTrip();
|
||||
const tour = createPackageTour({
|
||||
@@ -181,3 +242,75 @@ describe('updatePackageTour', () => {
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTourDay', () => {
|
||||
it('creates a day plan under the tour with auto day number title', () => {
|
||||
const trip = makeTrip();
|
||||
const tour = createPackageTour({ tripId: trip.id, userId: 'u1', operatorName: 'Viking' });
|
||||
const day = createTourDay({ tripId: trip.id, userId: 'u1', tourPlanId: tour.plan_id });
|
||||
expect(day.plan_id).toBeTruthy();
|
||||
expect(day.day_number).toBe(1);
|
||||
expect(day.title).toBe('Day 1');
|
||||
expect(day.notes).toBeNull();
|
||||
expect(day.childPlans).toHaveLength(0);
|
||||
|
||||
const row = db.get<{ type: string; parent_id: string }>(
|
||||
'SELECT type, parent_id FROM plans WHERE id = ?',
|
||||
[day.plan_id]
|
||||
);
|
||||
expect(row?.type).toBe('day');
|
||||
expect(row?.parent_id).toBe(tour.plan_id);
|
||||
});
|
||||
|
||||
it('uses the provided title', () => {
|
||||
const trip = makeTrip();
|
||||
const tour = createPackageTour({ tripId: trip.id, userId: 'u1', operatorName: 'Viking' });
|
||||
const day = createTourDay({
|
||||
tripId: trip.id,
|
||||
userId: 'u1',
|
||||
tourPlanId: tour.plan_id,
|
||||
title: 'The Rainforest',
|
||||
notes: 'A day in the Amazon jungle.'
|
||||
});
|
||||
expect(day.title).toBe('The Rainforest');
|
||||
expect(day.notes).toBe('A day in the Amazon jungle.');
|
||||
});
|
||||
|
||||
it('assigns sequential day numbers', () => {
|
||||
const trip = makeTrip();
|
||||
const tour = createPackageTour({ tripId: trip.id, userId: 'u1', operatorName: 'Viking' });
|
||||
createTourDay({ tripId: trip.id, userId: 'u1', tourPlanId: tour.plan_id });
|
||||
const day2 = createTourDay({ tripId: trip.id, userId: 'u1', tourPlanId: tour.plan_id });
|
||||
expect(day2.day_number).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTourDay', () => {
|
||||
it('updates title and notes', () => {
|
||||
const trip = makeTrip();
|
||||
const tour = createPackageTour({ tripId: trip.id, userId: 'u1', operatorName: 'Viking' });
|
||||
const day = createTourDay({ tripId: trip.id, userId: 'u1', tourPlanId: tour.plan_id });
|
||||
updateTourDay({
|
||||
dayPlanId: day.plan_id,
|
||||
userId: 'u1',
|
||||
title: 'Machu Picchu',
|
||||
notes: 'The big day.'
|
||||
});
|
||||
|
||||
const row = db.get<{ title: string; notes: string }>(
|
||||
'SELECT title, notes FROM plans WHERE id = ?',
|
||||
[day.plan_id]
|
||||
);
|
||||
expect(row?.title).toBe('Machu Picchu');
|
||||
expect(row?.notes).toBe('The big day.');
|
||||
});
|
||||
|
||||
it('throws when the user does not own the day', () => {
|
||||
const trip = makeTrip();
|
||||
const tour = createPackageTour({ tripId: trip.id, userId: 'u1', operatorName: 'Viking' });
|
||||
const day = createTourDay({ tripId: trip.id, userId: 'u1', tourPlanId: tour.plan_id });
|
||||
expect(() =>
|
||||
updateTourDay({ dayPlanId: day.plan_id, userId: 'u2', title: 'Hijacked' })
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { db } from './db/index.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { PlanStatus } from './plans.js';
|
||||
import { listDaysForOperatorTour } from './admin/data.js';
|
||||
|
||||
export interface PackageTour {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
operator_name: string;
|
||||
tour_name: string | null;
|
||||
confirmation_number: string | null;
|
||||
start_date: string | null;
|
||||
start_time: string | null;
|
||||
@@ -27,11 +29,20 @@ export interface ChildPlanSummary {
|
||||
start_date: string | null;
|
||||
}
|
||||
|
||||
export interface TourDay {
|
||||
plan_id: string;
|
||||
day_number: number;
|
||||
title: string | null;
|
||||
notes: string | null;
|
||||
childPlans: ChildPlanSummary[];
|
||||
}
|
||||
|
||||
export interface CreatePackageTourInput {
|
||||
tripId: string;
|
||||
userId: string;
|
||||
status?: PlanStatus;
|
||||
operatorName: string;
|
||||
tourName?: string;
|
||||
confirmationNumber?: string;
|
||||
startDate?: string;
|
||||
startTime?: string;
|
||||
@@ -49,6 +60,7 @@ export interface UpdatePackageTourInput {
|
||||
userId: string;
|
||||
status?: PlanStatus;
|
||||
operatorName: string;
|
||||
tourName?: string;
|
||||
confirmationNumber?: string;
|
||||
startDate?: string;
|
||||
startTime?: string;
|
||||
@@ -69,24 +81,26 @@ export function createPackageTour(input: CreatePackageTourInput): PackageTour {
|
||||
);
|
||||
const position = maxPos?.pos ?? 0;
|
||||
|
||||
const title = input.tourName ? `${input.operatorName}: ${input.tourName}` : input.operatorName;
|
||||
db.run(
|
||||
`INSERT INTO plans (id, trip_id, user_id, type, status, title, position)
|
||||
VALUES (?, ?, ?, 'tour', ?, ?, ?)`,
|
||||
[planId, input.tripId, input.userId, input.status ?? 'idea', input.operatorName, position]
|
||||
[planId, input.tripId, input.userId, input.status ?? 'idea', title, position]
|
||||
);
|
||||
|
||||
const tourId = randomUUID();
|
||||
db.run(
|
||||
`INSERT INTO package_tours (
|
||||
id, plan_id, operator_name, confirmation_number,
|
||||
id, plan_id, operator_name, tour_name, confirmation_number,
|
||||
start_date, start_time, start_timezone,
|
||||
end_date, end_time, end_timezone,
|
||||
price, currency
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
tourId,
|
||||
planId,
|
||||
input.operatorName,
|
||||
input.tourName ?? null,
|
||||
input.confirmationNumber ?? null,
|
||||
input.startDate ?? null,
|
||||
input.startTime ?? null,
|
||||
@@ -118,7 +132,8 @@ export function getPackageToursForTrip(
|
||||
PackageTour & {
|
||||
travellerIds: string[];
|
||||
planStatus: PlanStatus;
|
||||
childPlans: ChildPlanSummary[];
|
||||
days: TourDay[];
|
||||
ungroupedChildPlans: ChildPlanSummary[];
|
||||
highlight_color: string | null;
|
||||
}
|
||||
> {
|
||||
@@ -139,16 +154,46 @@ export function getPackageToursForTrip(
|
||||
}>('SELECT person_id FROM package_tour_travellers WHERE package_tour_id = ?', [tour.id])
|
||||
.map((r) => r.person_id);
|
||||
|
||||
const childPlans = db.all<ChildPlanSummary>(
|
||||
`SELECT id, type, title, status, start_date
|
||||
FROM plans
|
||||
WHERE parent_id = ? AND user_id = ?
|
||||
// Fetch day plans (type='day') directly under the tour, ordered by position
|
||||
const dayRows = db.all<{ id: string; title: string; notes: string | null; position: number }>(
|
||||
`SELECT id, title, notes, position FROM plans
|
||||
WHERE parent_id = ? AND user_id = ? AND type = 'day'
|
||||
ORDER BY position ASC, created_at ASC`,
|
||||
[tour.plan_id, userId]
|
||||
);
|
||||
|
||||
const days: TourDay[] = dayRows.map((day, i) => {
|
||||
const childPlans = db.all<ChildPlanSummary>(
|
||||
`SELECT id, type, title, status, start_date FROM plans
|
||||
WHERE parent_id = ? AND user_id = ? AND type != 'day'
|
||||
ORDER BY position ASC, created_at ASC`,
|
||||
[day.id, userId]
|
||||
);
|
||||
return {
|
||||
plan_id: day.id,
|
||||
day_number: i + 1,
|
||||
title: day.title || null,
|
||||
notes: day.notes,
|
||||
childPlans
|
||||
};
|
||||
});
|
||||
|
||||
// Ungrouped: direct children of the tour that are NOT days
|
||||
const ungroupedChildPlans = db.all<ChildPlanSummary>(
|
||||
`SELECT id, type, title, status, start_date FROM plans
|
||||
WHERE parent_id = ? AND user_id = ? AND type != 'day'
|
||||
ORDER BY position ASC, created_at ASC`,
|
||||
[tour.plan_id, userId]
|
||||
);
|
||||
|
||||
const { plan_status, ...rest } = tour;
|
||||
return { ...rest, travellerIds, planStatus: plan_status as PlanStatus, childPlans };
|
||||
return {
|
||||
...rest,
|
||||
travellerIds,
|
||||
planStatus: plan_status as PlanStatus,
|
||||
days,
|
||||
ungroupedChildPlans
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,15 +206,16 @@ export function updatePackageTour(input: UpdatePackageTourInput): void {
|
||||
);
|
||||
if (!tour) throw new Error('Package tour not found or not authorized');
|
||||
|
||||
const title = input.tourName ? `${input.operatorName}: ${input.tourName}` : input.operatorName;
|
||||
db.run(`UPDATE plans SET status = ?, title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [
|
||||
input.status ?? 'idea',
|
||||
input.operatorName,
|
||||
title,
|
||||
tour.plan_id
|
||||
]);
|
||||
|
||||
db.run(
|
||||
`UPDATE package_tours SET
|
||||
operator_name = ?, confirmation_number = ?,
|
||||
operator_name = ?, tour_name = ?, confirmation_number = ?,
|
||||
start_date = ?, start_time = ?, start_timezone = ?,
|
||||
end_date = ?, end_time = ?, end_timezone = ?,
|
||||
price = ?, currency = ?,
|
||||
@@ -177,6 +223,7 @@ export function updatePackageTour(input: UpdatePackageTourInput): void {
|
||||
WHERE id = ?`,
|
||||
[
|
||||
input.operatorName,
|
||||
input.tourName ?? null,
|
||||
input.confirmationNumber ?? null,
|
||||
input.startDate ?? null,
|
||||
input.startTime ?? null,
|
||||
@@ -200,3 +247,77 @@ export function updatePackageTour(input: UpdatePackageTourInput): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createTourDay(input: {
|
||||
tripId: string;
|
||||
userId: string;
|
||||
tourPlanId: string;
|
||||
title?: string;
|
||||
notes?: string;
|
||||
}): TourDay {
|
||||
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;
|
||||
|
||||
// Count existing days to determine day number for the default title
|
||||
const dayCount = db.get<{ count: number }>(
|
||||
`SELECT COUNT(*) as count FROM plans WHERE parent_id = ? AND user_id = ? AND type = 'day'`,
|
||||
[input.tourPlanId, input.userId]
|
||||
);
|
||||
const dayNumber = (dayCount?.count ?? 0) + 1;
|
||||
const title = input.title?.trim() || `Day ${dayNumber}`;
|
||||
|
||||
db.run(
|
||||
`INSERT INTO plans (id, trip_id, user_id, type, status, title, notes, parent_id, position)
|
||||
VALUES (?, ?, ?, 'day', 'confirmed', ?, ?, ?, ?)`,
|
||||
[planId, input.tripId, input.userId, title, input.notes ?? null, input.tourPlanId, position]
|
||||
);
|
||||
|
||||
return {
|
||||
plan_id: planId,
|
||||
day_number: dayNumber,
|
||||
title,
|
||||
notes: input.notes ?? null,
|
||||
childPlans: []
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTourDay(input: {
|
||||
dayPlanId: string;
|
||||
userId: string;
|
||||
title?: string;
|
||||
notes?: string;
|
||||
}): void {
|
||||
const existing = db.get<{ id: string }>(
|
||||
`SELECT id FROM plans WHERE id = ? AND user_id = ? AND type = 'day'`,
|
||||
[input.dayPlanId, input.userId]
|
||||
);
|
||||
if (!existing) throw new Error('Day not found or not authorized');
|
||||
|
||||
db.run(`UPDATE plans SET title = ?, notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [
|
||||
input.title?.trim() || null,
|
||||
input.notes ?? null,
|
||||
input.dayPlanId
|
||||
]);
|
||||
}
|
||||
|
||||
export function cloneTemplateDaysToTour(input: {
|
||||
operatorTourId: number;
|
||||
tripId: string;
|
||||
userId: string;
|
||||
tourPlanId: string;
|
||||
}): void {
|
||||
const days = listDaysForOperatorTour(input.operatorTourId);
|
||||
for (const day of days) {
|
||||
createTourDay({
|
||||
tripId: input.tripId,
|
||||
userId: input.userId,
|
||||
tourPlanId: input.tourPlanId,
|
||||
title: day.title ?? undefined,
|
||||
notes: day.notes ?? undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ export type PlanType =
|
||||
| 'restaurant'
|
||||
| 'tour'
|
||||
| 'packing'
|
||||
| 'todo';
|
||||
| 'todo'
|
||||
| 'day';
|
||||
|
||||
export type PlanStatus = 'idea' | 'tentative' | 'confirmed';
|
||||
|
||||
@@ -106,9 +107,7 @@ export interface Country {
|
||||
|
||||
export function getCountries(query?: string): Country[] {
|
||||
if (!query?.trim()) {
|
||||
return db.all<Country>(
|
||||
`SELECT name, country_code FROM countries ORDER BY name`
|
||||
);
|
||||
return db.all<Country>(`SELECT name, country_code FROM countries ORDER BY name`);
|
||||
}
|
||||
const pattern = `%${query.trim()}%`;
|
||||
return db.all<Country>(
|
||||
|
||||
Reference in New Issue
Block a user