trips) adding transportation, checklist, and experience planning
This commit is contained in:
@@ -84,13 +84,52 @@ export const actions: Actions = {
|
||||
const dayId = parseInt(formData.get('dayId') as string);
|
||||
const type = (formData.get('type') as string)?.trim();
|
||||
const get = (k: string) => (formData.get(k) as string)?.trim() || undefined;
|
||||
const title = get('name') || get('title');
|
||||
const transportationType = (get('transportation_type') || get('transport_kind') || 'other') as
|
||||
| 'other'
|
||||
| 'flight'
|
||||
| 'private_vehicle';
|
||||
|
||||
if (isNaN(dayId)) return fail(400, { error: 'Invalid day ID' });
|
||||
if (type !== 'transport' && type !== 'lodging') return fail(400, { error: 'Invalid plan type' });
|
||||
if (!title) return fail(400, { error: 'Name is required' });
|
||||
if (
|
||||
type !== 'transport' &&
|
||||
type !== 'lodging' &&
|
||||
type !== 'activity' &&
|
||||
type !== 'restaurant' &&
|
||||
type !== 'packing' &&
|
||||
type !== 'todo'
|
||||
)
|
||||
return fail(400, { error: 'Invalid plan type' });
|
||||
|
||||
let title = get('name') || get('title');
|
||||
const notes = get('notes') ?? undefined;
|
||||
let transportFields:
|
||||
| {
|
||||
transport_kind: 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;
|
||||
}
|
||||
| undefined;
|
||||
let 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;
|
||||
}
|
||||
| undefined;
|
||||
let checklistItemsJson: string | null | undefined;
|
||||
const lodgingFields =
|
||||
type === 'lodging'
|
||||
? {
|
||||
@@ -103,14 +142,103 @@ export const actions: Actions = {
|
||||
postal_code: get('postal_code') ?? null
|
||||
}
|
||||
: undefined;
|
||||
if (type === 'transport') {
|
||||
if (transportationType === 'flight') {
|
||||
const segmentIndices = new Set<number>();
|
||||
for (const key of formData.keys()) {
|
||||
const match = key.match(/^segments\[(\d+)\]/);
|
||||
if (match) segmentIndices.add(parseInt(match[1]));
|
||||
}
|
||||
const sorted = Array.from(segmentIndices).sort((a, b) => a - b);
|
||||
const first = sorted[0];
|
||||
const last = sorted[sorted.length - 1];
|
||||
const firstAirline =
|
||||
(first != null && get(`segments[${first}][airline_name]`)) ||
|
||||
(first != null && get(`segments[${first}][airline_iata]`)) ||
|
||||
'Flight';
|
||||
const firstNumber = (first != null && get(`segments[${first}][flight_number]`)) || '';
|
||||
if (!title) title = `${firstAirline} ${firstNumber}`.trim();
|
||||
const depDate =
|
||||
(first != null && get(`segments[${first}][departure_date]`)) ||
|
||||
(first != null && get(`segments[${first}][departure_datetime]`)?.split('T')[0]) ||
|
||||
null;
|
||||
const depTime =
|
||||
(first != null && get(`segments[${first}][departure_datetime]`)?.split('T')[1]) || null;
|
||||
const arrDate =
|
||||
(last != null && get(`segments[${last}][arrival_datetime]`)?.split('T')[0]) || null;
|
||||
const arrTime =
|
||||
(last != null && get(`segments[${last}][arrival_datetime]`)?.split('T')[1]) || null;
|
||||
transportFields = {
|
||||
transport_kind: 'flight',
|
||||
start_date: depDate,
|
||||
start_time: depTime,
|
||||
start_timezone: null,
|
||||
end_date: arrDate,
|
||||
end_time: arrTime,
|
||||
end_timezone: null
|
||||
};
|
||||
} else if (transportationType === 'private_vehicle') {
|
||||
const startAddress = get('start_address');
|
||||
const endAddress = get('end_address');
|
||||
if (!title) title = `Private vehicle: ${startAddress ?? 'Start'} to ${endAddress ?? 'End'}`;
|
||||
transportFields = {
|
||||
transport_kind: 'private_vehicle',
|
||||
start_date: get('departure_date') ?? null,
|
||||
start_time: get('departure_time') ?? null,
|
||||
start_timezone: get('departure_timezone') ?? null,
|
||||
end_date: get('arrival_date') ?? null,
|
||||
end_time: get('arrival_time') ?? null,
|
||||
end_timezone: get('arrival_timezone') ?? null
|
||||
};
|
||||
} else {
|
||||
transportFields = {
|
||||
transport_kind: 'other',
|
||||
start_date: get('start_date') ?? null,
|
||||
start_time: get('start_time') ?? null,
|
||||
start_timezone: get('start_timezone') ?? null,
|
||||
end_date: get('end_date') ?? null,
|
||||
end_time: get('end_time') ?? null,
|
||||
end_timezone: get('end_timezone') ?? null
|
||||
};
|
||||
}
|
||||
}
|
||||
if (type === 'activity' || type === 'restaurant') {
|
||||
const totalCostRaw = get('total_cost');
|
||||
const totalCost = totalCostRaw ? parseFloat(totalCostRaw) : null;
|
||||
experienceFields = {
|
||||
booking_id: get('booking_id') ?? null,
|
||||
total_cost: totalCost != null && Number.isFinite(totalCost) ? totalCost : null,
|
||||
description: get('description') ?? null,
|
||||
website: get('website') ?? null,
|
||||
address: get('address') ?? null,
|
||||
contact_number: get('contact_number') ?? null,
|
||||
start_date: get('start_date') ?? null,
|
||||
start_time: get('start_time') ?? null,
|
||||
start_timezone: get('start_timezone') ?? null,
|
||||
end_date: get('end_date') ?? null,
|
||||
end_time: get('end_time') ?? null,
|
||||
end_timezone: get('end_timezone') ?? null
|
||||
};
|
||||
}
|
||||
if (type === 'packing' || type === 'todo') {
|
||||
const items = formData
|
||||
.getAll('items[]')
|
||||
.map((item) => String(item).trim())
|
||||
.filter(Boolean);
|
||||
checklistItemsJson = JSON.stringify(items);
|
||||
}
|
||||
if (!title) return fail(400, { error: 'Name is required' });
|
||||
|
||||
try {
|
||||
data.createOperatorTourDayPlan(
|
||||
dayId,
|
||||
type as 'transport' | 'lodging',
|
||||
type as 'transport' | 'lodging' | 'activity' | 'restaurant' | 'packing' | 'todo',
|
||||
title,
|
||||
notes,
|
||||
lodgingFields
|
||||
lodgingFields,
|
||||
transportFields,
|
||||
experienceFields,
|
||||
checklistItemsJson
|
||||
);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
@@ -125,10 +253,25 @@ export const actions: Actions = {
|
||||
const id = parseInt(formData.get('id') as string);
|
||||
if (isNaN(id)) return fail(400, { error: 'Invalid plan ID' });
|
||||
|
||||
const title = (formData.get('name') as string)?.trim() || (formData.get('title') as string)?.trim();
|
||||
const title =
|
||||
(formData.get('name') as string)?.trim() || (formData.get('title') as string)?.trim();
|
||||
if (!title) return fail(400, { error: 'Name is required' });
|
||||
|
||||
const get = (k: string) => (formData.get(k) as string)?.trim() || undefined;
|
||||
const transportationType = (get('transportation_type') || get('transport_kind')) as
|
||||
| 'other'
|
||||
| 'flight'
|
||||
| 'private_vehicle'
|
||||
| undefined;
|
||||
let checklistItemsJson: string | undefined;
|
||||
const type = (formData.get('type') as string)?.trim();
|
||||
if (type === 'packing' || type === 'todo') {
|
||||
const items = formData
|
||||
.getAll('items[]')
|
||||
.map((item) => String(item).trim())
|
||||
.filter(Boolean);
|
||||
checklistItemsJson = JSON.stringify(items);
|
||||
}
|
||||
try {
|
||||
data.updateOperatorTourDayPlan(id, {
|
||||
title,
|
||||
@@ -139,7 +282,26 @@ export const actions: Actions = {
|
||||
city_name: get('city_name') ?? null,
|
||||
country: get('country') ?? null,
|
||||
country_code: get('country_code') ?? null,
|
||||
postal_code: get('postal_code') ?? null
|
||||
postal_code: get('postal_code') ?? null,
|
||||
transport_kind: transportationType ?? get('transport_kind') ?? undefined,
|
||||
start_date: get('start_date') ?? undefined,
|
||||
start_time: get('start_time') ?? undefined,
|
||||
start_timezone: get('start_timezone') ?? undefined,
|
||||
end_date: get('end_date') ?? undefined,
|
||||
end_time: get('end_time') ?? undefined,
|
||||
end_timezone: get('end_timezone') ?? undefined,
|
||||
booking_id: get('booking_id') ?? undefined,
|
||||
total_cost: (() => {
|
||||
const raw = get('total_cost');
|
||||
if (!raw) return undefined;
|
||||
const parsed = parseFloat(raw);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
})(),
|
||||
description: get('description') ?? undefined,
|
||||
website: get('website') ?? undefined,
|
||||
address: get('address') ?? undefined,
|
||||
contact_number: get('contact_number') ?? undefined,
|
||||
items_json: checklistItemsJson
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { base } from '$app/paths';
|
||||
import AddPlanMenu, { type AddPlanMenuItem } from '$lib/components/AddPlanMenu.svelte';
|
||||
import { PLAN_TYPE_MAP } from '$lib/components/plan-types.js';
|
||||
import LodgingCard from '$lib/components/LodgingCard.svelte';
|
||||
import OtherTransportCard from '$lib/components/OtherTransportCard.svelte';
|
||||
import ExperienceCard from '$lib/components/ExperienceCard.svelte';
|
||||
import ExperienceModal from '$lib/components/ExperienceModal.svelte';
|
||||
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
||||
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
|
||||
import AddTransportationModal from '$lib/components/AddTransportationModal.svelte';
|
||||
import EditTransportationModal from '$lib/components/EditTransportationModal.svelte';
|
||||
import EditLodgingModal from '$lib/components/EditLodgingModal.svelte';
|
||||
import AddLodgingModal from '$lib/components/AddLodgingModal.svelte';
|
||||
import type { PageData } from './$types';
|
||||
@@ -16,7 +25,7 @@
|
||||
let addTitle = $state('');
|
||||
let addNotes = $state('');
|
||||
|
||||
// Day plans (transport/lodging) per day
|
||||
// Day plans per day
|
||||
let plansByDay = $derived.by(() => {
|
||||
const map: Record<number, (typeof data.dayPlans)[number][]> = {};
|
||||
for (const d of data.days) map[d.id] = [];
|
||||
@@ -26,23 +35,20 @@
|
||||
return map;
|
||||
});
|
||||
|
||||
// Add day plan: transport inline form; lodging uses AddLodgingModal
|
||||
let addPlanFor = $state<{ dayId: number; type: 'transport' | 'lodging' } | null>(null);
|
||||
let addPlanTitle = $state('');
|
||||
let addPlanNotes = $state('');
|
||||
|
||||
// Edit: transport inline; lodging uses EditLodgingModal
|
||||
let editingPlanId = $state<number | null>(null);
|
||||
let editPlanTitle = $state('');
|
||||
let editPlanNotes = $state('');
|
||||
let addingTransportDayId = $state<number | null>(null);
|
||||
let editingTransportPlan = $state<(typeof data.dayPlans)[number] | null>(null);
|
||||
let editingLodgingPlan = $state<(typeof data.dayPlans)[number] | null>(null);
|
||||
let addingExperienceDay = $state<{ dayId: number; type: 'activity' | 'restaurant' } | null>(null);
|
||||
let editingExperiencePlan = $state<(typeof data.dayPlans)[number] | null>(null);
|
||||
let addingChecklistDay = $state<{ dayId: number; type: 'packing' | 'todo' } | null>(null);
|
||||
let editingChecklistPlan = $state<(typeof data.dayPlans)[number] | null>(null);
|
||||
let addLodgingDayId = $state<number | null>(null);
|
||||
|
||||
// Delete day plan form (for LodgingCard onDelete)
|
||||
let deletePlanFormRef = $state<HTMLFormElement | null>(null);
|
||||
let planToDeleteId = $state<number | null>(null);
|
||||
function submitDeletePlan(planId: number) {
|
||||
if (!confirm('Remove this lodging from the day?')) return;
|
||||
if (!confirm('Remove this plan from the day?')) return;
|
||||
planToDeleteId = planId;
|
||||
setTimeout(() => deletePlanFormRef?.requestSubmit(), 0);
|
||||
}
|
||||
@@ -59,52 +65,99 @@
|
||||
editNotes = '';
|
||||
}
|
||||
|
||||
function openAddPlan(dayId: number, type: 'transport' | 'lodging') {
|
||||
addPlanFor = { dayId, type };
|
||||
addPlanTitle = '';
|
||||
addPlanNotes = '';
|
||||
function openAddPlan(
|
||||
dayId: number,
|
||||
type: 'transport' | 'lodging' | 'activity' | 'restaurant' | 'packing' | 'todo'
|
||||
) {
|
||||
if (type === 'transport') {
|
||||
addingTransportDayId = dayId;
|
||||
} else if (type === 'lodging') {
|
||||
openAddLodging(dayId);
|
||||
} else if (type === 'activity' || type === 'restaurant') {
|
||||
addingExperienceDay = { dayId, type };
|
||||
} else {
|
||||
addingChecklistDay = { dayId, type };
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAddPlan() {
|
||||
addPlanFor = null;
|
||||
addPlanTitle = '';
|
||||
addPlanNotes = '';
|
||||
addingTransportDayId = null;
|
||||
addingExperienceDay = null;
|
||||
addingChecklistDay = null;
|
||||
}
|
||||
|
||||
function startEditPlan(plan: { id: number; title: string; notes: string | null; type: string }) {
|
||||
if (plan.type === 'lodging') {
|
||||
editingLodgingPlan = plan as (typeof data.dayPlans)[number];
|
||||
} else if (plan.type === 'transport') {
|
||||
editingTransportPlan = plan as (typeof data.dayPlans)[number];
|
||||
} else if (plan.type === 'activity' || plan.type === 'restaurant') {
|
||||
editingExperiencePlan = plan as (typeof data.dayPlans)[number];
|
||||
} else {
|
||||
editingPlanId = plan.id;
|
||||
editPlanTitle = plan.title;
|
||||
editPlanNotes = plan.notes ?? '';
|
||||
editingChecklistPlan = plan as (typeof data.dayPlans)[number];
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEditPlan() {
|
||||
editingPlanId = null;
|
||||
editPlanTitle = '';
|
||||
editPlanNotes = '';
|
||||
editingLodgingPlan = null;
|
||||
}
|
||||
|
||||
function openAddLodging(dayId: number) {
|
||||
addLodgingDayId = dayId;
|
||||
addPlanFor = null;
|
||||
addPlanTitle = '';
|
||||
addPlanNotes = '';
|
||||
addingTransportDayId = null;
|
||||
addingExperienceDay = null;
|
||||
addingChecklistDay = null;
|
||||
}
|
||||
|
||||
function closeAddLodging() {
|
||||
addLodgingDayId = null;
|
||||
}
|
||||
|
||||
function dayAddPlanItems(dayId: number): AddPlanMenuItem[] {
|
||||
return [
|
||||
{
|
||||
id: `${dayId}-transport`,
|
||||
label: 'Transportation',
|
||||
icon: PLAN_TYPE_MAP.transport.icon,
|
||||
onclick: () => openAddPlan(dayId, 'transport')
|
||||
},
|
||||
{
|
||||
id: `${dayId}-lodging`,
|
||||
label: 'Lodging',
|
||||
icon: PLAN_TYPE_MAP.lodging.icon,
|
||||
onclick: () => openAddLodging(dayId)
|
||||
},
|
||||
{
|
||||
id: `${dayId}-activity`,
|
||||
label: 'Attractions & Activities',
|
||||
icon: PLAN_TYPE_MAP.activity.icon,
|
||||
onclick: () => openAddPlan(dayId, 'activity')
|
||||
},
|
||||
{
|
||||
id: `${dayId}-restaurant`,
|
||||
label: 'Restaurant',
|
||||
icon: PLAN_TYPE_MAP.restaurant.icon,
|
||||
onclick: () => openAddPlan(dayId, 'restaurant')
|
||||
},
|
||||
{
|
||||
id: `${dayId}-packing`,
|
||||
label: 'Packing List',
|
||||
icon: PLAN_TYPE_MAP.packingList.icon,
|
||||
onclick: () => openAddPlan(dayId, 'packing')
|
||||
},
|
||||
{
|
||||
id: `${dayId}-todo`,
|
||||
label: 'To-do',
|
||||
icon: PLAN_TYPE_MAP.todo.icon,
|
||||
onclick: () => openAddPlan(dayId, 'todo')
|
||||
}
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteDayPlan"
|
||||
bind:this={deletePlanFormRef}
|
||||
use:enhance={() => ({ update }) => update()}
|
||||
use:enhance={() =>
|
||||
({ update }) =>
|
||||
update()}
|
||||
class="hidden"
|
||||
aria-hidden="true"
|
||||
>
|
||||
@@ -119,6 +172,108 @@
|
||||
formAction="?/editDayPlan"
|
||||
/>
|
||||
|
||||
<EditTransportationModal
|
||||
open={editingTransportPlan != null}
|
||||
onclose={() => (editingTransportPlan = null)}
|
||||
variant="template"
|
||||
formAction="?/editDayPlan"
|
||||
idFieldName="id"
|
||||
idValue={editingTransportPlan?.id}
|
||||
transportation={editingTransportPlan
|
||||
? {
|
||||
type: 'other',
|
||||
planStatus: 'idea',
|
||||
planTitle: editingTransportPlan.title,
|
||||
planNotes: editingTransportPlan.notes,
|
||||
transportKind: editingTransportPlan.transport_kind ?? 'other',
|
||||
otherTransport: {
|
||||
id: editingTransportPlan.id,
|
||||
plan_id: editingTransportPlan.id,
|
||||
start_date: editingTransportPlan.start_date ?? null,
|
||||
start_time: editingTransportPlan.start_time ?? null,
|
||||
start_timezone: editingTransportPlan.start_timezone ?? null,
|
||||
end_date: editingTransportPlan.end_date ?? null,
|
||||
end_time: editingTransportPlan.end_time ?? null,
|
||||
end_timezone: editingTransportPlan.end_timezone ?? null,
|
||||
created_at: '',
|
||||
updated_at: ''
|
||||
}
|
||||
}
|
||||
: null}
|
||||
/>
|
||||
<ExperienceModal
|
||||
open={addingExperienceDay != null}
|
||||
onclose={() => (addingExperienceDay = null)}
|
||||
variant="template"
|
||||
planType={addingExperienceDay?.type ?? 'activity'}
|
||||
mode="add"
|
||||
formAction="?/addDayPlan"
|
||||
dayId={addingExperienceDay?.dayId}
|
||||
/>
|
||||
<ExperienceModal
|
||||
open={editingExperiencePlan != null}
|
||||
onclose={() => (editingExperiencePlan = null)}
|
||||
variant="template"
|
||||
planType={(editingExperiencePlan?.type as 'activity' | 'restaurant') ?? 'activity'}
|
||||
mode="edit"
|
||||
formAction="?/editDayPlan"
|
||||
idFieldName="id"
|
||||
idValue={editingExperiencePlan?.id}
|
||||
initial={editingExperiencePlan
|
||||
? {
|
||||
name: editingExperiencePlan.title,
|
||||
description: editingExperiencePlan.description ?? editingExperiencePlan.notes,
|
||||
booking_id: editingExperiencePlan.booking_id,
|
||||
total_cost: editingExperiencePlan.total_cost,
|
||||
website: editingExperiencePlan.website,
|
||||
address: editingExperiencePlan.address,
|
||||
contact_number: editingExperiencePlan.contact_number,
|
||||
start_date: editingExperiencePlan.start_date,
|
||||
start_time: editingExperiencePlan.start_time,
|
||||
start_timezone: editingExperiencePlan.start_timezone,
|
||||
end_date: editingExperiencePlan.end_date,
|
||||
end_time: editingExperiencePlan.end_time,
|
||||
end_timezone: editingExperiencePlan.end_timezone
|
||||
}
|
||||
: null}
|
||||
/>
|
||||
<ChecklistModal
|
||||
open={addingChecklistDay != null}
|
||||
onclose={() => (addingChecklistDay = null)}
|
||||
variant="template"
|
||||
checklistType={addingChecklistDay?.type ?? 'packing'}
|
||||
mode="add"
|
||||
formAction="?/addDayPlan"
|
||||
dayId={addingChecklistDay?.dayId}
|
||||
/>
|
||||
<ChecklistModal
|
||||
open={editingChecklistPlan != null}
|
||||
onclose={() => (editingChecklistPlan = null)}
|
||||
variant="template"
|
||||
checklistType={(editingChecklistPlan?.type as 'packing' | 'todo') ?? 'packing'}
|
||||
mode="edit"
|
||||
formAction="?/editDayPlan"
|
||||
idFieldName="id"
|
||||
idValue={editingChecklistPlan?.id}
|
||||
initial={editingChecklistPlan
|
||||
? {
|
||||
name: editingChecklistPlan.title,
|
||||
items: (() => {
|
||||
try {
|
||||
const parsed = JSON.parse(editingChecklistPlan.items_json ?? '[]');
|
||||
return Array.isArray(parsed)
|
||||
? parsed.map((item) => ({
|
||||
content: typeof item === 'string' ? item : String(item?.content ?? '')
|
||||
}))
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})()
|
||||
}
|
||||
: null}
|
||||
/>
|
||||
|
||||
<AddLodgingModal
|
||||
open={addLodgingDayId != null}
|
||||
onclose={closeAddLodging}
|
||||
@@ -126,6 +281,13 @@
|
||||
dayId={addLodgingDayId ?? 0}
|
||||
formAction="?/addDayPlan"
|
||||
/>
|
||||
<AddTransportationModal
|
||||
open={addingTransportDayId != null}
|
||||
onclose={cancelAddPlan}
|
||||
variant="template"
|
||||
dayId={addingTransportDayId ?? undefined}
|
||||
formAction="?/addDayPlan"
|
||||
/>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.tour.name} Itinerary — Admin — Trips</title>
|
||||
@@ -136,8 +298,9 @@
|
||||
<nav class="mb-6 flex items-center gap-2 text-sm text-gray-500">
|
||||
<a href="{base}/admin/tour-operators" class="hover:text-gray-700">Tour Operators</a>
|
||||
<span>›</span>
|
||||
<a href="{base}/admin/tour-operators/{data.operator.id}" class="text-gray-700 hover:text-gray-900"
|
||||
>{data.operator.name}</a
|
||||
<a
|
||||
href="{base}/admin/tour-operators/{data.operator.id}"
|
||||
class="text-gray-700 hover:text-gray-900">{data.operator.name}</a
|
||||
>
|
||||
<span>›</span>
|
||||
<span class="font-medium text-gray-900">{data.tour.name} — Itinerary</span>
|
||||
@@ -282,14 +445,11 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Attached plans (transport / lodging) for this day -->
|
||||
<!-- Attached plans for this day -->
|
||||
<div class="mt-3 border-t border-gray-100 pt-3">
|
||||
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-gray-400">
|
||||
Transport & lodging
|
||||
</p>
|
||||
<p class="mb-2 text-xs font-medium tracking-wide text-gray-400 uppercase">Day Plans</p>
|
||||
{#each dayPlanList as plan (plan.id)}
|
||||
{#if plan.type === 'lodging'}
|
||||
<!-- Lodging: same card + modal as trip page -->
|
||||
<div class="mb-2">
|
||||
<LodgingCard
|
||||
templatePlan={plan}
|
||||
@@ -297,167 +457,88 @@
|
||||
onDelete={() => submitDeletePlan(plan.id)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Transport: inline edit -->
|
||||
{#if editingPlanId === plan.id}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/editDayPlan"
|
||||
use:enhance={() => {
|
||||
return ({ result, update }) => {
|
||||
update();
|
||||
if (result.type === 'success') cancelEditPlan();
|
||||
};
|
||||
{:else if plan.type === 'transport'}
|
||||
<div class="mb-2">
|
||||
<OtherTransportCard
|
||||
variant="template"
|
||||
plan={{
|
||||
...plan,
|
||||
status: 'idea'
|
||||
}}
|
||||
class="mb-2 rounded-md border border-gray-200 bg-gray-50/50 p-3"
|
||||
>
|
||||
<input type="hidden" name="id" value={plan.id} />
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
bind:value={editPlanTitle}
|
||||
placeholder="Title"
|
||||
class="rounded border border-gray-300 px-2 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
name="notes"
|
||||
bind:value={editPlanNotes}
|
||||
placeholder="Notes (optional)"
|
||||
rows="2"
|
||||
class="resize-none rounded border border-gray-300 px-2 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
></textarea>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={cancelEditPlan}
|
||||
class="rounded border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<div
|
||||
class="mb-2 flex items-start justify-between gap-2 rounded-md border border-gray-100 bg-gray-50/50 px-3 py-2"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="mr-2 inline-block rounded px-1.5 py-0.5 text-xs font-medium bg-sky-100 text-sky-800">
|
||||
Transport
|
||||
</span>
|
||||
<span class="font-medium text-gray-900">{plan.title}</span>
|
||||
{#if plan.notes}
|
||||
<p class="mt-0.5 text-sm text-gray-500">{plan.notes}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => startEditPlan(plan)}
|
||||
class="rounded p-1 text-gray-400 hover:bg-gray-200 hover:text-gray-600"
|
||||
title="Edit"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<form method="POST" action="?/deleteDayPlan" use:enhance={() => ({ update }) => update()}>
|
||||
<input type="hidden" name="id" value={plan.id} />
|
||||
<button
|
||||
type="submit"
|
||||
onclick={(e) => {
|
||||
if (!confirm('Remove this plan from the day?')) e.preventDefault();
|
||||
}}
|
||||
class="rounded p-1 text-gray-400 hover:bg-gray-200 hover:text-red-600"
|
||||
title="Remove"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||
<path d="M10 11v6M14 11v6" />
|
||||
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
otherTransport={{
|
||||
id: plan.id,
|
||||
plan_id: String(plan.id),
|
||||
start_date: plan.start_date ?? null,
|
||||
start_time: plan.start_time ?? null,
|
||||
start_timezone: plan.start_timezone ?? null,
|
||||
end_date: plan.end_date ?? null,
|
||||
end_time: plan.end_time ?? null,
|
||||
end_timezone: plan.end_timezone ?? null,
|
||||
created_at: '',
|
||||
updated_at: ''
|
||||
}}
|
||||
onEdit={() => startEditPlan(plan)}
|
||||
onDelete={() => submitDeletePlan(plan.id)}
|
||||
/>
|
||||
</div>
|
||||
{:else if plan.type === 'activity' || plan.type === 'restaurant'}
|
||||
<div class="mb-2">
|
||||
<ExperienceCard
|
||||
variant="template"
|
||||
plan={{ title: plan.title, status: 'idea' }}
|
||||
experience={{
|
||||
booking_id: plan.booking_id ?? null,
|
||||
total_cost: plan.total_cost ?? null,
|
||||
description: plan.description ?? plan.notes,
|
||||
website: plan.website ?? null,
|
||||
address: plan.address ?? null,
|
||||
contact_number: plan.contact_number ?? null,
|
||||
start_date: plan.start_date ?? null,
|
||||
start_time: plan.start_time ?? null,
|
||||
start_timezone: plan.start_timezone ?? null,
|
||||
end_date: plan.end_date ?? null,
|
||||
end_time: plan.end_time ?? null,
|
||||
end_timezone: plan.end_timezone ?? null
|
||||
}}
|
||||
onEdit={() => startEditPlan(plan)}
|
||||
onDelete={() => submitDeletePlan(plan.id)}
|
||||
/>
|
||||
</div>
|
||||
{:else if plan.type === 'packing' || plan.type === 'todo'}
|
||||
<div class="mb-2">
|
||||
<ChecklistCard
|
||||
variant="template"
|
||||
plan={{ title: plan.title, status: 'idea' }}
|
||||
items={(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(plan.items_json ?? '[]');
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.map((item, index) => ({
|
||||
id: `${plan.id}-${index}`,
|
||||
content: typeof item === 'string' ? item : String(item?.content ?? ''),
|
||||
is_checked:
|
||||
typeof item === 'object' && item && 'is_checked' in item
|
||||
? Number(item.is_checked)
|
||||
: 0
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})()}
|
||||
onEdit={() => startEditPlan(plan)}
|
||||
onDelete={() => submitDeletePlan(plan.id)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if addPlanFor?.dayId === day.id && addPlanFor?.type === 'transport'}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/addDayPlan"
|
||||
use:enhance={() => {
|
||||
return ({ result, update }) => {
|
||||
update();
|
||||
if (result.type === 'success') cancelAddPlan();
|
||||
};
|
||||
}}
|
||||
class="mb-2 rounded-md border border-dashed border-gray-200 bg-gray-50/30 p-3"
|
||||
>
|
||||
<input type="hidden" name="dayId" value={day.id} />
|
||||
<input type="hidden" name="type" value="transport" />
|
||||
<p class="mb-2 text-xs font-medium text-gray-500">Add transport</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
name="title"
|
||||
type="text"
|
||||
bind:value={addPlanTitle}
|
||||
placeholder="e.g. Flight to Rome"
|
||||
required
|
||||
class="rounded border border-gray-300 px-2 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
name="notes"
|
||||
bind:value={addPlanNotes}
|
||||
placeholder="Notes (optional)"
|
||||
rows="2"
|
||||
class="resize-none rounded border border-gray-300 px-2 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
></textarea>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={cancelAddPlan}
|
||||
class="rounded border border-gray-300 px-3 py-1.5 text-xs text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => openAddPlan(day.id, 'transport')}
|
||||
class="rounded border border-dashed border-gray-200 px-2 py-1.5 text-xs text-gray-500 hover:border-sky-300 hover:bg-sky-50/50 hover:text-sky-700"
|
||||
>
|
||||
+ Transport
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => openAddLodging(day.id)}
|
||||
class="rounded border border-dashed border-gray-200 px-2 py-1.5 text-xs text-gray-500 hover:border-amber-300 hover:bg-amber-50/50 hover:text-amber-700"
|
||||
>
|
||||
+ Lodging
|
||||
</button>
|
||||
</div>
|
||||
{#if addingTransportDayId !== day.id && addLodgingDayId !== day.id && addingExperienceDay?.dayId !== day.id && addingChecklistDay?.dayId !== day.id}
|
||||
<AddPlanMenu
|
||||
buttonLabel="Add"
|
||||
items={dayAddPlanItems(day.id)}
|
||||
menuWidthClass="w-40"
|
||||
showChevron={false}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,16 @@ import {
|
||||
removeTravellerFromTrip
|
||||
} from '$lib/server/travellers.js';
|
||||
import { createFlight, updateFlight, getFlightBookingsForTrip } from '$lib/server/flights.js';
|
||||
import {
|
||||
createPrivateVehicle,
|
||||
updatePrivateVehicle,
|
||||
getPrivateVehiclesForTrip
|
||||
} from '$lib/server/private-vehicles.js';
|
||||
import {
|
||||
createOtherTransport,
|
||||
updateOtherTransport,
|
||||
getOtherTransportsForTrip
|
||||
} from '$lib/server/other-transports.js';
|
||||
import { createLodging, updateLodging, getLodgingsForTrip } from '$lib/server/lodgings.js';
|
||||
import {
|
||||
createPackageTour,
|
||||
@@ -17,6 +27,17 @@ import {
|
||||
updateTourDay,
|
||||
cloneTemplateDaysToTour
|
||||
} from '$lib/server/package-tours.js';
|
||||
import {
|
||||
createExperience,
|
||||
updateExperience,
|
||||
getExperiencesForTrip
|
||||
} from '$lib/server/experiences.js';
|
||||
import {
|
||||
createChecklist,
|
||||
updateChecklist,
|
||||
getChecklistsForTrip,
|
||||
toggleChecklistItem
|
||||
} from '$lib/server/checklists.js';
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
@@ -31,8 +52,14 @@ export const load: PageServerLoad = async (event) => {
|
||||
const travellers = getTravellersForTrip(trip.id, userId);
|
||||
const people = getPeopleForUser(userId);
|
||||
const flightBookings = getFlightBookingsForTrip(trip.id, userId);
|
||||
const privateVehicles = getPrivateVehiclesForTrip(trip.id, userId);
|
||||
const otherTransports = getOtherTransportsForTrip(trip.id, userId);
|
||||
const lodgings = getLodgingsForTrip(trip.id, userId);
|
||||
const packageTours = getPackageToursForTrip(trip.id, userId);
|
||||
const activities = getExperiencesForTrip(trip.id, userId, 'activity');
|
||||
const restaurants = getExperiencesForTrip(trip.id, userId, 'restaurant');
|
||||
const packingLists = getChecklistsForTrip(trip.id, userId, 'packing');
|
||||
const todos = getChecklistsForTrip(trip.id, userId, 'todo');
|
||||
|
||||
return {
|
||||
trip,
|
||||
@@ -42,8 +69,14 @@ export const load: PageServerLoad = async (event) => {
|
||||
travellerCount: travellers.length,
|
||||
people,
|
||||
flightBookings,
|
||||
privateVehicles,
|
||||
otherTransports,
|
||||
lodgings,
|
||||
packageTours
|
||||
packageTours,
|
||||
activities,
|
||||
restaurants,
|
||||
packingLists,
|
||||
todos
|
||||
};
|
||||
};
|
||||
|
||||
@@ -194,12 +227,94 @@ export const actions: Actions = {
|
||||
if (!trip) return fail(404, { error: 'Trip not found' });
|
||||
|
||||
const data = await event.request.formData();
|
||||
const transportationType = ((data.get('transportation_type') as string)?.trim() || 'flight') as
|
||||
| 'flight'
|
||||
| 'private_vehicle'
|
||||
| 'other';
|
||||
const parentPlanId = (data.get('parent_plan_id') as string)?.trim() || undefined;
|
||||
const status = data.get('status') as string as 'idea' | 'tentative' | 'confirmed';
|
||||
|
||||
if (transportationType === 'private_vehicle') {
|
||||
const startAddress = (data.get('start_address') as string)?.trim();
|
||||
const endAddress = (data.get('end_address') as string)?.trim();
|
||||
const departureDate = (data.get('departure_date') as string)?.trim() || undefined;
|
||||
const departureTime = (data.get('departure_time') as string)?.trim() || undefined;
|
||||
const departureTimezone = (data.get('departure_timezone') as string)?.trim() || undefined;
|
||||
const arrivalDate = (data.get('arrival_date') as string)?.trim() || undefined;
|
||||
const arrivalTime = (data.get('arrival_time') as string)?.trim() || undefined;
|
||||
const arrivalTimezone = (data.get('arrival_timezone') as string)?.trim() || undefined;
|
||||
const startPlanId = (data.get('start_plan_id') as string)?.trim() || undefined;
|
||||
const endPlanId = (data.get('end_plan_id') as string)?.trim() || undefined;
|
||||
|
||||
if (!startAddress || !endAddress) {
|
||||
return fail(400, { error: 'Start and end addresses are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
createPrivateVehicle({
|
||||
tripId: trip.id,
|
||||
userId,
|
||||
parentId: parentPlanId,
|
||||
status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea',
|
||||
startAddress,
|
||||
endAddress,
|
||||
departureDate,
|
||||
departureTime,
|
||||
departureTimezone,
|
||||
arrivalDate,
|
||||
arrivalTime,
|
||||
arrivalTimezone,
|
||||
startPlanId,
|
||||
endPlanId
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return fail(400, {
|
||||
error:
|
||||
err instanceof Error ? err.message : 'Failed to create private vehicle transportation'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (transportationType === 'other') {
|
||||
const title = (data.get('title') as string)?.trim();
|
||||
const notes = (data.get('notes') as string)?.trim() || undefined;
|
||||
const startDate = (data.get('start_date') as string)?.trim() || undefined;
|
||||
const startTime = (data.get('start_time') as string)?.trim() || undefined;
|
||||
const startTimezone = (data.get('start_timezone') as string)?.trim() || undefined;
|
||||
const endDate = (data.get('end_date') as string)?.trim() || undefined;
|
||||
const endTime = (data.get('end_time') as string)?.trim() || undefined;
|
||||
const endTimezone = (data.get('end_timezone') as string)?.trim() || undefined;
|
||||
|
||||
if (!title) return fail(400, { error: 'Title is required' });
|
||||
|
||||
try {
|
||||
createOtherTransport({
|
||||
tripId: trip.id,
|
||||
userId,
|
||||
parentId: parentPlanId,
|
||||
status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea',
|
||||
title,
|
||||
notes,
|
||||
startDate,
|
||||
startTime,
|
||||
startTimezone,
|
||||
endDate,
|
||||
endTime,
|
||||
endTimezone
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return fail(400, {
|
||||
error: err instanceof Error ? err.message : 'Failed to create transportation'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const confirmationNumber = (data.get('confirmation_number') as string)?.trim() || undefined;
|
||||
const priceRaw = (data.get('price') as string)?.trim();
|
||||
const price = priceRaw ? parseFloat(priceRaw) : undefined;
|
||||
const currency = (data.get('currency') as string)?.trim() || 'USD';
|
||||
const status = data.get('status') as string as 'idea' | 'tentative' | 'confirmed';
|
||||
const passengerIds = data.getAll('passenger_ids[]') as string[];
|
||||
|
||||
// Parse segments - form data comes as segments[0][field], segments[1][field], etc.
|
||||
@@ -316,6 +431,91 @@ export const actions: Actions = {
|
||||
if (!trip) return fail(404, { error: 'Trip not found' });
|
||||
|
||||
const data = await event.request.formData();
|
||||
const transportationType = ((data.get('transportation_type') as string)?.trim() || 'flight') as
|
||||
| 'flight'
|
||||
| 'private_vehicle'
|
||||
| 'other';
|
||||
const status = data.get('status') as string as 'idea' | 'tentative' | 'confirmed';
|
||||
|
||||
if (transportationType === 'private_vehicle') {
|
||||
const privateVehicleId = (data.get('private_vehicle_id') as string)?.trim();
|
||||
const startAddress = (data.get('start_address') as string)?.trim();
|
||||
const endAddress = (data.get('end_address') as string)?.trim();
|
||||
const departureDate = (data.get('departure_date') as string)?.trim() || undefined;
|
||||
const departureTime = (data.get('departure_time') as string)?.trim() || undefined;
|
||||
const departureTimezone = (data.get('departure_timezone') as string)?.trim() || undefined;
|
||||
const arrivalDate = (data.get('arrival_date') as string)?.trim() || undefined;
|
||||
const arrivalTime = (data.get('arrival_time') as string)?.trim() || undefined;
|
||||
const arrivalTimezone = (data.get('arrival_timezone') as string)?.trim() || undefined;
|
||||
const startPlanId = (data.get('start_plan_id') as string)?.trim() || undefined;
|
||||
const endPlanId = (data.get('end_plan_id') as string)?.trim() || undefined;
|
||||
|
||||
if (!privateVehicleId) return fail(400, { error: 'Private vehicle ID is required' });
|
||||
if (!startAddress || !endAddress) {
|
||||
return fail(400, { error: 'Start and end addresses are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
updatePrivateVehicle({
|
||||
privateVehicleId,
|
||||
userId,
|
||||
status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea',
|
||||
startAddress,
|
||||
endAddress,
|
||||
departureDate,
|
||||
departureTime,
|
||||
departureTimezone,
|
||||
arrivalDate,
|
||||
arrivalTime,
|
||||
arrivalTimezone,
|
||||
startPlanId,
|
||||
endPlanId
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return fail(400, {
|
||||
error:
|
||||
err instanceof Error ? err.message : 'Failed to update private vehicle transportation'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (transportationType === 'other') {
|
||||
const otherTransportId = (data.get('other_transport_id') as string)?.trim();
|
||||
const title = (data.get('title') as string)?.trim();
|
||||
const notes = (data.get('notes') as string)?.trim() || undefined;
|
||||
const startDate = (data.get('start_date') as string)?.trim() || undefined;
|
||||
const startTime = (data.get('start_time') as string)?.trim() || undefined;
|
||||
const startTimezone = (data.get('start_timezone') as string)?.trim() || undefined;
|
||||
const endDate = (data.get('end_date') as string)?.trim() || undefined;
|
||||
const endTime = (data.get('end_time') as string)?.trim() || undefined;
|
||||
const endTimezone = (data.get('end_timezone') as string)?.trim() || undefined;
|
||||
|
||||
if (!otherTransportId) return fail(400, { error: 'Other transportation ID is required' });
|
||||
if (!title) return fail(400, { error: 'Title is required' });
|
||||
|
||||
try {
|
||||
updateOtherTransport({
|
||||
otherTransportId,
|
||||
userId,
|
||||
status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea',
|
||||
title,
|
||||
notes,
|
||||
startDate,
|
||||
startTime,
|
||||
startTimezone,
|
||||
endDate,
|
||||
endTime,
|
||||
endTimezone
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return fail(400, {
|
||||
error: err instanceof Error ? err.message : 'Failed to update transportation'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const bookingId = (data.get('booking_id') as string)?.trim();
|
||||
if (!bookingId) return fail(400, { error: 'Booking ID is required' });
|
||||
|
||||
@@ -323,7 +523,6 @@ export const actions: Actions = {
|
||||
const priceRaw = (data.get('price') as string)?.trim();
|
||||
const price = priceRaw ? parseFloat(priceRaw) : undefined;
|
||||
const currency = (data.get('currency') as string)?.trim() || 'USD';
|
||||
const status = data.get('status') as string as 'idea' | 'tentative' | 'confirmed';
|
||||
const passengerIds = data.getAll('passenger_ids[]') as string[];
|
||||
|
||||
const segments: Array<{
|
||||
@@ -534,6 +733,206 @@ export const actions: Actions = {
|
||||
}
|
||||
},
|
||||
|
||||
addExperience: async (event) => {
|
||||
const session = await event.locals.auth();
|
||||
const userId = session?.user?.id;
|
||||
if (!userId) return fail(401, { error: 'Not authenticated' });
|
||||
|
||||
const trip = getTripById(event.params.id, userId);
|
||||
if (!trip) return fail(404, { error: 'Trip not found' });
|
||||
|
||||
const formData = await event.request.formData();
|
||||
const type = (formData.get('type') as string)?.trim() as 'activity' | 'restaurant';
|
||||
if (type !== 'activity' && type !== 'restaurant')
|
||||
return fail(400, { error: 'Invalid plan type' });
|
||||
|
||||
const name = (formData.get('name') as string)?.trim();
|
||||
if (!name) return fail(400, { error: 'Name is required' });
|
||||
|
||||
const str = (key: string) => (formData.get(key) as string)?.trim() || undefined;
|
||||
const num = (key: string) => {
|
||||
const v = str(key);
|
||||
return v ? parseFloat(v) : undefined;
|
||||
};
|
||||
const parentPlanId = str('parent_plan_id');
|
||||
const status = formData.get('status') as string as 'idea' | 'tentative' | 'confirmed';
|
||||
|
||||
try {
|
||||
createExperience({
|
||||
tripId: trip.id,
|
||||
userId,
|
||||
type,
|
||||
name,
|
||||
parentId: parentPlanId,
|
||||
status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea',
|
||||
bookingId: str('booking_id'),
|
||||
totalCost: num('total_cost'),
|
||||
description: str('description'),
|
||||
website: str('website'),
|
||||
address: str('address'),
|
||||
contactNumber: str('contact_number'),
|
||||
startDate: str('start_date'),
|
||||
startTime: str('start_time'),
|
||||
startTimezone: str('start_timezone'),
|
||||
endDate: str('end_date'),
|
||||
endTime: str('end_time'),
|
||||
endTimezone: str('end_timezone')
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return fail(400, {
|
||||
error: err instanceof Error ? err.message : 'Failed to create plan'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
editExperience: async (event) => {
|
||||
const session = await event.locals.auth();
|
||||
const userId = session?.user?.id;
|
||||
if (!userId) return fail(401, { error: 'Not authenticated' });
|
||||
|
||||
const trip = getTripById(event.params.id, userId);
|
||||
if (!trip) return fail(404, { error: 'Trip not found' });
|
||||
|
||||
const formData = await event.request.formData();
|
||||
const experienceId = (formData.get('experience_id') as string)?.trim();
|
||||
if (!experienceId) return fail(400, { error: 'Plan ID is required' });
|
||||
|
||||
const name = (formData.get('name') as string)?.trim();
|
||||
if (!name) return fail(400, { error: 'Name is required' });
|
||||
|
||||
const str = (key: string) => (formData.get(key) as string)?.trim() || undefined;
|
||||
const num = (key: string) => {
|
||||
const v = str(key);
|
||||
return v ? parseFloat(v) : undefined;
|
||||
};
|
||||
const status = formData.get('status') as string as 'idea' | 'tentative' | 'confirmed';
|
||||
|
||||
try {
|
||||
updateExperience({
|
||||
experienceId,
|
||||
userId,
|
||||
name,
|
||||
status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea',
|
||||
bookingId: str('booking_id'),
|
||||
totalCost: num('total_cost'),
|
||||
description: str('description'),
|
||||
website: str('website'),
|
||||
address: str('address'),
|
||||
contactNumber: str('contact_number'),
|
||||
startDate: str('start_date'),
|
||||
startTime: str('start_time'),
|
||||
startTimezone: str('start_timezone'),
|
||||
endDate: str('end_date'),
|
||||
endTime: str('end_time'),
|
||||
endTimezone: str('end_timezone')
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return fail(400, {
|
||||
error: err instanceof Error ? err.message : 'Failed to update plan'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addChecklist: async (event) => {
|
||||
const session = await event.locals.auth();
|
||||
const userId = session?.user?.id;
|
||||
if (!userId) return fail(401, { error: 'Not authenticated' });
|
||||
|
||||
const trip = getTripById(event.params.id, userId);
|
||||
if (!trip) return fail(404, { error: 'Trip not found' });
|
||||
|
||||
const formData = await event.request.formData();
|
||||
const type = (formData.get('type') as string)?.trim() as 'packing' | 'todo';
|
||||
if (type !== 'packing' && type !== 'todo') return fail(400, { error: 'Invalid plan type' });
|
||||
|
||||
const name = (formData.get('name') as string)?.trim();
|
||||
if (!name) return fail(400, { error: 'Name is required' });
|
||||
const parentPlanId = (formData.get('parent_plan_id') as string)?.trim() || undefined;
|
||||
const status = formData.get('status') as string as 'idea' | 'tentative' | 'confirmed';
|
||||
const items = formData
|
||||
.getAll('items[]')
|
||||
.map((item) => String(item).trim())
|
||||
.filter(Boolean);
|
||||
|
||||
try {
|
||||
createChecklist({
|
||||
tripId: trip.id,
|
||||
userId,
|
||||
type,
|
||||
name,
|
||||
parentId: parentPlanId,
|
||||
status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea',
|
||||
items
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return fail(400, {
|
||||
error: err instanceof Error ? err.message : 'Failed to create checklist'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
editChecklist: async (event) => {
|
||||
const session = await event.locals.auth();
|
||||
const userId = session?.user?.id;
|
||||
if (!userId) return fail(401, { error: 'Not authenticated' });
|
||||
|
||||
const trip = getTripById(event.params.id, userId);
|
||||
if (!trip) return fail(404, { error: 'Trip not found' });
|
||||
|
||||
const formData = await event.request.formData();
|
||||
const checklistId = (formData.get('checklist_id') as string)?.trim();
|
||||
if (!checklistId) return fail(400, { error: 'Checklist ID is required' });
|
||||
const name = (formData.get('name') as string)?.trim();
|
||||
if (!name) return fail(400, { error: 'Name is required' });
|
||||
const status = formData.get('status') as string as 'idea' | 'tentative' | 'confirmed';
|
||||
const items = formData
|
||||
.getAll('items[]')
|
||||
.map((item) => String(item).trim())
|
||||
.filter(Boolean)
|
||||
.map((content) => ({ content, isChecked: false }));
|
||||
|
||||
try {
|
||||
updateChecklist({
|
||||
checklistId,
|
||||
userId,
|
||||
name,
|
||||
status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea',
|
||||
items
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return fail(400, {
|
||||
error: err instanceof Error ? err.message : 'Failed to update checklist'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleChecklistItem: async (event) => {
|
||||
const session = await event.locals.auth();
|
||||
const userId = session?.user?.id;
|
||||
if (!userId) return fail(401, { error: 'Not authenticated' });
|
||||
|
||||
const trip = getTripById(event.params.id, userId);
|
||||
if (!trip) return fail(404, { error: 'Trip not found' });
|
||||
|
||||
const formData = await event.request.formData();
|
||||
const itemId = (formData.get('item_id') as string)?.trim();
|
||||
if (!itemId) return fail(400, { error: 'Checklist item ID is required' });
|
||||
const isChecked = (formData.get('is_checked') as string)?.trim() === '1';
|
||||
|
||||
try {
|
||||
toggleChecklistItem({ itemId, userId, isChecked });
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return fail(400, {
|
||||
error: err instanceof Error ? err.message : 'Failed to update checklist item'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
addPackageTour: async (event) => {
|
||||
const session = await event.locals.auth();
|
||||
const userId = session?.user?.id;
|
||||
|
||||
@@ -3,15 +3,23 @@
|
||||
import TripWelcome from '$lib/components/TripWelcome.svelte';
|
||||
import AddDestinationModal from '$lib/components/AddDestinationModal.svelte';
|
||||
import AddTravellerModal from '$lib/components/AddTravellerModal.svelte';
|
||||
import AddFlightModal from '$lib/components/AddFlightModal.svelte';
|
||||
import EditFlightModal from '$lib/components/EditFlightModal.svelte';
|
||||
import AddTransportationModal from '$lib/components/AddTransportationModal.svelte';
|
||||
import EditTransportationModal from '$lib/components/EditTransportationModal.svelte';
|
||||
import AddLodgingModal from '$lib/components/AddLodgingModal.svelte';
|
||||
import EditLodgingModal from '$lib/components/EditLodgingModal.svelte';
|
||||
import AddPackageTourModal from '$lib/components/AddPackageTourModal.svelte';
|
||||
import EditPackageTourModal from '$lib/components/EditPackageTourModal.svelte';
|
||||
import ExperienceModal from '$lib/components/ExperienceModal.svelte';
|
||||
import ExperienceCard from '$lib/components/ExperienceCard.svelte';
|
||||
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
|
||||
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
||||
import PackageTourCard from '$lib/components/PackageTourCard.svelte';
|
||||
import AddPlanMenu, { type AddPlanMenuItem } from '$lib/components/AddPlanMenu.svelte';
|
||||
import { PLAN_TYPE_DEFINITIONS, type PlanTypeId } from '$lib/components/plan-types.js';
|
||||
import PlanCard from '$lib/components/PlanCard.svelte';
|
||||
import FlightCard from '$lib/components/FlightCard.svelte';
|
||||
import OtherTransportCard from '$lib/components/OtherTransportCard.svelte';
|
||||
import PrivateVehicleCard from '$lib/components/PrivateVehicleCard.svelte';
|
||||
import LodgingCard from '$lib/components/LodgingCard.svelte';
|
||||
import TravellerChip from '$lib/components/TravellerChip.svelte';
|
||||
|
||||
@@ -24,85 +32,96 @@
|
||||
let people = $derived(data.people ?? []);
|
||||
let tripTravellerIds = $derived(travellers.map((t) => t.id));
|
||||
let flightBookings = $derived(data.flightBookings ?? []);
|
||||
let privateVehicles = $derived(data.privateVehicles ?? []);
|
||||
let otherTransports = $derived(data.otherTransports ?? []);
|
||||
let lodgings = $derived(data.lodgings ?? []);
|
||||
let packageTours = $derived(data.packageTours ?? []);
|
||||
let activities = $derived(data.activities ?? []);
|
||||
let restaurants = $derived(data.restaurants ?? []);
|
||||
let packingLists = $derived(data.packingLists ?? []);
|
||||
let todos = $derived(data.todos ?? []);
|
||||
let editing = $state(false);
|
||||
let showAddDestination = $state(false);
|
||||
let showAddTraveller = $state(false);
|
||||
let showAddFlight = $state(false);
|
||||
let showAddTransportation = $state(false);
|
||||
let showAddLodging = $state(false);
|
||||
let showAddPackageTour = $state(false);
|
||||
let showAddMenu = $state(false);
|
||||
let editingFlight = $state<(typeof flightBookings)[0] | null>(null);
|
||||
let editingFlightPlan = $derived(
|
||||
editingFlight ? (plans.find((p) => p.id === editingFlight!.plan_id) ?? null) : null
|
||||
);
|
||||
let showAddActivity = $state(false);
|
||||
let showAddRestaurant = $state(false);
|
||||
let showAddPackingList = $state(false);
|
||||
let showAddTodo = $state(false);
|
||||
let editingTransportation = $state<
|
||||
| {
|
||||
type: 'flight';
|
||||
planStatus: 'idea' | 'tentative' | 'confirmed';
|
||||
flightBooking: (typeof flightBookings)[0];
|
||||
}
|
||||
| {
|
||||
type: 'private_vehicle';
|
||||
planStatus: 'idea' | 'tentative' | 'confirmed';
|
||||
privateVehicle: (typeof privateVehicles)[0];
|
||||
}
|
||||
| {
|
||||
type: 'other';
|
||||
planStatus: 'idea' | 'tentative' | 'confirmed';
|
||||
planTitle: string;
|
||||
planNotes: string | null;
|
||||
otherTransport: (typeof otherTransports)[0];
|
||||
}
|
||||
| null
|
||||
>(null);
|
||||
let editingLodging = $state<(typeof lodgings)[0] | null>(null);
|
||||
let editingTour = $state<(typeof packageTours)[0] | null>(null);
|
||||
let editingActivity = $state<{
|
||||
plan: (typeof plans)[0];
|
||||
experience: (typeof activities)[0];
|
||||
} | null>(null);
|
||||
let editingRestaurant = $state<{
|
||||
plan: (typeof plans)[0];
|
||||
experience: (typeof restaurants)[0];
|
||||
} | null>(null);
|
||||
let editingPackingList = $state<{
|
||||
plan: (typeof plans)[0];
|
||||
list: (typeof packingLists)[0];
|
||||
} | null>(null);
|
||||
let editingTodo = $state<{ plan: (typeof plans)[0]; list: (typeof todos)[0] } | null>(null);
|
||||
let checklistToggleFormRef = $state<HTMLFormElement | null>(null);
|
||||
let togglingChecklistItemId = $state<string | null>(null);
|
||||
let togglingChecklistItemChecked = $state<'0' | '1'>('0');
|
||||
let addingChildToPlanId = $state<string | null>(null);
|
||||
|
||||
const menuItems = [
|
||||
let transportationLocationOptions = $derived(
|
||||
plans
|
||||
.filter((plan) => plan.type !== 'transport' && plan.type !== 'day')
|
||||
.map((plan) => ({ id: plan.id, label: plan.title }))
|
||||
);
|
||||
|
||||
const addPlanHandlers: Partial<Record<PlanTypeId, () => void>> = {
|
||||
destination: () => (showAddDestination = true),
|
||||
activity: () => (showAddActivity = true),
|
||||
transport: () => (showAddTransportation = true),
|
||||
lodging: () => (showAddLodging = true),
|
||||
restaurant: () => (showAddRestaurant = true),
|
||||
packageTour: () => (showAddPackageTour = true),
|
||||
packingList: () => (showAddPackingList = true),
|
||||
todo: () => (showAddTodo = true)
|
||||
};
|
||||
|
||||
const addToTripMenuItems = $derived<AddPlanMenuItem[]>([
|
||||
{
|
||||
label: 'Destinations',
|
||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#3B82F6" stroke-width="1.5"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5"/></svg>`,
|
||||
onclick: () => {
|
||||
showAddDestination = true;
|
||||
showAddMenu = false;
|
||||
}
|
||||
id: 'travellers',
|
||||
label: 'Travellers',
|
||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#6366F1" stroke-width="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></svg>`,
|
||||
onclick: () => (showAddTraveller = true)
|
||||
},
|
||||
{
|
||||
label: 'Attractions & Activities',
|
||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22C55E" stroke-width="1.5"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`,
|
||||
onclick: () => {
|
||||
showAddMenu = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Transportation',
|
||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#F97316" stroke-width="1.5"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-3.99-3.99A19.79 19.79 0 0 1 4.1 6.18 2 2 0 0 1 6.08 4h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L10.09 11a16 16 0 0 0 5.91 5.91l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 24 18z"/></svg>`,
|
||||
onclick: () => {
|
||||
showAddFlight = true;
|
||||
showAddMenu = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Lodgings',
|
||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#A855F7" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`,
|
||||
onclick: () => {
|
||||
showAddLodging = true;
|
||||
showAddMenu = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Restaurants',
|
||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#F43F5E" stroke-width="1.5"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>`,
|
||||
onclick: () => {
|
||||
showAddMenu = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Package Tours',
|
||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="1.5"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/><line x1="12" y1="12" x2="12" y2="16"/><line x1="10" y1="14" x2="14" y2="14"/></svg>`,
|
||||
onclick: () => {
|
||||
showAddPackageTour = true;
|
||||
showAddMenu = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Packing List',
|
||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#14B8A6" stroke-width="1.5"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>`,
|
||||
onclick: () => {
|
||||
showAddMenu = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'To-dos',
|
||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="1.5"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`,
|
||||
onclick: () => {
|
||||
showAddMenu = false;
|
||||
}
|
||||
}
|
||||
];
|
||||
...PLAN_TYPE_DEFINITIONS.map((definition, index) => ({
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
icon: definition.icon,
|
||||
dividerBefore: index === 0,
|
||||
onclick: () => addPlanHandlers[definition.id]?.()
|
||||
}))
|
||||
]);
|
||||
|
||||
function formatDate(d: string | null) {
|
||||
if (!d) return 'TBD';
|
||||
@@ -112,6 +131,12 @@
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
function toggleChecklistItem(itemId: string | number, nextChecked: boolean) {
|
||||
togglingChecklistItemId = String(itemId);
|
||||
togglingChecklistItemChecked = nextChecked ? '1' : '0';
|
||||
setTimeout(() => checklistToggleFormRef?.requestSubmit(), 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -125,23 +150,24 @@
|
||||
{people}
|
||||
{tripTravellerIds}
|
||||
/>
|
||||
<AddFlightModal
|
||||
open={showAddFlight}
|
||||
<AddTransportationModal
|
||||
open={showAddTransportation}
|
||||
onclose={() => {
|
||||
showAddFlight = false;
|
||||
showAddTransportation = false;
|
||||
addingChildToPlanId = null;
|
||||
}}
|
||||
{people}
|
||||
{tripTravellerIds}
|
||||
planOptions={transportationLocationOptions}
|
||||
parentPlanId={addingChildToPlanId ?? undefined}
|
||||
/>
|
||||
<EditFlightModal
|
||||
open={!!editingFlight}
|
||||
flightBooking={editingFlight}
|
||||
planStatus={editingFlightPlan?.status}
|
||||
onclose={() => (editingFlight = null)}
|
||||
<EditTransportationModal
|
||||
open={!!editingTransportation}
|
||||
transportation={editingTransportation}
|
||||
onclose={() => (editingTransportation = null)}
|
||||
{people}
|
||||
{tripTravellerIds}
|
||||
planOptions={transportationLocationOptions}
|
||||
/>
|
||||
<AddLodgingModal
|
||||
open={showAddLodging}
|
||||
@@ -173,6 +199,123 @@
|
||||
{people}
|
||||
{tripTravellerIds}
|
||||
/>
|
||||
<ExperienceModal
|
||||
open={showAddActivity}
|
||||
onclose={() => {
|
||||
showAddActivity = false;
|
||||
addingChildToPlanId = null;
|
||||
}}
|
||||
planType="activity"
|
||||
mode="add"
|
||||
formAction="?/addExperience"
|
||||
parentPlanId={addingChildToPlanId ?? undefined}
|
||||
/>
|
||||
<ExperienceModal
|
||||
open={showAddRestaurant}
|
||||
onclose={() => {
|
||||
showAddRestaurant = false;
|
||||
addingChildToPlanId = null;
|
||||
}}
|
||||
planType="restaurant"
|
||||
mode="add"
|
||||
formAction="?/addExperience"
|
||||
parentPlanId={addingChildToPlanId ?? undefined}
|
||||
/>
|
||||
<ExperienceModal
|
||||
open={editingActivity != null}
|
||||
onclose={() => (editingActivity = null)}
|
||||
planType="activity"
|
||||
mode="edit"
|
||||
formAction="?/editExperience"
|
||||
idValue={editingActivity?.experience.id}
|
||||
initial={editingActivity
|
||||
? {
|
||||
name: editingActivity.plan.title,
|
||||
status: editingActivity.plan.status,
|
||||
...editingActivity.experience
|
||||
}
|
||||
: null}
|
||||
/>
|
||||
<ExperienceModal
|
||||
open={editingRestaurant != null}
|
||||
onclose={() => (editingRestaurant = null)}
|
||||
planType="restaurant"
|
||||
mode="edit"
|
||||
formAction="?/editExperience"
|
||||
idValue={editingRestaurant?.experience.id}
|
||||
initial={editingRestaurant
|
||||
? {
|
||||
name: editingRestaurant.plan.title,
|
||||
status: editingRestaurant.plan.status,
|
||||
...editingRestaurant.experience
|
||||
}
|
||||
: null}
|
||||
/>
|
||||
<ChecklistModal
|
||||
open={showAddPackingList}
|
||||
onclose={() => {
|
||||
showAddPackingList = false;
|
||||
addingChildToPlanId = null;
|
||||
}}
|
||||
checklistType="packing"
|
||||
mode="add"
|
||||
formAction="?/addChecklist"
|
||||
parentPlanId={addingChildToPlanId ?? undefined}
|
||||
/>
|
||||
<ChecklistModal
|
||||
open={showAddTodo}
|
||||
onclose={() => {
|
||||
showAddTodo = false;
|
||||
addingChildToPlanId = null;
|
||||
}}
|
||||
checklistType="todo"
|
||||
mode="add"
|
||||
formAction="?/addChecklist"
|
||||
parentPlanId={addingChildToPlanId ?? undefined}
|
||||
/>
|
||||
<ChecklistModal
|
||||
open={editingPackingList != null}
|
||||
onclose={() => (editingPackingList = null)}
|
||||
checklistType="packing"
|
||||
mode="edit"
|
||||
formAction="?/editChecklist"
|
||||
idValue={editingPackingList?.list.id}
|
||||
initial={editingPackingList
|
||||
? {
|
||||
name: editingPackingList.plan.title,
|
||||
status: editingPackingList.plan.status,
|
||||
items: editingPackingList.list.items.map((item) => ({ content: item.content }))
|
||||
}
|
||||
: null}
|
||||
/>
|
||||
<ChecklistModal
|
||||
open={editingTodo != null}
|
||||
onclose={() => (editingTodo = null)}
|
||||
checklistType="todo"
|
||||
mode="edit"
|
||||
formAction="?/editChecklist"
|
||||
idValue={editingTodo?.list.id}
|
||||
initial={editingTodo
|
||||
? {
|
||||
name: editingTodo.plan.title,
|
||||
status: editingTodo.plan.status,
|
||||
items: editingTodo.list.items.map((item) => ({ content: item.content }))
|
||||
}
|
||||
: null}
|
||||
/>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/toggleChecklistItem"
|
||||
bind:this={checklistToggleFormRef}
|
||||
use:enhance={() =>
|
||||
({ update }) =>
|
||||
update()}
|
||||
class="hidden"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<input type="hidden" name="item_id" value={togglingChecklistItemId ?? ''} />
|
||||
<input type="hidden" name="is_checked" value={togglingChecklistItemChecked} />
|
||||
</form>
|
||||
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<!-- Header -->
|
||||
@@ -183,84 +326,13 @@
|
||||
<h1 class="text-2xl font-bold text-gray-900">{trip.name}</h1>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
{#if planCount > 0 || travellerCount > 0}
|
||||
<div class="relative">
|
||||
<button
|
||||
onclick={() => (showAddMenu = !showAddMenu)}
|
||||
class="flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
Add to trip
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{#if showAddMenu}
|
||||
<div
|
||||
class="fixed inset-0 z-10"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
onclick={() => (showAddMenu = false)}
|
||||
onkeydown={(e) => e.key === 'Escape' && (showAddMenu = false)}
|
||||
></div>
|
||||
<div
|
||||
class="absolute right-0 z-20 mt-1 w-56 overflow-hidden rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
|
||||
>
|
||||
<!-- Travellers — separate from plan types -->
|
||||
<button
|
||||
onclick={() => {
|
||||
showAddTraveller = true;
|
||||
showAddMenu = false;
|
||||
}}
|
||||
class="flex w-full items-center gap-3 px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#6366F1"
|
||||
stroke-width="1.5"
|
||||
><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle
|
||||
cx="9"
|
||||
cy="7"
|
||||
r="4"
|
||||
/><path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path
|
||||
d="M16 3.13a4 4 0 0 1 0 7.75"
|
||||
/></svg
|
||||
>
|
||||
Travellers
|
||||
</button>
|
||||
<hr class="my-1 border-gray-100" />
|
||||
{#each menuItems as item}
|
||||
<button
|
||||
onclick={item.onclick}
|
||||
class="flex w-full items-center gap-3 px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
{@html item.icon}
|
||||
{item.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<AddPlanMenu
|
||||
buttonLabel="Add to trip"
|
||||
items={addToTripMenuItems}
|
||||
variant="primary"
|
||||
align="right"
|
||||
menuWidthClass="w-56"
|
||||
/>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => (editing = true)}
|
||||
@@ -462,8 +534,13 @@
|
||||
tripName={trip.name}
|
||||
onAddDestination={() => (showAddDestination = true)}
|
||||
onAddTraveller={() => (showAddTraveller = true)}
|
||||
onAddFlight={() => (showAddFlight = true)}
|
||||
onAddActivity={() => (showAddActivity = true)}
|
||||
onAddTransportation={() => (showAddTransportation = true)}
|
||||
onAddLodging={() => (showAddLodging = true)}
|
||||
onAddRestaurant={() => (showAddRestaurant = true)}
|
||||
onAddPackageTour={() => (showAddPackageTour = true)}
|
||||
onAddPackingList={() => (showAddPackingList = true)}
|
||||
onAddTodo={() => (showAddTodo = true)}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Plans list -->
|
||||
@@ -496,15 +573,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flights section -->
|
||||
{#if flightBookings.length > 0}
|
||||
<!-- Transportation section -->
|
||||
{#if flightBookings.length > 0 || privateVehicles.length > 0 || otherTransports.length > 0}
|
||||
<div class="mt-8">
|
||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
||||
Transportation
|
||||
</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each flightBookings as flightBooking (flightBooking.id)}
|
||||
{@const plan = plans.find((p) => p.id === flightBooking.plan_id)}
|
||||
{#each plans.filter((p) => p.type === 'transport') as plan (plan.id)}
|
||||
{@const flightBooking = flightBookings.find((b) => b.plan_id === plan.id)}
|
||||
{@const privateVehicle = privateVehicles.find((pv) => pv.plan_id === plan.id)}
|
||||
{@const otherTransport = otherTransports.find((ot) => ot.plan_id === plan.id)}
|
||||
{#if plan}
|
||||
{@const formId = `remove-plan-${plan.id}`}
|
||||
{@const submitForm = () => {
|
||||
@@ -523,10 +602,83 @@
|
||||
class="contents"
|
||||
>
|
||||
<input type="hidden" name="plan_id" value={plan.id} />
|
||||
<FlightCard
|
||||
{#if flightBooking}
|
||||
<FlightCard
|
||||
{plan}
|
||||
{flightBooking}
|
||||
onEdit={() =>
|
||||
(editingTransportation = {
|
||||
type: 'flight',
|
||||
flightBooking,
|
||||
planStatus: plan.status
|
||||
})}
|
||||
onDelete={submitForm}
|
||||
/>
|
||||
{:else if privateVehicle}
|
||||
<PrivateVehicleCard
|
||||
{plan}
|
||||
{privateVehicle}
|
||||
onEdit={() =>
|
||||
(editingTransportation = {
|
||||
type: 'private_vehicle',
|
||||
privateVehicle,
|
||||
planStatus: plan.status
|
||||
})}
|
||||
onDelete={submitForm}
|
||||
/>
|
||||
{:else if otherTransport}
|
||||
<OtherTransportCard
|
||||
{plan}
|
||||
{otherTransport}
|
||||
onEdit={() =>
|
||||
(editingTransportation = {
|
||||
type: 'other',
|
||||
otherTransport,
|
||||
planStatus: plan.status,
|
||||
planTitle: plan.title,
|
||||
planNotes: plan.notes
|
||||
})}
|
||||
onDelete={submitForm}
|
||||
/>
|
||||
{/if}
|
||||
</form>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Activities section -->
|
||||
{#if activities.length > 0}
|
||||
<div class="mt-8">
|
||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
||||
Attractions & Activities
|
||||
</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each activities as activity (activity.id)}
|
||||
{@const plan = plans.find((p) => p.id === activity.plan_id)}
|
||||
{#if plan}
|
||||
{@const formId = `remove-plan-${plan.id}`}
|
||||
{@const submitForm = () => {
|
||||
const form = document.getElementById(formId) as HTMLFormElement;
|
||||
form?.requestSubmit();
|
||||
}}
|
||||
<form
|
||||
id={formId}
|
||||
method="POST"
|
||||
action="?/removePlan"
|
||||
use:enhance={() => {
|
||||
return ({ update }) => {
|
||||
update();
|
||||
};
|
||||
}}
|
||||
class="contents"
|
||||
>
|
||||
<input type="hidden" name="plan_id" value={plan.id} />
|
||||
<ExperienceCard
|
||||
{plan}
|
||||
{flightBooking}
|
||||
onEdit={() => (editingFlight = flightBooking)}
|
||||
experience={activity}
|
||||
onEdit={() => (editingActivity = { plan, experience: activity })}
|
||||
onDelete={submitForm}
|
||||
/>
|
||||
</form>
|
||||
@@ -536,6 +688,126 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Restaurants section -->
|
||||
{#if restaurants.length > 0}
|
||||
<div class="mt-8">
|
||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
||||
Restaurants
|
||||
</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each restaurants as restaurant (restaurant.id)}
|
||||
{@const plan = plans.find((p) => p.id === restaurant.plan_id)}
|
||||
{#if plan}
|
||||
{@const formId = `remove-plan-${plan.id}`}
|
||||
{@const submitForm = () => {
|
||||
const form = document.getElementById(formId) as HTMLFormElement;
|
||||
form?.requestSubmit();
|
||||
}}
|
||||
<form
|
||||
id={formId}
|
||||
method="POST"
|
||||
action="?/removePlan"
|
||||
use:enhance={() => {
|
||||
return ({ update }) => {
|
||||
update();
|
||||
};
|
||||
}}
|
||||
class="contents"
|
||||
>
|
||||
<input type="hidden" name="plan_id" value={plan.id} />
|
||||
<ExperienceCard
|
||||
{plan}
|
||||
experience={restaurant}
|
||||
onEdit={() => (editingRestaurant = { plan, experience: restaurant })}
|
||||
onDelete={submitForm}
|
||||
/>
|
||||
</form>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Packing list section -->
|
||||
{#if packingLists.length > 0}
|
||||
<div class="mt-8">
|
||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">
|
||||
Packing Lists
|
||||
</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each packingLists as list (list.id)}
|
||||
{@const plan = plans.find((p) => p.id === list.plan_id)}
|
||||
{#if plan}
|
||||
{@const formId = `remove-plan-${plan.id}`}
|
||||
{@const submitForm = () => {
|
||||
const form = document.getElementById(formId) as HTMLFormElement;
|
||||
form?.requestSubmit();
|
||||
}}
|
||||
<form
|
||||
id={formId}
|
||||
method="POST"
|
||||
action="?/removePlan"
|
||||
use:enhance={() => {
|
||||
return ({ update }) => {
|
||||
update();
|
||||
};
|
||||
}}
|
||||
class="contents"
|
||||
>
|
||||
<input type="hidden" name="plan_id" value={plan.id} />
|
||||
<ChecklistCard
|
||||
{plan}
|
||||
items={list.items}
|
||||
onEdit={() => (editingPackingList = { plan, list })}
|
||||
onDelete={submitForm}
|
||||
onToggleItem={toggleChecklistItem}
|
||||
/>
|
||||
</form>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- To-dos section -->
|
||||
{#if todos.length > 0}
|
||||
<div class="mt-8">
|
||||
<h2 class="mb-3 text-sm font-semibold tracking-wider text-gray-400 uppercase">To-dos</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each todos as list (list.id)}
|
||||
{@const plan = plans.find((p) => p.id === list.plan_id)}
|
||||
{#if plan}
|
||||
{@const formId = `remove-plan-${plan.id}`}
|
||||
{@const submitForm = () => {
|
||||
const form = document.getElementById(formId) as HTMLFormElement;
|
||||
form?.requestSubmit();
|
||||
}}
|
||||
<form
|
||||
id={formId}
|
||||
method="POST"
|
||||
action="?/removePlan"
|
||||
use:enhance={() => {
|
||||
return ({ update }) => {
|
||||
update();
|
||||
};
|
||||
}}
|
||||
class="contents"
|
||||
>
|
||||
<input type="hidden" name="plan_id" value={plan.id} />
|
||||
<ChecklistCard
|
||||
{plan}
|
||||
items={list.items}
|
||||
onEdit={() => (editingTodo = { plan, list })}
|
||||
onDelete={submitForm}
|
||||
onToggleItem={toggleChecklistItem}
|
||||
/>
|
||||
</form>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Lodgings section -->
|
||||
{#if lodgings.length > 0}
|
||||
<div class="mt-8">
|
||||
@@ -608,17 +880,17 @@
|
||||
{tour}
|
||||
onEdit={() => (editingTour = tour)}
|
||||
onDelete={submitForm}
|
||||
onAddFlight={() => {
|
||||
onAddTransportation={() => {
|
||||
addingChildToPlanId = plan.id;
|
||||
showAddFlight = true;
|
||||
showAddTransportation = true;
|
||||
}}
|
||||
onAddLodging={() => {
|
||||
addingChildToPlanId = plan.id;
|
||||
showAddLodging = true;
|
||||
}}
|
||||
onAddFlightToDay={(dayPlanId) => {
|
||||
onAddTransportationToDay={(dayPlanId) => {
|
||||
addingChildToPlanId = dayPlanId;
|
||||
showAddFlight = true;
|
||||
showAddTransportation = true;
|
||||
}}
|
||||
onAddLodgingToDay={(dayPlanId) => {
|
||||
addingChildToPlanId = dayPlanId;
|
||||
|
||||
Reference in New Issue
Block a user