major claude changes

This commit is contained in:
2026-01-28 21:55:14 -05:00
parent f9d0e4c0fd
commit f9b5364c53
81 changed files with 11155 additions and 10086 deletions

View File

@@ -35,8 +35,8 @@
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<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';
@@ -44,7 +44,7 @@ import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import { CalendarOptions, EventClickArg } from '@fullcalendar/core';
import ServiceEditModal from './ServiceEditModal.vue';
import ServiceEditModal from './ServiceEditModal.vue';
import axios from 'axios';
import authHeader from '../../services/auth.header';
@@ -60,113 +60,111 @@ interface ServiceCall {
service_cost: string;
}
export default defineComponent({
name: 'ServiceCalendar',
components: { Header, SideBar, Footer, FullCalendar, ServiceEditModal },
data() {
return {
user: null,
selectedServiceForEdit: null as Partial<ServiceCall> | null,
calendarOptions: {
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
weekends: true,
// Instead of a static array, we use a function source.
// This is the standard way FullCalendar fetches events.
events: `${import.meta.env.VITE_BASE_URL}/service/all`,
eventClick: this.handleEventClick,
// Add headers for authentication if needed by your API
eventSourceSuccess: (content) => {
// This is where you could transform data if needed
return content;
},
eventSourceFailure: (error) => {
console.error("Failed to fetch calendar events:", error);
}
} as CalendarOptions,
};
// Reactive data
const user = ref(null)
const selectedServiceForEdit = ref(null as Partial<ServiceCall> | null)
const fullCalendar = ref()
// Functions
// We can remove the fetchEvents method as FullCalendar now handles it.
// async fetchEvents(): Promise<void> { ... }
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,
};
}
// Calendar options
const calendarOptions = ref({
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
weekends: true,
// Instead of a static array, we use a function source.
// This is the standard way FullCalendar fetches events.
events: `${import.meta.env.VITE_BASE_URL}/service/all`,
eventClick: handleEventClick,
// Add headers for authentication if needed by your API
eventSourceSuccess: (content) => {
// This is where you could transform data if needed
return content;
},
created() {
this.userStatus();
// We no longer need to call fetchEvents() here because FullCalendar does it automatically.
},
methods: {
// We can remove the fetchEvents method as FullCalendar now handles it.
// async fetchEvents(): Promise<void> { ... }
eventSourceFailure: (error) => {
console.error("Failed to fetch calendar events:", error);
}
} as CalendarOptions)
handleEventClick(clickInfo: EventClickArg): void {
// This logic remains the same, as it correctly pulls data from extendedProps
this.selectedServiceForEdit = {
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,
};
},
// Lifecycle
onMounted(() => {
userStatus();
// We no longer need to call fetchEvents() here because FullCalendar does it automatically.
})
closeEditModal() {
this.selectedServiceForEdit = null;
},
const closeEditModal = () => {
selectedServiceForEdit.value = null;
}
// =================== THIS IS THE CORRECTED SECTION ===================
async handleSaveChanges(updatedService: ServiceCall) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
// =================== 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
const calendarApi = (this.$refs.fullCalendar 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();
}
this.closeEditModal();
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
// Get the FullCalendar component instance from the ref
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
const calendarApi = (fullCalendar.value as any).getApi();
if (calendarApi) {
calendarApi.refetchEvents();
}
closeEditModal();
} catch (error) {
console.error("Error deleting event:", error);
}
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;
}
},
// =================== END OF CORRECTED SECTION ===================
async handleDeleteService(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
const calendarApi = (this.$refs.fullCalendar as any).getApi();
if (calendarApi) {
calendarApi.refetchEvents();
}
this.closeEditModal();
} catch (error) {
console.error("Error deleting event:", error);
}
},
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
})
},
},
});
})
.catch(() => {
user.value = null
})
}
</script>

View File

@@ -99,8 +99,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
<script setup lang="ts">
import { ref, watch } from 'vue';
import dayjs from 'dayjs';
import axios from 'axios';
import authHeader from '../../services/auth.header';
@@ -111,88 +111,94 @@ interface EditableService extends Omit<ServiceCall, 'scheduled_date'> { date: st
interface Customer { id: number; account_number: string; customer_first_name: string; customer_last_name: string; customer_address: string; customer_town: string; customer_state: number; customer_zip: string; customer_phone_number: string; }
interface ServiceParts { customer_id: number; oil_filter: string; oil_filter_2: string; oil_nozzle: string; oil_nozzle_2: string; }
export default defineComponent({
name: 'ServiceEditModal',
props: { service: { type: Object as PropType<Partial<ServiceCall>>, required: true } },
data() {
return {
editableService: {} as Partial<EditableService>,
customer: null as Customer | null,
serviceParts: null as ServiceParts | null,
isLoadingParts: true,
serviceOptions: [
{ text: 'Tune-up', value: 0 }, { text: 'No Heat', value: 1 }, { text: 'Fix', value: 2 },
{ text: 'Tank Install', value: 3 }, { text: 'Other', value: 4 },
],
};
},
watch: {
service: {
handler(newVal) {
if (!newVal) return;
const scheduled = dayjs(newVal.scheduled_date || new Date());
this.editableService = { ...newVal, date: scheduled.format('YYYY-MM-DD'), time: scheduled.hour() };
if (newVal.customer_id) {
this.getCustomer(newVal.customer_id);
this.getServiceParts(newVal.customer_id);
}
},
immediate: true,
deep: true,
},
},
methods: {
getCustomer(customerId: number) {
this.customer = null;
let path = import.meta.env.VITE_BASE_URL + '/customer/' + customerId;
axios.get(path, { headers: authHeader() })
.then((response: any) => { this.customer = response.data; })
.catch((error: any) => { console.error("Failed to fetch customer details for modal:", error); });
},
getServiceParts(customerId: number) {
this.isLoadingParts = true;
this.serviceParts = null;
let path = `${import.meta.env.VITE_BASE_URL}/service/parts/customer/${customerId}`;
axios.get(path, { headers: authHeader() })
.then((response: any) => { this.serviceParts = response.data; })
.catch((error: any) => { console.error("Failed to fetch service parts:", error); })
.finally(() => { this.isLoadingParts = false; });
},
async saveChanges() {
const date = this.editableService.date;
const time = this.editableService.time || 0;
const combinedDateTime = dayjs(`${date} ${time}:00`).format('YYYY-MM-DDTHH:mm:ss');
const finalPayload = { ...this.service, ...this.editableService, 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 });
this.$emit('save-changes', finalPayload);
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
},
confirmDelete() {
if (this.service.id && window.confirm(`Are you sure you want to delete this service call?`)) {
this.$emit('delete-service', this.service.id);
}
},
getServiceTypeName(typeId: number | undefined | null): string {
if (typeId === undefined || typeId === null) return 'Unknown';
const typeMap: { [key: number]: string } = { 0: 'Tune-up', 1: 'No Heat', 2: 'Fix', 3: 'Tank Install', 4: 'Other' };
return typeMap[typeId] || 'Unknown';
},
getServiceTypeColor(typeId: number | undefined | null): string {
if (typeId === undefined || typeId === null) return 'gray';
const colorMap: { [key: number]: string } = { 0: 'blue', 1: 'red', 2: 'green', 3: '#B58900', 4: 'black' };
return colorMap[typeId] || 'gray';
},
getStateAbbrev(stateId: number | undefined | null): string {
if (stateId === undefined || stateId === null) return 'Unknown';
const stateMap: { [key: number]: string } = { 0: 'MA', 1: 'RI', 2: 'NH', 3: 'ME', 4: 'VT', 5: 'CT', 6: 'NY' };
return stateMap[stateId] || 'Unknown';
}
},
});
// Props and Emits
const props = defineProps<{
service: Partial<ServiceCall>
}>()
const emit = defineEmits<{
'close-modal': []
'save-changes': [service: ServiceCall]
'delete-service': [serviceId: number]
}>()
// Reactive data
const editableService = ref({} as Partial<EditableService>)
const customer = ref(null as Customer | null)
const serviceParts = ref(null as ServiceParts | null)
const isLoadingParts = ref(true)
const serviceOptions = ref([
{ text: 'Tune-up', value: 0 }, { text: 'No Heat', value: 1 }, { text: 'Fix', value: 2 },
{ 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
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; })
.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; })
.catch((error: any) => { console.error("Failed to fetch service parts:", error); })
.finally(() => { isLoadingParts.value = false; });
}
const saveChanges = async () => {
const date = editableService.value.date;
const time = editableService.value.time || 0;
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 });
emit('save-changes', finalPayload as ServiceCall);
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
}
const confirmDelete = () => {
if (props.service.id && window.confirm(`Are you sure you want to delete this service call?`)) {
emit('delete-service', props.service.id);
}
}
const getServiceTypeName = (typeId: number | undefined | null): string => {
if (typeId === undefined || typeId === null) return 'Unknown';
const typeMap: { [key: number]: string } = { 0: 'Tune-up', 1: 'No Heat', 2: 'Fix', 3: 'Tank Install', 4: 'Other' };
return typeMap[typeId] || 'Unknown';
}
const getServiceTypeColor = (typeId: number | undefined | null): string => {
if (typeId === undefined || typeId === null) return 'gray';
const colorMap: { [key: number]: string } = { 0: 'blue', 1: 'red', 2: 'green', 3: '#B58900', 4: 'black' };
return colorMap[typeId] || 'gray';
}
const getStateAbbrev = (stateId: number | undefined | null): string => {
if (stateId === undefined || stateId === null) return 'Unknown';
const stateMap: { [key: number]: string } = { 0: 'MA', 1: 'RI', 2: 'NH', 3: 'ME', 4: 'VT', 5: 'CT', 6: 'NY' };
return stateMap[stateId] || 'Unknown';
}
</script>

View File

@@ -156,187 +156,176 @@
@delete-service="handleDeleteService"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import authHeader from '../../services/auth.header'
import { ServiceCall } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue'
import ServiceEditModal from './ServiceEditModal.vue'
import dayjs from 'dayjs';
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;
payment_status?: number;
// Reactive data
const user = ref(null)
const services = ref<ServiceCall[]>([])
const isLoading = ref(true)
const selectedServiceForEdit = ref<ServiceCall | null>(null)
// --- ADDITIONS FOR TRUNCATION ---
const wordLimit = ref(50)
const expandedIds = ref<number[]>([])
// Lifecycle
onMounted(() => {
userStatus();
fetchUpcomingServices();
})
// Functions
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,
});
services.value = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch upcoming service calls:", error);
} finally {
isLoading.value = false;
}
}
export default defineComponent({
name: 'ServiceHome',
components: { Footer, ServiceEditModal },
data() {
return {
user: null,
services: [] as ServiceCall[],
isLoading: true,
selectedServiceForEdit: null as ServiceCall | null,
// --- ADDITIONS FOR TRUNCATION ---
wordLimit: 50,
expandedIds: [] as number[],
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;
}
})
.catch(() => {
user.value = null
})
}
const openEditModal = (service: ServiceCall) => {
selectedServiceForEdit.value = service;
}
const closeEditModal = () => {
selectedServiceForEdit.value = null;
}
const isLongDescription = (text: string): boolean => {
if (!text) return false;
return text.split(/\s+/).length > wordLimit.value;
}
const truncateDescription = (text: string): string => {
if (!isLongDescription(text)) return text;
const words = text.split(/\s+/);
return words.slice(0, wordLimit.value).join(' ') + '...';
}
const isExpanded = (id: number): boolean => {
return expandedIds.value.includes(id);
}
const toggleExpand = (id: number): void => {
const index = expandedIds.value.indexOf(id);
if (index === -1) {
expandedIds.value.push(id);
} else {
expandedIds.value.splice(index, 1);
}
}
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 index = services.value.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
services.value[index] = response.data.service;
}
closeEditModal();
}
},
created() {
this.userStatus();
this.fetchUpcomingServices();
},
methods: {
async fetchUpcomingServices(): Promise<void> {
this.isLoading = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/upcoming';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
this.services = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch upcoming service calls:", error);
} finally {
this.isLoading = false;
}
},
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
})
},
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
}
openEditModal(service: ServiceCall) {
this.selectedServiceForEdit = service;
},
closeEditModal() {
this.selectedServiceForEdit = null;
},
isLongDescription(text: string): boolean {
if (!text) return false;
return text.split(/\s+/).length > this.wordLimit;
},
truncateDescription(text: string): string {
if (!this.isLongDescription(text)) return text;
const words = text.split(/\s+/);
return words.slice(0, this.wordLimit).join(' ') + '...';
},
isExpanded(id: number): boolean {
return this.expandedIds.includes(id);
},
toggleExpand(id: number): void {
const index = this.expandedIds.indexOf(id);
if (index === -1) {
this.expandedIds.push(id);
} else {
this.expandedIds.splice(index, 1);
}
},
async handleSaveChanges(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 index = this.services.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
this.services[index] = response.data.service;
}
this.closeEditModal();
}
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
},
async handleDeleteService(serviceId: number) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
this.services = this.services.filter(s => s.id !== serviceId);
this.closeEditModal();
}
} catch (error) {
console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
},
formatDate(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
},
formatTime(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
},
getServiceTypeName(typeId: number): string {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
},
// --- ADD THIS METHOD ---
formatCurrency(value: string | number): string {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
},
getServiceTypeColor(typeId: number): string {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
},
shouldShowChargeButton(service: any): boolean {
return service.payment_status === null || service.payment_status === undefined;
},
shouldShowCaptureButton(service: any): boolean {
return service.payment_status === 1;
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 });
if (response.data.ok) {
services.value = services.value.filter(s => s.id !== serviceId);
closeEditModal();
}
},
})
} catch (error) {
console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
}
const formatDate = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
}
const formatTime = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
}
const getServiceTypeName = (typeId: number): string => {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
}
const formatCurrency = (value: string | number): string => {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
}
const getServiceTypeColor = (typeId: number): string => {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
}
const shouldShowChargeButton = (service: any): boolean => {
return service.payment_status === null || service.payment_status === undefined;
}
const shouldShowCaptureButton = (service: any): boolean => {
return service.payment_status === 1;
}
</script>

View File

@@ -163,193 +163,176 @@
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import authHeader from '../../services/auth.header'
import { ServiceCall } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue'
import ServiceEditModal from './ServiceEditModal.vue'
import dayjs from 'dayjs';
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;
payment_status?: number;
// Reactive data
const user = ref(null)
const services = ref<ServiceCall[]>([])
const isLoading = ref(true)
const selectedServiceForEdit = ref<ServiceCall | null>(null)
// --- ADDITIONS FOR TRUNCATION ---
const wordLimit = ref(50)
const expandedIds = ref<number[]>([])
// Lifecycle
onMounted(() => {
userStatus();
fetchPastServices();
})
// Functions
const isLongDescription = (text: string): boolean => {
if (!text) return false;
return text.split(/\s+/).length > wordLimit.value;
}
export default defineComponent({
name: 'ServiceHPast',
components: { Footer, ServiceEditModal },
data() {
return {
user: null,
services: [] as ServiceCall[],
isLoading: true,
selectedServiceForEdit: null as ServiceCall | null,
// --- ADDITIONS FOR TRUNCATION ---
wordLimit: 50,
expandedIds: [] as number[],
const truncateDescription = (text: string): string => {
if (!isLongDescription(text)) return text;
const words = text.split(/\s+/);
return words.slice(0, wordLimit.value).join(' ') + '...';
}
const isExpanded = (id: number): boolean => {
return expandedIds.value.includes(id);
}
const toggleExpand = (id: number): void => {
const index = expandedIds.value.indexOf(id);
if (index === -1) {
expandedIds.value.push(id);
} else {
expandedIds.value.splice(index, 1);
}
}
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,
});
services.value = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch past service calls:", error);
} finally {
isLoading.value = false;
}
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;
}
})
.catch(() => {
user.value = null
})
}
const openEditModal = (service: ServiceCall) => {
selectedServiceForEdit.value = service;
}
const closeEditModal = () => {
selectedServiceForEdit.value = null;
}
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 index = services.value.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
services.value[index] = response.data.service;
}
closeEditModal();
}
},
created() {
this.userStatus();
this.fetchPastServices();
},
methods: {
// --- NEW METHODS FOR TRUNCATION ---
isLongDescription(text: string): boolean {
if (!text) return false;
return text.split(/\s+/).length > this.wordLimit;
},
truncateDescription(text: string): string {
if (!this.isLongDescription(text)) return text;
const words = text.split(/\s+/);
return words.slice(0, this.wordLimit).join(' ') + '...';
},
isExpanded(id: number): boolean {
return this.expandedIds.includes(id);
},
toggleExpand(id: number): void {
const index = this.expandedIds.indexOf(id);
if (index === -1) {
this.expandedIds.push(id);
} else {
this.expandedIds.splice(index, 1);
}
},
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
}
// --- API and Data Handling Methods ---
async fetchPastServices(): Promise<void> {
this.isLoading = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/past';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
this.services = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch past service calls:", error);
} finally {
this.isLoading = false;
}
},
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
})
},
openEditModal(service: ServiceCall) {
this.selectedServiceForEdit = service;
},
closeEditModal() {
this.selectedServiceForEdit = null;
},
async handleSaveChanges(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 index = this.services.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
this.services[index] = response.data.service;
}
this.closeEditModal();
}
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
},
async handleDeleteService(serviceId: number) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
this.services = this.services.filter(s => s.id !== serviceId);
this.closeEditModal();
}
} catch (error) {
console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
},
// --- Formatting and Display Methods ---
formatCurrency(value: string | number): string {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
},
formatDate(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
},
formatTime(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
},
getServiceTypeName(typeId: number): string {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank_Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
},
getServiceTypeColor(typeId: number): string {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
},
shouldShowChargeButton(service: any): boolean {
return service.payment_status === null || service.payment_status === undefined;
},
shouldShowCaptureButton(service: any): boolean {
return service.payment_status === 1;
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 });
if (response.data.ok) {
services.value = services.value.filter(s => s.id !== serviceId);
closeEditModal();
}
},
})
} catch (error) {
console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
}
const formatCurrency = (value: string | number): string => {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
}
const formatDate = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
}
const formatTime = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
}
const getServiceTypeName = (typeId: number): string => {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank_Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
}
const getServiceTypeColor = (typeId: number): string => {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
}
const shouldShowChargeButton = (service: any): boolean => {
return service.payment_status === null || service.payment_status === undefined;
}
const shouldShowCaptureButton = (service: any): boolean => {
return service.payment_status === 1;
}
</script>

View File

@@ -123,124 +123,109 @@
<Footer />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import authHeader from '../../services/auth.header'
import { ServicePlan } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue'
import dayjs from 'dayjs';
interface ServicePlan {
id: number;
customer_id: number;
customer_name: string;
customer_address: string;
customer_town: string;
contract_plan: number;
contract_years: number;
contract_start_date: string;
// Reactive data
const user = ref(null)
const servicePlans = ref<ServicePlan[]>([])
const isLoading = ref(true)
// Lifecycle
onMounted(() => {
userStatus();
fetchServicePlans();
})
// Functions
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,
});
servicePlans.value = response.data;
} catch (error) {
console.error("Failed to fetch service plans:", error);
} finally {
isLoading.value = false;
}
}
export default defineComponent({
name: 'ServicePlans',
components: { Footer },
data() {
return {
user: null,
servicePlans: [] as ServicePlan[],
isLoading: true,
}
},
created() {
this.userStatus();
this.fetchServicePlans();
},
methods: {
async fetchServicePlans(): Promise<void> {
this.isLoading = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/plans/active';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
this.servicePlans = response.data;
} catch (error) {
console.error("Failed to fetch service plans:", error);
} finally {
this.isLoading = false;
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;
}
},
})
.catch(() => {
user.value = null
})
}
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
})
},
const getPlanName = (planType: number): string => {
const planNames: { [key: number]: string } = {
1: 'Standard',
2: 'Premium'
};
return planNames[planType] || 'Unknown';
}
getPlanName(planType: number): string {
const planNames: { [key: number]: string } = {
1: 'Standard',
2: 'Premium'
};
return planNames[planType] || 'Unknown';
},
const getPlanColor = (planType: number): string => {
const planColors: { [key: number]: string } = {
1: 'blue',
2: 'gold'
};
return planColors[planType] || 'gray';
}
getPlanColor(planType: number): string {
const planColors: { [key: number]: string } = {
1: 'blue',
2: 'gold'
};
return planColors[planType] || 'gray';
},
const formatDate = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMM D, YYYY');
}
formatDate(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMM D, YYYY');
},
const formatEndDate = (startDate: string, years: number): string => {
if (!startDate) return 'N/A';
return dayjs(startDate).add(years, 'year').format('MMM D, YYYY');
}
formatEndDate(startDate: string, years: number): string {
if (!startDate) return 'N/A';
return dayjs(startDate).add(years, 'year').format('MMM D, YYYY');
},
const getStatusText = (startDate: string, years: number): string => {
if (!startDate) return 'Unknown';
const endDate = dayjs(startDate).add(years, 'year');
const now = dayjs();
if (now.isAfter(endDate)) {
return 'Expired';
} else if (now.isAfter(endDate.subtract(30, 'day'))) {
return 'Expiring Soon';
} else {
return 'Active';
}
}
getStatusText(startDate: string, years: number): string {
if (!startDate) return 'Unknown';
const endDate = dayjs(startDate).add(years, 'year');
const now = dayjs();
if (now.isAfter(endDate)) {
return 'Expired';
} else if (now.isAfter(endDate.subtract(30, 'day'))) {
return 'Expiring Soon';
} else {
return 'Active';
}
},
getStatusBadge(startDate: string, years: number): string {
if (!startDate) return 'badge-ghost';
const endDate = dayjs(startDate).add(years, 'year');
const now = dayjs();
if (now.isAfter(endDate)) {
return 'badge-error';
} else if (now.isAfter(endDate.subtract(30, 'day'))) {
return 'badge-warning';
} else {
return 'badge-success';
}
}
},
})
const getStatusBadge = (startDate: string, years: number): string => {
if (!startDate) return 'badge-ghost';
const endDate = dayjs(startDate).add(years, 'year');
const now = dayjs();
if (now.isAfter(endDate)) {
return 'badge-error';
} else if (now.isAfter(endDate.subtract(30, 'day'))) {
return 'badge-warning';
} else {
return 'badge-success';
}
}
</script>

View File

@@ -163,193 +163,176 @@
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import authHeader from '../../services/auth.header'
import { ServiceCall } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue'
import ServiceEditModal from './ServiceEditModal.vue'
import dayjs from 'dayjs';
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;
payment_status?: number;
// Reactive data
const user = ref(null)
const services = ref<ServiceCall[]>([])
const isLoading = ref(true)
const selectedServiceForEdit = ref<ServiceCall | null>(null)
// --- ADDITIONS FOR TRUNCATION ---
const wordLimit = ref(50)
const expandedIds = ref<number[]>([])
// Lifecycle
onMounted(() => {
userStatus();
fetchTodayServices();
})
// Functions
const isLongDescription = (text: string): boolean => {
if (!text) return false;
return text.split(/\s+/).length > wordLimit.value;
}
export default defineComponent({
name: 'ServiceToday',
components: { Footer, ServiceEditModal },
data() {
return {
user: null,
services: [] as ServiceCall[],
isLoading: true,
selectedServiceForEdit: null as ServiceCall | null,
// --- ADDITIONS FOR TRUNCATION ---
wordLimit: 50,
expandedIds: [] as number[],
const truncateDescription = (text: string): string => {
if (!isLongDescription(text)) return text;
const words = text.split(/\s+/);
return words.slice(0, wordLimit.value).join(' ') + '...';
}
const isExpanded = (id: number): boolean => {
return expandedIds.value.includes(id);
}
const toggleExpand = (id: number): void => {
const index = expandedIds.value.indexOf(id);
if (index === -1) {
expandedIds.value.push(id);
} else {
expandedIds.value.splice(index, 1);
}
}
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,
});
services.value = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch today's service calls:", error);
} finally {
isLoading.value = false;
}
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;
}
})
.catch(() => {
user.value = null
})
}
const openEditModal = (service: ServiceCall) => {
selectedServiceForEdit.value = service;
}
const closeEditModal = () => {
selectedServiceForEdit.value = null;
}
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 index = services.value.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
services.value[index] = response.data.service;
}
closeEditModal();
}
},
created() {
this.userStatus();
this.fetchTodayServices();
},
methods: {
// --- NEW METHODS FOR TRUNCATION ---
isLongDescription(text: string): boolean {
if (!text) return false;
return text.split(/\s+/).length > this.wordLimit;
},
truncateDescription(text: string): string {
if (!this.isLongDescription(text)) return text;
const words = text.split(/\s+/);
return words.slice(0, this.wordLimit).join(' ') + '...';
},
isExpanded(id: number): boolean {
return this.expandedIds.includes(id);
},
toggleExpand(id: number): void {
const index = this.expandedIds.indexOf(id);
if (index === -1) {
this.expandedIds.push(id);
} else {
this.expandedIds.splice(index, 1);
}
},
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
}
// --- API and Data Handling Methods ---
async fetchTodayServices(): Promise<void> {
this.isLoading = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/today';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
this.services = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch today's service calls:", error);
} finally {
this.isLoading = false;
}
},
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
})
},
openEditModal(service: ServiceCall) {
this.selectedServiceForEdit = service;
},
closeEditModal() {
this.selectedServiceForEdit = null;
},
async handleSaveChanges(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 index = this.services.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
this.services[index] = response.data.service;
}
this.closeEditModal();
}
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
},
async handleDeleteService(serviceId: number) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
this.services = this.services.filter(s => s.id !== serviceId);
this.closeEditModal();
}
} catch (error) {
console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
},
// --- Formatting and Display Methods ---
formatCurrency(value: string | number): string {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
},
formatDate(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
},
formatTime(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
},
getServiceTypeName(typeId: number): string {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank_Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
},
getServiceTypeColor(typeId: number): string {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
},
shouldShowChargeButton(service: any): boolean {
return service.payment_status === null || service.payment_status === undefined;
},
shouldShowCaptureButton(service: any): boolean {
return service.payment_status === 1;
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 });
if (response.data.ok) {
services.value = services.value.filter(s => s.id !== serviceId);
closeEditModal();
}
},
})
} catch (error) {
console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
}
const formatCurrency = (value: string | number): string => {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
}
const formatDate = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
}
const formatTime = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
}
const getServiceTypeName = (typeId: number): string => {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank_Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
}
const getServiceTypeColor = (typeId: number): string => {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
}
const shouldShowChargeButton = (service: any): boolean => {
return service.payment_status === null || service.payment_status === undefined;
}
const shouldShowCaptureButton = (service: any): boolean => {
return service.payment_status === 1;
}
</script>

View File

@@ -55,156 +55,152 @@
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import Header from '../../../layouts/headers/headerauth.vue';
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import { CalendarOptions, EventClickArg } from '@fullcalendar/core';
import EventSidebar from './EventSidebar.vue';
import ServiceEditModal from '../../service/ServiceEditModal.vue';
import ServiceEditModal from '../../service/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;}
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; }
export default defineComponent({
name: 'CalendarCustomer',
components: { Header, FullCalendar, EventSidebar, ServiceEditModal },
data() {
return {
isLoading: false,
selectedServiceForEdit: null as Partial<ServiceCall> | null,
calendarOptions: {
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
weekends: true,
events: [] as any[],
eventClick: this.handleEventClick,
} as CalendarOptions,
customer: null as Customer | null,
};
},
watch: {
'$route.params.id': {
handler(newId) {
if (newId) this.getCustomer(newId as string);
},
immediate: true,
},
},
created() {
this.fetchEvents();
},
methods: {
// --- THIS IS THE FIX ---
// The logic from ServiceCalendar.vue is now correctly applied here.
handleEventClick(clickInfo: EventClickArg): void {
const events = (this.calendarOptions.events as any[]) || [];
const originalEvent = events.find(e => e.id == clickInfo.event.id);
// Route
const route = useRoute();
if (originalEvent) {
// We "flatten" the nested object from the calendar into the simple,
// flat structure that the modal expects, ensuring customer_id is included.
this.selectedServiceForEdit = {
id: originalEvent.id,
scheduled_date: originalEvent.start,
customer_id: originalEvent.customer_id, // This was the missing piece
customer_name: originalEvent.title.split(': ')[1] || 'Unknown Customer',
type_service_call: originalEvent.extendedProps.type_service_call,
description: originalEvent.extendedProps.description,
service_cost: originalEvent.extendedProps.description,
customer_address: '',
customer_town: '',
};
}
},
// Functions declared first (needed for calendarOptions)
const handleEventClick = (clickInfo: EventClickArg): void => {
const events = (calendarOptions.value.events as any[]) || [];
const originalEvent = events.find(e => e.id == clickInfo.event.id);
closeEditModal() {
this.selectedServiceForEdit = null;
},
if (originalEvent) {
// We "flatten" the nested object from the calendar into the simple,
// flat structure that the modal expects, ensuring customer_id is included.
selectedServiceForEdit.value = {
id: originalEvent.id,
scheduled_date: originalEvent.start,
customer_id: originalEvent.customer_id, // This was the missing piece
customer_name: originalEvent.title.split(': ')[1] || 'Unknown Customer',
type_service_call: originalEvent.extendedProps.type_service_call,
description: originalEvent.extendedProps.description,
service_cost: originalEvent.extendedProps.description,
customer_address: '',
customer_town: '',
};
}
};
async handleSaveChanges(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 this.fetchEvents();
this.closeEditModal();
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
},
async handleDeleteService(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 this.fetchEvents();
this.closeEditModal();
} else {
console.error("Failed to delete event:", response.data.error);
}
} catch (error) {
console.error("Error deleting event:", error);
}
},
async getCustomer(customerId: string): Promise<void> {
this.isLoading = true;
this.customer = null;
try {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${customerId}`;
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
if (response.data && response.data.id) {
this.customer = response.data;
}
} catch (error) {
console.error("API call to get customer FAILED:", error);
} finally {
this.isLoading = false;
}
},
async fetchEvents(): Promise<void> {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/all`;
const response = await axios.get(path, { headers: authHeader(), withCredentials: true });
this.calendarOptions.events = response.data;
} catch (error) {
console.error("Error fetching all calendar events:", error);
}
},
async handleEventScheduled(eventData: any): Promise<void> {
if (!this.customer) {
alert("Error: A customer must be loaded in the sidebar to create a new event.");
return;
}
try {
const payload = {
expected_delivery_date: eventData.start, type_service_call: eventData.type_service_call,
customer_id: this.customer.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) {
await this.fetchEvents();
} else {
console.error("Failed to create event:", response.data.error);
}
} catch (error) {
console.error("Error creating event:", error);
}
},
async handleEventDelete(eventId: string): Promise<void> {
// This is a simple alias now, as handleDeleteService is more specific
await this.handleDeleteService(Number(eventId));
},
},
// Reactive data
const isLoading = ref(false);
const selectedServiceForEdit = ref<Partial<ServiceCall> | null>(null);
const calendarOptions = ref<CalendarOptions>({
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
weekends: true,
events: [] as any[],
eventClick: handleEventClick,
});
const customer = ref<Customer | null>(null);
// Watchers
watch(() => route.params.id, (newId) => {
if (newId) getCustomer(newId as string);
}, { immediate: true });
// Lifecycle
onMounted(() => {
fetchEvents();
});
// Functions
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() });
if (response.data && response.data.id) {
customer.value = response.data;
}
} catch (error) {
console.error("API call to get customer FAILED:", error);
} finally {
isLoading.value = false;
}
};
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 });
calendarOptions.value.events = response.data;
} catch (error) {
console.error("Error fetching all calendar events:", 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.");
return;
}
try {
const payload = {
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) {
await fetchEvents();
} else {
console.error("Failed to create event:", response.data.error);
}
} catch (error) {
console.error("Error creating event:", error);
}
};
const handleEventDelete = async (eventId: string): Promise<void> => {
// This is a simple alias now, as handleDeleteService is more specific
await handleDeleteService(Number(eventId));
};
</script>

View File

@@ -77,8 +77,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
<script setup lang="ts">
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
interface Customer {
@@ -94,80 +94,79 @@ interface Customer {
customer_apt: string;
}
export default defineComponent({
name: 'EventSidebar',
props: {
customer: {
type: Object as PropType<Customer | null>,
required: true,
},
},
data() {
return {
selectedService: '' as string | number,
serviceOptions: [
{ text: 'Tune-up', value: 0 },
{ text: 'No Heat', value: 1 },
{ text: 'Fix', value: 2 },
{ text: 'Tank Install', value: 3 },
{ text: 'Other', value: 4 },
],
event: {
title: '',
description: '',
date: dayjs().format('YYYY-MM-DD'),
endDate: '',
time: 12,
},
};
},
computed: {
customerStateName(): string {
if (!this.customer) return '';
const stateMap: { [key: number]: string } = {
0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire',
3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York',
};
return stateMap[this.customer.customer_state] || 'Unknown';
},
customerHomeType(): string {
if (!this.customer) return '';
const homeTypeMap: { [key: number]: string } = {
0: 'Residential', 1: 'Apartment', 2: 'Condo', 3: 'Commercial',
4: 'Business', 5: 'Construction', 6: 'Container',
};
return homeTypeMap[this.customer.customer_home_type] || 'Unknown';
}
},
methods: {
submitEvent() {
if (!this.customer) {
alert("Cannot submit: No customer data is loaded.");
return;
}
// Props
const props = defineProps<{
customer: Customer | null;
}>();
const startDateTime = dayjs(`${this.event.date} ${this.event.time}:00`).format('YYYY-MM-DDTHH:mm:ss');
const endDateTime = this.event.endDate ? dayjs(this.event.endDate).add(1, 'day').format('YYYY-MM-DD') : undefined;
// Emits
const emit = defineEmits<{
'event-scheduled': [eventData: any];
}>();
const eventPayload = {
title: this.event.title,
start: startDateTime,
type_service_call: this.selectedService,
end: endDateTime,
extendedProps: {
description: this.event.description,
},
};
this.$emit('event-scheduled', eventPayload);
this.event.title = '';
this.selectedService = '';
this.event.description = '';
this.event.endDate = '';
this.event.date = dayjs().format('YYYY-MM-DD');
this.event.time = 12;
},
},
// Reactive data
const selectedService = ref<string | number>('');
const serviceOptions = ref([
{ text: 'Tune-up', value: 0 },
{ text: 'No Heat', value: 1 },
{ text: 'Fix', value: 2 },
{ text: 'Tank Install', value: 3 },
{ text: 'Other', value: 4 },
]);
const event = ref({
title: '',
description: '',
date: dayjs().format('YYYY-MM-DD'),
endDate: '',
time: 12,
});
// Computed properties
const customerStateName = computed((): string => {
if (!props.customer) return '';
const stateMap: { [key: number]: string } = {
0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire',
3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York',
};
return stateMap[props.customer.customer_state] || 'Unknown';
});
const customerHomeType = computed((): string => {
if (!props.customer) return '';
const homeTypeMap: { [key: number]: string } = {
0: 'Residential', 1: 'Apartment', 2: 'Condo', 3: 'Commercial',
4: 'Business', 5: 'Construction', 6: 'Container',
};
return homeTypeMap[props.customer.customer_home_type] || 'Unknown';
});
// Functions
const submitEvent = () => {
if (!props.customer) {
alert("Cannot submit: No customer data is loaded.");
return;
}
const startDateTime = dayjs(`${event.value.date} ${event.value.time}:00`).format('YYYY-MM-DDTHH:mm:ss');
const endDateTime = event.value.endDate ? dayjs(event.value.endDate).add(1, 'day').format('YYYY-MM-DD') : undefined;
const eventPayload = {
title: event.value.title,
start: startDateTime,
type_service_call: selectedService.value,
end: endDateTime,
extendedProps: {
description: event.value.description,
},
};
emit('event-scheduled', eventPayload);
event.value.title = '';
selectedService.value = '';
event.value.description = '';
event.value.endDate = '';
event.value.date = dayjs().format('YYYY-MM-DD');
event.value.time = 12;
};
</script>

View File

@@ -1,10 +1,10 @@
// Import the new component at the top
import ServiceHome from './ServiceHome.vue'
import ServicePast from './ServicePast.vue'
import CalendarCustomer from './calender/CalendarCustomer.vue'
import ServiceCalendar from './ServiceCalendar.vue'
import ServiceToday from './ServiceToday.vue'
import ServicePlans from './ServicePlans.vue'
const ServiceHome = () => import('./ServiceHome.vue')
const ServicePast = () => import('./ServicePast.vue')
const CalendarCustomer = () => import('./calender/CalendarCustomer.vue')
const ServiceCalendar = () => import('./ServiceCalendar.vue')
const ServiceToday = () => import('./ServiceToday.vue')
const ServicePlans = () => import('./ServicePlans.vue')
const serviceRoutes = [
{