get create client working

This commit is contained in:
Casey Wittrock 2025-11-25 06:22:44 -06:00
parent 4a3576168a
commit cb33d0c3b3
11 changed files with 1179 additions and 336 deletions

View File

@ -96,6 +96,8 @@ def get_client(client_name):
customer = frappe.get_doc("Customer", client_name) customer = frappe.get_doc("Customer", client_name)
clientData = {**clientData, **customer.as_dict()} clientData = {**clientData, **customer.as_dict()}
addresses = frappe.db.get_all("Address", fields=["*"], filters={"custom_customer_to_bill": client_name}) 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 []: for address in addresses if addresses else []:
addressData = {"jobs": []} addressData = {"jobs": []}
addressData = {**addressData, **address} addressData = {**addressData, **address}
@ -263,7 +265,7 @@ def upsert_client(data):
"customer_type": data.get("customer_type") "customer_type": data.get("customer_type")
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)
else: else:
customer_doc = frappe.get_doc("Customer", customer) customer_doc = frappe.get_doc("Customer", data.get("customer_name"))
print("Customer:", customer_doc.as_dict()) print("Customer:", customer_doc.as_dict())
@ -281,6 +283,7 @@ def upsert_client(data):
# Create address # Create address
address_doc = frappe.get_doc({ address_doc = frappe.get_doc({
"doctype": "Address", "doctype": "Address",
"address_title": data.get("address_title"),
"address_line1": data.get("address_line1"), "address_line1": data.get("address_line1"),
"address_line2": data.get("address_line2"), "address_line2": data.get("address_line2"),
"city": data.get("city"), "city": data.get("city"),
@ -297,12 +300,43 @@ def upsert_client(data):
} }
address_doc.append("links", link) address_doc.append("links", link)
address_doc.save(ignore_permissions=True) 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({ return build_success_response({
"customer": customer_doc.as_dict(), "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: except frappe.ValidationError as ve:
return build_error_response(str(ve), 400) return build_error_response(str(ve), 400)
except Exception as e: except Exception as e:
return build_error_response(str(e), 500) 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)

View File

@ -1,6 +1,7 @@
import frappe import frappe
import requests import requests
from urllib.parse import urlparse from urllib.parse import urlparse
from custom_ui.db_utils import build_success_response, build_error_response
import logging import logging
allowed_hosts = ["api.zippopotam.us", "nominatim.openstreetmap.org"] # Update this list with trusted domains as needed 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() resp.raise_for_status()
try: try:
return resp.json() return build_success_response(resp.json())
except ValueError: except ValueError:
return {"text": resp.text} return build_success_response({"text": resp.text})
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
frappe.log_error(message=str(e), title="Proxy Request Failed") frappe.log_error(message=str(e), title="Proxy Request Failed")
frappe.throw("Failed to fetch data from external API.") frappe.throw("Failed to fetch data from external API.")

View File

@ -16,6 +16,7 @@
"frappe-ui": "^0.1.205", "frappe-ui": "^0.1.205",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"primeicons": "^7.0.0",
"primevue": "^4.4.1", "primevue": "^4.4.1",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-chartjs": "^5.3.3", "vue-chartjs": "^5.3.3",
@ -3634,6 +3635,12 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "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": { "node_modules/primevue": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.4.1.tgz", "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.4.1.tgz",

View File

@ -17,6 +17,7 @@
"frappe-ui": "^0.1.205", "frappe-ui": "^0.1.205",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"primeicons": "^7.0.0",
"primevue": "^4.4.1", "primevue": "^4.4.1",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-chartjs": "^5.3.3", "vue-chartjs": "^5.3.3",

View File

@ -328,8 +328,12 @@ class Api {
return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { type }); return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { type });
} }
static async getClientNames(type) { static async getClientNames(clientName) {
return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { type }); 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() { static async getCompanyNames() {
@ -342,8 +346,7 @@ class Api {
// Create methods // Create methods
static async createClient(clientData) { static async createClient(clientData) {
const payload = DataUtils.toSnakeCaseObject(clientData); const result = await this.request(FRAPPE_UPSERT_CLIENT_METHOD, { data: clientData });
const result = await this.request(FRAPPE_UPSERT_CLIENT_METHOD, { data: payload });
console.log("DEBUG: API - Created/Updated Client: ", result); console.log("DEBUG: API - Created/Updated Client: ", result);
return result; return result;
} }
@ -391,7 +394,10 @@ class Api {
if (!places || places.length === 0) { if (!places || places.length === 0) {
throw new Error(`No location data found for zip code ${zipcode}`); 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) { static async getGeocode(address) {

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -1,11 +1,40 @@
<template> <template>
<div class="overview-container"> <div class="overview-container">
<!-- 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"
/>
<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"
/>
<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 --> <!-- Client Basic Info Card -->
<div class="info-card"> <div class="info-card">
<div class="card-header"> <div class="card-header">
<h3>Client Information</h3> <h3>Client Information</h3>
<Button <Button
v-if="!isNew && !editMode"
@click="toggleEditMode" @click="toggleEditMode"
icon="pi pi-pencil" icon="pi pi-pencil"
label="Edit" label="Edit"
@ -16,35 +45,17 @@
<div class="info-grid"> <div class="info-grid">
<div class="info-item"> <div class="info-item">
<label>Customer Name:</label> <label>Customer Name:</label>
<AutoComplete <span>{{ clientData?.customerName || "N/A" }}</span>
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>
<div class="info-item"> <div class="info-item">
<label>Customer Type:</label> <label>Customer Type:</label>
<Select <span>{{ clientData?.customerType || "N/A" }}</span>
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>
<div class="info-item" v-if="!isNew && !editMode"> <div class="info-item">
<label>Customer Group:</label> <label>Customer Group:</label>
<span>{{ clientData?.customerGroup || "N/A" }}</span> <span>{{ clientData?.customerGroup || "N/A" }}</span>
</div> </div>
<div class="info-item" v-if="!isNew && !editMode"> <div class="info-item">
<label>Territory:</label> <label>Territory:</label>
<span>{{ clientData?.territory || "N/A" }}</span> <span>{{ clientData?.territory || "N/A" }}</span>
</div> </div>
@ -52,121 +63,51 @@
</div> </div>
<!-- Address Info Card --> <!-- Address Info Card -->
<div class="info-card"> <div class="info-card" v-if="selectedAddressData">
<h3>Address Information</h3> <h3>Address Information</h3>
<div class="info-grid"> <div class="info-grid">
<div class="info-item" v-if="isNew || editMode"> <div class="info-item full-width">
<label>Address Line 1 *:</label> <label>Address Title:</label>
<InputText <span>{{ selectedAddressData.addressTitle || "N/A" }}</span>
v-model="formData.addressLine1"
placeholder="Street address"
class="w-full"
:disabled="isSubmitting"
required
/>
</div> </div>
<div class="info-item" v-if="isNew || editMode"> <div class="info-item full-width">
<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> <label>Full Address:</label>
<span>{{ fullAddress }}</span> <span>{{ fullAddress }}</span>
</div> </div>
<div class="info-item" v-if="!isNew && !editMode && selectedAddressData"> <div class="info-item">
<label>City:</label> <label>City:</label>
<span>{{ selectedAddressData.city || "N/A" }}</span> <span>{{ selectedAddressData.city || "N/A" }}</span>
</div> </div>
<div class="info-item" v-if="!isNew && !editMode && selectedAddressData"> <div class="info-item">
<label>State:</label> <label>State:</label>
<span>{{ selectedAddressData.state || "N/A" }}</span> <span>{{ selectedAddressData.state || "N/A" }}</span>
</div> </div>
<div class="info-item" v-if="!isNew && !editMode && selectedAddressData"> <div class="info-item">
<label>Zip Code:</label> <label>Zip Code:</label>
<span>{{ selectedAddressData.pincode || "N/A" }}</span> <span>{{ selectedAddressData.pincode || "N/A" }}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Contact Info Card (only for new/edit mode) --> <!-- Contact Info Card -->
<div class="info-card" v-if="isNew || editMode"> <div class="info-card" v-if="selectedAddressData">
<h3>Contact Information</h3> <h3>Contact Information</h3>
<div class="info-grid"> <div class="info-grid">
<div class="info-item"> <div class="info-item">
<label>Contact Name *:</label> <label>Contact Name:</label>
<AutoComplete <span>{{ contactFullName }}</span>
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
/>
</div> </div>
<div class="info-item"> <div class="info-item">
<label>Phone Number:</label> <label>Phone:</label>
<InputText <span>{{ selectedAddressData.phone || "N/A" }}</span>
v-model="formData.phoneNumber"
placeholder="(555) 123-4567"
class="w-full"
:disabled="isSubmitting"
/>
</div> </div>
<div class="info-item"> <div class="info-item">
<label>Email:</label> <label>Email:</label>
<InputText <span>{{ selectedAddressData.emailId || "N/A" }}</span>
v-model="formData.email"
placeholder="email@example.com"
type="email"
class="w-full"
:disabled="isSubmitting"
/>
</div> </div>
</div> </div>
</div> </div>
</template>
<!-- Status Cards (only for existing clients) --> <!-- Status Cards (only for existing clients) -->
<div class="status-cards" v-if="!isNew && !editMode && selectedAddressData"> <div class="status-cards" v-if="!isNew && !editMode && selectedAddressData">
@ -204,13 +145,13 @@
<div class="form-actions" v-if="isNew || editMode"> <div class="form-actions" v-if="isNew || editMode">
<Button <Button
@click="handleCancel" @click="handleCancel"
label="Cancel" :label="editMode ? 'Cancel' : 'Clear'"
severity="secondary" severity="secondary"
:disabled="isSubmitting" :disabled="isSubmitting"
/> />
<Button <Button
@click="handleSave" @click="handleSave"
:label="isNew ? 'Create Client' : 'Save Changes'" :label="isNew ? 'Create' : 'Update'"
:loading="isSubmitting" :loading="isSubmitting"
:disabled="!isFormValid" :disabled="!isFormValid"
/> />
@ -262,11 +203,11 @@
import { computed, ref, watch, onMounted } from "vue"; import { computed, ref, watch, onMounted } from "vue";
import Badge from "primevue/badge"; import Badge from "primevue/badge";
import Button from "primevue/button"; 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 Dialog from "primevue/dialog";
import LeafletMap from "../common/LeafletMap.vue"; 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 DataUtils from "../../utils";
import Api from "../../api"; import Api from "../../api";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
@ -290,46 +231,42 @@ const props = defineProps({
const router = useRouter(); const router = useRouter();
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
// Refs for child components
const clientInfoRef = ref(null);
const contactInfoRef = ref(null);
// Form state // Form state
const editMode = ref(false); const editMode = ref(false);
const showEditConfirmDialog = ref(false); const showEditConfirmDialog = ref(false);
const isSubmitting = ref(false); const isSubmitting = ref(false);
const zipcodeLookupDisabled = ref(true); const isNewClientMode = ref(false);
const availableContacts = ref([]);
// Form data // Form data
const formData = ref({ const formData = ref({
customerName: "", customerName: "",
customerType: "", customerType: "",
addressTitle: "",
addressLine1: "", addressLine1: "",
addressLine2: "", addressLine2: "",
zipcode: "", pincode: "",
city: "", city: "",
state: "", state: "",
contactName: "", firstName: "",
lastName: "",
phoneNumber: "", phoneNumber: "",
email: "", 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 // Initialize form data when component mounts
onMounted(() => { onMounted(() => {
if (props.isNew) { if (props.isNew) {
// Initialize empty form for new client
resetForm(); resetForm();
console.log("Mounted in new client mode - initialized empty form"); console.log("Mounted in new client mode - initialized empty form");
} else if (props.clientData && Object.keys(props.clientData).length > 0) { } else if (props.clientData && Object.keys(props.clientData).length > 0) {
// Populate form with existing client data
populateFormFromClientData(); populateFormFromClientData();
console.log("Mounted with existing client data - populated form"); console.log("Mounted with existing client data - populated form");
} else { } else {
// Default to empty form if no client data
resetForm(); resetForm();
console.log("Mounted with no client data - initialized empty form"); console.log("Mounted with no client data - initialized empty form");
} }
@ -340,32 +277,27 @@ watch(
() => props.clientData, () => props.clientData,
(newData) => { (newData) => {
if (props.isNew) { if (props.isNew) {
// Always keep form empty for new clients, regardless of clientData
resetForm(); resetForm();
} else if (newData && Object.keys(newData).length > 0) { } else if (newData && Object.keys(newData).length > 0) {
populateFormFromClientData(); populateFormFromClientData();
} else { } else {
// No client data, reset form
resetForm(); resetForm();
} }
}, },
{ deep: true }, { deep: true },
); );
// Watch for isNew prop changes to reset form when switching to new client mode // Watch for isNew prop changes
watch( watch(
() => props.isNew, () => props.isNew,
(isNewValue) => { (isNewValue) => {
if (isNewValue) { if (isNewValue) {
// Reset form when switching to new client mode
resetForm(); resetForm();
editMode.value = false; editMode.value = false;
console.log("Switched to new client mode - reset form data"); console.log("Switched to new client mode - reset form data");
} else if (props.clientData && Object.keys(props.clientData).length > 0) { } else if (props.clientData && Object.keys(props.clientData).length > 0) {
// Populate form when switching back to existing client
populateFormFromClientData(); populateFormFromClientData();
} else { } else {
// No client data, reset form
resetForm(); resetForm();
} }
}, },
@ -386,15 +318,11 @@ const selectedAddressData = computed(() => {
// Get coordinates from the selected address // Get coordinates from the selected address
const latitude = computed(() => { const latitude = computed(() => {
if (!selectedAddressData.value) return null; if (!selectedAddressData.value) return null;
// Check custom fields first, then fallback to regular fields
return selectedAddressData.value.customLatitude || selectedAddressData.value.latitude || null; return selectedAddressData.value.customLatitude || selectedAddressData.value.latitude || null;
}); });
const longitude = computed(() => { const longitude = computed(() => {
if (!selectedAddressData.value) return null; if (!selectedAddressData.value) return null;
// Check custom fields first, then fallback to regular fields
return ( return (
selectedAddressData.value.customLongitude || selectedAddressData.value.longitude || null selectedAddressData.value.customLongitude || selectedAddressData.value.longitude || null
); );
@ -406,16 +334,36 @@ const fullAddress = computed(() => {
return DataUtils.calculateFullAddress(selectedAddressData.value); 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 // Form validation
const isFormValid = computed(() => { 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 ( return (
formData.value.customerName && hasCustomerName &&
formData.value.customerType && hasCustomerType &&
formData.value.addressLine1 && hasAddressTitle &&
formData.value.zipcode && hasAddressLine1 &&
formData.value.city && hasPincode &&
formData.value.state && hasCity &&
formData.value.contactName hasState &&
hasFirstName &&
hasLastName
); );
}); });
@ -425,7 +373,7 @@ const getStatusSeverity = (status) => {
case "Not Started": case "Not Started":
return "secondary"; return "secondary";
case "In Progress": case "In Progress":
return "warn"; // Use 'warn' instead of 'warning' for PrimeVue Badge return "warn";
case "Completed": case "Completed":
return "success"; return "success";
default: default:
@ -438,18 +386,20 @@ const resetForm = () => {
formData.value = { formData.value = {
customerName: "", customerName: "",
customerType: "", customerType: "",
addressTitle: "",
addressLine1: "", addressLine1: "",
addressLine2: "", addressLine2: "",
zipcode: "", pincode: "",
city: "", city: "",
state: "", state: "",
contactName: "", firstName: "",
lastName: "",
phoneNumber: "", phoneNumber: "",
email: "", email: "",
}; };
selectedCustomerData.value = null; availableContacts.value = [];
zipcodeLookupDisabled.value = false; // Allow manual entry for new clients isNewClientMode.value = false;
editMode.value = false; // Ensure edit mode is off editMode.value = false;
console.log("Form reset - all fields cleared"); console.log("Form reset - all fields cleared");
}; };
@ -459,15 +409,46 @@ const populateFormFromClientData = () => {
formData.value = { formData.value = {
customerName: props.clientData.customerName || "", customerName: props.clientData.customerName || "",
customerType: props.clientData.customerType || "", customerType: props.clientData.customerType || "",
addressTitle: selectedAddressData.value.addressTitle || "",
addressLine1: selectedAddressData.value.addressLine1 || "", addressLine1: selectedAddressData.value.addressLine1 || "",
addressLine2: selectedAddressData.value.addressLine2 || "", addressLine2: selectedAddressData.value.addressLine2 || "",
zipcode: selectedAddressData.value.pincode || "", pincode: selectedAddressData.value.pincode || "",
city: selectedAddressData.value.city || "", city: selectedAddressData.value.city || "",
state: selectedAddressData.value.state || "", state: selectedAddressData.value.state || "",
contactName: selectedAddressData.value.customContactName || "", firstName: selectedAddressData.value.customContactFirstName || "",
lastName: selectedAddressData.value.customContactLastName || "",
phoneNumber: selectedAddressData.value.phone || "", phoneNumber: selectedAddressData.value.phone || "",
email: selectedAddressData.value.emailId || "", 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 // Edit mode methods
@ -478,95 +459,7 @@ const toggleEditMode = () => {
const confirmEdit = () => { const confirmEdit = () => {
showEditConfirmDialog.value = false; showEditConfirmDialog.value = false;
editMode.value = true; editMode.value = true;
populateFormFromClientData(); // Refresh form with current data populateFormFromClientData();
};
// 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;
}; };
// Save/Cancel actions // Save/Cancel actions
@ -579,33 +472,37 @@ const handleSave = async () => {
isSubmitting.value = true; isSubmitting.value = true;
try { try {
if (props.isNew) { // Prepare client data for upsert
// Create new client
const clientData = { const clientData = {
customerName: formData.value.customerName, customerName: formData.value.customerName,
customerType: formData.value.customerType, customerType: formData.value.customerType,
addressTitle: formData.value.addressTitle,
addressLine1: formData.value.addressLine1, addressLine1: formData.value.addressLine1,
addressLine2: formData.value.addressLine2, addressLine2: formData.value.addressLine2,
zipcode: formData.value.zipcode, pincode: formData.value.pincode,
city: formData.value.city, city: formData.value.city,
state: formData.value.state, state: formData.value.state,
contactName: formData.value.contactName, firstName: formData.value.firstName,
lastName: formData.value.lastName,
phoneNumber: formData.value.phoneNumber, phoneNumber: formData.value.phoneNumber,
email: formData.value.email, email: formData.value.email,
}; };
// TODO: Implement API call to create client console.log("Upserting client with data:", clientData);
console.log("Would create client with data:", clientData);
// For now, just show success and redirect // Call the upsert API
const fullAddress = DataUtils.calculateFullAddress({ const result = await Api.createClient(clientData);
addressLine1: formData.value.addressLine1,
addressLine2: formData.value.addressLine2,
city: formData.value.city,
state: formData.value.state,
pincode: formData.value.zipcode,
});
// 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) {
notificationStore.addSuccess( notificationStore.addSuccess(
`Client ${formData.value.customerName} created successfully!`, `Client ${formData.value.customerName} created successfully!`,
); );
@ -619,12 +516,11 @@ const handleSave = async () => {
}, },
}); });
} else { } 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!"); notificationStore.addSuccess("Client updated successfully!");
editMode.value = false; editMode.value = false;
// Reload the client data
// Note: Parent component should handle reloading
} }
} catch (error) { } catch (error) {
console.error("Error saving client:", error); console.error("Error saving client:", error);
@ -636,8 +532,8 @@ const handleSave = async () => {
const handleCancel = () => { const handleCancel = () => {
if (props.isNew) { if (props.isNew) {
// Go back for new client // Clear form for new client
router.back(); resetForm();
} else { } else {
// Exit edit mode and restore original data // Exit edit mode and restore original data
editMode.value = false; editMode.value = false;

View File

@ -7,6 +7,7 @@ import { globalSettings } from "./globalSettings";
import { createPinia } from "pinia"; import { createPinia } from "pinia";
// Vuetify // Vuetify
import "@primeuix/themes/aura";
import "vuetify/styles"; import "vuetify/styles";
import { createVuetify } from "vuetify"; import { createVuetify } from "vuetify";
import * as components from "vuetify/components"; import * as components from "vuetify/components";

View File

@ -55,3 +55,19 @@
justify-content: flex-end; justify-content: flex-end;
gap: 5px; 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;
}