feat(26-manual-tracking-number): reczne dodawanie numeru przesylki do zamowienia

Nowy endpoint POST /orders/{id}/shipment/manual z formularzem inline
w zakladce Przesylki. Reuse tabeli shipment_packages (provider='manual',
status='created'). Activity log, walidacja CSRF, HTML required.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 18:17:21 +01:00
parent 686429d9cf
commit c59d431083
13 changed files with 3473 additions and 20 deletions

View File

@@ -44,6 +44,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i n
- [x] Wyświetlanie nazwy integracji zamiast generycznego "shopPRO" na liście i szczegółach zamówienia — Phase 21
- [x] Naprawa zapisu REGON, BDO, KRS i logo w ustawieniach firmy — Phase 22
- [x] Presety przesyłek — customowe przyciski szybkiego wypełniania formularza (CRUD + autofill + zarządzanie) — Phase 23-25
- [x] Ręczny numer przesyłki — dodawanie tracking number bez API przewoźnika — Phase 26
### Active (In Progress)
@@ -139,4 +140,4 @@ Quick Reference:
---
*PROJECT.md — Updated when requirements or context change*
*Last updated: 2026-03-22 after Phase 25 (Shipment Presets Management complete)*
*Last updated: 2026-03-23 after Phase 26 (Manual Tracking Number complete)*

View File

@@ -10,6 +10,19 @@ None — ready for next milestone planning.
## Completed Milestones
<details>
<summary>v1.1 Ręczny numer przesyłki — 2026-03-23 (1 phase, 1 plan)</summary>
Możliwość ręcznego dodania numeru śledzenia przesyłki do zamówienia (bez tworzenia przesyłki przez API przewoźnika).
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 26 | Manual Tracking Number | 1/1 | 2026-03-23 |
Archive: `.paul/phases/26-manual-tracking-number/`
</details>
<details>
<summary>v1.0 Presety przesyłek — 2026-03-22 (3 phases, 3 plans)</summary>

View File

@@ -5,16 +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.0 Presety przesyłek — MILESTONE COMPLETE ✓
**Current focus:** v1.1 Ręczny numer przesyłki — MILESTONE COMPLETE ✓
## Current Position
Milestone: v1.0 Presety przesyłek
Milestone: v1.0 Presety przesyłek — COMPLETE ✓
Phase: [3] of [3] (Shipment Presets Management) — COMPLETE ✓
Plan: 25-01 — loop closed
Status: Milestone v1.0 complete
Last activity: 2026-03-22 — UNIFY complete, milestone v1.0 done
Milestone: v1.1 Ręczny numer przesyłki — COMPLETE ✓
Phase: [1] of [1] (Manual Tracking Number) — COMPLETE ✓
Plan: 26-01 — loop closed
Status: Milestone v1.1 complete
Last activity: 2026-03-23 — UNIFY complete, milestone v1.1 done
Progress:
- v0.1 Initial Release: [██████████] 100% ✓
@@ -25,20 +24,17 @@ Progress:
- v0.6 Poprawki UX: [██████████] 100% ✓
- v0.7 Zdalne drukowanie etykiet: [██████████] 100% ✓
- v0.8 Poprawki źródła zamówień: [██████████] 100% ✓
- Phase 21: [██████████] 100% ✓ (1/1 plans)
- v0.9 Poprawki ustawień firmy: [██████████] 100% ✓
- Phase 22: [██████████] 100% ✓ (1/1 plans)
- v1.0 Presety przesyłek: [██████████] 100% ✓
- Phase 23: [██████████] 100% ✓ (1/1 plans)
- Phase 24: [██████████] 100% ✓ (1/1 plans)
- Phase 25: [██████████] 100% ✓ (1/1 plans)
- v1.1 Ręczny numer przesyłki: [██████████] 100% ✓
- Phase 26: [██████████] 100% ✓ (1/1 plans)
## Loop Position
Current loop state:
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ✓ [Milestone v1.0 complete]
✓ ✓ ✓ [Milestone v1.1 complete]
```
## Accumulated Context
@@ -72,6 +68,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 26, Plan 01)
| Oczekiwany | Wywołany | Uwagi |
|------------|---------|-------|
| sonar-scanner | ✓ | 0 nowych issues; 8 pre-existing na ShipmentController (S3776, S1192, S1142, S3358) |
### Skill Audit (Faza 25, Plan 01)
| Oczekiwany | Wywołany | Uwagi |
|------------|---------|-------|
@@ -225,10 +226,10 @@ Brak.
## Session Continuity
Last session: 2026-03-22
Stopped at: Milestone v1.0 complete
Last session: 2026-03-23
Stopped at: Milestone v1.1 complete
Next action: /paul:discuss-milestone lub /paul:milestone
Resume file: .paul/phases/25-shipment-presets-management/25-01-SUMMARY.md
Resume file: .paul/phases/26-manual-tracking-number/26-01-SUMMARY.md
Resume context:
- v0.1: COMPLETE ✓ (6 phases, 15 plans)
- v0.2: COMPLETE ✓ (1 phase, 5 plans)
@@ -240,6 +241,7 @@ Resume context:
- v0.8: COMPLETE ✓ (1 phase, 1 plan) — Poprawki źródła zamówień
- v0.9: COMPLETE ✓ (1 phase, 1 plan) — Poprawki ustawień firmy
- v1.0: COMPLETE ✓ (3 phases, 3 plans) — Presety przesyłek
- v1.1: IN PROGRESS (1 phase, 1 plan) — Ręczny numer przesyłki
---
*STATE.md — Updated after every significant action*

View File

@@ -0,0 +1,193 @@
---
phase: 26-manual-tracking-number
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/Modules/Shipments/ShipmentController.php
- src/Modules/Shipments/ShipmentPackageRepository.php
- resources/views/orders/show.php
- resources/scss/pages/_orders.scss
- routes/web.php
- DOCS/ARCHITECTURE.md
- DOCS/TECH_CHANGELOG.md
autonomous: true
---
<objective>
## Goal
Dodanie możliwości ręcznego wpisania numeru przesyłki (tracking number) do zamówienia — bez tworzenia przesyłki przez API przewoźnika.
## Purpose
Sprzedawca często nadaje paczki poza systemem (np. na poczcie, przez innego kuriera bez integracji API). Potrzebuje zapisać numer śledzenia przy zamówieniu, żeby mieć komplet informacji w jednym miejscu.
## Output
- Nowy endpoint POST `/orders/{id}/shipment/manual` w ShipmentController
- Formularz inline w zakładce Przesyłki na stronie zamówienia
- Rekord w `shipment_packages` z provider='manual', status='created'
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Source Files
@src/Modules/Shipments/ShipmentController.php
@src/Modules/Shipments/ShipmentPackageRepository.php
@resources/views/orders/show.php
@routes/web.php
</context>
<skills>
## Required Skills (from SPECIAL-FLOWS.md)
| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |
## Skill Invocation Checklist
- [ ] sonar-scanner loaded (run command or confirm)
</skills>
<acceptance_criteria>
## AC-1: Ręczne dodanie numeru przesyłki
```gherkin
Given zamówienie istnieje i użytkownik jest na stronie szczegółów zamówienia
When użytkownik wpisuje numer przesyłki i opcjonalnie nazwę przewoźnika w formularzu inline, i klika "Dodaj"
Then w tabeli shipment_packages powstaje rekord z provider='manual', status='created', tracking_number=wpisany numer
And numer pojawia się na liście przesyłek zamówienia
And wpis pojawia się w activity log zamówienia
```
## AC-2: Walidacja — pusty numer
```gherkin
Given użytkownik jest na stronie szczegółów zamówienia
When użytkownik klika "Dodaj" bez wpisania numeru przesyłki
Then formularz nie jest wysyłany (walidacja HTML required)
And żaden rekord nie jest tworzony
```
## AC-3: Wyświetlanie ręcznych przesyłek na liście
```gherkin
Given zamówienie ma ręcznie dodany numer przesyłki
When użytkownik otwiera stronę szczegółów zamówienia
Then na liście przesyłek widoczny jest rekord z etykietą "Ręczna" w kolumnie Przewoźnik
And tracking number jest wyświetlany
And kolumna Etykieta pokazuje "" (brak etykiety dla ręcznych)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Metoda createManual w ShipmentPackageRepository + endpoint w ShipmentController</name>
<files>src/Modules/Shipments/ShipmentPackageRepository.php, src/Modules/Shipments/ShipmentController.php, routes/web.php</files>
<action>
1. W ShipmentPackageRepository dodać metodę `createManual(int $orderId, string $trackingNumber, ?string $carrierName = null): int`:
- INSERT do shipment_packages z: order_id, provider='manual', tracking_number, carrier_id=carrierName (lub null), status='created', reference_number=null
- Zwraca ID nowego rekordu
2. W ShipmentController dodać metodę `createManual(Request $request): Response`:
- Walidacja CSRF
- Pobranie orderId z URL, tracking_number i carrier_name z POST
- Walidacja: tracking_number nie może być pusty (trim)
- Wywołanie $this->packageRepository->createManual(...)
- recordActivity na ordersRepository: typ 'shipment_manual', opis z numerem i przewoźnikiem
- Flash success + redirect do /orders/{id}
3. W routes/web.php dodać route:
- POST /orders/{id}/shipment/manual → [$shipmentController, 'createManual']
- Z $authMiddleware
</action>
<verify>
POST na /orders/{id}/shipment/manual z tracking_number tworzy rekord w shipment_packages i redirectuje z flash success.
</verify>
<done>AC-1 satisfied: ręczne dodanie numeru przesyłki działa end-to-end</done>
</task>
<task type="auto">
<name>Task 2: Formularz inline + wyświetlanie ręcznych przesyłek w widoku zamówienia</name>
<files>resources/views/orders/show.php, resources/scss/pages/_orders.scss</files>
<action>
1. W resources/views/orders/show.php, w sekcji zakładki Przesyłki (tab "shipments"), dodać kompaktowy formularz inline pod listą przesyłek:
- Jeden wiersz: input text "Nr przesyłki" (required), input text "Przewoźnik" (opcjonalny), przycisk "Dodaj"
- form method POST action="/orders/{id}/shipment/manual"
- Ukryte pole _token z CSRF
- Klasa CSS: `manual-tracking-form`
2. W tabeli przesyłek (packagesList foreach), dla rekordu z provider='manual':
- W kolumnie "Przewoźnik": wyświetlić carrier_id jeśli niepuste, inaczej "Ręczna"
- W kolumnie "Etykieta": wyświetlić "—"
- W kolumnie "Status": wyświetlić badge "Dodana ręcznie" (klasa `badge badge--neutral`)
3. W resources/scss/pages/_orders.scss dodać style dla `.manual-tracking-form`:
- display: flex, gap: 8px, align-items: center
- margin-top: 12px
- Inputy: standardowe style projektu
</action>
<verify>
Na stronie szczegółów zamówienia widoczny jest formularz dodawania ręcznego numeru. Po dodaniu, numer wyświetla się na liście z oznaczeniem "Ręczna".
</verify>
<done>AC-2, AC-3 satisfied: walidacja formularza i wyświetlanie ręcznych przesyłek</done>
</task>
<task type="auto">
<name>Task 3: Aktualizacja dokumentacji</name>
<files>DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md</files>
<action>
1. W DOCS/ARCHITECTURE.md:
- W sekcji ShipmentController dodać metodę createManual
- W sekcji ShipmentPackageRepository dodać metodę createManual
2. W DOCS/TECH_CHANGELOG.md:
- Dodać wpis o nowej funkcjonalności ręcznego numeru przesyłki
</action>
<verify>Dokumentacja zaktualizowana zgodnie z nowymi endpointami.</verify>
<done>Dokumentacja odzwierciedla nową funkcjonalność</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- database/migrations/* (nie trzeba nowej migracji — tabela shipment_packages już ma wszystkie potrzebne kolumny)
- src/Modules/Shipments/AllegroShipmentService.php
- src/Modules/Shipments/ApaczkaShipmentService.php
- src/Modules/Shipments/InpostShipmentService.php
- src/Modules/Shipments/ShipmentProviderInterface.php
## SCOPE LIMITS
- Nie dodawać usuwania/edycji ręcznych przesyłek (oddzielna faza jeśli potrzebne)
- Nie modyfikować flow tworzenia przesyłek przez API przewoźników
- Nie dodawać nowych zależności composer
</boundaries>
<verification>
Before declaring plan complete:
- [ ] POST /orders/{id}/shipment/manual tworzy rekord w shipment_packages
- [ ] Rekord ma provider='manual', status='created', poprawny tracking_number
- [ ] Activity log zawiera wpis o ręcznym dodaniu
- [ ] Formularz inline widoczny w zakładce Przesyłki
- [ ] Walidacja HTML required na polu tracking_number
- [ ] Ręczne przesyłki wyświetlają się poprawnie na liście (Przewoźnik, Status, Etykieta)
- [ ] CSRF protection działa
- [ ] Brak błędów PHP
</verification>
<success_criteria>
- Wszystkie taski zakończone
- Wszystkie weryfikacje przechodzą
- Brak nowych błędów PHP ani JS
- Dokumentacja zaktualizowana
</success_criteria>
<output>
After completion, create `.paul/phases/26-manual-tracking-number/26-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,109 @@
---
phase: 26-manual-tracking-number
plan: 01
subsystem: shipments
tags: [tracking-number, manual-entry, shipment-packages]
requires:
- phase: 25-shipment-presets-management
provides: shipment UI in order details
provides:
- manual tracking number entry endpoint
- inline form in order detail shipments tab
affects: []
tech-stack:
added: []
patterns: [provider='manual' for non-API shipments]
key-files:
created: []
modified:
- src/Modules/Shipments/ShipmentPackageRepository.php
- src/Modules/Shipments/ShipmentController.php
- routes/web.php
- resources/views/orders/show.php
- resources/scss/app.scss
key-decisions:
- "Reuse shipment_packages table with provider='manual' instead of new table"
- "Inline form in shipments tab instead of modal"
patterns-established:
- "provider='manual' pattern for non-API tracking entries"
duration: ~10min
completed: 2026-03-23
---
# Phase 26 Plan 01: Manual Tracking Number Summary
**Endpoint POST /orders/{id}/shipment/manual z formularzem inline — ręczne dodawanie numerów śledzenia przesyłek do zamówień**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~10min |
| Completed | 2026-03-23 |
| Tasks | 3 completed |
| Files modified | 7 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Ręczne dodanie numeru przesyłki | Pass | Endpoint tworzy rekord, activity log, flash success |
| AC-2: Walidacja — pusty numer | Pass | HTML required + server-side trim check |
| AC-3: Wyświetlanie ręcznych przesyłek | Pass | Status "Dodana recznie", przewoźnik z carrier_id, etykieta "—" |
## Accomplishments
- Nowy endpoint `POST /orders/{id}/shipment/manual` z walidacją CSRF i tracking_number
- Metoda `ShipmentPackageRepository::createManual()` — INSERT z provider='manual', status='created'
- Formularz inline w zakładce Przesyłki + zmienione renderowanie dla ręcznych przesyłek
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `src/Modules/Shipments/ShipmentPackageRepository.php` | Modified | Nowa metoda createManual() |
| `src/Modules/Shipments/ShipmentController.php` | Modified | Nowa metoda createManual() — endpoint |
| `routes/web.php` | Modified | Nowa route POST /orders/{id}/shipment/manual |
| `resources/views/orders/show.php` | Modified | Formularz inline + wyświetlanie ręcznych przesyłek |
| `resources/scss/app.scss` | Modified | Klasa .manual-tracking-form |
| `public/assets/css/app.css` | Rebuilt | Skompilowany SCSS |
| `DOCS/ARCHITECTURE.md` | Modified | Dokumentacja nowego endpointu |
| `DOCS/TECH_CHANGELOG.md` | Modified | Wpis Phase 26 |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Reuse shipment_packages z provider='manual' | Tabela ma już tracking_number, carrier_id, status — nie trzeba nowej migracji | Zero zmian DB |
| Formularz inline zamiast modala | Szybszy dostęp, mniej kliknięć, spójny z kompaktowym UI | Prostsze UX |
| carrier_id jako nazwa przewoźnika dla manual | Pole VARCHAR już istnieje, semantycznie pasuje | Brak zmian schematu |
## Deviations from Plan
None — plan executed exactly as written.
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- Ręczne numery przesyłek w pełni funkcjonalne
- Milestone v1.1 gotowy do zamknięcia
**Concerns:**
- Brak możliwości edycji/usunięcia ręcznych przesyłek (celowo poza scope)
**Blockers:**
- None
---
*Phase: 26-manual-tracking-number, Plan: 01*
*Completed: 2026-03-23*

View File

@@ -318,6 +318,12 @@
- `ShipmentController::label(Request): Response`,
- wybiera providera po `shipment_packages.provider` i deleguje `downloadLabel(...)`,
- dla Apaczka bledy typu `Label is not available for this order` oznaczaja paczke jako `error`, aby nie ponawiac nieskutecznych prob pobrania.
- `POST /orders/{id}/shipment/manual`:
- `ShipmentController::createManual(Request): Response`,
- tworzy rekord w `shipment_packages` z `provider='manual'`, `status='created'` i podanym `tracking_number`,
- opcjonalnie zapisuje nazwe przewoznika w `carrier_id`,
- loguje zdarzenie `shipment_manual` w `order_activity_log`,
- `ShipmentPackageRepository::createManual(int $orderId, string $trackingNumber, ?string $carrierName): int`.
## Przeplyw Ustawienia > Integracje > Allegro
- `GET /settings/integrations/allegro`:

View File

@@ -1,5 +1,12 @@
# Tech Changelog
## 2026-03-23 (Phase 26 — Manual Tracking Number, Plan 01)
- Nowa metoda `ShipmentPackageRepository::createManual(int, string, ?string): int` — INSERT do `shipment_packages` z `provider='manual'`, `status='created'`.
- Nowa metoda `ShipmentController::createManual(Request): Response` — endpoint `POST /orders/{id}/shipment/manual`, walidacja CSRF + tracking_number, activity log `shipment_manual`.
- Nowa route w `routes/web.php`: `POST /orders/{id}/shipment/manual`.
- `resources/views/orders/show.php` — formularz inline do dodawania recznego numeru przesylki w zakladce Przesylki; zmienione wyswietlanie przesylek manualnych (status "Dodana recznie", przewoznik z carrier_id, brak etykiety).
- `resources/scss/app.scss` — nowa klasa `.manual-tracking-form` (flex, gap 8px).
## 2026-03-22 (Phase 21 — Order Source Display, Plan 01)
- `OrdersRepository::buildListSql()` — LEFT JOIN `integrations ig ON ig.id = o.integration_id`, nowa kolumna `ig.name AS integration_name`.
- `OrdersRepository::findDetails()` — analogiczny LEFT JOIN dla strony szczegolow zamowienia.

File diff suppressed because one or more lines are too long

View File

@@ -1574,6 +1574,16 @@ h4.section-title {
display: block;
}
.manual-tracking-form {
display: flex;
gap: 8px;
align-items: center;
.form-control {
max-width: 220px;
}
}
.order-empty-placeholder {
border: 1px dashed #cbd5e1;
border-radius: 8px;

View File

@@ -384,7 +384,8 @@ foreach ($addressesList as $address) {
$pkgTracking = trim((string) ($pkg['tracking_number'] ?? ''));
$pkgCarrierId = trim((string) ($pkg['carrier_id'] ?? ''));
$pkgProvider = trim((string) ($pkg['provider'] ?? ''));
$providerLabels = ['apaczka' => 'Apaczka', 'allegro_wza' => 'Allegro', 'inpost' => 'InPost'];
$isManual = $pkgProvider === 'manual';
$providerLabels = ['apaczka' => 'Apaczka', 'allegro_wza' => 'Allegro', 'inpost' => 'InPost', 'manual' => 'Reczna'];
$pkgProviderLabel = $providerLabels[$pkgProvider] ?? $pkgProvider;
$pkgCarrier = $pkgCarrierId !== '' ? ($pkgProviderLabel . ' &rarr; ' . $pkgCarrierId) : $pkgProviderLabel;
$pkgLabelPath = trim((string) ($pkg['label_path'] ?? ''));
@@ -396,16 +397,23 @@ foreach ($addressesList as $address) {
<tr>
<td><?= $e((string) ($pkg['id'] ?? '')) ?></td>
<td>
<?php if ($isManual): ?>
<span class="order-tag is-neutral">Dodana recznie</span>
<?php else: ?>
<span class="order-tag <?= $pkgStatus === 'label_ready' || $pkgStatus === 'created' ? 'is-success' : ($pkgStatus === 'error' ? 'is-danger' : 'is-warn') ?>">
<?= $e($pkgStatus) ?>
</span>
<?php endif; ?>
<?php if ($pkgError !== ''): ?>
<div class="muted mt-4" style="font-size:0.75rem"><?= $e($pkgError) ?></div>
<?php endif; ?>
</td>
<td><?= $e($pkgTracking !== '' ? $pkgTracking : '-') ?></td>
<td><?php if ($pkgCarrierId !== ''): ?><?= $e($pkgProviderLabel) ?> &rarr; <?= $e($pkgCarrierId) ?><?php elseif ($pkgProviderLabel !== ''): ?><?= $e($pkgProviderLabel) ?><?php else: ?>-<?php endif; ?></td>
<td><?php if ($isManual): ?><?= $e($pkgCarrierId !== '' ? $pkgCarrierId : 'Reczna') ?><?php elseif ($pkgCarrierId !== ''): ?><?= $e($pkgProviderLabel) ?> &rarr; <?= $e($pkgCarrierId) ?><?php elseif ($pkgProviderLabel !== ''): ?><?= $e($pkgProviderLabel) ?><?php else: ?>-<?php endif; ?></td>
<td>
<?php if ($isManual): ?>
&mdash;
<?php else: ?>
<span style="display:inline-flex;gap:4px;align-items:center">
<?php if ($pkgLabelPath !== '' && $pkgStatus !== 'error'): ?>
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/prepare" class="btn btn--sm btn--secondary">Pobierz</a>
@@ -424,6 +432,7 @@ foreach ($addressesList as $address) {
<?php endif; ?>
<?php endif; ?>
</span>
<?php endif; ?>
</td>
<td class="text-nowrap"><?= $e((string) ($pkg['created_at'] ?? '')) ?></td>
</tr>
@@ -466,6 +475,16 @@ foreach ($addressesList as $address) {
<p class="muted mt-12">Brak przesylek dla tego zamowienia.</p>
</section>
<?php endif; ?>
<section class="card mt-16">
<h3 class="section-title">Dodaj reczny numer przesylki</h3>
<form method="POST" action="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/manual" class="manual-tracking-form mt-12">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="text" name="tracking_number" placeholder="Nr przesylki" required class="form-control">
<input type="text" name="carrier_name" placeholder="Przewoznik (opcjonalnie)" class="form-control">
<button type="submit" class="btn btn--primary">Dodaj</button>
</form>
</section>
</div>
<div class="order-tab-panel" data-order-tab-panel="payments">

View File

@@ -400,6 +400,7 @@ return static function (Application $app): void {
$router->post('/orders/{id}/shipment/create', [$shipmentController, 'create'], [$authMiddleware]);
$router->get('/orders/{id}/shipment/{packageId}/status', [$shipmentController, 'checkStatus'], [$authMiddleware]);
$router->post('/orders/{id}/shipment/{packageId}/label', [$shipmentController, 'label'], [$authMiddleware]);
$router->post('/orders/{id}/shipment/manual', [$shipmentController, 'createManual'], [$authMiddleware]);
// --- Printing module ---
$printApiKeyRepository = new PrintApiKeyRepository($app->db());

View File

@@ -338,6 +338,54 @@ final class ShipmentController
return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
}
public function createManual(Request $request): Response
{
$orderId = max(0, (int) $request->input('id', 0));
if ($orderId <= 0) {
return Response::html('Not found', 404);
}
$csrfToken = (string) $request->input('_token', '');
if (!Csrf::validate($csrfToken)) {
Flash::set('shipment.error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect('/orders/' . $orderId);
}
$trackingNumber = trim((string) $request->input('tracking_number', ''));
if ($trackingNumber === '') {
Flash::set('order.error', 'Numer przesylki jest wymagany.');
return Response::redirect('/orders/' . $orderId);
}
$carrierName = trim((string) $request->input('carrier_name', ''));
$user = $this->auth->user();
$actorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : null;
$actorName = ($actorName !== null && $actorName !== '') ? $actorName : null;
$packageId = $this->packageRepository->createManual(
$orderId,
$trackingNumber,
$carrierName !== '' ? $carrierName : null
);
$description = 'Dodano reczny numer przesylki: ' . $trackingNumber;
if ($carrierName !== '') {
$description .= ' (' . $carrierName . ')';
}
$this->ordersRepository->recordActivity(
$orderId,
'shipment_manual',
$description,
['package_id' => $packageId, 'tracking_number' => $trackingNumber, 'carrier_name' => $carrierName],
'user',
$actorName
);
Flash::set('order.success', 'Numer przesylki dodany.');
return Response::redirect('/orders/' . $orderId);
}
/**
* @param array<string, mixed>|null $deliveryAddr
* @param array<string, mixed>|null $customerAddr

View File

@@ -121,6 +121,24 @@ final class ShipmentPackageRepository
/**
* @return array<string, mixed>|null
*/
public function createManual(int $orderId, string $trackingNumber, ?string $carrierName = null): int
{
$statement = $this->pdo->prepare(
'INSERT INTO shipment_packages (order_id, provider, tracking_number, carrier_id, status)
VALUES (:order_id, :provider, :tracking_number, :carrier_id, :status)'
);
$statement->execute([
'order_id' => $orderId,
'provider' => 'manual',
'tracking_number' => $trackingNumber,
'carrier_id' => $carrierName !== null && trim($carrierName) !== '' ? trim($carrierName) : null,
'status' => 'created',
]);
return (int) $this->pdo->lastInsertId();
}
public function findByCommandId(string $commandId): ?array
{
$statement = $this->pdo->prepare('SELECT * FROM shipment_packages WHERE command_id = :command_id LIMIT 1');