add date picker
This commit is contained in:
parent
09a514ae86
commit
82f9b1aac2
@ -1,4 +1,5 @@
|
|||||||
import frappe, json
|
import frappe, json, re
|
||||||
|
from datetime import datetime, date
|
||||||
from custom_ui.db_utils import calculate_appointment_scheduled_status, calculate_estimate_sent_status, calculate_payment_recieved_status, calculate_job_status
|
from custom_ui.db_utils import calculate_appointment_scheduled_status, calculate_estimate_sent_status, calculate_payment_recieved_status, calculate_job_status
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -316,5 +317,157 @@ def get_jobs(options):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def upsert_estimate():
|
def get_warranty_claims(options):
|
||||||
pass
|
options = json.loads(options)
|
||||||
|
print("DEBUG: Raw warranty options received:", options)
|
||||||
|
defaultOptions = {
|
||||||
|
"fields": ["*"],
|
||||||
|
"filters": {},
|
||||||
|
"sorting": {},
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 10,
|
||||||
|
"for_table": False
|
||||||
|
}
|
||||||
|
options = {**defaultOptions, **options}
|
||||||
|
print("DEBUG: Final warranty options:", options)
|
||||||
|
|
||||||
|
warranties = []
|
||||||
|
tableRows = []
|
||||||
|
|
||||||
|
# Map frontend field names to backend field names for Warranty Claim doctype
|
||||||
|
def map_warranty_field_name(frontend_field):
|
||||||
|
field_mapping = {
|
||||||
|
"warrantyId": "name",
|
||||||
|
"customer": "customer_name",
|
||||||
|
"serviceAddress": "service_address",
|
||||||
|
"complaint": "complaint",
|
||||||
|
"status": "status",
|
||||||
|
"complaintDate": "complaint_date",
|
||||||
|
"complaintRaisedBy": "complaint_raised_by",
|
||||||
|
"fromCompany": "from_company",
|
||||||
|
"territory": "territory",
|
||||||
|
"resolutionDate": "resolution_date",
|
||||||
|
"warrantyStatus": "warranty_amc_status"
|
||||||
|
}
|
||||||
|
return field_mapping.get(frontend_field, frontend_field)
|
||||||
|
|
||||||
|
# Process filters from PrimeVue format to Frappe format
|
||||||
|
processed_filters = {}
|
||||||
|
if options["filters"]:
|
||||||
|
for field_name, filter_obj in options["filters"].items():
|
||||||
|
if isinstance(filter_obj, dict) and "value" in filter_obj:
|
||||||
|
if filter_obj["value"] is not None and filter_obj["value"] != "":
|
||||||
|
# Map frontend field names to backend field names
|
||||||
|
backend_field = map_warranty_field_name(field_name)
|
||||||
|
|
||||||
|
# Handle different match modes
|
||||||
|
match_mode = filter_obj.get("matchMode", "contains")
|
||||||
|
if isinstance(match_mode, str):
|
||||||
|
match_mode = match_mode.lower()
|
||||||
|
|
||||||
|
if match_mode in ("contains", "contains"):
|
||||||
|
processed_filters[backend_field] = ["like", f"%{filter_obj['value']}%"]
|
||||||
|
elif match_mode in ("startswith", "startsWith"):
|
||||||
|
processed_filters[backend_field] = ["like", f"{filter_obj['value']}%"]
|
||||||
|
elif match_mode in ("endswith", "endsWith"):
|
||||||
|
processed_filters[backend_field] = ["like", f"%{filter_obj['value']}"]
|
||||||
|
elif match_mode in ("equals", "equals"):
|
||||||
|
processed_filters[backend_field] = filter_obj["value"]
|
||||||
|
else:
|
||||||
|
# Default to contains
|
||||||
|
processed_filters[backend_field] = ["like", f"%{filter_obj['value']}%"]
|
||||||
|
|
||||||
|
# Process sorting
|
||||||
|
order_by = None
|
||||||
|
if options.get("sorting") and options["sorting"]:
|
||||||
|
sorting_str = options["sorting"]
|
||||||
|
if sorting_str and sorting_str.strip():
|
||||||
|
# Parse "field_name asc/desc" format
|
||||||
|
parts = sorting_str.strip().split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
sort_field = parts[0]
|
||||||
|
sort_direction = parts[1].lower()
|
||||||
|
# Map frontend field to backend field
|
||||||
|
backend_sort_field = map_warranty_field_name(sort_field)
|
||||||
|
order_by = f"{backend_sort_field} {sort_direction}"
|
||||||
|
|
||||||
|
print("DEBUG: Processed warranty filters:", processed_filters)
|
||||||
|
print("DEBUG: Warranty order by:", order_by)
|
||||||
|
|
||||||
|
count = frappe.db.count("Warranty Claim", filters=processed_filters)
|
||||||
|
print("DEBUG: Total warranty claims count:", count)
|
||||||
|
|
||||||
|
warranty_claims = frappe.db.get_all(
|
||||||
|
"Warranty Claim",
|
||||||
|
fields=options["fields"],
|
||||||
|
filters=processed_filters,
|
||||||
|
limit=options["page_size"],
|
||||||
|
start=(options["page"] - 1) * options["page_size"],
|
||||||
|
order_by=order_by
|
||||||
|
)
|
||||||
|
|
||||||
|
for warranty in warranty_claims:
|
||||||
|
warranty_obj = {}
|
||||||
|
tableRow = {}
|
||||||
|
|
||||||
|
tableRow["id"] = warranty["name"]
|
||||||
|
tableRow["warrantyId"] = warranty["name"]
|
||||||
|
tableRow["customer"] = warranty.get("customer_name", "")
|
||||||
|
tableRow["serviceAddress"] = warranty.get("service_address", warranty.get("address_display", ""))
|
||||||
|
|
||||||
|
# Extract a brief description from the complaint HTML
|
||||||
|
complaint_text = warranty.get("complaint", "")
|
||||||
|
if complaint_text:
|
||||||
|
# Simple HTML stripping for display - take first 100 chars
|
||||||
|
clean_text = re.sub('<.*?>', '', complaint_text)
|
||||||
|
clean_text = clean_text.strip()
|
||||||
|
if len(clean_text) > 100:
|
||||||
|
clean_text = clean_text[:100] + "..."
|
||||||
|
tableRow["issueDescription"] = clean_text
|
||||||
|
else:
|
||||||
|
tableRow["issueDescription"] = ""
|
||||||
|
|
||||||
|
tableRow["status"] = warranty.get("status", "")
|
||||||
|
tableRow["complaintDate"] = warranty.get("complaint_date", "")
|
||||||
|
tableRow["complaintRaisedBy"] = warranty.get("complaint_raised_by", "")
|
||||||
|
tableRow["fromCompany"] = warranty.get("from_company", "")
|
||||||
|
tableRow["territory"] = warranty.get("territory", "")
|
||||||
|
tableRow["resolutionDate"] = warranty.get("resolution_date", "")
|
||||||
|
tableRow["warrantyStatus"] = warranty.get("warranty_amc_status", "")
|
||||||
|
|
||||||
|
# Add priority based on status and date (can be customized)
|
||||||
|
if warranty.get("status") == "Open":
|
||||||
|
# Calculate priority based on complaint date
|
||||||
|
if warranty.get("complaint_date"):
|
||||||
|
complaint_date = warranty.get("complaint_date")
|
||||||
|
if isinstance(complaint_date, str):
|
||||||
|
complaint_date = datetime.strptime(complaint_date, "%Y-%m-%d").date()
|
||||||
|
elif isinstance(complaint_date, datetime):
|
||||||
|
complaint_date = complaint_date.date()
|
||||||
|
|
||||||
|
days_old = (date.today() - complaint_date).days
|
||||||
|
if days_old > 7:
|
||||||
|
tableRow["priority"] = "High"
|
||||||
|
elif days_old > 3:
|
||||||
|
tableRow["priority"] = "Medium"
|
||||||
|
else:
|
||||||
|
tableRow["priority"] = "Low"
|
||||||
|
else:
|
||||||
|
tableRow["priority"] = "Medium"
|
||||||
|
else:
|
||||||
|
tableRow["priority"] = "Low"
|
||||||
|
|
||||||
|
tableRows.append(tableRow)
|
||||||
|
|
||||||
|
warranty_obj["warranty_claim"] = warranty
|
||||||
|
warranties.append(warranty_obj)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pagination": {
|
||||||
|
"total": count,
|
||||||
|
"page": options["page"],
|
||||||
|
"page_size": options["page_size"],
|
||||||
|
"total_pages": (count + options["page_size"] - 1) // options["page_size"]
|
||||||
|
},
|
||||||
|
"data": tableRows if options["for_table"] else warranties
|
||||||
|
}
|
||||||
@ -8,8 +8,8 @@ A highly flexible and configurable dynamic form component built with **PrimeVue*
|
|||||||
## ✨ New Features (PrimeVue Migration)
|
## ✨ New Features (PrimeVue Migration)
|
||||||
|
|
||||||
- **AutoComplete component** - Users can select from suggestions OR enter completely custom values
|
- **AutoComplete component** - Users can select from suggestions OR enter completely custom values
|
||||||
- **Better date/time pickers** with calendar popup and time selection
|
- **Enhanced Date/Time Pickers** - Comprehensive date handling with multiple formats, time selection, constraints, and smart defaults
|
||||||
- **Improved accessibility** with ARIA support
|
- **Better accessibility** with ARIA support
|
||||||
- **More flexible styling** with CSS custom properties
|
- **More flexible styling** with CSS custom properties
|
||||||
- **Enhanced mobile responsiveness** with CSS Grid
|
- **Enhanced mobile responsiveness** with CSS Grid
|
||||||
|
|
||||||
@ -363,41 +363,152 @@ Radio button group for single selection from multiple options.
|
|||||||
|
|
||||||
### Date Input (`type: 'date'`)
|
### Date Input (`type: 'date'`)
|
||||||
|
|
||||||
Date picker input field.
|
Enhanced date picker input field with comprehensive formatting and configuration options.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
// Basic date input
|
||||||
{
|
{
|
||||||
name: 'birthDate',
|
name: 'birthDate',
|
||||||
label: 'Birth Date',
|
label: 'Birth Date',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
required: true,
|
required: true,
|
||||||
min: '1900-01-01',
|
}
|
||||||
max: '2025-12-31'
|
|
||||||
|
// Date with custom format
|
||||||
|
{
|
||||||
|
name: 'eventDate',
|
||||||
|
label: 'Event Date',
|
||||||
|
type: 'date',
|
||||||
|
format: 'YYYY-MM-DD', // or 'mm/dd/yyyy', 'dd/mm/yyyy', etc.
|
||||||
|
required: true,
|
||||||
|
placeholder: 'Select event date'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date with time picker
|
||||||
|
{
|
||||||
|
name: 'appointmentDateTime',
|
||||||
|
label: 'Appointment Date & Time',
|
||||||
|
type: 'date',
|
||||||
|
showTime: true,
|
||||||
|
hourFormat: '12', // '12' or '24'
|
||||||
|
required: true,
|
||||||
|
defaultToNow: true, // Set to current date/time by default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time-only picker
|
||||||
|
{
|
||||||
|
name: 'preferredTime',
|
||||||
|
label: 'Preferred Time',
|
||||||
|
type: 'date',
|
||||||
|
timeOnly: true,
|
||||||
|
hourFormat: '12',
|
||||||
|
stepMinute: 15, // 15-minute intervals
|
||||||
|
defaultValue: 'now'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advanced date configuration
|
||||||
|
{
|
||||||
|
name: 'projectDeadline',
|
||||||
|
label: 'Project Deadline',
|
||||||
|
type: 'date',
|
||||||
|
format: 'dd/mm/yyyy',
|
||||||
|
minDate: 'today', // Can't select past dates
|
||||||
|
maxDate: '2025-12-31', // Maximum date
|
||||||
|
defaultToToday: true,
|
||||||
|
showButtonBar: true,
|
||||||
|
yearNavigator: true,
|
||||||
|
monthNavigator: true,
|
||||||
|
yearRange: '2024:2030',
|
||||||
|
helpText: 'Select a deadline for the project completion'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline date picker (always visible)
|
||||||
|
{
|
||||||
|
name: 'calendarDate',
|
||||||
|
label: 'Calendar',
|
||||||
|
type: 'date',
|
||||||
|
inline: true,
|
||||||
|
view: 'date', // 'date', 'month', 'year'
|
||||||
|
showWeek: true,
|
||||||
|
defaultValue: 'today'
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Additional Properties:**
|
**Additional Properties:**
|
||||||
|
|
||||||
- **`min`** (String) - Minimum allowed date (YYYY-MM-DD format)
|
- **`format`** (String) - Date format: `'YYYY-MM-DD'`, `'mm/dd/yyyy'`, `'dd/mm/yyyy'`, `'dd-mm-yyyy'`, `'mm-dd-yyyy'`
|
||||||
- **`max`** (String) - Maximum allowed date (YYYY-MM-DD format)
|
- **`dateFormat`** (String) - PrimeVue-specific format string (overrides `format`)
|
||||||
|
- **`showTime`** (Boolean, default: `false`) - Include time picker
|
||||||
|
- **`timeOnly`** (Boolean, default: `false`) - Show only time picker (no date)
|
||||||
|
- **`hourFormat`** (String, default: `'24'`) - Hour format: `'12'` or `'24'`
|
||||||
|
- **`stepHour`** (Number, default: `1`) - Hour step increment
|
||||||
|
- **`stepMinute`** (Number, default: `1`) - Minute step increment
|
||||||
|
- **`showSeconds`** (Boolean, default: `false`) - Show seconds in time picker
|
||||||
|
- **`stepSecond`** (Number, default: `1`) - Second step increment
|
||||||
|
- **`minDate`** (String|Date) - Minimum selectable date
|
||||||
|
- **`maxDate`** (String|Date) - Maximum selectable date
|
||||||
|
- **`defaultToToday`** (Boolean, default: `false`) - Set default to today's date
|
||||||
|
- **`defaultToNow`** (Boolean, default: `false`) - Set default to current date/time
|
||||||
|
- **`showButtonBar`** (Boolean, default: `true`) - Show today/clear buttons
|
||||||
|
- **`todayButtonLabel`** (String, default: `'Today'`) - Today button text
|
||||||
|
- **`clearButtonLabel`** (String, default: `'Clear'`) - Clear button text
|
||||||
|
- **`showWeek`** (Boolean, default: `false`) - Show week numbers
|
||||||
|
- **`manualInput`** (Boolean, default: `true`) - Allow manual date entry
|
||||||
|
- **`yearNavigator`** (Boolean, default: `false`) - Show year dropdown
|
||||||
|
- **`monthNavigator`** (Boolean, default: `false`) - Show month dropdown
|
||||||
|
- **`yearRange`** (String, default: `'1900:2100'`) - Available year range
|
||||||
|
- **`inline`** (Boolean, default: `false`) - Display picker inline (always visible)
|
||||||
|
- **`view`** (String, default: `'date'`) - Default view: `'date'`, `'month'`, `'year'`
|
||||||
|
- **`touchUI`** (Boolean, default: `false`) - Optimize for touch devices
|
||||||
|
- **`onDateChange`** (Function) - Custom date change handler
|
||||||
|
- **Signature:** `(dateValue: Date) => any`
|
||||||
|
|
||||||
### DateTime Input (`type: 'datetime'`)
|
**Default Value Options:**
|
||||||
|
|
||||||
Date and time picker input field.
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
// String values
|
||||||
|
defaultValue: "today"; // Set to today's date
|
||||||
|
defaultValue: "now"; // Set to current date/time
|
||||||
|
defaultValue: "2024-12-25"; // Specific date string
|
||||||
|
|
||||||
|
// Boolean flags
|
||||||
|
defaultToToday: true; // Set to today (date only)
|
||||||
|
defaultToNow: true; // Set to current date/time
|
||||||
|
|
||||||
|
// Date object
|
||||||
|
defaultValue: new Date(); // Specific Date object
|
||||||
|
```
|
||||||
|
|
||||||
|
### DateTime Input (`type: 'datetime'`) - LEGACY
|
||||||
|
|
||||||
|
**⚠️ DEPRECATED:** Use `type: 'date'` with `showTime: true` instead.
|
||||||
|
|
||||||
|
Legacy date and time picker input field. This is maintained for backward compatibility.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// LEGACY - Use date with showTime instead
|
||||||
{
|
{
|
||||||
name: 'appointmentTime',
|
name: 'appointmentTime',
|
||||||
label: 'Appointment Time',
|
label: 'Appointment Time',
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RECOMMENDED - Use this instead
|
||||||
|
{
|
||||||
|
name: 'appointmentTime',
|
||||||
|
label: 'Appointment Time',
|
||||||
|
type: 'date',
|
||||||
|
showTime: true,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Additional Properties:**
|
**Additional Properties:**
|
||||||
|
|
||||||
- **`min`** (String) - Minimum allowed datetime
|
- **`min`** (String) - Minimum allowed datetime
|
||||||
- **`max`** (String) - Maximum allowed datetime
|
- **`max`** (String) - Maximum allowed datetime
|
||||||
|
- **`hourFormat`** (String, default: `'24'`) - Hour format: `'12'` or `'24'`
|
||||||
|
|
||||||
### File Input (`type: 'file'`)
|
### File Input (`type: 'file'`)
|
||||||
|
|
||||||
@ -526,6 +637,18 @@ const contactFields = [
|
|||||||
format: "email",
|
format: "email",
|
||||||
required: true,
|
required: true,
|
||||||
cols: 12,
|
cols: 12,
|
||||||
|
md: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preferredContactDate",
|
||||||
|
label: "Preferred Contact Date",
|
||||||
|
type: "date",
|
||||||
|
format: "mm/dd/yyyy",
|
||||||
|
minDate: "today",
|
||||||
|
defaultToToday: true,
|
||||||
|
cols: 12,
|
||||||
|
md: 6,
|
||||||
|
helpText: "When would you like us to contact you?",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "message",
|
name: "message",
|
||||||
@ -675,6 +798,280 @@ const autoCompleteFields = [
|
|||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Enhanced Date Picker Examples
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const formData = ref({});
|
||||||
|
|
||||||
|
const dateFields = [
|
||||||
|
// Basic date picker
|
||||||
|
{
|
||||||
|
name: "birthDate",
|
||||||
|
label: "Birth Date",
|
||||||
|
type: "date",
|
||||||
|
format: "mm/dd/yyyy",
|
||||||
|
required: true,
|
||||||
|
cols: 6,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Date with time - appointment scheduling
|
||||||
|
{
|
||||||
|
name: "appointmentDateTime",
|
||||||
|
label: "Appointment Date & Time",
|
||||||
|
type: "date",
|
||||||
|
showTime: true,
|
||||||
|
hourFormat: "12",
|
||||||
|
stepMinute: 15,
|
||||||
|
defaultToNow: true,
|
||||||
|
minDate: "today",
|
||||||
|
cols: 6,
|
||||||
|
helpText: "Select appointment date and time",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Project deadline with constraints
|
||||||
|
{
|
||||||
|
name: "projectDeadline",
|
||||||
|
label: "Project Deadline",
|
||||||
|
type: "date",
|
||||||
|
format: "YYYY-MM-DD",
|
||||||
|
minDate: "today",
|
||||||
|
maxDate: "2025-12-31",
|
||||||
|
defaultToToday: true,
|
||||||
|
yearNavigator: true,
|
||||||
|
monthNavigator: true,
|
||||||
|
required: true,
|
||||||
|
cols: 6,
|
||||||
|
helpText: "Deadline cannot be in the past",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Time-only picker for preferences
|
||||||
|
{
|
||||||
|
name: "preferredTime",
|
||||||
|
label: "Preferred Contact Time",
|
||||||
|
type: "date",
|
||||||
|
timeOnly: true,
|
||||||
|
hourFormat: "12",
|
||||||
|
stepMinute: 30,
|
||||||
|
defaultValue: "09:00",
|
||||||
|
cols: 6,
|
||||||
|
helpText: "Best time to contact you",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Event date with week numbers
|
||||||
|
{
|
||||||
|
name: "eventDate",
|
||||||
|
label: "Event Date",
|
||||||
|
type: "date",
|
||||||
|
format: "dd/mm/yyyy",
|
||||||
|
showWeek: true,
|
||||||
|
yearNavigator: true,
|
||||||
|
monthNavigator: true,
|
||||||
|
yearRange: "2024:2026",
|
||||||
|
cols: 6,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Inline calendar for scheduling
|
||||||
|
{
|
||||||
|
name: "availableDate",
|
||||||
|
label: "Available Dates",
|
||||||
|
type: "date",
|
||||||
|
inline: true,
|
||||||
|
multiple: true, // Note: This would require custom implementation
|
||||||
|
showButtonBar: true,
|
||||||
|
cols: 12,
|
||||||
|
helpText: "Click dates when you are available",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSubmit = (data) => {
|
||||||
|
console.log("Date form submitted:", data);
|
||||||
|
|
||||||
|
// Example of working with date values
|
||||||
|
if (data.appointmentDateTime) {
|
||||||
|
console.log(
|
||||||
|
"Appointment scheduled for:",
|
||||||
|
data.appointmentDateTime.toLocaleString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.projectDeadline) {
|
||||||
|
const deadline = new Date(data.projectDeadline);
|
||||||
|
const daysUntilDeadline = Math.ceil(
|
||||||
|
(deadline - new Date()) / (1000 * 60 * 60 * 24),
|
||||||
|
);
|
||||||
|
console.log(`Days until deadline: ${daysUntilDeadline}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
:fields="dateFields"
|
||||||
|
v-model:form-data="formData"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
submit-button-text="Schedule"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Display current values -->
|
||||||
|
<div v-if="Object.keys(formData).length" class="mt-4">
|
||||||
|
<h3>Selected Dates:</h3>
|
||||||
|
<div v-for="(value, key) in formData" :key="key" class="mb-2">
|
||||||
|
<strong>{{ key }}:</strong>
|
||||||
|
<span v-if="value instanceof Date">
|
||||||
|
{{ value.toLocaleString() }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Real-World Date Picker Scenarios
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const bookingData = ref({});
|
||||||
|
|
||||||
|
// Hotel booking form with date constraints
|
||||||
|
const hotelBookingFields = [
|
||||||
|
{
|
||||||
|
name: "checkIn",
|
||||||
|
label: "Check-in Date",
|
||||||
|
type: "date",
|
||||||
|
format: "mm/dd/yyyy",
|
||||||
|
minDate: "today",
|
||||||
|
defaultToToday: true,
|
||||||
|
required: true,
|
||||||
|
cols: 6,
|
||||||
|
onDateChange: (date) => {
|
||||||
|
// Auto-update checkout date to be at least 1 day later
|
||||||
|
if (bookingData.value.checkOut && bookingData.value.checkOut <= date) {
|
||||||
|
const nextDay = new Date(date);
|
||||||
|
nextDay.setDate(nextDay.getDate() + 1);
|
||||||
|
bookingData.value.checkOut = nextDay;
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "checkOut",
|
||||||
|
label: "Check-out Date",
|
||||||
|
type: "date",
|
||||||
|
format: "mm/dd/yyyy",
|
||||||
|
minDate: "today",
|
||||||
|
required: true,
|
||||||
|
cols: 6,
|
||||||
|
validate: (value) => {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
bookingData.value.checkIn &&
|
||||||
|
value <= bookingData.value.checkIn
|
||||||
|
) {
|
||||||
|
return "Check-out date must be after check-in date";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Meeting scheduler with business hours
|
||||||
|
const meetingFields = [
|
||||||
|
{
|
||||||
|
name: "meetingDate",
|
||||||
|
label: "Meeting Date",
|
||||||
|
type: "date",
|
||||||
|
format: "dd/mm/yyyy",
|
||||||
|
minDate: "today",
|
||||||
|
// Exclude weekends
|
||||||
|
validate: (value) => {
|
||||||
|
if (value) {
|
||||||
|
const day = value.getDay();
|
||||||
|
if (day === 0 || day === 6) {
|
||||||
|
return "Please select a weekday";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
cols: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "meetingTime",
|
||||||
|
label: "Meeting Time",
|
||||||
|
type: "date",
|
||||||
|
timeOnly: true,
|
||||||
|
hourFormat: "12",
|
||||||
|
stepMinute: 30,
|
||||||
|
// Business hours only: 9 AM to 5 PM
|
||||||
|
validate: (value) => {
|
||||||
|
if (value) {
|
||||||
|
const hours = value.getHours();
|
||||||
|
if (hours < 9 || hours >= 17) {
|
||||||
|
return "Please select a time between 9:00 AM and 5:00 PM";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
cols: 6,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Birth date with age calculation
|
||||||
|
const personalInfoFields = [
|
||||||
|
{
|
||||||
|
name: "birthDate",
|
||||||
|
label: "Date of Birth",
|
||||||
|
type: "date",
|
||||||
|
format: "mm/dd/yyyy",
|
||||||
|
maxDate: "today",
|
||||||
|
yearNavigator: true,
|
||||||
|
yearRange: "1920:2010",
|
||||||
|
required: true,
|
||||||
|
onDateChange: (date) => {
|
||||||
|
// Calculate and display age
|
||||||
|
if (date) {
|
||||||
|
const today = new Date();
|
||||||
|
const age = today.getFullYear() - date.getFullYear();
|
||||||
|
const monthDiff = today.getMonth() - date.getMonth();
|
||||||
|
|
||||||
|
if (
|
||||||
|
monthDiff < 0 ||
|
||||||
|
(monthDiff === 0 && today.getDate() < date.getDate())
|
||||||
|
) {
|
||||||
|
age--;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Age: ${age} years`);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
},
|
||||||
|
cols: 12,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3>Hotel Booking</h3>
|
||||||
|
<Form :fields="hotelBookingFields" v-model:form-data="bookingData" />
|
||||||
|
|
||||||
|
<h3 class="mt-6">Meeting Scheduler</h3>
|
||||||
|
<Form :fields="meetingFields" />
|
||||||
|
|
||||||
|
<h3 class="mt-6">Personal Information</h3>
|
||||||
|
<Form :fields="personalInfoFields" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
### User Registration Form
|
### User Registration Form
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
@ -1028,8 +1425,8 @@ The component has been completely migrated from Vuetify to PrimeVue. Here's what
|
|||||||
### ✨ What's New
|
### ✨ What's New
|
||||||
|
|
||||||
- **AutoComplete component** - The star feature! Users can select from suggestions OR enter completely custom values
|
- **AutoComplete component** - The star feature! Users can select from suggestions OR enter completely custom values
|
||||||
- **Better date/time pickers** - Calendar popup, time selection, better formatting options
|
- **Enhanced Date/Time Pickers** - Multiple format support (`YYYY-MM-DD`, `mm/dd/yyyy`, etc.), smart defaults (`today`, `now`), time-only mode, inline calendars, business hour constraints, and comprehensive validation
|
||||||
- **Enhanced file uploads** - Drag & drop, better validation, file preview
|
- **Better file uploads** - Drag & drop, better validation, file preview
|
||||||
- **Improved accessibility** - Full ARIA support, better keyboard navigation
|
- **Improved accessibility** - Full ARIA support, better keyboard navigation
|
||||||
- **Flexible styling** - CSS custom properties, easier theming
|
- **Flexible styling** - CSS custom properties, easier theming
|
||||||
- **Mobile-first responsive** - Better grid system, improved mobile UX
|
- **Mobile-first responsive** - Better grid system, improved mobile UX
|
||||||
|
|||||||
@ -305,6 +305,37 @@ class Api {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get paginated warranty claims data with filtering and sorting
|
||||||
|
* @param {Object} paginationParams - Pagination parameters from store
|
||||||
|
* @param {Object} filters - Filter parameters from store
|
||||||
|
* @param {Object} sorting - Sorting parameters from store (optional)
|
||||||
|
* @returns {Promise<{data: Array, pagination: Object}>}
|
||||||
|
*/
|
||||||
|
static async getPaginatedWarrantyData(paginationParams = {}, filters = {}, sorting = null) {
|
||||||
|
const { page = 0, pageSize = 10, sortField = null, sortOrder = null } = paginationParams;
|
||||||
|
|
||||||
|
// Use sorting from the dedicated sorting parameter first, then fall back to pagination params
|
||||||
|
const actualSortField = sorting?.field || sortField;
|
||||||
|
const actualSortOrder = sorting?.order || sortOrder;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
page: page + 1, // Backend expects 1-based pages
|
||||||
|
page_size: pageSize,
|
||||||
|
filters,
|
||||||
|
sorting:
|
||||||
|
actualSortField && actualSortOrder
|
||||||
|
? `${actualSortField} ${actualSortOrder === -1 ? "desc" : "asc"}`
|
||||||
|
: null,
|
||||||
|
for_table: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("DEBUG: API - Sending warranty options to backend:", options);
|
||||||
|
|
||||||
|
const result = await this.request("custom_ui.api.db.get_warranty_claims", { options });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a list of documents from a specific doctype.
|
* Fetch a list of documents from a specific doctype.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -135,6 +135,9 @@
|
|||||||
:severity="getBadgeColor(slotProps.data[col.fieldName])"
|
:severity="getBadgeColor(slotProps.data[col.fieldName])"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="col.type === 'date'" #body="slotProps">
|
||||||
|
<span>{{ formatDate(slotProps.data[col.fieldName]) }}</span>
|
||||||
|
</template>
|
||||||
<template v-if="col.type === 'button'" #body="slotProps">
|
<template v-if="col.type === 'button'" #body="slotProps">
|
||||||
<Button
|
<Button
|
||||||
:label="slotProps.data[col.fieldName]"
|
:label="slotProps.data[col.fieldName]"
|
||||||
@ -626,16 +629,47 @@ const handleFilterInput = (fieldName, value, filterCallback) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getBadgeColor = (status) => {
|
const getBadgeColor = (status) => {
|
||||||
console.log("DEBUG: - getBadgeColor status", status);
|
|
||||||
switch (status?.toLowerCase()) {
|
switch (status?.toLowerCase()) {
|
||||||
case "completed":
|
case "completed":
|
||||||
return "success"; // green
|
case "open":
|
||||||
|
case "active":
|
||||||
|
return "success";
|
||||||
case "in progress":
|
case "in progress":
|
||||||
return "warn";
|
case "pending":
|
||||||
|
return "warning";
|
||||||
case "not started":
|
case "not started":
|
||||||
return "danger"; // red
|
case "closed":
|
||||||
|
case "cancelled":
|
||||||
|
return "danger";
|
||||||
default:
|
default:
|
||||||
return "info"; // blue fallback
|
return "info";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateValue) => {
|
||||||
|
if (!dateValue) return "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle different date formats
|
||||||
|
let date;
|
||||||
|
if (typeof dateValue === "string") {
|
||||||
|
date = new Date(dateValue);
|
||||||
|
} else if (dateValue instanceof Date) {
|
||||||
|
date = dateValue;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if date is valid
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return dateValue; // Return original value if can't parse
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format as MM/DD/YYYY
|
||||||
|
return date.toLocaleDateString("en-US");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error formatting date:", error);
|
||||||
|
return dateValue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
console.log("DEBUG: - DataTable props.columns", props.columns);
|
console.log("DEBUG: - DataTable props.columns", props.columns);
|
||||||
|
|||||||
@ -267,17 +267,35 @@
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
:id="field.name"
|
:id="field.name"
|
||||||
v-model="fieldValues[field.name]"
|
v-model="fieldValues[field.name]"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder || getDatePlaceholder(field)"
|
||||||
:disabled="field.disabled || isFormDisabled"
|
:disabled="field.disabled || isFormDisabled"
|
||||||
:readonly="field.readonly"
|
:readonly="field.readonly"
|
||||||
:minDate="field.minDate"
|
:minDate="parseDateValue(field.minDate)"
|
||||||
:maxDate="field.maxDate"
|
:maxDate="parseDateValue(field.maxDate)"
|
||||||
:invalid="!!getFieldError(field.name)"
|
:invalid="!!getFieldError(field.name)"
|
||||||
fluid
|
fluid
|
||||||
showIcon
|
showIcon
|
||||||
iconDisplay="input"
|
iconDisplay="input"
|
||||||
:dateFormat="field.dateFormat || 'dd/mm/yy'"
|
:dateFormat="getDateFormat(field)"
|
||||||
@update:model-value="handleFieldChange(field, $event)"
|
:showTime="field.showTime || false"
|
||||||
|
:timeOnly="field.timeOnly || false"
|
||||||
|
:hourFormat="field.hourFormat || '24'"
|
||||||
|
:stepHour="field.stepHour || 1"
|
||||||
|
:stepMinute="field.stepMinute || 1"
|
||||||
|
:showSeconds="field.showSeconds || false"
|
||||||
|
:stepSecond="field.stepSecond || 1"
|
||||||
|
:showButtonBar="field.showButtonBar !== false"
|
||||||
|
:todayButtonLabel="field.todayButtonLabel || 'Today'"
|
||||||
|
:clearButtonLabel="field.clearButtonLabel || 'Clear'"
|
||||||
|
:showWeek="field.showWeek || false"
|
||||||
|
:manualInput="field.manualInput !== false"
|
||||||
|
:yearNavigator="field.yearNavigator || false"
|
||||||
|
:monthNavigator="field.monthNavigator || false"
|
||||||
|
:yearRange="field.yearRange || '1900:2100'"
|
||||||
|
:inline="field.inline || false"
|
||||||
|
:view="field.view || 'date'"
|
||||||
|
:touchUI="field.touchUI || false"
|
||||||
|
@update:model-value="handleDateChange(field, $event)"
|
||||||
@blur="handleFieldBlur(field, fieldValues[field.name])"
|
@blur="handleFieldBlur(field, fieldValues[field.name])"
|
||||||
/>
|
/>
|
||||||
<small v-if="field.helpText" class="field-help">{{
|
<small v-if="field.helpText" class="field-help">{{
|
||||||
@ -293,7 +311,7 @@
|
|||||||
</Message>
|
</Message>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DateTime Input -->
|
<!-- DateTime Input (Legacy - use date with showTime: true) -->
|
||||||
<div v-else-if="field.type === 'datetime'" class="field-wrapper">
|
<div v-else-if="field.type === 'datetime'" class="field-wrapper">
|
||||||
<label v-if="field.label" :for="field.name" class="field-label">
|
<label v-if="field.label" :for="field.name" class="field-label">
|
||||||
{{ field.label }}
|
{{ field.label }}
|
||||||
@ -302,19 +320,35 @@
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
:id="field.name"
|
:id="field.name"
|
||||||
v-model="fieldValues[field.name]"
|
v-model="fieldValues[field.name]"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="
|
||||||
|
field.placeholder ||
|
||||||
|
getDatePlaceholder({ ...field, showTime: true })
|
||||||
|
"
|
||||||
:disabled="field.disabled || isFormDisabled"
|
:disabled="field.disabled || isFormDisabled"
|
||||||
:readonly="field.readonly"
|
:readonly="field.readonly"
|
||||||
:minDate="field.minDate"
|
:minDate="parseDateValue(field.minDate)"
|
||||||
:maxDate="field.maxDate"
|
:maxDate="parseDateValue(field.maxDate)"
|
||||||
:invalid="!!getFieldError(field.name)"
|
:invalid="!!getFieldError(field.name)"
|
||||||
fluid
|
fluid
|
||||||
showIcon
|
showIcon
|
||||||
iconDisplay="input"
|
iconDisplay="input"
|
||||||
showTime
|
showTime
|
||||||
:hourFormat="field.hourFormat || '24'"
|
:hourFormat="field.hourFormat || '24'"
|
||||||
:dateFormat="field.dateFormat || 'dd/mm/yy'"
|
:dateFormat="getDateFormat({ ...field, showTime: true })"
|
||||||
@update:model-value="handleFieldChange(field, $event)"
|
:stepHour="field.stepHour || 1"
|
||||||
|
:stepMinute="field.stepMinute || 1"
|
||||||
|
:showSeconds="field.showSeconds || false"
|
||||||
|
:stepSecond="field.stepSecond || 1"
|
||||||
|
:showButtonBar="field.showButtonBar !== false"
|
||||||
|
:todayButtonLabel="field.todayButtonLabel || 'Today'"
|
||||||
|
:clearButtonLabel="field.clearButtonLabel || 'Clear'"
|
||||||
|
:manualInput="field.manualInput !== false"
|
||||||
|
:yearNavigator="field.yearNavigator || false"
|
||||||
|
:monthNavigator="field.monthNavigator || false"
|
||||||
|
:yearRange="field.yearRange || '1900:2100'"
|
||||||
|
:inline="field.inline || false"
|
||||||
|
:touchUI="field.touchUI || false"
|
||||||
|
@update:model-value="handleDateChange(field, $event)"
|
||||||
@blur="handleFieldBlur(field, fieldValues[field.name])"
|
@blur="handleFieldBlur(field, fieldValues[field.name])"
|
||||||
/>
|
/>
|
||||||
<small v-if="field.helpText" class="field-help">{{
|
<small v-if="field.helpText" class="field-help">{{
|
||||||
@ -536,6 +570,105 @@ const initializeFormData = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Date utility functions
|
||||||
|
const getDateDefaultValue = (field) => {
|
||||||
|
if (field.defaultValue !== undefined) {
|
||||||
|
// Handle string date values
|
||||||
|
if (typeof field.defaultValue === "string") {
|
||||||
|
if (field.defaultValue === "today" || field.defaultValue === "now") {
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
// Try to parse the string as a date
|
||||||
|
const parsed = new Date(field.defaultValue);
|
||||||
|
return !isNaN(parsed.getTime()) ? parsed : null;
|
||||||
|
}
|
||||||
|
// Handle Date objects or null/undefined
|
||||||
|
return field.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default based on field configuration
|
||||||
|
if (field.defaultToToday || field.defaultToNow) {
|
||||||
|
const now = new Date();
|
||||||
|
if (field.type === "date" && !field.showTime) {
|
||||||
|
// For date-only fields, set to start of day
|
||||||
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
}
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No default value
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseDateValue = (value) => {
|
||||||
|
if (!value) return null;
|
||||||
|
if (value instanceof Date) return value;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const parsed = new Date(value);
|
||||||
|
return !isNaN(parsed.getTime()) ? parsed : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDateFormat = (field) => {
|
||||||
|
// Return custom format if provided
|
||||||
|
if (field.dateFormat) {
|
||||||
|
return field.dateFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle predefined format strings
|
||||||
|
if (field.format) {
|
||||||
|
switch (field.format.toLowerCase()) {
|
||||||
|
case "yyyy-mm-dd":
|
||||||
|
return "yy-mm-dd";
|
||||||
|
case "mm/dd/yyyy":
|
||||||
|
return "mm/dd/yy";
|
||||||
|
case "dd/mm/yyyy":
|
||||||
|
return "dd/mm/yy";
|
||||||
|
case "dd-mm-yyyy":
|
||||||
|
return "dd-mm-yy";
|
||||||
|
case "mm-dd-yyyy":
|
||||||
|
return "mm-dd-yy";
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default formats based on field configuration
|
||||||
|
if (field.showTime || field.type === "datetime") {
|
||||||
|
return "dd/mm/yy"; // PrimeVue will append time format automatically
|
||||||
|
}
|
||||||
|
|
||||||
|
return "dd/mm/yy"; // Default date format
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDatePlaceholder = (field) => {
|
||||||
|
if (field.placeholder) return field.placeholder;
|
||||||
|
|
||||||
|
const format = field.format || (field.showTime ? "dd/mm/yyyy hh:mm" : "dd/mm/yyyy");
|
||||||
|
|
||||||
|
if (field.timeOnly) {
|
||||||
|
return "Select time";
|
||||||
|
} else if (field.showTime || field.type === "datetime") {
|
||||||
|
return `Enter date and time (${format})`;
|
||||||
|
} else {
|
||||||
|
return `Enter date (${format})`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateChange = (field, value) => {
|
||||||
|
// Convert Date object to appropriate format if needed
|
||||||
|
let processedValue = value;
|
||||||
|
|
||||||
|
// Apply custom formatting if specified
|
||||||
|
if (field.onDateChange && typeof field.onDateChange === "function") {
|
||||||
|
processedValue = field.onDateChange(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the standard field change handler
|
||||||
|
handleFieldChange(field, processedValue);
|
||||||
|
};
|
||||||
|
|
||||||
// Get default value for a field based on its type
|
// Get default value for a field based on its type
|
||||||
const getDefaultValue = (field) => {
|
const getDefaultValue = (field) => {
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
@ -548,6 +681,9 @@ const getDefaultValue = (field) => {
|
|||||||
return field.defaultValue !== undefined ? field.defaultValue : "";
|
return field.defaultValue !== undefined ? field.defaultValue : "";
|
||||||
case "file":
|
case "file":
|
||||||
return null;
|
return null;
|
||||||
|
case "date":
|
||||||
|
case "datetime":
|
||||||
|
return getDateDefaultValue(field);
|
||||||
default:
|
default:
|
||||||
return field.defaultValue !== undefined ? field.defaultValue : "";
|
return field.defaultValue !== undefined ? field.defaultValue : "";
|
||||||
}
|
}
|
||||||
@ -634,6 +770,29 @@ const validateField = (field, value) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Date validation
|
||||||
|
if ((field.type === "date" || field.type === "datetime") && value) {
|
||||||
|
const dateValue = value instanceof Date ? value : parseDateValue(value);
|
||||||
|
|
||||||
|
if (!dateValue || isNaN(dateValue.getTime())) {
|
||||||
|
errors.push("Please enter a valid date");
|
||||||
|
} else {
|
||||||
|
// Min/Max date validation
|
||||||
|
if (field.minDate) {
|
||||||
|
const minDate = parseDateValue(field.minDate);
|
||||||
|
if (minDate && dateValue < minDate) {
|
||||||
|
errors.push(`Date must be on or after ${minDate.toLocaleDateString()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field.maxDate) {
|
||||||
|
const maxDate = parseDateValue(field.maxDate);
|
||||||
|
if (maxDate && dateValue > maxDate) {
|
||||||
|
errors.push(`Date must be on or before ${maxDate.toLocaleDateString()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Custom validation (always runs last)
|
// Custom validation (always runs last)
|
||||||
if (field.validate && typeof field.validate === "function") {
|
if (field.validate && typeof field.validate === "function") {
|
||||||
const customError = field.validate(value);
|
const customError = field.validate(value);
|
||||||
|
|||||||
204
frontend/src/components/pages/TestDateForm.vue
Normal file
204
frontend/src/components/pages/TestDateForm.vue
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<div class="test-date-form">
|
||||||
|
<h2>Enhanced Date Picker Test</h2>
|
||||||
|
<p>This page demonstrates the enhanced date picker functionality in the Form component.</p>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
:fields="dateFields"
|
||||||
|
:form-data="formData"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@change="handleFieldChange"
|
||||||
|
submit-button-text="Save Dates"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="Object.keys(formData).length" class="results mt-6">
|
||||||
|
<h3>Current Form Values:</h3>
|
||||||
|
<div class="results-grid">
|
||||||
|
<div v-for="(value, key) in formData" :key="key" class="result-item">
|
||||||
|
<strong>{{ formatFieldLabel(key) }}:</strong>
|
||||||
|
<span v-if="value instanceof Date" class="date-value">
|
||||||
|
{{ formatDateValue(key, value) }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="value" class="value">{{ value }}</span>
|
||||||
|
<span v-else class="empty">Not set</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import Form from "../common/Form.vue";
|
||||||
|
|
||||||
|
const formData = ref({});
|
||||||
|
|
||||||
|
const dateFields = [
|
||||||
|
// Basic date picker with US format
|
||||||
|
{
|
||||||
|
name: "birthDate",
|
||||||
|
label: "Birth Date",
|
||||||
|
type: "date",
|
||||||
|
format: "mm/dd/yyyy",
|
||||||
|
required: true,
|
||||||
|
maxDate: "today",
|
||||||
|
yearNavigator: true,
|
||||||
|
yearRange: "1920:2010",
|
||||||
|
cols: 12,
|
||||||
|
md: 6,
|
||||||
|
helpText: "Your date of birth",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Date with default to today
|
||||||
|
{
|
||||||
|
name: "startDate",
|
||||||
|
label: "Project Start Date",
|
||||||
|
type: "date",
|
||||||
|
format: "YYYY-MM-DD",
|
||||||
|
defaultToToday: true,
|
||||||
|
minDate: "today",
|
||||||
|
cols: 12,
|
||||||
|
md: 6,
|
||||||
|
helpText: "When should this project start?",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Date and time picker
|
||||||
|
{
|
||||||
|
name: "appointmentDateTime",
|
||||||
|
label: "Appointment Date & Time",
|
||||||
|
type: "date",
|
||||||
|
showTime: true,
|
||||||
|
hourFormat: "12",
|
||||||
|
stepMinute: 15,
|
||||||
|
defaultToNow: true,
|
||||||
|
minDate: "today",
|
||||||
|
cols: 12,
|
||||||
|
md: 6,
|
||||||
|
helpText: "Select appointment date and time",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Time-only picker
|
||||||
|
{
|
||||||
|
name: "preferredTime",
|
||||||
|
label: "Preferred Contact Time",
|
||||||
|
type: "date",
|
||||||
|
timeOnly: true,
|
||||||
|
hourFormat: "12",
|
||||||
|
stepMinute: 30,
|
||||||
|
defaultValue: "09:00",
|
||||||
|
cols: 12,
|
||||||
|
md: 6,
|
||||||
|
helpText: "Best time to contact you",
|
||||||
|
},
|
||||||
|
|
||||||
|
// European date format with week numbers
|
||||||
|
{
|
||||||
|
name: "eventDate",
|
||||||
|
label: "Event Date",
|
||||||
|
type: "date",
|
||||||
|
format: "dd/mm/yyyy",
|
||||||
|
showWeek: true,
|
||||||
|
monthNavigator: true,
|
||||||
|
yearNavigator: true,
|
||||||
|
yearRange: "2024:2026",
|
||||||
|
cols: 12,
|
||||||
|
md: 6,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Date with seconds
|
||||||
|
{
|
||||||
|
name: "preciseTime",
|
||||||
|
label: "Precise Timestamp",
|
||||||
|
type: "date",
|
||||||
|
showTime: true,
|
||||||
|
showSeconds: true,
|
||||||
|
hourFormat: "24",
|
||||||
|
stepSecond: 1,
|
||||||
|
cols: 12,
|
||||||
|
md: 6,
|
||||||
|
helpText: "Precise time with seconds",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSubmit = (data) => {
|
||||||
|
console.log("Date form submitted:", data);
|
||||||
|
alert("Form submitted! Check console for details.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFieldChange = (event) => {
|
||||||
|
console.log("Field changed:", event.fieldName, "->", event.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFieldLabel = (fieldName) => {
|
||||||
|
const field = dateFields.find((f) => f.name === fieldName);
|
||||||
|
return field ? field.label : fieldName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateValue = (fieldName, dateValue) => {
|
||||||
|
if (!dateValue) return "Not set";
|
||||||
|
|
||||||
|
const field = dateFields.find((f) => f.name === fieldName);
|
||||||
|
|
||||||
|
if (field?.timeOnly) {
|
||||||
|
return dateValue.toLocaleTimeString();
|
||||||
|
} else if (field?.showTime) {
|
||||||
|
return dateValue.toLocaleString();
|
||||||
|
} else {
|
||||||
|
return dateValue.toLocaleDateString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.test-date-form {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-value {
|
||||||
|
color: #0066cc;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.result-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -59,8 +59,8 @@ const columns = [
|
|||||||
filterable: true,
|
filterable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Address",
|
label: "Service Address",
|
||||||
fieldName: "address",
|
fieldName: "serviceAddress",
|
||||||
type: "text",
|
type: "text",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
@ -69,38 +69,50 @@ const columns = [
|
|||||||
label: "Issue Description",
|
label: "Issue Description",
|
||||||
fieldName: "issueDescription",
|
fieldName: "issueDescription",
|
||||||
type: "text",
|
type: "text",
|
||||||
sortable: true,
|
sortable: false,
|
||||||
|
filterable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Priority",
|
label: "Priority",
|
||||||
fieldName: "priority",
|
fieldName: "priority",
|
||||||
type: "status",
|
type: "status",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
filterable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Status",
|
label: "Status",
|
||||||
fieldName: "status",
|
fieldName: "status",
|
||||||
type: "status",
|
type: "status",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Assigned Technician",
|
label: "Complaint Date",
|
||||||
fieldName: "assignedTechnician",
|
fieldName: "complaintDate",
|
||||||
|
type: "date",
|
||||||
|
sortable: true,
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Raised By",
|
||||||
|
fieldName: "complaintRaisedBy",
|
||||||
type: "text",
|
type: "text",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Date Reported",
|
label: "Territory",
|
||||||
fieldName: "dateReported",
|
fieldName: "territory",
|
||||||
type: "text",
|
type: "text",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Est. Completion",
|
label: "Warranty Status",
|
||||||
fieldName: "estimatedCompletionDate",
|
fieldName: "warrantyStatus",
|
||||||
type: "text",
|
type: "status",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -155,21 +167,18 @@ const handleLazyLoad = async (event) => {
|
|||||||
|
|
||||||
console.log("Making API call with:", { paginationParams, filters });
|
console.log("Making API call with:", { paginationParams, filters });
|
||||||
|
|
||||||
// For now, use existing API but we should create a paginated version
|
// Get sorting from store for proper API call
|
||||||
// TODO: Create Api.getPaginatedWarrantyData() method
|
const sorting = filtersStore.getTableSorting("warranties");
|
||||||
let data = await Api.getWarrantyData();
|
|
||||||
|
|
||||||
// Simulate pagination on client side for now
|
// Use the new paginated API method
|
||||||
const startIndex = paginationParams.page * paginationParams.pageSize;
|
const result = await Api.getPaginatedWarrantyData(paginationParams, filters, sorting);
|
||||||
const endIndex = startIndex + paginationParams.pageSize;
|
|
||||||
const paginatedData = data.slice(startIndex, endIndex);
|
|
||||||
|
|
||||||
// Update local state
|
// Update local state
|
||||||
tableData.value = paginatedData;
|
tableData.value = result.data;
|
||||||
totalRecords.value = data.length;
|
totalRecords.value = result.pagination.total;
|
||||||
|
|
||||||
// Update pagination store with new total
|
// Update pagination store with new total
|
||||||
paginationStore.setTotalRecords("warranties", data.length);
|
paginationStore.setTotalRecords("warranties", result.pagination.total);
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
paginationStore.setCachedPage(
|
paginationStore.setCachedPage(
|
||||||
@ -180,14 +189,14 @@ const handleLazyLoad = async (event) => {
|
|||||||
paginationParams.sortOrder,
|
paginationParams.sortOrder,
|
||||||
filters,
|
filters,
|
||||||
{
|
{
|
||||||
records: paginatedData,
|
records: result.data,
|
||||||
totalRecords: data.length,
|
totalRecords: result.pagination.total,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("Loaded from API:", {
|
console.log("Loaded from API:", {
|
||||||
records: paginatedData.length,
|
records: result.data.length,
|
||||||
total: data.length,
|
total: result.pagination.total,
|
||||||
page: paginationParams.page + 1,
|
page: paginationParams.page + 1,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import Routes from "./components/pages/Routes.vue";
|
|||||||
import TimeSheets from "./components/pages/TimeSheets.vue";
|
import TimeSheets from "./components/pages/TimeSheets.vue";
|
||||||
import Warranties from "./components/pages/Warranties.vue";
|
import Warranties from "./components/pages/Warranties.vue";
|
||||||
import Home from "./components/pages/Home.vue";
|
import Home from "./components/pages/Home.vue";
|
||||||
|
import TestDateForm from "./components/pages/TestDateForm.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -21,6 +22,7 @@ const routes = [
|
|||||||
{ path: "/create", component: Create },
|
{ path: "/create", component: Create },
|
||||||
{ path: "/timesheets", component: TimeSheets },
|
{ path: "/timesheets", component: TimeSheets },
|
||||||
{ path: "/warranties", component: Warranties },
|
{ path: "/warranties", component: Warranties },
|
||||||
|
{ path: "/test-dates", component: TestDateForm },
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user