Versie 1.0.1 - versleepbare kolommen en een enkel Gitea-token #6
+5
-2
@@ -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=
|
||||
|
||||
@@ -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**
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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://<server-ip>: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` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vibefinance-frontend",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1-dev",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user