add pglite with migration runner, zod schemas, db hook
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
@@ -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(),
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user