================================================================ DIASPORA AIR — MASTER SYSTEM REVIEW & FULL SOURCE DUMP ================================================================ Purpose: hand this single file to another engineer or LLM. It contains (1) a system audit with the known gaps, and (2) the complete source code of the application, file by file. ---------------------------------------------------------------- 1. STACK ---------------------------------------------------------------- - TanStack Start v1 (React 19) + Vite 7, deployed to a Cloudflare Worker. - File-based routing in src/routes (routeTree.gen.ts is generated — never edit). - Server logic: `createServerFn` (src/lib/*.functions.ts) and server routes (src/routes/api/*). Server-only helpers use the *.server.ts suffix. - Tailwind CSS v4 via src/styles.css (@theme tokens; no tailwind.config.js). - Backend: Supabase (Lovable Cloud) — Postgres + RLS, Auth, Storage (bucket: campaign-covers), Realtime. - Email: Brevo (BREVO_API_KEY). CRM: Thenuvision webhook (CRM_WEBHOOK_URL, THENUVISION_CRM_API_KEY). ---------------------------------------------------------------- 2. DATABASE (public schema) ---------------------------------------------------------------- profiles, user_roles (app_role: admin|agent|manager), site_stats, leads, campaigns, campaign_updates, donations, votes, referrals, tnvcoin_transactions, tnv_wallet_mirror, country_groups, group_members, community_posts, invites, invite_templates, crm_events, crm_sync_log, affiliate_assignments, committee_memberships, committee_proposals, committee_votes, profile_platform_links, tax_receipt_requests. Enums: app_role, campaign_category (20 values incl. repatriation, machinery, orphanage, facilities, job_creation), campaign_status, donation_status, payment_provider, invite_channel, invite_status, membership_tier, membership_status, proposal_status. Security model: - RLS enabled on all public tables; role checks go through SECURITY DEFINER functions in the `private` schema (private.has_role, private.has_any_staff_role) so they are not callable from the Data API. - Sensitive columns (campaigns.admin_notes, donations.donor_email, votes.user_id, referrals.user_id, group_members.user_id) are column-revoked from anon. - affiliate_assignments intentionally has no policies (service-role only). ---------------------------------------------------------------- 3. WHAT IS FULLY WORKING ---------------------------------------------------------------- - Auth: register, login, forgot password, reset password (/login, /forgot-password, /reset-password) with redirect-back support. - Campaign creation (/campaigns/new) with cover-image upload to Storage, listing + category/search filtering (/campaigns), detail pages (/campaigns/$slug), voting, sharing, referral tracking (?ref=). - Donations & monthly pledges: POST /api/donate writes a pending donation, awards TNVCOIN 1:1, emails a Brevo receipt, and syncs to CRM. - Admin back office (/admin): stats editor, campaign approval/payout, leads, invites (email/SMS/WhatsApp via Brevo), CRM events (received + outbound with retry log), test-invite workflow. - CRM: /api/crm/webhook (Zod-validated, API-key authenticated) writes to crm_events; outbound sync has persistent logging + exponential-backoff retry in crm-sync.server.ts (crm_sync_log). - Community rooms (/community, /community/$country) with a realtime post feed. - Homepage: theme toggle, Haiti banner slider, Google-Translate language dropdown, live impact stats (realtime), 20 cause categories with photos, 12-platform grid, lead form, referral share. ---------------------------------------------------------------- 4. KNOWN GAPS / WHAT STILL NEEDS TO BE BUILT ---------------------------------------------------------------- A. Payments are NOT live. src/lib/tnv-gateway.server.ts and src/lib/tnv-pay.ts are stubs: every donation is recorded with status "pending" and no charge is made. Needs a real Stripe/PayPal/Square/NOWPayments checkout, a webhook to flip donations to "succeeded", and campaigns.raised_cents accumulation. B. Committee membership & weighted voting UI is NOT built yet. Tables exist (committee_memberships, committee_proposals, committee_votes; tiers Bronze $20/5 votes, Silver $50/10, Gold $75/15, Platinum $100/20 defined in src/lib/campaigns.ts as MEMBERSHIP_TIERS). Missing: /committee route with tier checkout, proposal list, weighted vote casting and tallies. C. Member profile dashboard is NOT built yet. Columns and tables exist (profiles.bio/avatar_url/phone/city + facebook/twitter/instagram/tiktok/ youtube/website URLs, profile_platform_links, tax_receipt_requests, campaigns.external_platform/external_url). Missing: /profile route to edit socials, attach the 12 external platform campaign URLs, view donation history, and request a tax write-off receipt; plus an admin tab to issue those receipts. D. Language-based community rooms: the 5 rooms (Kreyòl Ayisyen, English, Français, Español, Português) exist in country_groups, but /community still labels them as "country" rooms and does not group by language. E. Campaign moderation: new campaigns are inserted as "pending_review", so a creator sees nothing on /campaigns until an admin approves. Either auto-activate or add a "My campaigns" view for creators. F. /campaigns/new redirects unauthenticated users to /login without a ?redirect= back-link, which is what makes "Start a campaign" feel like a loop. G. The 12 crowdfunding platform links are placeholders marked "Pending verification"; they must be editable from the admin. H. Impact numbers fall back to manual site_stats values when live totals are lower — intentional, but should be disclosed. I. No automated tests and no Playwright/e2e coverage. ---------------------------------------------------------------- 5. ENVIRONMENT / SECRETS (names only) ---------------------------------------------------------------- Client: VITE_SUPABASE_URL, VITE_SUPABASE_PUBLISHABLE_KEY, VITE_SUPABASE_PROJECT_ID Server: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_ANON_KEY, BREVO_API_KEY, CRM_WEBHOOK_URL, THENUVISION_CRM_API_KEY, LOVABLE_API_KEY ---------------------------------------------------------------- 6. HOW TO REGENERATE THIS FILE ---------------------------------------------------------------- bun scripts/generate-master-review.mjs (Excluded from the dump: src/routeTree.gen.ts and src/integrations/supabase/types.ts — both are generated.) ================================================================ FILE INDEX (133 files) ================================================================ bunfig.toml components.json eslint.config.js package.json scripts/generate-master-review.mjs scripts/master-review-header.md src/components/AdminExtraPanels.tsx src/components/CampaignCard.tsx src/components/DonateDialog.tsx src/components/Footer.tsx src/components/FundraisingVideos.tsx src/components/LeadForm.tsx src/components/LocaleBanner.tsx src/components/NovaCastEmbed.tsx src/components/PublicInviteDialog.tsx src/components/ReferralShare.tsx src/components/SharePanel.tsx src/components/VoteButton.tsx src/components/ui/accordion.tsx src/components/ui/alert-dialog.tsx src/components/ui/alert.tsx src/components/ui/aspect-ratio.tsx src/components/ui/avatar.tsx src/components/ui/badge.tsx src/components/ui/breadcrumb.tsx src/components/ui/button.tsx src/components/ui/calendar.tsx src/components/ui/card.tsx src/components/ui/carousel.tsx src/components/ui/chart.tsx src/components/ui/checkbox.tsx src/components/ui/collapsible.tsx src/components/ui/command.tsx src/components/ui/context-menu.tsx src/components/ui/dialog.tsx src/components/ui/drawer.tsx src/components/ui/dropdown-menu.tsx src/components/ui/form.tsx src/components/ui/hover-card.tsx src/components/ui/input-otp.tsx src/components/ui/input.tsx src/components/ui/label.tsx src/components/ui/menubar.tsx src/components/ui/navigation-menu.tsx src/components/ui/pagination.tsx src/components/ui/popover.tsx src/components/ui/progress.tsx src/components/ui/radio-group.tsx src/components/ui/resizable.tsx src/components/ui/scroll-area.tsx src/components/ui/select.tsx src/components/ui/separator.tsx src/components/ui/sheet.tsx src/components/ui/sidebar.tsx src/components/ui/skeleton.tsx src/components/ui/slider.tsx src/components/ui/sonner.tsx src/components/ui/switch.tsx src/components/ui/table.tsx src/components/ui/tabs.tsx src/components/ui/textarea.tsx src/components/ui/toggle-group.tsx src/components/ui/toggle.tsx src/components/ui/tooltip.tsx src/hooks/use-mobile.tsx src/hooks/useAuth.ts src/integrations/supabase/auth-attacher.ts src/integrations/supabase/auth-middleware.ts src/integrations/supabase/client.server.ts src/integrations/supabase/client.ts src/integrations/supabase/previewAuthStorage.ts src/lib/campaigns.ts src/lib/committee.functions.ts src/lib/crm-sync.server.ts src/lib/crm.functions.ts src/lib/locale.ts src/lib/rate-limit.server.ts src/lib/rotation.functions.ts src/lib/templates.ts src/lib/tnv-gateway.server.ts src/lib/tnv-pay.ts src/lib/utils.ts src/lib/voting.functions.ts src/router.tsx src/routes/__root.tsx src/routes/admin.tsx src/routes/api/crm/webhook.ts src/routes/api/donate.ts src/routes/api/public/webhooks/stripe.ts src/routes/api/send-invite.ts src/routes/campaigns.$slug.tsx src/routes/campaigns.new.tsx src/routes/campaigns.tsx src/routes/committee.index.tsx src/routes/committee.proposals.$id.tsx src/routes/committee.proposals.index.tsx src/routes/committee.tsx src/routes/community.$country.tsx src/routes/community.tsx src/routes/donation-cancel.tsx src/routes/donation-success.tsx src/routes/forgot-password.tsx src/routes/index.tsx src/routes/legal.tsx src/routes/login.tsx src/routes/profile.tsx src/routes/reset-password.tsx src/routes/robots[.]txt.ts src/routes/sitemap[.]xml.ts src/routes/u.$id.tsx src/start.ts src/styles.css supabase/config.toml supabase/migrations/20260508141245_11cdea7d-4632-4048-99b4-460483d51e4b.sql supabase/migrations/20260508141308_b5a48fdd-fdb1-4213-897a-a73b98557e81.sql supabase/migrations/20260508141415_f772a914-cbc0-466a-b882-94c1fe08034f.sql supabase/migrations/20260510230718_7dbfb6cf-43f4-4b3f-8468-3fa9fb15e1e2.sql supabase/migrations/20260522164034_97611b4b-911d-469f-b8df-31e24faa0a36.sql supabase/migrations/20260522164056_c29f4889-9147-486a-8eb7-18b3b9305ce8.sql supabase/migrations/20260530012110_97f5088d-7991-4a5a-a81e-2dae4796a330.sql supabase/migrations/20260625070242_2221c37d-9e84-4055-9461-84e45c9b6de0.sql supabase/migrations/20260625070516_5a707599-04eb-4620-8655-7a182301ebf7.sql supabase/migrations/20260629040058_442323e4-ea27-48b8-a4ab-83c16180a9bb.sql supabase/migrations/20260630192600_9f319ff0-5da4-42b6-b9ae-e325c1e2fbdc.sql supabase/migrations/20260630201841_fce50ebd-0b09-491a-985e-16c45596d9cc.sql supabase/migrations/20260710152929_e2a3d04b-ff15-4029-acd7-d62094938966.sql supabase/migrations/20260711013127_cf4f2477-582a-40b7-9e3e-142e4a2557de.sql supabase/migrations/20260812014755_358d1a8b-0c72-4a35-8bd0-c9e72fb5c44c.sql supabase/migrations/20260817073531_e7b9a8a3-21bc-4a4f-b082-d974220eed09.sql supabase/migrations/20260902045732_f3febcbf-dd34-4059-9b23-4b450132ce16.sql tsconfig.json vite.config.ts wrangler.jsonc ================================================================ FILE: bunfig.toml ================================================================ [install] saveTextLockfile = false ================================================================ FILE: components.json ================================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": false, "tsx": true, "tailwind": { "css": "src/styles.css", "baseColor": "slate", "cssVariables": true, "prefix": "" }, "iconLibrary": "lucide", "rtl": false, "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "registries": {} } ================================================================ FILE: eslint.config.js ================================================================ import js from "@eslint/js"; import eslintPluginPrettier from "eslint-plugin-prettier/recommended"; import globals from "globals"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; export default tseslint.config( { ignores: ["dist", ".output", ".vinxi"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { "react-hooks": reactHooks, "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], "@typescript-eslint/no-unused-vars": "off", }, }, eslintPluginPrettier, ); ================================================================ FILE: package.json ================================================================ { "name": "tanstack_start_ts", "private": true, "sideEffects": false, "type": "module", "scripts": { "dev": "vite dev", "build": "vite build", "build:dev": "vite build --mode development", "preview": "vite preview", "lint": "eslint .", "format": "prettier --write ." }, "dependencies": { "@cloudflare/vite-plugin": "^1.25.5", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-aspect-ratio": "^1.1.8", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-menubar": "^1.1.16", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", "@supabase/supabase-js": "^2.105.3", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-query": "^5.83.0", "@tanstack/react-router": "^1.168.0", "@tanstack/react-start": "^1.167.14", "@tanstack/router-plugin": "^1.167.10", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^0.575.0", "nitro": "3.0.260603-beta", "react": "^19.2.0", "react-day-picker": "^9.14.0", "react-dom": "^19.2.0", "react-hook-form": "^7.71.2", "react-resizable-panels": "^4.6.5", "recharts": "^2.15.4", "sonner": "^2.0.7", "stripe": "^22.6.1", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", "tw-animate-css": "^1.3.4", "vaul": "^1.1.2", "vite-tsconfig-paths": "^6.0.2", "zod": "^3.24.2" }, "devDependencies": { "@eslint/js": "^9.32.0", "@lovable.dev/vite-tanstack-config": "2.13.1", "@types/node": "^22.16.5", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^5.0.4", "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^15.15.0", "prettier": "^3.7.3", "typescript": "^5.8.3", "typescript-eslint": "^8.56.1", "vite": "^7.3.1" } } ================================================================ FILE: scripts/generate-master-review.mjs ================================================================ // Generates public/diasporaair-master-review.txt — a full source + audit dump // Run: bun scripts/generate-master-review.mjs import { readdirSync, statSync, readFileSync, writeFileSync } from "node:fs"; import { join, extname } from "node:path"; const ROOTS = ["src", "supabase", "scripts"]; const TOP_FILES = [ "package.json", "vite.config.ts", "tsconfig.json", "wrangler.jsonc", "components.json", "eslint.config.js", "bunfig.toml", ]; const EXT = new Set([".ts", ".tsx", ".css", ".sql", ".toml", ".json", ".jsonc", ".js", ".mjs", ".md"]); const SKIP = ["src/routeTree.gen.ts", "src/integrations/supabase/types.ts"]; function walk(dir, out = []) { for (const name of readdirSync(dir)) { const p = join(dir, name); const st = statSync(p); if (st.isDirectory()) { walk(p, out); continue; } if (!EXT.has(extname(p))) continue; if (SKIP.includes(p)) continue; out.push(p); } return out; } const files = []; for (const f of TOP_FILES) { try { statSync(f); files.push(f); } catch {} } for (const r of ROOTS) { try { walk(r, files); } catch {} } files.sort(); const AUDIT = readFileSync("scripts/master-review-header.md", "utf8"); const parts = [AUDIT, "\n\n"]; parts.push("================================================================\n"); parts.push("FILE INDEX (" + files.length + " files)\n"); parts.push("================================================================\n"); for (const f of files) parts.push(` ${f}\n`); parts.push("\n\n"); let totalLines = 0; for (const f of files) { const src = readFileSync(f, "utf8"); totalLines += src.split("\n").length; parts.push("\n================================================================\n"); parts.push(`FILE: ${f}\n`); parts.push("================================================================\n"); parts.push(src.endsWith("\n") ? src : src + "\n"); } parts.push(`\n\n=== END OF DUMP — ${files.length} files, ${totalLines} lines, generated ${new Date().toISOString()} ===\n`); writeFileSync("public/diasporaair-master-review.txt", parts.join("")); console.log(`Wrote public/diasporaair-master-review.txt (${files.length} files, ${totalLines} lines)`); ================================================================ FILE: scripts/master-review-header.md ================================================================ ================================================================ DIASPORA AIR — MASTER SYSTEM REVIEW & FULL SOURCE DUMP ================================================================ Purpose: hand this single file to another engineer or LLM. It contains (1) a system audit with the known gaps, and (2) the complete source code of the application, file by file. ---------------------------------------------------------------- 1. STACK ---------------------------------------------------------------- - TanStack Start v1 (React 19) + Vite 7, deployed to a Cloudflare Worker. - File-based routing in src/routes (routeTree.gen.ts is generated — never edit). - Server logic: `createServerFn` (src/lib/*.functions.ts) and server routes (src/routes/api/*). Server-only helpers use the *.server.ts suffix. - Tailwind CSS v4 via src/styles.css (@theme tokens; no tailwind.config.js). - Backend: Supabase (Lovable Cloud) — Postgres + RLS, Auth, Storage (bucket: campaign-covers), Realtime. - Email: Brevo (BREVO_API_KEY). CRM: Thenuvision webhook (CRM_WEBHOOK_URL, THENUVISION_CRM_API_KEY). ---------------------------------------------------------------- 2. DATABASE (public schema) ---------------------------------------------------------------- profiles, user_roles (app_role: admin|agent|manager), site_stats, leads, campaigns, campaign_updates, donations, votes, referrals, tnvcoin_transactions, tnv_wallet_mirror, country_groups, group_members, community_posts, invites, invite_templates, crm_events, crm_sync_log, affiliate_assignments, committee_memberships, committee_proposals, committee_votes, profile_platform_links, tax_receipt_requests. Enums: app_role, campaign_category (20 values incl. repatriation, machinery, orphanage, facilities, job_creation), campaign_status, donation_status, payment_provider, invite_channel, invite_status, membership_tier, membership_status, proposal_status. Security model: - RLS enabled on all public tables; role checks go through SECURITY DEFINER functions in the `private` schema (private.has_role, private.has_any_staff_role) so they are not callable from the Data API. - Sensitive columns (campaigns.admin_notes, donations.donor_email, votes.user_id, referrals.user_id, group_members.user_id) are column-revoked from anon. - affiliate_assignments intentionally has no policies (service-role only). ---------------------------------------------------------------- 3. WHAT IS FULLY WORKING ---------------------------------------------------------------- - Auth: register, login, forgot password, reset password (/login, /forgot-password, /reset-password) with redirect-back support. - Campaign creation (/campaigns/new) with cover-image upload to Storage, listing + category/search filtering (/campaigns), detail pages (/campaigns/$slug), voting, sharing, referral tracking (?ref=). - Donations & monthly pledges: POST /api/donate writes a pending donation, awards TNVCOIN 1:1, emails a Brevo receipt, and syncs to CRM. - Admin back office (/admin): stats editor, campaign approval/payout, leads, invites (email/SMS/WhatsApp via Brevo), CRM events (received + outbound with retry log), test-invite workflow. - CRM: /api/crm/webhook (Zod-validated, API-key authenticated) writes to crm_events; outbound sync has persistent logging + exponential-backoff retry in crm-sync.server.ts (crm_sync_log). - Community rooms (/community, /community/$country) with a realtime post feed. - Homepage: theme toggle, Haiti banner slider, Google-Translate language dropdown, live impact stats (realtime), 20 cause categories with photos, 12-platform grid, lead form, referral share. ---------------------------------------------------------------- 4. KNOWN GAPS / WHAT STILL NEEDS TO BE BUILT ---------------------------------------------------------------- A. Payments are NOT live. src/lib/tnv-gateway.server.ts and src/lib/tnv-pay.ts are stubs: every donation is recorded with status "pending" and no charge is made. Needs a real Stripe/PayPal/Square/NOWPayments checkout, a webhook to flip donations to "succeeded", and campaigns.raised_cents accumulation. B. Committee membership & weighted voting UI is NOT built yet. Tables exist (committee_memberships, committee_proposals, committee_votes; tiers Bronze $20/5 votes, Silver $50/10, Gold $75/15, Platinum $100/20 defined in src/lib/campaigns.ts as MEMBERSHIP_TIERS). Missing: /committee route with tier checkout, proposal list, weighted vote casting and tallies. C. Member profile dashboard is NOT built yet. Columns and tables exist (profiles.bio/avatar_url/phone/city + facebook/twitter/instagram/tiktok/ youtube/website URLs, profile_platform_links, tax_receipt_requests, campaigns.external_platform/external_url). Missing: /profile route to edit socials, attach the 12 external platform campaign URLs, view donation history, and request a tax write-off receipt; plus an admin tab to issue those receipts. D. Language-based community rooms: the 5 rooms (Kreyòl Ayisyen, English, Français, Español, Português) exist in country_groups, but /community still labels them as "country" rooms and does not group by language. E. Campaign moderation: new campaigns are inserted as "pending_review", so a creator sees nothing on /campaigns until an admin approves. Either auto-activate or add a "My campaigns" view for creators. F. /campaigns/new redirects unauthenticated users to /login without a ?redirect= back-link, which is what makes "Start a campaign" feel like a loop. G. The 12 crowdfunding platform links are placeholders marked "Pending verification"; they must be editable from the admin. H. Impact numbers fall back to manual site_stats values when live totals are lower — intentional, but should be disclosed. I. No automated tests and no Playwright/e2e coverage. ---------------------------------------------------------------- 5. ENVIRONMENT / SECRETS (names only) ---------------------------------------------------------------- Client: VITE_SUPABASE_URL, VITE_SUPABASE_PUBLISHABLE_KEY, VITE_SUPABASE_PROJECT_ID Server: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_ANON_KEY, BREVO_API_KEY, CRM_WEBHOOK_URL, THENUVISION_CRM_API_KEY, LOVABLE_API_KEY ---------------------------------------------------------------- 6. HOW TO REGENERATE THIS FILE ---------------------------------------------------------------- bun scripts/generate-master-review.mjs (Excluded from the dump: src/routeTree.gen.ts and src/integrations/supabase/types.ts — both are generated.) ================================================================ FILE: src/components/AdminExtraPanels.tsx ================================================================ import { useEffect, useState } from "react"; import { Link } from "@tanstack/react-router"; import { supabase } from "@/integrations/supabase/client"; import { fmtMoney, MEMBERSHIP_TIERS } from "@/lib/campaigns"; import { RefreshCw, CheckCircle2, XCircle, Send } from "lucide-react"; const card = "rounded-2xl border border-white/15 bg-white/5 p-6"; const input = "w-full rounded-lg bg-navy border border-white/20 px-3 py-2 text-sm text-white placeholder:text-white/40"; const smallBtn = "inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold"; // ----- Tax receipts (admin fulfillment) ----- type Receipt = { id: string; user_id: string; donation_id: string | null; tax_year: number; full_name: string; address: string; email: string; status: string; notes: string | null; created_at: string; }; export function TaxReceiptsPanel({ isAdmin }: { isAdmin: boolean }) { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState<"all" | "pending" | "sent" | "rejected">("pending"); const [notes, setNotes] = useState>({}); const load = async () => { setLoading(true); const { data } = await supabase.from("tax_receipt_requests").select("*").order("created_at", { ascending: false }).limit(300); setRows((data ?? []) as Receipt[]); setLoading(false); }; useEffect(() => { load(); }, []); const setStatus = async (r: Receipt, status: "sent" | "rejected" | "pending") => { const { error } = await supabase.from("tax_receipt_requests") .update({ status, notes: notes[r.id] ?? r.notes ?? null }) .eq("id", r.id); if (error) { alert(error.message); return; } if (status === "sent") { // Fire-and-forget CRM/Brevo notification via existing invite pipeline is optional; keep it simple and just log. console.info("[tax-receipt] marked sent", r.id); } load(); }; const visible = rows.filter((r) => filter === "all" || r.status === filter); return (

Tax receipt requests

{(["pending", "sent", "rejected", "all"] as const).map((f) => ( ))}
{loading ?

Loading…

: visible.length === 0 ?

No {filter === "all" ? "" : filter} requests.

: (
{visible.map((r) => (
{r.full_name} · Tax year {r.tax_year} {r.status}
{r.email}
{r.address}
Requested {new Date(r.created_at).toLocaleString()} · donor profile {r.donation_id && <> · donation {r.donation_id.slice(0, 8)}…}
{isAdmin && (