feat(v1.6): inline status change on orders list
Phase 44 complete: - Clickable status badge opens dropdown with grouped statuses - AJAX POST changes status without page reload (optimistic update) - Fixed-position dropdown escapes table overflow:hidden - updateStatus() returns JSON for AJAX, redirect for standard POST Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,8 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Version | 1.0.0 |
|
||||
| Status | v1.5 Complete |
|
||||
| Last Updated | 2026-03-25 |
|
||||
| Status | v1.6 Complete |
|
||||
| Last Updated | 2026-03-27 |
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -52,10 +52,11 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
- [x] Ograniczenie szumu logow importu Allegro i deduplikacja wpisow activity log - Phase 41
|
||||
- [x] Automatyzacja: event `shipment.status_changed` + warunki statusowe przesylki - Phase 42
|
||||
- [x] Usuwanie wpisu z kolejki druku etykiet z panelu ustawien - Phase 43
|
||||
- [x] Szybka zmiana statusu zamowienia z listy zamowien (inline dropdown + AJAX) - Phase 44
|
||||
|
||||
### Active (In Progress)
|
||||
|
||||
- [ ] Brak aktywnych faz w milestone v1.5 (40-43 zakonczone)
|
||||
- [ ] Brak aktywnych faz
|
||||
|
||||
### Planned (Next)
|
||||
|
||||
@@ -147,5 +148,5 @@ Quick Reference:
|
||||
|
||||
---
|
||||
*PROJECT.md — Updated when requirements or context change*
|
||||
*Last updated: 2026-03-25 after Phase 40-43 completion (Operational Workflow Cleanup)*
|
||||
*Last updated: 2026-03-27 after Phase 44 completion (Inline Status Change)*
|
||||
|
||||
|
||||
@@ -6,25 +6,34 @@ orderPRO to narzÄ™dzie do wielokanaĹ‚owego zarzÄ…dzania sprzedaĹĽÄ
|
||||
|
||||
## Current Milestone
|
||||
|
||||
v1.5 Operational Workflow Cleanup - Complete (phases 40-43 complete)
|
||||
v1.6 Quick Status Change - Complete (2026-03-27)
|
||||
|
||||
Usprawnienia operacyjne po wdrozeniu modulu wydrukow i trackingu: usuniecie zbędnego bulk print z listy zamowien, ograniczenie szumu logow importu Allegro, rozszerzenie automatyzacji o zdarzenia statusu przesylki oraz mozliwosc usuwania wpisow z kolejki druku.
|
||||
Szybka zmiana statusu zamówienia bezpośrednio z listy zamówień — klikalny dropdown w kolumnie statusu, zmiana przez AJAX bez przeładowania strony.
|
||||
|
||||
| Phase | Name | Status | Plans |
|
||||
|------|------|--------|-------|
|
||||
| 40 | Remove Order List Bulk Print | Complete (2026-03-25) | 1/1 (`40-01-PLAN.md`) |
|
||||
| 41 | Allegro Import Log Rationalization | Complete (2026-03-25) | 1/1 (`41-01-PLAN.md`) |
|
||||
| 42 | Automation Shipment Status Event | Complete (2026-03-25) | 1/1 (`42-01-PLAN.md`) |
|
||||
| 43 | Print Queue Entry Removal | Complete (2026-03-25) | 1/1 (`43-01-PLAN.md`) |
|
||||
| 44 | Inline Status Change | Complete (2026-03-27) | 1/1 (`44-01-PLAN.md`) |
|
||||
|
||||
Active phase directories:
|
||||
- `.paul/phases/40-remove-order-list-bulk-print/`
|
||||
- `.paul/phases/41-allegro-import-log-rationalization/`
|
||||
- `.paul/phases/42-automation-shipment-status-event/`
|
||||
- `.paul/phases/43-print-queue-entry-removal/`
|
||||
Archive: `.paul/phases/44-inline-status-change/`
|
||||
|
||||
## Completed Milestones
|
||||
|
||||
<details>
|
||||
<summary>v1.5 Operational Workflow Cleanup - 2026-03-25 (4 phases, 4 plans)</summary>
|
||||
|
||||
Usprawnienia operacyjne: usunięcie bulk print, ograniczenie szumu logów importu Allegro, automatyzacja shipment.status_changed, usuwanie wpisów z kolejki druku.
|
||||
|
||||
| Phase | Name | Plans | Completed |
|
||||
|-------|------|-------|-----------|
|
||||
| 40 | Remove Order List Bulk Print | 1/1 | 2026-03-25 |
|
||||
| 41 | Allegro Import Log Rationalization | 1/1 | 2026-03-25 |
|
||||
| 42 | Automation Shipment Status Event | 1/1 | 2026-03-25 |
|
||||
| 43 | Print Queue Entry Removal | 1/1 | 2026-03-25 |
|
||||
|
||||
Archive: `.paul/phases/40-*`, `.paul/phases/41-*`, `.paul/phases/42-*`, `.paul/phases/43-*`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.4 UI Readability Tweaks - 2026-03-25 (1 phase, 1 plan)</summary>
|
||||
|
||||
@@ -145,20 +154,6 @@ Archive: `.paul/phases/17-receipt-duplicate-guard/`
|
||||
|
||||
</details>
|
||||
|
||||
## Completed Milestones
|
||||
|
||||
<details>
|
||||
<summary>v1.4 UI Readability Tweaks - 2026-03-25 (1 phase, 1 plan)</summary>
|
||||
|
||||
Rozdzielenie semantyki kolorow UI: glowny kolor przyciskow akcji zostal oddzielony od koloru naglowkow sekcji, aby poprawic czytelnosc i szybkosc skanowania interfejsu.
|
||||
|
||||
| Phase | Name | Plans | Completed |
|
||||
|-------|------|-------|-----------|
|
||||
| 30 | Button Primary Color Distinction | 1/1 | 2026-03-25 |
|
||||
|
||||
Archive: .paul/phases/30-button-primary-color/
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>v0.5 Moduł Automatyzacji — 2026-03-18 (1 phase, 2 plans)</summary>
|
||||
|
||||
@@ -234,7 +229,7 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-03-12*
|
||||
*Last updated: 2026-03-25 - v1.5 phases 40-43 complete*
|
||||
*Last updated: 2026-03-27 - v1.6 Quick Status Change complete*
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
See: .paul/PROJECT.md (updated 2026-03-12)
|
||||
|
||||
**Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami.
|
||||
**Current focus:** v1.5 completed - phases 40-43 delivered
|
||||
**Current focus:** v1.6 complete — Phase 44 delivered
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v1.5 Operational Workflow Cleanup
|
||||
Phase: [4] of [4] (Print Queue Entry Removal) - Unified
|
||||
Plan: 43-01 completed with summary
|
||||
Status: PLAN/APPLY/UNIFY closed for phases 40-43
|
||||
Last activity: 2026-03-25 23:59 - Completed phases 41-43 and updated docs/summaries
|
||||
Milestone: v1.6 Quick Status Change — Complete
|
||||
Phase: [1] of [1] (Inline Status Change) — Unified
|
||||
Plan: 44-01 completed with summary
|
||||
Status: PLAN/APPLY/UNIFY closed for phase 44
|
||||
Last activity: 2026-03-27 — Phase 44 complete, milestone v1.6 closed
|
||||
|
||||
Progress:
|
||||
- v0.1 Initial Release: [##########] 100% done
|
||||
@@ -39,13 +39,15 @@ Progress:
|
||||
- Phase 41: [##########] Complete (1/1 plans)
|
||||
- Phase 42: [##########] Complete (1/1 plans)
|
||||
- Phase 43: [##########] Complete (1/1 plans)
|
||||
- v1.6 Quick Status Change: [##########] 100% done
|
||||
- Phase 44: [##########] Complete (1/1 plans)
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
PLAN --> APPLY --> UNIFY
|
||||
done done done [Loop closed for phases 40-43]
|
||||
done done done [Loop closed for phase 44]
|
||||
```
|
||||
|
||||
## Accumulated Context
|
||||
@@ -53,6 +55,8 @@ PLAN --> APPLY --> UNIFY
|
||||
### Decisions
|
||||
| Data | Decyzja | Faza | Wpływ |
|
||||
|------|---------|------|-------|
|
||||
| 2026-03-27 | Fixed positioning dropdown (document.body) zamiast absolute wewnatrz table-wrap | Faza 44 | Dropdown nie ucinany przez overflow:hidden na .table-wrap |
|
||||
| 2026-03-27 | AJAX detect przez X-Requested-With header z fallback na redirect | Faza 44 | updateStatus() obsluguje oba tryby w jednej metodzie |
|
||||
| 2026-03-25 | Import Allegro: trigger context + deduplikacja logow (`source_order_id + source_updated_at + trigger`) | Faza 41 | Czytelniejsza historia zamowienia i mniej duplikatow wpisow `import` |
|
||||
| 2026-03-25 | Automatyzacja: event `shipment.status_changed` z warunkiem `shipment_status` (mapowanie biznes->techniczny) | Faza 42 | Reguly moga reagowac na realny status dostawy bez przebudowy engine |
|
||||
| 2026-03-25 | Tracking cron triggeruje automatyzacje tylko przy realnej zmianie `delivery_status` | Faza 42 | Brak falszywych triggerow i mniejszy szum automatyzacji |
|
||||
@@ -88,6 +92,11 @@ PLAN --> APPLY --> UNIFY
|
||||
| 2026-03-17 | Email history jako wpisy w order_activity_log (nie osobna sekcja) | Faza 15 | Spójność z istniejącym UX — jeden timeline zamiast fragmentacji |
|
||||
| 2026-03-17 | VariableResolver wydzielony z EmailTemplateController | Faza 15 | Reuse logiki zmiennych; resolwer niezaleĹĽny od kontrolera szablonĂłw |
|
||||
|
||||
### Skill Audit (Faza 44, Plan 01)
|
||||
| Oczekiwany | Wywolany | Uwagi |
|
||||
|------------|---------|-------|
|
||||
| sonar-scanner | override | Pominieto na podstawie explicit user override; lint PHP + build CSS PASS |
|
||||
|
||||
### Skill Audit (Faza 43, Plan 01)
|
||||
| Oczekiwany | Wywołany | Uwagi |
|
||||
|------------|---------|-------|
|
||||
@@ -272,7 +281,7 @@ PLAN --> APPLY --> UNIFY
|
||||
- **Delivery mapping "Szukaj..." layout** — JS `attachSelectFilter()` w allegro.php tworzy input search dla InPost/Apaczka selectów, wizualnie wygląda jakby należał do wiersza powyżej. Pre-existing bug, do naprawy osobno.
|
||||
|
||||
### Git State
|
||||
Last commit: 3610571 — feat(v1.5): complete phases 40-43 workflow cleanup
|
||||
Last commit: pending (phase 44 commit) — feat(v1.5): complete phases 40-43 workflow cleanup
|
||||
Branch: main
|
||||
Feature branches merged: none
|
||||
|
||||
@@ -281,13 +290,10 @@ Brak.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-25 23:59
|
||||
Stopped at: v1.5 phases 40-43 completed (summaries + docs + state updated)
|
||||
Next action: Start next milestone planning ($paul-milestone or $paul-plan for next TODO batch)
|
||||
Last session: 2026-03-27
|
||||
Stopped at: v1.6 phase 44 completed (SUMMARY + docs + state updated)
|
||||
Next action: Start next milestone planning (/paul:milestone or /paul:plan)
|
||||
Resume file: .paul/ROADMAP.md
|
||||
Resume context:
|
||||
- v0.1-v1.4: COMPLETE done (30 phases, 42 plans)
|
||||
- v1.5: COMPLETE done (phases 40-43)
|
||||
---
|
||||
*STATE.md — Updated after every significant action*
|
||||
|
||||
|
||||
185
.paul/phases/44-inline-status-change/44-01-PLAN.md
Normal file
185
.paul/phases/44-inline-status-change/44-01-PLAN.md
Normal file
@@ -0,0 +1,185 @@
|
||||
---
|
||||
phase: 44-inline-status-change
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- resources/views/orders/list.php
|
||||
- resources/scss/modules/_orders.scss
|
||||
- resources/js/modules/inline-status-change.js
|
||||
- public/assets/css/modules/orders.css
|
||||
- public/assets/js/modules/inline-status-change.js
|
||||
autonomous: true
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Dodać szybką zmianę statusu zamówienia bezpośrednio z listy zamówień — kliknięcie w badge statusu otwiera dropdown z dostępnymi statusami, wybór zmienia status przez AJAX bez przeładowania strony.
|
||||
|
||||
## Purpose
|
||||
Operator nie musi wchodzić w szczegóły zamówienia aby zmienić status — oszczędność czasu przy obsłudze wielu zamówień.
|
||||
|
||||
## Output
|
||||
- Zmodyfikowany kontroler z obsługą AJAX w `updateStatus()`
|
||||
- Dropdown statusów w kolumnie statusu na liście zamówień
|
||||
- JS moduł do inline zmiany statusu
|
||||
- Style SCSS dla dropdowna
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Orders/OrdersController.php — metody: index(), updateStatus(), toTableRow(), statusBadge(), buildAllStatusOptions(), statusColorMap()
|
||||
@resources/views/orders/list.php — widok listy zamówień
|
||||
@resources/views/components/table-list.php — komponent tabeli (kolumna status_badges jest raw HTML)
|
||||
@routes/web.php — POST /orders/{id}/status endpoint (linia 337)
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## Required Skills (from SPECIAL-FLOWS.md)
|
||||
|
||||
| Skill | Priority | When to Invoke | Loaded? |
|
||||
|-------|----------|----------------|---------|
|
||||
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |
|
||||
|
||||
**BLOCKING:** Required skills MUST be loaded before APPLY proceeds.
|
||||
|
||||
## Skill Invocation Checklist
|
||||
- [ ] sonar-scanner loaded (run command or confirm)
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Dropdown statusów pojawia się po kliknięciu badge
|
||||
```gherkin
|
||||
Given lista zamówień wyświetla zamówienia z badge'ami statusów
|
||||
When operator kliknie w badge statusu zamówienia
|
||||
Then pojawia się dropdown z listą wszystkich dostępnych statusów pogrupowanych wg grup statusowych
|
||||
And aktualny status jest wyróżniony w dropdown
|
||||
And kliknięcie poza dropdown zamyka go
|
||||
```
|
||||
|
||||
## AC-2: Zmiana statusu przez AJAX
|
||||
```gherkin
|
||||
Given dropdown statusów jest otwarty przy zamówieniu
|
||||
When operator wybierze nowy status z listy
|
||||
Then wysyłany jest request AJAX POST /orders/{id}/status z new_status i _token
|
||||
And badge statusu zamienia się na nowy (z prawidłowym kolorem i etykietą)
|
||||
And dropdown zamyka się automatycznie
|
||||
And nie następuje przeładowanie strony
|
||||
```
|
||||
|
||||
## AC-3: Obsługa błędów AJAX
|
||||
```gherkin
|
||||
Given operator wybiera nowy status z dropdown
|
||||
When request AJAX zwróci błąd (CSRF expired, order not found, server error)
|
||||
Then wyświetla się komunikat błędu przez OrderProAlerts
|
||||
And badge wraca do poprzedniego stanu
|
||||
And dropdown zamyka się
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Rozszerzenie updateStatus() o tryb AJAX + przekazanie danych statusów do widoku listy</name>
|
||||
<files>src/Modules/Orders/OrdersController.php</files>
|
||||
<action>
|
||||
1. W metodzie `updateStatus()` — wykryj request AJAX przez nagłówek `X-Requested-With: XMLHttpRequest`:
|
||||
- Jeśli AJAX: zwróć `Response::json(...)` z kluczami: `success`, `status_code`, `status_label`, `status_color` (kolor hex z grupy)
|
||||
- Jeśli nie-AJAX: zachowaj obecne zachowanie (redirect)
|
||||
- Przy błędach AJAX: zwróć JSON z `success: false` i `error` message
|
||||
2. W metodzie `index()` — przekaż `allStatuses` (z `buildAllStatusOptions()`) i `statusColorMap` do widoku, aby JS miał dane do budowy dropdown
|
||||
3. W metodzie `toTableRow()` — dodaj do HTML badge `data-order-id` i `data-current-status` atrybuty, aby JS mógł zidentyfikować zamówienie i aktualny status
|
||||
|
||||
Unikaj: Zmiany zachowania dla nie-AJAX requestów; dodawania nowych zależności
|
||||
</action>
|
||||
<verify>
|
||||
- `curl -X POST /orders/1/status -H "X-Requested-With: XMLHttpRequest" -d "new_status=shipped&_token=..." ` zwraca JSON
|
||||
- Standardowy POST nadal zwraca redirect
|
||||
</verify>
|
||||
<done>AC-2 i AC-3 satisfied: endpoint zwraca JSON dla AJAX, zachowuje redirect dla form submit</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Dropdown UI + JavaScript moduł inline zmiany statusu + style SCSS</name>
|
||||
<files>resources/views/orders/list.php, resources/scss/modules/_orders.scss, resources/js/modules/inline-status-change.js, public/assets/js/modules/inline-status-change.js, public/assets/css/modules/orders.css</files>
|
||||
<action>
|
||||
1. W `resources/views/orders/list.php`:
|
||||
- Dodaj `<script>` tag z danymi statusów jako JSON (allStatuses, statusColorMap, csrfToken) w `data-*` atrybucie lub inline `<script>` zmienna
|
||||
- Załaduj `inline-status-change.js` na dole strony
|
||||
2. W `resources/js/modules/inline-status-change.js`:
|
||||
- Na klik w `.orders-status-wrap` (badge kontener) — zbuduj dropdown z pogrupowanymi statusami
|
||||
- Dropdown: pozycjonowanie absolutne pod/nad badge, grupy statusów z nagłówkami, opcje z kolorami
|
||||
- Zaznacz aktualny status (odczytaj z `data-current-status`)
|
||||
- Na wybór statusu: fetch POST do `/orders/{orderId}/status` z `X-Requested-With: XMLHttpRequest`
|
||||
- Po sukcesie: zaktualizuj badge HTML (nowy kolor, nowa etykieta), zamknij dropdown
|
||||
- Po błędzie: `OrderProAlerts.alert(...)`, zamknij dropdown
|
||||
- Klik poza dropdown: zamknij
|
||||
- Obsługa wielu otwartych dropdown (tylko jeden naraz)
|
||||
3. Skopiuj JS do `public/assets/js/modules/inline-status-change.js`
|
||||
4. W `resources/scss/modules/_orders.scss`:
|
||||
- Style dla `.orders-status-dropdown`: pozycja absolutna, max-height ze scroll, shadow, border-radius
|
||||
- `.orders-status-dropdown__group-header`: etykieta grupy (muted, uppercase, mały font)
|
||||
- `.orders-status-dropdown__item`: element opcji z kółkiem koloru i etykietą, hover state
|
||||
- `.orders-status-dropdown__item.is-current`: wyróżnienie aktualnego statusu
|
||||
- `.orders-status-wrap` cursor: pointer gdy dropdown aktywny
|
||||
5. Zbuilduj SCSS do CSS
|
||||
|
||||
Unikaj: natywnych alert()/confirm(); inline CSS w widoku; jQuery (vanilla JS)
|
||||
</action>
|
||||
<verify>
|
||||
- Na liście zamówień kliknięcie w status otwiera dropdown
|
||||
- Wybór statusu zmienia badge bez przeładowania
|
||||
- Kliknięcie poza dropdown zamyka go
|
||||
- Build CSS przechodzi bez błędów
|
||||
</verify>
|
||||
<done>AC-1, AC-2, AC-3 satisfied: dropdown działa, AJAX zmienia status, błędy wyświetlane</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- resources/views/components/table-list.php (komponent generyczny — nie modyfikować)
|
||||
- src/Modules/Orders/OrdersRepository.php (logika zapisu statusu działa poprawnie)
|
||||
- database/migrations/* (brak zmian w schemacie)
|
||||
- routes/web.php (endpoint POST /orders/{id}/status już istnieje)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Tylko lista zamówień (/orders/list) — nie strona szczegółów
|
||||
- Brak bulk zmiany statusu (tylko pojedyncze zamówienie)
|
||||
- Brak drag & drop ani kanban — prosty dropdown
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] Kliknięcie badge na liście otwiera dropdown ze statusami
|
||||
- [ ] Wybór statusu wysyła AJAX i aktualizuje badge
|
||||
- [ ] Błędy wyświetlane przez OrderProAlerts
|
||||
- [ ] Klik poza dropdown zamyka go
|
||||
- [ ] Standardowy updateStatus (non-AJAX) nadal działa
|
||||
- [ ] Build SCSS przechodzi bez błędów
|
||||
- [ ] `php -l` na zmienionych plikach PHP bez błędów
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie taski ukończone
|
||||
- Wszystkie kryteria akceptacji spełnione
|
||||
- Brak błędów PHP syntax
|
||||
- Build CSS bez błędów
|
||||
- Zmiana statusu działa na liście bez przeładowania
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/44-inline-status-change/44-01-SUMMARY.md`
|
||||
</output>
|
||||
123
.paul/phases/44-inline-status-change/44-01-SUMMARY.md
Normal file
123
.paul/phases/44-inline-status-change/44-01-SUMMARY.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
phase: 44-inline-status-change
|
||||
plan: 01
|
||||
subsystem: ui
|
||||
tags: [ajax, dropdown, status, orders-list, vanilla-js]
|
||||
|
||||
requires:
|
||||
- phase: none
|
||||
provides: existing updateStatus() endpoint and status config
|
||||
provides:
|
||||
- Inline status change dropdown on orders list
|
||||
- AJAX-enabled updateStatus() endpoint
|
||||
affects: []
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [fixed-position dropdown to escape overflow:hidden containers]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- public/assets/js/modules/inline-status-change.js
|
||||
modified:
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- resources/views/orders/list.php
|
||||
- resources/scss/app.scss
|
||||
- public/assets/css/app.css
|
||||
|
||||
key-decisions:
|
||||
- "Fixed positioning for dropdown to escape table-wrap overflow:hidden"
|
||||
- "AJAX with fallback — non-AJAX requests keep redirect behavior"
|
||||
|
||||
patterns-established:
|
||||
- "AJAX endpoint pattern: detect X-Requested-With header, return JSON or redirect"
|
||||
|
||||
duration: ~15min
|
||||
started: 2026-03-27
|
||||
completed: 2026-03-27
|
||||
---
|
||||
|
||||
# Phase 44 Plan 01: Inline Status Change Summary
|
||||
|
||||
**Klikalny dropdown zmiany statusu zamowienia bezposrednio z listy zamowien — AJAX bez przeladowania strony.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~15min |
|
||||
| Started | 2026-03-27 |
|
||||
| Completed | 2026-03-27 |
|
||||
| Tasks | 2 completed |
|
||||
| Files modified | 5 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Dropdown statusow po kliknieciu badge | Pass | Dropdown z pogrupowanymi statusami, aktualny wyrozniony |
|
||||
| AC-2: Zmiana statusu przez AJAX | Pass | POST z X-Requested-With, badge aktualizowany in-place |
|
||||
| AC-3: Obsluga bledow AJAX | Pass | OrderProAlerts.alert przy bledach, badge wraca do poprzedniego stanu |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- updateStatus() obsluguje AJAX (JSON response) i non-AJAX (redirect) w jednej metodzie
|
||||
- Dropdown renderowany z position:fixed na document.body — nie ucinany przez overflow:hidden na table-wrap
|
||||
- Optimistic update badge z rollback przy bledzie serwera
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `src/Modules/Orders/OrdersController.php` | Modified | AJAX w updateStatus(), allStatuses+statusColorMap do widoku, data-* na badge |
|
||||
| `resources/views/orders/list.php` | Modified | JSON config block + script tag dla inline-status-change.js |
|
||||
| `resources/scss/app.scss` | Modified | Style .orders-status-dropdown (fixed, grupy, hover, color dots) |
|
||||
| `public/assets/css/app.css` | Rebuilt | Skompilowane style |
|
||||
| `public/assets/js/modules/inline-status-change.js` | Created | Modul JS: dropdown, AJAX fetch, optimistic update, error handling |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Fixed positioning zamiast absolute | table-wrap ma overflow:hidden — absolute dropdown ucinany | Dropdown widoczny niezaleznie od pozycji w tabeli |
|
||||
| Dropdown na document.body | Jedyny sposob na unikniecie clip przez rodzicow | Wymaga getBoundingClientRect do pozycjonowania |
|
||||
| Optimistic update z rollback | Szybsza percepcja — badge zmienia sie natychmiast | Przy bledzie badge wraca do poprzedniego stanu |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Auto-fixed | 1 | Niezbedna poprawka UX |
|
||||
|
||||
**Total impact:** Jedna poprawka pozycjonowania — zero scope creep.
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [UI] Dropdown ucinany przez overflow:hidden**
|
||||
- **Found during:** Task 2 (UAT przez usera)
|
||||
- **Issue:** `.table-wrap` ma `overflow: hidden` — dropdown renderowany wewnatrz byl ucinany
|
||||
- **Fix:** Zmiana z `position: absolute` (wewnatrz wrap) na `position: fixed` (na document.body) z obliczaniem pozycji przez getBoundingClientRect
|
||||
- **Files:** app.scss, inline-status-change.js
|
||||
- **Verification:** User potwierdził poprawke
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Inline status change dziala na liscie zamowien
|
||||
- Endpoint updateStatus() gotowy do reuse w innych widokach
|
||||
|
||||
**Concerns:**
|
||||
- None
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
|
||||
---
|
||||
*Phase: 44-inline-status-change, Plan: 01*
|
||||
*Completed: 2026-03-27*
|
||||
File diff suppressed because one or more lines are too long
200
public/assets/js/modules/inline-status-change.js
Normal file
200
public/assets/js/modules/inline-status-change.js
Normal file
@@ -0,0 +1,200 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var activeDropdown = null;
|
||||
var activeWrap = null;
|
||||
|
||||
function closeDropdown() {
|
||||
if (activeDropdown) {
|
||||
activeDropdown.remove();
|
||||
activeDropdown = null;
|
||||
activeWrap = null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildBadgeHtml(statusCode, statusLabel, statusColor) {
|
||||
var label = statusLabel || '-';
|
||||
if (statusColor) {
|
||||
return '<span class="order-tag" style="background-color:' + escapeAttr(statusColor) + ';color:#fff">' + escapeHtml(label) + '</span>';
|
||||
}
|
||||
var code = statusCode.toLowerCase();
|
||||
var cls = 'is-neutral';
|
||||
if (code === 'shipped' || code === 'delivered') cls = 'is-success';
|
||||
else if (code === 'cancelled' || code === 'returned') cls = 'is-danger';
|
||||
else if (code === 'new' || code === 'confirmed') cls = 'is-info';
|
||||
else if (code === 'processing' || code === 'packed' || code === 'paid') cls = 'is-warn';
|
||||
return '<span class="order-tag ' + cls + '">' + escapeHtml(label) + '</span>';
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(str));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeAttr(str) {
|
||||
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function createDropdown(wrap, allStatuses, statusColorMap, csrfToken) {
|
||||
closeDropdown();
|
||||
|
||||
var currentStatus = (wrap.getAttribute('data-current-status') || '').toLowerCase();
|
||||
var orderId = wrap.getAttribute('data-order-id');
|
||||
|
||||
var dropdown = document.createElement('div');
|
||||
dropdown.className = 'orders-status-dropdown';
|
||||
|
||||
var lastGroup = null;
|
||||
for (var i = 0; i < allStatuses.length; i++) {
|
||||
var s = allStatuses[i];
|
||||
if (s.group && s.group !== lastGroup) {
|
||||
var header = document.createElement('div');
|
||||
header.className = 'orders-status-dropdown__group-header';
|
||||
header.textContent = s.group;
|
||||
dropdown.appendChild(header);
|
||||
lastGroup = s.group;
|
||||
}
|
||||
|
||||
var item = document.createElement('div');
|
||||
item.className = 'orders-status-dropdown__item';
|
||||
if (s.code.toLowerCase() === currentStatus) {
|
||||
item.classList.add('is-current');
|
||||
}
|
||||
item.setAttribute('data-status-code', s.code);
|
||||
|
||||
var color = statusColorMap[s.code.toLowerCase()] || '#94a3b8';
|
||||
var dot = document.createElement('span');
|
||||
dot.className = 'orders-status-dropdown__color-dot';
|
||||
dot.style.backgroundColor = color;
|
||||
item.appendChild(dot);
|
||||
|
||||
var label = document.createElement('span');
|
||||
label.textContent = s.name;
|
||||
item.appendChild(label);
|
||||
|
||||
item.addEventListener('click', (function (statusCode) {
|
||||
return function (e) {
|
||||
e.stopPropagation();
|
||||
changeStatus(wrap, orderId, statusCode, allStatuses, statusColorMap, csrfToken);
|
||||
};
|
||||
})(s.code));
|
||||
|
||||
dropdown.appendChild(item);
|
||||
}
|
||||
|
||||
dropdown.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
activeDropdown = dropdown;
|
||||
activeWrap = wrap;
|
||||
|
||||
var wrapRect = wrap.getBoundingClientRect();
|
||||
var dropdownHeight = dropdown.offsetHeight;
|
||||
var spaceBelow = window.innerHeight - wrapRect.bottom - 8;
|
||||
var spaceAbove = wrapRect.top - 8;
|
||||
|
||||
if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) {
|
||||
dropdown.style.top = (wrapRect.bottom + 4) + 'px';
|
||||
} else {
|
||||
dropdown.style.top = (wrapRect.top - dropdownHeight - 4) + 'px';
|
||||
}
|
||||
dropdown.style.left = wrapRect.left + 'px';
|
||||
}
|
||||
|
||||
function changeStatus(wrap, orderId, newStatusCode, allStatuses, statusColorMap, csrfToken) {
|
||||
var prevHtml = wrap.innerHTML;
|
||||
var prevStatus = wrap.getAttribute('data-current-status');
|
||||
|
||||
closeDropdown();
|
||||
|
||||
var statusInfo = null;
|
||||
for (var i = 0; i < allStatuses.length; i++) {
|
||||
if (allStatuses[i].code === newStatusCode) {
|
||||
statusInfo = allStatuses[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (statusInfo) {
|
||||
var color = statusColorMap[newStatusCode.toLowerCase()] || '';
|
||||
wrap.innerHTML = buildBadgeHtml(newStatusCode, statusInfo.name, color);
|
||||
wrap.setAttribute('data-current-status', newStatusCode.toLowerCase());
|
||||
}
|
||||
|
||||
var body = new FormData();
|
||||
body.append('new_status', newStatusCode);
|
||||
body.append('_token', csrfToken);
|
||||
|
||||
fetch('/orders/' + orderId + '/status', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body: body
|
||||
})
|
||||
.then(function (resp) {
|
||||
return resp.json().then(function (data) {
|
||||
return { ok: resp.ok, data: data };
|
||||
});
|
||||
})
|
||||
.then(function (result) {
|
||||
if (!result.ok || !result.data.success) {
|
||||
wrap.innerHTML = prevHtml;
|
||||
wrap.setAttribute('data-current-status', prevStatus || '');
|
||||
var msg = (result.data && result.data.error) ? result.data.error : 'Nie udalo sie zmienic statusu';
|
||||
if (window.OrderProAlerts && typeof window.OrderProAlerts.alert === 'function') {
|
||||
window.OrderProAlerts.alert({ title: 'Blad', message: msg });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var d = result.data;
|
||||
wrap.innerHTML = buildBadgeHtml(d.status_code, d.status_label, d.status_color);
|
||||
wrap.setAttribute('data-current-status', d.status_code);
|
||||
})
|
||||
.catch(function () {
|
||||
wrap.innerHTML = prevHtml;
|
||||
wrap.setAttribute('data-current-status', prevStatus || '');
|
||||
if (window.OrderProAlerts && typeof window.OrderProAlerts.alert === 'function') {
|
||||
window.OrderProAlerts.alert({ title: 'Blad', message: 'Blad polaczenia z serwerem' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!e.target || !e.target.closest) return;
|
||||
|
||||
var wrap = e.target.closest('.orders-status-wrap');
|
||||
|
||||
if (!wrap || !wrap.hasAttribute('data-order-id')) {
|
||||
closeDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeDropdown && activeWrap === wrap) {
|
||||
closeDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
var configEl = document.getElementById('js-inline-status-config');
|
||||
if (!configEl) return;
|
||||
|
||||
var allStatuses, statusColorMap, csrfToken;
|
||||
try {
|
||||
var config = JSON.parse(configEl.textContent);
|
||||
allStatuses = config.allStatuses || [];
|
||||
statusColorMap = config.statusColorMap || {};
|
||||
csrfToken = config.csrfToken || '';
|
||||
} catch (ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
createDropdown(wrap, allStatuses, statusColorMap, csrfToken);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') closeDropdown();
|
||||
});
|
||||
})();
|
||||
@@ -1082,6 +1082,66 @@ h4.section-title {
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
cursor: pointer;
|
||||
|
||||
.order-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.orders-status-dropdown {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
min-width: 180px;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border: 1px solid #d8e1ef;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
padding: 4px 0;
|
||||
|
||||
&__group-header {
|
||||
padding: 6px 12px 2px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #94a3b8;
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid #f1f5f9;
|
||||
margin-top: 2px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 12px;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
&.is-current {
|
||||
font-weight: 700;
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
&__color-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.order-tag {
|
||||
|
||||
@@ -23,6 +23,13 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="application/json" id="js-inline-status-config"><?= json_encode([
|
||||
'allStatuses' => is_array($allStatuses ?? null) ? $allStatuses : [],
|
||||
'statusColorMap' => is_array($statusColorMap ?? null) ? $statusColorMap : [],
|
||||
'csrfToken' => (string) ($csrfToken ?? ''),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?></script>
|
||||
<script src="/assets/js/modules/inline-status-change.js"></script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var POPUP_GAP = 12;
|
||||
|
||||
@@ -142,6 +142,8 @@ final class OrdersController
|
||||
],
|
||||
'stats' => $stats,
|
||||
'statusPanel' => $statusPanel,
|
||||
'allStatuses' => $this->buildAllStatusOptions($statusConfig),
|
||||
'statusColorMap' => $statusColorMap,
|
||||
'errorMessage' => (string) ($result['error'] ?? ''),
|
||||
], 'layouts/app');
|
||||
|
||||
@@ -241,19 +243,28 @@ final class OrdersController
|
||||
|
||||
public function updateStatus(Request $request): Response
|
||||
{
|
||||
$isAjax = strtolower($request->header('X-Requested-With')) === 'xmlhttprequest';
|
||||
$orderId = max(0, (int) $request->input('id', 0));
|
||||
if ($orderId <= 0) {
|
||||
return Response::html('Not found', 404);
|
||||
return $isAjax
|
||||
? Response::json(['success' => false, 'error' => 'Not found'], 404)
|
||||
: Response::html('Not found', 404);
|
||||
}
|
||||
|
||||
$csrfToken = (string) $request->input('_token', '');
|
||||
if (!Csrf::validate($csrfToken)) {
|
||||
if ($isAjax) {
|
||||
return Response::json(['success' => false, 'error' => $this->translator->get('auth.errors.csrf_expired')], 403);
|
||||
}
|
||||
Flash::set('order.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
$newStatus = trim((string) $request->input('new_status', ''));
|
||||
if ($newStatus === '') {
|
||||
if ($isAjax) {
|
||||
return Response::json(['success' => false, 'error' => $this->translator->get('orders.details.status_change.status_required')], 422);
|
||||
}
|
||||
Flash::set('order.error', $this->translator->get('orders.details.status_change.status_required'));
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
@@ -262,6 +273,23 @@ final class OrdersController
|
||||
$actorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : null;
|
||||
|
||||
$success = $this->orders->updateOrderStatus($orderId, $newStatus, 'user', $actorName !== '' ? $actorName : null);
|
||||
|
||||
if ($isAjax) {
|
||||
if (!$success) {
|
||||
return Response::json(['success' => false, 'error' => $this->translator->get('orders.details.status_change.failed')], 500);
|
||||
}
|
||||
$statusConfig = $this->orders->statusPanelConfig();
|
||||
$statusLabelMap = $this->statusLabelMap($statusConfig);
|
||||
$statusColorMap = $this->statusColorMap($statusConfig);
|
||||
$normalizedCode = strtolower(trim($newStatus));
|
||||
return Response::json([
|
||||
'success' => true,
|
||||
'status_code' => $normalizedCode,
|
||||
'status_label' => $this->statusLabel($normalizedCode, $statusLabelMap),
|
||||
'status_color' => $statusColorMap[$normalizedCode] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
Flash::set('order.success', $this->translator->get('orders.details.status_change.success'));
|
||||
} else {
|
||||
@@ -317,7 +345,7 @@ final class OrdersController
|
||||
. '<span>' . htmlspecialchars($buyerCity, ENT_QUOTES, 'UTF-8') . '</span>'
|
||||
. '</div>'
|
||||
. '</div>',
|
||||
'status_badges' => '<div class="orders-status-wrap">'
|
||||
'status_badges' => '<div class="orders-status-wrap" data-order-id="' . (int) ($row['id'] ?? 0) . '" data-current-status="' . htmlspecialchars($status, ENT_QUOTES, 'UTF-8') . '">'
|
||||
. $this->statusBadge($status, $this->statusLabel($status, $statusLabelMap), $statusColorMap[strtolower(trim($status))] ?? '')
|
||||
. '</div>',
|
||||
'products' => $this->productsHtml($itemsPreview, $itemsCount, $itemsQty),
|
||||
|
||||
Reference in New Issue
Block a user