diff --git a/.env.example b/.env.example index 94375c6..86357f3 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,9 @@ CORS_ORIGIN=http://localhost:3400 # Update-notificaties (leeg = melding uit) UPDATE_CHECK_URL=https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest -# Nodig zolang de Gitea-repo niet publiek leesbaar is; scope read:repository. -# Zonder token geeft releases/latest 404 en blijft de update-melding leeg. +# De Gitea-organisatie is publiek, dus de check werkt anoniem — laat dit staan. +# Alleen nodig als de organisatie weer privé wordt; scope read:repository. +# Dan geeft releases/latest zonder token 404 en blijft de melding leeg. +# Let op: een ongeldig token geeft 401, ook op een publieke repo. Bij twijfel +# de regel uitgecommentarieerd laten in plaats van een token te gokken. # UPDATE_CHECK_TOKEN= diff --git a/BEHEER.md b/BEHEER.md index fb30e79..ceaea8e 100644 --- a/BEHEER.md +++ b/BEHEER.md @@ -32,6 +32,25 @@ Vite proxyt `/api` naar `localhost:3001` binnen dezelfde container. > ⚠️ Alleen `.\dev.ps1 down` wist de development-database (`docker compose down -v`). > `.\dev.ps1 up` doet een gewone `down` en laat het volume staan. +### Welke versie de dev-app toont + +De versie in "Over VibeFinance" komt uit `VITE_APP_VERSION`. `dev.ps1` leest die +uit de **root** `package.json`, zet hem in de omgeving en synct hem naar +`frontend/package.json`. In de dev-container is het een *runtime*-variabele +(`docker-compose.dev.yml`, sectie `environment`) en geen build-arg: Vite bakt +`__APP_VERSION__` in bij het **starten** van de dev-server, niet bij het bouwen +van het image. Is hij leeg, dan valt `vite.config.js` terug op +`frontend/package.json`. + +Ziet de app een oude versie, dan draait de dev-server nog van vóór de bump — +`.\dev.ps1 up` hercreëert de container en start Vite opnieuw. Start je de stack +handmatig met `docker compose -f docker-compose.dev.yml up`, dan staat +`VITE_APP_VERSION` niet in je shell en zie je de versie uit +`frontend/package.json`. + +> Een versie met een suffix (`1.0.1-dev`) onderdrukt de update-melding +> permanent — zie [Update-notificaties](#update-notificaties). + --- ## Testen @@ -77,27 +96,35 @@ docker compose -f docker-compose.dev.yml exec -w /app/frontend app npm run lint ## Tokens +> **De `Vibe`-organisatie staat op public.** Alles wat alleen *leest* heeft +> daardoor geen token meer nodig: `git clone`, `docker compose pull` en de +> update-check van de app werken anoniem. Het host-token hieronder is dus +> optioneel geworden. *Schrijven* blijft geauthenticeerd — een publieke +> organisatie verandert daar niets aan. + Twee tokens, gesplitst op machine en op read/write. De grens die iets betekent ligt tussen werkstation en server, niet tussen push en release: op één machine kan wie het ene token kan lezen, het andere ook. -| Token | Machine | Scopes | Gebruikt voor | -|---|---|---|---| -| `vibefinance-workstation` | dit werkstation | `write:package`, `write:repository` | `docker push` (`release.ps1`, `dev.ps1 push`) en het aanmaken van de Gitea-release | -| `vibefinance-host` | de Docker-host | `read:package`, `read:repository` | `docker compose pull` en de update-check van de app | +| Token | Machine | Scopes | Gebruikt voor | Nog nodig? | +|---|---|---|---|---| +| `vibefinance-workstation` | dit werkstation | `write:package`, `write:repository` | `docker push` (`release.ps1`, `dev.ps1 push`) en het aanmaken van de Gitea-release | **Ja** | +| `vibefinance-host` | de Docker-host | `read:package`, `read:repository` | `docker compose pull` en de update-check van de app | Nee, zolang de organisatie public is | Aanmaken: Gitea → Settings → Applications → Generate New Token. Doe dat onder een -account dat lid is van de `Vibe`-organisatie — die is privé, dus een token van -een buitenstaander ziet de repo niet. +account dat lid is van de `Vibe`-organisatie: voor schrijven volstaat +organisatie-lidmaatschap niet automatisch, het account moet ook push-rechten op de +repo hebben. **Waar ze terechtkomen** | | Werkstation | Docker-host | |---|---|---| -| Registry | `docker login` (credential store), of `REGISTRY_USER` + `REGISTRY_TOKEN` in `.env.release` | `docker login` als de gebruiker die deployt | -| Gitea-API | `GITEA_TOKEN` in `.env.release` | `UPDATE_CHECK_TOKEN` in `.env` | +| Registry | `docker login` (credential store), of `REGISTRY_USER` + `REGISTRY_TOKEN` in `.env.release` | niets nodig voor een pull; anders `docker login` als de gebruiker die deployt | +| Gitea-API | `GITEA_TOKEN` in `.env.release` | niets nodig; optioneel `UPDATE_CHECK_TOKEN` in `.env` | -**Waarom het host-token read-only blijft.** Dat staat in platte tekst in `.env` op +**Waarom een host-token read-only blijft** — voor het geval de organisatie weer +privé gaat en je er alsnog één neerzet. Dat staat in platte tekst in `.env` op een server die de app op poort 3400 publiceert. Met schrijfrechten kan wie bij dat bestand komt — via de container, een shell op die host, of een backup — een eigen image als `latest` naar de registry pushen, en die host pullt `latest` zelf. @@ -109,7 +136,7 @@ Read-only houdt je eigen deploy-pad uit de aanvalsketen. |---|---| | 401 | Token ongeldig of verlopen. Op de registry: helemaal niet ingelogd. | | 403 | Het token mag de repo wél zien, maar mist de **scope** voor dit endpoint. Gitea noemt de vereiste scope in de response-body. | -| 404 | Het token mag de repo niet zien. Gitea geeft geen 403, om niet te verklappen dat iets bestaat: verkeerd account, of geen toegang tot de organisatie. | +| 404 | Het token mag de repo niet zien. Gitea geeft geen 403, om niet te verklappen dat iets bestaat: verkeerd account, of geen toegang tot de organisatie. Nu de organisatie public is, wijst een 404 op een leesactie eerder op een verkeerde URL dan op rechten. | Scopes van een bestaand token zijn in Gitea niet aan te passen. Bij een 403 maak je dus een nieuw token aan; bijwerken kan niet. @@ -126,9 +153,21 @@ Vanuit de app-container op de host valt de body van een fout anders weg — busybox-`wget` gooit die bij een foutstatus weg, en Node zit in het image: ```bash -docker exec vibefinance_app node -e 'fetch(process.env.UPDATE_CHECK_URL,{headers:{Accept:"application/json",Authorization:`token ${process.env.UPDATE_CHECK_TOKEN}`}}).then(async r=>console.log(r.status, (await r.text()).slice(0,300)))' +# Anoniem, zoals de app het doet zonder UPDATE_CHECK_TOKEN +docker exec vibefinance_app node -e 'fetch(process.env.UPDATE_CHECK_URL,{headers:{Accept:"application/json"}}).then(async r=>console.log(r.status, (await r.text()).slice(0,300)))' + +# Mét token, precies zoals routes/update.js de header opbouwt +docker exec vibefinance_app node -e 'const t=process.env.UPDATE_CHECK_TOKEN; fetch(process.env.UPDATE_CHECK_URL,{headers:{Accept:"application/json",...(t&&{Authorization:`token ${t}`})}}).then(async r=>console.log(r.status, (await r.text()).slice(0,300)))' ``` +> Let op: een **ongeldig** token geeft `401`, ook op deze publieke repo — Gitea +> valt bij een verkeerd token niet terug op anonieme toegang. Krijg je 401 +> terwijl anoniem `200` geeft, haal `UPDATE_CHECK_TOKEN` dan uit `.env` in plaats +> van hem te vervangen. Een *leeg* token is onschuldig: `Authorization: token ` +> negeert Gitea (`200`), en `config/index.js` zet met `|| null` bij een lege +> waarde helemaal geen header. Nagemeten op 2026-09-10: +> geen header `200`, `token ongeldig123` `401`, leeg `200`. + ### Tokens opruimen De lijst onder Settings → Applications heeft een kolom **Last used**. Zet eerst @@ -406,20 +445,8 @@ docker-credential-desktop list ### Pullen op een deploy-machine -Het package is privé, dus ook een **pull** vereist een login — anoniem geeft -`401 Unauthorized` (`docker compose pull` meldt dan "no basic auth credentials" -of "pull access denied"). Eenmalig op de Docker-host: - -```bash -docker login vibehoogie.duckdns.org -# gebruikersnaam: je Gitea-account -# wachtwoord: het token van deze host (vibefinance-host, read:package) -``` - -Daarna werkt `docker compose pull && docker compose up -d`. De login komt in -`~/.docker/config.json` van de gebruiker die het commando uitvoert — doe het dus -als dezelfde gebruiker (bij `sudo docker` is dat root). Log in op -`vibehoogie.duckdns.org` **zonder** `:3000`. +De organisatie is public, dus een **pull** werkt anoniem — `docker compose pull && +docker compose up -d` en klaar, zonder `docker login`. Controleren zonder te pullen: @@ -427,8 +454,23 @@ Controleren zonder te pullen: docker manifest inspect vibehoogie.duckdns.org/vibe/vibefinance-app:latest ``` -Ditzelfde token gaat als `UPDATE_CHECK_TOKEN` in de `.env` op deze host, voor de -update-check van de app — één token voor beide, zie [Tokens](#tokens). +Krijg je `401 Unauthorized`, "no basic auth credentials" of "pull access denied", +dan is het package niet publiek leesbaar (organisatie weer op privé, of de +package-eigenaar wijkt af van de organisatie). Dan alsnog eenmalig op de host: + +```bash +docker login vibehoogie.duckdns.org +# gebruikersnaam: je Gitea-account +# wachtwoord: een token met read:package +``` + +De login komt in `~/.docker/config.json` van de gebruiker die het commando +uitvoert — doe het dus als dezelfde gebruiker (bij `sudo docker` is dat root). +Log in op `vibehoogie.duckdns.org` **zonder** `:3000`. + +Moet je terug naar een token, dan kan datzelfde token ook als +`UPDATE_CHECK_TOKEN` in de `.env` op deze host — één token voor beide, zie +[Tokens](#tokens). --- @@ -464,7 +506,7 @@ uit `.env` (of de default in `docker-compose.yml`). Beschikbare backend-variabel | Variabele | Betekenis | |---|---| | `UPDATE_CHECK_URL` | Release-API die gepolld wordt. Leeg = melding uit. | -| `UPDATE_CHECK_TOKEN` | Nodig zolang de `Vibe`-organisatie in Gitea privé is: het token van de host (`read:repository`, zie [Tokens](#tokens)). Zonder token geeft `releases/latest` 404, met een verkeerde scope 403 — in beide gevallen blijft de melding leeg, met alleen een `WARN` in het containerlog. | +| `UPDATE_CHECK_TOKEN` | Optioneel. De `Vibe`-organisatie is public, dus `releases/latest` antwoordt anoniem met `200` en de check heeft geen token nodig. Gaat de organisatie weer privé, dan is het host-token nodig (`read:repository`, zie [Tokens](#tokens)): zonder geeft `releases/latest` dan 404, met een verkeerde scope 403 — in beide gevallen blijft de melding leeg, met alleen een `WARN` in het containerlog. | **Een release publiceren** diff --git a/Changelog.md b/Changelog.md index 920eeee..5405180 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,11 +2,40 @@ ## [Niet vrijgegeven] +### Toegevoegd +- Kolommen in de tabellen op Eigen Vermogen ("Geïnvesteerde Fiat") en Voortgang + zijn versleepbaar: pak de rechterrand van een kolomkop. De breedtes gaan per + tabel naar localStorage, dus ze overleven een tabwissel en een herlaad; + dubbelklik op de greep zet één kolom terug op zijn standaardbreedte. De logica + staat in de nieuwe hook `frontend/src/hooks/useKolomBreedtes.js`, de greep zelf + is `.kolom-grip` in `App.css` + ### Gewijzigd +- De `Vibe`-organisatie in Gitea staat op **public**, dus lezen vereist geen + token meer: `git clone`, `docker compose pull` en de update-check werken + anoniem. `UPDATE_CHECK_TOKEN` in `.env` is daarmee optioneel — de code + ondersteunt hem nog als vangnet voor het geval de organisatie weer privé + wordt. Schrijven verandert niet: `docker push` en het aanmaken van een release + hebben nog steeds het werkstation-token nodig +- De tabellen op Eigen Vermogen en Voortgang staan op `table-layout: fixed`, + nodig om ingestelde kolombreedtes te respecteren. Gevolg: lange tekst rekt een + kolom niet meer op maar wordt afgekapt met een ellips. De Opmerkingen-cellen + hebben een `title`, dus hover geeft de volledige tekst - Het label bij een beschikbare update in "Over VibeFinance" is "Nieuwe versie" (was "Nieuwste versie" in 1.0.0) ### Opgelost +- De sticky kolomkop van die tabellen was in dark mode maar 30% dekkend: hij + gebruikte `T.card` (`rgba(22,23,27,0.30)`), waardoor de rijen tijdens het + scrollen zichtbaar door de koptekst heen liepen. Nu `T.popupBg`, de + ondoorzichtige variant die al in het thema stond +- In dev bakte Vite de versie uit `frontend/package.json` in in plaats van de + versie uit de root `package.json`. `VITE_APP_VERSION` kwam alleen als + build-arg mee, maar de `development`-stage in de `Dockerfile` declareert die + `ARG` niet — en Vite bakt de constante in bij het *starten* van de dev-server, + niet bij het bouwen van het image. De variabele staat nu in de + runtime-omgeving van de dev-container; `vite.config.js` valt met `||` (was + `??`) terug op `package.json` als hij leeg is - API-antwoorden sturen nu `Cache-Control: no-store`. Ze hadden alleen een `ETag` en geen cache-beleid, waardoor de browser zelf mocht bepalen hoe lang hij ze hergebruikte — per browser en profiel verschillend. Gevolg: de diff --git a/README.md b/README.md index bd0d0f1..58b26ae 100644 --- a/README.md +++ b/README.md @@ -49,12 +49,17 @@ openssl rand -hex 64 ```bash cp .env.example .env # en vul de waarden in -# Het app-image staat in een privé-registry, dus eerst inloggen: -docker login vibehoogie.duckdns.org # wachtwoord = Gitea-token met read:package - docker compose pull && docker compose up -d ``` +De `Vibe`-organisatie in Gitea is publiek, dus een pull werkt anoniem — geen +`docker login` nodig. Krijg je toch `401 Unauthorized` of "pull access denied", +dan staat de organisatie weer op privé: + +```bash +docker login vibehoogie.duckdns.org # wachtwoord = Gitea-token met read:package +``` + Navigeer naar `http://:3400` en registreer je account. De **eerste** geregistreerde gebruiker krijgt automatisch de Admin-rol. De container luistert intern op poort 3001 en serveert daar zowel de frontend @@ -135,6 +140,10 @@ pusht naar de registry, zet de Git-tag en publiceert de Gitea-release. | 📈 **Voortgang** | Grafieken en trends van EV en schuld over tijd (apart bij te houden) | | 👥 **Gebruikers** | Accounts aanmaken, bewerken en beheren (Admin-rol vereist) | +De kolommen in de tabellen op Eigen vermogen en Voortgang zijn versleepbaar: pak +de rechterrand van een kolomkop. De breedtes worden per tabel in de browser +bewaard; dubbelklik op de greep voor de standaardbreedte. + --- ## 🏗️ Architectuur @@ -165,7 +174,7 @@ vibefinance/ │ ├── version.js # Draaiende versie (__APP_VERSION__ uit de build) │ ├── constants/ # Kleuren, tabs, begindata │ ├── utils/ # Formatters, helpers -│ ├── hooks/ # useTheme, useUpdateCheck +│ ├── hooks/ # useTheme, useUpdateCheck, useKolomBreedtes │ ├── context/ # AppContext (global state + API sync) │ ├── components/ │ │ ├── ui/ # Primitieve UI (VCard, DonutChart…) @@ -259,7 +268,7 @@ Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via | `PORT` | `3001` | Poort binnen de container | | `STATIC_DIR` | `/app/public` | Map met de frontend-build | | `UPDATE_CHECK_URL` | Gitea releases-API | Bron update-melding (leeg = uit) | -| `UPDATE_CHECK_TOKEN` | — | Nodig zolang de Gitea-repo privé is; scope `read:repository` | +| `UPDATE_CHECK_TOKEN` | — | Optioneel: de Gitea-repo is publiek, dus de check werkt anoniem. Alleen nodig als de organisatie weer privé wordt; scope `read:repository` | --- diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 77159d7..9c8cefc 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -49,6 +49,11 @@ services: environment: NODE_ENV: development PORT: 3001 + # Vite bakt de versie in bij het STARTEN van de dev-server, dus die moet + # in de runtime-omgeving staan; de build-arg hierboven bereikt alleen de + # frontend-build-stage en doet niets voor target: development. + # Leeg = vite.config.js valt terug op frontend/package.json. + VITE_APP_VERSION: ${VITE_APP_VERSION:-} JWT_SECRET: dev-secret-change-in-production JWT_EXPIRES: 7d CORS_ORIGIN: http://localhost diff --git a/frontend/package.json b/frontend/package.json index 0eed82e..3de1039 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "vibefinance-frontend", - "version": "1.0.0", + "version": "1.0.1-dev", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/App.css b/frontend/src/App.css index f75a672..731aefb 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -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; +} diff --git a/frontend/src/hooks/useKolomBreedtes.js b/frontend/src/hooks/useKolomBreedtes.js new file mode 100644 index 0000000..eef3ea6 --- /dev/null +++ b/frontend/src/hooks/useKolomBreedtes.js @@ -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 , 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 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 }; +} diff --git a/frontend/src/pages/EigenVermogenTab.jsx b/frontend/src/pages/EigenVermogenTab.jsx index fc5acdf..3d4a706 100644 --- a/frontend/src/pages/EigenVermogenTab.jsx +++ b/frontend/src/pages/EigenVermogenTab.jsx @@ -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 (
@@ -326,15 +341,22 @@ function EVVoortgangTabel() { {evRows.length > 0 ? (
- +
+ + {kolommen.map((k) => )} + - - {cats.map((c) => )} - - - - {!locked && } + + {cats.map((c) => ( + + ))} + + + + {!locked && } @@ -349,7 +371,8 @@ function EVVoortgangTabel() { {cats.map((c) => )} - + {/* title: bij een smalle kolom is de tekst afgekapt */} + {!locked && (
Datum{c.naam}Netto WaardeVerschilOpmerkingenDatum + {c.naam} + Netto WaardeVerschilOpmerkingen
{fmt(row[c.id] || 0)}{fmt(row.nettoWaarde || 0)} {vLabel}{row.opmerkingen || "—"}{row.opmerkingen || "—"}
diff --git a/frontend/src/pages/VoortgangTab.jsx b/frontend/src/pages/VoortgangTab.jsx index 06c2004..5001975 100644 --- a/frontend/src/pages/VoortgangTab.jsx +++ b/frontend/src/pages/VoortgangTab.jsx @@ -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 (
@@ -396,15 +410,18 @@ export default function VoortgangTab() { )} {voortgangRows.length > 0 ? (
- +
+ + {kolommen.map((k) => )} + - - - - - - {!locked && } + + + + + + {!locked && } @@ -414,7 +431,8 @@ export default function VoortgangTab() { - + {/* title: bij een smalle kolom is de tekst afgekapt */} + {!locked && (
DatumEigen VermogenSchuldNettowaardeOpmerkingenDatumEigen VermogenSchuldNettowaardeOpmerkingen
{row.ev !== null ? fmt(row.ev) : "—"} {row.schuld !== null ? fmt(row.schuld) : "—"} = 0 ? GREEN : RED) : T.muted }}>{row.netto !== null ? fmt(row.netto) : "—"}{row.opmerkingen || "—"}{row.opmerkingen || "—"} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index ce46b5e..ff8615f 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -41,6 +41,6 @@ export default defineConfig({ define: { // VITE_API_URL wordt ingebakken tijdens build __API_URL__: JSON.stringify(process.env.VITE_API_URL ?? "/api"), - __APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION ?? pkg.version), + __APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION || pkg.version), }, }); diff --git a/package.json b/package.json index 88e575a..ba17b36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vibefinance", - "version": "1.0.0", + "version": "1.0.1", "description": "VibeFinance — persoonlijk vermogensbeheer", "private": true, "repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",