From f30934d1a48bd5e980ba9ea2e12346d173eafd28 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sat, 28 Feb 2026 19:55:09 +0800 Subject: [PATCH 01/16] refactor: log full request URL in server hook --- src/hooks.server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 3f12a26d..3fbb6cd0 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -65,12 +65,12 @@ export const handleDevTools: Handle = async ({ event, resolve }) => { export const log: Handle = async ({ event, resolve }) => { const { request: { method }, - url: { pathname, origin }, + url, locals: { user, session }, } = event; console.info( - `[${user && session ? "Authenticated" : "Unauthenticated"}] ${new Date().toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "numeric", second: "numeric", hour12: true })} | ${method} | ${origin}${pathname}` + `[${user && session ? "Authenticated" : "Unauthenticated"}] ${new Date().toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "numeric", second: "numeric", hour12: true })} | [${method}]: ${url}` ); return resolve(event); From 0d05959e6a878fd4817f80dc0a4190a8f0b7e614 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sat, 28 Feb 2026 19:56:06 +0800 Subject: [PATCH 02/16] refactor: exclude ARIA/role props and use dynamic element. --- .../split-reveal/SplitReveal.svelte | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/lib/motion-core/split-reveal/SplitReveal.svelte b/src/lib/motion-core/split-reveal/SplitReveal.svelte index a37ae81b..246a8ce4 100644 --- a/src/lib/motion-core/split-reveal/SplitReveal.svelte +++ b/src/lib/motion-core/split-reveal/SplitReveal.svelte @@ -75,6 +75,15 @@ ...restProps }: ComponentProps = $props(); + // Filter out ARIA and role attributes so ancestor ARIA labels/roles are not forwarded + // to the internal wrapper element. This prevents duplicate/prohibited ARIA usage + // when consumers pass `aria-*` or `role` to this component. + const forwardedProps = $derived( + Object.fromEntries( + Object.entries(restProps ?? {}).filter(([k]) => !k.startsWith("aria-") && k !== "role") + ) + ); + const resolvedConfig = $derived.by(() => { const overrides = config?.[mode]; const defaults = DEFAULT_CONFIG[mode]; @@ -91,7 +100,7 @@ } } - function initSplitReveal(node: HTMLSpanElement) { + function initSplitReveal(node: HTMLElement) { gsap.registerPlugin(SplitText, CustomEase, ScrollTrigger); CustomEase.create("motion-core-ease", "0.625, 0.05, 0, 1"); @@ -155,10 +164,11 @@ } - {@render children?.()} - + From d629cae67820181ae5e1ba29234ad64065913a00 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sat, 28 Feb 2026 19:56:26 +0800 Subject: [PATCH 03/16] refactor(style): adjust --muted-foreground colors in layout.css for light and dark themes. --- src/routes/layout.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/layout.css b/src/routes/layout.css index c04be53f..1c5bd24a 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -99,7 +99,7 @@ --secondary: oklch(0.967 0.001 286.375); --secondary-foreground: oklch(0.21 0.006 285.885); --muted: oklch(0.967 0.001 286.375); - --muted-foreground: oklch(0.552 0.016 285.938); + --muted-foreground: oklch(0.35 0.02 285.938); --accent: oklch(0.967 0.001 286.375); --accent-foreground: oklch(0.21 0.006 285.885); --destructive: oklch(0.577 0.245 27.325); @@ -143,7 +143,7 @@ --secondary: oklch(0.3095 0.0266 266.7132); --secondary-foreground: oklch(0.9219 0 0); --muted: oklch(0.3095 0.0266 266.7132); - --muted-foreground: oklch(0.7155 0 0); + --muted-foreground: oklch(0.92 0 0); --accent: oklch(0.338 0.0589 267.5867); --accent-foreground: oklch(0.8823 0.0571 254.1284); --destructive: oklch(0.6368 0.2078 25.3313); From 1daf7f049d71e9682c7f41a573450a64010f6011 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sat, 28 Feb 2026 20:04:32 +0800 Subject: [PATCH 04/16] feat: add status column to sub_meter --- .../migration.sql | 1 + .../snapshot.json | 1221 +++++++++++++++++ src/lib/server/db/schema/sub-meter.ts | 1 + 3 files changed, 1223 insertions(+) create mode 100644 drizzle/20260228120254_furry_black_panther/migration.sql create mode 100644 drizzle/20260228120254_furry_black_panther/snapshot.json diff --git a/drizzle/20260228120254_furry_black_panther/migration.sql b/drizzle/20260228120254_furry_black_panther/migration.sql new file mode 100644 index 00000000..c8264a29 --- /dev/null +++ b/drizzle/20260228120254_furry_black_panther/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "sub_meter" ADD COLUMN "status" text DEFAULT '' NOT NULL; \ No newline at end of file diff --git a/drizzle/20260228120254_furry_black_panther/snapshot.json b/drizzle/20260228120254_furry_black_panther/snapshot.json new file mode 100644 index 00000000..876680bb --- /dev/null +++ b/drizzle/20260228120254_furry_black_panther/snapshot.json @@ -0,0 +1,1221 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "a4278953-37ff-4cb4-aee9-36ca03205a16", + "prevIds": [ + "79ffe6ff-a523-4237-b869-f9391de5ec59" + ], + "ddl": [ + { + "isRlsEnabled": false, + "name": "billing_info", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "email_verification_request", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "password_reset_session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "payment", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "sub_meter", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "billing_info" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "billing_info" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "date", + "entityType": "columns", + "schema": "public", + "table": "billing_info" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "total_kWh", + "entityType": "columns", + "schema": "public", + "table": "billing_info" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "balance", + "entityType": "columns", + "schema": "public", + "table": "billing_info" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "billing_info" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pay_per_kWh", + "entityType": "columns", + "schema": "public", + "table": "billing_info" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payment_id", + "entityType": "columns", + "schema": "public", + "table": "billing_info" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "billing_info" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "billing_info" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "email_verification_request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "email_verification_request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "email_verification_request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "email_verification_request" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "email_verification_request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "password_reset_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "password_reset_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "password_reset_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "password_reset_session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "password_reset_session" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "public", + "table": "password_reset_session" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "two_factor_verified", + "entityType": "columns", + "schema": "public", + "table": "password_reset_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "payment" + }, + { + "type": "real", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "amount", + "entityType": "columns", + "schema": "public", + "table": "payment" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "date", + "entityType": "columns", + "schema": "public", + "table": "payment" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "payment" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "payment" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "two_factor_verified", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sub_meter" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "public", + "table": "sub_meter" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "billing_info_id", + "entityType": "columns", + "schema": "public", + "table": "sub_meter" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sub_kWh", + "entityType": "columns", + "schema": "public", + "table": "sub_meter" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reading", + "entityType": "columns", + "schema": "public", + "table": "sub_meter" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "sub_meter" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "payment_id", + "entityType": "columns", + "schema": "public", + "table": "sub_meter" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "sub_meter" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "sub_meter" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "github_id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "totp_key", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "recovery_code", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "registered_two_factor", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password_hash", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "billing_info_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "billing_info" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "email_verification_request_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "email_verification_request" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "password_reset_session_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "password_reset_session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "billing_info_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sub_meter_billing_info_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sub_meter" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "payment_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sub_meter_payment_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sub_meter" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "user_email_key", + "entityType": "indexes", + "schema": "public", + "table": "user" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "billing_info_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "billing_info" + }, + { + "nameExplicit": false, + "columns": [ + "payment_id" + ], + "schemaTo": "public", + "tableTo": "payment", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "billing_info_payment_id_payment_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "billing_info" + }, + { + "nameExplicit": true, + "columns": [ + "payment_id" + ], + "schemaTo": "public", + "tableTo": "payment", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "billing_info_payment_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "billing_info" + }, + { + "nameExplicit": true, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "billing_info_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "billing_info" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "email_verification_request_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "email_verification_request" + }, + { + "nameExplicit": true, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "email_verification_request_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "email_verification_request" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "password_reset_session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "password_reset_session" + }, + { + "nameExplicit": true, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "password_reset_session_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "password_reset_session" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "session_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "billing_info_id" + ], + "schemaTo": "public", + "tableTo": "billing_info", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "sub_meter_billing_info_id_billing_info_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sub_meter" + }, + { + "nameExplicit": false, + "columns": [ + "payment_id" + ], + "schemaTo": "public", + "tableTo": "payment", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "sub_meter_payment_id_payment_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sub_meter" + }, + { + "nameExplicit": true, + "columns": [ + "billing_info_id" + ], + "schemaTo": "public", + "tableTo": "billing_info", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "sub_meter_billing_info_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sub_meter" + }, + { + "nameExplicit": true, + "columns": [ + "payment_id" + ], + "schemaTo": "public", + "tableTo": "payment", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "sub_meter_payment_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sub_meter" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "billing_info_pkey", + "schema": "public", + "table": "billing_info", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "email_verification_request_pkey", + "schema": "public", + "table": "email_verification_request", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "password_reset_session_pkey", + "schema": "public", + "table": "password_reset_session", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "payment_pkey", + "schema": "public", + "table": "payment", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sub_meter_pkey", + "schema": "public", + "table": "sub_meter", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "github_id" + ], + "nullsNotDistinct": false, + "name": "user_github_id_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/src/lib/server/db/schema/sub-meter.ts b/src/lib/server/db/schema/sub-meter.ts index ab4a6dbc..a483eef6 100644 --- a/src/lib/server/db/schema/sub-meter.ts +++ b/src/lib/server/db/schema/sub-meter.ts @@ -13,6 +13,7 @@ export const subMeter = pgTable( .references(() => billingInfo.id, { onDelete: "cascade", onUpdate: "cascade" }), subkWh: integer("sub_kWh").notNull(), reading: integer().notNull(), + status: text().notNull().default(""), paymentId: text("payment_id") .references(() => payment.id, { onDelete: "cascade", From 68dca81e921bc4ac03087134a93fed7d54927c19 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sat, 28 Feb 2026 20:24:36 +0800 Subject: [PATCH 05/16] refactor: remove fade and simplify hero verb rendering --- src/routes/(components)/hero.svelte | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/routes/(components)/hero.svelte b/src/routes/(components)/hero.svelte index dea09489..7297af93 100644 --- a/src/routes/(components)/hero.svelte +++ b/src/routes/(components)/hero.svelte @@ -15,7 +15,6 @@ import { Card, CardDescription, CardHeader, CardTitle } from "$/components/ui/card"; import { Zap, PhilippinePeso, Banknote } from "$lib/assets/icons"; import { TextLoop, Magnetic, ScrollReveal, ScrollStagger } from "$lib/motion-core"; - import { fade } from "svelte/transition"; let { user, session, loading }: HeroProps = $props(); @@ -33,10 +32,6 @@ needs2FA: user && user.registeredTwoFactor && (!session || !session.twoFactorVerified), currentText: texts[currentIndex], }); - - let { verb } = $derived({ - verb: currentText === "Payments" ? "Generate" : "Track", - });

- {#key verb} - {verb} - {/key}{" "} + {currentText === "Payments" ? "Generate" : "Track"} - {" "} + Easily

From 1991186b6a068644a348a643c2e93cb47b18a512 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sat, 28 Feb 2026 20:30:03 +0800 Subject: [PATCH 06/16] refactor: remove dev-only generateRandomBillingInfos command and its schema. Add subMeter "status" fallback with picklist 'Paid','Pending','N/A' and default 'Pending' --- src/lib/api/billing-info.remote.ts | 86 ------------------------------ src/lib/validators/billing-info.ts | 22 +------- 2 files changed, 1 insertion(+), 107 deletions(-) diff --git a/src/lib/api/billing-info.remote.ts b/src/lib/api/billing-info.remote.ts index e96aa83c..7c8b7a69 100644 --- a/src/lib/api/billing-info.remote.ts +++ b/src/lib/api/billing-info.remote.ts @@ -8,7 +8,6 @@ import { getBillingInfoSchema, deleteBillingInfoSchema, deleteBillingInfoSchemaBatch, - generateRandomBillingInfosSchema, } from "$/validators/billing-info"; import type { BillingInfo, @@ -31,7 +30,6 @@ import { error, invalid } from "@sveltejs/kit"; import { addSubMeter, deleteSubMeterBy, updateSubMeterBy } from "$/server/crud/sub-meter-crud"; import { updatePaymentBy } from "$/server/crud/payment-crud"; import type { HelperResult } from "$/server/types/helper"; -import { dev } from "$app/environment"; import { getTotalUserCount } from "./user.remote"; import { getTotalPaymentsAmount } from "./payment.remote"; @@ -208,90 +206,6 @@ export const createBillingInfo = form( } ); -// Form to generate random billing infos for testing -export const generateRandomBillingInfos = command( - generateRandomBillingInfosSchema, - async (data): Promise> => { - const { - session: { userId }, - } = requireAuth(); - - if (!dev) error(404, "Not found"); - - const { count, minSubMeters, maxSubMeters } = data as { - count: number; - minSubMeters: number; - maxSubMeters: number; - }; - - console.log("Generating random bills:", { count, minSubMeters, maxSubMeters }); - - // Get latest billing info for sub meter readings - const { valid: _validLatest, value: latestInfos } = await getExtendedBillingInfos({ userId }); - const latest = latestInfos[0]; - let subMeterReadings: Record = {}; - if (latest) { - latest.subMeters?.forEach((sub) => { - subMeterReadings[sub.label] = sub.reading; - }); - } - - // Generate sequential months from 2000 to current year - const end = new Date(); - const totalYears = end.getFullYear() - 2000; - const totalMonths = totalYears * 12 + end.getMonth() + 1; - const monthsPerBill = totalMonths / count; - - let created = 0; - console.log("Starting loop for", count, "bills"); - for (let i = 0; i < count; i++) { - console.log("Creating bill", i + 1); - const monthIndex = Math.floor(i * monthsPerBill); - const year = 2000 + Math.floor(monthIndex / 12); - const month = (monthIndex % 12) + 1; - const day = Math.floor(Math.random() * 28) + 1; // 1-28 to avoid invalid dates - const date = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; - const numSub = Math.floor(Math.random() * (maxSubMeters - minSubMeters + 1)) + minSubMeters; - const totalkWh = Math.floor(Math.random() * 2000) + 1000 + numSub * 100; // 1000-3000 + buffer - const balance = Math.floor(Math.random() * 2000) + 1000 + numSub * 100; // 1000-3000 + buffer - const status = Math.random() > 0.5 ? "Paid" : "Pending"; - const subMeters = []; - for (let j = 1; j <= numSub; j++) { - const label = `Sub Meter ${j}`; - const prevReading = subMeterReadings[label] || 0; - const reading = prevReading + Math.floor(Math.random() * 50) + 10; // +10-60 - subMeters.push({ label, reading }); - subMeterReadings[label] = reading; - } - - try { - await createBillingInfoLogic( - { - date, - totalkWh, - balance, - status, - subMeters, - }, - userId - ); - created++; - console.log("Created bill", i + 1); - } catch (err) { - console.error(`Failed to create billing info ${i + 1}:`, err); - } - } - console.log("Created", created, "bills"); - - getExtendedBillingInfos({ userId }).refresh(); - return { - valid: true, - value: created, - message: `Generated ${created} random billing infos`, - }; - } -); - // Form to update an existing billing info with multiple sub meters export const updateBillingInfo = form( updateBillingInfoSchema, diff --git a/src/lib/validators/billing-info.ts b/src/lib/validators/billing-info.ts index 85d21f5c..49c67c0d 100644 --- a/src/lib/validators/billing-info.ts +++ b/src/lib/validators/billing-info.ts @@ -6,6 +6,7 @@ export const subMeterSchema = v.object({ v.string(), v.check((val) => !!val, "is required") ), + status: v.fallback(v.picklist(["Paid", "Pending", "N/A"]), "Pending"), reading: v.pipe(v.number("must be a number"), v.minValue(0, "must be 0 or greater")), }); @@ -50,24 +51,3 @@ export const deleteBillingInfoSchemaBatch = v.object({ ids: v.array(v.string()), count: v.number(), }); - -export const generateRandomBillingInfosSchema = v.object({ - count: v.pipe( - v.unknown(), - v.transform((v) => Number(v)), - v.number(), - v.minValue(1, "must be at least 1") - ), - minSubMeters: v.pipe( - v.unknown(), - v.transform((v) => Number(v)), - v.number(), - v.minValue(0, "must be 0 or greater") - ), - maxSubMeters: v.pipe( - v.unknown(), - v.transform((v) => Number(v)), - v.number(), - v.minValue(0, "must be 0 or greater") - ), -}); From 2302d16a72a5d8c3212853cb74705c408c71e6b7 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sat, 28 Feb 2026 20:43:56 +0800 Subject: [PATCH 07/16] refactor: normalize status literals to lowercase and replace 'N/A' with 'due' --- src/lib/types/billing-info.ts | 4 +++- src/lib/types/sub-meter.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/types/billing-info.ts b/src/lib/types/billing-info.ts index d9a071cd..6f34e5b9 100644 --- a/src/lib/types/billing-info.ts +++ b/src/lib/types/billing-info.ts @@ -10,13 +10,15 @@ export type BillingInfoWithPaymentAndSubMetersWithPayment = BillingInfo & { subMeters?: SubMeterWithPayment[]; }; +export type Status = "paid" | "pending" | "due"; + export type BillingInfoDTO = { id: string; userId: string; date: Date; totalkWh: number; balance: number; - status: "Paid" | "Pending" | "N/A"; + status: Status; payPerkWh: number; paymentId: string | null; createdAt: Date; diff --git a/src/lib/types/sub-meter.ts b/src/lib/types/sub-meter.ts index b135c5c4..4e53c731 100644 --- a/src/lib/types/sub-meter.ts +++ b/src/lib/types/sub-meter.ts @@ -1,4 +1,5 @@ import type { subMeter, payment, billingInfo } from "$/server/db/schema"; +import type { Status } from "$/types/billing-info"; export type Payment = typeof payment.$inferSelect; export type BillingInfo = typeof billingInfo.$inferSelect; @@ -15,6 +16,7 @@ export type SubMeterDTO = { paymentId: string; createdAt: Date; updatedAt: Date; + status: Status; }; export type SubMeterTableView = Omit & { From ad094497c43451df1d4c586e880eb6d602e3b913 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sat, 28 Feb 2026 21:06:40 +0800 Subject: [PATCH 08/16] refactor: introduce STATUS_VALUES constant and derive Status type. Update billing validators to use STATUS_VALUES and standardize defaults to "pending" (replace previous mixed casing/N/A). --- src/lib/types/billing-info.ts | 5 +++-- src/lib/validators/billing-info.ts | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/lib/types/billing-info.ts b/src/lib/types/billing-info.ts index 6f34e5b9..7695fe58 100644 --- a/src/lib/types/billing-info.ts +++ b/src/lib/types/billing-info.ts @@ -10,8 +10,6 @@ export type BillingInfoWithPaymentAndSubMetersWithPayment = BillingInfo & { subMeters?: SubMeterWithPayment[]; }; -export type Status = "paid" | "pending" | "due"; - export type BillingInfoDTO = { id: string; userId: string; @@ -68,3 +66,6 @@ export type BillingCreateForm = { status: string; subMeters: { label: string; reading: number }[]; }; + +export const STATUS_VALUES = ["paid", "pending", "due"] as const; +export type Status = (typeof STATUS_VALUES)[number]; diff --git a/src/lib/validators/billing-info.ts b/src/lib/validators/billing-info.ts index 49c67c0d..f15a0abb 100644 --- a/src/lib/validators/billing-info.ts +++ b/src/lib/validators/billing-info.ts @@ -1,3 +1,4 @@ +import { STATUS_VALUES } from "$/types/billing-info"; import * as v from "valibot"; // Schema for individual sub meter entry @@ -6,7 +7,7 @@ export const subMeterSchema = v.object({ v.string(), v.check((val) => !!val, "is required") ), - status: v.fallback(v.picklist(["Paid", "Pending", "N/A"]), "Pending"), + status: v.fallback(v.picklist(STATUS_VALUES), "pending"), reading: v.pipe(v.number("must be a number"), v.minValue(0, "must be 0 or greater")), }); @@ -22,7 +23,7 @@ export const billFormSchema = v.object({ totalkWh: v.pipe(v.number("must be a number"), v.minValue(1, "must be greater than 0")), // Multiple sub meters instead of single subReading subMeters: v.fallback(v.array(subMeterSchema), []), - status: v.fallback(v.picklist(["Paid", "Pending", "N/A"]), "Pending"), + status: v.fallback(v.picklist(STATUS_VALUES), "pending"), }); // Schema for updating billing info with multiple sub meters @@ -36,7 +37,7 @@ export const updateBillingInfoSchema = v.object({ totalkWh: v.pipe(v.number("must be a number"), v.minValue(1, "must be greater than 0")), // Multiple sub meters instead of single subReading subMeters: v.fallback(v.array(updateSubMeterSchema), []), - status: v.fallback(v.picklist(["Paid", "Pending", "N/A"]), "Pending"), + status: v.fallback(v.picklist(STATUS_VALUES), "pending"), }); export const billingInfoSchema = updateBillingInfoSchema; From 100a5907bfec69b3388a17d18a5a01512544b960 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sat, 28 Feb 2026 23:31:46 +0800 Subject: [PATCH 09/16] fix: use convertToNormalText for the status value and change the class conditional to match the normalized 'paid' string --- .../(components)/history-data-table-row-actions.svelte | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/history/(components)/history-data-table-row-actions.svelte b/src/routes/history/(components)/history-data-table-row-actions.svelte index 2885ba47..3ff6d3de 100644 --- a/src/routes/history/(components)/history-data-table-row-actions.svelte +++ b/src/routes/history/(components)/history-data-table-row-actions.svelte @@ -29,6 +29,7 @@ import { useBillingStore } from "$/stores/billing.svelte"; import { useConsumptionStore } from "$/stores/consumption.svelte"; import { billingInfoToDto } from "$/utils/mapper/billing-info"; + import { convertToNormalText } from "$/utils/text"; let { row }: BillingInfoDataTableRowActionsProps = $props(); @@ -72,8 +73,8 @@ }, { label: "Status", - value: row.original.status, - class: `font-semibold ${row.original.status === "Paid" ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}`, + value: convertToNormalText(row.original.status), + class: `font-semibold ${row.original.status === "paid" ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}`, }, { label: "Pay Per kWh", From cd850a1536fe95e0b18d4b3b21e77807ff2048af Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sat, 28 Feb 2026 23:33:27 +0800 Subject: [PATCH 10/16] refactor: render status with convertToNormalText; map 'paid' to default, 'pending' to outline, and other statuses to destructive --- src/routes/history/(components)/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/history/(components)/index.ts b/src/routes/history/(components)/index.ts index b8b0d1d2..46a821ef 100644 --- a/src/routes/history/(components)/index.ts +++ b/src/routes/history/(components)/index.ts @@ -6,6 +6,7 @@ import type { ColumnDef, Table } from "@tanstack/table-core"; import { createRawSnippet } from "svelte"; import { HistoryDataTableRowActions, SubPaymentsButton } from "."; import { formatDate, formatNumber } from "$/utils/format"; +import { convertToNormalText } from "$/utils/text"; export function historyTableColumns() { return [ @@ -132,10 +133,10 @@ export function historyTableColumns() { const status = row.original.status; return renderComponent(Badge, { title: status, - variant: status === "Paid" ? "default" : "destructive", + variant: status === "paid" ? "default" : status === "pending" ? "outline" : "destructive", children: createRawSnippet(() => { return { - render: () => `${status}`, + render: () => `${convertToNormalText(status)}`, }; }), }); From 485fa0a2ff2a69585a20e7ee77f2995a92dcbf87 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sat, 28 Feb 2026 23:34:00 +0800 Subject: [PATCH 11/16] refactor: add status row to sub-payments dialog --- .../history/(components)/sub-payments-dialog.svelte | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/routes/history/(components)/sub-payments-dialog.svelte b/src/routes/history/(components)/sub-payments-dialog.svelte index fe087aa9..df3a612a 100644 --- a/src/routes/history/(components)/sub-payments-dialog.svelte +++ b/src/routes/history/(components)/sub-payments-dialog.svelte @@ -1,6 +1,7 @@ -

+ Sub Payments for [{billingInfo.dateFormatted}] From 0dd843b09a28ff230bd8e48828d2a083c8ce2bc8 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sun, 1 Mar 2026 00:03:33 +0800 Subject: [PATCH 13/16] refactor: handle status field in sub-meter CRUD --- src/lib/server/crud/sub-meter-crud.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/server/crud/sub-meter-crud.ts b/src/lib/server/crud/sub-meter-crud.ts index 1ee223ef..7e676414 100644 --- a/src/lib/server/crud/sub-meter-crud.ts +++ b/src/lib/server/crud/sub-meter-crud.ts @@ -183,6 +183,9 @@ export async function mapNewSubMeter_to_DTO( reading: Object.prototype.hasOwnProperty.call(_sub_meter, "reading") ? (_sub_meter as any).reading : undefined, + status: Object.prototype.hasOwnProperty.call(_sub_meter, "status") + ? (_sub_meter as any).status + : undefined, paymentId: Object.prototype.hasOwnProperty.call(_sub_meter, "paymentId") ? (_sub_meter as any).paymentId : undefined, @@ -275,6 +278,8 @@ function buildWhereSQL(where: Record): SQL | undefined { conditions.push(eq(subMeter.reading, value as number)); } else if (key === "paymentId") { conditions.push(eq(subMeter.paymentId, value as string)); + } else if (key === "status") { + conditions.push(eq(subMeter.status, value as string)); } } return conditions.length > 0 ? and(...conditions) : undefined; From 63e8ddb16f0c2512560c8a386579647d1aab56fe Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sun, 1 Mar 2026 00:07:29 +0800 Subject: [PATCH 14/16] feat: include submeter status in updates and add logs --- src/lib/api/billing-info.remote.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/lib/api/billing-info.remote.ts b/src/lib/api/billing-info.remote.ts index 7c8b7a69..321c11ea 100644 --- a/src/lib/api/billing-info.remote.ts +++ b/src/lib/api/billing-info.remote.ts @@ -212,7 +212,7 @@ export const updateBillingInfo = form( async (data): Promise => { const { session } = requireAuth(); const { id: billingInfoId, subMeters, ...updateData } = data; - + console.log(JSON.stringify(data, null, 2)); const { valid: validBillingInfo, value: [billingInfoWithSubMetersToUpdate], @@ -245,8 +245,6 @@ export const updateBillingInfo = form( updatedData ); - console.log({ billingInfoWithSubMetersToUpdate, changed_data }); - // Determine whether provided subMeters actually differ from existing ones // (cheap checks) so we can skip heavy work when they don't. const existingSubMeters = billingInfoWithSubMetersToUpdate.subMeters ?? []; @@ -269,7 +267,11 @@ export const updateBillingInfo = form( subMetersHaveChanges = true; break; } - if (existing.label !== s.label || existing.reading !== s.reading) { + if ( + existing.label !== s.label || + existing.reading !== s.reading || + existing.status !== s.status + ) { subMetersHaveChanges = true; break; } @@ -286,6 +288,17 @@ export const updateBillingInfo = form( } } + console.log( + JSON.stringify( + { + billingInfoWithSubMetersToUpdate, + changed_data, + subMetersHaveChanges, + }, + null, + 2 + ) + ); if (Object.keys(changed_data).length === 0 && !subMetersHaveChanges) { console.info("Bail out, no changed data"); return billingInfoWithSubMetersToUpdate as BillingInfo; @@ -316,6 +329,7 @@ export const updateBillingInfo = form( label: sub.label, reading: sub.reading, subkWh, + status: sub.status, paymentAmount, paymentId: currentMeter.paymentId, }; @@ -331,6 +345,7 @@ export const updateBillingInfo = form( reading: sub.reading, subkWh, paymentAmount, + status: sub.status, }; } }) ?? []; @@ -389,6 +404,7 @@ export const updateBillingInfo = form( { reading: subData.reading, subkWh: subData.subkWh, + status: subData.status, } ); @@ -421,6 +437,7 @@ export const updateBillingInfo = form( reading: subData.reading, subkWh: subData.subkWh, paymentId, + status: subData.status, }, ]; const { valid: validSubMeterInsert } = await addSubMeter(subMeterInserts, tx); From 61ce2c6a85058e5986757602cad9b75bd79621d0 Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sun, 1 Mar 2026 00:08:11 +0800 Subject: [PATCH 15/16] feat: add status handling to billing and sub-meter forms --- .../(components)/billing-info-form.svelte | 123 ++++++++++++------ 1 file changed, 84 insertions(+), 39 deletions(-) diff --git a/src/routes/history/(components)/billing-info-form.svelte b/src/routes/history/(components)/billing-info-form.svelte index d0644bb9..26b70cd7 100644 --- a/src/routes/history/(components)/billing-info-form.svelte +++ b/src/routes/history/(components)/billing-info-form.svelte @@ -20,6 +20,7 @@ id: string; label: string; reading: number; + status?: Status; }; type NormalizedBillingData = { @@ -27,7 +28,7 @@ date?: string; balance?: number; totalkWh?: number; - status?: string; + status?: Status; subMeters?: SubMeterForm[]; }; @@ -36,13 +37,12 @@ date?: string; balance?: number | string; totalkWh?: number | string; - status?: string; + status?: Status; subMeters?: SubMeterForm[]; }; type BillingInfoFormState = { dateValue: CalendarDate | undefined; - status: BillingInfoDTO["status"]; subMeters: SubMeterForm[]; asyncState: AsyncState; openDatePicker: boolean; @@ -62,7 +62,11 @@ import { ChevronDown, CirclePlus, Loader, Trash2 } from "$/assets/icons"; import { Calendar } from "$/components/ui/calendar"; import * as Card from "$/components/ui/card/index.js"; - import type { BillingInfoDTO, BillingInfoDTOWithSubMeters } from "$/types/billing-info"; + import { + STATUS_VALUES, + type BillingInfoDTOWithSubMeters, + type Status, + } from "$/types/billing-info"; import { formatDate, formatEnergy } from "$/utils/format"; import { convertToNormalText } from "$/utils/text"; import * as v from "valibot"; @@ -86,14 +90,13 @@ let subMeters: BillingInfoFormState["subMeters"] = $state([]); - let { dateValue, status, openDatePicker, asyncState } = $derived< - Omit - >({ - dateValue: undefined, - status: "Pending", - openDatePicker: false, - asyncState: "idle", - }); + let { dateValue, openDatePicker, asyncState } = $derived>( + { + dateValue: undefined, + openDatePicker: false, + asyncState: "idle", + } + ); const { currentAction } = $derived({ currentAction: action === "add" ? createBillingInfo : updateBillingInfo, @@ -109,6 +112,7 @@ id: s.id, label: s.label, reading: s.reading, + status: s.status, })); } else if (key === "date") { const d = new Date(billingInfo.date); @@ -163,7 +167,11 @@ subsChanged = true; break; } - if (ex.label !== s.label || Number(ex.reading) !== Number(s.reading)) { + if ( + ex.label !== s.label || + Number(ex.reading) !== Number(s.reading) || + ex.status !== s.status + ) { subsChanged = true; break; } @@ -192,7 +200,7 @@ : ""); const balanceVal = currentAction.fields.balance.value(); const totalkWhVal = currentAction.fields.totalkWh.value(); - const statusVal = currentAction.fields.status.value() ?? status; + const statusVal = currentAction.fields.status.value(); const balance = typeof balanceVal === "string" || typeof balanceVal === "number" ? Number(balanceVal) : NaN; @@ -208,9 +216,12 @@ totalkWh, status: statusVal, subMeters: subMeters.map((s: SubMeterForm) => { - const mapped: { label: string; reading: number; id?: string } = { + const mapped: Omit & { + id?: string; + } = { label: s.label, reading: Number(s.reading), + status: s.status, }; // Only include `id` for update operations where an id is meaningful if (action === "update" && s.id) mapped.id = s.id; @@ -231,6 +242,7 @@ id: crypto.randomUUID(), label: "", reading: 0, + status: undefined, }); // Ensure action fields get updated with current form values plus the new sub-meter @@ -242,9 +254,10 @@ id: s.id, label: currentFormValues[idx].label ?? s.label, reading: currentFormValues[idx].reading ?? s.reading, + status: currentFormValues[idx].status ?? s.status, }; } - return { id: s.id, label: s.label, reading: s.reading }; + return { id: s.id, label: s.label, reading: s.reading, status: s.status }; }) ); } @@ -267,9 +280,10 @@ id: s.id, label: currentFormValues[formIdx].label ?? s.label, reading: currentFormValues[formIdx].reading ?? s.reading, + status: currentFormValues[formIdx].status ?? s.status, }; } - return { id: s.id, label: s.label, reading: s.reading }; + return { id: s.id, label: s.label, reading: s.reading, status: s.status }; }) ); } @@ -291,7 +305,6 @@ ? new Date(Date.UTC(dateValue.year, dateValue.month - 1, dateValue.day)).toISOString() : "" ); - status = billingInfo.status; //@ts-expect-error id exists for update action currentAction.fields.id.set(billingInfo.id); currentAction.fields.balance.set(billingInfo.balance); @@ -307,7 +320,6 @@ 1 ) : undefined; - status = "Pending"; // Sync date with action fields so client-side validation can run // Convert CalendarDate to UTC ISO string to prevent timezone issues currentAction?.fields?.date?.set?.( @@ -322,6 +334,7 @@ id: sub.id, label: sub.label, reading: sub.reading, + status: sub.status, })) ?? []; currentAction.fields.subMeters.set( @@ -329,6 +342,7 @@ id: s.id, label: s.label, reading: s.reading, + status: s.status, })) ); }); @@ -487,25 +501,24 @@ Status currentAction?.fields?.status?.set?.(v as BillingInfoDTO["status"])} + onValueChange={(v) => currentAction?.fields.status.set(v as Status)} > - {convertToNormalText(status) || "Select status"} + {convertToNormalText(currentAction.fields.status.value() || "pending")} Status - {#each ["Paid", "Pending"] as option (option)} + {#each STATUS_VALUES as option (option)} - {option} + {convertToNormalText(option)} {/each} - + Select billing status @@ -524,7 +537,6 @@
{#each subMeters as subMeter, subIndex (subMeter.id)} - {@const currentMeter = currentAction.fields.subMeters[subIndex].value()}
@@ -555,11 +567,11 @@ /> {/if} - + - Label + Label - Current Reading + Current Reading + - {#if currentMeter} + {#if subMeter.reading > 0} Previous reading: [{subMeter.reading}] {:else} Current sub-meter reading for calculation @@ -585,16 +598,48 @@ - + + Status + + currentAction.fields.subMeters[subIndex]["status"].set(v as Status)} + > + + {#if action === "add"} + Pending + {:else} + {convertToNormalText( + currentAction.fields.subMeters[subIndex]["status"].value() || + "Select status" + )} + {/if} + + + + Status + {#each STATUS_VALUES as option (option)} + + {convertToNormalText(option)} + + {/each} + + + + + {#if subMeter?.reading != 0} +
- Consumption: {(currentMeter.reading && isNaN(currentMeter.reading)) || - currentMeter.reading === 0 - ? formatEnergy(0) - : formatEnergy( - currentMeter.reading ? currentMeter.reading - subMeter.reading : 0 - )} + Consumption: + {#if Number.isFinite(Number(currentAction?.fields?.subMeters?.[subIndex]?.["reading"]?.value?.()))} + {formatEnergy( + Number(currentAction?.fields?.subMeters?.[subIndex]?.["reading"]?.value?.()) - + Number(subMeter.reading) + )} + {/if}
{/if}
From c37e4a40ad38afabf5be4d2a54e6cc821848e19c Mon Sep 17 00:00:00 2001 From: pitzzahh Date: Sun, 1 Mar 2026 00:13:14 +0800 Subject: [PATCH 16/16] fix: cast status fields to the shared Status type and remove the payment property from subMeters using omit util. --- src/lib/utils/mapper/billing-info.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/utils/mapper/billing-info.ts b/src/lib/utils/mapper/billing-info.ts index 6e9bb44a..5bb00888 100644 --- a/src/lib/utils/mapper/billing-info.ts +++ b/src/lib/utils/mapper/billing-info.ts @@ -4,8 +4,10 @@ import type { BillingInfoTableView, ExtendedBillingInfo, ExtendedBillingInfoTableView, + Status, } from "$/types/billing-info"; import { formatDate, DateFormat } from "$/utils/format"; +import { omit } from "$/utils/mapper"; export function billingInfoToDto( original: ExtendedBillingInfoTableView @@ -17,11 +19,14 @@ export function billingInfoToDto( totalkWh: original.totalkWh, balance: original.balance, payPerkWh: original.payPerkWh, - status: original.status as "Pending" | "Paid", + status: original.status as Status, createdAt: new Date(original.createdAt), updatedAt: new Date(original.updatedAt), paymentId: original.paymentId, - subMeters: original.subMeters, + subMeters: original.subMeters.map((s) => ({ + ...omit(s, ["payment"]), + status: s.status as Status, + })), }; }