feat: add Khairat, emergency-contact auto-notify, and 5 lifestyle-companion tabs
Big round covering two different asks: Khairat/emergency infrastructure (tightly coupled to what's already built) and a Muslim-lifestyle companion surface (a genuinely different product, added at the user's explicit request after being offered a smaller scope). Khairat & emergency contacts: - nf_khairat_memberships (per-member scheme membership) + nf_emergency_fund (per-family shared reserve tracker) — records intent/contacts only, never holds or moves real money. - nf_trusted_contacts gains category (mosque/police/ambulance/hospital/ khairat/family/other) and email columns. - New notify-emergency-contacts Edge Function, auto-invoked the moment a mutawalli fires a member's death trigger (the one place in this app where auto-send-on-trigger is actually correct), alongside — not instead of — the existing heir notification. Verified with a live fire end-to-end (e2e-emergency-notify.cjs, 5/5). Lifestyle-companion tabs — all computed/searched live, nothing fabricated: - Qibla: pure great-circle bearing math to the Kaaba from geolocation, no API/key. Verified against Kuala Lumpur (293°, matches expected ~292°). - Prayer Times: client-side astronomical calculation (single-pass solar position, MWL angles), sanity-checked against known KL/London times before shipping. - Locate: mosque/halal/cemetery search via the free, keyless OpenStreetMap Overpass API — real community-sourced results only, honest empty state when nothing's mapped nearby. - Quran: Surah list + Arabic/translation via the free alquran.cloud API, fetched fresh each time, nothing stored in this app's own database. - Neighbourhood: a join-by-code community announcement board — a genuinely separate multi-tenant concept from the estate-planning family structure, scoped to the user account. Found and fixed a real UX gap during testing: the create/join form was only reachable with zero existing neighbourhoods, with no way to join a second one. Also found and fixed a real bug: the heir-notify and emergency-notify status messages shared the same CSS class, breaking any script (including the pre-existing e2e-per-member.cjs) that targeted '.notify-status' without further filtering — gave each its own distinguishing class. Covered by e2e-khairat-lifestyle.cjs (13/13) and e2e-emergency-notify.cjs (5/5). Full regression: 316/316 across all suites (several transient flakes under heavy mail-relay load during the sweep, all confirmed clean on rerun — one led to the real class-collision fix above).
This commit is contained in:
@@ -23,7 +23,8 @@
|
||||
|
||||
let form = $state(emptyForm());
|
||||
let editingId = $state(null);
|
||||
let contactForm = $state({ name: '', method: '' });
|
||||
let contactForm = $state({ name: '', method: '', category: 'family', email: '' });
|
||||
const CONTACT_CATEGORIES = ['family', 'mosque', 'khairat', 'police', 'ambulance', 'hospital', 'other'];
|
||||
let liabilityForm = $state(emptyLiabilityForm());
|
||||
let editingLiabilityId = $state(null);
|
||||
|
||||
@@ -74,8 +75,8 @@
|
||||
|
||||
async function addContact() {
|
||||
if (!contactForm.name) return;
|
||||
await addTrustedContact(familyId, contactForm.name, contactForm.method);
|
||||
contactForm = { name: '', method: '' };
|
||||
await addTrustedContact(familyId, contactForm.name, contactForm.method, contactForm.category, contactForm.email);
|
||||
contactForm = { name: '', method: '', category: 'family', email: '' };
|
||||
await refresh();
|
||||
}
|
||||
|
||||
@@ -226,14 +227,20 @@
|
||||
|
||||
<div class="contacts-section">
|
||||
<h3>Trusted contacts</h3>
|
||||
<p class="note">Notified or given a read-only export on a triggering event — not an automated death-detection or legal-transfer mechanism.</p>
|
||||
<p class="note">Notified automatically by email when your mutawalli fires your death trigger — mosque, khairat officer, police, ambulance/hospital, or family. Not an automated death-detection mechanism.</p>
|
||||
<div class="form-card">
|
||||
<label class="field"><span>Name</span><input type="text" bind:value={contactForm.name} /></label>
|
||||
<label class="field"><span>Contact method</span><input type="text" bind:value={contactForm.method} placeholder="email or phone" /></label>
|
||||
<label class="field"><span>Category</span>
|
||||
<select bind:value={contactForm.category}>
|
||||
{#each CONTACT_CATEGORIES as cat}<option value={cat}>{cat}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="field"><span>Contact method (phone, etc.)</span><input type="text" bind:value={contactForm.method} placeholder="e.g. phone number" /></label>
|
||||
<label class="field"><span>Email (for automatic notification)</span><input type="email" bind:value={contactForm.email} placeholder="required to auto-notify this contact" /></label>
|
||||
<button class="btn-secondary" onclick={addContact}>Add trusted contact</button>
|
||||
</div>
|
||||
{#each trustedContacts as c (c.id)}
|
||||
<div class="contact-row"><span>{c.name} — {c.method}</span><button onclick={() => removeContact(c.id)}>✕</button></div>
|
||||
<div class="contact-row"><span><span class="contact-category">{c.category}</span> {c.name} — {c.method}{c.email ? ` · ${c.email}` : ''}</span><button onclick={() => removeContact(c.id)}>✕</button></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -332,6 +339,7 @@
|
||||
.contacts-section h3 { font-size: 15px; color: #C9A84C; margin-bottom: 4px; }
|
||||
.note { font-size: 11.5px; color: #8A8478; margin-bottom: 12px; }
|
||||
.contact-row { display: flex; justify-content: space-between; padding: 8px 0; font-size: 13px; color: #E8E4DC; border-bottom: 1px solid rgba(255,255,255,0.06); }
|
||||
.contact-category { font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px; color: #C9A84C; background: rgba(201,168,76,0.1); padding: 1px 6px; border-radius: 4px; margin-right: 6px; }
|
||||
.verify-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 0 0 12px; margin-top: -6px; border-bottom: 1px solid rgba(255,255,255,0.06); }
|
||||
.verify-badge { font-size: 10.5px; padding: 2px 8px; border-radius: 999px; background: rgba(255,255,255,0.06); color: #8A8478; }
|
||||
.verify-badge.verified { background: rgba(46,204,113,0.15); color: #2ECC71; }
|
||||
|
||||
Reference in New Issue
Block a user