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

@@ -1,8 +1,8 @@
<!-- src/pages/service/ServiceCalendar.vue -->
<template>
<div class="flex">
<div class="w-full px-10">
<div class="calendar-page">
<div class="w-full px-4 md:px-10">
<!-- Breadcrumbs -->
<div class="text-sm breadcrumbs mb-4">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
@@ -11,21 +11,104 @@
</ul>
</div>
<div class="flex text-2xl mb-5 font-bold">
Master Service Calendar
<!-- Page Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-3xl font-bold mb-1">Service Calendar</h1>
<p class="text-base-content/60">Manage and schedule service calls</p>
</div>
<div class="flex gap-3">
<button @click="goToToday" class="btn btn-ghost btn-sm gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
</svg>
Today
</button>
<router-link :to="{ name: 'ServiceHome' }" class="btn btn-primary btn-sm gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
New Service Call
</router-link>
</div>
</div>
<div class="flex h-screen font-sans">
<div class="flex-1 p-4 overflow-auto">
<!-- The 'ref' is important for accessing the calendar's API -->
<!-- Calendar Container -->
<div class="calendar-container bg-gradient-to-br from-neutral to-neutral/80 rounded-xl shadow-strong overflow-hidden">
<!-- Custom Calendar Header -->
<div class="calendar-header bg-base-200 px-6 py-4 border-b border-base-300">
<div class="flex items-center justify-between">
<!-- Navigation -->
<div class="flex items-center gap-2">
<button @click="previousMonth" class="btn btn-ghost btn-sm btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
</button>
<h2 class="text-2xl font-bold min-w-[200px] text-center">{{ currentMonthYear }}</h2>
<button @click="nextMonth" class="btn btn-ghost btn-sm btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
</div>
<!-- View Switcher -->
<div class="btn-group">
<button
@click="changeView('dayGridMonth')"
class="btn btn-sm"
:class="{ 'btn-active': currentView === 'dayGridMonth' }"
>
Month
</button>
<button
@click="changeView('dayGridWeek')"
class="btn btn-sm"
:class="{ 'btn-active': currentView === 'dayGridWeek' }"
>
Week
</button>
<button
@click="changeView('dayGridDay')"
class="btn btn-sm"
:class="{ 'btn-active': currentView === 'dayGridDay' }"
>
Day
</button>
</div>
<!-- Legend -->
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-info"></div>
<span class="text-sm">Scheduled</span>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-success"></div>
<span class="text-sm">Completed</span>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-warning"></div>
<span class="text-sm">In Progress</span>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-accent"></div>
<span class="text-sm">Federal Holiday</span>
</div>
</div>
</div>
</div>
<!-- FullCalendar -->
<div class="calendar-body p-6">
<FullCalendar ref="fullCalendar" :options="calendarOptions" />
</div>
</div>
</div>
</div>
<Footer />
<!-- Edit Modal -->
<ServiceEditModal
v-if="selectedServiceForEdit"
:service="selectedServiceForEdit"
@@ -36,49 +119,56 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import Header from '../../layouts/headers/headerauth.vue';
import SideBar from '../../layouts/sidebar/sidebar.vue';
import Footer from '../../layouts/footers/footer.vue';
import { ref, onMounted, computed } from 'vue';
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import { CalendarOptions, EventClickArg } from '@fullcalendar/core';
import { CalendarOptions, EventClickArg, DayCellContentArg } from '@fullcalendar/core';
import ServiceEditModal from './ServiceEditModal.vue';
import axios from 'axios';
import authHeader from '../../services/auth.header';
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;
}
import { serviceService } from '../../services/serviceService';
import { authService } from '../../services/authService';
import { AxiosResponse, AxiosError, ServiceCall } from '../../types/models';
import { getFederalHolidays, type Holiday } from '../../utils/holidays';
// Reactive data
const user = ref(null)
const selectedServiceForEdit = ref(null as Partial<ServiceCall> | null)
const fullCalendar = ref()
const currentView = ref('dayGridMonth')
const holidays = ref<Holiday[]>([])
const currentDate = ref(new Date())
// Functions
// We can remove the fetchEvents method as FullCalendar now handles it.
// async fetchEvents(): Promise<void> { ... }
// Computed
const currentMonthYear = computed(() => {
return currentDate.value.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
})
// Load holidays for current year and next year
const loadHolidays = () => {
const currentYear = new Date().getFullYear()
const allHolidays = [
...getFederalHolidays(currentYear - 1),
...getFederalHolidays(currentYear),
...getFederalHolidays(currentYear + 1),
]
holidays.value = allHolidays
}
// Check if a date is a holiday
const isHolidayDate = (dateStr: string): Holiday | undefined => {
return holidays.value.find(h => h.date === dateStr)
}
// Event handlers
const handleEventClick = (clickInfo: EventClickArg): void => {
// This logic remains the same, as it correctly pulls data from extendedProps
selectedServiceForEdit.value = {
id: parseInt(clickInfo.event.id),
scheduled_date: clickInfo.event.startStr,
customer_name: clickInfo.event.title.split(': ')[1] || 'Unknown Customer',
customer_id: clickInfo.event.extendedProps.customer_id,
type_service_call: clickInfo.event.extendedProps.type_service_call,
description: clickInfo.event.extendedProps.description,
service_cost: clickInfo.event.extendedProps.service_cost,
id: parseInt(clickInfo.event.id),
scheduled_date: clickInfo.event.startStr,
customer_name: clickInfo.event.title.split(': ')[1] || 'Unknown Customer',
customer_id: clickInfo.event.extendedProps.customer_id,
type_service_call: clickInfo.event.extendedProps.type_service_call,
description: clickInfo.event.extendedProps.description,
service_cost: clickInfo.event.extendedProps.service_cost,
};
}
@@ -89,73 +179,116 @@ const fetchCalendarEvents = async (
failureCallback: (error: Error) => void
) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/all`;
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
// Backend returns { ok: true, events: [...] }
const events = response.data?.events || [];
successCallback(events);
} catch (error) {
const response = await serviceService.getAll();
const serviceEvents = response.data?.events || [];
// Add federal holidays as background events
const holidayEvents = holidays.value.map(holiday => ({
id: `holiday-${holiday.date}`,
title: holiday.name,
start: holiday.date,
allDay: true,
display: 'background',
classNames: ['holiday-event']
}));
// Combine service events and holiday events
const allEvents = [...serviceEvents, ...holidayEvents];
successCallback(allEvents);
} catch (err: unknown) {
const error = err as AxiosError;
console.error("Failed to fetch calendar events:", error);
failureCallback(error as Error);
}
};
// Calendar navigation
const goToToday = () => {
const calendarApi = (fullCalendar.value as any).getApi()
calendarApi.today()
currentDate.value = calendarApi.getDate()
}
const previousMonth = () => {
const calendarApi = (fullCalendar.value as any).getApi()
calendarApi.prev()
currentDate.value = calendarApi.getDate()
}
const nextMonth = () => {
const calendarApi = (fullCalendar.value as any).getApi()
calendarApi.next()
currentDate.value = calendarApi.getDate()
}
const changeView = (viewName: string) => {
currentView.value = viewName
const calendarApi = (fullCalendar.value as any).getApi()
calendarApi.changeView(viewName)
currentDate.value = calendarApi.getDate()
}
// Day cell class names for holidays
const getDayCellClassNames = (arg: any) => {
// Format date as YYYY-MM-DD
const year = arg.date.getFullYear()
const month = String(arg.date.getMonth() + 1).padStart(2, '0')
const day = String(arg.date.getDate()).padStart(2, '0')
const dateStr = `${year}-${month}-${day}`
const holiday = isHolidayDate(dateStr)
if (holiday) {
console.log('Holiday found:', holiday.name, 'on', dateStr)
}
return holiday ? ['holiday-cell'] : []
}
// Calendar options
const calendarOptions = ref({
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
headerToolbar: false, // We're using custom header
weekends: true,
// Use function source to fetch events with auth headers and transform response
height: 'auto',
events: fetchCalendarEvents,
eventClick: handleEventClick,
eventClassNames: 'custom-event',
dayCellClassNames: getDayCellClassNames,
eventDisplay: 'block',
displayEventTime: false,
eventBackgroundColor: 'transparent',
eventBorderColor: 'transparent',
} as CalendarOptions)
// Lifecycle
onMounted(() => {
userStatus();
// We no longer need to call fetchEvents() here because FullCalendar does it automatically.
})
// Modal handlers
const closeEditModal = () => {
selectedServiceForEdit.value = null;
selectedServiceForEdit.value = null;
}
// =================== THIS IS THE CORRECTED SECTION ===================
const handleSaveChanges = async (updatedService: ServiceCall) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
// Get the FullCalendar component instance from the ref
await serviceService.update(updatedService.id, updatedService);
const calendarApi = (fullCalendar.value as any).getApi();
if (calendarApi) {
// Tell FullCalendar to re-fetch its events from the source.
// This is the most reliable way to refresh the view immediately.
calendarApi.refetchEvents();
}
closeEditModal();
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
}
// =================== END OF CORRECTED SECTION ===================
const handleDeleteService = async (serviceId: number) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
await axios.delete(path, { withCredentials: true, headers: authHeader() });
// Also refresh the calendar after a delete
await serviceService.delete(serviceId);
const calendarApi = (fullCalendar.value as any).getApi();
if (calendarApi) {
calendarApi.refetchEvents();
}
closeEditModal();
} catch (error) {
console.error("Error deleting event:", error);
@@ -163,20 +296,162 @@ const handleDeleteService = async (serviceId: number) => {
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
authService.whoami().then((response: AxiosResponse<any>) => {
if (response.data.ok) {
user.value = response.data.user;
}
})
.catch(() => {
user.value = null
})
.then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;
}
})
.catch(() => {
user.value = null
})
}
</script>
// Lifecycle
onMounted(() => {
userStatus();
loadHolidays();
})
</script>
<style scoped>
.calendar-page {
@apply min-h-screen animate-fade-in;
}
.calendar-container {
@apply transition-all duration-300;
}
/* FullCalendar Custom Styling */
:deep(.fc) {
@apply font-sans;
}
:deep(.fc-theme-standard td),
:deep(.fc-theme-standard th) {
border-color: hsl(var(--bc) / 0.2) !important;
border-width: 2px !important;
}
:deep(.fc-scrollgrid) {
border-width: 2px !important;
border-color: hsl(var(--bc) / 0.2) !important;
}
:deep(.fc-col-header-cell) {
@apply bg-base-200 font-semibold text-sm uppercase tracking-wider py-3;
border-width: 2px !important;
border-color: hsl(var(--bc) / 0.2) !important;
}
:deep(.fc-daygrid-day) {
@apply transition-colors hover:bg-base-200/50;
border-width: 2px !important;
border-color: hsl(var(--bc) / 0.2) !important;
}
:deep(.fc-daygrid-day-number) {
@apply text-base-content/80 font-medium p-2;
}
:deep(.fc-day-today) {
@apply bg-primary/5 !important;
}
:deep(.fc-day-today .fc-daygrid-day-number) {
@apply text-primary font-bold;
}
/* Custom Event Styling */
:deep(.fc-event) {
@apply rounded-lg px-2 py-1 mb-1 cursor-pointer;
@apply bg-info/20 border-l-4 border-info;
@apply hover:bg-info/30 transition-colors;
@apply shadow-sm hover:shadow-md;
}
:deep(.fc-event-title) {
@apply text-sm font-medium text-base-content truncate;
}
/* Day cell styling */
:deep(.fc-daygrid-day-frame) {
@apply min-h-[100px];
}
:deep(.fc-daygrid-day-top) {
@apply flex justify-center;
}
/* Remove default FullCalendar button styling since we have custom header */
:deep(.fc-toolbar) {
@apply hidden;
}
/* Holiday styling - Using accent color from theme */
:deep(.holiday-cell) {
background-color: hsl(var(--a) / 0.15) !important;
border-color: hsl(var(--a) / 0.4) !important;
}
:deep(.holiday-cell .fc-daygrid-day-number) {
color: hsl(var(--a)) !important;
@apply font-bold;
}
/* Holiday background events */
:deep(.fc-bg-event.holiday-event) {
background-color: hsl(var(--a) / 0.5) !important;
opacity: 1 !important;
z-index: 1 !important;
inset: 0 !important;
margin: 0 !important;
border-radius: 0 !important;
}
:deep(.fc-daygrid-day-bg .fc-bg-event.holiday-event) {
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100% !important;
height: 100% !important;
}
/* Holiday event title positioning */
:deep(.fc-bg-event.holiday-event .fc-event-title) {
position: absolute !important;
bottom: 4px !important;
left: 4px !important;
right: 4px !important;
top: auto !important;
font-size: 0.7rem !important;
font-weight: 600 !important;
color: hsl(var(--a)) !important;
text-align: center !important;
line-height: 1.2 !important;
padding: 2px !important;
}
/* Weekend styling - Using theme colors */
:deep(.fc-day-sat),
:deep(.fc-day-sun) {
background-color: hsl(var(--b3)) !important;
border-color: hsl(var(--bc) / 0.3) !important;
}
:deep(.fc-day-sat .fc-daygrid-day-number),
:deep(.fc-day-sun .fc-daygrid-day-number) {
color: hsl(var(--bc) / 0.8) !important;
@apply font-semibold;
}
/* Weekend header cells */
:deep(.fc-col-header-cell.fc-day-sat),
:deep(.fc-col-header-cell.fc-day-sun) {
background-color: hsl(var(--b3)) !important;
color: hsl(var(--bc)) !important;
@apply font-bold;
}
</style>

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>

View File

@@ -146,7 +146,7 @@
</div>
</div>
<Footer />
<ServiceEditModal
v-if="selectedServiceForEdit"
@@ -158,10 +158,9 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import authHeader from '../../services/auth.header'
import { serviceService } from '../../services/serviceService'
import { authService } from '../../services/authService'
import { ServiceCall } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue'
import ServiceEditModal from './ServiceEditModal.vue'
import dayjs from 'dayjs';
@@ -184,13 +183,11 @@ onMounted(() => {
const fetchUpcomingServices = async (): Promise<void> => {
isLoading.value = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/upcoming';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
const serviceList = response.data?.services || [];
services.value = serviceList.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
const response = await serviceService.getUpcoming();
if (response.data.ok) {
const serviceList = response.data.services || [];
services.value = serviceList.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
}
} catch (error) {
console.error("Failed to fetch upcoming service calls:", error);
} finally {
@@ -199,13 +196,7 @@ const fetchUpcomingServices = async (): Promise<void> => {
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
authService.whoami()
.then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;
@@ -250,8 +241,7 @@ const toggleExpand = (id: number): void => {
const handleSaveChanges = async (updatedService: ServiceCall) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
const response = await serviceService.update(updatedService.id, updatedService);
if (response.data.ok) {
const index = services.value.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
@@ -267,8 +257,7 @@ const handleSaveChanges = async (updatedService: ServiceCall) => {
const handleDeleteService = async (serviceId: number) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
const response = await serviceService.delete(serviceId);
if (response.data.ok) {
services.value = services.value.filter(s => s.id !== serviceId);
closeEditModal();

View File

@@ -152,7 +152,7 @@
</div>
</div>
<Footer />
<ServiceEditModal
v-if="selectedServiceForEdit"
@@ -165,15 +165,14 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import authHeader from '../../services/auth.header'
import serviceService from '../../services/serviceService'
import authService from '../../services/authService'
import { ServiceCall } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue'
import ServiceEditModal from './ServiceEditModal.vue'
import dayjs from 'dayjs';
// Reactive data
const user = ref(null)
const user = ref<any>(null)
const services = ref<ServiceCall[]>([])
const isLoading = ref(true)
const selectedServiceForEdit = ref<ServiceCall | null>(null)
@@ -183,7 +182,9 @@ const expandedIds = ref<number[]>([])
// Lifecycle
onMounted(() => {
userStatus();
if (authService) {
userStatus();
}
fetchPastServices();
})
@@ -215,13 +216,12 @@ const toggleExpand = (id: number): void => {
const fetchPastServices = async (): Promise<void> => {
isLoading.value = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/past';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
const serviceList = response.data?.services || [];
services.value = serviceList.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
const response = await serviceService.getPast();
if (response.data && response.data.services) {
services.value = response.data.services.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} else {
services.value = [];
}
} catch (error) {
console.error("Failed to fetch past service calls:", error);
} finally {
@@ -230,13 +230,7 @@ const fetchPastServices = async (): Promise<void> => {
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
authService.whoami()
.then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;
@@ -257,9 +251,8 @@ const closeEditModal = () => {
const handleSaveChanges = async (updatedService: ServiceCall) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
const response = await serviceService.update(updatedService.id, updatedService);
if (response.data.service) { // Based on ServiceResponse type
const index = services.value.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
services.value[index] = response.data.service;
@@ -274,8 +267,7 @@ const handleSaveChanges = async (updatedService: ServiceCall) => {
const handleDeleteService = async (serviceId: number) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
const response = await serviceService.delete(serviceId);
if (response.data.ok) {
services.value = services.value.filter(s => s.id !== serviceId);
closeEditModal();

View File

@@ -120,25 +120,26 @@
</div>
</div>
<Footer />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import authHeader from '../../services/auth.header'
import serviceService from '../../services/serviceService'
import authService from '../../services/authService'
import { ServicePlan } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue'
import dayjs from 'dayjs';
// Reactive data
const user = ref(null)
const user = ref<any>(null)
const servicePlans = ref<ServicePlan[]>([])
const isLoading = ref(true)
// Lifecycle
onMounted(() => {
userStatus();
if (authService) {
userStatus();
}
fetchServicePlans();
})
@@ -146,13 +147,12 @@ onMounted(() => {
const fetchServicePlans = async (): Promise<void> => {
isLoading.value = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/plans/active';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
// Backend returns { ok: true, plans: [...] }
servicePlans.value = response.data?.plans || [];
const response = await serviceService.plans.getActive();
if (response.data && response.data.plans) {
servicePlans.value = response.data.plans;
} else {
servicePlans.value = [];
}
} catch (error) {
console.error("Failed to fetch service plans:", error);
} finally {
@@ -161,13 +161,7 @@ const fetchServicePlans = async (): Promise<void> => {
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
authService.whoami()
.then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;

View File

@@ -152,7 +152,7 @@
</div>
</div>
<Footer />
<ServiceEditModal
v-if="selectedServiceForEdit"
@@ -165,15 +165,14 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import authHeader from '../../services/auth.header'
import serviceService from '../../services/serviceService'
import authService from '../../services/authService'
import { ServiceCall } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue'
import ServiceEditModal from './ServiceEditModal.vue'
import dayjs from 'dayjs';
// Reactive data
const user = ref(null)
const user = ref<any>(null)
const services = ref<ServiceCall[]>([])
const isLoading = ref(true)
const selectedServiceForEdit = ref<ServiceCall | null>(null)
@@ -183,7 +182,9 @@ const expandedIds = ref<number[]>([])
// Lifecycle
onMounted(() => {
userStatus();
if (authService) { // Check if imported correctly
userStatus();
}
fetchTodayServices();
})
@@ -215,13 +216,16 @@ const toggleExpand = (id: number): void => {
const fetchTodayServices = async (): Promise<void> => {
isLoading.value = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/today';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
const serviceList = response.data?.services || [];
services.value = serviceList.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
const response = await serviceService.getToday();
// According to serviceService.ts, getToday returns AxiosResponse<ServicesResponse>
// ServicesResponse has { ok: boolean, services: ServiceCall[] }
// However, the api unwrap interceptor might put properties directly on data
// Let's assume the response structure follows the type
if (response.data && response.data.services) {
services.value = response.data.services.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} else {
services.value = [];
}
} catch (error) {
console.error("Failed to fetch today's service calls:", error);
} finally {
@@ -230,13 +234,7 @@ const fetchTodayServices = async (): Promise<void> => {
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
authService.whoami()
.then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;
@@ -257,9 +255,8 @@ const closeEditModal = () => {
const handleSaveChanges = async (updatedService: ServiceCall) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
const response = await serviceService.update(updatedService.id, updatedService);
if (response.data.service) { // Based on ServiceResponse type
const index = services.value.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
services.value[index] = response.data.service;
@@ -274,8 +271,7 @@ const handleSaveChanges = async (updatedService: ServiceCall) => {
const handleDeleteService = async (serviceId: number) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
const response = await serviceService.delete(serviceId);
if (response.data.ok) {
services.value = services.value.filter(s => s.id !== serviceId);
closeEditModal();

View File

@@ -65,8 +65,8 @@ import interactionPlugin from '@fullcalendar/interaction';
import { CalendarOptions, EventClickArg } from '@fullcalendar/core';
import EventSidebar from './EventSidebar.vue';
import ServiceEditModal from '../../service/ServiceEditModal.vue';
import axios from 'axios';
import authHeader from '../../../services/auth.header';
import serviceService from '../../../services/serviceService';
import customerService from '../../../services/customerService';
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;}
interface Customer { id: number; customer_last_name: string; customer_first_name: string; customer_town: string; customer_state: number; customer_zip: string; customer_phone_number: string; customer_address: string; customer_home_type: number; customer_apt: string; }
@@ -108,58 +108,19 @@ const calendarOptions = ref<CalendarOptions>({
});
const customer = ref<Customer | null>(null);
// Watchers
watch(() => route.params.id, (newId) => {
if (newId) getCustomer(newId as string);
}, { immediate: true });
// Lifecycle
onMounted(() => {
fetchEvents();
});
// Functions
// Functions (defined before watchers to avoid hoisting issues)
const closeEditModal = () => {
selectedServiceForEdit.value = null;
};
const handleSaveChanges = async (updatedService: ServiceCall) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
await fetchEvents();
closeEditModal();
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
};
const handleDeleteService = async (serviceId: number) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { withCredentials: true, headers: authHeader() });
if (response.data.ok === true) {
await fetchEvents();
closeEditModal();
} else {
console.error("Failed to delete event:", response.data.error);
}
} catch (error) {
console.error("Error deleting event:", error);
}
};
const getCustomer = async (customerId: string): Promise<void> => {
isLoading.value = true;
customer.value = null;
try {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${customerId}`;
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
const response = await customerService.getById(Number(customerId));
const customerData = response.data?.customer || response.data;
if (customerData && customerData.id) {
customer.value = customerData;
if (customerData && (customerData as any).id) {
customer.value = customerData as unknown as Customer;
}
} catch (error) {
console.error("API call to get customer FAILED:", error);
@@ -170,14 +131,39 @@ const getCustomer = async (customerId: string): Promise<void> => {
const fetchEvents = async (): Promise<void> => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/all`;
const response = await axios.get(path, { headers: authHeader(), withCredentials: true });
const response = await serviceService.getAll();
calendarOptions.value.events = response.data?.events || [];
} catch (error) {
console.error("Error fetching all calendar events:", error);
}
};
const handleSaveChanges = async (updatedService: ServiceCall) => {
try {
await serviceService.update(updatedService.id, updatedService);
await fetchEvents();
closeEditModal();
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
};
const handleDeleteService = async (serviceId: number) => {
try {
const response = await serviceService.delete(serviceId);
if (response.data.ok === true) {
await fetchEvents();
closeEditModal();
} else {
// console.error("Failed to delete event:", response.data.error);
// Error property might not exist on typed response, but checking ok is enough
}
} catch (error) {
console.error("Error deleting event:", error);
}
};
const handleEventScheduled = async (eventData: any): Promise<void> => {
if (!customer.value) {
alert("Error: A customer must be loaded in the sidebar to create a new event.");
@@ -188,12 +174,13 @@ const handleEventScheduled = async (eventData: any): Promise<void> => {
expected_delivery_date: eventData.start, type_service_call: eventData.type_service_call,
customer_id: customer.value.id, description: eventData.extendedProps.description,
};
const path = import.meta.env.VITE_BASE_URL + "/service/create";
const response = await axios.post(path, payload, { withCredentials: true, headers: authHeader() });
if (response.data.ok === true) {
const response = await serviceService.create(payload);
// Service response has { ok: boolean, service: ServiceCall }
if (response.data.service) {
await fetchEvents();
} else {
console.error("Failed to create event:", response.data.error);
console.error("Failed to create event");
}
} catch (error) {
console.error("Error creating event:", error);
@@ -204,4 +191,14 @@ const handleEventDelete = async (eventId: string): Promise<void> => {
// This is a simple alias now, as handleDeleteService is more specific
await handleDeleteService(Number(eventId));
};
// Watchers (after function definitions)
watch(() => route.params.id, (newId) => {
if (newId) getCustomer(newId as string);
}, { immediate: true });
// Lifecycle
onMounted(() => {
fetchEvents();
});
</script>

View File

@@ -1,24 +0,0 @@
import axios from 'axios';
const BASE_URL = import.meta.env.VITE_BASE_URL;
function authHeader() {
// Return authorization header
return {};
}
export function createEvent(payload) {
const path = `${BASE_URL}/service/create`; // Example endpoint
return axios.post(path, payload, {
withCredentials: true,
headers: authHeader(),
});
}
export function deleteEventById(eventId) {
const path = `${BASE_URL}/service/delete/${eventId}`; // Example endpoint
return axios.delete(path, {
withCredentials: true,
headers: authHeader(),
});
}