From 82f9b1aac23f17993f47ea475d64c90ad257cd41 Mon Sep 17 00:00:00 2001 From: Casey Wittrock Date: Fri, 7 Nov 2025 10:29:37 -0600 Subject: [PATCH] add date picker --- custom_ui/api/db.py | 159 ++++++- frontend/documentation/components/Form.md | 421 +++++++++++++++++- frontend/src/api.js | 31 ++ frontend/src/components/common/DataTable.vue | 44 +- frontend/src/components/common/Form.vue | 181 +++++++- .../src/components/pages/TestDateForm.vue | 204 +++++++++ frontend/src/components/pages/Warranties.vue | 57 ++- frontend/src/router.js | 2 + 8 files changed, 1044 insertions(+), 55 deletions(-) create mode 100644 frontend/src/components/pages/TestDateForm.vue diff --git a/custom_ui/api/db.py b/custom_ui/api/db.py index 64cc4a0..dca46d0 100644 --- a/custom_ui/api/db.py +++ b/custom_ui/api/db.py @@ -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 @frappe.whitelist() @@ -316,5 +317,157 @@ def get_jobs(options): } @frappe.whitelist() -def upsert_estimate(): - pass +def get_warranty_claims(options): + 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 + } \ No newline at end of file diff --git a/frontend/documentation/components/Form.md b/frontend/documentation/components/Form.md index cf8d4f4..2c80671 100644 --- a/frontend/documentation/components/Form.md +++ b/frontend/documentation/components/Form.md @@ -8,8 +8,8 @@ A highly flexible and configurable dynamic form component built with **PrimeVue* ## ✨ New Features (PrimeVue Migration) - **AutoComplete component** - Users can select from suggestions OR enter completely custom values -- **Better date/time pickers** with calendar popup and time selection -- **Improved accessibility** with ARIA support +- **Enhanced Date/Time Pickers** - Comprehensive date handling with multiple formats, time selection, constraints, and smart defaults +- **Better accessibility** with ARIA support - **More flexible styling** with CSS custom properties - **Enhanced mobile responsiveness** with CSS Grid @@ -363,41 +363,152 @@ Radio button group for single selection from multiple options. ### Date Input (`type: 'date'`) -Date picker input field. +Enhanced date picker input field with comprehensive formatting and configuration options. ```javascript +// Basic date input { name: 'birthDate', label: 'Birth Date', type: 'date', 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:** -- **`min`** (String) - Minimum allowed date (YYYY-MM-DD format) -- **`max`** (String) - Maximum 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'` +- **`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'`) - -Date and time picker input field. +**Default Value Options:** ```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', label: 'Appointment Time', type: 'datetime', required: true } + +// RECOMMENDED - Use this instead +{ + name: 'appointmentTime', + label: 'Appointment Time', + type: 'date', + showTime: true, + required: true +} ``` **Additional Properties:** - **`min`** (String) - Minimum allowed datetime - **`max`** (String) - Maximum allowed datetime +- **`hourFormat`** (String, default: `'24'`) - Hour format: `'12'` or `'24'` ### File Input (`type: 'file'`) @@ -526,6 +637,18 @@ const contactFields = [ format: "email", required: true, 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", @@ -675,6 +798,280 @@ const autoCompleteFields = [ ``` +### Enhanced Date Picker Examples + +```vue + + + +``` + +### Real-World Date Picker Scenarios + +```vue + + + +``` + +```vue + +``` + ### User Registration Form ```vue @@ -1028,8 +1425,8 @@ The component has been completely migrated from Vuetify to PrimeVue. Here's what ### ✨ What's New - **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 file uploads** - Drag & drop, better validation, file preview +- **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 +- **Better file uploads** - Drag & drop, better validation, file preview - **Improved accessibility** - Full ARIA support, better keyboard navigation - **Flexible styling** - CSS custom properties, easier theming - **Mobile-first responsive** - Better grid system, improved mobile UX diff --git a/frontend/src/api.js b/frontend/src/api.js index cdc1010..a71f8e2 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -305,6 +305,37 @@ class Api { 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. * diff --git a/frontend/src/components/common/DataTable.vue b/frontend/src/components/common/DataTable.vue index c078a04..7d15c2f 100644 --- a/frontend/src/components/common/DataTable.vue +++ b/frontend/src/components/common/DataTable.vue @@ -135,6 +135,9 @@ :severity="getBadgeColor(slotProps.data[col.fieldName])" /> +