// Family-scoped data layer over Supabase. Replaces the old per-device localStorage // calls in storage.js for estate data — Asset Registry, Hibah, Waqf, Nominations, // Attestors, and the Death Trigger — so an owner and their assigned agent(s) see // and manage the same live data, with the "agent cannot fire the trigger" rule // enforced by RLS on nf_death_triggers (see the migration), not just hidden in // the UI. import { supabase } from './supabaseClient.js'; // ── Assets ── export async function listAssets(familyId) { const { data, error } = await supabase.from('nf_assets').select('*').eq('family_id', familyId).order('created_at'); if (error) throw error; return (data || []).map(a => ({ id: a.id, type: a.type, description: a.description, value: a.value, location: a.location, ownershipShare: a.ownership_share, verified: a.verified, verifiedBy: a.verified_by, verifiedAt: a.verified_at, proofDocumentPath: a.proof_document_path })); } export async function addAsset(familyId, userId, a) { const { data, error } = await supabase.from('nf_assets').insert({ family_id: familyId, type: a.type, description: a.description, value: Number(a.value), location: a.location, ownership_share: Number(a.ownershipShare ?? 100), created_by: userId }).select().single(); if (error) throw error; return data; } export async function updateAsset(id, a) { const { error } = await supabase.from('nf_assets').update({ type: a.type, description: a.description, value: Number(a.value), location: a.location, ownership_share: Number(a.ownershipShare ?? 100) }).eq('id', id); if (error) throw error; } export async function removeAsset(id) { const { error } = await supabase.from('nf_assets').delete().eq('id', id); if (error) throw error; } // ── Trusted contacts ── export async function listTrustedContacts(familyId) { const { data, error } = await supabase.from('nf_trusted_contacts').select('*').eq('family_id', familyId).order('created_at'); if (error) throw error; return data || []; } export async function addTrustedContact(familyId, name, method, category, email) { const { error } = await supabase.from('nf_trusted_contacts').insert({ family_id: familyId, name, method, category: category || 'family', email: email || null }); if (error) throw error; } export async function removeTrustedContact(id) { const { error } = await supabase.from('nf_trusted_contacts').delete().eq('id', id); if (error) throw error; } // ── Khairat — per-member scheme membership + a per-family emergency fund. ── export async function listKhairatMemberships(familyId) { const { data, error } = await supabase.from('nf_khairat_memberships').select('*').eq('family_id', familyId).order('created_at'); if (error) throw error; return (data || []).map(k => ({ id: k.id, memberId: k.member_id, schemeName: k.scheme_name, organization: k.organization, membershipNumber: k.membership_number, contactPhone: k.contact_phone, contactEmail: k.contact_email, notes: k.notes })); } export async function addKhairatMembership(familyId, memberId, createdBy, fields) { const { error } = await supabase.from('nf_khairat_memberships').insert({ family_id: familyId, member_id: memberId, created_by: createdBy, scheme_name: fields.schemeName, organization: fields.organization, membership_number: fields.membershipNumber, contact_phone: fields.contactPhone, contact_email: fields.contactEmail, notes: fields.notes }); if (error) throw error; } export async function removeKhairatMembership(id) { const { error } = await supabase.from('nf_khairat_memberships').delete().eq('id', id); if (error) throw error; } export async function getEmergencyFund(familyId) { const { data, error } = await supabase.from('nf_emergency_fund').select('*').eq('family_id', familyId).maybeSingle(); if (error) throw error; return data ? { targetAmount: data.target_amount, currentBalance: data.current_balance, notes: data.notes } : null; } export async function upsertEmergencyFund(familyId, fields) { const { error } = await supabase.from('nf_emergency_fund').upsert({ family_id: familyId, target_amount: fields.targetAmount ? Number(fields.targetAmount) : null, current_balance: Number(fields.currentBalance) || 0, notes: fields.notes || null, updated_at: new Date().toISOString() }, { onConflict: 'family_id' }); if (error) throw error; } /** Sends an emergency notice to trusted contacts in the given categories via the notify-emergency-contacts Edge Function. */ export async function notifyEmergencyContacts(memberId, familyId, categories) { const { data, error } = await supabase.functions.invoke('notify-emergency-contacts', { body: { memberId, familyId, categories } }); if (error) throw error; return data; } // ── Neighbourhood announcement board — join-by-code community, independent // of the family estate structure. ── function genJoinCode() { return Array.from({ length: 6 }, () => 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'[Math.floor(Math.random() * 32)]).join(''); } export async function createNeighbourhood(name, userId) { const joinCode = genJoinCode(); const { data, error } = await supabase.from('nf_neighbourhoods').insert({ name, join_code: joinCode, created_by: userId }).select().single(); if (error) throw error; await supabase.from('nf_neighbourhood_members').insert({ neighbourhood_id: data.id, user_id: userId }); return data; } export async function joinNeighbourhoodByCode(joinCode, userId, displayName) { const { data: n, error: findErr } = await supabase.from('nf_neighbourhoods').select('*').eq('join_code', joinCode.toUpperCase().trim()).maybeSingle(); if (findErr) throw findErr; if (!n) throw new Error('No neighbourhood found with that code.'); const { error } = await supabase.from('nf_neighbourhood_members').insert({ neighbourhood_id: n.id, user_id: userId, display_name: displayName || null }); if (error) throw error; return n; } export async function listMyNeighbourhoods(userId) { const { data, error } = await supabase.from('nf_neighbourhood_members').select('neighbourhood_id, nf_neighbourhoods(id, name, join_code)').eq('user_id', userId); if (error) throw error; return (data || []).map(r => ({ id: r.neighbourhood_id, name: r.nf_neighbourhoods?.name, joinCode: r.nf_neighbourhoods?.join_code })); } export async function leaveNeighbourhood(neighbourhoodId, userId) { const { error } = await supabase.from('nf_neighbourhood_members').delete().eq('neighbourhood_id', neighbourhoodId).eq('user_id', userId); if (error) throw error; } export async function listNeighbourhoodPosts(neighbourhoodId) { const { data, error } = await supabase.from('nf_neighbourhood_posts').select('*').eq('neighbourhood_id', neighbourhoodId).order('created_at', { ascending: false }); if (error) throw error; return data || []; } export async function addNeighbourhoodPost(neighbourhoodId, authorId, authorName, title, body) { const { error } = await supabase.from('nf_neighbourhood_posts').insert({ neighbourhood_id: neighbourhoodId, author_id: authorId, author_name: authorName, title, body }); if (error) throw error; } export async function removeNeighbourhoodPost(id) { const { error } = await supabase.from('nf_neighbourhood_posts').delete().eq('id', id); if (error) throw error; } // ── Hibah ── export async function listHibahGifts(familyId) { const { data, error } = await supabase.from('nf_hibah_gifts').select('*').eq('family_id', familyId).order('created_at'); if (error) throw error; return (data || []).map(g => ({ id: g.id, recipient: g.recipient, relation: g.relation, description: g.description, date: g.gift_date, statement: g.statement, linkedAssetId: g.linked_asset_id, flagged: g.flagged, cap: g.cap })); } export async function addHibahGift(familyId, g) { const { error } = await supabase.from('nf_hibah_gifts').insert({ family_id: familyId, recipient: g.recipient, relation: g.relation, description: g.description, gift_date: g.date, statement: g.statement, linked_asset_id: g.linkedAssetId || null, flagged: !!g.flagged, cap: g.cap ?? null, acknowledgment_link: g.acknowledgmentLink }); if (error) throw error; } export async function removeHibahGift(id) { const { error } = await supabase.from('nf_hibah_gifts').delete().eq('id', id); if (error) throw error; } // ── Waqf ── 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, email: b.beneficiary_email })) }; } 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, equal_split: fields.equalSplit, jurisdiction: fields.jurisdiction }).eq('id', existingId); if (error) throw error; return existingId; } const { data, error } = await supabase.from('nf_waqf_designations').insert({ 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, 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; } // ── Nominations ── export async function listNominations(familyId) { const { data, error } = await supabase.from('nf_nominations').select('*').eq('family_id', familyId).order('created_at'); if (error) throw error; return (data || []).map(n => ({ id: n.id, linkedAssetId: n.linked_asset_id, type: n.type, institution: n.institution, nomineeeName: n.nominee_name, referenceNumber: n.reference_number, trusteeName: n.trustee_name, successorTrustee: n.successor_trustee, trustBeneficiaries: n.trust_beneficiaries, businessStructure: n.business_structure, businessInstrument: n.business_instrument, successorOwner: n.successor_owner, buySellTerms: n.buy_sell_terms, custodyType: n.custody_type, platform: n.platform, keyHolderName: n.key_holder_name, accessInstructionsRef: n.access_instructions_ref })); } export async function addNomination(familyId, n) { const { error } = await supabase.from('nf_nominations').insert({ family_id: familyId, linked_asset_id: n.linkedAssetId || null, type: n.type, institution: n.institution, nominee_name: n.nomineeeName, reference_number: n.referenceNumber, trustee_name: n.trusteeName, successor_trustee: n.successorTrustee, trust_beneficiaries: n.trustBeneficiaries, business_structure: n.businessStructure, business_instrument: n.businessInstrument, successor_owner: n.successorOwner, buy_sell_terms: n.buySellTerms, custody_type: n.custodyType, platform: n.platform, key_holder_name: n.keyHolderName, access_instructions_ref: n.accessInstructionsRef }); if (error) throw error; } export async function removeNomination(id) { const { error } = await supabase.from('nf_nominations').delete().eq('id', id); if (error) throw error; } // ── Attestors & Death Trigger ── export async function listAttestors(familyId) { const { data, error } = await supabase.from('nf_attestors').select('*').eq('family_id', familyId).order('created_at'); if (error) throw error; return data || []; } export async function addAttestor(familyId, name) { const { data, error } = await supabase.from('nf_attestors').insert({ family_id: familyId, name }).select().single(); if (error) throw error; return data; } export async function setAttestorConfirmed(id, confirmed) { const { error } = await supabase.from('nf_attestors').update({ confirmed, confirmed_at: confirmed ? new Date().toISOString() : null }).eq('id', id); if (error) throw error; } export async function updateAttestorName(id, name) { const { error } = await supabase.from('nf_attestors').update({ name }).eq('id', id); if (error) throw error; } export async function getDeathTrigger(familyId) { const { data, error } = await supabase.from('nf_death_triggers').select('*').eq('family_id', familyId).maybeSingle(); if (error) throw error; return data; } export async function upsertDeathTriggerSetup(familyId, fields) { const { error } = await supabase.from('nf_death_triggers').upsert({ 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' }); if (error) throw error; } /** * Updates a single death-trigger setup field. Used instead of * upsertDeathTriggerSetup for per-keystroke autosave on individual inputs — * three fields each firing their own full-snapshot upsert can complete * out of order over the network, and the last one to *finish* (not the last * one to *fire*) silently clobbers the other two fields back to whatever * stale value it captured. A single-column update can't do that. */ export async function updateDeathTriggerField(familyId, column, value) { const { error } = await supabase.from('nf_death_triggers').upsert({ family_id: familyId, [column]: value, updated_at: new Date().toISOString() }, { onConflict: 'family_id' }); if (error) throw error; } /** Only succeeds (per RLS) if the caller has role='owner' on this family. */ export async function fireDeathTrigger(familyId) { const { error } = await supabase.from('nf_death_triggers').upsert({ family_id: familyId, triggered: true, triggered_at: new Date().toISOString(), updated_at: new Date().toISOString() }, { onConflict: 'family_id' }); if (error) throw error; } export function estateTotal(assets) { return (assets || []).reduce((sum, a) => sum + (Number(a.value) || 0) * (Number(a.ownershipShare ?? 100) / 100), 0); } // ── 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, recipientEmail: b.recipient_email })); } export async function addWassiyahBequest(familyId, authorId, b) { const { error } = await supabase.from('nf_wassiyah_bequests').insert({ 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; } 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, 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, authorId, fields) { const { error } = await supabase.from('nf_wassiyah_settings').upsert({ family_id: familyId, author_id: authorId, jurisdiction: fields.jurisdiction, witness1: fields.witness1, witness2: fields.witness2, testator_name: fields.testatorName, updated_at: new Date().toISOString() }, { 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 || []; } // ── Professional review workflow — a structured status, not a marketplace. // Any family member can request review of their own Wassiyah; anyone (in // practice the mutawalli/agent, coordinating with an actual lawyer outside // the app) can mark it reviewed once done. export async function requestWassiyahReview(familyId, authorId) { const { error } = await supabase.from('nf_wassiyah_settings').upsert({ family_id: familyId, author_id: authorId, review_status: 'requested', requested_at: new Date().toISOString(), updated_at: new Date().toISOString() }, { onConflict: 'family_id,author_id' }); if (error) throw error; } export async function markWassiyahReviewed(familyId, authorId, reviewerName) { const { error } = await supabase.from('nf_wassiyah_settings').upsert({ family_id: familyId, author_id: authorId, review_status: 'reviewed', reviewer_name: reviewerName, reviewed_at: new Date().toISOString(), updated_at: new Date().toISOString() }, { onConflict: 'family_id,author_id' }); if (error) throw error; } /** For the mutawalli dashboard: every family member's Wassiyah settings (jurisdiction, review status). */ export async function listAllWassiyahSettingsForFamily(familyId) { const { data, error } = await supabase.from('nf_wassiyah_settings').select('*').eq('family_id', familyId); 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, familyId) { const { data, error } = await supabase.functions.invoke('notify-heirs', { body: { memberId, familyId } }); if (error) throw error; return data; } // ── Family Genealogy ── // nf_people are lightweight — most real tree nodes (grandparents, deceased // relatives, young children) never have a login. linked_user_id is optional. const PHOTO_BUCKET = 'nf-people-photos'; export async function listPeople(familyId) { const { data, error } = await supabase.from('nf_people').select('*').eq('family_id', familyId).order('created_at'); if (error) throw error; return (data || []).map(p => ({ id: p.id, familyId: p.family_id, linkedUserId: p.linked_user_id, fullName: p.full_name, gender: p.gender, birthDate: p.birth_date, deathDate: p.death_date, notes: p.notes, photoPath: p.photo_path })); } export async function addPerson(familyId, createdBy, fields) { const { data, error } = await supabase.from('nf_people').insert({ family_id: familyId, created_by: createdBy, full_name: fields.fullName, gender: fields.gender || null, birth_date: fields.birthDate || null, death_date: fields.deathDate || null, notes: fields.notes || null }).select().single(); if (error) throw error; return data; } export async function updatePerson(id, fields) { const { error } = await supabase.from('nf_people').update({ full_name: fields.fullName, gender: fields.gender || null, birth_date: fields.birthDate || null, death_date: fields.deathDate || null, notes: fields.notes || null, updated_at: new Date().toISOString() }).eq('id', id); if (error) throw error; } export async function removePerson(id) { const { error } = await supabase.from('nf_people').delete().eq('id', id); if (error) throw error; } /** Uploads a photo for a person and stores its storage path on the record. Path: {familyId}/{personId}/{filename} so RLS can check family membership from the path alone. */ export async function uploadPersonPhoto(familyId, personId, file) { const ext = (file.name.split('.').pop() || 'jpg').toLowerCase(); const path = `${familyId}/${personId}/${Date.now()}.${ext}`; const { error: upErr } = await supabase.storage.from(PHOTO_BUCKET).upload(path, file, { upsert: true, contentType: file.type }); if (upErr) throw upErr; const { error: dbErr } = await supabase.from('nf_people').update({ photo_path: path, updated_at: new Date().toISOString() }).eq('id', personId); if (dbErr) throw dbErr; return path; } export async function removePersonPhoto(personId, photoPath) { if (photoPath) await supabase.storage.from(PHOTO_BUCKET).remove([photoPath]); const { error } = await supabase.from('nf_people').update({ photo_path: null, updated_at: new Date().toISOString() }).eq('id', personId); if (error) throw error; } /** Signed URL (private bucket) valid for 1 hour — call fresh each time the photo is displayed. */ export async function getPersonPhotoUrl(photoPath) { if (!photoPath) return null; const { data, error } = await supabase.storage.from(PHOTO_BUCKET).createSignedUrl(photoPath, 3600); if (error) return null; return data.signedUrl; } export async function listRelationships(familyId) { const { data, error } = await supabase.from('nf_relationships').select('*').eq('family_id', familyId); if (error) throw error; return (data || []).map(r => ({ id: r.id, personAId: r.person_a_id, personBId: r.person_b_id, type: r.relationship_type })); } export async function addRelationship(familyId, personAId, personBId, type) { const { error } = await supabase.from('nf_relationships').insert({ family_id: familyId, person_a_id: personAId, person_b_id: personBId, relationship_type: type }); if (error) throw error; } export async function removeRelationship(id) { const { error } = await supabase.from('nf_relationships').delete().eq('id', id); if (error) throw error; } // ── Sadaqah — private daily journal. Family visibility is limited to // streak counts via nf_family_sadaqah_streaks (never amounts/causes/notes). export async function listSadaqahLog(familyId, memberId) { const { data, error } = await supabase.from('nf_sadaqah_log').select('*').eq('family_id', familyId).eq('member_id', memberId).order('log_date', { ascending: false }); if (error) throw error; return (data || []).map(s => ({ id: s.id, logDate: s.log_date, cause: s.cause, amount: s.amount, inMemoryOfPersonId: s.in_memory_of_person_id, notes: s.notes })); } export async function addSadaqahEntry(familyId, memberId, fields) { const { error } = await supabase.from('nf_sadaqah_log').insert({ family_id: familyId, member_id: memberId, log_date: fields.logDate || new Date().toISOString().slice(0, 10), cause: fields.cause || null, amount: fields.amount ? Number(fields.amount) : null, in_memory_of_person_id: fields.inMemoryOfPersonId || null, notes: fields.notes || null }); if (error) throw error; } export async function removeSadaqahEntry(id) { const { error } = await supabase.from('nf_sadaqah_log').delete().eq('id', id); if (error) throw error; } /** Family-visible streak counts only — no amounts, causes, or notes. */ export async function getFamilySadaqahStreaks(familyId) { const { data, error } = await supabase.rpc('nf_family_sadaqah_streaks', { p_family_id: familyId }); if (error) throw error; return (data || []).map(r => ({ memberId: r.member_id, currentStreak: r.current_streak, gaveToday: r.gave_today })); } /** Count of sadaqah given in memory of a person — count only, shown on their Tree card. */ export async function getMemorialSadaqahCount(personId) { const { data, error } = await supabase.rpc('nf_memorial_sadaqah_count', { p_person_id: personId }); if (error) throw error; return data || 0; } // ── Insurance / Takaful — per-member: each family member logs their own // policies, but any family member (and the mutawalli dashboard) can read them. ── export async function listInsurancePolicies(familyId, memberId) { const { data, error } = await supabase.from('nf_insurance_policies').select('*').eq('family_id', familyId).eq('member_id', memberId).order('created_at'); if (error) throw error; return (data || []).map(p => ({ id: p.id, policyType: p.policy_type, provider: p.provider, policyNumber: p.policy_number, sumAssured: p.sum_assured, beneficiaryName: p.beneficiary_name, beneficiaryRelation: p.beneficiary_relation, expiryDate: p.expiry_date, notes: p.notes, documentPath: p.document_path })); } export async function addInsurancePolicy(familyId, memberId, createdBy, p) { const { data, error } = await supabase.from('nf_insurance_policies').insert({ family_id: familyId, member_id: memberId, created_by: createdBy, policy_type: p.policyType, provider: p.provider, policy_number: p.policyNumber, sum_assured: p.sumAssured ? Number(p.sumAssured) : null, beneficiary_name: p.beneficiaryName, beneficiary_relation: p.beneficiaryRelation, expiry_date: p.expiryDate || null, notes: p.notes }).select().single(); if (error) throw error; return data; } export async function updateInsurancePolicy(id, p) { const { error } = await supabase.from('nf_insurance_policies').update({ policy_type: p.policyType, provider: p.provider, policy_number: p.policyNumber, sum_assured: p.sumAssured ? Number(p.sumAssured) : null, beneficiary_name: p.beneficiaryName, beneficiary_relation: p.beneficiaryRelation, expiry_date: p.expiryDate || null, notes: p.notes, updated_at: new Date().toISOString() }).eq('id', id); if (error) throw error; } export async function removeInsurancePolicy(id) { const { error } = await supabase.from('nf_insurance_policies').delete().eq('id', id); if (error) throw error; } /** For the mutawalli dashboard: every family member's insurance/Takaful policies. */ export async function listAllInsuranceForFamily(familyId) { const { data, error } = await supabase.from('nf_insurance_policies').select('*').eq('family_id', familyId).order('created_at'); if (error) throw error; return data || []; } const POLICY_DOC_BUCKET = 'nf-asset-documents'; export async function uploadPolicyDocument(familyId, policyId, file) { const ext = (file.name.split('.').pop() || 'pdf').toLowerCase(); const path = `${familyId}/policy-${policyId}/${Date.now()}.${ext}`; const { error: upErr } = await supabase.storage.from(POLICY_DOC_BUCKET).upload(path, file, { upsert: true, contentType: file.type }); if (upErr) throw upErr; const { error: dbErr } = await supabase.from('nf_insurance_policies').update({ document_path: path, updated_at: new Date().toISOString() }).eq('id', policyId); if (dbErr) throw dbErr; return path; } export async function removePolicyDocument(policyId, docPath) { if (docPath) await supabase.storage.from(POLICY_DOC_BUCKET).remove([docPath]); const { error } = await supabase.from('nf_insurance_policies').update({ document_path: null, updated_at: new Date().toISOString() }).eq('id', policyId); if (error) throw error; } // ── Liabilities — family-shared, same ownership model as Assets. Kept // distinct because Faraid requires debts settled before distribution. ── export async function listLiabilities(familyId) { const { data, error } = await supabase.from('nf_liabilities').select('*').eq('family_id', familyId).order('created_at'); if (error) throw error; return (data || []).map(l => ({ id: l.id, liabilityType: l.liability_type, lender: l.lender, outstandingBalance: l.outstanding_balance, linkedAssetId: l.linked_asset_id, notes: l.notes, documentPath: l.document_path })); } export async function addLiability(familyId, createdBy, l) { const { data, error } = await supabase.from('nf_liabilities').insert({ family_id: familyId, created_by: createdBy, liability_type: l.liabilityType, lender: l.lender, outstanding_balance: l.outstandingBalance ? Number(l.outstandingBalance) : null, linked_asset_id: l.linkedAssetId || null, notes: l.notes }).select().single(); if (error) throw error; return data; } export async function updateLiability(id, l) { const { error } = await supabase.from('nf_liabilities').update({ liability_type: l.liabilityType, lender: l.lender, outstanding_balance: l.outstandingBalance ? Number(l.outstandingBalance) : null, linked_asset_id: l.linkedAssetId || null, notes: l.notes, updated_at: new Date().toISOString() }).eq('id', id); if (error) throw error; } export async function removeLiability(id) { const { error } = await supabase.from('nf_liabilities').delete().eq('id', id); if (error) throw error; } export async function uploadLiabilityDocument(familyId, liabilityId, file) { const ext = (file.name.split('.').pop() || 'pdf').toLowerCase(); const path = `${familyId}/liability-${liabilityId}/${Date.now()}.${ext}`; const { error: upErr } = await supabase.storage.from(POLICY_DOC_BUCKET).upload(path, file, { upsert: true, contentType: file.type }); if (upErr) throw upErr; const { error: dbErr } = await supabase.from('nf_liabilities').update({ document_path: path, updated_at: new Date().toISOString() }).eq('id', liabilityId); if (dbErr) throw dbErr; return path; } export async function removeLiabilityDocument(liabilityId, docPath) { if (docPath) await supabase.storage.from(POLICY_DOC_BUCKET).remove([docPath]); const { error } = await supabase.from('nf_liabilities').update({ document_path: null, updated_at: new Date().toISOString() }).eq('id', liabilityId); if (error) throw error; } // ── Asset ownership verification — a proof document (car grant, land title, // cover note, loan agreement) plus a dual-path confirm: either the // mutawalli/agent OR any other family member can mark an asset verified. ── export async function uploadAssetDocument(familyId, assetId, file) { const ext = (file.name.split('.').pop() || 'pdf').toLowerCase(); const path = `${familyId}/asset-${assetId}/${Date.now()}.${ext}`; const { error: upErr } = await supabase.storage.from(POLICY_DOC_BUCKET).upload(path, file, { upsert: true, contentType: file.type }); if (upErr) throw upErr; const { error: dbErr } = await supabase.from('nf_assets').update({ proof_document_path: path }).eq('id', assetId); if (dbErr) throw dbErr; return path; } export async function removeAssetDocument(assetId, docPath) { if (docPath) await supabase.storage.from(POLICY_DOC_BUCKET).remove([docPath]); const { error } = await supabase.from('nf_assets').update({ proof_document_path: null }).eq('id', assetId); if (error) throw error; } export async function getDocumentUrl(path) { if (!path) return null; const { data, error } = await supabase.storage.from(POLICY_DOC_BUCKET).createSignedUrl(path, 3600); if (error) return null; return data.signedUrl; } /** Verifying isn't restricted to the mutawalli — any other family member who has seen the proof can confirm. */ export async function setAssetVerified(assetId, verifiedBy, verified) { const { error } = await supabase.from('nf_assets').update({ verified, verified_by: verified ? verifiedBy : null, verified_at: verified ? new Date().toISOString() : null }).eq('id', assetId); if (error) throw error; } // ── Zakat — per-member, one live record per family/member pair. ── export async function getZakatRecord(familyId, memberId) { const { data, error } = await supabase.from('nf_zakat_records').select('*').eq('family_id', familyId).eq('member_id', memberId).maybeSingle(); if (error) throw error; if (!data) return null; return { cash: data.cash, gold: data.gold, silver: data.silver, businessAssets: data.business_assets, investments: data.investments, otherZakatable: data.other_zakatable, deductibleLiabilities: data.deductible_liabilities, nisabThreshold: data.nisab_threshold }; } export async function upsertZakatRecord(familyId, memberId, fields) { const { error } = await supabase.from('nf_zakat_records').upsert({ family_id: familyId, member_id: memberId, cash: Number(fields.cash) || 0, gold: Number(fields.gold) || 0, silver: Number(fields.silver) || 0, business_assets: Number(fields.businessAssets) || 0, investments: Number(fields.investments) || 0, other_zakatable: Number(fields.otherZakatable) || 0, deductible_liabilities: Number(fields.deductibleLiabilities) || 0, nisab_threshold: Number(fields.nisabThreshold) || 0, updated_at: new Date().toISOString() }, { onConflict: 'family_id,member_id' }); if (error) throw error; }