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 <noreply@anthropic.com>
192 lines
10 KiB
Markdown
192 lines
10 KiB
Markdown
---
|
|
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) | `<a class="order-notes-badge" href="/orders/{id}#notes">[N]</a>` 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 | `<script>` dla `order-notes.js` |
|
|
| `resources/lang/pl.php` | Modified | 9 nowych kluczy `orders.details.notes_user_*` + `notes_imported_title` |
|
|
| `resources/scss/app.scss` | Modified | `@use "modules/order-notes"` |
|
|
| `public/assets/css/app.css` | Modified | Rebuilt by `npm run build:css` |
|
|
|
|
## Decisions Made
|
|
|
|
| Decision | Rationale | Impact |
|
|
|----------|-----------|--------|
|
|
| Reuse `order_notes` z `note_type='user'` | Jedna tabela = mniej obiektow DB, prosciej testowac, UNIQUE NULL nie koliduje | Importowane notatki ze zrodla nadal dzialaja niezmienione; w `loadOrderNotes()` dorzucony filtr `note_type <> 'user'` |
|
|
| Brak admin override (tylko autor edit/delete) | Brak systemu rol w aplikacji (`grep is_admin\|role=` → 0 hits) | Operator ktory dodal notatke moze ja modyfikowac; admin override odlozony do osobnej fazy gdy beda role |
|
|
| Badge `[N]` w `order_ref` (NIE osobna kolumna) | Minimalny footprint w tabeli, spojnie z `risk-return-badge` (przy `buyer_name`) | Badge widoczny bez zmian w naglowkach tabeli `/orders/list` |
|
|
| Body limit 2000 znakow (`mb_strlen`) | TEXT moze przechowac wiecej, ale UX podpowiada krotkie notatki; spojnie z polem comment | Walidacja w `OrderNotesService::sanitizeBody()` — rzut `InvalidArgumentException` gdy przekroczenie |
|
|
| Migracja idempotentna z DDL no-op fallback | Decyzja z Phase 115/125 — `SELECT 1` powoduje SQLSTATE 2014 przy PDO unbuffered | Re-run migracji = no-op (`ALTER TABLE COMMENT`) bez bledu |
|
|
| Edit toggle (ukryty form per notatka) zamiast modala | Mniej UI ceremoniaiu, spojnie z istniejacym `order-event` layoutem | JS prosty (show/hide pary `js-order-note-body` ↔ `js-order-note-edit-form`) |
|
|
|
|
## Deviations from Plan
|
|
|
|
### Summary
|
|
|
|
| Type | Count | Impact |
|
|
|------|-------|--------|
|
|
| Auto-fixed | 1 | Cosmetic naming |
|
|
| Scope additions | 0 | None |
|
|
| Deferred | 0 | None |
|
|
|
|
**Total impact:** Minimalne deviation — plan wykonany niemal 1:1.
|
|
|
|
### Auto-fixed Issues
|
|
|
|
**1. [Naming] `formatOrderRow` → `toTableRow`**
|
|
- **Found during:** Task 2 (badge w controllerze)
|
|
- **Issue:** Plan referowal do nieistniejacej metody `formatOrderRow()` w `OrdersController`
|
|
- **Fix:** Edycja `toTableRow()` (rzeczywista nazwa metody) — semantyka identyczna
|
|
- **Files:** `src/Modules/Orders/OrdersController.php`
|
|
- **Verification:** `grep -n "public function|toTableRow"` potwierdzilo `toTableRow` jako wlasciwa nazwa
|
|
- **Commit:** N/A (jeden Task 2 commit obejmie wszystkie zmiany)
|
|
|
|
### Deferred Items
|
|
|
|
None — plan executed exactly as written.
|
|
|
|
## Issues Encountered
|
|
|
|
| Issue | Resolution |
|
|
|-------|------------|
|
|
| Plan referowal `formatOrderRow()` (nieistniejacy) | Sprawdzono rzeczywiste metody przez `Grep "public function"` — `toTableRow` jest poprawna nazwa. Patch zaaplikowany. |
|
|
|
|
## Next Phase Readiness
|
|
|
|
**Ready:**
|
|
- Migracja czeka na operator (`php bin/migrate.php`).
|
|
- UI sekcji notatek + badge gotowe — manualny smoke test moze byc wykonany po migracji.
|
|
- Pattern `userNotesCountSubquerySql` + nullable `OrderNotesService` w `OrdersController` — gotowe do reuse w przyszlych phasach (np. event automatyzacji `note.created` lub admin override).
|
|
|
|
**Concerns:**
|
|
- Bez systemu rol nie ma admin override — jezeli operator chce zeby kazdy user mogl edytowac/usuwac kazda notatke, trzeba zmienic warunek w `OrderNotesService::update()/delete()` (usunac `AND user_id = :user_id`).
|
|
- Brak filtra "ma notatki" / "nie ma notatek" w liscie zamowien — kandydat na rozszerzenie jezeli operator zechce.
|
|
|
|
**Blockers:**
|
|
- None — plan wdrozony, smoke test po stronie operatora.
|
|
|
|
---
|
|
*Phase: 129-order-user-notes, Plan: 01*
|
|
*Completed: 2026-05-14*
|