create form and modal components, update datatable persistant filtering
This commit is contained in:
parent
b70e08026d
commit
8d9bb81fe2
23 changed files with 3502 additions and 74 deletions
413
frontend/src/components/modals/CreatClientModal.vue
Normal file
413
frontend/src/components/modals/CreatClientModal.vue
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
<template>
|
||||
<Modal
|
||||
:visible="isVisible"
|
||||
:options="modalOptions"
|
||||
@update:visible="handleVisibilityChange"
|
||||
@close="handleClose"
|
||||
>
|
||||
<template #title>
|
||||
Create New Client
|
||||
</template>
|
||||
|
||||
<!-- Status Message -->
|
||||
<div v-if="statusMessage" class="status-message" :class="`status-${statusType}`">
|
||||
<v-icon
|
||||
:icon="statusType === 'warning' ? 'mdi-alert' : statusType === 'error' ? 'mdi-alert-circle' : 'mdi-information'"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
/>
|
||||
{{ statusMessage }}
|
||||
</div>
|
||||
|
||||
<Form
|
||||
:fields="formFields"
|
||||
:form-data="formData"
|
||||
:on-submit="handleSubmit"
|
||||
:show-cancel-button="true"
|
||||
:validate-on-change="false"
|
||||
:validate-on-blur="true"
|
||||
:validate-on-submit="true"
|
||||
submit-button-text="Create Client"
|
||||
cancel-button-text="Cancel"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
@change="handleFieldChange"
|
||||
@blur="handleFieldBlur"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useModalStore } from '@/stores/modal'
|
||||
import Modal from '@/components/common/Modal.vue'
|
||||
import Form from '@/components/common/Form.vue'
|
||||
import Api from '@/api'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
|
||||
// Modal visibility computed property
|
||||
const isVisible = computed(() => modalStore.isModalOpen('createClient'))
|
||||
|
||||
// Form data
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
address: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
zipcode: '',
|
||||
city: '',
|
||||
state: ''
|
||||
})
|
||||
|
||||
// Available cities for the selected zipcode
|
||||
const availableCities = ref([])
|
||||
|
||||
// Loading state for zipcode lookup
|
||||
const isLoadingZipcode = ref(false)
|
||||
|
||||
// Status message for user feedback
|
||||
const statusMessage = ref('')
|
||||
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
|
||||
const modalOptions = {
|
||||
maxWidth: '600px',
|
||||
persistent: false,
|
||||
showActions: false,
|
||||
title: 'Create New Client',
|
||||
overlayColor: 'rgb(59, 130, 246)', // Blue background
|
||||
overlayOpacity: 0.8,
|
||||
cardClass: 'create-client-modal',
|
||||
closeOnOutsideClick: true,
|
||||
closeOnEscape: true
|
||||
}
|
||||
|
||||
// Form field definitions
|
||||
const formFields = computed(() => [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Client Name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'Enter client name',
|
||||
cols: 12,
|
||||
md: 12
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
label: 'Address',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'Enter street address',
|
||||
cols: 12,
|
||||
md: 12
|
||||
},
|
||||
{
|
||||
name: 'phone',
|
||||
label: 'Phone Number',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'Enter phone number',
|
||||
format: 'tel',
|
||||
cols: 12,
|
||||
md: 6,
|
||||
validate: (value) => {
|
||||
if (value && !/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/.test(value)) {
|
||||
return 'Please enter a valid phone number'
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: 'Email Address',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'Enter email address',
|
||||
format: 'email',
|
||||
cols: 12,
|
||||
md: 6
|
||||
},
|
||||
{
|
||||
name: 'zipcode',
|
||||
label: 'Zip Code',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: 'Enter zip code',
|
||||
cols: 12,
|
||||
md: 4,
|
||||
onChangeOverride: handleZipcodeChange,
|
||||
validate: (value) => {
|
||||
if (value && !/^\d{5}(-\d{4})?$/.test(value)) {
|
||||
return 'Please enter a valid zip code'
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
label: 'City',
|
||||
type: availableCities.value.length > 0 ? 'select' : 'text',
|
||||
required: true,
|
||||
disabled: false,
|
||||
placeholder: availableCities.value.length > 0 ? 'Select city' : 'Enter city name',
|
||||
options: availableCities.value.map(place => ({
|
||||
label: place['place name'],
|
||||
value: place['place name']
|
||||
})),
|
||||
cols: 12,
|
||||
md: 4,
|
||||
helpText: isLoadingZipcode.value
|
||||
? 'Loading cities...'
|
||||
: availableCities.value.length > 0
|
||||
? 'Select from available cities'
|
||||
: 'Enter city manually (auto-lookup unavailable)'
|
||||
},
|
||||
{
|
||||
name: 'state',
|
||||
label: 'State',
|
||||
type: 'text',
|
||||
required: true,
|
||||
disabled: availableCities.value.length > 0,
|
||||
placeholder: availableCities.value.length > 0 ? 'Auto-populated' : 'Enter state (e.g., CA, TX, NY)',
|
||||
cols: 12,
|
||||
md: 4,
|
||||
helpText: availableCities.value.length > 0
|
||||
? 'Auto-populated from zip code'
|
||||
: 'Enter state abbreviation manually',
|
||||
validate: (value) => {
|
||||
// Only validate manually entered states (when API lookup failed)
|
||||
if (availableCities.value.length === 0 && value) {
|
||||
const upperValue = value.toUpperCase()
|
||||
if (!US_STATES.includes(upperValue)) {
|
||||
return 'Please enter a valid US state abbreviation (e.g., CA, TX, NY)'
|
||||
}
|
||||
// Auto-correct to uppercase
|
||||
if (value !== upperValue) {
|
||||
formData.state = upperValue
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// Handle zipcode change and API lookup
|
||||
async function handleZipcodeChange(value, fieldName, formData) {
|
||||
if (fieldName === 'zipcode' && value && value.length >= 5) {
|
||||
// Only process if it's a valid zipcode format
|
||||
const zipcode = value.replace(/\D/g, '').substring(0, 5)
|
||||
|
||||
if (zipcode.length === 5) {
|
||||
isLoadingZipcode.value = true
|
||||
|
||||
try {
|
||||
const places = await Api.getCityStateByZip(zipcode)
|
||||
|
||||
if (places && places.length > 0) {
|
||||
availableCities.value = places
|
||||
|
||||
// Auto-populate state from first result
|
||||
formData.state = places[0].state
|
||||
|
||||
// If only one city, auto-select it
|
||||
if (places.length === 1) {
|
||||
formData.city = places[0]['place name']
|
||||
showStatusMessage(`Location found: ${places[0]['place name']}, ${places[0].state}`, 'success')
|
||||
} else {
|
||||
// Clear city selection if multiple cities
|
||||
formData.city = ''
|
||||
showStatusMessage(`Found ${places.length} cities for this zip code. Please select one.`, 'info')
|
||||
}
|
||||
} else {
|
||||
// No results found - enable manual entry
|
||||
handleApiFailure(formData, 'No location data found for this zip code')
|
||||
}
|
||||
} 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(formData, 'Unable to fetch location data. Please enter city and state manually.')
|
||||
} else {
|
||||
handleApiFailure(formData, 'Location lookup failed. Please enter city and state manually.')
|
||||
}
|
||||
} finally {
|
||||
isLoadingZipcode.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle API failure by enabling manual entry
|
||||
function handleApiFailure(formData, message) {
|
||||
console.warn('Zipcode API failed:', message)
|
||||
|
||||
// Clear existing data
|
||||
availableCities.value = []
|
||||
formData.city = ''
|
||||
formData.state = ''
|
||||
|
||||
// Show user-friendly message
|
||||
showStatusMessage(message, 'warning')
|
||||
}
|
||||
|
||||
// Show status message to user
|
||||
function showStatusMessage(message, type = 'info') {
|
||||
statusMessage.value = message
|
||||
statusType.value = type
|
||||
|
||||
// Auto-clear message after 5 seconds
|
||||
setTimeout(() => {
|
||||
statusMessage.value = ''
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
// Handle form field changes
|
||||
function handleFieldChange(event) {
|
||||
console.log('Field changed:', event)
|
||||
}
|
||||
|
||||
// Handle form field blur
|
||||
function handleFieldBlur(event) {
|
||||
console.log('Field blurred:', event)
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
function handleSubmit(data) {
|
||||
console.log('Form submitted with data:', data)
|
||||
|
||||
// TODO: Add API call to create client when ready
|
||||
// For now, just log the data and close the modal
|
||||
|
||||
// Show success message (you can customize this)
|
||||
alert('Client would be created with the following data:\n' + JSON.stringify(data, null, 2))
|
||||
|
||||
// Close the modal
|
||||
handleClose()
|
||||
}
|
||||
|
||||
// Handle cancel action
|
||||
function handleCancel() {
|
||||
handleClose()
|
||||
}
|
||||
|
||||
// Handle modal close
|
||||
function handleClose() {
|
||||
modalStore.closeCreateClient()
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// Handle visibility changes
|
||||
function handleVisibilityChange(visible) {
|
||||
if (!visible) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
// Reset form data
|
||||
function resetForm() {
|
||||
Object.keys(formData).forEach(key => {
|
||||
formData[key] = ''
|
||||
})
|
||||
availableCities.value = []
|
||||
isLoadingZipcode.value = false
|
||||
statusMessage.value = ''
|
||||
statusType.value = 'info'
|
||||
}
|
||||
|
||||
// Initialize modal in store when component mounts
|
||||
modalStore.initializeModal('createClient', {
|
||||
closeOnEscape: true,
|
||||
closeOnOutsideClick: true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.create-client-modal {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Custom styling for the modal content */
|
||||
:deep(.modal-header) {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.modal-title) {
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
:deep(.modal-close-btn) {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
:deep(.modal-content) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Form styling adjustments */
|
||||
:deep(.v-text-field) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.v-select) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.v-btn) {
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.v-btn.v-btn--variant-elevated) {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Status message styling */
|
||||
.status-message {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
background-color: #e3f2fd;
|
||||
color: #1565c0;
|
||||
border-left-color: #2196f3;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background-color: #fff3e0;
|
||||
color: #ef6c00;
|
||||
border-left-color: #ff9800;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
border-left-color: #f44336;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue