bugfixes
This commit is contained in:
@@ -26,3 +26,37 @@ input[type="number"]::-webkit-inner-spin-button {
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Sleepvlak op de rechterrand van een tabelkop — zie hooks/useKolomBreedtes.js.
|
||||
Blijft binnen de cel (geen negatieve offset), anders duwt de laatste kolom
|
||||
een horizontale scrollbar tevoorschijn. */
|
||||
.kolom-grip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/* Het streepje erft de kleur van de kop, dus per categorie de accentkleur. */
|
||||
.kolom-grip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
right: 2px;
|
||||
width: 1px;
|
||||
height: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.kolom-grip:hover::after,
|
||||
.kolom-grip:active::after {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 2px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { loadFromStorage, saveToStorage } from "../utils/storage.js";
|
||||
|
||||
// Smaller dan dit wordt een kolom onleesbaar; de greep blijft wel pakbaar.
|
||||
const MIN_BREEDTE = 60;
|
||||
|
||||
/**
|
||||
* Versleepbare kolombreedtes voor een tabel.
|
||||
*
|
||||
* `kolommen` is een array van { id, breedte } in weergave-volgorde, waarbij
|
||||
* `breedte` de standaard in pixels is. De aanpassingen van de gebruiker gaan
|
||||
* per tabel naar localStorage, zodat ze een tabwissel of herlaad overleven.
|
||||
* Kolommen die later verschijnen (een categorie die de gebruiker toevoegt)
|
||||
* krijgen hun standaardbreedte; opgeslagen breedtes van verdwenen kolommen
|
||||
* blijven ongebruikt staan en storen niets.
|
||||
*
|
||||
* De tabel moet `tableLayout: "fixed"` krijgen met een <colgroup>, en als
|
||||
* `width` de teruggegeven `totaal`. Met `width: "100%"` verdeelt de browser de
|
||||
* ruimte namelijk proportioneel en levert slepen niet exact de gesleepte
|
||||
* pixels op.
|
||||
*
|
||||
* Geeft terug:
|
||||
* breedtes px per kolom-id, standaard plus aanpassing
|
||||
* totaal som van de breedtes, voor de `width` van de tabel
|
||||
* grip props voor het sleepvlak in de <th> van die kolom
|
||||
* herstel alle kolommen terug naar hun standaardbreedte
|
||||
*/
|
||||
export function useKolomBreedtes(opslagKey, kolommen) {
|
||||
const [aangepast, setAangepast] = useState(() => loadFromStorage(opslagKey) ?? {});
|
||||
// Spiegelt `aangepast` zodat het slepen en het opslaan bij het loslaten niet
|
||||
// van de state-updater afhangen (die mag geen bijwerkingen hebben).
|
||||
const laatste = useRef(aangepast);
|
||||
const sleep = useRef(null);
|
||||
|
||||
const breedtes = {};
|
||||
for (const k of kolommen) breedtes[k.id] = aangepast[k.id] ?? k.breedte;
|
||||
const totaal = kolommen.reduce((som, k) => som + breedtes[k.id], 0);
|
||||
|
||||
const zet = (volgende) => { laatste.current = volgende; setAangepast(volgende); };
|
||||
|
||||
const grip = (id) => ({
|
||||
className: "kolom-grip",
|
||||
role: "separator",
|
||||
title: "Sleep om de kolom breder of smaller te maken — dubbelklik voor de standaardbreedte",
|
||||
onPointerDown: (e) => {
|
||||
// Pointer capture: het slepen loopt door buiten de greep, ook boven de
|
||||
// rijen of naast de tabel, zonder losse listeners op window.
|
||||
e.preventDefault();
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
sleep.current = { id, startX: e.clientX, startBreedte: breedtes[id] };
|
||||
},
|
||||
onPointerMove: (e) => {
|
||||
if (!sleep.current) return;
|
||||
const { id: kolomId, startX, startBreedte } = sleep.current;
|
||||
const nieuw = Math.max(MIN_BREEDTE, Math.round(startBreedte + e.clientX - startX));
|
||||
if (laatste.current[kolomId] === nieuw) return;
|
||||
zet({ ...laatste.current, [kolomId]: nieuw });
|
||||
},
|
||||
onPointerUp: () => {
|
||||
if (!sleep.current) return;
|
||||
sleep.current = null;
|
||||
// Pas bij het loslaten opslaan; tijdens het slepen zou elke pixel een
|
||||
// localStorage-write kosten.
|
||||
saveToStorage(opslagKey, laatste.current);
|
||||
},
|
||||
onDoubleClick: () => {
|
||||
// Zonder destructuring-omit: die laat een ongebruikte variabele achter.
|
||||
const rest = { ...laatste.current };
|
||||
delete rest[id];
|
||||
zet(rest);
|
||||
saveToStorage(opslagKey, rest);
|
||||
},
|
||||
});
|
||||
|
||||
const herstel = () => { zet({}); saveToStorage(opslagKey, {}); };
|
||||
|
||||
return { breedtes, totaal, grip, herstel };
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useApp } from "../context/AppContext.jsx";
|
||||
import { fmt, fmtDatum } from "../utils/format.js";
|
||||
import { useKolomBreedtes } from "../hooks/useKolomBreedtes.js";
|
||||
import { PURPLE, PURPLE_LIGHT, GREEN, RED, CAT_COLORS } from "../constants/index.js";
|
||||
import {
|
||||
VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter,
|
||||
@@ -250,8 +251,22 @@ function EVVoortgangTabel() {
|
||||
|
||||
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}` };
|
||||
// Kolommen in weergave-volgorde, met hun standaardbreedte. De categorieën
|
||||
// zijn dynamisch, dus de lijst wordt per render opgebouwd.
|
||||
const kolommen = [
|
||||
{ id: "datum", breedte: 100 },
|
||||
...cats.map((c) => ({ id: c.id, breedte: 100 })),
|
||||
{ id: "nettoWaarde", breedte: 115 },
|
||||
{ id: "verschil", breedte: 105 },
|
||||
{ id: "opmerkingen", breedte: 150 },
|
||||
...(!locked ? [{ id: "acties", breedte: 75 }] : []),
|
||||
];
|
||||
const { breedtes, totaal, grip } = useKolomBreedtes("ev_kolom_breedtes", kolommen);
|
||||
|
||||
// overflow/ellipsis horen bij tableLayout "fixed": de inhoud rekt de kolom
|
||||
// niet meer op, dus zonder afkappen loopt lange tekst de buurcel binnen.
|
||||
const th = { padding: "8px 10px", fontSize: 11, color: T.muted, fontWeight: 600, textAlign: "left", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", background: T.popupBg, position: "sticky", top: 0, zIndex: 1, borderBottom: `1px solid ${T.border}` };
|
||||
const td = { padding: "7px 10px", fontSize: 12, color: T.text, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", borderBottom: `1px solid ${T.border}` };
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -326,15 +341,22 @@ function EVVoortgangTabel() {
|
||||
{evRows.length > 0 ? (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<div style={{ maxHeight: 340, overflowY: "auto" }}>
|
||||
<table style={{ width: "100%", borderCollapse: "collapse", minWidth: 600 }}>
|
||||
<table style={{ width: totaal, borderCollapse: "collapse", tableLayout: "fixed" }}>
|
||||
<colgroup>
|
||||
{kolommen.map((k) => <col key={k.id} style={{ width: breedtes[k.id] }} />)}
|
||||
</colgroup>
|
||||
<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>}
|
||||
<th style={th}>Datum<span {...grip("datum")} /></th>
|
||||
{cats.map((c) => (
|
||||
<th key={c.id} style={{ ...th, color: c.color }}>
|
||||
{c.naam}<span {...grip(c.id)} />
|
||||
</th>
|
||||
))}
|
||||
<th style={th}>Netto Waarde<span {...grip("nettoWaarde")} /></th>
|
||||
<th style={th}>Verschil<span {...grip("verschil")} /></th>
|
||||
<th style={th}>Opmerkingen<span {...grip("opmerkingen")} /></th>
|
||||
{!locked && <th style={th}><span {...grip("acties")} /></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -349,7 +371,8 @@ function EVVoortgangTabel() {
|
||||
{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>
|
||||
{/* title: bij een smalle kolom is de tekst afgekapt */}
|
||||
<td style={{ ...td, color: T.muted }} title={row.opmerkingen || ""}>{row.opmerkingen || "—"}</td>
|
||||
{!locked && (
|
||||
<td style={td}>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useApp } from "../context/AppContext.jsx";
|
||||
import { fmt, fmtDatum } from "../utils/format.js";
|
||||
import { useKolomBreedtes } from "../hooks/useKolomBreedtes.js";
|
||||
import { PURPLE, PURPLE_LIGHT, RED, GREEN } from "../constants/index.js";
|
||||
import { VCard, DonutChart, Overlay, ModalBox, ModalHeader, ModalFooter } from "../components/ui/index.jsx";
|
||||
|
||||
@@ -261,8 +262,21 @@ export default function VoortgangTab() {
|
||||
setData({ ...data, schuld: { ...schuld, schuldHistory: schuldHistory.filter((s) => s.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}` };
|
||||
// Kolommen in weergave-volgorde, met hun standaardbreedte.
|
||||
const kolommen = [
|
||||
{ id: "datum", breedte: 120 },
|
||||
{ id: "ev", breedte: 175 },
|
||||
{ id: "schuld", breedte: 160 },
|
||||
{ id: "netto", breedte: 175 },
|
||||
{ id: "opmerkingen", breedte: 340 },
|
||||
...(!locked ? [{ id: "acties", breedte: 90 }] : []),
|
||||
];
|
||||
const { breedtes, totaal, grip } = useKolomBreedtes("voortgang_kolom_breedtes", kolommen);
|
||||
|
||||
// overflow/ellipsis horen bij tableLayout "fixed": de inhoud rekt de kolom
|
||||
// niet meer op, dus zonder afkappen loopt lange tekst de buurcel binnen.
|
||||
const th = { padding: "8px 10px", fontSize: 11, color: T.muted, fontWeight: 600, textAlign: "left", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", background: T.popupBg, position: "sticky", top: 0, zIndex: 1, borderBottom: `1px solid ${T.border}` };
|
||||
const td = { padding: "7px 10px", fontSize: 12, color: T.text, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", borderBottom: `1px solid ${T.border}` };
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -396,15 +410,18 @@ export default function VoortgangTab() {
|
||||
)}
|
||||
{voortgangRows.length > 0 ? (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<table style={{ width: "100%", borderCollapse: "collapse", minWidth: 560 }}>
|
||||
<table style={{ width: totaal, borderCollapse: "collapse", tableLayout: "fixed" }}>
|
||||
<colgroup>
|
||||
{kolommen.map((k) => <col key={k.id} style={{ width: breedtes[k.id] }} />)}
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={th}>Datum</th>
|
||||
<th style={{ ...th, color: GREEN }}>Eigen Vermogen</th>
|
||||
<th style={{ ...th, color: RED }}>Schuld</th>
|
||||
<th style={{ ...th, color: PURPLE_LIGHT }}>Nettowaarde</th>
|
||||
<th style={th}>Opmerkingen</th>
|
||||
{!locked && <th style={th}></th>}
|
||||
<th style={th}>Datum<span {...grip("datum")} /></th>
|
||||
<th style={{ ...th, color: GREEN }}>Eigen Vermogen<span {...grip("ev")} /></th>
|
||||
<th style={{ ...th, color: RED }}>Schuld<span {...grip("schuld")} /></th>
|
||||
<th style={{ ...th, color: PURPLE_LIGHT }}>Nettowaarde<span {...grip("netto")} /></th>
|
||||
<th style={th}>Opmerkingen<span {...grip("opmerkingen")} /></th>
|
||||
{!locked && <th style={th}><span {...grip("acties")} /></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -414,7 +431,8 @@ export default function VoortgangTab() {
|
||||
<td style={{ ...td, color: GREEN, fontWeight: 600 }}>{row.ev !== null ? fmt(row.ev) : "—"}</td>
|
||||
<td style={{ ...td, color: RED, fontWeight: 600 }}>{row.schuld !== null ? fmt(row.schuld) : "—"}</td>
|
||||
<td style={{ ...td, fontWeight: 700, color: row.netto !== null ? (row.netto >= 0 ? GREEN : RED) : T.muted }}>{row.netto !== null ? fmt(row.netto) : "—"}</td>
|
||||
<td style={{ ...td, color: T.muted }}>{row.opmerkingen || "—"}</td>
|
||||
{/* title: bij een smalle kolom is de tekst afgekapt */}
|
||||
<td style={{ ...td, color: T.muted }} title={row.opmerkingen || ""}>{row.opmerkingen || "—"}</td>
|
||||
{!locked && (
|
||||
<td style={{ ...td, whiteSpace: "nowrap" }}>
|
||||
<button onClick={() => setEditRow(row)} style={{ padding: "2px 8px", background: `${PURPLE}22`, border: `1px solid ${PURPLE}44`, borderRadius: 6, color: PURPLE_LIGHT, cursor: "pointer", fontSize: 12, marginRight: 4 }}>✏️</button>
|
||||
|
||||
Reference in New Issue
Block a user