diff --git a/frontend/app/trees/[id]/page.tsx b/frontend/app/trees/[id]/page.tsx index cf3f2db..b7907d7 100644 --- a/frontend/app/trees/[id]/page.tsx +++ b/frontend/app/trees/[id]/page.tsx @@ -81,11 +81,15 @@ export default function TreeDetailPage() { diff --git a/frontend/app/trees/[id]/persons/[personId]/page.tsx b/frontend/app/trees/[id]/persons/[personId]/page.tsx new file mode 100644 index 0000000..5a0729e --- /dev/null +++ b/frontend/app/trees/[id]/persons/[personId]/page.tsx @@ -0,0 +1,280 @@ +"use client"; + +import Link from "next/link"; +import { useParams, useRouter } from "next/navigation"; +import { useCallback, useEffect, useMemo, useState } from "react"; + +import { api } from "@/lib/api/client"; +import type { components } from "@/lib/api/schema"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; + +type Person = components["schemas"]["PersonRead"]; +type Event = components["schemas"]["EventRead"]; +type Relationship = components["schemas"]["RelationshipRead"]; +type Qualifier = components["schemas"]["ParentChildQualifier"]; +type RelCreate = components["schemas"]["RelationshipCreate"]; + +const fieldCls = + "h-10 rounded-md border border-[var(--border)] bg-[var(--surface)] px-2 text-sm"; + +const QUALIFIERS: Qualifier[] = ["biological", "adoptive", "step", "foster", "donor", "guardian"]; + +export default function PersonDetailPage() { + const router = useRouter(); + const params = useParams<{ id: string; personId: string }>(); + const treeId = params.id; + const personId = params.personId; + + const [person, setPerson] = useState(null); + const [people, setPeople] = useState([]); + const [events, setEvents] = useState([]); + const [rels, setRels] = useState([]); + const [ready, setReady] = useState(false); + + const [evType, setEvType] = useState("birth"); + const [evDate, setEvDate] = useState(""); + + const [relKind, setRelKind] = useState<"parent" | "child" | "partner" | "sibling">("parent"); + const [relOther, setRelOther] = useState(""); + const [relQual, setRelQual] = useState("biological"); + + const load = useCallback(async () => { + const p = await api.GET("/api/v1/trees/{tree_id}/persons/{person_id}", { + params: { path: { tree_id: treeId, person_id: personId } }, + }); + if (p.response.status === 401) { + router.push("/login"); + return; + } + setPerson(p.data ?? null); + const [all, ev, rl] = await Promise.all([ + api.GET("/api/v1/trees/{tree_id}/persons", { params: { path: { tree_id: treeId } } }), + api.GET("/api/v1/trees/{tree_id}/persons/{person_id}/events", { + params: { path: { tree_id: treeId, person_id: personId } }, + }), + api.GET("/api/v1/trees/{tree_id}/persons/{person_id}/relationships", { + params: { path: { tree_id: treeId, person_id: personId } }, + }), + ]); + setPeople(all.data ?? []); + setEvents(ev.data ?? []); + setRels(rl.data ?? []); + setReady(true); + }, [router, treeId, personId]); + + useEffect(() => { + load(); + }, [load]); + + const nameOf = useMemo(() => { + const m = new Map(people.map((p) => [p.id, p.primary_name ?? "Unnamed"])); + return (id: string) => m.get(id) ?? "Unknown"; + }, [people]); + + const others = people.filter((p) => p.id !== personId); + const parents = rels.filter((r) => r.type === "parent_child" && r.person_to_id === personId); + const children = rels.filter((r) => r.type === "parent_child" && r.person_from_id === personId); + const partners = rels.filter((r) => r.type === "partnership"); + const siblings = rels.filter((r) => r.type === "sibling"); + + async function addEvent(e: React.FormEvent) { + e.preventDefault(); + if (!evType.trim()) return; + const { error } = await api.POST("/api/v1/trees/{tree_id}/events", { + params: { path: { tree_id: treeId } }, + body: { event_type: evType, person_id: personId, date_value: evDate || null }, + }); + if (!error) { + setEvDate(""); + load(); + } + } + + async function removeEvent(id: string) { + await api.DELETE("/api/v1/trees/{tree_id}/events/{event_id}", { + params: { path: { tree_id: treeId, event_id: id } }, + }); + load(); + } + + async function addRel(e: React.FormEvent) { + e.preventDefault(); + if (!relOther) return; + let body: RelCreate; + if (relKind === "parent") { + body = { type: "parent_child", person_from_id: relOther, person_to_id: personId, qualifier: relQual }; + } else if (relKind === "child") { + body = { type: "parent_child", person_from_id: personId, person_to_id: relOther, qualifier: relQual }; + } else if (relKind === "partner") { + body = { type: "partnership", person_from_id: personId, person_to_id: relOther }; + } else { + body = { type: "sibling", person_from_id: personId, person_to_id: relOther }; + } + const { error } = await api.POST("/api/v1/trees/{tree_id}/relationships", { + params: { path: { tree_id: treeId } }, + body, + }); + if (!error) { + setRelOther(""); + load(); + } + } + + async function removeRel(id: string) { + await api.DELETE("/api/v1/trees/{tree_id}/relationships/{relationship_id}", { + params: { path: { tree_id: treeId, relationship_id: id } }, + }); + load(); + } + + if (!ready) return

Loading…

; + if (!person) return

Not found.

; + + const relGroup = (label: string, items: Relationship[], otherId: (r: Relationship) => string) => + items.length > 0 && ( +
+

{label}

+
    + {items.map((r) => ( +
  • + + {nameOf(otherId(r))} + {r.qualifier ? · {r.qualifier} : null} + + +
  • + ))} +
+
+ ); + + return ( +
+ + ← Back to tree + + +

{person.primary_name ?? "Unnamed person"}

+ + + + Life events + + + {events.length === 0 ? ( +

No events yet.

+ ) : ( +
    + {events.map((ev) => ( +
  • + + {ev.event_type} + {ev.date_value ? ( + — {ev.date_value} + ) : null} + + +
  • + ))} +
+ )} +
+ setEvType(e.target.value)} + /> + setEvDate(e.target.value)} + /> + +
+
+
+ + + + Relationships + + + {rels.length === 0 ? ( +

No relationships yet.

+ ) : ( +
+ {relGroup("Parents", parents, (r) => r.person_from_id)} + {relGroup("Children", children, (r) => r.person_to_id)} + {relGroup("Partners", partners, (r) => + r.person_from_id === personId ? r.person_to_id : r.person_from_id, + )} + {relGroup("Siblings", siblings, (r) => + r.person_from_id === personId ? r.person_to_id : r.person_from_id, + )} +
+ )} + + {others.length === 0 ? ( +

Add more people to the tree to link them.

+ ) : ( +
+ Add + + + {(relKind === "parent" || relKind === "child") && ( + + )} + +
+ )} +
+
+
+ ); +} diff --git a/frontend/lib/api/schema.d.ts b/frontend/lib/api/schema.d.ts index fde55b1..ac8b28c 100644 --- a/frontend/lib/api/schema.d.ts +++ b/frontend/lib/api/schema.d.ts @@ -210,10 +210,197 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/trees/{tree_id}/persons/{person_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Person */ + get: operations["get_person_api_v1_trees__tree_id__persons__person_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/trees/{tree_id}/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create Event */ + post: operations["create_event_api_v1_trees__tree_id__events_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/trees/{tree_id}/persons/{person_id}/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Person Events */ + get: operations["list_person_events_api_v1_trees__tree_id__persons__person_id__events_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/trees/{tree_id}/events/{event_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Event */ + delete: operations["delete_event_api_v1_trees__tree_id__events__event_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/trees/{tree_id}/relationships": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create Relationship */ + post: operations["create_relationship_api_v1_trees__tree_id__relationships_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/trees/{tree_id}/persons/{person_id}/relationships": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List Person Relationships */ + get: operations["list_person_relationships_api_v1_trees__tree_id__persons__person_id__relationships_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/trees/{tree_id}/relationships/{relationship_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete Relationship */ + delete: operations["delete_relationship_api_v1_trees__tree_id__relationships__relationship_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { schemas: { + /** EventCreate */ + EventCreate: { + /** Event Type */ + event_type: string; + /** Person Id */ + person_id?: string | null; + /** Relationship Id */ + relationship_id?: string | null; + /** Place Id */ + place_id?: string | null; + /** Date Value */ + date_value?: string | null; + /** Date Start */ + date_start?: string | null; + /** Date End */ + date_end?: string | null; + /** Date Precision */ + date_precision?: string | null; + /** + * Calendar + * @default gregorian + */ + calendar?: string; + /** Detail */ + detail?: string | null; + /** Notes */ + notes?: string | null; + }; + /** EventRead */ + EventRead: { + /** + * Id + * Format: uuid + */ + id: string; + /** + * Tree Id + * Format: uuid + */ + tree_id: string; + /** Event Type */ + event_type: string; + /** Person Id */ + person_id: string | null; + /** Relationship Id */ + relationship_id: string | null; + /** Place Id */ + place_id: string | null; + /** Date Value */ + date_value: string | null; + /** Date Start */ + date_start: string | null; + /** Date End */ + date_end: string | null; + /** Date Precision */ + date_precision: string | null; + /** Calendar */ + calendar: string; + /** Detail */ + detail: string | null; + /** Notes */ + notes: string | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + }; /** HTTPValidationError */ HTTPValidationError: { /** Detail */ @@ -226,6 +413,13 @@ export interface components { /** Password */ password: string; }; + /** + * ParentChildQualifier + * @description Qualifies a parent_child edge so adoption/donor/blended families are + * first-class rather than edge cases (ARCHITECTURE §5). + * @enum {string} + */ + ParentChildQualifier: "biological" | "adoptive" | "step" | "foster" | "donor" | "guardian"; /** PasswordResetConfirm */ PasswordResetConfirm: { /** Token */ @@ -293,6 +487,60 @@ export interface components { /** Display Name */ display_name?: string | null; }; + /** RelationshipCreate */ + RelationshipCreate: { + type: components["schemas"]["RelationshipType"]; + /** + * Person From Id + * Format: uuid + */ + person_from_id: string; + /** + * Person To Id + * Format: uuid + */ + person_to_id: string; + qualifier?: components["schemas"]["ParentChildQualifier"] | null; + /** Notes */ + notes?: string | null; + }; + /** RelationshipRead */ + RelationshipRead: { + /** + * Id + * Format: uuid + */ + id: string; + /** + * Tree Id + * Format: uuid + */ + tree_id: string; + type: components["schemas"]["RelationshipType"]; + /** + * Person From Id + * Format: uuid + */ + person_from_id: string; + /** + * Person To Id + * Format: uuid + */ + person_to_id: string; + qualifier: components["schemas"]["ParentChildQualifier"] | null; + /** Notes */ + notes: string | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + }; + /** + * RelationshipType + * @enum {string} + */ + RelationshipType: "parent_child" | "partnership" | "sibling"; /** SessionRead */ SessionRead: { user: components["schemas"]["UserRead"]; @@ -782,4 +1030,230 @@ export interface operations { }; }; }; + get_person_api_v1_trees__tree_id__persons__person_id__get: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + person_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PersonRead"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_event_api_v1_trees__tree_id__events_post: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EventCreate"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventRead"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_person_events_api_v1_trees__tree_id__persons__person_id__events_get: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + person_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventRead"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_event_api_v1_trees__tree_id__events__event_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + event_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_relationship_api_v1_trees__tree_id__relationships_post: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RelationshipCreate"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RelationshipRead"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_person_relationships_api_v1_trees__tree_id__persons__person_id__relationships_get: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + person_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RelationshipRead"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_relationship_api_v1_trees__tree_id__relationships__relationship_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + relationship_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; } diff --git a/frontend/openapi.json b/frontend/openapi.json index 1857ff9..cdd5477 100644 --- a/frontend/openapi.json +++ b/frontend/openapi.json @@ -484,10 +484,646 @@ } } } + }, + "/api/v1/trees/{tree_id}/persons/{person_id}": { + "get": { + "tags": [ + "persons" + ], + "summary": "Get Person", + "operationId": "get_person_api_v1_trees__tree_id__persons__person_id__get", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + }, + { + "name": "person_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Person Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonRead" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/trees/{tree_id}/events": { + "post": { + "tags": [ + "events" + ], + "summary": "Create Event", + "operationId": "create_event_api_v1_trees__tree_id__events_post", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventRead" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/trees/{tree_id}/persons/{person_id}/events": { + "get": { + "tags": [ + "events" + ], + "summary": "List Person Events", + "operationId": "list_person_events_api_v1_trees__tree_id__persons__person_id__events_get", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + }, + { + "name": "person_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Person Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventRead" + }, + "title": "Response List Person Events Api V1 Trees Tree Id Persons Person Id Events Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/trees/{tree_id}/events/{event_id}": { + "delete": { + "tags": [ + "events" + ], + "summary": "Delete Event", + "operationId": "delete_event_api_v1_trees__tree_id__events__event_id__delete", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + }, + { + "name": "event_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Event Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/trees/{tree_id}/relationships": { + "post": { + "tags": [ + "relationships" + ], + "summary": "Create Relationship", + "operationId": "create_relationship_api_v1_trees__tree_id__relationships_post", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelationshipCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelationshipRead" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/trees/{tree_id}/persons/{person_id}/relationships": { + "get": { + "tags": [ + "relationships" + ], + "summary": "List Person Relationships", + "operationId": "list_person_relationships_api_v1_trees__tree_id__persons__person_id__relationships_get", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + }, + { + "name": "person_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Person Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RelationshipRead" + }, + "title": "Response List Person Relationships Api V1 Trees Tree Id Persons Person Id Relationships Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/trees/{tree_id}/relationships/{relationship_id}": { + "delete": { + "tags": [ + "relationships" + ], + "summary": "Delete Relationship", + "operationId": "delete_relationship_api_v1_trees__tree_id__relationships__relationship_id__delete", + "parameters": [ + { + "name": "tree_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + } + }, + { + "name": "relationship_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Relationship Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } } }, "components": { "schemas": { + "EventCreate": { + "properties": { + "event_type": { + "type": "string", + "title": "Event Type" + }, + "person_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Person Id" + }, + "relationship_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Relationship Id" + }, + "place_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Place Id" + }, + "date_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date Value" + }, + "date_start": { + "anyOf": [ + { + "type": "string", + "format": "date" + }, + { + "type": "null" + } + ], + "title": "Date Start" + }, + "date_end": { + "anyOf": [ + { + "type": "string", + "format": "date" + }, + { + "type": "null" + } + ], + "title": "Date End" + }, + "date_precision": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date Precision" + }, + "calendar": { + "type": "string", + "title": "Calendar", + "default": "gregorian" + }, + "detail": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Detail" + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + } + }, + "type": "object", + "required": [ + "event_type" + ], + "title": "EventCreate" + }, + "EventRead": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "tree_id": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + }, + "event_type": { + "type": "string", + "title": "Event Type" + }, + "person_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Person Id" + }, + "relationship_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Relationship Id" + }, + "place_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Place Id" + }, + "date_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date Value" + }, + "date_start": { + "anyOf": [ + { + "type": "string", + "format": "date" + }, + { + "type": "null" + } + ], + "title": "Date Start" + }, + "date_end": { + "anyOf": [ + { + "type": "string", + "format": "date" + }, + { + "type": "null" + } + ], + "title": "Date End" + }, + "date_precision": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Date Precision" + }, + "calendar": { + "type": "string", + "title": "Calendar" + }, + "detail": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Detail" + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + } + }, + "type": "object", + "required": [ + "id", + "tree_id", + "event_type", + "person_id", + "relationship_id", + "place_id", + "date_value", + "date_start", + "date_end", + "date_precision", + "calendar", + "detail", + "notes", + "created_at" + ], + "title": "EventRead" + }, "HTTPValidationError": { "properties": { "detail": { @@ -519,6 +1155,19 @@ ], "title": "LoginRequest" }, + "ParentChildQualifier": { + "type": "string", + "enum": [ + "biological", + "adoptive", + "step", + "foster", + "donor", + "guardian" + ], + "title": "ParentChildQualifier", + "description": "Qualifies a parent_child edge so adoption/donor/blended families are\nfirst-class rather than edge cases (ARCHITECTURE \u00a75)." + }, "PasswordResetConfirm": { "properties": { "token": { @@ -721,6 +1370,125 @@ ], "title": "RegisterRequest" }, + "RelationshipCreate": { + "properties": { + "type": { + "$ref": "#/components/schemas/RelationshipType" + }, + "person_from_id": { + "type": "string", + "format": "uuid", + "title": "Person From Id" + }, + "person_to_id": { + "type": "string", + "format": "uuid", + "title": "Person To Id" + }, + "qualifier": { + "anyOf": [ + { + "$ref": "#/components/schemas/ParentChildQualifier" + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + } + }, + "type": "object", + "required": [ + "type", + "person_from_id", + "person_to_id" + ], + "title": "RelationshipCreate" + }, + "RelationshipRead": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "tree_id": { + "type": "string", + "format": "uuid", + "title": "Tree Id" + }, + "type": { + "$ref": "#/components/schemas/RelationshipType" + }, + "person_from_id": { + "type": "string", + "format": "uuid", + "title": "Person From Id" + }, + "person_to_id": { + "type": "string", + "format": "uuid", + "title": "Person To Id" + }, + "qualifier": { + "anyOf": [ + { + "$ref": "#/components/schemas/ParentChildQualifier" + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + } + }, + "type": "object", + "required": [ + "id", + "tree_id", + "type", + "person_from_id", + "person_to_id", + "qualifier", + "notes", + "created_at" + ], + "title": "RelationshipRead" + }, + "RelationshipType": { + "type": "string", + "enum": [ + "parent_child", + "partnership", + "sibling" + ], + "title": "RelationshipType" + }, "SessionRead": { "properties": { "user": {