get create client working
This commit is contained in:
parent
4a3576168a
commit
cb33d0c3b3
@ -96,6 +96,8 @@ def get_client(client_name):
|
||||
customer = frappe.get_doc("Customer", client_name)
|
||||
clientData = {**clientData, **customer.as_dict()}
|
||||
addresses = frappe.db.get_all("Address", fields=["*"], filters={"custom_customer_to_bill": client_name})
|
||||
contacts = frappe.db.get_all("Contact", fields=["*"], filters={"custom_customer": client_name})
|
||||
clientData["contacts"] = contacts
|
||||
for address in addresses if addresses else []:
|
||||
addressData = {"jobs": []}
|
||||
addressData = {**addressData, **address}
|
||||
@ -263,7 +265,7 @@ def upsert_client(data):
|
||||
"customer_type": data.get("customer_type")
|
||||
}).insert(ignore_permissions=True)
|
||||
else:
|
||||
customer_doc = frappe.get_doc("Customer", customer)
|
||||
customer_doc = frappe.get_doc("Customer", data.get("customer_name"))
|
||||
|
||||
print("Customer:", customer_doc.as_dict())
|
||||
|
||||
@ -281,6 +283,7 @@ def upsert_client(data):
|
||||
# Create address
|
||||
address_doc = frappe.get_doc({
|
||||
"doctype": "Address",
|
||||
"address_title": data.get("address_title"),
|
||||
"address_line1": data.get("address_line1"),
|
||||
"address_line2": data.get("address_line2"),
|
||||
"city": data.get("city"),
|
||||
@ -297,12 +300,43 @@ def upsert_client(data):
|
||||
}
|
||||
address_doc.append("links", link)
|
||||
address_doc.save(ignore_permissions=True)
|
||||
contact_exists = frappe.db.exists("Contact", {"email_id": data.get("contact_email")})
|
||||
if not contact_exists:
|
||||
contact_doc = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": data.get("first_name"),
|
||||
"last_name": data.get("last_name"),
|
||||
"email_id": data.get("email"),
|
||||
"phone": data.get("phone_number"),
|
||||
"custom_customer": customer_doc.name,
|
||||
"links": [{
|
||||
"link_doctype": "Customer",
|
||||
"link_name": customer_doc.name
|
||||
}]
|
||||
}).insert(ignore_permissions=True)
|
||||
print("Created new contact:", contact_doc.as_dict())
|
||||
else:
|
||||
contact_doc = frappe.get_doc("Contact", {"email_id": data.get("contact_email")})
|
||||
print("Contact already exists:", contact_doc.as_dict())
|
||||
|
||||
return build_success_response({
|
||||
"customer": customer_doc.as_dict(),
|
||||
"address": address_doc.as_dict()
|
||||
"address": address_doc.as_dict(),
|
||||
"contact": contact_doc.as_dict()
|
||||
})
|
||||
except frappe.ValidationError as ve:
|
||||
return build_error_response(str(ve), 400)
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_client_names(search_term):
|
||||
"""Search for client names matching the search term."""
|
||||
try:
|
||||
search_pattern = f"%{search_term}%"
|
||||
client_names = frappe.db.get_all(
|
||||
"Customer",
|
||||
pluck="name")
|
||||
return build_success_response(client_names)
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import frappe
|
||||
import requests
|
||||
from urllib.parse import urlparse
|
||||
from custom_ui.db_utils import build_success_response, build_error_response
|
||||
import logging
|
||||
|
||||
allowed_hosts = ["api.zippopotam.us", "nominatim.openstreetmap.org"] # Update this list with trusted domains as needed
|
||||
@ -24,9 +25,9 @@ def request(url, method="GET", data=None, headers=None):
|
||||
)
|
||||
resp.raise_for_status()
|
||||
try:
|
||||
return resp.json()
|
||||
return build_success_response(resp.json())
|
||||
except ValueError:
|
||||
return {"text": resp.text}
|
||||
return build_success_response({"text": resp.text})
|
||||
except requests.exceptions.RequestException as e:
|
||||
frappe.log_error(message=str(e), title="Proxy Request Failed")
|
||||
frappe.throw("Failed to fetch data from external API.")
|
||||
7
frontend/package-lock.json
generated
7
frontend/package-lock.json
generated
@ -16,6 +16,7 @@
|
||||
"frappe-ui": "^0.1.205",
|
||||
"leaflet": "^1.9.4",
|
||||
"pinia": "^3.0.3",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.4.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-chartjs": "^5.3.3",
|
||||
@ -3634,6 +3635,12 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/primeicons": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
||||
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/primevue": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.4.1.tgz",
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"frappe-ui": "^0.1.205",
|
||||
"leaflet": "^1.9.4",
|
||||
"pinia": "^3.0.3",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.4.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-chartjs": "^5.3.3",
|
||||
|
||||
@ -328,8 +328,12 @@ class Api {
|
||||
return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { type });
|
||||
}
|
||||
|
||||
static async getClientNames(type) {
|
||||
return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { type });
|
||||
static async getClientNames(clientName) {
|
||||
return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { searchTerm: clientName });
|
||||
}
|
||||
|
||||
static async searchClientNames(searchTerm) {
|
||||
return await this.request("custom_ui.api.db.clients.search_client_names", { searchTerm });
|
||||
}
|
||||
|
||||
static async getCompanyNames() {
|
||||
@ -342,8 +346,7 @@ class Api {
|
||||
// Create methods
|
||||
|
||||
static async createClient(clientData) {
|
||||
const payload = DataUtils.toSnakeCaseObject(clientData);
|
||||
const result = await this.request(FRAPPE_UPSERT_CLIENT_METHOD, { data: payload });
|
||||
const result = await this.request(FRAPPE_UPSERT_CLIENT_METHOD, { data: clientData });
|
||||
console.log("DEBUG: API - Created/Updated Client: ", result);
|
||||
return result;
|
||||
}
|
||||
@ -391,7 +394,10 @@ class Api {
|
||||
if (!places || places.length === 0) {
|
||||
throw new Error(`No location data found for zip code ${zipcode}`);
|
||||
}
|
||||
return places;
|
||||
return places.map((place) => ({
|
||||
city: place["place name"],
|
||||
state: place["state abbreviation"],
|
||||
}));
|
||||
}
|
||||
|
||||
static async getGeocode(address) {
|
||||
|
||||
@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div class="form-section">
|
||||
<h3>Address Information</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-field full-width">
|
||||
<label for="address-title"> Address Title <span class="required">*</span> </label>
|
||||
<InputText
|
||||
id="address-title"
|
||||
v-model="localFormData.addressTitle"
|
||||
:disabled="isSubmitting || isEditMode"
|
||||
placeholder="e.g., Home, Office, Site A"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field full-width">
|
||||
<label for="address-line1"> Address Line 1 <span class="required">*</span> </label>
|
||||
<InputText
|
||||
id="address-line1"
|
||||
v-model="localFormData.addressLine1"
|
||||
:disabled="isSubmitting"
|
||||
placeholder="Street address"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field full-width">
|
||||
<label for="address-line2">Address Line 2</label>
|
||||
<InputText
|
||||
id="address-line2"
|
||||
v-model="localFormData.addressLine2"
|
||||
:disabled="isSubmitting"
|
||||
placeholder="Apt, suite, unit, etc."
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="zipcode"> Zip Code <span class="required">*</span> </label>
|
||||
<InputText
|
||||
id="zipcode"
|
||||
v-model="localFormData.pincode"
|
||||
:disabled="isSubmitting"
|
||||
@input="handleZipcodeInput"
|
||||
maxlength="5"
|
||||
placeholder="12345"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="city"> City <span class="required">*</span> </label>
|
||||
<InputText
|
||||
id="city"
|
||||
v-model="localFormData.city"
|
||||
:disabled="isSubmitting || zipcodeLookupDisabled"
|
||||
placeholder="City"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="state"> State <span class="required">*</span> </label>
|
||||
<InputText
|
||||
id="state"
|
||||
v-model="localFormData.state"
|
||||
:disabled="isSubmitting || zipcodeLookupDisabled"
|
||||
placeholder="State"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Api from "../../api";
|
||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEditMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData"]);
|
||||
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
const localFormData = computed({
|
||||
get: () => props.formData,
|
||||
set: (value) => emit("update:formData", value),
|
||||
});
|
||||
|
||||
const zipcodeLookupDisabled = ref(true);
|
||||
|
||||
const handleZipcodeInput = async (event) => {
|
||||
const input = event.target.value;
|
||||
|
||||
// Only allow digits
|
||||
const digitsOnly = input.replace(/\D/g, "");
|
||||
|
||||
// Limit to 5 digits
|
||||
if (digitsOnly.length > 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
localFormData.value.pincode = digitsOnly;
|
||||
|
||||
// Reset city/state if zipcode is not complete
|
||||
if (digitsOnly.length < 5 && zipcodeLookupDisabled.value) {
|
||||
localFormData.value.city = "";
|
||||
localFormData.value.state = "";
|
||||
zipcodeLookupDisabled.value = false;
|
||||
}
|
||||
|
||||
// Fetch city/state when 5 digits entered
|
||||
if (digitsOnly.length === 5) {
|
||||
try {
|
||||
console.log("DEBUG: Looking up city/state for zip code:", digitsOnly);
|
||||
const places = await Api.getCityStateByZip(digitsOnly);
|
||||
console.log("DEBUG: Retrieved places:", places);
|
||||
if (places && places.length > 0) {
|
||||
// Auto-populate city and state
|
||||
localFormData.value.city = places[0]["city"];
|
||||
localFormData.value.state = places[0]["state"];
|
||||
zipcodeLookupDisabled.value = true;
|
||||
notificationStore.addSuccess(`Found: ${places[0]["city"]}, ${places[0]["state"]}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Enable manual entry if lookup fails
|
||||
zipcodeLookupDisabled.value = false;
|
||||
notificationStore.addWarning(
|
||||
"Could not find city/state for this zip code. Please enter manually.",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--red-500);
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
322
frontend/src/components/clientSubPages/ClientInformationForm.vue
Normal file
322
frontend/src/components/clientSubPages/ClientInformationForm.vue
Normal file
@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h3>Client Information</h3>
|
||||
<div class="toggle-container" v-if="!isEditMode">
|
||||
<label for="new-client-toggle" class="toggle-label">New Client</label>
|
||||
<ToggleSwitch v-model="isNewClient" inputId="new-client-toggle" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label for="customer-name"> Customer Name <span class="required">*</span> </label>
|
||||
<div class="input-with-button">
|
||||
<InputText
|
||||
id="customer-name"
|
||||
v-model="localFormData.customerName"
|
||||
:disabled="isSubmitting || isEditMode"
|
||||
placeholder="Enter customer name"
|
||||
class="w-full"
|
||||
/>
|
||||
<Button
|
||||
v-if="!isNewClient && !isEditMode"
|
||||
@click="searchCustomers"
|
||||
:disabled="isSubmitting || !localFormData.customerName.trim()"
|
||||
size="small"
|
||||
class="iconoir-btn"
|
||||
>
|
||||
<IconoirMagnifyingGlass width="20" height="20" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="customer-type"> Customer Type <span class="required">*</span> </label>
|
||||
<Select
|
||||
id="customer-type"
|
||||
v-model="localFormData.customerType"
|
||||
:options="customerTypeOptions"
|
||||
:disabled="isSubmitting || (!isNewClient && !isEditMode)"
|
||||
placeholder="Select customer type"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer Search Results Modal -->
|
||||
<Dialog
|
||||
v-model:visible="showCustomerSearchModal"
|
||||
header="Select Customer"
|
||||
:modal="true"
|
||||
class="search-dialog"
|
||||
>
|
||||
<div class="search-results">
|
||||
<div v-if="customerSearchResults.length === 0" class="no-results">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
<p>No customers found matching your search.</p>
|
||||
</div>
|
||||
<div v-else class="results-list">
|
||||
<div
|
||||
v-for="(customerName, index) in customerSearchResults"
|
||||
:key="index"
|
||||
class="result-item"
|
||||
@click="selectCustomer(customerName)"
|
||||
>
|
||||
<strong>{{ customerName }}</strong>
|
||||
<i class="pi pi-chevron-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Cancel" severity="secondary" @click="showCustomerSearchModal = false" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from "vue";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Select from "primevue/select";
|
||||
import ToggleSwitch from "primevue/toggleswitch";
|
||||
import Dialog from "primevue/dialog";
|
||||
import Api from "../../api";
|
||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||
import { DocMagnifyingGlass as IconoirMagnifyingGlass } from "@iconoir/vue";
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEditMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData", "newClientToggle", "customerSelected"]);
|
||||
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
const localFormData = computed({
|
||||
get: () => props.formData,
|
||||
set: (value) => emit("update:formData", value),
|
||||
});
|
||||
|
||||
const isNewClient = ref(true);
|
||||
const showCustomerSearchModal = ref(false);
|
||||
const customerSearchResults = ref([]);
|
||||
const customerTypeOptions = ["Individual", "Partnership", "Company"];
|
||||
|
||||
// Watch for toggle changes
|
||||
watch(isNewClient, (newValue) => {
|
||||
emit("newClientToggle", newValue);
|
||||
});
|
||||
|
||||
const searchCustomers = async () => {
|
||||
const searchTerm = localFormData.value.customerName.trim();
|
||||
if (!searchTerm) return;
|
||||
|
||||
try {
|
||||
// Get all customers and filter by search term
|
||||
const allCustomers = await Api.getClientNames(searchTerm);
|
||||
const matchingNames = allCustomers.filter((name) =>
|
||||
name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
if (matchingNames.length === 0) {
|
||||
notificationStore.addWarning("No customers found matching your search criteria.");
|
||||
} else {
|
||||
// Store just the names for display
|
||||
customerSearchResults.value = matchingNames;
|
||||
showCustomerSearchModal.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error searching customers:", error);
|
||||
notificationStore.addError("Failed to search customers. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const selectCustomer = async (customerName) => {
|
||||
try {
|
||||
// Fetch full customer data
|
||||
const clientData = await Api.getClient(customerName);
|
||||
|
||||
localFormData.value.customerName = clientData.customerName;
|
||||
localFormData.value.customerType = clientData.customerType;
|
||||
showCustomerSearchModal.value = false;
|
||||
|
||||
// Pass the full client data including contacts
|
||||
emit("customerSelected", clientData);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching client ${customerName}:`, error);
|
||||
notificationStore.addError("Failed to load customer details. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
isNewClient,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toggle-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--red-500);
|
||||
}
|
||||
|
||||
.input-with-button {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.search-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.no-results i {
|
||||
font-size: 2em;
|
||||
color: var(--orange-500);
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
background-color: var(--surface-hover);
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.customer-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.customer-type {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.iconoir-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.iconoir-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.iconoir-btn:hover:not(:disabled) {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h3>Contact Information</h3>
|
||||
<div class="toggle-container" v-if="!isEditMode">
|
||||
<label for="new-contact-toggle" class="toggle-label">New Contact</label>
|
||||
<ToggleSwitch
|
||||
v-model="isNewContact"
|
||||
inputId="new-contact-toggle"
|
||||
:disabled="isNewClientLocked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<!-- Select existing contact mode -->
|
||||
<template v-if="!isNewContact">
|
||||
<div class="form-field full-width">
|
||||
<label for="contact-select"> Contact <span class="required">*</span> </label>
|
||||
<Select
|
||||
id="contact-select"
|
||||
v-model="selectedContact"
|
||||
:options="contactOptions"
|
||||
optionLabel="label"
|
||||
:disabled="isSubmitting || contactOptions.length === 0"
|
||||
placeholder="Select a contact"
|
||||
class="w-full"
|
||||
@change="handleContactSelect"
|
||||
/>
|
||||
<small v-if="contactOptions.length === 0" class="helper-text">
|
||||
No contacts available. Toggle "New Contact" to add one.
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="contact-phone">Phone</label>
|
||||
<InputText
|
||||
id="contact-phone"
|
||||
v-model="localFormData.phoneNumber"
|
||||
disabled
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="contact-email">Email</label>
|
||||
<InputText
|
||||
id="contact-email"
|
||||
v-model="localFormData.email"
|
||||
disabled
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- New contact mode -->
|
||||
<template v-else>
|
||||
<div class="form-field full-width">
|
||||
<div class="checkbox-container">
|
||||
<Checkbox
|
||||
v-model="sameAsClientName"
|
||||
inputId="same-as-client"
|
||||
:binary="true"
|
||||
:disabled="isSubmitting || isEditMode || !isNewClientLocked"
|
||||
/>
|
||||
<label for="same-as-client" class="checkbox-label">
|
||||
Same as Client Name
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="first-name"> First Name <span class="required">*</span> </label>
|
||||
<InputText
|
||||
id="first-name"
|
||||
v-model="localFormData.firstName"
|
||||
:disabled="isSubmitting || sameAsClientName"
|
||||
placeholder="Enter first name"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="last-name"> Last Name <span class="required">*</span> </label>
|
||||
<InputText
|
||||
id="last-name"
|
||||
v-model="localFormData.lastName"
|
||||
:disabled="isSubmitting || sameAsClientName"
|
||||
placeholder="Enter last name"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="phone-number">Phone</label>
|
||||
<InputText
|
||||
id="phone-number"
|
||||
v-model="localFormData.phoneNumber"
|
||||
:disabled="isSubmitting"
|
||||
placeholder="(555) 123-4567"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="email">Email</label>
|
||||
<InputText
|
||||
id="email"
|
||||
v-model="localFormData.email"
|
||||
:disabled="isSubmitting"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed, onMounted } from "vue";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Select from "primevue/select";
|
||||
import ToggleSwitch from "primevue/toggleswitch";
|
||||
import Checkbox from "primevue/checkbox";
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEditMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isNewClientLocked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
availableContacts: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData", "newContactToggle"]);
|
||||
|
||||
const localFormData = computed({
|
||||
get: () => props.formData,
|
||||
set: (value) => emit("update:formData", value),
|
||||
});
|
||||
|
||||
const isNewContact = ref(false);
|
||||
const selectedContact = ref(null);
|
||||
const sameAsClientName = ref(false);
|
||||
|
||||
// Compute contact options from available contacts
|
||||
const contactOptions = computed(() => {
|
||||
if (!props.availableContacts || props.availableContacts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return props.availableContacts.map((contact) => ({
|
||||
label: `${contact.firstName} ${contact.lastName}`,
|
||||
value: contact,
|
||||
}));
|
||||
});
|
||||
|
||||
// Ensure New Contact is ON and locked when New Client is ON
|
||||
watch(
|
||||
() => props.isNewClientLocked,
|
||||
(locked) => {
|
||||
if (locked) {
|
||||
isNewContact.value = true;
|
||||
} else {
|
||||
isNewContact.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// On mount, set isNewContact to true if isNewClientLocked is true
|
||||
onMounted(() => {
|
||||
if (props.isNewClientLocked) {
|
||||
isNewContact.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-check "Same as Client Name" when customer type is Individual
|
||||
watch(
|
||||
() => props.formData.customerType,
|
||||
(customerType) => {
|
||||
if (customerType === "Individual" && props.isNewClientLocked && !props.isEditMode) {
|
||||
sameAsClientName.value = true;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Reset "Same as Client Name" when editing or using existing customer
|
||||
watch([() => props.isEditMode, () => props.isNewClientLocked], ([editMode, newClientLocked]) => {
|
||||
if (editMode || !newClientLocked) {
|
||||
sameAsClientName.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-fill name fields when "Same as Client Name" is checked
|
||||
watch(sameAsClientName, (checked) => {
|
||||
if (checked && props.formData.customerName) {
|
||||
const nameParts = props.formData.customerName.trim().split(" ");
|
||||
if (nameParts.length === 1) {
|
||||
localFormData.value.firstName = nameParts[0];
|
||||
localFormData.value.lastName = "";
|
||||
} else if (nameParts.length >= 2) {
|
||||
localFormData.value.firstName = nameParts[0];
|
||||
localFormData.value.lastName = nameParts.slice(1).join(" ");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for customer name changes when "Same as Client Name" is checked
|
||||
watch(
|
||||
() => props.formData.customerName,
|
||||
(newName) => {
|
||||
if (sameAsClientName.value && newName) {
|
||||
const nameParts = newName.trim().split(" ");
|
||||
if (nameParts.length === 1) {
|
||||
localFormData.value.firstName = nameParts[0];
|
||||
localFormData.value.lastName = "";
|
||||
} else if (nameParts.length >= 2) {
|
||||
localFormData.value.firstName = nameParts[0];
|
||||
localFormData.value.lastName = nameParts.slice(1).join(" ");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Watch for toggle changes
|
||||
watch(isNewContact, (newValue) => {
|
||||
if (newValue) {
|
||||
// Clear contact selection when switching to new contact mode
|
||||
selectedContact.value = null;
|
||||
localFormData.value.firstName = "";
|
||||
localFormData.value.lastName = "";
|
||||
localFormData.value.phoneNumber = "";
|
||||
localFormData.value.email = "";
|
||||
}
|
||||
emit("newContactToggle", newValue);
|
||||
});
|
||||
|
||||
const handleContactSelect = () => {
|
||||
if (selectedContact.value && selectedContact.value.value) {
|
||||
const contact = selectedContact.value.value;
|
||||
localFormData.value.firstName = contact.firstName;
|
||||
localFormData.value.lastName = contact.lastName;
|
||||
localFormData.value.phoneNumber = contact.phone || "";
|
||||
localFormData.value.email = contact.email || "";
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
isNewContact,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toggle-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--red-500);
|
||||
}
|
||||
|
||||
.helper-text {
|
||||
color: var(--text-color-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,172 +1,113 @@
|
||||
<template>
|
||||
<div class="overview-container">
|
||||
<!-- Client Basic Info Card -->
|
||||
<div class="info-card">
|
||||
<div class="card-header">
|
||||
<h3>Client Information</h3>
|
||||
<Button
|
||||
v-if="!isNew && !editMode"
|
||||
@click="toggleEditMode"
|
||||
icon="pi pi-pencil"
|
||||
label="Edit"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
/>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Customer Name:</label>
|
||||
<AutoComplete
|
||||
v-if="isNew || editMode"
|
||||
v-model="formData.customerName"
|
||||
:suggestions="customerSuggestions"
|
||||
@complete="searchCustomers"
|
||||
@item-select="onCustomerSelect"
|
||||
placeholder="Type or select customer name"
|
||||
class="w-full"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
<span v-else>{{ clientData?.customerName || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Customer Type:</label>
|
||||
<Select
|
||||
v-if="isNew || editMode"
|
||||
v-model="formData.customerType"
|
||||
:options="customerTypeOptions"
|
||||
placeholder="Select customer type"
|
||||
class="w-full"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
<span v-else>{{ clientData?.customerType || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="!isNew && !editMode">
|
||||
<label>Customer Group:</label>
|
||||
<span>{{ clientData?.customerGroup || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="!isNew && !editMode">
|
||||
<label>Territory:</label>
|
||||
<span>{{ clientData?.territory || "N/A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Mode (new=true or edit mode) -->
|
||||
<template v-if="isNew || editMode">
|
||||
<ClientInformationForm
|
||||
ref="clientInfoRef"
|
||||
v-model:form-data="formData"
|
||||
:is-submitting="isSubmitting"
|
||||
:is-edit-mode="editMode"
|
||||
@new-client-toggle="handleNewClientToggle"
|
||||
@customer-selected="handleCustomerSelected"
|
||||
/>
|
||||
|
||||
<!-- Address Info Card -->
|
||||
<div class="info-card">
|
||||
<h3>Address Information</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item" v-if="isNew || editMode">
|
||||
<label>Address Line 1 *:</label>
|
||||
<InputText
|
||||
v-model="formData.addressLine1"
|
||||
placeholder="Street address"
|
||||
class="w-full"
|
||||
:disabled="isSubmitting"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item" v-if="isNew || editMode">
|
||||
<label>Address Line 2:</label>
|
||||
<InputText
|
||||
v-model="formData.addressLine2"
|
||||
placeholder="Apt, suite, unit, etc."
|
||||
class="w-full"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item" v-if="isNew || editMode">
|
||||
<label>Zip Code *:</label>
|
||||
<InputText
|
||||
v-model="formData.zipcode"
|
||||
placeholder="12345"
|
||||
@input="handleZipcodeInput"
|
||||
maxlength="5"
|
||||
class="w-full"
|
||||
:disabled="isSubmitting"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item" v-if="isNew || editMode">
|
||||
<label>City *:</label>
|
||||
<InputText
|
||||
v-model="formData.city"
|
||||
placeholder="City"
|
||||
class="w-full"
|
||||
:disabled="isSubmitting || zipcodeLookupDisabled"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item" v-if="isNew || editMode">
|
||||
<label>State *:</label>
|
||||
<InputText
|
||||
v-model="formData.state"
|
||||
placeholder="State"
|
||||
class="w-full"
|
||||
:disabled="isSubmitting || zipcodeLookupDisabled"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- Read-only mode for existing clients -->
|
||||
<div
|
||||
class="info-item full-width"
|
||||
v-if="!isNew && !editMode && selectedAddressData"
|
||||
>
|
||||
<label>Full Address:</label>
|
||||
<span>{{ fullAddress }}</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="!isNew && !editMode && selectedAddressData">
|
||||
<label>City:</label>
|
||||
<span>{{ selectedAddressData.city || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="!isNew && !editMode && selectedAddressData">
|
||||
<label>State:</label>
|
||||
<span>{{ selectedAddressData.state || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="!isNew && !editMode && selectedAddressData">
|
||||
<label>Zip Code:</label>
|
||||
<span>{{ selectedAddressData.pincode || "N/A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ContactInformationForm
|
||||
ref="contactInfoRef"
|
||||
v-model:form-data="formData"
|
||||
:is-submitting="isSubmitting"
|
||||
:is-edit-mode="editMode"
|
||||
:is-new-client-locked="isNewClientMode"
|
||||
:available-contacts="availableContacts"
|
||||
@new-contact-toggle="handleNewContactToggle"
|
||||
/>
|
||||
|
||||
<!-- Contact Info Card (only for new/edit mode) -->
|
||||
<div class="info-card" v-if="isNew || editMode">
|
||||
<h3>Contact Information</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Contact Name *:</label>
|
||||
<AutoComplete
|
||||
v-model="formData.contactName"
|
||||
:suggestions="contactSuggestions"
|
||||
@complete="searchContacts"
|
||||
@item-select="onContactSelect"
|
||||
placeholder="Type or select contact name"
|
||||
class="w-full"
|
||||
:disabled="isSubmitting || !formData.customerName"
|
||||
required
|
||||
<AddressInformationForm
|
||||
v-model:form-data="formData"
|
||||
:is-submitting="isSubmitting"
|
||||
:is-edit-mode="editMode"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Display Mode (existing client view) -->
|
||||
<template v-else>
|
||||
<!-- Client Basic Info Card -->
|
||||
<div class="info-card">
|
||||
<div class="card-header">
|
||||
<h3>Client Information</h3>
|
||||
<Button
|
||||
@click="toggleEditMode"
|
||||
icon="pi pi-pencil"
|
||||
label="Edit"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Phone Number:</label>
|
||||
<InputText
|
||||
v-model="formData.phoneNumber"
|
||||
placeholder="(555) 123-4567"
|
||||
class="w-full"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Email:</label>
|
||||
<InputText
|
||||
v-model="formData.email"
|
||||
placeholder="email@example.com"
|
||||
type="email"
|
||||
class="w-full"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Customer Name:</label>
|
||||
<span>{{ clientData?.customerName || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Customer Type:</label>
|
||||
<span>{{ clientData?.customerType || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Customer Group:</label>
|
||||
<span>{{ clientData?.customerGroup || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Territory:</label>
|
||||
<span>{{ clientData?.territory || "N/A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Info Card -->
|
||||
<div class="info-card" v-if="selectedAddressData">
|
||||
<h3>Address Information</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item full-width">
|
||||
<label>Address Title:</label>
|
||||
<span>{{ selectedAddressData.addressTitle || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item full-width">
|
||||
<label>Full Address:</label>
|
||||
<span>{{ fullAddress }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>City:</label>
|
||||
<span>{{ selectedAddressData.city || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>State:</label>
|
||||
<span>{{ selectedAddressData.state || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Zip Code:</label>
|
||||
<span>{{ selectedAddressData.pincode || "N/A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info Card -->
|
||||
<div class="info-card" v-if="selectedAddressData">
|
||||
<h3>Contact Information</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Contact Name:</label>
|
||||
<span>{{ contactFullName }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Phone:</label>
|
||||
<span>{{ selectedAddressData.phone || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Email:</label>
|
||||
<span>{{ selectedAddressData.emailId || "N/A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Status Cards (only for existing clients) -->
|
||||
<div class="status-cards" v-if="!isNew && !editMode && selectedAddressData">
|
||||
@ -204,13 +145,13 @@
|
||||
<div class="form-actions" v-if="isNew || editMode">
|
||||
<Button
|
||||
@click="handleCancel"
|
||||
label="Cancel"
|
||||
:label="editMode ? 'Cancel' : 'Clear'"
|
||||
severity="secondary"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
<Button
|
||||
@click="handleSave"
|
||||
:label="isNew ? 'Create Client' : 'Save Changes'"
|
||||
:label="isNew ? 'Create' : 'Update'"
|
||||
:loading="isSubmitting"
|
||||
:disabled="!isFormValid"
|
||||
/>
|
||||
@ -262,11 +203,11 @@
|
||||
import { computed, ref, watch, onMounted } from "vue";
|
||||
import Badge from "primevue/badge";
|
||||
import Button from "primevue/button";
|
||||
import InputText from "primevue/inputtext";
|
||||
import AutoComplete from "primevue/autocomplete";
|
||||
import Select from "primevue/select";
|
||||
import Dialog from "primevue/dialog";
|
||||
import LeafletMap from "../common/LeafletMap.vue";
|
||||
import ClientInformationForm from "./ClientInformationForm.vue";
|
||||
import ContactInformationForm from "./ContactInformationForm.vue";
|
||||
import AddressInformationForm from "./AddressInformationForm.vue";
|
||||
import DataUtils from "../../utils";
|
||||
import Api from "../../api";
|
||||
import { useRouter } from "vue-router";
|
||||
@ -290,46 +231,42 @@ const props = defineProps({
|
||||
const router = useRouter();
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
// Refs for child components
|
||||
const clientInfoRef = ref(null);
|
||||
const contactInfoRef = ref(null);
|
||||
|
||||
// Form state
|
||||
const editMode = ref(false);
|
||||
const showEditConfirmDialog = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
const zipcodeLookupDisabled = ref(true);
|
||||
const isNewClientMode = ref(false);
|
||||
const availableContacts = ref([]);
|
||||
|
||||
// Form data
|
||||
const formData = ref({
|
||||
customerName: "",
|
||||
customerType: "",
|
||||
addressTitle: "",
|
||||
addressLine1: "",
|
||||
addressLine2: "",
|
||||
zipcode: "",
|
||||
pincode: "",
|
||||
city: "",
|
||||
state: "",
|
||||
contactName: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
});
|
||||
|
||||
// Autocomplete data
|
||||
const customerSuggestions = ref([]);
|
||||
const contactSuggestions = ref([]);
|
||||
const selectedCustomerData = ref(null);
|
||||
|
||||
// Options
|
||||
const customerTypeOptions = ref(["Company", "Individual"]);
|
||||
|
||||
// Initialize form data when component mounts
|
||||
onMounted(() => {
|
||||
if (props.isNew) {
|
||||
// Initialize empty form for new client
|
||||
resetForm();
|
||||
console.log("Mounted in new client mode - initialized empty form");
|
||||
} else if (props.clientData && Object.keys(props.clientData).length > 0) {
|
||||
// Populate form with existing client data
|
||||
populateFormFromClientData();
|
||||
console.log("Mounted with existing client data - populated form");
|
||||
} else {
|
||||
// Default to empty form if no client data
|
||||
resetForm();
|
||||
console.log("Mounted with no client data - initialized empty form");
|
||||
}
|
||||
@ -340,32 +277,27 @@ watch(
|
||||
() => props.clientData,
|
||||
(newData) => {
|
||||
if (props.isNew) {
|
||||
// Always keep form empty for new clients, regardless of clientData
|
||||
resetForm();
|
||||
} else if (newData && Object.keys(newData).length > 0) {
|
||||
populateFormFromClientData();
|
||||
} else {
|
||||
// No client data, reset form
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// Watch for isNew prop changes to reset form when switching to new client mode
|
||||
// Watch for isNew prop changes
|
||||
watch(
|
||||
() => props.isNew,
|
||||
(isNewValue) => {
|
||||
if (isNewValue) {
|
||||
// Reset form when switching to new client mode
|
||||
resetForm();
|
||||
editMode.value = false;
|
||||
console.log("Switched to new client mode - reset form data");
|
||||
} else if (props.clientData && Object.keys(props.clientData).length > 0) {
|
||||
// Populate form when switching back to existing client
|
||||
populateFormFromClientData();
|
||||
} else {
|
||||
// No client data, reset form
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
@ -386,15 +318,11 @@ const selectedAddressData = computed(() => {
|
||||
// Get coordinates from the selected address
|
||||
const latitude = computed(() => {
|
||||
if (!selectedAddressData.value) return null;
|
||||
|
||||
// Check custom fields first, then fallback to regular fields
|
||||
return selectedAddressData.value.customLatitude || selectedAddressData.value.latitude || null;
|
||||
});
|
||||
|
||||
const longitude = computed(() => {
|
||||
if (!selectedAddressData.value) return null;
|
||||
|
||||
// Check custom fields first, then fallback to regular fields
|
||||
return (
|
||||
selectedAddressData.value.customLongitude || selectedAddressData.value.longitude || null
|
||||
);
|
||||
@ -406,16 +334,36 @@ const fullAddress = computed(() => {
|
||||
return DataUtils.calculateFullAddress(selectedAddressData.value);
|
||||
});
|
||||
|
||||
// Calculate contact full name
|
||||
const contactFullName = computed(() => {
|
||||
if (!selectedAddressData.value) return "N/A";
|
||||
const firstName = selectedAddressData.value.customContactFirstName || "";
|
||||
const lastName = selectedAddressData.value.customContactLastName || "";
|
||||
return `${firstName} ${lastName}`.trim() || "N/A";
|
||||
});
|
||||
|
||||
// Form validation
|
||||
const isFormValid = computed(() => {
|
||||
const hasCustomerName = formData.value.customerName?.trim();
|
||||
const hasCustomerType = formData.value.customerType?.trim();
|
||||
const hasAddressTitle = formData.value.addressTitle?.trim();
|
||||
const hasAddressLine1 = formData.value.addressLine1?.trim();
|
||||
const hasPincode = formData.value.pincode?.trim();
|
||||
const hasCity = formData.value.city?.trim();
|
||||
const hasState = formData.value.state?.trim();
|
||||
const hasFirstName = formData.value.firstName?.trim();
|
||||
const hasLastName = formData.value.lastName?.trim();
|
||||
|
||||
return (
|
||||
formData.value.customerName &&
|
||||
formData.value.customerType &&
|
||||
formData.value.addressLine1 &&
|
||||
formData.value.zipcode &&
|
||||
formData.value.city &&
|
||||
formData.value.state &&
|
||||
formData.value.contactName
|
||||
hasCustomerName &&
|
||||
hasCustomerType &&
|
||||
hasAddressTitle &&
|
||||
hasAddressLine1 &&
|
||||
hasPincode &&
|
||||
hasCity &&
|
||||
hasState &&
|
||||
hasFirstName &&
|
||||
hasLastName
|
||||
);
|
||||
});
|
||||
|
||||
@ -425,7 +373,7 @@ const getStatusSeverity = (status) => {
|
||||
case "Not Started":
|
||||
return "secondary";
|
||||
case "In Progress":
|
||||
return "warn"; // Use 'warn' instead of 'warning' for PrimeVue Badge
|
||||
return "warn";
|
||||
case "Completed":
|
||||
return "success";
|
||||
default:
|
||||
@ -438,18 +386,20 @@ const resetForm = () => {
|
||||
formData.value = {
|
||||
customerName: "",
|
||||
customerType: "",
|
||||
addressTitle: "",
|
||||
addressLine1: "",
|
||||
addressLine2: "",
|
||||
zipcode: "",
|
||||
pincode: "",
|
||||
city: "",
|
||||
state: "",
|
||||
contactName: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
};
|
||||
selectedCustomerData.value = null;
|
||||
zipcodeLookupDisabled.value = false; // Allow manual entry for new clients
|
||||
editMode.value = false; // Ensure edit mode is off
|
||||
availableContacts.value = [];
|
||||
isNewClientMode.value = false;
|
||||
editMode.value = false;
|
||||
console.log("Form reset - all fields cleared");
|
||||
};
|
||||
|
||||
@ -459,15 +409,46 @@ const populateFormFromClientData = () => {
|
||||
formData.value = {
|
||||
customerName: props.clientData.customerName || "",
|
||||
customerType: props.clientData.customerType || "",
|
||||
addressTitle: selectedAddressData.value.addressTitle || "",
|
||||
addressLine1: selectedAddressData.value.addressLine1 || "",
|
||||
addressLine2: selectedAddressData.value.addressLine2 || "",
|
||||
zipcode: selectedAddressData.value.pincode || "",
|
||||
pincode: selectedAddressData.value.pincode || "",
|
||||
city: selectedAddressData.value.city || "",
|
||||
state: selectedAddressData.value.state || "",
|
||||
contactName: selectedAddressData.value.customContactName || "",
|
||||
firstName: selectedAddressData.value.customContactFirstName || "",
|
||||
lastName: selectedAddressData.value.customContactLastName || "",
|
||||
phoneNumber: selectedAddressData.value.phone || "",
|
||||
email: selectedAddressData.value.emailId || "",
|
||||
};
|
||||
|
||||
// Populate available contacts if any
|
||||
if (selectedAddressData.value.contacts && selectedAddressData.value.contacts.length > 0) {
|
||||
availableContacts.value = selectedAddressData.value.contacts;
|
||||
}
|
||||
};
|
||||
|
||||
// Event handlers
|
||||
const handleNewClientToggle = (isNewClient) => {
|
||||
isNewClientMode.value = isNewClient;
|
||||
if (isNewClient) {
|
||||
// Reset form when toggling to new client
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomerSelected = (clientData) => {
|
||||
// When a customer is selected, populate available contacts from the first address
|
||||
if (clientData.addresses && clientData.addresses.length > 0) {
|
||||
availableContacts.value = clientData.addresses[0].contacts || [];
|
||||
} else {
|
||||
availableContacts.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewContactToggle = (isNewContact) => {
|
||||
if (!isNewContact && availableContacts.value.length === 0) {
|
||||
notificationStore.addWarning("No contacts available for this customer.");
|
||||
}
|
||||
};
|
||||
|
||||
// Edit mode methods
|
||||
@ -478,95 +459,7 @@ const toggleEditMode = () => {
|
||||
const confirmEdit = () => {
|
||||
showEditConfirmDialog.value = false;
|
||||
editMode.value = true;
|
||||
populateFormFromClientData(); // Refresh form with current data
|
||||
};
|
||||
|
||||
// Zipcode handling
|
||||
const handleZipcodeInput = async (event) => {
|
||||
const input = event.target.value;
|
||||
|
||||
// Only allow digits
|
||||
const digitsOnly = input.replace(/\D/g, "");
|
||||
|
||||
// Limit to 5 digits
|
||||
if (digitsOnly.length > 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
formData.value.zipcode = digitsOnly;
|
||||
|
||||
// Fetch city/state when 5 digits entered
|
||||
if (digitsOnly.length === 5) {
|
||||
try {
|
||||
const places = await Api.getCityStateByZip(digitsOnly);
|
||||
if (places && places.length > 0) {
|
||||
// Auto-populate city and state
|
||||
formData.value.city = places[0]["place name"];
|
||||
formData.value.state = places[0]["state abbreviation"];
|
||||
zipcodeLookupDisabled.value = true;
|
||||
notificationStore.addSuccess(
|
||||
`Found: ${places[0]["place name"]}, ${places[0]["state abbreviation"]}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Enable manual entry if lookup fails
|
||||
zipcodeLookupDisabled.value = false;
|
||||
notificationStore.addWarning(
|
||||
"Could not find city/state for this zip code. Please enter manually.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Reset city/state if zipcode is incomplete
|
||||
if (zipcodeLookupDisabled.value) {
|
||||
formData.value.city = "";
|
||||
formData.value.state = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Customer search
|
||||
const searchCustomers = async (event) => {
|
||||
try {
|
||||
const customers = await Api.getCustomerNames("all");
|
||||
customerSuggestions.value = customers.filter((name) =>
|
||||
name.toLowerCase().includes(event.query.toLowerCase()),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error searching customers:", error);
|
||||
customerSuggestions.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const onCustomerSelect = (event) => {
|
||||
// Store selected customer for contact lookup
|
||||
selectedCustomerData.value = event.value;
|
||||
// Reset contact data when customer changes
|
||||
formData.value.contactName = "";
|
||||
formData.value.phoneNumber = "";
|
||||
formData.value.email = "";
|
||||
};
|
||||
|
||||
// Contact search
|
||||
const searchContacts = async (event) => {
|
||||
if (!selectedCustomerData.value) {
|
||||
contactSuggestions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Implement contact search API method
|
||||
// For now, just allow typing
|
||||
contactSuggestions.value = [event.query];
|
||||
} catch (error) {
|
||||
console.error("Error searching contacts:", error);
|
||||
contactSuggestions.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const onContactSelect = (event) => {
|
||||
// TODO: Auto-populate phone and email from selected contact
|
||||
// For now, just set the name
|
||||
formData.value.contactName = event.value;
|
||||
populateFormFromClientData();
|
||||
};
|
||||
|
||||
// Save/Cancel actions
|
||||
@ -579,33 +472,37 @@ const handleSave = async () => {
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
// Prepare client data for upsert
|
||||
const clientData = {
|
||||
customerName: formData.value.customerName,
|
||||
customerType: formData.value.customerType,
|
||||
addressTitle: formData.value.addressTitle,
|
||||
addressLine1: formData.value.addressLine1,
|
||||
addressLine2: formData.value.addressLine2,
|
||||
pincode: formData.value.pincode,
|
||||
city: formData.value.city,
|
||||
state: formData.value.state,
|
||||
firstName: formData.value.firstName,
|
||||
lastName: formData.value.lastName,
|
||||
phoneNumber: formData.value.phoneNumber,
|
||||
email: formData.value.email,
|
||||
};
|
||||
|
||||
console.log("Upserting client with data:", clientData);
|
||||
|
||||
// Call the upsert API
|
||||
const result = await Api.createClient(clientData);
|
||||
|
||||
// Calculate full address for redirect
|
||||
const fullAddressParts = [formData.value.addressLine1];
|
||||
if (formData.value.addressLine2?.trim()) {
|
||||
fullAddressParts.push(formData.value.addressLine2);
|
||||
}
|
||||
fullAddressParts.push(`${formData.value.city}, ${formData.value.state}`);
|
||||
fullAddressParts.push(formData.value.pincode);
|
||||
const fullAddress = fullAddressParts.join(" ");
|
||||
|
||||
if (props.isNew) {
|
||||
// Create new client
|
||||
const clientData = {
|
||||
customerName: formData.value.customerName,
|
||||
customerType: formData.value.customerType,
|
||||
addressLine1: formData.value.addressLine1,
|
||||
addressLine2: formData.value.addressLine2,
|
||||
zipcode: formData.value.zipcode,
|
||||
city: formData.value.city,
|
||||
state: formData.value.state,
|
||||
contactName: formData.value.contactName,
|
||||
phoneNumber: formData.value.phoneNumber,
|
||||
email: formData.value.email,
|
||||
};
|
||||
|
||||
// TODO: Implement API call to create client
|
||||
console.log("Would create client with data:", clientData);
|
||||
|
||||
// For now, just show success and redirect
|
||||
const fullAddress = DataUtils.calculateFullAddress({
|
||||
addressLine1: formData.value.addressLine1,
|
||||
addressLine2: formData.value.addressLine2,
|
||||
city: formData.value.city,
|
||||
state: formData.value.state,
|
||||
pincode: formData.value.zipcode,
|
||||
});
|
||||
|
||||
notificationStore.addSuccess(
|
||||
`Client ${formData.value.customerName} created successfully!`,
|
||||
);
|
||||
@ -619,12 +516,11 @@ const handleSave = async () => {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Update existing client (edit mode)
|
||||
// TODO: Implement API call to update client
|
||||
console.log("Would update client with data:", formData.value);
|
||||
|
||||
notificationStore.addSuccess("Client updated successfully!");
|
||||
editMode.value = false;
|
||||
|
||||
// Reload the client data
|
||||
// Note: Parent component should handle reloading
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving client:", error);
|
||||
@ -636,8 +532,8 @@ const handleSave = async () => {
|
||||
|
||||
const handleCancel = () => {
|
||||
if (props.isNew) {
|
||||
// Go back for new client
|
||||
router.back();
|
||||
// Clear form for new client
|
||||
resetForm();
|
||||
} else {
|
||||
// Exit edit mode and restore original data
|
||||
editMode.value = false;
|
||||
|
||||
@ -7,6 +7,7 @@ import { globalSettings } from "./globalSettings";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
// Vuetify
|
||||
import "@primeuix/themes/aura";
|
||||
import "vuetify/styles";
|
||||
import { createVuetify } from "vuetify";
|
||||
import * as components from "vuetify/components";
|
||||
|
||||
@ -55,3 +55,19 @@
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* Fix ToggleSwitch z-index so slider is visible but input receives clicks */
|
||||
.p-toggleswitch {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.p-toggleswitch-slider {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.p-toggleswitch-input {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user