diff --git a/pitch-deck/app/api/finanzplan/[sheetName]/route.ts b/pitch-deck/app/api/finanzplan/[sheetName]/route.ts new file mode 100644 index 0000000..acf46c8 --- /dev/null +++ b/pitch-deck/app/api/finanzplan/[sheetName]/route.ts @@ -0,0 +1,100 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db' + +const TABLE_MAP: Record = { + kunden: 'fp_kunden', + kunden_summary: 'fp_kunden_summary', + umsatzerloese: 'fp_umsatzerloese', + materialaufwand: 'fp_materialaufwand', + personalkosten: 'fp_personalkosten', + betriebliche: 'fp_betriebliche_aufwendungen', + investitionen: 'fp_investitionen', + sonst_ertraege: 'fp_sonst_ertraege', + liquiditaet: 'fp_liquiditaet', + guv: 'fp_guv', +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ sheetName: string }> } +) { + const { sheetName } = await params + const table = TABLE_MAP[sheetName] + if (!table) { + return NextResponse.json({ error: `Unknown sheet: ${sheetName}` }, { status: 400 }) + } + + const scenarioId = request.nextUrl.searchParams.get('scenarioId') + + try { + let query = `SELECT * FROM ${table}` + const params: string[] = [] + + if (scenarioId) { + query += ' WHERE scenario_id = $1' + params.push(scenarioId) + } else { + query += ' WHERE scenario_id = (SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1)' + } + query += ' ORDER BY sort_order' + + const { rows } = await pool.query(query, params) + return NextResponse.json({ sheet: sheetName, rows }) + } catch (error) { + return NextResponse.json({ error: String(error) }, { status: 500 }) + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ sheetName: string }> } +) { + const { sheetName } = await params + const table = TABLE_MAP[sheetName] + if (!table) { + return NextResponse.json({ error: `Unknown sheet: ${sheetName}` }, { status: 400 }) + } + + try { + const body = await request.json() + const { rowId, updates } = body // updates: { field: value } or { m3: 1500 } for monthly values + + if (!rowId) { + return NextResponse.json({ error: 'rowId required' }, { status: 400 }) + } + + // Check if updating monthly values (JSONB) or scalar fields + const monthlyKeys = Object.keys(updates).filter(k => k.startsWith('m') && !isNaN(parseInt(k.substring(1)))) + const scalarKeys = Object.keys(updates).filter(k => !k.startsWith('m') || isNaN(parseInt(k.substring(1)))) + + if (monthlyKeys.length > 0) { + // Update specific months in the values JSONB + const jsonbSet = monthlyKeys.map(k => `'${k}', '${updates[k]}'::jsonb`).join(', ') + const valuesCol = sheetName === 'personalkosten' ? 'values_brutto' : 'values' + // Use jsonb_set for each key + let updateSql = `UPDATE ${table} SET ` + const setClauses: string[] = [] + for (const k of monthlyKeys) { + setClauses.push(`${valuesCol} = jsonb_set(${valuesCol}, '{${k}}', '${updates[k]}')`) + } + setClauses.push(`updated_at = NOW()`) + updateSql += setClauses.join(', ') + ` WHERE id = $1` + await pool.query(updateSql, [rowId]) + } + + if (scalarKeys.length > 0) { + // Update scalar columns directly + const setClauses = scalarKeys.map((k, i) => `${k} = $${i + 2}`).join(', ') + await pool.query( + `UPDATE ${table} SET ${setClauses}, updated_at = NOW() WHERE id = $1`, + [rowId, ...scalarKeys.map(k => updates[k])] + ) + } + + // Return updated row + const { rows } = await pool.query(`SELECT * FROM ${table} WHERE id = $1`, [rowId]) + return NextResponse.json({ updated: rows[0] }) + } catch (error) { + return NextResponse.json({ error: String(error) }, { status: 500 }) + } +} diff --git a/pitch-deck/app/api/finanzplan/compute/route.ts b/pitch-deck/app/api/finanzplan/compute/route.ts new file mode 100644 index 0000000..1e6df56 --- /dev/null +++ b/pitch-deck/app/api/finanzplan/compute/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db' +import { computeFinanzplan } from '@/lib/finanzplan/engine' + +export async function POST(request: NextRequest) { + try { + const body = await request.json().catch(() => ({})) + const scenarioId = body.scenarioId + + // Get scenario ID + let sid = scenarioId + if (!sid) { + const { rows } = await pool.query('SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1') + if (rows.length === 0) { + return NextResponse.json({ error: 'No default scenario found' }, { status: 404 }) + } + sid = rows[0].id + } + + const result = await computeFinanzplan(pool, sid) + + return NextResponse.json({ + scenarioId: sid, + computed: true, + summary: { + total_revenue_y2026: result.umsatzerloese.total.m1 !== undefined + ? Object.values(result.umsatzerloese.total).reduce((a, b) => a + b, 0) + : 0, + total_costs_y2026: Object.values(result.betriebliche.total_gesamt).reduce((a, b) => a + b, 0), + headcount_m60: result.personalkosten.headcount.m60 || 0, + cash_balance_m60: result.liquiditaet.endstand.m60 || 0, + }, + }) + } catch (error) { + console.error('Finanzplan compute error:', error) + return NextResponse.json({ error: String(error) }, { status: 500 }) + } +} diff --git a/pitch-deck/app/api/finanzplan/route.ts b/pitch-deck/app/api/finanzplan/route.ts new file mode 100644 index 0000000..6788e9f --- /dev/null +++ b/pitch-deck/app/api/finanzplan/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from 'next/server' +import pool from '@/lib/db' +import { SHEET_LIST } from '@/lib/finanzplan/types' + +export async function GET() { + try { + const scenarios = await pool.query('SELECT * FROM fp_scenarios ORDER BY is_default DESC, name') + + // Get row counts per sheet + const sheets = await Promise.all( + SHEET_LIST.map(async (s) => { + const tableName = `fp_${s.name}` + try { + const { rows } = await pool.query( + `SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE is_editable = true) as editable FROM ${tableName} WHERE scenario_id = (SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1)` + ) + return { ...s, rows: parseInt(rows[0]?.total || '0'), editable_rows: parseInt(rows[0]?.editable || '0') } + } catch { + return s + } + }) + ) + + return NextResponse.json({ + sheets, + scenarios: scenarios.rows, + months: { start: '2026-01', end: '2030-12', count: 60, founding: '2026-08' }, + }) + } catch (error) { + return NextResponse.json({ error: String(error) }, { status: 500 }) + } +} diff --git a/pitch-deck/components/PitchDeck.tsx b/pitch-deck/components/PitchDeck.tsx index 8616604..f70230d 100644 --- a/pitch-deck/components/PitchDeck.tsx +++ b/pitch-deck/components/PitchDeck.tsx @@ -41,6 +41,7 @@ import RegulatorySlide from './slides/RegulatorySlide' import EngineeringSlide from './slides/EngineeringSlide' import AIPipelineSlide from './slides/AIPipelineSlide' import SDKDemoSlide from './slides/SDKDemoSlide' +import FinanzplanSlide from './slides/FinanzplanSlide' interface PitchDeckProps { lang: Language @@ -163,6 +164,8 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) { return case 'annex-sdk-demo': return + case 'annex-finanzplan': + return default: return null } diff --git a/pitch-deck/components/slides/CompetitionSlide.tsx b/pitch-deck/components/slides/CompetitionSlide.tsx index aca4b7e..57b2769 100644 --- a/pitch-deck/components/slides/CompetitionSlide.tsx +++ b/pitch-deck/components/slides/CompetitionSlide.tsx @@ -280,7 +280,8 @@ const PRICING_COMPARISON: CompetitorPricing[] = [ model: 'Cloud (BSI DE / OVH FR)', publicPricing: true, tiers: [ - { name: { de: '<50 MA', en: '<50 emp.' }, price: 'ab €1.250/mo', annual: 'ab €15.000/yr', notes: { de: 'Cloud, modular, 84 Regularien', en: 'Cloud, modular, 84 regulations' } }, + { name: { de: 'Startup/<10', en: 'Startup/<10' }, price: 'ab €300/mo', annual: 'ab €3.600/yr', notes: { de: '14-Tage-Test, Kreditkarte', en: '14-day trial, credit card' } }, + { name: { de: '10-50 MA', en: '10-50 emp.' }, price: 'ab €1.250/mo', annual: 'ab €15.000/yr', notes: { de: 'Cloud, modular, 84 Regularien', en: 'Cloud, modular, 84 regulations' } }, { name: { de: '50-250 MA', en: '50-250 emp.' }, price: 'ab €2.500/mo', annual: 'ab €30.000/yr', notes: { de: 'Cloud, alle Module, Priority', en: 'Cloud, all modules, priority' } }, { name: { de: '250+ MA', en: '250+ emp.' }, price: 'ab €3.500/mo', annual: 'ab €40.000/yr', notes: { de: 'Cloud, Enterprise, Dedicated', en: 'Cloud, enterprise, dedicated' } }, ], diff --git a/pitch-deck/components/slides/FinanzplanSlide.tsx b/pitch-deck/components/slides/FinanzplanSlide.tsx new file mode 100644 index 0000000..6aed2b9 --- /dev/null +++ b/pitch-deck/components/slides/FinanzplanSlide.tsx @@ -0,0 +1,251 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' +import { Language } from '@/lib/types' +import { t } from '@/lib/i18n' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' +import GlassCard from '../ui/GlassCard' +import { RefreshCw, Download, ChevronLeft, ChevronRight } from 'lucide-react' + +interface FinanzplanSlideProps { + lang: Language +} + +interface SheetMeta { + name: string + label_de: string + label_en: string + rows: number +} + +interface SheetRow { + id: number + row_label?: string + person_name?: string + item_name?: string + category?: string + section?: string + is_editable?: boolean + is_sum_row?: boolean + values?: Record + values_total?: Record + values_brutto?: Record + brutto_monthly?: number + position?: string + start_date?: string + purchase_amount?: number +} + +const MONTH_LABELS = [ + 'Jan', 'Feb', 'Mrz', 'Apr', 'Mai', 'Jun', + 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez', +] + +function getLabel(row: SheetRow): string { + return row.row_label || row.person_name || row.item_name || '—' +} + +function getValues(row: SheetRow): Record { + return row.values || row.values_total || row.values_brutto || {} +} + +function formatCell(v: number | undefined): string { + if (v === undefined || v === null) return '' + if (v === 0) return '—' + if (Math.abs(v) >= 1000) return v.toLocaleString('de-DE', { maximumFractionDigits: 0 }) + return v.toLocaleString('de-DE', { maximumFractionDigits: 2 }) +} + +export default function FinanzplanSlide({ lang }: FinanzplanSlideProps) { + const [sheets, setSheets] = useState([]) + const [activeSheet, setActiveSheet] = useState('personalkosten') + const [rows, setRows] = useState([]) + const [loading, setLoading] = useState(false) + const [computing, setComputing] = useState(false) + const [yearOffset, setYearOffset] = useState(0) // 0=2026, 1=2027, ... + const de = lang === 'de' + + // Load sheet list + useEffect(() => { + fetch('/api/finanzplan') + .then(r => r.json()) + .then(data => setSheets(data.sheets || [])) + .catch(() => {}) + }, []) + + // Load sheet data + const loadSheet = useCallback(async (name: string) => { + setLoading(true) + try { + const r = await fetch(`/api/finanzplan/${name}`) + const data = await r.json() + setRows(data.rows || []) + } catch { /* ignore */ } + setLoading(false) + }, []) + + useEffect(() => { loadSheet(activeSheet) }, [activeSheet, loadSheet]) + + // Compute + const handleCompute = async () => { + setComputing(true) + try { + await fetch('/api/finanzplan/compute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }) + await loadSheet(activeSheet) + } catch { /* ignore */ } + setComputing(false) + } + + // Cell edit + const handleCellEdit = async (rowId: number, monthKey: string, newValue: string) => { + const numVal = parseFloat(newValue.replace(/[^\d.-]/g, '')) + if (isNaN(numVal)) return + + try { + await fetch(`/api/finanzplan/${activeSheet}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ rowId, updates: { [monthKey]: numVal } }), + }) + await loadSheet(activeSheet) + } catch { /* ignore */ } + } + + const currentYear = 2026 + yearOffset + const monthStart = yearOffset * 12 + 1 + const monthEnd = monthStart + 11 + + return ( +
+ +

+ {de ? 'Finanzplan' : 'Financial Plan'} +

+

{de ? '2026–2030 · Monatliche Granularitaet · Editierbar' : '2026–2030 · Monthly Granularity · Editable'}

+
+ + {/* Tab Bar */} +
+ {sheets.map(s => ( + + ))} +
+ +
+ + {/* Year Navigation */} +
+ + {currentYear} + +
+ + {/* Data Grid */} + + {loading ? ( +
{de ? 'Lade...' : 'Loading...'}
+ ) : ( + + + + + + {MONTH_LABELS.map((label, idx) => ( + + ))} + + + + {rows.map(row => { + const values = getValues(row) + const label = getLabel(row) + const isSumRow = row.is_sum_row || label.includes('GESAMT') || label.includes('Summe') || label.includes('UEBERSCHUSS') || label.includes('LIQUIDITAET') + const isEditable = row.is_editable + + // Annual sum for visible year + let annual = 0 + for (let m = monthStart; m <= monthEnd; m++) annual += values[`m${m}`] || 0 + + return ( + + + + {Array.from({ length: 12 }, (_, idx) => { + const mKey = `m${monthStart + idx}` + const v = values[mKey] || 0 + + return ( + + ) + })} + + ) + })} + +
+ {de ? 'Position' : 'Item'} + + {currentYear} + + {label} +
+
+ {isEditable && } + {label} + {row.position && ({row.position})} + {row.section && [{row.section}]} +
+
+ {formatCell(annual)} + 0 ? (isSumRow ? 'text-white/70' : 'text-white/50') : 'text-white/15' + } ${isEditable ? 'cursor-pointer hover:bg-indigo-500/10' : ''}`} + onDoubleClick={() => { + if (!isEditable) return + const input = prompt(`${label} — ${MONTH_LABELS[idx]} ${currentYear}`, String(v)) + if (input !== null) handleCellEdit(row.id, mKey, input) + }} + > + {formatCell(v)} +
+ )} +
+ +

+ {de + ? 'Doppelklick auf blaue Zellen zum Bearbeiten · Gruendung: 01.08.2026' + : 'Double-click blue cells to edit · Founding: 01.08.2026'} +

+
+ ) +} diff --git a/pitch-deck/components/slides/ProductSlide.tsx b/pitch-deck/components/slides/ProductSlide.tsx index 01c6fec..dba2aa1 100644 --- a/pitch-deck/components/slides/ProductSlide.tsx +++ b/pitch-deck/components/slides/ProductSlide.tsx @@ -27,7 +27,8 @@ const MODULES = [ ] const PRICING_TIERS = [ - { employees: '< 50', priceDe: 'ab 15.000 EUR/Jahr', priceEn: 'from EUR 15,000/yr', highlight: false }, + { employees: 'Startup / < 10', priceDe: 'ab 3.600 EUR/Jahr', priceEn: 'from EUR 3,600/yr', highlight: false, noteDe: '14 Tage Test (Kreditkarte)', noteEn: '14-day trial (credit card)' }, + { employees: '10 – 50', priceDe: 'ab 15.000 EUR/Jahr', priceEn: 'from EUR 15,000/yr', highlight: false }, { employees: '50 – 250', priceDe: 'ab 30.000 EUR/Jahr', priceEn: 'from EUR 30,000/yr', highlight: false }, { employees: '250+', priceDe: 'ab 40.000 EUR/Jahr', priceEn: 'from EUR 40,000/yr', highlight: true }, ] @@ -78,9 +79,14 @@ export default function ProductSlide({ lang }: ProductSlideProps) { {tier.employees} {de ? 'Mitarbeiter' : 'employees'}
- - {de ? tier.priceDe : tier.priceEn} - +
+ + {de ? tier.priceDe : tier.priceEn} + + {tier.noteDe && ( +

{de ? tier.noteDe : tier.noteEn}

+ )} +
))} diff --git a/pitch-deck/lib/finanzplan/engine.ts b/pitch-deck/lib/finanzplan/engine.ts new file mode 100644 index 0000000..aa35306 --- /dev/null +++ b/pitch-deck/lib/finanzplan/engine.ts @@ -0,0 +1,412 @@ +/** + * Finanzplan Compute Engine + * + * Dependency order: + * Personalkosten (independent inputs) + * Investitionen (independent inputs) + * Kunden → Umsatzerlöse → Materialaufwand + * Betriebliche Aufwendungen (needs Personal + Invest) + * Sonst. betr. Erträge (independent) + * Liquidität (aggregates all above) + * GuV (annual summary) + */ + +import { Pool } from 'pg' +import { + MonthlyValues, AnnualValues, MONTHS, FOUNDING_MONTH, + emptyMonthly, sumMonthly, annualSums, dateToMonth, monthToDate, + FPPersonalkosten, FPInvestitionen, FPBetrieblicheAufwendungen, + FPLiquiditaet, FPComputeResult +} from './types' + +// --- Sheet Calculators --- + +export function computePersonalkosten(positions: FPPersonalkosten[]): FPPersonalkosten[] { + return positions.map(p => { + const brutto = emptyMonthly() + const sozial = emptyMonthly() + const total = emptyMonthly() + + if (!p.start_date || !p.brutto_monthly) return { ...p, values_brutto: brutto, values_sozial: sozial, values_total: total } + + const startDate = new Date(p.start_date) + const startM = dateToMonth(startDate.getFullYear(), startDate.getMonth() + 1) + const endM = p.end_date + ? dateToMonth(new Date(p.end_date).getFullYear(), new Date(p.end_date).getMonth() + 1) + : MONTHS + + for (let m = Math.max(1, startM); m <= Math.min(MONTHS, endM); m++) { + const { year } = monthToDate(m) + const yearsFromStart = year - startDate.getFullYear() + const raise = Math.pow(1 + (p.annual_raise_pct || 0) / 100, yearsFromStart) + const monthlyBrutto = Math.round(p.brutto_monthly * raise * 100) / 100 + + brutto[`m${m}`] = monthlyBrutto + sozial[`m${m}`] = Math.round(monthlyBrutto * (p.ag_sozial_pct || 20.425) / 100 * 100) / 100 + total[`m${m}`] = brutto[`m${m}`] + sozial[`m${m}`] + } + + return { ...p, values_brutto: brutto, values_sozial: sozial, values_total: total } + }) +} + +export function computeInvestitionen(items: FPInvestitionen[]): FPInvestitionen[] { + return items.map(item => { + const invest = emptyMonthly() + const afa = emptyMonthly() + + if (!item.purchase_date || !item.purchase_amount) return { ...item, values_invest: invest, values_afa: afa } + + const d = new Date(item.purchase_date) + const purchaseM = dateToMonth(d.getFullYear(), d.getMonth() + 1) + + if (purchaseM >= 1 && purchaseM <= MONTHS) { + invest[`m${purchaseM}`] = item.purchase_amount + } + + // AfA (linear depreciation) + if (item.afa_years && item.afa_years > 0) { + const afaMonths = item.afa_years * 12 + const monthlyAfa = Math.round(item.purchase_amount / afaMonths * 100) / 100 + for (let m = purchaseM; m < purchaseM + afaMonths && m <= MONTHS; m++) { + if (m >= 1) afa[`m${m}`] = monthlyAfa + } + } else { + // GWG: full depreciation in purchase month + if (purchaseM >= 1 && purchaseM <= MONTHS) { + afa[`m${purchaseM}`] = item.purchase_amount + } + } + + return { ...item, values_invest: invest, values_afa: afa } + }) +} + +function sumRows(rows: { values: MonthlyValues }[]): MonthlyValues { + const result = emptyMonthly() + for (const row of rows) { + for (let m = 1; m <= MONTHS; m++) { + result[`m${m}`] += row.values[`m${m}`] || 0 + } + } + return result +} + +function sumField(rows: { [key: string]: MonthlyValues }[], field: string): MonthlyValues { + const result = emptyMonthly() + for (const row of rows) { + const v = row[field] as MonthlyValues + if (!v) continue + for (let m = 1; m <= MONTHS; m++) { + result[`m${m}`] += v[`m${m}`] || 0 + } + } + return result +} + +// --- Main Engine --- + +export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise { + // 1. Load all editable data from DB + const [ + personalRows, + investRows, + betriebRows, + liquidRows, + kundenSummary, + umsatzRows, + materialRows, + ] = await Promise.all([ + pool.query('SELECT * FROM fp_personalkosten WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), + pool.query('SELECT * FROM fp_investitionen WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), + pool.query('SELECT * FROM fp_betriebliche_aufwendungen WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), + pool.query('SELECT * FROM fp_liquiditaet WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), + pool.query('SELECT * FROM fp_kunden_summary WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), + pool.query('SELECT * FROM fp_umsatzerloese WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), + pool.query('SELECT * FROM fp_materialaufwand WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]), + ]) + + // 2. Compute Personalkosten + const personal = computePersonalkosten(personalRows.rows as FPPersonalkosten[]) + const totalBrutto = sumField(personal as any, 'values_brutto') + const totalSozial = sumField(personal as any, 'values_sozial') + const totalPersonal = sumField(personal as any, 'values_total') + const headcount = emptyMonthly() + for (let m = 1; m <= MONTHS; m++) { + headcount[`m${m}`] = personal.filter(p => (p.values_total[`m${m}`] || 0) > 0).length + } + + // Write computed values back to DB + for (const p of personal) { + await pool.query( + 'UPDATE fp_personalkosten SET values_brutto = $1, values_sozial = $2, values_total = $3 WHERE id = $4', + [JSON.stringify(p.values_brutto), JSON.stringify(p.values_sozial), JSON.stringify(p.values_total), p.id] + ) + } + + // 3. Compute Investitionen + const invest = computeInvestitionen(investRows.rows as FPInvestitionen[]) + const totalInvest = sumField(invest as any, 'values_invest') + const totalAfa = sumField(invest as any, 'values_afa') + + for (const i of invest) { + await pool.query( + 'UPDATE fp_investitionen SET values_invest = $1, values_afa = $2 WHERE id = $3', + [JSON.stringify(i.values_invest), JSON.stringify(i.values_afa), i.id] + ) + } + + // 4. Umsatzerlöse (quantity × price) + const prices = (umsatzRows.rows as FPUmsatzerloese[]).filter(r => r.section === 'price') + const quantities = (umsatzRows.rows as FPUmsatzerloese[]).filter(r => r.section === 'quantity') + const revenueRows = (umsatzRows.rows as FPUmsatzerloese[]).filter(r => r.section === 'revenue') + const totalRevenue = emptyMonthly() + + // Revenue = quantity × price for each module + for (const rev of revenueRows) { + if (rev.row_label === 'GESAMTUMSATZ') continue + const qty = quantities.find(q => q.row_label === rev.row_label) + const price = prices.find(p => p.row_label === rev.row_label) + if (qty && price) { + for (let m = 1; m <= MONTHS; m++) { + const v = (qty.values[`m${m}`] || 0) * (price.values[`m${m}`] || 0) + rev.values[`m${m}`] = Math.round(v * 100) / 100 + totalRevenue[`m${m}`] += rev.values[`m${m}`] + } + await pool.query('UPDATE fp_umsatzerloese SET values = $1 WHERE id = $2', [JSON.stringify(rev.values), rev.id]) + } + } + // Update GESAMTUMSATZ + const gesamtUmsatz = revenueRows.find(r => r.row_label === 'GESAMTUMSATZ') + if (gesamtUmsatz) { + await pool.query('UPDATE fp_umsatzerloese SET values = $1 WHERE id = $2', [JSON.stringify(totalRevenue), gesamtUmsatz.id]) + } + + // 5. Materialaufwand (quantity × unit_cost) — simplified + const matCosts = (materialRows.rows as FPMaterialaufwand[]).filter(r => r.section === 'cost') + const matUnitCosts = (materialRows.rows as FPMaterialaufwand[]).filter(r => r.section === 'unit_cost') + const totalMaterial = emptyMonthly() + + for (const cost of matCosts) { + if (cost.row_label === 'SUMME') continue + const uc = matUnitCosts.find(u => u.row_label === cost.row_label) + const qty = quantities.find(q => q.row_label === cost.row_label) + if (uc && qty) { + for (let m = 1; m <= MONTHS; m++) { + const v = (qty.values[`m${m}`] || 0) * (uc.values[`m${m}`] || 0) + cost.values[`m${m}`] = Math.round(v * 100) / 100 + totalMaterial[`m${m}`] += cost.values[`m${m}`] + } + await pool.query('UPDATE fp_materialaufwand SET values = $1 WHERE id = $2', [JSON.stringify(cost.values), cost.id]) + } + } + const matSumme = matCosts.find(r => r.row_label === 'SUMME') + if (matSumme) { + await pool.query('UPDATE fp_materialaufwand SET values = $1 WHERE id = $2', [JSON.stringify(totalMaterial), matSumme.id]) + } + + // 6. Betriebliche Aufwendungen — compute sum rows + const betrieb = betriebRows.rows as FPBetrieblicheAufwendungen[] + // Update Personalkosten row + const persBetrieb = betrieb.find(r => r.row_label === 'Personalkosten') + if (persBetrieb) { + await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(totalPersonal), persBetrieb.id]) + persBetrieb.values = totalPersonal + } + // Update Abschreibungen row + const abrBetrieb = betrieb.find(r => r.row_label === 'Abschreibungen') + if (abrBetrieb) { + await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(totalAfa), abrBetrieb.id]) + abrBetrieb.values = totalAfa + } + + // Compute category sums + const categories = ['steuern', 'versicherungen', 'besondere', 'marketing', 'sonstige'] + for (const cat of categories) { + const sumRow = betrieb.find(r => r.category === cat && r.is_sum_row) + const detailRows = betrieb.filter(r => r.category === cat && !r.is_sum_row) + if (sumRow && detailRows.length > 0) { + const s = sumRows(detailRows) + await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(s), sumRow.id]) + sumRow.values = s + } + } + + // Summe sonstige (ohne Personal, Abschreibungen) + const sonstSumme = betrieb.find(r => r.row_label.includes('Summe sonstige')) + if (sonstSumme) { + const nonPersonNonAbr = betrieb.filter(r => + r.row_label !== 'Personalkosten' && r.row_label !== 'Abschreibungen' && + !r.row_label.includes('Summe sonstige') && !r.row_label.includes('Gesamtkosten') && + !r.is_sum_row && r.category !== 'personal' && r.category !== 'abschreibungen' + ) + const s = sumRows(nonPersonNonAbr) + await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(s), sonstSumme.id]) + sonstSumme.values = s + } + + // Gesamtkosten + const gesamtBetrieb = betrieb.find(r => r.row_label.includes('Gesamtkosten')) + const totalSonstige = sonstSumme?.values || emptyMonthly() + if (gesamtBetrieb) { + const g = emptyMonthly() + for (let m = 1; m <= MONTHS; m++) { + g[`m${m}`] = (totalPersonal[`m${m}`] || 0) + (totalAfa[`m${m}`] || 0) + (totalSonstige[`m${m}`] || 0) + } + await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(g), gesamtBetrieb.id]) + gesamtBetrieb.values = g + } + + // 7. Liquidität + const liquid = liquidRows.rows as FPLiquiditaet[] + const findLiq = (label: string) => liquid.find(r => r.row_label === label) + + // Computed rows + const liqUmsatz = findLiq('Umsatzerloese') + if (liqUmsatz) { + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalRevenue), liqUmsatz.id]) + liqUmsatz.values = totalRevenue + } + const liqMaterial = findLiq('Materialaufwand') + if (liqMaterial) { + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalMaterial), liqMaterial.id]) + liqMaterial.values = totalMaterial + } + const liqPersonal = findLiq('Personalkosten') + if (liqPersonal) { + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalPersonal), liqPersonal.id]) + liqPersonal.values = totalPersonal + } + const liqSonstige = findLiq('Sonstige Kosten') + if (liqSonstige) { + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalSonstige), liqSonstige.id]) + liqSonstige.values = totalSonstige + } + const liqInvest = findLiq('Investitionen') + if (liqInvest) { + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalInvest), liqInvest.id]) + liqInvest.values = totalInvest + } + + // Compute sums and rolling balance + const sumEin = findLiq('Summe EINZAHLUNGEN') + const sumAus = findLiq('Summe AUSZAHLUNGEN') + const uebVorInv = findLiq('UEBERSCHUSS VOR INVESTITIONEN') + const uebVorEnt = findLiq('UEBERSCHUSS VOR ENTNAHMEN') + const ueberschuss = findLiq('UEBERSCHUSS') + const kontostand = findLiq('Kontostand zu Beginn des Monats') + const liquiditaet = findLiq('LIQUIDITAET') + + const einzahlungen = ['Umsatzerloese', 'Sonst. betriebl. Ertraege', 'Anzahlungen', 'Neuer Eigenkapitalzugang', 'Erhaltenes Fremdkapital'] + const auszahlungen = ['Materialaufwand', 'Personalkosten', 'Sonstige Kosten', 'Kreditrueckzahlungen', 'Umsatzsteuer', 'Gewerbesteuer', 'Koerperschaftsteuer'] + + if (sumEin) { + const s = emptyMonthly() + for (const label of einzahlungen) { + const row = findLiq(label) + if (row) for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += row.values[`m${m}`] || 0 + } + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumEin.id]) + sumEin.values = s + } + + if (sumAus) { + const s = emptyMonthly() + for (const label of auszahlungen) { + const row = findLiq(label) + if (row) for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += row.values[`m${m}`] || 0 + } + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumAus.id]) + sumAus.values = s + } + + // Überschüsse und Kontostand + if (uebVorInv && sumEin && sumAus) { + const s = emptyMonthly() + for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = (sumEin.values[`m${m}`] || 0) - (sumAus.values[`m${m}`] || 0) + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorInv.id]) + uebVorInv.values = s + } + + if (uebVorEnt && uebVorInv && liqInvest) { + const s = emptyMonthly() + for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = (uebVorInv.values[`m${m}`] || 0) - (liqInvest.values[`m${m}`] || 0) + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorEnt.id]) + uebVorEnt.values = s + } + + const entnahmen = findLiq('Kapitalentnahmen/Ausschuettungen') + if (ueberschuss && uebVorEnt && entnahmen) { + const s = emptyMonthly() + for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = (uebVorEnt.values[`m${m}`] || 0) - (entnahmen.values[`m${m}`] || 0) + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), ueberschuss.id]) + ueberschuss.values = s + } + + // Rolling Kontostand + if (kontostand && liquiditaet && ueberschuss) { + const ks = emptyMonthly() + const lq = emptyMonthly() + for (let m = 1; m <= MONTHS; m++) { + ks[`m${m}`] = m === 1 ? 0 : lq[`m${m - 1}`] + lq[`m${m}`] = ks[`m${m}`] + (ueberschuss.values[`m${m}`] || 0) + } + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ks), kontostand.id]) + await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(lq), liquiditaet.id]) + kontostand.values = ks + liquiditaet.values = lq + } + + // 8. GuV — compute annual values + const guv: AnnualValues[] = [] + const umsatzAnnual = annualSums(totalRevenue) + const materialAnnual = annualSums(totalMaterial) + const personalBruttoAnnual = annualSums(totalBrutto) + const personalSozialAnnual = annualSums(totalSozial) + const personalAnnual = annualSums(totalPersonal) + const afaAnnual = annualSums(totalAfa) + const sonstigeAnnual = annualSums(totalSonstige) + + // Write GuV rows + const guvUpdates: { label: string; values: AnnualValues }[] = [ + { label: 'Umsatzerloese', values: umsatzAnnual }, + { label: 'Gesamtleistung', values: umsatzAnnual }, + { label: 'Summe Materialaufwand', values: materialAnnual }, + { label: 'Loehne und Gehaelter', values: personalBruttoAnnual }, + { label: 'Soziale Abgaben', values: personalSozialAnnual }, + { label: 'Summe Personalaufwand', values: personalAnnual }, + { label: 'Abschreibungen', values: afaAnnual }, + { label: 'Sonst. betriebl. Aufwendungen', values: sonstigeAnnual }, + ] + + for (const { label, values } of guvUpdates) { + await pool.query( + 'UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', + [JSON.stringify(values), scenarioId, label] + ) + } + + // EBIT + const ebit: AnnualValues = {} + for (let y = 2026; y <= 2030; y++) { + const k = `y${y}` + ebit[k] = (umsatzAnnual[k] || 0) - (materialAnnual[k] || 0) - (personalAnnual[k] || 0) - (afaAnnual[k] || 0) - (sonstigeAnnual[k] || 0) + } + await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ebit), scenarioId, 'EBIT']) + await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ebit), scenarioId, 'Ergebnis nach Steuern']) + await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ebit), scenarioId, 'Jahresueberschuss']) + + return { + personalkosten: { total_brutto: totalBrutto, total_sozial: totalSozial, total: totalPersonal, positions: personal, headcount }, + investitionen: { total_invest: totalInvest, total_afa: totalAfa, items: invest }, + umsatzerloese: { total: totalRevenue }, + materialaufwand: { total: totalMaterial }, + betriebliche: { total_sonstige: totalSonstige, total_gesamt: gesamtBetrieb?.values || emptyMonthly() }, + liquiditaet: { rows: liquid, endstand: liquiditaet?.values || emptyMonthly() }, + guv: [ebit], + } +} + +// Import to fix type errors +type FPUmsatzerloese = import('./types').FPUmsatzerloese +type FPMaterialaufwand = import('./types').FPMaterialaufwand diff --git a/pitch-deck/lib/finanzplan/types.ts b/pitch-deck/lib/finanzplan/types.ts new file mode 100644 index 0000000..b73984a --- /dev/null +++ b/pitch-deck/lib/finanzplan/types.ts @@ -0,0 +1,210 @@ +/** + * Finanzplan Types — mirrors the fp_* database tables. + * Monthly values stored as Record with keys m1..m60. + * Annual values stored with keys y2026..y2030. + */ + +export type MonthlyValues = Record // m1..m60 +export type AnnualValues = Record // y2026..y2030 + +export const MONTHS = 60 +export const START_YEAR = 2026 +export const END_YEAR = 2030 +export const FOUNDING_MONTH = 8 // August 2026 = m8 + +// Month index (1-based) to year/month +export function monthToDate(m: number): { year: number; month: number } { + const year = START_YEAR + Math.floor((m - 1) / 12) + const month = ((m - 1) % 12) + 1 + return { year, month } +} + +// Year/month to month index (1-based) +export function dateToMonth(year: number, month: number): number { + return (year - START_YEAR) * 12 + month +} + +export function emptyMonthly(): MonthlyValues { + const v: MonthlyValues = {} + for (let m = 1; m <= MONTHS; m++) v[`m${m}`] = 0 + return v +} + +export function sumMonthly(values: MonthlyValues, fromM: number, toM: number): number { + let s = 0 + for (let m = fromM; m <= toM; m++) s += values[`m${m}`] || 0 + return s +} + +export function annualSums(values: MonthlyValues): AnnualValues { + const r: AnnualValues = {} + for (let y = START_YEAR; y <= END_YEAR; y++) { + const startM = dateToMonth(y, 1) + const endM = dateToMonth(y, 12) + r[`y${y}`] = sumMonthly(values, startM, endM) + } + return r +} + +// --- DB row types --- + +export interface FPScenario { + id: string + name: string + description?: string + is_default: boolean +} + +export interface FPPersonalkosten { + id: number + scenario_id: string + person_name: string + person_nr?: string + position?: string + start_date?: string + end_date?: string + brutto_monthly: number + annual_raise_pct: number + ag_sozial_pct: number + values_brutto: MonthlyValues + values_sozial: MonthlyValues + values_total: MonthlyValues + sort_order: number +} + +export interface FPBetrieblicheAufwendungen { + id: number + scenario_id: string + category: string + row_label: string + row_index: number + is_editable: boolean + is_sum_row: boolean + formula_desc?: string + values: MonthlyValues + sort_order: number +} + +export interface FPInvestitionen { + id: number + scenario_id: string + item_name: string + category?: string + purchase_amount: number + purchase_date?: string + afa_years?: number + afa_end_date?: string + values_invest: MonthlyValues + values_afa: MonthlyValues + sort_order: number +} + +export interface FPLiquiditaet { + id: number + scenario_id: string + row_label: string + row_type: string + is_editable: boolean + formula_desc?: string + values: MonthlyValues + sort_order: number +} + +export interface FPGuV { + id: number + scenario_id: string + row_label: string + row_index: number + is_sum_row: boolean + formula_desc?: string + values: AnnualValues + sort_order: number +} + +export interface FPKunden { + id: number + scenario_id: string + segment_name: string + segment_index: number + row_label: string + row_index: number + percentage?: number + formula_type?: string + is_editable: boolean + values: MonthlyValues + sort_order: number +} + +export interface FPUmsatzerloese { + id: number + scenario_id: string + section: string // 'revenue', 'quantity', 'price' + row_label: string + row_index: number + is_editable: boolean + values: MonthlyValues + sort_order: number +} + +export interface FPMaterialaufwand { + id: number + scenario_id: string + section: string // 'cost', 'quantity', 'unit_cost' + row_label: string + row_index: number + is_editable: boolean + values: MonthlyValues + sort_order: number +} + +// --- Compute result --- + +export interface FPComputeResult { + personalkosten: { + total_brutto: MonthlyValues + total_sozial: MonthlyValues + total: MonthlyValues + positions: FPPersonalkosten[] + headcount: MonthlyValues + } + investitionen: { + total_invest: MonthlyValues + total_afa: MonthlyValues + items: FPInvestitionen[] + } + umsatzerloese: { + total: MonthlyValues + } + materialaufwand: { + total: MonthlyValues + } + betriebliche: { + total_sonstige: MonthlyValues + total_gesamt: MonthlyValues + } + liquiditaet: { + rows: FPLiquiditaet[] + endstand: MonthlyValues + } + guv: AnnualValues[] +} + +export interface SheetMeta { + name: string + label_de: string + label_en: string + rows: number + editable_rows: number +} + +export const SHEET_LIST: SheetMeta[] = [ + { name: 'kunden', label_de: 'Kunden', label_en: 'Customers', rows: 0, editable_rows: 0 }, + { name: 'umsatzerloese', label_de: 'Umsatzerloese', label_en: 'Revenue', rows: 0, editable_rows: 0 }, + { name: 'materialaufwand', label_de: 'Materialaufwand', label_en: 'Material Costs', rows: 0, editable_rows: 0 }, + { name: 'personalkosten', label_de: 'Personalkosten', label_en: 'Personnel', rows: 0, editable_rows: 0 }, + { name: 'betriebliche', label_de: 'Betriebliche Aufwendungen', label_en: 'Operating Expenses', rows: 0, editable_rows: 0 }, + { name: 'investitionen', label_de: 'Investitionen', label_en: 'Investments', rows: 0, editable_rows: 0 }, + { name: 'sonst_ertraege', label_de: 'Sonst. Ertraege', label_en: 'Other Income', rows: 0, editable_rows: 0 }, + { name: 'liquiditaet', label_de: 'Liquiditaet', label_en: 'Cash Flow', rows: 0, editable_rows: 0 }, + { name: 'guv', label_de: 'GuV', label_en: 'P&L', rows: 0, editable_rows: 0 }, +] diff --git a/pitch-deck/lib/i18n.ts b/pitch-deck/lib/i18n.ts index c8724d0..b576fce 100644 --- a/pitch-deck/lib/i18n.ts +++ b/pitch-deck/lib/i18n.ts @@ -31,6 +31,7 @@ const translations = { 'Anhang: Engineering', 'Anhang: KI-Pipeline', 'Anhang: SDK Demo', + 'Anhang: Finanzplan', ], executiveSummary: { title: 'Executive Summary', @@ -321,6 +322,7 @@ const translations = { 'Appendix: Engineering', 'Appendix: AI Pipeline', 'Appendix: SDK Demo', + 'Appendix: Financial Plan', ], executiveSummary: { title: 'Executive Summary', diff --git a/pitch-deck/lib/slide-order.ts b/pitch-deck/lib/slide-order.ts index b913209..3d253d8 100644 --- a/pitch-deck/lib/slide-order.ts +++ b/pitch-deck/lib/slide-order.ts @@ -24,6 +24,7 @@ export const SLIDE_ORDER: SlideId[] = [ 'annex-engineering', 'annex-aipipeline', 'annex-sdk-demo', + 'annex-finanzplan', ] export const TOTAL_SLIDES = SLIDE_ORDER.length diff --git a/pitch-deck/lib/types.ts b/pitch-deck/lib/types.ts index caad3a7..91823de 100644 --- a/pitch-deck/lib/types.ts +++ b/pitch-deck/lib/types.ts @@ -224,3 +224,4 @@ export type SlideId = | 'annex-engineering' | 'annex-aipipeline' | 'annex-sdk-demo' + | 'annex-finanzplan' diff --git a/pitch-deck/next-env.d.ts b/pitch-deck/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/pitch-deck/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/pitch-deck/package-lock.json b/pitch-deck/package-lock.json new file mode 100644 index 0000000..699104f --- /dev/null +++ b/pitch-deck/package-lock.json @@ -0,0 +1,2692 @@ +{ + "name": "breakpilot-pitch-deck", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "breakpilot-pitch-deck", + "version": "1.0.0", + "dependencies": { + "framer-motion": "^11.15.0", + "lucide-react": "^0.468.0", + "next": "^15.1.0", + "pg": "^8.13.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "recharts": "^2.15.0" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "@types/pg": "^8.11.10", + "@types/react": "^18.3.16", + "@types/react-dom": "^18.3.5", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.16", + "typescript": "^5.7.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.14.tgz", + "integrity": "sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.14.tgz", + "integrity": "sha512-Y9K6SPzobnZvrRDPO2s0grgzC+Egf0CqfbdvYmQVaztV890zicw8Z8+4Vqw8oPck8r1TjUHxVh8299Cg4TrxXg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.14.tgz", + "integrity": "sha512-aNnkSMjSFRTOmkd7qoNI2/rETQm/vKD6c/Ac9BZGa9CtoOzy3c2njgz7LvebQJ8iPxdeTuGnAjagyis8a9ifBw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.14.tgz", + "integrity": "sha512-tjlpia+yStPRS//6sdmlVwuO1Rioern4u2onafa5n+h2hCS9MAvMXqpVbSrjgiEOoCs0nJy7oPOmWgtRRNSM5Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.14.tgz", + "integrity": "sha512-8B8cngBaLadl5lbDRdxGCP1Lef8ipD6KlxS3v0ElDAGil6lafrAM3B258p1KJOglInCVFUjk751IXMr2ixeQOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.14.tgz", + "integrity": "sha512-bAS6tIAg8u4Gn3Nz7fCPpSoKAexEt2d5vn1mzokcqdqyov6ZJ6gu6GdF9l8ORFrBuRHgv3go/RfzYz5BkZ6YSQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.14.tgz", + "integrity": "sha512-mMxv/FcrT7Gfaq4tsR22l17oKWXZmH/lVqcvjX0kfp5I0lKodHYLICKPoX1KRnnE+ci6oIUdriUhuA3rBCDiSw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.14.tgz", + "integrity": "sha512-OTmiBlYThppnvnsqx0rBqjDRemlmIeZ8/o4zI7veaXoeO1PVHoyj2lfTfXTiiGjCyRDhA10y4h6ZvZvBiynr2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.14.tgz", + "integrity": "sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.325", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz", + "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.14.tgz", + "integrity": "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.14", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.14", + "@next/swc-darwin-x64": "15.5.14", + "@next/swc-linux-arm64-gnu": "15.5.14", + "@next/swc-linux-arm64-musl": "15.5.14", + "@next/swc-linux-x64-gnu": "15.5.14", + "@next/swc-linux-x64-musl": "15.5.14", + "@next/swc-win32-arm64-msvc": "15.5.14", + "@next/swc-win32-x64-msvc": "15.5.14", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/pitch-deck/scripts/001_finanzplan_tables.sql b/pitch-deck/scripts/001_finanzplan_tables.sql new file mode 100644 index 0000000..88ea3f1 --- /dev/null +++ b/pitch-deck/scripts/001_finanzplan_tables.sql @@ -0,0 +1,246 @@ +-- ============================================================================ +-- BreakPilot ComplAI — Finanzplan Database Schema +-- Mirrors Excel: "Breakpilot ComplAI Finanzplan.xlsm" (10 Reiter) +-- Monthly granularity: Jan 2026 – Dec 2030 (60 months) +-- Values stored as JSONB: {"m1": ..., "m2": ..., "m60": ...} +-- ============================================================================ + +-- Scenarios (extends existing pitch_fm_scenarios) +CREATE TABLE IF NOT EXISTS fp_scenarios ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL DEFAULT 'Base Case', + description TEXT, + is_default BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Insert default scenario +INSERT INTO fp_scenarios (name, description, is_default) +VALUES ('Base Case', 'Basisdaten aus Excel-Import', true) +ON CONFLICT DO NOTHING; + +-- ============================================================================ +-- KUNDEN (6 Segmente × ~20 Zeilen = ~120 Datenzeilen) +-- Each segment has: base customer count (editable) + module percentages +-- Formulas: module_count = ROUNDDOWN(base_count * percentage) +-- ============================================================================ +CREATE TABLE fp_kunden ( + id SERIAL PRIMARY KEY, + scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + segment_name TEXT NOT NULL, -- 'Care (Privat)', 'Horse (Händler)', etc. + segment_index INT NOT NULL, -- 1-6 + row_label TEXT NOT NULL, -- 'Modul 1', 'Modul 2', ... + row_index INT NOT NULL, -- position within segment + percentage NUMERIC(5,3), -- multiplier (e.g. 0.9, 0.25) + formula_type TEXT, -- 'literal', 'roundup_pct', 'rounddown_pct', 'cumulative', null + is_editable BOOLEAN DEFAULT false, -- true for base inputs (Modul 1 per segment) + values JSONB NOT NULL DEFAULT '{}', -- {m1: 0, m2: 0, ... m60: 0} + excel_row INT, -- original Excel row number + sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Summary rows (aggregated across all 6 segments) +CREATE TABLE fp_kunden_summary ( + id SERIAL PRIMARY KEY, + scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + row_label TEXT NOT NULL, -- 'Modul 1', 'Modul 2', ... + row_index INT NOT NULL, + values JSONB NOT NULL DEFAULT '{}', -- computed: sum of 6 segments + excel_row INT, + sort_order INT NOT NULL +); + +-- ============================================================================ +-- UMSATZERLOESE (Revenue = Quantity × Price) +-- Section 1 (rows 3-23): Computed revenue per module +-- Section 2 (rows 27-46): Quantity (from Kunden) +-- Section 3 (rows 49-73): Prices (editable VK excl. MwSt.) +-- ============================================================================ +CREATE TABLE fp_umsatzerloese ( + id SERIAL PRIMARY KEY, + scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + section TEXT NOT NULL, -- 'revenue', 'quantity', 'price' + row_label TEXT NOT NULL, -- 'Modul 1', 'Modul 2', ... + row_index INT NOT NULL, + is_editable BOOLEAN DEFAULT false, -- only prices are editable + values JSONB NOT NULL DEFAULT '{}', + excel_row INT, + sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================================ +-- MATERIALAUFWAND (sehr einfach: nur Mac Mini + Mac Studio) +-- Cost = Quantity × Unit Cost (EK) +-- Mac Mini EK: 3.200 EUR, VK: 4.800 EUR (50% Aufschlag) +-- Mac Studio EK: 13.000 EUR, VK: 19.500 EUR (50% Aufschlag) +-- ============================================================================ +CREATE TABLE fp_materialaufwand ( + id SERIAL PRIMARY KEY, + scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + section TEXT NOT NULL, -- 'cost', 'quantity', 'unit_cost' + row_label TEXT NOT NULL, + row_index INT NOT NULL, + is_editable BOOLEAN DEFAULT false, -- only unit costs are editable + values JSONB NOT NULL DEFAULT '{}', + excel_row INT, + sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================================ +-- PERSONALKOSTEN (20 Positionen) +-- Structured: Name, Position, Start, End, Brutto, Raise% +-- Computed: monthly salary × AG-Sozialversicherung +-- ============================================================================ +CREATE TABLE fp_personalkosten ( + id SERIAL PRIMARY KEY, + scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + person_name TEXT NOT NULL, + person_nr TEXT, -- '001', '002', ... + position TEXT, -- 'GF', 'Vertrieb', 'Entwicklung', ... + start_date DATE, + end_date DATE, -- null = permanent + brutto_monthly NUMERIC(10,2), -- Bruttogehalt/Monat + annual_raise_pct NUMERIC(5,2) DEFAULT 3.0, + ag_sozial_pct NUMERIC(5,2) DEFAULT 20.425, -- AG-Anteil Sozialversicherung + is_editable BOOLEAN DEFAULT true, + -- Computed monthly values + values_brutto JSONB NOT NULL DEFAULT '{}', -- monthly brutto + values_sozial JSONB NOT NULL DEFAULT '{}', -- monthly AG-Sozial + values_total JSONB NOT NULL DEFAULT '{}', -- brutto + sozial + excel_row INT, + sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================================ +-- BETRIEBLICHE AUFWENDUNGEN (~40 Kostenposten) +-- Most are fixed monthly values (editable) +-- Some are computed (Summen, Abschreibungen) +-- ============================================================================ +CREATE TABLE fp_betriebliche_aufwendungen ( + id SERIAL PRIMARY KEY, + scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + category TEXT NOT NULL, -- 'raumkosten', 'versicherungen', 'marketing', etc. + row_label TEXT NOT NULL, + row_index INT NOT NULL, + is_editable BOOLEAN DEFAULT true, + is_sum_row BOOLEAN DEFAULT false, -- true for category subtotals + formula_desc TEXT, -- e.g. 'SUM(rows 9-18)', '=Personalkosten!Y4' + values JSONB NOT NULL DEFAULT '{}', + excel_row INT, + sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================================ +-- INVESTITIONEN (Anlagegüter mit AfA) +-- Each item: name, amount, purchase date, useful life, depreciation +-- ============================================================================ +CREATE TABLE fp_investitionen ( + id SERIAL PRIMARY KEY, + scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + item_name TEXT NOT NULL, + category TEXT, -- 'gwg', 'ausstattung', etc. + purchase_amount NUMERIC(12,2) NOT NULL, + purchase_date DATE, + afa_years INT, -- useful life in years + afa_end_date DATE, -- computed end date + is_editable BOOLEAN DEFAULT true, + -- Computed: monthly investment amount (in purchase month) and depreciation + values_invest JSONB NOT NULL DEFAULT '{}', -- investment amount per month + values_afa JSONB NOT NULL DEFAULT '{}', -- monthly depreciation + excel_row INT, + sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================================ +-- SONST. BETRIEBLICHE ERTRAEGE (6 Kategorien × 3 Zeilen) +-- ============================================================================ +CREATE TABLE fp_sonst_ertraege ( + id SERIAL PRIMARY KEY, + scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + category TEXT NOT NULL, -- '1-Interne Kostenstellen', '2-Entwicklung National', etc. + row_label TEXT, + row_index INT NOT NULL, + is_editable BOOLEAN DEFAULT true, + is_sum_row BOOLEAN DEFAULT false, + values JSONB NOT NULL DEFAULT '{}', + excel_row INT, + sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================================ +-- LIQUIDITAET (computed from all above) +-- ============================================================================ +CREATE TABLE fp_liquiditaet ( + id SERIAL PRIMARY KEY, + scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + row_label TEXT NOT NULL, + row_type TEXT NOT NULL, -- 'einzahlung', 'auszahlung', 'ueberschuss', 'kontostand' + is_editable BOOLEAN DEFAULT false, -- only Eigenkapital, Fremdkapital, Entnahmen editable + formula_desc TEXT, + values JSONB NOT NULL DEFAULT '{}', + excel_row INT, + sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================================ +-- GUV JAHRESABSCHLUSS (annual summary, 5 years) +-- ============================================================================ +CREATE TABLE fp_guv ( + id SERIAL PRIMARY KEY, + scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + row_label TEXT NOT NULL, + row_index INT NOT NULL, + is_sum_row BOOLEAN DEFAULT false, + formula_desc TEXT, + values JSONB NOT NULL DEFAULT '{}', -- {y2026: ..., y2027: ..., y2030: ...} + excel_row INT, + sort_order INT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- ============================================================================ +-- CELL OVERRIDES (for scenario-specific manual edits) +-- ============================================================================ +CREATE TABLE fp_cell_overrides ( + id SERIAL PRIMARY KEY, + scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE, + sheet_name TEXT NOT NULL, -- 'kunden', 'personalkosten', etc. + row_id INT NOT NULL, -- references the id in the sheet table + month_key TEXT NOT NULL, -- 'm1', 'm2', ... 'm60' or 'y2026', etc. + override_value NUMERIC, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(scenario_id, sheet_name, row_id, month_key) +); + +-- ============================================================================ +-- INDEXES +-- ============================================================================ +CREATE INDEX idx_fp_kunden_scenario ON fp_kunden(scenario_id); +CREATE INDEX idx_fp_kunden_summary_scenario ON fp_kunden_summary(scenario_id); +CREATE INDEX idx_fp_umsatz_scenario ON fp_umsatzerloese(scenario_id); +CREATE INDEX idx_fp_material_scenario ON fp_materialaufwand(scenario_id); +CREATE INDEX idx_fp_personal_scenario ON fp_personalkosten(scenario_id); +CREATE INDEX idx_fp_betrieb_scenario ON fp_betriebliche_aufwendungen(scenario_id); +CREATE INDEX idx_fp_invest_scenario ON fp_investitionen(scenario_id); +CREATE INDEX idx_fp_sonst_scenario ON fp_sonst_ertraege(scenario_id); +CREATE INDEX idx_fp_liquid_scenario ON fp_liquiditaet(scenario_id); +CREATE INDEX idx_fp_guv_scenario ON fp_guv(scenario_id); +CREATE INDEX idx_fp_overrides_lookup ON fp_cell_overrides(scenario_id, sheet_name, row_id); diff --git a/pitch-deck/scripts/import-finanzplan.py b/pitch-deck/scripts/import-finanzplan.py new file mode 100644 index 0000000..b7094f6 --- /dev/null +++ b/pitch-deck/scripts/import-finanzplan.py @@ -0,0 +1,583 @@ +#!/usr/bin/env python3 +""" +Import BreakPilot Finanzplan Excel into PostgreSQL fp_* tables. +Usage: python3 scripts/import-finanzplan.py + +Requires: pip3 install openpyxl psycopg2-binary +""" + +import sys +import json +import os +from datetime import date, datetime +import openpyxl +import psycopg2 +from psycopg2.extras import Json + +# --- Config --- +DB_URL = os.environ.get('DATABASE_URL', 'postgresql://breakpilot:breakpilot@localhost:5432/breakpilot_db') +MONTHS = 60 # Jan 2026 – Dec 2030 +# Excel columns: D=4 (month 1) through BQ=69 (month 60, approx) +# Actually: D=m1(Jan2026), E=m2(Feb2026), ..., O=m12(Dec2026), +# Q=m13(Jan2027), ..., AB=m24(Dec2027), etc. +# Year-column (C, P, AC, AP, BC) = annual sums, skip those +# Monthly columns per year: D-O (12), Q-AB (12), AD-AO (12), AQ-BB (12), BD-BO (12) + +# Map: month_index (1-60) -> Excel column index (1-based) +def build_month_columns(): + """Build mapping from month 1-60 to Excel column index.""" + cols = [] + # Year 1 (2026): cols D(4) - O(15) = 12 months + for c in range(4, 16): + cols.append(c) + # Year 2 (2027): cols Q(17) - AB(28) = 12 months + for c in range(17, 29): + cols.append(c) + # Year 3 (2028): cols AD(30) - AO(41) = 12 months + for c in range(30, 42): + cols.append(c) + # Year 4 (2029): cols AQ(43) - BB(54) = 12 months + for c in range(43, 55): + cols.append(c) + # Year 5 (2030): cols BD(56) - BO(67) = 12 months + for c in range(56, 68): + cols.append(c) + return cols + +MONTH_COLS = build_month_columns() + +def read_monthly_values(ws, row, data_only=True): + """Read 60 monthly values from an Excel row.""" + values = {} + for m_idx, col in enumerate(MONTH_COLS): + v = ws.cell(row, col).value + if v is not None and v != '' and not isinstance(v, str): + try: + values[f'm{m_idx+1}'] = round(float(v), 2) + except (ValueError, TypeError): + values[f'm{m_idx+1}'] = 0 + else: + values[f'm{m_idx+1}'] = 0 + return values + +def safe_str(v): + if v is None: + return '' + return str(v).strip() + +def safe_float(v, default=0): + if v is None: + return default + try: + return float(v) + except (ValueError, TypeError): + return default + +def safe_date(v): + if v is None: + return None + if isinstance(v, datetime): + return v.date() + if isinstance(v, date): + return v + return None + + +def import_personalkosten(cur, ws, scenario_id): + """Import Personalkosten sheet (rows 10-29 = 20 positions).""" + print(" Importing Personalkosten...") + count = 0 + for i in range(20): + row = 10 + i + name = safe_str(ws.cell(row, 1).value) + nr = safe_str(ws.cell(row, 2).value) + position = safe_str(ws.cell(row, 3).value) + start = safe_date(ws.cell(row, 4).value) + end = safe_date(ws.cell(row, 5).value) + brutto = safe_float(ws.cell(row, 7).value) + raise_pct = safe_float(ws.cell(row, 8).value, 3.0) + + if not name and not nr: + continue + + # Read computed monthly totals from the "Personalaufwand" section + # The actual monthly values are in a different row range (rows 32-51 for brutto, 56-75 for sozial) + # But we store the inputs and let the engine compute + values_total = {} # will be computed by engine + + cur.execute(""" + INSERT INTO fp_personalkosten + (scenario_id, person_name, person_nr, position, start_date, end_date, + brutto_monthly, annual_raise_pct, is_editable, + values_brutto, values_sozial, values_total, excel_row, sort_order) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, true, %s, %s, %s, %s, %s) + """, (scenario_id, name, nr, position, start, end, + brutto, raise_pct, Json({}), Json({}), Json({}), row, i + 1)) + count += 1 + print(f" -> {count} Positionen importiert") + + +def import_betriebliche_aufwendungen(cur, ws, scenario_id): + """Import Betriebliche Aufwendungen (rows 3-47).""" + print(" Importing Betriebliche Aufwendungen...") + + # Define the structure based on Excel analysis + rows_config = [ + (3, 'personal', 'Personalkosten', False, True, '=Personalkosten total'), + (4, 'raumkosten', 'Raumkosten', True, False, None), + (5, 'steuern', 'Betriebliche Steuern', False, True, '=SUM(6,7)'), + (6, 'steuern', 'Gewerbesteuer', True, False, None), + (7, 'steuern', 'KFZ-Steuern', True, False, None), + (8, 'versicherungen', 'Versich./Beitraege', False, True, '=SUM(9:18)'), + (9, 'versicherungen', 'IHK', True, False, None), + (10, 'versicherungen', 'Rundfunkbeitrag', True, False, None), + (11, 'versicherungen', 'Berufsgenossenschaft', True, False, None), + (12, 'versicherungen', 'Bundesanzeiger/Transparenzregister', True, False, None), + (13, 'versicherungen', 'D&O-Versicherung', True, False, None), + (14, 'versicherungen', 'E&O-Versicherung', True, False, None), + (15, 'versicherungen', 'Produkthaftpflicht', True, False, None), + (16, 'versicherungen', 'Cyber-Versicherung', True, False, None), + (17, 'versicherungen', 'Rechtsschutzversicherung', True, False, None), + (18, 'versicherungen', 'KFZ-Versicherung', True, False, None), + (19, 'besondere', 'Besondere Kosten', False, True, '=SUM(20:22)'), + (20, 'besondere', 'Schutzrechte/Lizenzkosten', True, False, None), + (21, 'besondere', 'Marketing Videos', True, False, None), + (22, 'besondere', 'Fort-/Weiterbildungskosten', True, False, None), + (23, 'fahrzeug', 'Fahrzeugkosten', True, False, None), + (24, 'marketing', 'Werbe-/Reisekosten', False, True, '=SUM(25:30)'), + (25, 'marketing', 'Reisekosten', True, False, None), + (26, 'marketing', 'Teilnahme an Messen', True, False, None), + (27, 'marketing', 'Allgemeine Marketingkosten', True, False, None), + (28, 'marketing', 'Marketing-Agentur', True, False, None), + (29, 'marketing', 'Editorial Content', True, False, None), + (30, 'marketing', 'Bewirtungskosten', True, False, None), + (31, 'warenabgabe', 'Kosten Warenabgabe', True, False, None), + (32, 'abschreibungen', 'Abschreibungen', False, False, '=Investitionen AfA'), + (33, 'reparatur', 'Reparatur/Instandh.', True, False, None), + (34, 'sonstige', 'Sonstige Kosten', False, True, '=SUM(35:45)'), + (35, 'sonstige', 'Telefon', True, False, None), + (36, 'sonstige', 'Bankgebuehren', True, False, None), + (37, 'sonstige', 'Buchfuehrung', True, False, None), + (38, 'sonstige', 'Jahresabschluss', True, False, None), + (39, 'sonstige', 'Rechts-/Beratungskosten', True, False, None), + (40, 'sonstige', 'Werkzeuge/Kleingeraete', True, False, None), + (41, 'sonstige', 'Serverkosten (Cloud)', True, False, None), + (42, 'sonstige', 'Verbrauchsmaterialien', True, False, None), + (43, 'sonstige', 'Mietkosten Software', True, False, None), + (44, 'sonstige', 'Nebenkosten Geldverkehr', True, False, None), + (46, 'summe', 'Summe sonstige (ohne Pers., Abschr.)', False, True, '=computed'), + (47, 'summe', 'Gesamtkosten (Klasse 6)', False, True, '=computed'), + ] + + count = 0 + for idx, (row, cat, label, editable, is_sum, formula) in enumerate(rows_config): + values = read_monthly_values(ws, row) + cur.execute(""" + INSERT INTO fp_betriebliche_aufwendungen + (scenario_id, category, row_label, row_index, is_editable, is_sum_row, + formula_desc, values, excel_row, sort_order) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, (scenario_id, cat, label, row, editable, is_sum, + formula, Json(values), row, idx + 1)) + count += 1 + print(f" -> {count} Kostenposten importiert") + + +def import_investitionen(cur, ws, scenario_id): + """Import Investitionen (rows 6-42).""" + print(" Importing Investitionen...") + count = 0 + for i in range(37): + row = 6 + i + name = safe_str(ws.cell(row, 1).value) + amount = safe_float(ws.cell(row, 2).value) + purchase = safe_date(ws.cell(row, 3).value) + afa_years = safe_float(ws.cell(row, 4).value) + afa_end = safe_date(ws.cell(row, 5).value) + + if not name and amount == 0: + continue + + # Read monthly investment values (col G onwards in Investitionen sheet) + # This sheet has different column mapping — dates as headers + # For simplicity, store purchase amount and let engine compute AfA + cur.execute(""" + INSERT INTO fp_investitionen + (scenario_id, item_name, category, purchase_amount, purchase_date, + afa_years, afa_end_date, is_editable, + values_invest, values_afa, excel_row, sort_order) + VALUES (%s, %s, %s, %s, %s, %s, %s, true, %s, %s, %s, %s) + """, (scenario_id, name, 'ausstattung' if 'Ausstattung' in name else 'gwg', + amount, purchase, int(afa_years) if afa_years else None, afa_end, + Json({}), Json({}), row, count + 1)) + count += 1 + print(f" -> {count} Investitionsgueter importiert") + + +def import_sonst_ertraege(cur, ws, scenario_id): + """Import Sonst. betr. Ertraege (6 categories).""" + print(" Importing Sonst. betr. Ertraege...") + categories = [ + (3, 8, 'Interne Kostenstellen'), + (9, 12, 'Entwicklung National'), + (13, 16, 'Entwicklung International'), + (17, 20, 'Beratungsdienstleistung'), + (21, 24, 'Zuwendungen'), + (25, 28, 'TBD'), + ] + count = 0 + for sum_row, end_row, cat_name in categories: + # Sum row + values = read_monthly_values(ws, sum_row) + cur.execute(""" + INSERT INTO fp_sonst_ertraege + (scenario_id, category, row_label, row_index, is_editable, is_sum_row, + values, excel_row, sort_order) + VALUES (%s, %s, %s, %s, false, true, %s, %s, %s) + """, (scenario_id, cat_name, cat_name, sum_row, Json(values), sum_row, count + 1)) + count += 1 + # Detail rows + for r in range(sum_row + 1, end_row + 1): + values = read_monthly_values(ws, r) + label = safe_str(ws.cell(r, 1).value) or f'Position {r - sum_row}' + cur.execute(""" + INSERT INTO fp_sonst_ertraege + (scenario_id, category, row_label, row_index, is_editable, is_sum_row, + values, excel_row, sort_order) + VALUES (%s, %s, %s, %s, true, false, %s, %s, %s) + """, (scenario_id, cat_name, label, r, Json(values), r, count + 1)) + count += 1 + # Total row (29) + values = read_monthly_values(ws, 29) + cur.execute(""" + INSERT INTO fp_sonst_ertraege + (scenario_id, category, row_label, row_index, is_editable, is_sum_row, + values, excel_row, sort_order) + VALUES (%s, %s, %s, %s, false, true, %s, %s, %s) + """, (scenario_id, 'GESAMT', 'GESAMTUMSATZ', 29, Json(values), 29, count + 1)) + print(f" -> {count + 1} Ertragsposten importiert") + + +def import_liquiditaet(cur, ws, scenario_id): + """Import Liquiditaet (rows 4-27).""" + print(" Importing Liquiditaet...") + rows_config = [ + (4, 'Umsatzerloese', 'einzahlung', False, '=Umsatzerloese!GESAMT'), + (5, 'Sonst. betriebl. Ertraege', 'einzahlung', False, '=Sonst.Ertraege!GESAMT'), + (6, 'Anzahlungen', 'einzahlung', True, None), + (7, 'Neuer Eigenkapitalzugang', 'einzahlung', True, None), + (8, 'Erhaltenes Fremdkapital', 'einzahlung', True, None), + (9, 'Summe EINZAHLUNGEN', 'einzahlung', False, '=SUM(4:8)'), + (12, 'Materialaufwand', 'auszahlung', False, '=Materialaufwand!SUMME'), + (13, 'Personalkosten', 'auszahlung', False, '=Personalkosten!Total'), + (14, 'Sonstige Kosten', 'auszahlung', False, '=Betriebliche!Summe_sonstige'), + (15, 'Kreditrueckzahlungen', 'auszahlung', True, None), + (16, 'Umsatzsteuer', 'auszahlung', True, None), + (17, 'Gewerbesteuer', 'auszahlung', True, None), + (18, 'Koerperschaftsteuer', 'auszahlung', True, None), + (19, 'Summe AUSZAHLUNGEN', 'auszahlung', False, '=SUM(12:18)'), + (21, 'UEBERSCHUSS VOR INVESTITIONEN', 'ueberschuss', False, '=Einzahlungen-Auszahlungen'), + (22, 'Investitionen', 'ueberschuss', False, '=Investitionen!Gesamt'), + (23, 'UEBERSCHUSS VOR ENTNAHMEN', 'ueberschuss', False, '=21-22'), + (24, 'Kapitalentnahmen/Ausschuettungen', 'ueberschuss', True, None), + (25, 'UEBERSCHUSS', 'ueberschuss', False, '=23-24'), + (26, 'Kontostand zu Beginn des Monats', 'kontostand', False, '=prev_month_liquiditaet'), + (27, 'LIQUIDITAET', 'kontostand', False, '=26+25'), + ] + count = 0 + for row, label, row_type, editable, formula in rows_config: + values = read_monthly_values(ws, row) + cur.execute(""" + INSERT INTO fp_liquiditaet + (scenario_id, row_label, row_type, is_editable, formula_desc, + values, excel_row, sort_order) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + """, (scenario_id, label, row_type, editable, formula, + Json(values), row, count + 1)) + count += 1 + print(f" -> {count} Liquiditaetszeilen importiert") + + +def import_kunden(cur, ws, scenario_id): + """Import Kunden (6 segments, rows 26-167).""" + print(" Importing Kunden...") + + # Summary rows (4-23) = aggregation across segments + for i in range(20): + row = 4 + i + label = safe_str(ws.cell(row, 1).value) + if not label: + label = f'Produkt {i + 1}' + values = read_monthly_values(ws, row) + cur.execute(""" + INSERT INTO fp_kunden_summary + (scenario_id, row_label, row_index, values, excel_row, sort_order) + VALUES (%s, %s, %s, %s, %s, %s) + """, (scenario_id, label, row, Json(values), row, i + 1)) + + # 6 segments, each starts 24 rows apart: 26, 50, 74, 98, 122, 146 + segment_starts = [ + (26, 'Care (Privat)'), + (50, 'Horse (Haendler)'), + (74, 'Segment 3'), + (98, 'Segment 4'), + (122, 'Segment 5'), + (146, 'Segment 6'), + ] + + # Read segment names from Excel + for idx, (start_row, default_name) in enumerate(segment_starts): + seg_name = safe_str(ws.cell(start_row, 1).value) or default_name + + # Each segment: row+2 = header, row+2..row+21 = module rows + module_start = start_row + 2 # row 28, 52, 76, 100, 124, 148 + count = 0 + for m in range(20): + row = module_start + m + label = safe_str(ws.cell(row, 1).value) + if not label: + continue + pct = safe_float(ws.cell(row, 2).value) + pct_label = safe_str(ws.cell(row, 2).value) + values = read_monthly_values(ws, row) + + # First module per segment (m=0) is the base editable input + is_base = (m == 0) + formula_type = None + if pct_label == 'gerechnet': + formula_type = 'cumulative' + elif pct > 0 and not is_base: + formula_type = 'rounddown_pct' + + cur.execute(""" + INSERT INTO fp_kunden + (scenario_id, segment_name, segment_index, row_label, row_index, + percentage, formula_type, is_editable, values, excel_row, sort_order) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, (scenario_id, seg_name, idx + 1, label, row, + pct if pct else None, formula_type, is_base, + Json(values), row, count + 1)) + count += 1 + print(f" -> Kunden importiert (6 Segmente)") + + +def import_umsatzerloese(cur, ws, scenario_id): + """Import Umsatzerloese (revenue, quantity, prices).""" + print(" Importing Umsatzerloese...") + count = 0 + + # Revenue rows (3-23): computed = quantity * price + for i in range(21): + row = 3 + i + label = safe_str(ws.cell(row, 1).value) or f'Produkt {i+1}' + if not label.strip(): + continue + values = read_monthly_values(ws, row) + cur.execute(""" + INSERT INTO fp_umsatzerloese + (scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order) + VALUES (%s, 'revenue', %s, %s, false, %s, %s, %s) + """, (scenario_id, label, row, Json(values), row, count + 1)) + count += 1 + + # Total row (24) + values = read_monthly_values(ws, 24) + cur.execute(""" + INSERT INTO fp_umsatzerloese + (scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order) + VALUES (%s, 'revenue', 'GESAMTUMSATZ', 24, false, %s, 24, %s) + """, (scenario_id, Json(values), count + 1)) + count += 1 + + # Quantity rows (27-46): from Kunden + for i in range(20): + row = 27 + i + label = safe_str(ws.cell(row, 1).value) or f'Produkt {i+1}' + if not label.strip(): + continue + values = read_monthly_values(ws, row) + cur.execute(""" + INSERT INTO fp_umsatzerloese + (scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order) + VALUES (%s, 'quantity', %s, %s, false, %s, %s, %s) + """, (scenario_id, label, row, Json(values), row, count + 1)) + count += 1 + + # Price rows (49-73): editable VK prices + for i in range(25): + row = 49 + i + label = safe_str(ws.cell(row, 1).value) or f'Produkt {i+1}' + if not label.strip(): + continue + values = read_monthly_values(ws, row) + cur.execute(""" + INSERT INTO fp_umsatzerloese + (scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order) + VALUES (%s, 'price', %s, %s, true, %s, %s, %s) + """, (scenario_id, label, row, Json(values), row, count + 1)) + count += 1 + print(f" -> {count} Umsatzzeilen importiert") + + +def import_materialaufwand(cur, ws, scenario_id): + """Import Materialaufwand (simplified: only Mac Mini + Mac Studio).""" + print(" Importing Materialaufwand...") + count = 0 + + # Cost rows (3-23): computed = quantity * unit_cost + for i in range(21): + row = 3 + i + label = safe_str(ws.cell(row, 1).value) + if not label: + continue + values = read_monthly_values(ws, row) + cur.execute(""" + INSERT INTO fp_materialaufwand + (scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order) + VALUES (%s, 'cost', %s, %s, false, %s, %s, %s) + """, (scenario_id, label, row, Json(values), row, count + 1)) + count += 1 + + # Total (24) + values = read_monthly_values(ws, 24) + cur.execute(""" + INSERT INTO fp_materialaufwand + (scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order) + VALUES (%s, 'cost', 'SUMME', 24, false, %s, 24, %s) + """, (scenario_id, Json(values), count + 1)) + count += 1 + + # Unit cost rows (51-73): editable EK prices + for i in range(23): + row = 51 + i + label = safe_str(ws.cell(row, 1).value) + if not label: + continue + values = read_monthly_values(ws, row) + cur.execute(""" + INSERT INTO fp_materialaufwand + (scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order) + VALUES (%s, 'unit_cost', %s, %s, true, %s, %s, %s) + """, (scenario_id, label, row, Json(values), row, count + 1)) + count += 1 + print(f" -> {count} Materialzeilen importiert (Mac Mini 3.200 / Mac Studio 13.000 EK)") + + +def import_guv(cur, ws, scenario_id): + """Import GuV Jahresabschluss (annual summary).""" + print(" Importing GuV Jahresabschluss...") + # Annual columns: B=2026, C=2027, D=2028, E=2029, F=2030 + year_cols = {2: 'y2026', 3: 'y2027', 4: 'y2028', 5: 'y2029', 6: 'y2030'} + + rows_config = [ + (3, 'Umsatzerloese', False, '=Umsatzerloese!Jahressumme'), + (4, 'Bestandsveraenderungen', True, None), + (5, 'Gesamtleistung', False, '=SUM(3:4)'), + (9, 'Sonst. betriebl. Ertraege', False, '=Sonst.Ertraege!Jahressumme'), + (10, 'Summe sonst. Ertraege', False, '=SUM(8:9)'), + (13, 'Materialaufwand Waren', False, '=Materialaufwand!Jahressumme'), + (14, 'Materialaufwand Leistungen', False, '=Materialaufwand!bezogene_Leistungen'), + (15, 'Summe Materialaufwand', False, '=13+14'), + (17, 'Rohergebnis', False, '=5+10-15'), + (20, 'Loehne und Gehaelter', False, '=Personalkosten!Brutto'), + (21, 'Soziale Abgaben', False, '=Personalkosten!Sozial'), + (22, 'Summe Personalaufwand', False, '=20+21'), + (25, 'Abschreibungen', False, '=Investitionen!AfA'), + (27, 'Sonst. betriebl. Aufwendungen', False, '=Betriebliche!Summe_sonstige'), + (29, 'EBIT', False, '=5+10-15-22-25-27'), + (31, 'Zinsertraege', True, None), + (33, 'Zinsaufwendungen', True, None), + (35, 'Steuern gesamt', False, '=36+37'), + (36, 'Koerperschaftssteuer', False, '=computed'), + (37, 'Gewerbesteuer', False, '=computed'), + (39, 'Ergebnis nach Steuern', False, '=29+31-33-35'), + (41, 'Sonstige Steuern', True, None), + (43, 'Jahresueberschuss', False, '=39-41'), + ] + + count = 0 + for row, label, is_edit, formula in rows_config: + values = {} + for col, key in year_cols.items(): + v = ws.cell(row, col).value + values[key] = round(float(v), 2) if v and not isinstance(v, str) else 0 + cur.execute(""" + INSERT INTO fp_guv + (scenario_id, row_label, row_index, is_sum_row, formula_desc, + values, excel_row, sort_order) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + """, (scenario_id, label, row, formula is not None and not is_edit, + formula, Json(values), row, count + 1)) + count += 1 + print(f" -> {count} GuV-Zeilen importiert") + + +def main(): + if len(sys.argv) < 2: + print("Usage: python3 import-finanzplan.py ") + sys.exit(1) + + xlsx_path = sys.argv[1] + print(f"Opening: {xlsx_path}") + wb = openpyxl.load_workbook(xlsx_path, data_only=True) + + print(f"Connecting to: {DB_URL.split('@')[1] if '@' in DB_URL else DB_URL}") + conn = psycopg2.connect(DB_URL) + cur = conn.cursor() + + # Schema already applied separately — skip if tables exist + cur.execute("SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'fp_scenarios')") + if not cur.fetchone()[0]: + schema_path = os.path.join(os.path.dirname(__file__), '001_finanzplan_tables.sql') + print(f"Applying schema: {schema_path}") + with open(schema_path) as f: + cur.execute(f.read()) + conn.commit() + else: + print("Schema already exists, skipping.") + + # Get or create default scenario + cur.execute("SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1") + row = cur.fetchone() + if row: + scenario_id = row[0] + # Clear existing data for re-import + for table in ['fp_kunden', 'fp_kunden_summary', 'fp_umsatzerloese', 'fp_materialaufwand', + 'fp_personalkosten', 'fp_betriebliche_aufwendungen', 'fp_investitionen', + 'fp_sonst_ertraege', 'fp_liquiditaet', 'fp_guv']: + cur.execute(f"DELETE FROM {table} WHERE scenario_id = %s", (scenario_id,)) + else: + cur.execute("INSERT INTO fp_scenarios (name, is_default) VALUES ('Base Case', true) RETURNING id") + scenario_id = cur.fetchone()[0] + + print(f"Scenario ID: {scenario_id}") + print(f"\nImporting sheets...") + + # Import each sheet + import_kunden(cur, wb['Kunden'], scenario_id) + import_umsatzerloese(cur, wb['Umsatzerlöse'], scenario_id) + import_materialaufwand(cur, wb['Materialaufwand'], scenario_id) + import_personalkosten(cur, wb['Personalkosten'], scenario_id) + import_betriebliche_aufwendungen(cur, wb['Betriebliche Aufwendungen'], scenario_id) + import_investitionen(cur, wb['Investitionen'], scenario_id) + import_sonst_ertraege(cur, wb['Sonst. betr. Erträge'], scenario_id) + import_liquiditaet(cur, wb['Liquidität'], scenario_id) + import_guv(cur, wb['GuV Jahresabschluss'], scenario_id) + + conn.commit() + print(f"\nImport abgeschlossen!") + + # Summary + for table in ['fp_kunden', 'fp_kunden_summary', 'fp_umsatzerloese', 'fp_materialaufwand', + 'fp_personalkosten', 'fp_betriebliche_aufwendungen', 'fp_investitionen', + 'fp_sonst_ertraege', 'fp_liquiditaet', 'fp_guv']: + cur.execute(f"SELECT COUNT(*) FROM {table} WHERE scenario_id = %s", (scenario_id,)) + count = cur.fetchone()[0] + print(f" {table}: {count} Zeilen") + + cur.close() + conn.close() + + +if __name__ == '__main__': + main()