diff --git a/Dockerfile b/Dockerfile index 91e52cb7..974635eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ COPY app/client/package*.json ./ RUN npm install COPY app/client/ ./ ENV PUBLIC_API_BASE_URL=/ +ENV PUBLIC_DEMO_MODE=false ENV NODE_ENV=production RUN npm run build diff --git a/app/client/package.json b/app/client/package.json index 54785b13..5fb3e570 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -34,6 +34,7 @@ "chart.js": "^4.5.0", "dayjs": "^1.11.13", "dot-env": "^0.0.1", + "svelte-loading-spinners": "^0.3.6", "svelte5-chartjs": "^1.0.0" } } diff --git a/app/client/src/components/auth/PinInput.svelte b/app/client/src/components/auth/PinInput.svelte index 6fdd6e93..c93a4d11 100644 --- a/app/client/src/components/auth/PinInput.svelte +++ b/app/client/src/components/auth/PinInput.svelte @@ -1,10 +1,8 @@
- export let id = ''; - export let type: string = 'text'; - export let placeholder: string = ''; - export let value: any; - export let icon: any = null; - export let required: boolean = false; - export let ariaLabel: string = ''; - export let disabled: boolean = false; - export let inputClass: string = ''; - export let onInput: ((e: Event) => void) | undefined = undefined; + let { + id, + type = 'text', + placeholder = '', + value = $bindable(), + icon = null, + required = false, + ariaLabel = '', + disabled = false, + inputClass = '', + onInput = undefined + } = $props(); + const Icon = icon;
@@ -22,14 +25,21 @@ {required} aria-label={ariaLabel} {disabled} - on:input={onInput} + oninput={onInput} + autocomplete="off" + step=".01" /> {#if icon} -
+ + diff --git a/app/client/src/components/common/FormSubmitButton.svelte b/app/client/src/components/common/FormSubmitButton.svelte new file mode 100644 index 00000000..64d7607e --- /dev/null +++ b/app/client/src/components/common/FormSubmitButton.svelte @@ -0,0 +1,20 @@ + + +
+ {#if !loading} + + {:else} + + {/if} +
diff --git a/app/client/src/components/common/ModalContainer.svelte b/app/client/src/components/common/ModalContainer.svelte new file mode 100644 index 00000000..4cb6e68c --- /dev/null +++ b/app/client/src/components/common/ModalContainer.svelte @@ -0,0 +1,39 @@ + + +
+
+ +

+ {title} +

+
+ {@render children()} +
+
diff --git a/app/client/src/components/common/TabContainer.svelte b/app/client/src/components/common/TabContainer.svelte new file mode 100644 index 00000000..3aa4fe33 --- /dev/null +++ b/app/client/src/components/common/TabContainer.svelte @@ -0,0 +1,14 @@ + + +
+

+ {title} +

+ {@render children()} +
diff --git a/app/client/src/components/common/ThemeToggle.svelte b/app/client/src/components/common/ThemeToggle.svelte index 248227c0..0a77f26c 100644 --- a/app/client/src/components/common/ThemeToggle.svelte +++ b/app/client/src/components/common/ThemeToggle.svelte @@ -1,5 +1,6 @@ -

- Log Fuel Refill -

-
- - - -{/if} diff --git a/app/client/src/components/fuel/FuelRefillFormComponent.svelte b/app/client/src/components/fuel/FuelRefillFormComponent.svelte deleted file mode 100644 index 2b45640c..00000000 --- a/app/client/src/components/fuel/FuelRefillFormComponent.svelte +++ /dev/null @@ -1,84 +0,0 @@ - - -
- - - - - - {#if error} - - {/if} -
- -
- diff --git a/app/client/src/components/fuel/FuelRefillList.svelte b/app/client/src/components/fuel/FuelRefillList.svelte deleted file mode 100644 index aceaf1d8..00000000 --- a/app/client/src/components/fuel/FuelRefillList.svelte +++ /dev/null @@ -1,107 +0,0 @@ - - -
-

Fuel Refill History

- - {#if loading} -

Loading fuel logs...

- {:else if error} -

Error: {error}

- {:else if fuelLogs.length === 0} -

No fuel refill logs found for this vehicle.

- {:else} -
- - - - - - - - - - - - - {#each fuelLogs as log (log.id)} - - - - - - - - - {/each} - -
DateOdometerFuel Amount (L)CostMileage (km/L)Notes
{formatDate(log.date)}{log.odometer} km{log.fuelAmount} L{formatCurrency(log.cost)}{log.mileage ? log.mileage.toFixed(2) : 'N/A'}{log.notes || 'N/A'}
-
- {/if} -
diff --git a/app/client/src/components/insurance/InsuranceDetails.svelte b/app/client/src/components/insurance/InsuranceDetails.svelte deleted file mode 100644 index 5891e44f..00000000 --- a/app/client/src/components/insurance/InsuranceDetails.svelte +++ /dev/null @@ -1,182 +0,0 @@ - - -
-

Insurance Details

- {#if loading} -
Loading insurance details...
- {:else if error} - - {:else if insurance} -
-
-
- - {insurance.provider} -
-
- - -
-
-
-
- - Policy Number: - {insurance.policyNumber} -
-
- - Cost: - {formatCurrency(insurance.cost)} -
-
- - Start Date: - {formatDate(insurance.startDate)} -
-
- - End Date: - {formatDate(insurance.endDate)} -
-
-
- {:else} -
-

No insurance details found for this vehicle.

- -
- {/if} - - {#if showInsuranceFormModal} - - {/if} -
diff --git a/app/client/src/components/insurance/InsuranceDetailsList.svelte b/app/client/src/components/insurance/InsuranceDetailsList.svelte new file mode 100644 index 00000000..2fbbb1bc --- /dev/null +++ b/app/client/src/components/insurance/InsuranceDetailsList.svelte @@ -0,0 +1,159 @@ + + +{#if loading} +
Loading insurance details...
+{:else if error} + +{:else if insurances.length === 0} +
No Insurance found for this vehicle.
+{:else} + {#each insurances as ins (ins.id)} +
+
+
+ + {ins.provider} +
+
+ + +
+
+
+
+ + Policy Number: + {ins.policyNumber} +
+
+ + Cost: + {formatCurrency(ins.cost)} +
+
+ + Start Date: + {formatDate(ins.startDate)} +
+
+ + End Date: + {formatDate(ins.endDate)} +
+
+
+ {/each} +{/if} diff --git a/app/client/src/components/insurance/InsuranceForm.svelte b/app/client/src/components/insurance/InsuranceForm.svelte index c271f821..0d073d9d 100644 --- a/app/client/src/components/insurance/InsuranceForm.svelte +++ b/app/client/src/components/insurance/InsuranceForm.svelte @@ -1,51 +1,42 @@ -{#if showModal} -
-
-

- {initialData ? 'Edit' : 'Add'} Insurance Details -

+
{ + persistInsurance(); + e.preventDefault(); + }} +> + + + + + + + + - -
-
+{#if status.message} +

+ {#if status.type === 'ERROR'} + Error: {status.message} + {:else} + {status.message} + {/if} +

{/if} diff --git a/app/client/src/components/insurance/InsuranceFormComponent.svelte b/app/client/src/components/insurance/InsuranceFormComponent.svelte index 41b1509e..e581b9bc 100644 --- a/app/client/src/components/insurance/InsuranceFormComponent.svelte +++ b/app/client/src/components/insurance/InsuranceFormComponent.svelte @@ -1,15 +1,15 @@
diff --git a/app/client/src/components/insurance/InsuranceModal.svelte b/app/client/src/components/insurance/InsuranceModal.svelte new file mode 100644 index 00000000..35e4cf17 --- /dev/null +++ b/app/client/src/components/insurance/InsuranceModal.svelte @@ -0,0 +1,43 @@ + + +{#if showModal} + + + +{/if} diff --git a/app/client/src/components/maintenance/MaintenanceLogForm.svelte b/app/client/src/components/maintenance/MaintenanceLogForm.svelte index 1155a5db..b715da76 100644 --- a/app/client/src/components/maintenance/MaintenanceLogForm.svelte +++ b/app/client/src/components/maintenance/MaintenanceLogForm.svelte @@ -1,94 +1,152 @@ -{#if showModal} -
-
-

- {initialData ? 'Edit Maintenance Log' : 'Add Maintenance Log'} -

- - -
-
+ { + persistLog(); + e.preventDefault(); + }} + class="space-y-6" +> + + + + + + + +{#if status.message} +

+ {#if status.type === 'ERROR'} + Error: {status.message} + {:else} + {status.message} + {/if} +

{/if} diff --git a/app/client/src/components/maintenance/MaintenanceLogFormComponent.svelte b/app/client/src/components/maintenance/MaintenanceLogFormComponent.svelte deleted file mode 100644 index 4647e3b8..00000000 --- a/app/client/src/components/maintenance/MaintenanceLogFormComponent.svelte +++ /dev/null @@ -1,98 +0,0 @@ - - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - {#if error} - - {/if} - - {#if success} - - {/if} - -
- - -
-
diff --git a/app/client/src/components/maintenance/MaintenanceLogList.svelte b/app/client/src/components/maintenance/MaintenanceLogList.svelte index bba3b149..fd3f0411 100644 --- a/app/client/src/components/maintenance/MaintenanceLogList.svelte +++ b/app/client/src/components/maintenance/MaintenanceLogList.svelte @@ -2,17 +2,14 @@ import { onMount } from 'svelte'; import { browser } from '$app/environment'; import { env } from '$env/dynamic/public'; - import MaintenanceLogForm from './MaintenanceLogForm.svelte'; - import { config } from '../../lib/states/config'; - import dayjs from 'dayjs'; + import { formatCurrency, formatDate } from '$lib/utils/formatting'; + import { Pencil, Trash2 } from '@lucide/svelte'; + import { maintenanceModelStore } from '$lib/stores/maintenance'; - $: formatDate = (date: string) => dayjs(date).format($config.dateFormat); - $: formatCurrency = (amount: number) => `${$config.currency} ${amount.toLocaleString()}` + let { vehicleId } = $props(); - export let vehicleId: number | null = null; - - interface MaintenanceLogEntry { - id: number; + interface MaintenanceLog { + id: string; date: string; odometer: number; service: string; @@ -20,11 +17,18 @@ notes?: string; } - let maintenanceLogs: MaintenanceLogEntry[] = []; - let loading = false; - let error = ''; - let showEditModal = false; - let selectedLog: MaintenanceLogEntry | null = null; + let maintenanceLogs: MaintenanceLog[] = $state([]); + let loading = $state(false); + let error = $state(''); + + $effect(() => { + if (!vehicleId) { + error = 'Vehicle ID is required.'; + loading = false; + } else { + fetchMaintenanceLogs(); + } + }); async function fetchMaintenanceLogs() { if (!vehicleId) { @@ -35,7 +39,7 @@ error = ''; try { const response = await fetch( - `${env.PUBLIC_API_BASE_URL||""}/api/vehicles/${vehicleId}/maintenance-logs`, + `${env.PUBLIC_API_BASE_URL || ''}/api/vehicles/${vehicleId}/maintenance-logs`, { headers: { 'X-User-PIN': browser ? localStorage.getItem('userPin') || '' : '' @@ -55,19 +59,22 @@ } } - async function handleDelete(logId: number) { + async function deleteMaintenenceLog(logId: string) { if (!confirm('Are you sure you want to delete this maintenance log?')) { return; } try { - const response = await fetch(`${env.PUBLIC_API_BASE_URL}/api/maintenance-logs/${logId}`, { - method: 'DELETE', - headers: { - 'X-User-PIN': browser ? localStorage.getItem('userPin') || '' : '' + const response = await fetch( + `${env.PUBLIC_API_BASE_URL || ''}/api/vehicles/${vehicleId}/maintenance-logs/${logId}`, + { + method: 'DELETE', + headers: { + 'X-User-PIN': browser ? localStorage.getItem('userPin') || '' : '' + } } - }); + ); if (response.ok) { - await fetchMaintenanceLogs(); // Refresh the list + await fetchMaintenanceLogs(); } else { const data = await response.json(); alert(data.message || 'Failed to delete maintenance log.'); @@ -77,102 +84,68 @@ } } - function handleEdit(log: MaintenanceLogEntry) { - selectedLog = log; - showEditModal = true; - } - - function closeEditModal() { - showEditModal = false; - selectedLog = null; - } - - function handleSuccess() { - fetchMaintenanceLogs(); // Refresh logs after add/edit - closeEditModal(); - } - - // Refetch when vehicleId changes - $: if (vehicleId) { - fetchMaintenanceLogs(); - } - onMount(() => { - if (vehicleId) fetchMaintenanceLogs(); + fetchMaintenanceLogs(); }); -
-

Maintenance History

- {#if loading} -
Loading maintenance logs...
- {:else if error} - - {:else if maintenanceLogs.length === 0} -
No maintenance logs for this vehicle.
- {:else} -
- - - - - - - - - - - - - {#each maintenanceLogs as log (log.id)} - - Loading maintenance logs... +{:else if error} + +{:else if maintenanceLogs.length === 0} +
No maintenance logs for this vehicle.
+{:else} +
+
DateOdometerServiceCostNotesActions
{formatDate(log.date)}
+ + + + + + + + + + + + {#each maintenanceLogs as log (log.id)} + + + + + + + - - - - - - {/each} - -
DateOdometerServiceCostNotesActions
{formatDate(log.date)}{log.odometer}{log.service}{formatCurrency(log.cost)}{log.notes || '-'} + {log.odometer}{log.service}{formatCurrency(log.cost)}{log.notes || '-'} - - -
-
- {/if} - - {#if showEditModal} - - {/if} -
+ + + + + + {/each} + + + +{/if} diff --git a/app/client/src/components/maintenance/MaintenanceLogModal.svelte b/app/client/src/components/maintenance/MaintenanceLogModal.svelte new file mode 100644 index 00000000..b9ac5514 --- /dev/null +++ b/app/client/src/components/maintenance/MaintenanceLogModal.svelte @@ -0,0 +1,42 @@ + + +{#if showModal} + + + +{/if} diff --git a/app/client/src/components/pucc/PollutionCertificateDetails.svelte b/app/client/src/components/pucc/PollutionCertificateDetails.svelte deleted file mode 100644 index 3ec008d9..00000000 --- a/app/client/src/components/pucc/PollutionCertificateDetails.svelte +++ /dev/null @@ -1,187 +0,0 @@ - - -
-

- Pollution Certificate Details -

- {#if loading} -
Loading pollution certificate details...
- {:else if error} - - {:else if pollutionCertificate} -
-
-
- - {pollutionCertificate.certificateNumber} -
-
- - -
-
-
-
- - Issue Date: - {formatDate(pollutionCertificate.issueDate)} -
-
- - Expiry Date: - {formatDate(pollutionCertificate.expiryDate)} -
-
- - Testing Center: - {pollutionCertificate.testingCenter} -
- {#if pollutionCertificate.notes} -
- - Notes: - {pollutionCertificate.notes} -
- {/if} -
-
- {:else} -
-

- No pollution certificate details found for this vehicle. -

- -
- {/if} - - {#if showPollutionCertificateFormModal} - - {/if} -
diff --git a/app/client/src/components/pucc/PollutionCertificateForm.svelte b/app/client/src/components/pucc/PollutionCertificateForm.svelte index 7ea76a67..90a4779e 100644 --- a/app/client/src/components/pucc/PollutionCertificateForm.svelte +++ b/app/client/src/components/pucc/PollutionCertificateForm.svelte @@ -1,68 +1,57 @@ -{#if showModal} -
-
-

- {initialData ? 'Edit' : 'Add'} Pollution Certificate Details -

+
{ + persistCertificate(); + e.preventDefault(); + }} + class="space-y-6" +> + + + + + + + - -
-
+{#if status.message} +

+ {#if status.type === 'ERROR'} + Error: {status.message} + {:else} + {status.message} + {/if} +

{/if} diff --git a/app/client/src/components/pucc/PollutionCertificateFormComponent.svelte b/app/client/src/components/pucc/PollutionCertificateFormComponent.svelte deleted file mode 100644 index 4051168d..00000000 --- a/app/client/src/components/pucc/PollutionCertificateFormComponent.svelte +++ /dev/null @@ -1,95 +0,0 @@ - - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - {#if error} - - {/if} - - {#if success} - - {/if} - -
- - -
-
diff --git a/app/client/src/components/pucc/PollutionCertificateList.svelte b/app/client/src/components/pucc/PollutionCertificateList.svelte new file mode 100644 index 00000000..4686faf9 --- /dev/null +++ b/app/client/src/components/pucc/PollutionCertificateList.svelte @@ -0,0 +1,162 @@ + + +{#if loading} +
Loading pollution certificate details...
+{:else if error} + +{:else if pollutionCertificates.length === 0} +
No maintenance logs for this vehicle.
+{:else} + {#each pollutionCertificates as pucc (pucc.id)} +
+
+
+ + {pucc.certificateNumber} +
+
+ + +
+
+
+
+ + Issue Date: + {formatDate(pucc.issueDate)} +
+
+ + Expiry Date: + {formatDate(pucc.expiryDate)} +
+
+ + Testing Center: + {pucc.testingCenter} +
+ {#if pucc.notes} +
+ + Notes: + {pucc.notes} +
+ {/if} +
+
+ {/each} +{/if} diff --git a/app/client/src/components/pucc/PollutionCertificateModal.svelte b/app/client/src/components/pucc/PollutionCertificateModal.svelte new file mode 100644 index 00000000..86801d30 --- /dev/null +++ b/app/client/src/components/pucc/PollutionCertificateModal.svelte @@ -0,0 +1,42 @@ + + +{#if showModal} + + + +{/if} diff --git a/app/client/src/components/tabs/DashboardTab.svelte b/app/client/src/components/tabs/DashboardTab.svelte new file mode 100644 index 00000000..1c824a26 --- /dev/null +++ b/app/client/src/components/tabs/DashboardTab.svelte @@ -0,0 +1,135 @@ + + + +
+ {#if fuelCostData?.datasets?.length > 0 && mileageData?.datasets?.length > 0} + + + {:else} +
+

+ No fuel or mileage data available for this vehicle. +

+
+ {/if} +
+
diff --git a/app/client/src/components/tabs/FuelLogTab.svelte b/app/client/src/components/tabs/FuelLogTab.svelte new file mode 100644 index 00000000..7973ddb0 --- /dev/null +++ b/app/client/src/components/tabs/FuelLogTab.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/app/client/src/components/tabs/InsuranceTab.svelte b/app/client/src/components/tabs/InsuranceTab.svelte new file mode 100644 index 00000000..a5d697a1 --- /dev/null +++ b/app/client/src/components/tabs/InsuranceTab.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/app/client/src/components/tabs/MaintenenceLogTab.svelte b/app/client/src/components/tabs/MaintenenceLogTab.svelte new file mode 100644 index 00000000..138ef875 --- /dev/null +++ b/app/client/src/components/tabs/MaintenenceLogTab.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/app/client/src/components/tabs/PollutionTab.svelte b/app/client/src/components/tabs/PollutionTab.svelte new file mode 100644 index 00000000..cff2a952 --- /dev/null +++ b/app/client/src/components/tabs/PollutionTab.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/app/client/src/components/tabs/TabHeader.svelte b/app/client/src/components/tabs/TabHeader.svelte new file mode 100644 index 00000000..dceb70b8 --- /dev/null +++ b/app/client/src/components/tabs/TabHeader.svelte @@ -0,0 +1,53 @@ + + +
    + {#each tabs as tab (tab)} + + {/each} +
diff --git a/app/client/src/components/vehicle/AddVehicleForm.svelte b/app/client/src/components/vehicle/AddVehicleForm.svelte deleted file mode 100644 index 3e914b4d..00000000 --- a/app/client/src/components/vehicle/AddVehicleForm.svelte +++ /dev/null @@ -1,71 +0,0 @@ - - -{#if showModal} -
-
- -

- {editMode ? 'Edit Vehicle Details' : 'Add a New Vehicle'} -

-
- -
-
-{/if} - - diff --git a/app/client/src/components/vehicle/VehicleCard.svelte b/app/client/src/components/vehicle/VehicleCard.svelte index cd284b92..8a4b025a 100644 --- a/app/client/src/components/vehicle/VehicleCard.svelte +++ b/app/client/src/components/vehicle/VehicleCard.svelte @@ -12,22 +12,38 @@ Shield, BadgeCheck } from '@lucide/svelte'; - import { createEventDispatcher } from 'svelte'; + import { formatDistance } from '$lib/utils/formatting'; + import { vehicleModelStore, vehiclesStore } from '$lib/stores/vehicle'; + import { maintenanceModelStore } from '$lib/stores/maintenance'; + import { fuelLogModelStore } from '$lib/stores/fuel-log'; + import { insuranceModelStore } from '$lib/stores/insurance'; + import { puccModelStore } from '$lib/stores/pucc'; - export let vehicle: { - id: number; - make: string; - model: string; - year: number; - licensePlate: string; - vin?: string; - color?: string; - odometer?: number; - insuranceStatus?: string; - puccStatus?: string; - }; + const { vehicle, updateCallback } = $props(); - const dispatch = createEventDispatcher(); + async function deleteVehicle(vehicleId: string) { + if (!confirm('Are you sure you want to delete this vehicle?')) { + return; + } + try { + const response = await fetch(`http://localhost:3000/api/vehicles/${vehicleId}`, { + method: 'DELETE', + headers: { + 'X-User-PIN': localStorage.getItem('userPin') || '' + } + }); + if (response.ok) { + alert('Vehicle deleted successfully.'); + vehicleModelStore.hide(); + vehiclesStore.fetchVehicles(); + } else { + const data = await response.json(); + alert(data.message || 'Failed to delete vehicle.'); + } + } catch (e) { + alert('Failed to connect to the server.'); + } + }
{vehicle.licensePlate}

- {#if vehicle.vin} -

- VIN: - {vehicle.vin} -

- {/if} - {#if vehicle.color} -

- Color: - {vehicle.color} -

- {/if} - {#if vehicle.odometer} -

- - Odometer: - {vehicle.odometer} km -

- {/if} +

+ VIN: + {vehicle.vin ? vehicle.vin : '-'} +

+ +

+ Color: + {vehicle.color ? vehicle.color : '-'} +

+

+ + Odometer: + {vehicle.odometer ? formatDistance(vehicle.odometer) : '-'} +

{#if vehicle.insuranceStatus}

@@ -94,43 +105,65 @@

{/if}
-
- - - - +
+
+ + + + +
+
+ + +
diff --git a/app/client/src/components/vehicle/VehicleForm.svelte b/app/client/src/components/vehicle/VehicleForm.svelte index fb398bf9..88ffc4a4 100644 --- a/app/client/src/components/vehicle/VehicleForm.svelte +++ b/app/client/src/components/vehicle/VehicleForm.svelte @@ -9,17 +9,93 @@ Building2 } from '@lucide/svelte'; import FormField from '../common/FormField.svelte'; + import type { NewVehicle, Vehicle } from '$lib/models/vehicle'; + import { env } from '$env/dynamic/public'; + import { onMount } from 'svelte'; + import FormSubmitButton from '$components/common/FormSubmitButton.svelte'; + import { simulateNetworkDelay } from '$lib/utils/dev'; + import { vehiclesStore } from '$lib/stores/vehicle'; - export let vehicle; - export let onSubmit; - export let error = ''; - export let success = ''; - export let editMode = false; + let { vehicleToEdit = null, editMode = false, modalVisibility = $bindable(), loading } = $props(); + + const vehicle: NewVehicle = $state({ + make: '', + model: '', + year: null, + licensePlate: '', + vin: '', + color: '', + odometer: null + }); + + let status = $state<{ + message: string | null; + type: 'ERROR' | 'SUCCESS' | null; + }>({ + message: null, + type: null + }); + + $effect(() => { + if (vehicleToEdit) { + Object.assign(vehicle, vehicleToEdit); + } + }); + + async function persistVehicle() { + if (!vehicle.make || !vehicle.model || !vehicle.year || !vehicle.licensePlate) { + status.message = 'Please fill in all required fields.'; + status.type = 'ERROR'; + return; + } + try { + if (loading) return; // Prevent multiple submissions + loading = true; + status.message = null; + status.type = null; + // await simulateNetworkDelay(2000); // Simulate network delay for development + const response = await fetch( + `${env.PUBLIC_API_BASE_URL || ''}/api/vehicles/${editMode ? vehicleToEdit.id : ''}`, + { + method: editMode ? 'PUT' : 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-User-PIN': localStorage.getItem('userPin') || '' + }, + body: JSON.stringify(vehicle) + } + ); + + if (response.ok) { + status.message = `Vehicle ${editMode ? 'updated' : 'added'} successfully!`; + status.type = 'SUCCESS'; + Object.assign(vehicle, { + make: '', + model: '', + year: null, + licensePlate: '', + vin: '', + color: '', + odometer: null + }); + modalVisibility = false; + vehiclesStore.fetchVehicles(); // Refresh the vehicle list after closing the modal + } else { + const data = await response.json(); + status.message = data.message || `Failed to ${editMode ? 'update' : 'add'} vehicle.`; + status.type = 'ERROR'; + } + } catch (e) { + status.message = 'Failed to connect to the server.'; + status.type = 'ERROR'; + } + loading = false; + }
{ - onSubmit(); + onsubmit={(e) => { + persistVehicle(); e.preventDefault(); }} class="space-y-6" @@ -84,20 +160,20 @@ icon={Gauge} ariaLabel="Odometer (Optional)" /> -
- -
+ + + {#if editMode} + + {/if} -{#if error} -

{error}

-{/if} -{#if success} -

- {editMode ? 'Vehicle updated successfully!' : success} +{#if status.message} +

+ {#if status.type === 'ERROR'} + Error: {status.message} + {:else} + {status.message} + {/if}

{/if} diff --git a/app/client/src/components/vehicle/VehicleList.svelte b/app/client/src/components/vehicle/VehicleList.svelte index f67dad39..5bb70051 100644 --- a/app/client/src/components/vehicle/VehicleList.svelte +++ b/app/client/src/components/vehicle/VehicleList.svelte @@ -1,17 +1,11 @@ @@ -30,13 +24,7 @@ class:ring-blue-500={selectedVehicleId === vehicle.id} class="cursor-pointer rounded-2xl transition-all duration-300 ease-in-out" > - dispatch('editVehicle', e.detail)} - on:deleteVehicle={(e) => dispatch('deleteVehicle', e.detail)} - on:refillFuel={(e) => dispatch('refillFuel', e.detail)} - on:addMaintenance={(e) => dispatch('addMaintenance', e.detail)} - /> + {/each} diff --git a/app/client/src/components/vehicle/VehicleModal.svelte b/app/client/src/components/vehicle/VehicleModal.svelte new file mode 100644 index 00000000..d81c5d55 --- /dev/null +++ b/app/client/src/components/vehicle/VehicleModal.svelte @@ -0,0 +1,27 @@ + + +{#if showModal} + + + +{/if} diff --git a/app/client/src/lib/models/fuel-log.ts b/app/client/src/lib/models/fuel-log.ts new file mode 100644 index 00000000..b3f789fd --- /dev/null +++ b/app/client/src/lib/models/fuel-log.ts @@ -0,0 +1,11 @@ +export interface NewFuelLog { + date: string; + odometer: number | null; + fuelAmount: number | null; + cost: number | null; + notes?: string; +} + +export interface FuelLog extends NewFuelLog { + id: string; +} diff --git a/app/client/src/lib/models/vehicle.ts b/app/client/src/lib/models/vehicle.ts new file mode 100644 index 00000000..083237ef --- /dev/null +++ b/app/client/src/lib/models/vehicle.ts @@ -0,0 +1,16 @@ +export interface NewVehicle { + make: string; + model: string; + year: number | null; + licensePlate: string; + vin?: string; + color?: string; + odometer?: number | null; +} + +export interface Vehicle extends NewVehicle { + vehicleType: string; + id: string; + insuranceStatus?: string; + puccStatus?: string; +} diff --git a/app/client/src/lib/states/auth.svelte.ts b/app/client/src/lib/stores/auth.ts similarity index 100% rename from app/client/src/lib/states/auth.svelte.ts rename to app/client/src/lib/stores/auth.ts diff --git a/app/client/src/lib/states/config.ts b/app/client/src/lib/stores/config.ts similarity index 74% rename from app/client/src/lib/states/config.ts rename to app/client/src/lib/stores/config.ts index 0afd655d..39d0bc80 100644 --- a/app/client/src/lib/states/config.ts +++ b/app/client/src/lib/stores/config.ts @@ -8,26 +8,8 @@ export interface Config { description?: string; } -export interface ConfigStore { - dateFormat: string; - currency: string; - theme: string; - language: string; -} - -const defaultConfig: ConfigStore = { - dateFormat: 'DD/MM/YYYY', - currency: 'USD', - theme: 'light', - language: 'en' -}; - -// const defaultConfig: ConfigStore = []; - const createConfigStore = () => { const { subscribe, set } = writable([]); - const configJson = writable(defaultConfig); - async function fetchConfig() { if (browser) { try { @@ -38,13 +20,6 @@ const createConfigStore = () => { }); if (response.ok) { const data: Config[] = await response.json(); - const newConfig: any = {}; - data.forEach((item) => { - if (item.key && item.value !== undefined) { - newConfig[item.key] = item.value; - } - }); - configJson.set(newConfig); set(data); } else { console.error('Failed to fetch config'); @@ -81,7 +56,6 @@ const createConfigStore = () => { return { subscribe, - configJson, save: (localConfig: Config[]) => { console.log('Saving config:', localConfig); updateConfig(localConfig); diff --git a/app/client/src/lib/stores/dark-mode.ts b/app/client/src/lib/stores/dark-mode.ts new file mode 100644 index 00000000..1d89553a --- /dev/null +++ b/app/client/src/lib/stores/dark-mode.ts @@ -0,0 +1,25 @@ +import { writable } from 'svelte/store'; + +const createDarkModeStore = () => { + const { subscribe, set } = writable(false); + + function loadDarkModePreference() { + if (typeof window !== 'undefined') { + const darkMode = localStorage.getItem('darkMode'); + if (darkMode !== null) { + set(darkMode === 'true'); + } else { + set(false); // Default to light mode if no preference is set + } + } + } + + loadDarkModePreference(); + + return { + subscribe, + set + }; +}; + +export const darkModeStore = createDarkModeStore(); diff --git a/app/client/src/lib/stores/fuel-log.ts b/app/client/src/lib/stores/fuel-log.ts new file mode 100644 index 00000000..2059d7b8 --- /dev/null +++ b/app/client/src/lib/stores/fuel-log.ts @@ -0,0 +1,49 @@ +import { writable } from 'svelte/store'; + +const createFuelLogModalStore = () => { + const { subscribe, set } = writable<{ + logToEdit?: any; + vehicleId?: string; + editMode: boolean; + show: boolean; + callback: (status: boolean) => void; + }>({ + logToEdit: undefined, + vehicleId: undefined, + editMode: false, + show: false, + callback: () => {} + }); + + function show( + vehicleId: string, + logToEdit?: any, + editMode: boolean = false, + callback: any = undefined + ) { + set({ + logToEdit, + vehicleId, + editMode, + show: true, + callback + }); + } + function hide() { + set({ + logToEdit: undefined, + vehicleId: undefined, + editMode: false, + show: false, + callback: () => {} + }); + } + + return { + subscribe, + show, + hide + }; +}; + +export const fuelLogModelStore = createFuelLogModalStore(); diff --git a/app/client/src/lib/stores/insurance.ts b/app/client/src/lib/stores/insurance.ts new file mode 100644 index 00000000..68dd18df --- /dev/null +++ b/app/client/src/lib/stores/insurance.ts @@ -0,0 +1,49 @@ +import { writable } from 'svelte/store'; + +const createInsuranceModalStore = () => { + const { subscribe, set } = writable<{ + entryToEdit?: any; + vehicleId?: string; + editMode: boolean; + show: boolean; + callback: (status: boolean) => void; + }>({ + entryToEdit: undefined, + vehicleId: undefined, + editMode: false, + show: false, + callback: () => {} + }); + + function show( + vehicleId: string, + entryToEdit?: any, + editMode: boolean = false, + callback: any = undefined + ) { + set({ + entryToEdit, + vehicleId, + editMode, + show: true, + callback + }); + } + function hide() { + set({ + entryToEdit: undefined, + vehicleId: undefined, + editMode: false, + show: false, + callback: () => {} + }); + } + + return { + subscribe, + show, + hide + }; +}; + +export const insuranceModelStore = createInsuranceModalStore(); diff --git a/app/client/src/lib/stores/maintenance.ts b/app/client/src/lib/stores/maintenance.ts new file mode 100644 index 00000000..7c4b5d80 --- /dev/null +++ b/app/client/src/lib/stores/maintenance.ts @@ -0,0 +1,49 @@ +import { writable } from 'svelte/store'; + +const createMaintenanceModalStore = () => { + const { subscribe, set } = writable<{ + vehicleId?: string; + logToEdit?: any; + editMode: boolean; + show: boolean; + callback?: any; + }>({ + vehicleId: undefined, + logToEdit: undefined, + editMode: false, + show: false, + callback: undefined + }); + + function show( + vehicleId: string, + logToEdit?: any, + editMode: boolean = false, + callback: any = undefined + ) { + set({ + vehicleId, + logToEdit, + editMode, + show: true, + callback + }); + } + + function hide() { + set({ + vehicleId: undefined, + logToEdit: undefined, + editMode: false, + show: false + }); + } + + return { + subscribe, + show, + hide + }; +}; + +export const maintenanceModelStore = createMaintenanceModalStore(); diff --git a/app/client/src/lib/stores/pucc.ts b/app/client/src/lib/stores/pucc.ts new file mode 100644 index 00000000..d59e95b1 --- /dev/null +++ b/app/client/src/lib/stores/pucc.ts @@ -0,0 +1,49 @@ +import { writable } from 'svelte/store'; + +const createPuccModalStore = () => { + const { subscribe, set } = writable<{ + entryToEdit?: any; + vehicleId?: string; + editMode: boolean; + show: boolean; + callback: (status: boolean) => void; + }>({ + entryToEdit: undefined, + vehicleId: undefined, + editMode: false, + show: false, + callback: () => {} + }); + + function show( + vehicleId: string, + entryToEdit?: any, + editMode: boolean = false, + callback: any = undefined + ) { + set({ + entryToEdit, + vehicleId, + editMode, + show: true, + callback + }); + } + function hide() { + set({ + entryToEdit: undefined, + vehicleId: undefined, + editMode: false, + show: false, + callback: () => {} + }); + } + + return { + subscribe, + show, + hide + }; +}; + +export const puccModelStore = createPuccModalStore(); diff --git a/app/client/src/lib/stores/vehicle.ts b/app/client/src/lib/stores/vehicle.ts new file mode 100644 index 00000000..0fbafce3 --- /dev/null +++ b/app/client/src/lib/stores/vehicle.ts @@ -0,0 +1,131 @@ +import { env } from '$env/dynamic/public'; +import type { Vehicle } from '$lib/models/vehicle'; +import { simulateNetworkDelay } from '$lib/utils/dev'; +import { writable } from 'svelte/store'; + +const createVehicleModalStore = () => { + const { subscribe, set } = writable<{ + vehicleToEdit?: any; + editMode: boolean; + show: boolean; + }>({ + vehicleToEdit: undefined, + editMode: false, + show: false + }); + + function show(vehicleToEdit?: any, editMode: boolean = false) { + set({ + vehicleToEdit, + editMode, + show: true + }); + } + function hide() { + set({ + vehicleToEdit: undefined, + editMode: false, + show: false + }); + } + + return { + subscribe, + show, + hide + }; +}; + +const createVehiclesStore = () => { + const { subscribe, set, update } = writable<{ + loading: boolean; + error: string; + vehicles: Vehicle[]; + selectedVehicleId?: string; + }>({ + loading: true, + error: '', + vehicles: [], + selectedVehicleId: undefined + }); + + async function fetchVehicles() { + let tempSelection: string | undefined = undefined; + update((current) => { + if (current.selectedVehicleId) { + tempSelection = current.selectedVehicleId; + } + return { + loading: true, + error: current.error, + vehicles: [], + selectedVehicleId: undefined + }; + }); + // await simulateNetworkDelay(2000); // Simulate network delay for development + try { + const response = await fetch(`${env.PUBLIC_API_BASE_URL || ''}/api/vehicles`, { + headers: { + 'X-User-PIN': localStorage.getItem('userPin') || '' + } + }); + if (response.ok) { + const vehicles = await response.json(); + if (Array.isArray(vehicles)) { + set({ + loading: false, + error: '', + vehicles: vehicles + }); + } else { + console.error('Invalid vehicles data format', vehicles); + set({ + loading: false, + error: 'Invalid vehicles data format.', + vehicles: [] + }); + } + } else { + console.log('Failed to fetch vehicles', response); + const data = await response.json(); + const error = data.message || 'Failed to fetch vehicles.'; + set({ + loading: false, + error, + vehicles: [] + }); + } + } catch (e) { + console.error('Failed to connect to the server.', e); + set({ + loading: false, + error: 'Failed to connect to the server.', + vehicles: [] + }); + } + update((current) => ({ + loading: current.loading, + error: '', + vehicles: current.vehicles, + selectedVehicleId: tempSelection + })); + } + + function selectVehicle(vehicleId: string) { + update((current) => ({ + ...current, + selectedVehicleId: vehicleId + })); + } + + fetchVehicles(); + + return { + subscribe, + fetchVehicles, + selectVehicle + }; +}; + +export const vehicleModelStore = createVehicleModalStore(); +export const vehiclesStore = createVehiclesStore(); diff --git a/app/client/src/lib/utils/dev.ts b/app/client/src/lib/utils/dev.ts new file mode 100644 index 00000000..fe8c71d2 --- /dev/null +++ b/app/client/src/lib/utils/dev.ts @@ -0,0 +1,5 @@ +const simulateNetworkDelay = (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +export { simulateNetworkDelay }; diff --git a/app/client/src/lib/utils/formatting.ts b/app/client/src/lib/utils/formatting.ts new file mode 100644 index 00000000..dea32075 --- /dev/null +++ b/app/client/src/lib/utils/formatting.ts @@ -0,0 +1,108 @@ +import dayjs from 'dayjs'; +import { config, type Config } from '$lib/stores/config'; + +export interface ConfigStore { + dateFormat: string; + currency: string; + unitOfMeasure?: string; +} + +const configs: ConfigStore = { + dateFormat: 'DD/MM/YYYY', + currency: 'USD', + unitOfMeasure: 'metric' +}; + +config.subscribe((value) => { + if (value && value.length > 0) { + value.forEach((item) => { + if (item.key === 'dateFormat') { + configs.dateFormat = item.value || configs.dateFormat; + } else if (item.key === 'currency') { + configs.currency = item.value || configs.currency; + } else if (item.key === 'unitOfMeasure') { + configs.unitOfMeasure = item.value || configs.unitOfMeasure; + } + }); + } +}); + +const formatDate = (date: Date | string): string => { + return dayjs(date).format(configs.dateFormat); +}; + +const getCurrencySymbol = (): string => { + return ( + new Intl.NumberFormat('en-US', { + style: 'currency', + currency: configs.currency + }) + .formatToParts(0) + .find((part) => part.type === 'currency')?.value || '' + ); +}; + +const formatCurrency = (amount: number): string => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: configs.currency + }).format(amount); +}; + +const getDistanceUnit = (): string => { + if (configs.unitOfMeasure === 'metric') { + return 'km'; + } + if (configs.unitOfMeasure === 'imperial') { + return 'mi'; + } + return ''; +}; + +const formatDistance = (distance: number): string => { + if (configs.unitOfMeasure === 'metric') { + return `${distance} km`; + } else if (configs.unitOfMeasure === 'imperial') { + return `${distance} mi`; + } + return `${distance}`; +}; + +const formatVolume = (volume: number): string => { + if (configs.unitOfMeasure === 'metric') { + return `${volume} l`; + } else if (configs.unitOfMeasure === 'imperial') { + return `${volume} gal`; + } + return `${volume}`; +}; + +const getMileageUnit = (): string => { + if (configs.unitOfMeasure === 'metric') { + return 'kmpl'; + } + if (configs.unitOfMeasure === 'imperial') { + return 'mpg'; + } + return ''; +}; + +const formatMileage = (mileage: number): string => { + if (configs.unitOfMeasure === 'metric') { + return `${mileage} kmpl`; + } else if (configs.unitOfMeasure === 'imperial') { + return `${mileage} mpg`; + } + return `${mileage}`; +}; + +export { + formatDate, + getCurrencySymbol, + formatCurrency, + getDistanceUnit, + formatDistance, + formatVolume, + getMileageUnit, + formatMileage +}; diff --git a/app/client/src/routes/+layout.svelte b/app/client/src/routes/+layout.svelte index 7a00e4b0..b8fb0c40 100644 --- a/app/client/src/routes/+layout.svelte +++ b/app/client/src/routes/+layout.svelte @@ -5,7 +5,7 @@ import '../styles/app.css'; import { tick } from 'svelte'; import { Car, LogOut, Tractor, Settings } from '@lucide/svelte'; - import ThemeToggle from '../components/common/ThemeToggle.svelte'; + import ThemeToggle from '$components/common/ThemeToggle.svelte'; import { onMount } from 'svelte'; import { env } from '$env/dynamic/public'; @@ -54,7 +54,7 @@ >🚜 Demo Mode: This is a demo instance. Data will be reset periodically and is not saved permanently. Please avoid adding any persoanl info. -
+
Default PIN : 123456 {/if} @@ -76,7 +76,10 @@
- +
@@ -85,7 +88,6 @@ class="flex items-center gap-1 text-gray-600 transition-colors duration-300 hover:text-red-500 dark:text-gray-300" > - Logout
diff --git a/app/client/src/routes/+page.svelte b/app/client/src/routes/+page.svelte index 5e7f5a3a..bae4370a 100644 --- a/app/client/src/routes/+page.svelte +++ b/app/client/src/routes/+page.svelte @@ -1,6 +1,7 @@ -
+
+

Redirecting...

diff --git a/app/client/src/routes/config/+page.svelte b/app/client/src/routes/config/+page.svelte index 51477d29..dacce45e 100644 --- a/app/client/src/routes/config/+page.svelte +++ b/app/client/src/routes/config/+page.svelte @@ -1,13 +1,14 @@ @@ -292,254 +49,53 @@
{#if loading} -

Loading vehicles...

+

+ + Loading Vehicles... +

{:else if error}

Error: {error}

{:else} - { - selectedVehicleId = e.detail.vehicle.id; - showFuelRefillModal = true; - }} - on:addMaintenance={(e) => { - selectedVehicleId = e.detail.vehicle.id; - showMaintenanceLogModal = true; - }} - /> - {/if} - - - - {#if showEditVehicleModal} - + {/if} {#if selectedVehicleId}
-
    - - - - - -
+
{#if activeTab === 'dashboard'} -
-

- Fuel Cost & Mileage Trends -

-
- {#if fuelCostData?.datasets?.length > 0 && mileageData?.datasets?.length > 0} - - - {:else} -
-

- No fuel or mileage data available for this vehicle. -

-
- {/if} -
-
+ {:else if activeTab === 'fuel'} -
- -
+ {:else if activeTab === 'maintenance'} -
- -
+ {:else if activeTab === 'insurance'} -
- -
+ {:else if activeTab === 'pollution'} -
- -
+ {/if}
- (showFuelRefillModal = false)} - on:success={() => { - if (selectedVehicleId) fetchChartData(selectedVehicleId); - showFuelRefillModal = false; - }} - /> - (showMaintenanceLogModal = false)} - on:success={() => { - showMaintenanceLogModal = false; - }} - /> - {:else} + {:else if vehicles.length > 0 && !loading}

Select a vehicle to view fuel and mileage data.

{/if} + + + + + + diff --git a/app/client/src/routes/login/+page.svelte b/app/client/src/routes/login/+page.svelte index f7ebfca7..71c61aef 100644 --- a/app/client/src/routes/login/+page.svelte +++ b/app/client/src/routes/login/+page.svelte @@ -1,9 +1,12 @@