added ability to link address/contacts if they already exist

This commit is contained in:
Casey 2026-02-14 08:47:53 -06:00
parent cf577f3ac7
commit 8ebd77540c
11 changed files with 450 additions and 224 deletions

View File

@ -3,6 +3,32 @@ import json
from custom_ui.db_utils import build_error_response, build_success_response
from custom_ui.services import ClientService, AddressService, ContactService
@frappe.whitelist()
def check_addresses_exist(addresses):
"""Check if any of the provided addresses already exist in the system."""
if isinstance(addresses, str):
addresses = json.loads(addresses)
print(f"DEBUG: check_addresses_exist called with addresses: {addresses}")
existing_addresses = []
for address in addresses:
filters = {
"doctype": "Address",
"address_line1": address.get("address_line1"),
"city": address.get("city"),
# "state": address.get("state"),
"pincode": address.get("pincode")
}
if address.get("address_line2"):
filters["address_line2"] = address.get("address_line2")
print(f"DEBUG: Checking existence for address with filters: {filters}")
if frappe.db.exists(filters):
print("DEBUG: Address exists:", filters)
existing_addresses.append(address)
else:
print("DEBUG: Address does not exist:", filters)
return build_success_response(existing_addresses)
@frappe.whitelist()
def get_address_by_full_address(full_address):
"""Get address by full_address, including associated contacts."""

View File

@ -9,6 +9,25 @@ from custom_ui.services import AddressService, ContactService, ClientService
# CLIENT MANAGEMENT API METHODS
# ===============================================================================
@frappe.whitelist()
def check_client_exists(client_name):
"""Check if a client exists as either a Customer or a Lead.
Additionally, return a list of potential matches based on the client name."""
print("DEBUG: check_client_exists called with client_name:", client_name)
try:
exact_customer_match = frappe.db.exists("Customer", client_name)
exact_lead_match = frappe.db.exists("Lead", {"custom_customer_name": client_name})
customer_matches = frappe.get_all("Customer", pluck="name", filters={"name": ["like", f"%{client_name}%"]})
lead_matches = frappe.get_all("Lead", pluck="custom_customer_name", filters={"custom_customer_name": ["like", f"%{client_name}%"]})
# remove duplicates from potential matches between customers and leads
return build_success_response({
"exact_match": exact_customer_match or exact_lead_match,
"potential_matches": list(set(customer_matches + lead_matches))
})
except Exception as e:
return build_error_response(str(e), 500)
@frappe.whitelist()
def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=None):
"""Get counts of clients by status categories with optional weekly filtering."""
@ -363,15 +382,15 @@ def upsert_client(data):
client_doc = check_and_get_client_doc(customer_name)
if client_doc:
return build_error_response(f"Client with name '{customer_name}' already exists.", 400)
for address in addresses:
if address_exists(
address.get("address_line1"),
address.get("address_line2"),
address.get("city"),
address.get("state"),
address.get("pincode")
):
return build_error_response("This address already exists. Please use a different address or search for the address to find the associated client.", 400)
# for address in addresses:
# if address_exists(
# address.get("address_line1"),
# address.get("address_line2"),
# address.get("city"),
# address.get("state"),
# address.get("pincode")
# ):
# return build_error_response("This address already exists. Please use a different address or search for the address to find the associated client.", 400)
# Handle customer creation/update
@ -444,25 +463,36 @@ def upsert_client(data):
# Handle address creation
address_docs = []
for address in addresses:
is_billing = True if address.get("is_billing_address") else False
is_service = True if address.get("is_service_address") else False
print("#####DEBUG: Creating address with data:", address)
address_doc = AddressService.create_address({
"address_title": AddressService.build_address_title(customer_name, address),
address_exists = frappe.db.exists("Address", {
"address_line1": address.get("address_line1"),
"address_line2": address.get("address_line2"),
"address_type": "Billing" if is_billing else "Service",
"custom_billing_address": is_billing,
"is_service_address": is_service,
"is_primary_address": is_billing,
"city": address.get("city"),
"state": address.get("state"),
"country": "United States",
"pincode": address.get("pincode"),
"customer_type": "Lead",
"customer_name": client_doc.name,
"companies": [{ "company": data.get("company_name") }]
"pincode": address.get("pincode")
})
address_doc = None
if address_exists:
address_doc = frappe.get_doc("Address", address_exists)
else:
print("#####DEBUG: Creating address with data:", address)
address_doc = AddressService.create_address({
"address_title": AddressService.build_address_title(customer_name, address),
"address_line1": address.get("address_line1"),
"address_line2": address.get("address_line2"),
"address_type": "Billing" if is_billing else "Service",
"custom_billing_address": is_billing,
"is_service_address": is_service,
"is_primary_address": is_billing,
"city": address.get("city"),
"state": address.get("state"),
"country": "United States",
"pincode": address.get("pincode"),
"customer_type": "Lead",
"customer_name": client_doc.name,
"companies": [{ "company": data.get("company_name") }]
})
AddressService.link_address_to_customer(address_doc, "Lead", client_doc.name)
address_doc.reload()
if is_billing:

View File

@ -1,4 +1,23 @@
import frappe
import json
from custom_ui.db_utils import build_error_response, build_success_response
@frappe.whitelist()
def check_contacts_exist(contacts):
"""Check if any of the provided contacts already exist in the system."""
if isinstance(contacts, str):
contacts = json.loads(contacts)
print(f"DEBUG: check_contacts_exist called with contacts: {contacts}")
existing_contacts = []
for contact in contacts:
if frappe.db.exists("Contact", {
"first_name": contact.get("first_name"),
"last_name": contact.get("last_name"),
"email_id": contact.get("email"),
"phone": contact.get("phone_number")
}):
existing_contacts.append(contact)
return build_success_response(existing_contacts)
def existing_contact_name(first_name: str, last_name: str, email: str, phone: str) -> str:
"""Check if a contact exists based on provided details."""

View File

@ -56,7 +56,10 @@ const FRAPPE_GET_BID_MEETING_NOTE_FORM_METHOD = "custom_ui.api.db.bid_meetings.g
const FRAPPE_GET_ONSITE_MEETINGS_METHOD = "custom_ui.api.db.bid_meetings.get_bid_meetings";
const FRAPPE_SUBMIT_BID_MEETING_NOTE_FORM_METHOD = "custom_ui.api.db.bid_meetings.submit_bid_meeting_note_form";
// Address methods
const FRAPPE_CHECK_ADDRESSES_EXIST_METHOD = "custom_ui.api.db.addresses.check_addresses_exist";
const FRAPPE_GET_ADDRESSES_METHOD = "custom_ui.api.db.addresses.get_addresses";
// Contact methods
const FRAPPE_CHECK_CONTACTS_EXIST_METHOD = "custom_ui.api.db.contacts.check_contacts_exist";
// Client methods
const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.clients.upsert_client";
const FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD = "custom_ui.api.db.clients.get_client_status_counts";
@ -64,6 +67,7 @@ const FRAPPE_GET_CLIENT_TABLE_DATA_METHOD = "custom_ui.api.db.clients.get_client
const FRAPPE_GET_CLIENT_TABLE_DATA_V2_METHOD = "custom_ui.api.db.clients.get_clients_table_data_v2";
const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.clients.get_client_v2";
const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names";
const FRAPPE_CHECK_CLIENT_EXISTS_METHOD = "custom_ui.api.db.clients.check_client_exists";
// Employee methods
const FRAPPE_GET_EMPLOYEES_METHOD = "custom_ui.api.db.employees.get_employees";
const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_employees_organized";
@ -105,6 +109,10 @@ class Api {
// CLIENT METHODS
// ============================================================================
static async checkCustomerExists(clientName) {
return await this.request(FRAPPE_CHECK_CLIENT_EXISTS_METHOD, { clientName });
}
static async getClientStatusCounts(params = {}) {
return await this.request(FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD, params);
}
@ -644,10 +652,22 @@ class Api {
return result;
}
// ============================================================================
// CONTACT METHODS
// ============================================================================
static async checkContactsExist(contacts) {
return await this.request(FRAPPE_CHECK_CONTACTS_EXIST_METHOD, { contacts });
}
// ============================================================================
// ADDRESS METHODS
// ============================================================================
static async checkAddressesExist(addresses) {
return await this.request(FRAPPE_CHECK_ADDRESSES_EXIST_METHOD, { addresses });
}
static async getAddressByFullAddress(fullAddress) {
return await this.request("custom_ui.api.db.addresses.get_address_by_full_address", {
full_address: fullAddress,

View File

@ -9,6 +9,7 @@
v-for="(address, index) in localFormData.addresses"
:key="index"
class="address-item"
:class="{ 'existing-highlight': isExistingAddress(address) }"
>
<div class="address-header">
<div class="address-title">
@ -169,6 +170,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
existingAddresses: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["update:formData"]);
@ -310,48 +315,53 @@ const handleServiceChange = (selectedIndex) => {
}
};
const getFullAddress = (address) => {
return `${address.addressLine1 || ''} ${address.addressLine2 || ''} ${address.city || ''} ${address.state || ''} ${address.pincode || ''}`.trim().replace(/\s+/g, ' ');
};
const handleZipcodeInput = async (index, 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.addresses[index].pincode = digitsOnly;
// Reset city/state if zipcode is not complete
if (digitsOnly.length < 5 && localFormData.value.addresses[index].zipcodeLookupDisabled) {
localFormData.value.addresses[index].city = "";
localFormData.value.addresses[index].state = "";
localFormData.value.addresses[index].zipcodeLookupDisabled = false;
}
// Fetch city/state when 5 digits entered
if (digitsOnly.length === 5) {
const value = event.target.value;
localFormData.value.addresses[index].pincode = value;
if (value.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.addresses[index].city = places[0]["city"];
localFormData.value.addresses[index].state = places[0]["state"];
localFormData.value.addresses[index].zipcodeLookupDisabled = true;
notificationStore.addSuccess(`Found: ${places[0]["city"]}, ${places[0]["state"]}`);
const zipInfo = await Api.getCityStateByZip(value);
console.log("Zipcode lookup result:", zipInfo);
if (zipInfo && zipInfo.length > 0) {
localFormData.value.addresses[index].city = zipInfo[0].city;
localFormData.value.addresses[index].state = zipInfo[0].state;
localFormData.value.addresses[index].zipcodeLookupDisabled = false;
} else {
throw new Error("No data returned");
}
} catch (error) {
// Enable manual entry if lookup fails
localFormData.value.addresses[index].zipcodeLookupDisabled = false;
notificationStore.addWarning(
"Could not find city/state for this zip code. Please enter manually.",
);
console.error("Zipcode lookup failed:", error);
localFormData.value.addresses[index].zipcodeLookupDisabled = true;
localFormData.value.addresses[index].city = '';
localFormData.value.addresses[index].state = '';
notificationStore.addError("Invalid zipcode or lookup failed");
}
} else {
localFormData.value.addresses[index].zipcodeLookupDisabled = true;
localFormData.value.addresses[index].city = '';
localFormData.value.addresses[index].state = '';
}
};
const normalizeAddressString = (s = '') => {
return (s || '')
.toString()
.replace(/,/g, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
};
const isExistingAddress = (address) => {
const fullAddr = getFullAddress(address);
const normFull = normalizeAddressString(fullAddr);
if (!props.existingAddresses || props.existingAddresses.length === 0) return false;
return props.existingAddresses.some((ea) => normalizeAddressString(ea) === normFull);
};
</script>
<style scoped>
@ -479,13 +489,8 @@ const handleZipcodeInput = async (index, event) => {
cursor: pointer;
}
@media (max-width: 768px) {
.form-row {
flex-direction: column;
}
.form-grid {
grid-template-columns: 1fr;
}
.address-item.existing-highlight {
border-color: var(--red-500);
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
</style>

View File

@ -28,13 +28,12 @@
class="w-full"
/>
<Button
label="Check"
size="small"
icon="pi pi-check-circle"
class="check-btn"
@click="checkCustomerExists"
:disabled="isSubmitting"
/>
>
<i class="pi pi-check-circle"></i> Check
</Button>
<Button
v-if="!isNewClient && !isEditMode"
@click="searchCustomers"
@ -52,7 +51,7 @@
<Dialog
:visible="showCustomerSearchModal"
@update:visible="showCustomerSearchModal = $event"
header="Select Customer"
header="Potential Matches"
:modal="true"
class="search-dialog"
>
@ -61,15 +60,18 @@
<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 v-else>
<p class="potential-matches-message">Here are potential matches for your search. Click on a customer to view their details.</p>
<div class="results-list">
<div
v-for="(customerName, index) in customerSearchResults"
:key="index"
class="result-item"
@click="router.push(`/client?client=${encodeURIComponent(customerName)}`)"
>
<strong>{{ customerName }}</strong>
<i class="pi pi-chevron-right"></i>
</div>
</div>
</div>
</div>
@ -77,12 +79,30 @@
<Button label="Cancel" severity="secondary" @click="showCustomerSearchModal = false" />
</template>
</Dialog>
<!-- Exact Match Modal -->
<Dialog
:visible="showExactMatchModal"
@update:visible="showExactMatchModal = $event"
header="Customer Already Exists"
:modal="true"
>
<p>The customer "{{ exactMatchClient }}" already exists.</p>
<template #footer>
<Button label="Cancel" severity="secondary" @click="showExactMatchModal = false" />
<Button label="Go to Customer" @click="goToCustomer(exactMatchClient)" />
</template>
</Dialog>
</div>
</template><script setup>
</template>
<script setup>
import { ref, watch, computed } from "vue";
import { useRouter } from "vue-router";
import InputText from "primevue/inputtext";
import Select from "primevue/select";
import Dialog from "primevue/dialog";
import Button from "primevue/button";
import Api from "../../api";
import { useNotificationStore } from "../../stores/notifications-primevue";
@ -104,6 +124,7 @@ const props = defineProps({
const emit = defineEmits(["update:formData", "newClientToggle", "customerSelected"]);
const notificationStore = useNotificationStore();
const router = useRouter();
const localFormData = computed({
get: () => props.formData,
@ -114,6 +135,8 @@ const isNewClient = ref(true);
const showCustomerSearchModal = ref(false);
const customerSearchResults = ref([]);
const customerTypeOptions = ["Individual", "Partnership", "Company"];
const showExactMatchModal = ref(false);
const exactMatchClient = ref(null);
const mapContactsFromClient = (contacts = []) => {
if (!Array.isArray(contacts) || contacts.length === 0) {
@ -191,58 +214,26 @@ const checkCustomerExists = async () => {
notificationStore.addWarning("Please ensure a customer name is entered before checking.");
return;
}
try {
const client = await Api.getClient(searchTerm);
if (!client) {
notificationStore.addInfo("Customer is not in our system yet.");
return;
}
localFormData.value.customerName = client.customerName || searchTerm;
localFormData.value.customerType = client.customerType || localFormData.value.customerType;
localFormData.value.contacts = mapContactsFromClient(client.contacts);
isNewClient.value = false;
showCustomerSearchModal.value = false;
emit("customerSelected", client);
notificationStore.addSuccess(
`Customer ${localFormData.value.customerName} found and loaded from system.`,
);
} catch (error) {
console.error("Error checking customer:", error);
const message =
typeof error?.message === "string" &&
error.message.toLowerCase().includes("not found")
? "Customer is not in our system yet."
: "Failed to check customer. Please try again.";
if (message.includes("not in our system")) {
notificationStore.addInfo(message);
const result = await Api.checkCustomerExists(searchTerm);
if (result.exactMatch) {
exactMatchClient.value = result.exactMatch;
showExactMatchModal.value = true;
} else if (result.potentialMatches && result.potentialMatches.length > 0) {
customerSearchResults.value = result.potentialMatches;
showCustomerSearchModal.value = true;
} else {
notificationStore.addError(message);
notificationStore.addInfo("No matching customers found.");
}
} catch (error) {
console.error("Error checking customer exists:", error);
notificationStore.addError("Failed to check customer existence. 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;
localFormData.value.contacts = mapContactsFromClient(clientData.contacts);
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.");
}
const goToCustomer = (clientName) => {
router.push(`/client?client=${encodeURIComponent(clientName)}`);
showExactMatchModal.value = false;
};
defineExpose({
@ -330,7 +321,8 @@ defineExpose({
}
.search-dialog {
max-width: 500px;
max-width: 800px;
width: 90vw;
}
.search-results {
@ -374,15 +366,14 @@ defineExpose({
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;
.potential-matches-message {
margin-bottom: 1rem;
padding: 0.75rem;
background-color: var(--surface-section);
border: 1px solid var(--surface-border);
border-radius: 4px;
color: var(--text-color-secondary);
font-size: 0.9rem;
}
.iconoir-btn {
@ -412,6 +403,9 @@ defineExpose({
padding: 0.5rem 0.75rem;
font-size: 0.8rem;
height: 100%;
background-color: var(--primary-color);
color: white;
border: none;
}
.search-btn {

View File

@ -9,6 +9,7 @@
v-for="(contact, index) in localFormData.contacts"
:key="index"
class="contact-item"
:class="{ 'existing-highlight': isExistingContact(contact) }"
>
<div class="contact-header">
<div class="contact-title">
@ -139,6 +140,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
existingContacts: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["update:formData"]);
@ -278,7 +283,14 @@ const handlePhoneKeydown = (event, index) => {
}
};
defineExpose({});
const getFullName = (contact) => {
return `${contact.firstName || ''} ${contact.lastName || ''}`.trim();
};
const isExistingContact = (contact) => {
const fullName = getFullName(contact);
return props.existingContacts.includes(fullName);
};
</script>
<style scoped>
@ -406,15 +418,8 @@ defineExpose({});
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;
}
.contact-item.existing-highlight {
border-color: var(--red-500);
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
</style>

View File

@ -1,64 +1,124 @@
<template>
<div class="client-page">
<!-- Client Header -->
<GeneralClientInfo
v-if="client.customerName"
:client-data="client"
/>
<AdditionalInfoBar :address="client.addresses[selectedAddressIdx]" v-if="client.customerName" />
<!-- New Client Form -->
<div v-if="isNew">
<ClientInformationForm
:formData="client"
:is-submitting="isSubmitting"
@update:formData="handleClientUpdate"
@newClientToggle="handleNewClientToggle"
@customerSelected="handleCustomerSelected"
/>
<ContactInformationForm
:formData="client"
:is-submitting="isSubmitting"
:existing-contacts="existingContacts.map(contact => `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || contact.email || 'Unknown Contact')"
@update:formData="handleClientUpdate"
/>
<AddressInformationForm
:formData="client"
:is-submitting="isSubmitting"
:existing-addresses="existingAddresses.map(addr => DataUtils.calculateFullAddress(addr))"
@update:formData="handleClientUpdate"
/>
</div>
<!-- Address Selector (only shows if multiple addresses) -->
<AddressSelector
v-if="!isNew && client.addresses && client.addresses.length > 1"
:addresses="client.addresses"
:selected-address-idx="selectedAddressIdx"
:contacts="client.contacts"
@update:selected-address-idx="handleAddressChange"
/>
<!-- Existing Client View -->
<div v-else>
<!-- Client Header -->
<GeneralClientInfo
v-if="client.customerName"
:client-data="client"
/>
<AdditionalInfoBar :address="client.addresses[selectedAddressIdx]" v-if="client.customerName" />
<!-- Main Content Tabs -->
<Tabs value="0" class="overview-tabs">
<TabList>
<Tab value="0">Overview</Tab>
<Tab value="1">Projects</Tab>
<Tab value="2">Financials</Tab>
</TabList>
<TabPanels>
<!-- Overview Tab -->
<TabPanel value="0">
<Overview
:selected-address="selectedAddressData"
:all-contacts="client.contacts"
:edit-mode="editMode"
:is-new="isNew"
:full-address="fullAddress"
:client="client"
@edit-mode-enabled="enableEditMode"
@update:address-contacts="handleAddressContactsUpdate"
@update:primary-contact="handlePrimaryContactUpdate"
@update:client="handleClientUpdate"
/>
</TabPanel>
<!-- Address Selector (only shows if multiple addresses) -->
<AddressSelector
v-if="!isNew && client.addresses && client.addresses.length > 1"
:addresses="client.addresses"
:selected-address-idx="selectedAddressIdx"
:contacts="client.contacts"
@update:selected-address-idx="handleAddressChange"
/>
<!-- Projects Tab -->
<TabPanel value="1">
<div class="coming-soon-section">
<i class="pi pi-wrench"></i>
<h3>Projects</h3>
<p>Section coming soon</p>
</div>
</TabPanel>
<!-- Main Content Tabs -->
<Tabs value="0" class="overview-tabs">
<TabList>
<Tab value="0">Overview</Tab>
<Tab value="1">Projects</Tab>
<Tab value="2">Financials</Tab>
</TabList>
<TabPanels>
<!-- Overview Tab -->
<TabPanel value="0">
<Overview
:selected-address="selectedAddressData"
:all-contacts="client.contacts"
:edit-mode="editMode"
:is-new="isNew"
:full-address="fullAddress"
:client="client"
@edit-mode-enabled="enableEditMode"
@update:address-contacts="handleAddressContactsUpdate"
@update:primary-contact="handlePrimaryContactUpdate"
@update:client="handleClientUpdate"
/>
</TabPanel>
<!-- Financials Tab -->
<TabPanel value="2">
<div class="coming-soon-section">
<i class="pi pi-dollar"></i>
<h3>Financials</h3>
<p>Section coming soon</p>
</div>
</TabPanel>
</TabPanels>
</Tabs>
<!-- Projects Tab -->
<TabPanel value="1">
<div class="coming-soon-section">
<i class="pi pi-wrench"></i>
<h3>Projects</h3>
<p>Section coming soon</p>
</div>
</TabPanel>
<!-- Financials Tab -->
<TabPanel value="2">
<div class="coming-soon-section">
<i class="pi pi-dollar"></i>
<h3>Financials</h3>
<p>Section coming soon</p>
</div>
</TabPanel>
</TabPanels>
</Tabs>
</div>
<!-- Existing Addresses/Contacts Modal -->
<Dialog
:visible="showExistingModal"
@update:visible="showExistingModal = $event"
header="Existing Addresses and Contacts Found"
:modal="true"
class="existing-modal"
>
<div class="modal-content">
<p>The following addresses and/or contacts already exist in the system:</p>
<div v-if="existingAddresses && existingAddresses.length > 0" class="existing-section">
<h4>Existing Addresses:</h4>
<ul>
<li v-for="addr in existingAddresses" :key="addr">
{{ addr.addressLine1 }} {{ addr.addressLine2 }}, {{ addr.city }}, {{ addr.state }} {{ addr.pincode }}
</li>
</ul>
</div>
<div v-if="existingContacts && existingContacts.length > 0" class="existing-section">
<h4>Existing Contacts:</h4>
<ul>
<li v-for="contact in existingContacts" :key="contact">
{{ contact.firstName }} {{ contact.lastName }}
</li>
</ul>
</div>
<p>Would you like to link these existing addresses/contacts with this new client, or cancel the creation?</p>
</div>
<template #footer>
<Button label="Cancel" severity="secondary" @click="cancelExisting" />
<Button label="Continue and Link" @click="continueWithExisting" />
</template>
</Dialog>
<!-- Form Actions (for edit mode or new client) -->
<div class="form-actions" v-if="editMode || isNew">
@ -84,6 +144,7 @@ import Tab from "primevue/tab";
import TabPanels from "primevue/tabpanels";
import TabPanel from "primevue/tabpanel";
import Button from "primevue/button";
import Dialog from "primevue/dialog";
import Api from "../../api";
import { useRoute, useRouter } from "vue-router";
import { useLoadingStore } from "../../stores/loading";
@ -94,6 +155,9 @@ import AddressSelector from "../clientView/AddressSelector.vue";
import GeneralClientInfo from "../clientView/GeneralClientInfo.vue";
import AdditionalInfoBar from "../clientView/AdditionalInfoBar.vue";
import Overview from "../clientView/Overview.vue";
import ClientInformationForm from "../clientSubPages/ClientInformationForm.vue";
import AddressInformationForm from "../clientSubPages/AddressInformationForm.vue";
import ContactInformationForm from "../clientSubPages/ContactInformationForm.vue";
const route = useRoute();
const router = useRouter();
@ -128,6 +192,9 @@ const nextVisitDate = ref(null); // Placeholder, update as needed
// Tab and edit state
const editMode = ref(false);
const isSubmitting = ref(false);
const showExistingModal = ref(false);
const existingAddresses = ref([]);
const existingContacts = ref([]);
const selectedAddressIdx = computed({
get: () => addresses.value.indexOf(selectedAddress.value),
@ -154,17 +221,17 @@ const fullAddress = computed(() => {
return DataUtils.calculateFullAddress(selectedAddressData.value);
});
const getClientNames = async (type) => {
loadingStore.setLoading(true);
try {
const names = await Api.getClientNames(type);
clientNames.value = names;
} catch (error) {
console.error("Error fetching client names in Client.vue: ", error.message || error);
} finally {
loadingStore.setLoading(false);
}
};
// const getClientNames = async (type) => {
// loadingStore.setLoading(true);
// try {
// const names = await Api.getClientNames(type);
// clientNames.value = names;
// } catch (error) {
// console.error("Error fetching client names in Client.vue: ", error.message || error);
// } finally {
// loadingStore.setLoading(false);
// }
// };
const getClient = async (name) => {
loadingStore.setLoading(true);
@ -315,6 +382,19 @@ const handleSubmit = async () => {
isSubmitting.value = true;
try {
if (isNew.value) {
const clientExists = await Api.checkCustomerExists(client.value.customerName);
if (clientExists.exactMatch) {
notificationStore.addError("A client with this name already exists. Please choose a different name.");
return;
}
const addressesExist = await Api.checkAddressesExist(client.value.addresses);
const contactsExist = await Api.checkContactsExist(client.value.contacts);
if (addressesExist || contactsExist) {
// existingAddresses.value = Array.isArray(addressesExist) ? addressesExist : [];
// existingContacts.value = Array.isArray(contactsExist) ? contactsExist : [];
showExistingModal.value = true;
return;
}
const createdClient = await Api.createClient(client.value);
console.log("Created client:", createdClient);
notificationStore.addSuccess("Client created successfully!");
@ -350,6 +430,35 @@ const handlePrimaryContactUpdate = (contactName) => {
const handleClientUpdate = (newClientData) => {
client.value = { ...client.value, ...newClientData };
};
const cancelExisting = () => {
showExistingModal.value = false;
// TODO: Highlight existing addresses/contacts with red outline
};
const continueWithExisting = async () => {
showExistingModal.value = false;
try {
const createdClient = await Api.createClient(client.value);
console.log("Created client:", createdClient);
notificationStore.addSuccess("Client created successfully!");
const strippedName = createdClient.name.split("-#-")[0].trim();
// Navigate to the created client
router.push('/client?client=' + encodeURIComponent(strippedName));
} catch (error) {
console.error("Error creating client:", error);
notificationStore.addError("Failed to create client");
}
};
const handleNewClientToggle = (isNewClient) => {
// Handle toggle if needed
};
const handleCustomerSelected = (clientData) => {
// Handle customer selected from search
client.value = { ...client.value, ...clientData };
};
</script>
<style lang="css">
.tab-info-alert {
@ -420,4 +529,32 @@ const handleClientUpdate = (newClientData) => {
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.existing-modal {
max-width: 600px;
}
.modal-content {
padding: 1rem 0;
}
.existing-section {
margin-bottom: 1rem;
}
.existing-section h4 {
margin: 0 0 0.5rem 0;
color: var(--text-color);
font-size: 1rem;
}
.existing-section ul {
margin: 0;
padding-left: 1.5rem;
}
.existing-section li {
margin-bottom: 0.25rem;
color: var(--text-color-secondary);
}
</style>

View File

@ -890,12 +890,7 @@ const isPackageItem = (item) => {
};
const onTabClick = () => {
console.log('Bid notes tab clicked');
console.log('Current showDrawer value:', showDrawer.value);
console.log('bidMeeting:', bidMeeting.value);
console.log('bidMeeting?.bidNotes:', bidMeeting.value?.bidNotes);
showDrawer.value = true;
console.log('Set showDrawer to true');
};
const totalCost = computed(() => {
@ -922,17 +917,11 @@ watch(() => formData.projectTemplate, async (newValue) => {
isLoadingQuotationItems.value = true;
try {
quotationItems.value = await Api.getItemsByProjectTemplate(newValue);
console.log("DEBUG: quotationItems after API call:", quotationItems.value);
console.log("DEBUG: quotationItems type:", typeof quotationItems.value);
console.log("DEBUG: quotationItems keys length:", quotationItems.value ? Object.keys(quotationItems.value).length : 0);
console.log("DEBUG: hasQuotationItems computed value:", hasQuotationItems.value);
} catch (error) {
console.error("Error fetching items by project template:", error);
notificationStore.addNotification("Failed to load items for selected project template", "error");
quotationItems.value = {};
} finally {
isLoadingQuotationItems.value = false;
console.log("DEBUG: Loading finished, isLoadingQuotationItems:", isLoadingQuotationItems.value);
}
})

View File

@ -576,8 +576,9 @@ const initializeMap = async () => {
const createInvoiceForJob = async () => {
if (!job.value) return;
try {
await Api.createInvoiceForJob(job.value.name);
const invoice = await Api.createInvoiceForJob(job.value.name);
job.value.invoiceStatus = "Invoice Created";
job.value.invoice = invoice;
notifications.addSuccess("Invoice created successfully");
} catch (error) {
console.error("Error creating invoice:", error);

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB