feat: add PDF reports, timezone management, GPS QR codes, and fix GPS tracking gaps
Issue #1: QR button on GPS Devices tab for re-enrollment Issue #2: App-wide timezone setting with TimezoneContext, useFormattedDate hook, and admin timezone selector. All date displays now respect the configured timezone. Issue #3: PDF export for Accountability Roster using @react-pdf/renderer with professional styling matching VIPSchedulePDF. Added Signal send button. Issue #4: Fixed GPS "teleporting" gaps - syncPositions now fetches position history per device instead of only latest position. Changed cron to every 30s, added unique constraint on deviceId+timestamp for deduplication, lowered min interval to 10s. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -215,60 +215,181 @@ export class SeedService {
|
||||
|
||||
private getFlightData(vips: any[]) {
|
||||
const flights: any[] = [];
|
||||
const airlines = ['AA', 'UA', 'DL', 'SW', 'AS', 'JB'];
|
||||
const origins = ['JFK', 'LAX', 'ORD', 'DFW', 'ATL', 'SFO', 'SEA', 'BOS', 'DEN', 'MIA'];
|
||||
const destination = 'SLC';
|
||||
|
||||
vips.forEach((vip, index) => {
|
||||
const airline = airlines[index % airlines.length];
|
||||
const flightNum = `${airline}${1000 + index * 123}`;
|
||||
const origin = origins[index % origins.length];
|
||||
// Build a name->vip lookup for named scenarios
|
||||
const vipByName = new Map<string, any>();
|
||||
vips.forEach(v => vipByName.set(v.name, v));
|
||||
|
||||
// Arrival flight - times relative to now
|
||||
const arrivalOffset = (index % 8) * 30 - 60;
|
||||
const scheduledArrival = this.relativeTime(arrivalOffset);
|
||||
// Helper: create a flight record
|
||||
const makeFlight = (vipId: string, opts: any) => ({
|
||||
vipId,
|
||||
flightDate: new Date(),
|
||||
...opts,
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// NAMED MULTI-SEGMENT SCENARIOS
|
||||
// ============================================================
|
||||
|
||||
// Roger Krone: 3-segment journey, all landed cleanly
|
||||
// BWI -> ORD -> DEN -> SLC
|
||||
const krone = vipByName.get('Roger A. Krone');
|
||||
if (krone) {
|
||||
flights.push(makeFlight(krone.id, {
|
||||
segment: 1, flightNumber: 'UA410', departureAirport: 'BWI', arrivalAirport: 'ORD',
|
||||
scheduledDeparture: this.relativeTime(-360), scheduledArrival: this.relativeTime(-240),
|
||||
actualDeparture: this.relativeTime(-358), actualArrival: this.relativeTime(-235),
|
||||
status: 'landed',
|
||||
}));
|
||||
flights.push(makeFlight(krone.id, {
|
||||
segment: 2, flightNumber: 'UA672', departureAirport: 'ORD', arrivalAirport: 'DEN',
|
||||
scheduledDeparture: this.relativeTime(-150), scheduledArrival: this.relativeTime(-60),
|
||||
actualDeparture: this.relativeTime(-148), actualArrival: this.relativeTime(-55),
|
||||
status: 'landed',
|
||||
}));
|
||||
flights.push(makeFlight(krone.id, {
|
||||
segment: 3, flightNumber: 'UA1190', departureAirport: 'DEN', arrivalAirport: destination,
|
||||
scheduledDeparture: this.relativeTime(-20), scheduledArrival: this.relativeTime(40),
|
||||
actualDeparture: this.relativeTime(-18), actualArrival: null,
|
||||
status: 'active',
|
||||
arrivalTerminal: '2', arrivalGate: 'B7',
|
||||
}));
|
||||
}
|
||||
|
||||
// Sarah Chen: 2-segment, leg 1 landed, leg 2 active/arriving
|
||||
// JFK -> ORD -> SLC
|
||||
const chen = vipByName.get('Sarah Chen');
|
||||
if (chen) {
|
||||
flights.push(makeFlight(chen.id, {
|
||||
segment: 1, flightNumber: 'AA234', departureAirport: 'JFK', arrivalAirport: 'ORD',
|
||||
scheduledDeparture: this.relativeTime(-300), scheduledArrival: this.relativeTime(-180),
|
||||
actualDeparture: this.relativeTime(-298), actualArrival: this.relativeTime(-175),
|
||||
status: 'landed',
|
||||
}));
|
||||
flights.push(makeFlight(chen.id, {
|
||||
segment: 2, flightNumber: 'AA1456', departureAirport: 'ORD', arrivalAirport: destination,
|
||||
scheduledDeparture: this.relativeTime(-90), scheduledArrival: this.relativeTime(30),
|
||||
actualDeparture: this.relativeTime(-88), actualArrival: null,
|
||||
status: 'active',
|
||||
arrivalTerminal: '1', arrivalGate: 'A12',
|
||||
}));
|
||||
}
|
||||
|
||||
// Roberto Gonzalez: 2-segment, leg 1 DELAYED 45min - threatens connection
|
||||
// LAX -> DFW -> SLC (90min layover scheduled, now ~45min WARNING)
|
||||
const gonzalez = vipByName.get('Roberto Gonzalez');
|
||||
if (gonzalez) {
|
||||
flights.push(makeFlight(gonzalez.id, {
|
||||
segment: 1, flightNumber: 'DL890', departureAirport: 'LAX', arrivalAirport: 'DFW',
|
||||
scheduledDeparture: this.relativeTime(-240), scheduledArrival: this.relativeTime(-90),
|
||||
estimatedArrival: this.relativeTime(-45), // 45min late
|
||||
actualDeparture: this.relativeTime(-195), // departed 45min late
|
||||
departureDelay: 45, arrivalDelay: 45,
|
||||
status: 'active', // still in the air
|
||||
}));
|
||||
flights.push(makeFlight(gonzalez.id, {
|
||||
segment: 2, flightNumber: 'DL1522', departureAirport: 'DFW', arrivalAirport: destination,
|
||||
scheduledDeparture: this.relativeTime(0), scheduledArrival: this.relativeTime(150),
|
||||
status: 'scheduled',
|
||||
departureTerminal: 'E', departureGate: 'E14',
|
||||
}));
|
||||
}
|
||||
|
||||
// Thomas Anderson: 2-segment, MISSED CONNECTION
|
||||
// BOS -> ORD -> SLC (leg 1 arrived 25min late, leg 2 already departed)
|
||||
const anderson = vipByName.get('Thomas Anderson');
|
||||
if (anderson) {
|
||||
flights.push(makeFlight(anderson.id, {
|
||||
segment: 1, flightNumber: 'JB320', departureAirport: 'BOS', arrivalAirport: 'ORD',
|
||||
scheduledDeparture: this.relativeTime(-240), scheduledArrival: this.relativeTime(-90),
|
||||
actualDeparture: this.relativeTime(-215), actualArrival: this.relativeTime(-65),
|
||||
departureDelay: 25, arrivalDelay: 25,
|
||||
status: 'landed',
|
||||
}));
|
||||
flights.push(makeFlight(anderson.id, {
|
||||
segment: 2, flightNumber: 'JB988', departureAirport: 'ORD', arrivalAirport: destination,
|
||||
scheduledDeparture: this.relativeTime(-75), scheduledArrival: this.relativeTime(15),
|
||||
actualDeparture: this.relativeTime(-75), // departed on time - before leg 1 landed
|
||||
status: 'active', // plane left without him
|
||||
}));
|
||||
}
|
||||
|
||||
// Marcus Johnson: 2-segment, both landed cleanly
|
||||
// ATL -> DEN -> SLC
|
||||
const johnson = vipByName.get('Marcus Johnson');
|
||||
if (johnson) {
|
||||
flights.push(makeFlight(johnson.id, {
|
||||
segment: 1, flightNumber: 'DL512', departureAirport: 'ATL', arrivalAirport: 'DEN',
|
||||
scheduledDeparture: this.relativeTime(-360), scheduledArrival: this.relativeTime(-210),
|
||||
actualDeparture: this.relativeTime(-358), actualArrival: this.relativeTime(-205),
|
||||
status: 'landed',
|
||||
}));
|
||||
flights.push(makeFlight(johnson.id, {
|
||||
segment: 2, flightNumber: 'DL1780', departureAirport: 'DEN', arrivalAirport: destination,
|
||||
scheduledDeparture: this.relativeTime(-120), scheduledArrival: this.relativeTime(-30),
|
||||
actualDeparture: this.relativeTime(-118), actualArrival: this.relativeTime(-25),
|
||||
status: 'landed',
|
||||
arrivalTerminal: '2', arrivalGate: 'C4', arrivalBaggage: '3',
|
||||
}));
|
||||
}
|
||||
|
||||
// James O'Brien: 2-segment, both scheduled (future)
|
||||
// DFW -> DEN -> SLC
|
||||
const obrien = vipByName.get("James O'Brien");
|
||||
if (obrien) {
|
||||
flights.push(makeFlight(obrien.id, {
|
||||
segment: 1, flightNumber: 'UA780', departureAirport: 'DFW', arrivalAirport: 'DEN',
|
||||
scheduledDeparture: this.relativeTime(60), scheduledArrival: this.relativeTime(180),
|
||||
status: 'scheduled',
|
||||
}));
|
||||
flights.push(makeFlight(obrien.id, {
|
||||
segment: 2, flightNumber: 'UA1340', departureAirport: 'DEN', arrivalAirport: destination,
|
||||
scheduledDeparture: this.relativeTime(240), scheduledArrival: this.relativeTime(330),
|
||||
status: 'scheduled',
|
||||
}));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DIRECT FLIGHTS (single segment)
|
||||
// ============================================================
|
||||
|
||||
const directFlights: Array<{ name: string; airline: string; num: string; origin: string; offset: number; statusOverride?: string }> = [
|
||||
{ name: 'Jennifer Wu', airline: 'AA', num: 'AA1023', origin: 'ORD', offset: 60 },
|
||||
{ name: 'Priya Sharma', airline: 'UA', num: 'UA567', origin: 'SFO', offset: -15, statusOverride: 'active' },
|
||||
{ name: 'David Okonkwo', airline: 'DL', num: 'DL1345', origin: 'SEA', offset: 120 },
|
||||
{ name: 'Yuki Tanaka', airline: 'AA', num: 'AA890', origin: 'LAX', offset: 90 },
|
||||
{ name: 'Isabella Costa', airline: 'SW', num: 'SW2210', origin: 'MIA', offset: -45, statusOverride: 'active' },
|
||||
{ name: 'Fatima Al-Rahman', airline: 'AS', num: 'AS440', origin: 'SEA', offset: 180 },
|
||||
{ name: 'William Zhang', airline: 'DL', num: 'DL1678', origin: 'ATL', offset: -90, statusOverride: 'landed' },
|
||||
{ name: 'Alexander Volkov', airline: 'UA', num: 'UA2100', origin: 'DEN', offset: 45 },
|
||||
];
|
||||
|
||||
for (const df of directFlights) {
|
||||
const vip = vipByName.get(df.name);
|
||||
if (!vip) continue;
|
||||
|
||||
const scheduledArrival = this.relativeTime(df.offset);
|
||||
const scheduledDeparture = new Date(scheduledArrival.getTime() - 3 * 60 * 60 * 1000);
|
||||
|
||||
let status = 'scheduled';
|
||||
let status = df.statusOverride || 'scheduled';
|
||||
let actualArrival = null;
|
||||
if (arrivalOffset < -30) {
|
||||
status = 'landed';
|
||||
actualArrival = new Date(scheduledArrival.getTime() + (Math.random() * 20 - 10) * 60000);
|
||||
} else if (arrivalOffset < 0) {
|
||||
status = 'landing';
|
||||
} else if (index % 5 === 0) {
|
||||
status = 'delayed';
|
||||
if (status === 'landed') {
|
||||
actualArrival = new Date(scheduledArrival.getTime() + 5 * 60000);
|
||||
}
|
||||
|
||||
flights.push({
|
||||
vipId: vip.id,
|
||||
flightNumber: flightNum,
|
||||
flightDate: new Date(),
|
||||
flights.push(makeFlight(vip.id, {
|
||||
segment: 1,
|
||||
departureAirport: origin,
|
||||
flightNumber: df.num,
|
||||
departureAirport: df.origin,
|
||||
arrivalAirport: destination,
|
||||
scheduledDeparture,
|
||||
scheduledArrival,
|
||||
actualDeparture: status !== 'scheduled' ? scheduledDeparture : null,
|
||||
actualArrival,
|
||||
status,
|
||||
});
|
||||
|
||||
// Some VIPs have connecting flights (segment 2)
|
||||
if (index % 4 === 0) {
|
||||
const connectOrigin = origins[(index + 3) % origins.length];
|
||||
flights.push({
|
||||
vipId: vip.id,
|
||||
flightNumber: `${airline}${500 + index}`,
|
||||
flightDate: new Date(),
|
||||
segment: 2,
|
||||
departureAirport: connectOrigin,
|
||||
arrivalAirport: origin,
|
||||
scheduledDeparture: new Date(scheduledDeparture.getTime() - 4 * 60 * 60 * 1000),
|
||||
scheduledArrival: new Date(scheduledDeparture.getTime() - 1 * 60 * 60 * 1000),
|
||||
status: 'landed',
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
return flights;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user