Major Refactor

This commit is contained in:
2025-09-01 16:42:44 -04:00
parent 76cbca94e3
commit 992a1a217d
69 changed files with 12683 additions and 8082 deletions

7462
package-lock.json generated Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"moment": "^2.30.1",
"pinia": "^2.1.7",
"pinia": "^2.3.1",
"v-pagination-3": "^0.1.7",
"vue": "^3.3.11",
"vue-debounce": "^5.0.0",

View File

@@ -1,21 +1,44 @@
<script setup lang="ts">
</script>
<!-- App.vue -->
<template>
<!--
This is the new main layout for your entire application.
- `drawer`: The base class for the component.
- `lg:drawer-open`: On large screens (lg), the drawer is permanently open, creating the desktop sidebar view.
-->
<div class="drawer lg:drawer-open">
<!-- The hidden checkbox that the hamburger button in the header will toggle -->
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<!-- DRAWER CONTENT: This is what's visible on the main page -->
<div class="drawer-content flex flex-col">
<!-- The Header now lives here, at the top of the content -->
<HeaderAuth />
<html class="">
<router-view />
<notifications position="top center" />
</html>
<!-- The Main Content Area for your 30 pages -->
<main class="flex-1 p-4 md:p-8 bg-base-200">
<!-- <router-view> renders the current page (e.g., deliveryEdit.vue) -->
<router-view />
</main>
<!-- Your global search results and notifications still live here -->
<SearchResults v-if="searchStore.showResults" />
<notifications position="top center" />
</div>
<!-- DRAWER SIDE: This is the sidebar that slides out on mobile -->
<div class="drawer-side">
<label for="my-drawer-2" class="drawer-overlay"></label>
<!-- The Sidebar component is now placed here -->
<SideBar />
</div>
</div>
</template>
<style lang="scss">
<script setup lang="ts">
import { useSearchStore } from './stores/search';
import HeaderAuth from './layouts/headers/headerauth.vue'; // Adjust path if needed
import SideBar from './layouts/sidebar/sidebar.vue'; // Adjust path if needed
import SearchResults from './components/SearchResults.vue'; // Adjust path if needed
#myfont {
font-family: Arial, Helvetica, sans-serif, sans-serif !important;
}
</style>
const searchStore = useSearchStore();
</script>

View File

@@ -0,0 +1,53 @@
<!-- SearchResults.vue -->
<template>
<div class="fixed top-16 left-1/2 -translate-x-1/2 w-full max-w-2xl px-4">
<div class="overflow-x-auto bg-base-100 rounded-lg shadow-2xl border border-gray-700">
<table class="table w-full">
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th>Phone</th>
</tr>
</thead>
<tbody>
<!-- It now reads `searchResults` directly from the store -->
<tr v-for="user in searchStore.searchResults" :key="user.id" class="hover cursor-pointer" @click="viewProfile(user.id)">
<td>
<div class="font-bold">{{ user.customer_first_name }} {{ user.customer_last_name }}</div>
</td>
<td>
<div>{{ user.customer_address }}</div>
<div class="text-sm opacity-70">{{ user.customer_town }}, {{ user.customer_state }}</div>
</td>
<td>{{ user.customer_phone_number }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
// 1. Import the store
import { useSearchStore } from '../stores/search'; // Adjust path if needed
export default defineComponent({
name: "SearchResults",
setup() {
// 2. Make the store available
const searchStore = useSearchStore();
return { searchStore };
},
methods: {
viewProfile(customerId: number) {
// When a user is clicked, navigate to their profile...
this.$router.push({ name: 'customerProfile', params: { id: customerId } });
// ...and clear the search so the overlay disappears.
this.searchStore.clearSearch();
}
}
});
</script>c

View File

@@ -1,36 +0,0 @@
<template>
<section >
<div v-for="user in customers" :key="user.id">
<router-link :to="{ name: 'customerProfile', params: { id: user['id'] } }">
<div class="grid grid-cols-12 bg-neutral pb-5 hover:bg-accent ">
<div class="col-span-12"> {{ user['customer_first_name'] }} {{ user['customer_last_name'] }}</div>
<div class="col-span-12">
{{ user['customer_address'] }} {{ user['customer_town'] }} {{user.state}}
</div>
<div class="col-span-12"> {{user['customer_phone_number']}}</div>
<div class="col-span-12"> {{user['account_number']}}</div>
</div>
</router-link>
<hr/>
</div>
</section>
</template>
<script lang="ts">
import {defineComponent} from "vue";
export default defineComponent({
name: "SearchResults",
props: ["customers"],
});
</script>

View File

@@ -1,271 +1,128 @@
<!-- headerauth.vue -->
<template>
<div class="navbar bg-primary border-b border-gray-700 sticky top-0 z-30">
<!-- Navbar Start Section -->
<div class="navbar-start">
<!-- Hamburger Menu Toggle for Mobile -->
<label for="my-drawer-2" class="btn btn-ghost btn-circle drawer-button lg:hidden">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" /></svg>
</label>
<div class="navbar bg-primary border-b border-bottom-500 border-gray-500">
<div class="basis-1/4 md:basis-1/4">
<router-link :to="{ name: 'home' }">
<div class="text-3xl">
<img src="../../assets/images/1.png" alt="" width="250" height="250" />
</div>
<!-- Logo -->
<router-link :to="{ name: 'home' }" class="btn btn-ghost normal-case text-xl">
<img src="../../assets/images/1.png" alt="Company Logo" class="h-8 md:h-10 w-auto" />
</router-link>
</div>
<div class="basis-1/4 md:basis-1/2 justify-center text-center">
<input type="text" placeholder="Search " class="input input-bordered w-24 md:w-auto grow" v-model="searchTerm" />
<!-- Navbar Center Section (Desktop Search Bar) -->
<div class="navbar-center hidden lg:flex">
<input
type="text"
placeholder="Search Customers..."
class="input input-bordered w-full max-w-xs"
v-model="searchStore.searchTerm"
@input="searchStore.fetchSearchResults"
/>
</div>
<div class="basis-1/2 md:basis-1/4 justify-end gap-5">
<!-- <button class="btn btn-green btn-sm" @click.prevent="increaseCall()">Call</button> -->
<router-link :to="{ name: 'customerCreate' }">
<button class="btn bg-blue-700 btn-sm">Create Customer</button>
<!-- Navbar End Section (Buttons and User Dropdown) -->
<div class="navbar-end gap-2">
<!-- Create Customer Button (Desktop Only) -->
<router-link :to="{ name: 'customerCreate' }" class="btn btn-accent btn-sm hidden lg:inline-flex">
Create Customer
</router-link>
<div v-if="employee.id" class="relative ">
<button @click="toggleDropdown" class="flex items-center gap-2 ">
<div>{{ user.user_name }}</div>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div v-if="isDropdownOpen" class="absolute right-0 mt-2 w-48 bg-gray-800 border border-gray-300 rounded shadow-lg z-10">
<router-link :to="{ name: 'employeeProfile', params: { id: employee.id } }" class="block px-4 py-2 text-white hover:bg-gray-700" @click="closeDropdown">
User Profile
</router-link>
<button @click="logout" class="block w-full text-left px-4 py-2 text-white hover:bg-gray-700">
Logout
</button>
</div>
<!-- User Dropdown -->
<!-- v-if="employee.id" only renders this block AFTER the API call is successful and an employee ID exists. -->
<div v-if="employee.id" class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full bg-neutral text-neutral-content flex items-center justify-center">
<span class="text-lg font-bold">{{ userInitials }}</span>
</div>
</label>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
<li class="p-2 font-semibold">{{ user.user_name }}</li>
<div class="divider my-0"></div>
<li>
<router-link :to="{ name: 'employeeProfile', params: { id: employee.id } }">
Profile
</router-link>
</li>
<li><a @click="logout">Logout</a></li>
</ul>
</div>
<!-- This provides the loading indicator while we wait for the API call to finish. -->
<div v-else class="px-4">
<span class="loading loading-spinner loading-sm"></span>
</div>
</div>
</div>
<div class="grid grid-cols-12 ">
<div class="grow col-start-4 col-span-6 ">
<SearchResults v-if="customers.length" :customers="customers" />
</div>
</div>
</template>
<script lang="ts">
import { debounce } from "vue-debounce";
import { defineComponent } from "vue";
import axios from "axios";
import authHeader from "../../services/auth.header";
import SearchResults from "./SearchResults.vue";
import { defineComponent } from 'vue'
import axios from 'axios'
import authHeader from '../../services/auth.header'
import { useSearchStore } from '../../stores/search' // Adjust path if needed
// Define the shape of your data for internal type safety
interface User {
user_name: string;
}
interface Employee {
id: number;
}
export default defineComponent({
name: "HeaderAuth",
components: {
SearchResults,
data() {
return {
// Initialize with empty objects to prevent template errors
employee: {} as Employee,
user: {} as User,
}
},
computed: {
searchStore() {
return useSearchStore();
},
userInitials(): string {
if (!this.user || !this.user.user_name) return '';
const parts = this.user.user_name.split(' ');
return parts.length > 1
? `${parts[0][0]}${parts[1][0]}`.toUpperCase()
: this.user.user_name.substring(0, 2).toUpperCase();
}
},
created() {
this.userStatus();
this.fetchUserData();
},
data() {
return {
user: {
user_id: 0,
user_name: '',
},
company_id: 0,
isDropdownOpen: false,
company: {
creation_date: "",
account_prefix: "",
company_name: "",
company_address: "",
company_town: "",
company_zip: "",
company_state: "",
company_phone_number: "",
},
employee: {
id: '',
user_id: '',
employee_last_name: "",
employee_first_name: "",
employee_town: "",
employee_address: "",
employee_apt: "",
employee_zip: "",
employee_birthday: "",
employee_phone_number: "",
employee_start_date: "",
employee_end_date: "",
employee_type: '',
employee_state: '',
},
loaded: false,
searchTerm: "",
customers: [],
type_of_search: 0,
};
},
watch: {
searchTerm(this: any) {
this.performSearch();
},
},
mounted() {
this.getCompany();
},
methods: {
performSearch: debounce(async function (this: any) {
if (this.searchTerm === "") {
this.customers = [];
return;
}
if (this.searchTerm.length < 2) {
return;
}
fetchUserData() {
axios.get('/auth/whoami', { headers: authHeader() })
.then((response: any) => {
console.log("User Data Response from API:", response.data);
if (this.searchTerm.startsWith("@")) {
this.type_of_search = 0
} else if (this.searchTerm.startsWith("!")) {
this.type_of_search = 1
} else if (this.searchTerm.startsWith("#")) {
this.type_of_search = 2
}else if (this.searchTerm.startsWith("$")) {
this.type_of_search = 3
} else {
this.type_of_search = 0
}
const searchUrl = this.getSearchUrl();
const response = await (await fetch(searchUrl)).json();
this.customers = response;
}, 600),
getSearchUrl() {
if (this.type_of_search == 0) {
const url = import.meta.env.VITE_BASE_URL + "/search/customer";
const params = {
q: this.searchTerm,
};
const searchParams = new URLSearchParams(params);
return `${url}?${searchParams}`;
}
else if (this.type_of_search == 1) {
const url = import.meta.env.VITE_BASE_URL +"/search/customer";
const params = {
q: this.searchTerm,
};
const searchParams = new URLSearchParams(params);
return `${url}?${searchParams}`;
}
else if (this.type_of_search == 2) {
const url = import.meta.env.VITE_BASE_URL +"/search/customer";
const params = {
q: this.searchTerm,
};
const searchParams = new URLSearchParams(params);
return `${url}?${searchParams}`;
}
else if (this.type_of_search == 3) {
const url = import.meta.env.VITE_BASE_URL +"/search/customer";
const params = {
q: this.searchTerm,
};
const searchParams = new URLSearchParams(params);
return `${url}?${searchParams}`;
}
else {
const url = import.meta.env.VITE_BASE_URL +"/search/customer";
const params = {
q: this.searchTerm,
};
const searchParams = new URLSearchParams(params);
return `${url}?${searchParams}`;
}
},
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
// This check is now more robust. It only checks for what it truly needs.
if (response.data && response.data.ok && response.data.employee && response.data.employee.id) {
this.user = response.data.user;
this.employee = response.data.employee;
} else {
console.error("API response was successful, but the expected employee data with an ID is missing.");
}
})
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
this.employeeStatus()
this.loaded = true;
} else {
localStorage.removeItem('user');
this.$router.push('/login');
}
})
.catch(() => {
this.loaded = true;
this.$router.push('/login');
});
},
employeeStatus() {
let path = import.meta.env.VITE_BASE_URL + '/employee/userid/' + this.user.user_id;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.employee = response.data;
this.loaded = true;
})
},
// increaseCall() {
// let path = import.meta.env.VITE_BASE_URL + '/stats/calls/add';
// axios({
// method: "put",
// url: path,
// withCredentials: true,
// headers: authHeader(),
// })
// .then((response: any) => {
// this.number++;
// })
// },
getCompany() {
let path = import.meta.env.VITE_BASE_URL + '/admin/company/' + import.meta.env.VITE_COMPANY_ID;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.company = response.data;
this.company_id = import.meta.env.VITE_COMPANY_ID
})
},
toggleDropdown() {
this.isDropdownOpen = !this.isDropdownOpen;
},
closeDropdown() {
this.isDropdownOpen = false;
},
logout() {
localStorage.removeItem('auth_user');
localStorage.removeItem('auth_token');
this.$router.push('/login');
},
.catch((error: any) => {
console.error("CRITICAL: Failed to fetch user data. The API call itself failed.", error);
});
},
});
</script>
<style scoped></style>
logout() {
console.log("Logging out...");
// Your full logout logic here
}
}
});
</script>

View File

@@ -1,25 +1,16 @@
<template>
<div class="navbar bg-primary border-b border-bottom-500 border-gray-500">
<div class="basis-1/4 md:basis-1/4">
<div class="navbar bg-primary border-b border-gray-700">
<div class="navbar-start">
<router-link :to="{ name: 'home' }">
Auburn Oil
<img src="../../assets/images/1.png" alt="Company Logo" class="h-10 md:h-12 w-auto" />
</router-link>
</div>
<div class="basis-1/4 md:basis-1/2 justify-center text-center">
</div>
<div class="basis-1/2 md:basis-1/4 justify-end gap-5">
<router-link :to="{ name: 'login' }">
<button class="btn btn-primary">Login</button>
</router-link>
<router-link :to="{ name: 'register' }">
<button class="btn btn-primary">Register</button>
</router-link>
<div class="navbar-end gap-2">
<router-link :to="{ name: 'login' }" class="btn btn-ghost btn-sm">Login</router-link>
<router-link :to="{ name: 'register' }" class="btn btn-accent btn-sm">Register</router-link>
</div>
</div>
</template>

View File

@@ -1,137 +1,118 @@
<!-- sidebar.vue -->
<template>
<!--
The sidebar is now just the menu. The layout logic lives in App.vue.
This is much cleaner and works correctly with the mobile hamburger button.
-->
<ul class="menu p-4 w-80 min-h-full bg-base-100 text-base-content">
<!-- Logo at the top of the sidebar for mobile view -->
<li class="mb-4 lg:hidden">
<router-link :to="{ name: 'home' }">
<img src="../assets/images/1.png" alt="Company Logo" class="h-10 w-auto" />
</router-link>
</li>
<li>
<router-link :to="{ name: 'home' }" exact-active-class="active">
Home
</router-link>
</li>
<div class="drawer sm:drawer-open bg-primary">
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col items-center justify-center ">
</div>
<!-- Customer Section - Open by default -->
<li>
<details open>
<summary class="font-bold text-lg">Customer</summary>
<ul>
<li><router-link :to="{ name: 'customer' }" exact-active-class="active">All Customers</router-link></li>
</ul>
</details>
</li>
<div class="drawer-side">
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
<!-- Delivery Section - Open by default -->
<li>
<details open>
<summary class="font-bold text-lg">Delivery</summary>
<ul>
<li><router-link :to="{ name: 'delivery' }" exact-active-class="active">Home</router-link></li>
<li>
<router-link :to="{ name: 'deliveryOutForDelivery' }" exact-active-class="active">
Todays Deliveries
<span v-if="today_count > 0" class="badge badge-secondary">{{ today_count }}</span>
</router-link>
</li>
<li>
<router-link :to="{ name: 'deliveryTommorrow' }" exact-active-class="active">
Tomorrows Deliveries
<span v-if="tommorrow_count > 0" class="badge badge-secondary">{{ tommorrow_count }}</span>
</router-link>
</li>
<li>
<router-link :to="{ name: 'deliveryWaiting' }" exact-active-class="active">
Waiting Deliveries
<span v-if="waiting_count > 0" class="badge badge-info">{{ waiting_count }}</span>
</router-link>
</li>
<li><router-link :to="{ name: 'deliveryIssue' }" exact-active-class="active">Issue Tickets</router-link></li>
<li>
<router-link :to="{ name: 'deliveryPending' }" exact-active-class="active">
Pending Payment
<span v-if="pending_count > 0" class="badge badge-warning">{{ pending_count }}</span>
</router-link>
</li>
<li><router-link :to="{ name: 'deliveryFinalized' }" exact-active-class="active">Finalized Tickets</router-link></li>
</ul>
</details>
</li>
<ul class="menu p-4 w-80 min-h-full text-base-content bg-base-100 ">
<!-- Service Section - Open by default -->
<li>
<details open>
<summary class="font-bold text-lg">Service</summary>
<ul>
<li><router-link :to="{ name: 'ServiceCalendar' }" exact-active-class="active">Service Calendar</router-link></li>
<li>
<router-link :to="{ name: 'ServiceHome' }" exact-active-class="active">
Upcoming Service
<span v-if="upcoming_service_count > 0" class="badge badge-info">{{ upcoming_service_count }}</span>
</router-link>
</li>
<li><router-link :to="{ name: 'ServicePast' }" exact-active-class="active">Past Service</router-link></li>
</ul>
</details>
</li>
<!-- Sidebar content here -->
<li>
<router-link :to="{ name: 'home' }">
<div class=" hover:underline py-1 px-5 font-bold">Home</div>
</router-link>
</li>
<!-- Automatics Section - Now has its own header -->
<li>
<details>
<summary class="font-bold text-lg">Automatics</summary>
<ul>
<li>
<router-link :to="{ name: 'auto' }" exact-active-class="active">
All Automatics
<span v-if="automatic_count > 0" class="badge badge-info">{{ automatic_count }}</span>
</router-link>
</li>
</ul>
</details>
</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' }">
<div class=" hover:underline py-1">Home</div>
</router-link>
<router-link :to="{ name: 'deliveryOutForDelivery' }">
<div class=" hover:underline py-1" v-if="today_count > 0">
<div class="flex gap-5">
<div class="">Todays Deliveries </div>
<div class="text-orange-600"> ({{ today_count }})</div>
</div>
</div>
<div class=" hover:underline py-1" v-else>Todays Deliveries </div>
</router-link>
<router-link :to="{ name: 'deliveryTommorrow' }">
<div class=" hover:underline py-1" v-if="tommorrow_count > 0">
<div class="flex gap-5">
<div class="">Tommorrows Deliveries </div>
<div class="text-orange-600"> ({{ tommorrow_count }})</div>
</div>
</div>
<div class=" hover:underline py-1" v-else>Tommorrow Deliveries </div>
</router-link>
<router-link :to="{ name: 'deliveryWaiting' }">
<div class=" hover:underline py-1" v-if="waiting_count > 0">
<div class="flex gap-5">
<div class="">Waiting Deliveries </div>
<div class="text-orange-600"> ({{ waiting_count }})</div>
</div>
</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>
<router-link :to="{ name: 'deliveryPending' }">
<div class=" hover:underline py-1" v-if="pending_count > 0">
<div class="flex gap-5">
<div class="">Pending Payment </div>
<div class="text-orange-600"> ({{ pending_count }})</div>
</div>
</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: 'ServiceCalendar' }">
<div class=" hover:underline py-1">Service Calendar</div>
</router-link>
<router-link :to="{ name: 'ServiceHome' }">
<div class=" hover:underline py-1" v-if="upcoming_service_count > 0">
<div class="flex gap-5">
<div class="">Service Upcomming</div>
<div class="text-orange-600"> ({{ upcoming_service_count }})</div>
</div>
</div>
<div class=" hover:underline py-1" v-else>Services</div>
</router-link>
<router-link :to="{ name: 'ServicePast' }">
<div class=" hover:underline py-1">Past Service</div>
</router-link>
</li>
<div class="font-bold text-lg text-gray-500 pt-5">Automatics</div>
<li class="text-white">
<router-link :to="{ name: 'auto' }">
<div class=" hover:underline py-1" v-if="automatic_count > 0">
<div class="flex gap-5">
<div class="">Automatics </div>
<div class="text-orange-600"> ({{ automatic_count }})</div>
</div>
</div>
<div class=" hover:underline py-1" v-else>Automatics </div>
</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' }">
<div class=" hover:underline py-1">Oil Pricing</div>
</router-link>
<router-link :to="{ name: 'promo' }">
<div class=" hover:underline py-1">Promos</div>
</router-link>
<router-link :to="{ name: 'MoneyYear' }">
<div class=" hover:underline py-1">Money</div>
</router-link>
</li>
</ul>
</div>
</div>
<!-- Admin Section - Closed by default and contains Employees -->
<li>
<details>
<summary class="font-bold text-lg">Admin</summary>
<ul>
<!-- Employees is now here -->
<li><router-link :to="{ name: 'employee' }" exact-active-class="active">Employees</router-link></li>
<li><router-link :to="{ name: 'oilprice' }" exact-active-class="active">Oil Pricing</router-link></li>
<li><router-link :to="{ name: 'promo' }" exact-active-class="active">Promos</router-link></li>
<li><router-link :to="{ name: 'MoneyYear' }" exact-active-class="active">Money</router-link></li>
</ul>
</details>
</li>
</ul>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import axios from 'axios';
import authHeader from '../../services/auth.header';
@@ -169,8 +150,6 @@ export default defineComponent({
headers: authHeader(),
}).then((response: any) => {
this.upcoming_service_count = response.data.count;
// --- THIS IS THE FIX ---
// Explicitly type the 'error' parameter as 'any'
}).catch((error: any) => {
console.error("Failed to get upcoming service count:", error);
this.upcoming_service_count = 0;
@@ -185,7 +164,7 @@ export default defineComponent({
}).then((response: any) => {
if (response.data.update)
console.log("Updated Status of Deliveries")
})
}).catch((error: any) => console.error("Update status failed:", error));
},
updatetemp() {
let path = import.meta.env.VITE_AUTO_URL + '/main/temp';
@@ -196,7 +175,7 @@ export default defineComponent({
}).then((response: any) => {
if (response.data.ok)
console.log("Updated Temp")
})
}).catch((error: any) => console.error("Update temp failed:", error));
},
updateautos() {
let path = import.meta.env.VITE_AUTO_URL + '/main/update';
@@ -207,7 +186,7 @@ export default defineComponent({
}).then((response: any) => {
if (response.data.ok)
console.log("Updated Autos")
})
}).catch((error: any) => console.error("Update autos failed:", error));
},
getAutoCount() {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/count/automatic';
@@ -217,7 +196,7 @@ export default defineComponent({
headers: authHeader(),
}).then((response: any) => {
this.automatic_count = response.data.count
})
}).catch((error: any) => console.error("Get auto count failed:", error));
},
getTodayCount() {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/count/today';
@@ -227,8 +206,7 @@ export default defineComponent({
headers: authHeader(),
}).then((response: any) => {
this.today_count = response.data.count
})
}).catch((error: any) => console.error("Get today count failed:", error));
},
getTommorrowCount() {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/count/tommorrow';
@@ -238,8 +216,7 @@ export default defineComponent({
headers: authHeader(),
}).then((response: any) => {
this.tommorrow_count = response.data.count
})
}).catch((error: any) => console.error("Get tomorrow count failed:", error));
},
getPendingCount() {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/count/pending';
@@ -249,8 +226,7 @@ export default defineComponent({
headers: authHeader(),
}).then((response: any) => {
this.pending_count = response.data.count
})
}).catch((error: any) => console.error("Get pending count failed:", error));
},
getWaitingCount() {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/count/waiting';
@@ -260,11 +236,8 @@ export default defineComponent({
headers: authHeader(),
}).then((response: any) => {
this.waiting_count = response.data.count
})
}).catch((error: any) => console.error("Get waiting count failed:", error));
},
},
});
</script>
<style scoped></style>
</script>

View File

@@ -1,67 +1,105 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="w-full px-4 md:px-10 ">
<!-- Breadcrumbs & Welcome Header -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
</ul>
</div>
<div class="flex text-2xl mb-5">
Welcome {{ employee.employee_first_name }} {{ employee.employee_last_name }}!
</div>
<h1 class="text-3xl font-bold mt-4">
Welcome, {{ employee.employee_first_name }}!
</h1>
<div class="grid grid-cols-12 gap-5 ">
<div class="col-span-12 bg-secondary ">
<div class="grid grid-cols-12 p-5 bg-neutral m-5">
<div class="col-span-12 font-bold text-xl">Todays stats</div>
<div class="col-span-6 py-2"> Total Deliveries: {{ delivery_count }}</div>
<div class="col-span-6 py-2"> Completed: {{ delivery_count_delivered }} / {{ delivery_count }}</div>
<!-- Main Dashboard Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 my-6">
<!-- Card 1: Today's Stats -->
<div class="bg-neutral rounded-lg p-5 xl:col-span-2">
<h3 class="text-xl font-bold mb-4">Today's Stats</h3>
<div class="space-y-4">
<div>
<span class="font-semibold">Total Deliveries Today:</span>
<span class="text-lg ml-2">{{ delivery_count }}</span>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span>Completed</span>
<span>{{ delivery_count_delivered }} / {{ delivery_count }}</span>
</div>
<progress class="progress progress-primary w-full" :value="delivery_count_delivered" :max="delivery_count"></progress>
</div>
</div>
</div>
<!-- Card 2: Today's Oil Price -->
<div class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Today's Oil Price</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span>Price / Gallon:</span>
<span class="font-mono">${{ today_oil_price }}</span>
</div>
<div class="flex justify-between">
<span>Same Day Fee:</span>
<span class="font-mono">${{ price_same_day }}</span>
</div>
<div class="flex justify-between">
<span>Prime Fee:</span>
<span class="font-mono">${{ price_prime }}</span>
</div>
<div class="flex justify-between">
<span>Emergency Fee:</span>
<span class="font-mono">${{ price_emergency }}</span>
</div>
</div>
</div>
<!-- Card 2: Today's Oil Price -->
<div class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Service Price</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span>Price / Hour:</span>
<span class="font-mono">$125</span>
</div>
<div class="flex justify-between">
<span>Price / Emergency:</span>
<span class="font-mono">$200</span>
</div>
<!-- <div class="col-span-6 py-2"> Total Calls: {{ call_count }}</div> -->
</div>
</div>
<div class="col-span-6 bg-secondary">
<div class="grid grid-cols-12 p-5 bg-neutral m-5">
<div class="col-span-12 font-bold text-xl">Todays Oil Price</div>
<div class="col-span-12 py-2"> Price / Gallon: ${{ today_oil_price }}</div>
<div class="col-span-12 py-2"> Same Day: ${{ price_same_day }}</div>
<div class="col-span-12 py-2"> Prime: ${{ price_prime }}</div>
<div class="col-span-12 py-2"> Emergency: ${{ price_emergency }}</div>
</div>
</div>
<div class="col-span-6 bg-secondary">
<div class="grid grid-cols-12 p-5 bg-neutral m-5">
<div class="col-span-12 font-bold text-xl">Quick Tips</div>
<div class="col-span-12 py-2"> search: @ = last name search</div>
<div class="col-span-12 py-2"> search: ! = address</div>
<div class="col-span-12 py-2"> search: $ = account number</div>
</div>
</div>
<div class="col-span-12 bg-secondary">
<div class="grid grid-cols-12 p-5 bg-neutral m-5">
<div class="col-span-12 font-bold text-xl">This Weeks Stats</div>
<div class="col-span-12 py-2"> Total Deliveries: {{ total_deliveries }}</div>
<div class="col-span-12 py-2"> Total Gallons : {{ total_gallons_past_week }}</div>
<div class="col-span-12 py-2"> Total Profit: ${{ total_profit_past_week }}</div>
<!-- Card 4: This Week's Stats -->
<div class="bg-neutral rounded-lg p-5 xl:col-span-4">
<h3 class="text-xl font-bold mb-4">This Week's Stats</h3>
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-100 w-full">
<div class="stat">
<div class="stat-title">Total Deliveries</div>
<div class="stat-value">{{ total_deliveries }}</div>
<div class="stat-desc">In the last 7 days</div>
</div>
<div class="stat">
<div class="stat-title">Total Gallons</div>
<div class="stat-value">{{ total_gallons_past_week }}</div>
<div class="stat-desc">Delivered this week</div>
</div>
<div class="stat">
<div class="stat-title">Total Profit</div>
<div class="stat-value text-success">${{ total_profit_past_week }}</div>
<div class="stat-desc">Estimated earnings</div>
</div>
</div>
</div>
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'

View File

@@ -1,221 +1,174 @@
<template>
<Header/>
<div class="flex">
<div class="">
<SideBar/>
</div>
<div class=" w-full px-10">
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'customer' }">
Customers
</router-link>
</li>
</ul>
</div>
<div class="grid grid-cols-1 rounded-md p-6 ">
<div class="text-[24px]">
Add Oil Price
</div>
<form class="rounded-md px-8 pt-6 pb-8 mb-4 w-full"
enctype="multipart/form-data"
@submit.prevent="onSubmit">
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 gap-10">
<label class="block text-white text-sm font-bold cursor-pointer label">Price Customer</label>
<input v-model="CreateOilForm.basicInfo.price_for_customer"
class="input input-bordered input-sm w-full max-w-xs"
id="title" type="text" placeholder="Todays Price"/>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Price Employee</label>
<input v-model="CreateOilForm.basicInfo.price_for_employee"
class="input input-bordered input-sm w-full max-w-xs"
id="title" type="text" placeholder="Todays Price Employee"/>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2"> Price from Supplier</label>
<input v-model="CreateOilForm.basicInfo.price_from_supplier"
class="input input-bordered input-sm w-full max-w-xs"
id="prime" type="text" placeholder="Price Prime"/>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Price same day</label>
<input v-model="CreateOilForm.basicInfo.price_same_day"
class="input input-bordered input-sm w-full max-w-xs"
id="title" type="text" placeholder="Price Same Day"/>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Price Emergency Call (After Hours)</label>
<input v-model="CreateOilForm.basicInfo.price_emergency"
class="input input-bordered input-sm w-full max-w-xs"
id="title" type="text" placeholder="Price After hours same day call"/>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Price Prime</label>
<input v-model="CreateOilForm.basicInfo.price_prime"
class="input input-bordered input-sm w-full max-w-xs"
id="title" type="text" placeholder="Price Prime"/>
</div>
<div class="col-span-12 md:col-span-12 flex mt-5 mb-5">
<button
class="btn btn-secondary btn-sm">
Create Pricing
</button>
</div>
</form>
</div>
</div>
<div class="flex">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Set Oil Pricing</li>
</ul>
</div>
<h1 class="text-3xl font-bold mt-4">
Set Today's Oil Pricing
</h1>
<Footer/>
</template>
<!-- Main Form Card -->
<div class="bg-neutral rounded-lg p-6 mt-6">
<form @submit.prevent="onSubmit" class="space-y-6">
<!-- SECTION 1: Base Pricing -->
<div>
<h2 class="text-lg font-bold">Base Pricing</h2>
<p class="text-xs text-gray-400">Set the core price per gallon for different groups.</p>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Price from Supplier -->
<div class="form-control">
<label class="label"><span class="label-text">Price from Supplier</span></label>
<label class="input-group input-group-sm">
<span>$</span>
<input v-model.number="OilForm.price_from_supplier" type="number" step="0.01" placeholder="3.50" class="input input-bordered input-sm w-full" />
</label>
</div>
<!-- Price for Customer -->
<div class="form-control">
<label class="label"><span class="label-text">Price for Customer</span></label>
<label class="input-group input-group-sm">
<span>$</span>
<input v-model.number="OilForm.price_for_customer" type="number" step="0.01" placeholder="4.50" class="input input-bordered input-sm w-full" />
</label>
</div>
<!-- Price for Employee -->
<div class="form-control">
<label class="label"><span class="label-text">Price for Employee</span></label>
<label class="input-group input-group-sm">
<span>$</span>
<input v-model.number="OilForm.price_for_employee" type="number" step="0.01" placeholder="4.00" class="input input-bordered input-sm w-full" />
</label>
</div>
</div>
</div>
<!-- SECTION 2: Service Fees -->
<div>
<h2 class="text-lg font-bold">Service Fees</h2>
<p class="text-xs text-gray-400">Set the flat fees for special delivery services.</p>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Price Same Day -->
<div class="form-control">
<label class="label"><span class="label-text">Same Day Fee</span></label>
<label class="input-group input-group-sm">
<span>$</span>
<input v-model.number="OilForm.price_same_day" type="number" step="1.00" placeholder="50.00" class="input input-bordered input-sm w-full" />
</label>
</div>
<!-- Price Prime -->
<div class="form-control">
<label class="label"><span class="label-text">Prime Fee</span></label>
<label class="input-group input-group-sm">
<span>$</span>
<input v-model.number="OilForm.price_prime" type="number" step="1.00" placeholder="75.00" class="input input-bordered input-sm w-full" />
</label>
</div>
<!-- Price Emergency -->
<div class="form-control">
<label class="label"><span class="label-text">Emergency Fee</span></label>
<label class="input-group input-group-sm">
<span>$</span>
<input v-model.number="OilForm.price_emergency" type="number" step="1.00" placeholder="150.00" class="input input-bordered input-sm w-full" />
</label>
</div>
</div>
</div>
<!-- SUBMIT BUTTON -->
<div class="pt-4">
<button type="submit" class="btn btn-primary btn-sm">Update Pricing</button>
</div>
</form>
</div>
</div>
</div>
<Footer />
</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 useValidate from "@vuelidate/core";
import {notify} from "@kyvg/vue3-notification";
export default defineComponent({
name: 'OilPrice',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
v$: useValidate(),
user: null,
CreateOilForm: {
basicInfo: {
price_from_supplier: '',
price_for_customer: '',
price_for_employee: '',
price_same_day: '',
price_prime: '',
price_emergency: '',
},
},
}
},
created() {
this.userStatus()
},
watch: {
$route() {
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'
import authHeader from '../../services/auth.header'
import Footer from '../../layouts/footers/footer.vue'
import { notify } from "@kyvg/vue3-notification";
export default defineComponent({
name: 'OilPrice',
components: {
Footer,
},
data() {
return {
user: null,
// --- REFACTORED: Simplified, flat form object ---
OilForm: {
price_from_supplier: 0,
price_for_customer: 0,
price_for_employee: 0,
price_same_day: 0,
price_prime: 0,
price_emergency: 0,
},
},
mounted() {
this.getCurrentPrices()
},
methods: {
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
}
},
created() {
this.userStatus();
},
mounted() {
this.getCurrentPrices();
},
methods: {
userStatus() {
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) { this.user = response.data.user; }
})
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
})
},
.catch(() => { this.user = null; });
},
getCurrentPrices() {
let path = import.meta.env.VITE_BASE_URL + "/admin/oil/get";
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
this.CreateOilForm.basicInfo.price_from_supplier = response.data.price_from_supplier;
this.CreateOilForm.basicInfo.price_for_customer = response.data.price_for_customer;
this.CreateOilForm.basicInfo.price_for_employee = response.data.price_for_employee;
this.CreateOilForm.basicInfo.price_same_day = response.data.price_same_day;
this.CreateOilForm.basicInfo.price_prime = response.data.price_prime;
this.CreateOilForm.basicInfo.price_emergency = response.data.price_emergency;
}
})
const path = import.meta.env.VITE_BASE_URL + "/admin/oil/get";
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data) {
// --- REFACTORED: Populate the flat form object ---
this.OilForm = response.data;
}
});
},
CreatePricing(payload: {
price_from_supplier: string;
price_for_customer: string;
price_for_employee: string;
price_same_day: string;
price_prime: string;
price_emergency: string;
}) {
let path = import.meta.env.VITE_BASE_URL + "/admin/oil/create";
axios({
method: "post",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
notify({
title: "update",
text: "Prices have been updated!",
type: "success",
});
this.$router.push({name: "home"});
}
if (response.data.error) {
this.$router.push("/");
}
})
},
onSubmit() {
let payload = {
price_from_supplier: this.CreateOilForm.basicInfo.price_from_supplier,
price_for_customer: this.CreateOilForm.basicInfo.price_for_customer,
price_for_employee: this.CreateOilForm.basicInfo.price_for_employee,
price_same_day: this.CreateOilForm.basicInfo.price_same_day,
price_prime: this.CreateOilForm.basicInfo.price_prime,
price_emergency: this.CreateOilForm.basicInfo.price_emergency,
};
this.CreatePricing(payload);
},
CreatePricing(payload: any) {
const path = import.meta.env.VITE_BASE_URL + "/admin/oil/create";
axios.post(path, payload, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "Prices have been updated!",
type: "success",
});
this.$router.push({ name: "home" });
} else {
notify({
title: "Error",
text: response.data.error || "An unknown error occurred.",
type: "error",
});
}
});
},
})
</script>
<style scoped>
</style>
onSubmit() {
// --- REFACTORED: Submit the flat form object ---
this.CreatePricing(this.OilForm);
},
},
});
</script>

View File

@@ -1,9 +1,6 @@
<template>
<Header/>
<div class="flex">
<div class="">
<SideBar/>
</div>
<div class=" w-full px-10">
<div class="text-sm breadcrumbs">
<ul>

View File

@@ -1,9 +1,6 @@
<template>
<Header/>
<div class="flex">
<div class="">
<SideBar/>
</div>
<div class=" w-full px-10">
<div class="text-sm breadcrumbs">
<ul>

View File

@@ -1,9 +1,6 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs pb-10">
<ul>

View File

@@ -1,6 +1,5 @@
<template>
<Header />
<div class="WrapperPlain">
<div class="container max-w-3xl mx-auto text-white">
<div class="mt-5 mb-5 px-10 ">

View File

@@ -1,6 +1,5 @@
<template>
<Header />
<div class="WrapperPlain">
<div class="max-w-7xl mx-auto ">
<div class="mx-auto max-w-lg flex items-center justify-center mt-4">

View File

@@ -1,6 +1,5 @@
<template>
<Header />
<div class="WrapperPlain">
<div class="container max-w-3xl mx-auto text-white">
<div class="mt-5 mb-5">

View File

@@ -1,6 +1,5 @@
<template>
<Header />
<div class="WrapperPlain">
<div class="container mx-auto max-w-lg text-white">
<div class="mx-auto flex items-center justify-center ">

View File

@@ -1,252 +1,251 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs mb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'delivery' }">
Delivery
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Automatic Deliveries</li>
</ul>
</div>
<div class="flex start text-2xl mb-10">Automatics </div>
<div class="mb-10">
<div class="">Home Factor</div>
<div class="pl-10">1.50 = large oil usage</div>
<div class="pl-10"> 1.00 = medium oil usage</div>
<div class="pl-10">0.50 = small oil usage</div>
<!-- <form class="col-span-12 rounded-md px-8 pt-6 pb-8 mb-4 w-full" enctype="multipart/form-data"
@submit.prevent="get_auto_assignment">
<div class="flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">Delivery Driver </label>
<select class="select select-bordered select-sm w-full max-w-xs" aria-label="Default select example"
id="customer_state" v-model="CreateOilOrderForm.basicInfo.driver_driver">
<option class="text-white" v-for="(driver, index) in truckDriversList" :key="index" :value="driver['id']">
{{ driver['employee_first_name'] }} {{ driver['employee_last_name'] }}
</option>
</select>
<h1 class="text-3xl font-bold mt-4">Automatic Deliveries</h1>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">Customers on Automatic Delivery</h2>
<div class="badge badge-ghost">{{ deliveries.length }} customers found</div>
</div>
<div class="divider"></div>
<!-- Data Display -->
<div>
<!-- DESKTOP VIEW: Sortable Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<!-- SORTABLE HEADERS -->
<th @click="sortBy('tank_level_percent')" class="cursor-pointer hover:text-white">Tank Level</th>
<th @click="sortBy('days_since_last_fill')" class="cursor-pointer hover:text-white">Days Since Fill</th>
<th @click="sortBy('customer_full_name')" class="cursor-pointer hover:text-white">Name</th>
<th>Address</th>
<th @click="sortBy('house_factor')" class="cursor-pointer hover:text-white">Usage Factor</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<!-- Loop over the new 'sortedDeliveries' computed property -->
<tr v-for="oil in sortedDeliveries" :key="oil.id" class="hover:bg-blue-600 hover:text-white">
<td>
<div v-if="oil.last_fill === null" class="text-gray-500">New Auto</div>
<div v-else class="flex items-center gap-3">
<progress class="progress w-24"
:value="oil.estimated_gallons_left"
:max="oil.tank_size"
:class="{
'progress-success': getTankLevelPercentage(oil) > 60,
'progress-warning': getTankLevelPercentage(oil) >= 25 && getTankLevelPercentage(oil) <= 60,
'progress-error': getTankLevelPercentage(oil) < 25
}"
></progress>
<span class="font-mono text-xs">{{ oil.estimated_gallons_left }} / {{ oil.tank_size }} gal</span>
</div>
</td>
<td>{{ oil.days_since_last_fill }} days</td>
<td>
<router-link :to="{ name: 'customerProfile', params: { id: oil.customer_id } }" class="link link-hover">
{{ oil.customer_full_name }}
</router-link>
</td>
<td>{{ oil.customer_address }}, {{ oil.customer_town }}</td>
<td>{{ oil.house_factor }}</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'customerEdit', params: { id: oil.customer_id } }" class="btn btn-sm btn-secondary">Edit Customer</router-link>
<router-link :to="{ name: 'finalizeTicketAuto', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Finalize</button>
</router-link>
<router-link :to="{ name: 'TicketAuto', params: { id: oil['id'] } }">
<button class="btn btn-success btn-sm">
Print Ticket
</button>
</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<button class="btn btn-secondary btn-sm">
Send for delivery
</button>
</form> -->
</div>
<div class="overflow-x-auto bg-neutral">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Account #</th>
<th>Gallons Left</th>
<th>Last Fill</th>
<th>Days</th>
<th>Name</th>
<th>Address</th>
<th>Town</th>
<th>Home Factor</th>
<th>Tank Size</th>
</tr>
</thead>
<tbody>
<!-- row 1 -->
<tr v-for="oil in deliveries" :key="oil['id']">
<router-link :to="{ name: 'customerProfile', params: { id: oil['customer_id'] } }">
<td>
{{ oil['customer_id'] }}
</td>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="oil in sortedDeliveries" :key="oil.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ oil.customer_full_name }}</h2>
<p class="text-xs text-gray-400">{{ oil.customer_address }}, {{ oil.customer_town }}</p>
</div>
<div class="text-right">
<div class="font-bold">{{ oil.days_since_last_fill }}</div>
<div class="text-xs text-gray-400">days ago</div>
</div>
</div>
<div class="mt-4">
<label class="label p-0 mb-1"><span class="label-text">Tank Level</span></label>
<div v-if="oil.last_fill === null" class="text-gray-500 text-sm">New Auto Customer</div>
<div v-else>
<progress class="progress w-full"
:value="oil.estimated_gallons_left"
:max="oil.tank_size"
:class="{
'progress-success': getTankLevelPercentage(oil) > 60,
'progress-warning': getTankLevelPercentage(oil) >= 25 && getTankLevelPercentage(oil) <= 60,
'progress-error': getTankLevelPercentage(oil) < 25
}"
></progress>
<div class="text-xs text-gray-400 text-right">{{ oil.estimated_gallons_left }} / {{ oil.tank_size }} gal estimated</div>
</div>
</div>
<div class="card-actions justify-end flex-wrap gap-2 mt-4">
<router-link :to="{ name: 'customerEdit', params: { id: oil.customer_id } }" class="btn btn-sm btn-secondary">Edit Customer</router-link>
<router-link :to="{ name: 'finalizeTicketAuto', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Finalize</button>
</router-link>
<td>
<div class="" v-if="oil['last_fill'] === null"></div>
<div class="" v-else> {{ oil['estimated_gallons_left'] }}</div>
</td>
<td>
<div class="" v-if="oil['last_fill'] === null">New Auto</div>
<div class="" v-else> {{ oil['last_fill'] }}</div>
</td>
<td>{{ oil['days_since_last_fill'] }}</td>
<router-link :to="{ name: 'customerProfile', params: { id: oil['customer_id'] } }">
<td>{{ oil['customer_full_name'] }}</td>
<router-link :to="{ name: 'TicketAuto', params: { id: oil['id'] } }">
<button class="btn btn-success btn-sm">
Print Ticket
</button>
</router-link>
<td>{{ oil['customer_address'] }}</td>
<td>{{ oil['customer_town'] }}</td>
<td>{{ oil['house_factor'] }}</td>
<td>{{ oil['tank_size'] }}</td>
<td class="flex gap-5">
<router-link :to="{ name: 'customerEdit', params: { id: oil['customer_id'] } }">
<button class="btn btn-secondary btn-sm">
Edit Customer
</button>
</router-link>
<router-link :to="{ name: 'finalizeTicketAuto', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Finalize</button>
</router-link>
<router-link :to="{ name: 'TicketAuto', params: { id: oil['id'] } }">
<button class="btn btn-success btn-sm">
Print Ticket
</button>
</router-link>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<Footer />
</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 { notify } from "@kyvg/vue3-notification";
// Define a type for the delivery object for better code quality
interface AutoDelivery {
id: number;
customer_id: number;
last_fill: string | null;
estimated_gallons_left: number;
days_since_last_fill: number;
customer_full_name: string;
customer_address: string;
customer_town: string;
house_factor: number;
tank_size: number;
}
export default defineComponent({
name: 'AutomaticHome',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
token: null,
user: null,
deliveries: [],
checkedMaterials: [],
truckDriversList: [],
CreateOilOrderForm: {
basicInfo: {
driver_driver: '',
},
},
deliveries: [] as AutoDelivery[],
// --- NEW: Data properties for sorting ---
sortKey: 'estimated_gallons_left' as keyof AutoDelivery | 'tank_level_percent',
sortAsc: true,
}
},
computed: {
// --- NEW: Computed property to handle sorting ---
sortedDeliveries(): AutoDelivery[] {
// Create a copy to avoid mutating the original array
const sorted = [...this.deliveries];
created() {
sorted.sort((a, b) => {
let valA: any;
let valB: any;
this.userStatus()
this.getDriversList()
this.get_oil_orders()
// Special case for our calculated percentage
if (this.sortKey === 'tank_level_percent') {
valA = this.getTankLevelPercentage(a);
valB = this.getTankLevelPercentage(b);
} else {
valA = a[this.sortKey as keyof AutoDelivery];
valB = b[this.sortKey as keyof AutoDelivery];
}
// Handle nulls or different types if necessary
if (valA === null) return 1;
if (valB === null) return -1;
// Comparison logic
if (valA < valB) {
return this.sortAsc ? -1 : 1;
}
if (valA > valB) {
return this.sortAsc ? 1 : -1;
}
return 0;
});
return sorted;
}
},
mounted() {
this.get_oil_orders()
created() {
this.userStatus();
this.get_oil_orders();
},
methods: {
// --- NEW: Method to handle sorting ---
sortBy(key: keyof AutoDelivery | 'tank_level_percent') {
if (this.sortKey === key) {
// If clicking the same key, reverse the direction
this.sortAsc = !this.sortAsc;
} else {
// If clicking a new key, set it and default to ascending
this.sortKey = key;
this.sortAsc = true;
}
},
// --- NEW: Helper method for percentage calculation ---
getTankLevelPercentage(oil: AutoDelivery): number {
if (!oil.tank_size || oil.tank_size === 0 || oil.last_fill === null) {
return 0; // Return 0 if tank size is invalid or it's a new customer
}
return (oil.estimated_gallons_left / oil.tank_size) * 100;
},
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
})
},
get_oil_orders() {
let path = import.meta.env.VITE_AUTO_URL + '/delivery/all/customers';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
}).then((response: any) => {
this.deliveries = response.data
})
},
getDriversList() {
let path = import.meta.env.VITE_BASE_URL + "/employee/drivers";
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.truckDriversList = response.data;
})
.catch(() => {
this.user = null;
});
},
get_auto_assignment() {
let path = import.meta.env.VITE_AUTO_URL + '/delivery/create';
const selectedValues: any[] = [];
for (const id in this.checkedMaterials) {
if (this.checkedMaterials[id]) {
selectedValues.push(this.checkedMaterials[id]);
}
}
let payload = {
driver_employee_id: this.CreateOilOrderForm.basicInfo.driver_driver,
values: selectedValues
}
axios({
method: 'post',
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
}).then((response: any) => {
if (response.data.count > 0) {
notify({
title: "Success",
text: "Added automatics to delivery",
type: "success",
});
} else {
notify({
title: "Failure",
text: "Incorrect driver or no auto selection.",
type: "error",
});
}
this.get_oil_orders()
})
get_oil_orders() {
const path = import.meta.env.VITE_AUTO_URL + '/delivery/all/customers';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
this.deliveries = response.data;
})
.catch((error: any) => {
console.error("Failed to fetch automatic deliveries:", error);
});
},
},
})
</script>
<style scoped></style>
</script>

View File

@@ -1,217 +1,122 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'customer' }">
Customers
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li><router-link :to="{ name: 'customer' }">Customers</router-link></li>
<li v-if="customer.id"><router-link :to="{ name: 'customerProfile', params: { id: customer.id } }">Profile</router-link></li>
<li>Add Credit Card</li>
</ul>
</div>
<div class="grid grid-cols-1 rounded-md p-6 ">
<div class="grid grid-cols-12">
<div class="col-span-12 text-center mb-10 text-2xl">Add a Credit Card</div>
<div class="col-span-6">
<form class="rounded-md px-8 pt-6 pb-8 mb-4 w-full" enctype="multipart/form-data"
@submit.prevent="onSubmit">
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 gap-10">
<label class="block text-white text-sm font-bold cursor-pointer label">Main Card</label>
<input v-model="CreateCardForm.basicInfo.main_card" class="checkbox" id="fill" type="checkbox" />
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Name on Card</label>
<input v-model="CreateCardForm.basicInfo.card_name"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text"
placeholder="Name on Card" />
<span v-if="v$.CreateCardForm.basicInfo.card_name.$error" class="text-red-600 text-center">
{{ v$.CreateCardForm.basicInfo.card_name.$errors[0].$message }}
</span>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Card Number</label>
<input v-model="CreateCardForm.basicInfo.card_number"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text"
placeholder="Card Number" />
<span v-if="v$.CreateCardForm.basicInfo.card_number.$error" class="text-red-600 text-center">
{{ v$.CreateCardForm.basicInfo.card_number.$errors[0].$message }}
</span>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Expiration Month</label>
<select v-model="CreateCardForm.basicInfo.expiration_month"
class="input input-bordered input-sm w-full max-w-xs" id="Month">
<option>01</option>
<option>02</option>
<option>03</option>
<option>04</option>
<option>05</option>
<option>06</option>
<option>07</option>
<option>08</option>
<option>09</option>
<option>10</option>
<option>11</option>
<option>12</option>
</select>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Expiration Year</label>
<select v-model="CreateCardForm.basicInfo.expiration_year"
class="input input-bordered input-sm w-full max-w-xs" id="Month">
<option>2025</option>
<option>2026</option>
<option>2027</option>
<option>2028</option>
<option>2029</option>
<option>2030</option>
<option>2031</option>
<option>2032</option>
</select>
<span v-if="v$.CreateCardForm.basicInfo.expiration_year.$error" class="text-red-600 text-center">
{{ v$.CreateCardForm.basicInfo.expiration_year.$errors[0].$message }}
</span>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Type of Card</label>
<select v-model="CreateCardForm.basicInfo.type_of_card"
class="input input-bordered input-sm w-full max-w-xs" id="Month">
<option>Visa</option>
<option>MasterCard</option>
<option>Discover</option>
<option>American Express</option>
</select>
<span v-if="v$.CreateCardForm.basicInfo.type_of_card.$error" class="text-red-600 text-center">
{{ v$.CreateCardForm.basicInfo.type_of_card.$errors[0].$message }}
</span>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Security Number</label>
<input v-model="CreateCardForm.basicInfo.security_number"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text"
placeholder="Back of card" />
<span v-if="v$.CreateCardForm.basicInfo.security_number.$error" class="text-red-600 text-center">
{{ v$.CreateCardForm.basicInfo.security_number.$errors[0].$message }}
</span>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Zip Code</label>
<input v-model="CreateCardForm.basicInfo.zip_code"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text"
placeholder="Zip Code" />
</div>
<div class="col-span-12 md:col-span-12 flex mt-5 mb-5">
<button class="btn btn-sm btn-secondary">
Save Credit Card
</button>
</div>
</form>
<!-- TOP SECTION: Customer Info -->
<div class="my-6">
<div class="bg-neutral rounded-lg p-5">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4">
<div>
<div class="text-xl font-bold">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</div>
<div class="text-sm text-gray-400">Account: {{ customer.account_number }}</div>
</div>
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm mt-2 sm:mt-0">
View Profile
</router-link>
</div>
<div class="col-span-6">
<div class="col-span-12 font-bold flex pb-5 ">{{ customer.account_number }}</div>
<div class="col-span-12 font-bold flex">
{{ customer.customer_first_name }}
{{ customer.customer_last_name }}
</div>
<div class="col-span-12 font-bold flex">
{{ customer.customer_address }}
<div v-if="customer.customer_apt != 'None'">
{{ customer.customer_apt }}
</div>
</div>
<div class="col-span-12 font-bold flex">
<div class="pr-2">
{{ customer.customer_town }},
</div>
<div class="pr-2">
<div v-if="customer.customer_state == 0">Massachusetts</div>
<div v-else-if="customer.customer_state == 1">Rhode Island</div>
<div v-else-if="customer.customer_state == 2">New Hampshire</div>
<div v-else-if="customer.customer_state == 3">Maine</div>
<div v-else-if="customer.customer_state == 4">Vermont</div>
<div v-else-if="customer.customer_state == 5">Maine</div>
<div v-else-if="customer.customer_state == 6">New York</div>
<div v-else>Unknown state</div>
</div>
<div class="pr-2">
{{ customer.customer_zip }}
</div>
</div>
<div class="col-span-12 font-bold flex" v-if="customer.customer_apt !== 'None'">
{{ customer.customer_apt }}
</div>
<div class="col-span-12 font-bold flex">
<div v-if="customer.customer_home_type == 0">Residential</div>
<div v-else-if="customer.customer_home_type == 1">apartment</div>
<div v-else-if="customer.customer_home_type == 2">condo</div>
<div v-else-if="customer.customer_home_type == 3">commercial</div>
<div v-else-if="customer.customer_home_type == 4">business</div>
<div v-else-if="customer.customer_home_type == 5">construction</div>
<div v-else-if="customer.customer_home_type == 6">container</div>
</div>
<div class="col-span-12 font-bold flex">
{{ customer.customer_phone_number }}
</div>
<div>
<div>{{ customer.customer_address }}</div>
<div v-if="customer.customer_apt && customer.customer_apt !== 'None'">{{ customer.customer_apt }}</div>
<div>{{ customer.customer_town }}, {{ customer.customer_state }} {{ customer.customer_zip }}</div>
</div>
</div>
</div>
<!-- BOTTOM SECTION: Add Card Form -->
<div class="bg-neutral rounded-lg p-6">
<h2 class="text-2xl font-bold mb-4">Add a New Credit Card</h2>
<form @submit.prevent="onSubmit" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Name on Card -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">Name on Card</span></label>
<input v-model="CardForm.card_name" type="text" placeholder="John M. Doe" class="input input-bordered input-sm w-full" />
<span v-if="v$.CardForm.card_name.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Card Number -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">Card Number</span></label>
<input v-model="CardForm.card_number" type="text" placeholder="4242..." class="input input-bordered input-sm w-full" />
<span v-if="v$.CardForm.card_number.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Expiration -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">Expiration</span></label>
<div class="flex gap-2">
<select v-model="CardForm.expiration_month" class="select select-bordered select-sm w-full">
<option disabled value="">MM</option>
<option v-for="m in 12" :key="m" :value="String(m).padStart(2, '0')">{{ String(m).padStart(2, '0') }}</option>
</select>
<select v-model="CardForm.expiration_year" class="select select-bordered select-sm w-full">
<option disabled value="">YYYY</option>
<option v-for="y in 10" :key="y" :value="new Date().getFullYear() + y - 1">{{ new Date().getFullYear() + y - 1 }}</option>
</select>
</div>
<span v-if="v$.CardForm.expiration_month.$error || v$.CardForm.expiration_year.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Security Number (CVV) -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">CVV</span></label>
<input v-model="CardForm.security_number" type="text" placeholder="123" class="input input-bordered input-sm w-full" />
<span v-if="v$.CardForm.security_number.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Card Type -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">Card Type</span></label>
<select v-model="CardForm.type_of_card" class="select select-bordered select-sm w-full">
<option disabled value="">Select Type</option>
<option>Visa</option>
<option>MasterCard</option>
<option>Discover</option>
<option>American Express</option>
</select>
<span v-if="v$.CardForm.type_of_card.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Billing Zip Code -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">Billing Zip Code</span></label>
<input v-model="CardForm.zip_code" type="text" placeholder="01234" class="input input-bordered input-sm w-full" />
</div>
<!-- Main Card Checkbox -->
<div class="form-control md:col-span-2">
<label class="label cursor-pointer justify-start gap-4">
<span class="label-text font-bold">Set as Main Card for this customer</span>
<input v-model="CardForm.main_card" type="checkbox" class="checkbox checkbox-sm" />
</label>
</div>
</div>
<!-- SUBMIT BUTTON -->
<div class="pt-4">
<button type="submit" class="btn btn-primary btn-sm">Save Credit Card</button>
</div>
</form>
</div>
</div>
</div>
<Footer />
</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 useValidate from "@vuelidate/core";
import { notify } from "@kyvg/vue3-notification"
@@ -219,151 +124,80 @@ import { minLength, required } from "@vuelidate/validators";
export default defineComponent({
name: 'AddCardCreate',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
v$: useValidate(),
user: null,
customer: {
id: 0,
user_id: 0,
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_address: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
account_number: '',
},
CreateCardForm: {
basicInfo: {
card_name: '',
expiration_month: '',
expiration_year: '',
type_of_card: '',
security_number: '',
card_number: '',
zip_code: '',
main_card: false,
},
customer: {} as any,
// --- REFACTORED: Simplified, flat form object ---
CardForm: {
card_name: '',
expiration_month: '',
expiration_year: '',
type_of_card: '',
security_number: '',
card_number: '',
zip_code: '',
main_card: false,
},
}
},
validations() {
return {
CreateCardForm: {
basicInfo: {
card_name: { required, minLength: minLength(1) },
expiration_month: { required, minLength: minLength(1) },
expiration_year: { required, minLength: minLength(1) },
security_number: { required, minLength: minLength(1) },
type_of_card: { required, minLength: minLength(1) },
card_number: { required, minLength: minLength(1) },
},
// --- REFACTORED: Validation points to the flat form object ---
CardForm: {
card_name: { required, minLength: minLength(1) },
expiration_month: { required },
expiration_year: { required },
security_number: { required, minLength: minLength(1) },
type_of_card: { required },
card_number: { required, minLength: minLength(1) },
},
};
},
created() {
this.userStatus()
},
watch: {
$route() {
this.getCustomer(this.$route.params.id);
},
},
mounted() {
this.getCustomer(this.$route.params.id)
this.userStatus();
this.getCustomer(this.$route.params.id);
},
methods: {
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
if (response.data.ok) { this.user = response.data.user; }
})
.catch(() => { this.user = null; });
},
getCustomer(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/customer/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.customer = response.data;
})
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => { this.customer = response.data; })
.catch(() => {
notify({
title: "Error",
text: "Could not find customer",
type: "error",
});
notify({ title: "Error", text: "Could not find customer", type: "error" });
});
},
CreateCard(payload: {
card_name: string;
expiration_month: string;
expiration_year: string;
type_of_card: string;
security_number: string;
zip_code: string;
card_number: string;
main_card: boolean;
}) {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/create/" + this.customer.id;
axios({
method: "post",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
CreateCard(payload: any) {
const path = `${import.meta.env.VITE_BASE_URL}/payment/card/create/${this.customer.id}`;
axios.post(path, payload, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
} else {
notify({ title: "Error", text: response.data.error || "Failed to create card.", type: "error" });
}
if (response.data.error) {
this.$router.push("/");
}
})
});
},
onSubmit() {
let payload = {
card_name: this.CreateCardForm.basicInfo.card_name,
card_number: this.CreateCardForm.basicInfo.card_number,
expiration_month: this.CreateCardForm.basicInfo.expiration_month,
expiration_year: this.CreateCardForm.basicInfo.expiration_year,
type_of_card: this.CreateCardForm.basicInfo.type_of_card,
security_number: this.CreateCardForm.basicInfo.security_number,
main_card: this.CreateCardForm.basicInfo.main_card,
zip_code: this.CreateCardForm.basicInfo.zip_code,
};
this.CreateCard(payload);
this.v$.$validate();
if (!this.v$.$error) {
this.CreateCard(this.CardForm);
} else {
notify({ title: "Validation Error", text: "Please fill out all required fields.", type: "error" });
}
},
},
})
</script>
<style scoped></style>
});
</script>

View File

@@ -1,354 +1,240 @@
<template>
<Header/>
<div class="flex">
<div class="">
<SideBar/>
<div class="flex">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs -->
<div class="text-sm breadcrumbs">
<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.id"><router-link :to="{ name: 'customerProfile', params: { id: customer.id } }">Profile</router-link></li>
<li>Edit Credit Card</li>
</ul>
</div>
<!-- TOP SECTION: Customer and Card Info -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 my-6">
<!-- Customer Info Card -->
<div class="bg-neutral rounded-lg p-5">
<div class="flex justify-between items-center mb-4">
<div>
<div class="text-xl font-bold">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</div>
<div class="text-sm text-gray-400">Account: {{ customer.account_number }}</div>
</div>
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm">
View Profile
</router-link>
</div>
<div>
<div>{{ customer.customer_address }}</div>
<div v-if="customer.customer_apt && customer.customer_apt !== 'None'">{{ customer.customer_apt }}</div>
<div>{{ customer.customer_town }}, {{ customer.customer_state }} {{ customer.customer_zip }}</div>
</div>
</div>
<div class=" w-full px-10">
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'customer' }">
Customers
</router-link>
</li>
</ul>
</div>
<div class="grid grid-cols-1 rounded-md p-6 ">
<div class="text-[24px]">
Credit Card Customer: {{ customer.customer_first_name }}
</div>
<div class="text-[20px]">
Card Id: {{card.id}}
</div>
<form class="rounded-md px-8 pt-6 pb-8 mb-4 w-full"
enctype="multipart/form-data"
@submit.prevent="onSubmit">
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 gap-10">
<label class="block text-white text-sm font-bold cursor-pointer label">Main Card</label>
<input v-model="CreateCardForm.basicInfo.main_card"
class="checkbox checkbox-xs"
id="fill"
type="checkbox"/>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Name on Card</label>
<input v-model="CreateCardForm.basicInfo.card_name"
class="input input-bordered input-sm w-full max-w-xs"
id="title" type="text" placeholder="Name on Card"/>
<span v-if="v$.CreateCardForm.basicInfo.card_name.$error"
class="text-red-600 text-center">
{{ v$.CreateCardForm.basicInfo.card_name.$errors[0].$message }}
</span>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Card Number</label>
<input v-model="CreateCardForm.basicInfo.card_number"
class="input input-bordered input-sm w-full max-w-xs"
id="title" type="text" placeholder="Card Number"/>
<span v-if="v$.CreateCardForm.basicInfo.card_number.$error"
class="text-red-600 text-center">
{{ v$.CreateCardForm.basicInfo.card_number.$errors[0].$message }}
</span>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Expiration Month</label>
<select
v-model="CreateCardForm.basicInfo.expiration_month"
class="input input-bordered input-sm w-full max-w-xs"
id="Month"
>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
<option>11</option>
<option>12</option>
</select>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Expiration Year</label>
<select
v-model="CreateCardForm.basicInfo.expiration_year"
class="input input-bordered input-sm w-full max-w-xs"
id="Month"
>
<option>2025</option>
<option>2026</option>
<option>2027</option>
<option>2028</option>
<option>2029</option>
<option>2030</option>
<option>2031</option>
<option>2032</option>
</select>
<span v-if="v$.CreateCardForm.basicInfo.expiration_year.$error"
class="text-red-600 text-center">
{{ v$.CreateCardForm.basicInfo.expiration_year.$errors[0].$message }}
</span>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Type of Card</label>
<select
v-model="CreateCardForm.basicInfo.type_of_card"
class="input input-bordered input-sm w-full max-w-xs"
id="Month"
>
<option>Visa</option>
<option>MasterCard</option>
<option>Discover</option>
<option>American Express</option>
</select>
<span v-if="v$.CreateCardForm.basicInfo.type_of_card.$error"
class="text-red-600 text-center">
{{ v$.CreateCardForm.basicInfo.type_of_card.$errors[0].$message }}
</span>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Zip Code</label>
<input v-model="CreateCardForm.basicInfo.zip_code"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text"
placeholder="Zip Code" />
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Security Number</label>
<input v-model="CreateCardForm.basicInfo.security_number"
class="input input-bordered input-sm w-full max-w-xs"
id="title" type="text" placeholder="Back of card"/>
<span v-if="v$.CreateCardForm.basicInfo.security_number.$error"
class="text-red-600 text-center">
{{ v$.CreateCardForm.basicInfo.security_number.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 md:col-span-12 flex mt-5 mb-5">
<button class="btn">
Edit Card
</button>
</div>
</form>
<!-- Card Being Edited Info Card -->
<div class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Editing Card</h3>
<div v-if="card.id" class="space-y-2">
<p><strong class="font-semibold">Card Type:</strong> {{ card.type_of_card }}</p>
<p><strong class="font-semibold">Card Number:</strong> **** **** **** {{ card.last_four_digits }}</p>
<p><strong class="font-semibold">Card ID:</strong> {{ card.id }}</p>
</div>
<div v-else class="text-gray-400">Loading card details...</div>
</div>
</div>
<Footer/>
</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 useValidate from "@vuelidate/core";
import {minLength, required} from "@vuelidate/validators";
export default defineComponent({
name: 'EditCard',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
v$: useValidate(),
user: {
id: '',
},
customer: {
id: 0,
user_id: 0,
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_address: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
account_number: '',
<!-- BOTTOM SECTION: Edit Card Form -->
<div class="bg-neutral rounded-lg p-6">
<h2 class="text-2xl font-bold mb-4">Update Card Details</h2>
<form @submit.prevent="onSubmit" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Name on Card -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">Name on Card</span></label>
<input v-model="CardForm.name_on_card" type="text" placeholder="John M Doe" class="input input-bordered input-sm w-full" />
<span v-if="v$.CardForm.name_on_card.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Card Number -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">Card Number</span></label>
<input v-model="CardForm.card_number" type="text" placeholder="4242..." class="input input-bordered input-sm w-full" />
<span v-if="v$.CardForm.card_number.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Expiration -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">Expiration</span></label>
<div class="flex gap-2">
<select v-model="CardForm.expiration_month" class="select select-bordered select-sm w-full">
<option disabled value="">MM</option>
<option v-for="m in 12" :key="m" :value="String(m).padStart(2, '0')">{{ String(m).padStart(2, '0') }}</option>
</select>
<select v-model="CardForm.expiration_year" class="select select-bordered select-sm w-full">
<option disabled value="">YYYY</option>
<option v-for="y in 10" :key="y" :value="new Date().getFullYear() + y - 1">{{ new Date().getFullYear() + y - 1 }}</option>
</select>
</div>
<span v-if="v$.CardForm.expiration_month.$error || v$.CardForm.expiration_year.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Security Number (CVV) -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">CVV</span></label>
<input v-model="CardForm.security_number" type="text" placeholder="123" class="input input-bordered input-sm w-full" />
<span v-if="v$.CardForm.security_number.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Card Type -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">Card Type</span></label>
<select v-model="CardForm.type_of_card" class="select select-bordered select-sm w-full">
<option disabled value="">Select Type</option>
<option>Visa</option>
<option>MasterCard</option>
<option>Discover</option>
<option>American Express</option>
</select>
<span v-if="v$.CardForm.type_of_card.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Billing Zip Code -->
<div class="form-control">
<label class="label"><span class="label-text font-bold">Billing Zip Code</span></label>
<input v-model="CardForm.zip_code" type="text" placeholder="01234" class="input input-bordered input-sm w-full" />
</div>
<!-- Main Card Checkbox -->
<div class="form-control md:col-span-2">
<label class="label cursor-pointer justify-start gap-4">
<span class="label-text font-bold">Set as Main Card</span>
<input v-model="CardForm.main_card" type="checkbox" class="checkbox checkbox-sm" />
</label>
</div>
</div>
<!-- SUBMIT BUTTON -->
<div class="pt-4">
<button type="submit" class="btn btn-primary btn-sm">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'
import authHeader from '../../services/auth.header'
import Footer from '../../layouts/footers/footer.vue'
import useValidate from "@vuelidate/core";
import { minLength, required } from "@vuelidate/validators";
export default defineComponent({
name: 'EditCard',
components: {
Footer,
},
data() {
return {
v$: useValidate(),
user: null as any,
customer: {} as any,
card: {} as any, // To store original card details for display
// --- REFACTORED: Simplified, flat form object ---
CardForm: {
name_on_card: '',
expiration_month: '',
expiration_year: '',
type_of_card: '',
security_number: '',
card_number: '',
zip_code: '',
main_card: false,
},
card: {
id: '',
card_name: '',
expiration_month: '',
expiration_year: '',
type_of_card: '',
security_number: '',
main_card: '',
zip_code: '',
user_id: '',
},
card_id: null,
customer_id: null,
CreateCardForm: {
basicInfo: {
card_name: '',
expiration_month: '',
expiration_year: '',
type_of_card: '',
security_number: '',
card_number: '',
zip_code: '',
main_card: false,
},
},
}
},
validations() {
return {
// --- REFACTORED: Validation points to the flat form object ---
CardForm: {
name_on_card: { required, minLength: minLength(1) },
expiration_month: { required },
expiration_year: { required },
security_number: { required, minLength: minLength(1) },
type_of_card: { required },
card_number: { required, minLength: minLength(1) },
},
};
},
created() {
this.userStatus();
this.getCard(this.$route.params.id);
},
methods: {
userStatus() {
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) { this.user = response.data.user; }
})
.catch(() => { this.user = null; });
},
getCustomer(userid: any) {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${userid}`;
axios.get(path, { headers: authHeader() })
.then((response: any) => { this.customer = response.data; });
},
getCard(card_id: any) {
const path = `${import.meta.env.VITE_BASE_URL}/payment/card/${card_id}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
this.card = response.data; // Store original details for display
// Populate the flat form object for editing
this.CardForm.name_on_card = response.data.name_on_card;
this.CardForm.expiration_month = response.data.expiration_month;
this.CardForm.expiration_year = response.data.expiration_year;
this.CardForm.type_of_card = response.data.type_of_card;
this.CardForm.security_number = response.data.security_number;
this.CardForm.main_card = response.data.main_card;
this.CardForm.card_number = response.data.card_number;
this.CardForm.zip_code = response.data.zip_code;
if (response.data.user_id) {
this.getCustomer(response.data.user_id);
}
});
},
editCard(payload: any) {
const path = `${import.meta.env.VITE_BASE_URL}/payment/card/edit/${this.$route.params.id}`;
// The backend expects 'card_name', but our form now uses 'name_on_card'.
// We must create a new payload that matches the backend's expectation.
const backendPayload = {
...payload,
card_name: payload.name_on_card,
};
delete backendPayload.name_on_card; // Clean up the object
axios.put(path, backendPayload, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
} else {
console.error("Failed to edit card:", response.data.error);
}
});
},
onSubmit() {
this.v$.$validate();
if (!this.v$.$error) {
this.editCard(this.CardForm);
} else {
console.log("Form validation failed.");
}
},
validations() {
return {
CreateCardForm: {
basicInfo: {
card_name: {required, minLength: minLength(1)},
expiration_month: {required, minLength: minLength(1)},
expiration_year: {required, minLength: minLength(1)},
security_number: {required, minLength: minLength(1)},
type_of_card: {required, minLength: minLength(1)},
card_number: {required, minLength: minLength(1)},
},
},
};
},
created() {
this.userStatus()
},
watch: {
$route() {
this.getCard(this.$route.params.id);
},
},
mounted() {
this.getCard(this.$route.params.id)
},
methods: {
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;
this.user.id = response.data.user.id;
}
})
.catch(() => {
this.user.id = '';
})
},
getCustomer(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/customer/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customer = response.data
})
},
getCard (card_id:any) {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id ;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.CreateCardForm.basicInfo.card_name= response.data.name_on_card;
this.CreateCardForm.basicInfo.expiration_month= response.data.expiration_month;
this.CreateCardForm.basicInfo.expiration_year= response.data.expiration_year;
this.CreateCardForm.basicInfo.type_of_card= response.data.type_of_card;
this.CreateCardForm.basicInfo.security_number= response.data.security_number;
this.CreateCardForm.basicInfo.main_card= response.data.main_card;
this.CreateCardForm.basicInfo.card_number= response.data.card_number;
this.CreateCardForm.basicInfo.zip_code= response.data.zip_code;
console.log(response.data)
this.user.id = response.data.user_id
this.card=response.data
this.getCustomer(response.data.user_id)
})
},
editCard(payload: {
card_name: string;
expiration_month: string;
expiration_year: string;
type_of_card: string;
security_number: string;
zip_code: string;
main_card: boolean;
}) {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/edit/" + this.$route.params.id ;
axios({
method: "put",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.$router.push({name: "customerProfile", params: { id: this.card.user_id }});
}
if (response.data.error) {
this.$router.push("/");
}
})
},
onSubmit() {
let payload = {
card_name: this.CreateCardForm.basicInfo.card_name,
expiration_month: this.CreateCardForm.basicInfo.expiration_month,
expiration_year: this.CreateCardForm.basicInfo.expiration_year,
type_of_card: this.CreateCardForm.basicInfo.type_of_card,
security_number: this.CreateCardForm.basicInfo.security_number,
card_number: this.CreateCardForm.basicInfo.card_number,
zip_code: this.CreateCardForm.basicInfo.zip_code,
main_card: this.CreateCardForm.basicInfo.main_card,
};
this.editCard(payload);
},
},
})
</script>
<style scoped>
</style>
},
});
</script>

View File

@@ -1,9 +1,6 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs">
<ul>

View File

@@ -1,154 +1,138 @@
<template>
<Header />
<div v-if="user">
<div class="flex">
<div class="">
<SideBar />
<div class="flex">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li><router-link :to="{ name: 'customer' }">Customers</router-link></li>
<li>Create New Customer</li>
</ul>
</div>
<div class="w-full px-10">
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'customer' }">
Customers
</router-link>
</li>
</ul>
</div>
<div class="grid grid-cols-12 rounded-md p-6 ">
<div class="col-span-12 text-[24px] ">Create a customer</div>
<form class="col-span-12 rounded-md px-8 pt-6 pb-8 mb-4 w-full" enctype="multipart/form-data"
@submit.prevent="onSubmit">
<h1 class="text-3xl font-bold mt-4">
Create New Customer
</h1>
<div class="grid grid-cols-12">
<div class="col-span-6">
<div class="col-span-12 text-[18px] mt-5 mb-5">General Info</div>
<div class="col-span-12 mb-4">
<label class="block text-white text-sm font-bold mb-2"> First Name</label>
<input v-model="CreateCustomerForm.basicInfo.customer_first_name"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text"
placeholder="First Name" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_first_name.$error"
class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_first_name.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 mb-4">
<label class="block text-white text-sm font-bold mb-2"> Last Name</label>
<input v-model="CreateCustomerForm.basicInfo.customer_last_name"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text"
placeholder="Last Name" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_last_name.$error"
class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_last_name.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 mb-4">
<label class="block text-white text-sm font-bold mb-2">Phone Number</label>
<input v-model="CreateCustomerForm.basicInfo.customer_phone_number"
class="input input-bordered input-sm w-full max-w-xs" id="phone number" type="tel"
placeholder="Phone Number" @input="acceptNumber()" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_phone_number.$error"
class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_phone_number.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 mb-4">
<div class="flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">Customer Type</label>
<select class="select select-bordered select-sm w-full max-w-xs" aria-label="Default select example"
id="customer_type" v-model="CreateCustomerForm.basicInfo.customer_home_type">
<option class="text-white" v-for="(customer, index) in custList" :key="index"
:value="customer['value']">
{{ customer['text'] }}
</option>
</select>
</div>
</div>
<div class="col-span-12 mb-4">
<label class="block text-white text-sm font-bold mb-2">Email (Optional)</label>
<input v-model="CreateCustomerForm.basicInfo.customer_email"
class="input input-bordered input-sm w-full max-w-xs" id="email" type="text" placeholder="Email" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_email.$error" class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_email.$errors[0].$message }}
</span>
</div>
<!-- Main Form Card -->
<div class="bg-neutral rounded-lg p-6 mt-6">
<form @submit.prevent="onSubmit" class="space-y-6">
<!-- SECTION 1: General Info -->
<div>
<h2 class="text-lg font-bold">General Info</h2>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- First Name -->
<div class="form-control">
<label class="label"><span class="label-text">First Name</span></label>
<input v-model="CreateCustomerForm.customer_first_name" type="text" placeholder="First Name" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.customer_first_name.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.customer_first_name.$errors[0].$message }}
</span>
</div>
<div class="col-span-6">
<div class="grid grid-cols-12">
<div class="text-[18px] mt-5 mb-5">Customer Address</div>
<div class="col-span-12 mb-5 md:mb-5">
<label class="block text-white text-sm font-bold mb-2">Street Address</label>
<input v-model="CreateCustomerForm.basicInfo.customer_address"
class="input input-bordered input-sm w-full max-w-xs" id="address" type="text"
placeholder="Address" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_address.$error"
class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_address.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 mb-5 md:mb-5">
<input v-model="CreateCustomerForm.basicInfo.customer_apt"
class="input input-bordered input-sm w-full max-w-xs" id="apt" type="text"
placeholder="Apt, suite, unit, building, floor, etc" />
</div>
<div class="col-span-12 mb-20 md:mb-5 ">
<label class="block text-white text-sm font-bold mb-2">Town</label>
<input v-model="CreateCustomerForm.basicInfo.customer_town"
class="input input-bordered input-sm w-full max-w-xs" id="town" type="text" placeholder="Town" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_town.$error" class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_town.$errors[0].$message }}
</span>
</div>
<div class=" col-span-12 flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">State</label>
<select class="select select-bordered select-sm w-full max-w-xs" aria-label="Default select example"
id="customer_state" v-model="CreateCustomerForm.basicInfo.customer_state">
<option class="text-white" v-for="(state, index) in stateList" :key="index"
:value="state['value']">
{{ state['text'] }}
</option>
</select>
<span v-if="v$.CreateCustomerForm.basicInfo.customer_state.$error" class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_state.$errors[0].$message }}
</span>
</div>
<div class="col-span-4 mb-5 md:mb-5">
<label class="block text-white text-sm font-bold mb-2">Zip Code</label>
<input v-model="CreateCustomerForm.basicInfo.customer_zip" class="w-full input input-bordered input-sm "
id="zip" type="text" placeholder="Zip" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_zip.$error" class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_zip.$errors[0].$message }}
</span>
</div>
</div>
<!-- Last Name -->
<div class="form-control">
<label class="label"><span class="label-text">Last Name</span></label>
<input v-model="CreateCustomerForm.customer_last_name" type="text" placeholder="Last Name" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.customer_last_name.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.customer_last_name.$errors[0].$message }}
</span>
</div>
<div class="col-span-6">
<div class="text-[18px] mt-5 mb-5"> Description</div>
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0">
<textarea v-model="CreateCustomerForm.basicInfo.customer_description" rows="4"
class="textarea block p-2.5 w-full input-bordered " id="description" type="text"
placeholder="Description of Customer House" />
</div>
<!-- Phone Number -->
<div class="form-control">
<label class="label"><span class="label-text">Phone Number</span></label>
<input v-model="CreateCustomerForm.customer_phone_number" type="tel" placeholder="Phone Number" class="input input-bordered input-sm w-full" @input="acceptNumber()" />
<span v-if="v$.CreateCustomerForm.customer_phone_number.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.customer_phone_number.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 md:col-span-12 flex mt-5 mb-5">
<button class="btn btn-accent btn-sm">
Submit Create Customer
</button>
<!-- Email -->
<div class="form-control">
<label class="label"><span class="label-text">Email (Optional)</span></label>
<input v-model="CreateCustomerForm.customer_email" type="text" placeholder="Email" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.customer_email.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.customer_email.$errors[0].$message }}
</span>
</div>
<!-- Customer Type -->
<div class="form-control">
<label class="label"><span class="label-text">Customer Type</span></label>
<select v-model="CreateCustomerForm.customer_home_type" class="select select-bordered select-sm w-full">
<option disabled :value="0">Select a type</option>
<option v-for="customer in custList" :key="customer.value" :value="customer.value">
{{ customer.text }}
</option>
</select>
<span v-if="v$.CreateCustomerForm.customer_home_type.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- SECTION 2: Address -->
<div>
<h2 class="text-lg font-bold">Address</h2>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Street Address -->
<div class="form-control">
<label class="label"><span class="label-text">Street Address</span></label>
<input v-model="CreateCustomerForm.customer_address" type="text" placeholder="Street Address" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.customer_address.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.customer_address.$errors[0].$message }}
</span>
</div>
<!-- Apt, Suite, etc. -->
<div class="form-control">
<label class="label"><span class="label-text">Apt, Suite, etc. (Optional)</span></label>
<input v-model="CreateCustomerForm.customer_apt" type="text" placeholder="Apt, suite, unit..." class="input input-bordered input-sm w-full" />
</div>
<!-- Town -->
<div class="form-control">
<label class="label"><span class="label-text">Town</span></label>
<input v-model="CreateCustomerForm.customer_town" type="text" placeholder="Town" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.customer_town.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.customer_town.$errors[0].$message }}
</span>
</div>
<!-- State -->
<div class="form-control">
<label class="label"><span class="label-text">State</span></label>
<select v-model="CreateCustomerForm.customer_state" class="select select-bordered select-sm w-full">
<option disabled :value="0">Select a state</option>
<option v-for="state in stateList" :key="state.value" :value="state.value">
{{ state.text }}
</option>
</select>
<span v-if="v$.CreateCustomerForm.customer_state.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Zip Code -->
<div class="form-control">
<label class="label"><span class="label-text">Zip Code</span></label>
<input v-model="CreateCustomerForm.customer_zip" type="text" placeholder="Zip Code" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.customer_zip.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.customer_zip.$errors[0].$message }}
</span>
</div>
</div>
</div>
<!-- SECTION 3: Delivery Details -->
<div>
<h2 class="text-lg font-bold">Delivery Details</h2>
<div class="divider mt-2 mb-4"></div>
<div class="form-control">
<label class="label"><span class="label-text">Description / Notes (Optional)</span></label>
<textarea v-model="CreateCustomerForm.customer_description" rows="4" placeholder="Description of customer's house, tank, etc." class="textarea textarea-bordered"></textarea>
</div>
</div>
<!-- SUBMIT BUTTON -->
<div class="pt-4">
<button type="submit" class="btn btn-primary btn-sm">Create Customer</button>
</div>
</form>
</div>
</div>
</div>
<Footer />
@@ -158,224 +142,115 @@
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 useValidate from "@vuelidate/core";
import { email, minLength, required } from "@vuelidate/validators";
import { notify } from "@kyvg/vue3-notification";
interface SelectOption {
text: string;
value: number;
}
export default defineComponent({
name: 'CustomerCreate',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
v$: useValidate(),
user: null,
stateList: [],
x: '',
custList: [],
new_user_id: 0,
company: {
creation_date: "",
account_prefix: "",
company_name: "",
company_address: "",
company_town: "",
company_zip: "",
company_state: "",
company_phone_number: "",
},
stateList: [] as SelectOption[],
custList: [] as SelectOption[],
// --- REFACTORED: Simplified, flat form object ---
CreateCustomerForm: {
basicInfo: {
customer_last_name: "",
customer_first_name: "",
customer_town: "",
customer_apt: "",
customer_home_type: 0,
customer_zip: "",
customer_automatic: "",
customer_email: "",
customer_phone_number: "",
customer_state: 0,
customer_address: "",
customer_description: "",
},
customer_last_name: "",
customer_first_name: "",
customer_town: "",
customer_address: "",
customer_apt: "",
customer_zip: "",
customer_email: "",
customer_phone_number: "",
customer_description: "",
// --- FIX: Initialized as numbers for proper v-model binding ---
customer_home_type: 0,
customer_state: 0,
},
}
},
validations() {
return {
// --- REFACTORED: Validation rules point to the flat form object ---
CreateCustomerForm: {
basicInfo: {
customer_last_name: { required, minLength: minLength(1) },
customer_first_name: { required, minLength: minLength(1) },
customer_town: { required, minLength: minLength(1) },
customer_home_type: { required },
customer_zip: { required, minLength: minLength(5) },
customer_email: { email, required },
customer_phone_number: { required },
customer_state: { required },
customer_address: { required },
},
customer_last_name: { required, minLength: minLength(1) },
customer_first_name: { required, minLength: minLength(1) },
customer_town: { required, minLength: minLength(1) },
customer_zip: { required, minLength: minLength(5) },
customer_email: { email }, // Optional, so only validate format
customer_phone_number: { required },
customer_home_type: { required },
customer_state: { required },
customer_address: { required },
},
};
},
created() {
this.userStatus()
this.userStatus();
},
mounted() {
this.getCustomerTypeList();
this.getStatesList();
this.getCompany();
},
methods: {
acceptNumber() {
let x = this.CreateCustomerForm.basicInfo.customer_phone_number.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/);
const x = this.CreateCustomerForm.customer_phone_number.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/);
if (x) {
this.CreateCustomerForm.basicInfo.customer_phone_number = !x[2] ? x[1] : '(' + x[1] + ') ' + x[2] + (x[3] ? '-' + x[3] : '');
this.CreateCustomerForm.customer_phone_number = !x[2] ? x[1] : `(${x[1]}) ${x[2]}${x[3] ? `-${x[3]}` : ''}`;
}
else {
this.CreateCustomerForm.basicInfo.customer_phone_number = ''
}
},
getCompany() {
let path = import.meta.env.VITE_BASE_URL + '/admin/company/' + import.meta.env.VITE_COMPANY_ID;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.company = response.data;
})
},
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
if (response.data.ok) { this.user = response.data.user; }
})
.catch(() => { this.user = null; });
},
CreateCustomer(payload: {
customer_last_name: string;
customer_first_name: string;
customer_town: string;
customer_zip: string;
customer_email: string;
customer_phone_number: string;
customer_address: string;
customer_apt: string;
customer_home_type: number,
customer_state: number;
customer_description: string;
})
{
let path = import.meta.env.VITE_BASE_URL + "/customer/create";
axios({
method: "post",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
CreateCustomer(payload: any) {
const path = import.meta.env.VITE_BASE_URL + "/customer/create";
axios.post(path, payload, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.new_user_id = response.data.user.user_id
this.$router.push({ name: 'customerProfile', params: { id: this.new_user_id } });
const new_user_id = response.data.user.user_id;
this.$router.push({ name: 'customerProfile', params: { id: new_user_id } });
} else {
notify({ title: "Error", text: response.data.error || "Failed to create customer.", type: "error" });
}
if (response.data.error) {
this.$router.push("/");
}
})
});
},
onSubmit() {
if (this.CreateCustomerForm.basicInfo.customer_zip === ''){
notify({
title: "Error",
text: "No zip code added!",
type: "error",
});
}
if (this.CreateCustomerForm.basicInfo.customer_last_name === ''){
notify({
title: "Error",
text: "No last name added!",
type: "error",
});
}
if (this.CreateCustomerForm.basicInfo.customer_address === ''){
notify({
title: "Error",
text: "No address added!",
type: "error",
});
}
let payload = {
customer_last_name: this.CreateCustomerForm.basicInfo.customer_last_name,
customer_first_name: this.CreateCustomerForm.basicInfo.customer_first_name,
customer_town: this.CreateCustomerForm.basicInfo.customer_town,
customer_zip: this.CreateCustomerForm.basicInfo.customer_zip,
customer_email: this.CreateCustomerForm.basicInfo.customer_email,
customer_phone_number: this.CreateCustomerForm.basicInfo.customer_phone_number,
customer_home_type: this.CreateCustomerForm.basicInfo.customer_home_type,
customer_state: this.CreateCustomerForm.basicInfo.customer_state,
customer_apt: this.CreateCustomerForm.basicInfo.customer_apt,
customer_address: this.CreateCustomerForm.basicInfo.customer_address,
customer_description: this.CreateCustomerForm.basicInfo.customer_description,
};
this.CreateCustomer(payload);
this.v$.$validate(); // Trigger validation
if (!this.v$.$error) {
// If validation passes, submit the form
this.CreateCustomer(this.CreateCustomerForm);
} else {
// If validation fails, show a single notification
notify({ title: "Validation Error", text: "Please fill out all required fields correctly.", type: "error" });
console.log("Form validation failed.");
}
},
getCustomerTypeList() {
let path = import.meta.env.VITE_BASE_URL + "/query/customertype";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.custList = response.data;
})
.catch(() => {
});
const path = import.meta.env.VITE_BASE_URL + "/query/customertype";
axios.get(path, { withCredentials: true })
.then((response: any) => { this.custList = response.data; });
},
getStatesList() {
let path = import.meta.env.VITE_BASE_URL + "/query/states";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.stateList = response.data;
})
.catch(() => {
});
const path = import.meta.env.VITE_BASE_URL + "/query/states";
axios.get(path, { withCredentials: true })
.then((response: any) => { this.stateList = response.data; });
},
},
})
</script>
<style scoped></style>
});
</script>

View File

@@ -1,196 +1,176 @@
<template>
<Header />
<div v-if="user">
<div class="flex">
<div class="">
<SideBar />
<div class="flex">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li><router-link :to="{ name: 'customer' }">Customers</router-link></li>
<li>Edit Customer</li>
</ul>
</div>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mt-4">
<h1 class="text-3xl font-bold">
Edit Customer: {{ customer.account_number }}
</h1>
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm mt-2 sm:mt-0">
View Profile
</router-link>
</div>
<div class="w-full px-10">
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'customer' }">
Customers
</router-link>
</li>
</ul>
</div>
<div class="grid grid-cols-12 rounded-md p-6 ">
<div class="col-span-12 text-2xl">Edit customer: {{ customer.account_number }}</div>
<div class="col-span-12 py-5">
<router-link :to="{ name: 'customerProfile', params: { id: customer['id'] } }"
class="btn btn-secondary btn-sm">
View Profile
</router-link>
</div>
<form class="col-span-12 rounded-md px-8 pt-6 pb-8 mb-4 " enctype="multipart/form-data"
@submit.prevent="onSubmit">
<div class="grid grid-cols-12">
<div class="col-span-6">
<div class="col-span-12 text-[18px] mt-5 mb-5">General Info</div>
<div class="col-span-12 mb-4">
<label class="block text-white text-sm font-bold mb-2"> First Name</label>
<input v-model="CreateCustomerForm.basicInfo.customer_first_name"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text"
placeholder="First Name" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_first_name.$error"
class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_first_name.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 mb-4">
<label class="block text-white text-sm font-bold mb-2"> Last Name</label>
<input v-model="CreateCustomerForm.basicInfo.customer_last_name"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text"
placeholder="Last Name" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_last_name.$error"
class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_last_name.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 md:col-span-4 mb-5 md:mb-5">
<label class="block text-white text-sm font-bold mb-2">Phone Number</label>
<input v-model="CreateCustomerForm.basicInfo.customer_phone_number"
class="input input-bordered input-sm w-full max-w-xs" id="phone number" type="text"
placeholder="Phone Number" @input="acceptNumber()" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_phone_number.$error"
class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_phone_number.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 flex gap-5">
<div class="flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">Customer Type</label>
<select class="select select-bordered select-sm w-full max-w-xs" aria-label="Default select example"
id="customer_type" v-model="CreateCustomerForm.basicInfo.customer_home_type">
<option class="text-white" v-for="(customer, index) in custList" :key="index"
:value="customer['value']">
{{ customer['text'] }}
</option>
</select>
</div>
</div>
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0">
<label class="block text-white text-sm font-bold mb-2">Email (Optional)</label>
<input v-model="CreateCustomerForm.basicInfo.customer_email"
class="input input-bordered input-sm w-full max-w-xs" id="email" type="text" placeholder="Email" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_email.$error" class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_email.$errors[0].$message }}
</span>
</div>
<!-- Main Form Card -->
<div class="bg-neutral rounded-lg p-6 mt-6">
<form @submit.prevent="onSubmit" class="space-y-6">
<!-- SECTION 1: General Info -->
<div>
<h2 class="text-lg font-bold">General Info</h2>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- First Name -->
<div class="form-control">
<label class="label"><span class="label-text">First Name</span></label>
<input v-model="CreateCustomerForm.basicInfo.customer_first_name" type="text" placeholder="First Name" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_first_name.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.basicInfo.customer_first_name.$errors[0].$message }}
</span>
</div>
<div class="col-span-6 ">
<div class="text-[18px] mt-5 mb-5">Customer Address</div>
<div class="grid grid-cols-12">
<div class="col-span-12 mb-5 ">
<label class="block text-white text-sm font-bold mb-2">Street Address</label>
<input v-model="CreateCustomerForm.basicInfo.customer_address"
class="input input-bordered input-sm w-full max-w-xs" id="address" type="text"
placeholder="Address" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_address.$error"
class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_address.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 mb-5 ">
<label class="block text-white text-sm font-bold mb-2">Apt</label>
<input v-model="CreateCustomerForm.basicInfo.customer_apt"
class="input input-bordered input-sm w-full max-w-xs" id="apt" type="text"
placeholder="Apt, suite, unit, building, floor, etc" />
</div>
<div class="col-span-12 ">
<label class="block text-white text-sm font-bold mb-2">Town</label>
<input v-model="CreateCustomerForm.basicInfo.customer_town"
class="input input-bordered input-sm w-full max-w-xs" id="town" type="text" placeholder="town" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_town.$error" class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_town.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 flex-1 mb-4 ">
<label class="block text-white text-sm font-bold mb-2">State</label>
<select class="select select-bordered select-sm w-full max-w-xs" aria-label="Default select example"
id="customer_state" v-model="CreateCustomerForm.basicInfo.customer_state">
<option class="text-white" v-for="(state, index) in stateList" :key="index"
:value="state['value']">
{{ state['text'] }}
</option>
</select>
<span v-if="v$.CreateCustomerForm.basicInfo.customer_state.$error" class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_state.$errors[0].$message }}
</span>
</div>
<div class="col-span-4 mb-5 md:mb-5">
<label class="block text-white text-sm font-bold mb-2">Zip Code</label>
<input v-model="CreateCustomerForm.basicInfo.customer_zip"
class="input input-bordered input-sm w-full max-w-xs" id="zip" type="text" placeholder="Zip" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_zip.$error" class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.customer_zip.$errors[0].$message }}
</span>
</div>
</div>
<!-- Last Name -->
<div class="form-control">
<label class="label"><span class="label-text">Last Name</span></label>
<input v-model="CreateCustomerForm.basicInfo.customer_last_name" type="text" placeholder="Last Name" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_last_name.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.basicInfo.customer_last_name.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 mb-5 md:mb-5">
<div class="col-span-12 text-[18px] mt-5 mb-5"> Description</div>
<div class="col-span-12 md:col-span-4 mb-2 ">
<label class="block text-white text-sm font-bold mb-2">Fill Location</label>
<input v-model="CreateCustomerForm.basicInfo.customer_fill_location"
class="input input-bordered input-sm w-full max-w-xs" id="fill" type="text" placeholder="Fill (1-12)" />
</div>
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0">
<textarea v-model="CreateCustomerForm.basicInfo.customer_description" rows="4"
class="textarea block p-2.5 w-full input-bordered " id="description" type="text"
placeholder="Description of Customer House" />
<!-- Phone Number -->
<div class="form-control">
<label class="label"><span class="label-text">Phone Number</span></label>
<input v-model="CreateCustomerForm.basicInfo.customer_phone_number" type="text" placeholder="Phone Number" class="input input-bordered input-sm w-full" @input="acceptNumber()" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_phone_number.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.basicInfo.customer_phone_number.$errors[0].$message }}
</span>
</div>
<!-- Email -->
<div class="form-control">
<label class="label"><span class="label-text">Email (Optional)</span></label>
<input v-model="CreateCustomerForm.basicInfo.customer_email" type="text" placeholder="Email" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_email.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.basicInfo.customer_email.$errors[0].$message }}
</span>
</div>
<!-- Customer Type -->
<div class="form-control">
<label class="label"><span class="label-text">Customer Type</span></label>
<select v-model="CreateCustomerForm.basicInfo.customer_home_type" class="select select-bordered select-sm w-full">
<option v-for="customer in custList" :key="customer.value" :value="customer.value">
{{ customer.text }}
</option>
</select>
</div>
</div>
</div>
<div class="col-span-12 md:col-span-12 flex mt-5 mb-5">
<button class="btn-sm btn btn-accent">
Save Changes
</button>
<!-- SECTION 2: Address -->
<div>
<h2 class="text-lg font-bold">Address</h2>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Street Address -->
<div class="form-control">
<label class="label"><span class="label-text">Street Address</span></label>
<input v-model="CreateCustomerForm.basicInfo.customer_address" type="text" placeholder="Street Address" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_address.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.basicInfo.customer_address.$errors[0].$message }}
</span>
</div>
<!-- Apt, Suite, etc. -->
<div class="form-control">
<label class="label"><span class="label-text">Apt, Suite, etc. (Optional)</span></label>
<input v-model="CreateCustomerForm.basicInfo.customer_apt" type="text" placeholder="Apt, suite, unit..." class="input input-bordered input-sm w-full" />
</div>
<!-- Town -->
<div class="form-control">
<label class="label"><span class="label-text">Town</span></label>
<input v-model="CreateCustomerForm.basicInfo.customer_town" type="text" placeholder="Town" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_town.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.basicInfo.customer_town.$errors[0].$message }}
</span>
</div>
<!-- State -->
<div class="form-control">
<label class="label"><span class="label-text">State</span></label>
<select v-model="CreateCustomerForm.basicInfo.customer_state" class="select select-bordered select-sm w-full">
<option v-for="state in stateList" :key="state.value" :value="state.value">
{{ state.text }}
</option>
</select>
<span v-if="v$.CreateCustomerForm.basicInfo.customer_state.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.basicInfo.customer_state.$errors[0].$message }}
</span>
</div>
<!-- Zip Code -->
<div class="form-control">
<label class="label"><span class="label-text">Zip Code</span></label>
<input v-model="CreateCustomerForm.basicInfo.customer_zip" type="text" placeholder="Zip Code" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateCustomerForm.basicInfo.customer_zip.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateCustomerForm.basicInfo.customer_zip.$errors[0].$message }}
</span>
</div>
</div>
</form>
</div>
</div>
<!-- SECTION 3: Delivery Details -->
<div>
<h2 class="text-lg font-bold">Delivery Details</h2>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Fill Location -->
<div class="form-control">
<label class="label"><span class="label-text">Fill Location</span></label>
<input v-model="CreateCustomerForm.basicInfo.customer_fill_location" type="text" placeholder="e.g., Left side of house" class="input input-bordered input-sm w-full" />
</div>
<!-- Description -->
<div class="form-control md:col-span-2">
<label class="label"><span class="label-text">Description / Notes</span></label>
<textarea v-model="CreateCustomerForm.basicInfo.customer_description" rows="4" placeholder="Description of customer's house, tank, etc." class="textarea textarea-bordered"></textarea>
</div>
</div>
</div>
<!-- SUBMIT BUTTON -->
<div class="pt-4">
<button type="submit" class="btn btn-primary btn-sm">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
<Footer />
</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 useValidate from "@vuelidate/core";
import { email, minLength, required } from "@vuelidate/validators";
// --- NEW: Interface for select options for better type safety ---
interface SelectOption {
text: string;
value: number;
}
export default defineComponent({
name: 'CustomerEdit',
components: {
Header,
SideBar,
// Removed unused Header and SideBar
Footer,
},
@@ -199,8 +179,8 @@ export default defineComponent({
v$: useValidate(),
user: null,
stateList: [],
custList: [],
stateList: [] as SelectOption[],
custList: [] as SelectOption[],
customer: {
id: 0,
user_id: 0,
@@ -229,16 +209,17 @@ export default defineComponent({
customer_first_name: "",
customer_town: "",
customer_apt: "",
customer_home_type: "",
// --- FIX: Initialized as a number ---
customer_home_type: 0,
customer_zip: "",
customer_automatic: false,
customer_email: "",
customer_phone_number: "",
customer_state: "",
// --- FIX: Initialized as a number ---
customer_state: 0,
customer_address: "",
customer_description: "",
customer_fill_location: 0,
},
},
}
@@ -252,7 +233,7 @@ export default defineComponent({
customer_town: { required, minLength: minLength(1) },
customer_home_type: { required },
customer_zip: { required, minLength: minLength(5) },
customer_email: { email, required },
customer_email: { email }, // Removed required to match template label "Optional"
customer_phone_number: { required },
customer_state: { required },
customer_address: { required },
@@ -276,7 +257,6 @@ export default defineComponent({
} else {
this.CreateCustomerForm.basicInfo.customer_phone_number = '';
}
},
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
@@ -302,14 +282,11 @@ export default defineComponent({
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customerDescription = response.data
this.CreateCustomerForm.basicInfo.customer_description = this.customerDescription.description;
this.CreateCustomerForm.basicInfo.customer_fill_location = this.customerDescription.fill_location
})
},
// gets the item from parameter router
getCustomer(userid: any) {
let path = import.meta.env.VITE_BASE_URL + "/customer/" + userid;
axios({
@@ -320,47 +297,29 @@ export default defineComponent({
})
.then((response: any) => {
if (response.data) {
this.customer = response.data;
this.getCustomerDescription(this.customer.id)
this.getCustomerDescription(this.customer.id);
this.CreateCustomerForm.basicInfo.customer_last_name = response.data.customer_last_name;
this.CreateCustomerForm.basicInfo.customer_first_name = response.data.customer_first_name;
this.CreateCustomerForm.basicInfo.customer_town = response.data.customer_town;
this.CreateCustomerForm.basicInfo.customer_state = response.data.customer_state;
this.CreateCustomerForm.basicInfo.customer_zip = response.data.customer_zip;
this.CreateCustomerForm.basicInfo.customer_phone_number = response.data.customer_phone_number;
this.CreateCustomerForm.basicInfo.customer_home_type = response.data.customer_home_type;
this.CreateCustomerForm.basicInfo.customer_apt = response.data.customer_apt;
this.CreateCustomerForm.basicInfo.customer_email = response.data.customer_email;
this.CreateCustomerForm.basicInfo.customer_address = response.data.customer_address;
if (response.data.customer_automatic === 1) {
this.CreateCustomerForm.basicInfo.customer_automatic = true
}
if (response.data.customer_automatic === 0) {
this.CreateCustomerForm.basicInfo.customer_automatic = false
}
}
})
},
editItem(payload: {
customer_last_name: string;
customer_first_name: string;
customer_apt: string;
customer_town: string;
customer_zip: string;
customer_email: string;
customer_phone_number: string;
customer_home_type: string,
customer_state: string;
customer_address: string;
customer_description: string;
customer_fill_location: number;
}) {
editItem(payload: any) { // Simplified payload type for brevity
let path = import.meta.env.VITE_BASE_URL + "/customer/edit/" + this.customer.id;
axios({
method: "put",
@@ -372,61 +331,31 @@ export default defineComponent({
.then((response: any) => {
if (response.data.ok) {
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
}
;
if (response.data.error) {
} else if (response.data.error) {
// Handle specific errors if needed
this.$router.push("/");
}
;
})
},
onSubmit() {
let payload = {
customer_last_name: this.CreateCustomerForm.basicInfo.customer_last_name,
customer_first_name: this.CreateCustomerForm.basicInfo.customer_first_name,
customer_town: this.CreateCustomerForm.basicInfo.customer_town,
customer_zip: this.CreateCustomerForm.basicInfo.customer_zip,
customer_email: this.CreateCustomerForm.basicInfo.customer_email,
customer_phone_number: this.CreateCustomerForm.basicInfo.customer_phone_number,
customer_home_type: this.CreateCustomerForm.basicInfo.customer_home_type,
customer_apt: this.CreateCustomerForm.basicInfo.customer_apt,
customer_state: this.CreateCustomerForm.basicInfo.customer_state,
customer_address: this.CreateCustomerForm.basicInfo.customer_address,
customer_fill_location: this.CreateCustomerForm.basicInfo.customer_fill_location,
customer_description: this.CreateCustomerForm.basicInfo.customer_description,
};
this.editItem(payload);
// Create payload directly from the form object
this.editItem(this.CreateCustomerForm.basicInfo);
},
getCustomerTypeList() {
let path = import.meta.env.VITE_BASE_URL + "/query/customertype";
axios({
method: "get",
url: path,
withCredentials: true,
})
axios.get(path, { withCredentials: true })
.then((response: any) => {
this.custList = response.data;
})
.catch(() => {
});
},
getStatesList() {
let path = import.meta.env.VITE_BASE_URL + "/query/states";
axios({
method: "get",
url: path,
withCredentials: true,
})
axios.get(path, { withCredentials: true })
.then((response: any) => {
this.stateList = response.data;
})
.catch(() => {
});
},
},
})
</script>
<style scoped></style>
<script setup lang="ts">
</script>

View File

@@ -1,107 +1,123 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 pb-10">
<div class="w-full px-4 md:px-10 ">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'customer' }">
Customers
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Customers</li>
</ul>
</div>
<h1 class="text-3xl font-bold mt-4">Customers</h1>
<div class="flex justify-end mb-10">
Customers {{ customer_count }}
</div>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Search, Count, and Add Button -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<!-- SEARCH AND COUNT (IMPROVED ALIGNMENT) -->
<div class="form-control">
<label class="label pt-1 pb-0">
<span class="label-text-alt">{{ customer_count }} customers found</span>
</label>
</div>
<router-link to="/customers/create" class="btn btn-primary btn-sm">
Add New Customer
</router-link>
</div>
<div class="col-span-12 bg-secondary">
<div class="grid grid-cols-12 p-5 bg-neutral m-5">
<div class="col-span-12 font-bold text-xl">Quick Tips</div>
<div class="col-span-3 py-2"> @ = last name search</div>
<div class="col-span-3 py-2"> ! = address</div>
<div class="col-span-3 py-2"> $ = account number</div>
<div class="divider"></div>
<!-- DESKTOP VIEW: Table (Now breaks at XL) -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Account #</th>
<th>Name</th>
<th>Town</th>
<th>Automatic</th>
<th>Phone Number</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="person in customers" :key="person.id" class="hover:bg-blue-600 hover:text-white">
<td>
<router-link :to="{ name: 'customerProfile', params: { id: person.id } }" class="link link-hover">
{{ person.account_number }}
</router-link>
</td>
<td>{{ person.customer_first_name }} {{ person.customer_last_name }}</td>
<td>{{ person.customer_town }}</td>
<td><span :class="person.customer_automatic ? 'text-success' : 'text-gray-500'">{{ person.customer_automatic ? 'Yes' : 'No' }}</span></td>
<td>{{ person.customer_phone_number }}</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'deliveryCreate', params: { id: person.id } }" class="btn btn-sm btn-primary">
New Delivery
</router-link>
<router-link :to="{ name: 'CalenderCustomer', params: { id: person.id } }" class="btn btn-sm btn-accent">
New Service
</router-link>
<router-link :to="{ name: 'customerEdit', params: { id: person.id } }" class="btn btn-sm btn-secondary">
Edit
</router-link>
<router-link :to="{ name: 'customerProfile', params: { id: person.id } }" class="btn btn-sm btn-ghost">
View
</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards (Now breaks at XL) -->
<div class="xl:hidden space-y-4">
<div v-for="person in customers" :key="person.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ person.customer_first_name }} {{ person.customer_last_name }}</h2>
<p class="text-xs text-gray-400">#{{ person.account_number }}</p>
</div>
<div class="badge" :class="person.customer_automatic ? 'badge-success' : 'badge-ghost'">
{{ person.customer_automatic ? 'Automatic' : 'Will Call' }}
</div>
</div>
<div class="text-sm mt-2">
<p>{{ person.customer_town }}</p>
<p>{{ person.customer_phone_number }}</p>
</div>
<div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'deliveryCreate', params: { id: person.id } }" class="btn btn-sm btn-primary">
New Delivery
</router-link>
<router-link :to="{ name: 'CalenderCustomer', params: { id: person.id } }" class="btn btn-sm btn-accent">
New Service
</router-link>
<router-link :to="{ name: 'customerEdit', params: { id: person.id } }" class="btn btn-sm btn-secondary">
Edit
</router-link>
<router-link :to="{ name: 'customerProfile', params: { id: person.id } }" class="btn btn-sm btn-ghost">
View
</router-link>
</div>
</div>
</div>
</div>
<div class="overflow-x-auto bg-neutral font-bold">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Account Number</th>
<th>Name</th>
<th>Town</th>
<th>Automatic</th>
<th>Phone Number</th>
<th></th>
</tr>
</thead>
<tbody>
<!-- row 1 -->
<tr v-for="person in customers" :key="person['id']" >
<td>
<router-link :to="{ name: 'customerProfile', params: { id: person['id'] } }">{{ person['account_number'] }}
</router-link>
</td>
<td>
<router-link :to="{ name: 'customerProfile', params: { id: person['id'] } }">
{{ person['customer_first_name'] }} {{ person['customer_last_name'] }}
</router-link>
</td>
<td>{{ person['customer_town'] }}</td>
<td>
<div v-if="person['customer_automatic'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>{{ person['customer_phone_number'] }}</td>
<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
</router-link>
<router-link :to="{ name: 'CalenderCustomer', params: { id: person['id'] } }"
class="btn-sm btn bg-indigo-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
</router-link>
<router-link :to="{ name: 'customerProfile', params: { id: person['id'] } }"
class="btn btn-secondary btn-sm">
View Profile
</router-link>
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-10">
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="customer_count" v-model="page" :per-page="10" :options="options">
</pagination>
<div class="flex justify-center mb-10"> {{ customer_count }} items Found</div>
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'
@@ -125,7 +141,7 @@ export default defineComponent({
return {
token: null,
user: null,
customers: [],
customers: [] as any[],
customer_count: 0,
page: 1,
perPage: 50,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,602 @@
<!-- src/views/Profile.vue -->
<template>
<div class="w-full min-h-screen bg-base-200 px-4 md:px-10">
<!-- ... breadcrumbs ... -->
<div v-if="customer && customer.id" class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- FIX: Changed `lg:` to `xl:` for a later breakpoint -->
<div class="grid grid-cols-1 xl:grid-cols-12 gap-6">
<!-- FIX: Changed `lg:` to `xl:` -->
<div class="xl:col-span-8 space-y-6">
<div class="grid grid-cols-1 xl:grid-cols-12 gap-6">
<ProfileMap
class="xl:col-span-7"
:customer="customer"
/>
<ProfileSummary
class="xl:col-span-5"
:customer="customer"
:automatic_status="automatic_status"
@toggle-automatic="userAutomatic"
/>
</div>
<AutomaticDeliveries v-if="automatic_status === 1 && autodeliveries.length > 0" :deliveries="autodeliveries" />
<HistoryTabs
:deliveries="deliveries"
:service-calls="serviceCalls"
@open-service-modal="openEditModal"
/>
</div>
<!-- FIX: Changed `lg:` to `xl:` -->
<div class="xl:col-span-4 space-y-6">
<CustomerComments
:comments="comments"
@add-comment="onSubmitSocial"
@delete-comment="deleteCustomerSocial"
/>
<CustomerStats :stats="customer_stats" :last_delivery="customer_last_delivery" />
<TankInfo :customer_id="customer.id" :tank="customer_tank" :description="customer_description" />
<EquipmentParts :parts="currentParts" @open-parts-modal="openPartsModal" />
<CreditCards
:cards="credit_cards"
:count="credit_cards_count"
:user_id="customer.user_id"
@edit-card="editCard"
@remove-card="removeCard"
/>
</div>
</div>
</div>
<!-- A loading indicator is shown while the API call is in progress -->
<div v-else class="flex justify-center items-center mt-20">
<span class="loading loading-spinner loading-lg"></span>
</div>
<!-- The Footer can be placed here if it's specific to this page -->
<Footer />
</div>
<!-- Modals remain at the root of the template for proper display -->
<ServiceEditModal
v-if="selectedServiceForEdit"
:service="selectedServiceForEdit"
@close-modal="closeEditModal"
@save-changes="handleSaveChanges"
@delete-service="handleDeleteService"
/>
<PartsEditModal
v-if="isPartsModalOpen && currentParts"
:customer-id="customer.id"
:existing-parts="currentParts"
@close-modal="closePartsModal"
@save-parts="handleSaveParts"
/>
</template>
<script lang="ts">
// --- SCRIPT REMAINS EXACTLY THE SAME AS YOUR ORIGINAL FILE ---
// All data properties, computed, methods, and imports are kept here.
// No changes are needed in the script block.
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 { notify } from "@kyvg/vue3-notification";
import "leaflet/dist/leaflet.css";
import L from 'leaflet';
import iconUrl from 'leaflet/dist/images/marker-icon.png';
import shadowUrl from 'leaflet/dist/images/marker-shadow.png';
import { LMap, LTileLayer } from "@vue-leaflet/vue-leaflet";
import dayjs from 'dayjs';
import ServiceEditModal from '../../service/ServiceEditModal.vue';
import PartsEditModal from '../service/PartsEditModal.vue';
// Import new child components
import ProfileMap from './profile/ProfileMap.vue';
import ProfileSummary from './profile/ProfileSummary.vue';
import CustomerStats from './profile/CustomerStats.vue';
import TankInfo from './profile/TankInfo.vue';
import EquipmentParts from './profile/EquipmentParts.vue';
import CreditCards from './profile/CreditCards.vue';
import CustomerComments from './profile/CustomerComments.vue';
import AutomaticDeliveries from './profile/AutomaticDeliveries.vue';
import HistoryTabs from './profile/HistoryTabs.vue';
L.Icon.Default.mergeOptions({
iconUrl: iconUrl,
shadowUrl: shadowUrl,
});
interface Delivery {
id: number;
delivery_status: number;
customer_name: string;
customer_asked_for_fill: number | boolean;
gallons_ordered: number | string;
gallons_delivered: number | string | null;
expected_delivery_date: string;
}
interface AutomaticDelivery {
id: number;
customer_full_name: string;
gallons_delivered: number | string;
fill_date: string;
}
interface CreditCard {
id: number;
main_card: boolean;
type_of_card: string;
name_on_card: string;
card_number: string;
expiration_month: number;
expiration_year: string | number;
zip_code: string;
security_number: string;
}
// You already have these, just make sure they exist
interface ServiceCall {
id: number;
scheduled_date: string;
customer_name: string;
customer_address: string;
customer_town: string;
type_service_call: number;
description: string;
}
interface ServiceParts {
id?: number;
customer_id: number;
oil_filter: string;
oil_filter_2: string;
oil_nozzle: string;
oil_nozzle_2: string;
}
export default defineComponent({
name: 'CustomerProfile',
components: {
Header,
SideBar,
Footer,
LMap,
LTileLayer,
ServiceEditModal,
PartsEditModal,
// Register new components
ProfileMap,
ProfileSummary,
CustomerStats,
TankInfo,
EquipmentParts,
CreditCards,
CustomerComments,
AutomaticDeliveries,
HistoryTabs,
},
data() {
return {
zoom: 14,
user: null as { user_id: number; user_name: string; confirmed: string; } | null,
automatic_status: 0,
customer_last_delivery: '',
comments: [ { id: 0, created: '', customer_id: 0, poster_employee_id: 0, comment: '' } ],
CreateSocialForm: { basicInfo: { comment: '' } },
// --- UPDATE THESE LINES ---
credit_cards: [] as CreditCard[],
deliveries: [] as Delivery[],
autodeliveries: [] as AutomaticDelivery[],
serviceCalls: [] as ServiceCall[],
// --- END OF UPDATES ---
automatic_response: 0,
credit_cards_count: 0,
customer: { id: 0, user_id: 0, customer_first_name: '', customer_last_name: '', customer_town: '', customer_address: '', customer_state: 0, customer_zip: '', customer_apt: '', customer_home_type: 0, customer_phone_number: '', customer_latitude: 0, customer_longitude: 0, correct_address: true, account_number: '' },
customer_description: { id: 0, customer_id: 0, account_number: '', company_id: '', fill_location: 0, description: '' },
customer_tank: { id: 0, last_tank_inspection: null, tank_status: false, outside_or_inside: false, tank_size: 0 },
customer_stats: { id: 0, customer_id: 0, total_calls: 0, service_calls_total: 0, service_calls_total_spent: 0, service_calls_total_profit: 0, oil_deliveries: 0, oil_total_gallons: 0, oil_total_spent: 0, oil_total_profit: 0 },
delivery_page: 1,
selectedServiceForEdit: null as ServiceCall | null,
isPartsModalOpen: false,
currentParts: null as ServiceParts | null,
}
},
computed: {
hasPartsData() {
if (!this.currentParts) return false;
return !!(this.currentParts.oil_filter || this.currentParts.oil_filter_2 || this.currentParts.oil_nozzle || this.currentParts.oil_nozzle_2);
}
},
created() {
this.getCustomer(this.$route.params.id);
},
mounted() {
// getPage is now called from within getCustomer, so this can be removed if it's redundant
},
watch: {
'$route.params.id'(newId) {
if (newId) {
this.getCustomer(newId);
}
},
},
methods: {
// ALL YOUR METHODS from the original file go here without any changes.
// getCustomer, userStatus, userAutomatic, etc...
getPage: function (page: any) {
if (this.customer && this.customer.id) {
this.getCustomerDelivery(this.customer.id, page);
}
},
getCustomer(userid: any) {
if (!userid) return;
let path = import.meta.env.VITE_BASE_URL + '/customer/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customer = response.data;
// --- DEPENDENT API CALLS ---
this.userStatus();
// FIX: Pass the correct ID for payment-related calls
this.getCreditCards(this.customer.id);
this.getCreditCardsCount(this.customer.id);
// These other calls are likely correct as they are customer-specific
this.getCustomerSocial(this.customer.id, 1);
this.getPage(this.delivery_page);
this.checktotalOil(this.customer.id);
this.getCustomerTank(this.customer.id);
this.userAutomaticStatus(this.customer.id);
this.getCustomerDescription(this.customer.id);
this.getCustomerStats(this.customer.id);
this.getCustomerLastDelivery(this.customer.id);
this.getServiceCalls(this.customer.id);
this.fetchCustomerParts(this.customer.id);
}).catch((error: any) => {
console.error("CRITICAL: Failed to fetch main customer data. Aborting other calls.", 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 });
},
userAutomaticStatus(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/customer/automatic/status/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.automatic_status = response.data.status
if (this.automatic_status === 1){
this.getCustomerAutoDelivery(this.customer.id)
}
this.checktotalOil(this.customer.id)
})
},
userAutomatic(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/customer/automatic/assign/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.automatic_response = response.data.status
if (this.automatic_response == 1) {
this.$notify({ title: "Automatic Status", text: 'Customer is now Automatic Customer', type: 'Success' });
} else if (this.automatic_response == 2) {
this.$notify({ title: "Automatic Status", text: 'Customer does not have a main credit card. Can not make automatic.', type: 'Error' });
} else if (this.automatic_response == 3) {
this.$notify({ title: "Automatic Status", text: 'Customer is now a Call in ', type: 'Info' });
} else {
this.$notify({ title: "Automatic Status", text: 'Customer is now Manual Customer', type: 'Warning' });
}
this.getCustomer(this.$route.params.id);
})
},
getNozzleColor(nozzleString: string): string {
if (!nozzleString || typeof nozzleString !== 'string') return '';
const firstChar = nozzleString.trim().toLowerCase().charAt(0);
switch (firstChar) {
case 'a': return '#EF4444';
case 'b': return '#3B82F6';
case 'w': return '#16a34a';
default: return 'inherit';
}
},
getCustomerLastDelivery(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/stats/user/lastdelivery/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customer_last_delivery = response.data.date
})
},
getCustomerStats(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/stats/user/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customer_stats = response.data
})
},
checktotalOil(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/stats/gallons/check/total/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
})
},
getCustomerDescription(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/customer/description/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customer_description = response.data
})
},
getCustomerTank(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/customer/tank/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customer_tank = response.data
})
},
getCreditCards(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/payment/cards/' + user_id;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.credit_cards = response.data
})
},
getCreditCardsCount(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/payment/cards/onfile/' + user_id;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.credit_cards_count = response.data.cards
})
},
getCustomerAutoDelivery(userid: any) {
let path = import.meta.env.VITE_AUTO_URL + '/delivery/all/profile/' + userid ;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.autodeliveries = response.data
})
},
getCustomerDelivery(userid: any, delivery_page: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/customer/' + userid + '/' + delivery_page;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.deliveries = response.data
})
},
editCard(card_id: any) {
this.$router.push({ name: "cardedit", params: { id: card_id } });
},
removeCard(card_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/payment/card/remove/' + card_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then(() => {
this.getCreditCards(this.customer.user_id)
this.getCreditCardsCount(this.customer.user_id)
notify({ title: "Card Status", text: "Card Removed", type: "Success" });
})
},
deleteCall(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({ title: "Success", text: "deleted delivery", type: "success" });
this.getPage(1)
} else {
notify({ title: "Failure", text: "error deleting delivery", type: "success" });
}
})
},
deleteCustomerSocial(comment_id: number) {
let path = import.meta.env.VITE_BASE_URL + '/social/delete/' + comment_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
console.log(response)
this.getCustomerSocial(this.customer.id, 1)
})
},
getCustomerSocial(userid: any, delivery_page: any) {
let path = import.meta.env.VITE_BASE_URL + '/social/posts/' + userid + '/' + delivery_page;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.comments = response.data
})
},
CreateSocialComment(payload: { comment: string; poster_employee_id: number }) {
let path = import.meta.env.VITE_BASE_URL + "/social/create/" + this.customer.id;
axios({
method: "post",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.getCustomerSocial(this.customer.id, 1)
}
if (response.data.error) {
this.$router.push("/");
}
})
},
onSubmitSocial(commentText: string) {
if (!this.user) {
console.error("Cannot submit comment: user is not logged in.");
return;
}
let payload = { comment: commentText, poster_employee_id: this.user.user_id };
this.CreateSocialComment(payload);
},
getServiceCalls(customerId: number) {
let path = `${import.meta.env.VITE_BASE_URL}/service/for-customer/${customerId}`;
axios({
method: 'get',
url: path,
headers: authHeader(),
withCredentials: true,
}).then((response: any) => {
this.serviceCalls = response.data;
}).catch((error: any) => {
console.error("Failed to get customer service calls:", error);
});
},
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}`;
await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
this.getServiceCalls(this.customer.id);
this.closeEditModal();
} catch (error) {
console.error("Failed to save service call changes:", error);
}
},
async handleDeleteService(serviceId: number) {
if (!window.confirm("Are you sure you want to delete this service call?")) return;
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.getServiceCalls(this.customer.id);
this.closeEditModal();
notify({ title: "Success", text: "Service call deleted!", type: "success" });
}
} catch (error) {
console.error("Failed to delete service call:", error);
}
},
async fetchCustomerParts(customerId: number) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/parts/customer/${customerId}`;
const response = await axios.get(path, { headers: authHeader() });
this.currentParts = response.data;
} catch (error) {
console.error("Failed to fetch customer parts:", error);
notify({ title: "Error", text: "Could not fetch equipment parts.", type: "error" });
}
},
openPartsModal() {
if (this.currentParts) {
this.isPartsModalOpen = true;
} else {
notify({ title: "Info", text: "Parts data still loading, please wait.", type: "info" });
}
},
closePartsModal() {
this.isPartsModalOpen = false;
},
async handleSaveParts(partsToSave: ServiceParts) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/parts/update/${partsToSave.customer_id}`;
const response = await axios.post(path, partsToSave, { headers: authHeader() });
if(response.data.ok) {
this.currentParts = partsToSave;
notify({ title: "Success", text: "Equipment parts saved successfully!", type: "success" });
}
this.closePartsModal();
} catch (error) {
console.error("Failed to save parts:", error);
notify({ title: "Error", text: "Failed to save equipment parts.", type: "error" });
}
},
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';
}
},
})
</script>

View File

@@ -0,0 +1,46 @@
<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4 sm:p-6">
<h2 class="card-title">Automatic Delivery History</h2>
<div class="divider my-2"></div>
<div class="overflow-x-auto">
<table class="table table-sm w-full">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Gallons</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<tr v-for="auto in deliveries" :key="auto.id" class="hover">
<td>{{ auto.id }}</td>
<td>{{ auto.customer_full_name }}</td>
<td>{{ auto.gallons_delivered }}</td>
<td>{{ auto.fill_date }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 1. Define the AutomaticDelivery interface
interface AutomaticDelivery {
id: number;
customer_full_name: string;
gallons_delivered: number | string;
fill_date: string;
}
// 2. Define Props using the interface
interface Props {
deliveries: AutomaticDelivery[];
}
// 3. Use the typed defineProps
defineProps<Props>();
</script>

View File

@@ -0,0 +1,78 @@
<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4 sm:p-6">
<div class="card-title flex justify-between items-center">
<h2>Credit Cards</h2>
<router-link :to="{ name: 'cardadd', params: { id: user_id } }">
<button class="btn btn-xs btn-outline btn-success">Add New</button>
</router-link>
</div>
<div class="mt-2 text-sm">
<div v-if="count === 0" class="text-warning font-semibold">
No cards on file. This is a Cash/Check customer until a card is added.
</div>
<div v-else class="text-success font-semibold">
{{ count }} card(s) on file.
</div>
</div>
<div class="mt-4 space-y-4">
<div v-for="card in cards" :key="card.id"
class="p-4 rounded-lg border"
:class="card.main_card ? 'bg-primary/10 border-primary' : 'bg-base-200 border-base-300'">
<div class="flex justify-between items-start">
<div>
<div class="font-bold">{{ card.name_on_card }}</div>
<div class="text-xs opacity-70">{{ card.type_of_card }}</div>
</div>
<div v-if="card.main_card" class="badge badge-primary badge-sm">Primary</div>
</div>
<div class="mt-3 text-sm font-mono tracking-wider">
<p>{{ card.card_number }}</p>
<p>
Exp:
<span v-if="card.expiration_month < 10">0</span>{{ card.expiration_month }} / {{ card.expiration_year }}
</p>
</div>
<div class="divider my-2"></div>
<div class="flex justify-end gap-2">
<a @click.prevent="$emit('edit-card', card.id)" class="link link-hover text-xs">Edit</a>
<a @click.prevent="$emit('remove-card', card.id)" class="link link-hover text-error text-xs">Remove</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 1. Define the interface for a single credit card object
interface CreditCard {
id: number;
main_card: boolean;
type_of_card: string;
name_on_card: string;
card_number: string;
expiration_month: number;
expiration_year: string | number;
zip_code: string;
security_number: string;
}
// 2. Define the interface for the component's props
interface Props {
cards: CreditCard[];
count: number;
user_id: number;
}
// 3. Use the generic defineProps to apply the types
defineProps<Props>();
defineEmits(['edit-card', 'remove-card']);
</script>

View File

@@ -0,0 +1,59 @@
<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4 sm:p-6">
<h2 class="card-title">Comments & Notes</h2>
<!-- Styled Form for Adding a New Comment -->
<form class="mt-4" @submit.prevent="handleSubmit">
<div class="form-control">
<textarea v-model="commentText" class="textarea textarea-bordered" rows="3" placeholder="Add a new note..."></textarea>
<button type="submit" class="btn btn-primary btn-sm mt-2 self-end">Post Comment</button>
</div>
</form>
<div class="divider"></div>
<!-- Styled List of Existing Comments -->
<div class="mt-2 space-y-4">
<div v-if="comments.length === 0" class="text-center text-sm opacity-60 py-4">
No comments yet.
</div>
<div v-else v-for="comment in comments" :key="comment.id" class="bg-base-200 rounded-lg p-3">
<div class="flex justify-between items-center text-xs opacity-70">
<!-- You can display the user/employee who posted it here if you have the data -->
<span class="font-semibold">{{ comment.created }}</span>
<button @click="$emit('delete-comment', comment.id)" class="btn btn-ghost btn-xs text-error">Delete</button>
</div>
<p class="mt-2 text-sm whitespace-pre-wrap">{{ comment.comment }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
interface Comment {
id: number;
created: string;
comment: string;
}
interface Props {
comments: Comment[];
}
defineProps<Props>();
const emit = defineEmits(['add-comment', 'delete-comment']);
const commentText = ref('');
const handleSubmit = () => {
if (commentText.value.trim()) {
emit('add-comment', commentText.value);
commentText.value = ''; // Clear the textarea after submission
}
};
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4 sm:p-6">
<div class="flex justify-between items-center">
<h2 class="card-title">Customer Details</h2>
<span class="badge" :class="automatic_status === 1 ? 'badge-success' : 'badge-ghost'">
{{ automatic_status === 1 ? 'Automatic' : 'Will Call' }}
</span>
</div>
<div class="text-error font-semibold mt-2" v-if="!customer.correct_address">
Possible Incorrect Address!
</div>
<div class="mt-4 space-y-2 text-sm">
<p><strong>{{ customer.customer_first_name }} {{ customer.customer_last_name }}</strong></p>
<p>{{ customer.customer_address }}<span v-if="customer.customer_apt">, {{ customer.customer_apt }}</span></p>
<p>{{ customer.customer_town }}, {{ stateName(customer.customer_state) }} {{ customer.customer_zip }}</p>
<p class="pt-2">{{ customer.customer_phone_number }}</p>
<p><span class="badge badge-outline badge-sm">{{ homeTypeName(customer.customer_home_type) }}</span></p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
customer: { type: Object, required: true },
automatic_status: { type: Number, required: true },
});
const stateName = (id: number) => ['MA', 'RI', 'NH', 'ME', 'VT', 'CT', 'NY'][id] || 'N/A';
const homeTypeName = (id: number) => ['Residential', 'Apartment', 'Condo', 'Commercial', 'Business', 'Construction', 'Container'][id] || 'Unknown';
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4 sm:p-6">
<h2 class="card-title">Stats</h2>
<div class="text-sm mt-2 space-y-1">
<div class="flex justify-between"><span>Total Deliveries:</span> <strong>{{ stats.oil_deliveries }}</strong></div>
<div class="flex justify-between"><span>Total Gallons:</span> <strong>{{ stats.oil_total_gallons }}</strong></div>
<div class="flex justify-between"><span>Total Service Calls:</span> <strong>{{ stats.total_calls }}</strong></div>
<div class="flex justify-between"><span>Last Delivery:</span> <strong>{{ last_delivery || 'N/A' }}</strong></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
stats: { type: Object, required: true },
last_delivery: { type: String, default: '' },
});
</script>

View File

@@ -0,0 +1,88 @@
<template>
<div v-if="!deliveries || deliveries.length === 0" class="text-center p-10">
<p>No will-call delivery history found.</p>
</div>
<div v-else>
<!-- DESKTOP TABLE -->
<div class="overflow-x-auto hidden lg:block">
<table class="table table-sm w-full">
<thead>
<tr>
<th>ID</th>
<th>Status</th>
<th>Name</th>
<th>Gallons</th>
<th>Date</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="oil in deliveries" :key="oil.id" class="hover:bg-blue-600 hover:text-white">
<td>{{ oil.id }}</td>
<td><span class="badge badge-sm" :class="statusClass(oil.delivery_status)">{{ deliveryStatus(oil.delivery_status) }}</span></td>
<td>{{ oil.customer_name }}</td>
<td>
<span v-if="oil.delivery_status !== 10">
{{ oil.customer_asked_for_fill ? 'FILL' : oil.gallons_ordered }}
</span>
<span v-else>{{ oil.gallons_delivered }}</span>
</td>
<td>{{ oil.expected_delivery_date }}</td>
<td class="text-right">
<div class="flex items-center justify-end gap-1">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-xs btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-xs btn-secondary">Edit</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-xs btn-success">Print</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE CARDS -->
<div class="lg:hidden space-y-4">
<div v-for="oil in deliveries" :key="oil.id" class="card card-compact bg-base-200 shadow ">
<div class="card-body">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ oil.customer_name }}</h2>
<p class="text-xs opacity-70">#{{ oil.id }} on {{ oil.expected_delivery_date }}</p>
</div>
<div class="badge badge-sm" :class="statusClass(oil.delivery_status)">{{ deliveryStatus(oil.delivery_status) }}</div>
</div>
<p class="text-sm">Gallons: <strong>{{ oil.customer_asked_for_fill ? 'FILL' : (oil.gallons_delivered || oil.gallons_ordered) }}</strong></p>
<div class="card-actions justify-end mt-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-xs btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-xs btn-secondary">Edit</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-xs btn-success">Print</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 1. Define the shape of a single delivery object
interface Delivery {
id: number;
delivery_status: number;
customer_name: string;
customer_asked_for_fill: number | boolean;
gallons_ordered: number | string;
gallons_delivered: number | string | null;
expected_delivery_date: string;
}
// 2. Define the props using the interface
interface Props {
deliveries: Delivery[];
}
// 3. Use the generic version of defineProps to apply the types
defineProps<Props>();
const deliveryStatus = (s: number) => ({0:'Waiting',1:'Cancelled',2:'Out',3:'Tomorrow',5:'Issue',10:'Finalized'}[s] || 'N/A');
const statusClass = (s: number) => ({0:'badge-warning',1:'badge-error',2:'badge-info',3:'badge-ghost',5:'badge-error',10:'badge-success'}[s] || '');
</script>

View File

@@ -0,0 +1,80 @@
<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4 sm:p-6">
<div class="card-title flex justify-between items-center">
<h2>Equipment Parts</h2>
<button @click="$emit('open-parts-modal')" class="btn btn-xs btn-outline btn-success">
Edit
</button>
</div>
<!-- v-if="parts" correctly handles the null case, preventing errors below -->
<div v-if="parts" class="mt-2 text-sm">
<div v-if="hasPartsData" class="grid grid-cols-2 gap-x-4 gap-y-2">
<div v-if="parts.oil_filter">
<div class="font-bold">Oil Filter 1:</div>
<div>{{ parts.oil_filter }}</div>
</div>
<div v-if="parts.oil_filter_2">
<div class="font-bold">Oil Filter 2:</div>
<div>{{ parts.oil_filter_2 }}</div>
</div>
<div v-if="parts.oil_nozzle">
<div class="font-bold">Oil Nozzle 1:</div>
<div :style="{ color: getNozzleColor(parts.oil_nozzle), fontWeight: 'bold' }">{{ parts.oil_nozzle }}</div>
</div>
<div v-if="parts.oil_nozzle_2">
<div class="font-bold">Oil Nozzle 2:</div>
<div :style="{ color: getNozzleColor(parts.oil_nozzle_2), fontWeight: 'bold' }">{{ parts.oil_nozzle_2 }}</div>
</div>
</div>
<div v-else>
<p class="text-xs opacity-70">No equipment parts information available.</p>
</div>
</div>
<div v-else class="text-xs opacity-70 mt-2">
Loading parts info...
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
// 1. Define the interface for the parts object.
interface ServiceParts {
id?: number;
customer_id: number;
oil_filter: string;
oil_filter_2: string;
oil_nozzle: string;
oil_nozzle_2: string;
}
// 2. Define the Props interface, explicitly allowing 'null'.
interface Props {
parts: ServiceParts | null;
}
// 3. Use the typed defineProps and assign to a const.
const props = defineProps<Props>();
defineEmits(['open-parts-modal']);
const hasPartsData = computed(() => {
if (!props.parts) return false;
return !!(props.parts.oil_filter || props.parts.oil_filter_2 || props.parts.oil_nozzle || props.parts.oil_nozzle_2);
});
const getNozzleColor = (nozzleString: string): string => {
if (!nozzleString) return 'inherit';
const firstChar = nozzleString.trim().toLowerCase().charAt(0);
switch (firstChar) {
case 'a': return '#EF4444'; // Red
case 'b': return '#3B82F6'; // Blue
case 'w': return '#16a34a'; // Green
default: return 'inherit';
}
};
</script>

View File

@@ -0,0 +1,54 @@
<template>
<div role="tablist" class="tabs tabs-lifted">
<a role="tab" class="tab [--tab-bg:oklch(var(--b1))] text-base-content" :class="{ 'tab-active': activeTab === 'deliveries' }" @click="activeTab = 'deliveries'">
Will-Call Deliveries
</a>
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 rounded-box p-4" v-show="activeTab === 'deliveries'">
<DeliveriesTable :deliveries="deliveries" />
</div>
<a role="tab" class="tab [--tab-bg:oklch(var(--b1))] text-base-content" :class="{ 'tab-active': activeTab === 'service' }" @click="activeTab = 'service'">
Service History
</a>
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 rounded-box p-4" v-show="activeTab === 'service'">
<ServiceCallsTable :service-calls="serviceCalls" @open-service-modal="(service) => $emit('openServiceModal', service)" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import DeliveriesTable from './DeliveriesTable.vue';
import ServiceCallsTable from './ServiceCallsTable.vue';
// 1. Define the interfaces for the data this component receives and passes down.
// These should match the interfaces in the child components.
interface Delivery {
id: number;
delivery_status: number;
customer_name: string;
customer_asked_for_fill: number | boolean;
gallons_ordered: number | string;
gallons_delivered: number | string | null;
expected_delivery_date: string;
}
interface ServiceCall {
id: number;
scheduled_date: string;
type_service_call: number;
description: string;
}
// 2. Define the Props interface
interface Props {
deliveries: Delivery[];
serviceCalls: ServiceCall[];
}
// 3. Use the typed defineProps
defineProps<Props>();
defineEmits(['openServiceModal']);
const activeTab = ref('deliveries');
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4 sm:p-6">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-2">
<h2 class="card-title text-2xl">{{ customer.account_number }}</h2>
<div class="flex flex-wrap gap-2 justify-start sm:justify-end">
<router-link :to="{ name: 'deliveryCreate', params: { id: customer.id } }" class="btn btn-sm btn-primary">New Delivery</router-link>
<router-link :to="{ name: 'CalenderCustomer', params: { id: customer.id } }" class="btn btn-sm btn-info">New Service</router-link>
<router-link :to="{ name: 'customerEdit', params: { id: customer.id } }" class="btn btn-sm btn-secondary">Edit Customer</router-link>
<button @click="$emit('toggleAutomatic', customer.id)" class="btn btn-sm"
:class="automatic_status === 1 ? 'btn-success' : 'btn-warning'">
{{ automatic_status === 1 ? 'Set to Will Call' : 'Set to Automatic' }}
</button>
</div>
</div>
<div class="divider my-2"></div>
<div class="rounded-lg overflow-hidden" style="height:400px; width:100%">
<l-map ref="map" v-model:zoom="zoom" :center="[customer.customer_latitude, customer.customer_longitude]">
<l-tile-layer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" layer-type="base" name="OpenStreetMap"></l-tile-layer>
</l-map>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import "leaflet/dist/leaflet.css";
import { LMap, LTileLayer } from "@vue-leaflet/vue-leaflet";
defineProps({
customer: { type: Object, required: true },
automatic_status: { type: Number, required: true },
});
defineEmits(['toggleAutomatic']);
const zoom = ref(14);
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div class="card bg-base-100 shadow-xl h-full">
<div class="card-body p-4 sm:p-6">
<h2 class="card-title text-2xl mb-4">{{ customer.account_number }}</h2>
<div class="rounded-lg overflow-hidden h-full min-h-[400px]">
<l-map ref="map" v-model:zoom="zoom" :center="[customer.customer_latitude, customer.customer_longitude]">
<l-tile-layer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" layer-type="base" name="OpenStreetMap"></l-tile-layer>
</l-map>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import "leaflet/dist/leaflet.css";
import { LMap, LTileLayer } from "@vue-leaflet/vue-leaflet";
interface Customer {
account_number: string;
customer_latitude: number;
customer_longitude: number;
}
interface Props {
customer: Customer;
}
defineProps<Props>();
const zoom = ref(14);
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div class="card bg-base-100 shadow-xl h-full">
<div class="card-body p-4 sm:p-6">
<!-- Action Buttons -->
<div class="flex flex-wrap gap-2">
<router-link :to="{ name: 'deliveryCreate', params: { id: customer.id } }" class="btn btn-sm btn-primary">New Delivery</router-link>
<router-link :to="{ name: 'CalenderCustomer', params: { id: customer.id } }" class="btn btn-sm btn-info">New Service</router-link>
<router-link :to="{ name: 'customerEdit', params: { id: customer.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<button @click="$emit('toggleAutomatic', customer.id)" class="btn btn-sm" :class="automatic_status === 1 ? 'btn-success' : 'btn-warning'">
{{ automatic_status === 1 ? 'Set to Will Call' : 'Set to Automatic' }}
</button>
</div>
<div class="divider my-4"></div>
<!-- Customer Details -->
<div>
<div class="flex justify-between items-center">
<h2 class="card-title text-lg">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</h2>
<span class="badge" :class="automatic_status === 1 ? 'badge-success' : 'badge-ghost'">
{{ automatic_status === 1 ? 'Automatic' : 'Will Call' }}
</span>
</div>
<div class="text-error font-semibold mt-2" v-if="!customer.correct_address">
Possible Incorrect Address!
</div>
<div class="mt-4 space-y-2 text-sm">
<p>{{ customer.customer_address }}<span v-if="customer.customer_apt">, {{ customer.customer_apt }}</span></p>
<p>{{ customer.customer_town }}, {{ stateName(customer.customer_state) }} {{ customer.customer_zip }}</p>
<p class="pt-2">{{ customer.customer_phone_number }}</p>
<p><span class="badge badge-outline badge-sm">{{ homeTypeName(customer.customer_home_type) }}</span></p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Customer {
id: number;
customer_first_name: string;
customer_last_name: string;
correct_address: boolean;
customer_address: string;
customer_apt: string | null;
customer_town: string;
customer_state: number;
customer_zip: string;
customer_phone_number: string;
customer_home_type: number;
}
interface Props {
customer: Customer;
automatic_status: number;
}
defineProps<Props>();
defineEmits(['toggleAutomatic']);
const stateName = (id: number) => ['MA', 'RI', 'NH', 'ME', 'VT', 'CT', 'NY'][id] || 'N/A';
const homeTypeName = (id: number) => ['Residential', 'Apartment', 'Condo', 'Commercial', 'Business', 'Construction', 'Container'][id] || 'Unknown';
</script>

View File

@@ -0,0 +1,116 @@
<template>
<div>
<div v-if="!serviceCalls || serviceCalls.length === 0" class="text-center p-10">
<p>No service call history found for this customer.</p>
</div>
<div v-else class="overflow-x-auto">
<table class="table table-sm w-full">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<!-- Note: The @click handler is removed from the <tr> since the row itself is no longer the primary action -->
<tr v-for="service in serviceCalls" :key="service.id" class="hover">
<td class="align-top">
<div>{{ formatDate(service.scheduled_date) }}</div>
<div class="text-xs opacity-70">{{ formatTime(service.scheduled_date) }}</div>
</td>
<td class="align-top">
<span class="font-medium" :style="{ color: getServiceTypeColor(service.type_service_call) }">
{{ getServiceTypeName(service.type_service_call) }}
</span>
</td>
<td class="whitespace-normal text-sm align-top">
<!-- If the text is short OR this row is expanded, show the full text -->
<div v-if="!isLongDescription(service.description) || isExpanded(service.id)">
{{ service.description }}
<a v-if="isLongDescription(service.description)"
@click.prevent="toggleExpand(service.id)"
href="#"
class="link link-info link-hover text-xs ml-1 whitespace-nowrap">
Show less
</a>
</div>
<!-- Otherwise, show the truncated text -->
<div v-else>
{{ truncateDescription(service.description) }}
<a @click.prevent="toggleExpand(service.id)"
href="#"
class="link link-info link-hover text-xs ml-1 whitespace-nowrap">
Read more
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import dayjs from 'dayjs';
interface ServiceCall {
id: number;
scheduled_date: string;
type_service_call: number;
description: string;
}
interface Props {
serviceCalls: ServiceCall[];
}
defineProps<Props>();
// --- NEW LOGIC FOR TEXT TRUNCATION ---
// 1. Define the word limit as a constant for easy changes
const WORD_LIMIT = 50;
// 2. Create a reactive array to store the IDs of expanded rows
const expandedIds = ref<number[]>([]);
// 3. Helper function to check if a description is long
const isLongDescription = (text: string): boolean => {
if (!text) return false;
return text.split(/\s+/).length > WORD_LIMIT;
};
// 4. Helper function to truncate the description
const truncateDescription = (text: string): string => {
if (!isLongDescription(text)) return text;
const words = text.split(/\s+/);
return words.slice(0, WORD_LIMIT).join(' ') + '...';
};
// 5. Helper function to check if a specific row is expanded
const isExpanded = (id: number): boolean => {
return expandedIds.value.includes(id);
};
// 6. Function to add/remove an ID from the expanded list
const toggleExpand = (id: number): void => {
const index = expandedIds.value.indexOf(id);
if (index === -1) {
// If not found, add it to expand
expandedIds.value.push(id);
} else {
// If found, remove it to collapse
expandedIds.value.splice(index, 1);
}
};
// --- EXISTING HELPER FUNCTIONS ---
const formatDate = (dateString: string) => dayjs(dateString).format('MMMM D, YYYY');
const formatTime = (dateString: string) => dayjs(dateString).format('h:mm A');
const getServiceTypeName = (typeId: number) => ({ 0: 'Tune-up', 1: 'No Heat', 2: 'Fix', 3: 'Tank Install', 4: 'Other' }[typeId] || 'Unknown');
const getServiceTypeColor = (typeId: number) => ({ 0: 'blue', 1: 'red', 2: 'green', 3: '#B58900', 4: 'black' }[typeId] || 'gray');
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4 sm:p-6">
<div class="card-title flex justify-between items-center">
<h2>Tank Info</h2>
<router-link :to="{ name: 'TankEdit', params: { id: customer_id } }" class="btn btn-xs btn-outline btn-success">
Edit
</router-link>
</div>
<div class="text-sm mt-2 space-y-1">
<div class="flex justify-between">
<span>Status:</span>
<span class="badge" :class="tank.tank_status ? 'badge-success' : 'badge-error'">
{{ tank.tank_status ? 'Inspected' : 'Needs Inspection' }}
</span>
</div>
<div class="flex justify-between"><span>Last Inspection:</span> <strong>{{ tank.last_tank_inspection || 'N/A' }}</strong></div>
<div class="flex justify-between"><span>Location:</span> <strong class="badge" :class="tank.outside_or_inside ? '' : 'badge-warning'">{{ tank.outside_or_inside ? 'Inside' : 'Outside' }}</strong></div>
<div class="flex justify-between"><span>Size:</span> <strong>{{ tank.tank_size }} Gallons</strong></div>
<div class="flex justify-between"><span>Fill Location:</span> <strong>{{ description.fill_location }}</strong></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
customer_id: { type: Number, required: true },
tank: { type: Object, required: true },
description: { type: Object, required: true }
});
</script>

View File

@@ -3,7 +3,7 @@
import CustomerHome from '../customer/home.vue';
import CustomerCreate from '../customer/create.vue';
import CustomerEdit from "../customer/edit.vue";
import CustomerProfile from "./profile/home.vue"
import CustomerProfile from "./profile/profile.vue"
import TankEdit from "./tank/edit.vue"

View File

@@ -1,280 +1,186 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10">
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'customer' }">
Customers
</router-link>
</li>
</ul>
<div class="flex">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li><router-link :to="{ name: 'customer' }">Customers</router-link></li>
<li>Edit Tank Details</li>
</ul>
</div>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mt-4">
<h1 v-if="customer.id" class="text-3xl font-bold">
Tank for: {{ customer.customer_first_name }} {{ customer.customer_last_name }}
</h1>
<router-link v-if="customer.id" :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm mt-2 sm:mt-0">
Back to Profile
</router-link>
</div>
<!-- Main Form Card -->
<div class="bg-neutral rounded-lg p-6 mt-6">
<form @submit.prevent="onSubmit" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<!-- Inspection Date -->
<div class="form-control">
<label class="label"><span class="label-text">Last Inspection Date</span></label>
<input v-model="TankForm.last_tank_inspection" type="date" class="input input-bordered input-sm w-full" />
</div>
<div class="grid grid-cols-1 rounded-md p-6 ">
<div class="text-[24px]">
Customer: {{ customer.customer_first_name }} {{ customer.customer_last_name }} | {{ customer.account_number }}
</div>
<form class="rounded-md px-8 pt-6 pb-8 mb-4 w-full" enctype="multipart/form-data"
@submit.prevent="onSubmit">
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Inspection Date </label>
<input v-model="CreateTankForm.basicInfo.last_tank_inspection"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="date"
min="2023-01-01" max="2030-01-01" />
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Good Or bad Tank</label>
<label>
<input type="radio" name="goodtank1" value="true" class="radio"
v-model="CreateTankForm.basicInfo.tank_status" />
Good
</label>
<label>
<input type="radio" name="goodtank2" value="false" class="radio"
v-model="CreateTankForm.basicInfo.tank_status" />
Bad
</label>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Tank Size</label>
<input v-model="CreateTankForm.basicInfo.tank_size"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text"
placeholder="Gallon size of tank" />
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Inside or Outside</label>
<label>
<input type="radio" name="insideoutside1" value="true" class="radio"
v-model="CreateTankForm.basicInfo.outside_or_inside" />
Inside
</label>
<label>
<input type="radio" name="insideoutside2" value="false" class="radio"
v-model="CreateTankForm.basicInfo.outside_or_inside" />
Outside
</label>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Fill Location</label>
<input v-model="CreateTankForm.basicInfo.fill_location"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text"
placeholder="Fill Location" />
</div>
<div class="col-span-12 md:col-span-12 flex mt-5 mb-5">
<button class="btn btn-accent btn-sm">
Save Changes
</button>
</div>
</form>
<!-- Tank Size -->
<div class="form-control">
<label class="label"><span class="label-text">Tank Size (Gallons)</span></label>
<input v-model="TankForm.tank_size" type="number" placeholder="e.g., 275" class="input input-bordered input-sm w-full" />
</div>
</div>
<!-- Tank Status -->
<div class="form-control">
<label class="label"><span class="label-text">Tank Status</span></label>
<div class="flex items-center gap-6 bg-base-100 p-2 rounded-lg">
<label class="label cursor-pointer gap-2">
<span class="label-text">Good</span>
<input type="radio" v-model="TankForm.tank_status" :value="true" class="radio radio-primary" />
</label>
<label class="label cursor-pointer gap-2">
<span class="label-text">Bad</span>
<input type="radio" v-model="TankForm.tank_status" :value="false" class="radio radio-primary" />
</label>
</div>
</div>
<!-- Tank Location -->
<div class="form-control">
<label class="label"><span class="label-text">Tank Location</span></label>
<div class="flex items-center gap-6 bg-base-100 p-2 rounded-lg">
<label class="label cursor-pointer gap-2">
<span class="label-text">Inside</span>
<input type="radio" v-model="TankForm.outside_or_inside" :value="true" class="radio radio-primary" />
</label>
<label class="label cursor-pointer gap-2">
<span class="label-text">Outside</span>
<input type="radio" v-model="TankForm.outside_or_inside" :value="false" class="radio radio-primary" />
</label>
</div>
</div>
<!-- Fill Location -->
<div class="form-control md:col-span-2">
<label class="label"><span class="label-text">Fill Location Description</span></label>
<input v-model="TankForm.fill_location" type="text" placeholder="e.g., Left side of house, behind shed" class="input input-bordered input-sm w-full" />
</div>
</div>
<!-- SUBMIT BUTTON -->
<div class="pt-4">
<button type="submit" class="btn btn-primary btn-sm">Save Changes</button>
</div>
</form>
</div>
</div>
<Footer />
</div>
<Footer />
</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'
// Interface for our flat form model
interface TankFormData {
last_tank_inspection: string | null;
tank_status: boolean;
outside_or_inside: boolean;
tank_size: number;
fill_location: string;
}
export default defineComponent({
name: 'TankEdit',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
user: {
id: '',
},
customer: {
id: 0,
user_id: 0,
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_address: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
account_number: '',
},
tank: {
customer_id: 0,
last_tank_inspection: true,
tank_status: true,
outside_or_inside: true,
tank_size: 0,
},
CreateTankForm: {
basicInfo: {
last_tank_inspection: null,
tank_status: true,
outside_or_inside: true,
tank_size: 0,
fill_location: 0,
},
},
}
},
created() {
this.userStatus()
},
watch: {
$route() {
this.getCustomer(this.$route.params.id);
this.getCustomerDescription(this.$route.params.id);
this.getTank(this.$route.params.id);
},
},
mounted() {
this.getCustomer(this.$route.params.id);
this.getCustomerDescription(this.$route.params.id);
this.getTank(this.$route.params.id);
},
methods: {
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;
this.user.id = response.data.user.id;
}
})
.catch(() => {
this.user.id = '';
})
},
getCustomer(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/customer/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customer = response.data
})
},
getCustomerDescription(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/customer/description/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.CreateTankForm.basicInfo.fill_location = response.data.fill_location;
name: 'TankEdit',
components: {
Footer,
},
data() {
return {
user: null as any,
customer: {} as any,
// --- REFACTORED: Simplified, flat form object ---
TankForm: {
last_tank_inspection: null,
tank_status: true,
outside_or_inside: true,
tank_size: 0,
fill_location: '',
} as TankFormData,
}
},
created() {
this.userStatus();
const customerId = this.$route.params.id;
this.getCustomer(customerId);
this.getCustomerDescription(customerId);
this.getTank(customerId);
},
methods: {
userStatus() {
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
},
getTank(customer_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/customer/tank/" + customer_id;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.tank = response.data
this.CreateTankForm.basicInfo.last_tank_inspection = response.data.last_tank_inspection;
this.CreateTankForm.basicInfo.tank_status = response.data.tank_status;
this.CreateTankForm.basicInfo.outside_or_inside = response.data.outside_or_inside;
this.CreateTankForm.basicInfo.tank_size = response.data.tank_size;
console.log(this.CreateTankForm.basicInfo.outside_or_inside)
console.log(this.CreateTankForm.basicInfo.tank_status)
})
},
editTank(payload: {
last_tank_inspection: any;
tank_status: boolean;
outside_or_inside: boolean;
tank_size: number;
fill_location: number;
}) {
let path = import.meta.env.VITE_BASE_URL + "/customer/edit/tank/" + this.$route.params.id;
axios({
method: "put",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.$router.push({ name: "customerProfile", params: { id: this.tank.customer_id } });
}
if (response.data.error) {
this.$router.push("/");
}
})
},
onSubmit() {
console.log(this.CreateTankForm.basicInfo.outside_or_inside)
console.log(this.CreateTankForm.basicInfo.tank_status)
let payload = {
last_tank_inspection: this.CreateTankForm.basicInfo.last_tank_inspection,
tank_status: this.CreateTankForm.basicInfo.tank_status,
outside_or_inside: this.CreateTankForm.basicInfo.outside_or_inside,
tank_size: this.CreateTankForm.basicInfo.tank_size,
fill_location: this.CreateTankForm.basicInfo.fill_location,
};
this.editTank(payload);
},
.catch(() => { this.user = null; });
},
getCustomer(userid: any) {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${userid}`;
axios.get(path, { headers: authHeader() })
.then((response: any) => {
this.customer = response.data;
});
},
getCustomerDescription(userid: any) {
const path = `${import.meta.env.VITE_BASE_URL}/customer/description/${userid}`;
axios.get(path, { headers: authHeader() })
.then((response: any) => {
// Only update fill_location if the response has it
if (response.data && response.data.fill_location) {
this.TankForm.fill_location = response.data.fill_location;
}
});
},
getTank(customer_id: any) {
const path = `${import.meta.env.VITE_BASE_URL}/customer/tank/${customer_id}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data) {
// Update the form model with data from the tank endpoint
this.TankForm.last_tank_inspection = response.data.last_tank_inspection;
this.TankForm.tank_status = response.data.tank_status;
this.TankForm.outside_or_inside = response.data.outside_or_inside;
this.TankForm.tank_size = response.data.tank_size;
}
});
},
editTank(payload: TankFormData) {
const path = `${import.meta.env.VITE_BASE_URL}/customer/edit/tank/${this.$route.params.id}`;
axios.put(path, payload, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
} else {
console.error("Failed to edit tank:", response.data.error);
}
});
},
onSubmit() {
// The payload is simply the entire form object now
this.editTank(this.TankForm);
},
},
})
</script>
<style scoped></style>
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,290 +1,153 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10">
<!-- Main Content -->
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs Navigation -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Edit Oil Delivery #{{ deliveryOrder.id }}</li>
</ul>
</div>
<div class="grid grid-cols-2 rounded-md p-6 ">
<div class="col-span-1">
<div class="text-2xl border-b border-gray-500 mb-10">
Edit Oil Delivery
</div>
<div class=" rounded-md">
<div class="flex text-2xl">
Delivery id: {{ deliveryOrder.id }}
</div>
<div class="grid grid-cols-12">
<div class="col-span-12 py-5">
<router-link :to="{ name: 'customerProfile', params: { id: customer['id'] } }"
class="btn btn-secondary btn-sm">
View Profile
</router-link>
</div>
<div class="col-span-6 p-5">
<div class="grid grid-cols-12">
<div class="col-span-12 flex">
{{ customer.customer_first_name }}
{{ customer.customer_last_name }}
</div>
<div class="col-span-12 flex">{{customer.customer_address}}</div>
<div class="col-span-12 flex">
<div class="pr-2">
{{ customer.customer_town }},
</div>
<div class="pr-2">
<div v-if="customer.customer_state == 0">Massachusetts</div>
<div v-else-if="customer.customer_state == 1">Rhode Island</div>
<div v-else-if="customer.customer_state == 2">New Hampshire</div>
<div v-else-if="customer.customer_state == 3">Maine</div>
<div v-else-if="customer.customer_state == 4">Vermont</div>
<div v-else-if="customer.customer_state == 5">Maine</div>
<div v-else-if="customer.customer_state == 6">New York</div>
<div v-else>Unknown state</div>
</div>
<div class="pr-2">
{{ customer.customer_zip }}
</div>
</div>
<div class="col-span-12 flex" v-if="customer.customer_apt !== 'None'">
{{ customer.customer_apt }}
</div>
<div class="col-span-12 flex">
<div v-if="customer.customer_home_type == 0">Residential</div>
<div v-else-if="customer.customer_home_type == 1">apartment</div>
<div v-else-if="customer.customer_home_type == 2">condo</div>
<div v-else-if="customer.customer_home_type == 3">commercial</div>
<div v-else-if="customer.customer_home_type == 4">business</div>
<div v-else-if="customer.customer_home_type == 5">construction</div>
<div v-else-if="customer.customer_home_type == 6">container</div>
</div>
<div class="col-span-12 flex">
{{ customer.customer_phone_number }}
</div>
</div>
<!-- TOP SECTION: Customer and Payment Info -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 my-6">
<!-- Customer Info Card -->
<div v-if="customer.id" class="bg-neutral rounded-lg p-5">
<div class="flex justify-between items-center mb-4">
<div>
<div class="text-xl font-bold">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</div>
<div class="text-sm text-gray-400">Account: {{ customer.account_number }}</div>
</div>
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm">
View Profile
</router-link>
</div>
<div>
<div>{{ customer.customer_address }}</div>
<div v-if="customer.customer_apt && customer.customer_apt !== 'None'">{{ customer.customer_apt }}</div>
<div>{{ customer.customer_town }}, {{ stateName }} {{ customer.customer_zip }}</div>
<div class="mt-2">{{ customer.customer_phone_number }}</div>
</div>
</div>
<div class="grid grid-cols-1 " v-if="deliveryOrder.payment_type == 1">
<div class=" p-2 ">
<div class="rounded-md border-2 bg-neutral">
<div class="flex p-3">
{{ userCard.type_of_card }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.name_on_card }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.card_number }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.expiration_month }}/ {{ userCard.expiration_year }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.security_number }}
</div>
</div>
</div>
</div>
<div class=" rounded-md mx-5 my-5">
<div class="flex">
Order Date:
</div>
<div class="mb-5">
{{ deliveryOrder.when_ordered }}
</div>
<div class="flex">
Expected Delivery Date:
</div>
<div class="mb-5">
{{ deliveryOrder.expected_delivery_date }}
</div>
<div class="flex">
Price per gallon:
</div>
<div class="mb-5">
{{ deliveryOrder.customer_price }}
<!-- Payment Card on File Card -->
<div v-if="deliveryOrder.payment_type === 1 && userCard.id" class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Card on File</h3>
<div class="space-y-1">
<p><span class="font-semibold">Card Type:</span> {{ userCard.type_of_card }}</p>
<p><span class="font-semibold">Card Number:</span> {{ userCard.card_number }}</p>
<p><span class="font-semibold">Name:</span> {{ userCard.name_on_card }}</p>
<p><span class="font-semibold">Expires:</span> {{ userCard.expiration_month }}/{{ userCard.expiration_year }}</p>
</div>
</div>
</div>
<div class="col-span-1">
<form class="rounded-md px-8 pb-8 mb-4 w-full" enctype="multipart/form-data" @submit.prevent="onSubmit">
<div class="col-span-12 mb-4">
<label class="block text-white text-sm font-bold mb-2">Gallons Ordered</label>
<input v-model="CreateOilOrderForm.basicInfo.gallons_ordered"
:disabled="CreateOilOrderForm.basicInfo.customer_asked_for_fill"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text" placeholder="# gallons" />
<span v-if="v$.CreateOilOrderForm.basicInfo.gallons_ordered.$error" class="text-red-600 text-center">
{{ v$.CreateOilOrderForm.basicInfo.gallons_ordered.$errors[0].$message }}
</span>
<!-- BOTTOM SECTION: Edit Form -->
<div class="bg-neutral rounded-lg p-6">
<h2 class="text-2xl font-bold mb-4">Update Delivery Details</h2>
<form @submit.prevent="onSubmit" class="space-y-4">
<!-- Gallons Ordered & Fill Checkbox -->
<div class="flex items-end gap-4">
<div class="flex-grow">
<label class="label"><span class="label-text font-bold">Gallons Ordered</span></label>
<input v-model="CreateOilOrderForm.basicInfo.gallons_ordered" :disabled="CreateOilOrderForm.basicInfo.customer_asked_for_fill"
class="input input-bordered input-sm w-full max-w-xs" type="number" placeholder="# gallons" />
<span v-if="v$.CreateOilOrderForm.basicInfo.gallons_ordered.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateOilOrderForm.basicInfo.gallons_ordered.$errors[0].$message }}
</span>
</div>
<div class="form-control pb-1">
<label class="label cursor-pointer justify-start gap-4">
<span class="label-text font-bold">Fill</span>
<input v-model="CreateOilOrderForm.basicInfo.customer_asked_for_fill" type="checkbox" class="checkbox checkbox-sm" />
</label>
</div>
</div>
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 gap-10">
<label class="block text-white text-sm font-bold cursor-pointer label">Fill </label>
<input v-model="CreateOilOrderForm.basicInfo.customer_asked_for_fill" class="checkbox checkbox-xs" id="fill"
type="checkbox" />
<!-- Date Fields -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="label"><span class="label-text font-bold">Order Created Date</span></label>
<input v-model="CreateOilOrderForm.basicInfo.created_delivery_date" type="date" class="input input-bordered input-sm w-full" />
</div>
<div>
<label class="label"><span class="label-text font-bold">Expected Delivery Date</span></label>
<input v-model="CreateOilOrderForm.basicInfo.expected_delivery_date" type="date" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateOilOrderForm.basicInfo.expected_delivery_date.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateOilOrderForm.basicInfo.expected_delivery_date.$errors[0].$message }}
</span>
</div>
</div>
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 gap-10">
<label class="block text-white text-sm font-bold cursor-pointer label">Cash</label>
<input v-model="CreateOilOrderForm.basicInfo.cash" class="checkbox checkbox-xs" id="cash" type="checkbox" />
</div>
<div v-if="userCards.length > 0">
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 gap-10">
<label class="block text-white text-sm font-bold cursor-pointer label">Credit </label>
<input v-model="CreateOilOrderForm.basicInfo.card" class="checkbox checkbox-xs" id="Credit"
type="checkbox" />
</div>
</div>
<div v-if="userCards.length > 0">
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 gap-10">
<label class="block text-white text-sm font-bold cursor-pointer label">Check</label>
<input v-model="CreateOilOrderForm.basicInfo.check" class="checkbox checkbox-xs" id="check"
type="checkbox" />
</div>
<!-- Fees & Options -->
<div class="p-4 border rounded-md">
<label class="label-text font-bold">Fees & Options</label>
<div class="flex flex-wrap gap-x-6 gap-y-2 mt-2">
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Prime</span><input v-model="CreateOilOrderForm.basicInfo.prime" type="checkbox" class="checkbox checkbox-xs" /></label></div>
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Emergency</span><input v-model="CreateOilOrderForm.basicInfo.emergency" type="checkbox" class="checkbox checkbox-xs" /></label></div>
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Same Day</span><input v-model="CreateOilOrderForm.basicInfo.same_day" type="checkbox" class="checkbox checkbox-xs" /></label></div>
</div>
</div>
<div v-if="userCards.length > 0">
<div class="flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">Customer Cards Payment</label>
<select class="select select-bordered select-sm w-full max-w-xs" aria-label="Default select example"
id="userCards" v-model="CreateOilOrderForm.basicInfo.userCards">
<option class="text-white" v-for="(card, index) in userCards" :key="index" :value="card['id']">
{{ card['type_of_card'] }} {{ card['card_number'] }}
<!-- Payment Type Radio Group -->
<div class="p-4 border rounded-md space-y-3">
<label class="label-text font-bold">Payment Method</label>
<div class="flex flex-wrap gap-x-6 gap-y-2">
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Cash</span><input type="radio" v-model="CreateOilOrderForm.basicInfo.payment_type" :value="0" class="radio radio-xs" /></label></div>
<div v-if="userCards.length > 0" class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Card</span><input type="radio" v-model="CreateOilOrderForm.basicInfo.payment_type" :value="1" class="radio radio-xs" /></label></div>
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Check</span><input type="radio" v-model="CreateOilOrderForm.basicInfo.payment_type" :value="3" class="radio radio-xs" /></label></div>
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Other</span><input type="radio" v-model="CreateOilOrderForm.basicInfo.payment_type" :value="4" class="radio radio-xs" /></label></div>
</div>
<!-- Customer Card Selection -->
<div v-if="userCards.length > 0 && CreateOilOrderForm.basicInfo.payment_type === 1">
<label class="label"><span class="label-text">Select Card</span></label>
<select v-model="CreateOilOrderForm.basicInfo.credit_card_id" class="select select-bordered select-sm w-full max-w-xs">
<option disabled :value="0">Select a card</option>
<option v-for="card in userCards" :key="card.id" :value="card.id">
{{ card.type_of_card }} {{ card.card_number }}
</option>
</select>
</div>
</div>
<div v-else>
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 py-5">
No Cards on File!
<router-link :to="{ name: 'cardadd', params: { id: customer.user_id } }">
<button class="btn btn-sm bg-blue-700 text-white">Add CreditCard</button>
</router-link>
<!-- Delivery Status & Driver -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="label"><span class="label-text font-bold">Delivery Status</span></label>
<select v-model="CreateOilOrderForm.basicInfo.delivery_status" class="select select-bordered select-sm w-full">
<option v-for="status in deliveryStatus" :key="status.value" :value="status.value">
{{ status.text }}
</option>
</select>
</div>
<div>
<label class="label"><span class="label-text font-bold">Assigned Driver</span></label>
<select v-model="CreateOilOrderForm.basicInfo.driver_employee_id" class="select select-bordered select-sm w-full">
<option disabled :value="0">Select a driver</option>
<option v-for="driver in truckDriversList" :key="driver.id" :value="driver.id">
{{ driver.employee_first_name }} {{ driver.employee_last_name }}
</option>
</select>
</div>
</div>
<div class="flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">Delivery Status</label>
<select class="select select-bordered select-sm w-full max-w-xs" aria-label="Default select example"
id="delivery_status" v-model="CreateOilOrderForm.basicInfo.delivery_status">
<option class="text-white" v-for="(delivery, index) in deliveryStatus" :key="index"
:value="delivery['value']">
{{ delivery['text'] }}
</option>
</select>
<span v-if="v$.CreateOilOrderForm.basicInfo.delivery_status.$error" class="text-red-600 text-center">
{{ v$.CreateCustomerForm.basicInfo.delivery_status.$errors[0].$message }}
</span>
<!-- Dispatcher Notes -->
<div>
<label class="label"><span class="label-text font-bold">Dispatcher Notes</span></label>
<textarea v-model="CreateOilOrderForm.basicInfo.dispatcher_notes_taken" rows="3" class="textarea textarea-bordered w-full"></textarea>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Delivery Created </label>
<input v-model="CreateOilOrderForm.basicInfo.created_delivery_date"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="date" min="2023-01-01"
max="2030-01-01" />
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Expected </label>
<input v-model="CreateOilOrderForm.basicInfo.expected_delivery_date"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="date" min="2023-01-01"
max="2030-01-01" />
<span v-if="v$.CreateOilOrderForm.basicInfo.expected_delivery_date.$error" class="text-red-600 text-center">
{{ v$.CreateOilOrderForm.basicInfo.expected_delivery_date.$errors[0].$message }}
</span>
</div>
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 gap-10">
<label class="block text-white text-sm font-bold cursor-pointer label">Prime</label>
<input v-model="CreateOilOrderForm.basicInfo.prime" class="checkbox checkbox-xs" id="prime"
type="checkbox" />
</div>
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 gap-10">
<label class="block text-white text-sm font-bold cursor-pointer label">Emergency</label>
<input v-model="CreateOilOrderForm.basicInfo.emergency" class="checkbox checkbox-xs" id="emergency"
type="checkbox" />
</div>
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0 gap-10">
<label class="block text-white text-sm font-bold cursor-pointer label">Same Day </label>
<input v-model="CreateOilOrderForm.basicInfo.same_day" class="checkbox checkbox-xs" id="same_day"
type="checkbox" />
</div>
<div class="flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">Delivery Driver </label>
<select class="select select-bordered select-sm w-full max-w-xs" aria-label="Default select example"
id="customer_state" v-model="CreateOilOrderForm.basicInfo.driver_driver">
<option class="text-white" v-for="(driver, index) in truckDriversList" :key="index" :value="driver['id']">
{{ driver['employee_first_name'] }} {{ driver['employee_last_name'] }}
</option>
</select>
</div>
<div class="flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">Select a Promo</label>
<select class="select select-bordered select-sm w-full max-w-xs" aria-label="Default select example"
id="promos" v-model="CreateOilOrderForm.basicInfo.promo_id">
<option class="text-white" v-for="(promo, index) in promos" :key="index" :value="promo['id']">
{{ promo['name_of_promotion'] }} {{ promo['money_off_delivery'] }}
</option>
</select>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Dispatcher Note on Ticket</label>
<textarea v-model="CreateOilOrderForm.basicInfo.dispatcher_notes_taken" rows="4"
class="textarea block p-2.5 w-full input-bordered " id="description" type="text" placeholder="Notes on ticket" />
</div>
<div class="col-span-12 md:col-span-12 flex mt-5 mb-5">
<button class="btn btn-accent btn-sm">
Save Changes
</button>
<!-- Submit Button -->
<div class="pt-2">
<button type="submit" class="btn btn-primary btn-sm">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'
@@ -293,413 +156,176 @@ import Header from '../../layouts/headers/headerauth.vue'
import SideBar from '../../layouts/sidebar/sidebar.vue'
import Footer from '../../layouts/footers/footer.vue'
import useValidate from "@vuelidate/core";
import { minLength, required } from "@vuelidate/validators";
import { required, minLength } from "@vuelidate/validators";
import { notify } from "@kyvg/vue3-notification";
// Interfaces to describe the shape of your data
interface Customer { account_number: string; id: number; customer_first_name: string; customer_last_name: string; customer_address: string; customer_apt: string; customer_town: string; customer_state: number; customer_zip: string; customer_phone_number: string; }
interface DeliveryOrder { id: string; customer_id: number; payment_type: number; payment_card_id: number; gallons_ordered: number; customer_asked_for_fill: boolean | number; delivery_status: number; driver_employee_id: number; promo_id: number; expected_delivery_date: string; when_ordered: string; prime: boolean | number; emergency: boolean | number; same_day: boolean | number; dispatcher_notes: string; }
interface UserCard { id: number; type_of_card: string; card_number: string; name_on_card: string; expiration_month: string; expiration_year: string; last_four_digits: string; }
const STATE_MAP: { [key: number]: string } = { 0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire', 3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York' };
export default defineComponent({
name: 'deliveryEdit',
components: {
Header,
SideBar,
Footer,
},
components: { Header, SideBar, Footer },
data() {
return {
v$: useValidate(),
user: null,
deliveryStatus: [],
truckDriversList: [],
userCards: [],
promos: [],
userCard: {
date_added: '',
user_id: '',
card_number: '',
last_four_digits: '',
name_on_card: '',
expiration_month: '',
expiration_year: '',
type_of_card: '',
security_number: '',
accepted_or_declined: '',
main_card: '',
},
customer: {
id: 0,
user_id: 0,
customer_first_name: '',
customer_address: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
},
deliveryOrder: {
id: '',
customer_id: 0,
customer_name: '',
customer_address: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
gallons_ordered: 0,
customer_asked_for_fill: 0,
gallons_delivered: '',
customer_filled: 0,
delivery_status: 0,
when_ordered: '',
when_delivered: '',
expected_delivery_date: '',
created_delivery_date: '',
automatic: 0,
oil_id: 0,
supplier_price: '',
customer_price: '',
customer_temperature: '',
dispatcher_notes: '',
prime: 0,
same_day: 0,
emergency: 0,
payment_type: 0,
payment_card_id: 0,
driver_employee_id: 0,
driver_first_name: '',
driver_last_name: '',
promo_id: 0,
promo_money_discount: '',
},
deliveryStatus: [] as any[],
truckDriversList: [] as any[],
userCards: [] as UserCard[],
promos: [] as any[],
customer: {} as Customer,
deliveryOrder: {} as DeliveryOrder,
userCard: {} as UserCard,
// RESTORED: Add all form fields to the data model
CreateOilOrderForm: {
basicInfo: {
gallons_ordered: '',
customer_asked_for_fill: false,
expected_delivery_date: '',
created_delivery_date: '',
dispatcher_notes_taken: '',
expected_delivery_date: '',
prime: false,
same_day: false,
emergency: false,
delivery_status: '',
userCards: [],
promos: [],
credit_card_id: 0,
same_day: false,
delivery_status: 0,
driver_employee_id: 0,
dispatcher_notes_taken: '',
promo_id: 0,
cash: false,
card: false,
other: false,
check: false,
driver_driver: '',
payment_type: 0,
credit_card_id: 0,
},
},
}
},
validations() {
return {
CreateOilOrderForm: {
basicInfo: {
gallons_ordered: { required, minLength: minLength(1) },
// RESTORED: Add validation for expected date
expected_delivery_date: { required },
delivery_status: { required },
},
},
};
},
created() {
this.getPromos()
this.userStatus()
this.getDriversList()
},
watch: {
$route() {
this.getDeliveryForm(this.$route.params.id);
this.getDeliveryOrder(this.$route.params.id);
computed: {
stateName(): string {
if (this.customer && this.customer.customer_state !== undefined) {
return STATE_MAP[this.customer.customer_state];
}
return '';
},
},
mounted() {
this.getDeliveryForm(this.$route.params.id)
this.getDeliveryOrder(this.$route.params.id);
this.getDeliveryStatusList();
this.fetchInitialData();
},
methods: {
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
})
fetchInitialData() {
const deliveryId = this.$route.params.id as string;
this.getPromos();
this.getDriversList();
this.getDeliveryStatusList();
this.getDeliveryOrder(deliveryId);
},
getPromos() {
let path = import.meta.env.VITE_BASE_URL + "/promo/all";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.promos = response.data;
})
.catch(() => {
});
},
getDeliveryOrder(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/delivery/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
console.log( response.data.delivery.delivery_dispatcher_notes)
this.CreateOilOrderForm.basicInfo.gallons_ordered = response.data.delivery.delivery_gallons_ordered;
this.CreateOilOrderForm.basicInfo.customer_asked_for_fill = response.data.delivery.delivery_asked_for_fill;
this.CreateOilOrderForm.basicInfo.expected_delivery_date = response.data.delivery.delivery_expected_delivery_date;
this.CreateOilOrderForm.basicInfo.created_delivery_date = response.data.delivery.when_ordered;
this.CreateOilOrderForm.basicInfo.dispatcher_notes_taken = response.data.delivery.dispatcher_notes_taken;
this.CreateOilOrderForm.basicInfo.prime = response.data.delivery.delivery_prime;
this.CreateOilOrderForm.basicInfo.emergency = response.data.delivery.delivery_emergency;
this.CreateOilOrderForm.basicInfo.same_day = response.data.delivery.delivery_same_day;
this.CreateOilOrderForm.basicInfo.delivery_status = response.data.delivery.delivery_status;
this.CreateOilOrderForm.basicInfo.driver_driver = response.data.delivery.driver_employee_id;
if (response.data.delivery.delivery_asked_for_fill == 1) {
this.CreateOilOrderForm.basicInfo.customer_asked_for_fill = true
}
if (response.data.delivery.payment_type == 1) {
this.CreateOilOrderForm.basicInfo.userCards = response.data.delivery.payment_card_id;
}
if (response.data.delivery.delivery_prime == 1) {
this.CreateOilOrderForm.basicInfo.prime = true
}
if (response.data.delivery.delivery_same_day == 1) {
this.CreateOilOrderForm.basicInfo.same_day = true
}
if (response.data.delivery.delivery_emergency == 1) {
this.CreateOilOrderForm.basicInfo.emergency = true
}
if (response.data.delivery.payment_type == 0) {
this.CreateOilOrderForm.basicInfo.card = false
this.CreateOilOrderForm.basicInfo.cash = true
this.CreateOilOrderForm.basicInfo.check = false
}
if (response.data.delivery.payment_type == 1) {
this.CreateOilOrderForm.basicInfo.card = true
this.CreateOilOrderForm.basicInfo.cash = false
this.CreateOilOrderForm.basicInfo.check = false
}
if (response.data.delivery.payment_type == 2) {
this.CreateOilOrderForm.basicInfo.card = true
this.CreateOilOrderForm.basicInfo.cash = true
this.CreateOilOrderForm.basicInfo.check = false
}
if (response.data.delivery.payment_type == 3) {
this.CreateOilOrderForm.basicInfo.card = false
this.CreateOilOrderForm.basicInfo.cash = false
this.CreateOilOrderForm.basicInfo.check = true
}
// Other
if (response.data.delivery.payment_type == 4) {
this.CreateOilOrderForm.basicInfo.card = false
this.CreateOilOrderForm.basicInfo.cash = false
this.CreateOilOrderForm.basicInfo.check = false
this.CreateOilOrderForm.basicInfo.other = true
}
if (response.data.delivery.promo_id !== 0) {
this.CreateOilOrderForm.basicInfo.promo_id = response.data.delivery.promo_id;
}
}
})
},
getDeliveryForm(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/delivery/order/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
getDeliveryOrder(deliveryId: string) {
axios.get(`${import.meta.env.VITE_BASE_URL}/delivery/order/${deliveryId}`, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data) {
this.deliveryOrder = response.data
this.getCustomer(this.deliveryOrder.customer_id)
}
this.deliveryOrder = response.data;
// RESTORED: Populate all form fields from the API response
this.CreateOilOrderForm.basicInfo = {
gallons_ordered: String(response.data.gallons_ordered),
customer_asked_for_fill: !!response.data.customer_asked_for_fill,
created_delivery_date: response.data.when_ordered,
expected_delivery_date: response.data.expected_delivery_date,
prime: !!response.data.prime,
emergency: !!response.data.emergency,
same_day: !!response.data.same_day,
delivery_status: response.data.delivery_status,
driver_employee_id: response.data.driver_employee_id || 0,
dispatcher_notes_taken: response.data.dispatcher_notes,
promo_id: response.data.promo_id || 0,
payment_type: response.data.payment_type,
credit_card_id: response.data.payment_card_id || 0,
};
this.getCustomer(response.data.customer_id);
})
.catch((error: any) => console.error("Error fetching delivery order:", error));
},
getCustomer(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/customer/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
getCustomer(customerId: number) {
axios.get(`${import.meta.env.VITE_BASE_URL}/customer/${customerId}`, { withCredentials: true })
.then((response: any) => {
this.customer = response.data;
this.getPaymentCards(this.deliveryOrder.customer_id);
if (this.deliveryOrder.payment_type == 1) {
this.getPaymentCard(this.deliveryOrder.payment_card_id)
}
if (this.deliveryOrder.payment_type == 2) {
this.getPaymentCard(this.deliveryOrder.payment_card_id)
this.getPaymentCards(customerId);
if (this.deliveryOrder.payment_type === 1 && this.deliveryOrder.payment_card_id) {
this.getPaymentCard(this.deliveryOrder.payment_card_id);
}
})
.catch(() => {
notify({
title: "Error",
text: "Could not find customer",
type: "error",
});
});
.catch((error: any) => console.error("Error fetching customer:", error));
},
getPaymentCard(card_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.userCard = response.data;
this.CreateOilOrderForm.basicInfo.userCards = response.data.id
})
.catch(() => {
});
getPaymentCards(customerId: number) {
axios.get(`${import.meta.env.VITE_BASE_URL}/payment/cards/${customerId}`, { withCredentials: true })
.then((response: any) => { this.userCards = response.data; })
.catch((error: any) => console.error("Error fetching payment cards:", error));
},
getPaymentCards(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/payment/cards/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.userCards = response.data;
})
.catch(() => {
});
getPaymentCard(cardId: number) {
axios.get(`${import.meta.env.VITE_BASE_URL}/payment/card/${cardId}`, { withCredentials: true })
.then((response: any) => { this.userCard = response.data; })
.catch((error: any) => console.error("Error fetching specific payment card:", error));
},
updatestatus() {
let path = import.meta.env.VITE_BASE_URL + '/delivery/updatestatus';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.update)
console.log("ok")
})
getPromos() {
axios.get(`${import.meta.env.VITE_BASE_URL}/promo/all`, { withCredentials: true })
.then((response: any) => { this.promos = response.data; });
},
getDriversList() {
axios.get(`${import.meta.env.VITE_BASE_URL}/employee/drivers`, { headers: authHeader(), withCredentials: true })
.then((response: any) => { this.truckDriversList = response.data; });
},
getDeliveryStatusList() {
let path = import.meta.env.VITE_BASE_URL + "/query/deliverystatus";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.deliveryStatus = response.data;
})
.catch(() => {
});
},
getDriversList() {
let path = import.meta.env.VITE_BASE_URL + "/employee/drivers";
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.truckDriversList = response.data;
})
.catch(() => {
});
},
editOilOrder(payload: {
gallons_ordered: string;
customer_asked_for_fill: boolean;
prime: boolean;
same_day: boolean;
emergency: boolean;
delivery_status: string;
expected_delivery_date: string;
created_delivery_date: string;
dispatcher_notes_taken: string;
cash: boolean;
credit: boolean;
check: boolean;
credit_card_id: any;
promo_id: any;
driver_employee_id: string,
}) {
let path = import.meta.env.VITE_BASE_URL + "/delivery/edit/" + this.deliveryOrder.id;
axios({
method: "post",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok === true) {
this.updatestatus()
this.$router.push({ name: "deliveryOrder", params: { id: this.deliveryOrder.id } });
}
if (response.data.error) {
this.$router.push("/");
}
})
axios.get(`${import.meta.env.VITE_BASE_URL}/query/deliverystatus`, { withCredentials: true })
.then((response: any) => { this.deliveryStatus = response.data; });
},
onSubmit() {
let payload = {
gallons_ordered: this.CreateOilOrderForm.basicInfo.gallons_ordered,
customer_asked_for_fill: this.CreateOilOrderForm.basicInfo.customer_asked_for_fill,
expected_delivery_date: this.CreateOilOrderForm.basicInfo.expected_delivery_date,
created_delivery_date: this.CreateOilOrderForm.basicInfo.created_delivery_date,
dispatcher_notes_taken: this.CreateOilOrderForm.basicInfo.dispatcher_notes_taken,
prime: this.CreateOilOrderForm.basicInfo.prime,
emergency: this.CreateOilOrderForm.basicInfo.emergency,
same_day: this.CreateOilOrderForm.basicInfo.same_day,
delivery_status: this.CreateOilOrderForm.basicInfo.delivery_status,
driver_employee_id: this.CreateOilOrderForm.basicInfo.driver_driver,
cash: this.CreateOilOrderForm.basicInfo.cash,
credit: this.CreateOilOrderForm.basicInfo.card,
check: this.CreateOilOrderForm.basicInfo.check,
promo_id: this.CreateOilOrderForm.basicInfo.promo_id,
credit_card_id: this.CreateOilOrderForm.basicInfo.userCards,
};
this.editOilOrder(payload);
this.v$.$validate();
if (this.v$.$error) {
notify({ type: 'error', title: 'Validation Error', text: 'Please check the form fields.' });
return;
}
},
const formInfo = this.CreateOilOrderForm.basicInfo;
// The payload now automatically includes all the restored fields
const payload = {
...formInfo,
cash: formInfo.payment_type === 0,
credit: formInfo.payment_type === 1,
check: formInfo.payment_type === 3,
other: formInfo.payment_type === 4,
credit_card_id: formInfo.payment_type === 1 ? formInfo.credit_card_id : null,
};
axios.post(`${import.meta.env.VITE_BASE_URL}/delivery/edit/${this.deliveryOrder.id}`, payload, { withCredentials: true, headers: authHeader() })
.then(() => {
notify({ type: 'success', title: 'Success!', text: 'Delivery updated.' });
this.$router.push({ name: 'deliveryOrder', params: { id: this.deliveryOrder.id } });
})
.catch((error: any) => {
console.error("Error submitting form:", error);
notify({ type: 'error', title: 'Update Failed', text: 'Could not save changes.' });
});
},
},
})
</script>
<style scoped></style>
</script>

View File

@@ -1,150 +1,176 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs mb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Deliveries</li>
</ul>
</div>
<div class="flex text-2xl mb-5">
Delivery Home
</div>
<div class="grid grid-cols-12 gap-5 ">
<div class="col-span-12 bg-secondary ">
<div class="grid grid-cols-12 p-5 bg-neutral m-5">
<div class="col-span-12 font-bold text-xl">Todays stats</div>
<div class="col-span-6 py-2"> Total Deliveries: {{ delivery_count }}</div>
<div class="col-span-6 py-2"> Completed: {{ delivery_count_delivered }} / {{ delivery_count }}</div>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Search and Stats -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-6 mb-4">
<div class="form-control">
<h2 class="text-lg font-bold">Deliveries </h2>
</div>
<!-- Today's Stats Card -->
<div class="stats stats-vertical sm:stats-horizontal shadow bg-base-100 text-center text-sm">
<div class="stat p-3">
<div class="stat-title text-xs">Today's Deliveries</div>
<div class="stat-value text-lg">{{ delivery_count }}</div>
</div>
<div class="stat p-3">
<div class="stat-title text-xs">Completed</div>
<div class="stat-value text-lg">{{ delivery_count_delivered }} / {{ delivery_count }}</div>
</div>
</div>
</div>
<div class="col-span-12 flex start pb-10 text-2xl">Recent 100 delivieres </div>
<div class="divider">Recent Deliveries</div>
<div class="col-span-12 overflow-x-auto bg-neutral">
<table class="table">
<!-- head -->
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Account #</th>
<th>Delivery #</th>
<th>Name</th>
<th>Status</th>
<th>Town</th>
<th>Address</th>
<th>Town / Address</th>
<th>Gallons</th>
<th>Date</th>
<th>Automatic</th>
<th>Prime</th>
<th>Same Day</th>
<th>Emergency</th>
<th>Options</th>
<th>Payment</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<!-- row 1 -->
<tr v-for="oil in deliveries" :key="oil['id']">
<td>{{ oil['id'] }} </td>
<router-link :to="{ name: 'customerProfile', params: { id: oil['id'] } }">
<td>
<div class="hover:text-accent">{{ oil['customer_name'] }} </div>
</td>
</router-link>
<tr v-for="oil in deliveries" :key="oil.id" class="hover:bg-blue-600 hover:text-white">
<td>{{ oil.id }}</td>
<td>
<div v-if="oil['delivery_status'] == 0">Waiting</div>
<div v-else-if="oil['delivery_status'] == 1">delivered</div>
<div v-else-if="oil['delivery_status'] == 2">Out for Delivery</div>
<div v-else-if="oil['delivery_status'] == 3">Cancelled</div>
<div v-else-if="oil['delivery_status'] == 4">Partial Delivery</div>
<div v-else-if="oil['delivery_status'] == 5">Issue</div>
<div v-else-if="oil['delivery_status'] == 10">Finalized</div>
<div v-else></div>
</td>
<td>{{ oil['customer_town'] }}</td>
<td>{{ oil['customer_address'] }}</td>
<td>
<div v-if="oil['customer_asked_for_fill'] == 1">Fill</div>
<div v-else> {{ oil['gallons_ordered'] }}</div>
</td>
<td>{{ oil['expected_delivery_date'] }}</td>
<td>
<div v-if="oil['automatic'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['prime'] == 0">No</div>
<div v-else class="text-red-600">Yes</div>
</td>
<td>
<div v-if="oil['same_day'] == 0">No</div>
<div v-else class="text-red-600">Yes</div>
</td>
<td>
<div v-if="oil['emergency'] == 0">No</div>
<div v-else class="text-red-600">Yes</div>
</td>
<td>
<div v-if="oil['payment_type'] == 0">Cash</div>
<div v-else-if="oil['payment_type'] == 1">CC</div>
<div v-else-if="oil['payment_type'] == 2">Cash/CC</div>
<div v-else-if="oil['payment_type'] == 3">Check</div>
<div v-else-if="oil['payment_type'] == 4">Other</div>
<div v-else></div>
</td>
<td class="flex gap-5">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">View Delivery</button>
</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Edit Delivery</button>
</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil['id'] } }"
v-if="oil['delivery_status'] != 10">
<button class="btn btn-secondary btn-sm">Finalize</button>
</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil['id'] } }">
<button class="btn btn-success btn-sm">
Print Ticket
</button>
<router-link :to="{ name: 'customerProfile', params: { id: oil.customer_id } }" class="link link-hover">
{{ oil.customer_name }}
</router-link>
</td>
<td>
<span class="badge badge-sm" :class="{
'badge-warning': oil.delivery_status == 0,
'badge-success': [1, 10].includes(oil.delivery_status),
'badge-info': oil.delivery_status == 2,
'badge-error': [3, 5].includes(oil.delivery_status),
}">
<span v-if="oil.delivery_status == 0">Waiting</span>
<span v-else-if="oil.delivery_status == 1">Delivered</span>
<span v-else-if="oil.delivery_status == 2">Out for Delivery</span>
<span v-else-if="oil.delivery_status == 3">Cancelled</span>
<span v-else-if="oil.delivery_status == 4">Partial Delivery</span>
<span v-else-if="oil.delivery_status == 5">Issue</span>
<span v-else-if="oil.delivery_status == 10">Finalized</span>
</span>
</td>
<td>
<div>{{ oil.customer_town }}</div>
<div class="text-xs opacity-70">{{ oil.customer_address }}</div>
</td>
<td>
<span v-if="oil.customer_asked_for_fill == 1" class="badge badge-info">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</td>
<td>{{ oil.expected_delivery_date }}</td>
<td>
<div class="flex flex-col gap-1">
<span v-if="oil.prime" class="badge badge-error badge-xs">PRIME</span>
<span v-if="oil.same_day" class="badge badge-error badge-xs">SAME DAY</span>
<span v-if="oil.emergency" class="badge badge-error badge-xs">EMERGENCY</span>
</div>
</td>
<td>
<span v-if="oil.payment_type == 0">Cash</span>
<span v-else-if="oil.payment_type == 1">CC</span>
<span v-else-if="oil.payment_type == 2">Cash/CC</span>
<span v-else-if="oil.payment_type == 3">Check</span>
<span v-else-if="oil.payment_type == 4">Other</span>
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="oil.delivery_status != 10" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="25" :options="options" class="mt-10">
</pagination>
<div class="flex justify-center mb-10"> {{ recordsLength }} items Found</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="oil in deliveries" :key="oil.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ oil.customer_name }}</h2>
<p class="text-xs text-gray-400">Delivery #{{ oil.id }}</p>
</div>
<div class="badge" :class="{
'badge-warning': oil.delivery_status == 0,
'badge-success': [1, 10].includes(oil.delivery_status),
'badge-info': oil.delivery_status == 2,
'badge-error': [3, 5].includes(oil.delivery_status),
}">
<span v-if="oil.delivery_status == 0">Waiting</span>
<span v-else-if="oil.delivery_status == 1">Delivered</span>
<span v-else-if="oil.delivery_status == 2">Out for Delivery</span>
<span v-else-if="oil.delivery_status == 3">Cancelled</span>
<span v-else-if="oil.delivery_status == 4">Partial Delivery</span>
<span v-else-if="oil.delivery_status == 5">Issue</span>
<span v-else-if="oil.delivery_status == 10">Finalized</span>
</div>
</div>
<div class="flex gap-2 mt-2">
<div v-if="oil.prime" class="badge badge-error badge-sm">PRIME</div>
<div v-if="oil.same_day" class="badge badge-error badge-sm">SAME DAY</div>
<div v-if="oil.emergency" class="badge badge-error badge-sm">EMERGENCY</div>
</div>
<div class="text-sm mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<p><strong class="font-semibold">Address:</strong> {{ oil.customer_address }}</p>
<p><strong class="font-semibold">Town:</strong> {{ oil.customer_town }}</p>
<p><strong class="font-semibold">Date:</strong> {{ oil.expected_delivery_date }}</p>
<p><strong class="font-semibold">Gallons:</strong>
<span v-if="oil.customer_asked_for_fill" class="badge badge-info badge-xs">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</p>
</div>
<div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="oil.delivery_status != 10" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="25" :options="options">
</pagination>
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'
@@ -170,7 +196,7 @@ export default defineComponent({
delivery_count_delivered: 0,
token: null,
user: null,
deliveries: [],
deliveries: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,

View File

@@ -1,10 +1,7 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs mb-10">
<ul>

View File

@@ -1,10 +1,7 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs mb-10">
<ul>

View File

@@ -1,384 +1,202 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10">
<div class="text-sm breadcrumbs mb-10">
<!-- Main Content -->
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'customer' }">
Customers
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li><router-link :to="{ name: 'customer' }">Customers</router-link></li>
<li>View Delivery</li>
</ul>
</div>
<h1 class="text-3xl font-bold mt-4 border-b border-gray-600 pb-2">
Delivery #{{ deliveryOrder.id }}
</h1>
<div class="grid grid-cols-1 rounded-md pb-5">
<div class="text-2xl border-b-2 border-gray-500 mb-10">
View Delivery # {{ deliveryOrder.id }}
</div>
</div>
<div class="grid grid-cols-12">
<div class="col-span-6">
<div class="col-span-12 font-bold">
Customer
<!-- TOP SECTION: Customer & Status Info -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 my-6">
<!-- Customer Info Card -->
<div class="bg-neutral rounded-lg p-5">
<div class="flex justify-between items-center mb-4">
<div>
<div class="text-xl font-bold">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</div>
<div class="text-sm text-gray-400">Account: {{ customer.account_number }}</div>
</div>
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm">
View Profile
</router-link>
</div>
<div class="col-span-6 p-5 text-gray-500 text-sm">
<div class="grid grid-cols-12">
<div class="col-span-12 flex ">
{{ customer.account_number }}
</div>
<div class="col-span-12 flex">
{{ customer.customer_first_name }}
{{ customer.customer_last_name }}
</div>
<div class="col-span-12 flex">{{customer.customer_address}}</div>
<div class="col-span-12 flex ">
<div class="pr-2">
{{ customer.customer_town }},
</div>
<div class="pr-2">
<div v-if="customer.customer_state == 0">Massachusetts</div>
<div v-else-if="customer.customer_state == 1">Rhode Island</div>
<div v-else-if="customer.customer_state == 2">New Hampshire</div>
<div v-else-if="customer.customer_state == 3">Maine</div>
<div v-else-if="customer.customer_state == 4">Vermont</div>
<div v-else-if="customer.customer_state == 5">Maine</div>
<div v-else-if="customer.customer_state == 6">New York</div>
<div v-else>Unknown state</div>
</div>
<div class="pr-2">
{{ customer.customer_zip }}
</div>
</div>
<div>
<div>{{ customer.customer_address }}</div>
<div v-if="customer.customer_apt && customer.customer_apt !== 'None'">Apt: {{ customer.customer_apt }}</div>
<div>
{{ customer.customer_town }},
<span v-if="customer.customer_state == 0">Massachusetts</span>
<span v-else-if="customer.customer_state == 1">Rhode Island</span>
<span v-else-if="customer.customer_state == 2">New Hampshire</span>
<span v-else-if="customer.customer_state == 3">Maine</span>
<span v-else-if="customer.customer_state == 4">Vermont</span>
<span v-else-if="customer.customer_state == 5">Connecticut</span>
<span v-else-if="customer.customer_state == 6">New York</span>
<span v-else>Unknown state</span>
{{ customer.customer_zip }}
</div>
<div class="text-sm text-gray-400 mt-1">
<span v-if="customer.customer_home_type == 0">Residential</span>
<span v-else-if="customer.customer_home_type == 1">Apartment</span>
<span v-else-if="customer.customer_home_type == 2">Condo</span>
<span v-else-if="customer.customer_home_type == 3">Commercial</span>
<span v-else-if="customer.customer_home_type == 4">Business</span>
<span v-else-if="customer.customer_home_type == 5">Construction</span>
<span v-else-if="customer.customer_home_type == 6">Container</span>
</div>
<div class="mt-2">{{ customer.customer_phone_number }}</div>
</div>
</div>
<div class="col-span-12 flex" v-if="customer.customer_apt !== 'None'">
{{ customer.customer_apt }}
</div>
<div class="col-span-12 flex">
<div v-if="customer.customer_home_type == 0">Residential</div>
<div v-else-if="customer.customer_home_type == 1">apartment</div>
<div v-else-if="customer.customer_home_type == 2">condo</div>
<div v-else-if="customer.customer_home_type == 3">commercial</div>
<div v-else-if="customer.customer_home_type == 4">business</div>
<div v-else-if="customer.customer_home_type == 5">construction</div>
<div v-else-if="customer.customer_home_type == 6">container</div>
</div>
<div class="col-span-12 flex">
{{ customer.customer_phone_number }}
<!-- Delivery Status Card -->
<div class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Delivery Status</h3>
<div class="space-y-3">
<div>
<div class="font-bold text-sm">Current Status</div>
<div class="badge badge-lg"
:class="{
'badge-success': [1, 10].includes(deliveryOrder.delivery_status),
'badge-info': deliveryOrder.delivery_status == 2,
'badge-error': deliveryOrder.delivery_status == 5,
'badge-warning': ![1, 10, 2, 5].includes(deliveryOrder.delivery_status)
}">
<span v-if="deliveryOrder.delivery_status == 0">Waiting</span>
<span v-else-if="deliveryOrder.delivery_status == 1">Delivered</span>
<span v-else-if="deliveryOrder.delivery_status == 2">Out for Delivery</span>
<span v-else-if="deliveryOrder.delivery_status == 3">Tomorrow</span>
<span v-else-if="deliveryOrder.delivery_status == 4">Partial Delivery</span>
<span v-else-if="deliveryOrder.delivery_status == 5">Misdelivery</span>
<span v-else-if="deliveryOrder.delivery_status == 6">Unknown</span>
<span v-else-if="deliveryOrder.delivery_status == 10">Finalized</span>
</div>
</div>
<div>
<div class="font-bold text-sm">Assigned Driver</div>
<div>{{ deliveryOrder.driver_first_name }} {{ deliveryOrder.driver_last_name }}</div>
</div>
<div>
<div class="font-bold text-sm">Scheduled Date</div>
<div>{{ deliveryOrder.expected_delivery_date }}</div>
</div>
<div v-if="deliveryOrder.delivery_status === 10">
<div class="font-bold text-sm">Date Delivered</div>
<div>{{ (deliveryOrder.when_delivered) }}</div>
</div>
<div>
<div class="font-bold text-sm">Date Ordered</div>
<div>{{ (deliveryOrder.when_ordered) }}</div>
</div>
</div>
</div>
<div class="col-span-6 ">
</div>
</div>
<div class="grid grid-cols-12">
<div class="col-span-6">
<div class="col-span-12 font-bold">
Delivery Status
</div>
<div class="grid grid-cols-12 p-5">
<div class="col-span-12 font-bold">
Status
</div>
<div class="col-span-12 text-sm mb-5 text-gray-500">
<div v-if="deliveryOrder.delivery_status == 0">waiting</div>
<div v-else-if="deliveryOrder.delivery_status == 1">delivered</div>
<div v-else-if="deliveryOrder.delivery_status == 2">Out for Delivery</div>
<div v-else-if="deliveryOrder.delivery_status == 3">tommorrow</div>
<div v-else-if="deliveryOrder.delivery_status == 4">Partial Delivery</div>
<div v-else-if="deliveryOrder.delivery_status == 5">misdelivery</div>
<div v-else-if="deliveryOrder.delivery_status == 6">unknown</div>
<div v-else-if="deliveryOrder.delivery_status == 10">Finalized</div>
<div v-else></div>
</div>
<div class="col-span-12 font-bold ">
Scheduled date/time
</div>
<div class="col-span-12 mb-5 text-sm text-gray-500">
{{ deliveryOrder.expected_delivery_date }}
</div>
<div class="col-span-12 font-bold">
When Called
</div>
<div class="col-span-12 mb-5 text-sm text-gray-500">
{{ (deliveryOrder.when_ordered) }}
</div>
<div v-if="deliveryOrder.delivery_status == 10">
<div class="col-span-12 font-bold">
When Delivered
</div>
<div class="col-span-12 mb-5 text-sm text-gray-500">
{{ (deliveryOrder.when_delivered) }}
<!-- BOTTOM SECTION: Delivery & Financial Details -->
<div class="bg-neutral rounded-lg p-6">
<div class="grid grid-cols-1 xl:grid-cols-2 gap-8">
<!-- Left Column: Pricing, Gallons, Promo -->
<div class="space-y-4">
<!-- Gallons -->
<div class="p-4 border rounded-md">
<label class="label-text font-bold">Gallons Ordered</label>
<div class="text-lg mt-1">
<span v-if="deliveryOrder.customer_asked_for_fill == 1" class="badge badge-lg badge-info">FILL</span>
<span v-else>{{ deliveryOrder.gallons_ordered }} gallons</span>
</div>
</div>
<div class="col-span-12 font-bold ">
Driver:
</div>
<div class="col-span-12 text-gray-500">
{{ deliveryOrder.driver_first_name }} {{ deliveryOrder.driver_last_name }}
</div>
</div>
<div class="col-span-12 font-bold">
Info
</div>
<div class="grid grid-cols-12 p-5">
<div class="col-span-12 text-gray-500">
<div v-if="deliveryOrder.prime == 1">
Prime Required: Yes
<!-- Pricing & Fees -->
<div class="p-4 border rounded-md space-y-2">
<label class="label-text font-bold">Estimated / Finalized Total</label>
<!-- Finalized View -->
<div v-if="deliveryOrder.delivery_status == 10" class="text-2xl font-mono">
${{ deliveryMoney.total_amount_oil }}
</div>
<div v-if="deliveryOrder.prime == 0">
Prime Required: No
</div>
</div>
<div class="col-span-12 text-gray-500">
<div v-if="deliveryOrder.same_day == 1">
Same Day: Yes
</div>
<div v-if="deliveryOrder.same_day == 0">
Same Day: No
</div>
</div>
<div class="col-span-12 text-gray-500">
<div v-if="deliveryOrder.emergency == 1">
Emergency: Yes
</div>
<div v-if="deliveryOrder.emergency == 0">
Emergency: No
</div>
</div>
</div>
<div class="col-span-12 font-bold">
Dispatcher Notes
</div>
<div class="grid grid-cols-12 p-5">
<div class="col-span-12 text-gray-500">
{{ deliveryOrder.dispatcher_notes }}
</div>
</div>
</div>
<div class="col-span-6">
<div class="col-span-12 font-bold">
Gallons
</div>
<div class="grid grid-cols-12 p-5">
<div class="col-span-12 text-sm text-gray-500">
<div v-if="deliveryOrder.customer_asked_for_fill == 1">FILL</div>
<div v-else>{{ deliveryOrder.gallons_ordered }} gallons</div>
</div>
</div>
<div v-if="deliveryOrder.delivery_status !== 10">
<div class="col-span-12 font-bold">
Promo
</div>
<div class="grid grid-cols-12 p-5">
<div class="col-span-12 text-sm text-gray-500">
<!-- Estimated View -->
<div v-else class="space-y-1">
<div v-if="deliveryOrder.promo_id !== null">
<div class="">{{ promo.name_of_promotion }}</div>
<div class="">{{ promo.description }}</div>
<div class="">{{ promo.money_off_delivery }} off a gallon</div>
<div class="">{{ promo.text_on_ticket }}</div>
<div>Before Discount: ${{ total_amount }}</div>
<div>Discount: -${{ discount }}</div>
<div class="font-bold">Subtotal: ${{ total_amount_after_discount }}</div>
</div>
<div v-else>No Promo Added</div>
<div v-else class="font-bold text-lg">${{ total_amount }}</div>
<div v-if="deliveryOrder.prime == 1" class="text-sm text-gray-400">+ ${{ pricing.price_prime }} Prime Fee</div>
<div v-if="deliveryOrder.emergency == 1" class="text-sm text-gray-400">+ ${{ pricing.price_emergency }} Emergency Fee</div>
<div v-if="deliveryOrder.same_day == 1" class="text-sm text-gray-400">+ ${{ pricing.price_same_day }} Same Day Fee</div>
</div>
</div>
<!-- Promotion -->
<div v-if="deliveryOrder.promo_id !== null && deliveryOrder.delivery_status !== 10" class="p-4 border rounded-md">
<label class="label-text font-bold">Promotion Applied</label>
<div class="mt-1">
<div class="font-semibold">{{ promo.name_of_promotion }} (${{ promo.money_off_delivery }} off)</div>
<div class="text-sm text-gray-400">{{ promo.description }}</div>
</div>
</div>
</div>
<div v-if="deliveryOrder.delivery_status !== 10">
<div class="col-span-12 font-bold">
Estimated Amount
</div>
<div class="grid grid-cols-12 p-5">
<div class="col-span-12 text-sm text-gray-500">
<div class="col-span-12 text-gray-500">
<div v-if="deliveryOrder.customer_asked_for_fill == 1"> FILL (250)</div>
<div v-else> Gallons Ordered: {{ deliveryOrder.gallons_ordered }}</div>
<!-- Right Column: Payment, Notes, Actions -->
<div class="space-y-4">
<!-- Payment -->
<div class="p-4 border rounded-md">
<label class="label-text font-bold">Payment Method</label>
<div class="mt-1">
<div class="text-lg">
<span v-if="deliveryOrder.payment_type == 0">Cash</span>
<span v-else-if="deliveryOrder.payment_type == 1">Credit Card</span>
<span v-else-if="deliveryOrder.payment_type == 2">Credit Card & Cash</span>
<span v-else-if="deliveryOrder.payment_type == 3">Check</span>
<span v-else-if="deliveryOrder.payment_type == 4">Other</span>
<span v-else>Not Specified</span>
</div>
<div v-if="deliveryOrder.promo_id !== null">
<div class="col-span-12 text-sm text-gray-500">
Before Discount: {{ total_amount }}
</div>
<div class="col-span-12 text-sm text-gray-500">
Discount Amount: ${{ discount }}
</div>
<div class="col-span-12 text-sm text-gray-500">
Promo Amount: ${{ total_amount_after_discount }}
</div>
</div>
<div v-else>
<div class="col-span-12 text-sm text-gray-500">
${{ total_amount }}
</div>
</div>
<div class="col-span-12 py-3" v-if="deliveryOrder.prime == 1">
Prime Fee: {{ pricing.price_prime }}
</div>
<div class="col-span-12 py-3" v-if="deliveryOrder.emergency == 1">
Emergency Fee: {{ pricing.price_emergency }}
</div>
<div class="col-span-12 py-3" v-if="deliveryOrder.same_day == 1">
Same Day: {{ pricing.price_same_day }}
<div v-if="userCardfound && [1, 2, 3].includes(deliveryOrder.payment_type)" class="bg-base-100 p-3 rounded-md mt-2 text-sm">
<div class="font-mono">{{ userCard.type_of_card }}</div>
<div class="font-mono">{{ userCard.card_number }}</div>
<div>{{ userCard.name_on_card }}</div>
<div>Expires: {{ userCard.expiration_month }}/{{ userCard.expiration_year }}</div>
</div>
</div>
</div>
<!-- Notes & Options -->
<div class="p-4 border rounded-md">
<label class="label-text font-bold">Notes & Options</label>
<div class="mt-2 space-y-1 text-sm">
<div><span class="font-semibold">Prime Required:</span> {{ deliveryOrder.prime ? 'Yes' : 'No' }}</div>
<div><span class="font-semibold">Same Day:</span> {{ deliveryOrder.same_day ? 'Yes' : 'No' }}</div>
<div><span class="font-semibold">Emergency:</span> {{ deliveryOrder.emergency ? 'Yes' : 'No' }}</div>
</div>
<div class="prose prose-sm mt-4 max-w-none">
<blockquote class="text-gray-400">{{ deliveryOrder.dispatcher_notes || 'No dispatcher notes provided.' }}</blockquote>
</div>
</div>
<!-- Actions -->
<div class="flex flex-wrap gap-2 pt-4">
<router-link :to="{ name: 'deliveryEdit', params: { id: deliveryOrder.id } }">
<button class="btn btn-secondary btn-sm">Edit Delivery</button>
</router-link>
<router-link :to="{ name: 'Ticket', params: { id: deliveryOrder.id } }">
<button class="btn btn-success btn-sm">Print Ticket</button>
</router-link>
</div>
</div>
<div v-if="deliveryOrder.delivery_status == 10">
<div class="col-span-12 font-bold" v-if="deliveryOrder.delivery_status == 10">
Finalized Amount
</div>
<div class="grid grid-cols-12 p-5">
<div class="col-span-12 text-sm text-gray-500">
<div>{{ deliveryMoney.total_amount_oil }}</div>
</div>
</div>
</div>
<div class="col-span-12 font-bold">
Payment
</div>
<div class="grid grid-cols-12 p-5">
<div class="col-span-12 text-sm">
<div v-if="deliveryOrder.payment_type == 0">Cash</div>
<div v-else-if="deliveryOrder.payment_type == 1">Credit Card</div>
<div v-else-if="deliveryOrder.payment_type == 2">Credit Card & cash</div>
<div v-else-if="deliveryOrder.payment_type == 3">Check</div>
<div v-else-if="deliveryOrder.payment_type == 4">Check</div>
<div v-else>No Payment Type Added</div>
</div>
<div class="col-span-12" v-if="deliveryOrder.payment_type == 1">
<div class="flex" v-if="userCardfound">
<div class="basis-1/3 p-2">
<div class="rounded-md border-2 bg-accent">
<div class="flex p-3">
{{ userCard.type_of_card }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.name_on_card }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.card_number }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.expiration_month }}/ {{ userCard.expiration_year }}
</div>
</div>
</div>
</div>
</div>
<div class="col-span-12" v-if="deliveryOrder.payment_type == 2">
<div class="flex" v-if="userCardfound">
<div class="basis-1/3 p-2">
<div class="bg-accent rounded-md border-2 ">
<div class="flex p-3">
{{ userCard.type_of_card }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.name_on_card }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.card_number }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.expiration_month }}/ {{ userCard.expiration_year }}
</div>
</div>
</div>
</div>
</div>
<div class="col-span-12" v-if="deliveryOrder.payment_type == 3">
<div class="flex" v-if="userCardfound">
<div class="basis-1/3 p-2">{{ userCard }}
<div class="bg-accent rounded-md border-2 ">
<div class="flex p-3">
{{ userCard.type_of_card }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.name_on_card }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.card_number }}
</div>
<div class="flex p-1 pl-4">
{{ userCard.expiration_month }}/ {{ userCard.expiration_year }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-span-12 " v-if="deliveryOrder.id">
<router-link :to="{ name: 'customerProfile', params: { id: deliveryOrder['customer_id'] } }">
<button class="btn btn-neutral btn-sm">View Customer</button>
</router-link>
</div>
<div class="col-span-12 pt-5" v-if="deliveryOrder.id">
<router-link :to="{ name: 'deliveryEdit', params: { id: deliveryOrder['id'] } }">
<button class="btn btn-sm btn-secondary">Edit</button>
</router-link>
</div>
<div class="col-span-12 pt-5" v-if="deliveryOrder.id">
<router-link :to="{ name: 'Ticket', params: { id: deliveryOrder['id'] } }">
<button class="btn btn-success btn-sm">Print Ticket</button>
</router-link>
</div>
</div>
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'

View File

@@ -1,104 +1,123 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs pb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Cancelled Deliveries</li>
</ul>
</div>
<h1 class="text-3xl font-bold mt-4">Cancelled Deliveries</h1>
<div class="flex start pb-10 text-2xl">Cancelled Deliveries </div>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Title and Count -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">Archived Cancelled Deliveries</h2>
<div class="badge badge-ghost">{{ recordsLength }} items Found</div>
</div>
<div class="overflow-x-auto bg-neutral">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Address</th>
<th>Town</th>
<th>Gallons</th>
<th>Date</th>
<th>Automatic</th>
<th>Prime</th>
<th>Same Day</th>
</tr>
</thead>
<tbody>
<!-- row 1 -->
<tr v-for="oil in deliveries" :key="oil['id']"> <router-link :to="{ name: 'customerProfile', params: { id: oil['customer_id'] } }">
<td>
<div class="hover:text-accent">{{ oil['customer_name'] }} </div>
</td>
</router-link>
<td>
<div v-if="oil['delivery_status'] == 0">Waiting</div>
<div v-else-if="oil['delivery_status'] == 1">cancelled</div>
<div v-else-if="oil['delivery_status'] == 2">Out for Delivery</div>
<div v-else-if="oil['delivery_status'] == 3">tommorrow</div>
<div v-else-if="oil['delivery_status'] == 4">Partial Delivery</div>
<div v-else-if="oil['delivery_status'] == 5">Issue</div>
<div v-else-if="oil['delivery_status'] == 10" class="bg-green-600">Finalized</div>
<div v-else></div>
<div class="divider"></div>
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Delivery #</th>
<th>Name</th>
<th>Status</th>
<th>Town / Address</th>
<th>Gallons</th>
<th>Date</th>
<th>Options</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="oil in deliveries" :key="oil.id" class="hover:bg-blue-600 hover:text-white">
<td>{{ oil.id }}</td>
<td>
<router-link :to="{ name: 'customerProfile', params: { id: oil.customer_id } }" class="link link-hover">
{{ oil.customer_name }}
</router-link>
</td>
<td>
<span class="badge badge-sm badge-error">Cancelled</span>
</td>
<td>
<div>{{ oil.customer_town }}</div>
<div class="text-xs opacity-70">{{ oil.customer_address }}</div>
</td>
<td>
<span v-if="oil.customer_asked_for_fill == 1" class="badge badge-info">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</td>
<td>{{ oil.expected_delivery_date }}</td>
<td>
<div class="flex flex-col gap-1">
<span v-if="oil.prime" class="badge badge-error badge-xs">PRIME</span>
<span v-if="oil.same_day" class="badge badge-error badge-xs">SAME DAY</span>
</div>
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="oil in deliveries" :key="oil.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ oil.customer_name }}</h2>
<p class="text-xs text-gray-400">Delivery #{{ oil.id }}</p>
</div>
<div class="badge badge-error">
Cancelled
</div>
</div>
<div class="flex gap-2 mt-2">
<div v-if="oil.prime" class="badge badge-error badge-sm">PRIME</div>
<div v-if="oil.same_day" class="badge badge-error badge-sm">SAME DAY</div>
</div>
<div class="text-sm mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<p><strong class="font-semibold">Address:</strong> {{ oil.customer_address }}</p>
<p><strong class="font-semibold">Town:</strong> {{ oil.customer_town }}</p>
<p><strong class="font-semibold">Gallons:</strong>
<span v-if="oil.customer_asked_for_fill" class="badge badge-info badge-xs">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</p>
<p><strong class="font-semibold">Date:</strong> {{ oil.expected_delivery_date }}</p>
</div>
</td>
<td>{{ oil['customer_address'] }}</td>
<td>{{ oil['customer_town'] }}</td>
<td>
<div v-if="oil['customer_asked_for_fill'] == 1">Fill</div>
<div v-else> {{ oil['gallons_ordered'] }}</div>
</td>
<td>{{ oil['expected_delivery_date'] }}</td>
<td>
<div v-if="oil['automatic'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['prime'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['same_day'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td class="flex gap-5">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil['id'] } }">
<button class="btn btn-secondary">View delivery</button>
</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil['id'] } }">
<button class="btn btn-secondary">Edit Delivery</button>
</router-link>
<!-- <button @click.prevent="deleteCall(oil['id'])" class="btn bg-red-600 text-black">DELETE</button> -->
</td>
</tr>
</tbody>
</table>
<div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
</div>
</div>
</div>
</div>
</div>
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50"
:options="options" class="mt-10">
</pagination>
<div class="flex justify-center mb-10"> {{ recordsLength }} items Found</div>
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options">
</pagination>
</div>
</div>
</div>
<Footer/>
<Footer />
</template>
<script lang="ts">
@@ -124,7 +143,7 @@ export default defineComponent({
return {
token: null,
user: null,
deliveries: [],
deliveries: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,

View File

@@ -1,99 +1,123 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs pb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Delivered Deliveries</li>
</ul>
</div>
<h1 class="text-3xl font-bold mt-4">Delivered Deliveries</h1>
<div class="flex start pb-10 text-2xl">Delivered Deliveries </div>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Title and Count -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">Deliveries Awaiting Finalization</h2>
<div class="badge badge-ghost">{{ recordsLength }} items Found</div>
</div>
<div class="divider"></div>
<div class="overflow-x-auto bg-neutral">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Address</th>
<th>Town</th>
<th>Gallons</th>
<th>Date</th>
<th>Automatic</th>
<th>Prime</th>
<th>Same Day</th>
</tr>
</thead>
<tbody>
<!-- row 1 -->
<tr v-for="oil in deliveries" :key="oil['id']">
<router-link :to="{ name: 'customerProfile', params: { id: oil['customer_id'] } }">
<td>
<div class="hover:text-accent">{{ oil['customer_name'] }} </div>
</td>
</router-link>
<td>
<div v-if="oil['delivery_status'] == 0">Waiting</div>
<div v-else-if="oil['delivery_status'] == 1">cancelled</div>
<div v-else-if="oil['delivery_status'] == 2">Out for Delivery</div>
<div v-else-if="oil['delivery_status'] == 3">tommorrow</div>
<div v-else-if="oil['delivery_status'] == 4">Partial Delivery</div>
<div v-else-if="oil['delivery_status'] == 5">Issue</div>
<div v-else-if="oil['delivery_status'] == 10" class="bg-green-600">Finalized</div>
<div v-else></div>
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Delivery #</th>
<th>Name</th>
<th>Status</th>
<th>Town / Address</th>
<th>Gallons</th>
<th>Date</th>
<th>Options</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="oil in deliveries" :key="oil.id" class="hover:bg-blue-600 hover:text-white">
<td>{{ oil.id }}</td>
<td>
<router-link :to="{ name: 'customerProfile', params: { id: oil.customer_id } }" class="link link-hover">
{{ oil.customer_name }}
</router-link>
</td>
<td>
<span class="badge badge-sm badge-success">Delivered</span>
</td>
<td>
<div>{{ oil.customer_town }}</div>
<div class="text-xs opacity-70">{{ oil.customer_address }}</div>
</td>
<td>
<span v-if="oil.customer_asked_for_fill == 1" class="badge badge-info">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</td>
<td>{{ oil.expected_delivery_date }}</td>
<td>
<div class="flex flex-col gap-1">
<span v-if="oil.prime" class="badge badge-error badge-xs">PRIME</span>
<span v-if="oil.same_day" class="badge badge-error badge-xs">SAME DAY</span>
</div>
</td>
<td class="text-right">
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" class="btn btn-sm btn-accent">
Finalize
</router-link>
</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="oil in deliveries" :key="oil.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ oil.customer_name }}</h2>
<p class="text-xs text-gray-400">Delivery #{{ oil.id }}</p>
</div>
<div class="badge badge-success">
Delivered
</div>
</div>
<div class="flex gap-2 mt-2">
<div v-if="oil.prime" class="badge badge-error badge-sm">PRIME</div>
<div v-if="oil.same_day" class="badge badge-error badge-sm">SAME DAY</div>
</div>
<div class="text-sm mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<p><strong class="font-semibold">Address:</strong> {{ oil.customer_address }}</p>
<p><strong class="font-semibold">Town:</strong> {{ oil.customer_town }}</p>
<p><strong class="font-semibold">Gallons:</strong>
<span v-if="oil.customer_asked_for_fill" class="badge badge-info badge-xs">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</p>
<p><strong class="font-semibold">Date:</strong> {{ oil.expected_delivery_date }}</p>
</div>
</td>
<td>{{ oil['customer_address'] }}</td>
<td>{{ oil['customer_town'] }}</td>
<td>
<div v-if="oil['customer_asked_for_fill'] == 1">Fill</div>
<div v-else> {{ oil['gallons_ordered'] }}</div>
</td>
<td>{{ oil['expected_delivery_date'] }}</td>
<td>
<div v-if="oil['automatic'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['prime'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['same_day'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td class="flex gap-5">
<router-link :to="{ name: 'finalizeTicket', params: { id: oil['id'] } }">
<button class="btn btn-success btn-sm">Finalize</button>
</router-link>
</td>
</tr>
</tbody>
</table>
<div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" class="btn btn-sm btn-accent">
Finalize
</router-link>
</div>
</div>
</div>
</div>
</div>
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50"
:options="options" class="mt-10">
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options">
</pagination>
<div class="flex justify-center mb-10"> {{ recordsLength }} items Found</div>
</div>
</div>
</div>
<Footer/>
<Footer />
</template>
<script lang="ts">
@@ -119,7 +143,7 @@ export default defineComponent({
return {
token: null,
user: null,
deliveries: [],
deliveries: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,

View File

@@ -1,111 +1,125 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs pb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Finalized Deliveries</li>
</ul>
</div>
<div class="flex start pb-10 text-2xl">Finalized Delivery </div>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Title and Count -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">Completed and Finalized Deliveries</h2>
<div class="badge badge-ghost">{{ recordsLength }} items Found</div>
</div>
<div class="divider"></div>
<div class="overflow-x-auto bg-neutral">
<table class="table">
<!-- head -->
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Ticket Id</th>
<th>Name</th>
<th>Status</th>
<th>Address</th>
<th>Town</th>
<th>Gallons</th>
<th>Date</th>
<th>Automatic</th>
<th>Prime</th>
<th>Same Day</th>
</tr>
<tr>
<th>Ticket #</th>
<th>Name</th>
<th>Status</th>
<th>Town / Address</th>
<th>Gallons</th>
<th>Date</th>
<th>Options</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<!-- row 1 -->
<tr v-for="oil in deliveries" :key="oil['id']">
<td>{{ oil['id'] }} </td>
<router-link :to="{ name: 'customerProfile', params: { id: oil['customer_id'] } }">
<td>
<div class="hover:text-accent">{{ oil['customer_name'] }} </div>
<tr v-for="oil in deliveries" :key="oil.id" class="hover:bg-blue-600 hover:text-white">
<td>{{ oil.id }}</td>
<td>
<router-link :to="{ name: 'customerProfile', params: { id: oil.customer_id } }" class="link link-hover">
{{ oil.customer_name }}
</router-link>
</td>
</router-link>
<td>
<div v-if="oil['delivery_status'] == 0">Waiting</div>
<div v-else-if="oil['delivery_status'] == 1">cancelled</div>
<div v-else-if="oil['delivery_status'] == 2">Out for Delivery</div>
<div v-else-if="oil['delivery_status'] == 3">tommorrow</div>
<div v-else-if="oil['delivery_status'] == 4">Partial Delivery</div>
<div v-else-if="oil['delivery_status'] == 5">Issue</div>
<div v-else-if="oil['delivery_status'] == 10" class="bg-green-600">Finalized</div>
<div v-else></div>
</td>
<td>{{ oil['customer_address'] }}</td>
<td>{{ oil['customer_town'] }}</td>
<td>
<div v-if="oil['customer_asked_for_fill'] == 1">Fill</div>
<div v-else> {{ oil['gallons_ordered'] }}</div>
</td>
<td>{{ oil['expected_delivery_date'] }}</td>
<td>
<div v-if="oil['automatic'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['prime'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['same_day'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td class="flex gap-5">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">View Delivery</button>
</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Edit Delivery</button>
</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil['id'] } }">
<button class="btn btn-success btn-sm">
Print Ticket
</button>
</router-link>
<!-- <button @click.prevent="deleteCall(oil['id'])" class="btn bg-red-600 text-black">Delete</button> -->
</td>
</tr>
<td>
<span class="badge badge-sm badge-success">Finalized</span>
</td>
<td>
<div>{{ oil.customer_town }}</div>
<div class="text-xs opacity-70">{{ oil.customer_address }}</div>
</td>
<td>
<span v-if="oil.customer_asked_for_fill == 1" class="badge badge-info">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</td>
<td>{{ oil.expected_delivery_date }}</td>
<td>
<div class="flex flex-col gap-1">
<span v-if="oil.prime" class="badge badge-error badge-xs">PRIME</span>
<span v-if="oil.same_day" class="badge badge-error badge-xs">SAME DAY</span>
</div>
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50"
:options="options" class="mt-10">
</pagination>
<div class="flex justify-center mb-10"> {{ recordsLength }} items Found</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="oil in deliveries" :key="oil.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ oil.customer_name }}</h2>
<p class="text-xs text-gray-400">Ticket #{{ oil.id }}</p>
</div>
<div class="badge badge-success">
Finalized
</div>
</div>
<div class="flex gap-2 mt-2">
<div v-if="oil.prime" class="badge badge-error badge-sm">PRIME</div>
<div v-if="oil.same_day" class="badge badge-error badge-sm">SAME DAY</div>
</div>
<div class="text-sm mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<p><strong class="font-semibold">Address:</strong> {{ oil.customer_address }}</p>
<p><strong class="font-semibold">Town:</strong> {{ oil.customer_town }}</p>
<p><strong class="font-semibold">Gallons:</strong>
<span v-if="oil.customer_asked_for_fill" class="badge badge-info badge-xs">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</p>
<p><strong class="font-semibold">Date:</strong> {{ oil.expected_delivery_date }}</p>
</div>
<div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options">
</pagination>
</div>
</div>
<Footer/>
</template>
</div>
<Footer />
</template>
<script lang="ts">
import {defineComponent} from 'vue'
@@ -130,7 +144,7 @@
return {
token: null,
user: null,
deliveries: [],
deliveries: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,

View File

@@ -1,111 +1,124 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs pb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
</ul>
</div>
<div class="flex start pb-10 text-2xl">Issue with Delivery </div>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Title and Count -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">Deliveries Requiring Attention</h2>
<div class="badge badge-ghost">{{ recordsLength }} items Found</div>
</div>
<div class="divider"></div>
<div class="overflow-x-auto bg-neutral">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Ticket Id</th>
<th>Name</th>
<th>Status</th>
<th>Address</th>
<th>Town</th>
<th>Gallons</th>
<th>Date</th>
<th>Automatic</th>
<th>Prime</th>
<th>Same Day</th>
</tr>
</thead>
<tbody>
<!-- row 1 -->
<tr v-for="oil in deliveries" :key="oil['id']">
<td>{{ oil['id'] }} </td>
<router-link :to="{ name: 'customerProfile', params: { id: oil['customer_id'] } }">
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Ticket #</th>
<th>Name</th>
<th>Status</th>
<th>Town / Address</th>
<th>Gallons</th>
<th>Date</th>
<th>Options</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="oil in deliveries" :key="oil.id" class="hover:bg-blue-600 hover:text-white">
<td>{{ oil.id }}</td>
<td>
<div class="hover:text-accent">{{ oil['customer_name'] }} </div>
</td>
</router-link>
<td>
<div v-if="oil['delivery_status'] == 0">Waiting</div>
<div v-else-if="oil['delivery_status'] == 1">cancelled</div>
<div v-else-if="oil['delivery_status'] == 2">Out for Delivery</div>
<div v-else-if="oil['delivery_status'] == 3">tommorrow</div>
<div v-else-if="oil['delivery_status'] == 4">Partial Delivery</div>
<div v-else-if="oil['delivery_status'] == 5">Issue</div>
<div v-else-if="oil['delivery_status'] == 10" class="bg-green-600">Finalized</div>
<div v-else></div>
<router-link :to="{ name: 'customerProfile', params: { id: oil.customer_id } }" class="link link-hover">
{{ oil.customer_name }}
</router-link>
</td>
<td>
<span class="badge badge-sm badge-error">Issue</span>
</td>
<td>
<div>{{ oil.customer_town }}</div>
<div class="text-xs opacity-70">{{ oil.customer_address }}</div>
</td>
<td>
<span v-if="oil.customer_asked_for_fill == 1" class="badge badge-info">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</td>
<td>{{ oil.expected_delivery_date }}</td>
<td>
<div class="flex flex-col gap-1">
<span v-if="oil.prime" class="badge badge-error badge-xs">PRIME</span>
<span v-if="oil.same_day" class="badge badge-error badge-xs">SAME DAY</span>
</div>
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="oil in deliveries" :key="oil.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ oil.customer_name }}</h2>
<p class="text-xs text-gray-400">Ticket #{{ oil.id }}</p>
</div>
<div class="badge badge-error">
Issue
</div>
</div>
<div class="flex gap-2 mt-2">
<div v-if="oil.prime" class="badge badge-error badge-sm">PRIME</div>
<div v-if="oil.same_day" class="badge badge-error badge-sm">SAME DAY</div>
</div>
<div class="text-sm mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<p><strong class="font-semibold">Address:</strong> {{ oil.customer_address }}</p>
<p><strong class="font-semibold">Town:</strong> {{ oil.customer_town }}</p>
<p><strong class="font-semibold">Gallons:</strong>
<span v-if="oil.customer_asked_for_fill" class="badge badge-info badge-xs">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</p>
<p><strong class="font-semibold">Date:</strong> {{ oil.expected_delivery_date }}</p>
</div>
</td>
<td>{{ oil['customer_address'] }}</td>
<td>{{ oil['customer_town'] }}</td>
<td>
<div v-if="oil['customer_asked_for_fill'] == 1">Fill</div>
<div v-else> {{ oil['gallons_ordered'] }}</div>
</td>
<td>{{ oil['expected_delivery_date'] }}</td>
<td>
<div v-if="oil['automatic'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['prime'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['same_day'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td class="flex gap-5">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil['id'] } }">
<button class="btn btn-secondary">View</button>
</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil['id'] } }">
<button class="btn btn-secondary ">Edit</button>
</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">
Print Ticket
</button>
</router-link>
<!-- <button @click.prevent="deleteCall(oil['id'])" class="btn bg-red-600 text-black">Delete</button> -->
</td>
</tr>
</tbody>
</table>
<div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</div>
</div>
</div>
</div>
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50"
:options="options" class="mt-10">
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options">
</pagination>
<div class="flex justify-center mb-10"> {{ recordsLength }} items Found</div>
</div>
</div>
</div>
<Footer/>
<Footer />
</template>
<script lang="ts">
@@ -131,7 +144,7 @@ export default defineComponent({
return {
token: null,
user: null,
deliveries: [],
deliveries: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,

View File

@@ -1,139 +1,160 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
<div class="flex">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
</ul>
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs pb-10">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
</ul>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Title and Count -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">Deliveries Awaiting Payment</h2>
<div class="badge badge-ghost">{{ recordsLength }} items Found</div>
</div>
<div class="flex start pb-10 text-2xl">Pending Payment Deliveries </div>
<div class="flex justify-end pb-5">
</div>
<div class="overflow-x-auto">
<table class="table">
<!-- head -->
<thead class=" bg-neutral">
<div class="divider"></div>
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Id</th>
<th>Delivery #</th>
<th>Name</th>
<th>Status</th>
<th>Address</th>
<th>Town</th>
<th>Town / Address</th>
<th>Gallons</th>
<th>Date</th>
<th>Auto</th>
<th>Prime</th>
<th>Same Day</th>
<th>Emergency</th>
<th>Payment</th>
<th></th>
<th>Options</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody class="bg-neutral">
<!-- row 1 -->
<tr v-for="oil in deliveries" :key="oil['id']">
<td>{{ oil['id'] }} </td>
<router-link :to="{ name: 'customerProfile', params: { id: oil['customer_id'] } }">
<td>
<div class="hover:text-accent">{{ oil['customer_name'] }} </div>
</td>
</router-link>
<tbody>
<tr v-for="oil in deliveries" :key="oil.id" class="hover:bg-blue-600 hover:text-white">
<td>{{ oil.id }}</td>
<td>
<div v-if="oil['delivery_status'] == 0">Waiting</div>
<div v-else-if="oil['delivery_status'] == 1">cancelled</div>
<div v-else-if="oil['delivery_status'] == 2">Out for Delivery</div>
<div v-else-if="oil['delivery_status'] == 3">tommorrow</div>
<div v-else-if="oil['delivery_status'] == 4">Partial Delivery</div>
<div v-else-if="oil['delivery_status'] == 5">Issue</div>
<div v-else-if="oil['delivery_status'] == 10" class="bg-green-600">Finalized</div>
<div v-else></div>
</td>
<td>{{ oil['customer_address'] }}</td>
<td>{{ oil['customer_town'] }}</td>
<td>
<div v-if="oil['customer_asked_for_fill'] == 1">Fill</div>
<div v-else> {{ oil['gallons_ordered'] }}</div>
</td>
<td>{{ oil['expected_delivery_date'] }}</td>
<td>
<div v-if="oil['automatic'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['prime'] == 0">No</div>
<div v-else class="text-red-600">Yes</div>
</td>
<td>
<div v-if="oil['same_day'] == 0">No</div>
<div v-else class="text-red-600">Yes</div>
</td>
<td>
<div v-if="oil['emergency'] == 0">No</div>
<div v-else class="text-red-600">Yes</div>
</td>
<td>
<div v-if="oil['payment_type'] == 0">Cash</div>
<div v-else-if="oil['payment_type'] == 1">CC</div>
<div v-else-if="oil['payment_type'] == 2">Cash/CC</div>
<div v-else-if="oil['payment_type'] == 3">Check</div>
<div v-else-if="oil['payment_type'] == 4">Other</div>
<div v-else></div>
</td>
<td class="flex gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">View Delivery</button>
<router-link :to="{ name: 'customerProfile', params: { id: oil.customer_id } }" class="link link-hover">
{{ oil.customer_name }}
</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Edit Delivery</button>
</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Finalize</button>
</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil['id'] } }">
<button class="btn btn-success btn-sm">
Print Ticket
</button>
</router-link>
<!-- <button @click.prevent="deleteCall(oil['id'])" class="btn btn-error btn-sm">
Delete
</button> -->
</td>
<td>
<span class="badge badge-sm" :class="{
'badge-warning': oil.delivery_status == 0,
'badge-success': oil.delivery_status == 10,
'badge-info': oil.delivery_status == 2,
'badge-error': [1, 5].includes(oil.delivery_status),
}">
<span v-if="oil.delivery_status == 0">Waiting</span>
<span v-else-if="oil.delivery_status == 1">Cancelled</span>
<span v-else-if="oil.delivery_status == 2">Out for Delivery</span>
<span v-else-if="oil.delivery_status == 10">Finalized</span>
<span v-else>N/A</span>
</span>
</td>
<td>
<div>{{ oil.customer_town }}</div>
<div class="text-xs opacity-70">{{ oil.customer_address }}</div>
</td>
<td>
<span v-if="oil.customer_asked_for_fill == 1" class="badge badge-info">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</td>
<td>
<span v-if="oil.payment_type == 0">Cash</span>
<span v-else-if="oil.payment_type == 1">CC</span>
<span v-else-if="oil.payment_type == 2">Cash/CC</span>
<span v-else-if="oil.payment_type == 3">Check</span>
<span v-else-if="oil.payment_type == 4">Other</span>
</td>
<td>
<div class="flex flex-col gap-1">
<span v-if="oil.prime" class="badge badge-error badge-xs">PRIME</span>
<span v-if="oil.same_day" class="badge badge-error badge-xs">SAME DAY</span>
<span v-if="oil.emergency" class="badge badge-error badge-xs">EMERGENCY</span>
</div>
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options" class="mt-10">
</pagination>
<div class="flex justify-center mb-10"> {{ recordsLength }} items Found</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="oil in deliveries" :key="oil.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ oil.customer_name }}</h2>
<p class="text-xs text-gray-400">Delivery #{{ oil.id }}</p>
</div>
<div class="badge" :class="{
'badge-warning': oil.delivery_status == 0,
'badge-success': oil.delivery_status == 10,
'badge-info': oil.delivery_status == 2,
'badge-error': [1, 5].includes(oil.delivery_status),
}">
<span v-if="oil.delivery_status == 0">Waiting</span>
<span v-else-if="oil.delivery_status == 1">Cancelled</span>
<span v-else-if="oil.delivery_status == 2">Out for Delivery</span>
<span v-else-if="oil.delivery_status == 10">Finalized</span>
<span v-else>N/A</span>
</div>
</div>
<div class="flex gap-2 mt-2">
<div v-if="oil.prime" class="badge badge-error badge-sm">PRIME</div>
<div v-if="oil.same_day" class="badge badge-error badge-sm">SAME DAY</div>
<div v-if="oil.emergency" class="badge badge-error badge-sm">EMERGENCY</div>
</div>
<div class="text-sm mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<p><strong class="font-semibold">Address:</strong> {{ oil.customer_address }}</p>
<p><strong class="font-semibold">Town:</strong> {{ oil.customer_town }}</p>
<p><strong class="font-semibold">Gallons:</strong>
<span v-if="oil.customer_asked_for_fill" class="badge badge-info badge-xs">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</p>
<p><strong class="font-semibold">Payment:</strong>
<span v-if="oil.payment_type == 0">Cash</span>
<span v-else-if="oil.payment_type == 1">CC</span>
<span v-else-if="oil.payment_type == 2">Cash/CC</span>
<span v-else-if="oil.payment_type == 3">Check</span>
<span v-else-if="oil.payment_type == 4">Other</span>
</p>
</div>
<div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options">
</pagination>
</div>
</div>
<Footer />
</template>
</div>
<Footer />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
@@ -158,7 +179,7 @@
return {
token: null,
user: null,
deliveries: [],
deliveries: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,

View File

@@ -1,138 +1,170 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs pb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Today's Deliveries</li>
</ul>
</div>
<div class="flex start pb-10 text-2xl">Todays Deliveries </div>
<div class="flex justify-end pb-5">
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Search and Count -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">Todays Deliveries</h2>
<div class="form-control">
<label class="label pt-1 pb-0">
<span class="label-text-alt">{{ recordsLength }} deliveries found</span>
</label>
</div>
</div>
</div>
<div class="divider"></div>
<div class="overflow-x-auto">
<table class="table">
<!-- head -->
<thead class=" bg-neutral">
<tr>
<th>Id</th>
<th>Name</th>
<th>Status</th>
<th>Town</th>
<th>Address</th>
<th>Gallons</th>
<th>Date</th>
<th>Auto</th>
<th>Prime</th>
<th>Same Day</th>
<th>Emergency</th>
<th>Payment</th>
<th></th>
</tr>
</thead>
<tbody class="bg-neutral">
<!-- row 1 -->
<tr v-for="oil in deliveries" :key="oil['id']">
<td>{{ oil['id'] }} </td>
<router-link :to="{ name: 'customerProfile', params: { id: oil['customer_id'] } }">
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Delivery #</th>
<th>Name</th>
<th>Status</th>
<th>Town / Address</th>
<th>Gallons</th>
<th>Options</th>
<th>Payment</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="oil in deliveries" :key="oil.id" class="hover:bg-blue-600 hover:text-white">
<td>{{ oil.id }}</td>
<td>
<div class="hover:text-accent">{{ oil['customer_name'] }} </div>
<router-link :to="{ name: 'customerProfile', params: { id: oil.customer_id } }" class="link link-hover">
{{ oil.customer_name }}
</router-link>
</td>
</router-link>
<td>
<div v-if="oil['delivery_status'] == 0">Waiting</div>
<div v-else-if="oil['delivery_status'] == 1">cancelled</div>
<div v-else-if="oil['delivery_status'] == 2">Out for Delivery</div>
<div v-else-if="oil['delivery_status'] == 3">tommorrow</div>
<div v-else-if="oil['delivery_status'] == 4">Partial Delivery</div>
<div v-else-if="oil['delivery_status'] == 5">Issue</div>
<div v-else-if="oil['delivery_status'] == 10" class="bg-green-600">Finalized</div>
<div v-else></div>
<td>
<span class="badge badge-sm" :class="{
'badge-warning': oil.delivery_status == 0,
'badge-success': oil.delivery_status == 10,
'badge-info': oil.delivery_status == 2,
'badge-error': [1, 5].includes(oil.delivery_status),
}">
<span v-if="oil.delivery_status == 0">Waiting</span>
<span v-else-if="oil.delivery_status == 1">Cancelled</span>
<span v-else-if="oil.delivery_status == 2">Out for Delivery</span>
<span v-else-if="oil.delivery_status == 3">Tomorrow</span>
<span v-else-if="oil.delivery_status == 4">Partial</span>
<span v-else-if="oil.delivery_status == 5">Issue</span>
<span v-else-if="oil.delivery_status == 10">Finalized</span>
</span>
</td>
<td>
<div>{{ oil.customer_town }}</div>
<div class="text-xs opacity-70">{{ oil.customer_address }}</div>
</td>
<td>
<span v-if="oil.customer_asked_for_fill == 1" class="badge badge-info">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</td>
<td>
<div class="flex flex-col gap-1">
<span v-if="oil.prime" class="badge badge-error badge-xs">PRIME</span>
<span v-if="oil.same_day" class="badge badge-error badge-xs">SAME DAY</span>
<span v-if="oil.emergency" class="badge badge-error badge-xs">EMERGENCY</span>
</div>
</td>
<td>
<span v-if="oil.payment_type == 0">Cash</span>
<span v-else-if="oil.payment_type == 1">CC</span>
<span v-else-if="oil.payment_type == 2">Cash/CC</span>
<span v-else-if="oil.payment_type == 3">Check</span>
<span v-else-if="oil.payment_type == 4">Other</span>
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="oil.delivery_status != 10" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="oil in deliveries" :key="oil.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ oil.customer_name }}</h2>
<p class="text-xs text-gray-400">Delivery #{{ oil.id }}</p>
</div>
<div class="badge" :class="{
'badge-warning': oil.delivery_status == 0,
'badge-success': oil.delivery_status == 10,
'badge-info': oil.delivery_status == 2,
'badge-error': [1, 5].includes(oil.delivery_status),
}">
<span v-if="oil.delivery_status == 0">Waiting</span>
<span v-else-if="oil.delivery_status == 1">Cancelled</span>
<span v-else-if="oil.delivery_status == 2">Out for Delivery</span>
<span v-else-if="oil.delivery_status == 3">Tomorrow</span>
<span v-else-if="oil.delivery_status == 4">Partial</span>
<span v-else-if="oil.delivery_status == 5">Issue</span>
<span v-else-if="oil.delivery_status == 10">Finalized</span>
</div>
</div>
</td>
<div class="flex gap-2 mt-2">
<div v-if="oil.prime" class="badge badge-error badge-sm">PRIME</div>
<div v-if="oil.same_day" class="badge badge-error badge-sm">SAME DAY</div>
<div v-if="oil.emergency" class="badge badge-error badge-sm">EMERGENCY</div>
</div>
<td>{{ oil['customer_town'] }}</td>
<td>{{ oil['customer_address'] }}</td>
<td>
<div v-if="oil['customer_asked_for_fill'] == 1">Fill</div>
<div v-else> {{ oil['gallons_ordered'] }}</div>
</td>
<td>{{ oil['expected_delivery_date'] }}</td>
<td>
<div v-if="oil['automatic'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['prime'] == 0">No</div>
<div v-else class="text-red-600">Yes</div>
</td>
<td>
<div v-if="oil['same_day'] == 0">No</div>
<div v-else class="text-red-600">Yes</div>
</td>
<td>
<div v-if="oil['emergency'] == 0">No</div>
<div v-else class="text-red-600">Yes</div>
</td>
<td>
<div v-if="oil['payment_type'] == 0">Cash</div>
<div v-else-if="oil['payment_type'] == 1">CC</div>
<div v-else-if="oil['payment_type'] == 2">Cash/CC</div>
<div v-else-if="oil['payment_type'] == 3">Check</div>
<div v-else-if="oil['payment_type'] == 4">Other</div>
<div v-else></div>
</td>
<td class="flex gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">View Delivery</button>
</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Edit Delivery</button>
</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Finalize</button>
</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil['id'] } }">
<button class="btn btn-success btn-sm">
Print Ticket
</button>
</router-link>
<!-- <button @click.prevent="deleteCall(oil['id'])" class="btn btn-error btn-sm">
Delete
</button> -->
</td>
</tr>
</tbody>
</table>
<div class="text-sm mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<p><strong class="font-semibold">Address:</strong> {{ oil.customer_address }}</p>
<p><strong class="font-semibold">Town:</strong> {{ oil.customer_town }}</p>
<p><strong class="font-semibold">Gallons:</strong>
<span v-if="oil.customer_asked_for_fill" class="badge badge-info badge-xs">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</p>
<p><strong class="font-semibold">Payment:</strong>
<span v-if="oil.payment_type == 0">Cash</span>
<span v-else-if="oil.payment_type == 1">CC</span>
<span v-else-if="oil.payment_type == 2">Cash/CC</span>
<span v-else-if="oil.payment_type == 3">Check</span>
<span v-else-if="oil.payment_type == 4">Other</span>
</p>
</div>
<div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="oil.delivery_status != 10" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options">
</pagination>
</div>
</div>
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options"
class="mt-10">
</pagination>
<div class="flex justify-center mb-10"> {{ recordsLength }} items Found</div>
</div>
</div>
<Footer />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'
@@ -156,7 +188,7 @@ export default defineComponent({
return {
token: null,
user: null,
deliveries: [],
deliveries: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,

View File

@@ -1,115 +1,146 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs mb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
</ul>
</div>
<div class="flex start pb-10 text-2xl">Tommorrows Deliveries </div>
<div class="flex justify-end pb-5">
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Title and Count (No Search Input) -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">Deliveries Scheduled</h2>
<div class="badge badge-ghost">{{ recordsLength }} deliveries found</div>
</div>
</div>
<div class="divider"></div>
<div class="overflow-x-auto bg-neutral">
<div class="flex start"> </div>
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Account Id</th>
<th>Name</th>
<th>Status</th>
<th>Address</th>
<th>Town</th>
<th>Gallons</th>
<th>Date</th>
<th>Automatic</th>
<th>Prime</th>
<th>Same Day</th>
</tr>
</thead>
<tbody>
<!-- row 1 -->
<tr v-for="oil in deliveries" :key="oil['id']">
<td>{{ oil['id'] }} </td>
<router-link :to="{ name: 'customerProfile', params: { id: oil['customer_id'] } }">
<td>
<div class="hover:text-accent">{{ oil['customer_name'] }} </div>
</td>
</router-link>
<td>
<div v-if="oil['delivery_status'] == 0">Waiting</div>
<div v-else-if="oil['delivery_status'] == 1">cancelled</div>
<div v-else-if="oil['delivery_status'] == 2">Out for Delivery</div>
<div v-else-if="oil['delivery_status'] == 3">tommorrow</div>
<div v-else-if="oil['delivery_status'] == 4">Partial Delivery</div>
<div v-else-if="oil['delivery_status'] == 5">Issue</div>
<div v-else-if="oil['delivery_status'] == 10">Finalized</div>
<div v-else></div>
</td>
<td>{{ oil['customer_address'] }}</td>
<td>{{ oil['customer_town'] }}</td>
<td>
<div v-if="oil['customer_asked_for_fill'] == 1">Fill</div>
<div v-else> {{ oil['gallons_ordered'] }}</div>
</td>
<td>{{ oil['expected_delivery_date'] }}</td>
<td>
<div v-if="oil['automatic'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['prime'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['same_day'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td class="flex gap-5">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">View Delivery</button>
</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Edit Delivery</button>
</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Finalize</button>
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Delivery #</th>
<th>Name</th>
<th>Status</th>
<th>Town / Address</th>
<th>Gallons</th>
<th>Options</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="oil in deliveries" :key="oil.id" class="hover:bg-blue-600 hover:text-white">
<td>{{ oil.id }}</td>
<td>
<router-link :to="{ name: 'customerProfile', params: { id: oil.customer_id } }" class="link link-hover">
{{ oil.customer_name }}
</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil['id'] } }">
<button class="btn btn-success btn-sm ">
Print Ticket
</button>
</router-link>
<!-- <button @click.prevent="deleteCall(oil['id'])" class="btn btn-error btn-sm">
Delete
</button> -->
</td>
</tr>
</tbody>
</table>
</td>
<td>
<span class="badge badge-sm" :class="{
'badge-warning': oil.delivery_status == 0,
'badge-success': oil.delivery_status == 10,
'badge-info': oil.delivery_status == 2,
'badge-error': [1, 5].includes(oil.delivery_status),
}">
<span v-if="oil.delivery_status == 0">Waiting</span>
<span v-else-if="oil.delivery_status == 1">Cancelled</span>
<span v-else-if="oil.delivery_status == 2">Out for Delivery</span>
<span v-else-if="oil.delivery_status == 3">Tomorrow</span>
<span v-else-if="oil.delivery_status == 4">Partial</span>
<span v-else-if="oil.delivery_status == 5">Issue</span>
<span v-else-if="oil.delivery_status == 10">Finalized</span>
</span>
</td>
<td>
<div>{{ oil.customer_town }}</div>
<div class="text-xs opacity-70">{{ oil.customer_address }}</div>
</td>
<td>
<span v-if="oil.customer_asked_for_fill == 1" class="badge badge-info">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</td>
<td>
<div class="flex flex-col gap-1">
<span v-if="oil.prime" class="badge badge-error badge-xs">PRIME</span>
<span v-if="oil.same_day" class="badge badge-error badge-xs">SAME DAY</span>
</div>
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="oil.delivery_status != 10" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="oil in deliveries" :key="oil.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ oil.customer_name }}</h2>
<p class="text-xs text-gray-400">Delivery #{{ oil.id }}</p>
</div>
<div class="badge" :class="{
'badge-warning': oil.delivery_status == 0,
'badge-success': oil.delivery_status == 10,
'badge-info': oil.delivery_status == 2,
'badge-error': [1, 5].includes(oil.delivery_status),
}">
<span v-if="oil.delivery_status == 0">Waiting</span>
<span v-else-if="oil.delivery_status == 1">Cancelled</span>
<span v-else-if="oil.delivery_status == 2">Out for Delivery</span>
<span v-else-if="oil.delivery_status == 3">Tomorrow</span>
<span v-else-if="oil.delivery_status == 4">Partial</span>
<span v-else-if="oil.delivery_status == 5">Issue</span>
<span v-else-if="oil.delivery_status == 10">Finalized</span>
</div>
</div>
<div class="flex gap-2 mt-2">
<div v-if="oil.prime" class="badge badge-error badge-sm">PRIME</div>
<div v-if="oil.same_day" class="badge badge-error badge-sm">SAME DAY</div>
</div>
<div class="text-sm mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<p><strong class="font-semibold">Address:</strong> {{ oil.customer_address }}</p>
<p><strong class="font-semibold">Town:</strong> {{ oil.customer_town }}</p>
<p><strong class="font-semibold">Gallons:</strong>
<span v-if="oil.customer_asked_for_fill" class="badge badge-info badge-xs">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</p>
<p><strong class="font-semibold">Date:</strong> {{ oil.expected_delivery_date }}</p>
</div>
<div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="oil.delivery_status != 10" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</div>
</div>
</div>
</div>
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options" class="mt-10">
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options">
</pagination>
<!-- <div class="flex justify-center mb-10"> {{ recordsLength }} items Found</div> -->
</div>
</div>
</div>
<Footer />
</template>
@@ -136,7 +167,7 @@ export default defineComponent({
return {
token: null,
user: null,
deliveries: [],
deliveries: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,

View File

@@ -1,115 +1,126 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs pb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
</ul>
</div>
<div class="flex start pb-10 text-2xl">Waiting Deliveries </div>
<div class="flex justify-end pb-5">
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Title and Count -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">Deliveries Awaiting Dispatch</h2>
<div class="badge badge-ghost">{{ recordsLength }} deliveries found</div>
</div>
</div>
<div class="divider"></div>
<div class="overflow-x-auto bg-neutral">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Account Id</th>
<th>Name</th>
<th>Status</th>
<th>Address</th>
<th>Town</th>
<th>Gallons</th>
<th>Date</th>
<th>Automatic</th>
<th>Prime</th>
<th>Same Day</th>
</tr>
</thead>
<tbody>
<!-- row 1 -->
<tr v-for="oil in deliveries" :key="oil['id']">
<td>{{ oil['id'] }} </td>
<router-link :to="{ name: 'customerProfile', params: { id: oil['customer_id'] } }">
<td>
<div class="hover:text-accent">{{ oil['customer_name'] }} </div>
</td>
</router-link>
<td>
<div v-if="oil['delivery_status'] == 0">Waiting</div>
<div v-else-if="oil['delivery_status'] == 1">delivered</div>
<div v-else-if="oil['delivery_status'] == 2">Out for Delivery</div>
<div v-else-if="oil['delivery_status'] == 3">Cancelled</div>
<div v-else-if="oil['delivery_status'] == 4">Partial Delivery</div>
<div v-else-if="oil['delivery_status'] == 5">Issue</div>
<div v-else-if="oil['delivery_status'] == 10">Finalized</div>
<div v-else></div>
</td>
<td>{{ oil['customer_address'] }}</td>
<td>{{ oil['customer_town'] }}</td>
<td>
<div v-if="oil['customer_asked_for_fill'] == 1">Fill</div>
<div v-else> {{ oil['gallons_ordered'] }}</div>
</td>
<td>{{ oil['expected_delivery_date'] }}</td>
<td>
<div v-if="oil['automatic'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['prime'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td>
<div v-if="oil['same_day'] == 0">No</div>
<div v-else>Yes</div>
</td>
<td class="flex gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">View Delivery</button>
</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Edit Delivery</button>
</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil['id'] } }">
<button class="btn btn-secondary btn-sm">Finalize</button>
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Delivery #</th>
<th>Name</th>
<th>Status</th>
<th>Town / Address</th>
<th>Gallons</th>
<th>Date</th>
<th>Options</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="oil in deliveries" :key="oil.id" class="hover:bg-blue-600 hover:text-white">
<td>{{ oil.id }}</td>
<td>
<router-link :to="{ name: 'customerProfile', params: { id: oil.customer_id } }" class="link link-hover">
{{ oil.customer_name }}
</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil['id'] } }">
<button class="btn btn-success btn-sm">
Print Ticket
</button>
</router-link>
<!-- <button @click.prevent="deleteCall(oil['id'])" class="btn btn-error btn-sm">
Delete
</button> -->
</td>
</tr>
</tbody>
</table>
</td>
<td>
<span class="badge badge-sm badge-warning">
Waiting
</span>
</td>
<td>
<div>{{ oil.customer_town }}</div>
<div class="text-xs opacity-70">{{ oil.customer_address }}</div>
</td>
<td>
<span v-if="oil.customer_asked_for_fill == 1" class="badge badge-info">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</td>
<td>{{ oil.expected_delivery_date }}</td>
<td>
<div class="flex flex-col gap-1">
<span v-if="oil.prime" class="badge badge-error badge-xs">PRIME</span>
<span v-if="oil.same_day" class="badge badge-error badge-xs">SAME DAY</span>
</div>
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="oil in deliveries" :key="oil.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ oil.customer_name }}</h2>
<p class="text-xs text-gray-400">Delivery #{{ oil.id }}</p>
</div>
<div class="badge badge-warning">
Waiting
</div>
</div>
<div class="flex gap-2 mt-2">
<div v-if="oil.prime" class="badge badge-error badge-sm">PRIME</div>
<div v-if="oil.same_day" class="badge badge-error badge-sm">SAME DAY</div>
</div>
<div class="text-sm mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<p><strong class="font-semibold">Address:</strong> {{ oil.customer_address }}</p>
<p><strong class="font-semibold">Town:</strong> {{ oil.customer_town }}</p>
<p><strong class="font-semibold">Gallons:</strong>
<span v-if="oil.customer_asked_for_fill" class="badge badge-info badge-xs">FILL</span>
<span v-else>{{ oil.gallons_ordered }}</span>
</p>
<p><strong class="font-semibold">Date:</strong> {{ oil.expected_delivery_date }}</p>
</div>
<div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div>
</div>
</div>
</div>
</div>
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options" class="mt-10">
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options">
</pagination>
<div class="flex justify-center mb-10"> {{ recordsLength }} items Found</div>
</div>
</div>
</div>
<Footer />
</template>
@@ -136,7 +147,7 @@ export default defineComponent({
return {
token: null,
user: null,
deliveries: [],
deliveries: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,

View File

@@ -1,233 +1,214 @@
<template>
<Header />
<div v-if="user">
<div class="flex">
<div class="">
<SideBar />
<div class="flex">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Employees</li>
<li>Create New Employee</li>
</ul>
</div>
<div class="w-full px-10">
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'customer' }">
Customers
</router-link>
</li>
</ul>
</div>
<div class="grid grid-cols-1 rounded-md p-6 ">
<div class="text-[24px]">Create a new employee</div>
<form class="rounded-md px-8 pt-6 pb-8 mb-4 w-full" enctype="multipart/form-data" @submit.prevent="onSubmit">
<h1 class="text-3xl font-bold mt-4">
Create New Employee
</h1>
<!-- Main Form Card -->
<div class="bg-neutral rounded-lg p-6 mt-6">
<form @submit.prevent="onSubmit" class="space-y-6">
<div class="text-[18px] mt-10 mb-10">General Info</div>
<!-- last name -->
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Last Name</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_last_name"
class="input input-bordered w-full max-w-xs" id="title" type="text" placeholder="Last Name" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_last_name.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_last_name.$errors[0].$message }}
</span>
</div>
<!-- first name -->
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">First Name</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_first_name"
class="input input-bordered w-full max-w-xs" id="title" type="text" placeholder="First Name" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_first_name.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_first_name.$errors[0].$message }}
</span>
</div>
<!-- employee type -->
<div class="flex gap-5">
<div class="flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">Type of employee</label>
<select class="select select-bordered w-full max-w-xs" aria-label="Default select example"
id="employee_type" v-model="CreateEmployeeForm.basicInfo.employee_type">
<option class="text-white" v-for="(employee, index) in employList" :key="index"
:value="employee['value']">
{{ employee['text'] }}
<!-- SECTION 1: General Info -->
<div>
<h2 class="text-lg font-bold">General Info</h2>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- First Name -->
<div class="form-control">
<label class="label"><span class="label-text">First Name</span></label>
<input v-model="CreateEmployeeForm.employee_first_name" type="text" placeholder="First Name" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_first_name.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_first_name.$errors[0].$message }}
</span>
</div>
<!-- Last Name -->
<div class="form-control">
<label class="label"><span class="label-text">Last Name</span></label>
<input v-model="CreateEmployeeForm.employee_last_name" type="text" placeholder="Last Name" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_last_name.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_last_name.$errors[0].$message }}
</span>
</div>
<!-- Phone Number -->
<div class="form-control">
<label class="label"><span class="label-text">Phone Number</span></label>
<input v-model="CreateEmployeeForm.employee_phone_number" @input="acceptNumber()" type="text" placeholder="Phone Number" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_phone_number.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_phone_number.$errors[0].$message }}
</span>
</div>
<!-- Employee Type -->
<div class="form-control">
<label class="label"><span class="label-text">Employee Role</span></label>
<select v-model="CreateEmployeeForm.employee_type" class="select select-bordered select-sm w-full">
<option disabled :value="0">Select a role</option>
<option v-for="employee in employList" :key="employee.value" :value="employee.value">
{{ employee.text }}
</option>
</select>
<span v-if="v$.CreateEmployeeForm.employee_type.$error" class="text-red-500 text-xs mt-1">
Role is required.
</span>
</div>
</div>
</div>
<div class="text-[18px] mt-10 mb-10">Employee Address</div>
<!-- street address -->
<div class="col-span-12 mb-5 md:mb-5">
<label class="block text-white text-sm font-bold mb-2">Street Address</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_address" class="input input-bordered w-full max-w-xs"
id="address" type="text" placeholder="Address" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_address.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_address.$errors[0].$message }}
</span>
<!-- SECTION 2: Address -->
<div>
<h2 class="text-lg font-bold">Address</h2>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Street Address -->
<div class="form-control">
<label class="label"><span class="label-text">Street Address</span></label>
<input v-model="CreateEmployeeForm.employee_address" type="text" placeholder="Street Address" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_address.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_address.$errors[0].$message }}
</span>
</div>
<!-- Apt, Suite, etc. -->
<div class="form-control">
<label class="label"><span class="label-text">Apt, Suite, etc. (Optional)</span></label>
<input v-model="CreateEmployeeForm.employee_apt" type="text" placeholder="Apt, suite, unit..." class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_apt.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<!-- Town -->
<div class="form-control">
<label class="label"><span class="label-text">Town</span></label>
<input v-model="CreateEmployeeForm.employee_town" type="text" placeholder="Town" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_town.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_town.$errors[0].$message }}
</span>
</div>
<!-- State -->
<div class="form-control">
<label class="label"><span class="label-text">State</span></label>
<select v-model="CreateEmployeeForm.employee_state" class="select select-bordered select-sm w-full">
<option disabled :value="0">Select a state</option>
<option v-for="state in stateList" :key="state.value" :value="state.value">
{{ state.text }}
</option>
</select>
<span v-if="v$.CreateEmployeeForm.employee_state.$error" class="text-red-500 text-xs mt-1">
State is required.
</span>
</div>
<!-- Zip Code -->
<div class="form-control">
<label class="label"><span class="label-text">Zip Code</span></label>
<input v-model="CreateEmployeeForm.employee_zip" type="text" placeholder="Zip Code" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_zip.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_zip.$errors[0].$message }}
</span>
</div>
</div>
</div>
<!-- apt -->
<div class="col-span-12 mb-5 md:mb-5">
<input v-model="CreateEmployeeForm.basicInfo.employee_apt" class="input input-bordered w-full max-w-xs"
id="apt" type="text" placeholder="Apt, suite, unit, building, floor, etc" />
<!-- SECTION 3: Employment Dates -->
<div>
<h2 class="text-lg font-bold">Employment Dates & Details</h2>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-control">
<label class="label"><span class="label-text">Birthday</span></label>
<input v-model="CreateEmployeeForm.employee_birthday" type="date" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_birthday.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Start Date</span></label>
<input v-model="CreateEmployeeForm.employee_start_date" type="date" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_start_date.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<div class="form-control">
<label class="label"><span class="label-text">End Date (Optional)</span></label>
<input v-model="CreateEmployeeForm.employee_end_date" type="date" class="input input-bordered input-sm w-full" />
</div>
</div>
</div>
<!-- customer_town -->
<div class="col-span-12 md:col-span-4 mb-20 md:mb-5 ">
<label class="block text-white text-sm font-bold mb-2">Town</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_town" class="input input-bordered w-full max-w-xs"
id="city" type="text" placeholder="Town" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_town.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_town.$errors[0].$message }}
</span>
</div>
<!-- phone number -->
<div class="col-span-12 md:col-span-4 mb-5 md:mb-5">
<label class="block text-white text-sm font-bold mb-2">Phone Number</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_phone_number"
class="input input-bordered w-full max-w-xs" id="phone number" type="text" placeholder="Phone Number"
@input="acceptNumber()" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_phone_number.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_phone_number.$errors[0].$message }}
</span>
</div>
<!-- state -->
<div class="flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">State</label>
<select class="select select-bordered w-full max-w-xs" aria-label="Default select example"
id="employee_state" v-model="CreateEmployeeForm.basicInfo.employee_state">
<option class="text-white" v-for="(state, index) in stateList" :key="index" :value="state['value']">
{{ state['text'] }}
</option>
</select>
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_state.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_state.$errors[0].$message }}
</span>
</div>
<!-- zip -->
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0">
<label class="block text-white text-sm font-bold mb-2">Zip Code</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_zip" class="input input-bordered w-full max-w-xs"
id="zip" type="text" placeholder="Zip" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_zip.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_zip.$errors[0].$message }}
</span>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Employee BirthDay</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_birthday" class="input input-bordered w-full max-w-xs"
id="title" type="date" min="1945-01-01" max="2030-01-01" />
</div>
<div class="text-[18px] mt-10 mb-10">Employee Dates</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Employee Start Date</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_start_date"
class="input input-bordered w-full max-w-xs" id="title" type="date" min="2023-01-01" max="2030-01-01" />
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Employee End Date</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_end_date" class="input input-bordered w-full max-w-xs"
id="title" type="date" min="2023-01-01" max="2030-01-01" />
</div>
<div class="col-span-12 md:col-span-12 flex mt-5 mb-5">
<button class="btn">
Create Employee
</button>
</div>
</form>
</div>
<!-- SUBMIT BUTTON -->
<div class="pt-4">
<button type="submit" class="btn btn-primary btn-sm">Create Employee</button>
</div>
</form>
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts">
import {defineComponent} from 'vue'
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 useValidate from "@vuelidate/core";
import {minLength, required} from "@vuelidate/validators";
import { minLength, required } from "@vuelidate/validators";
interface SelectOption {
text: string;
value: number;
}
export default defineComponent({
name: 'EmployeeCreate',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
v$: useValidate(),
user: null,
stateList: [],
employList: [],
employee_id: '',
stateList: [] as SelectOption[],
employList: [] as SelectOption[],
// --- REFACTORED: Simplified, flat form object ---
CreateEmployeeForm: {
basicInfo: {
employee_last_name: "",
employee_first_name: "",
employee_town: "",
employee_address: "",
employee_apt: "",
employee_zip: "",
employee_birthday: "",
employee_phone_number: "",
employee_start_date: "",
employee_end_date: "",
employee_type: "",
employee_state: "",
},
employee_last_name: "",
employee_first_name: "",
employee_town: "",
employee_address: "",
employee_apt: "",
employee_zip: "",
employee_birthday: "",
employee_phone_number: "",
employee_start_date: "",
employee_end_date: "",
// --- FIX: Initialized as a number for proper v-model binding ---
employee_type: 0,
employee_state: 0,
},
}
},
validations() {
return {
// --- REFACTORED: Validation rules point to the flat form object ---
CreateEmployeeForm: {
basicInfo: {
employee_last_name: {required, minLength: minLength(1)},
employee_first_name: {required, minLength: minLength(1)},
employee_town: {required, minLength: minLength(1)},
employee_type: {required},
employee_zip: {required, minLength: minLength(5)},
employee_state: {required},
employee_apt: {required},
employee_address: {required},
employee_birthday: {required},
employee_phone_number: {required},
employee_start_date: {required},
},
employee_last_name: { required, minLength: minLength(1) },
employee_first_name: { required, minLength: minLength(1) },
employee_town: { required, minLength: minLength(1) },
employee_type: { required },
employee_zip: { required, minLength: minLength(5) },
employee_state: { required },
employee_apt: { required },
employee_address: { required },
employee_birthday: { required },
employee_phone_number: { required },
employee_start_date: { required },
},
};
},
created() {
this.userStatus()
this.userStatus();
},
mounted() {
this.getEmployeeTypeList();
@@ -235,108 +216,50 @@ export default defineComponent({
},
methods: {
acceptNumber() {
let x = this.CreateEmployeeForm.basicInfo.employee_phone_number.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/);
if (x){
this.CreateEmployeeForm.basicInfo.employee_phone_number = !x[2] ? x[1] : '(' + x[1] + ') ' + x[2] + (x[3] ? '-' + x[3] : '');
}
else {
this.CreateEmployeeForm.basicInfo.employee_phone_number = ''
}
const x = this.CreateEmployeeForm.employee_phone_number.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/);
if (x) {
this.CreateEmployeeForm.employee_phone_number = !x[2] ? x[1] : `(${x[1]}) ${x[2]}${x[3] ? `-${x[3]}` : ''}`;
}
},
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 path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) { this.user = response.data.user; }
})
.catch(() => { this.user = null; });
},
CreateItem(payload: {
employee_last_name: string;
employee_first_name: string;
employee_town: string;
employee_address: string;
employee_zip: string;
employee_apt: string,
employee_birthday: string;
employee_phone_number: string;
employee_start_date: string,
employee_end_date: string;
employee_type: string;
employee_state: string;
}) {
let path = import.meta.env.VITE_BASE_URL + "/employee/create";
axios({
method: "post",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.employee_id= response.data['user_id']
this.$router.push({name: "employeeProfile", params: { id: this.employee_id }});
}
if (response.data.error) {
this.$router.push("/");
}
})
CreateItem(payload: any) {
const path = import.meta.env.VITE_BASE_URL + "/employee/create";
axios.post(path, payload, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
const employee_id = response.data['user_id'];
this.$router.push({ name: "employeeProfile", params: { id: employee_id } });
} else {
console.error("Failed to create employee:", response.data.error);
// Optionally, show a notification to the user
}
});
},
onSubmit() {
let payload = {
employee_last_name: this.CreateEmployeeForm.basicInfo.employee_last_name,
employee_first_name: this.CreateEmployeeForm.basicInfo.employee_first_name,
employee_town: this.CreateEmployeeForm.basicInfo.employee_town,
employee_address: this.CreateEmployeeForm.basicInfo.employee_address,
employee_zip: this.CreateEmployeeForm.basicInfo.employee_zip,
employee_apt: this.CreateEmployeeForm.basicInfo.employee_apt,
employee_birthday: this.CreateEmployeeForm.basicInfo.employee_birthday,
employee_phone_number: this.CreateEmployeeForm.basicInfo.employee_phone_number,
employee_start_date: this.CreateEmployeeForm.basicInfo.employee_start_date,
employee_end_date: this.CreateEmployeeForm.basicInfo.employee_end_date,
employee_type: this.CreateEmployeeForm.basicInfo.employee_type,
employee_state: this.CreateEmployeeForm.basicInfo.employee_state,
};
this.CreateItem(payload);
this.v$.$validate(); // Trigger validation
if (!this.v$.$error) {
this.CreateItem(this.CreateEmployeeForm);
} else {
console.log("Form validation failed.");
}
},
getEmployeeTypeList() {
let path = import.meta.env.VITE_BASE_URL + "/query/employeetype";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.employList = response.data;
})
.catch(() => {
});
const path = import.meta.env.VITE_BASE_URL + "/query/employeetype";
axios.get(path, { withCredentials: true })
.then((response: any) => { this.employList = response.data; });
},
getStatesList() {
let path = import.meta.env.VITE_BASE_URL + "/query/states";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.stateList = response.data;
})
.catch(() => {
});
const path = import.meta.env.VITE_BASE_URL + "/query/states";
axios.get(path, { withCredentials: true })
.then((response: any) => { this.stateList = response.data; });
},
},
})
</script>
<style scoped></style>
</script>

View File

@@ -1,383 +1,275 @@
<template>
<Header />
<div v-if="user">
<div class="flex">
<div class="">
<SideBar />
<div class="flex">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Employees</li>
<li>Edit Employee</li>
</ul>
</div>
<div class="w-full px-10">
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'customer' }">
Customers
</router-link>
</li>
</ul>
</div>
<div class="grid grid-cols-1 rounded-md p-6 ">
<div class="text-[24px]">Edit employee</div>
<form class="rounded-md px-8 pt-6 pb-8 mb-4 w-full" enctype="multipart/form-data" @submit.prevent="onSubmit">
<div class="text-[18px] mt-10 mb-10">General Info</div>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mt-4">
<h1 class="text-3xl font-bold">
Edit Employee
</h1>
<router-link v-if="employee_id" :to="{ name: 'employeeProfile', params: { id: employee_id } }" class="btn btn-secondary btn-sm mt-2 sm:mt-0">
Back to Profile
</router-link>
</div>
<!-- first name -->
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">First Name</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_first_name"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text" placeholder="First Name" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_first_name.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_first_name.$errors[0].$message }}
</span>
</div>
<!-- Main Form Card -->
<div class="bg-neutral rounded-lg p-6 mt-6">
<form @submit.prevent="onSubmit" class="space-y-6">
<!-- last name -->
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Last Name</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_last_name"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="text" placeholder="Last Name" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_last_name.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_last_name.$errors[0].$message }}
</span>
</div>
<!-- employee type -->
<div class="flex gap-5">
<div class="flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">Type of employee</label>
<select class="select select-bordered select-sm w-full max-w-xs" aria-label="Default select example"
id="employee_type" v-model="CreateEmployeeForm.basicInfo.employee_type">
<option class="text-white" v-for="(employee, index) in employList" :key="index"
:value="employee['value']">
{{ employee['text'] }}
<!-- SECTION 1: General Info -->
<div>
<h2 class="text-lg font-bold">General Info</h2>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- First Name -->
<div class="form-control">
<label class="label"><span class="label-text">First Name</span></label>
<input v-model="CreateEmployeeForm.employee_first_name" type="text" placeholder="First Name" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_first_name.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_first_name.$errors[0].$message }}
</span>
</div>
<!-- Last Name -->
<div class="form-control">
<label class="label"><span class="label-text">Last Name</span></label>
<input v-model="CreateEmployeeForm.employee_last_name" type="text" placeholder="Last Name" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_last_name.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_last_name.$errors[0].$message }}
</span>
</div>
<!-- Phone Number -->
<div class="form-control">
<label class="label"><span class="label-text">Phone Number</span></label>
<input v-model="CreateEmployeeForm.employee_phone_number" @input="acceptNumber()" type="text" placeholder="Phone Number" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_phone_number.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_phone_number.$errors[0].$message }}
</span>
</div>
<!-- Employee Type -->
<div class="form-control">
<label class="label"><span class="label-text">Employee Role</span></label>
<select v-model="CreateEmployeeForm.employee_type" class="select select-bordered select-sm w-full">
<option disabled :value="0">Select a role</option>
<option v-for="employee in employList" :key="employee.value" :value="employee.value">
{{ employee.text }}
</option>
</select>
<span v-if="v$.CreateEmployeeForm.employee_type.$error" class="text-red-500 text-xs mt-1">
Role is required.
</span>
</div>
</div>
</div>
<div class="text-[18px] mt-10 mb-10">Employee Address</div>
<!-- street address -->
<div class="col-span-12 mb-5 md:mb-5">
<label class="block text-white text-sm font-bold mb-2">Street Address</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_address"
class="input input-bordered input-sm w-full max-w-xs"
id="address" type="text" placeholder="Address" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_address.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_address.$errors[0].$message }}
</span>
<!-- SECTION 2: Address -->
<div>
<h2 class="text-lg font-bold">Address</h2>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Street Address -->
<div class="form-control">
<label class="label"><span class="label-text">Street Address</span></label>
<input v-model="CreateEmployeeForm.employee_address" type="text" placeholder="Street Address" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_address.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_address.$errors[0].$message }}
</span>
</div>
<!-- Apt, Suite, etc. -->
<div class="form-control">
<label class="label"><span class="label-text">Apt, Suite, etc. (Optional)</span></label>
<input v-model="CreateEmployeeForm.employee_apt" type="text" placeholder="Apt, suite, unit..." class="input input-bordered input-sm w-full" />
</div>
<!-- Town -->
<div class="form-control">
<label class="label"><span class="label-text">Town</span></label>
<input v-model="CreateEmployeeForm.employee_town" type="text" placeholder="Town" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_town.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_town.$errors[0].$message }}
</span>
</div>
<!-- State -->
<div class="form-control">
<label class="label"><span class="label-text">State</span></label>
<select v-model="CreateEmployeeForm.employee_state" class="select select-bordered select-sm w-full">
<option disabled :value="0">Select a state</option>
<option v-for="state in stateList" :key="state.value" :value="state.value">
{{ state.text }}
</option>
</select>
<span v-if="v$.CreateEmployeeForm.employee_state.$error" class="text-red-500 text-xs mt-1">
State is required.
</span>
</div>
<!-- Zip Code -->
<div class="form-control">
<label class="label"><span class="label-text">Zip Code</span></label>
<input v-model="CreateEmployeeForm.employee_zip" type="text" placeholder="Zip Code" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_zip.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateEmployeeForm.employee_zip.$errors[0].$message }}
</span>
</div>
</div>
</div>
<!-- apt -->
<div class="col-span-12 mb-5 md:mb-5">
<input v-model="CreateEmployeeForm.basicInfo.employee_apt"
class="input input-bordered input-sm w-full max-w-xs"
id="apt" type="text" placeholder="Apt, suite, unit, building, floor, etc" />
<!-- SECTION 3: Employment Dates -->
<div>
<h2 class="text-lg font-bold">Employment Dates & Details</h2>
<div class="divider mt-2 mb-4"></div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-control">
<label class="label"><span class="label-text">Birthday</span></label>
<input v-model="CreateEmployeeForm.employee_birthday" type="date" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_birthday.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Start Date</span></label>
<input v-model="CreateEmployeeForm.employee_start_date" type="date" class="input input-bordered input-sm w-full" />
<span v-if="v$.CreateEmployeeForm.employee_start_date.$error" class="text-red-500 text-xs mt-1">Required.</span>
</div>
<div class="form-control">
<label class="label"><span class="label-text">End Date (Optional)</span></label>
<input v-model="CreateEmployeeForm.employee_end_date" type="date" class="input input-bordered input-sm w-full" />
</div>
</div>
</div>
<!-- employee_town -->
<div class="col-span-12 md:col-span-4 mb-20 md:mb-5 ">
<label class="block text-white text-sm font-bold mb-2">Town</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_town"
class="input input-bordered input-sm w-full max-w-xs"
id="city" type="text" placeholder="Town" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_town.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_town.$errors[0].$message }}
</span>
</div>
<!-- phone number -->
<div class="col-span-12 md:col-span-4 mb-5 md:mb-5">
<label class="block text-white text-sm font-bold mb-2">Phone Number</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_phone_number" @input="acceptNumber()"
class="input input-bordered input-sm w-full max-w-xs" id="phone number" type="text" placeholder="Phone Number" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_phone_number.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_phone_number.$errors[0].$message }}
</span>
</div>
<!-- state -->
<div class="flex-1 mb-4">
<label class="block text-white text-sm font-bold mb-2">State</label>
<select class="select select-bordered select-sm w-full max-w-xs" aria-label="Default select example"
id="employee_state" v-model="CreateEmployeeForm.basicInfo.employee_state">
<option class="text-white" v-for="(state, index) in stateList" :key="index" defaultValue="state_default"
:value="state['value']">
{{ state['text'] }}
</option>
</select>
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_state.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_state.$errors[0].$message }}
</span>
</div>
<!-- zip -->
<div class="col-span-12 md:col-span-4 mb-5 md:mb-0">
<label class="block text-white text-sm font-bold mb-2">Zip Code</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_zip"
class="input input-bordered input-sm w-full max-w-xs"
id="zip" type="text" placeholder="Zip" />
<span v-if="v$.CreateEmployeeForm.basicInfo.employee_zip.$error" class="text-red-600 text-center">
{{ v$.CreateEmployeeForm.basicInfo.employee_zip.$errors[0].$message }}
</span>
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Employee BirthDay</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_birthday"
class="input input-bordered input-sm w-full max-w-xs"
id="title" type="date" min="1945-01-01" max="2030-01-01" />
</div>
<div class="text-[18px] mt-10 mb-10">Employee Dates</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Employee Start Date</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_start_date"
class="input input-bordered input-sm w-full max-w-xs" id="title" type="date" min="2023-01-01" max="2030-01-01" />
</div>
<div class="mb-4">
<label class="block text-white text-sm font-bold mb-2">Employee End Date</label>
<input v-model="CreateEmployeeForm.basicInfo.employee_end_date"
class="input input-bordered input-sm w-full max-w-xs"
id="title" type="date" min="2023-01-01" max="2030-01-01" />
</div>
<div class="col-span-12 md:col-span-12 flex mt-5 mb-5">
<button class="btn btn-sm btn-secondary">
Edit Employee
</button>
</div>
</form>
</div>
<!-- SUBMIT BUTTON -->
<div class="pt-4">
<button type="submit" class="btn btn-primary btn-sm">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts">
import {defineComponent} from 'vue'
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 useValidate from "@vuelidate/core";
import {minLength, required} from "@vuelidate/validators";
import { minLength, required } from "@vuelidate/validators";
interface SelectOption {
text: string;
value: number;
}
export default defineComponent({
name: 'EmployeeEdit',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
v$: useValidate(),
user: null,
stateList: [],
employList: [],
employee_id: '',
state_default: '',
stateList: [] as SelectOption[],
employList: [] as SelectOption[],
employee_id: this.$route.params.id as string,
CreateEmployeeForm: {
basicInfo: {
employee_last_name: "",
employee_first_name: "",
employee_town: "",
employee_address: "",
employee_apt: "",
employee_zip: "",
employee_birthday: "",
employee_phone_number: "",
employee_start_date: "",
employee_end_date: "",
employee_type: "",
employee_state: "",
},
employee_last_name: "",
employee_first_name: "",
employee_town: "",
employee_address: "",
employee_apt: "",
employee_zip: "",
employee_birthday: "",
employee_phone_number: "",
employee_start_date: "",
employee_end_date: "",
employee_type: 0,
employee_state: 0,
},
}
},
validations() {
return {
CreateEmployeeForm: {
basicInfo: {
employee_last_name: {required, minLength: minLength(1)},
employee_first_name: {required, minLength: minLength(1)},
employee_town: {required, minLength: minLength(1)},
employee_type: {required},
employee_zip: {required, minLength: minLength(5)},
employee_state: {required},
employee_apt: {required},
employee_address: {required},
employee_birthday: {required},
employee_phone_number: {required},
employee_start_date: {required},
},
employee_last_name: { required, minLength: minLength(1) },
employee_first_name: { required, minLength: minLength(1) },
employee_town: { required, minLength: minLength(1) },
employee_type: { required },
employee_zip: { required, minLength: minLength(5) },
employee_state: { required },
employee_address: { required },
employee_birthday: { required },
employee_phone_number: { required },
employee_start_date: { required },
},
};
},
created() {
this.userStatus()
this.getEmployee(this.$route.params.id)
this.userStatus();
this.getEmployee(this.employee_id);
},
mounted() {
this.getEmployeeTypeList();
this.getStatesList();
this.getEmployee(this.$route.params.id)
},
watch: {
$route() {
this.getEmployee(this.$route.params.id);
},
},
methods: {
// --- FIX APPLIED HERE ---
acceptNumber() {
let x = this.CreateEmployeeForm.basicInfo.employee_phone_number.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/);
if (x){
this.CreateEmployeeForm.basicInfo.employee_phone_number = !x[2] ? x[1] : '(' + x[1] + ') ' + x[2] + (x[3] ? '-' + x[3] : '');
}
else {
this.CreateEmployeeForm.basicInfo.employee_phone_number = ''
}
const x = this.CreateEmployeeForm.employee_phone_number.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/);
// This 'if' block ensures 'x' is not null before we use it.
if (x) {
this.CreateEmployeeForm.employee_phone_number = !x[2] ? x[1] : `(${x[1]}) ${x[2]}${x[3] ? `-${x[3]}` : ''}`;
}
},
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
})
},
EditEmployee(payload: {
employee_last_name: string;
employee_first_name: string;
employee_town: string;
employee_address: string;
employee_zip: string;
employee_apt: string,
employee_birthday: string;
employee_phone_number: string;
employee_start_date: string,
employee_end_date: string;
employee_type: string;
employee_state: string;
}) {
let path = import.meta.env.VITE_BASE_URL + "/employee/edit/" + this.employee_id;
axios({
method: "post",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.$router.push({name: "employeeProfile", params: { id: this.employee_id }});
}
if (response.data.error) {
this.$router.push("/");
}
})
},
getEmployee (userid:any) {
let path = import.meta.env.VITE_BASE_URL + "/employee/" + userid;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
this.employee_id = response.data.id
this.CreateEmployeeForm.basicInfo.employee_last_name= response.data.employee_last_name;
this.CreateEmployeeForm.basicInfo.employee_first_name= response.data.employee_first_name;
this.CreateEmployeeForm.basicInfo.employee_town= response.data.employee_town;
this.CreateEmployeeForm.basicInfo.employee_type= response.data.employee_type;
this.CreateEmployeeForm.basicInfo.employee_state= response.data.employee_state;
this.CreateEmployeeForm.basicInfo.employee_zip= response.data.employee_zip;
this.CreateEmployeeForm.basicInfo.employee_apt= response.data.employee_apt;
this.CreateEmployeeForm.basicInfo.employee_address= response.data.employee_address;
this.CreateEmployeeForm.basicInfo.employee_birthday= response.data.employee_birthday;
this.CreateEmployeeForm.basicInfo.employee_phone_number= response.data.employee_phone_number;
this.CreateEmployeeForm.basicInfo.employee_start_date= response.data.employee_start_date;
this.CreateEmployeeForm.basicInfo.employee_end_date= response.data.employee_end_date;
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => { this.user = null; });
},
EditEmployee(payload: any) {
const path = `${import.meta.env.VITE_BASE_URL}/employee/edit/${this.employee_id}`;
axios.post(path, payload, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.$router.push({ name: "employeeProfile", params: { id: this.employee_id } });
} else {
console.error("Failed to edit employee:", response.data.error);
}
});
},
getEmployee(userid: any) {
const path = `${import.meta.env.VITE_BASE_URL}/employee/${userid}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
this.CreateEmployeeForm = response.data;
});
},
onSubmit() {
let payload = {
employee_last_name: this.CreateEmployeeForm.basicInfo.employee_last_name,
employee_first_name: this.CreateEmployeeForm.basicInfo.employee_first_name,
employee_town: this.CreateEmployeeForm.basicInfo.employee_town,
employee_address: this.CreateEmployeeForm.basicInfo.employee_address,
employee_zip: this.CreateEmployeeForm.basicInfo.employee_zip,
employee_apt: this.CreateEmployeeForm.basicInfo.employee_apt,
employee_birthday: this.CreateEmployeeForm.basicInfo.employee_birthday,
employee_phone_number: this.CreateEmployeeForm.basicInfo.employee_phone_number,
employee_start_date: this.CreateEmployeeForm.basicInfo.employee_start_date,
employee_end_date: this.CreateEmployeeForm.basicInfo.employee_end_date,
employee_type: this.CreateEmployeeForm.basicInfo.employee_type,
employee_state: this.CreateEmployeeForm.basicInfo.employee_state,
};
this.EditEmployee(payload);
this.v$.$validate();
if (!this.v$.$error) {
this.EditEmployee(this.CreateEmployeeForm);
} else {
console.log("Form validation failed.");
}
},
getEmployeeTypeList() {
let path = import.meta.env.VITE_BASE_URL + "/query/employeetype";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.employList = response.data;
})
.catch(() => {
});
const path = import.meta.env.VITE_BASE_URL + "/query/employeetype";
axios.get(path, { withCredentials: true })
.then((response: any) => { this.employList = response.data; });
},
getStatesList() {
let path = import.meta.env.VITE_BASE_URL + "/query/states";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.stateList = response.data;
})
.catch(() => {
});
const path = import.meta.env.VITE_BASE_URL + "/query/states";
axios.get(path, { withCredentials: true })
.then((response: any) => { this.stateList = response.data; });
},
},
})
</script>
<style scoped></style>
</script>

View File

@@ -1,109 +1,109 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs mb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'employee' }">
employees
</router-link>
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Employees</li>
</ul>
</div>
<h1 class="text-3xl font-bold mt-4">Employees</h1>
<div class="flex start pb-10 text-2xl">Employees </div>
<div class="flex justify-end mb-10">
<router-link :to="{ name: 'employeeCreate' }">
<button class="btn btn-secondary btn-sm">Create Employee</button>
</router-link>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Count and Add Button -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<div class="badge badge-ghost">{{ recordsLength }} employees found</div>
<router-link :to="{ name: 'employeeCreate' }" class="btn btn-primary btn-sm">
Create New Employee
</router-link>
</div>
<div class="divider"></div>
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Employee ID</th>
<th>Name</th>
<th>Role</th>
<th>Town</th>
<th>Phone Number</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="person in employees" :key="person.id" class="hover:bg-blue-600 hover:text-white">
<td>{{ person.id }}</td>
<td>{{ person.employee_first_name }} {{ person.employee_last_name }}</td>
<td><span class="badge badge-ghost badge-sm">{{ getEmployeeTypeName(person.employee_type) }}</span></td>
<td>{{ person.employee_town }}</td>
<td>{{ person.employee_phone_number }}</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'employeeEdit', params: { id: person.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'employeeProfile', params: { id: person.id } }" class="btn btn-sm btn-ghost">View</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="person in employees" :key="person.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ person.employee_first_name }} {{ person.employee_last_name }}</h2>
<p class="text-xs text-gray-400">ID: #{{ person.id }}</p>
</div>
<div class="badge badge-ghost">
{{ getEmployeeTypeName(person.employee_type) }}
</div>
</div>
<div class="text-sm mt-2">
<p>{{ person.employee_town }}</p>
<p>{{ person.employee_phone_number }}</p>
</div>
<div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'employeeEdit', params: { id: person.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'employeeProfile', params: { id: person.id } }" class="btn btn-sm btn-ghost">View</router-link>
</div>
</div>
</div>
</div>
</div>
<div class="overflow-x-auto bg-neutral">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Employee ID</th>
<th>Name</th>
<th>Type</th>
<th>Town</th>
<th>Phone Number</th>
<th></th>
</tr>
</thead>
<tbody>
<!-- row 1 -->
<tr v-for="person in employees" :key="person['id']">
<td>{{ person['id'] }} </td>
<td>{{ person['employee_first_name'] }} {{ person['employee_last_name'] }}</td>
<td>
<div v-if="person['employee_type'] == 0">Owner</div>
<div v-else-if="person['employee_type'] == 1">Manager</div>
<div v-else-if="person['employee_type'] == 2">Secretary</div>
<div v-else-if="person['employee_type'] == 3">Office</div>
<div v-else-if="person['employee_type'] == 4">Driver</div>
<div v-else-if="person['employee_type'] == 5">Service Tech</div>
<div v-else-if="person['employee_type'] == 6">Contract</div>
<div v-else-if="person['employee_type'] == 7">Cash</div>
<div v-else-if="person['employee_type'] == 8">Driver/Tech</div>
<div v-else></div>
</td>
<td>{{ person['employee_town'] }}</td>
<td>{{ person['employee_phone_number'] }}</td>
<td class="flex gap-5">
<router-link :to="{ name: 'employeeEdit', params: { id: person['id'] } }">
<button class="btn btn-secondary btn-sm">Edit</button>
</router-link>
<router-link :to="{ name: 'employeeProfile', params: { id: person['id'] } }">
<button class="btn btn-secondary btn-sm">View</button>
</router-link>
</td>
</tr>
</tbody>
</table>
</div>
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options" class="mt-10">
<!-- Pagination -->
<div class="mt-6 flex justify-center">
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options">
</pagination>
<div class="flex justify-center mb-10"> {{ recordsLength }} items Found</div>
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts">
</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 PaginationComp from '../../components/pagination.vue'
import SideBar from '../../layouts/sidebar/sidebar.vue'
import Footer from '../../layouts/footers/footer.vue'
export default defineComponent({
name: 'EmployeeHome',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
token: null,
user: null,
employees: [],
employees: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,
@@ -114,49 +114,55 @@ export default defineComponent({
}
}
},
created() {
this.userStatus()
this.userStatus();
},
mounted() {
this.getPage(this.page)
this.getPage(this.page);
},
methods: {
getPage: function (page: any) {
// we simulate an api call that fetch the records from a backend
this.employees = [];
this.get_employees(page)
getPage: function (page: number) {
this.get_employees(page);
},
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
this.user = null;
});
},
// --- METHOD CORRECTED TO MATCH YOUR SIMPLE ARRAY API RESPONSE ---
get_employees(page: number) {
// Using your original, working URL
const path = `${import.meta.env.VITE_BASE_URL}/employee/all/${page}`;
axios.get(path, { headers: authHeader() })
.then((response: any) => {
// --- FIX 1: Assign the response data directly, as it's an array ---
this.employees = response.data;
// --- FIX 2: Set the recordsLength from the array length ---
// NOTE: For full pagination, your API will eventually need to send the *total* count.
// For now, this will show the count of items on the current page.
// If you update your API to send { data: [], total_records: X }, this is the only line you'd change.
this.recordsLength = response.data.length;
})
.catch((error: any) => {
console.error("Failed to fetch employees:", error);
});
},
get_employees(page: any) {
let path = import.meta.env.VITE_BASE_URL + '/employee/all/' + page;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.employees = response.data
})
},
getEmployeeTypeName(typeId: number | string): string {
const typeMap: { [key: string]: string } = {
'0': 'Owner', '1': 'Manager', '2': 'Secretary', '3': 'Office',
'4': 'Driver', '5': 'Service Tech', '6': 'Contractor', '7': 'Cash Driver', '8': 'Driver/Tech'
};
return typeMap[String(typeId)] || 'Unknown Role';
}
},
})
</script>
<style scoped></style>
</script>

View File

@@ -1,108 +1,84 @@
<template>
<Header />
<div class="wrapper">
<div class="flex">
<div class="">
<SideBar />
<div class="flex">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs -->
<div class="text-sm breadcrumbs">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Employees</li>
<li>Profile</li>
</ul>
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
<router-link :to="{ name: 'customer' }">
Customers
</router-link>
</li>
</ul>
<div class=" w-full mt-10" v-if="loaded">
<div class="grid grid-cols-12 gap-5 ">
<div class="col-span-3 ">
<img src="../../../assets/images/user_placeholder.png" alt="Drone Image" width="200" height="250" />
<!-- Loading State -->
<div v-if="!loaded" class="text-center p-10">
<span class="loading loading-spinner loading-lg"></span>
</div>
<!-- Main Content -->
<div v-else>
<!-- Employee Header Card -->
<div class="bg-neutral rounded-lg p-6 mt-6">
<div class="flex flex-col md:flex-row gap-6">
<!-- Left Side: Avatar and Name -->
<div class="flex-none flex flex-col items-center md:items-start text-center md:text-left">
<div class="avatar">
<div class="w-32 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2">
<img src="../../../assets/images/user_placeholder.png" alt="Employee Avatar" />
</div>
</div>
<div class="col-span-9">
<div class="grid grid-cols-12">
<div class="col-span-12 font-bold flex justify-end">
<div class="btn btn-sm btn-secondary">
<router-link :to="{ name: 'employeeEdit', params: { id: employee.id } }">
Edit Employee
</router-link>
</div>
</div>
<div class="col-span-12 font-bold flex">
{{ employee.employee_first_name }}
{{ employee.employee_last_name }}
</div>
<div class="col-span-12 font-bold flex">
{{ employee.employee_address }}
<div v-if="employee.employee_apt != 'None'">
{{ employee.employee_apt }}
</div>
</div>
<div class="col-span-12 font-bold flex">
<div class="pr-2">
{{ employee.employee_town }},
</div>
<h1 class="text-2xl font-bold mt-4">{{ employee.employee_first_name }} {{ employee.employee_last_name }}</h1>
<span class="badge badge-ghost mt-1">{{ employeeTypeName }}</span>
<router-link :to="{ name: 'employeeEdit', params: { id: employee.id } }" class="btn btn-sm btn-secondary mt-4">
Edit Employee
</router-link>
</div>
<div class="pr-2">
<div v-if="employee.employee_state == '0'">Massachusetts</div>
<div v-else-if="employee.employee_state == '1'">Rhode Island</div>
<div v-else-if="employee.employee_state == '2'">New Hampshire</div>
<div v-else-if="employee.employee_state == '3'">Maine</div>
<div v-else-if="employee.employee_state == '4'">Vermont</div>
<div v-else-if="employee.employee_state == '5'">Maine</div>
<div v-else-if="employee.employee_state == '6'">New York</div>
<div v-else>Unknown state</div>
</div>
<div class="pr-2">
{{ employee.employee_zip }}
</div>
</div>
<div class="col-span-12 font-bold flex" v-if="employee.employee_apt !== 'None'">
{{ employee.employee_apt }}
</div>
<div class="col-span-12 font-bold flex">
{{ employee.employee_phone_number }}
</div>
<div class="col-span-12 font-bold flex">
<div v-if="employee.employee_type == '0'">owner</div>
<div v-else-if="employee.employee_type == '1'">manager</div>
<div v-else-if="employee.employee_type == '2'">secretary</div>
<div v-else-if="employee.employee_type == '3'">office</div>
<div v-else-if="employee.employee_type == '4'">driver</div>
<div v-else-if="employee.employee_type == '5'">tech</div>
<div v-else-if="employee.employee_type == '6'">contract</div>
<div v-else-if="employee.employee_type == '7'">cash</div>
<div v-else-if="employee.employee_type == '8'">driver/tech</div>
<div v-else>Unknown employee type</div>
</div>
<!-- Right Side: Details Grid -->
<div class="flex-grow grid grid-cols-1 sm:grid-cols-2 gap-6 pt-4 border-t md:border-t-0 md:border-l border-base-100 md:pl-6">
<div>
<h3 class="font-bold text-sm uppercase text-gray-400">Contact Information</h3>
<div class="mt-2 space-y-1 text-sm">
<p>{{ employee.employee_phone_number }}</p>
<p class="link link-hover">{{ employee.employee_email }}</p>
</div>
</div>
<div>
<h3 class="font-bold text-sm uppercase text-gray-400">Address</h3>
<div class="mt-2 space-y-1 text-sm">
<p>{{ employee.employee_address }}</p>
<p v-if="employee.employee_apt && employee.employee_apt !== 'None'">{{ employee.employee_apt }}</p>
<p>{{ employee.employee_town }}, {{ employeeStateName }} {{ employee.employee_zip }}</p>
</div>
</div>
<div>
<h3 class="font-bold text-sm uppercase text-gray-400">Employment Dates</h3>
<div class="mt-2 space-y-1 text-sm">
<p><strong>Start Date:</strong> {{ employee.employee_start_date || 'N/A' }}</p>
<p><strong>End Date:</strong> {{ employee.employee_end_date || 'N/A' }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-span-12 p-5 mt-5">
<div class="grid grid-cols-12">
<div class="col-span-12 font-bold flex text-2xl">
Delivery
</div>
<div class="col-span-6">
<div class="col-span-12 py-2">
Total Deliverys Done: {{ total_deliviers_done }}
</div>
<div class="col-span-12 py-2">
Total Gallons Delivered: {{ total_gallons_delivered }}
</div>
<div class="col-span-12 py-2">
Total Prime: {{ total_primes }}
</div>
</div>
</div>
<!-- Statistics Card -->
<div class="bg-neutral rounded-lg p-6 mt-6">
<h2 class="text-xl font-bold mb-4">Delivery Statistics</h2>
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-100 w-full">
<div class="stat">
<div class="stat-title">Total Deliveries</div>
<div class="stat-value">{{ total_deliviers_done }}</div>
</div>
<div class="stat">
<div class="stat-title">Total Gallons</div>
<div class="stat-value">{{ total_gallons_delivered }}</div>
<div class="stat-desc">Delivered</div>
</div>
<div class="stat">
<div class="stat-title">Total Primes</div>
<div class="stat-value text-error">{{ total_primes }}</div>
</div>
</div>
</div>
@@ -116,24 +92,15 @@
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'
export default defineComponent({
name: 'employeeProfile',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
token: null,
user_placeholder: '',
user: null,
loaded: false,
employee: {
@@ -144,128 +111,101 @@ export default defineComponent({
employee_address: "",
employee_apt: "",
employee_zip: "",
employee_birthday: "",
employee_phone_number: "",
employee_start_date: "",
employee_end_date: "",
employee_type: '',
employee_state: '',
employee_email: '', // Added for completeness
},
total_deliviers_done: 0,
total_gallons_delivered: 0,
total_primes: 0,
}
},
computed: {
employeeTypeName(): string {
const typeMap: Record<string, string> = {
'0': 'Owner', '1': 'Manager', '2': 'Secretary', '3': 'Office',
'4': 'Driver', '5': 'Tech', '6': 'Contractor', '7': 'Cash Driver', '8': 'Driver/Tech'
};
return typeMap[this.employee.employee_type] || 'Unknown Role';
},
employeeStateName(): string {
const stateMap: Record<string, string> = {
'0': 'MA', '1': 'RI', '2': 'NH', '3': 'ME', '4': 'VT', '5': 'CT', '6': 'NY'
};
return stateMap[this.employee.employee_state] || 'Unknown State';
}
},
created() {
this.userStatus()
this.userStatus();
this.getEmployee(this.$route.params.id);
},
mounted() {
this.getEmployee(this.$route.params.id)
},
watch: {
$route() {
this.getEmployee(this.$route.params.id);
// Watch the route to refetch data if the employee ID changes
'$route.params.id'(newId) {
if (newId) {
this.getEmployee(newId);
}
},
},
methods: {
getPage: function (page: any) {
// we simulate an api call that fetch the records from a backend
this.getEmployee(page)
},
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
})
this.user = null;
});
},
getEmployee(userid: any) {
let path = import.meta.env.VITE_BASE_URL + "/employee/" + userid;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
this.loaded = false; // Set loading state
const path = `${import.meta.env.VITE_BASE_URL}/employee/${userid}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data) {
this.employee = response.data;
if (this.employee.id){
this.getEmployeeStatsDeliveriesDone(this.employee.id)
this.getEmployeeStatsGallonsDone(this.employee.id)
this.getEmployeeStatsPrimesDone(this.employee.id)
// Fetch stats only after we confirm we have a valid employee
if (this.employee.id) {
this.getEmployeeStatsDeliveriesDone(this.employee.id);
this.getEmployeeStatsGallonsDone(this.employee.id);
this.getEmployeeStatsPrimesDone(this.employee.id);
}
this.loaded = true
}
})
.catch((error: any) => {
console.error("Failed to fetch employee data:", error);
})
.finally(() => {
this.loaded = true; // End loading state
});
},
getEmployeeStatsDeliveriesDone(userid: any) {
let path = import.meta.env.VITE_BASE_URL + "/stats/delivery/total/" + userid;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
getEmployeeStatsDeliveriesDone(userid: string) {
const path = `${import.meta.env.VITE_BASE_URL}/stats/delivery/total/${userid}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data) {
this.total_deliviers_done = response.data.data;
}
})
this.total_deliviers_done = response.data.data || 0;
});
},
getEmployeeStatsGallonsDone(userid: any) {
let path = import.meta.env.VITE_BASE_URL + "/stats/gallons/total/" + userid;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
getEmployeeStatsGallonsDone(userid: string) {
const path = `${import.meta.env.VITE_BASE_URL}/stats/gallons/total/${userid}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data) {
this.total_gallons_delivered = response.data.data;
}
})
this.total_gallons_delivered = response.data.data || 0;
});
},
getEmployeeStatsPrimesDone(userid: any) {
let path = import.meta.env.VITE_BASE_URL + "/stats/primes/total/" + userid;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
getEmployeeStatsPrimesDone(userid: string) {
const path = `${import.meta.env.VITE_BASE_URL}/stats/primes/total/${userid}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data) {
this.total_primes = response.data.data;
}
})
this.total_primes = response.data.data || 0;
});
},
},
})
</script>
<style scoped></style>
});
</script>

View File

@@ -1,9 +1,6 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs mb-10">
<ul>

View File

@@ -1,9 +1,6 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10">
<div class="text-sm breadcrumbs">
<ul>

View File

@@ -1,9 +1,6 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class="w-full px-10">
<div class="text-sm breadcrumbs mb-4">
<ul>
@@ -78,7 +75,7 @@ export default defineComponent({
events: `${import.meta.env.VITE_BASE_URL}/service/all`,
eventClick: this.handleEventClick,
// Add headers for authentication if needed by your API
eventSourceSuccess: (content, response) => {
eventSourceSuccess: (content) => {
// This is where you could transform data if needed
return content;
},

View File

@@ -1,66 +1,125 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class=" w-full px-10 ">
<div class="text-sm breadcrumbs mb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
Service Calls
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Service Calls</li>
</ul>
</div>
<h1 class="text-3xl font-bold mt-4">Service Calls</h1>
<div class="flex text-2xl mb-5 font-bold">
Service Calls
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Title and Count -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">Upcoming and Active Service Calls</h2>
<div v-if="!isLoading" class="badge badge-ghost">{{ services.length }} calls found</div>
</div>
<div class="divider"></div>
<!-- Loading State -->
<div v-if="isLoading" class="text-center p-10">
<span class="loading loading-spinner loading-lg"></span>
<p class="mt-2">Loading service calls...</p>
</div>
<!-- Empty State -->
<div v-else-if="services.length === 0" class="text-center p-10">
<p>No active service calls found.</p>
</div>
<!-- Data Display -->
<div v-else>
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Date / Time</th>
<th>Customer</th>
<th>Address</th>
<th>Service Type</th>
<th>Description</th>
<th class="text-right">Cost</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<!-- Removed @click from tr to avoid conflicting actions -->
<tr v-for="service in services" :key="service.id" class="hover">
<td class="align-top">
<div>{{ formatDate(service.scheduled_date) }}</div>
<div class="text-xs opacity-70">{{ formatTime(service.scheduled_date) }}</div>
</td>
<td class="align-top">{{ service.customer_name }}</td>
<td class="align-top">{{ service.customer_address }}, {{ service.customer_town }}</td>
<td class="align-top">
<span class="font-medium" :style="{ color: getServiceTypeColor(service.type_service_call) }">
{{ getServiceTypeName(service.type_service_call) }}
</span>
</td>
<td class="whitespace-normal text-sm align-top">
<!-- TRUNCATION LOGIC FOR DESKTOP -->
<div v-if="!isLongDescription(service.description) || isExpanded(service.id)">
{{ service.description }}
<a v-if="isLongDescription(service.description)" @click.prevent="toggleExpand(service.id)" href="#" class="link link-info link-hover text-xs ml-1 whitespace-nowrap">Show less</a>
</div>
<div v-else>
{{ truncateDescription(service.description) }}
<a @click.prevent="toggleExpand(service.id)" href="#" class="link link-info link-hover text-xs ml-1 whitespace-nowrap">Read more</a>
</div>
</td>
<td class="text-right font-mono align-top">{{ formatCurrency(service.service_cost) }}</td>
<td class="text-right align-top">
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="service in services" :key="service.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ service.customer_name }}</h2>
<p class="text-xs text-gray-400">{{ service.customer_address }}, {{ service.customer_town }}</p>
</div>
<div class="badge badge-outline text-right" :style="{ 'border-color': getServiceTypeColor(service.type_service_call), color: getServiceTypeColor(service.type_service_call) }">
{{ getServiceTypeName(service.type_service_call) }}
</div>
</div>
<div class="text-sm mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<p><strong class="font-semibold">Date:</strong> {{ formatDate(service.scheduled_date) }}</p>
<p><strong class="font-semibold">Time:</strong> {{ formatTime(service.scheduled_date) }}</p>
<p><strong class="font-semibold">Cost:</strong> <span class="font-mono">{{ formatCurrency(service.service_cost) }}</span></p>
</div>
<!-- TRUNCATION LOGIC FOR MOBILE -->
<div v-if="service.description" class="text-sm mt-2 p-2 bg-base-200 rounded-md prose max-w-none">
<div v-if="!isLongDescription(service.description) || isExpanded(service.id)">
{{ service.description }}
<a v-if="isLongDescription(service.description)" @click.prevent="toggleExpand(service.id)" href="#" class="link link-info link-hover text-xs ml-1 whitespace-nowrap">Show less</a>
</div>
<div v-else>
{{ truncateDescription(service.description) }}
<a @click.prevent="toggleExpand(service.id)" href="#" class="link link-info link-hover text-xs ml-1 whitespace-nowrap">Read more</a>
</div>
</div>
<div class="card-actions justify-end mt-2">
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="isLoading" class="text-center p-10">
<p>Loading service calls...</p>
</div>
<div v-else-if="services.length === 0" class="text-center p-10 bg-base-200 rounded-md">
<p>No service calls found.</p>
</div>
<div v-else class="overflow-x-auto rounded-lg">
<table class="min-w-full divide-y divide-gray-700">
<thead class="bg-base-200">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Scheduled Date</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Time</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Customer Name</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Address</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Service Type</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Description</th>
</tr>
</thead>
<tbody class="bg-base-100 divide-y divide-gray-700">
<tr v-for="service in services" :key="service.id" @click="openEditModal(service)" class="hover:bg-base-300 cursor-pointer">
<td class="px-6 py-4 whitespace-nowrap">{{ formatDate(service.scheduled_date) }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ formatTime(service.scheduled_date) }}</td>
<td class="px-6 py-4 whitespace-nowrap hover:text-blue-600">{{ 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">{{ service.description }}</td>
<td class="px-6 py-4 whitespace-normal text-sm">{{ service.service_cost }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
@@ -74,16 +133,13 @@
@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'
import dayjs from 'dayjs'; // Import dayjs to handle date/time formatting
import dayjs from 'dayjs';
interface ServiceCall {
id: number;
@@ -99,13 +155,16 @@ interface ServiceCall {
export default defineComponent({
name: 'ServiceHome',
components: { Header, SideBar, Footer, ServiceEditModal },
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[],
}
},
created() {
@@ -154,7 +213,26 @@ export default defineComponent({
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}`;
@@ -206,7 +284,17 @@ export default defineComponent({
};
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',

View File

@@ -1,75 +1,126 @@
<template>
<Header />
<div class="flex">
<div class="">
<SideBar />
</div>
<div class="w-full px-10">
<div class="text-sm breadcrumbs mb-10">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li>
<router-link :to="{ name: 'home' }">
Home
</router-link>
</li>
<li>
PastService Calls
</li>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Past Service Calls</li>
</ul>
</div>
<div class="flex text-2xl mb-5 font-bold">
Past Service Calls
</div>
<div v-if="isLoading" class="text-center p-10">
<p>Loading service calls...</p>
</div>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Title and Count -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">Service Call History</h2>
<div v-if="!isLoading" class="badge badge-ghost">{{ services.length }} calls found</div>
</div>
<div class="divider"></div>
<div v-else-if="services.length === 0" class="text-center p-10 bg-base-200 rounded-md">
<p>No service calls found.</p>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="text-center p-10">
<span class="loading loading-spinner loading-lg"></span>
<p class="mt-2">Loading service calls...</p>
</div>
<div v-else class="overflow-x-auto rounded-lg">
<table class="min-w-full divide-y divide-gray-700 table-fixed">
<!-- =================== THIS IS THE CORRECTED SECTION =================== -->
<thead class="bg-base-200">
<tr>
<!-- Columns with predictable, shorter content get fixed widths -->
<th scope="col" class="w-48 px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Scheduled Date</th>
<th scope="col" class="w-32 px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Time</th>
<!-- Columns with variable text content can share the remaining space -->
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Customer Name</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Address</th>
<!-- Another fixed-width column -->
<th scope="col" class="w-32 px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Service Type</th>
<!-- Empty State -->
<div v-else-if="services.length === 0" class="text-center p-10">
<p>No past service calls found.</p>
</div>
<!-- Description can be left to fill remaining space -->
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Description</th>
<!-- Service Cost now has a smaller, fixed width -->
<th scope="col" class="w-20 px-6 py-3 text-left text-xs font-medium text-base-content uppercase tracking-wider">Service Cost</th>
</tr>
</thead>
<!-- =================== END OF CORRECTED SECTION =================== -->
<!-- Data Display -->
<div v-else>
<!-- DESKTOP VIEW: Table (Revamped) -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Date / Time</th>
<th>Customer</th>
<th>Address</th>
<th>Service Type</th>
<th>Description</th>
<th class="text-right">Cost</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<!-- Removed @click from tr to avoid conflicting with "Read more" -->
<tr v-for="service in services" :key="service.id" class="hover">
<td class="align-top">
<div>{{ formatDate(service.scheduled_date) }}</div>
<div class="text-xs opacity-70">{{ formatTime(service.scheduled_date) }}</div>
</td>
<td class="align-top">{{ service.customer_name }}</td>
<td class="align-top">{{ service.customer_address }}, {{ service.customer_town }}</td>
<td class="align-top">
<span class="font-medium" :style="{ color: getServiceTypeColor(service.type_service_call) }">
{{ getServiceTypeName(service.type_service_call) }}
</span>
</td>
<td class="whitespace-normal text-sm align-top">
<!-- TRUNCATION LOGIC FOR DESKTOP -->
<div v-if="!isLongDescription(service.description) || isExpanded(service.id)">
{{ service.description }}
<a v-if="isLongDescription(service.description)" @click.prevent="toggleExpand(service.id)" href="#" class="link link-info link-hover text-xs ml-1 whitespace-nowrap">Show less</a>
</div>
<div v-else>
{{ truncateDescription(service.description) }}
<a @click.prevent="toggleExpand(service.id)" href="#" class="link link-info link-hover text-xs ml-1 whitespace-nowrap">Read more</a>
</div>
</td>
<td class="text-right font-mono align-top">{{ formatCurrency(service.service_cost) }}</td>
<td class="text-right align-top">
<!-- Moved @click handler to the button for explicit action -->
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
</td>
</tr>
</tbody>
</table>
</div>
<tbody class="bg-base-100 divide-y divide-gray-700">
<tr v-for="service in services" :key="service.id" @click="openEditModal(service)" class="hover:bg-base-300 cursor-pointer">
<td class="px-6 py-4 whitespace-nowrap">{{ formatDate(service.scheduled_date) }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ formatTime(service.scheduled_date) }}</td>
<td class="px-6 py-4 whitespace-nowrap hover:text-blue-600 truncate">{{ service.customer_name }}</td>
<td class="px-6 py-4 whitespace-nowrap truncate">{{ 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">{{ service.description }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-right">{{ formatCurrency(service.service_cost) }}</td>
</tr>
</tbody>
</table>
<!-- MOBILE VIEW: Cards (Revamped) -->
<div class="xl:hidden space-y-4">
<!-- Removed @click from card div -->
<div v-for="service in services" :key="service.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">{{ service.customer_name }}</h2>
<p class="text-xs text-gray-400">{{ service.customer_address }}, {{ service.customer_town }}</p>
</div>
<div class="badge badge-outline text-right" :style="{ 'border-color': getServiceTypeColor(service.type_service_call), color: getServiceTypeColor(service.type_service_call) }">
{{ getServiceTypeName(service.type_service_call) }}
</div>
</div>
<div class="text-sm mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<p><strong class="font-semibold">Date:</strong> {{ formatDate(service.scheduled_date) }}</p>
<p><strong class="font-semibold">Time:</strong> {{ formatTime(service.scheduled_date) }}</p>
<p><strong class="font-semibold">Cost:</strong> <span class="font-mono">{{ formatCurrency(service.service_cost) }}</span></p>
</div>
<!-- TRUNCATION LOGIC FOR MOBILE -->
<div v-if="service.description" class="text-sm mt-2 p-2 bg-base-200 rounded-md prose max-w-none">
<div v-if="!isLongDescription(service.description) || isExpanded(service.id)">
{{ service.description }}
<a v-if="isLongDescription(service.description)" @click.prevent="toggleExpand(service.id)" href="#" class="link link-info link-hover text-xs ml-1 whitespace-nowrap">Show less</a>
</div>
<div v-else>
{{ truncateDescription(service.description) }}
<a @click.prevent="toggleExpand(service.id)" href="#" class="link link-info link-hover text-xs ml-1 whitespace-nowrap">Read more</a>
</div>
</div>
<div class="card-actions justify-end mt-2">
<!-- Moved @click handler to the button -->
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -84,13 +135,10 @@
@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'
import dayjs from 'dayjs';
@@ -109,22 +157,47 @@ interface ServiceCall {
export default defineComponent({
name: 'ServiceHPast',
components: { Header, SideBar, Footer, ServiceEditModal },
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[],
}
},
created() {
this.userStatus();
this.fetchUpcomingServices();
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);
}
},
// --- API and Data Handling Methods ---
async fetchUpcomingServices(): Promise<void> {
async fetchPastServices(): Promise<void> {
this.isLoading = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/past';
@@ -134,7 +207,7 @@ export default defineComponent({
});
this.services = response.data;
} catch (error) {
console.error("Failed to fetch upcoming service calls:", error);
console.error("Failed to fetch past service calls:", error);
} finally {
this.isLoading = false;
}
@@ -198,16 +271,15 @@ export default defineComponent({
},
// --- Formatting and Display Methods ---
formatCurrency(value: string): string {
if (!value) return '$0.00';
const costAsNumber = parseFloat(value);
if (isNaN(costAsNumber)) {
return value;
}
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(costAsNumber);
}).format(numberValue);
},
formatDate(dateString: string): string {

View File

@@ -1,39 +1,57 @@
<template>
<Header />
<div class="flex">
<div class="w-full px-10">
<div class="text-sm breadcrumbs mb-4">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<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>
<li>Service Calendar</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>
<!--
Main Responsive Container:
- Stacks vertically on mobile (flex-col)
- Sits side-by-side on large screens (lg:flex-row)
-->
<div class="flex flex-col lg:flex-row gap-6 mt-6">
<!-- Sidebar Area (Uses our new responsive EventSidebar) -->
<EventSidebar v-if="!isLoading && customer" :customer="customer" @event-scheduled="handleEventScheduled" />
<!-- Loading/Error States (Styled to match the sidebar) -->
<div v-else class="w-full lg:w-96 lg:flex-none p-4">
<div class="bg-neutral rounded-lg p-6 sticky top-4 text-center">
<div v-if="isLoading">
<span class="loading loading-spinner"></span>
<p class="mt-2">Loading Customer...</p>
</div>
<div v-else>
<h2 class="text-xl font-bold text-error">Error</h2>
<p>Could not load customer data. You can still view the master calendar.</p>
</div>
</div>
</div>
<div class="flex-1 p-4 overflow-auto">
<!-- Main Calendar Area -->
<div class="flex-grow bg-neutral rounded-lg p-4">
<FullCalendar ref="fullCalendar" :options="calendarOptions" />
</div>
<ServiceEditModal
v-if="selectedServiceForEdit"
:service="selectedServiceForEdit"
@close-modal="closeEditModal"
@save-changes="handleSaveChanges"
@delete-service="handleDeleteService"
/>
</div>
</div>
</div>
<!-- Modal remains at the root level, which is correct -->
<ServiceEditModal
v-if="selectedServiceForEdit"
:service="selectedServiceForEdit"
@close-modal="closeEditModal"
@save-changes="handleSaveChanges"
@delete-service="handleDeleteService"
/>
</template>
<script lang="ts">

View File

@@ -1,65 +1,77 @@
<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>
<!--
This container is now responsive. It's full-width on mobile
and becomes a fixed-width sidebar on large screens.
-->
<div class="w-full lg:w-96 lg:flex-none p-4">
<!-- The sidebar is now sticky to the top on large screens -->
<div class="bg-neutral rounded-lg p-6 sticky top-4">
<h2 class="text-xl font-bold mb-4">Schedule Service</h2>
<form @submit.prevent="submitEvent" class="space-y-4">
<!-- Calendar Label -->
<div class="form-control">
<label class="label"><span class="label-text">Calendar Label</span></label>
<input type="text" v-model="event.title" required class="input input-bordered input-sm w-full" placeholder="e.g., Boiler Tune-up">
</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>
<!-- Service Type -->
<div class="form-control">
<label class="label"><span class="label-text">Type of Service</span></label>
<select class="select select-bordered select-sm w-full" 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>
<!-- Description -->
<div class="form-control">
<label class="label"><span class="label-text">Description</span></label>
<textarea v-model="event.description" rows="3" required class="textarea textarea-bordered textarea-sm" placeholder="Notes for the technician..."></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>
<!-- Date & Time Grid -->
<div class="grid grid-cols-2 gap-4">
<!-- Date -->
<div class="form-control">
<label class="label"><span class="label-text">Date</span></label>
<input type="date" v-model="event.date" required class="input input-bordered input-sm w-full">
</div>
<!-- Time -->
<div class="form-control">
<label class="label"><span class="label-text">Time</span></label>
<select v-model="event.time" class="select select-bordered select-sm w-full">
<option v-for="hour in 24" :key="hour" :value="hour - 1">{{ (hour - 1).toString().padStart(2, '0') }}:00</option>
</select>
</div>
</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>
<button type="submit" class="btn btn-primary btn-sm w-full mt-4">
Add Event
</button>
</form>
<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 }}
<!-- Customer Info Section -->
<div v-if="customer" class="mt-6">
<div class="divider">For Customer</div>
<!-- Customer Info "Card within a Card" -->
<div class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<h3 class="card-title text-base">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</h3>
<div class="text-sm mt-2 space-y-1">
<p>{{ customer.customer_address }}</p>
<p v-if="customer.customer_apt && customer.customer_apt !== 'None'">{{ customer.customer_apt }}</p>
<p>{{ customer.customer_town }}, {{ customerStateName }} {{ customer.customer_zip }}</p>
<p class="pt-2 font-semibold">{{ customer.customer_phone_number }}</p>
</div>
<div class="card-actions justify-end">
<div class="badge badge-outline">{{ customerHomeType }}</div>
</div>
</div>
</div>
</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>

89
src/stores/search.ts Normal file
View File

@@ -0,0 +1,89 @@
// src/stores/search.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import axios from 'axios'
// Define a type for what a search result looks like. This is good practice.
interface CustomerSearchResult {
id: number;
customer_first_name: string;
customer_last_name: string;
customer_address: string;
customer_town: string;
customer_state: string;
customer_phone_number: string;
}
export const useSearchStore = defineStore('search', () => {
// --- STATE ---
const searchTerm = ref('');
const searchResults = ref<CustomerSearchResult[]>([]);
const isLoading = ref(false);
// --- NEW: A variable to hold our timer ID for debouncing ---
// Using `any` is okay here, but `number` for browser or `Timeout` for Node is more specific.
let debounceTimer: any = null;
// --- GETTERS ---
const showResults = computed(() => {
return searchTerm.value.length > 1 && searchResults.value.length > 0;
});
// --- ACTIONS ---
// This is the original function that makes the API call. We'll keep it.
async function fetchSearchResults() {
if (searchTerm.value.length < 2) {
searchResults.value = [];
return;
}
isLoading.value = true;
try {
const response = await axios.get(`/search/customer?q=${searchTerm.value}`);
searchResults.value = response.data;
} catch (error) {
console.error("Failed to fetch search results:", error);
searchResults.value = [];
} finally {
isLoading.value = false;
}
}
// --- NEW: This is the debounced action your component will call ---
function debouncedSearch() {
// 1. Clear any existing timer. If the user is still typing, this
// cancels the previous plan to make an API call.
clearTimeout(debounceTimer);
// 2. Set a new timer. We will only call `fetchSearchResults` after the
// user has stopped typing for 400 milliseconds.
debounceTimer = setTimeout(() => {
fetchSearchResults();
}, 400); // 400ms is a good balance between responsiveness and efficiency
}
function clearSearch() {
searchTerm.value = '';
searchResults.value = [];
// Also clear the timer if a search is in progress
clearTimeout(debounceTimer);
}
// --- RETURN ---
return {
// State
searchTerm,
searchResults,
isLoading,
// Getters
showResults,
// Actions
fetchSearchResults, // You might not need to export this anymore
debouncedSearch, // Export the new debounced function
clearSearch,
}
})

View File

@@ -5,7 +5,7 @@
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"esModuleInterop": true,
/* Bundler mode */
"moduleResolution": "bundler",

View File

@@ -1,13 +1,29 @@
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {},
build: {
sourcemap: true,
chunkSizeWarningLimit: 1600,
assetsInlineLimit: 2048, // 2kb
},
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
// Rule #1: Handle all search requests
'/search': {
target: 'http://backend_office_dev:4056',
changeOrigin: true,
},
// Rule #2 (NEW): Handle all authentication requests
'/auth': {
target: 'http://backend_office_dev:4056',
changeOrigin: true,
},
// Add more rules here for your other API blueprints if needed
// For example, if you have delivery routes:
// '/delivery': {
// target: 'http://backend_office_dev:4056',
// changeOrigin: true,
// },
}
}
})