feat(109): checkbox multiselect filters

Phase 109 complete:
- Add checkbox dropdown enhancement for statistics multi-select filters
- Preserve GET contract for channels[] and status_groups[]
- Update PAUL plan context to read .paul/codebase docs

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
2026-04-28 22:13:52 +02:00
parent 1009957fff
commit 6d3dba89ed
13 changed files with 681 additions and 29 deletions

View File

@@ -12,9 +12,9 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
| Attribute | Value |
|-----------|-------|
| Version | 3.2.0 |
| Status | v3.2 shipped — Delivery Status Management complete |
| Last Updated | 2026-04-27 |
| Version | 3.3.0 |
| Status | v3.3 shipped - UI Filters complete |
| Last Updated | 2026-04-28 |
## Requirements
@@ -112,6 +112,7 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
- [x] Alert o kliencie z historia zwrotow: badge w liscie zamowien (kolumna buyer) + czerwony banner u gory szczegolow zamowienia; matching OR po email/phone/name; `<details>` z lista zwroconych zamowien — Phase 106
- [x] Idempotentna jednorazowa wysylka e-mail per zamowienie: tabela deduplikacji `automation_email_once_deliveries` (UNIQUE KEY rule_id+action_id+order_id), checkbox "Wyslij tylko raz" w konfiguracji akcji, markSent() tylko po sukcesie — Phase 107
- [x] Delivery Status Management: tabela `delivery_statuses` z CRUD panelem `/settings/delivery-statuses`, `DeliveryStatus::setRepository()` z DB fallbackiem, integracja DB-driven w dropdownach automatyzacji (warunek shipment_status + akcja update_shipment_status), osobna podstrona formularza CRUD (BREAKING: drop backward compat dla starych grupowych kluczy automatyzacji) — Phase 108
- [x] Checkbox dropdown multi-select filters: `/statistics/orders` korzysta z progresywnie ulepszanych selectow multiple z checkboxami, opcja "Wszystkie" i zachowanym kontraktem GET — Phase 109
### Deferred
@@ -120,7 +121,7 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
### Active (In Progress)
- [ ] (brak — milestone v3.0 zakonczony, oczekiwanie na kolejny)
- [ ] (brak — v3.3 zakonczony, oczekiwanie na kolejny milestone)
### Planned (Next)
@@ -228,6 +229,6 @@ Quick Reference:
---
*PROJECT.md — Updated when requirements or context change*
*Last updated: 2026-04-27 after v3.2 Delivery Status Management milestone completion (Phase 108)*
*Last updated: 2026-04-28 after v3.3 UI Filters milestone completion (Phase 109)*

View File

@@ -6,7 +6,7 @@ orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechod
## Current Milestone
Brak aktywnego milestone v3.2 zamkniety. Nastepny milestone do zaplanowania.
Brak aktywnego milestone - v3.3 zamkniety. Nastepny milestone do zaplanowania.
## Next Milestone
@@ -19,6 +19,19 @@ Kandydaci w kolejce:
## Completed Milestones
<details>
<summary>v3.3 UI Filters - 2026-04-28 (1 phase, 1 plan)</summary>
Usprawnienie wielokrotnego wyboru w filtrach: natywne selecty multiple na `/statistics/orders` zostaly zastapione kompaktowym dropdownem z checkboxami, bez zmiany kontraktu GET i backendu statystyk.
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 109 | Checkbox Multiselect Filters | 1/1 | Complete |
Archive: `.paul/phases/109-checkbox-multiselect-filters/`
</details>
<details>
<summary>v3.2 Delivery Status Management - 2026-04-27 (1 phase, 2 plans)</summary>

View File

@@ -2,46 +2,45 @@
## Project Reference
See: .paul/PROJECT.md (updated 2026-04-27)
See: .paul/PROJECT.md (updated 2026-04-28)
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
**Current focus:** Brak aktywnego milestone v3.2 zamkniety
**Current focus:** Brak aktywnego milestone - v3.3 zamkniety
## Current Position
Milestone: v3.2 — COMPLETE (Delivery Status Management)
Phase: 108 of 108 — COMPLETE
Plan: 108-01 — COMPLETE / 108-02 — COMPLETE
Version: 3.2.0
Status: v3.2 shipped gotowy do nastepnego milestone
Milestone: v3.3 - COMPLETE (UI Filters)
Phase: 109 of 109 - COMPLETE
Plan: 109-01 - COMPLETE
Version: 3.3.0
Status: v3.3 shipped - gotowy do nastepnego milestone
Last activity: 2026-04-27 — TRANSITION Phase 108 / v3.2 milestone complete
Last activity: 2026-04-28 - UNIFY Phase 109 / v3.3 milestone complete
Progress:
- Milestone v3.2: [##########] 100% (1/1 phases, 2/2 plans)
- Milestone v3.3: [##########] 100% (1/1 phases, 1/1 plans)
## Loop Position
Current loop state:
```
v3.2 milestone:
Phase 108 (Delivery Status Management):
Plan 108-01: PLAN APPLY UNIFY
Plan 108-02: PLAN ✓ APPLY ✓ UNIFY ✓
→ Phase 108 closed
→ v3.2 milestone closed
v3.3 milestone:
Phase 109 (Checkbox Multiselect Filters):
Plan 109-01: PLAN done APPLY done UNIFY done
-> Phase 109 closed
-> v3.3 milestone closed
```
## Session Continuity
Last session: 2026-04-27
Stopped at: v3.2 milestone closed
Next action: /paul:milestone wybor i zaplanowanie nastepnego milestone
Resume file: .paul/ROADMAP.md
Last session: 2026-04-28
Stopped at: v3.3 milestone closed
Next action: /paul:milestone - wybor i zaplanowanie nastepnego milestone
Resume file: .paul/phases/109-checkbox-multiselect-filters/109-01-SUMMARY.md
## Git State
Last commit: 0063402 — feat(108): delivery status management
Last commit: feat(109): checkbox multiselect filters
Branch: main
Feature branches merged: none

View File

@@ -0,0 +1,23 @@
# 2026-04-28
## Co zrobiono
- [Phase 109, Plan 01] Wdrozono checkbox dropdown multi-select filters na `/statistics/orders`.
- Zachowano kontrakt GET `channels[]` i `status_groups[]` przez synchronizacje z natywnym `<select multiple>`.
- Zaktualizowano `paul:plan`, aby korzystala z `.paul/codebase/architecture.md` i `.paul/codebase/db_schema.md`.
- Zsynchronizowano lokalny PAUL framework z Claude do Codex z adaptacja sciezek `~/.claude` na `~/.codex`.
## Zmienione pliki
- `.paul/PROJECT.md`
- `.paul/ROADMAP.md`
- `.paul/STATE.md`
- `.paul/codebase/architecture.md`
- `.paul/codebase/tech_changelog.md`
- `.paul/phases/109-checkbox-multiselect-filters/109-01-PLAN.md`
- `.paul/phases/109-checkbox-multiselect-filters/109-01-SUMMARY.md`
- `public/assets/css/app.css`
- `public/assets/js/modules/checkbox-multiselect.js`
- `resources/scss/app.scss`
- `resources/views/layouts/app.php`
- `resources/views/statistics/orders.php`

View File

@@ -26,6 +26,7 @@ HTTP Request
| Repositories | `src/Modules/*/Repository.php` | PDO data access (34+ repos) |
| Views | `resources/views/` | PHP templates with `$e()` / `$t()` |
| Components | `resources/views/components/` | Reusable UI blocks |
| Frontend modules | `public/assets/js/modules/` | Small vanilla JS enhancements loaded by layout |
## Module Inventory (`src/Modules/`)
@@ -44,6 +45,15 @@ HTTP Request
| **Statistics** | 2 | `OrdersStatisticsController`, `OrdersStatisticsRepository` | Dashboard aggregates |
| **Info** | 1 | `InfoController` | Health check |
## Frontend Enhancement Modules
### Checkbox Multiselect (`public/assets/js/modules/checkbox-multiselect.js`)
- Loaded globally from `resources/views/layouts/app.php`.
- Enhances native `<select multiple data-checkbox-multiselect>` controls after `DOMContentLoaded`.
- Keeps the original select in the form, synchronizes option `selected` state, and preserves native GET/POST names such as `channels[]` and `status_groups[]`.
- Used by `/statistics/orders` filters to display a compact trigger, checkbox dropdown, "Wszystkie" bulk toggle, and selected count.
- Progressive enhancement: if JavaScript fails, the native multi-select remains visible.
## Key Data Flows
### Order Lifecycle
@@ -124,3 +134,28 @@ tests/
Unit/ PHPUnit tests (7+ service test files)
bootstrap.php PSR-4 autoloader for tests
```
## Phase 108 — Delivery Status Management
### DeliveryStatusRepository (`src/Modules/Shipments/DeliveryStatusRepository.php`)
- CRUD dla tabeli `delivery_statuses`
- Per-request static cache (`private static ?array $cache`)
- Blokuje edycję/usunięcie statusów systemowych (`is_system=1`)
- Blokuje usunięcie statusów używanych w `delivery_status_mappings` lub `shipment_packages`
### DeliveryStatusesController (`src/Modules/Settings/DeliveryStatusesController.php`)
- Panel `/settings/delivery-statuses`
- Dwie zakładki via `?tab=` param: `statuses` (CRUD) i `mapping` (embed mapowania)
- Wstrzykuje `DeliveryStatusRepository` i `DeliveryStatusMappingRepository`
### DeliveryStatus::setRepository() (dynamic loading)
- Wywoływane raz w `routes/web.php` po bootstrap
- `label()`, `getAllOptions()`, `getAllStatuses()`, `getColor()` ładują z DB gdy repo ustawione
- Fallback na hardcoded stałe gdy repo niedostępne
### AutomationController + AutomationService (Phase 108 Plan 02)
- `AutomationController::buildShipmentStatusOptions()` — buduje listę opcji `[key => ['label' => ...]]` z `DeliveryStatus::getAllOptions()` (DB-driven)
- Walidacja `shipment_status` warunku i `update_shipment_status` akcji w `parseConditionValue()`/`parseActionConfig()` używa `DeliveryStatus::getAllStatuses()`
- `AutomationService::evaluateShipmentStatusCondition()` — bezpośrednie porównanie kluczy DB (usunięto mapping grupowy `SHIPMENT_STATUS_OPTION_MAP`)
- `AutomationService::resolveStatusFromActionKey()` — bezpośredni klucz statusu z DB jako target
- BREAKING: stare reguły z grupowymi kluczami (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`) nie matchują się — operator musi je odtworzyć przy użyciu nowych kluczy DB

View File

@@ -0,0 +1,44 @@
# Technical Changelog
## 2026-04-28 - Phase 109 Plan 01: Checkbox Multiselect Filters
**Co zrobiono:**
- `public/assets/js/modules/checkbox-multiselect.js` - nowy vanilla JS enhancer dla natywnych `<select multiple data-checkbox-multiselect>`.
- `resources/views/layouts/app.php` - globalne podpiecie modulu z cache busting przez `filemtime()`.
- `resources/views/statistics/orders.php` - filtry `channels[]` i `status_groups[]` oznaczone do progresywnego ulepszenia bez zmiany nazw pol formularza.
- `resources/scss/app.scss` - kompaktowe style dropdownu z checkboxami i opcja "Wszystkie".
**Dlaczego:**
- Natywne selecty multiple byly malo czytelne i zajmowaly za duzo miejsca w filtrach statystyk.
- Zachowanie oryginalnego selecta w DOM utrzymuje obecny kontrakt GET i fallback bez JavaScript.
- Brak zmian w schemacie DB i logice agregacji statystyk.
> Chronologiczny log zmian technicznych — co i dlaczego.
## 2026-04-27 — Phase 108 Plan 02: Automation Dropdowns z DB
**Co zrobiono:**
- `AutomationController` — usunięto stałą `SHIPMENT_STATUS_OPTIONS` (8 grupowych kluczy)
- Dropdown statusów w warunku `shipment_status` i akcji `update_shipment_status` ładuje statusy z DB przez `DeliveryStatus::getAllOptions()`
- Walidacja w `parseConditionValue()` i `parseActionConfig()` używa `DeliveryStatus::getAllStatuses()`
- `AutomationService` — usunięto stałą `SHIPMENT_STATUS_OPTION_MAP`; ewaluacja `evaluateShipmentStatusCondition()` porównuje klucze bezpośrednio
- `resolveStatusFromActionKey()` — bezpośredni klucz statusu z DB jako target (zamiast pierwszego z grupy)
**Dlaczego:**
- Zamknięcie integracji z Plan 01 — operator dodaje status w `/settings/delivery-statuses` i jest on od razu dostępny w dropdownach automatyzacji bez deploymentu
- Eliminacja kolizji semantycznej: stary klucz grupowy `picked_up` mapował na `delivered` (paczka odebrana przez klienta), nowy klucz DB `picked_up` to "Odebrana przez kuriera" (od nadawcy)
- BREAKING: stare reguły z grupowymi kluczami (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`, oraz `picked_up`/`ready_for_pickup`/`cancelled` w starym znaczeniu) nie matchują — wymagają ręcznego odtworzenia z nowymi kluczami DB
## 2026-04-27 — Phase 108 Plan 01: Delivery Status Management
**Co zrobiono:**
- Tabela `delivery_statuses` z seedem 11 statusów (migracja `20260427_000103`)
- `DeliveryStatusRepository` — CRUD + per-request cache
- `DeliveryStatus.php` — dynamiczne ładowanie statusów z DB (`setRepository()`)
- Panel `/settings/delivery-statuses` z CRUD (zakładka "Statusy") i mapowaniem (zakładka "Mapowanie dostawy")
- Sidebar: "Statusy" → "Statusy zamówień", nowe "Statusy przesyłek" z badge niezmapowanych
- Badge przesyłek: inline CSS custom property `--status-color` dla niestandardowych statusów
**Dlaczego:**
- Dodanie nowego statusu wymagało zmiany kodu + deploymentu; teraz z UI
- Operator może definiować własne statusy znormalizowane bez ingerencji w kod

View File

@@ -0,0 +1,189 @@
---
phase: 109-checkbox-multiselect-filters
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- resources/views/statistics/orders.php
- resources/views/layouts/app.php
- public/assets/js/modules/checkbox-multiselect.js
- resources/scss/app.scss
- public/assets/css/app.css
- .paul/codebase/architecture.md
- .paul/codebase/tech_changelog.md
autonomous: false
delegation: off
---
<objective>
## Goal
Replace native multi-select boxes on `/statistics/orders` with compact dropdown controls containing checkboxes, matching the provided visual direction while preserving the current filter contract.
## Purpose
Statistics filters should be faster and clearer to scan. Users should be able to open one compact field, see checked options, toggle individual values or all values, and submit the existing GET form without backend changes.
## Output
Progressive-enhancement UI for multi-select fields, SCSS styling, compiled CSS, and technical documentation updates.
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@AGENTS.md
## Source Files
@resources/views/statistics/orders.php
@resources/views/layouts/app.php
@resources/scss/app.scss
@resources/scss/shared/_ui-components.scss
@package.json
## Notes
- Project technical docs live in `.paul/codebase/`; use `.paul/codebase/architecture.md`, `.paul/codebase/db_schema.md`, and `.paul/codebase/tech_changelog.md`.
- This plan does not change database schema, migrations, SQL queries, or request parameter names.
- Specialized flows: `.paul/SPECIAL-FLOWS.md` requires `sonar-scanner` after APPLY and before UNIFY; frontend-design is optional for this UI work.
</context>
<skills>
## Required Skills (from SPECIAL-FLOWS.md)
| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| sonar-scanner | required | After APPLY, before UNIFY | o |
| /frontend-design | optional | During UI implementation review | o |
**BLOCKING:** `sonar-scanner` must be run before UNIFY unless the environment blocks it.
</skills>
<acceptance_criteria>
## AC-1: Checkbox Dropdown Behavior
```gherkin
Given the `/statistics/orders` page has multi-select filters for channels and status groups
When the user opens either filter
Then the field displays a dropdown with checkboxes, a "Wszystkie" option, and all available choices without using the browser's native multi-select box as the visible control
```
## AC-2: Existing Filter Contract Preserved
```gherkin
Given a user changes selected channels or status groups in the new dropdown
When the user submits the filter form
Then the request still sends `channels[]` and `status_groups[]` values compatible with the existing controller
```
## AC-3: Compact Visual Fit
```gherkin
Given the new dropdown is displayed on desktop and smaller screens
When the options list is opened
Then it remains compact, scrollable when needed, visually aligned with current form controls, and does not place CSS inside PHP view files
```
## AC-4: Progressive Enhancement
```gherkin
Given JavaScript fails to load
When the user visits `/statistics/orders`
Then the original native multi-select remains available enough for filtering instead of breaking the form
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Mark statistics filters for checkbox enhancement</name>
<files>resources/views/statistics/orders.php</files>
<action>
Add semantic data attributes/classes to the existing `channels[]` and `status_groups[]` multi-selects so a reusable JavaScript module can enhance them.
Keep option values, selected logic, escaping with `$e`, names, and the current GET form action unchanged.
Add accessible labels only through attributes where needed; do not add inline CSS or duplicate option markup by hand.
</action>
<verify>Open the rendered PHP around both selects and confirm names remain `channels[]` and `status_groups[]` with `multiple`.</verify>
<done>AC-2 and AC-4 satisfied for the PHP view.</done>
</task>
<task type="auto">
<name>Task 2: Add reusable checkbox multiselect module</name>
<files>public/assets/js/modules/checkbox-multiselect.js, resources/views/layouts/app.php</files>
<action>
Create a small vanilla JS module that enhances marked native multi-selects:
- Wrap each select in a stable container and visually hide the select only after enhancement succeeds.
- Render a button-like trigger showing selected count, for example `11 zaznaczono`, or a clear empty state.
- Render a dropdown with a `Wszystkie` checkbox followed by one checkbox per option.
- Synchronize checkbox changes back to the original option `selected` values so native form submission remains unchanged.
- Keep "Wszystkie" checked only when all options are selected, unchecked when none are selected, and indeterminate for partial selections.
- Close on outside click and Escape, and keep keyboard focus behavior reasonable for form use.
Include the module once in `resources/views/layouts/app.php` with `filemtime()` cache busting, matching existing module includes.
</action>
<verify>Manual browser check: toggling options changes the original select state and submitted query string remains compatible.</verify>
<done>AC-1, AC-2, and AC-4 satisfied for client behavior.</done>
</task>
<task type="auto">
<name>Task 3: Style and document the new control</name>
<files>resources/scss/app.scss, public/assets/css/app.css, .paul/codebase/architecture.md, .paul/codebase/tech_changelog.md</files>
<action>
Add SCSS for the checkbox multiselect near the existing statistics/form styles:
- Match current `form-control` sizing, borders, focus ring, and compact density.
- Dropdown should have a white surface, thin blue focus/active border, subtle shadow, max height with internal scrolling, and readable checkbox rows similar to the supplied screenshot.
- Avoid inline styles in views and avoid one-hue decorative palettes.
Build CSS with `npm run build:css`.
Update `.paul/codebase/architecture.md` with the new frontend module and `.paul/codebase/tech_changelog.md` with a changelog entry. Do not update DB schema because this plan has no schema change.
</action>
<verify>`npm run build:css` succeeds; docs mention `checkbox-multiselect.js` and the `/statistics/orders` usage.</verify>
<done>AC-3 satisfied and documentation requirements from `AGENTS.md` addressed for this UI-only change.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Checkbox dropdown multi-select filters on `/statistics/orders`</what-built>
<how-to-verify>
1. Visit: `https://orderpro.projectpro.pl/statistics/orders` or local `/statistics/orders`.
2. Open channel and status filters.
3. Toggle individual checkboxes and "Wszystkie".
4. Submit filters and confirm results refresh with selected values.
5. Confirm the visual direction is close to the attached screenshot: compact trigger, checkbox list, scrollable dropdown.
</how-to-verify>
<resume-signal>Type "approved" to continue to UNIFY, or describe visual/behavior issues to fix.</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- `src/Modules/Statistics/OrdersStatisticsController.php` filter parsing contract unless a discovered bug makes it necessary.
- `src/Modules/Statistics/OrdersStatisticsRepository.php` and SQL aggregation logic.
- `database/migrations/*` and DB schema.
- Native `alert()`/`confirm()` patterns.
## SCOPE LIMITS
- This plan enhances multi-select UI only; it does not redesign the whole statistics page.
- Do not add external JS/CSS dependencies.
- Do not move CSS into PHP views.
- Apply the new control first to `/statistics/orders`; broader adoption can follow only by adding the same marker to other native multi-selects after this behavior is verified.
</boundaries>
<verification>
Before declaring plan complete:
- [ ] `npm run build:css`
- [ ] `php -l resources/views/statistics/orders.php`
- [ ] `php -l resources/views/layouts/app.php`
- [ ] Manual check or browser automation confirms dropdown opens, checks sync, and form submission uses existing GET names.
- [ ] `sonar-scanner` run before UNIFY, or blocker recorded if unavailable.
- [ ] All acceptance criteria met.
</verification>
<success_criteria>
- `/statistics/orders` multi-select filters are visibly checkbox dropdowns.
- Existing selected values are reflected on initial page load.
- Form submission stays compatible with current controller inputs.
- CSS is in SCSS and compiled to public CSS.
- Technical changelog and architecture docs are updated.
</success_criteria>
<output>
After completion, create `.paul/phases/109-checkbox-multiselect-filters/109-01-SUMMARY.md`.
</output>

View File

@@ -0,0 +1,62 @@
---
phase: 109-checkbox-multiselect-filters
plan: 01
completed: 2026-04-28T22:00:00+02:00
duration: same-session
---
<summary>
## Objective
Replace native multi-select boxes on `/statistics/orders` with compact checkbox dropdown controls while preserving the existing GET filter contract.
## What Was Built
| File | Purpose |
|------|---------|
| `resources/views/statistics/orders.php` | Marked `channels[]` and `status_groups[]` multi-selects for progressive enhancement. |
| `public/assets/js/modules/checkbox-multiselect.js` | Added reusable vanilla JS checkbox-dropdown enhancer. |
| `resources/views/layouts/app.php` | Loaded the new JS module with `filemtime()` cache busting. |
| `resources/scss/app.scss` | Added compact dropdown, trigger, checkbox, and open-state styles. |
| `public/assets/css/app.css` | Rebuilt compiled CSS. |
| `.paul/codebase/architecture.md` | Documented the frontend enhancement module pattern. |
| `.paul/codebase/tech_changelog.md` | Added Phase 109 technical changelog entry. |
| `C:\Users\jacek\.claude\commands\paul\plan.md` | Added codebase architecture and DB schema docs to plan context. |
| `C:\Users\jacek\.claude\paul-framework\workflows\plan-phase.md` | Added required reading/load-context instructions for codebase docs. |
| `C:\Users\jacek\.codex\paul-framework\...` | Synced Claude source framework into Codex target with `~/.codex` path adaptation. |
## Acceptance Criteria Results
| AC | Result | Evidence |
|----|--------|----------|
| AC-1 Checkbox Dropdown Behavior | PASS | Marked selects are enhanced into trigger + checkbox dropdown with "Wszystkie". |
| AC-2 Existing Filter Contract Preserved | PASS | Original selects remain in DOM with unchanged `channels[]` and `status_groups[]` names. |
| AC-3 Compact Visual Fit | PASS | SCSS defines compact 30px trigger, scrollable 200px dropdown, focus border, and subtle shadow. |
| AC-4 Progressive Enhancement | PASS | Native selects are hidden only after JS creates the wrapper and sets enhancement attribute. |
## Verification
| Check | Result |
|-------|--------|
| `npm install` | PASS |
| `npm run build:css` | PASS |
| `php -l resources\views\statistics\orders.php` | PASS |
| `php -l resources\views\layouts\app.php` | PASS |
| `node --check public\assets\js\modules\checkbox-multiselect.js` | PASS |
| `git diff --check -- ...` | PASS |
| `sonar-scanner` | PASS, task `c378ed6c-51bf-4815-acf8-b4e76ca9b9f2` |
## Deviations
- Added the requested PAUL framework sync and `paul:plan` context update during the same plan, because it was explicitly requested with implementation.
- Sonar issue lookup through public API returned `401 Unauthorized`; no SonarQube MCP tool is available in this Codex session.
## Key Patterns / Decisions
- Keep original native select controls as the source of truth for form submission.
- Apply the checkbox dropdown as progressive enhancement only after successful JS initialization.
- Treat Claude PAUL files as source and Codex PAUL files as target, with path adaptation from `~/.claude` to `~/.codex`.
## Skill Audit
- `sonar-scanner`: invoked.
## Next Phase
Phase 109 is complete. v3.3 UI Filters can be closed; next work returns to selecting a new milestone or phase from `.paul/ROADMAP.md`.
</summary>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,164 @@
(function () {
'use strict';
var ENHANCED_ATTR = 'data-checkbox-multiselect-enhanced';
function optionList(select) {
return Array.prototype.slice.call(select.options).filter(function (option) {
return option.value !== '';
});
}
function selectedOptions(select) {
return optionList(select).filter(function (option) {
return option.selected;
});
}
function selectedLabel(select) {
var selectedCount = selectedOptions(select).length;
if (selectedCount === 0) {
return select.dataset.emptyLabel || 'Nic nie wybrano';
}
var suffix = selectedCount === 1
? (select.dataset.selectedLabelSingular || 'zaznaczono')
: (select.dataset.selectedLabelPlural || 'zaznaczono');
return selectedCount + ' ' + suffix;
}
function setOpen(wrapper, isOpen) {
wrapper.classList.toggle('is-open', isOpen);
wrapper.querySelector('.checkbox-multiselect__trigger').setAttribute('aria-expanded', isOpen ? 'true' : 'false');
}
function updateAllState(select, allCheckbox) {
var options = optionList(select);
var selectedCount = selectedOptions(select).length;
allCheckbox.checked = options.length > 0 && selectedCount === options.length;
allCheckbox.indeterminate = selectedCount > 0 && selectedCount < options.length;
}
function syncTrigger(select, wrapper) {
wrapper.querySelector('.checkbox-multiselect__value').textContent = selectedLabel(select);
updateAllState(select, wrapper.querySelector('.checkbox-multiselect__all'));
}
function createCheckbox(option, select, wrapper, index) {
var row = document.createElement('label');
row.className = 'checkbox-multiselect__option';
var checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = option.selected;
checkbox.value = option.value;
checkbox.dataset.optionIndex = String(index);
var text = document.createElement('span');
text.textContent = option.text;
checkbox.addEventListener('change', function () {
option.selected = checkbox.checked;
syncTrigger(select, wrapper);
select.dispatchEvent(new Event('change', { bubbles: true }));
});
row.appendChild(checkbox);
row.appendChild(text);
return row;
}
function enhanceSelect(select) {
if (select.hasAttribute(ENHANCED_ATTR)) {
return;
}
var wrapper = document.createElement('div');
wrapper.className = 'checkbox-multiselect';
var trigger = document.createElement('button');
trigger.type = 'button';
trigger.className = 'checkbox-multiselect__trigger';
trigger.setAttribute('aria-haspopup', 'listbox');
trigger.setAttribute('aria-expanded', 'false');
var value = document.createElement('span');
value.className = 'checkbox-multiselect__value';
value.textContent = selectedLabel(select);
var arrow = document.createElement('span');
arrow.className = 'checkbox-multiselect__arrow';
arrow.setAttribute('aria-hidden', 'true');
var dropdown = document.createElement('div');
dropdown.className = 'checkbox-multiselect__dropdown';
dropdown.setAttribute('role', 'listbox');
var allRow = document.createElement('label');
allRow.className = 'checkbox-multiselect__option checkbox-multiselect__option--all';
var allCheckbox = document.createElement('input');
allCheckbox.type = 'checkbox';
allCheckbox.className = 'checkbox-multiselect__all';
var allText = document.createElement('span');
allText.textContent = select.dataset.allLabel || 'Wszystkie';
allCheckbox.addEventListener('change', function () {
var shouldSelect = allCheckbox.checked;
optionList(select).forEach(function (option) {
option.selected = shouldSelect;
});
wrapper.querySelectorAll('.checkbox-multiselect__option input:not(.checkbox-multiselect__all)').forEach(function (checkbox) {
checkbox.checked = shouldSelect;
});
syncTrigger(select, wrapper);
select.dispatchEvent(new Event('change', { bubbles: true }));
});
allRow.appendChild(allCheckbox);
allRow.appendChild(allText);
dropdown.appendChild(allRow);
optionList(select).forEach(function (option, index) {
dropdown.appendChild(createCheckbox(option, select, wrapper, index));
});
trigger.appendChild(value);
trigger.appendChild(arrow);
wrapper.appendChild(trigger);
wrapper.appendChild(dropdown);
select.parentNode.insertBefore(wrapper, select);
wrapper.appendChild(select);
select.setAttribute(ENHANCED_ATTR, '1');
trigger.addEventListener('click', function () {
setOpen(wrapper, !wrapper.classList.contains('is-open'));
});
wrapper.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
setOpen(wrapper, false);
trigger.focus();
}
});
syncTrigger(select, wrapper);
}
document.addEventListener('click', function (event) {
document.querySelectorAll('.checkbox-multiselect.is-open').forEach(function (wrapper) {
if (!wrapper.contains(event.target)) {
setOpen(wrapper, false);
}
});
});
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('select[data-checkbox-multiselect]').forEach(enhanceSelect);
});
}());

View File

@@ -1085,6 +1085,111 @@ h4.section-title {
padding-bottom: 6px;
}
.checkbox-multiselect {
position: relative;
width: 100%;
select[data-checkbox-multiselect-enhanced] {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
}
.checkbox-multiselect__trigger {
width: 100%;
min-height: 30px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
border: 1px solid var(--c-border);
border-radius: 6px;
padding: 4px 8px;
font: inherit;
color: var(--c-text-strong);
background: #ffffff;
cursor: pointer;
text-align: left;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.checkbox-multiselect__value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.checkbox-multiselect__arrow {
flex: 0 0 auto;
width: 7px;
height: 7px;
border-right: 1.5px solid #64748b;
border-bottom: 1.5px solid #64748b;
transform: rotate(45deg) translateY(-2px);
}
.checkbox-multiselect__dropdown {
display: none;
position: absolute;
z-index: 40;
top: calc(100% + 2px);
left: 0;
right: 0;
max-height: 200px;
overflow-y: auto;
padding: 8px 8px 6px;
border: 1px solid var(--c-primary);
border-radius: 0 0 6px 6px;
background: #ffffff;
box-shadow: 0 18px 38px rgba(45, 55, 72, 0.16);
}
.checkbox-multiselect.is-open {
.checkbox-multiselect__trigger {
border-color: var(--c-primary);
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
box-shadow: var(--focus-ring);
}
.checkbox-multiselect__dropdown {
display: grid;
gap: 4px;
}
.checkbox-multiselect__arrow {
transform: rotate(225deg) translate(-2px, -1px);
}
}
.checkbox-multiselect__option {
display: flex;
align-items: center;
gap: 6px;
min-height: 20px;
color: var(--c-text);
font-size: 13px;
line-height: 1.25;
cursor: pointer;
input {
width: 17px;
height: 17px;
margin: 0;
flex: 0 0 auto;
accent-color: var(--c-primary);
}
}
.checkbox-multiselect__option--all {
color: var(--c-text-strong);
font-weight: 700;
}
.statistics-orders-table-wrap {
overflow-x: auto;
}

View File

@@ -187,6 +187,7 @@
</div>
<script src="/assets/js/modules/jquery-alerts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/jquery-alerts.js') ?: 0 ?>"></script>
<script src="/assets/js/modules/global-search.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/global-search.js') ?: 0 ?>"></script>
<script src="/assets/js/modules/checkbox-multiselect.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/checkbox-multiselect.js') ?: 0 ?>"></script>
<script>
(function () {
var STORAGE_KEY = 'sidebarCollapsed';

View File

@@ -47,7 +47,15 @@ foreach ($channelOptions as $channelOption) {
<label class="form-field">
<span class="field-label"><?= $e($t('statistics.orders.filters.channels')) ?></span>
<select class="form-control statistics-orders-multiselect" name="channels[]" multiple size="6">
<select class="form-control statistics-orders-multiselect js-checkbox-multiselect"
name="channels[]"
multiple
size="6"
data-checkbox-multiselect
data-all-label="Wszystkie"
data-empty-label="Nic nie wybrano"
data-selected-label-singular="zaznaczono"
data-selected-label-plural="zaznaczono">
<?php foreach ($channelOptions as $channelOption): ?>
<?php
$key = (string) ($channelOption['key'] ?? '');
@@ -65,7 +73,15 @@ foreach ($channelOptions as $channelOption) {
<label class="form-field">
<span class="field-label"><?= $e($t('statistics.orders.filters.status_groups')) ?></span>
<select class="form-control statistics-orders-multiselect" name="status_groups[]" multiple size="6">
<select class="form-control statistics-orders-multiselect js-checkbox-multiselect"
name="status_groups[]"
multiple
size="6"
data-checkbox-multiselect
data-all-label="Wszystkie"
data-empty-label="Nic nie wybrano"
data-selected-label-singular="zaznaczono"
data-selected-label-plural="zaznaczono">
<?php foreach ($statusGroupOptions as $groupOption): ?>
<?php
$groupId = (int) ($groupOption['id'] ?? 0);