From 54bf3a8690b5e00b702f7377841a4eab00bda2b8 Mon Sep 17 00:00:00 2001 From: ivarsbariss Date: Thu, 26 Feb 2026 16:23:32 +0100 Subject: [PATCH] sales unites ordered power level and input voltage query --- .cursor/skills/add-query-to-table/SKILL.md | 101 ++++++++++++++++++ .../metadata.json | 1 + .../units_ordered_by_power_input.txt} | 14 +-- .../metadata.json | 35 ------ .../widgets/Tabs1/Table1Copy.json | 2 +- 5 files changed, 110 insertions(+), 43 deletions(-) create mode 100644 .cursor/skills/add-query-to-table/SKILL.md create mode 100644 pages/Sales - Units Ordered/queries/units_ordered_by_power_input/metadata.json rename pages/Sales - Units Ordered/queries/{units_shipped_by_power_input/units_shipped_by_power_input.txt => units_ordered_by_power_input/units_ordered_by_power_input.txt} (84%) delete mode 100644 pages/Sales - Units Ordered/queries/units_shipped_by_power_input/metadata.json diff --git a/.cursor/skills/add-query-to-table/SKILL.md b/.cursor/skills/add-query-to-table/SKILL.md new file mode 100644 index 0000000..c1bea5c --- /dev/null +++ b/.cursor/skills/add-query-to-table/SKILL.md @@ -0,0 +1,101 @@ +--- +name: add-query-to-table +description: Adds a new DB query to a page and wires it to a Table widget; documents how parameter/search syntax works (widget bindings, optional date range). Use when the user asks to add a query, connect a query to a table, create a new query, or how parameters/filters work in queries. +--- + +# Add Query to Table & Parameter Search Syntax + +Use this skill when adding a new query to a page, wiring a query to a table, or when you need the correct syntax for parameterized filters (e.g. date range from widgets). + +## 1. Query structure (single source of truth) + +Each query lives under the page in a folder named after the query: + +- **Path**: `pages//queries//` +- **Files**: + - **`.txt`** — Raw SQL. This is the **source of truth** for the query body. Keep it readable (real newlines, no escaping). + - **`metadata.json`** — Appsmith action config. The `body` inside `unpublishedAction.actionConfiguration` must match the SQL in the `.txt` file, stored as a single-line string with `\n` for newlines and `\"` for double quotes. + +**Naming**: Use lowercase with underscores (e.g. `units_ordered_by_series`, `units_shipped_by_power_input`). The query name is the identifier used in bindings (e.g. `{{query_name.data}}`). + +## 2. Metadata.json required fields + +- **`id`**: `"_"` (e.g. `"Sales - Units Ordered_units_ordered_by_series"`). +- **`name`**: `""` (must match the folder and the name used in bindings). +- **`unpublishedAction.actionConfiguration.body`**: The exact SQL from `.txt`, escaped for JSON (newlines → `\n`, `"` → `\"`). +- **`unpublishedAction.datasource`**: Use the same as other queries on the page (e.g. `xTuple_Sandbox`). +- **`unpublishedAction.pageId`**: `""`. +- **`unpublishedAction.dynamicBindingPathList`**: `[{"key":"body"}]` when the body contains `{{...}}` widget references. +- Keep **`pluginId`**: `"postgres-plugin"`, **`pluginType`**: `"DB"`, and existing flags like **`encodeParamsToggle`**, **`paginationType`**, **`pluginSpecifiedTemplates`**, **`timeoutInMillisecond`** consistent with other DB queries in the app. + +When adding a new query, copy an existing query’s `metadata.json` from the same page and change `id`, `name`, and `body` (and ensure `body` stays in sync with the new `.txt` file). + +## 3. Wiring the query to a Table widget + +- In the Table widget JSON (e.g. `widgets/.../Table1.json`), set: + - **`tableData`**: `"{{.data}}"` +- Table columns read from the query result by **key**. The key is the **SELECT alias** from the query (e.g. `"Product"`, `"Qty Ordered"`). In `primaryColumns`, the column **id** can be a safe identifier (e.g. `Qty_Ordered`) while **originalId** / **alias** / **label** match the display name; **computedValue** must use the same key as in the query result, e.g. `currentRow["Qty Ordered"]`. + +So: **query SELECT aliases = keys in the table’s row object**. Keep column keys and any `currentRow["..."]` references in the table in sync with those aliases. + +## 4. Parameter search syntax (widgets in SQL) + +### 4.1 Referencing a widget value + +- **Syntax**: `{{WidgetName.property}}` +- **Examples**: + - DatePicker date: `{{SeriesDateFrom.selectedDate}}`, `{{PowerDateTo.selectedDate}}` + - Other widgets: use the widget’s value property (e.g. `selectedOptionValue`, `text`) as per Appsmith docs. + +Values are injected as strings. For PostgreSQL dates you typically cast in SQL, e.g. `'{{DatePicker1.selectedDate}}'::date`. + +### 4.2 Optional date range (no filter when either date is empty) + +Use this pattern so that: +- If **either** date widget is empty → the date condition is **not** applied (all dates allowed). +- If **both** are set → filter by `date_column BETWEEN from ::date AND to ::date`. + +**SQL pattern** (replace widget names and column as needed): + +```sql +AND ( + NULLIF('{{DateFromWidget.selectedDate}}','') IS NULL + OR NULLIF('{{DateToWidget.selectedDate}}','') IS NULL + OR date_column BETWEEN + NULLIF('{{DateFromWidget.selectedDate}}','')::date + AND NULLIF('{{DateToWidget.selectedDate}}','')::date +) +``` + +- **Logic**: `NULLIF('{{...}}','')` turns an empty string into SQL `NULL`. If either widget is empty, one of the first two conditions is true, so the whole `AND (...)` is true and the BETWEEN is not applied. When both are non-empty, the third branch applies the range. +- **Widget names**: Use the actual widget names (e.g. `SeriesDateFrom` / `SeriesDateTo` for one tab, `PowerDateFrom` / `PowerDateTo` for another). Ensure those widgets exist on the same page and (if in a tab) the same tab so the query runs with the right context. + +### 4.3 Required parameters (always filter) + +If the filter must always be applied (no “show all” when empty): + +- Use the binding directly and ensure the widget always has a value, or use a default in the widget. +- Example: `AND cohead_orderdate BETWEEN '{{DateFrom.selectedDate}}'::date AND '{{DateTo.selectedDate}}'::date` — then empty dates may produce invalid SQL or empty results, so prefer the optional pattern above unless the UI guarantees non-empty values. + +## 5. Checklist when adding a new query + +1. Create `pages//queries//`. +2. Add `.txt` with the full SQL (source of truth). +3. Add `metadata.json` with correct `id`, `name`, `body` (body = SQL from .txt, JSON-escaped), and same datasource/pageId as other page queries. +4. If the SQL uses `{{...}}`, set `dynamicBindingPathList` to `[{"key":"body"}]`. +5. In the Table widget that should show the data, set `tableData` to `{{.data}}`. +6. Ensure table column keys / `currentRow["..."]` match the query’s SELECT aliases. + +## 6. Renaming or replacing a query + +- To **rename** (e.g. `units_shipped_by_series` → `units_ordered_by_series`): + - Create the new folder and files under the new name. + - Update every reference to the old query (e.g. `tableData`: `{{old_name.data}}` → `{{new_name.data}}`). + - Remove the old query folder (both `.txt` and `metadata.json`). +- When **replacing** the SQL but keeping the same name, update the `.txt` first, then update the `body` in `metadata.json` to match (same content, JSON-escaped). + +## Reference + +- **Optional date range**: NULLIF + IS NULL + OR + BETWEEN as above. +- **Table data binding**: `{{.data}}`. +- **Column keys**: Must match query SELECT aliases (e.g. `"Product"`, `"Qty Ordered"`). diff --git a/pages/Sales - Units Ordered/queries/units_ordered_by_power_input/metadata.json b/pages/Sales - Units Ordered/queries/units_ordered_by_power_input/metadata.json new file mode 100644 index 0000000..f296afd --- /dev/null +++ b/pages/Sales - Units Ordered/queries/units_ordered_by_power_input/metadata.json @@ -0,0 +1 @@ +{"gitSyncId":"6978a0503f1cbf3622b556da_a33c1a5c-f987-4855-97da-8817781be960","id":"Sales - Units Ordered_units_ordered_by_power_input","pluginId":"postgres-plugin","pluginType":"DB","unpublishedAction":{"actionConfiguration":{"body":"SELECT \nCASE WHEN item_number IN ('16688','25020') THEN 'XR Series'\nWHEN item_number IN ('22351','24952') THEN 'SL Series'\nWHEN item_number IN ('17861','25021','29243','29752') THEN 'TS Series'\nWHEN item_number IN ('17862') THEN 'MS Series'\nWHEN item_number IN ('17863') THEN 'MT Series'\nWHEN item_number IN ('17873') THEN 'Harmonic Neutralizer'\n\nELSE 'None'\nEND AS \"Product\",\nreportpower.charass_value as \"Power Output\", reportinput.charass_value || ' ' || reportphases.charass_value AS \"Input\", ROUND(SUM(coitem_qtyord),0) as \"Qty Ordered\"\nFROM coitem LEFT OUTER JOIN cohead ON cohead_id=coitem_cohead_id\nLEFT OUTER JOIN itemsite ON coitem_itemsite_id = itemsite_id\nLEFT OUTER JOIN item ON item_id = itemsite_item_id\nLEFT OUTER JOIN charass AS reportpower ON \n(coitem_id = reportpower.charass_target_id \nAND reportpower.charass_target_type = 'SI'\nAND reportpower.charass_char_id = 25)\nLEFT OUTER JOIN charass AS reportinput ON \n(coitem_id = reportinput.charass_target_id \nAND reportinput.charass_target_type = 'SI'\nAND reportinput.charass_char_id = 36)\nLEFT OUTER JOIN charass AS reportphases ON \n(coitem_id = reportphases.charass_target_id \nAND reportphases.charass_target_type = 'SI'\nAND reportphases.charass_char_id = 18)\nWHERE item_number IN ('16688','17861','17862','17863','22351','25020','25144','25147','25021','24952','17873','29243','29752')\nAND (coitem_status <> 'X') \nAND cohead_id NOT IN(SELECT cohead_id from rahead LEFT OUTER JOIN cohead ON rahead_new_cohead_id=cohead_id WHERE cohead_id IS NOT NULL)\nAND coitem_price != 0\nAND (\n\tNULLIF('{{PowerDateFrom.selectedDate}}','') IS NULL\n\tOR NULLIF('{{PowerDateTo.selectedDate}}','') IS NULL\n\tOR cohead_orderdate BETWEEN\n\tNULLIF('{{PowerDateFrom.selectedDate}}','')::date\n\tAND NULLIF('{{PowerDateTo.selectedDate}}','')::date\n)\nAND reportpower.charass_value IS NOT NULL\nGROUP BY reportpower.charass_value, \"Product\", \"Input\"\nORDER BY \"Product\", reportpower.charass_value","encodeParamsToggle":true,"paginationType":"NONE","pluginSpecifiedTemplates":[{"value":true}],"timeoutInMillisecond":10000},"confirmBeforeExecute":false,"datasource":{"id":"xTuple_Sandbox","isAutoGenerated":false,"name":"xTuple_Sandbox","pluginId":"postgres-plugin"},"dynamicBindingPathList":[{"key":"body"}],"name":"units_ordered_by_power_input","pageId":"Sales - Units Ordered","runBehaviour":"AUTOMATIC","userSetOnLoad":false}} diff --git a/pages/Sales - Units Ordered/queries/units_shipped_by_power_input/units_shipped_by_power_input.txt b/pages/Sales - Units Ordered/queries/units_ordered_by_power_input/units_ordered_by_power_input.txt similarity index 84% rename from pages/Sales - Units Ordered/queries/units_shipped_by_power_input/units_shipped_by_power_input.txt rename to pages/Sales - Units Ordered/queries/units_ordered_by_power_input/units_ordered_by_power_input.txt index 4ab3ead..c663edf 100644 --- a/pages/Sales - Units Ordered/queries/units_shipped_by_power_input/units_shipped_by_power_input.txt +++ b/pages/Sales - Units Ordered/queries/units_ordered_by_power_input/units_ordered_by_power_input.txt @@ -14,16 +14,16 @@ LEFT OUTER JOIN itemsite ON coitem_itemsite_id = itemsite_id LEFT OUTER JOIN item ON item_id = itemsite_item_id LEFT OUTER JOIN charass AS reportpower ON (coitem_id = reportpower.charass_target_id - AND reportpower.charass_target_type = 'SI' - AND reportpower.charass_char_id = 25) +AND reportpower.charass_target_type = 'SI' +AND reportpower.charass_char_id = 25) LEFT OUTER JOIN charass AS reportinput ON (coitem_id = reportinput.charass_target_id - AND reportinput.charass_target_type = 'SI' - AND reportinput.charass_char_id = 36) +AND reportinput.charass_target_type = 'SI' +AND reportinput.charass_char_id = 36) LEFT OUTER JOIN charass AS reportphases ON (coitem_id = reportphases.charass_target_id - AND reportphases.charass_target_type = 'SI' - AND reportphases.charass_char_id = 18) +AND reportphases.charass_target_type = 'SI' +AND reportphases.charass_char_id = 18) WHERE item_number IN ('16688','17861','17862','17863','22351','25020','25144','25147','25021','24952','17873','29243','29752') AND (coitem_status <> 'X') AND cohead_id NOT IN(SELECT cohead_id from rahead LEFT OUTER JOIN cohead ON rahead_new_cohead_id=cohead_id WHERE cohead_id IS NOT NULL) @@ -37,4 +37,4 @@ AND ( ) AND reportpower.charass_value IS NOT NULL GROUP BY reportpower.charass_value, "Product", "Input" -ORDER BY "Product", reportpower.charass_value \ No newline at end of file +ORDER BY "Product", reportpower.charass_value diff --git a/pages/Sales - Units Ordered/queries/units_shipped_by_power_input/metadata.json b/pages/Sales - Units Ordered/queries/units_shipped_by_power_input/metadata.json deleted file mode 100644 index 5618b18..0000000 --- a/pages/Sales - Units Ordered/queries/units_shipped_by_power_input/metadata.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "gitSyncId": "6978a0503f1cbf3622b556da_a33c1a5c-f987-4855-97da-8817781be960", - "id": "Sales - Units Ordered_units_shipped_by_power_input", - "pluginId": "postgres-plugin", - "pluginType": "DB", - "unpublishedAction": { - "actionConfiguration": { - "body": "SELECT \nCASE WHEN item_number IN ('16688','25020') THEN 'XR Series'\nWHEN item_number IN ('22351','24952') THEN 'SL Series'\nWHEN item_number IN ('17861','25021','29243','29752') THEN 'TS Series'\nWHEN item_number IN ('17862') THEN 'MS Series'\nWHEN item_number IN ('17863') THEN 'MT Series'\nWHEN item_number IN ('17873') THEN 'Harmonic Neutralizer'\n\nELSE 'None'\nEND AS \"Product\",\nreportpower.charass_value as \"Power Output\", reportinput.charass_value || ' ' || reportphases.charass_value AS \"Input\", ROUND(SUM(coitem_qtyord),0) as \"Qty Ordered\"\nFROM coitem LEFT OUTER JOIN cohead ON cohead_id=coitem_cohead_id\nLEFT OUTER JOIN itemsite ON coitem_itemsite_id = itemsite_id\nLEFT OUTER JOIN item ON item_id = itemsite_item_id\nLEFT OUTER JOIN charass AS reportpower ON \n(coitem_id = reportpower.charass_target_id \n AND reportpower.charass_target_type = 'SI'\n AND reportpower.charass_char_id = 25)\nLEFT OUTER JOIN charass AS reportinput ON \n(coitem_id = reportinput.charass_target_id \n AND reportinput.charass_target_type = 'SI'\n AND reportinput.charass_char_id = 36)\nLEFT OUTER JOIN charass AS reportphases ON \n(coitem_id = reportphases.charass_target_id \n AND reportphases.charass_target_type = 'SI'\n AND reportphases.charass_char_id = 18)\nWHERE item_number IN ('16688','17861','17862','17863','22351','25020','25144','25147','25021','24952','17873','29243','29752')\nAND (coitem_status <> 'X') \nAND cohead_id NOT IN(SELECT cohead_id from rahead LEFT OUTER JOIN cohead ON rahead_new_cohead_id=cohead_id WHERE cohead_id IS NOT NULL)\nAND coitem_price != 0\nAND (\n\tNULLIF('{{PowerDateFrom.selectedDate}}','') IS NULL\n\tOR NULLIF('{{PowerDateTo.selectedDate}}','') IS NULL\n\tOR cohead_orderdate BETWEEN\n\tNULLIF('{{PowerDateFrom.selectedDate}}','')::date\n\tAND NULLIF('{{PowerDateTo.selectedDate}}','')::date\n)\nAND reportpower.charass_value IS NOT NULL\nGROUP BY reportpower.charass_value, \"Product\", \"Input\"\nORDER BY \"Product\", reportpower.charass_value", - "encodeParamsToggle": true, - "paginationType": "NONE", - "pluginSpecifiedTemplates": [ - { - "value": true - } - ], - "timeoutInMillisecond": 10000 - }, - "confirmBeforeExecute": false, - "datasource": { - "id": "xTuple_Sandbox", - "isAutoGenerated": false, - "name": "xTuple_Sandbox", - "pluginId": "postgres-plugin" - }, - "dynamicBindingPathList": [ - { - "key": "body" - } - ], - "name": "units_shipped_by_power_input", - "pageId": "Sales - Units Ordered", - "runBehaviour": "AUTOMATIC", - "userSetOnLoad": false - } -} \ No newline at end of file diff --git a/pages/Sales - Units Ordered/widgets/Tabs1/Table1Copy.json b/pages/Sales - Units Ordered/widgets/Tabs1/Table1Copy.json index 3c8dd82..99ebbbe 100644 --- a/pages/Sales - Units Ordered/widgets/Tabs1/Table1Copy.json +++ b/pages/Sales - Units Ordered/widgets/Tabs1/Table1Copy.json @@ -252,7 +252,7 @@ "responsiveBehavior": "fill", "rightColumn": 61, "searchKey": "", - "tableData": "{{units_shipped_by_power_input.data}}", + "tableData": "{{units_ordered_by_power_input.data}}", "textSize": "0.775rem", "topRow": 7, "totalRecordsCount": 0,