from __future__ import annotations from datetime import datetime from typing import Callable from fastapi import Body, FastAPI, File, HTTPException, UploadFile def register_member_routes( app: FastAPI, *, get_conn, member_payload_cls, member_bulk_payload_cls, parse_as_of: Callable[[str | None], datetime | None], fetch_members: Callable[[], list[dict[str, object]]], fetch_members_as_of: Callable[[object, datetime], list[dict[str, object]]], build_member_compare_items: Callable[[list[dict[str, object]], list[dict[str, object]]], list[dict[str, object]]], serialize_member_payload: Callable[[object, int], tuple[object, ...]], sync_auth_users_from_members: Callable[[object], None], create_history_revision: Callable[[object, str, str], int], fetch_history_revision_created_at: Callable[[object, int], datetime], sync_member_versions: Callable[[object, list[int], str, int], None], sync_seat_assignment_versions: Callable[[object, list[int], str, int], None], replace_members: Callable[[list[object]], list[dict[str, object]]], parse_import_rows: Callable[[UploadFile, bytes], list[object]], ) -> None: @app.get("/api/members") def list_members(as_of: str | None = None) -> dict[str, list[dict[str, object]]]: parsed_as_of = parse_as_of(as_of) if parsed_as_of is None: return {"items": fetch_members()} with get_conn() as conn: with conn.cursor() as cur: return {"items": fetch_members_as_of(cur, parsed_as_of)} @app.get("/api/history/members/compare") def compare_members_history(from_date: str, to_date: str) -> dict[str, list[dict[str, object]]]: parsed_from = parse_as_of(from_date) parsed_to = parse_as_of(to_date) if parsed_from is None or parsed_to is None: raise HTTPException(status_code=400, detail="from_date and to_date are required.") with get_conn() as conn: with conn.cursor() as cur: from_items = fetch_members_as_of(cur, parsed_from) to_items = fetch_members_as_of(cur, parsed_to) return {"items": build_member_compare_items(from_items, to_items)} @app.post("/api/members") def create_member(payload: dict = Body(...)) -> dict[str, object]: payload = member_payload_cls.model_validate(payload) with get_conn() as conn: with conn.cursor() as cur: cur.execute("SELECT COALESCE(MAX(sort_order), -1) + 1 AS next_order FROM members") next_order = int(cur.fetchone()["next_order"]) cur.execute( """ INSERT INTO members ( name, employee_id, company, rank, role, department, grp, division, team, cell, work_status, work_time, phone, email, seat_label, photo_url, sort_order ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id, name, employee_id, company, rank, role, department, grp, division, team, cell, work_status, work_time, phone, email, seat_label, photo_url, sort_order, created_at, updated_at """, serialize_member_payload(payload, payload.sort_order if payload.sort_order is not None else next_order), ) member = cur.fetchone() sync_auth_users_from_members(cur) revision_no = create_history_revision(cur, "member-create", f"Member created id={int(member['id'])}") revision_created_at = fetch_history_revision_created_at(cur, revision_no) sync_member_versions(cur, [int(member["id"])], "member-create", revision_no, revision_created_at) conn.commit() return {"item": member} @app.put("/api/members/bulk-sync") def bulk_sync_members(payload: dict = Body(...)) -> dict[str, list[dict[str, object]]]: payload = member_bulk_payload_cls.model_validate(payload) return {"items": replace_members(payload.items)} @app.put("/api/members/{member_id}") def update_member(member_id: int, payload: dict = Body(...)) -> dict[str, object]: payload = member_payload_cls.model_validate(payload) with get_conn() as conn: with conn.cursor() as cur: cur.execute( """ UPDATE members SET name = %s, employee_id = %s, company = %s, rank = %s, role = %s, department = %s, grp = %s, division = %s, team = %s, cell = %s, work_status = %s, work_time = %s, phone = %s, email = %s, seat_label = %s, photo_url = %s, sort_order = COALESCE(%s, sort_order), updated_at = NOW() WHERE id = %s RETURNING id, name, employee_id, company, rank, role, department, grp, division, team, cell, work_status, work_time, phone, email, seat_label, photo_url, sort_order, created_at, updated_at """, (*serialize_member_payload(payload, payload.sort_order or 0)[:-1], payload.sort_order, member_id), ) member = cur.fetchone() if member is None: raise HTTPException(status_code=404, detail="Member not found.") sync_auth_users_from_members(cur) revision_no = create_history_revision(cur, "member-update", f"Member updated id={member_id}") revision_created_at = fetch_history_revision_created_at(cur, revision_no) sync_member_versions(cur, [member_id], "member-update", revision_no, revision_created_at) sync_seat_assignment_versions(cur, [member_id], "member-update", revision_no, revision_created_at) conn.commit() return {"item": member} @app.delete("/api/members/{member_id}") def delete_member(member_id: int) -> dict[str, bool]: with get_conn() as conn: with conn.cursor() as cur: cur.execute("DELETE FROM members WHERE id = %s", (member_id,)) deleted = cur.rowcount > 0 if deleted: sync_auth_users_from_members(cur) revision_no = create_history_revision(cur, "member-delete", f"Member deleted id={member_id}") revision_created_at = fetch_history_revision_created_at(cur, revision_no) sync_member_versions(cur, [member_id], "member-delete", revision_no, revision_created_at) sync_seat_assignment_versions(cur, [member_id], "member-delete", revision_no, revision_created_at) conn.commit() if not deleted: raise HTTPException(status_code=404, detail="Member not found.") return {"ok": True} @app.post("/api/members/import") async def import_members(file: UploadFile = File(...)) -> dict[str, list[dict[str, object]]]: content = await file.read() items = parse_import_rows(file, content) return {"items": replace_members(items)}