diff --git a/frontend/app/trees/[id]/page.tsx b/frontend/app/trees/[id]/page.tsx index f607b20..e4f75e6 100644 --- a/frontend/app/trees/[id]/page.tsx +++ b/frontend/app/trees/[id]/page.tsx @@ -122,23 +122,42 @@ export default function FamilyViewPage() { load(); } + async function postRel(body: components["schemas"]["RelationshipCreate"]) { + await api.POST("/api/v1/trees/{tree_id}/relationships", { + params: { path: { tree_id: treeId } }, + body, + }); + } + + // Create the relationship(s) connecting an (existing or new) person to anchor. + async function createLink(kind: AddKind, anchor: string, personId: string) { + if (kind === "parent") { + await postRel({ type: "parent_child", person_from_id: personId, person_to_id: anchor, qualifier: "biological" }); + } else if (kind === "partner") { + await postRel({ type: "partnership", person_from_id: anchor, person_to_id: personId }); + } else { + // child: link to anchor, and to anchor's spouse too (so both parents show) + await postRel({ type: "parent_child", person_from_id: anchor, person_to_id: personId, qualifier: "biological" }); + const partners = partnersOf(anchor); + if (partners.length === 1) { + await postRel({ type: "parent_child", person_from_id: partners[0], person_to_id: personId, qualifier: "biological" }); + } + } + } + + async function linkExisting(personId: string) { + if (!adding) return; + await createLink(adding.kind, adding.anchor, personId); + setAdding(null); + setAddName(""); + load(); + } + async function submitAdd(e: React.FormEvent) { e.preventDefault(); if (!adding || !addName.trim()) return; const newId = await addPerson(addName); - if (newId) { - const { kind, anchor } = adding; - const body = - kind === "parent" - ? { type: "parent_child" as const, person_from_id: newId, person_to_id: anchor, qualifier: "biological" as const } - : kind === "child" - ? { type: "parent_child" as const, person_from_id: anchor, person_to_id: newId, qualifier: "biological" as const } - : { type: "partnership" as const, person_from_id: anchor, person_to_id: newId }; - await api.POST("/api/v1/trees/{tree_id}/relationships", { - params: { path: { tree_id: treeId } }, - body, - }); - } + if (newId) await createLink(adding.kind, adding.anchor, newId); setAdding(null); setAddName(""); load(); @@ -210,26 +229,45 @@ export default function FamilyViewPage() { label: string; }) => adding?.key === formKey ? ( -
+ setAddName(e.target.value)} /> -
- - -
+ {addName.trim() && ( +
+ {people + .filter( + (p) => + p.id !== anchor && + (p.primary_name ?? "").toLowerCase().includes(addName.trim().toLowerCase()), + ) + .slice(0, 6) + .map((p) => ( + + ))} + +
+ )} +
) : ( + {editingPerson ? ( +
{ + e.preventDefault(); + savePerson(); + }} + className="space-y-3 rounded-lg border border-[var(--border)] p-4" + > +
+ setPGiven(e.target.value)} /> + setPSurname(e.target.value)} /> + setPGender(e.target.value)} /> + + +
+
+ + +
+
+ ) : ( +
+

{person.primary_name ?? "Unnamed person"}

+
+ {citeControl("p", { person_id: personId }, personCites)} + + +
- + )} @@ -335,26 +480,98 @@ export default function PersonDetailPage() {

No events yet.

) : ( )}
diff --git a/frontend/lib/api/schema.d.ts b/frontend/lib/api/schema.d.ts index 787e9ea..f6b65f3 100644 --- a/frontend/lib/api/schema.d.ts +++ b/frontend/lib/api/schema.d.ts @@ -243,7 +243,8 @@ export interface paths { delete: operations["delete_person_api_v1_trees__tree_id__persons__person_id__delete"]; options?: never; head?: never; - patch?: never; + /** Update Person */ + patch: operations["update_person_api_v1_trees__tree_id__persons__person_id__patch"]; trace?: never; }; "/api/v1/trees/{tree_id}/persons/{person_id}/restore": { @@ -312,7 +313,8 @@ export interface paths { delete: operations["delete_event_api_v1_trees__tree_id__events__event_id__delete"]; options?: never; head?: never; - patch?: never; + /** Update Event */ + patch: operations["update_event_api_v1_trees__tree_id__events__event_id__patch"]; trace?: never; }; "/api/v1/trees/{tree_id}/relationships": { @@ -676,6 +678,27 @@ export interface components { */ created_at: string; }; + /** EventUpdate */ + EventUpdate: { + /** Event Type */ + event_type?: 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 | null; + /** Detail */ + detail?: string | null; + /** Notes */ + notes?: string | null; + }; /** HTTPValidationError */ HTTPValidationError: { /** Detail */ @@ -798,6 +821,20 @@ export interface components { */ created_at: string; }; + /** PersonUpdate */ + PersonUpdate: { + /** Given */ + given?: string | null; + /** Surname */ + surname?: string | null; + /** Gender */ + gender?: string | null; + /** Is Living */ + is_living?: boolean | null; + privacy?: components["schemas"]["PersonPrivacy"] | null; + /** Notes */ + notes?: string | null; + }; /** RegisterRequest */ RegisterRequest: { /** Email */ @@ -1539,6 +1576,42 @@ export interface operations { }; }; }; + update_person_api_v1_trees__tree_id__persons__person_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + person_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PersonUpdate"]; + }; + }; + 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"]; + }; + }; + }; + }; restore_person_api_v1_trees__tree_id__persons__person_id__restore_post: { parameters: { query?: never; @@ -1699,6 +1772,42 @@ export interface operations { }; }; }; + update_event_api_v1_trees__tree_id__events__event_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + tree_id: string; + event_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EventUpdate"]; + }; + }; + 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"]; + }; + }; + }; + }; list_relationships_api_v1_trees__tree_id__relationships_get: { parameters: { query?: never; diff --git a/frontend/openapi.json b/frontend/openapi.json index 358ff5f..c897474 100644 --- a/frontend/openapi.json +++ b/frontend/openapi.json @@ -611,6 +611,67 @@ } }, "/api/v1/trees/{tree_id}/persons/{person_id}": { + "patch": { + "tags": [ + "persons" + ], + "summary": "Update Person", + "operationId": "update_person_api_v1_trees__tree_id__persons__person_id__patch", + "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" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonUpdate" + } + } + } + }, + "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" + } + } + } + } + } + }, "delete": { "tags": [ "persons" @@ -916,6 +977,67 @@ } }, "/api/v1/trees/{tree_id}/events/{event_id}": { + "patch": { + "tags": [ + "events" + ], + "summary": "Update Event", + "operationId": "update_event_api_v1_trees__tree_id__events__event_id__patch", + "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" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventRead" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, "delete": { "tags": [ "events" @@ -2361,6 +2483,114 @@ ], "title": "EventRead" }, + "EventUpdate": { + "properties": { + "event_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Event Type" + }, + "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": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Calendar" + }, + "detail": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Detail" + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + } + }, + "type": "object", + "title": "EventUpdate" + }, "HTTPValidationError": { "properties": { "detail": { @@ -2709,6 +2939,77 @@ ], "title": "PersonRead" }, + "PersonUpdate": { + "properties": { + "given": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Given" + }, + "surname": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Surname" + }, + "gender": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Gender" + }, + "is_living": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Living" + }, + "privacy": { + "anyOf": [ + { + "$ref": "#/components/schemas/PersonPrivacy" + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + } + }, + "type": "object", + "title": "PersonUpdate" + }, "RegisterRequest": { "properties": { "email": {