add contact tracker data layer: schemas, hooks, db queries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
71
src/features/kontakte/hooks.ts
Normal file
71
src/features/kontakte/hooks.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { dbExec, useDbQuery } from "@/shared/hooks/use-db";
|
||||
import type { KontaktFormData, TherapeutFormData } from "./schema";
|
||||
|
||||
interface TherapeutMitKontakte {
|
||||
id: number;
|
||||
name: string;
|
||||
stadt: string | null;
|
||||
therapieform: string | null;
|
||||
letzter_kontakt: string | null;
|
||||
letztes_ergebnis: string | null;
|
||||
kontakte_gesamt: number;
|
||||
}
|
||||
|
||||
interface KontaktRow {
|
||||
id: number;
|
||||
therapeut_id: number;
|
||||
datum: string;
|
||||
kanal: string;
|
||||
ergebnis: string;
|
||||
notiz: string | null;
|
||||
antwort_datum: string | null;
|
||||
}
|
||||
|
||||
export function useTherapeutenListe() {
|
||||
return useDbQuery<TherapeutMitKontakte>(`
|
||||
SELECT
|
||||
t.id, t.name, t.stadt, t.therapieform,
|
||||
(SELECT k.datum FROM kontakt k WHERE k.therapeut_id = t.id ORDER BY k.datum DESC LIMIT 1) as letzter_kontakt,
|
||||
(SELECT k.ergebnis FROM kontakt k WHERE k.therapeut_id = t.id ORDER BY k.datum DESC LIMIT 1) as letztes_ergebnis,
|
||||
(SELECT COUNT(*) FROM kontakt k WHERE k.therapeut_id = t.id) as kontakte_gesamt
|
||||
FROM therapeut t
|
||||
ORDER BY t.erstellt_am DESC
|
||||
`);
|
||||
}
|
||||
|
||||
export function useKontakteForTherapeut(therapeutId: number) {
|
||||
return useDbQuery<KontaktRow>(
|
||||
"SELECT * FROM kontakt WHERE therapeut_id = $1 ORDER BY datum DESC",
|
||||
[therapeutId],
|
||||
[therapeutId],
|
||||
);
|
||||
}
|
||||
|
||||
export async function createTherapeut(
|
||||
data: TherapeutFormData,
|
||||
): Promise<number> {
|
||||
const result = await dbExec(
|
||||
`INSERT INTO therapeut (name, adresse, plz, stadt, telefon, email, website, therapieform)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id`,
|
||||
[
|
||||
data.name,
|
||||
data.adresse,
|
||||
data.plz,
|
||||
data.stadt,
|
||||
data.telefon,
|
||||
data.email,
|
||||
data.website,
|
||||
data.therapieform,
|
||||
],
|
||||
);
|
||||
return (result.rows[0] as { id: number }).id;
|
||||
}
|
||||
|
||||
export async function createKontakt(data: KontaktFormData) {
|
||||
await dbExec(
|
||||
`INSERT INTO kontakt (therapeut_id, datum, kanal, ergebnis, notiz)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[data.therapeut_id, data.datum, data.kanal, data.ergebnis, data.notiz],
|
||||
);
|
||||
}
|
||||
8
src/features/kontakte/index.ts
Normal file
8
src/features/kontakte/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
createKontakt,
|
||||
createTherapeut,
|
||||
useKontakteForTherapeut,
|
||||
useTherapeutenListe,
|
||||
} from "./hooks";
|
||||
export type { KontaktFormData, TherapeutFormData } from "./schema";
|
||||
export { kontaktFormSchema, therapeutFormSchema } from "./schema";
|
||||
45
src/features/kontakte/schema.test.ts
Normal file
45
src/features/kontakte/schema.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { kontaktFormSchema, therapeutFormSchema } from "./schema";
|
||||
|
||||
describe("therapeutFormSchema", () => {
|
||||
it("accepts valid therapist", () => {
|
||||
const result = therapeutFormSchema.safeParse({
|
||||
name: "Dr. Schmidt",
|
||||
plz: "10115",
|
||||
stadt: "Berlin",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("requires name", () => {
|
||||
const result = therapeutFormSchema.safeParse({ name: "" });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("allows empty optional fields", () => {
|
||||
const result = therapeutFormSchema.safeParse({ name: "Dr. Schmidt" });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("kontaktFormSchema", () => {
|
||||
it("accepts valid contact", () => {
|
||||
const result = kontaktFormSchema.safeParse({
|
||||
therapeut_id: 1,
|
||||
datum: "2026-03-10",
|
||||
kanal: "telefon",
|
||||
ergebnis: "absage",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects missing date", () => {
|
||||
const result = kontaktFormSchema.safeParse({
|
||||
therapeut_id: 1,
|
||||
datum: "",
|
||||
kanal: "telefon",
|
||||
ergebnis: "absage",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
29
src/features/kontakte/schema.ts
Normal file
29
src/features/kontakte/schema.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { z } from "zod";
|
||||
import { kontaktErgebnisEnum, kontaktKanalEnum } from "@/shared/db/schema";
|
||||
|
||||
export const therapeutFormSchema = z.object({
|
||||
name: z.string().min(1, "Name ist erforderlich."),
|
||||
adresse: z.string().optional().default(""),
|
||||
plz: z
|
||||
.string()
|
||||
.regex(/^\d{5}$/, "Bitte gib eine gültige PLZ ein.")
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
stadt: z.string().optional().default(""),
|
||||
telefon: z.string().optional().default(""),
|
||||
email: z.string().email("Ungültige E-Mail.").optional().or(z.literal("")),
|
||||
website: z.string().optional().default(""),
|
||||
therapieform: z.string().optional().default(""),
|
||||
});
|
||||
|
||||
export type TherapeutFormData = z.infer<typeof therapeutFormSchema>;
|
||||
|
||||
export const kontaktFormSchema = z.object({
|
||||
therapeut_id: z.number(),
|
||||
datum: z.string().min(1, "Datum ist erforderlich."),
|
||||
kanal: kontaktKanalEnum,
|
||||
ergebnis: kontaktErgebnisEnum,
|
||||
notiz: z.string().optional().default(""),
|
||||
});
|
||||
|
||||
export type KontaktFormData = z.infer<typeof kontaktFormSchema>;
|
||||
Reference in New Issue
Block a user