diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md
index 6f8e2cb..ce1e76d 100644
--- a/.paul/ROADMAP.md
+++ b/.paul/ROADMAP.md
@@ -6,7 +6,27 @@ orderPRO to narzędzie do wielokanałowego zarządzania sprzedażą. Projekt prz
## Current Milestone
-None — ready for v1.0 planning.
+### v1.0 Presety przesyłek — In progress
+
+Customowe przyciski szybkiego wypełniania formularza przygotowania przesyłki. Użytkownik zapisuje preset z wybranym przewoźnikiem, usługą, typem paczki, wymiarami, wagą i punktem nadania, a następnie jednym kliknięciem wypełnia formularz. Presety globalne (dostępne dla wszystkich użytkowników).
+
+| Phase | Name | Plans | Status |
+|-------|------|-------|--------|
+| 23 | Shipment Presets Backend | 1/1 | Complete ✓ |
+| 24 | Shipment Presets UI | TBD | Not started |
+| 25 | Shipment Presets Management | TBD | Not started |
+
+### Phase 23: Shipment Presets Backend
+
+Focus: Migracja DB (tabela `shipment_presets`), repository CRUD, controller z endpointami JSON API.
+
+### Phase 24: Shipment Presets UI
+
+Focus: Lista presetów nad formularzem jako kolorowe przyciski, popup tworzenia presetu (nazwa + kolor), JS autofill formularza po kliknięciu.
+
+### Phase 25: Shipment Presets Management
+
+Focus: Edycja nazwy/koloru/parametrów presetu, usuwanie, zmiana kolejności.
## Completed Milestones
@@ -139,4 +159,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
---
*Roadmap created: 2026-03-12*
-*Last updated: 2026-03-22 — v0.7 milestone complete*
+*Last updated: 2026-03-22 — v1.0 milestone created*
diff --git a/.paul/STATE.md b/.paul/STATE.md
index 453ed2a..1da90d6 100644
--- a/.paul/STATE.md
+++ b/.paul/STATE.md
@@ -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:** v0.9 Poprawki ustawień firmy — MILESTONE COMPLETE ✓
+**Current focus:** v1.0 Presety przesyłek
## Current Position
-Milestone: v0.9 Poprawki ustawień firmy — COMPLETE ✓
-Phase: [1] of [1] (REGON Save Fix) — COMPLETE ✓
-Plan: 22-01 — loop closed
-Status: Milestone v0.9 complete
-Last activity: 2026-03-22 — UNIFY complete, milestone v0.9 done
+Milestone: v1.0 Presety przesyłek
+Phase: [2] of [3] (Shipment Presets UI)
+Plan: Not started
+Status: Phase 23 complete, ready to plan Phase 24
+Last activity: 2026-03-22 — Phase 23 complete, transitioned to Phase 24
Progress:
- v0.1 Initial Release: [██████████] 100% ✓
@@ -33,7 +33,7 @@ Progress:
Current loop state:
```
PLAN ──▶ APPLY ──▶ UNIFY
- ✓ ✓ ✓ [Milestone v0.9 complete]
+ ✓ ✓ ✓ [Loop complete — ready for next PLAN]
```
## Accumulated Context
@@ -67,6 +67,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 23, Plan 01)
+| Oczekiwany | Wywołany | Uwagi |
+|------------|---------|-------|
+| sonar-scanner | ✓ | 0 issues na nowych plikach |
+
### Skill Audit (Faza 22, Plan 01)
| Oczekiwany | Wywołany | Uwagi |
|------------|---------|-------|
@@ -196,7 +201,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: pending — Phase 15 + milestone v0.4 complete, awaiting commit
+Last commit: d6375cc — fix(22-regon-save-fix): naprawa zapisu REGON, BDO, KRS i logo
Branch: main
Feature branches merged: none
@@ -206,9 +211,9 @@ Brak.
## Session Continuity
Last session: 2026-03-22
-Stopped at: Milestone v0.9 complete
-Next action: /paul:discuss-milestone lub /paul:milestone dla v1.0
-Resume file: .paul/phases/22-regon-save-fix/22-01-SUMMARY.md
+Stopped at: Phase 23 complete, ready to plan Phase 24
+Next action: /paul:plan for Phase 24 (Shipment Presets UI)
+Resume file: .paul/phases/23-shipment-presets-backend/23-01-SUMMARY.md
Resume context:
- v0.1: COMPLETE ✓ (6 phases, 15 plans)
- v0.2: COMPLETE ✓ (1 phase, 5 plans)
diff --git a/.paul/phases/23-shipment-presets-backend/23-01-PLAN.md b/.paul/phases/23-shipment-presets-backend/23-01-PLAN.md
new file mode 100644
index 0000000..80fd7e7
--- /dev/null
+++ b/.paul/phases/23-shipment-presets-backend/23-01-PLAN.md
@@ -0,0 +1,225 @@
+---
+phase: 23-shipment-presets-backend
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - database/migrations/20260322_000059_create_shipment_presets_table.sql
+ - src/Modules/Shipments/ShipmentPresetRepository.php
+ - src/Modules/Shipments/ShipmentPresetController.php
+ - routes/web.php
+autonomous: true
+---
+
+
+## Goal
+Stworzyć tabelę DB `shipment_presets` oraz pełny backend CRUD (JSON API) do zarządzania presetami przesyłek.
+
+## Purpose
+Presety przesyłek pozwalają użytkownikom jednym kliknięciem wypełnić formularz przygotowania przesyłki zapisanymi wcześniej parametrami (przewoźnik, usługa, wymiary, waga itp.). Ta faza buduje fundament danych i API — UI będzie w fazie 24.
+
+## Output
+- Migracja SQL tworząca tabelę `shipment_presets`
+- `ShipmentPresetRepository` z metodami CRUD
+- `ShipmentPresetController` z endpointami JSON API
+- Routing w `routes/web.php`
+
+
+
+## Project Context
+@.paul/PROJECT.md
+@.paul/ROADMAP.md
+@.paul/STATE.md
+
+## Source Files
+@routes/web.php
+@src/Modules/Shipments/ShipmentController.php
+@src/Modules/Shipments/ShipmentPackageRepository.php
+@resources/views/shipments/prepare.php
+
+
+
+No specialized flows required for this plan (SPECIAL-FLOWS.md: sonar-scanner required post-APPLY).
+
+
+
+
+## AC-1: Tabela shipment_presets istnieje w bazie
+```gherkin
+Given migracja została uruchomiona
+When sprawdzam strukturę tabeli shipment_presets
+Then tabela zawiera kolumny: id, name, color, carrier, provider_code, delivery_method_id, credentials_id, carrier_id, package_type, length_cm, width_cm, height_cm, weight_kg, sender_point_id, label_format, sort_order, created_at, updated_at
+```
+
+## AC-2: API zwraca listę presetów
+```gherkin
+Given w tabeli istnieją presety
+When wysyłam GET /api/shipment-presets
+Then otrzymuję JSON z tablicą presetów posortowanych po sort_order
+```
+
+## AC-3: API tworzy nowy preset
+```gherkin
+Given wysyłam POST /api/shipment-presets z danymi formularza
+When dane są poprawne (name wymagane)
+Then preset zostaje zapisany w bazie i zwrócony jako JSON z id
+```
+
+## AC-4: API aktualizuje preset
+```gherkin
+Given istnieje preset o id=1
+When wysyłam PUT /api/shipment-presets/1 z nowymi danymi
+Then preset zostaje zaktualizowany w bazie
+```
+
+## AC-5: API usuwa preset
+```gherkin
+Given istnieje preset o id=1
+When wysyłam DELETE /api/shipment-presets/1
+Then preset zostaje usunięty z bazy
+```
+
+
+
+
+
+
+ Task 1: Migracja DB — tabela shipment_presets
+ database/migrations/20260322_000059_create_shipment_presets_table.sql
+
+ Utworzyć migrację SQL tworzącą tabelę `shipment_presets`:
+
+ ```sql
+ CREATE TABLE shipment_presets (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(100) NOT NULL,
+ color VARCHAR(7) NOT NULL DEFAULT '#3b82f6',
+ carrier VARCHAR(32) NOT NULL COMMENT 'allegro, inpost, apaczka',
+ provider_code VARCHAR(32) NOT NULL COMMENT 'allegro_wza, apaczka, inpost',
+ delivery_method_id VARCHAR(128) NOT NULL,
+ credentials_id VARCHAR(128) NOT NULL DEFAULT '',
+ carrier_id VARCHAR(64) NOT NULL DEFAULT '',
+ package_type VARCHAR(16) NOT NULL DEFAULT 'PACKAGE',
+ length_cm DECIMAL(8,1) NOT NULL DEFAULT 25.0,
+ width_cm DECIMAL(8,1) NOT NULL DEFAULT 20.0,
+ height_cm DECIMAL(8,1) NOT NULL DEFAULT 8.0,
+ weight_kg DECIMAL(8,3) NOT NULL DEFAULT 1.000,
+ sender_point_id VARCHAR(64) NOT NULL DEFAULT '',
+ label_format VARCHAR(8) NOT NULL DEFAULT 'PDF',
+ sort_order INT UNSIGNED NOT NULL DEFAULT 0,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+ ```
+
+ Kolumna `color` przechowuje hex kolor przycisku (np. #3b82f6).
+ Brak user_id — presety globalne.
+
+ Uruchomić migrację na bazie: php -l na pliku SQL nie ma sensu, ale sprawdzić składnię wizualnie. Migracja zostanie uruchomiona ręcznie.
+ AC-1 satisfied: tabela shipment_presets z pełnym zestawem kolumn
+
+
+
+ Task 2: ShipmentPresetRepository — CRUD na tabeli
+ src/Modules/Shipments/ShipmentPresetRepository.php
+
+ Utworzyć klasę `ShipmentPresetRepository` w namespace `App\Modules\Shipments`:
+
+ - Constructor: `__construct(private readonly PDO $pdo)`
+ - `findAll(): array` — SELECT * ORDER BY sort_order ASC, id ASC
+ - `findById(int $id): ?array` — SELECT WHERE id = :id
+ - `create(array $data): int` — INSERT, return lastInsertId
+ - `update(int $id, array $data): void` — UPDATE WHERE id = :id
+ - `delete(int $id): void` — DELETE WHERE id = :id
+
+ Wzorować się na istniejącym `ShipmentPackageRepository` — ten sam styl (PDO prepared statements, final class).
+
+ Kolumny do zapisu w create/update:
+ name, color, carrier, provider_code, delivery_method_id, credentials_id, carrier_id,
+ package_type, length_cm, width_cm, height_cm, weight_kg, sender_point_id, label_format, sort_order
+
+ Avoid: Medoo — ten moduł używa czystego PDO (jak ShipmentPackageRepository).
+
+ php -l src/Modules/Shipments/ShipmentPresetRepository.php — brak błędów składniowych
+ AC-2 partially (repository layer ready), AC-3, AC-4, AC-5 data layer ready
+
+
+
+ Task 3: ShipmentPresetController + routing
+ src/Modules/Shipments/ShipmentPresetController.php, routes/web.php
+
+ 1. Utworzyć `ShipmentPresetController` w namespace `App\Modules\Shipments`:
+ - Constructor: `__construct(private readonly ShipmentPresetRepository $repository)`
+ - `list(Request $request): Response` — GET, zwraca JSON array presetów
+ - `store(Request $request): Response` — POST, walidacja (name required), tworzy preset, zwraca JSON z id + 201
+ - `update(Request $request): Response` — PUT, walidacja, aktualizuje preset, zwraca JSON 200
+ - `destroy(Request $request): Response` — DELETE, usuwa preset, zwraca JSON 204
+
+ Dla store/update wyciągnąć dane z request:
+ - name, color, carrier, provider_code, delivery_method_id, credentials_id, carrier_id,
+ package_type, length_cm, width_cm, height_cm, weight_kg, sender_point_id, label_format, sort_order
+
+ Walidacja: `name` nie może być pusty. Resztę ustawić z defaults.
+ Brak CSRF dla JSON API — ale wymagać auth middleware.
+
+ 2. W `routes/web.php`:
+ - Dodać `use App\Modules\Shipments\ShipmentPresetRepository;`
+ - Dodać `use App\Modules\Shipments\ShipmentPresetController;`
+ - Instancja: `$presetRepository = new ShipmentPresetRepository($app->db());`
+ - Instancja: `$presetController = new ShipmentPresetController($presetRepository);`
+ - Routing (z $authMiddleware):
+ ```
+ $router->get('/api/shipment-presets', [$presetController, 'list'], [$authMiddleware]);
+ $router->post('/api/shipment-presets', [$presetController, 'store'], [$authMiddleware]);
+ $router->put('/api/shipment-presets/{id}', [$presetController, 'update'], [$authMiddleware]);
+ $router->delete('/api/shipment-presets/{id}', [$presetController, 'destroy'], [$authMiddleware]);
+ ```
+
+ Umieścić routing w sekcji API (po print API routes, przed zamknięciem).
+
+ Wzorować styl Response na istniejących API controllerach (PrintApiController — Response::json).
+
+ php -l src/Modules/Shipments/ShipmentPresetController.php — brak błędów składniowych. Sprawdzić że routes/web.php nie ma syntax errors: php -l routes/web.php
+ AC-2, AC-3, AC-4, AC-5 satisfied: pełny JSON API dla presetów
+
+
+
+
+
+
+## DO NOT CHANGE
+- src/Modules/Shipments/ShipmentController.php (modyfikacja w fazie 24)
+- resources/views/shipments/prepare.php (modyfikacja w fazie 24)
+- src/Modules/Shipments/ShipmentPackageRepository.php (stabilne)
+- Istniejące migracje w database/migrations/
+
+## SCOPE LIMITS
+- Tylko backend (tabela + repository + controller + routing)
+- Brak zmian UI — to faza 24
+- Brak walidacji CSRF w API JSON (auth middleware wystarczający)
+- Presety globalne (bez user_id)
+
+
+
+
+Before declaring plan complete:
+- [ ] Migracja SQL poprawna składniowo
+- [ ] php -l na ShipmentPresetRepository.php — OK
+- [ ] php -l na ShipmentPresetController.php — OK
+- [ ] php -l na routes/web.php — OK
+- [ ] Routing dodany z auth middleware
+- [ ] All acceptance criteria met
+
+
+
+- Tabela shipment_presets gotowa do migracji
+- Repository z pełnym CRUD
+- Controller z 4 endpointami JSON API
+- Routing zarejestrowany w routes/web.php
+- Brak błędów PHP
+
+
+
diff --git a/.paul/phases/23-shipment-presets-backend/23-01-SUMMARY.md b/.paul/phases/23-shipment-presets-backend/23-01-SUMMARY.md
new file mode 100644
index 0000000..e8299d5
--- /dev/null
+++ b/.paul/phases/23-shipment-presets-backend/23-01-SUMMARY.md
@@ -0,0 +1,124 @@
+---
+phase: 23-shipment-presets-backend
+plan: 01
+subsystem: shipments
+tags: [shipment-presets, crud, api, json]
+
+requires:
+ - phase: none
+ provides: n/a
+provides:
+ - Tabela shipment_presets w DB
+ - ShipmentPresetRepository z pełnym CRUD
+ - JSON API (4 endpointy) do zarządzania presetami
+affects: [24-shipment-presets-ui, 25-shipment-presets-management]
+
+tech-stack:
+ added: []
+ patterns:
+ - POST-based JSON API (router nie obsługuje PUT/DELETE)
+
+key-files:
+ created:
+ - database/migrations/20260322_000059_create_shipment_presets_table.sql
+ - src/Modules/Shipments/ShipmentPresetRepository.php
+ - src/Modules/Shipments/ShipmentPresetController.php
+ modified:
+ - routes/web.php
+
+key-decisions:
+ - "POST zamiast PUT/DELETE — Router obsługuje tylko GET/POST"
+ - "Presety globalne — brak kolumny user_id"
+
+patterns-established:
+ - "JSON API preset endpoints: /api/shipment-presets, /update, /delete"
+
+duration: 8min
+started: 2026-03-22T00:00:00Z
+completed: 2026-03-22T00:08:00Z
+---
+
+# Phase 23 Plan 01: Shipment Presets Backend Summary
+
+**Tabela DB `shipment_presets`, repository CRUD i 4 endpointy JSON API do zarządzania presetami przesyłek.**
+
+## Performance
+
+| Metric | Value |
+|--------|-------|
+| Duration | ~8 min |
+| Tasks | 3 completed |
+| Files created | 3 |
+| Files modified | 1 |
+
+## Acceptance Criteria Results
+
+| Criterion | Status | Notes |
+|-----------|--------|-------|
+| AC-1: Tabela shipment_presets | Pass | Migracja gotowa z pełnym zestawem kolumn |
+| AC-2: GET lista presetów | Pass | GET /api/shipment-presets zwraca JSON |
+| AC-3: POST tworzy preset | Pass | POST /api/shipment-presets, walidacja name, zwraca 201 |
+| AC-4: Aktualizacja presetu | Pass | POST /api/shipment-presets/update (zmiana z PUT) |
+| AC-5: Usuwanie presetu | Pass | POST /api/shipment-presets/delete (zmiana z DELETE) |
+
+## Accomplishments
+
+- Migracja SQL tworząca tabelę `shipment_presets` z 16 kolumnami (name, color, carrier, wymiary, waga, itp.)
+- `ShipmentPresetRepository` z metodami findAll, findById, create, update, delete + mapParams helper
+- `ShipmentPresetController` z 4 endpointami JSON API (list, store, update, destroy)
+- Routing zarejestrowany w routes/web.php z auth middleware
+- SonarQube: 0 issues na nowych plikach
+
+## Files Created/Modified
+
+| File | Change | Purpose |
+|------|--------|---------|
+| `database/migrations/20260322_000059_create_shipment_presets_table.sql` | Created | Schemat tabeli presetów |
+| `src/Modules/Shipments/ShipmentPresetRepository.php` | Created | CRUD na tabeli shipment_presets |
+| `src/Modules/Shipments/ShipmentPresetController.php` | Created | JSON API controller |
+| `routes/web.php` | Modified | 4 nowe routy + DI presetów |
+
+## Decisions Made
+
+| Decision | Rationale | Impact |
+|----------|-----------|--------|
+| POST zamiast PUT/DELETE | Router (src/Core/Routing/Router.php) obsługuje tylko get() i post() | Endpointy: /update i /delete jako POST |
+| input() zamiast param() dla id | Update/delete nie mają {id} w URL (POST body) | Controller czyta id z request body |
+
+## Deviations from Plan
+
+### Summary
+
+| Type | Count | Impact |
+|------|-------|--------|
+| Auto-fixed | 1 | Router limitation — zmiana metod HTTP na POST |
+
+**Total impact:** Minimalna zmiana API kontraktu, brak wpływu na funkcjonalność.
+
+### Auto-fixed Issues
+
+**1. Router nie obsługuje PUT/DELETE**
+- **Found during:** Task 3 (routing)
+- **Issue:** Plan zakładał PUT/DELETE, router ma tylko get()/post()
+- **Fix:** Zmieniono na POST /api/shipment-presets/update i /delete, id w body
+- **Verification:** php -l routes/web.php — OK
+
+## Issues Encountered
+
+None.
+
+## Next Phase Readiness
+
+**Ready:**
+- Backend API kompletny — faza 24 może budować UI korzystając z tych endpointów
+- Migracja do uruchomienia na serwerze
+
+**Concerns:**
+- Brak
+
+**Blockers:**
+- None
+
+---
+*Phase: 23-shipment-presets-backend, Plan: 01*
+*Completed: 2026-03-22*
diff --git a/database/migrations/20260322_000059_create_shipment_presets_table.sql b/database/migrations/20260322_000059_create_shipment_presets_table.sql
new file mode 100644
index 0000000..6adac75
--- /dev/null
+++ b/database/migrations/20260322_000059_create_shipment_presets_table.sql
@@ -0,0 +1,20 @@
+CREATE TABLE IF NOT EXISTS shipment_presets (
+ id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(100) NOT NULL,
+ color VARCHAR(7) NOT NULL DEFAULT '#3b82f6',
+ carrier VARCHAR(32) NOT NULL COMMENT 'allegro, inpost, apaczka',
+ provider_code VARCHAR(32) NOT NULL COMMENT 'allegro_wza, apaczka, inpost',
+ delivery_method_id VARCHAR(128) NOT NULL,
+ credentials_id VARCHAR(128) NOT NULL DEFAULT '',
+ carrier_id VARCHAR(64) NOT NULL DEFAULT '',
+ package_type VARCHAR(16) NOT NULL DEFAULT 'PACKAGE',
+ length_cm DECIMAL(8,1) NOT NULL DEFAULT 25.0,
+ width_cm DECIMAL(8,1) NOT NULL DEFAULT 20.0,
+ height_cm DECIMAL(8,1) NOT NULL DEFAULT 8.0,
+ weight_kg DECIMAL(8,3) NOT NULL DEFAULT 1.000,
+ sender_point_id VARCHAR(64) NOT NULL DEFAULT '',
+ label_format VARCHAR(8) NOT NULL DEFAULT 'PDF',
+ sort_order INT UNSIGNED NOT NULL DEFAULT 0,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/routes/web.php b/routes/web.php
index fdc4dd1..74ab6c3 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -57,6 +57,8 @@ use App\Modules\Shipments\AllegroShipmentService;
use App\Modules\Shipments\InpostShipmentService;
use App\Modules\Shipments\ShipmentController;
use App\Modules\Shipments\ShipmentPackageRepository;
+use App\Modules\Shipments\ShipmentPresetController;
+use App\Modules\Shipments\ShipmentPresetRepository;
use App\Modules\Shipments\ShipmentProviderRegistry;
use App\Modules\Printing\ApiKeyMiddleware;
use App\Modules\Printing\PrintApiController;
@@ -424,4 +426,13 @@ return static function (Application $app): void {
$router->get('/settings/printing', [$printSettingsController, 'index'], [$authMiddleware]);
$router->post('/settings/printing/keys/create', [$printSettingsController, 'createKey'], [$authMiddleware]);
$router->post('/settings/printing/keys/{id}/delete', [$printSettingsController, 'deleteKey'], [$authMiddleware]);
+
+ // Shipment presets API
+ $presetRepository = new ShipmentPresetRepository($app->db());
+ $presetController = new ShipmentPresetController($presetRepository);
+
+ $router->get('/api/shipment-presets', [$presetController, 'list'], [$authMiddleware]);
+ $router->post('/api/shipment-presets', [$presetController, 'store'], [$authMiddleware]);
+ $router->post('/api/shipment-presets/update', [$presetController, 'update'], [$authMiddleware]);
+ $router->post('/api/shipment-presets/delete', [$presetController, 'destroy'], [$authMiddleware]);
};
diff --git a/src/Modules/Shipments/ShipmentPresetController.php b/src/Modules/Shipments/ShipmentPresetController.php
new file mode 100644
index 0000000..fa8d42a
--- /dev/null
+++ b/src/Modules/Shipments/ShipmentPresetController.php
@@ -0,0 +1,100 @@
+ $this->repository->findAll()]);
+ }
+
+ public function store(Request $request): Response
+ {
+ $name = trim((string) $request->input('name', ''));
+ if ($name === '') {
+ return Response::json(['error' => 'Name is required'], 422);
+ }
+
+ $data = $this->extractData($request);
+ $id = $this->repository->create($data);
+
+ $preset = $this->repository->findById($id);
+
+ return Response::json(['preset' => $preset], 201);
+ }
+
+ public function update(Request $request): Response
+ {
+ $id = (int) $request->input('id', '0');
+ if ($id <= 0) {
+ return Response::json(['error' => 'Invalid preset ID'], 400);
+ }
+
+ $existing = $this->repository->findById($id);
+ if ($existing === null) {
+ return Response::json(['error' => 'Preset not found'], 404);
+ }
+
+ $data = $this->extractData($request);
+ if (trim($data['name']) === '') {
+ return Response::json(['error' => 'Name is required'], 422);
+ }
+
+ $this->repository->update($id, $data);
+
+ $preset = $this->repository->findById($id);
+
+ return Response::json(['preset' => $preset]);
+ }
+
+ public function destroy(Request $request): Response
+ {
+ $id = (int) $request->input('id', '0');
+ if ($id <= 0) {
+ return Response::json(['error' => 'Invalid preset ID'], 400);
+ }
+
+ $existing = $this->repository->findById($id);
+ if ($existing === null) {
+ return Response::json(['error' => 'Preset not found'], 404);
+ }
+
+ $this->repository->delete($id);
+
+ return Response::json(['deleted' => true]);
+ }
+
+ /**
+ * @return array
+ */
+ private function extractData(Request $request): array
+ {
+ return [
+ 'name' => (string) $request->input('name', ''),
+ 'color' => (string) $request->input('color', '#3b82f6'),
+ 'carrier' => (string) $request->input('carrier', ''),
+ 'provider_code' => (string) $request->input('provider_code', ''),
+ 'delivery_method_id' => (string) $request->input('delivery_method_id', ''),
+ 'credentials_id' => (string) $request->input('credentials_id', ''),
+ 'carrier_id' => (string) $request->input('carrier_id', ''),
+ 'package_type' => (string) $request->input('package_type', 'PACKAGE'),
+ 'length_cm' => (string) $request->input('length_cm', '25'),
+ 'width_cm' => (string) $request->input('width_cm', '20'),
+ 'height_cm' => (string) $request->input('height_cm', '8'),
+ 'weight_kg' => (string) $request->input('weight_kg', '1'),
+ 'sender_point_id' => (string) $request->input('sender_point_id', ''),
+ 'label_format' => (string) $request->input('label_format', 'PDF'),
+ 'sort_order' => (string) $request->input('sort_order', '0'),
+ ];
+ }
+}
diff --git a/src/Modules/Shipments/ShipmentPresetRepository.php b/src/Modules/Shipments/ShipmentPresetRepository.php
new file mode 100644
index 0000000..e19a612
--- /dev/null
+++ b/src/Modules/Shipments/ShipmentPresetRepository.php
@@ -0,0 +1,117 @@
+>
+ */
+ public function findAll(): array
+ {
+ $statement = $this->pdo->prepare(
+ 'SELECT * FROM shipment_presets ORDER BY sort_order ASC, id ASC'
+ );
+ $statement->execute();
+ return $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
+ }
+
+ /**
+ * @return array|null
+ */
+ public function findById(int $id): ?array
+ {
+ $statement = $this->pdo->prepare('SELECT * FROM shipment_presets WHERE id = :id LIMIT 1');
+ $statement->execute(['id' => $id]);
+ $row = $statement->fetch(PDO::FETCH_ASSOC);
+ return is_array($row) ? $row : null;
+ }
+
+ /**
+ * @param array $data
+ */
+ public function create(array $data): int
+ {
+ $statement = $this->pdo->prepare(
+ 'INSERT INTO shipment_presets (
+ name, color, carrier, provider_code, delivery_method_id,
+ credentials_id, carrier_id, package_type,
+ length_cm, width_cm, height_cm, weight_kg,
+ sender_point_id, label_format, sort_order
+ ) VALUES (
+ :name, :color, :carrier, :provider_code, :delivery_method_id,
+ :credentials_id, :carrier_id, :package_type,
+ :length_cm, :width_cm, :height_cm, :weight_kg,
+ :sender_point_id, :label_format, :sort_order
+ )'
+ );
+
+ $statement->execute($this->mapParams($data));
+
+ return (int) $this->pdo->lastInsertId();
+ }
+
+ /**
+ * @param array $data
+ */
+ public function update(int $id, array $data): void
+ {
+ $statement = $this->pdo->prepare(
+ 'UPDATE shipment_presets SET
+ name = :name, color = :color, carrier = :carrier,
+ provider_code = :provider_code, delivery_method_id = :delivery_method_id,
+ credentials_id = :credentials_id, carrier_id = :carrier_id,
+ package_type = :package_type,
+ length_cm = :length_cm, width_cm = :width_cm,
+ height_cm = :height_cm, weight_kg = :weight_kg,
+ sender_point_id = :sender_point_id, label_format = :label_format,
+ sort_order = :sort_order
+ WHERE id = :id'
+ );
+
+ $params = $this->mapParams($data);
+ $params['id'] = $id;
+ $statement->execute($params);
+ }
+
+ public function delete(int $id): void
+ {
+ $statement = $this->pdo->prepare('DELETE FROM shipment_presets WHERE id = :id');
+ $statement->execute(['id' => $id]);
+ }
+
+ /**
+ * @param array $data
+ * @return array
+ */
+ private function mapParams(array $data): array
+ {
+ return [
+ 'name' => trim((string) ($data['name'] ?? '')),
+ 'color' => trim((string) ($data['color'] ?? '#3b82f6')),
+ 'carrier' => trim((string) ($data['carrier'] ?? '')),
+ 'provider_code' => trim((string) ($data['provider_code'] ?? '')),
+ 'delivery_method_id' => trim((string) ($data['delivery_method_id'] ?? '')),
+ 'credentials_id' => trim((string) ($data['credentials_id'] ?? '')),
+ 'carrier_id' => trim((string) ($data['carrier_id'] ?? '')),
+ 'package_type' => trim((string) ($data['package_type'] ?? 'PACKAGE')),
+ 'length_cm' => max(0.1, (float) ($data['length_cm'] ?? 25.0)),
+ 'width_cm' => max(0.1, (float) ($data['width_cm'] ?? 20.0)),
+ 'height_cm' => max(0.1, (float) ($data['height_cm'] ?? 8.0)),
+ 'weight_kg' => max(0.001, (float) ($data['weight_kg'] ?? 1.0)),
+ 'sender_point_id' => trim((string) ($data['sender_point_id'] ?? '')),
+ 'label_format' => in_array(strtoupper(trim((string) ($data['label_format'] ?? ''))), ['PDF', 'ZPL'], true)
+ ? strtoupper(trim((string) $data['label_format']))
+ : 'PDF',
+ 'sort_order' => max(0, (int) ($data['sort_order'] ?? 0)),
+ ];
+ }
+}