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:
@@ -3,6 +3,7 @@ import { drizzle } from "drizzle-orm/libsql";
|
||||
import { existsSync, mkdirSync, accessSync, constants, statSync, writeFileSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import dotenv from "dotenv";
|
||||
import { getTableCreationSQL } from "./schema-sql";
|
||||
|
||||
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 */
|
||||
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
|
||||
)`,
|
||||
];
|
||||
}
|
||||
/** Get the SQL statements for creating all tables (re-exported from schema-sql) */
|
||||
export { getTableCreationSQL } from "./schema-sql";
|
||||
|
||||
/** Run table creation migrations on a client */
|
||||
export async function runTableMigrations(client: Client): Promise<{ success: boolean; errors: string[] }> {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createClient, Client } from "@libsql/client";
|
||||
import dotenv from "dotenv";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { getTableCreationSQL } from "./schema-sql";
|
||||
|
||||
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
|
||||
// =============================================================================
|
||||
|
||||
/** Get the full migration SQL string */
|
||||
export function getMigrationSQL(): 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,
|
||||
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
|
||||
);
|
||||
`;
|
||||
}
|
||||
/** Get the full migration SQL string (re-exported from schema-sql) */
|
||||
export { getTableCreationSQL };
|
||||
|
||||
/** Split SQL string into individual statements */
|
||||
export function splitSQLStatements(sql: string): string[] {
|
||||
@@ -112,8 +20,7 @@ export function splitSQLStatements(sql: string): string[] {
|
||||
|
||||
/** Execute migration statements on a client */
|
||||
export async function executeMigration(client: Client): Promise<{ success: boolean; executed: number; errors: string[] }> {
|
||||
const sql = getMigrationSQL();
|
||||
const statements = splitSQLStatements(sql);
|
||||
const statements = getTableCreationSQL();
|
||||
const errors: string[] = [];
|
||||
let executed = 0;
|
||||
|
||||
@@ -150,8 +57,7 @@ async function main() {
|
||||
|
||||
const client = createClient({ url });
|
||||
|
||||
const sql = getMigrationSQL();
|
||||
const statements = splitSQLStatements(sql);
|
||||
const statements = getTableCreationSQL();
|
||||
|
||||
for (const stmt of statements) {
|
||||
console.log("Executing:", getStatementPreview(stmt));
|
||||
|
||||
@@ -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
|
||||
)`,
|
||||
];
|
||||
}
|
||||
@@ -17,28 +17,29 @@ import {
|
||||
|
||||
// Import the exported utility functions from migrate.ts
|
||||
import {
|
||||
getMigrationSQL,
|
||||
getTableCreationSQL as getTableCreationSQLFromMigrate,
|
||||
splitSQLStatements,
|
||||
executeMigration,
|
||||
getStatementPreview,
|
||||
} from "../db/migrate.js";
|
||||
|
||||
describe("Migration Script Utilities", () => {
|
||||
describe("getMigrationSQL", () => {
|
||||
it("should return a non-empty SQL string", () => {
|
||||
const sql = getMigrationSQL();
|
||||
expect(typeof sql).toBe("string");
|
||||
expect(sql.length).toBeGreaterThan(100);
|
||||
describe("getTableCreationSQL", () => {
|
||||
it("should return a non-empty array of SQL statements", () => {
|
||||
const statements = getTableCreationSQL();
|
||||
expect(Array.isArray(statements)).toBe(true);
|
||||
expect(statements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should contain all table definitions", () => {
|
||||
const sql = getMigrationSQL();
|
||||
expect(sql).toContain("CREATE TABLE IF NOT EXISTS users");
|
||||
expect(sql).toContain("CREATE TABLE IF NOT EXISTS medications");
|
||||
expect(sql).toContain("CREATE TABLE IF NOT EXISTS user_settings");
|
||||
expect(sql).toContain("CREATE TABLE IF NOT EXISTS refresh_tokens");
|
||||
expect(sql).toContain("CREATE TABLE IF NOT EXISTS share_tokens");
|
||||
expect(sql).toContain("CREATE TABLE IF NOT EXISTS dose_tracking");
|
||||
const statements = getTableCreationSQL();
|
||||
const allSQL = statements.join(" ");
|
||||
expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS users");
|
||||
expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS medications");
|
||||
expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS user_settings");
|
||||
expect(allSQL).toContain("CREATE TABLE IF NOT EXISTS refresh_tokens");
|
||||
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);
|
||||
});
|
||||
|
||||
it("should split migration SQL into 6 statements", () => {
|
||||
const sql = getMigrationSQL();
|
||||
const statements = splitSQLStatements(sql);
|
||||
it("should handle getTableCreationSQL output correctly", () => {
|
||||
const statements = getTableCreationSQL();
|
||||
expect(statements).toHaveLength(6);
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import fastifyMultipart from "@fastify/multipart";
|
||||
import { createClient, Client } from "@libsql/client";
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
import { beforeAll, afterAll, beforeEach } from "vitest";
|
||||
import { getTableCreationSQL } from "../db/schema-sql";
|
||||
|
||||
// Type for our test database
|
||||
export type TestDb = ReturnType<typeof drizzle>;
|
||||
@@ -63,94 +64,7 @@ export async function buildTestApp(): Promise<TestContext> {
|
||||
* Create test database schema
|
||||
*/
|
||||
async function runTestMigrations(client: Client): Promise<void> {
|
||||
const tableCreations = [
|
||||
`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
|
||||
)`,
|
||||
];
|
||||
const tableCreations = getTableCreationSQL();
|
||||
|
||||
for (const sql of tableCreations) {
|
||||
await client.execute(sql);
|
||||
|
||||
+39
-45
@@ -358,15 +358,12 @@ function AppContent() {
|
||||
setScheduleDays(storedDays ? Number(storedDays) : 30);
|
||||
|
||||
// Load manually collapsed/expanded days from localStorage
|
||||
const storedCollapsed = localStorage.getItem(userStorageKey(user.id, "collapsedDays"));
|
||||
const storedExpanded = localStorage.getItem(userStorageKey(user.id, "expandedDays"));
|
||||
try {
|
||||
setManuallyCollapsedDays(storedCollapsed ? new Set(JSON.parse(storedCollapsed)) : new Set());
|
||||
setManuallyExpandedDays(storedExpanded ? new Set(JSON.parse(storedExpanded)) : new Set());
|
||||
} catch {
|
||||
setManuallyCollapsedDays(new Set());
|
||||
setManuallyExpandedDays(new Set());
|
||||
}
|
||||
const { collapsed, expanded } = loadCollapsedDaysFromStorage(
|
||||
userStorageKey(user.id, "collapsedDays"),
|
||||
userStorageKey(user.id, "expandedDays")
|
||||
);
|
||||
setManuallyCollapsedDays(collapsed);
|
||||
setManuallyExpandedDays(expanded);
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
@@ -2895,22 +2892,36 @@ function toIsoString(value: string) {
|
||||
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 {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
const now = new Date();
|
||||
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')}`;
|
||||
const fallback = Number.isNaN(d.getTime()) ? new Date() : d;
|
||||
return `${fallback.getFullYear()}-${pad2(fallback.getMonth() + 1)}-${pad2(fallback.getDate())}`;
|
||||
}
|
||||
|
||||
function toTimeValue(date: Date | string): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
const now = new Date();
|
||||
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')}`;
|
||||
const fallback = Number.isNaN(d.getTime()) ? new Date() : d;
|
||||
return `${pad2(fallback.getHours())}:${pad2(fallback.getMinutes())}`;
|
||||
}
|
||||
|
||||
function combineDateAndTime(dateStr: string, timeStr: string): string {
|
||||
@@ -2920,23 +2931,9 @@ function combineDateAndTime(dateStr: string, timeStr: string): string {
|
||||
|
||||
function toInputValue(value: string) {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
// Return current local time in datetime-local format
|
||||
const now = new Date();
|
||||
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}`;
|
||||
const d = Number.isNaN(date.getTime()) ? new Date() : date;
|
||||
// Use existing helper functions to avoid duplication
|
||||
return `${toDateValue(d)}T${toTimeValue(d)}`;
|
||||
}
|
||||
|
||||
function formatDateTime(value: string, locale: string) {
|
||||
@@ -3449,15 +3446,12 @@ function SharedSchedule() {
|
||||
// Load collapsed/expanded state from localStorage
|
||||
useEffect(() => {
|
||||
if (token && typeof window !== "undefined") {
|
||||
const storedCollapsed = localStorage.getItem(`share_${token}_collapsedDays`);
|
||||
const storedExpanded = localStorage.getItem(`share_${token}_expandedDays`);
|
||||
try {
|
||||
setManuallyCollapsedDays(storedCollapsed ? new Set(JSON.parse(storedCollapsed)) : new Set());
|
||||
setManuallyExpandedDays(storedExpanded ? new Set(JSON.parse(storedExpanded)) : new Set());
|
||||
} catch {
|
||||
setManuallyCollapsedDays(new Set());
|
||||
setManuallyExpandedDays(new Set());
|
||||
}
|
||||
const { collapsed, expanded } = loadCollapsedDaysFromStorage(
|
||||
`share_${token}_collapsedDays`,
|
||||
`share_${token}_expandedDays`
|
||||
);
|
||||
setManuallyCollapsedDays(collapsed);
|
||||
setManuallyExpandedDays(expanded);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user