[split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook) and split all 44 files exceeding 500 LOC into domain-focused modules: - consent-service (Go): models, handlers, services, database splits - backend-core (Python): security_api, rbac_api, pdf_service, auth splits - admin-core (TypeScript): 5 page.tsx + sidebar extractions - pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits - voice-service (Python): enhanced_task_orchestrator split Result: 0 violations, 36 exempted (pipeline, tests, pure-data files). Go build verified clean. No behavior changes — pure structural splits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
'use client'
|
||||
|
||||
import BilingualField from '@/components/pitch-admin/editors/BilingualField'
|
||||
import FormField from '@/components/pitch-admin/editors/FormField'
|
||||
import ArrayField from '@/components/pitch-admin/editors/ArrayField'
|
||||
import RowTable from '@/components/pitch-admin/editors/RowTable'
|
||||
import CardList from '@/components/pitch-admin/editors/CardList'
|
||||
|
||||
type R = Record<string, unknown>
|
||||
|
||||
interface TabEditorProps {
|
||||
activeTab: string
|
||||
data: unknown[]
|
||||
single: R
|
||||
jsonMode: boolean
|
||||
jsonText: string
|
||||
isDraft: boolean
|
||||
onJsonTextChange: (text: string) => void
|
||||
onDirty: () => void
|
||||
updateData: (newData: unknown[]) => void
|
||||
updateRecord: (index: number, key: string, value: unknown) => void
|
||||
updateSingle: (key: string, value: unknown) => void
|
||||
}
|
||||
|
||||
export default function TabEditor({
|
||||
activeTab,
|
||||
data,
|
||||
single,
|
||||
jsonMode,
|
||||
jsonText,
|
||||
isDraft,
|
||||
onJsonTextChange,
|
||||
onDirty,
|
||||
updateData,
|
||||
updateRecord,
|
||||
updateSingle,
|
||||
}: TabEditorProps) {
|
||||
if (jsonMode) {
|
||||
return (
|
||||
<textarea
|
||||
value={jsonText}
|
||||
onChange={e => { onJsonTextChange(e.target.value); onDirty() }}
|
||||
readOnly={!isDraft}
|
||||
className="w-full bg-transparent text-white/90 font-mono text-xs p-4 focus:outline-none resize-none"
|
||||
style={{ minHeight: '400px' }}
|
||||
spellCheck={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
switch (activeTab) {
|
||||
case 'company':
|
||||
return <CompanyEditor single={single} updateSingle={updateSingle} />
|
||||
case 'team':
|
||||
return <TeamEditor data={data as R[]} updateData={updateData} />
|
||||
case 'financials':
|
||||
return <FinancialsEditor data={data as R[]} updateData={updateData} />
|
||||
case 'market':
|
||||
return <MarketEditor data={data as R[]} updateData={updateData} />
|
||||
case 'competitors':
|
||||
return <CompetitorsEditor data={data as R[]} updateData={updateData} />
|
||||
case 'features':
|
||||
return <FeaturesEditor data={data as R[]} updateData={updateData} />
|
||||
case 'milestones':
|
||||
return <MilestonesEditor data={data as R[]} updateData={updateData} />
|
||||
case 'metrics':
|
||||
return <MetricsEditor data={data as R[]} updateData={updateData} />
|
||||
case 'funding':
|
||||
return <FundingEditor single={single} updateSingle={updateSingle} />
|
||||
case 'products':
|
||||
return <ProductsEditor data={data as R[]} updateData={updateData} />
|
||||
case 'fm_scenarios':
|
||||
return <FmScenariosEditor data={data as R[]} updateData={updateData} />
|
||||
case 'fm_assumptions':
|
||||
return <FmAssumptionsEditor data={data as R[]} updateData={updateData} />
|
||||
default:
|
||||
return <div className="p-4 text-white/40">No editor for this table</div>
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Individual tab editors --- */
|
||||
|
||||
function CompanyEditor({ single, updateSingle }: { single: R; updateSingle: (k: string, v: unknown) => void }) {
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<FormField label="Company Name" value={single.name as string || ''} onChange={v => updateSingle('name', v)} />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Legal Form" value={single.legal_form as string || ''} onChange={v => updateSingle('legal_form', v)} placeholder="GmbH" />
|
||||
<FormField label="Founding Date" value={single.founding_date as string || ''} onChange={v => updateSingle('founding_date', v)} type="date" />
|
||||
</div>
|
||||
<BilingualField label="Tagline" valueDe={single.tagline_de as string || ''} valueEn={single.tagline_en as string || ''} onChangeDe={v => updateSingle('tagline_de', v)} onChangeEn={v => updateSingle('tagline_en', v)} />
|
||||
<BilingualField label="Mission" valueDe={single.mission_de as string || ''} valueEn={single.mission_en as string || ''} onChangeDe={v => updateSingle('mission_de', v)} onChangeEn={v => updateSingle('mission_en', v)} multiline />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Website" value={single.website as string || ''} onChange={v => updateSingle('website', v)} type="url" />
|
||||
<FormField label="HQ City" value={single.hq_city as string || ''} onChange={v => updateSingle('hq_city', v)} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TeamEditor({ data, updateData }: { data: R[]; updateData: (d: unknown[]) => void }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data}
|
||||
onChange={updateData}
|
||||
titleKey="name"
|
||||
subtitleKey="role_en"
|
||||
addLabel="Add team member"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<FormField label="Name" value={item.name as string || ''} onChange={v => update('name', v)} />
|
||||
<BilingualField label="Role" valueDe={item.role_de as string || ''} valueEn={item.role_en as string || ''} onChangeDe={v => update('role_de', v)} onChangeEn={v => update('role_en', v)} />
|
||||
<BilingualField label="Bio" valueDe={item.bio_de as string || ''} valueEn={item.bio_en as string || ''} onChangeDe={v => update('bio_de', v)} onChangeEn={v => update('bio_en', v)} multiline />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Equity %" value={item.equity_pct as number || 0} onChange={v => update('equity_pct', v)} type="number" />
|
||||
<FormField label="LinkedIn" value={item.linkedin_url as string || ''} onChange={v => update('linkedin_url', v)} type="url" />
|
||||
</div>
|
||||
<ArrayField label="Expertise" values={(item.expertise as string[]) || []} onChange={v => update('expertise', v)} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FinancialsEditor({ data, updateData }: { data: R[]; updateData: (d: unknown[]) => void }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<RowTable
|
||||
rows={data}
|
||||
onChange={updateData}
|
||||
columns={[
|
||||
{ key: 'year', label: 'Year', type: 'number' },
|
||||
{ key: 'revenue_eur', label: 'Revenue (EUR)', type: 'number' },
|
||||
{ key: 'costs_eur', label: 'Costs (EUR)', type: 'number' },
|
||||
{ key: 'mrr_eur', label: 'MRR (EUR)', type: 'number' },
|
||||
{ key: 'arr_eur', label: 'ARR (EUR)', type: 'number' },
|
||||
{ key: 'customers_count', label: 'Customers', type: 'number' },
|
||||
{ key: 'employees_count', label: 'Employees', type: 'number' },
|
||||
{ key: 'burn_rate_eur', label: 'Burn (EUR)', type: 'number' },
|
||||
]}
|
||||
addLabel="Add year"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MarketEditor({ data, updateData }: { data: R[]; updateData: (d: unknown[]) => void }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<RowTable
|
||||
rows={data}
|
||||
onChange={updateData}
|
||||
columns={[
|
||||
{ key: 'market_segment', label: 'Segment' },
|
||||
{ key: 'label', label: 'Label' },
|
||||
{ key: 'value_eur', label: 'Value (EUR)', type: 'number' },
|
||||
{ key: 'growth_rate_pct', label: 'Growth %', type: 'number' },
|
||||
{ key: 'source', label: 'Source' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CompetitorsEditor({ data, updateData }: { data: R[]; updateData: (d: unknown[]) => void }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data}
|
||||
onChange={updateData}
|
||||
titleKey="name"
|
||||
subtitleKey="website"
|
||||
addLabel="Add competitor"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Name" value={item.name as string || ''} onChange={v => update('name', v)} />
|
||||
<FormField label="Website" value={item.website as string || ''} onChange={v => update('website', v)} type="url" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Customers" value={item.customers_count as number || 0} onChange={v => update('customers_count', v)} type="number" />
|
||||
<FormField label="Pricing Range" value={item.pricing_range as string || ''} onChange={v => update('pricing_range', v)} />
|
||||
</div>
|
||||
<ArrayField label="Strengths" values={(item.strengths as string[]) || []} onChange={v => update('strengths', v)} />
|
||||
<ArrayField label="Weaknesses" values={(item.weaknesses as string[]) || []} onChange={v => update('weaknesses', v)} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FeaturesEditor({ data, updateData }: { data: R[]; updateData: (d: unknown[]) => void }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data}
|
||||
onChange={updateData}
|
||||
titleKey="feature_name_en"
|
||||
subtitleKey="category"
|
||||
addLabel="Add feature"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<BilingualField label="Feature Name" valueDe={item.feature_name_de as string || ''} valueEn={item.feature_name_en as string || ''} onChangeDe={v => update('feature_name_de', v)} onChangeEn={v => update('feature_name_en', v)} />
|
||||
<FormField label="Category" value={item.category as string || ''} onChange={v => update('category', v)} />
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
<FormField label="BreakPilot" value={!!item.breakpilot} onChange={v => update('breakpilot', v)} type="checkbox" />
|
||||
<FormField label="Proliance" value={!!item.proliance} onChange={v => update('proliance', v)} type="checkbox" />
|
||||
<FormField label="DataGuard" value={!!item.dataguard} onChange={v => update('dataguard', v)} type="checkbox" />
|
||||
<FormField label="heyData" value={!!item.heydata} onChange={v => update('heydata', v)} type="checkbox" />
|
||||
<FormField label="Differentiator" value={!!item.is_differentiator} onChange={v => update('is_differentiator', v)} type="checkbox" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MilestonesEditor({ data, updateData }: { data: R[]; updateData: (d: unknown[]) => void }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data}
|
||||
onChange={updateData}
|
||||
titleKey="title_en"
|
||||
subtitleKey="milestone_date"
|
||||
addLabel="Add milestone"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<BilingualField label="Title" valueDe={item.title_de as string || ''} valueEn={item.title_en as string || ''} onChangeDe={v => update('title_de', v)} onChangeEn={v => update('title_en', v)} />
|
||||
<BilingualField label="Description" valueDe={item.description_de as string || ''} valueEn={item.description_en as string || ''} onChangeDe={v => update('description_de', v)} onChangeEn={v => update('description_en', v)} multiline />
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormField label="Date" value={item.milestone_date as string || ''} onChange={v => update('milestone_date', v)} />
|
||||
<FormField label="Status" value={item.status as string || ''} onChange={v => update('status', v)} type="select" options={[
|
||||
{ value: 'completed', label: 'Completed' }, { value: 'in_progress', label: 'In Progress' }, { value: 'planned', label: 'Planned' },
|
||||
]} />
|
||||
<FormField label="Category" value={item.category as string || ''} onChange={v => update('category', v)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricsEditor({ data, updateData }: { data: R[]; updateData: (d: unknown[]) => void }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data}
|
||||
onChange={updateData}
|
||||
titleKey="metric_name"
|
||||
subtitleKey="value"
|
||||
addLabel="Add metric"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<FormField label="Metric Key" value={item.metric_name as string || ''} onChange={v => update('metric_name', v)} />
|
||||
<BilingualField label="Label" valueDe={item.label_de as string || ''} valueEn={item.label_en as string || ''} onChangeDe={v => update('label_de', v)} onChangeEn={v => update('label_en', v)} />
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormField label="Value" value={item.value as string || ''} onChange={v => update('value', v)} />
|
||||
<FormField label="Unit" value={item.unit as string || ''} onChange={v => update('unit', v)} />
|
||||
<FormField label="Is Live" value={!!item.is_live} onChange={v => update('is_live', v)} type="checkbox" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FundingEditor({ single, updateSingle }: { single: R; updateSingle: (k: string, v: unknown) => void }) {
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<FormField label="Round Name" value={single.round_name as string || ''} onChange={v => updateSingle('round_name', v)} />
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormField label="Amount (EUR)" value={single.amount_eur as number || 0} onChange={v => updateSingle('amount_eur', v)} type="number" />
|
||||
<FormField label="Instrument" value={single.instrument as string || ''} onChange={v => updateSingle('instrument', v)} />
|
||||
<FormField label="Target Date" value={single.target_date as string || ''} onChange={v => updateSingle('target_date', v)} type="date" />
|
||||
</div>
|
||||
<FormField label="Status" value={single.status as string || ''} onChange={v => updateSingle('status', v)} type="select" options={[
|
||||
{ value: 'planned', label: 'Planned' }, { value: 'in_progress', label: 'In Progress' }, { value: 'completed', label: 'Completed' },
|
||||
]} />
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-white/60 uppercase tracking-wider mb-2">Use of Funds</label>
|
||||
<RowTable
|
||||
rows={(single.use_of_funds as R[]) || []}
|
||||
onChange={v => updateSingle('use_of_funds', v)}
|
||||
columns={[
|
||||
{ key: 'category', label: 'Category' },
|
||||
{ key: 'percentage', label: '%', type: 'number' },
|
||||
{ key: 'label_de', label: 'Label DE' },
|
||||
{ key: 'label_en', label: 'Label EN' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ProductsEditor({ data, updateData }: { data: R[]; updateData: (d: unknown[]) => void }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data}
|
||||
onChange={updateData}
|
||||
titleKey="name"
|
||||
subtitleKey="hardware"
|
||||
addLabel="Add product"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Name" value={item.name as string || ''} onChange={v => update('name', v)} />
|
||||
<FormField label="Hardware" value={item.hardware as string || ''} onChange={v => update('hardware', v)} />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormField label="HW Cost (EUR)" value={item.hardware_cost_eur as number || 0} onChange={v => update('hardware_cost_eur', v)} type="number" />
|
||||
<FormField label="Monthly Price (EUR)" value={item.monthly_price_eur as number || 0} onChange={v => update('monthly_price_eur', v)} type="number" />
|
||||
<FormField label="Operating Cost (EUR)" value={item.operating_cost_eur as number || 0} onChange={v => update('operating_cost_eur', v)} type="number" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="LLM Model" value={item.llm_model as string || ''} onChange={v => update('llm_model', v)} />
|
||||
<FormField label="LLM Size" value={item.llm_size as string || ''} onChange={v => update('llm_size', v)} />
|
||||
</div>
|
||||
<BilingualField label="LLM Capability" valueDe={item.llm_capability_de as string || ''} valueEn={item.llm_capability_en as string || ''} onChangeDe={v => update('llm_capability_de', v)} onChangeEn={v => update('llm_capability_en', v)} multiline />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<ArrayField label="Features (DE)" values={(item.features_de as string[]) || []} onChange={v => update('features_de', v)} />
|
||||
<ArrayField label="Features (EN)" values={(item.features_en as string[]) || []} onChange={v => update('features_en', v)} />
|
||||
</div>
|
||||
<FormField label="Popular" value={!!item.is_popular} onChange={v => update('is_popular', v)} type="checkbox" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FmScenariosEditor({ data, updateData }: { data: R[]; updateData: (d: unknown[]) => void }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data}
|
||||
onChange={updateData}
|
||||
titleKey="name"
|
||||
subtitleKey="description"
|
||||
addLabel="Add scenario"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<FormField label="Name" value={item.name as string || ''} onChange={v => update('name', v)} />
|
||||
<FormField label="Description" value={item.description as string || ''} onChange={v => update('description', v)} />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Color" value={item.color as string || '#6366f1'} onChange={v => update('color', v)} type="color" />
|
||||
<FormField label="Default" value={!!item.is_default} onChange={v => update('is_default', v)} type="checkbox" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FmAssumptionsEditor({ data, updateData }: { data: R[]; updateData: (d: unknown[]) => void }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<RowTable
|
||||
rows={data}
|
||||
onChange={updateData}
|
||||
columns={[
|
||||
{ key: 'key', label: 'Key' },
|
||||
{ key: 'label_de', label: 'Label DE' },
|
||||
{ key: 'label_en', label: 'Label EN' },
|
||||
{ key: 'category', label: 'Category' },
|
||||
{ key: 'unit', label: 'Unit' },
|
||||
]}
|
||||
addLabel="Add assumption"
|
||||
/>
|
||||
<p className="text-[10px] text-white/30 mt-2">Note: values, min/max/step are best edited via "Edit as JSON" mode for complex types.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,11 +4,7 @@ import { useEffect, useState, useCallback } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, Lock, Save, GitFork, Eye, Code } from 'lucide-react'
|
||||
import BilingualField from '@/components/pitch-admin/editors/BilingualField'
|
||||
import FormField from '@/components/pitch-admin/editors/FormField'
|
||||
import ArrayField from '@/components/pitch-admin/editors/ArrayField'
|
||||
import RowTable from '@/components/pitch-admin/editors/RowTable'
|
||||
import CardList from '@/components/pitch-admin/editors/CardList'
|
||||
import TabEditor from './_components/TabEditors'
|
||||
|
||||
const TABLE_LABELS: Record<string, string> = {
|
||||
company: 'Company', team: 'Team', financials: 'Financials', market: 'Market',
|
||||
@@ -65,7 +61,6 @@ export default function VersionEditorPage() {
|
||||
updateData(arr)
|
||||
}
|
||||
|
||||
// For single-record tables (company, funding)
|
||||
function updateSingle(key: string, value: unknown) { updateRecord(0, key, value) }
|
||||
|
||||
async function saveTable() {
|
||||
@@ -114,316 +109,6 @@ export default function VersionEditorPage() {
|
||||
const data = allData[activeTab] || []
|
||||
const single = (data as R[])[0] || {} as R
|
||||
|
||||
function renderEditor() {
|
||||
if (jsonMode) {
|
||||
return (
|
||||
<textarea
|
||||
value={jsonText}
|
||||
onChange={e => { setJsonText(e.target.value); setDirty(true) }}
|
||||
readOnly={!isDraft}
|
||||
className="w-full bg-transparent text-white/90 font-mono text-xs p-4 focus:outline-none resize-none"
|
||||
style={{ minHeight: '400px' }}
|
||||
spellCheck={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
switch (activeTab) {
|
||||
case 'company':
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<FormField label="Company Name" value={single.name as string || ''} onChange={v => updateSingle('name', v)} />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Legal Form" value={single.legal_form as string || ''} onChange={v => updateSingle('legal_form', v)} placeholder="GmbH" />
|
||||
<FormField label="Founding Date" value={single.founding_date as string || ''} onChange={v => updateSingle('founding_date', v)} type="date" />
|
||||
</div>
|
||||
<BilingualField label="Tagline" valueDe={single.tagline_de as string || ''} valueEn={single.tagline_en as string || ''} onChangeDe={v => updateSingle('tagline_de', v)} onChangeEn={v => updateSingle('tagline_en', v)} />
|
||||
<BilingualField label="Mission" valueDe={single.mission_de as string || ''} valueEn={single.mission_en as string || ''} onChangeDe={v => updateSingle('mission_de', v)} onChangeEn={v => updateSingle('mission_en', v)} multiline />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Website" value={single.website as string || ''} onChange={v => updateSingle('website', v)} type="url" />
|
||||
<FormField label="HQ City" value={single.hq_city as string || ''} onChange={v => updateSingle('hq_city', v)} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'team':
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data as R[]}
|
||||
onChange={updateData}
|
||||
titleKey="name"
|
||||
subtitleKey="role_en"
|
||||
addLabel="Add team member"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<FormField label="Name" value={item.name as string || ''} onChange={v => update('name', v)} />
|
||||
<BilingualField label="Role" valueDe={item.role_de as string || ''} valueEn={item.role_en as string || ''} onChangeDe={v => update('role_de', v)} onChangeEn={v => update('role_en', v)} />
|
||||
<BilingualField label="Bio" valueDe={item.bio_de as string || ''} valueEn={item.bio_en as string || ''} onChangeDe={v => update('bio_de', v)} onChangeEn={v => update('bio_en', v)} multiline />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Equity %" value={item.equity_pct as number || 0} onChange={v => update('equity_pct', v)} type="number" />
|
||||
<FormField label="LinkedIn" value={item.linkedin_url as string || ''} onChange={v => update('linkedin_url', v)} type="url" />
|
||||
</div>
|
||||
<ArrayField label="Expertise" values={(item.expertise as string[]) || []} onChange={v => update('expertise', v)} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'financials':
|
||||
return (
|
||||
<div className="p-4">
|
||||
<RowTable
|
||||
rows={data as R[]}
|
||||
onChange={updateData}
|
||||
columns={[
|
||||
{ key: 'year', label: 'Year', type: 'number' },
|
||||
{ key: 'revenue_eur', label: 'Revenue (EUR)', type: 'number' },
|
||||
{ key: 'costs_eur', label: 'Costs (EUR)', type: 'number' },
|
||||
{ key: 'mrr_eur', label: 'MRR (EUR)', type: 'number' },
|
||||
{ key: 'arr_eur', label: 'ARR (EUR)', type: 'number' },
|
||||
{ key: 'customers_count', label: 'Customers', type: 'number' },
|
||||
{ key: 'employees_count', label: 'Employees', type: 'number' },
|
||||
{ key: 'burn_rate_eur', label: 'Burn (EUR)', type: 'number' },
|
||||
]}
|
||||
addLabel="Add year"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'market':
|
||||
return (
|
||||
<div className="p-4">
|
||||
<RowTable
|
||||
rows={data as R[]}
|
||||
onChange={updateData}
|
||||
columns={[
|
||||
{ key: 'market_segment', label: 'Segment' },
|
||||
{ key: 'label', label: 'Label' },
|
||||
{ key: 'value_eur', label: 'Value (EUR)', type: 'number' },
|
||||
{ key: 'growth_rate_pct', label: 'Growth %', type: 'number' },
|
||||
{ key: 'source', label: 'Source' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'competitors':
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data as R[]}
|
||||
onChange={updateData}
|
||||
titleKey="name"
|
||||
subtitleKey="website"
|
||||
addLabel="Add competitor"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Name" value={item.name as string || ''} onChange={v => update('name', v)} />
|
||||
<FormField label="Website" value={item.website as string || ''} onChange={v => update('website', v)} type="url" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Customers" value={item.customers_count as number || 0} onChange={v => update('customers_count', v)} type="number" />
|
||||
<FormField label="Pricing Range" value={item.pricing_range as string || ''} onChange={v => update('pricing_range', v)} />
|
||||
</div>
|
||||
<ArrayField label="Strengths" values={(item.strengths as string[]) || []} onChange={v => update('strengths', v)} />
|
||||
<ArrayField label="Weaknesses" values={(item.weaknesses as string[]) || []} onChange={v => update('weaknesses', v)} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'features':
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data as R[]}
|
||||
onChange={updateData}
|
||||
titleKey="feature_name_en"
|
||||
subtitleKey="category"
|
||||
addLabel="Add feature"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<BilingualField label="Feature Name" valueDe={item.feature_name_de as string || ''} valueEn={item.feature_name_en as string || ''} onChangeDe={v => update('feature_name_de', v)} onChangeEn={v => update('feature_name_en', v)} />
|
||||
<FormField label="Category" value={item.category as string || ''} onChange={v => update('category', v)} />
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
<FormField label="BreakPilot" value={!!item.breakpilot} onChange={v => update('breakpilot', v)} type="checkbox" />
|
||||
<FormField label="Proliance" value={!!item.proliance} onChange={v => update('proliance', v)} type="checkbox" />
|
||||
<FormField label="DataGuard" value={!!item.dataguard} onChange={v => update('dataguard', v)} type="checkbox" />
|
||||
<FormField label="heyData" value={!!item.heydata} onChange={v => update('heydata', v)} type="checkbox" />
|
||||
<FormField label="Differentiator" value={!!item.is_differentiator} onChange={v => update('is_differentiator', v)} type="checkbox" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'milestones':
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data as R[]}
|
||||
onChange={updateData}
|
||||
titleKey="title_en"
|
||||
subtitleKey="milestone_date"
|
||||
addLabel="Add milestone"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<BilingualField label="Title" valueDe={item.title_de as string || ''} valueEn={item.title_en as string || ''} onChangeDe={v => update('title_de', v)} onChangeEn={v => update('title_en', v)} />
|
||||
<BilingualField label="Description" valueDe={item.description_de as string || ''} valueEn={item.description_en as string || ''} onChangeDe={v => update('description_de', v)} onChangeEn={v => update('description_en', v)} multiline />
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormField label="Date" value={item.milestone_date as string || ''} onChange={v => update('milestone_date', v)} />
|
||||
<FormField label="Status" value={item.status as string || ''} onChange={v => update('status', v)} type="select" options={[
|
||||
{ value: 'completed', label: 'Completed' }, { value: 'in_progress', label: 'In Progress' }, { value: 'planned', label: 'Planned' },
|
||||
]} />
|
||||
<FormField label="Category" value={item.category as string || ''} onChange={v => update('category', v)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'metrics':
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data as R[]}
|
||||
onChange={updateData}
|
||||
titleKey="metric_name"
|
||||
subtitleKey="value"
|
||||
addLabel="Add metric"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<FormField label="Metric Key" value={item.metric_name as string || ''} onChange={v => update('metric_name', v)} />
|
||||
<BilingualField label="Label" valueDe={item.label_de as string || ''} valueEn={item.label_en as string || ''} onChangeDe={v => update('label_de', v)} onChangeEn={v => update('label_en', v)} />
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormField label="Value" value={item.value as string || ''} onChange={v => update('value', v)} />
|
||||
<FormField label="Unit" value={item.unit as string || ''} onChange={v => update('unit', v)} />
|
||||
<FormField label="Is Live" value={!!item.is_live} onChange={v => update('is_live', v)} type="checkbox" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'funding':
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<FormField label="Round Name" value={single.round_name as string || ''} onChange={v => updateSingle('round_name', v)} />
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormField label="Amount (EUR)" value={single.amount_eur as number || 0} onChange={v => updateSingle('amount_eur', v)} type="number" />
|
||||
<FormField label="Instrument" value={single.instrument as string || ''} onChange={v => updateSingle('instrument', v)} />
|
||||
<FormField label="Target Date" value={single.target_date as string || ''} onChange={v => updateSingle('target_date', v)} type="date" />
|
||||
</div>
|
||||
<FormField label="Status" value={single.status as string || ''} onChange={v => updateSingle('status', v)} type="select" options={[
|
||||
{ value: 'planned', label: 'Planned' }, { value: 'in_progress', label: 'In Progress' }, { value: 'completed', label: 'Completed' },
|
||||
]} />
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-white/60 uppercase tracking-wider mb-2">Use of Funds</label>
|
||||
<RowTable
|
||||
rows={(single.use_of_funds as R[]) || []}
|
||||
onChange={v => updateSingle('use_of_funds', v)}
|
||||
columns={[
|
||||
{ key: 'category', label: 'Category' },
|
||||
{ key: 'percentage', label: '%', type: 'number' },
|
||||
{ key: 'label_de', label: 'Label DE' },
|
||||
{ key: 'label_en', label: 'Label EN' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'products':
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data as R[]}
|
||||
onChange={updateData}
|
||||
titleKey="name"
|
||||
subtitleKey="hardware"
|
||||
addLabel="Add product"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Name" value={item.name as string || ''} onChange={v => update('name', v)} />
|
||||
<FormField label="Hardware" value={item.hardware as string || ''} onChange={v => update('hardware', v)} />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormField label="HW Cost (EUR)" value={item.hardware_cost_eur as number || 0} onChange={v => update('hardware_cost_eur', v)} type="number" />
|
||||
<FormField label="Monthly Price (EUR)" value={item.monthly_price_eur as number || 0} onChange={v => update('monthly_price_eur', v)} type="number" />
|
||||
<FormField label="Operating Cost (EUR)" value={item.operating_cost_eur as number || 0} onChange={v => update('operating_cost_eur', v)} type="number" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="LLM Model" value={item.llm_model as string || ''} onChange={v => update('llm_model', v)} />
|
||||
<FormField label="LLM Size" value={item.llm_size as string || ''} onChange={v => update('llm_size', v)} />
|
||||
</div>
|
||||
<BilingualField label="LLM Capability" valueDe={item.llm_capability_de as string || ''} valueEn={item.llm_capability_en as string || ''} onChangeDe={v => update('llm_capability_de', v)} onChangeEn={v => update('llm_capability_en', v)} multiline />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<ArrayField label="Features (DE)" values={(item.features_de as string[]) || []} onChange={v => update('features_de', v)} />
|
||||
<ArrayField label="Features (EN)" values={(item.features_en as string[]) || []} onChange={v => update('features_en', v)} />
|
||||
</div>
|
||||
<FormField label="Popular" value={!!item.is_popular} onChange={v => update('is_popular', v)} type="checkbox" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'fm_scenarios':
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CardList
|
||||
items={data as R[]}
|
||||
onChange={updateData}
|
||||
titleKey="name"
|
||||
subtitleKey="description"
|
||||
addLabel="Add scenario"
|
||||
renderCard={(item, update) => (
|
||||
<div className="space-y-3">
|
||||
<FormField label="Name" value={item.name as string || ''} onChange={v => update('name', v)} />
|
||||
<FormField label="Description" value={item.description as string || ''} onChange={v => update('description', v)} />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField label="Color" value={item.color as string || '#6366f1'} onChange={v => update('color', v)} type="color" />
|
||||
<FormField label="Default" value={!!item.is_default} onChange={v => update('is_default', v)} type="checkbox" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'fm_assumptions':
|
||||
// Reuse the inline table approach from the FM editor (already works well for this)
|
||||
return (
|
||||
<div className="p-4">
|
||||
<RowTable
|
||||
rows={data as R[]}
|
||||
onChange={updateData}
|
||||
columns={[
|
||||
{ key: 'key', label: 'Key' },
|
||||
{ key: 'label_de', label: 'Label DE' },
|
||||
{ key: 'label_en', label: 'Label EN' },
|
||||
{ key: 'category', label: 'Category' },
|
||||
{ key: 'unit', label: 'Unit' },
|
||||
]}
|
||||
addLabel="Add assumption"
|
||||
/>
|
||||
<p className="text-[10px] text-white/30 mt-2">Note: values, min/max/step are best edited via "Edit as JSON" mode for complex types.</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return <div className="p-4 text-white/40">No editor for this table</div>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Link href="/pitch-admin/versions" className="inline-flex items-center gap-2 text-sm text-white/50 hover:text-white/80">
|
||||
@@ -467,17 +152,17 @@ export default function VersionEditorPage() {
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 overflow-x-auto pb-1">
|
||||
{TABLE_NAMES.map(t => (
|
||||
{TABLE_NAMES.map(tab => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => { if (dirty && !confirm('Discard unsaved changes?')) return; setActiveTab(t); setDirty(false); setJsonMode(false) }}
|
||||
key={tab}
|
||||
onClick={() => { if (dirty && !confirm('Discard unsaved changes?')) return; setActiveTab(tab); setDirty(false); setJsonMode(false) }}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs whitespace-nowrap transition-colors ${
|
||||
activeTab === t
|
||||
activeTab === tab
|
||||
? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30'
|
||||
: 'bg-white/[0.04] text-white/40 border border-transparent hover:text-white/60'
|
||||
}`}
|
||||
>
|
||||
{TABLE_LABELS[t]}
|
||||
{TABLE_LABELS[tab]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -504,15 +189,27 @@ export default function VersionEditorPage() {
|
||||
disabled={saving || !dirty}
|
||||
className="bg-indigo-500 hover:bg-indigo-600 text-white text-sm font-medium px-4 py-1.5 rounded-lg flex items-center gap-2 disabled:opacity-30"
|
||||
>
|
||||
<Save className="w-3.5 h-3.5" /> {saving ? 'Saving…' : 'Save'}
|
||||
<Save className="w-3.5 h-3.5" /> {saving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{renderEditor()}
|
||||
<TabEditor
|
||||
activeTab={activeTab}
|
||||
data={data}
|
||||
single={single}
|
||||
jsonMode={jsonMode}
|
||||
jsonText={jsonText}
|
||||
isDraft={isDraft}
|
||||
onJsonTextChange={setJsonText}
|
||||
onDirty={() => setDirty(true)}
|
||||
updateData={updateData}
|
||||
updateRecord={updateRecord}
|
||||
updateSingle={updateSingle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isDraft && <p className="text-xs text-white/30 text-center">Committed — read-only. Fork to edit.</p>}
|
||||
{!isDraft && <p className="text-xs text-white/30 text-center">Committed -- read-only. Fork to edit.</p>}
|
||||
|
||||
{toast && (
|
||||
<div className="fixed bottom-6 right-6 bg-indigo-500/20 border border-indigo-500/40 text-indigo-200 text-sm px-4 py-2 rounded-lg backdrop-blur-sm z-50">
|
||||
|
||||
Reference in New Issue
Block a user