Refactor frontend to Composition API and improve UI/UX

Major Changes:
- Migrate components from Options API to Composition API with <script setup>
- Add centralized service layer (serviceService, deliveryService, adminService)
- Implement new reusable components (EnhancedButton, EnhancedModal, StatCard, etc.)
- Add theme store for consistent theming across application
- Improve ServiceCalendar with federal holidays and better styling
- Refactor customer profile and tank estimation components
- Update all delivery and payment pages to use centralized services
- Add utility functions for formatting and validation
- Update Dockerfiles for better environment configuration
- Enhance Tailwind config with custom design tokens

UI Improvements:
- Modern, premium design with glassmorphism effects
- Improved form layouts with FloatingInput components
- Better loading states and empty states
- Enhanced modals and tables with consistent styling
- Responsive design improvements across all pages

Technical Improvements:
- Strict TypeScript types throughout
- Better error handling and validation
- Removed deprecated api.js in favor of TypeScript services
- Improved code organization and maintainability
This commit is contained in:
2026-02-01 19:04:07 -05:00
parent 72d8e35e06
commit 61f93ec4e8
86 changed files with 3931 additions and 2086 deletions

View File

@@ -102,8 +102,8 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import dayjs from 'dayjs';
import axios from 'axios';
import authHeader from '../../services/auth.header';
import serviceService from '../../services/serviceService';
import customerService from '../../services/customerService';
// --- Interfaces ---
interface ServiceCall { id: number; scheduled_date: string; customer_id: number; customer_name: string; customer_address: string; customer_town: string; type_service_call: number; description: string; service_cost: string }
@@ -132,32 +132,33 @@ const serviceOptions = ref([
{ text: 'Tank Install', value: 3 }, { text: 'Other', value: 4 },
])
// Watchers
watch(() => props.service, (newVal) => {
if (!newVal) return;
const scheduled = dayjs(newVal.scheduled_date || new Date());
editableService.value = { ...newVal, date: scheduled.format('YYYY-MM-DD'), time: scheduled.hour() };
if (newVal.customer_id) {
getCustomer(newVal.customer_id);
getServiceParts(newVal.customer_id);
}
}, { immediate: true, deep: true })
// Functions
// Functions (defined before watchers to avoid hoisting issues)
const getCustomer = (customerId: number) => {
customer.value = null;
let path = import.meta.env.VITE_BASE_URL + '/customer/' + customerId;
axios.get(path, { headers: authHeader() })
.then((response: any) => { customer.value = response.data; })
customerService.getById(customerId)
.then((response: any) => {
if (response.data.customer) {
customer.value = response.data.customer;
} else if (response.data.ok && response.data.id) {
customer.value = response.data as unknown as Customer;
}
})
.catch((error: any) => { console.error("Failed to fetch customer details for modal:", error); });
}
const getServiceParts = (customerId: number) => {
isLoadingParts.value = true;
serviceParts.value = null;
let path = `${import.meta.env.VITE_BASE_URL}/service/parts/customer/${customerId}`;
axios.get(path, { headers: authHeader() })
.then((response: any) => { serviceParts.value = response.data; })
serviceService.getPartsForCustomer(customerId)
.then((response: any) => {
if (response.data.parts) {
if (Array.isArray(response.data.parts) && response.data.parts.length > 0) {
serviceParts.value = response.data.parts[0];
} else {
serviceParts.value = response.data.parts as unknown as ServiceParts;
}
}
})
.catch((error: any) => { console.error("Failed to fetch service parts:", error); })
.finally(() => { isLoadingParts.value = false; });
}
@@ -168,9 +169,8 @@ const saveChanges = async () => {
const combinedDateTime = dayjs(`${date} ${time}:00`).format('YYYY-MM-DDTHH:mm:ss');
const finalPayload = { ...props.service, ...editableService.value, scheduled_date: combinedDateTime };
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${finalPayload.id}`;
try {
await axios.put(path, finalPayload, { headers: authHeader(), withCredentials: true });
await serviceService.update(finalPayload.id!, finalPayload);
emit('save-changes', finalPayload as ServiceCall);
} catch (error) {
console.error("Failed to save changes:", error);
@@ -201,4 +201,15 @@ const getStateAbbrev = (stateId: number | undefined | null): string => {
const stateMap: { [key: number]: string } = { 0: 'MA', 1: 'RI', 2: 'NH', 3: 'ME', 4: 'VT', 5: 'CT', 6: 'NY' };
return stateMap[stateId] || 'Unknown';
}
// Watchers (after function definitions)
watch(() => props.service, (newVal) => {
if (!newVal) return;
const scheduled = dayjs(newVal.scheduled_date || new Date());
editableService.value = { ...newVal, date: scheduled.format('YYYY-MM-DD'), time: scheduled.hour() };
if (newVal.customer_id) {
getCustomer(newVal.customer_id);
getServiceParts(newVal.customer_id);
}
}, { immediate: true, deep: true })
</script>