added ability to link address/contacts if they already exist
This commit is contained in:
parent
cf577f3ac7
commit
8ebd77540c
@ -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."""
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -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 |
Loading…
x
Reference in New Issue
Block a user