added ability to link address/contacts if they already exist
This commit is contained in:
parent
cf577f3ac7
commit
8ebd77540c
11 changed files with 450 additions and 224 deletions
|
|
@ -3,6 +3,32 @@ import json
|
||||||
from custom_ui.db_utils import build_error_response, build_success_response
|
from custom_ui.db_utils import build_error_response, build_success_response
|
||||||
from custom_ui.services import ClientService, AddressService, ContactService
|
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()
|
@frappe.whitelist()
|
||||||
def get_address_by_full_address(full_address):
|
def get_address_by_full_address(full_address):
|
||||||
"""Get address by full_address, including associated contacts."""
|
"""Get address by full_address, including associated contacts."""
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,25 @@ from custom_ui.services import AddressService, ContactService, ClientService
|
||||||
# CLIENT MANAGEMENT API METHODS
|
# 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()
|
@frappe.whitelist()
|
||||||
def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=None):
|
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."""
|
"""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)
|
client_doc = check_and_get_client_doc(customer_name)
|
||||||
if client_doc:
|
if client_doc:
|
||||||
return build_error_response(f"Client with name '{customer_name}' already exists.", 400)
|
return build_error_response(f"Client with name '{customer_name}' already exists.", 400)
|
||||||
for address in addresses:
|
# for address in addresses:
|
||||||
if address_exists(
|
# if address_exists(
|
||||||
address.get("address_line1"),
|
# address.get("address_line1"),
|
||||||
address.get("address_line2"),
|
# address.get("address_line2"),
|
||||||
address.get("city"),
|
# address.get("city"),
|
||||||
address.get("state"),
|
# address.get("state"),
|
||||||
address.get("pincode")
|
# 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)
|
# 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
|
# Handle customer creation/update
|
||||||
|
|
||||||
|
|
@ -444,8 +463,19 @@ def upsert_client(data):
|
||||||
# Handle address creation
|
# Handle address creation
|
||||||
address_docs = []
|
address_docs = []
|
||||||
for address in addresses:
|
for address in addresses:
|
||||||
|
|
||||||
is_billing = True if address.get("is_billing_address") else False
|
is_billing = True if address.get("is_billing_address") else False
|
||||||
is_service = True if address.get("is_service_address") else False
|
is_service = True if address.get("is_service_address") else False
|
||||||
|
address_exists = frappe.db.exists("Address", {
|
||||||
|
"address_line1": address.get("address_line1"),
|
||||||
|
"address_line2": address.get("address_line2"),
|
||||||
|
"city": address.get("city"),
|
||||||
|
"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)
|
print("#####DEBUG: Creating address with data:", address)
|
||||||
address_doc = AddressService.create_address({
|
address_doc = AddressService.create_address({
|
||||||
"address_title": AddressService.build_address_title(customer_name, address),
|
"address_title": AddressService.build_address_title(customer_name, address),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,23 @@
|
||||||
import frappe
|
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:
|
def existing_contact_name(first_name: str, last_name: str, email: str, phone: str) -> str:
|
||||||
"""Check if a contact exists based on provided details."""
|
"""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_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";
|
const FRAPPE_SUBMIT_BID_MEETING_NOTE_FORM_METHOD = "custom_ui.api.db.bid_meetings.submit_bid_meeting_note_form";
|
||||||
// Address methods
|
// 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";
|
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
|
// Client methods
|
||||||
const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.clients.upsert_client";
|
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";
|
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_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_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_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
|
// Employee methods
|
||||||
const FRAPPE_GET_EMPLOYEES_METHOD = "custom_ui.api.db.employees.get_employees";
|
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";
|
const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_employees_organized";
|
||||||
|
|
@ -105,6 +109,10 @@ class Api {
|
||||||
// CLIENT METHODS
|
// CLIENT METHODS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
static async checkCustomerExists(clientName) {
|
||||||
|
return await this.request(FRAPPE_CHECK_CLIENT_EXISTS_METHOD, { clientName });
|
||||||
|
}
|
||||||
|
|
||||||
static async getClientStatusCounts(params = {}) {
|
static async getClientStatusCounts(params = {}) {
|
||||||
return await this.request(FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD, params);
|
return await this.request(FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD, params);
|
||||||
}
|
}
|
||||||
|
|
@ -644,10 +652,22 @@ class Api {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CONTACT METHODS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static async checkContactsExist(contacts) {
|
||||||
|
return await this.request(FRAPPE_CHECK_CONTACTS_EXIST_METHOD, { contacts });
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ADDRESS METHODS
|
// ADDRESS METHODS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
static async checkAddressesExist(addresses) {
|
||||||
|
return await this.request(FRAPPE_CHECK_ADDRESSES_EXIST_METHOD, { addresses });
|
||||||
|
}
|
||||||
|
|
||||||
static async getAddressByFullAddress(fullAddress) {
|
static async getAddressByFullAddress(fullAddress) {
|
||||||
return await this.request("custom_ui.api.db.addresses.get_address_by_full_address", {
|
return await this.request("custom_ui.api.db.addresses.get_address_by_full_address", {
|
||||||
full_address: fullAddress,
|
full_address: fullAddress,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
v-for="(address, index) in localFormData.addresses"
|
v-for="(address, index) in localFormData.addresses"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="address-item"
|
class="address-item"
|
||||||
|
:class="{ 'existing-highlight': isExistingAddress(address) }"
|
||||||
>
|
>
|
||||||
<div class="address-header">
|
<div class="address-header">
|
||||||
<div class="address-title">
|
<div class="address-title">
|
||||||
|
|
@ -169,6 +170,10 @@ const props = defineProps({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
existingAddresses: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["update:formData"]);
|
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 handleZipcodeInput = async (index, event) => {
|
||||||
const input = event.target.value;
|
const value = event.target.value;
|
||||||
|
localFormData.value.addresses[index].pincode = value;
|
||||||
// Only allow digits
|
if (value.length === 5) {
|
||||||
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) {
|
|
||||||
try {
|
try {
|
||||||
console.log("DEBUG: Looking up city/state for zip code:", digitsOnly);
|
const zipInfo = await Api.getCityStateByZip(value);
|
||||||
const places = await Api.getCityStateByZip(digitsOnly);
|
console.log("Zipcode lookup result:", zipInfo);
|
||||||
console.log("DEBUG: Retrieved places:", places);
|
if (zipInfo && zipInfo.length > 0) {
|
||||||
if (places && places.length > 0) {
|
localFormData.value.addresses[index].city = zipInfo[0].city;
|
||||||
// Auto-populate city and state
|
localFormData.value.addresses[index].state = zipInfo[0].state;
|
||||||
localFormData.value.addresses[index].city = places[0]["city"];
|
localFormData.value.addresses[index].zipcodeLookupDisabled = false;
|
||||||
localFormData.value.addresses[index].state = places[0]["state"];
|
} else {
|
||||||
localFormData.value.addresses[index].zipcodeLookupDisabled = true;
|
throw new Error("No data returned");
|
||||||
notificationStore.addSuccess(`Found: ${places[0]["city"]}, ${places[0]["state"]}`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Enable manual entry if lookup fails
|
console.error("Zipcode lookup failed:", error);
|
||||||
localFormData.value.addresses[index].zipcodeLookupDisabled = false;
|
localFormData.value.addresses[index].zipcodeLookupDisabled = true;
|
||||||
notificationStore.addWarning(
|
localFormData.value.addresses[index].city = '';
|
||||||
"Could not find city/state for this zip code. Please enter manually.",
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -479,13 +489,8 @@ const handleZipcodeInput = async (index, event) => {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
.address-item.existing-highlight {
|
||||||
.form-row {
|
border-color: var(--red-500);
|
||||||
flex-direction: column;
|
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,12 @@
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Check"
|
|
||||||
size="small"
|
|
||||||
icon="pi pi-check-circle"
|
|
||||||
class="check-btn"
|
class="check-btn"
|
||||||
@click="checkCustomerExists"
|
@click="checkCustomerExists"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
/>
|
>
|
||||||
|
<i class="pi pi-check-circle"></i> Check
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="!isNewClient && !isEditMode"
|
v-if="!isNewClient && !isEditMode"
|
||||||
@click="searchCustomers"
|
@click="searchCustomers"
|
||||||
|
|
@ -52,7 +51,7 @@
|
||||||
<Dialog
|
<Dialog
|
||||||
:visible="showCustomerSearchModal"
|
:visible="showCustomerSearchModal"
|
||||||
@update:visible="showCustomerSearchModal = $event"
|
@update:visible="showCustomerSearchModal = $event"
|
||||||
header="Select Customer"
|
header="Potential Matches"
|
||||||
:modal="true"
|
:modal="true"
|
||||||
class="search-dialog"
|
class="search-dialog"
|
||||||
>
|
>
|
||||||
|
|
@ -61,28 +60,49 @@
|
||||||
<i class="pi pi-info-circle"></i>
|
<i class="pi pi-info-circle"></i>
|
||||||
<p>No customers found matching your search.</p>
|
<p>No customers found matching your search.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="results-list">
|
<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
|
<div
|
||||||
v-for="(customerName, index) in customerSearchResults"
|
v-for="(customerName, index) in customerSearchResults"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="result-item"
|
class="result-item"
|
||||||
@click="selectCustomer(customerName)"
|
@click="router.push(`/client?client=${encodeURIComponent(customerName)}`)"
|
||||||
>
|
>
|
||||||
<strong>{{ customerName }}</strong>
|
<strong>{{ customerName }}</strong>
|
||||||
<i class="pi pi-chevron-right"></i>
|
<i class="pi pi-chevron-right"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button label="Cancel" severity="secondary" @click="showCustomerSearchModal = false" />
|
<Button label="Cancel" severity="secondary" @click="showCustomerSearchModal = false" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</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>
|
</div>
|
||||||
</template><script setup>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
import { ref, watch, computed } from "vue";
|
import { ref, watch, computed } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import InputText from "primevue/inputtext";
|
import InputText from "primevue/inputtext";
|
||||||
import Select from "primevue/select";
|
import Select from "primevue/select";
|
||||||
import Dialog from "primevue/dialog";
|
import Dialog from "primevue/dialog";
|
||||||
|
import Button from "primevue/button";
|
||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||||
|
|
||||||
|
|
@ -104,6 +124,7 @@ const props = defineProps({
|
||||||
const emit = defineEmits(["update:formData", "newClientToggle", "customerSelected"]);
|
const emit = defineEmits(["update:formData", "newClientToggle", "customerSelected"]);
|
||||||
|
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const localFormData = computed({
|
const localFormData = computed({
|
||||||
get: () => props.formData,
|
get: () => props.formData,
|
||||||
|
|
@ -114,6 +135,8 @@ const isNewClient = ref(true);
|
||||||
const showCustomerSearchModal = ref(false);
|
const showCustomerSearchModal = ref(false);
|
||||||
const customerSearchResults = ref([]);
|
const customerSearchResults = ref([]);
|
||||||
const customerTypeOptions = ["Individual", "Partnership", "Company"];
|
const customerTypeOptions = ["Individual", "Partnership", "Company"];
|
||||||
|
const showExactMatchModal = ref(false);
|
||||||
|
const exactMatchClient = ref(null);
|
||||||
|
|
||||||
const mapContactsFromClient = (contacts = []) => {
|
const mapContactsFromClient = (contacts = []) => {
|
||||||
if (!Array.isArray(contacts) || contacts.length === 0) {
|
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.");
|
notificationStore.addWarning("Please ensure a customer name is entered before checking.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = await Api.getClient(searchTerm);
|
const result = await Api.checkCustomerExists(searchTerm);
|
||||||
|
if (result.exactMatch) {
|
||||||
if (!client) {
|
exactMatchClient.value = result.exactMatch;
|
||||||
notificationStore.addInfo("Customer is not in our system yet.");
|
showExactMatchModal.value = true;
|
||||||
return;
|
} else if (result.potentialMatches && result.potentialMatches.length > 0) {
|
||||||
}
|
customerSearchResults.value = result.potentialMatches;
|
||||||
|
showCustomerSearchModal.value = true;
|
||||||
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);
|
|
||||||
} else {
|
} 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) => {
|
const goToCustomer = (clientName) => {
|
||||||
try {
|
router.push(`/client?client=${encodeURIComponent(clientName)}`);
|
||||||
// Fetch full customer data
|
showExactMatchModal.value = false;
|
||||||
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.");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|
@ -330,7 +321,8 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-dialog {
|
.search-dialog {
|
||||||
max-width: 500px;
|
max-width: 800px;
|
||||||
|
width: 90vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-results {
|
.search-results {
|
||||||
|
|
@ -374,15 +366,14 @@ defineExpose({
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-info {
|
.potential-matches-message {
|
||||||
display: flex;
|
margin-bottom: 1rem;
|
||||||
flex-direction: column;
|
padding: 0.75rem;
|
||||||
gap: 0.25rem;
|
background-color: var(--surface-section);
|
||||||
}
|
border: 1px solid var(--surface-border);
|
||||||
|
border-radius: 4px;
|
||||||
.customer-type {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-color-secondary);
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconoir-btn {
|
.iconoir-btn {
|
||||||
|
|
@ -412,6 +403,9 @@ defineExpose({
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-btn {
|
.search-btn {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
v-for="(contact, index) in localFormData.contacts"
|
v-for="(contact, index) in localFormData.contacts"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="contact-item"
|
class="contact-item"
|
||||||
|
:class="{ 'existing-highlight': isExistingContact(contact) }"
|
||||||
>
|
>
|
||||||
<div class="contact-header">
|
<div class="contact-header">
|
||||||
<div class="contact-title">
|
<div class="contact-title">
|
||||||
|
|
@ -139,6 +140,10 @@ const props = defineProps({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
existingContacts: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["update:formData"]);
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -406,15 +418,8 @@ defineExpose({});
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
.contact-item.existing-highlight {
|
||||||
.form-grid {
|
border-color: var(--red-500);
|
||||||
grid-template-columns: 1fr;
|
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="client-page">
|
<div class="client-page">
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- Existing Client View -->
|
||||||
|
<div v-else>
|
||||||
<!-- Client Header -->
|
<!-- Client Header -->
|
||||||
<GeneralClientInfo
|
<GeneralClientInfo
|
||||||
v-if="client.customerName"
|
v-if="client.customerName"
|
||||||
|
|
@ -59,6 +84,41 @@
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</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) -->
|
<!-- Form Actions (for edit mode or new client) -->
|
||||||
<div class="form-actions" v-if="editMode || isNew">
|
<div class="form-actions" v-if="editMode || isNew">
|
||||||
|
|
@ -84,6 +144,7 @@ import Tab from "primevue/tab";
|
||||||
import TabPanels from "primevue/tabpanels";
|
import TabPanels from "primevue/tabpanels";
|
||||||
import TabPanel from "primevue/tabpanel";
|
import TabPanel from "primevue/tabpanel";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
|
import Dialog from "primevue/dialog";
|
||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useLoadingStore } from "../../stores/loading";
|
import { useLoadingStore } from "../../stores/loading";
|
||||||
|
|
@ -94,6 +155,9 @@ import AddressSelector from "../clientView/AddressSelector.vue";
|
||||||
import GeneralClientInfo from "../clientView/GeneralClientInfo.vue";
|
import GeneralClientInfo from "../clientView/GeneralClientInfo.vue";
|
||||||
import AdditionalInfoBar from "../clientView/AdditionalInfoBar.vue";
|
import AdditionalInfoBar from "../clientView/AdditionalInfoBar.vue";
|
||||||
import Overview from "../clientView/Overview.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 route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -128,6 +192,9 @@ const nextVisitDate = ref(null); // Placeholder, update as needed
|
||||||
// Tab and edit state
|
// Tab and edit state
|
||||||
const editMode = ref(false);
|
const editMode = ref(false);
|
||||||
const isSubmitting = ref(false);
|
const isSubmitting = ref(false);
|
||||||
|
const showExistingModal = ref(false);
|
||||||
|
const existingAddresses = ref([]);
|
||||||
|
const existingContacts = ref([]);
|
||||||
|
|
||||||
const selectedAddressIdx = computed({
|
const selectedAddressIdx = computed({
|
||||||
get: () => addresses.value.indexOf(selectedAddress.value),
|
get: () => addresses.value.indexOf(selectedAddress.value),
|
||||||
|
|
@ -154,17 +221,17 @@ const fullAddress = computed(() => {
|
||||||
return DataUtils.calculateFullAddress(selectedAddressData.value);
|
return DataUtils.calculateFullAddress(selectedAddressData.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const getClientNames = async (type) => {
|
// const getClientNames = async (type) => {
|
||||||
loadingStore.setLoading(true);
|
// loadingStore.setLoading(true);
|
||||||
try {
|
// try {
|
||||||
const names = await Api.getClientNames(type);
|
// const names = await Api.getClientNames(type);
|
||||||
clientNames.value = names;
|
// clientNames.value = names;
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error("Error fetching client names in Client.vue: ", error.message || error);
|
// console.error("Error fetching client names in Client.vue: ", error.message || error);
|
||||||
} finally {
|
// } finally {
|
||||||
loadingStore.setLoading(false);
|
// loadingStore.setLoading(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const getClient = async (name) => {
|
const getClient = async (name) => {
|
||||||
loadingStore.setLoading(true);
|
loadingStore.setLoading(true);
|
||||||
|
|
@ -315,6 +382,19 @@ const handleSubmit = async () => {
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
try {
|
try {
|
||||||
if (isNew.value) {
|
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);
|
const createdClient = await Api.createClient(client.value);
|
||||||
console.log("Created client:", createdClient);
|
console.log("Created client:", createdClient);
|
||||||
notificationStore.addSuccess("Client created successfully!");
|
notificationStore.addSuccess("Client created successfully!");
|
||||||
|
|
@ -350,6 +430,35 @@ const handlePrimaryContactUpdate = (contactName) => {
|
||||||
const handleClientUpdate = (newClientData) => {
|
const handleClientUpdate = (newClientData) => {
|
||||||
client.value = { ...client.value, ...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>
|
</script>
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
.tab-info-alert {
|
.tab-info-alert {
|
||||||
|
|
@ -420,4 +529,32 @@ const handleClientUpdate = (newClientData) => {
|
||||||
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
|
||||||
z-index: 1000;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -890,12 +890,7 @@ const isPackageItem = (item) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTabClick = () => {
|
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;
|
showDrawer.value = true;
|
||||||
console.log('Set showDrawer to true');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalCost = computed(() => {
|
const totalCost = computed(() => {
|
||||||
|
|
@ -922,17 +917,11 @@ watch(() => formData.projectTemplate, async (newValue) => {
|
||||||
isLoadingQuotationItems.value = true;
|
isLoadingQuotationItems.value = true;
|
||||||
try {
|
try {
|
||||||
quotationItems.value = await Api.getItemsByProjectTemplate(newValue);
|
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) {
|
} catch (error) {
|
||||||
console.error("Error fetching items by project template:", error);
|
|
||||||
notificationStore.addNotification("Failed to load items for selected project template", "error");
|
notificationStore.addNotification("Failed to load items for selected project template", "error");
|
||||||
quotationItems.value = {};
|
quotationItems.value = {};
|
||||||
} finally {
|
} finally {
|
||||||
isLoadingQuotationItems.value = false;
|
isLoadingQuotationItems.value = false;
|
||||||
console.log("DEBUG: Loading finished, isLoadingQuotationItems:", isLoadingQuotationItems.value);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -576,8 +576,9 @@ const initializeMap = async () => {
|
||||||
const createInvoiceForJob = async () => {
|
const createInvoiceForJob = async () => {
|
||||||
if (!job.value) return;
|
if (!job.value) return;
|
||||||
try {
|
try {
|
||||||
await Api.createInvoiceForJob(job.value.name);
|
const invoice = await Api.createInvoiceForJob(job.value.name);
|
||||||
job.value.invoiceStatus = "Invoice Created";
|
job.value.invoiceStatus = "Invoice Created";
|
||||||
|
job.value.invoice = invoice;
|
||||||
notifications.addSuccess("Invoice created successfully");
|
notifications.addSuccess("Invoice created successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating invoice:", error);
|
console.error("Error creating invoice:", error);
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
Loading…
Add table
Add a link
Reference in a new issue