feat: migrate taken_by to taken_by_json for multi-person support

- Added `taken_by_json` column to `medications` table to store an array of names.
- Updated migration scripts to convert existing `taken_by` data into JSON format.
- Modified backend routes to handle the new `taken_by_json` structure, including parsing and filtering logic.
- Updated frontend to support multi-value input for "Taken By" using tags.
- Adjusted validation and state management for the new array format in forms.
- Enhanced UI for displaying multiple names and added autocomplete suggestions for input.
- Updated translations for input placeholders to reflect new functionality.
- Added CSS styles for tag input components.
This commit is contained in:
Daniel Volz
2025-12-28 18:22:32 +01:00
parent abffd66e9c
commit 4a6aab338f
11 changed files with 292 additions and 77 deletions
+39 -20
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import { randomBytes } from "crypto";
import { db } from "../db/client.js";
import { medications, shareTokens, userSettings, users } from "../db/schema.js";
import { eq, and } from "drizzle-orm";
import { eq, and, sql } from "drizzle-orm";
import { requireAuth, optionalAuth, getAnonymousUserId } from "../plugins/auth.js";
import { env } from "../plugins/env.js";
import type { AuthUser } from "../types/fastify.js";
@@ -35,6 +35,17 @@ async function getUserId(request: any, reply: any): Promise<number> {
return authUser.id;
}
// Helper to parse takenByJson
function parseTakenByJson(takenByJson: string | null | undefined): string[] {
if (!takenByJson) return [];
try {
const parsed = JSON.parse(takenByJson);
return Array.isArray(parsed) ? parsed.filter((s: unknown) => typeof s === "string" && s.trim()) : [];
} catch {
return [];
}
}
// =============================================================================
// Share Routes
// =============================================================================
@@ -70,13 +81,15 @@ export async function shareRoutes(app: FastifyInstance) {
// Get user settings for stock thresholds
const [settings] = await db.select().from(userSettings).where(eq(userSettings.userId, share.userId));
// Get medications for this user filtered by takenBy
const meds = await db.select().from(medications).where(
and(
eq(medications.userId, share.userId),
eq(medications.takenBy, share.takenBy)
)
);
// Get medications for this user filtered by takenBy (search in JSON array)
// Use SQLite JSON function to check if takenBy is in the array
const allMeds = await db.select().from(medications).where(eq(medications.userId, share.userId));
// Filter medications where takenByJson array contains the share.takenBy value
const meds = allMeds.filter((med) => {
const takenByArray = parseTakenByJson(med.takenByJson);
return takenByArray.includes(share.takenBy);
});
// Parse blisters and build schedule data
const medicationsWithBlisters = meds.map((med) => {
@@ -135,15 +148,14 @@ export async function shareRoutes(app: FastifyInstance) {
const { takenBy, scheduleDays } = parsed.data;
// Check if user has medications for this takenBy
const [existingMed] = await db.select().from(medications).where(
and(
eq(medications.userId, userId),
eq(medications.takenBy, takenBy)
)
);
// Check if user has medications for this takenBy (search in JSON array)
const allMeds = await db.select().from(medications).where(eq(medications.userId, userId));
const medsForPerson = allMeds.filter((med) => {
const takenByArray = parseTakenByJson(med.takenByJson);
return takenByArray.includes(takenBy);
});
if (!existingMed) {
if (medsForPerson.length === 0) {
return reply.status(400).send({
error: "No medications found for this person",
code: "NO_MEDICATIONS",
@@ -182,14 +194,21 @@ export async function shareRoutes(app: FastifyInstance) {
async (request, reply) => {
const userId = await getUserId(request, reply);
// Get all unique takenBy values for this user
const meds = await db.select({ takenBy: medications.takenBy })
// Get all unique takenBy values for this user (from JSON arrays)
const meds = await db.select({ takenByJson: medications.takenByJson })
.from(medications)
.where(eq(medications.userId, userId));
const uniquePeople = [...new Set(meds.map((m) => m.takenBy).filter(Boolean))] as string[];
// Collect all unique person names from all takenByJson arrays
const allPeople = new Set<string>();
for (const med of meds) {
const takenByArray = parseTakenByJson(med.takenByJson);
for (const person of takenByArray) {
if (person) allPeople.add(person);
}
}
return { people: uniquePeople };
return { people: [...allPeople].sort() };
}
);
}