import { db } from './db/index.js'; import type { Trip } from './trips.js'; export type TripStatus = 'past' | 'in_progress' | 'planned'; export type MapStopStatus = 'past' | 'current' | 'future'; export interface DashboardTripSummary extends Trip { status: TripStatus; planCount: number; } export interface DashboardMapStop { tripId: string; tripName: string; label: string; country: string | null; countryCode: string; lat: number; lon: number; status: MapStopStatus; } export interface DashboardStats { pastTrips: number; daysAway: number; countriesVisited: number; citiesVisited: number; topCountries: Array<{ country: string; countryCode: string; count: number }>; } export interface DashboardData { trips: DashboardTripSummary[]; inProgressTrips: DashboardTripSummary[]; plannedTrips: DashboardTripSummary[]; mapStops: DashboardMapStop[]; stats: DashboardStats; } function isValidDate(value: string | null): value is string { return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value)); } function toDate(value: string | null): Date | null { if (!isValidDate(value)) return null; const parsed = new Date(`${value}T00:00:00Z`); return Number.isNaN(parsed.getTime()) ? null : parsed; } function dayDiffInclusive(start: string | null, end: string | null): number { const startDate = toDate(start); const endDate = toDate(end); if (!startDate || !endDate) return 0; const diffMs = endDate.getTime() - startDate.getTime(); if (Number.isNaN(diffMs)) return 0; return Math.max(0, Math.round(diffMs / 86400000) + 1); } function getTripStatus(trip: Trip, today: string): TripStatus { if (trip.end_date && trip.end_date < today) return 'past'; if (trip.start_date && trip.start_date <= today) return 'in_progress'; return 'planned'; } export function getDashboardData(userId: string): DashboardData { const today = new Date().toISOString().slice(0, 10); const trips = db.all( `SELECT * FROM trips WHERE user_id = ? ORDER BY COALESCE(start_date, created_at) ASC`, [userId] ); const planCounts = db.all<{ trip_id: string; count: number }>( `SELECT trip_id, COUNT(*) as count FROM plans WHERE user_id = ? GROUP BY trip_id`, [userId] ); const planCountByTripId = new Map(planCounts.map((row) => [row.trip_id, row.count])); const summaryTrips: DashboardTripSummary[] = trips.map((trip) => ({ ...trip, status: getTripStatus(trip, today), planCount: planCountByTripId.get(trip.id) ?? 0 })); const inProgressTrips = summaryTrips.filter((trip) => trip.status === 'in_progress'); const plannedTrips = summaryTrips.filter((trip) => trip.status === 'planned'); const pastTrips = summaryTrips.filter((trip) => trip.status === 'past'); const mapStopRows = db.all<{ trip_id: string; trip_name: string; label: string | null; country: string | null; country_code: string | null; lat: number | null; lon: number | null; }>( `SELECT p.trip_id as trip_id, MAX(t.name) as trip_name, COALESCE(MAX(p.city_name), MAX(p.country)) as label, MAX(p.country) as country, p.country_code as country_code, c.lat as lat, c.lon as lon FROM plans p JOIN trips t ON t.id = p.trip_id JOIN ( SELECT country_code, AVG(latitude) as lat, AVG(longitude) as lon FROM airports WHERE latitude IS NOT NULL AND longitude IS NOT NULL GROUP BY country_code ) c ON c.country_code = p.country_code WHERE p.user_id = ? AND p.type = 'destination' AND p.country_code IS NOT NULL AND TRIM(p.country_code) != '' GROUP BY p.trip_id, p.country_code ORDER BY MIN(p.created_at) ASC`, [userId] ); const statusByTripId = new Map(summaryTrips.map((trip) => [trip.id, trip.status])); const mapStops: DashboardMapStop[] = mapStopRows .filter((row) => row.country_code && row.lat !== null && row.lon !== null) .map((row) => { const status = statusByTripId.get(row.trip_id) ?? 'planned'; const mapStatus: MapStopStatus = status === 'past' ? 'past' : status === 'in_progress' ? 'current' : 'future'; return { tripId: row.trip_id, tripName: row.trip_name ?? 'Trip', label: row.label ?? row.country ?? 'Destination', country: row.country, countryCode: row.country_code ?? 'XX', lat: row.lat ?? 0, lon: row.lon ?? 0, status: mapStatus }; }); const destinationRows = db.all<{ country: string | null; country_code: string | null; city_name: string | null; }>( `SELECT p.country, p.country_code, p.city_name FROM plans p JOIN trips t ON t.id = p.trip_id WHERE p.user_id = ? AND t.user_id = ? AND t.end_date IS NOT NULL AND t.end_date < ? AND p.type = 'destination'`, [userId, userId, today] ); const countryCounts = new Map(); const cities = new Set(); for (const row of destinationRows) { if (row.city_name) cities.add(row.city_name); if (!row.country_code) continue; const existing = countryCounts.get(row.country_code) ?? { country: row.country ?? row.country_code, countryCode: row.country_code, count: 0 }; existing.count += 1; countryCounts.set(row.country_code, existing); } const topCountries = Array.from(countryCounts.values()) .sort((a, b) => b.count - a.count || a.country.localeCompare(b.country)) .slice(0, 5); const daysAway = pastTrips.reduce( (total, trip) => total + dayDiffInclusive(trip.start_date, trip.end_date), 0 ); return { trips: summaryTrips, inProgressTrips, plannedTrips, mapStops, stats: { pastTrips: pastTrips.length, daysAway, countriesVisited: countryCounts.size, citiesVisited: cities.size, topCountries } }; }