Add script to build and push Docker images to registry
- Introduced `push-images.sh` script for building and pushing backend and frontend images. - Added functionality to select or input image tags. - Integrated environment variable support for registry configuration. - Implemented prompts for user confirmation before building and pushing images. - Updated `docker-compose.prod.yml` with new image tags after pushing.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
email text NOT NULL UNIQUE,
|
||||
password_hash text NOT NULL,
|
||||
role text NOT NULL DEFAULT 'user',
|
||||
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,
|
||||
name text NOT NULL UNIQUE,
|
||||
count integer NOT NULL DEFAULT 0,
|
||||
strips integer NOT NULL DEFAULT 0,
|
||||
pack_count integer NOT NULL DEFAULT 1,
|
||||
strips_per_pack integer NOT NULL DEFAULT 1,
|
||||
tabs_per_strip integer NOT NULL DEFAULT 1,
|
||||
loose_tablets integer NOT NULL DEFAULT 0,
|
||||
usage_json text NOT NULL DEFAULT '[]',
|
||||
every_json text NOT NULL DEFAULT '[]',
|
||||
start_json text NOT NULL DEFAULT '[]',
|
||||
strip_size integer NOT NULL DEFAULT 1,
|
||||
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
|
||||
);
|
||||
|
||||
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 settings (
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
smtp_host text,
|
||||
smtp_port integer,
|
||||
smtp_user text,
|
||||
smtp_pass_encrypted text,
|
||||
smtp_from text,
|
||||
smtp_secure integer NOT NULL DEFAULT 0,
|
||||
emails_per_day integer NOT NULL DEFAULT 3,
|
||||
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE medications ADD COLUMN strips integer NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,15 @@
|
||||
ALTER TABLE medications ADD COLUMN pack_count integer NOT NULL DEFAULT 1;
|
||||
ALTER TABLE medications ADD COLUMN strips_per_pack integer NOT NULL DEFAULT 1;
|
||||
ALTER TABLE medications ADD COLUMN tabs_per_strip integer NOT NULL DEFAULT 1;
|
||||
ALTER TABLE medications ADD COLUMN loose_tablets integer NOT NULL DEFAULT 0;
|
||||
|
||||
-- Backfill from previous fields where possible
|
||||
UPDATE medications
|
||||
SET
|
||||
pack_count = COALESCE(pack_count, 1),
|
||||
strips_per_pack = CASE WHEN strips IS NOT NULL THEN strips ELSE 1 END,
|
||||
tabs_per_strip = CASE WHEN strip_size IS NOT NULL THEN strip_size ELSE 1 END,
|
||||
loose_tablets = CASE
|
||||
WHEN strip_size IS NOT NULL AND strips IS NOT NULL THEN MAX(count - (strips * strip_size), 0)
|
||||
ELSE 0
|
||||
END;
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"entries": [
|
||||
{ "idx": 0, "version": 1, "when": 1734633120, "tag": "0000_init", "breakpoint": false },
|
||||
{ "idx": 1, "version": 1, "when": 1734633121, "tag": "0001_add_strips", "breakpoint": false },
|
||||
{ "idx": 2, "version": 1, "when": 1734633122, "tag": "0002_pack_inventory", "breakpoint": false }
|
||||
]
|
||||
}
|
||||
@@ -14,6 +14,11 @@ export const medications = sqliteTable("medications", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name", { length: 100 }).notNull().unique(),
|
||||
count: integer("count").notNull().default(0),
|
||||
strips: integer("strips").notNull().default(0),
|
||||
packCount: integer("pack_count").notNull().default(1),
|
||||
stripsPerPack: integer("strips_per_pack").notNull().default(1),
|
||||
tabsPerStrip: integer("tabs_per_strip").notNull().default(1),
|
||||
looseTablets: integer("loose_tablets").notNull().default(0),
|
||||
usageJson: text("usage_json").notNull().default("[]"),
|
||||
everyJson: text("every_json").notNull().default("[]"),
|
||||
startJson: text("start_json").notNull().default("[]"),
|
||||
|
||||
@@ -12,8 +12,11 @@ const sliceSchema = z.object({
|
||||
|
||||
const medicationSchema = z.object({
|
||||
name: z.string().trim().min(1).max(100),
|
||||
count: z.number().int().min(0),
|
||||
stripSize: z.number().int().min(1),
|
||||
packCount: z.number().int().min(0).default(1),
|
||||
stripsPerPack: z.number().int().min(1).default(1),
|
||||
tabsPerStrip: z.number().int().min(1).default(1),
|
||||
looseTablets: z.number().int().min(0).default(0),
|
||||
// count will be derived on the backend
|
||||
slices: z.array(sliceSchema).min(1).max(12),
|
||||
});
|
||||
|
||||
@@ -44,7 +47,12 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
count: row.count,
|
||||
strips: row.strips,
|
||||
stripSize: row.stripSize,
|
||||
packCount: row.packCount ?? 1,
|
||||
stripsPerPack: row.stripsPerPack ?? row.strips ?? 1,
|
||||
tabsPerStrip: row.tabsPerStrip ?? row.stripSize ?? 1,
|
||||
looseTablets: row.looseTablets ?? 0,
|
||||
slices: parseSlices(row),
|
||||
updatedAt: row.updatedAt,
|
||||
}));
|
||||
@@ -54,21 +62,40 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
const parsed = medicationSchema.safeParse(req.body);
|
||||
if (!parsed.success) return reply.status(400).send(parsed.error.format());
|
||||
|
||||
const { name, count, stripSize, slices } = parsed.data;
|
||||
const { name, packCount, stripsPerPack, tabsPerStrip, looseTablets, slices } = parsed.data;
|
||||
const usageJson = JSON.stringify(slices.map((s) => s.usage));
|
||||
const everyJson = JSON.stringify(slices.map((s) => s.every));
|
||||
const startJson = JSON.stringify(slices.map((s) => s.start));
|
||||
|
||||
const derivedCount = deriveTotalTablets(packCount, stripsPerPack, tabsPerStrip, looseTablets);
|
||||
|
||||
const [inserted] = await db
|
||||
.insert(medications)
|
||||
.values({ name, count, stripSize, usageJson, everyJson, startJson })
|
||||
.values({
|
||||
name,
|
||||
count: derivedCount,
|
||||
strips: stripsPerPack,
|
||||
stripSize: tabsPerStrip,
|
||||
packCount,
|
||||
stripsPerPack,
|
||||
tabsPerStrip,
|
||||
looseTablets,
|
||||
usageJson,
|
||||
everyJson,
|
||||
startJson,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return {
|
||||
id: inserted.id,
|
||||
name: inserted.name,
|
||||
count: inserted.count,
|
||||
strips: inserted.strips,
|
||||
stripSize: inserted.stripSize,
|
||||
packCount: inserted.packCount,
|
||||
stripsPerPack: inserted.stripsPerPack,
|
||||
tabsPerStrip: inserted.tabsPerStrip,
|
||||
looseTablets: inserted.looseTablets,
|
||||
slices,
|
||||
updatedAt: inserted.updatedAt,
|
||||
};
|
||||
@@ -80,14 +107,29 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
const idNum = Number(req.params.id);
|
||||
if (Number.isNaN(idNum)) return reply.badRequest("Invalid id");
|
||||
|
||||
const { name, count, stripSize, slices } = parsed.data;
|
||||
const { name, packCount, stripsPerPack, tabsPerStrip, looseTablets, slices } = parsed.data;
|
||||
const usageJson = JSON.stringify(slices.map((s) => s.usage));
|
||||
const everyJson = JSON.stringify(slices.map((s) => s.every));
|
||||
const startJson = JSON.stringify(slices.map((s) => s.start));
|
||||
|
||||
const derivedCount = deriveTotalTablets(packCount, stripsPerPack, tabsPerStrip, looseTablets);
|
||||
|
||||
const result = await db
|
||||
.update(medications)
|
||||
.set({ name, count, stripSize, usageJson, everyJson, startJson, updatedAt: new Date() })
|
||||
.set({
|
||||
name,
|
||||
count: derivedCount,
|
||||
strips: stripsPerPack,
|
||||
stripSize: tabsPerStrip,
|
||||
packCount,
|
||||
stripsPerPack,
|
||||
tabsPerStrip,
|
||||
looseTablets,
|
||||
usageJson,
|
||||
everyJson,
|
||||
startJson,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(medications.id, idNum))
|
||||
.returning();
|
||||
|
||||
@@ -97,9 +139,82 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
id: result[0].id,
|
||||
name: result[0].name,
|
||||
count: result[0].count,
|
||||
strips: result[0].strips,
|
||||
stripSize: result[0].stripSize,
|
||||
packCount: result[0].packCount,
|
||||
stripsPerPack: result[0].stripsPerPack,
|
||||
tabsPerStrip: result[0].tabsPerStrip,
|
||||
looseTablets: result[0].looseTablets,
|
||||
slices,
|
||||
updatedAt: result[0].updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
app.delete<{ Params: { id: string } }>("/medications/:id", async (req, reply) => {
|
||||
const idNum = Number(req.params.id);
|
||||
if (Number.isNaN(idNum)) return reply.badRequest("Invalid id");
|
||||
|
||||
const deleted = await db.delete(medications).where(eq(medications.id, idNum)).returning();
|
||||
if (!deleted.length) return reply.notFound();
|
||||
return reply.status(204).send();
|
||||
});
|
||||
|
||||
app.post("/medications/usage", async (req, reply) => {
|
||||
const schema = z.object({ startDate: z.string().datetime(), endDate: z.string().datetime() });
|
||||
const parsed = schema.safeParse(req.body);
|
||||
if (!parsed.success) return reply.status(400).send(parsed.error.format());
|
||||
const { startDate, endDate } = parsed.data;
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end <= start) {
|
||||
return reply.badRequest("Invalid date range");
|
||||
}
|
||||
|
||||
const rows = await db.select().from(medications).orderBy(medications.id);
|
||||
const payload = rows.map((row) => {
|
||||
const slices = parseSlices(row);
|
||||
const usageTotal = calculateUsageInRange(slices, start, end);
|
||||
const tabsPerStrip = row.tabsPerStrip ?? row.stripSize ?? 1;
|
||||
const packCount = row.packCount ?? 1;
|
||||
const stripsPerPack = row.stripsPerPack ?? row.strips ?? 1;
|
||||
const looseTablets = row.looseTablets ?? 0;
|
||||
|
||||
const stripsNeeded = tabsPerStrip > 0 ? Math.ceil(usageTotal / tabsPerStrip) : 0;
|
||||
const stripsAvailable = packCount * stripsPerPack + (tabsPerStrip > 0 ? looseTablets / tabsPerStrip : 0);
|
||||
const enough = stripsAvailable >= stripsNeeded;
|
||||
return {
|
||||
medicationId: row.id,
|
||||
medicationName: row.name,
|
||||
plannerUsage: usageTotal,
|
||||
stripSize: tabsPerStrip,
|
||||
stripsNeeded,
|
||||
stripsAvailable,
|
||||
enough,
|
||||
};
|
||||
});
|
||||
|
||||
return payload;
|
||||
});
|
||||
}
|
||||
|
||||
function calculateUsageInRange(slices: Array<{ usage: number; every: number; start: string }>, start: Date, end: Date) {
|
||||
let total = 0;
|
||||
slices.forEach((slice) => {
|
||||
const sliceStart = new Date(slice.start);
|
||||
if (Number.isNaN(sliceStart.getTime())) return;
|
||||
// iterate occurrences from sliceStart up to end
|
||||
for (let dt = new Date(sliceStart); dt < end; dt.setDate(dt.getDate() + slice.every)) {
|
||||
if (dt >= start && dt < end) total += slice.usage;
|
||||
}
|
||||
});
|
||||
return Number(total.toFixed(2));
|
||||
}
|
||||
|
||||
function deriveTotalTablets(packCount: number, stripsPerPack: number, tabsPerStrip: number, looseTablets: number) {
|
||||
const packs = packCount || 0;
|
||||
const strips = stripsPerPack || 0;
|
||||
const tabs = tabsPerStrip || 1;
|
||||
const loose = looseTablets || 0;
|
||||
const packed = packs * strips * tabs;
|
||||
return packed + loose;
|
||||
}
|
||||
Reference in New Issue
Block a user