add pglite with migration runner, zod schemas, db hook

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 10:53:25 +01:00
parent 2c9e94247e
commit e8e2f8b3e8
6 changed files with 223 additions and 2 deletions
+41
View File
@@ -0,0 +1,41 @@
import { PGlite } from "@electric-sql/pglite"
let _db: PGlite | null = null
export async function getDb(): Promise<PGlite> {
if (!_db) {
_db = new PGlite("idb://therapyfinder")
await runMigrations(_db)
}
return _db
}
async function runMigrations(db: PGlite): Promise<void> {
await db.exec(`
CREATE TABLE IF NOT EXISTS _migrations (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`)
const migrations = import.meta.glob("./migrations/*.sql", {
query: "?raw",
import: "default",
})
const sortedPaths = Object.keys(migrations).sort()
for (const path of sortedPaths) {
const name = path.split("/").pop()!
const result = await db.query(
"SELECT 1 FROM _migrations WHERE name = $1",
[name],
)
if (result.rows.length > 0) continue
const sql = (await migrations[path]()) as string
await db.exec(sql)
await db.query("INSERT INTO _migrations (name) VALUES ($1)", [name])
}
}
+53
View File
@@ -0,0 +1,53 @@
-- User profile (single row per device)
CREATE TABLE nutzer (
id SERIAL PRIMARY KEY,
name TEXT,
plz TEXT,
ort TEXT,
krankenkasse TEXT,
aktueller_schritt TEXT NOT NULL DEFAULT 'neu',
dringlichkeitscode BOOLEAN NOT NULL DEFAULT FALSE,
dringlichkeitscode_datum DATE,
tss_beantragt BOOLEAN NOT NULL DEFAULT FALSE,
tss_beantragt_datum DATE,
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
aktualisiert_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Therapist contacts
CREATE TABLE therapeut (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
adresse TEXT,
plz TEXT,
stadt TEXT,
telefon TEXT,
email TEXT,
website TEXT,
therapieform TEXT,
kassenzulassung TEXT DEFAULT 'gkv',
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Contact attempts
CREATE TABLE kontakt (
id SERIAL PRIMARY KEY,
therapeut_id INTEGER NOT NULL REFERENCES therapeut(id) ON DELETE CASCADE,
datum DATE NOT NULL DEFAULT CURRENT_DATE,
kanal TEXT NOT NULL DEFAULT 'telefon',
ergebnis TEXT NOT NULL DEFAULT 'keine_antwort',
notiz TEXT,
antwort_datum DATE,
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Sprechstunden visits
CREATE TABLE sprechstunde (
id SERIAL PRIMARY KEY,
therapeut_id INTEGER NOT NULL REFERENCES therapeut(id) ON DELETE CASCADE,
datum DATE NOT NULL,
ergebnis TEXT,
diagnose TEXT,
dringlichkeitscode BOOLEAN NOT NULL DEFAULT FALSE,
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
+80
View File
@@ -0,0 +1,80 @@
import { z } from "zod"
export const prozessSchrittEnum = z.enum([
"neu",
"sprechstunde_absolviert",
"diagnose_erhalten",
"tss_beantragt",
"eigensuche",
"antrag_gestellt",
])
export type ProzessSchritt = z.infer<typeof prozessSchrittEnum>
export const kontaktKanalEnum = z.enum([
"telefon",
"email",
"online_formular",
"persoenlich",
])
export type KontaktKanal = z.infer<typeof kontaktKanalEnum>
export const kontaktErgebnisEnum = z.enum([
"keine_antwort",
"absage",
"warteliste",
"zusage",
])
export type KontaktErgebnis = z.infer<typeof kontaktErgebnisEnum>
export const therapieformEnum = z.enum([
"verhaltenstherapie",
"tiefenpsychologisch",
"analytisch",
"systemisch",
])
export type Therapieform = z.infer<typeof therapieformEnum>
export const nutzerSchema = z.object({
id: z.number(),
name: z.string().nullable(),
plz: z.string().nullable(),
ort: z.string().nullable(),
krankenkasse: z.string().nullable(),
aktueller_schritt: prozessSchrittEnum,
dringlichkeitscode: z.boolean(),
dringlichkeitscode_datum: z.string().nullable(),
tss_beantragt: z.boolean(),
tss_beantragt_datum: z.string().nullable(),
})
export const therapeutSchema = z.object({
id: z.number(),
name: z.string(),
adresse: z.string().nullable(),
plz: z.string().nullable(),
stadt: z.string().nullable(),
telefon: z.string().nullable(),
email: z.string().nullable(),
website: z.string().nullable(),
therapieform: z.string().nullable(),
kassenzulassung: z.string().nullable(),
})
export const kontaktSchema = z.object({
id: z.number(),
therapeut_id: z.number(),
datum: z.string(),
kanal: kontaktKanalEnum,
ergebnis: kontaktErgebnisEnum,
notiz: z.string().nullable(),
antwort_datum: z.string().nullable(),
})
export const sprechstundeSchema = z.object({
id: z.number(),
therapeut_id: z.number(),
datum: z.string(),
ergebnis: z.string().nullable(),
diagnose: z.string().nullable(),
dringlichkeitscode: z.boolean(),
})
+37
View File
@@ -0,0 +1,37 @@
import { useCallback, useEffect, useState } from "react"
import { getDb } from "../db/client"
export function useDbQuery<T>(
query: string,
params: unknown[] = [],
deps: unknown[] = [],
) {
const [data, setData] = useState<T[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
const refetch = useCallback(async () => {
setLoading(true)
try {
const db = await getDb()
const result = await db.query(query, params)
setData(result.rows as T[])
setError(null)
} catch (e) {
setError(e instanceof Error ? e : new Error(String(e)))
} finally {
setLoading(false)
}
}, [query, ...deps])
useEffect(() => {
refetch()
}, [refetch])
return { data, loading, error, refetch }
}
export async function dbExec(query: string, params: unknown[] = []) {
const db = await getDb()
return db.query(query, params)
}