Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions docs/plans/merge_my_into_available.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
name: Merge my into available
overview: Usunąć Moje pojazdy/składy; scalić w Dostępne z Odśwież/Dodaj/„Pokaż tylko moje”, filtrami kind/epoch, chipem epoki, podpisanymi akcjami; menu Moje; usunąć GET /vehicles i GET /trains.
todos:
- id: nav-routes
content: "Menu Moje: Dostępne pojazdy/składy; usuń Tabor; routing + redirecty fleet→my"
status: in_progress
- id: vehicles-toolbar
content: "AvailableVehicles: Odśwież, Dodaj, Pokaż tylko moje (default on)"
status: pending
- id: trains-toolbar
content: "AvailableTrains: Odśwież, Dodaj skład, Pokaż tylko moje (default on)"
status: pending
- id: frontend-my-hooks
content: Przepiąć TrainDialog/VehicleFunctionsPage z useMy* na catalogue; usuń useMyVehicles/useMyTrains
status: pending
- id: backend-cleanup
content: Usuń GET /vehicles i GET /trains + ListOwned/ListByOwner + testy; zostaw POST/PUT/DELETE i catalogue
status: pending
- id: kind-epoch-filters
content: "Belka pojazdów: filtry po typie (kind) i epoce (+ Pokaż tylko moje)"
status: pending
- id: epoch-chip
content: Chip epoki w szczegółach wiersza pojazdu (jeśli ustawiona, info/niebieski)
status: pending
- id: labeled-actions
content: "Akcje wiersza z podpisami; pojazdy: także Edytuj funkcje (z Moje)"
status: pending
- id: help-preserve
content: Zachować help myVehicles/myTrains na /my/vehicles i /my/trains (FAB z treścią Moje)
status: pending
- id: cleanup
content: Usuń My* pages/catalogues; popraw help/linki/i18n; typecheck + go test
status: pending
isProject: false
---

# Scalenie Moje → Dostępne (pojazdy i składy)

## Decyzja URL
Kanoniczne ścieżki: **`/my/vehicles`** i **`/my/trains`** (menu Moje). Stare `/fleet/vehicles` i `/fleet/trains` → redirect do `/my/*`. Strona funkcji zostaje pod `/my/vehicles/:vehicleId/functions`.

## Menu ([`AppShell.tsx`](bigfred/web/src/components/AppShell.tsx))
- Usunąć sekcję **Tabor** (`fleetItems` + `TopBarMenu` / mobile section).
- W **Moje** zamiast `nav.my.vehicles` / `nav.my.trains` wstawić pozycje z etykietami **Dostępne pojazdy** / **Dostępne składy** (`nav.fleet.availableVehicles` / `availableTrains`) wskazujące na `/my/vehicles` i `/my/trains`.
- Kolejność w Moje: Dostępne pojazdy, Dostępne składy, potem Wypożyczenia / Reszta jak dziś.

## Usunięcie starych widoków
Skasować (po przeniesieniu brakujących elementów do Available*):
- [`MyVehiclesPage.tsx`](bigfred/web/src/pages/MyVehiclesPage.tsx), [`MyVehiclesCatalogue.tsx`](bigfred/web/src/components/MyVehiclesCatalogue.tsx)
- [`MyTrainsPage.tsx`](bigfred/web/src/pages/MyTrainsPage.tsx), [`MyTrainsCatalogue.tsx`](bigfred/web/src/components/MyTrainsCatalogue.tsx)

W [`App.tsx`](bigfred/web/src/App.tsx):
- Route `/my/vehicles` → `AvailableVehiclesPage`, `/my/trains` → `AvailableTrainsPage`
- Redirect `/fleet/vehicles` → `/my/vehicles`, `/fleet/trains` → `/my/trains`

## Toolbar na Available (pojazdy i składy)

W [`AvailableVehiclesCatalogue.tsx`](bigfred/web/src/components/AvailableVehiclesCatalogue.tsx) i [`AvailableTrainsCatalogue.tsx`](bigfred/web/src/components/AvailableTrainsCatalogue.tsx):

1. **Odśwież listę** — `refetch()` catalogue (+ leases).
2. **Dodaj pojazd** / **Dodaj skład** — otwiera `VehicleDialog` / `TrainDialog` z `null` (create).
3. **Checkbox „Pokaż tylko moje”** — `useState(true)` przy każdym wejściu na widok (bez localStorage); filtr `ownerId === me.id` przed search/paginacją.
4. **Filtr typu pojazdu** — `Select` (single) na belce (opcje z `VEHICLE_KINDS` + „Wszystkie”); filtr `kind`. Single-select (4 zamknięte wartości) — prostsze i wystarczające.
5. **Filtr epoki** — `Select` (single) (opcje z `VEHICLE_EPOCHS` + „Wszystkie” / „Bez epoki”); filtr po `epoch` (puste `epoch` → łapane przez „Bez epoki”).

Kolejność filtrów na belce (pojazdy): checkbox „Pokaż tylko moje” → typ → epoka → Odśwież → Dodaj. Składy: tylko checkbox + Odśwież + Dodaj (bez kind/epoch).

Reset `page` przy zmianie dowolnego filtra (jak przy search). Wartości filtrów w `useState` lokalnym (nie localStorage) — domyślnie typ/epoka = wszystkie, mineOnly = true.

Dla pojazdów: `headerExtra` w [`VehiclesCatalogueTable`](bigfred/web/src/components/vehicles/VehiclesCatalogueTable.tsx) albo toolbar nad tabelą w AvailableVehiclesCatalogue. Filtry kind/epoch mogą być propsami table (`toolbarFilters`) albo żyć w AvailableVehiclesCatalogue i filtrować `rows` przed przekazaniem.

## Epoka w szczegółach wiersza (pojazdy)

W górnym podwierszu atrybutów (obok numeru i chipa „Na makiecie”), gdy `epoch` jest niepuste:

- Chip w stylu jak on-layout, ale **niebieski** — MUI `color="info"` (filled lub outlined).
- Etykieta np. `Epoka {{epoch}}` / `Epoch {{epoch}}` / `Epoche {{epoch}}` (`catalogue.epochChip`), wartość jak w dialogu (`III`, `IVa`, …).
- Puste `epoch` → brak chipa.
- Dodać `epoch` do typu wiersza w `VehiclesCatalogueTable` + mapowanie z catalogue; uwzględnić epokę w search haystack.

## Podpisane akcje w wierszu (pojazdy i składy)

Zamiast samych `IconButton` + tooltip — **`Button` `size="small"`** z `startIcon` i widocznym tekstem (istniejące klucze i18n):

- Dodaj do makiety / usuń z makiety (`list.actions.addToLayout`, `roster.removeButton`)
- Wypożycz (`rentals:granted.lend`)
- **Edytuj funkcje** (`list.actions.editFunctions`, ikona `Tune`) — **przenieść z Moje pojazdy na scaloną listę pojazdów**; widoczne dla właściciela / admina (jak edit/delete); nawigacja do `/my/vehicles/:id/functions`
- Edytuj / Usuń (`list.actions.edit` / `delete`)

Na składach: te same wzorce bez „Edytuj funkcje”. `variant="text"` lub `outlined`, `flexWrap` na wąskich ekranach.

## Frontend: usunięcie `useMyVehicles` / `useMyTrains`

Hooki wołają wyłącznie `GET /api/v1/vehicles` i `GET /api/v1/trains`. Po usunięciu stron My* nadal używane w:

- [`TrainDialog.tsx`](bigfred/web/src/components/TrainDialog.tsx) — lista pojazdów do składu → `useVehicleCatalogue(layoutId)` + filtr `ownerId === me.id` (ew. admin: własne + potrzebne do edycji cudzego składu — zachować możliwość wyboru pojazdów właściciela składu / własne).
- [`VehicleFunctionsPage.tsx`](bigfred/web/src/pages/VehicleFunctionsPage.tsx) — lookup pojazdu po id → `useVehicleCatalogue(layoutId)` (wymaga `useMe()` dla `layoutId`; `CatalogueVehicle` ma `name`/`epoch`/`kind` więc subtitle działa); przycisk „Wstecz” → `/my/vehicles`.

Potem usunąć `useMyVehicles` / `useMyTrains` z [`vehicles.ts`](bigfred/web/src/api/vehicles.ts).

**Uwaga TrainDialog:** przy tworzeniu składu użytkownik wybiera **własne** pojazdy — katalog z filtrem właściciela wystarczy. Przy edycji cudzego składu (admin) lista członków już jest w składzie; picker nowych członków: pojazdy właściciela składu albo wszystkie — **konkret: filtruj catalogue do `ownerId === editingTrain.ownerId` gdy edit, inaczej `me.id`**.

## Backend: uprzątnięcie endpointów „Moje”

Usunąć wyłącznie listę „owned-only” (strony Moje). **Zostawić** create/update/delete oraz catalogue:

| Usunąć | Zostawić |
|--------|----------|
| `GET /api/v1/vehicles` (`VehicleHandler.List`) | `GET /vehicles/catalogue`, `POST /vehicles`, `PUT/DELETE /vehicles/{id}`, by-external-id |
| `GET /api/v1/trains` (`TrainHandler.List`) | `GET /trains/catalogue`, `POST /trains`, `PUT/DELETE /trains/{id}`, patch members |

Warstwy do usunięcia, jeśli nic innego ich nie woła (stan dziś: tylko te handlery):

- [`cmd/vehicle.go`](bigfred/pkgs/bigfred/server/cmd/vehicle.go) — `ListOwned`
- [`cmd/train.go`](bigfred/pkgs/bigfred/server/cmd/train.go) — `ListOwned`
- [`repo/vehicles.go`](bigfred/pkgs/bigfred/server/repo/vehicles.go) — `ListByOwner` (nie mylić z lease `ListByOwner`)
- [`repo/trains.go`](bigfred/pkgs/bigfred/server/repo/trains.go) — `ListByOwner`
- Trasy w [`router.go`](bigfred/pkgs/bigfred/server/http/router.go)
- Testy unit pod `ListOwned` / HTTP List, jeśli istnieją

`ListCatalogue` / `ListAll` pozostają źródłem prawdy dla UI.

## Help (FAB) — treści z „Moje pojazdy” / „Moje składy”

Pływający przycisk pomocy ma **zostać** na scalonych podstronach z **tą samą treścią**, co dziś na Moje:

| Ścieżka kanoniczna | Wpis w [`helpRegistry.tsx`](bigfred/web/src/components/help/helpRegistry.tsx) | Klucz i18n |
|--------------------|-------------------------------------------------------------------------------|------------|
| `/my/vehicles` | `i18nKey: "myVehicles"` + `addIcon` / `functionsIcon` | `help.json` → `myVehicles` |
| `/my/trains` | `i18nKey: "myTrains"` | `help.json` → `myTrains` |

Ponieważ Available* renderujemy pod `/my/vehicles` i `/my/trains` (a `/fleet/*` tylko redirectuje), istniejące wpisy registry **nie wymagają przenosin** — nie dodawać osobnego helpa dla fleet, nie usuwać `myVehicles`/`myTrains`, nie zmieniać copy.

Weryfikacja po wdrożeniu: FAB widoczny na obu podstronach; dialog z ikonkami jak wcześniej; Menu → Pomoc nadal włącza ukryty FAB.

## Linki wewnętrzne
- [`RosterSection.tsx`](bigfred/web/src/components/RosterSection.tsx): manage → `/my/vehicles`, `/my/trains`.

## i18n
- `vehicle:catalogue.showOnlyMine` / `trainCatalogue.showOnlyMine` (pl/en/de).
- `vehicle:catalogue.filterKind` / `filterEpoch` / `filterAll` / `filterNoEpoch` (pl/en/de).
- `vehicle:catalogue.epochChip`: „Epoka {{epoch}}” / „Epoch {{epoch}}” / „Epoche {{epoch}}”.
- Reuse `list.refreshButton`, `list.addButton`, `trainList.addButton` oraz `vehicle:kind.*` dla etykiet typów.
- Usunąć nieużywane `nav.my.vehicles` / `nav.my.trains`; etykiety menu z `nav.fleet.available*`.
- Opcjonalnie usunąć lub ograniczyć martwe stringi `list.intro` / `trainList.intro` jeśli nikt ich nie renderuje.

## Poza zakresem
- Przebudowa tabeli składów do layoutu 2-kolumnowego jak pojazdy.
- Zmiana semantyki `POST /vehicles` / catalogue (bez nowych endpointów).
95 changes: 95 additions & 0 deletions docs/plans/unify_vehicles_catalogues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
name: Unify vehicles catalogues
status: completed
date: 2026-08-07
overview: "Uwspólnić tabele „Moje pojazdy” i „Dostępne pojazdy” w jeden komponent tabeli (bazujący na obecnym widoku dostępnych), z filtrem właściciela dla „Moje”, oraz przebudować wiersz: lewa kolumna nazwa+DCC, prawa kolumna atrybuty nad przyciskami."
todos:
- id: extract-table
content: Wydzielić VehiclesCatalogueTable + VehicleCatalogueRow (2-kolumnowy layout)
status: completed
- id: wire-available
content: Przepiąć AvailableVehiclesCatalogue na wspólny komponent
status: completed
- id: wire-my
content: Przepiąć MyVehiclesCatalogue (własne pojazdy, Add/Refresh, functions)
status: completed
- id: i18n-tsc
content: Uzupełnić i18n nagłówków jeśli potrzeba + typecheck
status: completed
---

# Uwspólnienie tabeli pojazdów + nowy układ wiersza

**Status:** zrealizowano (2026-08-07)

## Zakres
Tylko pojazdy (`MyVehiclesCatalogue` / `AvailableVehiclesCatalogue`). Składy i `RosterSection` na dashboardzie bez zmian.

## Podejście
Jeden wspólny komponent tabeli wyciągnięty ze struktury **Dostępne pojazdy** (wyszukiwanie, paginacja, Paper, Alert błędów mutacji). Strona **Moje pojazdy** używa tego samego komponentu z danymi własnych pojazdów (oraz dotychczasowymi akcjami: Dodaj/Odśwież w nagłówku, przycisk funkcji).

```mermaid
flowchart LR
MyPage["MyVehiclesPage"] --> MyCat["MyVehiclesCatalogue"]
AvailPage["AvailableVehiclesPage"] --> AvailCat["AvailableVehiclesCatalogue"]
MyCat --> Shared["VehiclesCatalogueTable"]
AvailCat --> Shared
MyCat -->|"useMyVehicles + layout set"| Shared
AvailCat -->|"useVehicleCatalogue"| Shared
```

Zachowanie danych bez zmian:
- **Moje:** `useMyVehicles()` + `useLayoutVehicles(layoutId)` do statusu na makiecie.
- **Dostępne:** `useVehicleCatalogue(layoutId)` z polem `onLayout`.
- Uprawnienia i zestaw przycisków pozostają w cienkich wrapperach / slotach.

## Nowy układ wiersza

Zamiast wielu kolumn (`Name | Number | DCC | OnLayout | Actions`) — **2 komórki**:

| Lewa | Prawa |
|------|-------|
| Nazwa (tooltip z id) | **Górny podwiersz:** numer + chip statusu na makiecie (Available) |
| Adres DCC w formacie `DCC: {num}` / chip Dummy | **Dolny podwiersz:** IconButtony (add/remove, lend, functions?, edit, delete) |
| Opcjonalnie caption właściciela (Available) | |

Technicznie: jedna `TableRow` z dwoma `TableCell`; prawa komórka to `Stack` z dwoma poziomymi `Stack`ami (attributes / actions). Nagłówek: „Pojazd” / „Szczegóły”.

## Zrealizowane pliki

- Nowy: `bigfred/web/src/components/vehicles/VehiclesCatalogueTable.tsx` — shell (Paper, search+pagination, loading/empty) + wiersz 2-kolumnowy.
- Przerobione: `MyVehiclesCatalogue.tsx`, `AvailableVehiclesCatalogue.tsx`.
- i18n: `vehicle.json` (pl/en/de) — `catalogue.table.*`, `catalogue.dccLabel`, dłuższe etykiety `onLayout.yes` / `onLayout.no`.

## Follow-upy po implementacji

- Chip: „Na makiecie” / „Nie jest na makiecie” (zamiast Tak/Nie).
- Adres DCC: `DCC: {{addr}}` (`catalogue.dccLabel`).

## API wspólnego komponentu

```tsx
type VehiclesCatalogueTableProps = {
rows: Array<{
id: string;
name: string;
number: string;
dccAddress: number | null;
onLayout: boolean;
ownerLabel?: string; // Available only
}>;
loading: boolean;
mutationError?: string | null;
showSearch?: boolean;
headerExtra?: ReactNode;
emptyLabel: string;
renderActions: (row) => ReactNode;
};
```

Na **Moje** `showSearch` włączone (wyszukiwanie lokalne).

## Poza zakresem
- Składy (`MyTrainsCatalogue` / `AvailableTrainsCatalogue`)
- Dashboard `RosterSection`
- Zmiany backendu / endpointów
9 changes: 0 additions & 9 deletions pkgs/bigfred/server/cmd/train.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,6 @@ func NewTrain(
}
}

// ListOwned returns every train owned by the user with member rows hydrated.
func (t *Train) ListOwned(ctx context.Context, ownerID uint) ([]TrainDetail, error) {
trains, err := t.trains.ListByOwner(ctx, ownerID)
if err != nil {
return nil, err
}
return t.hydrateDetails(ctx, trains)
}

// TrainCatalogueEntry is one row of the global train catalogue.
type TrainCatalogueEntry struct {
Train domain.Train
Expand Down
5 changes: 0 additions & 5 deletions pkgs/bigfred/server/cmd/vehicle.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,6 @@ func (v *Vehicle) GetByExternalID(ctx context.Context, externalID string) (domai
return row, nil
}

// ListOwned returns every vehicle owned by the user.
func (v *Vehicle) ListOwned(ctx context.Context, ownerID uint) ([]domain.Vehicle, error) {
return v.vehicles.ListByOwner(ctx, ownerID)
}

// VehicleCatalogueEntry is one row of the global vehicle catalogue.
type VehicleCatalogueEntry struct {
Vehicle domain.Vehicle
Expand Down
4 changes: 1 addition & 3 deletions pkgs/bigfred/server/http/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,7 @@ func NewRouter(cfg RouterConfig) http.Handler {
r.Put("/auth/me/profile", authH.UpdateProfile)
r.Get("/auth/me/dcc-pool", vehicleH.ListPool)

// Vehicle catalogue (own only for now).
r.Get("/vehicles", vehicleH.List)
// Vehicle catalogue.
r.Get("/vehicles/catalogue", vehicleH.ListCatalogue)
r.Post("/vehicles", vehicleH.Create)
r.Put("/vehicles/by-external-id/{externalId}", vehicleH.UpsertByExternalID)
Expand All @@ -177,7 +176,6 @@ func NewRouter(cfg RouterConfig) http.Handler {
r.Delete("/vehicle-templates/{id}/functions/{num}", functionH.DeleteTemplate)

// Train catalogue.
r.Get("/trains", trainH.List)
r.Get("/trains/catalogue", trainH.ListCatalogue)
r.Post("/trains", trainH.Create)
r.Put("/trains/{id}", trainH.Update)
Expand Down
20 changes: 0 additions & 20 deletions pkgs/bigfred/server/http/trains.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,6 @@ func NewTrainHandler(
return &TrainHandler{svc: svc, layoutTrains: layoutTrains, auth: auth}
}

// List handles GET /api/v1/trains — own trains only for now.
func (h *TrainHandler) List(w http.ResponseWriter, r *http.Request) {
id, ok := IdentityFromContext(r.Context())
if !ok {
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
return
}
rows, err := h.svc.ListOwned(r.Context(), id.User.ID)
if err != nil {
writeJSONError(w, http.StatusInternalServerError, "internal_error")
return
}
out := make([]protocol.TrainResponse, 0, len(rows))
for _, d := range rows {
out = append(out, protocol.ToTrainResponse(d))
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(out)
}

// ListCatalogue handles GET /api/v1/trains/catalogue — every
// registered train with owner metadata and on-layout flag for the
// caller's pinned session layout.
Expand Down
22 changes: 0 additions & 22 deletions pkgs/bigfred/server/http/vehicles.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,6 @@ func NewVehicleHandler(
return &VehicleHandler{svc: svc, layoutVehicles: layoutVehicles, pool: pool, auth: auth}
}

// List handles GET /api/v1/vehicles — own vehicles only for now.
// Leasing and signalman-overrides will join the union in the
// milestone that introduces VehicleLease.
func (h *VehicleHandler) List(w http.ResponseWriter, r *http.Request) {
id, ok := IdentityFromContext(r.Context())
if !ok {
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
return
}
rows, err := h.svc.ListOwned(r.Context(), id.User.ID)
if err != nil {
writeJSONError(w, http.StatusInternalServerError, "internal_error")
return
}
out := make([]protocol.VehicleResponse, 0, len(rows))
for _, v := range rows {
out = append(out, protocol.ToVehicleResponse(v))
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(out)
}

// ListCatalogue handles GET /api/v1/vehicles/catalogue — every
// registered vehicle with owner metadata and on-layout flag for the
// caller's pinned session layout.
Expand Down
13 changes: 0 additions & 13 deletions pkgs/bigfred/server/repo/trains.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,6 @@ func (t *Trains) CountByOwner(ctx context.Context, ownerID uint) (int, error) {
return t.repo.Count(ctx, "trains", where.Eq("owner_user_id", ownerID))
}

// ListByOwner returns every train owned by the user.
func (t *Trains) ListByOwner(ctx context.Context, ownerID uint) ([]domain.Train, error) {
var rows []domain.Train
err := t.repo.FindAll(ctx, &rows,
where.Eq("owner_user_id", ownerID),
sort.Asc("name"),
)
if err != nil {
return nil, err
}
return rows, nil
}

// ListAll returns every train in the catalogue (all owners).
func (t *Trains) ListAll(ctx context.Context) ([]domain.Train, error) {
var rows []domain.Train
Expand Down
13 changes: 0 additions & 13 deletions pkgs/bigfred/server/repo/vehicles.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,6 @@ func (v *Vehicles) ListAll(ctx context.Context) ([]domain.Vehicle, error) {
return rows, nil
}

// ListByOwner returns every vehicle owned by the user.
func (v *Vehicles) ListByOwner(ctx context.Context, ownerID uint) ([]domain.Vehicle, error) {
var rows []domain.Vehicle
err := v.repo.FindAll(ctx, &rows,
where.Eq("owner_user_id", ownerID),
sort.Asc("name"),
)
if err != nil {
return nil, err
}
return rows, nil
}

// ListByIDs returns vehicles by primary-key set.
func (v *Vehicles) ListByIDs(ctx context.Context, ids []domain.VehicleID) ([]domain.Vehicle, error) {
if len(ids) == 0 {
Expand Down
Loading
Loading