diff --git a/Distribution/docs/testbug.md b/Distribution/docs/testbug.md index 954c1f9..53cf28c 100644 --- a/Distribution/docs/testbug.md +++ b/Distribution/docs/testbug.md @@ -6,8 +6,7 @@ Running log of backend API failures found during testing and the corresponding f | # | Area | Backend API Failure | Frontend Change Needed | Date | |---|------|----------------------|--------------------------|------| -| 5 | Payments (Record Payment) | Cannot add new payment | Add a View icon to the action column in Payments table | 2026-09-01 | -| 6 | Sales → Visits (Route dropdown) | Selecting a specific route in the route dropdown does not filter route sales — all routes still show; check-in also not working | Fix route filter and check-in on Sales Visits page | 2026-09-02 | +| 6 | Sales → Visits (Route dropdown) | Selecting a specific route in the route dropdown does not filter route sales — all routes still show; check-in also not working | Fix route filter and check-in on Sales Visits page. **Partial fix landed (#49)**: `GET /stores?routeId=...` now actually filters server-side, so the store list for a selected route should now be correct — check-in itself still needs investigating | 2026-09-02 | | 7 | Catalogue → Categories (Subcategories) | Added subcategories don't display in the FE | Fix subcategory list rendering | Add a View icon to the action column in Categories table | 2026-09-02 | | 8 | Stock Balances | | Add a View icon to the action column in Stock Balances table; allow Edit | 2026-09-02 | | 9 | Stock Movements | Some rows missing From/To locations — every movement should always have both | Add a View icon and an Edit icon to the action column in Stock Movements table | 2026-09-02 | @@ -52,6 +51,14 @@ Running log of backend API failures found during testing and the corresponding f | 4 | Orders | `GET /orders` returns a plain array with no pagination support, but the frontend client treated it as a paginated response (same bug class as delivery tasks, #30) | No action icons on the Orders table; store shown as a raw ID | Fixed API client to match the real response shape; added a View icon + details modal; store name now resolved and shown instead of the raw store ID | 2026-09-03 | | 41 | Orders — TEMPORARY workaround | `UpdateAsync` had the same broken store-scope check as Delete/Sales (#28/#40) — always false, so status edits silently 404'd | Added Edit (status dropdown) and Delete icons to the Orders table, matching backend capability | TEMP: bypassed store-scope check in `OrderService.UpdateAsync`, same as `DeleteAsync`. Revert once auth (#28) is fixed | 2026-09-03 | | 42 | Orders (Delete) | `GetAllAsync` filtered `.Where(o => o.IsActive)` — Delete sets `IsActive=false`, so a deleted order vanished from the list entirely (violated the Implementation Guide's own rule) instead of staying visible as Cancelled, and any further `PUT` on it correctly 404'd since `UpdateAsync` also checks `IsActive` | — | Removed the `IsActive` filter from `GetAllAsync` — deleted orders now stay visible in the table with a Cancelled badge | 2026-09-03 | +| 43 | Payments | — | Table and View modal showed raw Store ID and Collected By user ID instead of names | Resolved and now show store name and collected-by user's full name (fetched from `/stores` and `/users`) | 2026-09-03 | +| 5 | Payments (Record Payment) — TEMPORARY workaround, resolved for now | `PaymentService.CreateAsync` threw an uncaught `UnauthorizedAccessException` (`currentUser.UserId` always unset, #28) — controller only catches `InvalidOperationException`, so this was an unhandled 500, not a clean error; separately, `CreateFlatPaymentRequest` never accepted `StoreId`/`CollectedAt` at all, yet the form required typing a raw Store ID and picking a date, both silently discarded | Rebuilt Record Payment form: Sale picked from a dropdown (auto-derives store), removed the dead Store ID/Collected-At fields, added a required "Collected By" user dropdown; added a View icon + details modal to the Payments table | TEMP: added `CollectedByUserId` fallback to `CreatePaymentRequest`/`CreateFlatPaymentRequest`, same pattern as Sale (#36). **Payment creation now works end-to-end** with the user picking themselves manually. **Still to do once real auth (#28) lands**: drop the `CollectedByUserId` fallback and go back to `currentUser.UserId` only, and remove the "Collected By" dropdown from the form — see Future Fixes #7–#8 below | 2026-09-03 | +| 44 | Stores | — | Route column showed the raw Route ID instead of the route name (table and the Store detail page) | Resolved and now shown as the route name (fetched from `/routes`) on both the Stores table and `/stores/{id}` detail page | 2026-09-04 | +| 45 | Stores (map, dev only) | — | Any store page with the map (create/edit/view) crashed with "Cannot read properties of undefined (reading 'appendChild')" — react-leaflet's `MapContainer` doesn't support React Strict Mode's dev-only double-invoke of mount effects | Disabled `reactStrictMode` in `next.config.ts`. Dev-only issue; production builds never double-invoke effects so this was never a prod bug — restart the dev server to pick up the change | 2026-09-04 | +| 46 | Stores (Edit / Activate-Deactivate) | `UpdateStoreRequest` had no `IsActive` field at all — unchecking "Active" on the Edit Store page, and the Activate/Deactivate row action on the Stores table (both use `PUT /stores/{id}`), silently had no effect since the backend DTO dropped the field entirely | — | Added `IsActive` to `UpdateStoreRequest` and applied it in `StoreService.UpdateAsync` | 2026-09-04 | +| 47 | Stores (list/detail hid inactive) | `GetAllAsync` filtered `.Where(s => s.IsActive)` (same violation as Orders, #42) — after #46's fix actually started persisting the deactivation, the store just vanished from the list instead of showing Inactive; `GetByIdAsync`/`UpdateAsync` also blocked viewing or re-editing any already-inactive store, so it couldn't even be reactivated | — | Removed the `IsActive` filter from `GetAllAsync`; removed the `!IsActive` guards from `GetByIdAsync`/`UpdateAsync` so a deactivated store stays visible and can still be viewed/reactivated | 2026-09-04 | +| 48 | Stores (Activate/Deactivate row action, View visit history) | The table's Activate/Deactivate action sent only `{ isActive }` to `PUT /stores/{id}`, which requires the FULL record (RouteId, OwnerName, StoreName, etc. all required/non-nullable) — missing fields bound to C# defaults (`RouteId` → `Guid.Empty`), corrupting the store or failing an FK constraint; `GetVisitHistoryAsync` also still had the stale `!store.IsActive` guard, so any store corrupted/deactivated by the above would 404 on View Visit History too | — | Added a dedicated `PATCH /stores/{id}/status` endpoint (`StoreService.SetActiveStatusAsync`) that only touches `IsActive`; wired the row action to it instead of the full update; removed the stale `IsActive` guard from `GetVisitHistoryAsync` | 2026-09-04 | +| 49 | Stores (list pagination/filtering) | `GET /stores` accepted no query params at all and returned a plain array, while the frontend `serverSide: true` table + `stores.list({ routeId, kycStatus, search, page, pageSize })` callers (Stores table, Orders/Payments/Sale-new/Route-manage/Sales-Visits store pickers) expected real server-side filtering and a paginated response — pagination totals were broken and filters (including the route filter behind #6) were silently ignored | — | Implemented real filtering + pagination in `StoreRepository`/`StoreService`/`StoreController` (`routeId`, `kycStatus`, `search`, `page`, `pageSize`), returning a proper `PagedResponse` | 2026-09-04 | ## Future Fixes — Pending Real Auth @@ -63,7 +70,9 @@ Everything below is a stopgap that only exists because `Distribution/Middleware/ 4. **Create Sale page** (`/sales/new`) — remove the "Created By" user dropdown and its form field/validation entirely; the acting user should come from the session, not be picked manually. 5. **`OrderService.UpdateAsync`** — restore the `HasStoreScopeAsync(order.FK_StoreId)` check that was bypassed in #41 (`DeleteAsync`'s equivalent check was bypassed earlier for the same reason). 6. **Sweep for the same pattern elsewhere** — `HasStoreScopeAsync`/`HasGlobalScope`-style checks exist in other services too (Store, etc., per #35's affected controllers) and likely have the same silent-fail behavior; audit and fix once auth is real, not just Sales/Orders. -7. **Re-verify `StatusCode(403)` usages from #35** — once real authentication exists, confirm whether any of those 6 call sites should become genuine ASP.NET `[Authorize]`-driven `403`/`401` responses instead of manual scope checks. +7. **`PaymentService.CreateAsync` / `CreatePaymentRequest.CollectedByUserId`** (`PaymentDTOs.cs`) — remove this fallback field, same as Sale's `CreatedByUserId` (#5); go back to `currentUser.UserId` only. +8. **Record Payment form** (`payment-sheet.tsx`) — remove the "Collected By" user dropdown once auth is fixed. +9. **Re-verify `StatusCode(403)` usages from #35** — once real authentication exists, confirm whether any of those 6 call sites should become genuine ASP.NET `[Authorize]`-driven `403`/`401` responses instead of manual scope checks. ## Implementation Guide diff --git a/distribution_fe/app/(dashboard)/stores/_components/stores-client.tsx b/distribution_fe/app/(dashboard)/stores/_components/stores-client.tsx index 6b8b3fe..cff3fb4 100644 --- a/distribution_fe/app/(dashboard)/stores/_components/stores-client.tsx +++ b/distribution_fe/app/(dashboard)/stores/_components/stores-client.tsx @@ -10,8 +10,8 @@ import type { TableConfig } from '@/components/shared/data-table' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/toast' -import { stores as storesApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { StoreResponse, KycStatus } from '@/types/api' +import { stores as storesApi, routes as routesApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { StoreResponse, KycStatus, RouteResponse } from '@/types/api' import { StoreSheet } from './store-sheet' import { StoreVisitHistorySheet } from './store-visit-history-sheet' @@ -34,6 +34,12 @@ export function StoresClient() { const [editing, setEditing] = useState(null) const [historyOpen, setHistoryOpen] = useState(false) const [viewingHistory, setViewingHistory] = useState(null) + const [routesList, setRoutesList] = useState([]) + + const routeNameById = useMemo( + () => new Map(routesList.map((r) => [r.id, r.routeName])), + [routesList] + ) const fetchData = useCallback(async (p: number, ps: number) => { setLoading(true) @@ -52,6 +58,12 @@ export function StoresClient() { fetchData(page, pageSize) }, [fetchData, page, pageSize]) + useEffect(() => { + routesApi.list({ pageSize: 200 }) + .then((res) => setRoutesList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load routes', description: getApiError(err), type: 'error' })) + }, []) + function openCreate() { setEditing(null) setSheetOpen(true) @@ -69,7 +81,7 @@ export function StoresClient() { async function toggleActive(store: StoreResponse) { try { - await storesApi.update(store.id, { isActive: !store.isActive }) + await storesApi.updateStatus(store.id, !store.isActive) toast.add({ title: store.isActive ? 'Store deactivated' : 'Store activated', type: 'success', @@ -102,8 +114,11 @@ export function StoresClient() { { key: 'routeId', title: 'Route', - fields: [{ key: 'routeId' }], + fields: ['routeId'], minWidth: 140, + cellRender: (row) => ( + {routeNameById.get(row.routeId) ?? row.routeId} + ), }, { key: 'kycStatus', @@ -159,7 +174,7 @@ export function StoresClient() { }, ], }, - }), [pageSize, openEdit, openHistory, toggleActive]) + }), [pageSize, openEdit, openHistory, toggleActive, routeNameById]) return ( <> diff --git a/distribution_fe/lib/api/stores.ts b/distribution_fe/lib/api/stores.ts index 4188802..c086c77 100644 --- a/distribution_fe/lib/api/stores.ts +++ b/distribution_fe/lib/api/stores.ts @@ -45,6 +45,9 @@ export const stores = { update: (id: string, body: Partial) => api.put(`/stores/${id}`, body).then(r => r.data), + updateStatus: (id: string, isActive: boolean) => + api.patch(`/stores/${id}/status`, { isActive }).then(r => r.data), + outstandingBalance: (id: string) => api.get(`/stores/${id}/outstanding-balance`).then(r => r.data),