fix client creation
This commit is contained in:
parent
0d7976b140
commit
0c1bb52f1b
9 changed files with 305 additions and 161 deletions
|
|
@ -136,67 +136,67 @@ def get_client(client_name):
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10):
|
def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10):
|
||||||
"""Get paginated client table data with filtering and sorting support."""
|
"""Get paginated client table data with filtering and sorting support."""
|
||||||
try:
|
# try:
|
||||||
|
|
||||||
print("DEBUG: Raw client table query received:", {
|
# print("DEBUG: Raw client table query received:", {
|
||||||
"filters": filters,
|
# "filters": filters,
|
||||||
"sortings": sortings,
|
# "sortings": sortings,
|
||||||
"page": page,
|
# "page": page,
|
||||||
"page_size": page_size
|
# "page_size": page_size
|
||||||
})
|
# })
|
||||||
|
|
||||||
processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size)
|
# processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size)
|
||||||
print("DEBUG: Processed filters:", processed_filters)
|
# print("DEBUG: Processed filters:", processed_filters)
|
||||||
print("DEBUG: Processed sortings:", processed_sortings)
|
# print("DEBUG: Processed sortings:", processed_sortings)
|
||||||
# Handle count with proper OR filter support
|
# # Handle count with proper OR filter support
|
||||||
if is_or:
|
# if is_or:
|
||||||
count = frappe.db.sql(*get_count_or_filters("Address", processed_filters))[0][0]
|
# count = frappe.db.sql(*get_count_or_filters("Address", processed_filters))[0][0]
|
||||||
else:
|
# else:
|
||||||
count = frappe.db.count("Address", filters=processed_filters)
|
# count = frappe.db.count("Address", filters=processed_filters)
|
||||||
|
|
||||||
print("DEBUG: Count of addresses matching filters:", count)
|
# print("DEBUG: Count of addresses matching filters:", count)
|
||||||
|
|
||||||
address_names = frappe.db.get_all(
|
# address_names = frappe.db.get_all(
|
||||||
"Address",
|
# "Address",
|
||||||
fields=["name"],
|
# fields=["name"],
|
||||||
filters=processed_filters if not is_or else None,
|
# filters=processed_filters if not is_or else None,
|
||||||
or_filters=processed_filters if is_or else None,
|
# or_filters=processed_filters if is_or else None,
|
||||||
limit=page_size,
|
# limit=page_size,
|
||||||
start=(page - 1) * page_size,
|
# start=(page - 1) * page_size,
|
||||||
order_by=processed_sortings
|
# order_by=processed_sortings
|
||||||
)
|
# )
|
||||||
|
|
||||||
addresses = [frappe.get_doc("Address", addr["name"]).as_dict() for addr in address_names]
|
# addresses = [frappe.get_doc("Address", addr["name"]).as_dict() for addr in address_names]
|
||||||
tableRows = []
|
# tableRows = []
|
||||||
for address in addresses:
|
# for address in addresses:
|
||||||
tableRow = {}
|
# tableRow = {}
|
||||||
links = address.links
|
# links = address.links
|
||||||
customer_links = [link for link in links if link.link_doctype == "Customer"] if links else None
|
# customer_links = [link for link in links if link.link_doctype == "Customer"] if links else None
|
||||||
customer_name = address.get("custom_customer_to_bill")
|
# customer_name = address.get("custom_customer_to_bill")
|
||||||
if not customer_name and not customer_links:
|
# if not customer_name and not customer_links:
|
||||||
print("DEBUG: No customer links found and no customer to bill.")
|
# print("DEBUG: No customer links found and no customer to bill.")
|
||||||
customer_name = "N/A"
|
# customer_name = "N/A"
|
||||||
elif not customer_name and customer_links:
|
# elif not customer_name and customer_links:
|
||||||
print("DEBUG: No customer to bill. Customer links found:", customer_links)
|
# print("DEBUG: No customer to bill. Customer links found:", customer_links)
|
||||||
customer_name = customer_links[0].link_name
|
# customer_name = customer_links[0].link_name
|
||||||
tableRow["id"] = address["name"]
|
# tableRow["id"] = address["name"]
|
||||||
tableRow["customer_name"] = customer_name
|
# tableRow["customer_name"] = customer_name
|
||||||
tableRow["address"] = (
|
# tableRow["address"] = (
|
||||||
f"{address['address_line1']}"
|
# f"{address['address_line1']}"
|
||||||
f"{' ' + address['address_line2'] if address['address_line2'] else ''} "
|
# f"{' ' + address['address_line2'] if address['address_line2'] else ''} "
|
||||||
f"{address['city']}, {address['state']} {address['pincode']}"
|
# f"{address['city']}, {address['state']} {address['pincode']}"
|
||||||
)
|
# )
|
||||||
tableRow["appointment_scheduled_status"] = address.custom_onsite_meeting_scheduled
|
# tableRow["appointment_scheduled_status"] = address.custom_onsite_meeting_scheduled
|
||||||
tableRow["estimate_sent_status"] = address.custom_estimate_sent_status
|
# tableRow["estimate_sent_status"] = address.custom_estimate_sent_status
|
||||||
tableRow["job_status"] = address.custom_job_status
|
# tableRow["job_status"] = address.custom_job_status
|
||||||
tableRow["payment_received_status"] = address.custom_payment_received_status
|
# tableRow["payment_received_status"] = address.custom_payment_received_status
|
||||||
tableRows.append(tableRow)
|
# tableRows.append(tableRow)
|
||||||
tableDataDict = build_datatable_dict(data=tableRows, count=count, page=page, page_size=page_size)
|
# tableDataDict = build_datatable_dict(data=tableRows, count=count, page=page, page_size=page_size)
|
||||||
return build_success_response(tableDataDict)
|
# return build_success_response(tableDataDict)
|
||||||
except frappe.ValidationError as ve:
|
# except frappe.ValidationError as ve:
|
||||||
return build_error_response(str(ve), 400)
|
# return build_error_response(str(ve), 400)
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
return build_error_response(str(e), 500)
|
# return build_error_response(str(e), 500)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|
@ -207,27 +207,37 @@ def upsert_client(data):
|
||||||
|
|
||||||
# Handle customer creation/update
|
# Handle customer creation/update
|
||||||
print("#####DEBUG: Upsert client data received:", data)
|
print("#####DEBUG: Upsert client data received:", data)
|
||||||
|
|
||||||
print("#####DEBUG: Checking for existing customer with name:", data.get("customer_name"))
|
print("#####DEBUG: Checking for existing customer with name:", data.get("customer_name"))
|
||||||
customer = frappe.db.exists("Customer", {"customer_name": data.get("customer_name")})
|
customer = frappe.db.exists("Customer", {"customer_name": data.get("customer_name")})
|
||||||
|
|
||||||
if not customer:
|
if not customer:
|
||||||
new_lead_data = {"doctype": "Lead"}
|
print("#####DEBUG: No existing customer found. Checking for existing lead")
|
||||||
|
customer = frappe.db.exists("Lead", {"lead_name": data.get("customer_name")})
|
||||||
|
else:
|
||||||
|
print("#####DEBUG: Existing customer found:", customer)
|
||||||
|
|
||||||
|
if not customer:
|
||||||
|
print("#####DEBUG: No existing lead found. Creating new lead.")
|
||||||
is_individual = data.get("customer_type") == "Individual"
|
is_individual = data.get("customer_type") == "Individual"
|
||||||
|
|
||||||
primary_contact = next((c for c in data.get("contacts", []) if c.get("is_primary")), None)
|
primary_contact = next((c for c in data.get("contacts", []) if c.get("is_primary")), None)
|
||||||
if not primary_contact:
|
if not primary_contact:
|
||||||
return build_error_response("Primary contact information is required to create a new customer.", 400)
|
return build_error_response("Primary contact information is required to create a new customer.", 400)
|
||||||
if is_individual:
|
print("#####DEBUG: Primary contact found:", primary_contact)
|
||||||
# Grab the contact that has is_primary true
|
|
||||||
new_lead_data["first_name"] = primary_contact.get("first_name")
|
new_lead_data = {
|
||||||
new_lead_data["last_name"] = primary_contact.get("last_name")
|
|
||||||
else:
|
|
||||||
new_lead_data["company_name"] = data.get("customer_name")
|
|
||||||
new_lead_data["email_id"] = primary_contact.get("email")
|
|
||||||
new_lead_data["phone"] = primary_contact.get("phone_number")
|
|
||||||
new_client_doc = frappe.get_doc({
|
|
||||||
"doctype": "Lead",
|
"doctype": "Lead",
|
||||||
"customer_name": data.get("customer_name"),
|
"lead_name": data.get("customer_name"),
|
||||||
"customer_type": data.get("customer_type")
|
"first_name": primary_contact.get("first_name"),
|
||||||
}).insert(ignore_permissions=True)
|
"last_name": primary_contact.get("last_name"),
|
||||||
|
"email_id": primary_contact.get("email"),
|
||||||
|
"phone": primary_contact.get("phone_number"),
|
||||||
|
"customer_type": data.get("customer_type"),
|
||||||
|
"company": data.get("company")
|
||||||
|
}
|
||||||
|
print("#####DEBUG: New lead data prepared:", new_lead_data)
|
||||||
|
new_client_doc = frappe.get_doc(new_lead_data).insert(ignore_permissions=True)
|
||||||
else:
|
else:
|
||||||
new_client_doc = frappe.get_doc("Customer", data.get("customer_name"))
|
new_client_doc = frappe.get_doc("Customer", data.get("customer_name"))
|
||||||
print(f"#####DEBUG: {new_client_doc.doctype}:", new_client_doc.as_dict())
|
print(f"#####DEBUG: {new_client_doc.doctype}:", new_client_doc.as_dict())
|
||||||
|
|
@ -265,6 +275,7 @@ def upsert_client(data):
|
||||||
contact_data = json.loads(contact_data)
|
contact_data = json.loads(contact_data)
|
||||||
print("#####DEBUG: Processing contact data:", contact_data)
|
print("#####DEBUG: Processing contact data:", contact_data)
|
||||||
contact_exists = frappe.db.exists("Contact", {"email_id": contact_data.get("email"), "phone": contact_data.get("phone_number")})
|
contact_exists = frappe.db.exists("Contact", {"email_id": contact_data.get("email"), "phone": contact_data.get("phone_number")})
|
||||||
|
print("Contact exists check:", contact_exists)
|
||||||
if not contact_exists:
|
if not contact_exists:
|
||||||
contact_doc = frappe.get_doc({
|
contact_doc = frappe.get_doc({
|
||||||
"doctype": "Contact",
|
"doctype": "Contact",
|
||||||
|
|
@ -288,7 +299,7 @@ def upsert_client(data):
|
||||||
}).insert(ignore_permissions=True)
|
}).insert(ignore_permissions=True)
|
||||||
print("Created new contact:", contact_doc.as_dict())
|
print("Created new contact:", contact_doc.as_dict())
|
||||||
else:
|
else:
|
||||||
contact_doc = frappe.get_doc("Contact", {"email_id": data.get("email")})
|
contact_doc = frappe.get_doc("Contact", {"email_id": contact_data.get("email"), "phone": contact_data.get("phone_number")})
|
||||||
print("Contact already exists:", contact_doc.as_dict())
|
print("Contact already exists:", contact_doc.as_dict())
|
||||||
contact_docs.append(contact_doc)
|
contact_docs.append(contact_doc)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import frappe
|
import frappe
|
||||||
from .utils import create_module
|
from .utils import create_module
|
||||||
|
|
||||||
|
|
@ -70,6 +71,15 @@ def add_custom_fields():
|
||||||
print("\n🔧 Adding custom fields to Address doctype...")
|
print("\n🔧 Adding custom fields to Address doctype...")
|
||||||
|
|
||||||
custom_fields = {
|
custom_fields = {
|
||||||
|
"Lead": [
|
||||||
|
dict(
|
||||||
|
fieldname="customer_type",
|
||||||
|
label="Customer Type",
|
||||||
|
fieldtype="Select",
|
||||||
|
options="Individual\nCompany\nPartnership",
|
||||||
|
insert_after="lead_name"
|
||||||
|
)
|
||||||
|
],
|
||||||
"Address": [
|
"Address": [
|
||||||
dict(
|
dict(
|
||||||
fieldname="full_address",
|
fieldname="full_address",
|
||||||
|
|
@ -315,21 +325,29 @@ def update_address_fields():
|
||||||
filled_length = int(bar_length * index // total_addresses)
|
filled_length = int(bar_length * index // total_addresses)
|
||||||
bar = '█' * filled_length + '░' * (bar_length - filled_length)
|
bar = '█' * filled_length + '░' * (bar_length - filled_length)
|
||||||
|
|
||||||
# Print a three-line, refreshing progress block to avoid terminal wrap
|
# Print a three-line, refreshing progress block without adding new lines each loop
|
||||||
progress_line = f"📊 Progress: [{bar}] {progress_percentage:3d}% ({index}/{total_addresses})"
|
progress_line = f"📊 Progress: [{bar}] {progress_percentage:3d}% ({index}/{total_addresses})"
|
||||||
counters_line = f" Fields updated: {total_field_updates} | Addresses updated: {addresses_updated}"
|
counters_line = f" Fields updated: {total_field_updates} | Addresses updated: {addresses_updated}"
|
||||||
detail_line = f" Processing: {name[:40]}..."
|
detail_line = f" Processing: {name[:40]}..."
|
||||||
|
|
||||||
if index == 1:
|
if index == 1:
|
||||||
# Save cursor position at the start of the progress block
|
# First render: write the three lines
|
||||||
print("\033[s", end='')
|
sys.stdout.write(
|
||||||
|
f"\r\033[K{progress_line}\n"
|
||||||
|
f"\033[K{counters_line}\n"
|
||||||
|
f"\033[K{detail_line}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Restore to the saved cursor position to rewrite the three-line block
|
# Move cursor up 3 lines, then rewrite each line in place
|
||||||
print("\033[u", end='')
|
sys.stdout.write("\033[2F")
|
||||||
|
sys.stdout.write(f"\r\033[K{progress_line}\n")
|
||||||
|
sys.stdout.write(f"\033[K{counters_line}\n")
|
||||||
|
sys.stdout.write(f"\033[K{detail_line}")
|
||||||
|
|
||||||
print(f"\r\033[K{progress_line}")
|
if index == total_addresses:
|
||||||
print(f"\r\033[K{counters_line}")
|
sys.stdout.write("\n")
|
||||||
print(f"\r\033[K{detail_line}", end='' if index != total_addresses else '\n', flush=True)
|
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
should_update = False
|
should_update = False
|
||||||
address = frappe.get_doc("Address", name)
|
address = frappe.get_doc("Address", name)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { IconoirProvider } from "@iconoir/vue";
|
import { IconoirProvider } from "@iconoir/vue";
|
||||||
import SideBar from "./components/SideBar.vue";
|
import SideBar from "./components/SideBar.vue";
|
||||||
import CompanySelector from "./components/CompanySelector.vue";
|
|
||||||
import { watchEffect, onMounted, ref } from "vue";
|
import { watchEffect, onMounted, ref } from "vue";
|
||||||
import { useCompanyStore } from "@/stores/company";
|
import { useCompanyStore } from "@/stores/company";
|
||||||
import { useThemeStore } from "@/stores/theme";
|
import { useThemeStore } from "@/stores/theme";
|
||||||
|
|
@ -48,7 +47,6 @@ watchEffect(() => {
|
||||||
>
|
>
|
||||||
<div id="snw-ui">
|
<div id="snw-ui">
|
||||||
<div class="sidebar-column">
|
<div class="sidebar-column">
|
||||||
<CompanySelector />
|
|
||||||
<SideBar />
|
<SideBar />
|
||||||
</div>
|
</div>
|
||||||
<div id="display-content">
|
<div id="display-content">
|
||||||
|
|
@ -84,7 +82,7 @@ watchEffect(() => {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
height: 90vh;
|
height: 90vh;
|
||||||
background: linear-gradient(145deg, var(--theme-surface) 0%, var(--theme-surface-alt) 60%);
|
background: var(--theme-gradient);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-column {
|
.sidebar-column {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ const selectedCompany = computed({
|
||||||
|
|
||||||
const fontSize = computed(() => {
|
const fontSize = computed(() => {
|
||||||
const len = selectedCompany.value.length;
|
const len = selectedCompany.value.length;
|
||||||
if (len > 12) return '0.75rem';
|
if (len > 12) return '0.71rem';
|
||||||
if (len > 10) return '0.8rem';
|
if (len > 10) return '0.75rem';
|
||||||
return '0.875rem';
|
return '0.875rem';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -41,7 +41,7 @@ const fontSize = computed(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.company-select {
|
.company-select {
|
||||||
width: 170px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.p-select) {
|
:deep(.p-select) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { ref, nextTick } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
import { useNotificationStore } from "@/stores/notifications-primevue"
|
import { useNotificationStore } from "@/stores/notifications-primevue"
|
||||||
|
import CompanySelector from "./CompanySelector.vue";
|
||||||
import {
|
import {
|
||||||
Home,
|
Home,
|
||||||
Community,
|
Community,
|
||||||
|
|
@ -153,6 +154,9 @@ const handleCategoryClick = (category) => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="sidebar" class="sidebar" :class="{ collapsed: isCollapsed }">
|
<div id="sidebar" class="sidebar" :class="{ collapsed: isCollapsed }">
|
||||||
|
<div class="sidebar-top" :class="{ collapsed: isCollapsed }">
|
||||||
|
<CompanySelector />
|
||||||
|
</div>
|
||||||
<!-- Toggle Button -->
|
<!-- Toggle Button -->
|
||||||
<button
|
<button
|
||||||
class="sidebar-toggle"
|
class="sidebar-toggle"
|
||||||
|
|
@ -282,7 +286,6 @@ const handleCategoryClick = (category) => {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 170px;
|
width: 170px;
|
||||||
min-width: 170px;
|
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
background-color: var(--theme-surface-alt);
|
background-color: var(--theme-surface-alt);
|
||||||
|
|
@ -294,6 +297,15 @@ const handleCategoryClick = (category) => {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-top {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-top.collapsed {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
#sidebar.collapsed {
|
#sidebar.collapsed {
|
||||||
width: 56px;
|
width: 56px;
|
||||||
min-width: 56px;
|
min-width: 56px;
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ const handleItem = (item) => {
|
||||||
if (typeof item?.command === "function") {
|
if (typeof item?.command === "function") {
|
||||||
item.command();
|
item.command();
|
||||||
}
|
}
|
||||||
|
toggle();
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,87 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="form-section">
|
<div>
|
||||||
<div class="section-header">
|
<div class="form-section">
|
||||||
<h3>Client Information</h3>
|
<div class="section-header">
|
||||||
<label class="toggle-container" v-if="!isEditMode">
|
<h3>Client Information</h3>
|
||||||
<v-switch v-model="isNewClient" color="success" />
|
<label class="toggle-container" v-if="!isEditMode">
|
||||||
<span class="toggle-label">New Client</span>
|
<v-switch v-model="isNewClient" color="success" />
|
||||||
</label>
|
<span class="toggle-label">New Client</span>
|
||||||
</div>
|
</label>
|
||||||
<div class="form-grid">
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-grid">
|
||||||
<label for="customer-name"> Customer Name <span class="required">*</span> </label>
|
<div class="form-field">
|
||||||
<div class="input-with-button">
|
<label for="customer-name"> Customer Name <span class="required">*</span> </label>
|
||||||
<InputText
|
<div class="input-with-button">
|
||||||
id="customer-name"
|
<InputText
|
||||||
v-model="localFormData.customerName"
|
id="customer-name"
|
||||||
:disabled="isSubmitting || isEditMode"
|
v-model="localFormData.customerName"
|
||||||
placeholder="Enter customer name"
|
:disabled="isSubmitting || isEditMode"
|
||||||
|
placeholder="Enter customer name"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Check Client"
|
||||||
|
size="small"
|
||||||
|
icon="pi pi-user-check"
|
||||||
|
class="check-btn"
|
||||||
|
@click="checkCustomerExists"
|
||||||
|
:disabled="isSubmitting || !localFormData.customerName.trim()"
|
||||||
|
>Check</Button>
|
||||||
|
<Button
|
||||||
|
v-if="!isNewClient && !isEditMode"
|
||||||
|
@click="searchCustomers"
|
||||||
|
:disabled="isSubmitting || !localFormData.customerName.trim()"
|
||||||
|
size="small"
|
||||||
|
icon="pi pi-search"
|
||||||
|
class="search-btn"
|
||||||
|
></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="customer-type"> Customer Type <span class="required">*</span> </label>
|
||||||
|
<Select
|
||||||
|
id="customer-type"
|
||||||
|
v-model="localFormData.customerType"
|
||||||
|
:options="customerTypeOptions"
|
||||||
|
:disabled="isSubmitting || (!isNewClient && !isEditMode)"
|
||||||
|
placeholder="Select customer type"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
v-if="!isNewClient && !isEditMode"
|
|
||||||
@click="searchCustomers"
|
|
||||||
:disabled="isSubmitting || !localFormData.customerName.trim()"
|
|
||||||
size="small"
|
|
||||||
icon="pi pi-search"
|
|
||||||
class="search-btn"
|
|
||||||
></Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
|
||||||
<label for="customer-type"> Customer Type <span class="required">*</span> </label>
|
|
||||||
<Select
|
|
||||||
id="customer-type"
|
|
||||||
v-model="localFormData.customerType"
|
|
||||||
:options="customerTypeOptions"
|
|
||||||
:disabled="isSubmitting || (!isNewClient && !isEditMode)"
|
|
||||||
placeholder="Select customer type"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Customer Search Results Modal -->
|
||||||
|
<Dialog
|
||||||
|
:visible="showCustomerSearchModal"
|
||||||
|
@update:visible="showCustomerSearchModal = $event"
|
||||||
|
header="Select Customer"
|
||||||
|
:modal="true"
|
||||||
|
class="search-dialog"
|
||||||
|
>
|
||||||
|
<div class="search-results">
|
||||||
|
<div v-if="customerSearchResults.length === 0" class="no-results">
|
||||||
|
<i class="pi pi-info-circle"></i>
|
||||||
|
<p>No customers found matching your search.</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="results-list">
|
||||||
|
<div
|
||||||
|
v-for="(customerName, index) in customerSearchResults"
|
||||||
|
:key="index"
|
||||||
|
class="result-item"
|
||||||
|
@click="selectCustomer(customerName)"
|
||||||
|
>
|
||||||
|
<strong>{{ customerName }}</strong>
|
||||||
|
<i class="pi pi-chevron-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="Cancel" severity="secondary" @click="showCustomerSearchModal = false" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
</template><script setup>
|
||||||
<!-- Customer Search Results Modal -->
|
|
||||||
<Dialog
|
|
||||||
v-model:visible="showCustomerSearchModal"
|
|
||||||
header="Select Customer"
|
|
||||||
:modal="true"
|
|
||||||
class="search-dialog"
|
|
||||||
>
|
|
||||||
<div class="search-results">
|
|
||||||
<div v-if="customerSearchResults.length === 0" class="no-results">
|
|
||||||
<i class="pi pi-info-circle"></i>
|
|
||||||
<p>No customers found matching your search.</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="results-list">
|
|
||||||
<div
|
|
||||||
v-for="(customerName, index) in customerSearchResults"
|
|
||||||
:key="index"
|
|
||||||
class="result-item"
|
|
||||||
@click="selectCustomer(customerName)"
|
|
||||||
>
|
|
||||||
<strong>{{ customerName }}</strong>
|
|
||||||
<i class="pi pi-chevron-right"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<Button label="Cancel" severity="secondary" @click="showCustomerSearchModal = false" />
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, watch, computed } from "vue";
|
import { ref, watch, computed } from "vue";
|
||||||
import InputText from "primevue/inputtext";
|
import InputText from "primevue/inputtext";
|
||||||
import Select from "primevue/select";
|
import Select from "primevue/select";
|
||||||
|
|
@ -109,6 +118,30 @@ const showCustomerSearchModal = ref(false);
|
||||||
const customerSearchResults = ref([]);
|
const customerSearchResults = ref([]);
|
||||||
const customerTypeOptions = ["Individual", "Partnership", "Company"];
|
const customerTypeOptions = ["Individual", "Partnership", "Company"];
|
||||||
|
|
||||||
|
const mapContactsFromClient = (contacts = []) => {
|
||||||
|
if (!Array.isArray(contacts) || contacts.length === 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
phoneNumber: "",
|
||||||
|
email: "",
|
||||||
|
contactRole: "",
|
||||||
|
isPrimary: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return contacts.map((contact, index) => ({
|
||||||
|
firstName: contact.firstName || "",
|
||||||
|
lastName: contact.lastName || "",
|
||||||
|
phoneNumber: contact.phoneNumber || contact.phone || contact.mobileNo || "",
|
||||||
|
email: contact.email || contact.emailId || contact.customEmail || "",
|
||||||
|
contactRole: contact.contactRole || contact.role || "",
|
||||||
|
isPrimary: contact.isPrimary ?? contact.isPrimaryContact ?? index === 0,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
// Watch for toggle changes
|
// Watch for toggle changes
|
||||||
watch(isNewClient, (newValue) => {
|
watch(isNewClient, (newValue) => {
|
||||||
emit("newClientToggle", newValue);
|
emit("newClientToggle", newValue);
|
||||||
|
|
@ -138,6 +171,45 @@ const searchCustomers = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkCustomerExists = async () => {
|
||||||
|
const searchTerm = localFormData.value.customerName.trim();
|
||||||
|
if (!searchTerm) 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);
|
||||||
|
} else {
|
||||||
|
notificationStore.addError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const selectCustomer = async (customerName) => {
|
const selectCustomer = async (customerName) => {
|
||||||
try {
|
try {
|
||||||
// Fetch full customer data
|
// Fetch full customer data
|
||||||
|
|
@ -145,6 +217,7 @@ const selectCustomer = async (customerName) => {
|
||||||
|
|
||||||
localFormData.value.customerName = clientData.customerName;
|
localFormData.value.customerName = clientData.customerName;
|
||||||
localFormData.value.customerType = clientData.customerType;
|
localFormData.value.customerType = clientData.customerType;
|
||||||
|
localFormData.value.contacts = mapContactsFromClient(clientData.contacts);
|
||||||
showCustomerSearchModal.value = false;
|
showCustomerSearchModal.value = false;
|
||||||
|
|
||||||
// Pass the full client data including contacts
|
// Pass the full client data including contacts
|
||||||
|
|
@ -306,6 +379,24 @@ defineExpose({
|
||||||
background: var(--surface-hover);
|
background: var(--surface-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.check-btn {
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
background: var(--surface-card);
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
min-width: 8rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-btn:hover:not(:disabled) {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
.search-btn {
|
.search-btn {
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
border: 1px solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
|
|
@ -317,6 +408,7 @@ defineExpose({
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
color: white;
|
color: white;
|
||||||
|
min-width: 8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-btn:disabled {
|
.search-btn:disabled {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { defineStore } from "pinia";
|
||||||
|
|
||||||
export const useCompanyStore = defineStore("company", {
|
export const useCompanyStore = defineStore("company", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
companies: ["SprinklersNW", "Nuco Yard Care", "Lowe Fencing", "Veritas Stone", "Daniel's Landscape Supplies"],
|
companies: ["Sprinklers Northwest", "Nuco Yard Care", "Lowe Fencing", "Veritas Stone", "Daniels Landscape Supplies"],
|
||||||
selectedCompany: "SprinklersNW",
|
selectedCompany: "Sprinklers Northwest",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
const themeMap = {
|
const themeMap = {
|
||||||
SprinklersNW: {
|
"Sprinklers Northwest": {
|
||||||
primary: "#0f7ac7",
|
primary: "#75bdf1ff",
|
||||||
primaryStrong: "#0c639f",
|
primaryStrong: "#068ee9ff",
|
||||||
secondary: "#2ca66f",
|
secondary: "#2ca66f",
|
||||||
accent: "#8fd9a8",
|
accent: "#8fd9a8",
|
||||||
primaryGradientStart: "#0c639f",
|
primaryGradientStart: "#70b9e9ff",
|
||||||
primaryGradientEnd: "#1390e0",
|
primaryGradientEnd: "#1390e0",
|
||||||
|
primaryGradient: "linear-gradient(90deg, #a6d2f0ff 0%, #a1d7f8ff 100%)",
|
||||||
secondaryGradientStart: "#2c9b64",
|
secondaryGradientStart: "#2c9b64",
|
||||||
secondaryGradientEnd: "#38c487",
|
secondaryGradientEnd: "#38c487",
|
||||||
|
secondaryGradient: "linear-gradient(90deg, #2c9b64 0%, #38c487 100%)",
|
||||||
surface: "#cadcf1ff",
|
surface: "#cadcf1ff",
|
||||||
surfaceAlt: "#dbfdefff",
|
surfaceAlt: "#dbfdefff",
|
||||||
border: "#cfe5f7",
|
border: "#cfe5f7",
|
||||||
|
|
@ -26,8 +28,10 @@ const themeMap = {
|
||||||
accent: "#f7de6d",
|
accent: "#f7de6d",
|
||||||
primaryGradientStart: "#2f6425",
|
primaryGradientStart: "#2f6425",
|
||||||
primaryGradientEnd: "#4a9f3a",
|
primaryGradientEnd: "#4a9f3a",
|
||||||
|
primaryGradient: "linear-gradient(90deg, #2f6425 0%, #4a9f3a 100%)",
|
||||||
secondaryGradientStart: "#d2a106",
|
secondaryGradientStart: "#d2a106",
|
||||||
secondaryGradientEnd: "#f7de6d",
|
secondaryGradientEnd: "#f7de6d",
|
||||||
|
secondaryGradient: "linear-gradient(90deg, #d2a106 0%, #f7de6d 100%)",
|
||||||
surface: "#f7fbe9",
|
surface: "#f7fbe9",
|
||||||
surfaceAlt: "#f1f4d6",
|
surfaceAlt: "#f1f4d6",
|
||||||
border: "#dfe8b5",
|
border: "#dfe8b5",
|
||||||
|
|
@ -44,8 +48,10 @@ const themeMap = {
|
||||||
accent: "#9cc6ff",
|
accent: "#9cc6ff",
|
||||||
primaryGradientStart: "#232d3f",
|
primaryGradientStart: "#232d3f",
|
||||||
primaryGradientEnd: "#375073",
|
primaryGradientEnd: "#375073",
|
||||||
|
primaryGradient: "linear-gradient(90deg, #232d3f 0%, #375073 100%)",
|
||||||
secondaryGradientStart: "#4f8ee5",
|
secondaryGradientStart: "#4f8ee5",
|
||||||
secondaryGradientEnd: "#5fa4ff",
|
secondaryGradientEnd: "#5fa4ff",
|
||||||
|
secondaryGradient: "linear-gradient(90deg, #4f8ee5 0%, #5fa4ff 100%)",
|
||||||
surface: "#f5f7fb",
|
surface: "#f5f7fb",
|
||||||
surfaceAlt: "#e7ecf5",
|
surfaceAlt: "#e7ecf5",
|
||||||
border: "#ced6e5",
|
border: "#ced6e5",
|
||||||
|
|
@ -62,8 +68,10 @@ const themeMap = {
|
||||||
accent: "#d8d0c5",
|
accent: "#d8d0c5",
|
||||||
primaryGradientStart: "#5e564d",
|
primaryGradientStart: "#5e564d",
|
||||||
primaryGradientEnd: "#8a8073",
|
primaryGradientEnd: "#8a8073",
|
||||||
|
primaryGradient: "linear-gradient(90deg, #5e564d 0%, #8a8073 100%)",
|
||||||
secondaryGradientStart: "#b2a89c",
|
secondaryGradientStart: "#b2a89c",
|
||||||
secondaryGradientEnd: "#cfc6b8",
|
secondaryGradientEnd: "#cfc6b8",
|
||||||
|
secondaryGradient: "linear-gradient(90deg, #b2a89c 0%, #cfc6b8 100%)",
|
||||||
surface: "#f7f5f2",
|
surface: "#f7f5f2",
|
||||||
surfaceAlt: "#ebe6df",
|
surfaceAlt: "#ebe6df",
|
||||||
border: "#d8d0c5",
|
border: "#d8d0c5",
|
||||||
|
|
@ -73,15 +81,17 @@ const themeMap = {
|
||||||
textDark: "#231c16",
|
textDark: "#231c16",
|
||||||
textLight: "#ffffff",
|
textLight: "#ffffff",
|
||||||
},
|
},
|
||||||
"Daniel's Landscape Supplies": {
|
"Daniels Landscape Supplies": {
|
||||||
primary: "#2f6b2f",
|
primary: "#2f6b2f",
|
||||||
primaryStrong: "#245224",
|
primaryStrong: "#245224",
|
||||||
secondary: "#f28c28",
|
secondary: "#f28c28",
|
||||||
accent: "#ffc174",
|
accent: "#ffc174",
|
||||||
primaryGradientStart: "#245224",
|
primaryGradientStart: "#245224",
|
||||||
primaryGradientEnd: "#3a8a3a",
|
primaryGradientEnd: "#3a8a3a",
|
||||||
|
primaryGradient: "linear-gradient(90deg, #245224 0%, #3a8a3a 100%)",
|
||||||
secondaryGradientStart: "#f28c28",
|
secondaryGradientStart: "#f28c28",
|
||||||
secondaryGradientEnd: "#ffc174",
|
secondaryGradientEnd: "#ffc174",
|
||||||
|
secondaryGradient: "linear-gradient(90deg, #f28c28 0%, #ffc174 100%)",
|
||||||
surface: "#f8fbf4",
|
surface: "#f8fbf4",
|
||||||
surfaceAlt: "#f2f1e9",
|
surfaceAlt: "#f2f1e9",
|
||||||
border: "#d9e5cc",
|
border: "#d9e5cc",
|
||||||
|
|
@ -109,8 +119,10 @@ export const useThemeStore = defineStore("theme", {
|
||||||
root.style.setProperty("--theme-accent", theme.accent);
|
root.style.setProperty("--theme-accent", theme.accent);
|
||||||
root.style.setProperty("--theme-gradient-start", theme.primaryGradientStart);
|
root.style.setProperty("--theme-gradient-start", theme.primaryGradientStart);
|
||||||
root.style.setProperty("--theme-gradient-end", theme.primaryGradientEnd);
|
root.style.setProperty("--theme-gradient-end", theme.primaryGradientEnd);
|
||||||
|
root.style.setProperty("--theme-gradient", theme.primaryGradient);
|
||||||
root.style.setProperty("--theme-secondary-gradient-start", theme.secondaryGradientStart);
|
root.style.setProperty("--theme-secondary-gradient-start", theme.secondaryGradientStart);
|
||||||
root.style.setProperty("--theme-secondary-gradient-end", theme.secondaryGradientEnd);
|
root.style.setProperty("--theme-secondary-gradient-end", theme.secondaryGradientEnd);
|
||||||
|
root.style.setProperty("--theme-secondary-gradient", theme.secondaryGradient);
|
||||||
root.style.setProperty("--theme-surface", theme.surface);
|
root.style.setProperty("--theme-surface", theme.surface);
|
||||||
root.style.setProperty("--theme-surface-alt", theme.surfaceAlt);
|
root.style.setProperty("--theme-surface-alt", theme.surfaceAlt);
|
||||||
root.style.setProperty("--theme-border", theme.border);
|
root.style.setProperty("--theme-border", theme.border);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue