diff --git a/ToDo.txt b/ToDo.txt index c4ab384..317b25a 100644 --- a/ToDo.txt +++ b/ToDo.txt @@ -22,20 +22,11 @@ Last updated: 2026-01-20 === SEAZN TODOS === (Compromised User / Pool ) -• [x] Compromised User Fix (SAT) -• [x] Pools Complete Setup check and refactor -- Implementing Logging Layout from Alex -- Talk with him (SAT) -• [x] Adjust and add Functionality for Download Acc Data and Delete Acc (SAT) -• [X] News Management (own pages for news) + Adjust the Dashboard to Display Latest news • [ ] Unified Modal Design • [ ] Autorefresh of Site?? • [ ] UserMgmt table refactor with actions and filter options (SAT?) -• [x] Remove irrelevant statuses in userverify filter • [ ] User Status 1 Feld das wir nicht benutzen • [ ] Pool mulit user actions (select 5 -> add to pool) -• [x] reset edit templates -• [x] "Suspended" status should actually do something -• [] Matrix shit (tiefe dynamisch einstellen) -• [x] Git • [] Switching status -> confirmation modal • [] mobile scroll bug with double page on top • [] search modal unify -> only return userId(s) @@ -43,10 +34,13 @@ Last updated: 2026-01-20 + ================================================================================ QUICK SHARED / CROSSOVER ITEMS ================================================================================ +• [] Summary & Details bei abo abschluss - Country nur DE + Delivery Interval richtig? oder nur monthly +• [] Wieviele Abos können auf einen pool verlinkt werden? • [ ] Security Headers • [ ] Dependency Management • [ ] SYSTEM ABSCHALTUNG VERHINDERN -- Exoscale Role? / Security Konzept diff --git a/src/app/admin/finance-management/page.tsx b/src/app/admin/finance-management/page.tsx index 2d67869..3e315b4 100644 --- a/src/app/admin/finance-management/page.tsx +++ b/src/app/admin/finance-management/page.tsx @@ -4,12 +4,18 @@ import PageLayout from '../../components/PageLayout' import { useRouter } from 'next/navigation' import { useVatRates } from './hooks/getTaxes' import { useAdminInvoices } from './hooks/getInvoices' +import useAuthStore from '../../store/authStore' export default function FinanceManagementPage() { const router = useRouter() + const accessToken = useAuthStore(s => s.accessToken) const { rates, loading: vatLoading, error: vatError } = useVatRates() const [timeframe, setTimeframe] = useState<'7d' | '30d' | '90d' | 'ytd'>('30d') const [billFilter, setBillFilter] = useState({ query: '', status: 'all', from: '', to: '' }) + const [diagLoading, setDiagLoading] = useState(false) + const [diagError, setDiagError] = useState('') + const [diagData, setDiagData] = useState(null) + const [selectedInvoice, setSelectedInvoice] = useState(null) // NEW: fetch invoices from backend const { @@ -67,6 +73,47 @@ export default function FinanceManagementPage() { alert(`Export ${format.toUpperCase()} (dummy) for ${filteredBills.length} invoices`) } + const runPoolCheck = async (invoiceId: string | number) => { + setDiagLoading(true) + setDiagError('') + setDiagData(null) + try { + const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' + const url = `${base}/api/admin/pools/inflow-diagnostics?invoiceId=${encodeURIComponent(String(invoiceId))}` + const res = await fetch(url, { + method: 'GET', + credentials: 'include', + headers: { + Accept: 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + }) + const body = await res.json().catch(() => ({})) + if (!res.ok || body?.success === false) { + setDiagError(body?.message || `Check failed (${res.status})`) + return + } + setDiagData(body?.data || null) + } catch (e: any) { + setDiagError(e?.message || 'Network error') + } finally { + setDiagLoading(false) + } + } + + const exportInvoice = (inv: AdminInvoice) => { + const pretty = JSON.stringify(inv, null, 2) + const blob = new Blob([pretty], { type: 'application/json;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `invoice-${inv.invoice_number || inv.id}.json` + document.body.appendChild(a) + a.click() + a.remove() + URL.revokeObjectURL(url) + } + return (
@@ -178,6 +225,51 @@ export default function FinanceManagementPage() { {invError}
)} + {(diagLoading || diagError || diagData) && ( +
+ {diagLoading &&
Checking pool inflow...
} + {!diagLoading && diagError &&
{diagError}
} + {!diagLoading && !diagError && diagData && ( +
+
Pool inflow diagnostic for invoice #{diagData.invoice_id ?? '—'}
+
+ Status: {diagData.ok ? 'OK' : 'Blocked'} • Reason: {diagData.reason} +
+ {diagData.ok && ( +
+ Abonement: {diagData.abonement_id} • Will book: {diagData.will_book_count} • Already booked: {diagData.already_booked_count} +
+ )} + {Array.isArray(diagData.candidates) && diagData.candidates.length > 0 && ( +
+ + + + + + + + + + + + {diagData.candidates.map((c: any) => ( + + + + + + + + ))} + +
PoolCoffeeCapsulesNet AmountBooked
{c.pool_name}#{c.coffee_table_id}{c.capsules_count}€{Number(c.amount_net ?? 0).toFixed(2)}{c.already_booked ? 'yes' : 'no'}
+
+ )} +
+ )} +
+ )} @@ -229,8 +321,24 @@ export default function FinanceManagementPage() { )) @@ -238,6 +346,25 @@ export default function FinanceManagementPage() {
- - + + +
+ + {selectedInvoice && ( +
+
+
+ Invoice details: {selectedInvoice.invoice_number ?? selectedInvoice.id} +
+ +
+
+{JSON.stringify(selectedInvoice, null, 2)}
+                
+
+ )} diff --git a/src/app/admin/pool-management/components/createNewPoolModal.tsx b/src/app/admin/pool-management/components/createNewPoolModal.tsx index 2aa4e93..25b0b53 100644 --- a/src/app/admin/pool-management/components/createNewPoolModal.tsx +++ b/src/app/admin/pool-management/components/createNewPoolModal.tsx @@ -4,7 +4,8 @@ import React from 'react' interface Props { isOpen: boolean onClose: () => void - onCreate: (data: { pool_name: string; description: string; price: number; pool_type: 'coffee' | 'other' }) => void | Promise + onCreate: (data: { pool_name: string; description: string; price: number; pool_type: 'coffee' | 'other'; subscription_coffee_id: number | null }) => void | Promise + subscriptions: Array<{ id: number; title: string }> creating: boolean error?: string success?: string @@ -15,6 +16,7 @@ export default function CreateNewPoolModal({ isOpen, onClose, onCreate, + subscriptions, creating, error, success, @@ -24,6 +26,7 @@ export default function CreateNewPoolModal({ const [description, setDescription] = React.useState('') const [price, setPrice] = React.useState('0.00') const [poolType, setPoolType] = React.useState<'coffee' | 'other'>('other') + const [subscriptionCoffeeId, setSubscriptionCoffeeId] = React.useState('') const isDisabled = creating || !!success @@ -33,6 +36,7 @@ export default function CreateNewPoolModal({ setDescription('') setPrice('0.00') setPoolType('other') + setSubscriptionCoffeeId('') } }, [isOpen]) @@ -73,7 +77,13 @@ export default function CreateNewPoolModal({ onSubmit={e => { e.preventDefault() clearMessages() - onCreate({ pool_name: poolName, description, price: parseFloat(price) || 0, pool_type: poolType }) + onCreate({ + pool_name: poolName, + description, + price: parseFloat(price) || 0, + pool_type: poolType, + subscription_coffee_id: subscriptionCoffeeId ? Number(subscriptionCoffeeId) : null, + }) }} className="space-y-4" > @@ -100,7 +110,7 @@ export default function CreateNewPoolModal({ />
- + +

This value is stored as net price.

@@ -125,6 +136,20 @@ export default function CreateNewPoolModal({
+
+ + +
+
+

Linked Subscription

+
+
+ + +

Current: {currentSubscriptionTitle || 'Not linked'}

+
+ +
+ {subscriptionMessage && ( +
{subscriptionMessage}
+ )} + {subscriptionError && ( +
{subscriptionError}
+ )} +
+ {/* Stats (now zero until backend wired) */}
diff --git a/src/app/admin/pool-management/page.tsx b/src/app/admin/pool-management/page.tsx index e1cd830..6aa88c1 100644 --- a/src/app/admin/pool-management/page.tsx +++ b/src/app/admin/pool-management/page.tsx @@ -11,12 +11,15 @@ 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' type Pool = { id: string 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 @@ -34,13 +37,14 @@ export default function PoolManagementPage() { const [archiveError, setArchiveError] = React.useState('') // Token and API URL - const token = useAuthStore.getState().accessToken + 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) { @@ -48,10 +52,46 @@ 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' }) { + 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() @@ -62,7 +102,14 @@ export default function PoolManagementPage() { } setCreating(true) try { - const res = await addPool({ pool_name, description: description || undefined, price: data.price, pool_type: data.pool_type, is_active: true }) + 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?.() @@ -219,6 +266,9 @@ export default function PoolManagementPage() {

{pool.description || '-'}

+

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

Members @@ -241,6 +291,8 @@ 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 ?? '', }) @@ -285,6 +337,7 @@ export default function PoolManagementPage() { isOpen={createModalOpen} onClose={() => { setCreateModalOpen(false); setCreateError(''); setCreateSuccess(''); }} onCreate={handleCreatePool} + subscriptions={subscriptions} creating={creating} error={createError} success={createSuccess}