2026-09-04 09:19:14 +02:00
import { useState } from "react" ;
import { useApp } from "../context/AppContext.jsx" ;
import { fmt , fmtDatum } from "../utils/format.js" ;
import { PURPLE , PURPLE_LIGHT , GREEN , RED , CAT_COLORS } from "../constants/index.js" ;
import {
VCard , DonutChart , Overlay , ModalBox , ModalHeader , ModalFooter ,
EmojiPicker , AppIcon , ConfirmDeleteDialog , EmptyState ,
} from "../components/ui/index.jsx" ;
// ── AanpassenPopup (EV categorie) ─────────────────────────────────────────────
function AanpassenPopup ({ cat , onClose }) {
const { data , setData , ev , cats , darkMode , T } = useApp ();
const [ items , setItems ] = useState ( cat . items . map (( i ) => ({ ... i })));
const [ catNaam , setCatNaam ] = useState ( cat . naam );
const [ catColor , setCatColor ] = useState ( cat . color );
const [ catIcon , setCatIcon ] = useState ( cat . icon || "" );
const [ showEmoji , setShowEmoji ] = useState ( false );
const [ nieuwNaam , setNieuwNaam ] = useState ( "" );
const [ nieuwVal , setNieuwVal ] = useState ( "" );
const [ err , setErr ] = useState ( "" );
const [ confirmDel , setConfirmDel ] = useState ( false );
const is = inputStyle ( T );
const updateItem = ( id , field , val ) =>
setItems (( p ) => p . map (( i ) => i . id !== id ? i : { ... i , [ field ] : val }));
const removeItemLocal = ( id ) => {
if ( items . length <= 1 ) return ;
setItems (( p ) => p . filter (( i ) => i . id !== id ));
};
const addNieuw = () => {
if ( ! nieuwNaam . trim ()) { setErr ( "Naam is verplicht." ); return ; }
setItems (( p ) => [... p , { id : `i ${ Date . now () } ` , naam : nieuwNaam . trim (), waarde : parseFloat ( nieuwVal ) || 0 }]);
setNieuwNaam ( "" ); setNieuwVal ( "" ); setErr ( "" );
};
const handleSave = () => {
const updated = {
... data ,
eigenVermogen : {
... ev ,
categories : cats . map (( c ) =>
c . id !== cat . id ? c : {
... c ,
naam : catNaam . trim () || c . naam ,
color : catColor ,
icon : catIcon ,
items : items . map (( i ) => ({ ... i , waarde : parseFloat ( i . waarde ) || 0 })),
}
),
},
};
setData ( updated ); onClose ();
};
const handleDelete = () => {
setData ({ ... data , eigenVermogen : { ... ev , categories : cats . filter (( c ) => c . id !== cat . id ) } });
onClose ();
};
return (
< Overlay onClose = { onClose }>
< ModalBox >
< ModalHeader
icon = "✏️" iconBg = { cat . color }
title = "Aanpassen" subtitle = { cat . naam } subtitleColor = { cat . color }
onClose = { onClose }
headerBg = { darkMode ? ` ${ cat . color } 11` : ` ${ cat . color } 09` }
T = { T }
/>
< div style = {{ padding : "20px 22px" , overflowY : "auto" , maxHeight : "65vh" }}>
{ /* Naam + emoji */ }
< FieldLabel label = "Categorie naam" T = { T } />
< div style = {{ marginBottom : 16 }}>
< div style = {{ display : "flex" , gap : 8 , alignItems : "center" }}>
< button onClick = {() => setShowEmoji (( p ) => ! p )} style = {{
width : 38 , height : 38 , borderRadius : 8 ,
background : ` ${ cat . color } 22` ,
border : `2px solid ${ showEmoji ? cat . color : cat . color + "44" } ` ,
display : "flex" , alignItems : "center" , justifyContent : "center" ,
fontSize : 20 , flexShrink : 0 , cursor : "pointer" ,
}}>
< AppIcon value = { catIcon } fallback = "🏷" size = { 20 } />
</ button >
< input value = { catNaam } onChange = {( e ) => setCatNaam ( e . target . value )}
style = {{ ... is , flex : 1 , borderLeft : `3px solid ${ cat . color } ` }}
placeholder = "Naam categorie" />
</ div >
{ showEmoji && (
< EmojiPicker
selected = { catIcon } T = { T } accentColor = { cat . color }
onSelect = {( e ) => { setCatIcon ( e ); setShowEmoji ( false ); }}
onClear = {() => { setCatIcon ( "" ); setShowEmoji ( false ); }}
/>
)}
</ div >
{ /* Kleur */ }
< FieldLabel label = "Kleur" T = { T } />
< div style = {{ display : "flex" , gap : 8 , flexWrap : "wrap" , marginBottom : 16 }}>
{ CAT_COLORS . map (( c ) => (
< button key = { c } onClick = {() => setCatColor ( c )} style = {{
width : 28 , height : 28 , borderRadius : "50%" , background : c ,
border : `3px solid ${ catColor === c ? "#fff" : "transparent" } ` ,
cursor : "pointer" , outline : catColor === c ? `2px solid ${ c } ` : "none" , outlineOffset : 1 ,
}} />
))}
</ div >
{ /* Bestaande items */ }
< FieldLabel label = "Items bewerken" T = { T } />
{ items . map (( item ) => (
< div key = { item . id } style = {{
display : "flex" , alignItems : "center" , gap : 8 , marginBottom : 10 ,
background : darkMode ? "rgba(255,255,255,0.03)" : "#f8fafc" ,
border : `1px solid ${ T . border } ` , borderLeft : `3px solid ${ cat . color } ` ,
borderRadius : 10 , padding : "10px 12px" ,
}}>
< input value = { item . naam } onChange = {( e ) => updateItem ( item . id , "naam" , e . target . value )}
style = {{ ... is , flex : 1 }} placeholder = "Naam" />
< EuroInput value = { item . waarde } onChange = {( v ) => updateItem ( item . id , "waarde" , v )} is = { is } T = { T } />
{ items . length > 1 && (
< button onClick = {() => removeItemLocal ( item . id )} style = { deleteSmBtn }> × </ button >
)}
</ div >
))}
{ /* Nieuw item */ }
< div style = {{ marginTop : 18 , paddingTop : 16 , borderTop : `1px solid ${ T . border } ` }}>
< FieldLabel label = "Nieuw item toevoegen" T = { T } />
< div style = {{
display : "flex" , alignItems : "center" , gap : 8 ,
background : darkMode ? "rgba(255,255,255,0.03)" : "#f8fafc" ,
border : `1px solid ${ T . border } ` , borderLeft : `3px solid ${ PURPLE } ` ,
borderRadius : 10 , padding : "10px 12px" ,
}}>
< input value = { nieuwNaam }
onChange = {( e ) => { setNieuwNaam ( e . target . value ); setErr ( "" ); }}
placeholder = "Naam" style = {{ ... is , flex : 1 }} />
< EuroInput value = { nieuwVal } onChange = { setNieuwVal } is = { is } T = { T } />
< button onClick = { addNieuw } style = { addBtn }> + Voeg toe </ button >
</ div >
{ err && < ErrMsg msg = { err } />}
</ div >
</ div >
< ModalFooter T = { T }>
{ confirmDel ? (
< ConfirmDeleteDialog
naam = { cat . naam } T = { T }
onCancel = {() => setConfirmDel ( false )}
onConfirm = { handleDelete }
/>
) : (
<>
< button onClick = {() => setConfirmDel ( true )} style = { trashBtn }> 🗑 Verwijderen </ button >
< button onClick = { onClose } style = { cancelBtn ( T )}> Annuleren </ button >
< button onClick = { handleSave } style = {{
flex : 2 , padding : "10px" ,
background : `linear-gradient(135deg, ${ cat . color } , ${ cat . color } cc)` ,
border : "none" , borderRadius : 10 , color : "#fff" ,
fontWeight : 700 , cursor : "pointer" , fontSize : 13 ,
}}> Opslaan </ button >
</>
)}
</ ModalFooter >
</ ModalBox >
</ Overlay >
);
}
// ── CatKaart ──────────────────────────────────────────────────────────────────
function CatKaart ({ cat }) {
const { locked , T } = useApp ();
const [ showAanpassen , setShowAanpassen ] = useState ( false );
return (
< VCard style = {{ background : T . card , border : `1px solid ${ T . border } ` , borderTop : `3px solid ${ cat . color } ` }}>
{ showAanpassen && < AanpassenPopup cat = { cat } onClose = {() => setShowAanpassen ( false )} />}
< div style = {{ display : "flex" , justifyContent : "space-between" , alignItems : "center" , marginBottom : 10 }}>
< span style = {{ fontWeight : 700 , fontSize : 14 , color : cat . color }}>
{ cat . icon && < span style = {{ marginRight : 6 }}>< AppIcon value = { cat . icon } size = { 16 } /></ span >}
{ cat . naam }
</ span >
{ ! locked && (
< button onClick = {() => setShowAanpassen ( true )} style = {{
padding : "4px 10px" , background : ` ${ cat . color } 22` ,
border : `1px solid ${ cat . color } 44` , borderRadius : 7 ,
color : cat . color , cursor : "pointer" , fontSize : 11 , fontWeight : 700 ,
}}> Aanpassen </ button >
)}
</ div >
{ cat . items . map (( item ) => (
< div key = { item . id } style = {{ display : "flex" , alignItems : "center" , gap : 6 , marginBottom : 8 }}>
< span style = {{ fontSize : 13 , color : T . muted , flex : 1 , minWidth : 0 , wordBreak : "break-word" }}>
{ item . naam }
</ span >
< span style = {{ fontSize : 14 , fontWeight : 700 , color : T . text , whiteSpace : "nowrap" }}>
{ fmt ( item . waarde )}
</ span >
</ div >
))}
< div style = {{
marginTop : 10 , paddingTop : 10 ,
borderTop : `1px solid ${ T . border } ` ,
display : "flex" , justifyContent : "flex-end" ,
}}>
< span style = {{ fontSize : 14 , fontWeight : 700 , color : cat . color }}>
{ fmt ( cat . items . reduce (( a , i ) => a + ( i . waarde || 0 ), 0 ))}
</ span >
</ div >
</ VCard >
);
}
// ── EVVoortgangTabel ──────────────────────────────────────────────────────────
function EVVoortgangTabel () {
const { evRows , setEvRows , cats , locked , darkMode , T } = useApp ();
const empty = () => ({
datum : "" , nettoWaarde : "" , opmerkingen : "" ,
... cats . reduce (( a , c ) => ({ ... a , [ c . id ] : "" }), {}),
});
const [ showModal , setShowModal ] = useState ( false );
const [ input , setInput ] = useState ( empty );
const [ editDatum , setEditDatum ] = useState ( null );
const [ err , setErr ] = useState ( "" );
const is = inputStyle ( T );
const openModal = () => { setInput ( empty ()); setEditDatum ( null ); setErr ( "" ); setShowModal ( true ); };
const openEdit = ( row ) => {
setInput ({ ... row , opmerkingen : row . opmerkingen || "" });
setEditDatum ( row . datum ); setErr ( "" ); setShowModal ( true );
};
const closeModal = () => { setShowModal ( false ); setErr ( "" ); setEditDatum ( null ); };
const addRow = () => {
if ( ! input . datum ) { setErr ( "Datum is verplicht." ); return ; }
const parsedCats = cats . reduce (( a , c ) => ({ ... a , [ c . id ] : parseFloat ( input [ c . id ]) || 0 }), {});
const hasValue = cats . some (( c ) => parsedCats [ c . id ] > 0 );
if ( ! hasValue ) { setErr ( "Vul minimaal één bedrag in." ); return ; }
const row = {
... input ,
... parsedCats ,
nettoWaarde : parseFloat ( input . nettoWaarde ) || 0 ,
};
const updated = [... evRows . filter (( r ) => r . datum !== row . datum ), row ]
. sort (( a , b ) => b . datum . localeCompare ( a . datum ));
setEvRows ( updated ); closeModal ();
};
const removeRow = ( datum ) => setEvRows ( evRows . filter (( r ) => r . datum !== datum ));
const th = { padding : "8px 10px" , fontSize : 11 , color : T . muted , fontWeight : 600 , textAlign : "left" , whiteSpace : "nowrap" , background : T . card , position : "sticky" , top : 0 , zIndex : 1 , borderBottom : `1px solid ${ T . border } ` };
const td = { padding : "7px 10px" , fontSize : 12 , color : T . text , borderBottom : `1px solid ${ T . border } ` };
return (
< div >
{ /* Modal */ }
{ showModal && (
< Overlay onClose = { closeModal }>
< ModalBox maxWidth = { 560 }>
< ModalHeader
icon = "📊" iconBg = { PURPLE }
title = { editDatum ? "Rij bewerken" : "Nieuw geïnvesteerde fiat" }
subtitle = "Vul de waarden in voor dit moment"
subtitleColor = { T . muted }
onClose = { closeModal }
headerBg = { darkMode ? ` ${ PURPLE } 08` : "#f8f5ff" }
T = { T }
/>
< div style = {{ padding : "20px 22px" , overflowY : "auto" , maxHeight : "70vh" }}>
< FieldLabel label = "Datum *" T = { T } />
< input type = "date" value = { input . datum }
onChange = {( e ) => setInput (( s ) => ({ ... s , datum : e . target . value }))}
style = {{ ... is , width : "100%" , marginBottom : 16 }} />
< FieldLabel label = "Bedragen per categorie" T = { T } />
< div style = {{ display : "grid" , gridTemplateColumns : "1fr 1fr" , gap : 8 , marginBottom : 16 }}>
{ cats . map (( c ) => (
< div key = { c . id } style = {{
display : "flex" , alignItems : "center" , gap : 12 ,
background : darkMode ? "rgba(255,255,255,0.03)" : "#f8fafc" ,
border : `1px solid ${ T . border } ` , borderLeft : `3px solid ${ c . color } ` ,
borderRadius : 10 , padding : "8px 12px" ,
}}>
< span style = {{ fontSize : 12 , color : c . color , fontWeight : 700 , minWidth : 90 , flexShrink : 0 }}>{ c . naam }</ span >
< EuroInput
value = { input [ c . id ]}
onChange = {( v ) => setInput (( s ) => ({ ... s , [ c . id ] : v }))}
is = { is } T = { T }
/>
</ div >
))}
</ div >
< FieldLabel label = "Netto Waarde *" T = { T } />
< EuroInput
value = { input . nettoWaarde }
onChange = {( v ) => setInput (( s ) => ({ ... s , nettoWaarde : v }))}
is = { is } T = { T }
/>
< div style = {{ marginBottom : 16 }} />
< FieldLabel label = "Opmerkingen" T = { T } />
< input type = "text" value = { input . opmerkingen }
onChange = {( e ) => setInput (( s ) => ({ ... s , opmerkingen : e . target . value }))}
placeholder = "Optionele notitie..." style = {{ ... is , width : "100%" }} />
{ err && < ErrMsg msg = { err } />}
</ div >
< ModalFooter T = { T }>
< button onClick = { closeModal } style = { cancelBtn ( T )}> Annuleren </ button >
< button onClick = { addRow } style = { primaryBtn }>{ editDatum ? "Opslaan" : "Toevoegen" }</ button >
</ ModalFooter >
</ ModalBox >
</ Overlay >
)}
< div style = {{ display : "flex" , alignItems : "center" , justifyContent : "space-between" , marginBottom : 16 }}>
< div style = {{ fontWeight : 700 , fontSize : 14 , color : T . text }}> Geïnvesteerde Fiat </ div >
{ ! locked && (
< button onClick = { openModal } style = { primaryBtn }> + Toevoegen </ button >
)}
</ div >
{ evRows . length > 0 ? (
< div style = {{ overflowX : "auto" }}>
< div style = {{ maxHeight : 340 , overflowY : "auto" }}>
< table style = {{ width : "100%" , borderCollapse : "collapse" , minWidth : 600 }}>
< thead >
< tr >
< th style = { th }> Datum </ th >
{ cats . map (( c ) => < th key = { c . id } style = {{ ... th , color : c . color }}>{ c . naam }</ th >)}
< th style = { th }> Netto Waarde </ th >
< th style = { th }> Verschil </ th >
< th style = { th }> Opmerkingen </ th >
{ ! locked && < th style = { th }></ th >}
</ tr >
</ thead >
< tbody >
{ evRows . map (( row , i ) => {
const vorigeNW = i < evRows . length - 1 ? ( evRows [ i + 1 ]. nettoWaarde || 0 ) : null ;
const verschil = vorigeNW !== null ? ( row . nettoWaarde || 0 ) - vorigeNW : null ;
const vColor = verschil === null ? T . muted : verschil > 0 ? GREEN : verschil < 0 ? RED : T . muted ;
const vLabel = verschil === null ? "—" : ` ${ verschil >= 0 ? "+" : "" }${ fmt ( verschil ) } ` ;
return (
< tr key = { i }>
< td style = { td }>{ fmtDatum ( row . datum )}</ td >
{ cats . map (( c ) => < td key = { c . id } style = {{ ... td , color : c . color }}>{ fmt ( row [ c . id ] || 0 )}</ td >)}
< td style = {{ ... td , fontWeight : 700 , color : PURPLE_LIGHT }}>{ fmt ( row . nettoWaarde || 0 )}</ td >
< td style = {{ ... td , fontWeight : 700 , color : vColor }}>{ vLabel }</ td >
< td style = {{ ... td , color : T . muted }}>{ row . opmerkingen || "—" }</ td >
{ ! locked && (
< td style = { td }>
< div style = {{ display : "flex" , gap : 6 }}>
< button onClick = {() => openEdit ( row )} style = { editBtn }> ✏️ </ button >
< button onClick = {() => removeRow ( row . datum )} style = { deleteSmBtn }> × </ button >
</ div >
</ td >
)}
</ tr >
);
})}
</ tbody >
</ table >
</ div >
</ div >
) : (
< EmptyState
icon = "📊" text = "Nog geen datapunten"
hint = "Klik hier om je eerste datapunt toe te voegen"
locked = { locked } onClick = { openModal } T = { T }
/>
)}
</ div >
);
}
// ── DoelProgressie ────────────────────────────────────────────────────────────
function DoelProgressie ({ ev , data , setData , totVerdeling , locked , T }) {
const [ editing , setEditing ] = useState ( false );
const [ val , setVal ] = useState ( "" );
const doel = ev . doel || 1 ;
const progressPct = Math . min ( totVerdeling / doel , 1 );
const openEdit = () => { setVal ( String ( doel )); setEditing ( true ); };
const save = () => {
const n = parseFloat ( val );
if ( n > 0 ) setData ({ ... data , eigenVermogen : { ... ev , doel : n } });
setEditing ( false );
};
return (
< VCard style = {{ background : T . card , border : `1px solid ${ T . border } ` , marginBottom : 20 }}>
< div style = {{ display : "flex" , justifyContent : "space-between" , alignItems : "center" , marginBottom : 10 }}>
< div style = {{ display : "flex" , alignItems : "center" , gap : 8 }}>
< span style = {{ fontSize : 14 }}> 🎯 </ span >
< span style = {{ fontWeight : 600 , fontSize : 14 }}> Gewenst doel EV </ span >
</ div >
< div style = {{ display : "flex" , alignItems : "center" , gap : 12 }}>
< span style = {{ fontWeight : 700 , fontSize : 15 , color : PURPLE_LIGHT }}>{ fmt ( totVerdeling )}</ span >
{ editing ? (
< div style = {{ display : "flex" , alignItems : "center" , gap : 6 }}>
< span style = {{ color : T . muted , fontSize : 13 }}> / </ span >
< input
autoFocus type = "number" value = { val }
onChange = {( e ) => setVal ( e . target . value )}
onKeyDown = {( e ) => { if ( e . key === "Enter" ) save (); if ( e . key === "Escape" ) setEditing ( false ); }}
style = {{
width : 130 , padding : "4px 8px" , borderRadius : 8 , fontSize : 13 ,
background : T . inputBg , border : `1px solid ${ PURPLE } ` , color : T . text , outline : "none" ,
}}
/>
< button onClick = { save } style = {{ padding : "4px 10px" , background : PURPLE , border : "none" , borderRadius : 7 , color : "#fff" , cursor : "pointer" , fontSize : 12 , fontWeight : 700 }}> OK </ button >
</ div >
) : (
<>
< span style = {{ color : T . muted , fontSize : 13 }}> / { fmt ( doel )}</ span >
{ ! locked && (
< button onClick = { openEdit } style = {{ background : "transparent" , border : "none" , cursor : "pointer" , color : T . muted , fontSize : 14 , padding : 2 }}> ✏️ </ button >
)}
</>
)}
< span style = {{ color : PURPLE_LIGHT , fontWeight : 700 , fontSize : 13 }}>{ Math . round ( progressPct * 100 )} % </ span >
</ div >
</ div >
< div style = {{ height : 12 , background : "rgba(255,255,255,0.07)" , borderRadius : 99 , overflow : "hidden" }}>
< div style = {{
height : "100%" , width : ` ${ progressPct * 100 } %` ,
background : `linear-gradient(90deg, ${ PURPLE } ,#a855f7)` ,
borderRadius : 99 , transition : "width 0.7s ease" ,
}} />
</ div >
</ VCard >
);
}
// ── EigenVermogenTab (pagina) ─────────────────────────────────────────────────
export default function EigenVermogenTab () {
const { cats , catTotals , ev , data , setData , locked , darkMode , T , evRows } = useApp ();
const [ showAddCat , setShowAddCat ] = useState ( false );
const [ newNaam , setNewNaam ] = useState ( "" );
const [ newColor , setNewColor ] = useState ( CAT_COLORS [ 0 ]);
const totVerdeling = catTotals . reduce (( a , c ) => a + c . tot , 0 );
const sumCat = ( id ) => evRows . reduce (( a , r ) => a + ( r [ id ] || 0 ), 0 );
const totGeinv = cats . reduce (( a , c ) => a + sumCat ( c . id ), 0 );
const iStyle2 = inputStyle ( T );
const addCat = () => {
if ( ! newNaam . trim ()) return ;
setData ({
... data ,
eigenVermogen : {
... ev ,
categories : [... cats , {
id : `c ${ Date . now () } ` , naam : newNaam , color : newColor ,
items : [{ id : `i ${ Date . now () } ` , naam : "Item 1" , waarde : 0 }],
}],
},
});
setNewNaam ( "" ); setShowAddCat ( false );
};
const summaryBlocks = [
{
title : "Verdeling Eigen Vermogen" , accent : GREEN ,
rows : catTotals . map (( c ) => ({ label : c . naam , value : fmt ( c . tot ), color : c . color })),
total : fmt ( totVerdeling ), totalColor : GREEN ,
},
{
title : "Geïnvesteerde Fiat" , accent : PURPLE_LIGHT ,
rows : cats . map (( c ) => ({ label : c . naam , value : evRows . length > 0 ? fmt ( sumCat ( c . id )) : "—" , color : c . color })),
total : evRows . length > 0 ? fmt ( totGeinv ) : "—" , totalColor : PURPLE_LIGHT ,
},
{
title : "Rendement" , accent : "#f59e0b" ,
rows : cats . map (( c ) => {
const ev2 = catTotals . find (( ct ) => ct . id === c . id ) ? . tot ?? 0 ;
const g = sumCat ( c . id );
const r = g > 0 ? ( ev2 - g ) / g : null ;
return { label : c . naam , value : r === null ? "—" : ` ${ ( r * 100 ). toFixed ( 1 ) } %` , color : r === null ? T . muted : r >= 0 ? GREEN : RED };
}),
total : (() => {
const et = totVerdeling ;
const gt = totGeinv ;
return gt > 0 ? ` ${ ((( et - gt ) / gt ) * 100 ). toFixed ( 1 ) } %` : "—" ;
})(),
totalColor : (() => {
return totGeinv > 0 ? ( totVerdeling >= totGeinv ? GREEN : RED ) : T . muted ;
})(),
},
];
return (
< div >
< h1 style = {{ fontSize : 20 , fontWeight : 800 , margin : "0 0 20px 0" }}> Eigen Vermogen </ h1 >
{ /* Doel progressie */ }
< DoelProgressie ev = { ev } data = { data } setData = { setData } totVerdeling = { totVerdeling } locked = { locked } T = { T } />
{ /* Summary blokken */ }
< div style = {{ display : "flex" , gap : 16 , marginBottom : 20 , flexWrap : "wrap" }}>
{ summaryBlocks . map (( block ) => (
< div key = { block . title } style = {{
background : T . card , border : `1px solid ${ T . border } ` ,
borderTop : `3px solid ${ block . accent } ` , borderRadius : 12 ,
padding : "16px 18px" , flex : 1 , minWidth : 200 ,
}}>
< div style = {{
fontWeight : 700 , fontSize : 12 , letterSpacing : "0.1em" ,
textTransform : "uppercase" , color : block . accent , marginBottom : 12 ,
}}>{ block . title }</ div >
{ block . rows . map (({ label , value , color }) => (
< div key = { label } style = {{
display : "flex" , justifyContent : "space-between" ,
padding : "6px 0" , borderBottom : `1px solid ${ T . border } ` ,
}}>
< span style = {{ fontSize : 13 , color : T . muted }}>{ label }</ span >
< span style = {{ fontSize : 13 , fontWeight : 600 , color }}>{ value }</ span >
</ div >
))}
< div style = {{
display : "flex" , justifyContent : "space-between" ,
alignItems : "center" , marginTop : 10 , paddingTop : 8 ,
}}>
< span style = {{ fontSize : 12 , fontWeight : 700 , color : T . text }}> Totaal </ span >
< span style = {{ fontSize : 15 , fontWeight : 800 , color : block . totalColor }}>{ block . total }</ span >
</ div >
</ div >
))}
{ totVerdeling > 0 && (
< div style = {{
background : T . card , border : `1px solid ${ T . border } ` ,
borderRadius : 16 , padding : "20px" ,
flex : 1 , minWidth : 200 ,
display : "flex" , flexDirection : "column" , alignItems : "center" ,
}}>
2026-09-04 09:19:23 +02:00
< DonutChart segments = { catTotals . map (( c ) => ({ val : c . tot , color : c . color , naam : c . naam }))} total = { totVerdeling } size = { 200 } label = "Verdeling" centerVal = { fmt ( totVerdeling )} />
2026-09-04 09:19:14 +02:00
< div style = {{ display : "flex" , flexWrap : "wrap" , gap : "6px 14px" , marginTop : 12 , justifyContent : "center" }}>
{ catTotals . map (( c ) => (
< div key = { c . id } style = {{ display : "flex" , alignItems : "center" , gap : 5 }}>
< div style = {{ width : 7 , height : 7 , borderRadius : "50%" , background : c . color }} />
< span style = {{ fontSize : 11 , color : T . subtext }}>{ c . naam }</ span >
</ div >
))}
</ div >
</ div >
)}
</ div >
{ /* Nieuwe categorie */ }
{ ! locked && (
< div style = {{ marginBottom : 16 }}>
< button onClick = {() => setShowAddCat ( true )} style = {{
padding : "10px 20px" , background : ` ${ PURPLE } 22` ,
border : `1px solid ${ PURPLE } 44` , borderRadius : 10 ,
color : PURPLE_LIGHT , cursor : "pointer" , fontWeight : 700 , fontSize : 13 ,
}}> + Nieuwe categorie </ button >
</ div >
)}
{ showAddCat && (
< Overlay onClose = {() => setShowAddCat ( false )}>
< ModalBox maxWidth = { 400 }>
< ModalHeader icon = "➕ " iconBg = { PURPLE } title = "Nieuwe categorie" subtitle = "Voeg een EV-categorie toe" subtitleColor = { PURPLE_LIGHT } onClose = {() => setShowAddCat ( false )} headerBg = { darkMode ? ` ${ PURPLE } 08` : "#f8f5ff" } T = { T } />
< div style = {{ padding : "20px 22px" }}>
< div style = {{ fontSize : 11 , color : T . muted , fontWeight : 700 , textTransform : "uppercase" , marginBottom : 6 }}> Naam * </ div >
< input autoFocus placeholder = "Bijv. Aandelen" value = { newNaam }
onChange = {( e ) => setNewNaam ( e . target . value )}
onKeyDown = {( e ) => e . key === "Enter" && addCat ()}
style = {{ ... iStyle2 , marginBottom : 16 , width : "100%" , boxSizing : "border-box" }} />
< div style = {{ fontSize : 11 , color : T . muted , fontWeight : 700 , textTransform : "uppercase" , marginBottom : 8 }}> Kleur </ div >
< div style = {{ display : "flex" , gap : 8 , flexWrap : "wrap" }}>
{ CAT_COLORS . map (( c ) => (
< button key = { c } onClick = {() => setNewColor ( c )} style = {{
width : 28 , height : 28 , borderRadius : "50%" , background : c ,
border : `3px solid ${ newColor === c ? "#fff" : "transparent" } ` ,
cursor : "pointer" , outline : newColor === c ? `2px solid ${ c } ` : "none" , outlineOffset : 1 ,
}} />
))}
</ div >
</ div >
< ModalFooter T = { T }>
< button onClick = {() => setShowAddCat ( false )} style = {{ flex : 1 , padding : "10px" , background : "transparent" , border : `1px solid ${ T . border } ` , borderRadius : 10 , color : T . muted , cursor : "pointer" , fontWeight : 600 , fontSize : 13 }}> Annuleren </ button >
< button onClick = { addCat } style = {{ flex : 2 , padding : "10px" , background : `linear-gradient(135deg, ${ PURPLE } , #a855f7)` , border : "none" , borderRadius : 10 , color : "#fff" , fontWeight : 700 , cursor : "pointer" , fontSize : 13 }}> Aanmaken </ button >
</ ModalFooter >
</ ModalBox >
</ Overlay >
)}
{ /* Categorie kaarten */ }
< div style = {{
display : "grid" ,
gridTemplateColumns : "repeat(auto-fill, minmax(300px, 1fr))" ,
gap : 16 , marginBottom : 20 ,
}}>
{ cats . map (( cat ) => < CatKaart key = { cat . id } cat = { cat } />)}
</ div >
{ /* Voortgang tabel */ }
< VCard style = {{ background : T . card , border : `1px solid ${ T . border } ` }}>
< EVVoortgangTabel />
</ VCard >
</ div >
);
}
// ── Gedeelde kleine helpers ────────────────────────────────────────────────────
function inputStyle ( T ) {
return {
background : T . inputBg , border : `1px solid ${ T . inputBorder } ` ,
borderRadius : 8 , color : T . text , padding : "8px 10px" ,
fontSize : 13 , outline : "none" , boxSizing : "border-box" ,
};
}
function FieldLabel ({ label , T }) {
return (
< div style = {{
fontSize : 11 , color : T . muted , fontWeight : 700 ,
textTransform : "uppercase" , letterSpacing : "0.05em" , marginBottom : 8 ,
}}>{ label }</ div >
);
}
function EuroInput ({ value , onChange , is , T }) {
return (
< div style = {{ position : "relative" , width : 110 , flexShrink : 0 }}>
< span style = {{
position : "absolute" , left : 8 , top : "50%" , transform : "translateY(-50%)" ,
fontSize : 12 , color : T . muted , pointerEvents : "none" ,
}}> € </ span >
< input type = "number" value = { value } onChange = {( e ) => onChange ( e . target . value )}
placeholder = "0" style = {{ ... is , paddingLeft : 22 , width : "100%" }} />
</ div >
);
}
function ErrMsg ({ msg }) {
return (
< div style = {{
color : RED , fontSize : 12 , marginTop : 8 ,
padding : "6px 10px" , background : "rgba(239,68,68,0.1)" ,
borderRadius : 8 , border : "1px solid rgba(239,68,68,0.25)" ,
}}>{ msg }</ div >
);
}
const primaryBtn = { padding : "10px 20px" , background : PURPLE , border : "none" , borderRadius : 8 , color : "#fff" , fontWeight : 700 , cursor : "pointer" , fontSize : 13 };
const editBtn = { padding : "2px 10px" , background : ` ${ PURPLE } 22` , border : `1px solid ${ PURPLE } 44` , borderRadius : 6 , color : PURPLE_LIGHT , cursor : "pointer" , fontSize : 12 , fontWeight : 600 };
const deleteSmBtn = { padding : "2px 8px" , background : "rgba(239,68,68,0.1)" , border : "1px solid rgba(239,68,68,0.25)" , borderRadius : 6 , color : RED , cursor : "pointer" , fontSize : 12 };
const trashBtn = { padding : "10px 14px" , background : "rgba(239,68,68,0.08)" , border : "1px solid rgba(239,68,68,0.25)" , borderRadius : 10 , color : RED , cursor : "pointer" , fontWeight : 600 , fontSize : 13 };
const addBtn = { padding : "6px 14px" , background : ` ${ PURPLE } 22` , border : `1px solid ${ PURPLE } 44` , borderRadius : 8 , color : PURPLE_LIGHT , cursor : "pointer" , fontSize : 12 , fontWeight : 700 , flexShrink : 0 };
const cancelBtn = ( T ) => ({ padding : "10px" , background : "transparent" , border : `1px solid ${ T . border } ` , borderRadius : 10 , color : T . muted , cursor : "pointer" , fontWeight : 600 , fontSize : 13 });