major claude changes
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user