From df1df3f882b6c27a967749f59a30ae2c8cd8986b Mon Sep 17 00:00:00 2001 From: Casey Wittrock Date: Tue, 11 Nov 2025 09:50:23 -0600 Subject: [PATCH] add table actions to datatable, client page, start writing db method for clients --- custom_ui/api/db.py | 113 +-- frontend/action-behavior-test.html | 278 ++++++ .../documentation/components/DataTable.md | 449 +++++++++ frontend/src/api.js | 29 +- frontend/src/components/common/DataTable.vue | 895 +++++++++++++++++- frontend/src/components/pages/Client.vue | 9 + frontend/src/components/pages/Clients.vue | 133 ++- frontend/src/router.js | 2 + frontend/test-datatable-actions.html | 130 +++ 9 files changed, 1844 insertions(+), 194 deletions(-) create mode 100644 frontend/action-behavior-test.html create mode 100644 frontend/src/components/pages/Client.vue create mode 100644 frontend/test-datatable-actions.html diff --git a/custom_ui/api/db.py b/custom_ui/api/db.py index 9d152f2..167a503 100644 --- a/custom_ui/api/db.py +++ b/custom_ui/api/db.py @@ -1,6 +1,5 @@ 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() def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=None): @@ -16,13 +15,6 @@ def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=N filters = {status_field: status_value} filters.update(base_filters) return filters - - def get_status_total(counts_dicts, status_field): - sum_array = [] - for counts_dict in counts_dicts: - sum_array.append(counts_dict[status_field]) - return sum(sum_array) - onsite_meeting_scheduled_status_counts = { "label": "On-Site Meeting Scheduled", @@ -58,6 +50,7 @@ def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=N job_status_counts, payment_received_status_counts ] + categories = [] for status_dict in status_dicts: category = { @@ -85,7 +78,28 @@ def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=N return categories @frappe.whitelist() -def get_clients(options): +def get_client(client_name): + address = frappe.get_doc("Address", client_name) + customer_name = [link for link in address.links if link.link_doctype == "Customer"][0].link_name + project_names = frappe.db.get_all("Project", fields=["name"], filters=[ + ["or", [ + ["custom_installation_address", "=", address.address_title], + ["custom_address", "=", address.address_title] + ]] + ]) + + projects = [frappe.get_doc("Project", proj["name"]) for proj in project_names] + customer = frappe.get_doc("Customer", customer_name) + # get all associated data as needed + return { + "address": address, + "customer": customer, + "projects": projects + } + + +@frappe.whitelist() +def get_clients_table_data(options): options = json.loads(options) print("DEBUG: Raw options received:", options) defaultOptions = { @@ -98,9 +112,6 @@ def get_clients(options): options = {**defaultOptions, **options} print("DEBUG: Final options:", options) - clients = [] - tableRows = [] - # Map frontend field names to backend field names def map_field_name(frontend_field): field_mapping = { @@ -162,75 +173,23 @@ def get_clients(options): addresses = frappe.db.get_all( "Address", - fields=["address_title", "custom_onsite_meeting_scheduled", "custom_estimate_sent_status", "custom_job_status", "custom_payment_received_status"], + fields=["name", "address_title", "custom_onsite_meeting_scheduled", "custom_estimate_sent_status", "custom_job_status", "custom_payment_received_status"], filters=processed_filters, limit=options["page_size"], start=(options["page"] - 1) * options["page_size"], order_by=order_by ) - - # for address in addresses: - # client = {} - # tableRow = {} - - # on_site_meetings = frappe.db.get_all( - # "On-Site Meeting", - # fields=["*"], - # filters={"address": address["address_title"]} - # ) - - # quotations = frappe.db.get_all( - # "Quotation", - # fields=["*"], - # filters={"custom_installation_address": address["address_title"]} - # ) - - # sales_orders = frappe.db.get_all( - # "Sales Order", - # fields=["*"], - # filters={"custom_installation_address": address["address_title"]} - # ) - - # sales_invvoices = frappe.db.get_all( - # "Sales Invoice", - # fields=["*"], - # filters={"custom_installation_address": address["address_title"]} - # ) - - # payment_entries = frappe.db.get_all( - # "Payment Entry", - # fields=["*"], - # filters={"custom_installation_address": address["address_title"]} - # ) - - # jobs = frappe.db.get_all( - # "Project", - # fields=["*"], - # filters={ - # "custom_installation_address": address["address_title"], - # "project_template": "SNW Install" - # } - # ) - - # tasks = frappe.db.get_all( - # "Task", - # fields=["*"], - # filters={"project": jobs[0]["name"]} - # ) if jobs else [] - - # tableRow["id"] = address["name"] - # tableRow["address_title"] = address["address_title"] - # tableRow["appointment_scheduled_status"] = calculate_appointment_scheduled_status(on_site_meetings[0]) if on_site_meetings else "Not Started" - # tableRow["estimate_sent_status"] = calculate_estimate_sent_status(quotations[0]) if quotations else "Not Started" - # tableRow["payment_received_status"] = calculate_payment_recieved_status(sales_invvoices[0], payment_entries) if sales_invvoices and payment_entries else "Not Started" - # tableRow["job_status"] = calculate_job_status(jobs[0], tasks) if jobs and tasks else "Not Started" - # tableRows.append(tableRow) - - # client["address"] = address - # client["on_site_meetings"] = on_site_meetings - # client["jobs"] = jobs - # client["quotations"] = quotations - # clients.append(client) + + rows = [] + for address in addresses: + tableRow = {} + tableRow["id"] = address["name"] + tableRow["address_title"] = address["address_title"] + tableRow["appointment_scheduled_status"] = address["custom_onsite_meeting_scheduled"] + tableRow["estimate_sent_status"] = address["custom_estimate_sent_status"] + tableRow["job_status"] = address["custom_job_status"] + tableRow["payment_received_status"] = address["custom_payment_received_status"] + rows.append(tableRow) return { "pagination": { @@ -239,7 +198,7 @@ def get_clients(options): "page_size": options["page_size"], "total_pages": (count + options["page_size"] - 1) // options["page_size"] }, - "data": addresses + "data": rows } diff --git a/frontend/action-behavior-test.html b/frontend/action-behavior-test.html new file mode 100644 index 0000000..fad3c90 --- /dev/null +++ b/frontend/action-behavior-test.html @@ -0,0 +1,278 @@ + + + + + + Updated DataTable Actions Behavior Test + + + +

Updated DataTable Actions Behavior Test

+ +

✅ New Action Behavior Summary

+ +
+

Action Type Changes:

+ +
+ +
+

Updated Action Types Matrix:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Action TypePropertyLocationEnabled WhenData Received
GlobalNone (default)Above tableAlwaysNone
Single SelectionrequiresSelection: trueAbove tableExactly 1 row selectedSelected row object
Row ActionrowAction: trueActions columnAlways (per row)Individual row object
BulkrequiresMultipleSelection: trueAbove table (when selected)1+ rows selectedArray of selected rows
+
+ +
+

Implementation Changes Made:

+
    +
  1. + Computed Properties Updated: +
      +
    • + globalActions: Actions with no special + properties +
    • +
    • + singleSelectionActions: Actions with + requiresSelection: true +
    • +
    • + rowActions: Actions with + rowAction: true +
    • +
    • + bulkActions: Actions with + requiresMultipleSelection: true +
    • +
    +
  2. +
  3. + Template Updates: +
      +
    • Global Actions section now includes single selection actions
    • +
    • + Single selection actions are disabled unless exactly one row is + selected +
    • +
    • Visual feedback shows selection state
    • +
    • + Actions column only shows + rowAction: true actions +
    • +
    +
  4. +
  5. + New Handler Added: +
      +
    • + handleSingleSelectionAction: Passes selected + row data to action +
    • +
    +
  6. +
+
+ +
+

Example Configuration (Clients.vue):

+
+
+const tableActions = [
+  // Global action - always available
+  {
+    label: "Add Client",
+    action: () => modalStore.openModal("createClient"),
+    icon: "pi pi-plus",
+    style: "primary"
+  },
+  // Single selection action - enabled when exactly one row selected
+  {
+    label: "View Details", 
+    action: (rowData) => router.push(`/clients/${rowData.id}`),
+    icon: "pi pi-eye",
+    style: "info",
+    requiresSelection: true
+  },
+  // Bulk action - enabled when rows selected
+  {
+    label: "Export Selected",
+    action: (selectedRows) => exportData(selectedRows),
+    icon: "pi pi-download", 
+    style: "success",
+    requiresMultipleSelection: true
+  },
+  // Row actions - appear in each row
+  {
+    label: "Edit",
+    action: (rowData) => editClient(rowData),
+    icon: "pi pi-pencil",
+    style: "secondary",
+    rowAction: true
+  },
+  {
+    label: "Quick View",
+    action: (rowData) => showPreview(rowData),
+    icon: "pi pi-search",
+    style: "info", 
+    rowAction: true
+  }
+];
+
+
+ +
+

User Experience Improvements:

+ +
+ +
+

Action Flow Examples:

+ +
+ +

✅ Testing Checklist

+ + + diff --git a/frontend/documentation/components/DataTable.md b/frontend/documentation/components/DataTable.md index c30bacc..c4596a1 100644 --- a/frontend/documentation/components/DataTable.md +++ b/frontend/documentation/components/DataTable.md @@ -11,6 +11,7 @@ A feature-rich data table component built with PrimeVue's DataTable. This compon @@ -41,6 +42,32 @@ const tableData = ref([ { id: 2, name: "Jane Smith", status: "in progress" }, ]); +const tableActions = ref([ + { + label: "Add Item", + action: () => console.log("Add clicked"), + icon: "pi pi-plus", + style: "primary", + // Global action - always available + }, + { + label: "View Details", + action: (rowData) => console.log("View:", rowData), + icon: "pi pi-eye", + style: "info", + requiresSelection: true, + // Single selection action - enabled when exactly one row selected + }, + { + label: "Edit", + action: (rowData) => console.log("Edit:", rowData), + icon: "pi pi-pencil", + style: "secondary", + rowAction: true, + // Row action - appears in each row's actions column + }, +]); + const handleRowClick = (event) => { console.log("Row clicked:", event.data); }; @@ -85,6 +112,12 @@ const handleRowClick = (event) => { - **Type:** `Object` - **Default:** `{ global: { value: null, matchMode: FilterMatchMode.CONTAINS } }` +### `tableActions` (Array) + +- **Description:** Array of action objects that define interactive buttons for the table. Actions can be global (always available), single-selection (enabled when exactly one row is selected), row-specific (displayed per row), or bulk (for multiple selected rows). +- **Type:** `Array` +- **Default:** `[]` + ## Server-Side Pagination & Lazy Loading When `lazy` is set to `true`, the DataTable operates in server-side mode with the following features: @@ -159,6 +192,179 @@ Renders values as clickable buttons: } ``` +## Table Actions Configuration + +Table actions allow you to add interactive buttons to your DataTable. Actions can be either global (displayed above the table) or row-specific (displayed in an actions column). + +### Action Object Properties + +Each action object in the `tableActions` array supports the following properties: + +#### Basic Properties + +- **`label`** (String, required) - Display text for the button +- **`action`** (Function, required) - Function to execute when button is clicked +- **`icon`** (String, optional) - PrimeVue icon class (e.g., 'pi pi-plus') +- **`style`** (String, optional) - Button severity: 'primary', 'secondary', 'success', 'info', 'warning', 'danger' +- **`size`** (String, optional) - Button size: 'small', 'normal', 'large' +- **`requiresSelection`** (Boolean, default: false) - When true, action appears above table but is only enabled when exactly one row is selected +- **`requiresMultipleSelection`** (Boolean, default: false) - Determines if action is for bulk operations on selected rows +- **`rowAction`** (Boolean, default: false) - When true, action appears in each row's actions column +- **`layout`** (Object, optional) - Layout configuration for action positioning and styling + +#### Layout Configuration + +The `layout` property allows you to control where and how actions are displayed: + +##### For Top-Level Actions (Global and Single Selection) + +```javascript +layout: { + position: "left" | "center" | "right", // Where to position in action bar + variant: "filled" | "outlined" | "text" // Visual style variant +} +``` + +##### For Row Actions + +```javascript +layout: { + priority: "primary" | "secondary" | "dropdown", // Display priority in row + variant: "outlined" | "text" | "compact" | "icon-only" // Visual style +} +``` + +##### For Bulk Actions + +```javascript +layout: { + position: "left" | "center" | "right", // Where to position in bulk action bar + variant: "filled" | "outlined" | "text" // Visual style variant +} +``` + +#### Action Types + +##### Global Actions (default behavior) + +Global actions are displayed above the table and are always available: + +```javascript +{ + label: "Add New Item", + action: () => { + // Global action - no row data + console.log("Opening create modal"); + }, + icon: "pi pi-plus", + style: "primary" + // No requiresSelection, requiresMultipleSelection, or rowAction properties +} +``` + +##### Single Selection Actions (`requiresSelection: true`) + +Single selection actions are displayed above the table but are only enabled when exactly one row is selected. They receive the selected row data as a parameter: + +```javascript +{ + label: "View Details", + action: (rowData) => { + // Single selection action - receives selected row data + console.log("Viewing:", rowData.name); + router.push(`/items/${rowData.id}`); + }, + icon: "pi pi-eye", + style: "info", + requiresSelection: true +} +``` + +##### Row Actions (`rowAction: true`) + +Row actions are displayed in an "Actions" column for each row and receive that row's data as a parameter: + +```javascript +{ + label: "Edit", + action: (rowData) => { + // Row action - receives individual row data + console.log("Editing:", rowData.name); + openEditModal(rowData); + }, + icon: "pi pi-pencil", + style: "secondary", + rowAction: true +} +``` + +##### Bulk Actions (`requiresMultipleSelection: true`) + +Bulk actions are displayed above the table when rows are selected and receive an array of selected row data: + +```javascript +{ + label: "Delete Selected", + action: (selectedRows) => { + // Bulk action - receives array of selected row data + console.log("Deleting:", selectedRows.length, "items"); + selectedRows.forEach(row => deleteItem(row.id)); + }, + icon: "pi pi-trash", + style: "danger", + requiresMultipleSelection: true +} +``` + +### Example Table Actions Configuration + +```javascript +const tableActions = [ + // Global action - shows above table, always available + { + label: "Add Client", + action: () => modalStore.openModal("createClient"), + icon: "pi pi-plus", + style: "primary", + }, + // Single selection action - shows above table, enabled when exactly one row selected + { + label: "View Details", + action: (rowData) => router.push(`/clients/${rowData.id}`), + icon: "pi pi-eye", + style: "info", + requiresSelection: true, + }, + // Bulk action - shows when rows selected + { + label: "Delete Selected", + action: (selectedRows) => { + if (confirm(`Delete ${selectedRows.length} clients?`)) { + selectedRows.forEach((row) => deleteClient(row.id)); + } + }, + icon: "pi pi-trash", + style: "danger", + requiresMultipleSelection: true, + }, + // Row actions - show in each row's actions column + { + label: "Edit", + action: (rowData) => editClient(rowData), + icon: "pi pi-pencil", + style: "secondary", + rowAction: true, + }, + { + label: "Quick View", + action: (rowData) => showQuickPreview(rowData), + icon: "pi pi-search", + style: "info", + rowAction: true, + }, +]; +``` + ## Events ### `rowClick` @@ -253,6 +459,17 @@ const handleLazyLoad = async (event) => { - **Automatic filter initialization** on component mount - **Cross-component filter synchronization** +### Table Actions + +- **Global actions** displayed above the table for general operations +- **Row-specific actions** in dedicated actions column with row data access +- **Bulk actions** for selected rows with multi-selection support +- **Customizable button styles** with PrimeVue severity levels +- **Icon support** using PrimeVue icons +- **Automatic action handling** with error catching +- **Disabled state** during loading operations +- **Dynamic bulk action visibility** based on row selection + ## Usage Examples ### Server-Side Paginated Table (Recommended for Large Datasets) @@ -324,6 +541,126 @@ const handleLazyLoad = async (event) => { ``` +### Interactive Table with Actions + +```vue + + + +``` + ### Basic Client-Side Table ```vue @@ -427,6 +764,118 @@ const customFilters = { ``` +### Layout-Aware Actions Example + +```vue + + + +``` + ## Store Integration The component integrates with a Pinia store (`useFiltersStore`) for persistent filter state: diff --git a/frontend/src/api.js b/frontend/src/api.js index 2349fe1..c549762 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -7,18 +7,16 @@ const FRAPPE_UPSERT_ESTIMATE_METHOD = "custom_ui.api.db.upsert_estimate"; const FRAPPE_UPSERT_JOB_METHOD = "custom_ui.api.db.upsert_job"; const FRAPPE_UPSERT_INVOICE_METHOD = "custom_ui.api.db.upsert_invoice"; const FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD = "custom_ui.api.db.get_client_status_counts"; +const FRAPPE_GET_CLIENT_TABLE_DATA_METHOD = "custom_ui.api.db.get_clients_table_data"; +const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.get_client"; class Api { static async request(frappeMethod, args = {}) { args = DataUtils.toSnakeCaseObject(args); - console.log("DEBUG: API - Request Args: ", { method: frappeMethod, args }); + const request = { method: frappeMethod, args }; + console.log("DEBUG: API - Request Args: ", request); try { - let response = await frappe.call({ - method: frappeMethod, - args: { - ...args, - }, - }); + let response = await frappe.call(request); response = DataUtils.toCamelCaseObject(response); console.log("DEBUG: API - Request Response: ", response); return response.message; @@ -33,8 +31,8 @@ class Api { return await this.request(FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD, params); } - static async getClientDetails(options = {}) { - return await this.request("custom_ui.api.db.get_clients", { options }); + static async getClientDetails(clientName) { + return await this.request(FRAPPE_GET_CLIENT_DETAILS_METHOD); } static async getJobDetails() { @@ -51,13 +49,11 @@ class Api { }; data.push(tableRow); } - console.log("DEBUG: API - getJobDetails result: ", data); return data; } static async getServiceData() { const data = DataUtils.dummyServiceData; - console.log("DEBUG: API - getServiceData result: ", data); return data; } @@ -69,8 +65,6 @@ class Api { route = getDetailedDoc("Pre-Built Routes", rt.name); let tableRow = {}; } - - console.log("DEBUG: API - getRouteData result: ", data); return data; } @@ -102,6 +96,10 @@ class Api { return data; } + static async getClient(clientName) { + return await this.request(FRAPPE_GET_CLIENT_METHOD, { clientName }); + } + /** * Get paginated client data with filtering and sorting * @param {Object} paginationParams - Pagination parameters from store @@ -126,10 +124,7 @@ class Api { : null, for_table: true, }; - - console.log("DEBUG: API - Sending options to backend:", options); - - const result = await this.request("custom_ui.api.db.get_clients", { options }); + const result = await this.request(FRAPPE_GET_CLIENT_TABLE_DATA_METHOD, { options }); return result; } diff --git a/frontend/src/components/common/DataTable.vue b/frontend/src/components/common/DataTable.vue index de85ccd..b63cc55 100644 --- a/frontend/src/components/common/DataTable.vue +++ b/frontend/src/components/common/DataTable.vue @@ -3,44 +3,53 @@
-
-
- - -
-
-
-
+
+
+
+ +
-
-
- Active filters: {{ getActiveFiltersText() }} +
+
+
+ + Active filters: {{ getActiveFiltersText() }} +
@@ -48,31 +57,141 @@
-
-
- Quick navigation: +
+
+ + + Showing {{ getPageInfo().start }} - {{ getPageInfo().end }} of + {{ getPageInfo().total }} records +
-
+
+
-
- - Showing {{ getPageInfo().start }} - {{ getPageInfo().end }} of - {{ getPageInfo().total }} records - +
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+ + {{ selectedRows.length }} row{{ selectedRows.length !== 1 ? "s" : "" }} selected +
+
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+ + Select a row to enable single-selection actions + Select only one row to enable single-selection actions + Single-selection actions enabled
@@ -147,6 +266,62 @@ /> + + + + + diff --git a/frontend/src/components/pages/Client.vue b/frontend/src/components/pages/Client.vue new file mode 100644 index 0000000..2c2eeef --- /dev/null +++ b/frontend/src/components/pages/Client.vue @@ -0,0 +1,9 @@ + + + diff --git a/frontend/src/components/pages/Clients.vue b/frontend/src/components/pages/Clients.vue index cd283ca..e0272f5 100644 --- a/frontend/src/components/pages/Clients.vue +++ b/frontend/src/components/pages/Clients.vue @@ -11,15 +11,11 @@ />
-
- -
{ return filtersStore.getTableFilters("clients"); }); -const onClick = () => { - //frappe.new_doc("Customer"); - modalStore.openCreateClient(); -}; - // Handle week change from chart const handleWeekChange = async (weekParams) => { console.log("handleWeekChange called with:", weekParams); @@ -122,23 +115,99 @@ const columns = [ }, { label: "Appt. Scheduled", - fieldName: "customOnsiteMeetingScheduled", + fieldName: "appointmentScheduledStatus", type: "status", sortable: true, }, { label: "Estimate Sent", - fieldName: "customEstimateSentStatus", + fieldName: "estimateSentStatus", type: "status", sortable: true, }, { label: "Payment Received", - fieldName: "customPaymentReceivedStatus", + fieldName: "paymentReceivedStatus", type: "status", sortable: true, }, - { label: "Job Status", fieldName: "customJobStatus", type: "status", sortable: true }, + { label: "Job Status", fieldName: "jobStatus", type: "status", sortable: true }, +]; + +const tableActions = [ + { + label: "Add Client", + action: () => { + modalStore.openModal("createClient"); + }, + type: "button", + style: "primary", + icon: "pi pi-plus", + layout: { + position: "left", + variant: "filled" + } + // Global action - always available + }, + { + label: "View Details", + action: (rowData) => { + router.push(`/clients/${rowData.id}`); + }, + type: "button", + style: "info", + icon: "pi pi-eye", + requiresSelection: true, // Single selection action - appears above table, enabled when exactly one row selected + layout: { + position: "center", + variant: "outlined" + } + }, + { + label: "Export Selected", + action: (selectedRows) => { + console.log("Exporting", selectedRows.length, "clients:", selectedRows); + // Implementation would export selected clients + }, + type: "button", + style: "success", + icon: "pi pi-download", + requiresMultipleSelection: true, // Bulk action - operates on selected rows + layout: { + position: "right", + variant: "filled" + } + }, + { + label: "Edit", + action: (rowData) => { + console.log("Editing client:", rowData); + // Implementation would open edit modal + }, + type: "button", + style: "secondary", + icon: "pi pi-pencil", + rowAction: true, // Row action - appears in each row's actions column + layout: { + priority: "primary", + variant: "outlined" + } + }, + { + label: "Quick View", + action: (rowData) => { + console.log("Quick view for:", rowData.addressTitle); + // Implementation would show quick preview + }, + type: "button", + style: "info", + icon: "pi pi-search", + rowAction: true, // Row action - appears in each row's actions column + layout: { + priority: "secondary", + variant: "compact" + } + }, ]; // Handle lazy loading events from DataTable const handleLazyLoad = async (event) => { @@ -199,8 +268,6 @@ const handleLazyLoad = async (event) => { return; } - console.log("Making API call with:", { paginationParams, filters }); - // Call API with pagination, filters, and sorting const result = await Api.getPaginatedClientDetails(paginationParams, filters, sorting); @@ -210,14 +277,6 @@ const handleLazyLoad = async (event) => { // Update pagination store with new total paginationStore.setTotalRecords("clients", result.pagination.total); - - console.log("Updated pagination state:", { - tableData: tableData.value.length, - totalRecords: totalRecords.value, - storeTotal: paginationStore.getTablePagination("clients").totalRecords, - storeTotalPages: paginationStore.getTotalPages("clients"), - }); - // Cache the result paginationStore.setCachedPage( "clients", @@ -231,12 +290,6 @@ const handleLazyLoad = async (event) => { totalRecords: result.pagination.total, }, ); - - console.log("Loaded from API:", { - records: result.data.length, - total: result.pagination.total, - page: paginationParams.page + 1, - }); } catch (error) { console.error("Error loading client data:", error); // You could also show a toast or other error notification here @@ -287,24 +340,4 @@ onMounted(async () => { .chart-section { margin-bottom: 20px; } - -.filter-container { - margin-bottom: 15px; -} - -.interaction-button { - background: #3b82f6; - color: white; - border: none; - padding: 10px 20px; - border-radius: 6px; - cursor: pointer; - font-size: 14px; - font-weight: 500; - transition: background 0.2s; -} - -.interaction-button:hover { - background: #2563eb; -} diff --git a/frontend/src/router.js b/frontend/src/router.js index 98ea0a3..e83cff1 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -9,6 +9,7 @@ import TimeSheets from "./components/pages/TimeSheets.vue"; import Warranties from "./components/pages/Warranties.vue"; import Home from "./components/pages/Home.vue"; import TestDateForm from "./components/pages/TestDateForm.vue"; +import Client from "./components/pages/Client.vue"; const routes = [ { @@ -17,6 +18,7 @@ const routes = [ }, { path: "/calendar", component: Calendar }, { path: "/clients", component: Clients }, + { path: "/clients/:id", component: Client, props: true }, { path: "/jobs", component: Jobs }, { path: "/routes", component: Routes }, { path: "/create", component: Create }, diff --git a/frontend/test-datatable-actions.html b/frontend/test-datatable-actions.html new file mode 100644 index 0000000..6d2ed02 --- /dev/null +++ b/frontend/test-datatable-actions.html @@ -0,0 +1,130 @@ + + + + + + DataTable Actions Test + + +

DataTable Actions Implementation Test

+ +

Summary of Changes

+
    +
  • ✅ Added tableActions prop to DataTable component
  • +
  • ✅ Added global actions section above the DataTable
  • +
  • ✅ Added bulk actions section when rows are selected
  • +
  • ✅ Added actions column for row-specific actions
  • +
  • ✅ Implemented action handlers with row data passing
  • +
  • ✅ Added multi-selection support for bulk operations
  • +
  • ✅ Updated Clients component to use table actions
  • +
  • ✅ Updated documentation with action configuration examples
  • +
+ +

Key Features Implemented

+ +

Global Actions

+

Actions with requiresSelection: false appear above the table:

+
{
+  label: "Add Client",
+  action: () => modalStore.openModal("createClient"),
+  icon: "pi pi-plus", 
+  style: "primary",
+  requiresSelection: false
+}
+ +

Bulk Actions

+

Actions with requiresMultipleSelection: true appear when rows are selected:

+
{
+  label: "Export Selected",
+  action: (selectedRows) => exportData(selectedRows),
+  icon: "pi pi-download",
+  style: "success",
+  requiresMultipleSelection: true
+}
+ +

Row Actions

+

Actions with requiresSelection: true (or omitted) appear in actions column:

+
{
+  label: "View",
+  action: (rowData) => router.push(`/clients/${rowData.id}`),
+  icon: "pi pi-eye",
+  style: "secondary"
+}
+ +

Action Handlers

+

The DataTable automatically passes appropriate data to different action types:

+
// Global action handler
+const handleGlobalAction = (action) => {
+  action.action(); // No data passed
+};
+
+// Row action handler  
+const handleRowAction = (action, rowData) => {
+  action.action(rowData); // Single row data passed
+};
+
+// Bulk action handler
+const handleBulkAction = (action, selectedRows) => {
+  action.action(selectedRows); // Array of selected rows passed
+};
+ +

Action Types Supported

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Action TypePropertyData ReceivedDisplay LocationEnabled When
GlobalDefault (no special props)NoneAbove tableAlways
Single SelectionrequiresSelection: trueSelected row objectAbove tableExactly one row selected
RowrowAction: trueIndividual row objectActions columnAlways (per row)
BulkrequiresMultipleSelection: trueArray of selected rowsAbove table (when rows selected)One or more rows selected
+ +

Usage in Components

+

Components can now pass table actions to DataTable:

+
<DataTable
+  :data="tableData"
+  :columns="columns" 
+  :tableActions="tableActions"
+  tableName="clients"
+  :lazy="true"
+  :totalRecords="totalRecords"
+  :loading="isLoading"
+  @lazy-load="handleLazyLoad"
+/>
+ +

Browser Compatibility

+

✅ Vue 3 Composition API compatible
+ ✅ PrimeVue components integration
+ ✅ Reactive row data passing
+ ✅ Error handling for action execution

+ + + \ No newline at end of file