diff --git a/package-lock.json b/package-lock.json index 50b8b48..f54968f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "autoprefixer": "^10.4.21", + "baseline-browser-mapping": "^2.9.14", "eslint": "^9", "eslint-config-next": "15.5.4", "eslint-plugin-react-hooks": "^5.2.0", @@ -96,6 +97,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -506,6 +508,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -529,6 +532,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2660,9 +2664,9 @@ } }, "node_modules/@next/env": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz", - "integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz", + "integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -2676,9 +2680,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz", - "integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz", + "integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==", "cpu": [ "arm64" ], @@ -2692,9 +2696,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz", - "integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz", + "integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==", "cpu": [ "x64" ], @@ -2708,9 +2712,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz", - "integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz", + "integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==", "cpu": [ "arm64" ], @@ -2724,9 +2728,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz", - "integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz", + "integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==", "cpu": [ "arm64" ], @@ -2740,9 +2744,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz", - "integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz", + "integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==", "cpu": [ "x64" ], @@ -2756,9 +2760,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz", - "integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz", + "integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==", "cpu": [ "x64" ], @@ -2772,9 +2776,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz", - "integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz", + "integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==", "cpu": [ "arm64" ], @@ -2788,9 +2792,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz", - "integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz", + "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==", "cpu": [ "x64" ], @@ -3567,6 +3571,7 @@ "integrity": "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3633,6 +3638,7 @@ "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/types": "8.44.1", @@ -4156,6 +4162,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4525,9 +4532,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz", - "integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==", + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -4603,6 +4610,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -4996,7 +5004,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -5420,6 +5429,7 @@ "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5594,6 +5604,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7691,13 +7702,14 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.0.7", - "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", - "integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", + "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==", "license": "MIT", "dependencies": { - "@next/env": "16.0.7", + "@next/env": "16.1.1", "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -7709,14 +7721,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.0.7", - "@next/swc-darwin-x64": "16.0.7", - "@next/swc-linux-arm64-gnu": "16.0.7", - "@next/swc-linux-arm64-musl": "16.0.7", - "@next/swc-linux-x64-gnu": "16.0.7", - "@next/swc-linux-x64-musl": "16.0.7", - "@next/swc-win32-arm64-msvc": "16.0.7", - "@next/swc-win32-x64-msvc": "16.0.7", + "@next/swc-darwin-arm64": "16.1.1", + "@next/swc-darwin-x64": "16.1.1", + "@next/swc-linux-arm64-gnu": "16.1.1", + "@next/swc-linux-arm64-musl": "16.1.1", + "@next/swc-linux-x64-gnu": "16.1.1", + "@next/swc-linux-x64-musl": "16.1.1", + "@next/swc-win32-arm64-msvc": "16.1.1", + "@next/swc-win32-x64-msvc": "16.1.1", "sharp": "^0.34.4" }, "peerDependencies": { @@ -8113,6 +8125,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8831,6 +8844,7 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -8923,6 +8937,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8932,6 +8947,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8958,6 +8974,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9764,7 +9781,8 @@ "version": "4.1.13", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.2.3", @@ -9862,6 +9880,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10038,6 +10057,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index eebdb6b..0135661 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "autoprefixer": "^10.4.21", + "baseline-browser-mapping": "^2.9.14", "eslint": "^9", "eslint-config-next": "15.5.4", "eslint-plugin-react-hooks": "^5.2.0", diff --git a/src/app/admin/finance-management/hooks/getInvoices.ts b/src/app/admin/finance-management/hooks/getInvoices.ts new file mode 100644 index 0000000..ccdc95a --- /dev/null +++ b/src/app/admin/finance-management/hooks/getInvoices.ts @@ -0,0 +1,93 @@ +'use client'; + +import { useCallback, useEffect, useRef, useState } from 'react'; +import useAuthStore from '../../../store/authStore'; + +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; +}; + +export function useAdminInvoices(params?: { status?: string; limit?: number; offset?: number }) { + const accessToken = useAuthStore(s => s.accessToken); + const [invoices, setInvoices] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const inFlight = useRef(null); + + const fetchInvoices = useCallback(async () => { + setError(''); + // Abort previous + inFlight.current?.abort(); + const controller = new AbortController(); + inFlight.current = controller; + + try { + const base = process.env.NEXT_PUBLIC_API_BASE_URL || ''; + const qp = new URLSearchParams(); + if (params?.status) qp.set('status', params.status); + qp.set('limit', String(params?.limit ?? 200)); + qp.set('offset', String(params?.offset ?? 0)); + const url = `${base}/api/admin/invoices${qp.toString() ? `?${qp.toString()}` : ''}`; + + setLoading(true); + const res = await fetch(url, { + method: 'GET', + credentials: 'include', + headers: { + 'Accept': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + signal: controller.signal, + }); + + const body = await res.json().catch(() => ({})); + if (!res.ok || body?.success === false) { + setInvoices([]); + setError(body?.message || `Failed to load invoices (${res.status})`); + return; + } + const list: AdminInvoice[] = Array.isArray(body?.data) ? body.data : []; + // sort fallback (issued_at DESC then created_at DESC) + list.sort((a, b) => { + const ad = new Date(a.issued_at ?? a.created_at ?? 0).getTime(); + const bd = new Date(b.issued_at ?? b.created_at ?? 0).getTime(); + return bd - ad; + }); + setInvoices(list); + } catch (e: any) { + if (e?.name === 'AbortError') return; + setError(e?.message || 'Network error'); + setInvoices([]); + } finally { + setLoading(false); + if (inFlight.current === controller) inFlight.current = null; + } + }, [accessToken, params?.status, params?.limit, params?.offset]); + + useEffect(() => { + if (accessToken) fetchInvoices(); + return () => inFlight.current?.abort(); + }, [accessToken, fetchInvoices]); + + return { invoices, loading, error, reload: fetchInvoices }; +} diff --git a/src/app/admin/finance-management/page.tsx b/src/app/admin/finance-management/page.tsx index 97ccc1d..2d67869 100644 --- a/src/app/admin/finance-management/page.tsx +++ b/src/app/admin/finance-management/page.tsx @@ -3,23 +3,7 @@ import React, { useMemo, useState } from 'react' import PageLayout from '../../components/PageLayout' import { useRouter } from 'next/navigation' import { useVatRates } from './hooks/getTaxes' - -type VatRate = { country: string; code: string; rate: number } -type Bill = { - id: string - customer: string - amount: number - currency: string - date: string - status: 'paid' | 'open' | 'overdue' -} - -const dummyBills: Bill[] = [ - { id: 'INV-1001', customer: 'Acme GmbH', amount: 1200, currency: 'EUR', date: '2025-12-01', status: 'paid' }, - { id: 'INV-1002', customer: 'Beta SARL', amount: 860, currency: 'EUR', date: '2025-11-20', status: 'open' }, - { id: 'INV-1003', customer: 'Charlie SpA', amount: 540, currency: 'EUR', date: '2025-11-15', status: 'overdue' }, - { id: 'INV-1004', customer: 'Delta BV', amount: 2300, currency: 'EUR', date: '2025-10-02', status: 'paid' }, -] +import { useAdminInvoices } from './hooks/getInvoices' export default function FinanceManagementPage() { const router = useRouter() @@ -27,38 +11,60 @@ export default function FinanceManagementPage() { const [timeframe, setTimeframe] = useState<'7d' | '30d' | '90d' | 'ytd'>('30d') const [billFilter, setBillFilter] = useState({ query: '', status: 'all', from: '', to: '' }) + // NEW: fetch invoices from backend + const { + invoices, + loading: invLoading, + error: invError, + reload, + } = useAdminInvoices({ + status: billFilter.status !== 'all' ? billFilter.status : undefined, + limit: 200, + offset: 0, + }) + + // NEW: totals from backend invoices const totals = useMemo(() => { const now = new Date() - const filterDate = (d: string) => new Date(d) const inRange = (d: Date) => { const diff = (now.getTime() - d.getTime()) / (1000 * 60 * 60 * 24) if (timeframe === '7d') return diff <= 7 if (timeframe === '30d') return diff <= 30 if (timeframe === '90d') return diff <= 90 - return true // ytd or default + return true } - const filtered = dummyBills.filter(b => inRange(filterDate(b.date))) - const total = dummyBills.reduce((s, b) => s + b.amount, 0) - const totalRange = filtered.reduce((s, b) => s + b.amount, 0) - return { totalAll: total, totalRange } - }, [timeframe]) - - const filteredBills = useMemo(() => { - return dummyBills.filter(b => { - const matchesQuery = - billFilter.query === '' || - b.id.toLowerCase().includes(billFilter.query.toLowerCase()) || - b.customer.toLowerCase().includes(billFilter.query.toLowerCase()) - const matchesStatus = billFilter.status === 'all' || b.status === billFilter.status - const fromOk = billFilter.from ? new Date(b.date) >= new Date(billFilter.from) : true - const toOk = billFilter.to ? new Date(b.date) <= new Date(billFilter.to) : true - return matchesQuery && matchesStatus && fromOk && toOk + const range = invoices.filter(inv => { + const dStr = inv.issued_at ?? inv.created_at + if (!dStr) return false + const d = new Date(dStr) + return inRange(d) }) - }, [billFilter]) + const totalAll = invoices.reduce((s, inv) => s + Number(inv.total_gross ?? 0), 0) + const totalRange = range.reduce((s, inv) => s + Number(inv.total_gross ?? 0), 0) + return { totalAll, totalRange } + }, [invoices, timeframe]) + + // NEW: filtered rows for table + const filteredBills = useMemo(() => { + const q = billFilter.query.trim().toLowerCase() + const from = billFilter.from ? new Date(billFilter.from) : null + const to = billFilter.to ? new Date(billFilter.to) : null + + return invoices.filter(inv => { + const byQuery = + !q || + String(inv.invoice_number ?? inv.id).toLowerCase().includes(q) || + String(inv.buyer_name ?? '').toLowerCase().includes(q) + const issued = inv.issued_at ? new Date(inv.issued_at) : (inv.created_at ? new Date(inv.created_at) : null) + const byFrom = from ? (issued ? issued >= from : false) : true + const byTo = to ? (issued ? issued <= to : false) : true + return byQuery && byFrom && byTo + }) + }, [invoices, billFilter]) const exportBills = (format: 'csv' | 'pdf') => { - console.log('[export]', format, { filters: billFilter, bills: filteredBills }) - alert(`Export ${format.toUpperCase()} (dummy) for ${filteredBills.length} bills`) + console.log('[export]', format, { filters: billFilter, invoices: filteredBills }) + alert(`Export ${format.toUpperCase()} (dummy) for ${filteredBills.length} invoices`) } return ( @@ -129,12 +135,13 @@ export default function FinanceManagementPage() {
+
setBillFilter(f => ({ ...f, query: e.target.value }))} className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent" @@ -145,9 +152,11 @@ export default function FinanceManagementPage() { className="rounded-lg border border-gray-200 px-3 py-2 focus:ring-2 focus:ring-blue-900 focus:border-transparent" > + + - +
+ {invError && ( +
+ {invError} +
+ )} - + - {filteredBills.map(b => ( - - - - - - - - - ))} - {filteredBills.length === 0 && ( + {invLoading ? ( + <> + + + + ) : filteredBills.length === 0 ? ( + ) : ( + filteredBills.map(inv => ( + + + + + + + + + )) )}
Invoice CustomerDateIssued Amount Status Actions
{b.id}{b.customer}{new Date(b.date).toLocaleDateString()}€{b.amount.toFixed(2)} - - {b.status} - - - - -
Keine Rechnungen gefunden.
{inv.invoice_number ?? inv.id}{inv.buyer_name ?? '—'}{inv.issued_at ? new Date(inv.issued_at).toLocaleDateString() : '—'} + €{Number(inv.total_gross ?? 0).toFixed(2)}{' '} + {inv.currency ?? 'EUR'} + + + {inv.status} + + + + +
diff --git a/src/app/affiliate-links/page.tsx b/src/app/affiliate-links/page.tsx index 79a6c47..6887837 100644 --- a/src/app/affiliate-links/page.tsx +++ b/src/app/affiliate-links/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useState, useMemo } from 'react' import PageLayout from '../components/PageLayout' type Affiliate = { @@ -20,6 +20,8 @@ export default function AffiliateLinksPage() { const [affiliates, setAffiliates] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') + // NEW: selected category + const [selectedCategory, setSelectedCategory] = useState('all') useEffect(() => { async function fetchAffiliates() { @@ -63,130 +65,129 @@ export default function AffiliateLinksPage() { category: { title: affiliate.category, href: '#' }, commissionRate: affiliate.commissionRate })) + + // NEW: fixed categories from the provided image, merged with backend ones + const categories = useMemo(() => { + const fromImage = [ + 'Technology', + 'Energy', + 'Finance', + 'Healthcare', + 'Education', + 'Travel', + 'Retail', + 'Construction', + 'Food', + 'Automotive', + 'Fashion', + 'Pets', + ] + const set = new Set(fromImage) + affiliates.forEach(a => { if (a.category) set.add(a.category) }) + return ['all', ...Array.from(set)] + }, [affiliates]) + return ( -
- {/* Background Pattern */} - - - {/* Colored Blur Effect */} -