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

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>