Added calender

This commit is contained in:
2025-08-21 17:53:39 -04:00
parent eeaf45defe
commit b74fd5d3a2
17 changed files with 984 additions and 65 deletions

View File

@@ -18,13 +18,12 @@
</li>
<div class="font-bold text-lg text-gray-500 pt-5 ">Customer</div>
<li class="text-white">
<router-link :to="{ name: 'customer' }">
<div class=" hover:underline py-1">All Customers</div>
</router-link>
</li>
<div class="font-bold text-lg text-gray-500 pt-5">Delivery</div>
<li class="text-white">
<router-link :to="{ name: 'delivery' }">
@@ -57,7 +56,6 @@
</div>
<div class=" hover:underline py-1" v-else>Waiting Deliveries </div>
</router-link>
<router-link :to="{ name: 'deliveryIssue' }">
<div class=" hover:underline py-1">Issue Tickets</div>
</router-link>
@@ -70,16 +68,17 @@
</div>
<div class=" hover:underline py-1" v-else>Pending Payment </div>
</router-link>
<router-link :to="{ name: 'deliveryFinalized' }">
<div class=" hover:underline py-1">Finalized Tickets</div>
</router-link>
</li>
<div class="font-bold text-lg text-gray-500 pt-5">Service</div>
<li class="text-white">
<router-link :to="{ name: 'ServiceHome' }">
<div class=" hover:underline py-1">Service Home</div>
</router-link>
</li>
<div class="font-bold text-lg text-gray-500 pt-5">Automatics</div>
@@ -95,13 +94,13 @@
</router-link>
</li>
<div class="font-bold text-lg text-gray-500 pt-5">Employees</div>
<li class="text-white">
<router-link :to="{ name: 'employee' }">
<div class=" hover:underline py-1">Employees</div>
</router-link>
</li>
<div class="font-bold text-lg text-gray-500 pt-5">Admin</div>
<li class="text-white">
<router-link :to="{ name: 'oilprice' }">

View File

@@ -68,18 +68,19 @@
<td class="flex gap-5 ">
<router-link :to="{ name: 'deliveryCreate', params: { id: person['id'] } }"
class="btn-sm btn bg-orange-600 text-white">
Create Delivery
class="btn-sm btn bg-orange-600 text-white">
Create Delivery
</router-link>
<router-link :to="{ name: 'CalenderCustomer', params: { id: person['id'] } }"
class="btn-sm btn bg-orange-600 text-white">
Create Service Call
</router-link>
<router-link :to="{ name: 'customerEdit', params: { id: person['id'] } }" class="btn-sm btn btn-secondary">
Edit Customer
Edit Customer
</router-link>
<router-link :to="{ name: 'cardadd', params: { id: person['id'] } }">
<button class="btn btn-sm btn-secondary text-white ">Add CreditCard</button>
</router-link>
<router-link :to="{ name: 'customerProfile', params: { id: person['id'] } }"
class="btn btn-secondary btn-sm">
View Profile
class="btn btn-secondary btn-sm">
View Profile
</router-link>
</td>

View File

@@ -47,6 +47,12 @@
Create Delivery
</router-link>
<router-link :to="{ name: 'CalenderCustomer', params: { id: customer.id } }"
class="btn-sm btn bg-orange-600 text-white">
Create Service Call
</router-link>
<router-link :to="{ name: 'customerEdit', params: { id: customer.id } }"
class="btn-sm btn btn-secondary">
Edit Customer

View File

@@ -147,9 +147,7 @@
<div v-else>
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 py-5">
No Cards on File!
<a @click.prevent="test()" class="cursor-pointer underline hover:text-blue-300">
Edit Card
</a>
<!-- <router-link :to="{ name: 'cardadd', params: { id: customer.id } }">
<button class="btn btn-sm bg-blue-700 text-white">Add Credit Card</button>
</router-link> -->
@@ -506,9 +504,7 @@ export default defineComponent({
});
});
},
test() {
this.CreateOilOrderForm.basicInfo.gallons_ordered = '100'
},
getCustomerDelivery(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/customer/' + userid + '/1';
axios({

View File

@@ -665,7 +665,7 @@ export default defineComponent({
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok === 'true') {
if (response.data.ok === true) {
this.updatestatus()
this.$router.push({ name: "deliveryOrder", params: { id: this.deliveryOrder.id } });
}

View File

@@ -0,0 +1,157 @@
<template>
<!-- Modal Overlay -->
<div class="fixed inset-0 bg-gray-800 bg-opacity-75 flex items-center justify-center z-50">
<!-- Modal Content -->
<div class="relative bg-white p-6 rounded-lg shadow-xl w-full max-w-lg">
<!-- Modal Header -->
<div class="flex justify-between items-center border-b pb-3 mb-4">
<h3 class="text-2xl font-bold text-gray-800">Edit Service Call</h3>
<span class="font-bold text-white px-3 py-1 mr-10 rounded" :style="{ backgroundColor: getServiceTypeColor(editableService.type_service_call) }">
{{ getServiceTypeName(editableService.type_service_call) }}
</span>
</div>
<button
@click="$emit('close-modal')"
type="button"
class="absolute top-0 right-0 mt-4 mr-4 text-gray-400 hover:text-gray-600 focus:outline-none"
aria-label="Close modal"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<!-- Form for Editing -->
<form @submit.prevent="saveChanges">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Scheduled Date -->
<div class="mb-4">
<label for="edit-date" class="block text-sm font-medium text-gray-700">Scheduled Date</label>
<input type="date" id="edit-date" v-model="editableService.scheduled_date" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-black">
</div>
<!-- Service Type -->
<div class="mb-4">
<label for="edit-service-type" class="block text-sm font-medium text-gray-700">Type of Service</label>
<select id="edit-service-type" v-model.number="editableService.type_service_call" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-black">
<option v-for="option in serviceOptions" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
</div>
</div>
<!-- Description -->
<div class="mb-4">
<label for="edit-description" class="block text-sm font-medium text-gray-700">Description</label>
<!-- ====================================================== -->
<!-- ============== THIS LINE HAS BEEN UPDATED ============== -->
<!-- ====================================================== -->
<textarea
id="edit-description"
v-model="editableService.description"
rows="4"
required
class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm text-black focus:border-indigo-500 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
></textarea>
<!-- ====================================================== -->
<!-- ================ END OF UPDATED LINE ================ -->
<!-- ====================================================== -->
</div>
<!-- Action Buttons -->
<div class="mt-6 flex justify-between items-center">
<button @click.prevent="confirmDelete" type="button" class="px-4 py-2 bg-red-600 text-white font-medium rounded-md shadow-sm hover:bg-red-700">
Delete Call
</button>
<div class="flex space-x-3">
<button @click.prevent="$emit('close-modal')" type="button" class="px-4 py-2 bg-gray-200 text-gray-800 font-medium rounded-md shadow-sm hover:bg-gray-300">
Cancel
</button>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white font-medium rounded-md shadow-sm hover:bg-blue-700">
Save Changes
</button>
</div>
</div>
</form>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
interface ServiceCall {
id: number;
scheduled_date: string;
customer_name: string;
customer_address: string;
customer_town: string;
type_service_call: number;
description: string;
}
export default defineComponent({
name: 'ServiceEditModal',
props: {
service: {
type: Object as PropType<ServiceCall>,
required: true,
},
},
data() {
return {
editableService: {} as Partial<ServiceCall>,
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) {
this.editableService = JSON.parse(JSON.stringify(newVal));
if (this.editableService.scheduled_date) {
this.editableService.scheduled_date = this.editableService.scheduled_date.split('T')[0];
}
},
immediate: true,
deep: true,
},
},
methods: {
saveChanges() {
this.$emit('save-changes', this.editableService);
},
confirmDelete() {
if (window.confirm(`Are you sure you want to delete this service call for "${this.service.customer_name}"?`)) {
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';
}
},
});
</script>

View File

@@ -0,0 +1,205 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs mb-10">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Service Calls</li>
</ul>
</div>
<div class="flex text-2xl mb-5 font-bold">
Upcoming Service Calls
</div>
<div v-if="isLoading" class="text-center p-10">
<p>Loading upcoming service calls...</p>
</div>
<div v-else-if="services.length === 0" class="text-center p-10 bg-gray-100 rounded-md">
<p>No upcoming service calls found.</p>
</div>
<div v-else class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class=" bg-neutral">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Scheduled Date</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Customer Name</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Address</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Service Type</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
</tr>
</thead>
<tbody class=" divide-y bg-neutral">
<tr v-for="service in services" :key="service.id" @click="openEditModal(service)" class="hover:bg-blue-600 hover:text-black cursor-pointer">
<td class="px-6 py-4 whitespace-nowrap">{{ formatDate(service.scheduled_date) }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ service.customer_name }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ service.customer_address }}, {{ service.customer_town }}</td>
<td class="px-6 py-4 whitespace-nowrap font-medium" :style="{ color: getServiceTypeColor(service.type_service_call) }">
{{ getServiceTypeName(service.type_service_call) }}
</td>
<td class="px-6 py-4 whitespace-normal text-sm text-gray-500">{{ service.description }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<Footer />
<ServiceEditModal
v-if="selectedServiceForEdit"
:service="selectedServiceForEdit"
@close-modal="closeEditModal"
@save-changes="handleSaveChanges"
@delete-service="handleDeleteService"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import axios from 'axios';
import authHeader from '../../services/auth.header';
import Header from '../../layouts/headers/headerauth.vue';
import SideBar from '../../layouts/sidebar/sidebar.vue';
import Footer from '../../layouts/footers/footer.vue';
import ServiceEditModal from './ServiceEditModal.vue';
interface ServiceCall {
id: number;
scheduled_date: string;
customer_name: string;
customer_address: string;
customer_town: string;
type_service_call: number;
description: string;
}
export default defineComponent({
name: 'ServiceHome',
components: { Header, SideBar, Footer, ServiceEditModal },
data() {
return {
user: null,
services: [] as ServiceCall[],
isLoading: true,
selectedServiceForEdit: null as ServiceCall | null,
};
},
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;
} 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
})
},
// --- HELPER METHODS WITH IMPLEMENTATIONS RESTORED ---
formatDate(dateString: string): string {
const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' };
// Adding a timeZone option helps prevent off-by-one-day errors
return new Date(dateString).toLocaleDateString(undefined, { ...options, timeZone: 'UTC' });
},
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', // A darker yellow for text
4: 'black',
};
return colorMap[typeId] || 'gray';
},
// --- MODAL MANAGEMENT METHODS ---
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.");
}
},
},
});
</script>

View File

@@ -0,0 +1,155 @@
<template>
<Header />
<div class="flex">
<div class="w-full px-10">
<div class="text-sm breadcrumbs mb-4">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li><router-link :to="{ name: 'customer' }">Customers</router-link></li>
<li v-if="customer">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</li>
</ul>
</div>
<div class="flex h-screen font-sans">
<div v-if="isLoading" class="w-1/4 p-4 border-r">
<h2 class="text-xl font-bold">Loading Customer...</h2>
</div>
<EventSidebar v-else-if="customer" :customer="customer" @event-scheduled="handleEventScheduled" />
<div v-else class="w-1/4 p-4 border-r">
<h2 class="text-xl font-bold text-red-500">Error</h2>
<p>Could not load customer data. You can still view the master calendar.</p>
</div>
<div class="flex-1 p-4 overflow-auto">
<FullCalendar ref="fullCalendar" :options="calendarOptions" />
</div>
<EventModal v-if="selectedEvent" :event="selectedEvent" @close-modal="selectedEvent = null" @delete-event="handleEventDelete" />
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import Header from '../../../layouts/headers/headerauth.vue';
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import { CalendarOptions, EventApi, EventClickArg } from '@fullcalendar/core';
import EventSidebar from './EventSidebar.vue';
import EventModal from './EventModal.vue';
import axios from 'axios';
import authHeader from '../../../services/auth.header'; // Assuming you have this service
// --- Interfaces (no changes) ---
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; }
interface EventExtendedProps { description: string; type_service_call: number; }
interface AppEvent { id?: string; title: string; start: string; end?: string; extendedProps: EventExtendedProps; }
export default defineComponent({
name: 'CalendarCustomer',
components: { Header, FullCalendar, EventSidebar, EventModal },
data() {
return {
isLoading: false,
selectedEvent: null as EventApi | null,
calendarOptions: {} as CalendarOptions,
customer: null as Customer | null,
};
},
watch: {
'$route.params.id': {
handler(newId) {
if (newId) this.getCustomer(newId as string);
},
immediate: true,
},
},
created() {
this.calendarOptions = {
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
weekends: true,
events: [],
eventClick: this.handleEventClick,
};
// --- KEY CHANGE: Fetch ALL events when the component is created ---
this.fetchEvents();
},
methods: {
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 });
if (response.data && response.data.id) {
this.customer = response.data;
// --- REMOVED: No longer need to fetch events from here ---
}
} catch (error) {
console.error("API call to get customer FAILED:", error);
} finally {
this.isLoading = false;
}
},
// --- KEY CHANGE: This method now fetches ALL events and is independent ---
async fetchEvents(): Promise<void> {
try {
console.log("fetchEvents: Fetching ALL events for the master calendar.");
// Call the new '/service/all' endpoint
const path = `${import.meta.env.VITE_BASE_URL}/service/all`;
const response = await axios.get(path, { headers: authHeader(), withCredentials: true });
console.log("fetchEvents: Received all events from API:", response.data);
this.calendarOptions.events = response.data;
} catch (error) {
console.error("Error fetching all calendar events:", error);
}
},
handleEventClick(clickInfo: EventClickArg): void {
this.selectedEvent = clickInfo.event;
},
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) {
// After creating a new event, refresh the entire master calendar
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> {
// ... (no changes needed in this method)
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${eventId}`;
const response = await axios.delete(path, { withCredentials: true, headers: authHeader() });
if (response.data.ok === true) {
const calendarApi = (this.$refs.fullCalendar as any).getApi();
const eventToRemove = calendarApi.getEventById(eventId);
if (eventToRemove) eventToRemove.remove();
this.selectedEvent = null;
}
} catch (error) { console.error("Error deleting event:", error); }
},
},
});
</script>

View File

@@ -0,0 +1,59 @@
<template>
<div class="fixed inset-0 bg-gray-800 bg-opacity-75 flex items-center justify-center z-50">
<div class="relative bg-white p-6 rounded-lg shadow-xl w-full max-w-md">
<div class="text-left">
<h3 class="text-2xl leading-6 font-bold text-gray-900 mb-2">{{ event?.title }}</h3>
<div class="mt-4">
<p class="text-sm text-gray-600 whitespace-pre-wrap">{{ event?.extendedProps.description }}</p>
<p class="text-sm text-gray-500 mt-4">
<strong>Start:</strong> {{ formatEventDate(event?.start) }}
</p>
<p v-if="event?.end" class="text-sm text-gray-500">
<strong>End:</strong> {{ formatEventDate(event?.end, true) }}
</p>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button @click="confirmDelete" class="px-4 py-2 bg-red-600 text-white text-base font-medium rounded-md shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Delete
</button>
<button @click="$emit('close-modal')" class="px-4 py-2 bg-gray-200 text-gray-800 text-base font-medium rounded-md shadow-sm hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400">
Close
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { EventApi } from '@fullcalendar/core';
import dayjs from 'dayjs';
export default defineComponent({
name: 'EventModal',
props: {
event: {
type: Object as PropType<EventApi | null>,
required: true,
},
},
methods: {
confirmDelete() {
if (this.event && window.confirm(`Are you sure you want to delete "${this.event.title}"?`)) {
this.$emit('delete-event', this.event.id);
}
},
formatEventDate(date: Date | string | null | undefined, isEndDate: boolean = false): string {
if (!date) return 'N/A';
let dateObj = dayjs(date);
if (isEndDate) {
dateObj = dateObj.subtract(1, 'day');
}
return dateObj.format('MMMM D, YYYY');
}
},
});
</script>

View File

@@ -0,0 +1,164 @@
<template>
<div class="w-1/4 p-4 border-r">
<h2 class="text-xl font-bold mb-4">Schedule Service</h2>
<form @submit.prevent="submitEvent">
<div class="mb-4">
<!-- CHANGED: Class updated to 'text-gray-200' for visibility on dark backgrounds -->
<label for="event-label" class="block text-sm font-medium text-gray-200">Calendar Label</label>
<input type="text" id="event-label" v-model="event.title" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-black">
</div>
<div class="mb-4">
<!-- CHANGED: Class updated to 'text-gray-200' for visibility on dark backgrounds -->
<label for="service_type" class="block text-sm font-medium text-gray-200">Type of Service</label>
<select class="select select-bordered select-sm w-full max-w-xs bg-white text-black" id="service_type" v-model="selectedService" required>
<option disabled value="">Please select one</option>
<option v-for="option in serviceOptions" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
</div>
<div class="mb-4">
<!-- CHANGED: Class updated to 'text-gray-200' for visibility on dark backgrounds -->
<label for="event-description" class="block text-sm font-medium text-gray-200">Description</label>
<textarea id="event-description" v-model="event.description" rows="3" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-black"></textarea>
</div>
<div class="mb-4">
<!-- CHANGED: Class updated to 'text-gray-200' for visibility on dark backgrounds -->
<label for="event-date" class="block text-sm font-medium text-gray-200">Day / Month</label>
<input type="date" id="event-date" v-model="event.date" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-black">
</div>
<div class="mb-4">
<!-- CHANGED: Class updated to 'text-gray-200' for visibility on dark backgrounds -->
<label for="event-time" class="block text-sm font-medium text-gray-200">Time (Hour)</label>
<select id="event-time" v-model="event.time" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-black">
<option v-for="hour in 24" :key="hour" :value="hour - 1">{{ (hour - 1).toString().padStart(2, '0') }}:00</option>
</select>
</div>
<div class="mb-4">
<!-- CHANGED: Class updated to 'text-gray-200' for visibility on dark backgrounds -->
<label for="event-end-date" class="block text-sm font-medium text-gray-200">End Date (Optional for multi-day)</label>
<input type="date" id="event-end-date" v-model="event.endDate" :min="event.date" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-black">
</div>
<button type="submit" class="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700">
Add Event
</button>
</form>
<div v-if="customer" class="mt-10 border-t pt-4">
<div class="font-bold text-lg">
{{ customer.customer_first_name }} {{ customer.customer_last_name }}
</div>
<div>{{ customer.customer_address }}</div>
<div v-if="customer.customer_apt">{{ customer.customer_apt }}</div>
<div>
<span>{{ customer.customer_town }},</span>
<span class="pl-1">{{ customerStateName }}</span>
<span class="pl-1">{{ customer.customer_zip }}</span>
</div>
<div>{{ customer.customer_phone_number }}</div>
<div class="text-sm text-gray-500 mt-2">{{ customerHomeType }}</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import dayjs from 'dayjs';
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: '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;
}
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;
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;
},
},
});
</script>

View File

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

View File

@@ -0,0 +1,24 @@
import ServiceHome from './ServiceHome.vue' // Adjust the import path
import CalendarCustomer from './calender/CalendarCustomer.vue'
const serviceRoutes = [
{
path: '/service',
name: 'ServiceHome',
component: ServiceHome
},
{
path: '/service/calender/:id',
name: 'CalenderCustomer',
component: CalendarCustomer,
},
]
export default serviceRoutes
//sourceMappingURL=index.ts.map

View File

@@ -12,7 +12,7 @@ import Error404 from '../pages/error/Error404.vue'
import adminRoutes from "../pages/admin/routes.ts";
import tickerRoutes from "../pages/ticket/routes.ts";
import moneyRoutes from "../pages/money/routes.ts";
import serviceRoutes from "../pages/service/routes.ts";
const routes = [
...moneyRoutes,
...cardRoutes,
@@ -24,6 +24,7 @@ const routes = [
...autoRoutes,
...adminRoutes,
...tickerRoutes,
...serviceRoutes,
{
path: '/',
name: 'home',

View File

@@ -0,0 +1,3 @@
declare module '@fullcalendar/interaction';
declare module '@fullcalendar/daygrid';
declare module '@fullcalendar/vue3';

5
src/vite-env.d.ts vendored
View File

@@ -1 +1,6 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}