From 48351b5f36b6d42f2543c87194d5cda0013b6e1b Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Thu, 14 May 2026 15:20:05 +0200 Subject: [PATCH] feat(129): order user notes module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRUD notatek autorskich operatora per zamowienie z badge [N] na liscie zamowien. Reuse istniejacej tabeli `order_notes` przez nowy `note_type='user'` z `user_id` (FK->users SET NULL) i `author_name` (snapshot). Sekcja `#notes` w "Wiadomosci i zalaczniki" w `/orders/{id}` z inline edit form + delete przez `OrderProAlerts.confirm`. Autoryzacja DB-level (`WHERE user_id = :user_id`, rowCount=0 ⇒ 403) — bez admin override (brak systemu rol w aplikacji). - Migracja `20260514_000116_*.sql` (ADD COLUMN user_id + author_name + FK + indeks `idx_order_notes_type_order`); idempotentne z DDL no-op fallback. - `OrderNotesService` (CRUD + walidacja body ≤ 2000 znakow); subquery `user_notes_count` w paginate; badge HTML w `toTableRow()`. - 3 routy POST /orders/{id}/notes(/update|/delete). - SCSS module `_order-notes.scss` + vanilla JS `order-notes.js` (inline edit toggle + delete confirm; idempotent guard). - 9 kluczy i18n PL; PROJECT.md + ROADMAP.md + tech_changelog.md + db_schema.md zaktualizowane. Follow-up: `php bin/migrate.php` + manualny smoke test (autor vs inny user + badge na /orders/list). Co-Authored-By: Claude Sonnet 4.6 --- .paul/PROJECT.md | 9 +- .paul/ROADMAP.md | 3 +- .paul/STATE.md | 25 +- .paul/changelog/2026-05-14.md | 29 ++ .paul/codebase/db_schema.md | 20 ++ .paul/codebase/tech_changelog.md | 24 ++ .../129-order-user-notes/129-01-PLAN.md | 295 ++++++++++++++++++ .../129-order-user-notes/129-01-SUMMARY.md | 191 ++++++++++++ ...00116_extend_order_notes_user_authored.sql | 57 ++++ public/assets/css/app.css | 2 +- public/assets/js/modules/order-notes.js | 89 ++++++ resources/lang/pl.php | 9 + resources/scss/app.scss | 1 + resources/scss/modules/_order-notes.scss | 112 +++++++ resources/views/layouts/app.php | 1 + resources/views/orders/show.php | 64 +++- routes/web.php | 7 +- src/Modules/Orders/OrderNotesService.php | 182 +++++++++++ src/Modules/Orders/OrdersController.php | 148 ++++++++- src/Modules/Orders/OrdersRepository.php | 18 +- 20 files changed, 1261 insertions(+), 25 deletions(-) create mode 100644 .paul/phases/129-order-user-notes/129-01-PLAN.md create mode 100644 .paul/phases/129-order-user-notes/129-01-SUMMARY.md create mode 100644 database/migrations/20260514_000116_extend_order_notes_user_authored.sql create mode 100644 public/assets/js/modules/order-notes.js create mode 100644 resources/scss/modules/_order-notes.scss create mode 100644 src/Modules/Orders/OrderNotesService.php diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index ab1efad..1d08770 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -13,8 +13,8 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów | Attribute | Value | |-----------|-------| | Version | 3.7.0-dev | -| Status | v3.7 in progress — Phases 113-128 shipped (Fakturownia + HostedSMS/SMSPLANET + Alert unify + receipt VAT + SMS templates + invoice_requested import fix + invoice GUS mapping + polkurier foundation + polkurier shipment service) | -| Last Updated | 2026-05-14 (Phase 128 closed) | +| Status | v3.7 in progress — Phases 113-129 shipped (Fakturownia + HostedSMS/SMSPLANET + Alert unify + receipt VAT + SMS templates + invoice_requested import fix + invoice GUS mapping + polkurier foundation + polkurier shipment service + order user notes) | +| Last Updated | 2026-05-14 (Phase 129 closed) | ## Requirements @@ -128,6 +128,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów - [x] Bugfix detekcji faktury przy imporcie: shopPRO order z `firm_nip` ustawia `invoice_requested=1` (mapper jako jedyne zrodlo heurystyki, sync service propaguje `aggregate['invoice_detected']`); Allegro rozszerzony o `naturalPerson=false`/`address.taxId`/`companyName` (wczesniej tylko `invoice.required`); usunieta legacy kolumna `orders.is_invoice` (Phase 115 dryft) + backfill 7 zamowien — Phase 125 - [x] Integracja polkurier.pl (fundament): pojedyncza globalna konfiguracja w `/settings/integrations/polkurier`, szyfrowany Token API + login, karta w hubie integracji obok Apaczki i realny test polaczenia przez `apimetod=test_auth_api` zweryfikowany na zywym koncie operatora; `ShipmentProviderRegistry` netkniety — `PolkurierShipmentService/TrackingService` w kolejnych fazach — Phase 127 - [x] polkurier ShipmentService + TrackingService + UI prepare panel: pelen kontrakt API (createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers), `PolkurierShipmentService` implementujacy `ShipmentProviderInterface` z normalizacja shipmenttype (lowercase) i splitem ulicy na street/housenumber/flatnumber, `PolkurierTrackingService` mapujacy statusy O/P/A/WP/D/Z/W na znormalizowane, panel "polkurier" w `prepare.php` z dynamiczna lista uslug z `available_carriers`, seed migracja `delivery_status_mappings(provider='polkurier')` z 7 wpisami z PDF v1.11; live test na #114/#115 zakonczony sukcesem po 4 iteracjach (ReferenceError → uppercase shipmenttype → orderno parsing → A4/A6); rozmiar etykiety sterowany w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet), NIE przez API — Phase 128 +- [x] Order User Notes module (Phase 129): pelen CRUD notatek autorskich operatora per zamowienie. Reuse `order_notes` przez nowy `note_type='user'` z `user_id` (FK→users SET NULL) + `author_name` (snapshot) + indeks `idx_order_notes_type_order`. `OrderNotesService` z autoryzacja DB-level (`WHERE user_id = :user_id`, rowCount=0 ⇒ 403). Sekcja `#notes` w "Wiadomosci i zalaczniki" w `/orders/{id}` z inline edit form + delete przez `OrderProAlerts.confirm`. Badge `[N]` (indigo neutralny) przy nr zamowienia na `/orders/list` (subquery `user_notes_count` w paginate). Brak admin override (brak systemu rol w aplikacji) — edit/delete tylko dla autora — Phase 129 ### Deferred @@ -249,6 +250,10 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API | polkurier API nie ma parametru rozmiaru etykiety (A4/A6) | Zweryfikowane na PDF v1.11: `get_label` przyjmuje wylacznie `orderno: Array`, `create_order` nie ma pola format/size. Rozmiar sterowany w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet) — operator zmienia preferencje konta jednorazowo. `polkurier_integration_settings.default_label_format` (PDF/ZPL/EPL) odnosi sie tylko do typu pliku, NIE rozmiaru. | 2026-05-14 | Active | | Brak dedykowanego selektora punktow paczkomatowych w UI polkurier (Phase 128) | Istnieje juz pole `name="receiver_point_id"` w sekcji Adres odbiorcy z auto-fillem `parcel_external_id` z importu zamowienia — operator wpisuje ID recznie (np. `POP-RZE54`). Usuniete: `lookupPickupPoints`/`ShipmentController::polkurierPoints`/AJAX route/JS handler. `getInpostParcelMachines`/`getCourierPoints` zachowane jako stuby w API client — gotowe dla kolejnej fazy paczkomaty UI. | 2026-05-14 | Active | | Diagnostyka silent-fail w ShipmentService — zapis surowej odpowiedzi do `error_message` | Gdy parsing API odpowiedzi zwraca pusty wynik mimo `status=success` (np. nieznany shape pola order number), zapisuj fragment surowej odpowiedzi (400 znakow) do `shipment_packages.error_message` — widoczne operatorowi w UI bez podgladu serwerowych logow. Pattern uratowal 3. iteracje live testu Phase 128. Reuse dla nowych integracji z API o nieznanym shape odpowiedzi. | 2026-05-14 | Active | +| `order_notes` jako jedna tabela dla notatek importowanych ze zrodla i autorskich operatora (Phase 129) | Reuse istniejacej tabeli przez nowy `note_type='user'` z `user_id`/`author_name` — mniej obiektow DB, jeden punkt zarzadzania. UNIQUE `(order_id, source_note_id)` nadal dziala bo MySQL traktuje wiele NULL jako unique (user notes maja `source_note_id=NULL`). `loadOrderNotes()` zawezone do `note_type <> 'user'`; notatki autorskie ladowane przez `OrderNotesService::listUserNotes()`. | 2026-05-14 | Active | +| Autoryzacja CRUD przez `WHERE user_id = :user_id` + rowCount=0 ⇒ `RuntimeException(403)` (Phase 129) | Eliminacja konieczności osobnego SELECT pre-check'a — atomowy UPDATE/DELETE z filtrem user_id robi to w jednym query. Wzorzec do reuse dla innych zasobow "ownership-based" w aplikacji. | 2026-05-14 | Active | +| Brak admin override dla notatek (Phase 129) — tylko autor edit/delete | Aplikacja nie ma systemu rol (`grep is_admin\|role=` zwrocil 0 trafien). Odlozone do osobnej fazy gdy beda role; obecnie operator ktory dodal notatke moze ja modyfikowac, inni widzą ale nie modyfikują. | 2026-05-14 | Deferred | +| Badge `[N]` w `order_ref` przy nr zamowienia (Phase 129) — neutralny indigo, NIE alertowy | Subtelniejszy niz `.risk-return-badge` (czerwony, alertowy) — notatki to informacja, nie ostrzezenie. Klik scrolluje do `#notes` w szczegolach zamowienia. Pattern do reuse dla kolejnych metryk per-order (np. liczba SMS, liczba dokumentow). | 2026-05-14 | Active | ## Success Metrics diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 3122916..b6b9dec 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -28,6 +28,7 @@ Wystawianie faktur dla klientow z NIP poprzez integracje z Fakturownia (app.fakt | 126 | Invoice GUS Field Mapping Fix (KRS-based heuristic: JDG → name do "Imię i nazwisko", spółka → "Nazwa firmy") | 1/1 | Complete (2026-05-13; manual smoke pending operator) | | 127 | polkurier Integration Foundation (single-instance settings + Token API + realny test polaczenia; obok Apaczki) | 1/1 | Complete (2026-05-14; live API verified — `Autoryzacja: 1`) | | 128 | polkurier ShipmentService + TrackingService + UI prepare panel + delivery_status_mappings seed (live test na #114/#115) | 1/1 | Complete (2026-05-14; live test passed po 4 iteracjach; migracja + cron tracking weryfikacja pending) | +| 129 | Order User Notes module (extend `order_notes` o user_id/author_name/note_type='user' + pelen CRUD restricted to author + badge `[N]` na liscie zamowien) | 1/1 | Complete (2026-05-14; migracja + manualny smoke pending operator) | Planowane kolejne fazy v3.7 (kandydaci, do rozplanowania): - polkurier TrackingService + `delivery_status_mappings` (provider='polkurier') @@ -512,4 +513,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md` --- *Roadmap created: 2026-03-12* -*Last updated: 2026-05-14 - Phase 128 UNIFY closed (live test passed, etykiety A6 OK)* +*Last updated: 2026-05-14 - Phase 129 UNIFY closed (Order User Notes module; migracja + smoke pending operator)* diff --git a/.paul/STATE.md b/.paul/STATE.md index 459312e..6c0bbf0 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -10,14 +10,14 @@ See: .paul/PROJECT.md (updated 2026-05-07) ## Current Position Milestone: v3.7 Invoices (Fakturownia integration) - In progress -Phase: 128 of TBD (polkurier ShipmentService + Tracking + UI) - Complete -Plan: 128-01 complete (SUMMARY.md created) +Phase: 129 of TBD (Order User Notes module) - Complete +Plan: 129-01 complete (SUMMARY.md created) Status: UNIFY complete, transition pending (git commit + Decisions w PROJECT.md + ROADMAP status) -Last activity: 2026-05-14 - Phase 128-01 UNIFY zakonczony, SUMMARY + changelog utworzone +Last activity: 2026-05-14 - Phase 129-01 UNIFY zakonczony, SUMMARY + changelog utworzone Progress: -- Milestone v3.7: [##########] ~99% (Phase 113-128 complete; transition pending) -- Phase 128: [##########] 100% +- Milestone v3.7: [##########] ~99% (Phase 113-129 complete; transition pending) +- Phase 129: [##########] 100% ## Loop Position @@ -30,18 +30,18 @@ PLAN -> APPLY -> UNIFY ## Session Continuity Last session: 2026-05-14 -Stopped at: Phase 128-01 UNIFY closed; SUMMARY.md created -Next action: Phase transition (git commit `feat(128): polkurier shipment service + tracking + UI prepare` + Decisions w PROJECT.md + ROADMAP status update), potem wybor kolejnego kandydata v3.7 (paczkomaty UI / shipment_presets polkurier / OrderValuationV2 / invoice.created event / eksport XLSX faktur) -Resume file: .paul/ROADMAP.md +Stopped at: Phase 129-01 UNIFY closed; SUMMARY.md created +Next action: Phase transition (git commit `feat(129): order user notes module` + Decisions w PROJECT.md + ROADMAP status update), potem wybor kolejnego kandydata v3.7 (paczkomaty polkurier UI / event automatyzacji note.created / eksport XLSX faktur / invoice.created event / admin override dla notatek po wprowadzeniu rol) +Resume file: .paul/phases/129-order-user-notes/129-01-SUMMARY.md ## Pending parallel work - None — Phase 118, 121, 122 wszystkie zacommitowane (8f14851, 360eef1). ## Git State -Last phase commit: 3443879 feat(127): polkurier integration foundation -Previous: c758ec7 feat(126): invoice GUS field mapping fix (JDG/KRS heuristic) -Branch: main (6 commits ahead of origin/main) +Last phase commit: c78ac33 feat(128): polkurier shipment service + tracking + UI prepare +Previous: 3443879 feat(127): polkurier integration foundation +Branch: main (7 commits ahead of origin/main) ## Pending Actions @@ -71,6 +71,9 @@ Branch: main (6 commits ahead of origin/main) - Phase 128 follow-up: uruchom migracje gdy XAMPP MySQL online: `php bin/migrate.php` (seed 7 wpisow `provider='polkurier'` w `delivery_status_mappings`). - Phase 128 follow-up: weryfikacja crona `shipment_tracking_sync` przy pierwszej zywej paczce polkurier w `in_transit` — sprawdz ze `shipment_packages.delivery_status` aktualizuje sie z `D`/`WP`/`Z` przez `DeliveryStatus::normalizeWithOverrides('polkurier', ...)`. - Phase 128 follow-up: rozmiar etykiety A4 vs A6 sterowany jest w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet), NIE przez API — operator ustawil A6. +- Phase 129 follow-up: uruchom migracje gdy XAMPP MySQL online: `php bin/migrate.php` (utworzy `order_notes.user_id` + `author_name` + FK + indeks `idx_order_notes_type_order`). +- Phase 129 follow-up: manualny smoke — `/orders/{X}` → sekcja "Notatki" widoczna, dodanie notatki tworzy wiersz + wpis w `order_activity_log`. Drugi user (`session.user_id != note.user_id`) nie widzi przycisków Edytuj/Usuń; POST `/notes/{noteId}/delete` jako inny user → 403 flash. +- Phase 129 follow-up: `/orders/list` → badge `[N]` widoczny przy zamówieniach z notatkami autorskimi; klik scrolluje do `#notes` w szczegółach. Sprawdzić że badge zwrotów (Phase 106) działa równolegle. ## Deferred to Next Milestones diff --git a/.paul/changelog/2026-05-14.md b/.paul/changelog/2026-05-14.md index bbcba34..6a5787d 100644 --- a/.paul/changelog/2026-05-14.md +++ b/.paul/changelog/2026-05-14.md @@ -53,3 +53,32 @@ - `.paul/phases/128-polkurier-shipment-service/128-01-PLAN.md` (nowy plik) - `.paul/phases/128-polkurier-shipment-service/128-01-SUMMARY.md` (nowy plik) - `.paul/phases/128-polkurier-shipment-service/polkurier-api-docs.txt` (nowy plik — referencyjna doca z PDF v1.11) + +## Co zrobiono (cd. — Phase 129) + +- [Phase 129, Plan 01] Order User Notes module — pelen CRUD notatek autorskich operatora per zamowienie z badge `[N]` na liscie zamowien. Reuse istniejacej tabeli `order_notes` przez nowy `note_type='user'` z `user_id` (FK→users SET NULL) i `author_name` (snapshot). Sekcja `#notes` w "Wiadomosci i zalaczniki" w szczegolach zamowienia z inline edit form + delete przez `OrderProAlerts.confirm`. +- Task 1: Migracja `20260514_000116_extend_order_notes_user_authored.sql` (ADD COLUMN user_id + author_name + FK + indeks `idx_order_notes_type_order`) z idempotentnymi `INFORMATION_SCHEMA` guard'ami i DDL no-op fallback'iem. +- Task 2: `OrderNotesService` (5 metod CRUD + autoryzacja przez `WHERE user_id = :user_id`, rowCount=0 ⇒ 403). `OrdersRepository::userNotesCountSubquerySql()` + kolumna `user_notes_count` w paginate. `OrdersController::storeNote/updateNote/deleteNote` + badge HTML w `toTableRow()`. 3 nowe POST routes. +- Task 3: Sekcja `#notes` w `show.php` (3 bloki — lista user notes + form dodawania + opcjonalny block "Wiadomosci ze zrodla"). SCSS `_order-notes.scss` z `.order-notes-badge` (indigo neutralny). JS `order-notes.js` (inline edit toggle + delete confirm). 9 nowych kluczy i18n PL. `npm run build:css` rebuilt. +- Auto-fix: plan referowal nieistniejaca metode `formatOrderRow()` — wlasciwa nazwa `toTableRow()` znaleziona przez Grep "public function". Edycja zaaplikowana w wlasciwej metodzie. +- Brak admin override w CRUD (decyzja podczas planowania): aplikacja nie ma systemu rol, autoryzacja przez `note.user_id = session.user_id` — odlozone do osobnej fazy. + +## Zmienione pliki (cd. — Phase 129) + +- `database/migrations/20260514_000116_extend_order_notes_user_authored.sql` (nowy plik) +- `src/Modules/Orders/OrderNotesService.php` (nowy plik) +- `src/Modules/Orders/OrdersController.php` (3 nowe akcje + badge HTML) +- `src/Modules/Orders/OrdersRepository.php` (subquery `user_notes_count` + `loadOrderNotes` zawezone do `note_type <> 'user'`) +- `routes/web.php` (3 nowe routes + `OrderNotesService` instancjonowany) +- `resources/views/orders/show.php` (sekcja `#notes` + inline edit form) +- `resources/views/layouts/app.php` (script `order-notes.js`) +- `resources/lang/pl.php` (9 kluczy `orders.details.notes_user_*` + `notes_imported_title`) +- `resources/scss/modules/_order-notes.scss` (nowy plik) +- `resources/scss/app.scss` (`@use "modules/order-notes"`) +- `public/assets/js/modules/order-notes.js` (nowy plik) +- `public/assets/css/app.css` (rebuilt) +- `.paul/codebase/db_schema.md` (sekcja `order_notes` rozszerzona) +- `.paul/codebase/tech_changelog.md` (wpis Phase 129) +- `.paul/STATE.md`, `.paul/ROADMAP.md` +- `.paul/phases/129-order-user-notes/129-01-PLAN.md` (nowy plik) +- `.paul/phases/129-order-user-notes/129-01-SUMMARY.md` (nowy plik) diff --git a/.paul/codebase/db_schema.md b/.paul/codebase/db_schema.md index 73cc472..2aa4b7f 100644 --- a/.paul/codebase/db_schema.md +++ b/.paul/codebase/db_schema.md @@ -328,6 +328,26 @@ UNIQUE: `(integration_id, external_order_id)` UNIQUE: `(order_id, source_payment_id)` +**order_notes** — Notatki przypisane do zamówienia (importowane ze źródła + autorskie operatora) +| Column | Type | Nullable | Notes | +|--------|------|----------|-------| +| `id` | BIGINT UNSIGNED | NO | PK, AUTO_INCREMENT | +| `order_id` | BIGINT UNSIGNED | NO | FK → orders(id) CASCADE | +| `source_note_id` | VARCHAR(64) | YES | ID notatki ze źródła (shopPRO/Allegro); NULL dla notatek autorskich | +| `note_type` | VARCHAR(32) | NO | `shoppro`/`allegro`/`message` (imported) lub `user` (Phase 129 — autorska notatka operatora) | +| `user_id` | INT UNSIGNED | YES | FK → users(id) ON DELETE SET NULL (Phase 129); set tylko dla `note_type='user'` | +| `author_name` | VARCHAR(190) | YES | Snapshot `users.name` w momencie tworzenia (Phase 129); chroni przed zmianą nazwy usera | +| `created_at_external` | DATETIME | YES | Data ze źródła (import); NULL dla `note_type='user'` | +| `comment` | TEXT | NO | Treść notatki (reuse dla `note_type='user'` jako body) | +| `payload_json` | JSON | YES | Raw payload ze źródła; NULL dla `note_type='user'` | +| `created_at` | DATETIME | NO | DEFAULT CURRENT_TIMESTAMP | +| `updated_at` | DATETIME | NO | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | + +UNIQUE: `(order_id, source_note_id)` — note: MySQL traktuje wiele NULL jako unique, więc nie blokuje wielu rekordów `note_type='user'` (source_note_id zawsze NULL). +Indexes: `order_notes_order_idx (order_id)`, `idx_order_notes_type_order (note_type, order_id)` (Phase 129 — wspiera subquery `user_notes_count` na liście zamówień i `listUserNotes`). + +> Note (Phase 129-01, 2026-05-14): Dodano `user_id`/`author_name` oraz `note_type='user'` dla notatek autorskich operatora. Edycja/usuwanie dozwolone tylko dla autora (`note.user_id === session.user_id`) — brak admin override (brak systemu ról w aplikacji). Importowane notatki ze źródła (`note_type IN ('shoppro','allegro','message')`) zachowują `user_id=NULL` i pozostają nieedytowalne. + --- ## Order Statuses diff --git a/.paul/codebase/tech_changelog.md b/.paul/codebase/tech_changelog.md index 9d6b054..5304e4e 100644 --- a/.paul/codebase/tech_changelog.md +++ b/.paul/codebase/tech_changelog.md @@ -1,5 +1,29 @@ # Technical Changelog +## 2026-05-14 - Phase 129 Plan 01: Order User Notes module + +**Co zrobiono:** +- Migracja `database/migrations/20260514_000116_extend_order_notes_user_authored.sql` — `order_notes` rozszerzona o `user_id INT UNSIGNED NULL` (FK → `users(id)` ON DELETE SET NULL), `author_name VARCHAR(190) NULL`, oraz indeks `idx_order_notes_type_order (note_type, order_id)`. Wszystkie ADD owinięte w `INFORMATION_SCHEMA` guard z DDL no-op fallback (`ALTER TABLE COMMENT`) — pattern Phase 115/125. +- `src/Modules/Orders/OrderNotesService.php` — nowy serwis CRUD nad `order_notes` z `note_type='user'`. Metody: `listUserNotes`, `listImportedNotes`, `countUserNotes`, `findById`, `create`, `update`, `delete`. Autoryzacja przez `WHERE user_id = :user_id` w UPDATE/DELETE — rowCount=0 ⇒ rzut `RuntimeException(code=403)`. Walidacja `body`: trim, niepuste, ≤ 2000 znakow. +- `src/Modules/Orders/OrdersRepository.php` — dodany `userNotesCountSubquerySql($orderAlias)` (subquery `COUNT(*) FROM order_notes WHERE note_type='user'`) używany w `paginateSql()` jako kolumna `user_notes_count`. `loadOrderNotes()` zawężony do `note_type <> 'user'` (importowane ze źródła). `transformOrderRow()` ekspozuje `user_notes_count`. +- `src/Modules/Orders/OrdersController.php` — nowa opcjonalna zależność `OrderNotesService` w konstruktorze (na końcu, nullable, BC-safe). 3 metody: `storeNote`, `updateNote`, `deleteNote` (każda CSRF + sesja + try/catch `RuntimeException`/`InvalidArgumentException`; rejestruje `order_activity_log event_type='note'` przez `OrdersRepository::recordActivity`). `toTableRow()` renderuje `[N]` obok numeru zamówienia gdy `user_notes_count > 0`. `show()` pobiera `userNotes` + `currentUserId` i przekazuje do widoku. +- `routes/web.php` — 3 nowe route'y `POST /orders/{id}/notes`, `POST /orders/{id}/notes/{noteId}/update`, `POST /orders/{id}/notes/{noteId}/delete`. `OrderNotesService` instancjonowany przed `new OrdersController(...)` i przekazany ostatnim argumentem. +- `resources/views/orders/show.php` — sekcja "Wiadomości i załączniki" przebudowana na 3 bloki: (1) `
` z listą notatek (data · autor) + akcjami edit/delete dla autora + inline formularz dodawania, (2) ukryty `order-note-edit-form` per notatka rozwijany przez JS, (3) opcjonalny blok "Wiadomości ze źródła" gdy `$notesList !== []` (importowane, bez akcji). +- `resources/lang/pl.php` — 10 nowych kluczy `orders.details.notes_user_*` / `notes_imported_title` (UI labels w PL). +- `resources/scss/modules/_order-notes.scss` (nowy) + `@use` w `app.scss` — `.order-notes-badge` (niebieskoszary `#eef2ff/#4338ca`), `.order-user-notes`, `.order-event--user` (lewa krawędź `#6366f1`), `.order-imported-notes` (opacity 0.75), `.btn-link`, `.order-note-form`, `.order-note-edit-form`. CSS przebudowany via `npm run build:css`. +- `public/assets/js/modules/order-notes.js` (nowy) + ` + ``` + + Avoid: natywnego `confirm()` (zakaz CLAUDE.md); inline styles (zakaz CLAUDE.md — wszystko do SCSS); duplikowania renderowania importowanych notatek; ujawniania `user_id` w UI jeśli `users.id` jest wrażliwe (nie jest — to wewnętrzny ID). + + + Otwórz `/orders/{X}` → widać sekcję "Notatki" + form dodawania; dodaj notatkę → pojawia się w liście z datą i autorem. Spróbuj edit cudzej notatki (z innym session.user_id) → przyciski edit/delete niewidoczne, próba POST → 403. Otwórz `/orders/list` → badge `[N]` widoczny przy zamówieniach z notatkami. Sprawdź czy `risk-return-badge` (Phase 106) nadal działa obok. + + AC-4, AC-5 satisfied: UI sekcji notatek działa w show.php, badge w list.php widoczny, akcje edit/delete poprawnie ograniczone do autora. + + + + + + +## DO NOT CHANGE +- `order_notes.comment`, `order_notes.source_note_id`, `order_notes.payload_json` (kontrakt importu z shopPRO/Allegro — Phase 79 i wcześniejsze). +- `OrdersRepository::replaceNotes()`/`loadOrderNotes()` semantyka dla importu — jeśli rename, zachowaj BC alias lub zaktualizuj wszystkie wywołania (delta-only import, Phase 112). +- `.risk-return-badge` SCSS i logika (Phase 106) — badge notatek to osobna klasa, nie modyfikujemy zwrotów. +- `OrderProAlerts.confirm` API — używamy options-object (Phase 114 decyzja). + +## SCOPE LIMITS +- Brak mentions/@-tagowania userów. +- Brak załączników do notatek (tylko tekst). +- Brak edycji historii edycji notatki (audit log w `order_activity_log` ma tylko `Dodano/Edytowano/Usunięto notatkę` — bez before/after JSON). +- Brak globalnych uprawnień admin override — tylko autor edytuje/usuwa (system ról nie istnieje; odłożone do osobnej fazy). +- Brak filtrów listy zamówień po "ma/nie ma notatki" — można dodać w przyszłości. +- Brak emitowania eventu automatyzacji `note.created` — można dodać jako osobny plan jeśli operator chce. + + + + +Before declaring plan complete: +- [ ] `php bin/migrate.php` przechodzi bez błędu, re-run = no-op +- [ ] `php -l` na każdym zmienionym pliku PHP zwraca "No syntax errors" +- [ ] POST `/orders/{id}/notes` jako user A tworzy notatkę +- [ ] POST `/orders/{id}/notes/{noteId}/update` jako user A działa, jako user B zwraca 403 +- [ ] POST `/orders/{id}/notes/{noteId}/delete` jako user A usuwa, jako user B 403 +- [ ] `/orders/list` pokazuje badge `[N]` przy zamówieniach z notatkami, ukryty gdy N=0 +- [ ] `/orders/{id}#notes` scrolluje do sekcji notatek +- [ ] Importowane notatki ze źródła (shopPRO/allegro) renderują się jako osobny blok bez przycisków edit/delete +- [ ] Badge zwrotów (Phase 106) działa obok badge'a notatek (oba widoczne dla zamówień z obojgiem) +- [ ] SCSS skompilowany do `public/assets/css/app.css` +- [ ] CSRF wymagany w każdym POST — brak tokenu = 419/403 +- [ ] Brak natywnych `confirm()` w nowym JS — wszystko przez `OrderProAlerts.confirm` + + + +- AC-1..AC-6 spełnione +- Brak regresji w imporcie notatek shopPRO/Allegro (delta-only import z Phase 112 nadal działa, `replaceNotes` filtruje tylko `note_type != 'user'` jeśli musi) +- Czas wykonania `/orders/list` z subquery `user_notes_count` nie pogarsza się drastycznie (indeks `idx_order_notes_type_order` aktywny) +- `.paul/codebase/db_schema.md` i `.paul/codebase/architecture.md` zaktualizowane o nowy serwis i kolumny +- `.paul/codebase/tech_changelog.md` ma wpis dla Phase 129 + + + +After completion, create `.paul/phases/129-order-user-notes/129-01-SUMMARY.md` z: +- Krótki opis co zostało zbudowane +- Decisions (np. brak admin override → tylko autor edytuje) +- Files modified (lista z task'ów) +- Migration applied (numer + opis) +- Manual smoke checklist dla operatora (POST create, UPDATE as A, UPDATE as B → 403, DELETE, badge na liście, edit modal UX) +- Deferred / follow-up (event automatyzacji `note.created`, filtr listy "ma notatki", admin override po wprowadzeniu ról) + diff --git a/.paul/phases/129-order-user-notes/129-01-SUMMARY.md b/.paul/phases/129-order-user-notes/129-01-SUMMARY.md new file mode 100644 index 0000000..70d42b3 --- /dev/null +++ b/.paul/phases/129-order-user-notes/129-01-SUMMARY.md @@ -0,0 +1,191 @@ +--- +phase: 129-order-user-notes +plan: 01 +subsystem: orders +tags: [order-notes, crud, badge, audit-log, user-authored] + +requires: + - phase: 106-customer-return-alert + provides: badge pattern (`risk-return-badge`) + subquery liczby per zamowienie + - phase: 114-accounting-configs-refactor + provides: `OrderProAlerts.confirm` options-object API +provides: + - Pelen CRUD notatek autorskich operatora per zamowienie (`note_type='user'`) + - Subquery `user_notes_count` + badge `[N]` na `/orders/list` + - Inline edit (toggle textarea) + delete z `OrderProAlerts.confirm` +affects: + - Przyszle fazy z eventem automatyzacji `note.created` lub admin override po wprowadzeniu rol + +tech-stack: + added: [] + patterns: + - "Reuse istniejacej tabeli przez nowy `note_type` zamiast tworzenia osobnej tabeli" + - "Autoryzacja CRUD przez `WHERE user_id = :user_id` + rowCount=0 ⇒ RuntimeException(403)" + +key-files: + created: + - database/migrations/20260514_000116_extend_order_notes_user_authored.sql + - src/Modules/Orders/OrderNotesService.php + - resources/scss/modules/_order-notes.scss + - public/assets/js/modules/order-notes.js + modified: + - src/Modules/Orders/OrdersController.php + - src/Modules/Orders/OrdersRepository.php + - routes/web.php + - resources/views/orders/show.php + - resources/views/layouts/app.php + - resources/lang/pl.php + - resources/scss/app.scss + +key-decisions: + - "Reuse `order_notes` przez `note_type='user'` zamiast osobnej tabeli (clarification #1)" + - "Badge neutralny `[N]` (indigo `#eef2ff/#4338ca`) — subtelniejszy niz `.risk-return-badge`" + - "Brak admin override — edit/delete tylko dla autora (brak systemu rol w aplikacji)" + - "Sekcja `#notes` w istniejacej karcie 'Wiadomosci i zalaczniki' — split na 'Notatki' (user) + 'Wiadomosci ze zrodla' (imported)" + +patterns-established: + - "`userNotesCountSubquerySql($orderAlias)` — wzorzec dla COUNT-per-order subquery bez wplywu na ORDER BY/GROUP BY (analogiczny do `customerReturnedCountSubquerySql`)" + - "`OrderProAlerts.confirm` z `danger:true` + options-object API dla submit'u formularza DELETE (preventDefault + onConfirm submit)" + - "Migracje no-op zawsze jako DDL (`ALTER TABLE COMMENT`), nigdy `SELECT 1` (Phase 115 pattern)" + +duration: ~30min +started: 2026-05-14T00:00:00Z +completed: 2026-05-14T00:00:00Z +--- + +# Phase 129 Plan 01: Order User Notes Summary + +**Pelen CRUD notatek autorskich operatora w zamowieniach (extend `order_notes` o `user_id`/`author_name`/`note_type='user'`), z sekcja `#notes` w szczegolach i badge `[N]` na liscie zamowien.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~30min | +| Tasks | 3 of 3 completed | +| Files created | 5 | +| Files modified | 7 | +| AC pass rate | 6 of 6 (pending live smoke) | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Migracja DB — kolumny user notes | Pass (code) | Migracja `20260514_000116_*.sql` z `information_schema` guard + DDL no-op fallback. Aktywacja na zywej bazie: pending operator. | +| AC-2: Tworzenie notatki | Pass (code) | `OrderNotesService::create()` + `OrdersController::storeNote()` + `recordActivity('note', 'Dodano notatke')`. Redirect 302 → `/orders/{id}#notes`. | +| AC-3: Edycja/usuwanie tylko autor | Pass (code) | UPDATE/DELETE z `WHERE user_id = :user_id`; rowCount=0 ⇒ `RuntimeException(403)`. UI ukrywa przyciski gdy `note.user_id != session.user_id`. | +| AC-4: Lista w "Wiadomosci i zalaczniki" | Pass (code) | Sekcja `#notes` z 3 blokami (lista user notes → form dodawania → opcjonalny block "Wiadomosci ze zrodla"). | +| AC-5: Badge `[N]` na liscie zamowien | Pass (code) | `[N]` wstrzykniete w `order_ref` HTML; widoczne tylko gdy `user_notes_count >= 1`. | +| AC-6: Subquery liczy bez wplywu na paginacje | Pass (code) | `userNotesCountSubquerySql('o')` jako kolumna SELECT (NIE w WHERE/GROUP BY/ORDER). Wspierane indeksem `idx_order_notes_type_order (note_type, order_id)`. | + +> **Live smoke pending**: migracja na zywym XAMPP MySQL + manualny test wieloosobowy (autor vs inny user) — udokumentowane w STATE.md follow-ups. + +## Verification Results + +``` +$ C:/xampp/php/php.exe -l src/Modules/Orders/OrderNotesService.php +No syntax errors detected + +$ C:/xampp/php/php.exe -l src/Modules/Orders/OrdersController.php +No syntax errors detected + +$ C:/xampp/php/php.exe -l src/Modules/Orders/OrdersRepository.php +No syntax errors detected + +$ C:/xampp/php/php.exe -l routes/web.php +No syntax errors detected + +$ C:/xampp/php/php.exe -l resources/views/orders/show.php +No syntax errors detected + +$ node --check public/assets/js/modules/order-notes.js +JS OK + +$ npm run build:css +sass --style=compressed --no-source-map resources/scss/app.scss public/assets/css/app.css +(rebuilt successfully) +``` + +## Accomplishments + +- **Reuse `order_notes` zamiast osobnej tabeli**: jedna tabela obsluguje 2 semantyki (`note_type IN ('shoppro','allegro','message')` = imported, `note_type='user'` = autorska). `UNIQUE (order_id, source_note_id)` nie blokuje user notes bo MySQL traktuje wiele NULL jako unique. +- **CRUD z autoryzacja DB-level**: UPDATE/DELETE filtruja po `user_id = :user_id` w SQL; `rowCount=0` rzuca 403 — eliminuje konieczność osobnego SELECT pre-check'a. +- **Badge widoczny od razu w liscie**: subquery `user_notes_count` ekspozuje liczbe w `paginate()`, badge wlozony w `toTableRow()` obok numeru zamowienia (klik scrolluje do `#notes`). + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `database/migrations/20260514_000116_extend_order_notes_user_authored.sql` | Created | ADD COLUMN user_id + author_name + FK → users + indeks (idempotentne) | +| `src/Modules/Orders/OrderNotesService.php` | Created | CRUD service nad `order_notes` (`note_type='user'`) z autoryzacja po user_id | +| `resources/scss/modules/_order-notes.scss` | Created | `.order-notes-badge`, `.order-user-notes`, `.order-event--user`, `.btn-link`, `.order-note-form` | +| `public/assets/js/modules/order-notes.js` | Created | Vanilla JS: inline edit toggle + `OrderProAlerts.confirm` na delete (idempotent guard) | +| `src/Modules/Orders/OrdersController.php` | Modified | Dodano `OrderNotesService` jako nullable dep + `storeNote/updateNote/deleteNote` + badge w `toTableRow()` | +| `src/Modules/Orders/OrdersRepository.php` | Modified | `userNotesCountSubquerySql()` + kolumna `user_notes_count` w paginate; `loadOrderNotes()` zawezone do `note_type <> 'user'` | +| `routes/web.php` | Modified | 3 nowe routes (POST notes/store|update|delete) + `OrderNotesService` instancjonowany + przekazany do `OrdersController` | +| `resources/views/orders/show.php` | Modified | Sekcja `#notes` rozbita na user-notes + form + imported-notes; per-note edit form (ukryty) | +| `resources/views/layouts/app.php` | Modified | ` +