update create client form
This commit is contained in:
parent
1f33262e90
commit
2cfe7ed8e6
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,6 +5,8 @@
|
|||||||
tags
|
tags
|
||||||
node_modules
|
node_modules
|
||||||
__pycache__
|
__pycache__
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
*dist/
|
*dist/
|
||||||
.vscode/
|
.vscode/
|
||||||
@ -5,28 +5,27 @@ from urllib.parse import urlparse
|
|||||||
allowed_hosts = ["api.zippopotam.us"] # Update this list with trusted domains as needed
|
allowed_hosts = ["api.zippopotam.us"] # Update this list with trusted domains as needed
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def proxy_request(url, method="GET", data=None, headers=None):
|
def request(url, method="GET", data=None, headers=None):
|
||||||
"""
|
"""
|
||||||
Generic proxy for external API requests.
|
Generic proxy for external API requests.
|
||||||
WARNING: Only allow requests to trusted domains.
|
WARNING: Only allow requests to trusted domains.
|
||||||
"""
|
"""
|
||||||
parsed_url = urlparse(url)
|
parsed_url = urlparse(url)
|
||||||
if parsed_url.hostname not in allowed_hosts:
|
if parsed_url.hostname not in allowed_hosts:
|
||||||
frappe.throw(f"Rquests to {parsed_url.hostname} are not allowed.", frappe.PermissionError)
|
frappe.throw(f"Requests to {parsed_url.hostname} are not allowed.", frappe.PermissionError)
|
||||||
|
try:
|
||||||
|
resp = requests.request(
|
||||||
|
method=method.upper(),
|
||||||
|
url=url,
|
||||||
|
json=frappe.parse_json(data) if data else None,
|
||||||
|
headers=frappe.parse_json(headers) if headers else None,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
try:
|
try:
|
||||||
resp = requests.request(
|
return resp.json()
|
||||||
method=method.upper(),
|
except ValueError:
|
||||||
url=url,
|
return {"text": resp.text}
|
||||||
json=frappe.parse_json(data) if data else None,
|
except requests.exceptions.RequestException as e:
|
||||||
headers=frappe.parse_json(headers) if headers else None,
|
frappe.log_error(message=str(e), title="Proxy Request Failed")
|
||||||
timeout=10
|
frappe.throw("Failed to fetch data from external API.")
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
try:
|
|
||||||
return resp.json()
|
|
||||||
except ValueError:
|
|
||||||
return {"text": resp.text}
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
frappe.log_error(message=str(e), title="Proxy Request Failed")
|
|
||||||
frappe.throw("Failed to fetch data from external API.")
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -35,10 +35,10 @@ import ScrollPanel from "primevue/scrollpanel";
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 4px solid rgb(235, 230, 230);
|
border: 4px solid rgb(235, 230, 230);
|
||||||
max-width: 1280px;
|
max-width: 2500px;
|
||||||
min-width: 800px;
|
width: 100%;
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
min-height: 87vh;
|
height: 83vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#display-content {
|
#display-content {
|
||||||
@ -47,6 +47,6 @@ import ScrollPanel from "primevue/scrollpanel";
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
max-width: 50vw;
|
max-width: 50vw;
|
||||||
min-width: 80%;
|
min-width: 80%;
|
||||||
max-height: 87vh;
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,25 +1,20 @@
|
|||||||
import DataUtils from "./utils";
|
import DataUtils from "./utils";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const ZIPPOPOTAMUS_BASE_URL = "https://api.zippopotam.us/us";
|
const ZIPPOPOTAMUS_BASE_URL = "https://api.zippopotam.us/us";
|
||||||
|
|
||||||
class Api {
|
class Api {
|
||||||
|
static async request(url, method = "GET", data = {}) {
|
||||||
static async request(url, method = "get", data = {}) {
|
|
||||||
try {
|
try {
|
||||||
const response = await axios({
|
const response = await frappe.call({
|
||||||
url,
|
method: "custom_ui.api.proxy.request",
|
||||||
method,
|
args: {
|
||||||
data,
|
url,
|
||||||
withCredentials: false,
|
method,
|
||||||
timeout: 10000, // 10 second timeout
|
data: JSON.stringify(data),
|
||||||
headers: {
|
},
|
||||||
'Accept': 'application/json',
|
});
|
||||||
'Content-Type': 'application/json'
|
console.log("DEBUG: API - Request Response: ", response);
|
||||||
}
|
return response.message;
|
||||||
})
|
|
||||||
console.log("DEBUG: API - Request Response: ", response.data);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("DEBUG: API - Request Error: ", error);
|
console.error("DEBUG: API - Request Error: ", error);
|
||||||
// Re-throw the error so calling code can handle it
|
// Re-throw the error so calling code can handle it
|
||||||
@ -80,8 +75,8 @@ class Api {
|
|||||||
/**
|
/**
|
||||||
* Fetch a list of documents from a specific doctype.
|
* Fetch a list of documents from a specific doctype.
|
||||||
*
|
*
|
||||||
* @param {String} doctype
|
* @param {String} doctype
|
||||||
* @param {string[]} fields
|
* @param {string[]} fields
|
||||||
* @returns {Promise<Object[]>}
|
* @returns {Promise<Object[]>}
|
||||||
*/
|
*/
|
||||||
static async getDocsList(doctype, fields = []) {
|
static async getDocsList(doctype, fields = []) {
|
||||||
@ -93,8 +88,8 @@ class Api {
|
|||||||
/**
|
/**
|
||||||
* Fetch a detailed document by doctype and name.
|
* Fetch a detailed document by doctype and name.
|
||||||
*
|
*
|
||||||
* @param {String} doctype
|
* @param {String} doctype
|
||||||
* @param {String} name
|
* @param {String} name
|
||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
static async getDetailedDoc(doctype, name) {
|
static async getDetailedDoc(doctype, name) {
|
||||||
@ -105,36 +100,27 @@ class Api {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a list of places (city/state) by zipcode using Zippopotamus API.
|
* Fetch a list of places (city/state) by zipcode using Zippopotamus API.
|
||||||
*
|
*
|
||||||
* @param {String} zipcode
|
* @param {String} zipcode
|
||||||
* @returns {Promise<Object[]>}
|
* @returns {Promise<Object[]>}
|
||||||
*/
|
*/
|
||||||
static async getCityStateByZip(zipcode) {
|
static async getCityStateByZip(zipcode) {
|
||||||
const url = `${ZIPPOPOTAMUS_BASE_URL}/${zipcode}`;
|
const url = `${ZIPPOPOTAMUS_BASE_URL}/${zipcode}`;
|
||||||
try {
|
const response = await this.request(url);
|
||||||
const response = await this.request(url);
|
const { places } = response || {};
|
||||||
const { places } = response || {};
|
if (!places || places.length === 0) {
|
||||||
|
throw new Error(`No location data found for zip code ${zipcode}`);
|
||||||
if (!places || places.length === 0) {
|
|
||||||
throw new Error(`No location data found for zip code ${zipcode}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return places;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("DEBUG: API - getCityStateByZip Error: ", error);
|
|
||||||
|
|
||||||
// Provide more specific error information
|
|
||||||
if (error.code === 'ERR_NETWORK') {
|
|
||||||
throw new Error('Network error: Unable to connect to location service. This may be due to CORS restrictions or network connectivity issues.');
|
|
||||||
} else if (error.response?.status === 404) {
|
|
||||||
throw new Error(`Zip code ${zipcode} not found in database.`);
|
|
||||||
} else if (error.code === 'ECONNABORTED') {
|
|
||||||
throw new Error('Request timeout: Location service is taking too long to respond.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-throw the original error if we can't categorize it
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
return places;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a list of Customer names.
|
||||||
|
* @returns {Promise<String[]>}
|
||||||
|
*/
|
||||||
|
static async getCustomerNames() {
|
||||||
|
const customers = await this.getDocsList("Customer", ["name"]);
|
||||||
|
return customers.map((customer) => customer.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,336 +0,0 @@
|
|||||||
<!-- Modal Usage Examples -->
|
|
||||||
<template>
|
|
||||||
<div class="modal-examples">
|
|
||||||
<h2>Modal Component Examples</h2>
|
|
||||||
|
|
||||||
<!-- Example buttons to trigger different modal types -->
|
|
||||||
<div class="example-buttons">
|
|
||||||
<v-btn @click="showBasicModal" color="primary">Basic Modal</v-btn>
|
|
||||||
<v-btn @click="showFormModal" color="secondary">Form Modal</v-btn>
|
|
||||||
<v-btn @click="showConfirmModal" color="warning">Confirmation Modal</v-btn>
|
|
||||||
<v-btn @click="showFullscreenModal" color="success">Fullscreen Modal</v-btn>
|
|
||||||
<v-btn @click="showCustomModal" color="info">Custom Styled Modal</v-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Basic Modal -->
|
|
||||||
<Modal
|
|
||||||
v-model:visible="basicModalVisible"
|
|
||||||
:options="basicModalOptions"
|
|
||||||
@close="onBasicModalClose"
|
|
||||||
@confirm="onBasicModalConfirm"
|
|
||||||
>
|
|
||||||
<p>This is a basic modal with default settings.</p>
|
|
||||||
<p>You can put any content here!</p>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<!-- Form Modal -->
|
|
||||||
<Modal
|
|
||||||
v-model:visible="formModalVisible"
|
|
||||||
:options="formModalOptions"
|
|
||||||
@close="onFormModalClose"
|
|
||||||
@confirm="onFormModalConfirm"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<v-icon class="mr-2">mdi-account-plus</v-icon>
|
|
||||||
Add New User
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<v-form ref="userForm" v-model="formValid">
|
|
||||||
<v-text-field
|
|
||||||
v-model="userForm.name"
|
|
||||||
label="Full Name"
|
|
||||||
:rules="[v => !!v || 'Name is required']"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<v-text-field
|
|
||||||
v-model="userForm.email"
|
|
||||||
label="Email"
|
|
||||||
type="email"
|
|
||||||
:rules="emailRules"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<v-select
|
|
||||||
v-model="userForm.role"
|
|
||||||
:items="roleOptions"
|
|
||||||
label="Role"
|
|
||||||
:rules="[v => !!v || 'Role is required']"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</v-form>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<!-- Confirmation Modal -->
|
|
||||||
<Modal
|
|
||||||
v-model:visible="confirmModalVisible"
|
|
||||||
:options="confirmModalOptions"
|
|
||||||
@confirm="onDeleteConfirm"
|
|
||||||
@cancel="onDeleteCancel"
|
|
||||||
>
|
|
||||||
<div class="text-center">
|
|
||||||
<v-icon size="64" color="warning" class="mb-4">mdi-alert-circle</v-icon>
|
|
||||||
<h3 class="mb-2">Are you sure?</h3>
|
|
||||||
<p>This action cannot be undone. The item will be permanently deleted.</p>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<!-- Fullscreen Modal -->
|
|
||||||
<Modal
|
|
||||||
v-model:visible="fullscreenModalVisible"
|
|
||||||
:options="fullscreenModalOptions"
|
|
||||||
@close="onFullscreenModalClose"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
Fullscreen Content
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="fullscreen-content">
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<v-card>
|
|
||||||
<v-card-title>Left Panel</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<p>This is a fullscreen modal that can contain complex layouts.</p>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item v-for="i in 10" :key="i">
|
|
||||||
<v-list-item-title>Item {{ i }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<v-card>
|
|
||||||
<v-card-title>Right Panel</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<v-img
|
|
||||||
src="https://picsum.photos/400/200"
|
|
||||||
height="200"
|
|
||||||
class="mb-4"
|
|
||||||
/>
|
|
||||||
<p>You can include any Vue components here.</p>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<!-- Custom Styled Modal -->
|
|
||||||
<Modal
|
|
||||||
v-model:visible="customModalVisible"
|
|
||||||
:options="customModalOptions"
|
|
||||||
@close="onCustomModalClose"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<div class="custom-title">
|
|
||||||
<v-icon class="mr-2">mdi-palette</v-icon>
|
|
||||||
Custom Styled Modal
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="custom-content">
|
|
||||||
<v-card variant="outlined" class="mb-4">
|
|
||||||
<v-card-text>
|
|
||||||
<v-icon size="32" color="primary" class="mr-2">mdi-information</v-icon>
|
|
||||||
This modal demonstrates custom styling options.
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<v-timeline density="compact">
|
|
||||||
<v-timeline-item
|
|
||||||
v-for="item in timelineItems"
|
|
||||||
:key="item.id"
|
|
||||||
:dot-color="item.color"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<v-card>
|
|
||||||
<v-card-title>{{ item.title }}</v-card-title>
|
|
||||||
<v-card-subtitle>{{ item.time }}</v-card-subtitle>
|
|
||||||
</v-card>
|
|
||||||
</v-timeline-item>
|
|
||||||
</v-timeline>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #actions="{ close }">
|
|
||||||
<v-btn color="gradient" variant="elevated" @click="close">
|
|
||||||
<v-icon class="mr-1">mdi-check</v-icon>
|
|
||||||
Got it!
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive } from 'vue'
|
|
||||||
import Modal from './common/Modal.vue'
|
|
||||||
|
|
||||||
// Basic Modal
|
|
||||||
const basicModalVisible = ref(false)
|
|
||||||
const basicModalOptions = {
|
|
||||||
title: 'Basic Modal',
|
|
||||||
maxWidth: '400px',
|
|
||||||
showActions: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form Modal
|
|
||||||
const formModalVisible = ref(false)
|
|
||||||
const formValid = ref(false)
|
|
||||||
const userForm = reactive({
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
role: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const formModalOptions = {
|
|
||||||
maxWidth: '500px',
|
|
||||||
persistent: true,
|
|
||||||
confirmButtonText: 'Save User',
|
|
||||||
confirmButtonColor: 'success',
|
|
||||||
loading: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const roleOptions = ['Admin', 'User', 'Manager', 'Viewer']
|
|
||||||
const emailRules = [
|
|
||||||
v => !!v || 'Email is required',
|
|
||||||
v => /.+@.+\..+/.test(v) || 'Email must be valid'
|
|
||||||
]
|
|
||||||
|
|
||||||
// Confirmation Modal
|
|
||||||
const confirmModalVisible = ref(false)
|
|
||||||
const confirmModalOptions = {
|
|
||||||
title: 'Confirm Deletion',
|
|
||||||
maxWidth: '400px',
|
|
||||||
persistent: false,
|
|
||||||
confirmButtonText: 'Delete',
|
|
||||||
confirmButtonColor: 'error',
|
|
||||||
cancelButtonText: 'Keep',
|
|
||||||
cardColor: 'surface-variant'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fullscreen Modal
|
|
||||||
const fullscreenModalVisible = ref(false)
|
|
||||||
const fullscreenModalOptions = {
|
|
||||||
fullscreen: true,
|
|
||||||
showActions: false,
|
|
||||||
scrollable: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom Modal
|
|
||||||
const customModalVisible = ref(false)
|
|
||||||
const customModalOptions = {
|
|
||||||
maxWidth: '600px',
|
|
||||||
cardColor: 'primary',
|
|
||||||
cardVariant: 'elevated',
|
|
||||||
elevation: 12,
|
|
||||||
headerClass: 'custom-header',
|
|
||||||
contentClass: 'custom-content-class',
|
|
||||||
showActions: false,
|
|
||||||
overlayOpacity: 0.8,
|
|
||||||
transition: 'scale-transition'
|
|
||||||
}
|
|
||||||
|
|
||||||
const timelineItems = [
|
|
||||||
{ id: 1, title: 'Project Started', time: '2 hours ago', color: 'primary' },
|
|
||||||
{ id: 2, title: 'First Milestone', time: '1 hour ago', color: 'success' },
|
|
||||||
{ id: 3, title: 'Review Phase', time: '30 minutes ago', color: 'warning' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// Modal event handlers
|
|
||||||
const showBasicModal = () => {
|
|
||||||
basicModalVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onBasicModalClose = () => {
|
|
||||||
console.log('Basic modal closed')
|
|
||||||
}
|
|
||||||
|
|
||||||
const onBasicModalConfirm = () => {
|
|
||||||
console.log('Basic modal confirmed')
|
|
||||||
}
|
|
||||||
|
|
||||||
const showFormModal = () => {
|
|
||||||
formModalVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFormModalClose = () => {
|
|
||||||
// Reset form
|
|
||||||
Object.assign(userForm, { name: '', email: '', role: '' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFormModalConfirm = async () => {
|
|
||||||
if (formValid.value) {
|
|
||||||
formModalOptions.loading = true
|
|
||||||
// Simulate API call
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
console.log('User saved:', userForm)
|
|
||||||
formModalOptions.loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const showConfirmModal = () => {
|
|
||||||
confirmModalVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDeleteConfirm = () => {
|
|
||||||
console.log('Item deleted')
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDeleteCancel = () => {
|
|
||||||
console.log('Deletion cancelled')
|
|
||||||
}
|
|
||||||
|
|
||||||
const showFullscreenModal = () => {
|
|
||||||
fullscreenModalVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFullscreenModalClose = () => {
|
|
||||||
console.log('Fullscreen modal closed')
|
|
||||||
}
|
|
||||||
|
|
||||||
const showCustomModal = () => {
|
|
||||||
customModalVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCustomModalClose = () => {
|
|
||||||
console.log('Custom modal closed')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.modal-examples {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen-content {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-content {
|
|
||||||
background: linear-gradient(45deg, #f3f4f6 0%, #ffffff 100%);
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-header {
|
|
||||||
background: linear-gradient(45deg, #1976d2 0%, #42a5f5 100%);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-content-class {
|
|
||||||
background-color: #fafafa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,413 +1,473 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
:visible="isVisible"
|
:visible="isVisible"
|
||||||
:options="modalOptions"
|
:options="modalOptions"
|
||||||
@update:visible="handleVisibilityChange"
|
@update:visible="handleVisibilityChange"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title> Create New Client </template>
|
||||||
Create New Client
|
|
||||||
</template>
|
<!-- Status Message -->
|
||||||
|
<div v-if="statusMessage" class="status-message" :class="`status-${statusType}`">
|
||||||
<!-- Status Message -->
|
<i :class="getStatusIcon(statusType)" class="status-icon"></i>
|
||||||
<div v-if="statusMessage" class="status-message" :class="`status-${statusType}`">
|
{{ statusMessage }}
|
||||||
<v-icon
|
</div>
|
||||||
:icon="statusType === 'warning' ? 'mdi-alert' : statusType === 'error' ? 'mdi-alert-circle' : 'mdi-information'"
|
|
||||||
size="small"
|
<Form
|
||||||
class="mr-2"
|
:fields="formFields"
|
||||||
/>
|
:form-data="formData"
|
||||||
{{ statusMessage }}
|
:show-cancel-button="true"
|
||||||
</div>
|
:validate-on-change="false"
|
||||||
|
:validate-on-blur="true"
|
||||||
<Form
|
:validate-on-submit="true"
|
||||||
:fields="formFields"
|
submit-button-text="Create Client"
|
||||||
:form-data="formData"
|
cancel-button-text="Cancel"
|
||||||
:on-submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
:show-cancel-button="true"
|
@cancel="handleCancel"
|
||||||
:validate-on-change="false"
|
/>
|
||||||
:validate-on-blur="true"
|
</Modal>
|
||||||
:validate-on-submit="true"
|
|
||||||
submit-button-text="Create Client"
|
|
||||||
cancel-button-text="Cancel"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
@cancel="handleCancel"
|
|
||||||
@change="handleFieldChange"
|
|
||||||
@blur="handleFieldBlur"
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, watch } from 'vue'
|
import { ref, reactive, computed, watch, watchEffect } from "vue";
|
||||||
import { useModalStore } from '@/stores/modal'
|
import { useModalStore } from "@/stores/modal";
|
||||||
import Modal from '@/components/common/Modal.vue'
|
import Modal from "@/components/common/Modal.vue";
|
||||||
import Form from '@/components/common/Form.vue'
|
import Form from "@/components/common/Form.vue";
|
||||||
import Api from '@/api'
|
import Api from "@/api";
|
||||||
|
import DataUtils from "../../utils";
|
||||||
|
|
||||||
const modalStore = useModalStore()
|
const modalStore = useModalStore();
|
||||||
|
|
||||||
// Modal visibility computed property
|
// Modal visibility computed property
|
||||||
const isVisible = computed(() => modalStore.isModalOpen('createClient'))
|
const isVisible = computed(() => modalStore.isModalOpen("createClient"));
|
||||||
|
const customerNames = ref([]);
|
||||||
// Form data
|
// Form data
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
name: '',
|
name: "",
|
||||||
address: '',
|
address: "",
|
||||||
phone: '',
|
phone: "",
|
||||||
email: '',
|
email: "",
|
||||||
zipcode: '',
|
zipcode: "",
|
||||||
city: '',
|
city: "",
|
||||||
state: ''
|
state: "",
|
||||||
})
|
});
|
||||||
|
|
||||||
// Available cities for the selected zipcode
|
// Available cities for the selected zipcode
|
||||||
const availableCities = ref([])
|
const availableCities = ref([]);
|
||||||
|
|
||||||
// Loading state for zipcode lookup
|
// Loading state for zipcode lookup
|
||||||
const isLoadingZipcode = ref(false)
|
const isLoadingZipcode = ref(false);
|
||||||
|
|
||||||
// Status message for user feedback
|
// Status message for user feedback
|
||||||
const statusMessage = ref('')
|
const statusMessage = ref("");
|
||||||
const statusType = ref('info') // 'info', 'warning', 'error', 'success'
|
const statusType = ref("info"); // 'info', 'warning', 'error', 'success'
|
||||||
|
|
||||||
// US State abbreviations for validation
|
|
||||||
const US_STATES = [
|
|
||||||
'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA',
|
|
||||||
'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD',
|
|
||||||
'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ',
|
|
||||||
'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC',
|
|
||||||
'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY',
|
|
||||||
'DC' // District of Columbia
|
|
||||||
]
|
|
||||||
|
|
||||||
// Modal configuration
|
// Modal configuration
|
||||||
const modalOptions = {
|
const modalOptions = {
|
||||||
maxWidth: '600px',
|
maxWidth: "600px",
|
||||||
persistent: false,
|
persistent: false,
|
||||||
showActions: false,
|
showActions: false,
|
||||||
title: 'Create New Client',
|
title: "Create New Client",
|
||||||
overlayColor: 'rgb(59, 130, 246)', // Blue background
|
overlayColor: "rgb(59, 130, 246)", // Blue background
|
||||||
overlayOpacity: 0.8,
|
overlayOpacity: 0.8,
|
||||||
cardClass: 'create-client-modal',
|
cardClass: "create-client-modal",
|
||||||
closeOnOutsideClick: true,
|
closeOnOutsideClick: true,
|
||||||
closeOnEscape: true
|
closeOnEscape: true,
|
||||||
}
|
};
|
||||||
|
|
||||||
// Form field definitions
|
// Form field definitions
|
||||||
const formFields = computed(() => [
|
const formFields = computed(() => [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: "name",
|
||||||
label: 'Client Name',
|
label: "Client Name",
|
||||||
type: 'text',
|
type: "autocomplete", // Changed from 'select' to 'autocomplete'
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'Enter client name',
|
placeholder: "Type or select client name",
|
||||||
cols: 12,
|
cols: 12,
|
||||||
md: 12
|
options: customerNames.value, // Direct array of strings
|
||||||
},
|
forceSelection: false, // Allow custom entries not in the list
|
||||||
{
|
dropdown: true,
|
||||||
name: 'address',
|
// For string arrays, don't set optionLabel at all
|
||||||
label: 'Address',
|
helpText: "Select an existing client or enter a new client name",
|
||||||
type: 'text',
|
// Let the Form component handle filtering automatically
|
||||||
required: true,
|
},
|
||||||
placeholder: 'Enter street address',
|
{
|
||||||
cols: 12,
|
name: "address",
|
||||||
md: 12
|
label: "Address",
|
||||||
},
|
type: "text",
|
||||||
{
|
required: true,
|
||||||
name: 'phone',
|
placeholder: "Enter street address",
|
||||||
label: 'Phone Number',
|
cols: 12,
|
||||||
type: 'text',
|
md: 12,
|
||||||
required: true,
|
},
|
||||||
placeholder: 'Enter phone number',
|
{
|
||||||
format: 'tel',
|
name: "phone",
|
||||||
cols: 12,
|
label: "Phone Number",
|
||||||
md: 6,
|
type: "text",
|
||||||
validate: (value) => {
|
required: true,
|
||||||
if (value && !/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/.test(value)) {
|
placeholder: "Enter phone number",
|
||||||
return 'Please enter a valid phone number'
|
format: "tel",
|
||||||
}
|
cols: 12,
|
||||||
return null
|
md: 6,
|
||||||
}
|
validate: (value) => {
|
||||||
},
|
if (value && !/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/.test(value)) {
|
||||||
{
|
return "Please enter a valid phone number";
|
||||||
name: 'email',
|
}
|
||||||
label: 'Email Address',
|
return null;
|
||||||
type: 'text',
|
},
|
||||||
required: true,
|
},
|
||||||
placeholder: 'Enter email address',
|
{
|
||||||
format: 'email',
|
name: "email",
|
||||||
cols: 12,
|
label: "Email Address",
|
||||||
md: 6
|
type: "text",
|
||||||
},
|
required: true,
|
||||||
{
|
placeholder: "Enter email address",
|
||||||
name: 'zipcode',
|
format: "email",
|
||||||
label: 'Zip Code',
|
cols: 12,
|
||||||
type: 'text',
|
md: 6,
|
||||||
required: true,
|
},
|
||||||
placeholder: 'Enter zip code',
|
{
|
||||||
cols: 12,
|
name: "zipcode",
|
||||||
md: 4,
|
label: "Zip Code",
|
||||||
onChangeOverride: handleZipcodeChange,
|
type: "text",
|
||||||
validate: (value) => {
|
required: true,
|
||||||
if (value && !/^\d{5}(-\d{4})?$/.test(value)) {
|
placeholder: "Enter zip code",
|
||||||
return 'Please enter a valid zip code'
|
cols: 12,
|
||||||
}
|
md: 4,
|
||||||
return null
|
onChangeOverride: handleZipcodeChange,
|
||||||
}
|
validate: (value) => {
|
||||||
},
|
if (value && !/^\d{5}(-\d{4})?$/.test(value)) {
|
||||||
{
|
return "Please enter a valid zip code";
|
||||||
name: 'city',
|
}
|
||||||
label: 'City',
|
return null;
|
||||||
type: availableCities.value.length > 0 ? 'select' : 'text',
|
},
|
||||||
required: true,
|
},
|
||||||
disabled: false,
|
{
|
||||||
placeholder: availableCities.value.length > 0 ? 'Select city' : 'Enter city name',
|
name: "city",
|
||||||
options: availableCities.value.map(place => ({
|
label: "City",
|
||||||
label: place['place name'],
|
type: availableCities.value.length > 0 ? "select" : "text",
|
||||||
value: place['place name']
|
required: true,
|
||||||
})),
|
disabled: false,
|
||||||
cols: 12,
|
placeholder: availableCities.value.length > 0 ? "Select city" : "Enter city name",
|
||||||
md: 4,
|
options: availableCities.value.map((place) => ({
|
||||||
helpText: isLoadingZipcode.value
|
label: place["place name"],
|
||||||
? 'Loading cities...'
|
value: place["place name"],
|
||||||
: availableCities.value.length > 0
|
})),
|
||||||
? 'Select from available cities'
|
cols: 12,
|
||||||
: 'Enter city manually (auto-lookup unavailable)'
|
md: 4,
|
||||||
},
|
helpText: isLoadingZipcode.value
|
||||||
{
|
? "Loading cities..."
|
||||||
name: 'state',
|
: availableCities.value.length > 0
|
||||||
label: 'State',
|
? "Select from available cities"
|
||||||
type: 'text',
|
: "Enter city manually (auto-lookup unavailable)",
|
||||||
required: true,
|
},
|
||||||
disabled: availableCities.value.length > 0,
|
{
|
||||||
placeholder: availableCities.value.length > 0 ? 'Auto-populated' : 'Enter state (e.g., CA, TX, NY)',
|
name: "state",
|
||||||
cols: 12,
|
label: "State",
|
||||||
md: 4,
|
type: "select",
|
||||||
helpText: availableCities.value.length > 0
|
options: DataUtils.US_STATES.map((stateAbbr) => ({
|
||||||
? 'Auto-populated from zip code'
|
label: stateAbbr,
|
||||||
: 'Enter state abbreviation manually',
|
value: stateAbbr,
|
||||||
validate: (value) => {
|
})),
|
||||||
// Only validate manually entered states (when API lookup failed)
|
required: true,
|
||||||
if (availableCities.value.length === 0 && value) {
|
disabled: availableCities.value.length > 0,
|
||||||
const upperValue = value.toUpperCase()
|
placeholder:
|
||||||
if (!US_STATES.includes(upperValue)) {
|
availableCities.value.length > 0 ? "Auto-populated" : "Enter state (e.g., CA, TX, NY)",
|
||||||
return 'Please enter a valid US state abbreviation (e.g., CA, TX, NY)'
|
cols: 12,
|
||||||
}
|
md: 4,
|
||||||
// Auto-correct to uppercase
|
helpText:
|
||||||
if (value !== upperValue) {
|
availableCities.value.length > 0
|
||||||
formData.state = upperValue
|
? "Auto-populated from zip code"
|
||||||
}
|
: "Enter state abbreviation manually",
|
||||||
}
|
validate: (value) => {
|
||||||
return null
|
// Only validate manually entered states (when API lookup failed)
|
||||||
}
|
if (availableCities.value.length === 0 && value) {
|
||||||
}
|
const upperValue = value.toUpperCase();
|
||||||
])
|
if (!DataUtils.US_STATES.includes(upperValue)) {
|
||||||
|
return "Please enter a valid US state abbreviation (e.g., CA, TX, NY)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
// Handle zipcode change and API lookup
|
// Handle zipcode change and API lookup
|
||||||
async function handleZipcodeChange(value, fieldName, formData) {
|
async function handleZipcodeChange(value, fieldName, currentFormData) {
|
||||||
if (fieldName === 'zipcode' && value && value.length >= 5) {
|
if (fieldName === "zipcode" && value && value.length >= 5) {
|
||||||
// Only process if it's a valid zipcode format
|
// Only process if it's a valid zipcode format
|
||||||
const zipcode = value.replace(/\D/g, '').substring(0, 5)
|
const zipcode = value.replace(/\D/g, "").substring(0, 5);
|
||||||
|
|
||||||
if (zipcode.length === 5) {
|
if (zipcode.length === 5) {
|
||||||
isLoadingZipcode.value = true
|
isLoadingZipcode.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const places = await Api.getCityStateByZip(zipcode)
|
const places = await Api.getCityStateByZip(zipcode);
|
||||||
|
console.log("API response for zipcode", zipcode, ":", places);
|
||||||
if (places && places.length > 0) {
|
|
||||||
availableCities.value = places
|
if (places && places.length > 0) {
|
||||||
|
availableCities.value = places;
|
||||||
// Auto-populate state from first result
|
|
||||||
formData.state = places[0].state
|
// Update the reactive formData directly to ensure reactivity
|
||||||
|
// Use "state abbreviation" instead of "state" for proper abbreviation format
|
||||||
// If only one city, auto-select it
|
const stateValue = places[0]["state abbreviation"] || places[0].state;
|
||||||
if (places.length === 1) {
|
console.log("Setting state to:", stateValue, "from place object:", places[0]);
|
||||||
formData.city = places[0]['place name']
|
formData.state = stateValue;
|
||||||
showStatusMessage(`Location found: ${places[0]['place name']}, ${places[0].state}`, 'success')
|
|
||||||
} else {
|
// If only one city, auto-select it
|
||||||
// Clear city selection if multiple cities
|
if (places.length === 1) {
|
||||||
formData.city = ''
|
formData.city = places[0]["place name"];
|
||||||
showStatusMessage(`Found ${places.length} cities for this zip code. Please select one.`, 'info')
|
showStatusMessage(
|
||||||
}
|
`Location found: ${places[0]["place name"]}, ${places[0]["state abbreviation"] || places[0].state}`,
|
||||||
} else {
|
"success",
|
||||||
// No results found - enable manual entry
|
);
|
||||||
handleApiFailure(formData, 'No location data found for this zip code')
|
} else {
|
||||||
}
|
// Clear city selection if multiple cities
|
||||||
} catch (error) {
|
formData.city = "";
|
||||||
console.error('Error fetching city/state data:', error)
|
showStatusMessage(
|
||||||
|
`Found ${places.length} cities for this zip code. Please select one.`,
|
||||||
// Check if it's a network/CORS error
|
"info",
|
||||||
if (error.code === 'ERR_NETWORK' || error.message.includes('Network Error')) {
|
);
|
||||||
handleApiFailure(formData, 'Unable to fetch location data. Please enter city and state manually.')
|
}
|
||||||
} else {
|
} else {
|
||||||
handleApiFailure(formData, 'Location lookup failed. Please enter city and state manually.')
|
// No results found - enable manual entry
|
||||||
}
|
handleApiFailure("No location data found for this zip code");
|
||||||
} finally {
|
}
|
||||||
isLoadingZipcode.value = false
|
} catch (error) {
|
||||||
}
|
console.error("Error fetching city/state data:", error);
|
||||||
}
|
|
||||||
}
|
// Check if it's a network/CORS error
|
||||||
|
if (error.code === "ERR_NETWORK" || error.message.includes("Network Error")) {
|
||||||
|
handleApiFailure(
|
||||||
|
"Unable to fetch location data. Please enter city and state manually.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
handleApiFailure(
|
||||||
|
"Location lookup failed. Please enter city and state manually.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoadingZipcode.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle API failure by enabling manual entry
|
// Handle API failure by enabling manual entry
|
||||||
function handleApiFailure(formData, message) {
|
function handleApiFailure(message) {
|
||||||
console.warn('Zipcode API failed:', message)
|
console.warn("Zipcode API failed:", message);
|
||||||
|
|
||||||
// Clear existing data
|
// Clear existing data
|
||||||
availableCities.value = []
|
availableCities.value = [];
|
||||||
formData.city = ''
|
formData.city = "";
|
||||||
formData.state = ''
|
formData.state = "";
|
||||||
|
|
||||||
// Show user-friendly message
|
// Show user-friendly message
|
||||||
showStatusMessage(message, 'warning')
|
showStatusMessage(message, "warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show status message to user
|
// Show status message to user
|
||||||
function showStatusMessage(message, type = 'info') {
|
function showStatusMessage(message, type = "info") {
|
||||||
statusMessage.value = message
|
statusMessage.value = message;
|
||||||
statusType.value = type
|
statusType.value = type;
|
||||||
|
|
||||||
// Auto-clear message after 5 seconds
|
// Auto-clear message after 5 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
statusMessage.value = ''
|
statusMessage.value = "";
|
||||||
}, 5000)
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle form field changes
|
// Get icon class for status messages
|
||||||
function handleFieldChange(event) {
|
function getStatusIcon(type) {
|
||||||
console.log('Field changed:', event)
|
switch (type) {
|
||||||
}
|
case "warning":
|
||||||
|
return "pi pi-exclamation-triangle";
|
||||||
// Handle form field blur
|
case "error":
|
||||||
function handleFieldBlur(event) {
|
return "pi pi-times-circle";
|
||||||
console.log('Field blurred:', event)
|
case "success":
|
||||||
|
return "pi pi-check-circle";
|
||||||
|
default:
|
||||||
|
return "pi pi-info-circle";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
function handleSubmit(data) {
|
async function handleSubmit(submittedFormData) {
|
||||||
console.log('Form submitted with data:', data)
|
try {
|
||||||
|
showStatusMessage("Creating client...", "info");
|
||||||
// TODO: Add API call to create client when ready
|
|
||||||
// For now, just log the data and close the modal
|
// Convert form data to the expected format
|
||||||
|
const clientData = {
|
||||||
// Show success message (you can customize this)
|
name: submittedFormData.name,
|
||||||
alert('Client would be created with the following data:\n' + JSON.stringify(data, null, 2))
|
address: submittedFormData.address,
|
||||||
|
phone: submittedFormData.phone,
|
||||||
// Close the modal
|
email: submittedFormData.email,
|
||||||
handleClose()
|
zipcode: submittedFormData.zipcode,
|
||||||
|
city: submittedFormData.city,
|
||||||
|
state: submittedFormData.state,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call API to create client
|
||||||
|
const response = await Api.createClient(clientData);
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
showStatusMessage("Client created successfully!", "success");
|
||||||
|
|
||||||
|
// Close modal after a brief delay
|
||||||
|
setTimeout(() => {
|
||||||
|
handleClose();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
throw new Error(response?.message || "Failed to create client");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating client:", error);
|
||||||
|
showStatusMessage(error.message || "Failed to create client. Please try again.", "error");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle cancel action
|
// Handle cancel action
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
handleClose()
|
handleClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle modal close
|
// Handle modal close
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
modalStore.closeCreateClient()
|
modalStore.closeCreateClient();
|
||||||
resetForm()
|
resetForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle visibility changes
|
// Handle visibility changes
|
||||||
function handleVisibilityChange(visible) {
|
function handleVisibilityChange(visible) {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
handleClose()
|
handleClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset form data
|
// Reset form data
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
Object.keys(formData).forEach(key => {
|
Object.keys(formData).forEach((key) => {
|
||||||
formData[key] = ''
|
formData[key] = "";
|
||||||
})
|
});
|
||||||
availableCities.value = []
|
availableCities.value = [];
|
||||||
isLoadingZipcode.value = false
|
isLoadingZipcode.value = false;
|
||||||
statusMessage.value = ''
|
statusMessage.value = "";
|
||||||
statusType.value = 'info'
|
statusType.value = "info";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize modal in store when component mounts
|
// Initialize modal in store when component mounts
|
||||||
modalStore.initializeModal('createClient', {
|
modalStore.initializeModal("createClient", {
|
||||||
closeOnEscape: true,
|
closeOnEscape: true,
|
||||||
closeOnOutsideClick: true
|
closeOnOutsideClick: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
|
watch(isVisible, async () => {
|
||||||
|
if (isVisible.value) {
|
||||||
|
try {
|
||||||
|
const names = await Api.getCustomerNames();
|
||||||
|
console.log("Loaded customer names:", names);
|
||||||
|
console.log("Customer names type:", typeof names, Array.isArray(names));
|
||||||
|
console.log("First customer name:", names[0], typeof names[0]);
|
||||||
|
customerNames.value = names;
|
||||||
|
|
||||||
|
// Debug: Let's also set some test data to see if autocomplete works at all
|
||||||
|
console.log("Setting customerNames to:", customerNames.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading customer names:", error);
|
||||||
|
// Set some test data to debug if autocomplete works
|
||||||
|
customerNames.value = ["Test Customer 1", "Test Customer 2", "Another Client"];
|
||||||
|
console.log("Using test customer names:", customerNames.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.create-client-modal {
|
.create-client-modal {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom styling for the modal content */
|
/* Custom styling for the modal content */
|
||||||
:deep(.modal-header) {
|
:deep(.modal-header) {
|
||||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.modal-title) {
|
:deep(.modal-title) {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.modal-close-btn) {
|
:deep(.modal-close-btn) {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.modal-content) {
|
:deep(.modal-content) {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form styling adjustments */
|
/* Form styling adjustments for PrimeVue components */
|
||||||
:deep(.v-text-field) {
|
:deep(.p-inputtext),
|
||||||
margin-bottom: 8px;
|
:deep(.p-dropdown),
|
||||||
|
:deep(.p-autocomplete) {
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.v-select) {
|
/* Ensure AutoComplete panel appears above modal */
|
||||||
margin-bottom: 8px;
|
:global(.p-autocomplete-overlay) {
|
||||||
|
z-index: 9999 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.v-btn) {
|
:global(.p-autocomplete-panel) {
|
||||||
text-transform: none;
|
z-index: 9999 !important;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.v-btn.v-btn--variant-elevated) {
|
:deep(.p-button) {
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
text-transform: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-button:not(.p-button-text)) {
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status message styling */
|
/* Status message styling */
|
||||||
.status-message {
|
.status-message {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
border-left: 4px solid;
|
border-left: 4px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-info {
|
.status-info {
|
||||||
background-color: #e3f2fd;
|
background-color: #e3f2fd;
|
||||||
color: #1565c0;
|
color: #1565c0;
|
||||||
border-left-color: #2196f3;
|
border-left-color: #2196f3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-warning {
|
.status-warning {
|
||||||
background-color: #fff3e0;
|
background-color: #fff3e0;
|
||||||
color: #ef6c00;
|
color: #ef6c00;
|
||||||
border-left-color: #ff9800;
|
border-left-color: #ff9800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-error {
|
.status-error {
|
||||||
background-color: #ffebee;
|
background-color: #ffebee;
|
||||||
color: #c62828;
|
color: #c62828;
|
||||||
border-left-color: #f44336;
|
border-left-color: #f44336;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-success {
|
.status-success {
|
||||||
background-color: #e8f5e8;
|
background-color: #e8f5e8;
|
||||||
color: #2e7d32;
|
color: #2e7d32;
|
||||||
border-left-color: #4caf50;
|
border-left-color: #4caf50;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1639,6 +1639,14 @@ class DataUtils {
|
|||||||
materials: ["Sprinkler heads - 3", "Nozzles - 5", "Wire nuts - 10"],
|
materials: ["Sprinkler heads - 3", "Nozzles - 5", "Wire nuts - 10"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static US_STATES = [
|
||||||
|
'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA',
|
||||||
|
'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD',
|
||||||
|
'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ',
|
||||||
|
'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC',
|
||||||
|
'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY'
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DataUtils;
|
export default DataUtils;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user