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:
@@ -39,6 +39,7 @@ import {
|
||||
useTraccarSetupStatus,
|
||||
useTraccarSetup,
|
||||
useOpenTraccarAdmin,
|
||||
useDeviceQr,
|
||||
} from '@/hooks/useGps';
|
||||
import { Loading } from '@/components/Loading';
|
||||
import { ErrorMessage } from '@/components/ErrorMessage';
|
||||
@@ -113,6 +114,7 @@ export function GpsTracking() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedDriverId, setSelectedDriverId] = useState<string>('');
|
||||
const [enrollmentResult, setEnrollmentResult] = useState<any>(null);
|
||||
const [showQrDriverId, setShowQrDriverId] = useState<string | null>(null);
|
||||
|
||||
// Check admin access
|
||||
if (backendUser?.role !== 'ADMINISTRATOR') {
|
||||
@@ -134,6 +136,7 @@ export function GpsTracking() {
|
||||
const { data: devices, isLoading: devicesLoading } = useGpsDevices();
|
||||
const { data: traccarStatus } = useTraccarSetupStatus();
|
||||
const { data: driverStats } = useDriverStats(selectedDriverId);
|
||||
const { data: qrInfo, isLoading: qrLoading } = useDeviceQr(showQrDriverId);
|
||||
|
||||
// Mutations
|
||||
const updateSettings = useUpdateGpsSettings();
|
||||
@@ -491,7 +494,15 @@ export function GpsTracking() {
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground">
|
||||
{device.lastActive ? formatDistanceToNow(new Date(device.lastActive), { addSuffix: true }) : 'Never'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<td className="px-4 py-3 flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowQrDriverId(device.driverId)}
|
||||
className="inline-flex items-center px-3 py-1 text-primary hover:text-primary/80 transition-colors"
|
||||
title="Show QR code"
|
||||
>
|
||||
<QrCode className="h-4 w-4 mr-1" />
|
||||
QR
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(`Unenroll ${device.driver?.name} from GPS tracking?`)) {
|
||||
@@ -603,18 +614,21 @@ export function GpsTracking() {
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={30}
|
||||
min={10}
|
||||
max={300}
|
||||
defaultValue={settings.updateIntervalSeconds}
|
||||
onBlur={(e) => {
|
||||
const value = parseInt(e.target.value);
|
||||
if (value >= 30 && value <= 300 && value !== settings.updateIntervalSeconds) {
|
||||
if (value >= 10 && value <= 300 && value !== settings.updateIntervalSeconds) {
|
||||
updateSettings.mutate({ updateIntervalSeconds: value });
|
||||
}
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground focus:ring-primary focus:border-primary"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">How often drivers report their location (30-300 seconds)</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">How often drivers report their location (10-300 seconds)</p>
|
||||
<div className="mt-2 p-2 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 rounded text-xs text-amber-800 dark:text-amber-200">
|
||||
<strong>Tip:</strong> 15-30s recommended for active events (smooth routes), 60s for routine use (saves battery). Changing this only affects new QR code enrollments.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted/30 rounded-lg p-4">
|
||||
@@ -871,6 +885,129 @@ export function GpsTracking() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Device QR Code Modal */}
|
||||
{showQrDriverId && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-card border border-border rounded-lg shadow-lg w-full max-w-md">
|
||||
<div className="flex justify-between items-center p-4 border-b border-border">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{qrInfo ? `${qrInfo.driverName} - Setup QR` : 'Device QR Code'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowQrDriverId(null)}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
{qrLoading ? (
|
||||
<Loading message="Loading QR code..." />
|
||||
) : qrInfo ? (
|
||||
<>
|
||||
{/* QR Code */}
|
||||
<div className="bg-white dark:bg-gray-900 p-4 rounded-lg border border-border text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-3">
|
||||
<QrCode className="h-5 w-5 text-primary" />
|
||||
<span className="text-sm font-medium">Scan with Traccar Client</span>
|
||||
</div>
|
||||
<div className="flex justify-center mb-3">
|
||||
<QRCodeSVG
|
||||
value={qrInfo.qrCodeUrl}
|
||||
size={200}
|
||||
level="M"
|
||||
includeMargin
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Open Traccar Client app {'→'} tap the QR icon {'→'} scan this code
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Download links */}
|
||||
<div className="bg-blue-50 dark:bg-blue-950/30 p-3 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Smartphone className="h-4 w-4 text-blue-600" />
|
||||
<span className="text-sm font-medium text-blue-800 dark:text-blue-200">Download Traccar Client</span>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<a
|
||||
href="https://apps.apple.com/app/traccar-client/id843156974"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
|
||||
>
|
||||
iOS App Store
|
||||
</a>
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=org.traccar.client"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-3 py-1 bg-green-600 text-white text-xs rounded hover:bg-green-700"
|
||||
>
|
||||
Google Play
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Manual fallback */}
|
||||
<details className="bg-muted/30 rounded-lg border border-border">
|
||||
<summary className="p-3 text-sm font-medium cursor-pointer hover:bg-muted/50 transition-colors">
|
||||
Manual Setup (if QR doesn't work)
|
||||
</summary>
|
||||
<div className="px-3 pb-3 space-y-2">
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">Device ID</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 bg-muted p-2 rounded text-sm font-mono">
|
||||
{qrInfo.deviceIdentifier}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(qrInfo.deviceIdentifier)}
|
||||
className="p-2 hover:bg-muted rounded transition-colors"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground">Server URL</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 bg-muted p-2 rounded text-sm font-mono text-xs">
|
||||
{qrInfo.serverUrl}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(qrInfo.serverUrl)}
|
||||
className="p-2 hover:bg-muted rounded transition-colors"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ol className="text-xs space-y-1 list-decimal list-inside text-muted-foreground mt-2">
|
||||
<li>Open Traccar Client and enter Device ID and Server URL above</li>
|
||||
<li>Set frequency to {qrInfo.updateIntervalSeconds} seconds</li>
|
||||
<li>Tap "Service Status" to start tracking</li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
</>
|
||||
) : (
|
||||
<ErrorMessage message="Failed to load QR code info" />
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setShowQrDriverId(null)}
|
||||
className="w-full px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user