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:
2026-02-08 07:36:51 +01:00
parent 0f0f1cbf38
commit a4d360aae9
33 changed files with 2136 additions and 361 deletions

View File

@@ -5,16 +5,17 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatDate(date: string | Date): string {
export function formatDate(date: string | Date, timeZone?: string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
...(timeZone && { timeZone }),
});
}
export function formatDateTime(date: string | Date): string {
export function formatDateTime(date: string | Date, timeZone?: string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleString('en-US', {
year: 'numeric',
@@ -22,13 +23,15 @@ export function formatDateTime(date: string | Date): string {
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
...(timeZone && { timeZone }),
});
}
export function formatTime(date: string | Date): string {
export function formatTime(date: string | Date, timeZone?: string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
...(timeZone && { timeZone }),
});
}