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).
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { activeFamilyId } from './family.js';
|
||||
import { activeFamilyId, getFamilyJurisdiction } from './family.js';
|
||||
import { session } from './auth.js';
|
||||
import { getZakatRecord, upsertZakatRecord } from './db.js';
|
||||
import { zakatableWealth, zakatDue, meetsNisab } from './calc/zakat.js';
|
||||
import { CURRENCY_BY_JURISDICTION, DEFAULT_NISAB_BY_JURISDICTION } from './calc/zakat.js';
|
||||
import Disclaimer from './Disclaimer.svelte';
|
||||
import InfoPanel from './InfoPanel.svelte';
|
||||
|
||||
@@ -16,21 +17,23 @@
|
||||
let memberId = $state(null);
|
||||
session.subscribe(v => memberId = v?.user?.id ?? null);
|
||||
|
||||
const DEFAULT_NISAB = 24000; // rough 85g-gold-equivalent placeholder in RM; user should override with today's value
|
||||
let currency = $state('RM');
|
||||
|
||||
let fields = $state(emptyFields());
|
||||
let loaded = $state(false);
|
||||
|
||||
function emptyFields() {
|
||||
return { cash: '', gold: '', silver: '', businessAssets: '', investments: '', otherZakatable: '', deductibleLiabilities: '', nisabThreshold: String(DEFAULT_NISAB) };
|
||||
function emptyFields(jurisdiction) {
|
||||
return { cash: '', gold: '', silver: '', businessAssets: '', investments: '', otherZakatable: '', deductibleLiabilities: '', nisabThreshold: String(DEFAULT_NISAB_BY_JURISDICTION[jurisdiction] || 24000) };
|
||||
}
|
||||
|
||||
let fields = $state(emptyFields('MY'));
|
||||
let loaded = $state(false);
|
||||
|
||||
async function refresh() {
|
||||
if (!familyId || !memberId) return;
|
||||
const jurisdiction = await getFamilyJurisdiction(familyId);
|
||||
currency = CURRENCY_BY_JURISDICTION[jurisdiction] || 'RM';
|
||||
const record = await getZakatRecord(familyId, memberId);
|
||||
fields = record
|
||||
? { cash: String(record.cash), gold: String(record.gold), silver: String(record.silver), businessAssets: String(record.businessAssets), investments: String(record.investments), otherZakatable: String(record.otherZakatable), deductibleLiabilities: String(record.deductibleLiabilities), nisabThreshold: String(record.nisabThreshold) }
|
||||
: emptyFields();
|
||||
: emptyFields(jurisdiction);
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
@@ -62,18 +65,18 @@
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<p class="sub">Your own Zakat calculation — a personal, ongoing obligation, not tied to death or inheritance.</p>
|
||||
<p class="sub">Your own Zakat calculation — a personal, ongoing obligation, not tied to death or inheritance. Figures in {currency}.</p>
|
||||
|
||||
{#if loaded}
|
||||
<div class="form-card">
|
||||
<label class="field"><span>Cash & bank balances</span><input type="number" min="0" bind:value={fields.cash} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Gold (current market value)</span><input type="number" min="0" bind:value={fields.gold} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Silver (current market value)</span><input type="number" min="0" bind:value={fields.silver} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Business assets / inventory</span><input type="number" min="0" bind:value={fields.businessAssets} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Investments / shares / digital assets</span><input type="number" min="0" bind:value={fields.investments} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Other zakatable wealth</span><input type="number" min="0" bind:value={fields.otherZakatable} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Deductible liabilities (due within the year)</span><input type="number" min="0" bind:value={fields.deductibleLiabilities} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Nisab threshold (today's value)</span><input type="number" min="0" bind:value={fields.nisabThreshold} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Cash & bank balances ({currency})</span><input type="number" min="0" bind:value={fields.cash} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Gold — current market value ({currency})</span><input type="number" min="0" bind:value={fields.gold} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Silver — current market value ({currency})</span><input type="number" min="0" bind:value={fields.silver} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Business assets / inventory ({currency})</span><input type="number" min="0" bind:value={fields.businessAssets} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Investments / shares / digital assets ({currency})</span><input type="number" min="0" bind:value={fields.investments} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Other zakatable wealth ({currency})</span><input type="number" min="0" bind:value={fields.otherZakatable} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Deductible liabilities — due within the year ({currency})</span><input type="number" min="0" bind:value={fields.deductibleLiabilities} oninput={scheduleSave} /></label>
|
||||
<label class="field"><span>Nisab threshold — today's value ({currency})</span><input type="number" min="0" bind:value={fields.nisabThreshold} oninput={scheduleSave} /></label>
|
||||
</div>
|
||||
|
||||
<div class="result-card" class:due={meets}>
|
||||
|
||||
Reference in New Issue
Block a user