Merge pull request #11 from DanielVolz/copilot/remove-duplicate-code

Eliminate duplicate code: centralize database schema and date formatting utilities
This commit is contained in:
Daniel Volz
2026-01-01 19:52:55 +01:00
committed by GitHub
6 changed files with 164 additions and 339 deletions
+3 -91
View File
@@ -3,6 +3,7 @@ import { drizzle } from "drizzle-orm/libsql";
import { existsSync, mkdirSync, accessSync, constants, statSync, writeFileSync } from "fs"; import { existsSync, mkdirSync, accessSync, constants, statSync, writeFileSync } from "fs";
import { resolve } from "path"; import { resolve } from "path";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { getTableCreationSQL } from "./schema-sql";
dotenv.config({ path: process.env.DOTENV_PATH || ".env" }); dotenv.config({ path: process.env.DOTENV_PATH || ".env" });
@@ -43,97 +44,8 @@ export function ensureDataDirectory(dataDir: string): { success: boolean; error?
} }
} }
/** Get the SQL statements for creating all tables */ /** Get the SQL statements for creating all tables (re-exported from schema-sql) */
export function getTableCreationSQL(): string[] { export { getTableCreationSQL } from "./schema-sql";
return [
`CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
password_hash text,
avatar_url text,
auth_provider text NOT NULL DEFAULT 'local',
oidc_subject text,
is_active integer NOT NULL DEFAULT 1,
last_login_at integer,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
)`,
`CREATE TABLE IF NOT EXISTS medications (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
name text NOT NULL,
generic_name text,
taken_by_json text NOT NULL DEFAULT '[]',
pack_count integer NOT NULL DEFAULT 1,
blisters_per_pack integer NOT NULL DEFAULT 1,
pills_per_blister integer NOT NULL DEFAULT 1,
loose_tablets integer NOT NULL DEFAULT 0,
pill_weight_mg integer,
usage_json text NOT NULL DEFAULT '[]',
every_json text NOT NULL DEFAULT '[]',
start_json text NOT NULL DEFAULT '[]',
image_url text,
expiry_date text,
notes text,
intake_reminders_enabled integer NOT NULL DEFAULT 0,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS user_settings (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL UNIQUE,
email_enabled integer NOT NULL DEFAULT 0,
notification_email text,
email_stock_reminders integer NOT NULL DEFAULT 1,
email_intake_reminders integer NOT NULL DEFAULT 1,
shoutrrr_enabled integer NOT NULL DEFAULT 0,
shoutrrr_url text,
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
reminder_days_before integer NOT NULL DEFAULT 7,
repeat_daily_reminders integer NOT NULL DEFAULT 0,
low_stock_days integer NOT NULL DEFAULT 30,
normal_stock_days integer NOT NULL DEFAULT 90,
high_stock_days integer NOT NULL DEFAULT 180,
expiry_warning_days integer NOT NULL DEFAULT 90,
language text NOT NULL DEFAULT 'en',
stock_calculation_mode text NOT NULL DEFAULT 'automatic',
last_auto_email_sent text,
last_notification_type text,
last_notification_channel text,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS refresh_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token_id text NOT NULL UNIQUE,
expires_at integer NOT NULL,
rotated_at integer,
revoked integer NOT NULL DEFAULT 0,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS share_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token text NOT NULL UNIQUE,
taken_by text NOT NULL,
schedule_days integer NOT NULL DEFAULT 30,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
expires_at integer,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS dose_tracking (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
dose_id text NOT NULL,
taken_at integer NOT NULL DEFAULT (strftime('%s','now')),
marked_by text,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
];
}
/** Run table creation migrations on a client */ /** Run table creation migrations on a client */
export async function runTableMigrations(client: Client): Promise<{ success: boolean; errors: string[] }> { export async function runTableMigrations(client: Client): Promise<{ success: boolean; errors: string[] }> {
+5 -99
View File
@@ -2,6 +2,7 @@ import { createClient, Client } from "@libsql/client";
import dotenv from "dotenv"; import dotenv from "dotenv";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { getTableCreationSQL } from "./schema-sql";
dotenv.config({ path: process.env.DOTENV_PATH || ".env" }); dotenv.config({ path: process.env.DOTENV_PATH || ".env" });
@@ -9,101 +10,8 @@ dotenv.config({ path: process.env.DOTENV_PATH || ".env" });
// Exported utility functions for testing // Exported utility functions for testing
// ============================================================================= // =============================================================================
/** Get the full migration SQL string */ /** Get the full migration SQL string (re-exported from schema-sql) */
export function getMigrationSQL(): string { export { getTableCreationSQL };
return `
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
password_hash text,
avatar_url text,
auth_provider text NOT NULL DEFAULT 'local',
oidc_subject text,
is_active integer NOT NULL DEFAULT 1,
last_login_at integer,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
);
CREATE TABLE IF NOT EXISTS medications (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
name text NOT NULL,
generic_name text,
taken_by_json text NOT NULL DEFAULT '[]',
pack_count integer NOT NULL DEFAULT 1,
blisters_per_pack integer NOT NULL DEFAULT 1,
pills_per_blister integer NOT NULL DEFAULT 1,
loose_tablets integer NOT NULL DEFAULT 0,
pill_weight_mg integer,
usage_json text NOT NULL DEFAULT '[]',
every_json text NOT NULL DEFAULT '[]',
start_json text NOT NULL DEFAULT '[]',
image_url text,
expiry_date text,
notes text,
intake_reminders_enabled integer NOT NULL DEFAULT 0,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS user_settings (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL UNIQUE,
email_enabled integer NOT NULL DEFAULT 0,
notification_email text,
email_stock_reminders integer NOT NULL DEFAULT 1,
email_intake_reminders integer NOT NULL DEFAULT 1,
shoutrrr_enabled integer NOT NULL DEFAULT 0,
shoutrrr_url text,
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
reminder_days_before integer NOT NULL DEFAULT 7,
repeat_daily_reminders integer NOT NULL DEFAULT 0,
low_stock_days integer NOT NULL DEFAULT 30,
normal_stock_days integer NOT NULL DEFAULT 90,
high_stock_days integer NOT NULL DEFAULT 180,
language text NOT NULL DEFAULT 'en',
stock_calculation_mode text NOT NULL DEFAULT 'automatic',
last_auto_email_sent text,
last_notification_type text,
last_notification_channel text,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS refresh_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token_id text NOT NULL UNIQUE,
expires_at integer NOT NULL,
rotated_at integer,
revoked integer NOT NULL DEFAULT 0,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS share_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token text NOT NULL UNIQUE,
taken_by text NOT NULL,
schedule_days integer NOT NULL DEFAULT 30,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
expires_at integer,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS dose_tracking (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
dose_id text NOT NULL,
taken_at integer NOT NULL DEFAULT (strftime('%s','now')),
marked_by text,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`;
}
/** Split SQL string into individual statements */ /** Split SQL string into individual statements */
export function splitSQLStatements(sql: string): string[] { export function splitSQLStatements(sql: string): string[] {
@@ -112,8 +20,7 @@ export function splitSQLStatements(sql: string): string[] {
/** Execute migration statements on a client */ /** Execute migration statements on a client */
export async function executeMigration(client: Client): Promise<{ success: boolean; executed: number; errors: string[] }> { export async function executeMigration(client: Client): Promise<{ success: boolean; executed: number; errors: string[] }> {
const sql = getMigrationSQL(); const statements = getTableCreationSQL();
const statements = splitSQLStatements(sql);
const errors: string[] = []; const errors: string[] = [];
let executed = 0; let executed = 0;
@@ -150,8 +57,7 @@ async function main() {
const client = createClient({ url }); const client = createClient({ url });
const sql = getMigrationSQL(); const statements = getTableCreationSQL();
const statements = splitSQLStatements(sql);
for (const stmt of statements) { for (const stmt of statements) {
console.log("Executing:", getStatementPreview(stmt)); console.log("Executing:", getStatementPreview(stmt));
+99
View File
@@ -0,0 +1,99 @@
/**
* Shared SQL table creation statements for database initialization.
* Used by client.ts, migrate.ts, and test setup to avoid duplication.
*/
/**
* Get all SQL table creation statements as an array.
* Each statement creates a table if it doesn't exist.
*/
export function getTableCreationSQL(): string[] {
return [
`CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
password_hash text,
avatar_url text,
auth_provider text NOT NULL DEFAULT 'local',
oidc_subject text,
is_active integer NOT NULL DEFAULT 1,
last_login_at integer,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
)`,
`CREATE TABLE IF NOT EXISTS medications (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
name text NOT NULL,
generic_name text,
taken_by_json text NOT NULL DEFAULT '[]',
pack_count integer NOT NULL DEFAULT 1,
blisters_per_pack integer NOT NULL DEFAULT 1,
pills_per_blister integer NOT NULL DEFAULT 1,
loose_tablets integer NOT NULL DEFAULT 0,
pill_weight_mg integer,
usage_json text NOT NULL DEFAULT '[]',
every_json text NOT NULL DEFAULT '[]',
start_json text NOT NULL DEFAULT '[]',
image_url text,
expiry_date text,
notes text,
intake_reminders_enabled integer NOT NULL DEFAULT 0,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS user_settings (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL UNIQUE,
email_enabled integer NOT NULL DEFAULT 0,
notification_email text,
email_stock_reminders integer NOT NULL DEFAULT 1,
email_intake_reminders integer NOT NULL DEFAULT 1,
shoutrrr_enabled integer NOT NULL DEFAULT 0,
shoutrrr_url text,
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
reminder_days_before integer NOT NULL DEFAULT 7,
repeat_daily_reminders integer NOT NULL DEFAULT 0,
low_stock_days integer NOT NULL DEFAULT 30,
normal_stock_days integer NOT NULL DEFAULT 90,
high_stock_days integer NOT NULL DEFAULT 180,
expiry_warning_days integer NOT NULL DEFAULT 90,
language text NOT NULL DEFAULT 'en',
stock_calculation_mode text NOT NULL DEFAULT 'automatic',
last_auto_email_sent text,
last_notification_type text,
last_notification_channel text,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS refresh_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token_id text NOT NULL UNIQUE,
expires_at integer NOT NULL,
rotated_at integer,
revoked integer NOT NULL DEFAULT 0,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS share_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token text NOT NULL UNIQUE,
taken_by text NOT NULL,
schedule_days integer NOT NULL DEFAULT 30,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
expires_at integer,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS dose_tracking (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
dose_id text NOT NULL,
taken_at integer NOT NULL DEFAULT (strftime('%s','now')),
marked_by text,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
];
}
+16 -16
View File
@@ -17,28 +17,29 @@ import {
// Import the exported utility functions from migrate.ts // Import the exported utility functions from migrate.ts
import { import {
getMigrationSQL, getTableCreationSQL as getTableCreationSQLFromMigrate,
splitSQLStatements, splitSQLStatements,
executeMigration, executeMigration,
getStatementPreview, getStatementPreview,
} from "../db/migrate.js"; } from "../db/migrate.js";
describe("Migration Script Utilities", () => { describe("Migration Script Utilities", () => {
describe("getMigrationSQL", () => { describe("getTableCreationSQL", () => {
it("should return a non-empty SQL string", () => { it("should return a non-empty array of SQL statements", () => {
const sql = getMigrationSQL(); const statements = getTableCreationSQL();
expect(typeof sql).toBe("string"); expect(Array.isArray(statements)).toBe(true);
expect(sql.length).toBeGreaterThan(100); expect(statements.length).toBeGreaterThan(0);
}); });
it("should contain all table definitions", () => { it("should contain all table definitions", () => {
const sql = getMigrationSQL(); const statements = getTableCreationSQL();
expect(sql).toContain("CREATE TABLE IF NOT EXISTS users"); const allSQL = statements.join(" ");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS medications"); expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS users");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS user_settings"); expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS medications");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS refresh_tokens"); expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS user_settings");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS share_tokens"); expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS refresh_tokens");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS dose_tracking"); expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS share_tokens");
expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS dose_tracking");
}); });
}); });
@@ -61,9 +62,8 @@ describe("Migration Script Utilities", () => {
expect(statements).toHaveLength(2); expect(statements).toHaveLength(2);
}); });
it("should split migration SQL into 6 statements", () => { it("should handle getTableCreationSQL output correctly", () => {
const sql = getMigrationSQL(); const statements = getTableCreationSQL();
const statements = splitSQLStatements(sql);
expect(statements).toHaveLength(6); expect(statements).toHaveLength(6);
}); });
+2 -88
View File
@@ -10,6 +10,7 @@ import fastifyMultipart from "@fastify/multipart";
import { createClient, Client } from "@libsql/client"; import { createClient, Client } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql"; import { drizzle } from "drizzle-orm/libsql";
import { beforeAll, afterAll, beforeEach } from "vitest"; import { beforeAll, afterAll, beforeEach } from "vitest";
import { getTableCreationSQL } from "../db/schema-sql";
// Type for our test database // Type for our test database
export type TestDb = ReturnType<typeof drizzle>; export type TestDb = ReturnType<typeof drizzle>;
@@ -63,94 +64,7 @@ export async function buildTestApp(): Promise<TestContext> {
* Create test database schema * Create test database schema
*/ */
async function runTestMigrations(client: Client): Promise<void> { async function runTestMigrations(client: Client): Promise<void> {
const tableCreations = [ const tableCreations = getTableCreationSQL();
`CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
password_hash text,
avatar_url text,
auth_provider text NOT NULL DEFAULT 'local',
oidc_subject text,
is_active integer NOT NULL DEFAULT 1,
last_login_at integer,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
)`,
`CREATE TABLE IF NOT EXISTS medications (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
name text NOT NULL,
generic_name text,
taken_by_json text NOT NULL DEFAULT '[]',
pack_count integer NOT NULL DEFAULT 1,
blisters_per_pack integer NOT NULL DEFAULT 1,
pills_per_blister integer NOT NULL DEFAULT 1,
loose_tablets integer NOT NULL DEFAULT 0,
pill_weight_mg integer,
usage_json text NOT NULL DEFAULT '[]',
every_json text NOT NULL DEFAULT '[]',
start_json text NOT NULL DEFAULT '[]',
image_url text,
expiry_date text,
notes text,
intake_reminders_enabled integer NOT NULL DEFAULT 0,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS user_settings (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL UNIQUE,
email_enabled integer NOT NULL DEFAULT 0,
notification_email text,
email_stock_reminders integer NOT NULL DEFAULT 1,
email_intake_reminders integer NOT NULL DEFAULT 1,
shoutrrr_enabled integer NOT NULL DEFAULT 0,
shoutrrr_url text,
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
reminder_days_before integer NOT NULL DEFAULT 7,
repeat_daily_reminders integer NOT NULL DEFAULT 0,
low_stock_days integer NOT NULL DEFAULT 30,
normal_stock_days integer NOT NULL DEFAULT 90,
high_stock_days integer NOT NULL DEFAULT 180,
expiry_warning_days integer NOT NULL DEFAULT 90,
language text NOT NULL DEFAULT 'en',
stock_calculation_mode text NOT NULL DEFAULT 'automatic',
last_auto_email_sent text,
last_notification_type text,
last_notification_channel text,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS refresh_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token_id text NOT NULL UNIQUE,
expires_at integer NOT NULL,
rotated_at integer,
revoked integer NOT NULL DEFAULT 0,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS share_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token text NOT NULL UNIQUE,
taken_by text NOT NULL,
schedule_days integer NOT NULL DEFAULT 30,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
expires_at integer,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS dose_tracking (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
dose_id text NOT NULL,
taken_at integer NOT NULL DEFAULT (strftime('%s','now')),
marked_by text,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
];
for (const sql of tableCreations) { for (const sql of tableCreations) {
await client.execute(sql); await client.execute(sql);
+39 -45
View File
@@ -358,15 +358,12 @@ function AppContent() {
setScheduleDays(storedDays ? Number(storedDays) : 30); setScheduleDays(storedDays ? Number(storedDays) : 30);
// Load manually collapsed/expanded days from localStorage // Load manually collapsed/expanded days from localStorage
const storedCollapsed = localStorage.getItem(userStorageKey(user.id, "collapsedDays")); const { collapsed, expanded } = loadCollapsedDaysFromStorage(
const storedExpanded = localStorage.getItem(userStorageKey(user.id, "expandedDays")); userStorageKey(user.id, "collapsedDays"),
try { userStorageKey(user.id, "expandedDays")
setManuallyCollapsedDays(storedCollapsed ? new Set(JSON.parse(storedCollapsed)) : new Set()); );
setManuallyExpandedDays(storedExpanded ? new Set(JSON.parse(storedExpanded)) : new Set()); setManuallyCollapsedDays(collapsed);
} catch { setManuallyExpandedDays(expanded);
setManuallyCollapsedDays(new Set());
setManuallyExpandedDays(new Set());
}
} }
}, [user?.id]); }, [user?.id]);
@@ -2895,22 +2892,36 @@ function toIsoString(value: string) {
return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString(); return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString();
} }
function pad2(n: number): string {
return String(n).padStart(2, '0');
}
function loadCollapsedDaysFromStorage(collapsedKey: string, expandedKey: string): { collapsed: Set<string>; expanded: Set<string> } {
const storedCollapsed = localStorage.getItem(collapsedKey);
const storedExpanded = localStorage.getItem(expandedKey);
try {
return {
collapsed: storedCollapsed ? new Set(JSON.parse(storedCollapsed)) : new Set(),
expanded: storedExpanded ? new Set(JSON.parse(storedExpanded)) : new Set()
};
} catch {
return {
collapsed: new Set(),
expanded: new Set()
};
}
}
function toDateValue(date: Date | string): string { function toDateValue(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date; const d = typeof date === 'string' ? new Date(date) : date;
if (Number.isNaN(d.getTime())) { const fallback = Number.isNaN(d.getTime()) ? new Date() : d;
const now = new Date(); return `${fallback.getFullYear()}-${pad2(fallback.getMonth() + 1)}-${pad2(fallback.getDate())}`;
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
}
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
} }
function toTimeValue(date: Date | string): string { function toTimeValue(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date; const d = typeof date === 'string' ? new Date(date) : date;
if (Number.isNaN(d.getTime())) { const fallback = Number.isNaN(d.getTime()) ? new Date() : d;
const now = new Date(); return `${pad2(fallback.getHours())}:${pad2(fallback.getMinutes())}`;
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
}
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
} }
function combineDateAndTime(dateStr: string, timeStr: string): string { function combineDateAndTime(dateStr: string, timeStr: string): string {
@@ -2920,23 +2931,9 @@ function combineDateAndTime(dateStr: string, timeStr: string): string {
function toInputValue(value: string) { function toInputValue(value: string) {
const date = new Date(value); const date = new Date(value);
if (Number.isNaN(date.getTime())) { const d = Number.isNaN(date.getTime()) ? new Date() : date;
// Return current local time in datetime-local format // Use existing helper functions to avoid duplication
const now = new Date(); return `${toDateValue(d)}T${toTimeValue(d)}`;
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
// Convert to local time format for datetime-local input
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
} }
function formatDateTime(value: string, locale: string) { function formatDateTime(value: string, locale: string) {
@@ -3449,15 +3446,12 @@ function SharedSchedule() {
// Load collapsed/expanded state from localStorage // Load collapsed/expanded state from localStorage
useEffect(() => { useEffect(() => {
if (token && typeof window !== "undefined") { if (token && typeof window !== "undefined") {
const storedCollapsed = localStorage.getItem(`share_${token}_collapsedDays`); const { collapsed, expanded } = loadCollapsedDaysFromStorage(
const storedExpanded = localStorage.getItem(`share_${token}_expandedDays`); `share_${token}_collapsedDays`,
try { `share_${token}_expandedDays`
setManuallyCollapsedDays(storedCollapsed ? new Set(JSON.parse(storedCollapsed)) : new Set()); );
setManuallyExpandedDays(storedExpanded ? new Set(JSON.parse(storedExpanded)) : new Set()); setManuallyCollapsedDays(collapsed);
} catch { setManuallyExpandedDays(expanded);
setManuallyCollapsedDays(new Set());
setManuallyExpandedDays(new Set());
}
} }
}, [token]); }, [token]);