production: reminder cron, dashboard overhaul, shadcn components, setup wizard

- /api/cron/reminders: processes pending reminders every 15min, sends WhatsApp with email fallback
- /api/cron/overdue: marks overdue pledges daily (7d deferred, 14d immediate)
- /api/pledges: GET handler with filtering, search, pagination, sort by dueDate
- Dashboard overview: stats, collection progress bar, needs attention, upcoming payments
- Dashboard pledges: proper table with status tabs, search, actions, pagination
- New shadcn components: Table, Tabs, DropdownMenu, Progress
- Setup wizard: 4-step onboarding (org → bank → event → QR code)
- Settings API: PUT handler for org create/update
- Org resolver: single-tenant fallback to first org
- Cron jobs installed: reminders every 15min, overdue check at 6am
- Auto-generates installment dates when not provided
- HOSTNAME=0.0.0.0 in compose for multi-network binding
This commit is contained in:
2026-03-03 05:11:17 +08:00
parent 250221b530
commit c79b9bcabc
61 changed files with 3547 additions and 534 deletions

View File

@@ -3,27 +3,37 @@ import prisma from "@/lib/prisma"
/**
* Resolve organization ID from x-org-id header.
* The header may contain a slug or a direct ID — we try slug first, then ID.
* Falls back to first org if none specified (single-tenant mode).
*/
export async function resolveOrgId(headerValue: string | null): Promise<string | null> {
if (!prisma) return null
const val = headerValue?.trim()
if (!val) return null
// Try by slug first (most common from frontend)
const bySlug = await prisma.organization.findFirst({
where: {
OR: [
{ slug: val },
{ slug: { startsWith: val } },
],
},
select: { id: true },
})
if (bySlug) return bySlug.id
if (val && val !== "demo") {
// Try by slug first (most common from frontend)
const bySlug = await prisma.organization.findFirst({
where: {
OR: [
{ slug: val },
{ slug: { startsWith: val } },
],
},
select: { id: true },
})
if (bySlug) return bySlug.id
// Try direct ID
const byId = await prisma.organization.findUnique({
where: { id: val },
// Try direct ID
const byId = await prisma.organization.findUnique({
where: { id: val },
select: { id: true },
})
if (byId) return byId.id
}
// Single-tenant fallback: use first org
const first = await prisma.organization.findFirst({
select: { id: true },
orderBy: { createdAt: "asc" },
})
return byId?.id ?? null
return first?.id ?? null
}