소스 검색

persist filter and implement other tabs

Daniel Sheffield 9 달 전
부모
커밋
ba403efe09
10개의 변경된 파일566개의 추가작업 그리고 268개의 파일을 삭제
  1. 55 0
      sqlpage/categories.sql
  2. 7 0
      sqlpage/clear.sql
  3. 7 0
      sqlpage/cookie.sql
  4. 55 0
      sqlpage/groups.sql
  5. 267 0
      sqlpage/nav.sql
  6. 54 0
      sqlpage/products.sql
  7. 39 0
      sqlpage/sqlpage/migrations/README.md
  8. 20 0
      sqlpage/sqlpage/templates/README.md
  9. 62 0
      sqlpage/transactions.sql
  10. 0 268
      transactions-sqlpage.sql

+ 55 - 0
sqlpage/categories.sql

@@ -0,0 +1,55 @@
+SELECT
+  'dynamic' as component,
+  sqlpage.run_sql('cookie.sql') AS properties
+;
+
+SELECT
+  'dynamic' as component,
+  sqlpage.run_sql('nav.sql') AS properties
+;
+
+SELECT
+  'table' AS component,
+  TRUE AS search,
+  TRUE AS striped_rows,
+  TRUE AS small,
+  'Notice' AS markdown
+;
+
+DROP TABLE IF EXISTS sqlpage_categories;
+CREATE TEMPORARY TABLE IF NOT EXISTS sqlpage_categories AS (
+SELECT
+    "Products",
+    COALESCE("category", "Categories"||'') "Category",
+    COALESCE("group", "Groups"||'') "Group"
+FROM (SELECT
+  count(DISTINCT p.id) AS "Products",
+  count(DISTINCT c.id) AS "Categories",
+  count(DISTINCT g.id) AS "Groups",
+  p.name AS "product",
+  c.name AS "category",
+  g.name AS "group"
+FROM products p
+JOIN categories c ON p.category_id = c.id
+JOIN groups g ON c.group_id = g.id
+JOIN sqlpage_options op ON op.type = 'product' AND p.name = op.name
+JOIN sqlpage_options oc ON oc.type = 'category' AND c.name = oc.name
+JOIN sqlpage_options og ON og.type = 'group' AND g.name = og.name
+WHERE COALESCE(op.selected, oc.selected, og.selected, TRUE)
+AND
+  COALESCE(op.selected, oc.selected, og.selected, TRUE)
+AND
+  op.session = sqlpage.cookie('session')
+AND
+  oc.session = sqlpage.cookie('session')
+AND
+  og.session = sqlpage.cookie('session')
+GROUP BY ROLLUP (g.name, c.name, p.name)
+ORDER BY product, category, "group") q
+WHERE q.product IS NULL AND (q.category IS NOT NULL OR q.group IS NULL)
+);
+SELECT * FROM sqlpage_categories;
+
+SELECT 
+  '# No Data' AS "Notice"
+WHERE NOT EXISTS(SELECT * FROM sqlpage_categories LIMIT 1);

+ 7 - 0
sqlpage/clear.sql

@@ -0,0 +1,7 @@
+SELECT
+  'redirect' AS component,
+  (SELECT CASE
+    WHEN $path IN ('/products.sql', '/categories.sql', '/groups.sql')
+    THEN $path ELSE '/transactions.sql'
+    END||'?clear=Clear'
+  ) AS link;

+ 7 - 0
sqlpage/cookie.sql

@@ -0,0 +1,7 @@
+SELECT
+  'cookie' AS component,
+  'session' AS name,
+  sqlpage.random_string(32) AS value
+WHERE sqlpage.cookie('session') IS NULL
+;
+

+ 55 - 0
sqlpage/groups.sql

@@ -0,0 +1,55 @@
+SELECT
+  'dynamic' as component,
+  sqlpage.run_sql('cookie.sql') AS properties
+;
+
+SELECT
+  'dynamic' as component,
+  sqlpage.run_sql('nav.sql') AS properties
+;
+
+SELECT
+  'table' AS component,
+  TRUE AS search,
+  TRUE AS striped_rows,
+  TRUE AS small,
+  'Notice' AS markdown
+;
+
+DROP TABLE IF EXISTS sqlpage_groups;
+CREATE TEMPORARY TABLE IF NOT EXISTS sqlpage_groups AS (
+SELECT
+    "Products",
+    "Categories",
+    COALESCE("group", "Groups"||'') "Group"
+FROM (SELECT
+  count(DISTINCT p.id) AS "Products",
+  count(DISTINCT c.id) AS "Categories",
+  count(DISTINCT g.id) AS "Groups",
+  p.name AS "product",
+  c.name AS "category",
+  g.name AS "group"
+FROM products p
+JOIN categories c ON p.category_id = c.id
+JOIN groups g ON c.group_id = g.id
+JOIN sqlpage_options op ON op.type = 'product' AND p.name = op.name
+JOIN sqlpage_options oc ON oc.type = 'category' AND c.name = oc.name
+JOIN sqlpage_options og ON og.type = 'group' AND g.name = og.name
+WHERE COALESCE(op.selected, oc.selected, og.selected, TRUE)
+AND
+  COALESCE(op.selected, oc.selected, og.selected, TRUE)
+AND
+  op.session = sqlpage.cookie('session')
+AND
+  oc.session = sqlpage.cookie('session')
+AND
+  og.session = sqlpage.cookie('session')
+GROUP BY ROLLUP (g.name, c.name, p.name)
+ORDER BY product, category, "group") q
+WHERE q.category IS NULL
+);
+SELECT * FROM sqlpage_groups;
+
+SELECT 
+  '# No Data' AS "Notice"
+WHERE NOT EXISTS(SELECT * FROM sqlpage_groups LIMIT 1);

+ 267 - 0
sqlpage/nav.sql

@@ -0,0 +1,267 @@
+SELECT 'shell' AS component,
+  'dark' AS theme,
+  COALESCE($tab, 'Transactions') AS title
+;
+CREATE TABLE IF NOT EXISTS sqlpage_options(
+  session text,
+  "type" text,
+  name text,
+  selected boolean
+);
+DELETE FROM sqlpage_options
+WHERE
+  session = sqlpage.cookie('session')
+AND (
+    $clear = 'Clear'
+  OR
+    $products IS NOT NULL
+  OR
+    $categories IS NOT NULL
+  OR
+    $groups IS NOT NULL
+);
+
+CREATE TEMPORARY TABLE IF NOT EXISTS state(update boolean);
+TRUNCATE state;
+INSERT INTO state(update)
+SELECT NOT EXISTS (SELECT 1 FROM sqlpage_options WHERE session = sqlpage.cookie('session'));
+
+INSERT INTO sqlpage_options
+SELECT
+  sqlpage.cookie('session'),
+  UNNEST(ARRAY['product', 'category', 'group']) AS "type",
+  UNNEST(q.name) AS name,
+  UNNEST(selected)
+FROM (
+  SELECT
+    ARRAY[p.name, c.name, g.name] AS name,
+    ARRAY[
+      CASE WHEN $products IS NOT NULL THEN fp.v IS NOT NULL ELSE NULL END,
+      CASE WHEN $categories IS NOT NULL THEN fc.v IS NOT NULL ELSE NULL END,
+      CASE WHEN $groups IS NOT NULL THEN fg.v IS NOT NULL ELSE NULL END
+    ]
+  FROM products p
+  LEFT JOIN json_array_elements($products::json) fp(v)
+  ON p.name = fp.v#>>'{}'
+  JOIN categories c ON p.category_id = c.id
+  LEFT JOIN json_array_elements($categories::json) fc(v)
+  ON c.name = fc.v#>>'{}'
+  JOIN groups g ON c.group_id = g.id
+  LEFT JOIN json_array_elements($groups::json) fg(v)
+  ON g.name = fg.v#>>'{}'
+  WHERE (
+    (p.name = fp.v#>>'{}' OR $products IS NULL)
+  AND
+    (c.name = fc.v#>>'{}' OR $categories IS NULL)
+  AND
+    (g.name = fg.v#>>'{}' OR $groups IS NULL)
+  AND
+    EXISTS (SELECT 1 FROM state WHERE update IS TRUE)
+  )
+) q(name, selected)
+UNION
+SELECT
+  sqlpage.cookie('session'),
+  'group',
+  f.v#>>'{}',
+  TRUE
+FROM json_array_elements($groups::json) f(v)
+LEFT JOIN groups g ON g.name = f.v#>>'{}'
+WHERE g.name is NULL
+  AND
+    EXISTS (SELECT 1 FROM state WHERE update IS TRUE)
+UNION
+SELECT
+  sqlpage.cookie('session'),
+  'category',
+  f.v#>>'{}',
+  TRUE
+FROM json_array_elements($categories::json) f(v)
+LEFT JOIN categories c ON c.name = f.v#>>'{}'
+WHERE c.name is NULL
+  AND
+    EXISTS (SELECT 1 FROM state WHERE update IS TRUE)
+UNION
+SELECT
+  sqlpage.cookie('session'),
+  'product',
+  f.v#>>'{}',
+  TRUE
+FROM json_array_elements($products::json) f(v)
+LEFT JOIN products p ON p.name = f.v#>>'{}'
+WHERE p.name is NULL
+  AND
+    EXISTS (SELECT 1 FROM state WHERE update IS TRUE)
+;
+
+--SELECT
+--  'text' AS component,
+--  '<style>
+--.nav { pointer-events: none }
+--.navbar { display: none }
+--input[value=Products].form-control,
+--input[value=Categories].form-control,
+--input[value=Groups].form-control,
+--input[value=Transactions].form-control
+--{
+--    color: var(--tblr-secondary);
+--    border-right: unset;
+--    border-top: unset;
+--    border-left: unset;
+--}
+--input[value='||CASE
+--  WHEN $tab IN ('Products', 'Categories', 'Groups')
+--  THEN $tab ELSE 'Transactions'
+--END||'].form-control
+--{
+--    color: var(--tblr-primary);
+--    border-right: unset;
+--    border-top: unset;
+--    border-left: unset;
+--    border-bottom-color: var(--tblr-primary);
+--    border-bottom-width: 2px;
+--}
+--</style>' AS html
+--;
+  
+SELECT
+  'tab' AS component,
+  TRUE AS center
+;
+
+SELECT
+ 'products' AS id,
+ 'Products' AS title,
+ sqlpage.path() = '/products.sql' AS active,
+ 'products.sql' AS link
+;
+SELECT
+ 'categories' AS id,
+ 'Categories' AS title,
+ sqlpage.path() = '/categories.sql' AS active,
+ 'categories.sql' AS link
+;
+SELECT
+ 'groups' AS id,
+ 'Groups' AS title,
+ sqlpage.path() = '/groups.sql' AS active,
+ 'groups.sql' AS link
+;
+SELECT
+ 'transactions' AS id,
+ 'Transactions' AS title,
+ sqlpage.path() = '/transactions.sql' AS active,
+ 'transactions.sql' AS link
+;
+
+SELECT
+  'form' AS component,
+  'filter' AS id,
+  'get' AS method,
+  '' AS title,
+  'Apply' AS validate
+;
+
+SELECT
+  'path' AS name,
+  '' AS label,
+  sqlpage.path() AS value,
+  'hidden' AS type
+;
+
+--SELECT
+--  'tab' AS name,
+--  'Products' AS value,
+--  '' AS label,
+--  3 AS width,
+--  'submit' AS type,
+--  'transactions.sql' AS formaction
+--;
+--SELECT
+--  'tab' AS name,
+--  'Categories' AS value,
+--  '' AS label,
+--  3 AS width,
+--  'submit' AS type,
+--  'transactions.sql' AS formaction
+--;
+--SELECT
+--  'tab' AS name,
+--  'Groups' AS value,
+--  '' AS label,
+--  3 AS width,
+--  'submit' AS type,
+--  'transactions.sql' AS formaction
+--;
+--SELECT
+--  'tab' AS name,
+--  'Transactions' AS value,
+--  '' AS label,
+--  3 AS width,
+--  'submit' AS type,
+--  'transactions.sql' AS formaction
+--;
+
+SELECT
+  'products[]' AS name,
+  'Product' AS label,
+  4 AS width,
+  'select' AS "type",
+  TRUE AS dropdown,
+  TRUE AS multiple,
+  json_agg(json_build_object(
+    'label', o.name,
+    'value', o.name,
+    'selected', COALESCE(o.selected, FALSE)
+  ) ORDER BY o.name) AS options
+FROM sqlpage_options o
+WHERE
+  o.type = 'product'
+AND
+  o.session = sqlpage.cookie('session')
+;
+SELECT
+  'categories[]' AS name,
+  'Categories' AS label,
+  4 AS width,
+  'select' AS "type",
+  TRUE AS dropdown,
+  TRUE AS multiple,
+  json_agg(json_build_object(
+    'label', o.name,
+    'value', o.name,
+    'selected', COALESCE(o.selected, FALSE)
+  ) ORDER BY o.name) AS options
+FROM sqlpage_options o
+WHERE
+  o.type = 'category'
+AND
+  o.session = sqlpage.cookie('session')
+;
+SELECT
+  'groups[]' AS name,
+  'Groups' AS label,
+  4 AS width,
+  'select' AS "type",
+  TRUE AS dropdown,
+  TRUE AS multiple,
+  json_agg(json_build_object(
+    'label', o.name,
+    'value', o.name,
+    'selected', COALESCE(o.selected, FALSE)
+  ) ORDER BY o.name) AS options
+FROM sqlpage_options o
+WHERE
+  o.type = 'group'
+AND
+  o.session = sqlpage.cookie('session')
+;
+SELECT
+  'clear' AS name,
+  'Clear' AS value,
+  '' AS label,
+  1 AS width,
+  'submit' AS type,
+  'clear.sql?page='||sqlpage.path() AS formaction
+;
+

+ 54 - 0
sqlpage/products.sql

@@ -0,0 +1,54 @@
+SELECT
+  'dynamic' as component,
+  sqlpage.run_sql('cookie.sql') AS properties
+;
+
+SELECT
+  'dynamic' as component,
+  sqlpage.run_sql('nav.sql') AS properties
+;
+
+SELECT
+  'table' AS component,
+  TRUE AS search,
+  TRUE AS striped_rows,
+  TRUE AS small,
+  'Notice' AS markdown
+;
+
+DROP TABLE IF EXISTS sqlpage_products;
+CREATE TEMPORARY TABLE IF NOT EXISTS sqlpage_products AS (
+SELECT
+    COALESCE("product", "Products"||'') "Product",
+    COALESCE("category", "Categories"||'') "Category",
+    COALESCE("group", "Groups"||'') "Group"
+FROM (SELECT
+  count(DISTINCT p.id) AS "Products",
+  count(DISTINCT c.id) AS "Categories",
+  count(DISTINCT g.id) AS "Groups",
+  p.name AS "product",
+  c.name AS "category",
+  g.name AS "group"
+FROM products p
+JOIN categories c ON p.category_id = c.id
+JOIN groups g ON c.group_id = g.id
+JOIN sqlpage_options op ON op.type = 'product' AND p.name = op.name
+JOIN sqlpage_options oc ON oc.type = 'category' AND c.name = oc.name
+JOIN sqlpage_options og ON og.type = 'group' AND g.name = og.name
+AND
+  COALESCE(op.selected, oc.selected, og.selected, TRUE)
+AND
+  op.session = sqlpage.cookie('session')
+AND
+  oc.session = sqlpage.cookie('session')
+AND
+  og.session = sqlpage.cookie('session')
+GROUP BY ROLLUP (g.name, c.name, p.name)
+ORDER BY product, category, "group") q
+WHERE q.product IS NOT NULL OR q.group IS NULL
+);
+SELECT * FROM sqlpage_products;
+
+SELECT 
+  '# No Data' AS "Notice"
+WHERE NOT EXISTS(SELECT * FROM sqlpage_products LIMIT 1);

+ 39 - 0
sqlpage/sqlpage/migrations/README.md

@@ -0,0 +1,39 @@
+# SQLPage migrations
+
+SQLPage migrations are SQL scripts that you can use to create or update the database schema.
+They are entirely optional: you can use SQLPage without them, and manage the database schema yourself with other tools.
+
+## Creating a migration
+
+To create a migration, create a file in the `sqlpage/migrations` directory with the following name:
+
+```
+<version>_<name>.sql
+```
+
+Where `<version>` is a number that represents the version of the migration, and `<name>` is a name for the migration.
+For example, `001_initial.sql` or `002_add_users.sql`.
+
+When you need to update the database schema, always create a **new** migration file with a new version number
+that is greater than the previous one.
+Use commands like `ALTER TABLE` to update the schema declaratively instead of modifying the existing `CREATE TABLE`
+statements.
+
+If you try to edit an existing migration, SQLPage will not run it again, will detect
+
+## Running migrations
+
+Migrations that need to be applied are run automatically when SQLPage starts.
+You need to restart SQLPage each time you create a new migration.
+
+## How does it work?
+
+SQLPage keeps track of the migrations that have been applied in a table called `_sqlx_migrations`.
+This table is created automatically when SQLPage starts for the first time, if you create migration files.
+If you don't create any migration files, SQLPage will never touch the database schema on its own.
+
+When SQLPage starts, it checks the `_sqlx_migrations` table to see which migrations have been applied.
+It checks the `sqlpage/migrations` directory to see which migrations are available.
+If the checksum of a migration file is different from the checksum of the migration that has been applied,
+SQLPage will return an error and refuse to start.
+If you end up in this situation, you can remove the `_sqlx_migrations` table: all your old migrations will be reapplied, and SQLPage will start again.

+ 20 - 0
sqlpage/sqlpage/templates/README.md

@@ -0,0 +1,20 @@
+# SQLPage component templates
+
+SQLPage templates are handlebars[^1] files that are used to render the results of SQL queries.
+
+[^1]: https://handlebarsjs.com/
+
+## Default components
+
+SQLPage comes with a set of default[^2] components that you can use without having to write any code.
+These are documented on https://sql.ophir.dev/components.sql
+
+## Custom components
+
+You can [write your own component templates](https://sql.ophir.dev/custom_components.sql)
+and place them in the `sqlpage/templates` directory.
+To override a default component, create a file with the same name as the default component.
+If you want to start from an existing component, you can copy it from the `sqlpage/templates` directory
+in the SQLPage source code[^2].
+
+[^2]: A simple component to start from: https://github.com/lovasoa/SQLpage/blob/main/sqlpage/templates/code.handlebars

+ 62 - 0
sqlpage/transactions.sql

@@ -0,0 +1,62 @@
+SELECT
+  'dynamic' as component,
+  sqlpage.run_sql('cookie.sql') AS properties
+;
+
+SELECT
+  'dynamic' as component,
+  sqlpage.run_sql('nav.sql') AS properties
+;
+
+SELECT
+  'table' AS component,
+  TRUE AS sort,
+  TRUE AS search,
+  TRUE AS striped_rows,
+  TRUE AS small,
+  'Price' AS align_right,
+  'Quantity' AS align_right,
+  'Unit' AS align_right,
+  'Notice' AS markdown
+;
+
+DROP TABLE IF EXISTS sqlpage_txn;
+CREATE TEMPORARY TABLE IF NOT EXISTS sqlpage_txn AS (
+SELECT
+  to_char(
+    (ts AT TIME ZONE 'UTC') AT TIME ZONE 'Pacific/Auckland', --COALESCE('$ParameterTZ', 'Pacific/Auckland'),
+    'Mon DD HH24:MI:SS'
+  ) AS "Time",
+  t.description AS Description,
+  s.code AS "Store",
+  p.name AS "Product",
+  c.name AS "Category",
+  g.name AS "Group",
+  CASE WHEN t.organic THEN 'yes' ELSE 'no' END AS "Organic",
+  to_char(t.price, 'FM999999999.00') AS "Price",
+  --to_char(quantity, '9.'||repeat('9',3-1)||'EEEE') AS Quantity,
+  to_char(t.price, 'FM999999999.000') AS "Quantity",
+  u.name AS "Unit"
+FROM transactions t
+JOIN products p ON (product_id = p.id)
+JOIN categories c ON (category_id = c.id)
+JOIN groups g ON (group_id = g.id)
+JOIN stores s ON (store_id = s.id)
+JOIN units u ON (t.unit_id = u.id)
+JOIN sqlpage_options op ON op.type = 'product' AND p.name = op.name
+JOIN sqlpage_options oc ON oc.type = 'category' AND c.name = oc.name
+JOIN sqlpage_options og ON og.type = 'group' AND g.name = og.name
+WHERE
+  COALESCE(op.selected, oc.selected, og.selected, TRUE)
+AND
+  op.session = sqlpage.cookie('session')
+AND
+  oc.session = sqlpage.cookie('session')
+AND
+  og.session = sqlpage.cookie('session')
+LIMIT 100);
+SELECT * FROM sqlpage_txn;
+
+SELECT 
+  '# No Data' AS "Notice"
+WHERE NOT EXISTS(SELECT * FROM sqlpage_txn LIMIT 1);

+ 0 - 268
transactions-sqlpage.sql

@@ -1,268 +0,0 @@
-SELECT 'shell' AS component,
-  'dark' AS theme,
-  COALESCE($tab, 'Transactions') AS title
-;
-SELECT
-  'text' AS component,
-  '<style>
-.nav { pointer-events: none }
-.navbar { display: none }
-input[value=Products].form-control,
-input[value=Categories].form-control,
-input[value=Groups].form-control,
-input[value=Transactions].form-control
-{
-    color: var(--tblr-secondary);
-    border-right: unset;
-    border-top: unset;
-    border-left: unset;
-}
-input[value='||CASE
-  WHEN $tab IN ('Products', 'Categories', 'Groups')
-  THEN $tab ELSE 'Transactions'
-END||'].form-control
-{
-    color: var(--tblr-primary);
-    border-right: unset;
-    border-top: unset;
-    border-left: unset;
-    border-bottom-color: var(--tblr-primary);
-    border-bottom-width: 2px;
-}
-</style>' AS html
-;
-  
---SELECT
---  'tab' AS component,
---  TRUE AS center
---;
---
---SELECT
--- 'products' AS id,
--- 'Products' AS title,
--- (SELECT $tab = 'Products') AS active
---;
---SELECT
--- 'categories' AS id,
--- 'Categories' AS title,
--- (SELECT $tab = 'Categories') AS active
---;
---SELECT
--- 'groups' AS id,
--- 'Groups' AS title,
--- (SELECT $tab = 'Groups') AS active
---;
---SELECT
--- 'transactions' AS id,
--- 'Transactions' AS title,
--- (SELECT $tab = 'Transactions' OR $tab IS NULL) AS active
---;
-
-CREATE TEMPORARY TABLE IF NOT EXISTS options(
-  "type" text,
-  name text,
-  selected boolean
-);
-DELETE FROM options;
-INSERT INTO options
-SELECT
-  UNNEST(ARRAY['product', 'category', 'group']) AS "type",
-  UNNEST(q.name) AS name,
-  UNNEST(selected)
-FROM (
-  SELECT
-    ARRAY[p.name, c.name, g.name] AS name,
-    ARRAY[
-      CASE WHEN $products IS NOT NULL THEN fp.v IS NOT NULL ELSE NULL END,
-      CASE WHEN $categories IS NOT NULL THEN fc.v IS NOT NULL ELSE NULL END,
-      CASE WHEN $groups IS NOT NULL THEN fg.v IS NOT NULL ELSE NULL END
-    ]
-  FROM products p
-  LEFT JOIN json_array_elements($products::json) fp(v)
-  ON p.name = fp.v#>>'{}'
-  JOIN categories c ON p.category_id = c.id
-  LEFT JOIN json_array_elements($categories::json) fc(v)
-  ON c.name = fc.v#>>'{}'
-  JOIN groups g ON c.group_id = g.id
-  LEFT JOIN json_array_elements($groups::json) fg(v)
-  ON g.name = fg.v#>>'{}'
-  WHERE (
-    (p.name = fp.v#>>'{}' OR $products IS NULL)
-  AND
-    (c.name = fc.v#>>'{}' OR $categories IS NULL)
-  AND
-    (g.name = fg.v#>>'{}' OR $groups IS NULL)
-  )
-) q(name, selected)
-UNION
-SELECT
-  'group',
-  f.v#>>'{}',
-  TRUE
-FROM json_array_elements($groups::json) f(v)
-LEFT JOIN groups g ON g.name = f.v#>>'{}'
-WHERE g.name is NULL
-UNION
-SELECT
-  'category',
-  f.v#>>'{}',
-  TRUE
-FROM json_array_elements($categories::json) f(v)
-LEFT JOIN categories c ON c.name = f.v#>>'{}'
-WHERE c.name is NULL
-UNION
-SELECT
-  'product',
-  f.v#>>'{}',
-  TRUE
-FROM json_array_elements($products::json) f(v)
-LEFT JOIN products p ON p.name = f.v#>>'{}'
-WHERE p.name is NULL
-;
-
-SELECT
-  'form' AS component,
-  'filter' AS id,
-  'get' AS method,
-  '' AS title,
-  'transactions.sql' AS action,
-  'Apply' AS validate
-;
-
-SELECT
-  'tab' AS name,
-  '' AS label,
-  COALESCE($tab, 'Transactions') AS value,
-  'hidden' AS type
-;
-
-SELECT
-  'tab' AS name,
-  'Products' AS value,
-  '' AS label,
-  3 AS width,
-  'submit' AS type,
-  'transactions.sql' AS formaction
-;
-SELECT
-  'tab' AS name,
-  'Categories' AS value,
-  '' AS label,
-  3 AS width,
-  'submit' AS type,
-  'transactions.sql' AS formaction
-;
-SELECT
-  'tab' AS name,
-  'Groups' AS value,
-  '' AS label,
-  3 AS width,
-  'submit' AS type,
-  'transactions.sql' AS formaction
-;
-SELECT
-  'tab' AS name,
-  'Transactions' AS value,
-  '' AS label,
-  3 AS width,
-  'submit' AS type,
-  'transactions.sql' AS formaction
-;
-
-SELECT
-  'products[]' AS name,
-  'Product' AS label,
-  4 AS width,
-  'select' AS "type",
-  TRUE AS dropdown,
-  TRUE AS multiple,
-  json_agg(json_build_object(
-    'label', o.name,
-    'value', o.name,
-    'selected', COALESCE(o.selected, FALSE)
-  ) ORDER BY o.name) AS options
-FROM options o
-WHERE o.type = 'product'
-;
-SELECT
-  'categories[]' AS name,
-  'Categories' AS label,
-  4 AS width,
-  'select' AS "type",
-  TRUE AS dropdown,
-  TRUE AS multiple,
-  json_agg(json_build_object(
-    'label', o.name,
-    'value', o.name,
-    'selected', COALESCE(o.selected, FALSE)
-  ) ORDER BY o.name) AS options
-FROM options o
-WHERE o.type = 'category'
-;
-SELECT
-  'groups[]' AS name,
-  'Groups' AS label,
-  4 AS width,
-  'select' AS "type",
-  TRUE AS dropdown,
-  TRUE AS multiple,
-  json_agg(json_build_object(
-    'label', o.name,
-    'value', o.name,
-    'selected', COALESCE(o.selected, FALSE)
-  ) ORDER BY o.name) AS options
-FROM options o
-WHERE o.type = 'group'
-;
-SELECT
-  'clear' AS name,
-  'Clear' AS value,
-  '' AS label,
-  1 AS width,
-  'submit' AS type,
-  'entry.sql' AS formaction
-;
-
-SELECT
-  'table' AS component,
-  TRUE AS sort,
-  TRUE AS search,
-  TRUE AS striped_rows,
-  TRUE AS small,
-  'Price' AS align_right,
-  'Quantity' AS align_right,
-  'Unit' AS align_right,
-  'Notice' AS markdown
-;
-
-DROP TABLE IF EXISTS txn;
-CREATE TEMPORARY TABLE txn AS (SELECT
-  to_char(
-    (ts AT TIME ZONE 'UTC') AT TIME ZONE 'Pacific/Auckland', --COALESCE('$ParameterTZ', 'Pacific/Auckland'),
-    'Mon DD HH24:MI:SS'
-  ) AS "Time",
-  t.description AS Description,
-  s.code AS "Store",
-  p.name AS "Product",
-  c.name AS "Category",
-  g.name AS "Group",
-  CASE WHEN t.organic THEN 'yes' ELSE 'no' END AS "Organic",
-  to_char(t.price, 'FM999999999.00') AS "Price",
-  --to_char(quantity, '9.'||repeat('9',3-1)||'EEEE') AS Quantity,
-  to_char(t.price, 'FM999999999.000') AS "Quantity",
-  u.name AS "Unit"
-FROM transactions t
-JOIN products p ON (product_id = p.id)
-JOIN categories c ON (category_id = c.id)
-JOIN groups g ON (group_id = g.id)
-JOIN stores s ON (store_id = s.id)
-JOIN units u ON (t.unit_id = u.id)
-JOIN options op ON op.type = 'product' AND p.name = op.name
-JOIN options oc ON oc.type = 'category' AND c.name = oc.name
-JOIN options og ON og.type = 'group' AND g.name = og.name
-WHERE COALESCE(op.selected, oc.selected, og.selected, TRUE)
-LIMIT 20);
-SELECT * FROM txn;
-SELECT 
-  '# No Data' AS "Notice"
-WHERE NOT EXISTS(SELECT * FROM txn LIMIT 1);