update with migration data

This commit is contained in:
Casey 2026-02-19 15:06:14 -06:00
parent d84c9fd20c
commit 30031c4c56
13 changed files with 489314 additions and 11952 deletions

Binary file not shown.

View File

@ -2,6 +2,7 @@ import click
import os
import subprocess
import frappe
import json
from custom_ui.utils import create_module
from custom_ui.api.db.general import search_any_field
from custom_ui.install import create_companies, create_project_templates, create_task_types, create_tasks, create_bid_meeting_note_form_templates
@ -104,6 +105,142 @@ def create_module_command():
def setup_custom_ui():
pass
@click.command("import-aspire-migration")
@click.option("--path", required=True, help="Path to the migration output directory containing JSON files")
@click.option("--dry-run", is_flag=True, default=False, help="Print what would be done without inserting")
def import_aspire_migration(path, dry_run):
"""Import Aspire migration JSON files into ERPNext in dependency order."""
frappe.connect()
customers_file = os.path.join(path, "customers.json")
contacts_file = os.path.join(path, "contacts.json")
addresses_file = os.path.join(path, "addresses.json")
updates_file = os.path.join(path, "customer_updates.json")
for f in [customers_file, contacts_file, addresses_file, updates_file]:
if not os.path.exists(f):
click.echo(f"❌ Missing file: {f}")
return
# --- Step 1: Insert Customers ---
click.echo("📦 Step 1: Inserting Customers...")
with open(customers_file) as f:
customers = json.load(f)
success, skipped, failed = 0, 0, 0
for i, rec in enumerate(customers):
if dry_run:
click.echo(f" [DRY RUN] Would insert Customer: {rec.get('customer_name')}")
continue
try:
if frappe.db.exists("Customer", rec.get("customer_name")):
skipped += 1
continue
doc = frappe.get_doc(rec)
doc.insert(ignore_permissions=True)
success += 1
except Exception as e:
failed += 1
click.echo(f" ⚠️ Customer '{rec.get('customer_name')}': {e}")
if (i + 1) % 500 == 0:
frappe.db.commit()
click.echo(f" ... committed {i + 1}/{len(customers)}")
frappe.db.commit()
click.echo(f" ✅ Customers — inserted: {success}, skipped: {skipped}, failed: {failed}")
# --- Step 2: Insert Contacts ---
click.echo("📦 Step 2: Inserting Contacts...")
with open(contacts_file) as f:
contacts = json.load(f)
success, skipped, failed = 0, 0, 0
for i, rec in enumerate(contacts):
if dry_run:
click.echo(f" [DRY RUN] Would insert Contact: {rec.get('first_name')} {rec.get('last_name')}")
continue
try:
doc = frappe.get_doc(rec)
doc.insert(ignore_permissions=True)
success += 1
except Exception as e:
failed += 1
name = f"{rec.get('first_name', '')} {rec.get('last_name', '')}"
click.echo(f" ⚠️ Contact '{name}': {e}")
if (i + 1) % 500 == 0:
frappe.db.commit()
click.echo(f" ... committed {i + 1}/{len(contacts)}")
frappe.db.commit()
click.echo(f" ✅ Contacts — inserted: {success}, skipped: {skipped}, failed: {failed}")
# --- Step 3: Insert Addresses ---
click.echo("📦 Step 3: Inserting Addresses...")
with open(addresses_file) as f:
addresses = json.load(f)
success, skipped, failed = 0, 0, 0
for i, rec in enumerate(addresses):
if dry_run:
click.echo(f" [DRY RUN] Would insert Address: {rec.get('address_line1')}")
continue
try:
doc = frappe.get_doc(rec)
doc.insert(ignore_permissions=True)
success += 1
except Exception as e:
failed += 1
click.echo(f" ⚠️ Address '{rec.get('address_line1', '?')}': {e}")
if (i + 1) % 500 == 0:
frappe.db.commit()
click.echo(f" ... committed {i + 1}/{len(addresses)}")
frappe.db.commit()
click.echo(f" ✅ Addresses — inserted: {success}, skipped: {skipped}, failed: {failed}")
# --- Step 4: Update Customers with child tables ---
click.echo("📦 Step 4: Updating Customers with contact/property links...")
with open(updates_file) as f:
updates = json.load(f)
success, skipped, failed = 0, 0, 0
for i, rec in enumerate(updates):
customer_name = rec.get("customer_name")
if dry_run:
click.echo(f" [DRY RUN] Would update Customer: {customer_name}")
continue
try:
if not frappe.db.exists("Customer", customer_name):
skipped += 1
continue
doc = frappe.get_doc("Customer", customer_name)
for contact_row in rec.get("contacts", []):
doc.append("contacts", contact_row)
for property_row in rec.get("properties", []):
doc.append("properties", property_row)
doc.save(ignore_permissions=True)
success += 1
except Exception as e:
failed += 1
click.echo(f" ⚠️ Update '{customer_name}': {e}")
if (i + 1) % 500 == 0:
frappe.db.commit()
click.echo(f" ... committed {i + 1}/{len(updates)}")
frappe.db.commit()
click.echo(f" ✅ Updates — applied: {success}, skipped: {skipped}, failed: {failed}")
click.echo("🎉 Migration complete!")
frappe.destroy()
commands = [build_frontend, create_module_command]

View File

@ -1,795 +0,0 @@
[
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:22.939672",
"name": "System Manager",
"restrict_to_domain": null,
"role_name": "System Manager",
"two_factor_auth": 0
},
{
"desk_access": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:22.949185",
"name": "Guest",
"restrict_to_domain": null,
"role_name": "Guest",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:22.957171",
"name": "Administrator",
"restrict_to_domain": null,
"role_name": "Administrator",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:22.966723",
"name": "All",
"restrict_to_domain": null,
"role_name": "All",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:22.974828",
"name": "Desk User",
"restrict_to_domain": null,
"role_name": "Desk User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:23.925708",
"name": "Website Manager",
"restrict_to_domain": null,
"role_name": "Website Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:24.685089",
"name": "Dashboard Manager",
"restrict_to_domain": null,
"role_name": "Dashboard Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:26.798862",
"name": "Workspace Manager",
"restrict_to_domain": null,
"role_name": "Workspace Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:27.293623",
"name": "Report Manager",
"restrict_to_domain": null,
"role_name": "Report Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:29.149829",
"name": "Script Manager",
"restrict_to_domain": null,
"role_name": "Script Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:36.945427",
"name": "Inbox User",
"restrict_to_domain": null,
"role_name": "Inbox User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:39.761649",
"name": "Prepared Report User",
"restrict_to_domain": null,
"role_name": "Prepared Report User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:43.947516",
"name": "Blogger",
"restrict_to_domain": null,
"role_name": "Blogger",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:46.346917",
"name": "Newsletter Manager",
"restrict_to_domain": null,
"role_name": "Newsletter Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:47.243381",
"name": "Knowledge Base Contributor",
"restrict_to_domain": null,
"role_name": "Knowledge Base Contributor",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:47.262953",
"name": "Knowledge Base Editor",
"restrict_to_domain": null,
"role_name": "Knowledge Base Editor",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:56.661394",
"name": "Accounts Manager",
"restrict_to_domain": null,
"role_name": "Accounts Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:56.674846",
"name": "Purchase User",
"restrict_to_domain": null,
"role_name": "Purchase User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:56.683274",
"name": "Sales User",
"restrict_to_domain": null,
"role_name": "Sales User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:14:56.701284",
"name": "Accounts User",
"restrict_to_domain": null,
"role_name": "Accounts User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:15:07.963749",
"name": "Sales Master Manager",
"restrict_to_domain": null,
"role_name": "Sales Master Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:15:07.979912",
"name": "Maintenance Manager",
"restrict_to_domain": null,
"role_name": "Maintenance Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:15:08.013321",
"name": "Sales Manager",
"restrict_to_domain": null,
"role_name": "Sales Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:15:08.040169",
"name": "Maintenance User",
"restrict_to_domain": null,
"role_name": "Maintenance User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:15:08.049510",
"name": "Purchase Master Manager",
"restrict_to_domain": null,
"role_name": "Purchase Master Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:15:08.058859",
"name": "Purchase Manager",
"restrict_to_domain": null,
"role_name": "Purchase Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:15:16.260699",
"name": "Translator",
"restrict_to_domain": null,
"role_name": "Translator",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:15:24.541181",
"name": "Auditor",
"restrict_to_domain": null,
"role_name": "Auditor",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:15:28.563445",
"name": "Employee",
"restrict_to_domain": null,
"role_name": "Employee",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:15:28.593109",
"name": "Stock User",
"restrict_to_domain": null,
"role_name": "Stock User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:15:54.780680",
"name": "HR Manager",
"restrict_to_domain": null,
"role_name": "HR Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:00.248577",
"name": "Manufacturing Manager",
"restrict_to_domain": null,
"role_name": "Manufacturing Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:01.617491",
"name": "Stock Manager",
"restrict_to_domain": null,
"role_name": "Stock Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:04.521100",
"name": "Projects User",
"restrict_to_domain": null,
"role_name": "Projects User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:05.499661",
"name": "Projects Manager",
"restrict_to_domain": null,
"role_name": "Projects Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:06.888198",
"name": "Manufacturing User",
"restrict_to_domain": null,
"role_name": "Manufacturing User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:06.962029",
"name": "HR User",
"restrict_to_domain": null,
"role_name": "HR User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:15.058111",
"name": "Item Manager",
"restrict_to_domain": null,
"role_name": "Item Manager",
"two_factor_auth": 0
},
{
"desk_access": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:17:19.247810",
"name": "Customer",
"restrict_to_domain": null,
"role_name": "Customer",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:19.053592",
"name": "Delivery Manager",
"restrict_to_domain": null,
"role_name": "Delivery Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:19.110307",
"name": "Delivery User",
"restrict_to_domain": null,
"role_name": "Delivery User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:19.124541",
"name": "Fleet Manager",
"restrict_to_domain": null,
"role_name": "Fleet Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:19.602602",
"name": "Academics User",
"restrict_to_domain": null,
"role_name": "Academics User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:38.054879",
"name": "Fulfillment User",
"restrict_to_domain": null,
"role_name": "Fulfillment User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:41.401518",
"name": "Quality Manager",
"restrict_to_domain": null,
"role_name": "Quality Manager",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:51.029966",
"name": "Support Team",
"restrict_to_domain": null,
"role_name": "Support Team",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:58.882176",
"name": "Agriculture User",
"restrict_to_domain": null,
"role_name": "Agriculture User",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:16:58.966355",
"name": "Agriculture Manager",
"restrict_to_domain": null,
"role_name": "Agriculture Manager",
"two_factor_auth": 0
},
{
"desk_access": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:17:19.249024",
"name": "Supplier",
"restrict_to_domain": null,
"role_name": "Supplier",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2024-04-04 04:17:16.072081",
"name": "Analytics",
"restrict_to_domain": null,
"role_name": "Analytics",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": "/app",
"is_custom": 0,
"modified": "2025-01-28 15:46:59.075095",
"name": "Technician",
"restrict_to_domain": "Service",
"role_name": "Technician",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2025-01-13 10:13:13.163560",
"name": "Interviewer",
"restrict_to_domain": null,
"role_name": "Interviewer",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2025-01-13 10:13:15.161152",
"name": "Expense Approver",
"restrict_to_domain": null,
"role_name": "Expense Approver",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2025-01-13 10:13:16.149250",
"name": "Leave Approver",
"restrict_to_domain": null,
"role_name": "Leave Approver",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 1,
"modified": "2025-01-13 10:13:27.355987",
"name": "Employee Self Service",
"restrict_to_domain": null,
"role_name": "Employee Self Service",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": "/app/snw-foreman",
"is_custom": 0,
"modified": "2025-04-17 11:54:33.174189",
"name": "SNW Foreman",
"restrict_to_domain": null,
"role_name": "SNW Foreman",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": "/app/snw-front-office",
"is_custom": 0,
"modified": "2025-05-02 04:52:34.365177",
"name": "SNW Front Office",
"restrict_to_domain": null,
"role_name": "SNW Front Office",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2025-05-02 06:37:19.711082",
"name": "SNW Install Admin",
"restrict_to_domain": null,
"role_name": "SNW Install Admin",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2025-05-02 09:02:42.978358",
"name": "Nuco Admin",
"restrict_to_domain": null,
"role_name": "Nuco Admin",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2025-05-02 09:02:53.410754",
"name": "Lowe Admin",
"restrict_to_domain": null,
"role_name": "Lowe Admin",
"two_factor_auth": 0
},
{
"desk_access": 1,
"disabled": 0,
"docstatus": 0,
"doctype": "Role",
"home_page": null,
"is_custom": 0,
"modified": "2025-05-02 09:07:40.352031",
"name": "SNW Service Admin",
"restrict_to_domain": null,
"role_name": "SNW Service Admin",
"two_factor_auth": 0
}
]

View File

@ -1,725 +0,0 @@
[
{
"docstatus": 0,
"doctype": "Role Profile",
"modified": "2024-04-04 04:17:19.023597",
"name": "Inventory",
"role_profile": "Inventory",
"roles": [
{
"parent": "Inventory",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Stock User"
},
{
"parent": "Inventory",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Stock Manager"
},
{
"parent": "Inventory",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Item Manager"
}
]
},
{
"docstatus": 0,
"doctype": "Role Profile",
"modified": "2024-04-04 04:17:19.035761",
"name": "Manufacturing",
"role_profile": "Manufacturing",
"roles": [
{
"parent": "Manufacturing",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Stock User"
},
{
"parent": "Manufacturing",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Manufacturing User"
},
{
"parent": "Manufacturing",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Manufacturing Manager"
}
]
},
{
"docstatus": 0,
"doctype": "Role Profile",
"modified": "2024-12-30 09:14:46.881813",
"name": "Accounts",
"role_profile": "Accounts",
"roles": [
{
"parent": "Accounts",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Accounts User"
},
{
"parent": "Accounts",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Accounts Manager"
},
{
"parent": "Accounts",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Academics User"
}
]
},
{
"docstatus": 0,
"doctype": "Role Profile",
"modified": "2024-04-04 04:17:19.070987",
"name": "Purchase",
"role_profile": "Purchase",
"roles": [
{
"parent": "Purchase",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Item Manager"
},
{
"parent": "Purchase",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Stock User"
},
{
"parent": "Purchase",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Purchase User"
},
{
"parent": "Purchase",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Purchase Manager"
}
]
},
{
"docstatus": 0,
"doctype": "Role Profile",
"modified": "2025-02-10 14:08:13.619290",
"name": "System Manager",
"role_profile": "System Manager",
"roles": [
{
"parent": "System Manager",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "System Manager"
}
]
},
{
"docstatus": 0,
"doctype": "Role Profile",
"modified": "2025-01-28 13:40:31.163924",
"name": "HR",
"role_profile": "HR & Admin",
"roles": [
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "HR User"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "HR Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Leave Approver"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Expense Approver"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Dashboard Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "System Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Accounts Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Agriculture Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Analytics"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Auditor"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Delivery Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Fleet Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Inbox User"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Interviewer"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Item Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Maintenance Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Manufacturing Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Newsletter Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Prepared Report User"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Projects Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Purchase Master Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Quality Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Report Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Sales Master Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Stock Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Workspace Manager"
},
{
"parent": "HR",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Website Manager"
}
]
},
{
"docstatus": 0,
"doctype": "Role Profile",
"modified": "2025-01-28 15:49:20.863461",
"name": "Technician",
"role_profile": "Technician",
"roles": [
{
"parent": "Technician",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Projects User"
},
{
"parent": "Technician",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Maintenance User"
},
{
"parent": "Technician",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Stock User"
}
]
},
{
"docstatus": 0,
"doctype": "Role Profile",
"modified": "2025-02-10 14:33:01.847181",
"name": "Admin",
"role_profile": "Admin",
"roles": [
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Academics User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Accounts Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Agriculture Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Analytics"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Auditor"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Blogger"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Dashboard Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Delivery Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Fleet Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Fulfillment User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "HR User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Inbox User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Item Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Knowledge Base Editor"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Maintenance Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Maintenance User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Manufacturing Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Newsletter Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Prepared Report User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Projects Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Purchase Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Purchase Master Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Quality Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Report Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Sales Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Sales Master Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Script Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Stock Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Supplier"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Support Team"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "System Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Translator"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Website Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Workspace Manager"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Accounts User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Agriculture User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Delivery User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Employee"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Sales User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Stock User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Purchase User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Projects User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Manufacturing User"
},
{
"parent": "Admin",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Knowledge Base Contributor"
}
]
},
{
"docstatus": 0,
"doctype": "Role Profile",
"modified": "2025-02-12 10:37:54.633409",
"name": "Sales",
"role_profile": "Sales",
"roles": [
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Sales User"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Stock User"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Projects User"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Purchase User"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Maintenance User"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Delivery User"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Fulfillment User"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Agriculture User"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Support Team"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Prepared Report User"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Manufacturing User"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Inbox User"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Employee Self Service"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Employee"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Blogger"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Workspace Manager"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Website Manager"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "System Manager"
},
{
"parent": "Sales",
"parentfield": "roles",
"parenttype": "Role Profile",
"role": "Accounts Manager"
}
]
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -118,7 +118,7 @@
v-model="address.contacts"
:options="contactOptions"
optionLabel="label"
optionValue="value"
dataKey="value"
:disabled="isSubmitting || contactOptions.length === 0"
placeholder="Select contacts"
class="w-full"
@ -130,10 +130,10 @@
<Select
:id="`primaryContact-${index}`"
v-model="address.primaryContact"
:options="address.contacts.map(c => contactOptions.find(opt => opt.value === c))"
:options="address.contacts"
optionLabel="label"
optionValue="value"
:disabled="isSubmitting || contactOptions.length === 0"
dataKey="value"
:disabled="isSubmitting || !address.contacts || address.contacts.length === 0"
placeholder="Select primary contact"
class="w-full"
/>
@ -208,8 +208,13 @@ const localFormData = computed({
});
const contactOptions = computed(() => {
// When contactOptions prop is provided (e.g. from modal with merged contacts), use it
if (props.contactOptions && props.contactOptions.length > 0) {
return props.contactOptions;
}
// Fallback: derive from localFormData.contacts (e.g. client creation wizard)
if (!localFormData.value.contacts || localFormData.value.contacts.length === 0) {
return props.contactOptions;
return [];
}
return localFormData.value.contacts.map((contact, index) => ({
label: `${contact.firstName || ""} ${contact.lastName || ""}`.trim() || `Contact ${index + 1}`,
@ -262,21 +267,25 @@ const handleBillingChange = (selectedIndex) => {
}
});
// Auto-select all contacts
// Auto-select all contacts (store full objects)
if (contactOptions.value.length > 0) {
localFormData.value.addresses[selectedIndex].contacts = contactOptions.value.map(
(opt) => opt.value,
);
localFormData.value.addresses[selectedIndex].contacts = [...contactOptions.value];
}
// Auto-select primary contact
if (localFormData.value.contacts && localFormData.value.contacts.length > 0) {
const primaryIndex = localFormData.value.contacts.findIndex((c) => c.isPrimary);
if (primaryIndex !== -1) {
localFormData.value.addresses[selectedIndex].primaryContact = primaryIndex;
// Auto-select primary contact (store full object)
const allOpts = contactOptions.value;
if (allOpts.length > 0) {
// Try to find the primary from localFormData contacts
if (localFormData.value.contacts && localFormData.value.contacts.length > 0) {
const primaryIndex = localFormData.value.contacts.findIndex((c) => c.isPrimary);
if (primaryIndex !== -1) {
const primaryOpt = allOpts.find((o) => o.value === primaryIndex);
localFormData.value.addresses[selectedIndex].primaryContact = primaryOpt || allOpts[0];
} else {
localFormData.value.addresses[selectedIndex].primaryContact = allOpts[0];
}
} else {
// Fallback to first contact if no primary found
localFormData.value.addresses[selectedIndex].primaryContact = 0;
localFormData.value.addresses[selectedIndex].primaryContact = allOpts[0];
}
}
} else {

View File

@ -1,18 +1,47 @@
<template>
<Dialog :visible="visible" @update:visible="val => emit('update:visible', val)" modal :closable="false" :style="{ width: '700px', maxWidth: '95vw' }">
<template #header>
<span class="modal-title">Add Contact/Address</span>
<span class="modal-title">Add Contact / Address</span>
</template>
<div class="modal-body">
<ContactInformationForm
:formData="contactFormData.value"
@update:formData="val => contactFormData.value = val"
<!-- Toggle buttons -->
<div class="toggle-buttons">
<Button
:label="showContacts ? 'Hide Contacts' : 'Add Contacts'"
:icon="showContacts ? 'pi pi-minus' : 'pi pi-user-plus'"
:severity="showContacts ? 'secondary' : 'primary'"
:outlined="!showContacts"
@click="showContacts = !showContacts"
size="small"
/>
<Button
:label="showAddresses ? 'Hide Addresses' : 'Add Addresses'"
:icon="showAddresses ? 'pi pi-minus' : 'pi pi-map-marker'"
:severity="showAddresses ? 'secondary' : 'primary'"
:outlined="!showAddresses"
@click="showAddresses = !showAddresses"
size="small"
/>
</div>
<!-- Hint when nothing is selected -->
<div v-if="!showContacts && !showAddresses" class="empty-hint">
<i class="pi pi-info-circle"></i>
<span>Click a button above to add contacts or addresses to this client.</span>
</div>
<!-- Contact form -->
<ModalContactForm
v-if="showContacts"
:contacts="newContacts"
:isSubmitting="isSubmitting"
:existingContacts="existingContacts"
/>
<AddressInformationForm
:formData="addressFormData.value"
@update:formData="val => addressFormData.value = val"
<!-- Address form -->
<ModalAddressForm
v-if="showAddresses"
:addresses="newAddresses"
:isSubmitting="isSubmitting"
:contactOptions="allContactOptions"
:existingAddresses="existingAddresses"
@ -20,7 +49,13 @@
</div>
<template #footer>
<Button label="Cancel" @click="close" severity="secondary" />
<Button label="Create" @click="create" severity="primary" :loading="isSubmitting" />
<Button
label="Create"
@click="create"
severity="primary"
:loading="isSubmitting"
:disabled="!showContacts && !showAddresses"
/>
</template>
</Dialog>
</template>
@ -29,8 +64,8 @@
import { ref, computed, watch } from 'vue';
import Dialog from 'primevue/dialog';
import Button from 'primevue/button';
import ContactInformationForm from '../clientSubPages/ContactInformationForm.vue';
import AddressInformationForm from '../clientSubPages/AddressInformationForm.vue';
import ModalContactForm from './ModalContactForm.vue';
import ModalAddressForm from './ModalAddressForm.vue';
const props = defineProps({
visible: Boolean,
@ -41,46 +76,75 @@ const props = defineProps({
});
const emit = defineEmits(['update:visible', 'created']);
const contactFormData = ref({ contacts: [] });
const addressFormData = ref({ addresses: [], contacts: [] });
const showContacts = ref(false);
const showAddresses = ref(false);
// Keep addressFormData.contacts in sync with new contacts
watch(
() => contactFormData.value.contacts,
(newContacts) => {
addressFormData.value.contacts = newContacts || [];
},
{ deep: true }
);
// Direct arrays instead of wrapping in formData objects
const newContacts = ref([
{ firstName: '', lastName: '', phoneNumber: '', email: '', contactRole: '', isPrimary: true },
]);
const newAddresses = ref([
{ addressLine1: '', addressLine2: '', pincode: '', city: '', state: '', contacts: [], primaryContact: null, zipcodeLookupDisabled: true },
]);
// All contact options = clientContacts + new contacts
// Reset forms when modal opens
watch(() => props.visible, (isVisible) => {
if (isVisible) {
showContacts.value = false;
showAddresses.value = false;
newContacts.value = [
{ firstName: '', lastName: '', phoneNumber: '', email: '', contactRole: '', isPrimary: true },
];
newAddresses.value = [
{ addressLine1: '', addressLine2: '', pincode: '', city: '', state: '', contacts: [], primaryContact: null, zipcodeLookupDisabled: true },
];
}
});
// All contact options = existing client contacts + new contacts being created in this modal
const allContactOptions = computed(() => {
const clientOpts = (props.clientContacts || []).map((c, idx) => ({
label: `${c.firstName || ''} ${c.lastName || ''}`.trim() || `Contact ${idx + 1}`,
value: `client-${idx}`,
...c,
}));
const newOpts = (contactFormData.value.contacts || []).map((c, idx) => ({
label: `${c.firstName || ''} ${c.lastName || ''}`.trim() || `Contact ${idx + 1}`,
value: `new-${idx}`,
...c,
}));
const clientOpts = (props.clientContacts || []).map((c, idx) => {
const firstName = c.firstName || c.first_name || '';
const lastName = c.lastName || c.last_name || '';
const email = c.email || c.emailId || c.email_id || c.customEmail || '';
return {
label: `${firstName} ${lastName}`.trim() || c.fullName || c.name || `Contact ${idx + 1}`,
value: `client-${idx}`,
firstName,
lastName,
email,
};
});
const newOpts = showContacts.value
? (newContacts.value || []).map((c, idx) => {
const firstName = c.firstName || '';
const lastName = c.lastName || '';
const email = c.email || '';
return {
label: `${firstName} ${lastName}`.trim() || `New Contact ${idx + 1}`,
value: `new-${idx}`,
firstName,
lastName,
email,
};
})
: [];
return [...clientOpts, ...newOpts];
});
function close() {
emit('update:visible', false);
}
function create() {
// Dummy create handler
console.log('Create clicked', {
contacts: contactFormData.value.contacts,
addresses: addressFormData.value.addresses,
});
emit('created', {
contacts: contactFormData.value.contacts,
addresses: addressFormData.value.addresses,
});
const payload = {};
if (showContacts.value) {
payload.contacts = newContacts.value;
}
if (showAddresses.value) {
payload.addresses = newAddresses.value;
}
emit('created', payload);
close();
}
</script>
@ -93,7 +157,25 @@ function create() {
.modal-body {
display: flex;
flex-direction: column;
gap: 1.5rem;
gap: 1rem;
padding: 0.5rem 0;
}
.toggle-buttons {
display: flex;
gap: 0.75rem;
}
.empty-hint {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem;
background: var(--surface-ground);
border-radius: 6px;
color: var(--text-color-secondary);
font-size: 0.9rem;
}
.empty-hint i {
font-size: 1rem;
color: var(--primary-color);
}
</style>

View File

@ -0,0 +1,362 @@
<template>
<div class="form-section">
<div class="section-header">
<i class="pi pi-map-marker" style="color: var(--primary-color); font-size: 1.2rem;"></i>
<h3>Add Addresses</h3>
</div>
<div class="form-grid">
<div
v-for="(address, index) in addresses"
:key="index"
class="address-item"
:class="{ 'existing-highlight': isExistingAddress(address) }"
>
<div class="address-header">
<div class="address-title">
<i class="pi pi-home" style="font-size: 0.9rem; color: var(--primary-color);"></i>
<h4>Address {{ index + 1 }}</h4>
</div>
<Button
v-if="addresses.length > 1"
@click="removeAddress(index)"
size="small"
severity="danger"
icon="pi pi-trash"
class="remove-btn"
/>
</div>
<div class="address-fields">
<div class="form-field full-width">
<label :for="`modal-addr-line1-${index}`">
<i class="pi pi-map" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Address Line 1 <span class="required">*</span>
</label>
<InputText
:id="`modal-addr-line1-${index}`"
v-model="address.addressLine1"
:disabled="isSubmitting"
placeholder="Street address"
class="w-full"
@input="formatAddressLine(index, 'addressLine1', $event)"
/>
</div>
<div class="form-field full-width">
<label :for="`modal-addr-line2-${index}`"><i class="pi pi-map" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Address Line 2</label>
<InputText
:id="`modal-addr-line2-${index}`"
v-model="address.addressLine2"
:disabled="isSubmitting"
placeholder="Apt, suite, unit, etc."
class="w-full"
@input="formatAddressLine(index, 'addressLine2', $event)"
/>
</div>
<div class="form-row">
<div class="form-field">
<label :for="`modal-zip-${index}`">
<i class="pi pi-hashtag" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Zip Code <span class="required">*</span>
</label>
<InputText
:id="`modal-zip-${index}`"
v-model="address.pincode"
:disabled="isSubmitting"
@input="handleZipcodeInput(index, $event)"
maxlength="5"
placeholder="12345"
class="w-full"
/>
</div>
<div class="form-field">
<label :for="`modal-city-${index}`">
<i class="pi pi-building" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>City <span class="required">*</span>
</label>
<InputText
:id="`modal-city-${index}`"
v-model="address.city"
:disabled="isSubmitting || address.zipcodeLookupDisabled"
placeholder="City"
class="w-full"
/>
</div>
<div class="form-field">
<label :for="`modal-state-${index}`">
<i class="pi pi-flag" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>State <span class="required">*</span>
</label>
<InputText
:id="`modal-state-${index}`"
v-model="address.state"
:disabled="isSubmitting || address.zipcodeLookupDisabled"
placeholder="State"
class="w-full"
/>
</div>
</div>
<div class="form-row">
<div class="form-field">
<label :for="`modal-contacts-${index}`"><i class="pi pi-users" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Assigned Contacts</label>
<MultiSelect
:id="`modal-contacts-${index}`"
v-model="address.contacts"
:options="contactOptions"
optionLabel="label"
dataKey="value"
:disabled="isSubmitting || contactOptions.length === 0"
placeholder="Select contacts"
class="w-full"
display="chip"
/>
</div>
<div class="form-field">
<label :for="`modal-primary-${index}`"><i class="pi pi-star-fill" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Primary Contact</label>
<Select
:id="`modal-primary-${index}`"
v-model="address.primaryContact"
:options="address.contacts || []"
optionLabel="label"
dataKey="value"
:disabled="isSubmitting || !address.contacts || address.contacts.length === 0"
placeholder="Select primary contact"
class="w-full"
/>
</div>
</div>
</div>
</div>
<div class="form-field full-width">
<Button label="Add another address" @click="addAddress" :disabled="isSubmitting" size="small" />
</div>
</div>
</div>
</template>
<script setup>
import InputText from "primevue/inputtext";
import Select from "primevue/select";
import MultiSelect from "primevue/multiselect";
import Button from "primevue/button";
import Api from "../../api";
import { useNotificationStore } from "../../stores/notifications-primevue";
const props = defineProps({
addresses: {
type: Array,
required: true,
},
isSubmitting: {
type: Boolean,
default: false,
},
contactOptions: {
type: Array,
default: () => [],
},
existingAddresses: {
type: Array,
default: () => [],
},
});
const notificationStore = useNotificationStore();
const createEmptyAddress = () => ({
addressLine1: "",
addressLine2: "",
pincode: "",
city: "",
state: "",
contacts: [],
primaryContact: null,
zipcodeLookupDisabled: true,
});
const addAddress = () => {
props.addresses.push(createEmptyAddress());
};
const removeAddress = (index) => {
if (props.addresses.length > 1) {
props.addresses.splice(index, 1);
}
};
const formatAddressLine = (index, field, event) => {
const value = event.target.value;
if (!value) return;
const formatted = value
.split(" ")
.map((word) => {
if (!word) return word;
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
})
.join(" ");
props.addresses[index][field] = formatted;
};
const handleZipcodeInput = async (index, event) => {
const value = event.target.value;
props.addresses[index].pincode = value;
if (value.length === 5) {
try {
const zipInfo = await Api.getCityStateByZip(value);
if (zipInfo && zipInfo.length > 0) {
props.addresses[index].city = zipInfo[0].city;
props.addresses[index].state = zipInfo[0].state;
props.addresses[index].zipcodeLookupDisabled = false;
} else {
throw new Error("No data returned");
}
} catch (error) {
console.error("Zipcode lookup failed:", error);
props.addresses[index].zipcodeLookupDisabled = true;
props.addresses[index].city = "";
props.addresses[index].state = "";
notificationStore.addError("Invalid zipcode or lookup failed");
}
} else {
props.addresses[index].zipcodeLookupDisabled = true;
props.addresses[index].city = "";
props.addresses[index].state = "";
}
};
const getFullAddress = (address) => {
return `${address.addressLine1 || ""} ${address.addressLine2 || ""} ${address.city || ""} ${address.state || ""} ${address.pincode || ""}`
.trim()
.replace(/\s+/g, " ");
};
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>
.form-section {
background: var(--surface-card);
border-radius: 6px;
padding: 0.75rem;
border: 1px solid var(--surface-border);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.2s ease;
}
.form-section:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.section-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--surface-border);
}
.section-header h3 {
margin: 0;
color: var(--text-color);
font-size: 1.1rem;
font-weight: 600;
}
.form-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.address-item {
border: 1px solid var(--surface-border);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
background: var(--surface-section);
transition: all 0.2s ease;
}
.address-item:hover {
border-color: var(--primary-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.address-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--surface-border);
}
.address-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.address-header h4 {
margin: 0;
color: var(--text-color);
font-size: 0.95rem;
font-weight: 600;
}
.remove-btn {
margin-left: auto;
}
.address-fields {
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.form-row {
display: flex;
gap: 0.625rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.375rem;
flex: 1;
}
.form-field.full-width {
width: 100%;
}
.form-field label {
font-weight: 500;
color: var(--text-color);
font-size: 0.85rem;
display: flex;
align-items: center;
}
.required {
color: var(--red-500);
}
.w-full {
width: 100% !important;
}
.address-item.existing-highlight {
border-color: var(--red-500);
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
</style>

View File

@ -0,0 +1,357 @@
<template>
<div class="form-section">
<div class="section-header">
<i class="pi pi-users" style="color: var(--primary-color); font-size: 1.2rem;"></i>
<h3>Add Contacts</h3>
</div>
<div class="form-grid">
<div
v-for="(contact, index) in contacts"
:key="index"
class="contact-item"
:class="{ 'existing-highlight': isExistingContact(contact) }"
>
<div class="contact-header">
<div class="contact-title">
<i class="pi pi-user" style="font-size: 0.9rem; color: var(--primary-color);"></i>
<h4>Contact {{ index + 1 }}</h4>
</div>
<div class="interactables">
<div class="form-field header-row">
<input
type="checkbox"
class="contact-checkbox"
:id="`modal-check-${index}`"
v-model="contact.isPrimary"
:disabled="isSubmitting"
@change="setPrimary(index)"
/>
<label :for="`modal-check-${index}`">
<i class="pi pi-star-fill" style="font-size: 0.7rem; margin-right: 0.25rem;"></i>Primary
</label>
</div>
<Button
v-if="contacts.length > 1"
@click="removeContact(index)"
size="small"
severity="danger"
icon="pi pi-trash"
class="remove-btn"
/>
</div>
</div>
<div class="form-rows">
<div class="form-row">
<div class="form-field">
<label :for="`modal-first-name-${index}`">
<i class="pi pi-user" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>First Name <span class="required">*</span>
</label>
<InputText
:id="`modal-first-name-${index}`"
v-model="contact.firstName"
:disabled="isSubmitting"
placeholder="First name"
class="w-full"
@input="formatName(index, 'firstName', $event)"
/>
</div>
<div class="form-field">
<label :for="`modal-last-name-${index}`">
<i class="pi pi-user" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Last Name <span class="required">*</span>
</label>
<InputText
:id="`modal-last-name-${index}`"
v-model="contact.lastName"
:disabled="isSubmitting"
placeholder="Last name"
class="w-full"
@input="formatName(index, 'lastName', $event)"
/>
</div>
<div class="form-field">
<label :for="`modal-contact-role-${index}`"><i class="pi pi-briefcase" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Role</label>
<Select
:id="`modal-contact-role-${index}`"
v-model="contact.contactRole"
:options="roleOptions"
optionLabel="label"
optionValue="value"
:disabled="isSubmitting"
placeholder="Select role"
class="w-full"
/>
</div>
</div>
<div class="form-row">
<div class="form-field">
<label :for="`modal-email-${index}`"><i class="pi pi-envelope" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Email</label>
<InputText
:id="`modal-email-${index}`"
v-model="contact.email"
:disabled="isSubmitting"
type="email"
placeholder="email@example.com"
class="w-full"
/>
</div>
<div class="form-field">
<label :for="`modal-phone-${index}`"><i class="pi pi-phone" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Phone</label>
<InputText
:id="`modal-phone-${index}`"
v-model="contact.phoneNumber"
:disabled="isSubmitting"
placeholder="(555) 123-4567"
class="w-full"
@input="formatPhone(index, $event)"
@keydown="handlePhoneKeydown($event, index)"
/>
</div>
</div>
</div>
</div>
<div class="form-field full-width">
<Button label="Add another contact" @click="addContact" :disabled="isSubmitting" size="small" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import InputText from "primevue/inputtext";
import Select from "primevue/select";
import Button from "primevue/button";
const props = defineProps({
contacts: {
type: Array,
required: true,
},
isSubmitting: {
type: Boolean,
default: false,
},
existingContacts: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["update:contacts"]);
const roleOptions = ref([
{ label: "Owner", value: "Owner" },
{ label: "Property Manager", value: "Property Manager" },
{ label: "Tenant", value: "Tenant" },
{ label: "Builder", value: "Builder" },
{ label: "Neighbor", value: "Neighbor" },
{ label: "Family Member", value: "Family Member" },
{ label: "Realtor", value: "Realtor" },
{ label: "Other", value: "Other" },
]);
const addContact = () => {
props.contacts.push({
firstName: "",
lastName: "",
phoneNumber: "",
email: "",
contactRole: "",
isPrimary: false,
});
};
const removeContact = (index) => {
if (props.contacts.length > 1) {
const wasPrimary = props.contacts[index].isPrimary;
props.contacts.splice(index, 1);
if (wasPrimary && props.contacts.length > 0) {
props.contacts[0].isPrimary = true;
}
}
};
const setPrimary = (index) => {
props.contacts.forEach((contact, i) => {
contact.isPrimary = i === index;
});
};
const formatName = (index, field, event) => {
const value = event.target.value;
if (!value) return;
const formatted = value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
props.contacts[index][field] = formatted;
};
const formatPhoneNumber = (value) => {
const digits = value.replace(/\D/g, "").slice(0, 10);
if (digits.length <= 3) return digits;
if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
};
const formatPhone = (index, event) => {
const value = event.target.value;
props.contacts[index].phoneNumber = formatPhoneNumber(value);
};
const handlePhoneKeydown = (event, index) => {
const allowedKeys = [
"Backspace", "Delete", "Tab", "Escape", "Enter",
"ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End",
];
if (allowedKeys.includes(event.key)) return;
if (event.ctrlKey || event.metaKey) return;
if (!/\d/.test(event.key)) {
event.preventDefault();
return;
}
const currentDigits = props.contacts[index].phoneNumber.replace(/\D/g, "").length;
if (currentDigits >= 10) event.preventDefault();
};
const getFullName = (contact) => {
return `${contact.firstName || ""} ${contact.lastName || ""}`.trim();
};
const isExistingContact = (contact) => {
const fullName = getFullName(contact);
return props.existingContacts.includes(fullName);
};
</script>
<style scoped>
.form-section {
background: var(--surface-card);
border-radius: 6px;
padding: 0.75rem;
border: 1px solid var(--surface-border);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.2s ease;
}
.form-section:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.section-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--surface-border);
}
.section-header h3 {
margin: 0;
color: var(--text-color);
font-size: 1.1rem;
font-weight: 600;
}
.form-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.contact-item {
border: 1px solid var(--surface-border);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
background: var(--surface-section);
min-width: 33%;
transition: all 0.2s ease;
}
.contact-item:hover {
border-color: var(--primary-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.contact-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--surface-border);
}
.contact-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.contact-header h4 {
margin: 0;
color: var(--text-color);
font-size: 0.95rem;
font-weight: 600;
}
.interactables {
display: flex;
align-items: center;
flex-direction: row;
gap: 1rem;
}
.remove-btn {
margin-left: auto;
}
.form-rows {
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.form-row {
display: flex;
gap: 0.625rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.375rem;
flex: 1;
}
.form-field.full-width {
width: 100%;
}
.form-field label {
font-weight: 500;
color: var(--text-color-secondary);
font-size: 0.85rem;
}
.form-field.header-row {
flex-direction: row;
align-items: baseline;
}
.contact-checkbox {
margin-top: 0px;
}
.required {
color: var(--red-500);
}
.w-full {
width: 100% !important;
}
.contact-item.existing-highlight {
border-color: var(--red-500);
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
</style>