Files
nur-falah-prevention/src/lib/family.js
T
wmj 97a1d2ebe2 feat: close final competitive gap — professional review workflow + multi-country (MY/SG/UK)
Closes the 4th and final gap from the competitive benchmark: the
human-in-the-loop professional tier every commercial competitor pairs
with their software. Deliberately NOT a marketplace or payment
integration — a structured review-request status on each member's
Wassiyah (not_requested -> requested -> reviewed, with reviewer name
recorded), surfaced on the Mutawalli dashboard so nothing quietly ships
as final without the legally-required review being flagged.

Paired with a new whole-app jurisdiction setting since 'necessary in
certain countries' only makes sense per-jurisdiction:

- nf_families.jurisdiction (MY/SG/UK), owner-only to change — first
  UPDATE policy ever added to nf_families, which previously had none.
- New Settings-tab jurisdiction selector (JurisdictionSetting.svelte).
- Wassiyah tab and the draft will document now carry jurisdiction-
  specific legal notes: Wills Act 1959 + state Syariah (Malaysia),
  Wills Act 1838 + AMLA (Singapore), Wills Act 1837 (UK). Singapore
  added as a full third jurisdiction option, not just a stub.
- Zakat calculator's currency label and Nisab default now follow the
  family's jurisdiction (RM/SGD/GBP) instead of a hardcoded RM guess.

Covered by e2e-jurisdiction-review.cjs (13/13). Full regression:
253/253 across all suites (2 reruns confirmed as pre-existing
parallel-load flakes, not regressions).
2026-08-14 12:37:25 +08:00

124 lines
4.1 KiB
JavaScript

import { writable } from 'svelte/store';
import { supabase } from './supabaseClient.js';
import { currentUser } from './auth.js';
// The family currently being managed — an owner has exactly one (their own),
// an agent may have several and switches between them here.
export const activeFamilyId = writable(localStorage.getItem('nf.activeFamilyId') || null);
activeFamilyId.subscribe(id => {
if (id) localStorage.setItem('nf.activeFamilyId', id);
});
export async function createFamily(name) {
const user = currentUser();
if (!user) throw new Error('Not signed in');
const { data: fam, error: famErr } = await supabase
.from('nf_families')
.insert({ name, owner_id: user.id })
.select()
.single();
if (famErr) throw famErr;
const { error: memErr } = await supabase.from('nf_family_members').insert({
family_id: fam.id,
user_id: user.id,
invited_email: user.email,
role: 'owner',
status: 'active'
});
if (memErr) throw memErr;
activeFamilyId.set(fam.id);
return fam;
}
/** List every family this user belongs to, owner or agent, with their role. */
export async function listMyFamilies() {
const user = currentUser();
if (!user) return [];
const { data, error } = await supabase
.from('nf_family_members')
.select('role, status, family_id, nf_families(id, name)')
.eq('user_id', user.id)
.eq('status', 'active');
if (error) throw error;
return (data || []).map(r => ({ id: r.family_id, name: r.nf_families?.name, role: r.role }));
}
/** Pending invites addressed to this user's email, not yet accepted. */
export async function listPendingInvites() {
const user = currentUser();
if (!user) return [];
const { data, error } = await supabase
.from('nf_family_members')
.select('id, role, family_id, nf_families(name)')
.eq('invited_email', user.email)
.eq('status', 'invited');
if (error) throw error;
return (data || []).map(r => ({ membershipId: r.id, familyId: r.family_id, familyName: r.nf_families?.name, role: r.role }));
}
export async function acceptInvite(membershipId) {
const user = currentUser();
if (!user) throw new Error('Not signed in');
const { error } = await supabase
.from('nf_family_members')
.update({ user_id: user.id, status: 'active' })
.eq('id', membershipId);
if (error) throw error;
}
/** Owner invites an agent by email — creates a pending membership row. */
export async function inviteAgent(familyId, email) {
const { error } = await supabase.from('nf_family_members').insert({
family_id: familyId,
invited_email: email,
role: 'agent',
status: 'invited'
});
if (error) throw error;
}
/** Invite an ordinary family member — authors their own Wassiyah/Waqf, doesn't manage the family. */
export async function inviteMember(familyId, email) {
const { error } = await supabase.from('nf_family_members').insert({
family_id: familyId,
invited_email: email,
role: 'member',
status: 'invited'
});
if (error) throw error;
}
export async function listFamilyMembers(familyId) {
const { data, error } = await supabase
.from('nf_family_members')
.select('id, user_id, invited_email, role, status')
.eq('family_id', familyId);
if (error) throw error;
return data || [];
}
export async function removeMember(membershipId) {
const { error } = await supabase.from('nf_family_members').delete().eq('id', membershipId);
if (error) throw error;
}
/** Whole-app jurisdiction (MY/SG/UK) — drives Wassiyah legal notes, will
* document format, and Zakat currency defaults. Owner-only to change. */
export async function getFamilyJurisdiction(familyId) {
const { data, error } = await supabase.from('nf_families').select('jurisdiction').eq('id', familyId).maybeSingle();
if (error) throw error;
return data?.jurisdiction || 'MY';
}
export async function setFamilyJurisdiction(familyId, jurisdiction) {
const { error } = await supabase.from('nf_families').update({ jurisdiction }).eq('id', familyId);
if (error) throw error;
}
export function getActiveFamilyId() {
let id;
activeFamilyId.subscribe(v => id = v)();
return id;
}