Skip to content

V2.2 List order, health, ownership, favicon, mobile display#465

Merged
devpro merged 10 commits into
mainfrom
feature/health
Jul 17, 2026
Merged

V2.2 List order, health, ownership, favicon, mobile display#465
devpro merged 10 commits into
mainfrom
feature/health

Conversation

@devpro

@devpro devpro commented Jul 17, 2026

Copy link
Copy Markdown
Owner

No description provided.

devpro and others added 5 commits July 17, 2026 02:07
A third Car/House-shaped pair: HealthProfile (whose journal - the owner,
family members later) with HealthRecord children (Appointment/Sickness/Other,
date AND time like CarHistory, specialty, practitioner, description, notes).

The money model is the French reimbursement flow, four numbers the app does
the math over: Price, PublicReimbursement (assurance maladie),
InsuranceReimbursement (mutuelle), NotCovered (reste à charge). A record is
settled exactly when they sum to zero; anything else lands in the metrics'
"Reimbursements to check" list with the signed missing amount (positive =
money still expected, negative = over-received), plus a "to check" badge on
the journal row and a live balance hint in the edit modal.

HealthMetricsService also answers "when did I last see this practitioner"
(last visit + count per practitioner/specialty) and renders the yearly
Paid/Reimbursed/OutOfPocket review with an out-of-pocket bar chart.

Member-only, cascade delete of the journal with its profile, covered by
HealthMetricsServiceTest (balance/tolerance/over-payment cases),
HealthProfile/HealthRecord resource tests (metrics + cascade + time-of-day
round trip) and a Playwright smoke test; the mobile screenshot harness now
seeds and captures the health pages.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019MJHfZQLFr4Ghn1rnf1Jhh
…first

Import (POST /api/import/health, one-off like the car import): reads the
personal Journal_sante.xlsx - one sheet, every family member mixed in one
"Personne" column, so profiles are created/matched by name (case-insensitive).
The second "Personne" column is the practitioner (position-aware header
lookup), sparse bookkeeping columns (Lieu, Virt Ameli, payment dates,
Commentaire) are preserved as labeled notes, and the derived "Reste à charge"
formula column is deliberately NOT imported - the app recomputes the balance,
so unsettled history surfaces with the row badge for review. Shared cell
parsing extracted into ExcelCellParser (used by both Excel importers).
Verified against the real sample end-to-end: 3 profiles, 13 rows, 0 warnings.

Detail page rework per owner feedback: no summary panels above the journal
(the per-row "to check" badge is the whole warning surface), no chart, no
per-row reimbursement column, Last seen without the specialty column, and
the compact yearly Paid/Reimbursed/OutOfPocket table after the journal.
Midnight timestamps display date-only.

Also fixes CarHistoryImportController to MemberOnly: imports create data
through repositories, which bypassed CarController's own policy.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019MJHfZQLFr4Ghn1rnf1Jhh
Every list is now deterministically ordered:
- Default is newest first (_id descending — ObjectIds embed creation time, so no schema change), which also fixes the latent paging bug where an unsorted skip/limit read could duplicate or drop items across pages. Every other sort appends _id as tie-break.
- A sort picker in InventoryList's search bar on all nine list pages: Recently added / A–Z everywhere, plus Rating on the five media types. It's a ?sort= URL parameter through the existing ApplyQueryChanges path, so browser back/forward and bookmarks preserve it like search and filters.
- The key travels as PagedRequest.Sort (constants in Common.System/ListSort.cs); repositories opt in by overriding SortTitleField/SortRatingField — expressions, not field-name strings, which matters because Car.Name stores as commercial_name and a string sort would silently miss it.
- On the "optimized" front: the title sort is case/diacritic-insensitive via a per-query MongoDB collation (no shadow field, no extra index), rating descending puts unrated items last server-side, and it's still exactly one count + one find per page load. I deliberately added no new indexes — every query filters by owner first, and a single tenant's collection is small enough that sorting that subset in memory is negligible; ten speculative compound indexes would cost more on writes than they'd ever save on reads. One constraint documented in CLAUDE.md: collation can't combine with $text, which is safe because every repository searches via regex Contains.

Owned-versions editor — now follows the app's own two conventions instead of inventing a third:
- "+ Add version" opens a draft card with Save/Cancel (the list pages' Add-form pattern). Nothing persists — and the item doesn't become Owned — until Save; Cancel discards silently.
- The ambiguous ✕ is replaced by the same bin icon as list rows (extracted into a shared Components/Shared/TrashIcon.razor) with a ConfirmModal ("Remove this copy?"). The confirm is skipped when the copy is entirely empty, so undoing an accidental add stays one click.
- Saved copies keep auto-saving field edits, consistent with the rest of the detail page.

Verification

- Full suite green: 265 passed, 0 failed (30 skipped are the usual gated e2e/live-sync tests).
- New ListSortingRepositoryTest (real MongoDB) proves the collation's case-insensitive ordering, nulls-last rating sort, newest-first default, unknown-key fallback, and that title-sorted pages partition exactly with no duplicates/drops.
- OwnershipSmokeTest (Playwright, updated for the draft + confirm flow) and ListStateSmokeTest both pass against the real browser; the mobile screenshot harness confirms the visuals.

No data migration or index script change is needed — this deploys as-is. CLAUDE.md documents both patterns (including how a future list adds a sort key).
try
{
#pragma warning disable S5693
await using var stream = e.File.OpenReadStream(MaxFileSize);
Comment thread src/WebApi/Import/HealthImportController.cs Fixed
devpro added 5 commits July 18, 2026 00:08
- HealthProfileDetail.razor: journal is now split into per-year tabs (kt-season-picker/kt-page-btn, same header used by TvShowDetail's season picker), newest year first and selected first; the table shows only the selected year's entries. Tab selection persists across reloads when the year still exists, falling back to the newest year otherwise (mirrors TvShowDetail's season-selection logic).
- CarDetail.razor / CarHistoryRow.razor: history table now carries kt-table-grid, so on mobile it stays a real grid instead of stacking into labeled cards. City/Description are desktop-only (d-none d-md-table-cell); Edit/Del text buttons became kt-icon-btn ✎/✕ icons, matching HealthRecordRow. Now-unused data-label attributes were dropped.
- HouseDetail.razor / HouseHistoryRow.razor: same treatment — kt-table-grid on the history table, Provider/Description hidden on mobile, icon-based edit/delete.

The yearly cost-review summary tables (Car's charts, House's per-category breakdown) were left untouched — the request was about the record list, not those summaries.
- CarDetail.razor / HouseDetail.razor: history now split into per-year tabs (same kt-season-picker/kt-page-btn pattern used by TvShowDetail's seasons and HealthProfileDetail's journal), newest year selected by default and preserved across reloads when still valid.
- Ordering: Car's history now loads newest-first (OrderByDescending, previously ascending) to match Health/House; House was already descending, comment updated to reflect all three now share the same convention.
- Mileage-warning computation for Car is unaffected — it comes from the server-side CarMetricsDto, not from the local _history ordering.
Both verification passes succeeded — the mobile/desktop screenshots show the redesigned Platforms section matches the movies/books ownership UI, and the new Playwright smoke test confirms the interactive flow: draft-card with Save/Cancel, empty entries delete without prompting, and entries with data (state set) trigger the "Remove this platform?" confirmation.

Summary of changes to VideoGameDetail.razor:
- "Add a platform" is now a draft card (Platform select + copy-type buttons + Save/Cancel) opened via a "+ Add platform" button, instead of an always-visible form that persisted immediately.
- The remove action uses the shared TrashIcon instead of a plain "✕", and asks for confirmation via ConfirmModal — skipped only when the platform has no state/completion/playthroughs set (mirroring OwnedVersionsEditor's empty-copy shortcut).
- All existing fields/logic (Platform, CopyType, State, IsFullyCompleted/FullyCompletedAt, Playthroughs) and the data persisted to the DB are unchanged — this is UI/UX only.

Also, per your requests mid-task, I kept two test additions:
- MobileScreenshotTest.cs: added video-game-detail (mobile) and video-game-detail-desktop captures.
- New VideoGamePlatformSmokeTest.cs: an end-to-end Playwright test for the draft/save/cancel and confirm-before-remove flow, mirroring OwnershipSmokeTest.
Domain/schema (HouseModel, House entity, HouseDto):
- Removed Address, PostalCode, Country.
- City is now required on the model/entity; stays nullable on the DTO (forced by the InventoryPageBase<TDto> bare new() constraint — same as BookDto.Title), with the existing ToRequiredString fallback covering the gap.
- Added PropertyType enum (House/Apartment/Other) — a real discriminated enum, duplicated in WebApi.Contracts per the CarEnergyType/HouseEventType convention, required on the model, nullable on the DTO with a hand-written null→default fallback (mirroring CarDtoMapper.EnergyType).
- Added optional MovedInAt/MovedOutAt (DateOnly?, DateTime? in Mongo), wired through the shared CommonStorageMappings DateOnly↔DateTime conversion.

UI: Houses.razor list badges/add-form now show City + PropertyType instead of Address; HouseDetail.razor gets a PropertyType button row (matching TvShowDetail/VideoGameDetail's per-item selector pattern) plus City/Moved-in/Moved-out fields, replacing the old address block.

Migration: since the Mongo driver enforces C#'s required on read, I added scripts/migrate-house-schema.js to backfill city/property_type on existing documents (reporting affected houses for manual review) and drop the retired fields — run it once against any environment with existing house data before deploying.

Verified: full solution build succeeds (including Mapperly's escalated-to-error unmapped-member diagnostics), WebApi and BlazorApp unit test suites both pass. I didn't run the MongoDB-backed integration test for HouseResourceTest since I couldn't confirm a local MongoDB instance in this session — worth running dotnet test test/WebApi.IntegrationTests --filter-method "*HouseResourceTest*" yourself to confirm the full CRUD round-trip.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

@devpro devpro changed the title List order and health tracking V2.2 List order, health, ownership, favicon, mobile display Jul 17, 2026
@devpro
devpro merged commit 5cb7d52 into main Jul 17, 2026
7 of 8 checks passed
@devpro
devpro deleted the feature/health branch July 17, 2026 23:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants