Add mini family genealogy tree with per-person photos

Per explicit request: "mini family genealogy mapping with ability to add
photos on each family member." Scoped via clarifying questions: people are
lightweight records independent of login accounts (most real tree nodes —
grandparents, deceased relatives, young children — never sign up),
relationships support parent/child + spouse (enough to render a real
multi-generation tree), photos go to Supabase Storage (native to the
backend already in use, no new infrastructure).

Schema: nf_people (name, gender, birth/death dates, notes, optional
linked_user_id if the person also happens to have a real account,
photo_path) and nf_relationships (person_a/person_b + type: parent_of
directional, spouse_of symmetric). Both family-member-shared like the
Asset Registry — any owner/agent/member can view and edit, unlike the
per-author Wassiyah/Waqf model. New private Storage bucket
nf-people-photos, path convention {family_id}/{person_id}/{filename} so
RLS can check family membership straight from the path without a join.

New FamilyTree.svelte: add/edit/delete a person, upload/replace/remove
their photo (signed URLs, 1hr TTL, refreshed on every load since the
bucket is private), link/unlink relationships, and a recursive generational
tree render — roots are anyone with no recorded parent, spouses shown
inline next to their partner rather than as separate branches.

e2e-family-tree.cjs: 4-person 3-generation tree built end to end against
the live backend — add people, link parent/child + spouse relationships,
upload a real PNG to Supabase Storage and confirm it renders as the
avatar, remove it and confirm it reverts to the initial-letter fallback,
edit a person's notes, delete a relationship, and confirm a second family
under the same account sees none of this tree (isolation, same pattern
already proven for Wassiyah/Waqf). Found two real test-authoring bugs
along the way (not app bugs): a case-insensitive :has-text substring match
on "Spouse" was also matching the relationship-type select's own "is
spouse of" option text, and a person-summary click meant to inspect an
already-expanded card was instead toggling it closed. Both fixed by
selecting on DOM position/structure instead of loose text matching.
9/9 passing.

Full regression sweep: e2e-uat 32/32, e2e-fastpath 16/16, e2e-trust 12/12,
e2e-business 10/10, e2e-digital-vehicle 10/10 (one transient failure on
first run, passed clean on retry — consistent with earlier-observed
flakiness under heavy parallel test load, not a regression),
e2e-property 9/9, e2e-other 4/4, e2e-info 31/31, e2e-per-member 11/11,
e2e-family-tree 9/9 — 174/174 total.
This commit is contained in:
wmj
2026-08-14 09:16:42 +08:00
parent 5adbad6d53
commit e7a111c387
5 changed files with 548 additions and 5 deletions
+7 -5
View File
@@ -17,6 +17,7 @@
import FamilySwitcher from './lib/FamilySwitcher.svelte';
import FamilyManagement from './lib/FamilyManagement.svelte';
import MutawalliDashboard from './lib/MutawalliDashboard.svelte';
import FamilyTree from './lib/FamilyTree.svelte';
let currentLang = $state('en');
lang.subscribe(v => currentLang = v);
@@ -40,8 +41,8 @@
}
});
const tabs = ['Coverage', 'Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Nominate', 'Trigger', 'Mutawalli', 'Claims (H2)', 'Family', 'Settings'];
const icons = ['🎯', '📊', '📁', '📜', '🎁', '⛲', '📇', '⚡', '🕋', '🔗', '👥', '⚙️'];
const tabs = ['Coverage', 'Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Nominate', 'Trigger', 'Mutawalli', 'Tree', 'Claims (H2)', 'Family', 'Settings'];
const icons = ['🎯', '📊', '📁', '📜', '🎁', '⛲', '📇', '⚡', '🕋', '🌳', '🔗', '👥', '⚙️'];
let activeTab = $state(0);
function handleKeydown(e) {
@@ -104,9 +105,10 @@
{:else if activeTab === 6}<NominationRegistry />
{:else if activeTab === 7}<DeathTrigger />
{:else if activeTab === 8}<MutawalliDashboard />
{:else if activeTab === 9}<DigitalClaims />
{:else if activeTab === 10}<FamilyManagement />
{:else if activeTab === 11}
{:else if activeTab === 9}<FamilyTree />
{:else if activeTab === 10}<DigitalClaims />
{:else if activeTab === 11}<FamilyManagement />
{:else if activeTab === 12}
<div class="module">
<div class="module-header">
<h2>Settings</h2>