feat: add GPS tracking with Traccar integration

- Add GPS module with Traccar client service for device management
- Add driver enrollment flow with QR code generation
- Add real-time location tracking on driver profiles
- Add GPS settings configuration in admin tools
- Add Auth0 OpenID Connect setup script for Traccar
- Add deployment configs for production server
- Update nginx configs for SSL on GPS port 5055
- Add timezone setting support
- Various UI improvements and bug fixes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 18:13:17 +01:00
parent 3814d175ff
commit 5ded039793
91 changed files with 4403 additions and 68 deletions

View File

@@ -80,6 +80,7 @@ export function Layout({ children }: LayoutProps) {
// Admin dropdown items (nested under Admin)
const adminItems = [
{ name: 'Users', href: '/users', icon: UserCog },
{ name: 'GPS Tracking', href: '/gps-tracking', icon: Radio },
{ name: 'Admin Tools', href: '/admin-tools', icon: Settings },
];
@@ -89,8 +90,6 @@ export function Layout({ children }: LayoutProps) {
if (item.driverOnly) return isDriverRole;
// Coordinator-only items hidden from drivers
if (item.coordinatorOnly && isDriverRole) return false;
// Always show items
if (item.alwaysShow) return true;
// Permission-based items
if (item.requireRead) {
return ability.can(Action.Read, item.requireRead);

View File

@@ -3,25 +3,34 @@ import { Loader2 } from 'lucide-react';
interface LoadingProps {
message?: string;
fullPage?: boolean;
size?: 'small' | 'medium' | 'large';
}
export function Loading({ message = 'Loading...', fullPage = false }: LoadingProps) {
export function Loading({ message = 'Loading...', fullPage = false, size = 'medium' }: LoadingProps) {
const sizeClasses = {
small: { icon: 'h-5 w-5', text: 'text-sm', padding: 'py-4' },
medium: { icon: 'h-8 w-8', text: 'text-base', padding: 'py-12' },
large: { icon: 'h-12 w-12', text: 'text-lg', padding: 'py-16' },
};
const { icon, text, padding } = sizeClasses[size];
if (fullPage) {
return (
<div className="min-h-screen flex items-center justify-center bg-muted">
<div className="text-center">
<Loader2 className="h-12 w-12 text-primary animate-spin mx-auto mb-4" />
<p className="text-muted-foreground text-lg">{message}</p>
<Loader2 className={`${icon} text-primary animate-spin mx-auto mb-4`} />
<p className={`text-muted-foreground ${text}`}>{message}</p>
</div>
</div>
);
}
return (
<div className="flex items-center justify-center py-12">
<div className={`flex items-center justify-center ${padding}`}>
<div className="text-center">
<Loader2 className="h-8 w-8 text-primary animate-spin mx-auto mb-3" />
<p className="text-muted-foreground">{message}</p>
<Loader2 className={`${icon} text-primary animate-spin mx-auto mb-3`} />
<p className={`text-muted-foreground ${text}`}>{message}</p>
</div>
</div>
);

View File

@@ -13,6 +13,7 @@ import {
Eye,
ChevronDown,
ChevronUp,
Globe,
} from 'lucide-react';
import {
usePdfSettings,
@@ -38,7 +39,7 @@ export function PdfSettingsSection() {
});
const fileInputRef = useRef<HTMLInputElement>(null);
const { register, handleSubmit, watch, reset } = useForm<UpdatePdfSettingsDto>();
const { register, handleSubmit, watch, reset, setValue } = useForm<UpdatePdfSettingsDto>();
const accentColor = watch('accentColor');
@@ -60,6 +61,7 @@ export function PdfSettingsSection() {
showTimestamp: settings.showTimestamp,
showAppUrl: settings.showAppUrl,
pageSize: settings.pageSize,
timezone: settings.timezone,
showFlightInfo: settings.showFlightInfo,
showDriverNames: settings.showDriverNames,
showVehicleNames: settings.showVehicleNames,
@@ -350,7 +352,8 @@ export function PdfSettingsSection() {
<div className="flex items-center gap-3">
<input
type="color"
{...register('accentColor')}
value={accentColor || '#2c3e50'}
onChange={(e) => setValue('accentColor', e.target.value)}
className="h-10 w-20 border border-input rounded cursor-pointer"
/>
<input
@@ -554,6 +557,39 @@ export function PdfSettingsSection() {
<option value={PageSize.A4}>A4 (210mm x 297mm)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-2">
<Globe className="h-4 w-4 inline mr-1" />
System Timezone
</label>
<select
{...register('timezone')}
className="w-full px-3 py-2 bg-background text-foreground border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
>
<optgroup label="US Timezones">
<option value="America/New_York">Eastern Time (ET)</option>
<option value="America/Chicago">Central Time (CT)</option>
<option value="America/Denver">Mountain Time (MT)</option>
<option value="America/Phoenix">Arizona (no DST)</option>
<option value="America/Los_Angeles">Pacific Time (PT)</option>
<option value="America/Anchorage">Alaska Time (AKT)</option>
<option value="Pacific/Honolulu">Hawaii Time (HT)</option>
</optgroup>
<optgroup label="International">
<option value="UTC">UTC (Coordinated Universal Time)</option>
<option value="Europe/London">London (GMT/BST)</option>
<option value="Europe/Paris">Paris (CET/CEST)</option>
<option value="Europe/Berlin">Berlin (CET/CEST)</option>
<option value="Asia/Tokyo">Tokyo (JST)</option>
<option value="Asia/Shanghai">Shanghai (CST)</option>
<option value="Australia/Sydney">Sydney (AEST/AEDT)</option>
</optgroup>
</select>
<p className="text-xs text-muted-foreground mt-1">
All times in correspondence and exports will use this timezone
</p>
</div>
</div>
)}
</div>