diff --git a/src/app/admin/finance-management/components/InvoiceDetailModal.tsx b/src/app/admin/finance-management/components/InvoiceDetailModal.tsx new file mode 100644 index 0000000..f7abfb6 --- /dev/null +++ b/src/app/admin/finance-management/components/InvoiceDetailModal.tsx @@ -0,0 +1,535 @@ +'use client' + +import React, { Fragment, useEffect, useState, useCallback } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { + XMarkIcon, + DocumentTextIcon, + UserIcon, + BanknotesIcon, + CalendarDaysIcon, + ArrowDownTrayIcon, + ArrowPathIcon, + CheckCircleIcon, + ExclamationCircleIcon, + ClockIcon, + NoSymbolIcon, + PencilSquareIcon, + ShieldCheckIcon, +} from '@heroicons/react/24/outline' +import useAuthStore from '../../../store/authStore' + +/* ---------- types ---------- */ +export type AdminInvoice = { + id: string | number + invoice_number?: string | null + user_id?: string | number | null + buyer_name?: string | null + buyer_email?: string | null + buyer_street?: string | null + buyer_postal_code?: string | null + buyer_city?: string | null + buyer_country?: string | null + currency?: string | null + total_net?: number | null + total_tax?: number | null + total_gross?: number | null + vat_rate?: number | null + status?: string + issued_at?: string | null + due_at?: string | null + pdf_storage_key?: string | null + context?: any | null + created_at?: string | null + updated_at?: string | null +} + +type InvoiceItem = { + id?: number + invoice_id?: number + product_id?: number | null + sku?: string | null + description?: string | null + quantity?: number + unit_price?: number + tax_rate?: number | null + line_net?: number + line_tax?: number + line_gross?: number + created_at?: string +} + +type InvoicePayment = { + id?: number + invoice_id?: number + payment_method?: string | null + transaction_id?: string | null + amount?: number | null + paid_at?: string | null + status?: string | null + details?: string | null + created_at?: string +} + +interface InvoiceDetailModalProps { + invoice: AdminInvoice | null + open: boolean + onClose: () => void + onStatusChanged?: () => void + onRunPoolCheck?: (invoiceId: string | number) => void + onExport?: (invoice: AdminInvoice) => void +} + +/* ---------- constants ---------- */ +const STATUSES = ['draft', 'issued', 'paid', 'overdue', 'canceled'] as const +type InvoiceStatus = (typeof STATUSES)[number] + +const STATUS_CONFIG: Record = { + draft: { label: 'Draft', bg: 'bg-gray-100', text: 'text-gray-700', icon: PencilSquareIcon }, + issued: { label: 'Issued', bg: 'bg-indigo-100', text: 'text-indigo-700', icon: DocumentTextIcon }, + paid: { label: 'Paid', bg: 'bg-green-100', text: 'text-green-700', icon: CheckCircleIcon }, + overdue: { label: 'Overdue', bg: 'bg-red-100', text: 'text-red-700', icon: ExclamationCircleIcon }, + canceled: { label: 'Canceled', bg: 'bg-yellow-100', text: 'text-yellow-700', icon: NoSymbolIcon }, +} + +function fmtDate(d?: string | null) { + if (!d) return '—' + return new Date(d).toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: '2-digit' }) +} + +function fmtDateTime(d?: string | null) { + if (!d) return '—' + return new Date(d).toLocaleString('de-DE', { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) +} + +function fmtMoney(v?: number | null, currency = 'EUR') { + return `€ ${Number(v ?? 0).toFixed(2)}` +} + +/* ---------- component ---------- */ +export default function InvoiceDetailModal({ + invoice, + open, + onClose, + onStatusChanged, + onRunPoolCheck, + onExport, +}: InvoiceDetailModalProps) { + const token = useAuthStore((s) => s.accessToken) + + // detail data + const [items, setItems] = useState([]) + const [payments, setPayments] = useState([]) + const [detailLoading, setDetailLoading] = useState(false) + const [detailError, setDetailError] = useState('') + + // status change + const [changingStatus, setChangingStatus] = useState(false) + const [statusMsg, setStatusMsg] = useState('') + const [statusErr, setStatusErr] = useState('') + const [currentStatus, setCurrentStatus] = useState(invoice?.status ?? 'draft') + + // keep current status in sync with prop + useEffect(() => { + if (invoice) setCurrentStatus(invoice.status ?? 'draft') + }, [invoice]) + + // fetch detail (items + payments) when opened + const fetchDetail = useCallback(async () => { + if (!invoice?.id || !token) return + setDetailLoading(true) + setDetailError('') + try { + const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' + const res = await fetch(`${base}/api/admin/invoices/${encodeURIComponent(String(invoice.id))}/detail`, { + method: 'GET', + credentials: 'include', + headers: { + Accept: 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + }) + const body = await res.json().catch(() => ({})) + if (!res.ok || body?.success === false) { + setDetailError(body?.message || `Failed to load details (${res.status})`) + return + } + setItems(Array.isArray(body?.data?.items) ? body.data.items : []) + setPayments(Array.isArray(body?.data?.payments) ? body.data.payments : []) + } catch (e: any) { + setDetailError(e?.message || 'Network error') + } finally { + setDetailLoading(false) + } + }, [invoice?.id, token]) + + useEffect(() => { + if (open && invoice) { + void fetchDetail() + } else { + setItems([]) + setPayments([]) + setDetailError('') + setStatusMsg('') + setStatusErr('') + } + }, [open, invoice, fetchDetail]) + + // change status + async function handleStatusChange(newStatus: string) { + if (!invoice?.id || !token || newStatus === currentStatus) return + setChangingStatus(true) + setStatusErr('') + setStatusMsg('') + try { + const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' + const res = await fetch(`${base}/api/admin/invoices/${encodeURIComponent(String(invoice.id))}/status`, { + method: 'PATCH', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ status: newStatus }), + }) + const body = await res.json().catch(() => ({})) + if (!res.ok || body?.success === false) { + setStatusErr(body?.message || `Failed to update status (${res.status})`) + return + } + setCurrentStatus(newStatus) + // Build status message including pool booking info + let msg = `Status updated to "${newStatus}"` + if (body?.poolResult) { + const pr = body.poolResult + if (pr.error) { + msg += ` — Pool booking error: ${pr.error}` + } else if (pr.inserted > 0) { + msg += ` — ${pr.inserted} pool inflow(s) booked` + } else if (pr.reason && pr.reason !== 'ok') { + const reasonLabels: Record = { + invalid_invoice_id: 'Invalid invoice ID', + invoice_not_found: 'Invoice not found for pool booking', + invoice_not_paid: 'Invoice not marked as paid', + unsupported_source_type: 'Not a subscription invoice — no pool booking', + missing_abonement_relation: 'No linked subscription — no pool booking', + abonement_not_found: 'Linked subscription not found', + no_breakdown_lines: 'Subscription has no capsule breakdown — no pool booking', + no_active_system_pools: 'No active system pools found', + } + msg += ` — ${reasonLabels[pr.reason] || pr.reason}` + } + } + setStatusMsg(msg) + // Re-fetch detail to pick up any payment records (e.g. after marking paid) + void fetchDetail() + onStatusChanged?.() + } catch (e: any) { + setStatusErr(e?.message || 'Network error') + } finally { + setChangingStatus(false) + } + } + + if (!invoice) return null + + const statusConf = STATUS_CONFIG[(currentStatus as InvoiceStatus)] ?? STATUS_CONFIG.draft + const StatusIcon = statusConf.icon as React.ComponentType> + + return ( + + + {/* backdrop */} + +
+ + + {/* panel */} +
+
+ + + {/* ─── header ─── */} +
+
+
+ +
+
+ + Invoice {invoice.invoice_number ?? `#${invoice.id}`} + +

+ Created {fmtDateTime(invoice.created_at)} +

+
+
+ +
+ + {/* ─── body ─── */} +
+ + {/* status badge + status switcher */} +
+
+ + + {statusConf.label} + +
+
+
+ Change status: + {STATUSES.map((s) => { + const sc = STATUS_CONFIG[s] + const active = s === currentStatus + return ( + + ) + })} +
+
+ + {/* status feedback */} + {changingStatus && ( +
+ Updating status… +
+ )} + {statusMsg && ( +
+ {statusMsg} +
+ )} + {statusErr && ( +
+ {statusErr} +
+ )} + + {/* ── two-column info cards ── */} +
+ {/* Customer info */} +
+
+ Customer +
+ + + + + + +
+ + {/* Financial info */} +
+
+ Financials +
+ + + + + +
+
+ + {/* Dates */} +
+
+ Dates +
+
+ + + + +
+
+ + {/* Line items */} +
+
+ Line Items +
+ {detailLoading ? ( +
+
+
+
+ ) : detailError ? ( +
{detailError}
+ ) : items.length === 0 ? ( +
No line items found.
+ ) : ( +
+ + + + + + + + + + + + {items.map((item, i) => ( + + + + + + + + ))} + + + + + + + +
DescriptionQtyUnit PriceTaxGross
{item.description || '—'}{item.quantity ?? 0}{fmtMoney(item.unit_price)}{item.tax_rate != null ? `${item.tax_rate}%` : '—'}{fmtMoney(item.line_gross)}
Total{fmtMoney(invoice.total_gross)}
+
+ )} +
+ + {/* Payments */} + {payments.length > 0 && ( +
+
+ Payments +
+
+ + + + + + + + + + + + {payments.map((p, i) => ( + + + + + + + + ))} + +
MethodTransactionAmountPaid AtStatus
{p.payment_method ?? '—'}{p.transaction_id ?? '—'}{p.amount != null ? fmtMoney(p.amount) : '—'}{fmtDateTime(p.paid_at)} + + {p.status ?? '—'} + +
+
+
+ )} + + {/* Context (raw JSON if present) */} + {invoice.context && ( +
+ + Context / Metadata + (click to expand) + +
+                        {typeof invoice.context === 'string' ? invoice.context : JSON.stringify(invoice.context, null, 2)}
+                      
+
+ )} +
+ + {/* ─── footer actions ─── */} +
+
+ + +
+ +
+ + +
+
+
+
+ ) +} + +/* ---------- sub-components ---------- */ +function InfoRow({ label, value, highlight = false }: { label: string; value?: string | null; highlight?: boolean }) { + return ( +
+ {label} + + {value || '—'} + +
+ ) +} + +function DateChip({ label, value }: { label: string; value?: string | null }) { + return ( +
+
{label}
+
{fmtDate(value)}
+
+ ) +} diff --git a/src/app/admin/finance-management/page.tsx b/src/app/admin/finance-management/page.tsx index 3e315b4..9d6f73d 100644 --- a/src/app/admin/finance-management/page.tsx +++ b/src/app/admin/finance-management/page.tsx @@ -3,8 +3,9 @@ import React, { useMemo, useState } from 'react' import PageLayout from '../../components/PageLayout' import { useRouter } from 'next/navigation' import { useVatRates } from './hooks/getTaxes' -import { useAdminInvoices } from './hooks/getInvoices' +import { useAdminInvoices, type AdminInvoice } from './hooks/getInvoices' import useAuthStore from '../../store/authStore' +import InvoiceDetailModal from './components/InvoiceDetailModal' export default function FinanceManagementPage() { const router = useRouter() @@ -16,6 +17,7 @@ export default function FinanceManagementPage() { const [diagError, setDiagError] = useState('') const [diagData, setDiagData] = useState(null) const [selectedInvoice, setSelectedInvoice] = useState(null) + const [detailModalOpen, setDetailModalOpen] = useState(false) // NEW: fetch invoices from backend const { @@ -248,7 +250,7 @@ export default function FinanceManagementPage() { Pool Coffee Capsules - Net Amount + Amount (gross) Booked @@ -258,7 +260,7 @@ export default function FinanceManagementPage() { {c.pool_name} #{c.coffee_table_id} {c.capsules_count} - €{Number(c.amount_net ?? 0).toFixed(2)} + €{Number(c.amount_gross ?? c.amount_net ?? 0).toFixed(2)} {c.already_booked ? 'yes' : 'no'} ))} @@ -322,23 +324,11 @@ export default function FinanceManagementPage() { - - )) @@ -348,22 +338,14 @@ export default function FinanceManagementPage() { {selectedInvoice && ( -
-
-
- Invoice details: {selectedInvoice.invoice_number ?? selectedInvoice.id} -
- -
-
-{JSON.stringify(selectedInvoice, null, 2)}
-                
-
+ { setDetailModalOpen(false); setTimeout(() => setSelectedInvoice(null), 200) }} + onStatusChanged={reload} + onRunPoolCheck={(id) => { setDetailModalOpen(false); runPoolCheck(id) }} + onExport={(inv) => exportInvoice(inv)} + /> )} diff --git a/src/app/admin/pool-management/manage/page.tsx b/src/app/admin/pool-management/manage/page.tsx index 6fd61ec..c9881af 100644 --- a/src/app/admin/pool-management/manage/page.tsx +++ b/src/app/admin/pool-management/manage/page.tsx @@ -8,15 +8,14 @@ import { useRouter, useSearchParams } from 'next/navigation' import useAuthStore from '../../../store/authStore' import PageTransitionEffect from '../../../components/animation/pageTransitionEffect' import { AdminAPI } from '../../../utils/api' -import { authFetch } from '../../../utils/authFetch' import ConfirmActionModal from '../../../components/modals/ConfirmActionModal' type PoolUser = { id: string name: string email: string - contributed: number - joinedAt: string // NEW: member since + share: number + joinedAt: string } function PoolManagePageInner() { @@ -52,8 +51,6 @@ function PoolManagePageInner() { const poolName = searchParams.get('pool_name') ?? 'Unnamed Pool' const poolDescription = searchParams.get('description') ?? '' const poolPrice = parseFloat(searchParams.get('price') ?? '0') - const initialSubscriptionId = searchParams.get('subscription_coffee_id') ?? '' - const subscriptionTitle = searchParams.get('subscription_title') ?? '' const poolType = searchParams.get('pool_type') as 'coffee' | 'other' || 'other' const poolIsActive = searchParams.get('is_active') === 'true' const poolCreatedAt = searchParams.get('createdAt') ?? new Date().toISOString() @@ -80,12 +77,6 @@ function PoolManagePageInner() { const [removingMemberId, setRemovingMemberId] = React.useState(null) const [removeError, setRemoveError] = React.useState('') const [removeConfirm, setRemoveConfirm] = React.useState<{ userId: string; label: string } | null>(null) - const [subscriptions, setSubscriptions] = React.useState>([]) - const [linkedSubscriptionId, setLinkedSubscriptionId] = React.useState(initialSubscriptionId) - const [savingSubscription, setSavingSubscription] = React.useState(false) - const [subscriptionMessage, setSubscriptionMessage] = React.useState('') - const [subscriptionError, setSubscriptionError] = React.useState('') - const [currentSubscriptionTitle, setCurrentSubscriptionTitle] = React.useState(subscriptionTitle) async function fetchMembers() { if (!token || !poolId || poolId === 'pool-unknown') return @@ -102,7 +93,7 @@ function PoolManagePageInner() { id: String(row.id), name: name || String(row.email || '').trim() || 'Unnamed user', email: String(row.email || '').trim(), - contributed: 0, + share: Number(row.share ?? 0), joinedAt: row.joined_at || new Date().toISOString() } }) @@ -118,12 +109,14 @@ function PoolManagePageInner() { void fetchMembers() }, [token, poolId]) + // Fetch pool inflow stats React.useEffect(() => { + if (!token || !poolId || poolId === 'pool-unknown') return let cancelled = false - async function loadSubscriptions() { + async function loadStats() { try { const base = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001' - const response = await authFetch(`${base}/api/admin/coffee`, { + const res = await fetch(`${base}/api/admin/pools/${encodeURIComponent(poolId)}/stats`, { method: 'GET', credentials: 'include', headers: { @@ -131,62 +124,19 @@ function PoolManagePageInner() { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, }) - if (!response.ok) { - if (!cancelled) setSubscriptions([]) - return + const body = await res.json().catch(() => ({})) + if (!cancelled && res.ok && body?.success) { + setTotalAmount(Number(body.data?.total_amount ?? 0)) + setAmountThisYear(Number(body.data?.amount_this_year ?? 0)) + setAmountThisMonth(Number(body.data?.amount_this_month ?? 0)) } - const rows = await response.json().catch(() => []) - const mapped = Array.isArray(rows) - ? rows - .map((r: any) => ({ id: Number(r?.id), title: String(r?.title || '').trim() })) - .filter((r: { id: number; title: string }) => Number.isFinite(r.id) && r.id > 0 && !!r.title) - : [] - if (!cancelled) setSubscriptions(mapped) } catch { - if (!cancelled) setSubscriptions([]) + // ignore — stats are non-critical } } - void loadSubscriptions() - return () => { - cancelled = true - } - }, [token]) - - async function saveLinkedSubscription() { - if (!poolId || poolId === 'pool-unknown') return - setSavingSubscription(true) - setSubscriptionError('') - setSubscriptionMessage('') - try { - const base = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001' - const payload = { - subscription_coffee_id: linkedSubscriptionId ? Number(linkedSubscriptionId) : null, - } - const response = await authFetch(`${base}/api/admin/pools/${encodeURIComponent(String(poolId))}/subscription`, { - method: 'PATCH', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - body: JSON.stringify(payload), - }) - - const body = await response.json().catch(() => ({})) - if (!response.ok || body?.success === false) { - setSubscriptionError(body?.message || `Failed to update (${response.status})`) - return - } - const selectedTitle = subscriptions.find(s => String(s.id) === String(linkedSubscriptionId))?.title || '' - setCurrentSubscriptionTitle(selectedTitle) - setSubscriptionMessage(`Linked subscription updated${selectedTitle ? `: ${selectedTitle}` : ' (not linked)'}.`) - } catch (e: any) { - setSubscriptionError(e?.message || 'Failed to update linked subscription.') - } finally { - setSavingSubscription(false) - } - } + void loadStats() + return () => { cancelled = true } + }, [token, poolId]) // Early return AFTER all hooks are declared to keep consistent order if (!authChecked) return null @@ -316,34 +266,44 @@ function PoolManagePageInner() { } } + const isCore = poolName === 'Core' + return ( -
+
{/* main wrapper: avoid high z-index stacking */}
{/* Header (remove sticky/z-10) */} -
+
+ {isCore && ( +
+ + Core Pool — 1¢ per capsule per member +
+ )}
-
- +
+
-

{poolName}

-

+

{poolName}

+

{poolDescription ? poolDescription : 'Manage users and track pool funds'}

-
+
{!poolIsActive ? 'Inactive' : 'Active'} - Price/capsule (net): € {Number(poolPrice || 0).toFixed(2)} - - Subscription: {currentSubscriptionTitle || 'Not linked'} + Price/capsule (gross): € {Number(poolPrice || 0).toFixed(2)}{isCore ? ' × each member' : ''} Created {new Date(poolCreatedAt).toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: '2-digit' })} @@ -362,39 +322,6 @@ function PoolManagePageInner() {
-
-

Linked Subscription

-
-
- - -

Current: {currentSubscriptionTitle || 'Not linked'}

-
- -
- {subscriptionMessage && ( -
{subscriptionMessage}
- )} - {subscriptionError && ( -
{subscriptionError}
- )} -
- {/* Stats (now zero until backend wired) */}
@@ -435,7 +362,12 @@ function PoolManagePageInner() { {/* Unified Members card: add button + list */}
-

Members

+
+

Members

+ + {users.length} + +
)} -
- {users.map(u => ( -
-
-
-
- -
-
-

{u.name}

-

{u.email}

-
-
- - € {u.contributed.toLocaleString()} - -
-
- Member since:{' '} - - {new Date(u.joinedAt).toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: '2-digit' })} - -
-
- -
-
- ))} - {membersLoading && ( -
- Loading members... -
- )} - {membersError && !membersLoading && ( -
- {membersError} -
- )} - {users.length === 0 && !membersLoading && !membersError && ( -
- No users in this pool yet. -
- )} -
+ + {membersLoading && ( +
Loading members...
+ )} + {membersError && !membersLoading && ( +
{membersError}
+ )} + {users.length === 0 && !membersLoading && !membersError && ( +
No users in this pool yet.
+ )} + + {users.length > 0 && !membersLoading && ( +
+ + + + + + + + + + + {users.map(u => ( + + + + + + + + ))} + +
NameEmailMember Since{isCore ? 'Total Earned' : 'Share'} +
+
+
+ {(u.name?.[0] || '?').toUpperCase()} +
+ {u.name} +
+
{u.email} + {new Date(u.joinedAt).toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: '2-digit' })} + + 0 + ? 'bg-green-50 text-green-700 border border-green-200' + : 'bg-gray-50 text-gray-500 border border-gray-200' + }`}> + € {u.share.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + + + +
+
+ )}
@@ -509,7 +453,7 @@ function PoolManagePageInner() {
setSearchOpen(false)} />
-
+
{/* Header */}

Add user to pool

@@ -560,7 +504,7 @@ function PoolManagePageInner() {
{/* Results */} -
+
{error &&
{error}
} {!error && query.trim().length < 3 && (
diff --git a/src/app/admin/pool-management/page.tsx b/src/app/admin/pool-management/page.tsx index 78e0796..dba1be5 100644 --- a/src/app/admin/pool-management/page.tsx +++ b/src/app/admin/pool-management/page.tsx @@ -6,12 +6,9 @@ import Footer from '../../components/Footer' import { UsersIcon } from '@heroicons/react/24/outline' import { useAdminPools } from './hooks/getlist' import useAuthStore from '../../store/authStore' -import { addPool } from './hooks/addPool' import { useRouter } from 'next/navigation' import { setPoolInactive, setPoolActive } from './hooks/poolStatus' import PageTransitionEffect from '../../components/animation/pageTransitionEffect' -import CreateNewPoolModal from './components/createNewPoolModal' -import { authFetch } from '../../utils/authFetch' import ConfirmActionModal from '../../components/modals/ConfirmActionModal' type Pool = { @@ -19,8 +16,6 @@ type Pool = { pool_name: string description?: string price?: number - subscription_coffee_id?: number | null - subscription_title?: string | null pool_type?: 'coffee' | 'other' is_active?: boolean membersCount: number @@ -30,24 +25,14 @@ type Pool = { export default function PoolManagementPage() { const router = useRouter() - // Modal state - const [creating, setCreating] = React.useState(false) - const [createError, setCreateError] = React.useState('') - const [createSuccess, setCreateSuccess] = React.useState('') - const [createModalOpen, setCreateModalOpen] = React.useState(false) const [archiveError, setArchiveError] = React.useState('') const [poolStatusConfirm, setPoolStatusConfirm] = React.useState<{ poolId: string; action: 'archive' | 'activate' } | null>(null) const [poolStatusPending, setPoolStatusPending] = React.useState(false) - // Token and API URL - const token = useAuthStore(s => s.accessToken) - const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001' - // Replace local fetch with hook const { pools: initialPools, loading, error, refresh } = useAdminPools() const [pools, setPools] = React.useState([]) const [showInactive, setShowInactive] = React.useState(false) - const [subscriptions, setSubscriptions] = React.useState>([]) React.useEffect(() => { if (!loading && !error) { @@ -55,81 +40,8 @@ export default function PoolManagementPage() { } }, [initialPools, loading, error]) - React.useEffect(() => { - let cancelled = false - async function loadSubscriptions() { - try { - const base = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001' - const response = await authFetch(`${base}/api/admin/coffee`, { - method: 'GET', - credentials: 'include', - headers: { - Accept: 'application/json', - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - }) - - if (!response.ok) { - if (!cancelled) setSubscriptions([]) - return - } - - const rows = await response.json().catch(() => []) - const mapped = Array.isArray(rows) - ? rows - .map((r: any) => ({ id: Number(r?.id), title: String(r?.title || '').trim() })) - .filter((r: { id: number; title: string }) => Number.isFinite(r.id) && r.id > 0 && !!r.title) - : [] - if (!cancelled) setSubscriptions(mapped) - } catch { - if (!cancelled) setSubscriptions([]) - } - } - void loadSubscriptions() - return () => { - cancelled = true - } - }, [token]) - const filteredPools = pools.filter(p => showInactive ? !p.is_active : p.is_active) - // REPLACED: handleCreatePool to accept data from modal with new schema fields - async function handleCreatePool(data: { pool_name: string; description: string; price: number; pool_type: 'coffee' | 'other'; subscription_coffee_id: number | null }) { - setCreateError('') - setCreateSuccess('') - const pool_name = data.pool_name.trim() - const description = data.description.trim() - if (!pool_name) { - setCreateError('Please provide a pool name.') - return - } - setCreating(true) - try { - const res = await addPool({ - pool_name, - description: description || undefined, - price: data.price, - subscription_coffee_id: data.subscription_coffee_id, - pool_type: data.pool_type, - is_active: true, - }) - if (res.ok && res.body?.data) { - setCreateSuccess('Pool created successfully.') - await refresh?.() - setTimeout(() => { - setCreateModalOpen(false) - setCreateSuccess('') - }, 1500) - } else { - setCreateError(res.message || 'Failed to create pool.') - } - } catch { - setCreateError('Network error while creating pool.') - } finally { - setCreating(false) - } - } - async function handleArchive(poolId: string) { setPoolStatusConfirm({ poolId, action: 'archive' }) } @@ -196,14 +108,8 @@ export default function PoolManagementPage() {

Pool Management

-

Create and manage user pools.

+

Manage system pools and members.

-
Show: @@ -256,14 +162,28 @@ export default function PoolManagementPage() {
) : (
- {filteredPools.map(pool => ( -
+ {filteredPools.map(pool => { + const isCore = pool.pool_name === 'Core' + return ( +
+ {isCore && ( +
+ + Core Pool +
+ )}
-
- +
+
-

{pool.pool_name}

+

{pool.pool_name}

@@ -271,9 +191,6 @@ export default function PoolManagementPage() {

{pool.description || '-'}

-

- Subscription: {pool.subscription_title || 'Not linked'} -

Members @@ -296,8 +213,6 @@ export default function PoolManagementPage() { description: pool.description ?? '', price: String(pool.price ?? 0), pool_type: pool.pool_type ?? 'other', - subscription_coffee_id: pool.subscription_coffee_id != null ? String(pool.subscription_coffee_id) : '', - subscription_title: pool.subscription_title ?? '', is_active: pool.is_active ? 'true' : 'false', createdAt: pool.createdAt ?? '', }) @@ -325,7 +240,8 @@ export default function PoolManagementPage() { )}
- ))} + ) + })} {filteredPools.length === 0 && !loading && !error && (
{showInactive ? 'No inactive pools found.' : 'No active pools found.'} @@ -337,18 +253,6 @@ export default function PoolManagementPage() {
- {/* Modal for creating a new pool */} - { setCreateModalOpen(false); setCreateError(''); setCreateSuccess(''); }} - onCreate={handleCreatePool} - subscriptions={subscriptions} - creating={creating} - error={createError} - success={createSuccess} - clearMessages={() => { setCreateError(''); setCreateSuccess(''); }} - /> -