Per-member estate model: each family member authors their own Wassiyah/Waqf,
mutawalli executes on any member's trigger, heir email notification Per explicit product direction: "all members of the family can make their own wassiyah or waqif. The mutawali or the trustee agent can access and execute those wassiyah and waqif upon any event triggers. Warith or the heir will be automatically notified via email." Schema: nf_family_members.role now includes 'member' (authors own documents, doesn't manage the family). nf_wassiyah_settings/nf_wassiyah_bequests/ nf_waqf_designations gained author_id — each is now per-author, not per-family. Added recipient_email / beneficiary_email columns for warith notification targets. New nf_member_triggers (composite PK family_id+ member_id) and nf_member_attestors: a per-member death trigger, separate from the legacy family-wide nf_death_triggers (kept for backward compatibility, still exercised by existing suites). RLS: any family member can READ any other member's Wassiyah/Waqf (the mutawalli needs full visibility to execute), but only the document's own author can WRITE to it — not even the owner. Firing a member's trigger requires the caller to have role agent/owner AND not be the member themselves (enforced in the policy's WITH CHECK, not just the UI) — matches "the mutawalli executes, never for themselves." New UI: FamilyManagement gained a role selector (member vs agent) on invites. New MutawalliDashboard.svelte — the trustee's execution surface: pick any family member, see their Wassiyah/Waqf read-only, set up attestors + death cert ref, fire their trigger (blocked for self both by disabled UI and by RLS), then trigger heir email notifications. Edge Function notify-heirs deployed (Deno, uses Resend): reads the triggered member's Wassiyah recipients and Waqf beneficiaries wherever an email was recorded, sends each a notice. Returns a clear 501 rather than failing silently until RESEND_API_KEY is set as a project secret. Three real bugs found via testing against the live backend, not visible from code review alone: - listFamilyMembers() never selected user_id — every member-scoped lookup on the new dashboard was silently keying off undefined. - nf_member_triggers keyed by member_id alone: since a person can belong to multiple families, firing a trigger in one family marked them "triggered" in every other family they belong to. Fixed to composite (family_id, member_id) key. - Classic Svelte 5 $state pitfall: (proxyObject[key] ??= []).push(item) mutates the plain array literal the ??= expression evaluates to, not the proxy-wrapped array Svelte actually tracks — so pushed items were silently invisible to the UI forever. Fixed by building on a plain object and assigning to the $state variable once. Also found and fixed the same design smell in the older WassiyahGenerator/FamilyWaqfDesignator authorId handling: it was snapshotted once via currentUser()?.id at mount instead of read live off the session store, which could silently break writes on a remount that happened before session hydration finished — now reads live and guards refresh() on it being present. CoverageDashboard and DeathTrigger updated to check ANY family member's Waqf corpus for coverage (not just one author's), since coverage is a family-wide view even though authorship is per-member now. e2e-per-member.cjs: new suite covering the full flow — owner invites a member and an agent; member authors a private Wassiyah (invisible to other members, confirming per-author isolation); mutawalli sees it on their dashboard and fires the member's trigger; member cannot fire their own; heir notification call completes with either Sent or a clear "not configured" failure, never hangs. 11/11 passing. Full regression sweep after these changes: e2e-uat 32/32 (stable across 3 consecutive runs), e2e-fastpath 16/16, e2e-trust 12/12, e2e-business 10/10, e2e-digital-vehicle 10/10, e2e-property 9/9, e2e-other 4/4, e2e-info 31/31, e2e-family-agent 12/12 (updated for the new invite-form role selector), e2e-per-member 11/11 — 178/178 total, no regressions.
This commit is contained in:
+7
-5
@@ -16,6 +16,7 @@
|
||||
import AuthScreen from './lib/AuthScreen.svelte';
|
||||
import FamilySwitcher from './lib/FamilySwitcher.svelte';
|
||||
import FamilyManagement from './lib/FamilyManagement.svelte';
|
||||
import MutawalliDashboard from './lib/MutawalliDashboard.svelte';
|
||||
|
||||
let currentLang = $state('en');
|
||||
lang.subscribe(v => currentLang = v);
|
||||
@@ -39,8 +40,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = ['Coverage', 'Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Nominate', 'Trigger', 'Claims (H2)', 'Family', 'Settings'];
|
||||
const icons = ['🎯', '📊', '📁', '📜', '🎁', '⛲', '📇', '⚡', '🔗', '👥', '⚙️'];
|
||||
const tabs = ['Coverage', 'Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Nominate', 'Trigger', 'Mutawalli', 'Claims (H2)', 'Family', 'Settings'];
|
||||
const icons = ['🎯', '📊', '📁', '📜', '🎁', '⛲', '📇', '⚡', '🕋', '🔗', '👥', '⚙️'];
|
||||
let activeTab = $state(0);
|
||||
|
||||
function handleKeydown(e) {
|
||||
@@ -102,9 +103,10 @@
|
||||
{:else if activeTab === 5}<FamilyWaqfDesignator />
|
||||
{:else if activeTab === 6}<NominationRegistry />
|
||||
{:else if activeTab === 7}<DeathTrigger />
|
||||
{:else if activeTab === 8}<DigitalClaims />
|
||||
{:else if activeTab === 9}<FamilyManagement />
|
||||
{:else if activeTab === 10}
|
||||
{:else if activeTab === 8}<MutawalliDashboard />
|
||||
{:else if activeTab === 9}<DigitalClaims />
|
||||
{:else if activeTab === 10}<FamilyManagement />
|
||||
{:else if activeTab === 11}
|
||||
<div class="module">
|
||||
<div class="module-header">
|
||||
<h2>Settings</h2>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { computeCoverage, suggestedChannel } from './nonprobate.js';
|
||||
import { activeFamilyId } from './family.js';
|
||||
import { listAssets, listHibahGifts, listNominations, getWaqfDesignation } from './db.js';
|
||||
import { listAssets, listHibahGifts, listNominations, listAllWaqfForFamily } from './db.js';
|
||||
import Disclaimer from './Disclaimer.svelte';
|
||||
import InfoPanel from './InfoPanel.svelte';
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
|
||||
async function refresh() {
|
||||
if (!familyId) return;
|
||||
const [assets, gifts, nominations, waqf] = await Promise.all([
|
||||
listAssets(familyId), listHibahGifts(familyId), listNominations(familyId), getWaqfDesignation(familyId)
|
||||
const [assets, gifts, nominations, waqfDesignations] = await Promise.all([
|
||||
listAssets(familyId), listHibahGifts(familyId), listNominations(familyId), listAllWaqfForFamily(familyId)
|
||||
]);
|
||||
coverage = computeCoverage({ assets, gifts, nominations, waqfCorpusId: waqf?.corpusAssetId ?? null });
|
||||
const waqfCorpusIds = waqfDesignations.map(w => w.corpus_asset_id).filter(Boolean);
|
||||
coverage = computeCoverage({ assets, gifts, nominations, waqfCorpusIds });
|
||||
}
|
||||
|
||||
onMount(refresh);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { computeCoverage } from './nonprobate.js';
|
||||
import { activeFamilyId, listMyFamilies } from './family.js';
|
||||
import { listAssets, listHibahGifts, listNominations, getWaqfDesignation, listAttestors, addAttestor as addAttestorDb, updateAttestorName, setAttestorConfirmed, getDeathTrigger, upsertDeathTriggerSetup, updateDeathTriggerField, fireDeathTrigger } from './db.js';
|
||||
import { listAssets, listHibahGifts, listNominations, listAllWaqfForFamily, listAttestors, addAttestor as addAttestorDb, updateAttestorName, setAttestorConfirmed, getDeathTrigger, upsertDeathTriggerSetup, updateDeathTriggerField, fireDeathTrigger } from './db.js';
|
||||
import { supabase } from './supabaseClient.js';
|
||||
import InfoPanel from './InfoPanel.svelte';
|
||||
|
||||
@@ -36,10 +36,11 @@
|
||||
|
||||
async function refresh() {
|
||||
if (!familyId) return;
|
||||
const [assets, gifts, nominations, waqf, families] = await Promise.all([
|
||||
listAssets(familyId), listHibahGifts(familyId), listNominations(familyId), getWaqfDesignation(familyId), listMyFamilies()
|
||||
const [assets, gifts, nominations, waqfDesignations, families] = await Promise.all([
|
||||
listAssets(familyId), listHibahGifts(familyId), listNominations(familyId), listAllWaqfForFamily(familyId), listMyFamilies()
|
||||
]);
|
||||
coverage = computeCoverage({ assets, gifts, nominations, waqfCorpusId: waqf?.corpusAssetId ?? null });
|
||||
const waqfCorpusIds = waqfDesignations.map(w => w.corpus_asset_id).filter(Boolean);
|
||||
coverage = computeCoverage({ assets, gifts, nominations, waqfCorpusIds });
|
||||
myRole = families.find(f => f.id === familyId)?.role;
|
||||
|
||||
attestors = await listAttestors(familyId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { activeFamilyId, listFamilyMembers, inviteAgent, removeMember, listMyFamilies } from './family.js';
|
||||
import { activeFamilyId, listFamilyMembers, inviteAgent, inviteMember, removeMember, listMyFamilies } from './family.js';
|
||||
import { currentUser } from './auth.js';
|
||||
import InfoPanel from './InfoPanel.svelte';
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
let members = $state([]);
|
||||
let myFamilies = $state([]);
|
||||
let inviteEmail = $state('');
|
||||
let inviteRole = $state('member');
|
||||
let error = $state('');
|
||||
let myRole = $state(null);
|
||||
|
||||
@@ -27,7 +28,8 @@
|
||||
error = '';
|
||||
if (!inviteEmail) return;
|
||||
try {
|
||||
await inviteAgent(familyId, inviteEmail);
|
||||
if (inviteRole === 'agent') await inviteAgent(familyId, inviteEmail);
|
||||
else await inviteMember(familyId, inviteEmail);
|
||||
inviteEmail = '';
|
||||
await refresh();
|
||||
} catch (e) { error = e.message; }
|
||||
@@ -50,9 +52,9 @@
|
||||
<h2>Family</h2>
|
||||
<InfoPanel
|
||||
title="Family"
|
||||
what="Where the head of family assigns an estate agent — a trusted relative or professional — to help manage this estate's planning. An agent can do everything you can except one thing: they cannot confirm a death and fire the execution trigger. Only the owner (you) can do that."
|
||||
how="Enter the agent's email and invite them. They'll see the invite next time they sign in, and once accepted, this family shows up in their own dashboard alongside any other families they help manage."
|
||||
fields={[{ label: 'Agent email', hint: 'They need to create an account with this exact email to accept the invite.' }]}
|
||||
what="Where the head of family invites two kinds of people: ordinary members (spouse, adult children — each authors their own Wassiyah/Waqf, visible only to them until they choose to share) and an estate agent / mutawalli (a trusted relative or professional who can see and execute everyone's documents when the time comes, but never fires a trigger for themselves)."
|
||||
how="Enter an email, pick 'Family member' or 'Estate agent', and invite. They'll see the invite next time they sign in, and once accepted, this family shows up in their own dashboard."
|
||||
fields={[{ label: 'Email', hint: 'They need to create an account with this exact email to accept the invite.' }, { label: 'Role', hint: 'Member authors their own documents; agent/mutawalli can execute everyone\'s.' }]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -71,12 +73,20 @@
|
||||
|
||||
{#if myRole === 'owner'}
|
||||
<div class="form-card">
|
||||
<label class="field"><span>Invite an estate agent by email</span><input type="email" bind:value={inviteEmail} placeholder="agent@example.com" /></label>
|
||||
<label class="field"><span>Invite by email</span><input type="email" bind:value={inviteEmail} placeholder="name@example.com" /></label>
|
||||
<label class="field"><span>Role</span>
|
||||
<select bind:value={inviteRole}>
|
||||
<option value="member">Family member (authors own Wassiyah/Waqf)</option>
|
||||
<option value="agent">Estate agent / mutawalli (executes for everyone)</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn-primary" onclick={handleInvite}>Send invite</button>
|
||||
</div>
|
||||
{#if error}<p class="error-text">{error}</p>{/if}
|
||||
{:else if myRole === 'agent'}
|
||||
<div class="agent-note">You're managing this family as an estate agent. You can add and edit everything except firing the death trigger — that's reserved for the head of family.</div>
|
||||
<div class="agent-note">You're the mutawalli/estate agent for this family. You can see and execute every member's Wassiyah/Waqf when their trigger is confirmed — but you can never fire a member's own trigger for yourself, and you can't edit another member's document content, only your own.</div>
|
||||
{:else if myRole === 'member'}
|
||||
<div class="agent-note">You're a member of this family. Your Wassiyah and Waqf tabs show only your own documents — private to you until the mutawalli needs to execute them after your trigger fires.</div>
|
||||
{/if}
|
||||
|
||||
<div class="members-list">
|
||||
@@ -108,7 +118,7 @@
|
||||
.form-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 16px; }
|
||||
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
|
||||
.field span { font-size: 12px; color: #B8B2A6; }
|
||||
.field input { background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 10px 12px; color: #E8E4DC; font-size: 14px; width: 100%; }
|
||||
.field input, .field select { background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 10px 12px; color: #E8E4DC; font-size: 14px; width: 100%; }
|
||||
.btn-primary { width: 100%; padding: 12px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; background: #C9A84C; color: #070A0D; }
|
||||
.error-text { color: #EF4444; font-size: 12.5px; margin-bottom: 12px; }
|
||||
.agent-note { font-size: 12px; color: #C9A84C; background: rgba(201,168,76,0.08); border-radius: 10px; padding: 12px; margin-bottom: 16px; line-height: 1.5; }
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// invented certainty, consistent with this project's own stated practice.
|
||||
import { onMount } from 'svelte';
|
||||
import { activeFamilyId } from './family.js';
|
||||
import { session } from './auth.js';
|
||||
import { listAssets, getWaqfDesignation, upsertWaqfDesignation, addWaqfBeneficiary, removeWaqfBeneficiary, estateTotal } from './db.js';
|
||||
import MaradAlMawtGuard from './MaradAlMawtGuard.svelte';
|
||||
import Disclaimer from './Disclaimer.svelte';
|
||||
@@ -14,6 +15,9 @@
|
||||
|
||||
let familyId = $state(null);
|
||||
activeFamilyId.subscribe(v => familyId = v);
|
||||
// See WassiyahGenerator.svelte for why this reads live off the session store.
|
||||
let authorId = $state(null);
|
||||
session.subscribe(v => authorId = v?.user?.id ?? null);
|
||||
|
||||
let assets = $state([]);
|
||||
let total = $state(0);
|
||||
@@ -22,7 +26,7 @@
|
||||
let mutawalli = $state('');
|
||||
let successorMutawalli = $state('');
|
||||
let beneficiaries = $state([]);
|
||||
let beneficiaryForm = $state({ name: '', relation: '', sharePercent: '' });
|
||||
let beneficiaryForm = $state({ name: '', relation: '', sharePercent: '', email: '' });
|
||||
let equalSplit = $state(true);
|
||||
let deviationAcknowledged = $state(false);
|
||||
let jurisdiction = $state('Perak');
|
||||
@@ -30,10 +34,10 @@
|
||||
let guardResult = $state(null);
|
||||
|
||||
async function refresh() {
|
||||
if (!familyId) return;
|
||||
if (!familyId || !authorId) return;
|
||||
assets = await listAssets(familyId);
|
||||
total = estateTotal(assets);
|
||||
const w = await getWaqfDesignation(familyId);
|
||||
const w = await getWaqfDesignation(familyId, authorId);
|
||||
if (w) {
|
||||
waqfId = w.id;
|
||||
corpusAssetId = w.corpusAssetId || '';
|
||||
@@ -46,10 +50,10 @@
|
||||
}
|
||||
|
||||
onMount(refresh);
|
||||
$effect(() => { familyId; refresh(); });
|
||||
$effect(() => { familyId; authorId; refresh(); });
|
||||
|
||||
async function persistDesignation() {
|
||||
waqfId = await upsertWaqfDesignation(familyId, waqfId, { corpusAssetId, mutawalli, successorMutawalli, equalSplit, jurisdiction });
|
||||
waqfId = await upsertWaqfDesignation(familyId, authorId, waqfId, { corpusAssetId, mutawalli, successorMutawalli, equalSplit, jurisdiction });
|
||||
}
|
||||
|
||||
const corpusAsset = $derived(assets.find(a => a.id === corpusAssetId));
|
||||
@@ -58,7 +62,7 @@
|
||||
if (!beneficiaryForm.name) return;
|
||||
if (!waqfId) await persistDesignation();
|
||||
await addWaqfBeneficiary(waqfId, beneficiaryForm);
|
||||
beneficiaryForm = { name: '', relation: '', sharePercent: '' };
|
||||
beneficiaryForm = { name: '', relation: '', sharePercent: '', email: '' };
|
||||
await refresh();
|
||||
}
|
||||
|
||||
@@ -148,6 +152,7 @@
|
||||
<label class="field"><span>Beneficiary name</span><input type="text" bind:value={beneficiaryForm.name} /></label>
|
||||
<label class="field"><span>Relation</span><input type="text" bind:value={beneficiaryForm.relation} /></label>
|
||||
<label class="field"><span>Share % (if not equal split)</span><input type="number" min="0" max="100" bind:value={beneficiaryForm.sharePercent} /></label>
|
||||
<label class="field"><span>Email (optional)</span><input type="email" bind:value={beneficiaryForm.email} placeholder="notified automatically when your trigger fires" /></label>
|
||||
<button class="btn-primary" onclick={addBeneficiary} disabled={!equalSplit && !deviationAcknowledged}>Add beneficiary</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
<script>
|
||||
// The mutawalli's (trustee/agent's) execution surface: every family member's
|
||||
// Wassiyah/Waqf documents in one place, plus each member's own death trigger.
|
||||
// Firing a trigger is enforced server-side by RLS — an agent/owner can fire
|
||||
// ANY other member's trigger, but never their own (nf_member_triggers policy).
|
||||
import { onMount } from 'svelte';
|
||||
import { activeFamilyId, listFamilyMembers, listMyFamilies } from './family.js';
|
||||
import { currentUser } from './auth.js';
|
||||
import {
|
||||
listAllWassiyahForFamily, listAllWaqfForFamily,
|
||||
getMemberTrigger, upsertMemberTriggerSetup, fireMemberTrigger,
|
||||
listMemberAttestors, addMemberAttestor, updateMemberAttestorName, setMemberAttestorConfirmed,
|
||||
notifyHeirs
|
||||
} from './db.js';
|
||||
import InfoPanel from './InfoPanel.svelte';
|
||||
import Disclaimer from './Disclaimer.svelte';
|
||||
|
||||
let familyId = $state(null);
|
||||
activeFamilyId.subscribe(v => familyId = v);
|
||||
|
||||
let myRole = $state(null);
|
||||
let members = $state([]); // active members with user_id
|
||||
let selectedMemberId = $state(null);
|
||||
let wassiyahByAuthor = $state({});
|
||||
let waqfByAuthor = $state({});
|
||||
|
||||
let trigger = $state(null);
|
||||
let attestors = $state([]);
|
||||
let executorName = $state('');
|
||||
let dateOfDeath = $state('');
|
||||
let deathCertRef = $state('');
|
||||
let error = $state('');
|
||||
let notifyStatus = $state('');
|
||||
|
||||
async function refresh() {
|
||||
if (!familyId) return;
|
||||
const [allMembers, myFamilies, wassiyah, waqf] = await Promise.all([
|
||||
listFamilyMembers(familyId), listMyFamilies(), listAllWassiyahForFamily(familyId), listAllWaqfForFamily(familyId)
|
||||
]);
|
||||
myRole = myFamilies.find(f => f.id === familyId)?.role;
|
||||
members = allMembers.filter(m => m.status === 'active');
|
||||
|
||||
// Build on a plain object, then assign once — mutating a $state proxy via
|
||||
// (proxy[key] ??= []).push(x) pushes onto the detached RHS array literal
|
||||
// that ??= evaluates to, not the proxy-wrapped array Svelte actually
|
||||
// tracks, so the UI silently never sees the pushed items.
|
||||
const wassiyahGrouped = {};
|
||||
for (const b of wassiyah) (wassiyahGrouped[b.author_id] ??= []).push(b);
|
||||
wassiyahByAuthor = wassiyahGrouped;
|
||||
const waqfGrouped = {};
|
||||
for (const w of waqf) (waqfGrouped[w.author_id] ??= []).push(w);
|
||||
waqfByAuthor = waqfGrouped;
|
||||
|
||||
if (!selectedMemberId && members.length) selectedMemberId = members[0].user_id;
|
||||
if (selectedMemberId) await loadMemberTrigger(selectedMemberId);
|
||||
}
|
||||
|
||||
onMount(refresh);
|
||||
$effect(() => { familyId; refresh(); });
|
||||
|
||||
async function loadMemberTrigger(memberId) {
|
||||
trigger = await getMemberTrigger(memberId, familyId);
|
||||
executorName = trigger?.executor_name || '';
|
||||
dateOfDeath = trigger?.date_of_death || '';
|
||||
deathCertRef = trigger?.death_cert_ref || '';
|
||||
attestors = await listMemberAttestors(memberId, familyId);
|
||||
while (attestors.length < 2) {
|
||||
const row = await addMemberAttestor(memberId, familyId, '');
|
||||
attestors = [...attestors, { id: row.id, name: '', confirmed: false }];
|
||||
}
|
||||
}
|
||||
|
||||
function selectMember(id) {
|
||||
selectedMemberId = id;
|
||||
loadMemberTrigger(id);
|
||||
}
|
||||
|
||||
async function saveField(column, value) {
|
||||
await upsertMemberTriggerSetup(selectedMemberId, familyId, { executorName, dateOfDeath, deathCertRef, [column]: value });
|
||||
}
|
||||
|
||||
async function saveAttestorName(a) {
|
||||
await updateMemberAttestorName(a.id, a.name);
|
||||
}
|
||||
async function toggleConfirm(a) {
|
||||
a.confirmed = !a.confirmed;
|
||||
attestors = [...attestors];
|
||||
await setMemberAttestorConfirmed(a.id, a.confirmed);
|
||||
}
|
||||
|
||||
const confirmedCount = $derived(attestors.filter(a => a.confirmed && a.name).length);
|
||||
const threshold = 2;
|
||||
const isSelf = $derived(selectedMemberId === currentUser()?.id);
|
||||
const canFire = $derived(confirmedCount >= threshold && deathCertRef && dateOfDeath && !isSelf && (myRole === 'agent' || myRole === 'owner'));
|
||||
|
||||
async function fire() {
|
||||
error = '';
|
||||
try {
|
||||
await upsertMemberTriggerSetup(selectedMemberId, familyId, { executorName, dateOfDeath, deathCertRef });
|
||||
await fireMemberTrigger(selectedMemberId, familyId, currentUser()?.id);
|
||||
trigger = await getMemberTrigger(selectedMemberId, familyId);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendHeirNotifications() {
|
||||
notifyStatus = 'Sending…';
|
||||
try {
|
||||
await notifyHeirs(selectedMemberId);
|
||||
notifyStatus = 'Sent.';
|
||||
} catch (e) {
|
||||
notifyStatus = 'Failed: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function selectedEmail() {
|
||||
return members.find(m => m.user_id === selectedMemberId)?.invited_email || '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="module">
|
||||
<div class="module-header">
|
||||
<h2>Mutawalli</h2>
|
||||
<InfoPanel
|
||||
title="Mutawalli"
|
||||
what="The trustee's execution surface — every family member's Wassiyah and Waqf in one place, plus a per-member death trigger. Only visible if you're the agent/mutawalli or the head of family."
|
||||
how="Pick a member from the list, review their documents, set up attestors and a death certificate reference, and fire their trigger when confirmed. You can never fire your own trigger from here — that's enforced by the system, not just hidden."
|
||||
fields={[]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if myRole !== 'agent' && myRole !== 'owner'}
|
||||
<p class="empty">Only the mutawalli (agent) or head of family can access this dashboard.</p>
|
||||
{:else}
|
||||
<div class="member-list">
|
||||
{#each members as m}
|
||||
<button class="member-chip" class:active={m.user_id === selectedMemberId} onclick={() => selectMember(m.user_id)}>
|
||||
{m.invited_email} <span class="role-tag">{m.role}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if selectedMemberId}
|
||||
<div class="doc-summary">
|
||||
<h3>{selectedEmail()}'s documents</h3>
|
||||
<div class="doc-block">
|
||||
<span class="doc-label">Wassiyah bequests</span>
|
||||
{#each wassiyahByAuthor[selectedMemberId] || [] as b}
|
||||
<div class="doc-row">{b.recipient} ({b.relation || '—'}) — {Number(b.value).toLocaleString()}{b.recipient_email ? ` · ${b.recipient_email}` : ''}</div>
|
||||
{:else}<div class="doc-empty">None recorded</div>{/each}
|
||||
</div>
|
||||
<div class="doc-block">
|
||||
<span class="doc-label">Waqf designations</span>
|
||||
{#each waqfByAuthor[selectedMemberId] || [] as w}
|
||||
<div class="doc-row">Mutawalli: {w.mutawalli || '—'} · {(w.nf_waqf_beneficiaries || []).length} beneficiaries</div>
|
||||
{:else}<div class="doc-empty">None recorded</div>{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trigger-card" class:triggered={trigger?.triggered}>
|
||||
<h3>Death trigger — {selectedEmail()}</h3>
|
||||
{#if isSelf}
|
||||
<p class="self-note">This is your own trigger. You cannot fire it for yourself — another agent/owner or the head of family must.</p>
|
||||
{/if}
|
||||
{#if trigger?.triggered}
|
||||
<div class="triggered-banner">Triggered {trigger.triggered_at?.slice(0, 10)}.</div>
|
||||
<button class="btn-secondary" onclick={sendHeirNotifications}>Notify heirs by email</button>
|
||||
{#if notifyStatus}<p class="notify-status">{notifyStatus}</p>{/if}
|
||||
{:else}
|
||||
<label class="field"><span>Executor name</span><input type="text" bind:value={executorName} oninput={e => saveField('executor_name', e.target.value)} /></label>
|
||||
<div class="attestors">
|
||||
{#each attestors as a}
|
||||
<div class="attestor-row">
|
||||
<input type="text" placeholder="Attestor name" bind:value={a.name} onblur={() => saveAttestorName(a)} />
|
||||
<button class="confirm-btn" class:on={a.confirmed} onclick={() => toggleConfirm(a)}>{a.confirmed ? 'Confirmed' : 'Confirm death'}</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<label class="field"><span>Date of death</span><input type="date" bind:value={dateOfDeath} oninput={e => saveField('date_of_death', e.target.value)} /></label>
|
||||
<label class="field"><span>Death certificate reference</span><input type="text" bind:value={deathCertRef} oninput={e => saveField('death_cert_ref', e.target.value)} /></label>
|
||||
{#if error}<p class="error-text">{error}</p>{/if}
|
||||
<button class="btn-danger-solid" disabled={!canFire} onclick={fire}>Fire trigger for {selectedEmail()}</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Disclaimer text="Firing a trigger here does not itself move money or transfer title — see the Trigger tab's execution packets for what actually happens with each covered asset." />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.module { padding: 4px 0 40px; }
|
||||
.module-header { display: flex; align-items: center; margin-bottom: 12px; }
|
||||
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 0; }
|
||||
.empty { font-size: 13px; color: #8A8478; text-align: center; padding: 30px 0; }
|
||||
.member-list { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; }
|
||||
.member-chip { padding: 8px 12px; border-radius: 20px; border: 1px solid rgba(201,168,76,0.2); background: rgba(255,255,255,0.04); color: #B8B2A6; font-size: 12px; cursor: pointer; }
|
||||
.member-chip.active { background: rgba(201,168,76,0.15); color: #C9A84C; border-color: rgba(201,168,76,0.4); }
|
||||
.role-tag { font-size: 9px; text-transform: uppercase; opacity: 0.7; }
|
||||
.doc-summary { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 16px; }
|
||||
.doc-summary h3 { font-size: 14px; color: #C9A84C; margin-bottom: 10px; }
|
||||
.doc-block { margin-bottom: 10px; }
|
||||
.doc-label { display: block; font-size: 10.5px; text-transform: uppercase; color: #8A8478; margin-bottom: 4px; }
|
||||
.doc-row { font-size: 12.5px; color: #E8E4DC; padding: 3px 0; }
|
||||
.doc-empty { font-size: 12px; color: #8A8478; font-style: italic; }
|
||||
.trigger-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; }
|
||||
.trigger-card.triggered { background: rgba(239,68,68,0.06); }
|
||||
.trigger-card h3 { font-size: 14px; color: #C9A84C; margin-bottom: 10px; }
|
||||
.self-note { font-size: 12px; color: #EF4444; background: rgba(239,68,68,0.08); border-radius: 8px; padding: 10px; margin-bottom: 10px; line-height: 1.5; }
|
||||
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
|
||||
.field span { font-size: 12px; color: #B8B2A6; }
|
||||
.field input { background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 10px 12px; color: #E8E4DC; font-size: 14px; width: 100%; }
|
||||
.attestors { margin-bottom: 12px; }
|
||||
.attestor-row { display: flex; gap: 8px; margin-bottom: 8px; }
|
||||
.attestor-row input { flex: 1; background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 8px 10px; color: #E8E4DC; font-size: 13px; }
|
||||
.confirm-btn { padding: 8px 12px; border-radius: 8px; border: 1px solid rgba(239,68,68,0.3); background: rgba(239,68,68,0.1); color: #EF4444; font-size: 11px; cursor: pointer; white-space: nowrap; }
|
||||
.confirm-btn.on { border-color: rgba(46,204,113,0.4); background: rgba(46,204,113,0.15); color: #2ECC71; }
|
||||
.btn-danger-solid { width: 100%; padding: 14px; border-radius: 8px; border: none; background: #EF4444; color: white; font-weight: 700; cursor: pointer; }
|
||||
.btn-danger-solid:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
.btn-secondary { width: 100%; padding: 12px; border-radius: 8px; border: none; background: rgba(255,255,255,0.08); color: #E8E4DC; font-weight: 600; cursor: pointer; }
|
||||
.triggered-banner { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 10px; padding: 12px; font-size: 13px; color: #E8E4DC; margin-bottom: 12px; }
|
||||
.notify-status { font-size: 12px; color: #B8B2A6; margin-top: 8px; }
|
||||
.error-text { font-size: 12px; color: #EF4444; margin-bottom: 8px; }
|
||||
</style>
|
||||
@@ -2,29 +2,36 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { oneThirdCap, isQuranicHeirRelation } from './calc/faraid.js';
|
||||
import { activeFamilyId } from './family.js';
|
||||
import { session } from './auth.js';
|
||||
import { listAssets, estateTotal, listWassiyahBequests, addWassiyahBequest, removeWassiyahBequest, getWassiyahSettings, upsertWassiyahSettings } from './db.js';
|
||||
import Disclaimer from './Disclaimer.svelte';
|
||||
import InfoPanel from './InfoPanel.svelte';
|
||||
|
||||
let familyId = $state(null);
|
||||
activeFamilyId.subscribe(v => familyId = v);
|
||||
// Read live off the session store rather than snapshotting currentUser() once
|
||||
// at mount — components here get destroyed/recreated on every tab switch, and
|
||||
// a stale/null snapshot at one particular remount would silently break every
|
||||
// write until the next remount.
|
||||
let authorId = $state(null);
|
||||
session.subscribe(v => authorId = v?.user?.id ?? null);
|
||||
|
||||
let total = $state(0);
|
||||
let cap = $state(0);
|
||||
let jurisdiction = $state('UK');
|
||||
let bequests = $state([]);
|
||||
let form = $state({ recipient: '', relation: '', description: '', value: '' });
|
||||
let form = $state({ recipient: '', relation: '', description: '', value: '', recipientEmail: '' });
|
||||
let witness1 = $state('');
|
||||
let witness2 = $state('');
|
||||
let overrideAcknowledged = $state(false);
|
||||
|
||||
async function refresh() {
|
||||
if (!familyId) return;
|
||||
if (!familyId || !authorId) return;
|
||||
const assets = await listAssets(familyId);
|
||||
total = estateTotal(assets);
|
||||
cap = oneThirdCap(total);
|
||||
bequests = await listWassiyahBequests(familyId);
|
||||
const settings = await getWassiyahSettings(familyId);
|
||||
bequests = await listWassiyahBequests(familyId, authorId);
|
||||
const settings = await getWassiyahSettings(familyId, authorId);
|
||||
if (settings) {
|
||||
jurisdiction = settings.jurisdiction || 'UK';
|
||||
witness1 = settings.witness1 || '';
|
||||
@@ -33,10 +40,10 @@
|
||||
}
|
||||
|
||||
onMount(refresh);
|
||||
$effect(() => { familyId; refresh(); });
|
||||
$effect(() => { familyId; authorId; refresh(); });
|
||||
|
||||
async function saveSettings() {
|
||||
await upsertWassiyahSettings(familyId, { jurisdiction, witness1, witness2 });
|
||||
await upsertWassiyahSettings(familyId, authorId, { jurisdiction, witness1, witness2 });
|
||||
}
|
||||
|
||||
const bequestTotal = $derived(bequests.reduce((s, b) => s + Number(b.value), 0));
|
||||
@@ -45,8 +52,8 @@
|
||||
|
||||
async function addBequest() {
|
||||
if (!form.recipient || !form.value) return;
|
||||
await addWassiyahBequest(familyId, form);
|
||||
form = { recipient: '', relation: '', description: '', value: '' };
|
||||
await addWassiyahBequest(familyId, authorId, form);
|
||||
form = { recipient: '', relation: '', description: '', value: '', recipientEmail: '' };
|
||||
await refresh();
|
||||
}
|
||||
|
||||
@@ -127,6 +134,7 @@
|
||||
<label class="field"><span>Relation to you</span><input type="text" bind:value={form.relation} placeholder="e.g. nephew, charity, friend — not spouse/child/parent/sibling" /></label>
|
||||
<label class="field"><span>Description</span><input type="text" bind:value={form.description} /></label>
|
||||
<label class="field"><span>Value</span><input type="number" min="0" bind:value={form.value} /></label>
|
||||
<label class="field"><span>Recipient email (optional)</span><input type="email" bind:value={form.recipientEmail} placeholder="notified automatically when your trigger fires" /></label>
|
||||
{#if blockedRecipient}
|
||||
<p class="block-error">This relation ("{form.relation}") matches an existing Quranic heir. You cannot bequeath to an existing heir via wassiyah — the fixed Faraid shares already govern their portion. This entry is blocked.</p>
|
||||
{:else}
|
||||
|
||||
+80
-17
@@ -66,18 +66,18 @@ export async function removeHibahGift(id) {
|
||||
}
|
||||
|
||||
// ── Waqf ──
|
||||
export async function getWaqfDesignation(familyId) {
|
||||
const { data, error } = await supabase.from('nf_waqf_designations').select('*').eq('family_id', familyId).order('created_at', { ascending: false }).limit(1).maybeSingle();
|
||||
export async function getWaqfDesignation(familyId, authorId) {
|
||||
const { data, error } = await supabase.from('nf_waqf_designations').select('*').eq('family_id', familyId).eq('author_id', authorId).order('created_at', { ascending: false }).limit(1).maybeSingle();
|
||||
if (error) throw error;
|
||||
if (!data) return null;
|
||||
const { data: beneficiaries } = await supabase.from('nf_waqf_beneficiaries').select('*').eq('waqf_id', data.id);
|
||||
return {
|
||||
id: data.id, corpusAssetId: data.corpus_asset_id, mutawalli: data.mutawalli,
|
||||
successorMutawalli: data.successor_mutawalli, equalSplit: data.equal_split, jurisdiction: data.jurisdiction,
|
||||
beneficiaries: (beneficiaries || []).map(b => ({ id: b.id, name: b.name, relation: b.relation, sharePercent: b.share_percent }))
|
||||
beneficiaries: (beneficiaries || []).map(b => ({ id: b.id, name: b.name, relation: b.relation, sharePercent: b.share_percent, email: b.beneficiary_email }))
|
||||
};
|
||||
}
|
||||
export async function upsertWaqfDesignation(familyId, existingId, fields) {
|
||||
export async function upsertWaqfDesignation(familyId, authorId, existingId, fields) {
|
||||
if (existingId) {
|
||||
const { error } = await supabase.from('nf_waqf_designations').update({
|
||||
corpus_asset_id: fields.corpusAssetId || null, mutawalli: fields.mutawalli, successor_mutawalli: fields.successorMutawalli,
|
||||
@@ -87,16 +87,23 @@ export async function upsertWaqfDesignation(familyId, existingId, fields) {
|
||||
return existingId;
|
||||
}
|
||||
const { data, error } = await supabase.from('nf_waqf_designations').insert({
|
||||
family_id: familyId, corpus_asset_id: fields.corpusAssetId || null, mutawalli: fields.mutawalli,
|
||||
family_id: familyId, author_id: authorId, corpus_asset_id: fields.corpusAssetId || null, mutawalli: fields.mutawalli,
|
||||
successor_mutawalli: fields.successorMutawalli, equal_split: fields.equalSplit, jurisdiction: fields.jurisdiction
|
||||
}).select().single();
|
||||
if (error) throw error;
|
||||
return data.id;
|
||||
}
|
||||
export async function addWaqfBeneficiary(waqfId, b) {
|
||||
const { error } = await supabase.from('nf_waqf_beneficiaries').insert({ waqf_id: waqfId, name: b.name, relation: b.relation, share_percent: b.sharePercent || null });
|
||||
const { error } = await supabase.from('nf_waqf_beneficiaries').insert({ waqf_id: waqfId, name: b.name, relation: b.relation, share_percent: b.sharePercent || null, beneficiary_email: b.email || null });
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
/** For the mutawalli dashboard: every family member's Waqf designations. */
|
||||
export async function listAllWaqfForFamily(familyId) {
|
||||
const { data, error } = await supabase.from('nf_waqf_designations').select('*, nf_waqf_beneficiaries(*)').eq('family_id', familyId).order('created_at');
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
}
|
||||
export async function removeWaqfBeneficiary(id) {
|
||||
const { error } = await supabase.from('nf_waqf_beneficiaries').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
@@ -187,15 +194,16 @@ export function estateTotal(assets) {
|
||||
return (assets || []).reduce((sum, a) => sum + (Number(a.value) || 0) * (Number(a.ownershipShare ?? 100) / 100), 0);
|
||||
}
|
||||
|
||||
// ── Wassiyah ──
|
||||
export async function listWassiyahBequests(familyId) {
|
||||
const { data, error } = await supabase.from('nf_wassiyah_bequests').select('*').eq('family_id', familyId).order('created_at');
|
||||
// ── Wassiyah — per-author: each signed-in family member has their own ──
|
||||
export async function listWassiyahBequests(familyId, authorId) {
|
||||
const { data, error } = await supabase.from('nf_wassiyah_bequests').select('*').eq('family_id', familyId).eq('author_id', authorId).order('created_at');
|
||||
if (error) throw error;
|
||||
return (data || []).map(b => ({ id: b.id, recipient: b.recipient, relation: b.relation, description: b.description, value: b.value }));
|
||||
return (data || []).map(b => ({ id: b.id, recipient: b.recipient, relation: b.relation, description: b.description, value: b.value, recipientEmail: b.recipient_email }));
|
||||
}
|
||||
export async function addWassiyahBequest(familyId, b) {
|
||||
export async function addWassiyahBequest(familyId, authorId, b) {
|
||||
const { error } = await supabase.from('nf_wassiyah_bequests').insert({
|
||||
family_id: familyId, recipient: b.recipient, relation: b.relation, description: b.description, value: Number(b.value)
|
||||
family_id: familyId, author_id: authorId, recipient: b.recipient, relation: b.relation,
|
||||
description: b.description, value: Number(b.value), recipient_email: b.recipientEmail || null
|
||||
});
|
||||
if (error) throw error;
|
||||
}
|
||||
@@ -203,15 +211,70 @@ export async function removeWassiyahBequest(id) {
|
||||
const { error } = await supabase.from('nf_wassiyah_bequests').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
export async function getWassiyahSettings(familyId) {
|
||||
const { data, error } = await supabase.from('nf_wassiyah_settings').select('*').eq('family_id', familyId).maybeSingle();
|
||||
export async function getWassiyahSettings(familyId, authorId) {
|
||||
const { data, error } = await supabase.from('nf_wassiyah_settings').select('*').eq('family_id', familyId).eq('author_id', authorId).maybeSingle();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
export async function upsertWassiyahSettings(familyId, fields) {
|
||||
export async function upsertWassiyahSettings(familyId, authorId, fields) {
|
||||
const { error } = await supabase.from('nf_wassiyah_settings').upsert({
|
||||
family_id: familyId, jurisdiction: fields.jurisdiction, witness1: fields.witness1, witness2: fields.witness2,
|
||||
family_id: familyId, author_id: authorId, jurisdiction: fields.jurisdiction, witness1: fields.witness1, witness2: fields.witness2,
|
||||
updated_at: new Date().toISOString()
|
||||
}, { onConflict: 'family_id' });
|
||||
}, { onConflict: 'family_id,author_id' });
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
/** For the mutawalli dashboard: every family member's Wassiyah bequests, grouped by author. */
|
||||
export async function listAllWassiyahForFamily(familyId) {
|
||||
const { data, error } = await supabase.from('nf_wassiyah_bequests').select('*').eq('family_id', familyId).order('created_at');
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
}
|
||||
|
||||
// ── Per-member death trigger — the mutawalli/owner fires it for a specific
|
||||
// member, never the member themselves (enforced by RLS, not just the UI). ──
|
||||
export async function getMemberTrigger(memberId, familyId) {
|
||||
const { data, error } = await supabase.from('nf_member_triggers').select('*').eq('member_id', memberId).eq('family_id', familyId).maybeSingle();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
export async function upsertMemberTriggerSetup(memberId, familyId, fields) {
|
||||
const { error } = await supabase.from('nf_member_triggers').upsert({
|
||||
member_id: memberId, family_id: familyId, executor_name: fields.executorName,
|
||||
date_of_death: fields.dateOfDeath || null, death_cert_ref: fields.deathCertRef, updated_at: new Date().toISOString()
|
||||
}, { onConflict: 'family_id,member_id' });
|
||||
if (error) throw error;
|
||||
}
|
||||
/** Only succeeds (per RLS) if the caller is an agent/owner on this family and is NOT the member themselves. */
|
||||
export async function fireMemberTrigger(memberId, familyId, firedBy) {
|
||||
const { error } = await supabase.from('nf_member_triggers').upsert({
|
||||
member_id: memberId, family_id: familyId, triggered: true, triggered_at: new Date().toISOString(),
|
||||
fired_by: firedBy, updated_at: new Date().toISOString()
|
||||
}, { onConflict: 'family_id,member_id' });
|
||||
if (error) throw error;
|
||||
}
|
||||
export async function listMemberAttestors(memberId, familyId) {
|
||||
const { data, error } = await supabase.from('nf_member_attestors').select('*').eq('member_id', memberId).eq('family_id', familyId).order('created_at');
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
}
|
||||
export async function addMemberAttestor(memberId, familyId, name) {
|
||||
const { data, error } = await supabase.from('nf_member_attestors').insert({ member_id: memberId, family_id: familyId, name }).select().single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
export async function updateMemberAttestorName(id, name) {
|
||||
const { error } = await supabase.from('nf_member_attestors').update({ name }).eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
export async function setMemberAttestorConfirmed(id, confirmed) {
|
||||
const { error } = await supabase.from('nf_member_attestors').update({ confirmed, confirmed_at: confirmed ? new Date().toISOString() : null }).eq('id', id);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
/** Sends the heir notification email via the notify-heirs Edge Function. */
|
||||
export async function notifyHeirs(memberId) {
|
||||
const { data, error } = await supabase.functions.invoke('notify-heirs', { body: { memberId } });
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
+12
-1
@@ -79,10 +79,21 @@ export async function inviteAgent(familyId, email) {
|
||||
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, invited_email, role, status')
|
||||
.select('id, user_id, invited_email, role, status')
|
||||
.eq('family_id', familyId);
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
|
||||
@@ -23,12 +23,17 @@
|
||||
|
||||
// Takes already-fetched data rather than loading it itself — the caller (now
|
||||
// Supabase-backed via db.js instead of localStorage) owns the fetch.
|
||||
export function computeCoverage({ assets = [], gifts = [], nominations = [], waqfCorpusId = null } = {}) {
|
||||
export function computeCoverage({ assets = [], gifts = [], nominations = [], waqfCorpusId = null, waqfCorpusIds = null } = {}) {
|
||||
// waqfCorpusIds covers the per-member model: ANY family member's waqf
|
||||
// designation counts for coverage, not just one author's. waqfCorpusId
|
||||
// (singular) is kept for backward compatibility with any caller still
|
||||
// passing a single id.
|
||||
const corpusIdSet = new Set(waqfCorpusIds || (waqfCorpusId ? [waqfCorpusId] : []));
|
||||
const rows = assets.map(a => {
|
||||
const ownedValue = (Number(a.value) || 0) * (Number(a.ownershipShare ?? 100) / 100);
|
||||
const hibahMatch = gifts.find(g => g.linkedAssetId === a.id);
|
||||
const nominationMatch = nominations.find(n => n.linkedAssetId === a.id);
|
||||
const isWaqfCorpus = waqfCorpusId === a.id;
|
||||
const isWaqfCorpus = corpusIdSet.has(a.id);
|
||||
|
||||
let channel = 'none';
|
||||
let fast = false;
|
||||
|
||||
Reference in New Issue
Block a user