Skip to content

feat(charges): month-history navigation + clickable forgotten alert - #231

Merged
thierryvm merged 2 commits into
mainfrom
feat/charges-month-history
Jul 19, 2026
Merged

feat(charges): month-history navigation + clickable forgotten alert#231
thierryvm merged 2 commits into
mainfrom
feat/charges-month-history

Conversation

@thierryvm

@thierryvm thierryvm commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Historique par mois — navigation temporelle des factures (priorité validée @Thierry)

Ta demande : « on n'a aucun moyen de revenir en arrière pour voir les factures payées ou non ». C'est réglé — les données étaient en base depuis le début, l'UI ne permettait juste pas d'y aller.

Ce que ça fait

  • /app/charges?period=2026-06 : la page affiche l'état payé/non payé de n'importe quel mois (param validé + clampé serveur : jamais le futur, plancher janv. 2026, valeur invalide → mois courant).
  • Navigateur « ◀ juillet 2026 ▶ » au-dessus du bandeau + lien « Revenir à {mois courant} » quand tu consultes le passé.
  • Les coches restent modifiables dans le passé : tu peux solder la Taxe voiture de juin depuis juillet → l'alerte oubli du dashboard disparaît. (L'action était déjà paramétrée par période — authz/audit inchangés.)
  • Libellés adaptés : « Reste à payer en juin 2026 » / « Tout est payé pour juin 2026 » en vue historique.
  • L'alerte oubli du dashboard est cliquable : « … — vérifie qu'elle a bien été payée. Voir juin → » mène directement au mois concerné.

Sécurité / risque

Lecture supplémentaire RLS-scopée pour les périodes passées ; aucune migration, aucune nouvelle Server Action ; param URL jamais trusté (regex + clamp). Suite complète 1501 verte, typecheck + build clean.

Smoke @Thierry (desktop + mobile)

  1. Dashboard → clique « Voir juin → » dans l'alerte → tu arrives sur juin, tu vois la facture non cochée.
  2. Coche-la → reviens au dashboard → l'alerte a disparu.
  3. Navigue ◀ ▶ entre les mois ; « Revenir à juillet » te ramène au présent.

🤖 Generated with Claude Code

Summary by Sourcery

Ajouter une navigation par historique mensuel à la page des charges et faire en sorte que l’alerte de facture oubliée sur le tableau de bord pointe directement vers la vue du mois concerné.

Nouvelles fonctionnalités :

  • Prendre en charge la consultation et la modification des charges pour n’importe quel mois passé via un paramètre de requête period sur la page des charges, avec des libellés de mois localisés et un navigateur.
  • Ajouter un lien cliquable depuis l’alerte de facture oubliée du tableau de bord vers la page des charges pour le mois spécifique où les factures n’ont jamais été marquées comme payées.

Améliorations :

  • Adapter le texte récapitulatif payé/non payé sur la page des charges pour refléter si le mois affiché est le mois en cours ou un mois passé.
  • Étendre la logique de chargement côté serveur pour récupérer les données de paiement sur des périodes passées et dériver les métadonnées de navigation à partir de la période de l’espace de travail actuel et du paramètre d’URL.

Tests :

  • Ajouter des tests unitaires couvrant l’interface du navigateur d’historique mensuel, ses messages dépendant de la période, et le comportement du lien d’alerte de facture oubliée sur le tableau de bord.
  • Mettre à jour les helpers de test pour sérialiser les liens de navigation basés sur des objets afin que les tests puissent vérifier les URLs générées.
Original summary in English

Summary by Sourcery

Add month-history navigation to the charges page and make the dashboard forgotten-bill alert link directly to the relevant month view.

New Features:

  • Support viewing and editing charges for arbitrary past months via a period query parameter on the charges page, with localized month labels and a navigator.
  • Add a clickable link from the dashboard forgotten-bill alert to the charges page for the specific month where bills were never marked paid.

Enhancements:

  • Adjust paid/unpaid summary copy on the charges page to reflect whether the current or a past month is being viewed.
  • Extend server-side loading logic to fetch payment data for past periods and derive navigation metadata from the current workspace period and URL parameter.

Tests:

  • Add unit tests covering the month-history navigator UI, its period-scoped messaging, and the forgotten alert link behavior on the dashboard.
  • Update test helpers to serialize object-based navigation links so tests can assert generated URLs.

…Thierry priority 2026-07-19)

The paid/unpaid state of ANY past month is now browsable — the ledger had
the data all along, the UI just never let you go back.

- /app/charges?period=YYYY-MM selects the viewed month. The param is
  validated + clamped server-side (regex, floor 2026-01, never future);
  invalid values silently fall back to today. Past periods load their own
  RLS-scoped ledger read (current month stays free with the snapshot).
- ◀ juillet 2026 ▶ navigator above the banner: chevron links (disabled at
  the floor / at today), viewed-month label, and a 'Revenir à {mois}' link
  when browsing history.
- Ticks stay EDITABLE in past months (togglePaymentAction was already
  period-parameterized) — so a forgotten June bill can be settled from
  July, which clears the dashboard alert.
- Period-scoped banner copy: 'Reste à payer en juin 2026' / 'Tout est payé
  pour juin 2026' when not on the current month.
- The dashboard forgotten alert now links 'Voir juin →' straight to the
  concerned month's history view.

i18n x5: app.charges.{periodNav.*,remainingLabelPeriod,allPaidPeriod} +
dashboard.upcomingBills.forgottenSee. Link test stubs now serialize object
hrefs. Full suite 1501 green, typecheck + build clean.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ankora Ready Ready Preview, Comment Jul 19, 2026 3:15pm

@github-actions github-actions Bot added area:charges Charges management status:review-needed Ready for review type:feat New user-facing feature labels Jul 19, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Guide du relecteur

Implémente la navigation historique par mois pour les charges et fait en sorte que l’alerte de factures oubliées du tableau de bord pointe directement vers la vue des charges du mois concerné, incluant l’analyse côté serveur de la période, des lectures RLS pour les périodes passées, ainsi qu’une mise à jour des textes/tests i18n.

Diagramme de séquence pour la navigation historique par mois et l’alerte cliquable de factures oubliées

sequenceDiagram
  actor User
  participant DashboardPage
  participant ProchainesFacturesCard
  participant ChargesPage
  participant Supabase
  participant ChargesClient

  User ->> DashboardPage: load /app
  DashboardPage ->> ProchainesFacturesCard: ProchainesFacturesCard({ forgotten })
  ProchainesFacturesCard ->> User: render forgotten alert with Link(/app/charges?period=periodParam)

  User ->> ChargesPage: navigate /app/charges?period=YYYY-MM
  ChargesPage ->> ChargesPage: parseViewedPeriod(params.period, snapshot.currentPeriod)
  ChargesPage ->> ChargesPage: ordinal(viewed), shift(viewed, ±1), toParam(period)

  alt viewed isCurrent
    ChargesPage ->> ChargesPage: paidChargeIds = snapshot.currentMonthPayments.map
  else viewed is past month
    ChargesPage ->> Supabase: createClient()
    ChargesPage ->> Supabase: charge_payments.select().eq(...)
    Supabase -->> ChargesPage: data | error
    ChargesPage ->> ChargesPage: log.error(...)
    ChargesPage ->> ChargesPage: paidChargeIds = (data ?? []).map
  end

  ChargesPage ->> ChargesPage: monthLabel(viewed), monthLabel(current)
  ChargesPage ->> ChargesClient: ChargesClient({ currentPeriod: viewed, periodNav, paidChargeIds, ... })

  ChargesClient ->> User: render periodNav prev/next Links and back-to-current Link
  ChargesClient ->> User: render remaining/allPaid labels using periodNav.isCurrent and periodNav.label
Loading

Modifications au niveau des fichiers

Changement Détails Fichiers
Ajouter une navigation historique par mois à la page des charges, pilotée par un paramètre d’URL ?period=YYYY-MM validé et des métadonnées de navigation calculées côté serveur.
  • Étendre les props de ChargesClient avec periodNav décrivant le mois consulté, les périodes précédente/suivante, et le libellé du mois courant.
  • Afficher une barre de navigation avec des boutons chevron précédent/suivant et un lien conditionnel « retour au mois en cours » utilisant le composant Link compatible i18n.
  • Adapter le texte de la bannière restant/payé pour distinguer les périodes courantes des périodes historiques avec des clés i18n spécifiques à la période.
src/app/[locale]/app/charges/ChargesClient.tsx
Calculer la période consultée côté serveur, la borner aux limites historiques valides, et charger les charges payées pour les mois passés via une requête Supabase supplémentaire, soumise au RLS.
  • Introduire des helpers Period (validation par regex, constante de plancher, fonctions ordinal/shift/toParam) et parseViewedPeriod pour interpréter en toute sécurité searchParams.period.
  • Utiliser getLocale et formatMonth pour construire des libellés de mois localisés pour les périodes courante et consultée.
  • Lors de la consultation d’un mois passé, interroger charge_payments pour cette période et cet espace de travail ; en cas d’erreur, revenir aux paiements du mois courant ; transmettre les métadonnées viewed et periodNav à ChargesClient au lieu de snapshot.currentPeriod.
  • Mettre à jour la signature de ChargesPage pour accepter searchParams et récupérer snapshot/params/locale en parallèle.
src/app/[locale]/app/charges/page.tsx
Rendre l’alerte de factures oubliées du tableau de bord cliquable, avec un lien profond vers la page des charges pour le mois concerné, en utilisant le même paramètre period.
  • Étendre la prop forgotten sur ProchainesFacturesCard pour inclure une chaîne periodParam (YYYY-MM).
  • Afficher l’alerte de factures oubliées comme un div contenant le texte existant plus un Link de marque vers /app/charges?period={periodParam} avec un libellé localisé « Voir {month} ».
  • Renseigner periodParam sur le tableau de bord à partir de snapshot.previousPeriod lors de la construction du payload des factures oubliées.
src/components/dashboard/ProchainesFacturesCard.tsx
src/app/[locale]/app/page.tsx
Mettre à jour les tests et les mocks de navigation pour prendre en charge le nouveau comportement de Link et les flux de navigation historique par mois/alerte de factures oubliées.
  • Mocker @/i18n/navigation Link dans les tests de ChargesClient pour sérialiser les valeurs d’objet href, afin que les assertions puissent inspecter les URL générées.
  • Fournir un periodNav par défaut dans le helper de test renderCharges et ajouter une couverture pour l’UI de navigation du mois courant vs mois passé et le texte de bannière spécifique à la période.
  • Mettre à jour les tests de la carte du tableau de bord pour gérer la forme étendue de forgotten, sérialiser l’objet href dans le mock de Link, et vérifier que l’alerte de factures oubliées pointe vers /app/charges?period=YYYY-MM.
  • Ajuster les fichiers JSON de messages pour ajouter/renommer les clés i18n nécessaires à la navigation par période et au texte du lien de l’alerte de factures oubliées (clés référencées par t('periodNav.*') et t('forgottenSee')).
src/app/[locale]/app/charges/__tests__/ChargesClient.test.tsx
src/components/dashboard/__tests__/ProchainesFacturesCard.test.tsx
messages/de-DE.json
messages/en.json
messages/es-ES.json
messages/fr-BE.json
messages/nl-BE.json

Conseils et commandes

Interagir avec Sourcery

  • Déclencher une nouvelle revue : Commentez @sourcery-ai review sur la pull request.
  • Poursuivre les discussions : Répondez directement aux commentaires de revue de Sourcery.
  • Générer une issue GitHub à partir d’un commentaire de revue : Demandez à Sourcery de créer une issue à partir d’un commentaire de revue en y répondant. Vous pouvez également répondre à un commentaire de revue avec @sourcery-ai issue pour créer une issue à partir de celui-ci.
  • Générer un titre de pull request : Écrivez @sourcery-ai n’importe où dans le titre de la pull request pour générer un titre à tout moment. Vous pouvez aussi commenter @sourcery-ai title sur la pull request pour (re)générer le titre à tout moment.
  • Générer un résumé de pull request : Écrivez @sourcery-ai summary n’importe où dans le corps de la pull request pour générer un résumé de PR à tout moment exactement à l’endroit souhaité. Vous pouvez aussi commenter @sourcery-ai summary sur la pull request pour (re)générer le résumé à tout moment.
  • Générer le guide du relecteur : Commentez @sourcery-ai guide sur la pull request pour (re)générer le guide du relecteur à tout moment.
  • Résoudre tous les commentaires Sourcery : Commentez @sourcery-ai resolve sur la pull request pour résoudre tous les commentaires Sourcery. Utile si vous avez déjà traité tous les commentaires et ne souhaitez plus les voir.
  • Ignorer toutes les revues Sourcery : Commentez @sourcery-ai dismiss sur la pull request pour ignorer toutes les revues Sourcery existantes. Particulièrement utile si vous voulez repartir de zéro avec une nouvelle revue – n’oubliez pas de commenter @sourcery-ai review pour déclencher une nouvelle revue !

Personnaliser votre expérience

Accédez à votre tableau de bord pour :

  • Activer ou désactiver des fonctionnalités de revue comme le résumé de pull request généré par Sourcery, le guide du relecteur, et d’autres.
  • Modifier la langue de revue.
  • Ajouter, supprimer ou éditer des instructions personnalisées de revue.
  • Ajuster d’autres paramètres de revue.

Obtenir de l’aide

Original review guide in English

Reviewer's Guide

Implements month-history navigation for charges and makes the dashboard forgotten-bills alert link directly to the relevant month’s charges view, including server-side period parsing, RLS-scoped reads for past periods, and updated i18n copy/tests.

Sequence diagram for month-history navigation and clickable forgotten alert

sequenceDiagram
  actor User
  participant DashboardPage
  participant ProchainesFacturesCard
  participant ChargesPage
  participant Supabase
  participant ChargesClient

  User ->> DashboardPage: load /app
  DashboardPage ->> ProchainesFacturesCard: ProchainesFacturesCard({ forgotten })
  ProchainesFacturesCard ->> User: render forgotten alert with Link(/app/charges?period=periodParam)

  User ->> ChargesPage: navigate /app/charges?period=YYYY-MM
  ChargesPage ->> ChargesPage: parseViewedPeriod(params.period, snapshot.currentPeriod)
  ChargesPage ->> ChargesPage: ordinal(viewed), shift(viewed, ±1), toParam(period)

  alt viewed isCurrent
    ChargesPage ->> ChargesPage: paidChargeIds = snapshot.currentMonthPayments.map
  else viewed is past month
    ChargesPage ->> Supabase: createClient()
    ChargesPage ->> Supabase: charge_payments.select().eq(...)
    Supabase -->> ChargesPage: data | error
    ChargesPage ->> ChargesPage: log.error(...)
    ChargesPage ->> ChargesPage: paidChargeIds = (data ?? []).map
  end

  ChargesPage ->> ChargesPage: monthLabel(viewed), monthLabel(current)
  ChargesPage ->> ChargesClient: ChargesClient({ currentPeriod: viewed, periodNav, paidChargeIds, ... })

  ChargesClient ->> User: render periodNav prev/next Links and back-to-current Link
  ChargesClient ->> User: render remaining/allPaid labels using periodNav.isCurrent and periodNav.label
Loading

File-Level Changes

Change Details Files
Add month-history navigation to the charges page, driven by a validated ?period=YYYY-MM URL parameter and server-computed navigation metadata.
  • Extend ChargesClient props with periodNav describing the viewed month, previous/next periods, and current-month label.
  • Render a navigation bar with prev/next chevron buttons and a conditional “back to current month” link using the i18n-aware Link component.
  • Adapt the remaining/paid banner text to distinguish current vs historical periods with period-scoped i18n keys.
src/app/[locale]/app/charges/ChargesClient.tsx
Compute the viewed period server-side, clamp it to valid historical bounds, and load paid charges for past months via an additional Supabase RLS-scoped query.
  • Introduce Period helpers (regex validation, floor constant, ordinal/shift/toParam functions) and parseViewedPeriod to safely interpret searchParams.period.
  • Use getLocale and formatMonth to build localized month labels for the current and viewed periods.
  • When viewing a past month, query charge_payments for that period and workspace; fall back to current payments on error; pass viewed and periodNav metadata into ChargesClient instead of snapshot.currentPeriod.
  • Update ChargesPage signature to accept searchParams and fetch snapshot/params/locale in parallel.
src/app/[locale]/app/charges/page.tsx
Make the dashboard forgotten-bills alert clickable, deep-linking into the charges page for the relevant month with the same period parameter.
  • Extend the forgotten prop on ProchainesFacturesCard to include a periodParam string (YYYY-MM).
  • Render the forgotten alert as a div containing the existing text plus a branded Link to /app/charges?period={periodParam} with a localized “See {month}” label.
  • Populate periodParam on the dashboard from snapshot.previousPeriod when constructing the forgotten bills payload.
src/components/dashboard/ProchainesFacturesCard.tsx
src/app/[locale]/app/page.tsx
Update tests and navigation mocks to support the new Link behavior and month-history/forgotten-alert flows.
  • Mock @/i18n/navigation Link in ChargesClient tests to serialize object href values so assertions can inspect the generated URLs.
  • Provide default periodNav in renderCharges test helper and add coverage for current-month vs past-month navigation UI and period-scoped banner copy.
  • Update the dashboard card tests to handle the extended forgotten shape, serialize object href in the Link mock, and assert that the forgotten alert links to /app/charges?period=YYYY-MM.
  • Adjust message JSON files to add/rename any i18n keys required for period navigation and forgotten-alert link text (keys referenced by t('periodNav.*') and t('forgottenSee')).
src/app/[locale]/app/charges/__tests__/ChargesClient.test.tsx
src/components/dashboard/__tests__/ProchainesFacturesCard.test.tsx
messages/de-DE.json
messages/en.json
messages/es-ES.json
messages/fr-BE.json
messages/nl-BE.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - j’ai identifié 2 problèmes et laissé quelques remarques générales :

  • La prop currentPeriod dans ChargesClient représente désormais la période affichée plutôt que strictement le mois courant, ce qui peut prêter à confusion ; envisage de renommer cette prop (et les champs de snapshot associés si pertinent) pour mieux refléter sa nouvelle sémantique.
  • Les mocks de Link dans ChargesClient.test.tsx et ProchainesFacturesCard.test.tsx implémentent tous deux la même logique de sérialisation objet → href ; extraire cette logique dans un helper de test partagé réduirait la duplication et garantirait un comportement cohérent.
  • La signature de ChargesPage typée avec searchParams comme Promise<{ period?: string }> le traite comme un objet searchParams de Next.js ; aligner ce type sur la forme réelle à l’exécution (non-Promise) éviterait la confusion et clarifierait le contrat.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `currentPeriod` prop in `ChargesClient` now represents the viewed period rather than strictly the current month, which can be confusing; consider renaming this prop (and related snapshot fields if appropriate) to better reflect its new semantics.
- The test `Link` mocks in `ChargesClient.test.tsx` and `ProchainesFacturesCard.test.tsx` both implement the same object-href serialization logic; factoring this into a shared test helper would reduce duplication and keep behavior consistent.
- The `ChargesPage` signature types `searchParams` as a `Promise<{ period?: string }>` but treats it like a Next.js `searchParams` object; aligning this type with the actual runtime shape (non-Promise) would avoid confusion and make the contract clearer.

## Individual Comments

### Comment 1
<location path="src/app/[locale]/app/charges/page.tsx" line_range="48-62" />
<code_context>
+
+const toParam = (p: Period): string => `${p.year}-${String(p.month).padStart(2, '0')}`;
+
+export default async function ChargesPage({
+  searchParams,
+}: {
+  searchParams: Promise<{ period?: string }>;
</code_context>
<issue_to_address>
**suggestion:** The `searchParams` prop is typed and used as a `Promise`, which diverges from Next App Router conventions and may confuse future readers.

In the App Router, `searchParams` should be a plain object, not a promise. While `Promise.all` with a non-promise happens to work, this typing is unconventional and makes the signature harder to understand. Prefer a signature like `({ searchParams }: { searchParams?: { period?: string } })` and access `searchParams.period` directly to match Next’s conventions and keep the code clearer.

```suggestion
const toParam = (p: Period): string => `${p.year}-${String(p.month).padStart(2, '0')}`;

export default async function ChargesPage({
  searchParams,
}: {
  searchParams?: { period?: string };
}) {
  const [snapshot, locale] = await Promise.all([
    getWorkspaceSnapshot(),
    getLocale() as Promise<Locale>,
  ]);

  const params = searchParams ?? {};
  const current = snapshot.currentPeriod;
  const viewed = parseViewedPeriod(params.period, current);
```
</issue_to_address>

### Comment 2
<location path="src/app/[locale]/app/charges/page.tsx" line_range="25" />
<code_context>
+/** No payment data can exist before the app's first release month. */
+const PERIOD_FLOOR: Period = { year: 2026, month: 1 };
+
+const ordinal = (p: Period): number => p.year * 12 + p.month;
+
+/**
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the period handling by using direct year/month comparisons and treating `searchParams` as a plain object to reduce indirection and helper overhead.

You can keep all the new functionality while reducing indirection by inlining the month arithmetic and treating `searchParams` as a plain object again.

### 1. Simplify period parsing and comparisons (remove `ordinal` / `shift` / `toParam`)

The main rules are “invalid/future/pre-floor → current” and “prev/next month navigation with a floor”. Those can be implemented with direct year/month comparisons:

```ts
type Period = { year: number; month: number };

const PERIOD_RE = /^\d{4}-(0[1-9]|1[0-2])$/;
const PERIOD_FLOOR: Period = { year: 2026, month: 1 };

function isBefore(a: Period, b: Period): boolean {
  return a.year < b.year || (a.year === b.year && a.month < b.month);
}

function isAfter(a: Period, b: Period): boolean {
  return a.year > b.year || (a.year === b.year && a.month > b.month);
}

function parseViewedPeriod(raw: string | undefined, current: Period): Period {
  if (!raw || !PERIOD_RE.test(raw)) return current;

  const [year, month] = raw.split('-').map(Number) as [number, number];
  const candidate: Period = { year, month };

  if (isAfter(candidate, current) || isBefore(candidate, PERIOD_FLOOR)) {
    return current;
  }

  return candidate;
}
```

Then compute `prev` / `next` and params inline instead of via `shift` / `ordinal` / `toParam`:

```ts
const prev: Period =
  viewed.month === 1
    ? { year: viewed.year - 1, month: 12 }
    : { year: viewed.year, month: viewed.month - 1 };

const next: Period =
  viewed.month === 12
    ? { year: viewed.year + 1, month: 1 }
    : { year: viewed.year, month: viewed.month + 1 };

const prevParam =
  isBefore(prev, PERIOD_FLOOR)
    ? null
    : `${prev.year}-${String(prev.month).padStart(2, '0')}`;

const nextParam = isCurrent
  ? null
  : `${next.year}-${String(next.month).padStart(2, '0')}`;
```

This keeps all behavior but removes the mental mapping to an ordinal index and the extra helpers.

### 2. Treat `searchParams` as a plain object (avoid promise-based indirection)

You can still use `Promise.all` for the actual async work without turning `searchParams` into a promise. This keeps the Next.js mental model intact and removes one layer of indirection:

```ts
export default async function ChargesPage({
  searchParams,
}: {
  searchParams?: { period?: string };
}) {
  const [snapshot, locale] = await Promise.all([
    getWorkspaceSnapshot(),
    getLocale() as Promise<Locale>,
  ]);

  const current = snapshot.currentPeriod;
  const viewed = parseViewedPeriod(searchParams?.period, current);
  const isCurrent = !isAfter(viewed, current) && !isBefore(viewed, current);

  // ...rest of the logic (Supabase read when !isCurrent, periodNav, etc.)
}
```

This keeps the same behavior (concurrent snapshot + locale fetch, period-based Supabase query) but reduces control-flow complexity and matches typical Next.js usage.
</issue_to_address>

Sourcery est gratuit pour l’open source — si nos reviews vous plaisent, pensez à les partager ✨
Aidez-moi à être plus utile ! Cliquez sur 👍 ou 👎 sur chaque commentaire et j’utiliserai vos retours pour améliorer les reviews.
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • The currentPeriod prop in ChargesClient now represents the viewed period rather than strictly the current month, which can be confusing; consider renaming this prop (and related snapshot fields if appropriate) to better reflect its new semantics.
  • The test Link mocks in ChargesClient.test.tsx and ProchainesFacturesCard.test.tsx both implement the same object-href serialization logic; factoring this into a shared test helper would reduce duplication and keep behavior consistent.
  • The ChargesPage signature types searchParams as a Promise<{ period?: string }> but treats it like a Next.js searchParams object; aligning this type with the actual runtime shape (non-Promise) would avoid confusion and make the contract clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `currentPeriod` prop in `ChargesClient` now represents the viewed period rather than strictly the current month, which can be confusing; consider renaming this prop (and related snapshot fields if appropriate) to better reflect its new semantics.
- The test `Link` mocks in `ChargesClient.test.tsx` and `ProchainesFacturesCard.test.tsx` both implement the same object-href serialization logic; factoring this into a shared test helper would reduce duplication and keep behavior consistent.
- The `ChargesPage` signature types `searchParams` as a `Promise<{ period?: string }>` but treats it like a Next.js `searchParams` object; aligning this type with the actual runtime shape (non-Promise) would avoid confusion and make the contract clearer.

## Individual Comments

### Comment 1
<location path="src/app/[locale]/app/charges/page.tsx" line_range="48-62" />
<code_context>
+
+const toParam = (p: Period): string => `${p.year}-${String(p.month).padStart(2, '0')}`;
+
+export default async function ChargesPage({
+  searchParams,
+}: {
+  searchParams: Promise<{ period?: string }>;
</code_context>
<issue_to_address>
**suggestion:** The `searchParams` prop is typed and used as a `Promise`, which diverges from Next App Router conventions and may confuse future readers.

In the App Router, `searchParams` should be a plain object, not a promise. While `Promise.all` with a non-promise happens to work, this typing is unconventional and makes the signature harder to understand. Prefer a signature like `({ searchParams }: { searchParams?: { period?: string } })` and access `searchParams.period` directly to match Next’s conventions and keep the code clearer.

```suggestion
const toParam = (p: Period): string => `${p.year}-${String(p.month).padStart(2, '0')}`;

export default async function ChargesPage({
  searchParams,
}: {
  searchParams?: { period?: string };
}) {
  const [snapshot, locale] = await Promise.all([
    getWorkspaceSnapshot(),
    getLocale() as Promise<Locale>,
  ]);

  const params = searchParams ?? {};
  const current = snapshot.currentPeriod;
  const viewed = parseViewedPeriod(params.period, current);
```
</issue_to_address>

### Comment 2
<location path="src/app/[locale]/app/charges/page.tsx" line_range="25" />
<code_context>
+/** No payment data can exist before the app's first release month. */
+const PERIOD_FLOOR: Period = { year: 2026, month: 1 };
+
+const ordinal = (p: Period): number => p.year * 12 + p.month;
+
+/**
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the period handling by using direct year/month comparisons and treating `searchParams` as a plain object to reduce indirection and helper overhead.

You can keep all the new functionality while reducing indirection by inlining the month arithmetic and treating `searchParams` as a plain object again.

### 1. Simplify period parsing and comparisons (remove `ordinal` / `shift` / `toParam`)

The main rules are “invalid/future/pre-floor → current” and “prev/next month navigation with a floor”. Those can be implemented with direct year/month comparisons:

```ts
type Period = { year: number; month: number };

const PERIOD_RE = /^\d{4}-(0[1-9]|1[0-2])$/;
const PERIOD_FLOOR: Period = { year: 2026, month: 1 };

function isBefore(a: Period, b: Period): boolean {
  return a.year < b.year || (a.year === b.year && a.month < b.month);
}

function isAfter(a: Period, b: Period): boolean {
  return a.year > b.year || (a.year === b.year && a.month > b.month);
}

function parseViewedPeriod(raw: string | undefined, current: Period): Period {
  if (!raw || !PERIOD_RE.test(raw)) return current;

  const [year, month] = raw.split('-').map(Number) as [number, number];
  const candidate: Period = { year, month };

  if (isAfter(candidate, current) || isBefore(candidate, PERIOD_FLOOR)) {
    return current;
  }

  return candidate;
}
```

Then compute `prev` / `next` and params inline instead of via `shift` / `ordinal` / `toParam`:

```ts
const prev: Period =
  viewed.month === 1
    ? { year: viewed.year - 1, month: 12 }
    : { year: viewed.year, month: viewed.month - 1 };

const next: Period =
  viewed.month === 12
    ? { year: viewed.year + 1, month: 1 }
    : { year: viewed.year, month: viewed.month + 1 };

const prevParam =
  isBefore(prev, PERIOD_FLOOR)
    ? null
    : `${prev.year}-${String(prev.month).padStart(2, '0')}`;

const nextParam = isCurrent
  ? null
  : `${next.year}-${String(next.month).padStart(2, '0')}`;
```

This keeps all behavior but removes the mental mapping to an ordinal index and the extra helpers.

### 2. Treat `searchParams` as a plain object (avoid promise-based indirection)

You can still use `Promise.all` for the actual async work without turning `searchParams` into a promise. This keeps the Next.js mental model intact and removes one layer of indirection:

```ts
export default async function ChargesPage({
  searchParams,
}: {
  searchParams?: { period?: string };
}) {
  const [snapshot, locale] = await Promise.all([
    getWorkspaceSnapshot(),
    getLocale() as Promise<Locale>,
  ]);

  const current = snapshot.currentPeriod;
  const viewed = parseViewedPeriod(searchParams?.period, current);
  const isCurrent = !isAfter(viewed, current) && !isBefore(viewed, current);

  // ...rest of the logic (Supabase read when !isCurrent, periodNav, etc.)
}
```

This keeps the same behavior (concurrent snapshot + locale fetch, period-based Supabase query) but reduces control-flow complexity and matches typical Next.js usage.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/app/[locale]/app/charges/page.tsx
Comment thread src/app/[locale]/app/charges/page.tsx
…e menu (@Thierry + Sourcery #231)

- currentPeriod prop → viewedPeriod: the prop names the period IN VIEW
  (month-history), no longer strictly today's month (Sourcery overall-1).
- AppBreadcrumbs removed from the app layout + component/test deleted:
  with the header nav already marking the active page, the breadcrumb bar
  read as a confusing double menu on EVERY app page (@Thierry 2026-07-19).
  The public glossary keeps its own breadcrumb (deep SEO pages).
- searchParams stays Promise-typed: that IS the Next 15+/16 App Router
  contract (sync access removed in 16) and the repo-wide convention
  (login/page.tsx, glossaire, admin layout) — Sourcery's suggestion would
  break the build. Defended on the thread.
@thierryvm
thierryvm merged commit ff1cead into main Jul 19, 2026
9 of 10 checks passed
@thierryvm
thierryvm deleted the feat/charges-month-history branch July 19, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:charges Charges management status:review-needed Ready for review type:feat New user-facing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant