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

194
package-lock.json generated
View File

@@ -8,11 +8,15 @@
"name": "eamco_frontend_v2",
"version": "0.0.0",
"dependencies": {
"@fullcalendar/daygrid": "^6.1.19",
"@fullcalendar/interaction": "^6.1.19",
"@fullcalendar/vue3": "^6.1.19",
"@kyvg/vue3-notification": "^3.1.0",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^10.7.0",
"axios": "^1.6.2",
"axios": "^1.11.0",
"dayjs": "^1.11.13",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"moment": "^2.30.1",
@@ -469,6 +473,40 @@
"resolved": "https://registry.npmjs.org/@foliojs-fork/restructure/-/restructure-2.0.2.tgz",
"integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA=="
},
"node_modules/@fullcalendar/core": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.19.tgz",
"integrity": "sha512-z0aVlO5e4Wah6p6mouM0UEqtRf1MZZPt4mwzEyU6kusaNL+dlWQgAasF2cK23hwT4cmxkEmr4inULXgpyeExdQ==",
"peer": true,
"dependencies": {
"preact": "~10.12.1"
}
},
"node_modules/@fullcalendar/daygrid": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.19.tgz",
"integrity": "sha512-IAAfnMICnVWPjpT4zi87i3FEw0xxSza0avqY/HedKEz+l5MTBYvCDPOWDATpzXoLut3aACsjktIyw9thvIcRYQ==",
"peerDependencies": {
"@fullcalendar/core": "~6.1.19"
}
},
"node_modules/@fullcalendar/interaction": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.19.tgz",
"integrity": "sha512-GOciy79xe8JMVp+1evAU3ytdwN/7tv35t5i1vFkifiuWcQMLC/JnLg/RA2s4sYmQwoYhTw/p4GLcP0gO5B3X5w==",
"peerDependencies": {
"@fullcalendar/core": "~6.1.19"
}
},
"node_modules/@fullcalendar/vue3": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/@fullcalendar/vue3/-/vue3-6.1.19.tgz",
"integrity": "sha512-j5eUSxx0xIy3ADljo0f5B9PhjqXnCQ+7nUMPfsslc2eGVjp4F74YvY3dyd6OBbg13IvpsjowkjncGipYMQWmTA==",
"peerDependencies": {
"@fullcalendar/core": "~6.1.19",
"vue": "^3.0.11"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1204,12 +1242,12 @@
}
},
"node_modules/axios": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -1328,6 +1366,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@@ -1527,6 +1577,11 @@
"url": "https://opencollective.com/daisyui"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@@ -1609,6 +1664,19 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -1639,12 +1707,9 @@
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
@@ -1657,6 +1722,31 @@
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -1800,12 +1890,14 @@
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -1856,15 +1948,20 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -1873,6 +1970,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
@@ -1908,11 +2017,11 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1929,21 +2038,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
},
@@ -2200,6 +2298,14 @@
"node": ">=12"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/merge": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz",
@@ -2671,6 +2777,16 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"node_modules/preact": {
"version": "10.12.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",

View File

@@ -9,11 +9,15 @@
"preview": "vite preview"
},
"dependencies": {
"@fullcalendar/daygrid": "^6.1.19",
"@fullcalendar/interaction": "^6.1.19",
"@fullcalendar/vue3": "^6.1.19",
"@kyvg/vue3-notification": "^3.1.0",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^10.7.0",
"axios": "^1.6.2",
"axios": "^1.11.0",
"dayjs": "^1.11.13",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"moment": "^2.30.1",

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;
}