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).
124 lines
4.1 KiB
JavaScript
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;
|
|
}
|