update
This commit is contained in:
122
.claude/commands/wygeneruj-projekty.md
Normal file
122
.claude/commands/wygeneruj-projekty.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Generowanie projektów graficznych z zamówień
|
||||
|
||||
Jesteś silnikiem generowania projektów graficznych dla zamówień w orderPRO. Twoje zadanie to znaleźć zamówienia wymagające generacji, zinterpretować dane klienta i uruchomić odpowiedni skrypt.
|
||||
|
||||
## Przepływ
|
||||
|
||||
### 1. Połącz się z bazą danych
|
||||
|
||||
Wczytaj dane z pliku `.env` w katalogu projektu:
|
||||
- `DB_HOST_REMOTE` — host bazy (użyj remote, nie localhost)
|
||||
- `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `DB_PORT`
|
||||
|
||||
Użyj `"C:/xampp/mysql/bin/mysql.exe"` do zapytań (z flagą `--default-character-set=utf8mb4`).
|
||||
|
||||
### 2. Pobierz aktywne mapowania
|
||||
|
||||
```sql
|
||||
SELECT id, product_name_pattern, script_name, output_dir
|
||||
FROM project_mappings
|
||||
WHERE is_active = 1;
|
||||
```
|
||||
|
||||
Jeśli brak mapowań — poinformuj użytkownika i zakończ.
|
||||
|
||||
### 3. Znajdź zamówienia do generacji (z dopasowaniem do mapowań)
|
||||
|
||||
Schemat bazy danych — kluczowe kolumny:
|
||||
- `orders.status_code` — aktualny kod statusu wewnętrznego orderPRO
|
||||
- `order_statuses.code` — kody statusów, wiązanie: `orders.status_code = order_statuses.code`
|
||||
- `order_statuses.group_id` → `order_status_groups.id` — grupy statusów
|
||||
- `order_items.original_name` — nazwa produktu
|
||||
- `order_items.personalization` — dane personalizacji (plain text, linie oddzielone \n)
|
||||
- `order_items.project_generated` — 0 = nie wygenerowano, 1 = wygenerowano
|
||||
- `order_addresses.address_type` — typ adresu ('customer', 'delivery', 'invoice')
|
||||
- `order_addresses.name` — pełne imię i nazwisko kupującego
|
||||
|
||||
Jedno zapytanie łączy zamówienia z mapowaniami (JOIN eliminuje produkty bez mapowania):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
oi.id AS item_id,
|
||||
oi.original_name AS product_name,
|
||||
oi.personalization,
|
||||
oi.quantity,
|
||||
o.id AS order_id,
|
||||
o.internal_order_number,
|
||||
o.status_code AS status_code,
|
||||
oa.name AS buyer_name,
|
||||
pm.script_name,
|
||||
pm.output_dir
|
||||
FROM order_items oi
|
||||
JOIN orders o ON oi.order_id = o.id
|
||||
JOIN order_statuses os ON o.status_code = os.code
|
||||
JOIN order_status_groups osg ON os.group_id = osg.id
|
||||
JOIN project_mappings pm ON pm.is_active = 1
|
||||
AND oi.original_name LIKE CONCAT('%', pm.product_name_pattern, '%')
|
||||
LEFT JOIN order_addresses oa ON o.id = oa.order_id AND oa.address_type = 'customer'
|
||||
WHERE osg.id = 2
|
||||
AND oi.project_generated = 0
|
||||
AND oi.personalization IS NOT NULL
|
||||
AND oi.personalization <> ''
|
||||
ORDER BY o.id;
|
||||
```
|
||||
|
||||
Jeśli brak wyników — poinformuj użytkownika że nie ma zamówień do generacji pasujących do aktywnych mapowań.
|
||||
|
||||
### 5. Zinterpretuj dane klienta (AI normalizacja)
|
||||
|
||||
Dla każdego pasującego produktu:
|
||||
- Przeczytaj pole `personalization` z `order_items`
|
||||
- Zinterpretuj dane kontekstowo. Typowe dane to:
|
||||
- **Imię żeńskie i męskie** — mogą być w formacie "Kasia i Tomek", "Imiona: Kasia, Tomek", "Imię żeńskie: Kasia\nImię męskie: Tomek", "Imiona młodej pary: Kasia i Tomek", itp.
|
||||
- **Data uroczystości** — format DD.MM.YYYY, może być opisana jako "Data: 30.04.2026" lub "Data uroczystości: 30.04.2026" lub po prostu "30.04.2026"
|
||||
- **Życzenia** — opcjonalny tekst (np. "Na zdrowie!", "Dziękujemy!"). Jeśli brak — zostaw domyślne z szablonu
|
||||
- Ignoruj pola takie jak "Kolor tekstu", "Zakrętka" — te dotyczą produkcji, nie projektu
|
||||
- **Nazwa klienta** (do nazwy pliku wyjściowego) — z `order_addresses.name` (pole `buyer_name` w zapytaniu)
|
||||
|
||||
### 6. Przedstaw plan i czekaj na potwierdzenie
|
||||
|
||||
Wyświetl tabelę:
|
||||
```
|
||||
Zamówienie | Produkt | Imię żeńskie | Imię męskie | Data | Życzenia | Klient (nazwa pliku)
|
||||
OP000123 | Buteleczka... | Kinga | Łukasz | 30.04.2026 | (domyślne) | Kinga Klimczak
|
||||
```
|
||||
|
||||
Zapytaj: "Wygenerować projekty? (tak/nie)"
|
||||
|
||||
### 7. Uruchom generowanie
|
||||
|
||||
Przed generowaniem sprawdź czy Photoshop jest uruchomiony:
|
||||
```bash
|
||||
tasklist //FI "IMAGENAME eq Photoshop.exe" 2>/dev/null | grep -i photoshop
|
||||
```
|
||||
Jeśli Photoshop nie jest uruchomiony — poinformuj użytkownika i poczekaj aż go uruchomi.
|
||||
|
||||
Dla każdego potwierdzonego produktu:
|
||||
1. Uruchom skrypt: `python tools/generowanie/{script_name} --imie_zenskie "X" --imie_meskie "Y" --data "DD.MM.YYYY" --klient "Imię Nazwisko"` (dodaj `--zyczenia` jeśli klient podał niestandardowe)
|
||||
2. Jeśli mapowanie ma `output_dir` — skrypt powinien zapisać tam (jeśli obsługuje)
|
||||
|
||||
### 8. Oznacz w bazie
|
||||
|
||||
Po udanym generowaniu:
|
||||
```sql
|
||||
UPDATE order_items
|
||||
SET project_generated = 1, project_generated_at = NOW()
|
||||
WHERE id = {item_id};
|
||||
```
|
||||
|
||||
### 9. Raport końcowy
|
||||
|
||||
Wyświetl podsumowanie:
|
||||
- Ile projektów wygenerowano
|
||||
- Ile pominięto (brak mapowania, brak personalizacji)
|
||||
- Ewentualne błędy
|
||||
|
||||
## Ważne zasady
|
||||
|
||||
- **Zawsze pytaj o potwierdzenie** przed generowaniem
|
||||
- **Photoshop musi być uruchomiony** — sprawdź przed generowaniem
|
||||
- **Nie oznaczaj jako wygenerowane** jeśli skrypt zwrócił błąd
|
||||
- **Jeśli nie potrafisz zinterpretować personalizacji** — wyświetl surowe dane i zapytaj użytkownika
|
||||
- Skrypty generujące znajdują się w `tools/generowanie/` w katalogu projektu orderPRO
|
||||
162
.claude/commands/wygeneruj-skrypt.md
Normal file
162
.claude/commands/wygeneruj-skrypt.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Kreator skryptu generowania projektu graficznego
|
||||
|
||||
Tworzysz nowy skrypt Python do generowania projektow graficznych z szablonu PSD. Skrypt bedzie uzywal Photoshop API (photoshop-python-api) do podmiany tekstow w Smart Objects z zachowaniem pozycji warstw.
|
||||
|
||||
## Wymagania
|
||||
|
||||
- Photoshop musi byc uruchomiony — sprawdz: `tasklist //FI "IMAGENAME eq Photoshop.exe" 2>/dev/null | grep -i photoshop`
|
||||
- Python z pakietem `photoshop-python-api` musi byc zainstalowany
|
||||
|
||||
## Przepływ
|
||||
|
||||
### 1. Pobierz sciezke do PSD
|
||||
|
||||
Uzytkownik poda sciezke do pliku PSD szablonu. Zweryfikuj ze plik istnieje.
|
||||
|
||||
### 2. Eksploruj strukture warstw
|
||||
|
||||
Otworz PSD w Photoshopie przez Python i rekurencyjnie przejdz wszystkie warstwy:
|
||||
|
||||
```python
|
||||
import photoshop.api as ps
|
||||
|
||||
app = ps.Application()
|
||||
doc = app.open(PSD_PATH)
|
||||
|
||||
def explore(container, indent=0):
|
||||
try:
|
||||
for al in container.artLayers:
|
||||
extra = ''
|
||||
try: extra = f' text="{al.textItem.contents}"'
|
||||
except: pass
|
||||
print(f'{" "*indent}art: "{al.name}" kind={al.kind}{extra}')
|
||||
except: pass
|
||||
try:
|
||||
for ls in container.layerSets:
|
||||
print(f'{" "*indent}set: "{ls.name}"')
|
||||
explore(ls, indent+1)
|
||||
except: pass
|
||||
|
||||
explore(doc)
|
||||
```
|
||||
|
||||
Jezeli warstwa jest Smart Objectem (kind=5 lub 7) — wejdz do niego:
|
||||
```python
|
||||
app.activeDocument.activeLayer = smart_layer
|
||||
desc = ps.ActionDescriptor()
|
||||
ref = ps.ActionReference()
|
||||
ref.putEnumerated(app.stringIDToTypeID('layer'), app.stringIDToTypeID('ordinal'), app.stringIDToTypeID('targetEnum'))
|
||||
desc.putReference(app.stringIDToTypeID('null'), ref)
|
||||
app.executeAction(app.stringIDToTypeID('placedLayerEditContents'), desc)
|
||||
```
|
||||
|
||||
Eksploruj zawartosc Smart Object rekurencyjnie (moze byc wiele poziomow zagniezdzenia).
|
||||
|
||||
### 3. Wyswietl drzewo warstw
|
||||
|
||||
Pokaz uzytkownikowi pelna strukture warstw w czytelnym formacie drzewa, np.:
|
||||
```
|
||||
Projekt (Smart Object)
|
||||
└── Dekoracje (set)
|
||||
└── Tekst (set)
|
||||
└── Dane (set)
|
||||
├── "imie" kind=2 text="Jan"
|
||||
├── "data" kind=2 text="01.01.2026"
|
||||
└── "zyczenia" kind=2 text="Wszystkiego najlepszego"
|
||||
```
|
||||
|
||||
kind=2 to warstwy tekstowe — te sa kandydatami na zmienne.
|
||||
|
||||
### 4. Ustal zmienne
|
||||
|
||||
Zapytaj uzytkownika:
|
||||
- Ktore warstwy tekstowe maja byc parametrami skryptu?
|
||||
- Jakie nazwy parametrow CLI? (np. --imie, --data, --zyczenia)
|
||||
- Ktore parametry sa wymagane, ktore opcjonalne (opcjonalne = zostaw domyslny tekst z szablonu)?
|
||||
|
||||
### 5. Uproszczenie warstw (opcjonalnie)
|
||||
|
||||
Zaproponuj uproszczenie nazw warstw w PSD:
|
||||
- Przemianuj warstwy na czytelne nazwy (np. "Warstwa 5" -> "Tekst", "Kasia" -> "imie_zenskie")
|
||||
- Przemianuj grupy (np. "Warstwa 4" -> "Dane")
|
||||
- Zapytaj uzytkownika o akceptacje
|
||||
|
||||
Jezeli uzytkownik zaakceptuje — wykonaj rename warstw w PSD i zapisz szablon.
|
||||
|
||||
### 6. Ustal sciezki
|
||||
|
||||
Zapytaj uzytkownika:
|
||||
- Nazwa skryptu (np. `kubki_wzor1.py`)
|
||||
- Katalog wyjsciowy `_gotowe/` — domyslnie podfolder `_gotowe` w katalogu szablonu PSD
|
||||
- Nazwa Smart Object glownego (jezeli PSD ma wiele — ktory jest szablonem)
|
||||
|
||||
### 7. Wygeneruj skrypt
|
||||
|
||||
Uzyj wzorca z `tools/generowanie/buteleczki_wzor1.py` jako referencji. Kluczowe elementy:
|
||||
|
||||
```python
|
||||
import argparse
|
||||
import os
|
||||
import photoshop.api as ps
|
||||
|
||||
# Sciezki
|
||||
SZABLON_PATH = r"..."
|
||||
GOTOWE_DIR = r"..."
|
||||
SMART_OBJECT_LAYER = "Projekt"
|
||||
|
||||
def open_smart_object(app):
|
||||
desc = ps.ActionDescriptor()
|
||||
ref = ps.ActionReference()
|
||||
ref.putEnumerated(app.stringIDToTypeID('layer'), app.stringIDToTypeID('ordinal'), app.stringIDToTypeID('targetEnum'))
|
||||
desc.putReference(app.stringIDToTypeID('null'), ref)
|
||||
app.executeAction(app.stringIDToTypeID('placedLayerEditContents'), desc)
|
||||
|
||||
def change_text_preserve_position(layer, new_text):
|
||||
"""Zmienia tekst warstwy z zachowaniem oryginalnej pozycji."""
|
||||
bounds_before = [float(b) for b in layer.bounds]
|
||||
layer.textItem.contents = new_text
|
||||
bounds_after = [float(b) for b in layer.bounds]
|
||||
dx = bounds_before[0] - bounds_after[0]
|
||||
dy = bounds_before[1] - bounds_after[1]
|
||||
if dx != 0 or dy != 0:
|
||||
layer.translate(dx, dy)
|
||||
|
||||
def generate(**kwargs):
|
||||
# ... otworz PSD, wejdz do SO, podmien teksty, zapisz kopie
|
||||
pass
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(...)
|
||||
# ... argumenty na podstawie ustalonych zmiennych
|
||||
args = parser.parse_args()
|
||||
generate(...)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
Wazne:
|
||||
- **Zawsze** uzywaj `change_text_preserve_position()` zamiast bezposredniego `textItem.contents`
|
||||
- Nawigacja do warstw przez `layerSets['nazwa'].artLayers['nazwa']`
|
||||
- Po edycji Smart Object: `so_doc.save()` + `so_doc.close()`
|
||||
- Zapisz jako kopie: `app.activeDocument.saveAs(output_path, psd_opts, True)`
|
||||
- Zamknij oryginal bez zapisu: `app.activeDocument.close(ps.SaveOptions.DoNotSaveChanges)`
|
||||
- Parametr `--klient` zawsze wymagany (nazwa pliku wyjsciowego)
|
||||
|
||||
### 8. Przetestuj
|
||||
|
||||
Po wygenerowaniu skryptu:
|
||||
1. Uruchom go z przykladowymi danymi
|
||||
2. Sprawdz czy PSD zostal wygenerowany
|
||||
3. Popros uzytkownika o weryfikacje wizualna
|
||||
|
||||
### 9. Dodaj mapowanie
|
||||
|
||||
Zaproponuj uzytkownikowi dodanie mapowania w panelu (Ustawienia > Mapowanie projektow) — wzorzec nazwy produktu -> nowy skrypt.
|
||||
|
||||
## Wazne zasady
|
||||
|
||||
- Zamykaj PSD po zakonczeniu (nie zostawiaj otwartych dokumentow)
|
||||
- Przy bledzie — zamknij wszystkie otwarte dokumenty bez zapisu
|
||||
- Skrypty zapisuj w `tools/generowanie/` w katalogu projektu orderPRO
|
||||
- Testuj z realnymi danymi przed zakonczeniem
|
||||
@@ -96,6 +96,11 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
- [x] Naprawa zapisu delivery_price przy imporcie zamowien (Allegro + shopPRO) + backfill — Phase 90
|
||||
- [x] Resilient polling w OrderPROPrint — 3 warstwy timeout (HttpClient/CancellationToken/Watchdog) — Phase 91
|
||||
- [x] Ikona kopiowania imienia i nazwiska kupujacego w szczegolach zamowienia (clipboard copy) — Phase 92
|
||||
- [x] Checkbox "Zapamietaj mnie" z persistent cookie 30 dni + dzialajace komunikaty bledow logowania — Phase 93
|
||||
- [x] Popup podgladu zamowienia na liscie zamowien (zdjecia, personalizacja, notatki, kopiowanie) — Phase 94
|
||||
- [x] AJAX refresh tabeli i panelu statusow po zmianie statusu inline (bez reload strony) — Phase 95
|
||||
- [x] Automatyzacja: warunek "Metoda platnosci" (COD/Przelew/Karta/Inna) oparty na external_payment_type_id — Phase 96
|
||||
- [x] Polautomatyczne generowanie projektow graficznych PSD z zamowien (mapowania, komenda Claude, Photoshop API, flagi statusu) + rename external_status_id na status_code — Phase 97
|
||||
- [ ] Eliminacja zduplikowanego kodu: SslCertificateResolver, ToggleableRepositoryTrait, RedirectPathResolver, ReceiptService — Phase 68
|
||||
|
||||
### Active (In Progress)
|
||||
@@ -198,6 +203,6 @@ Quick Reference:
|
||||
|
||||
---
|
||||
*PROJECT.md — Updated when requirements or context change*
|
||||
*Last updated: 2026-04-09 after Phase 92 completion (Buyer Name Copy)*
|
||||
*Last updated: 2026-04-11 after Phase 96 completion (Automation Payment Method Condition)*
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,11 @@ Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na tel
|
||||
| 90 | Delivery Price Import Fix | 1/1 | Complete |
|
||||
| 91 | Print Client Timeout Resilience | 1/1 | Complete |
|
||||
| 92 | Buyer Name Copy | 1/1 | Complete |
|
||||
| 93 | Remember Me Login | 1/1 | Complete |
|
||||
| 94 | Order Preview Popup | 1/1 | Complete |
|
||||
| 95 | AJAX Table Refresh | 1/1 | Complete |
|
||||
| 96 | Automation Payment Method Condition | 1/1 | Complete |
|
||||
| 97 | Project Generation | 1/1 | Complete |
|
||||
| TBD | Mobile Orders List | - | Not started |
|
||||
| TBD | Mobile Order Details | - | Not started |
|
||||
| TBD | Mobile Settings | - | Not started |
|
||||
@@ -376,4 +381,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-03-12*
|
||||
*Last updated: 2026-04-09 - phase 92 unified (Buyer Name Copy)*
|
||||
*Last updated: 2026-04-11 - phase 96 unified (Automation Payment Method Condition)*
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
|
||||
## Project Reference
|
||||
|
||||
See: .paul/PROJECT.md (updated 2026-04-09)
|
||||
See: .paul/PROJECT.md (updated 2026-04-12)
|
||||
|
||||
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
||||
**Current focus:** Milestone v3.0 - Phase 92 complete, ready for next PLAN
|
||||
**Current focus:** Milestone v3.0 - Phase 97 complete, ready for next phase
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v3.0 Mobile Responsive - In progress
|
||||
Phase: 92 (Buyer Name Copy) — Complete
|
||||
Plan: 92-01 unified
|
||||
Status: Loop complete, ready for next PLAN
|
||||
Last activity: 2026-04-09 — Unified .paul/phases/92-buyer-name-copy/92-01-PLAN.md
|
||||
Phase: 97 (Project Generation) — Complete
|
||||
Plan: 97-01 unified
|
||||
Status: Loop closed, ready for next PLAN
|
||||
Last activity: 2026-04-12 — Unified phase 97
|
||||
|
||||
Progress:
|
||||
- Milestone: [#########.] ~93%
|
||||
- Phase 92: [##########] 100%
|
||||
- Milestone: [#########.] ~95%
|
||||
- Phase 97: [##########] 100%
|
||||
|
||||
## Loop Position
|
||||
|
||||
@@ -29,7 +29,7 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-04-09
|
||||
Stopped at: Plan 92-01 unified
|
||||
Next action: Run /paul:plan for the next prioritized phase
|
||||
Resume file: .paul/phases/92-buyer-name-copy/92-01-SUMMARY.md
|
||||
Last session: 2026-04-12
|
||||
Stopped at: Phase 97 unified
|
||||
Next action: Plan next phase or pause
|
||||
Resume file: .paul/phases/97-project-generation/97-01-SUMMARY.md
|
||||
|
||||
38
.paul/changelog/2026-04-10.md
Normal file
38
.paul/changelog/2026-04-10.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 2026-04-10
|
||||
|
||||
## Co zrobiono
|
||||
|
||||
- [Phase 93, Plan 01] Checkbox "Zapamietaj mnie" z persistent cookie 30 dni
|
||||
- Migracja: kolumna remember_token w tabeli users (hash SHA-256)
|
||||
- Backend: generowanie/walidacja tokena, auto-login z middleware, czyszczenie przy logout
|
||||
- Frontend: checkbox w formularzu logowania, usuniecie zasleppki bledu logowania
|
||||
- Tlumaczenie pl.php: klucz remember_me
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
- `database/migrations/20260410_000081_add_remember_token_to_users.sql`
|
||||
- `src/Modules/Users/UserRepository.php`
|
||||
- `src/Modules/Auth/AuthService.php`
|
||||
- `src/Modules/Auth/AuthController.php`
|
||||
- `src/Modules/Auth/AuthMiddleware.php`
|
||||
- `resources/views/auth/login.php`
|
||||
- `resources/scss/login.scss`
|
||||
- `resources/lang/pl.php`
|
||||
- `public/assets/css/login.css`
|
||||
|
||||
- [Phase 94, Plan 01] Popup podgladu zamowienia na liscie zamowien
|
||||
- Endpoint AJAX: GET /api/orders/{id}/preview (HTML fragment)
|
||||
- Modal z danymi kupujacego, adresem dostawy, produktami (zdjecia + personalizacja), notatkami klienta, podsumowaniem
|
||||
- Ikona oka w kolumnie nr zamowienia, kopiowanie do schowka (clipboard API)
|
||||
|
||||
## Zmienione pliki (Phase 94)
|
||||
|
||||
- `src/Modules/Orders/OrdersController.php`
|
||||
- `routes/web.php`
|
||||
- `resources/views/orders/partials/preview-content.php` (nowy)
|
||||
- `resources/views/orders/partials/preview-modal.php` (nowy)
|
||||
- `resources/views/orders/list.php`
|
||||
- `resources/scss/modules/_order-preview-modal.scss` (nowy)
|
||||
- `resources/scss/app.scss`
|
||||
- `resources/lang/pl.php`
|
||||
- `public/assets/css/app.css`
|
||||
22
.paul/changelog/2026-04-11.md
Normal file
22
.paul/changelog/2026-04-11.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 2026-04-11
|
||||
|
||||
## Co zrobiono
|
||||
|
||||
- [Phase 95, Plan 01] AJAX refresh tabeli i panelu statusow po zmianie statusu inline
|
||||
- Controller index() wykrywa AJAX request i zwraca JSON z fragmentami HTML (tableHtml + panelHtml)
|
||||
- JS inline-status-change.js: fetch + DOM replacement zamiast location.reload()
|
||||
- Scroll, filtry, paginacja zachowane po odswiezeniu
|
||||
|
||||
- [Phase 96, Plan 01] Nowy warunek automatyzacji "Metoda platnosci"
|
||||
- 4 opcje: Platnosc przy odbiorze (COD), Przelew bankowy, Karta/online, Inna
|
||||
- Ewaluacja oparta na external_payment_type_id z pattern matching (StringHelper::isCodPayment)
|
||||
- Frontend: checkbox-group w formularzu + dynamiczne generowanie JS
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
- `src/Modules/Orders/OrdersController.php`
|
||||
- `public/assets/js/modules/inline-status-change.js`
|
||||
- `src/Modules/Automation/AutomationController.php`
|
||||
- `src/Modules/Automation/AutomationService.php`
|
||||
- `resources/views/automation/form.php`
|
||||
- `public/assets/js/modules/automation-form.js`
|
||||
43
.paul/changelog/2026-04-12.md
Normal file
43
.paul/changelog/2026-04-12.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 2026-04-12
|
||||
|
||||
## Co zrobiono
|
||||
|
||||
- [Phase 97, Plan 01] Polautomatyczne generowanie projektow graficznych PSD z danych zamowien
|
||||
- Skrypt Python buteleczki_wzor1.py — Photoshop API, podmiana tekstow w Smart Object z zachowaniem pozycji
|
||||
- Tabela project_mappings + CRUD w panelu (Ustawienia > Mapowanie projektow)
|
||||
- Komenda Claude Code /wygeneruj-projekty — Claude jako silnik interpretacji danych klienta
|
||||
- Kolumny project_generated / project_generated_at w order_items
|
||||
- Flagi statusu projektow na liscie zamowien (ikonki) i w szczegolach (badge przy produkcie)
|
||||
- Rename kolumny orders.external_status_id na orders.status_code (13+ plikow + migracja)
|
||||
- Test end-to-end: zamowienie OP000000159 (Kinga Klimczak) wygenerowane i oznaczone
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
- `tools/generowanie/buteleczki_wzor1.py`
|
||||
- `database/migrations/20260412_000097_add_project_generation.sql`
|
||||
- `database/migrations/20260412_000098_rename_external_status_id_to_status_code.sql`
|
||||
- `src/Modules/Settings/ProjectMappingRepository.php`
|
||||
- `src/Modules/Settings/ProjectMappingController.php`
|
||||
- `resources/views/settings/project-mappings.php`
|
||||
- `resources/scss/modules/_project-mappings.scss`
|
||||
- `.claude/commands/wygeneruj-projekty.md`
|
||||
- `routes/web.php`
|
||||
- `resources/views/layouts/app.php`
|
||||
- `resources/lang/pl.php`
|
||||
- `resources/scss/app.scss`
|
||||
- `src/Modules/Orders/OrdersRepository.php`
|
||||
- `src/Modules/Orders/OrdersController.php`
|
||||
- `src/Modules/Orders/OrderImportRepository.php`
|
||||
- `resources/views/orders/show.php`
|
||||
- `src/Modules/Automation/OrderStatusAgedService.php`
|
||||
- `src/Modules/Automation/AutomationService.php`
|
||||
- `src/Modules/Settings/AllegroOrderImportService.php`
|
||||
- `src/Modules/Settings/AllegroStatusSyncService.php`
|
||||
- `src/Modules/Settings/ShopproStatusSyncService.php`
|
||||
- `src/Modules/Settings/ShopproPaymentStatusSyncService.php`
|
||||
- `src/Modules/Settings/ShopproOrderMapper.php`
|
||||
- `bin/randomize_order_statuses.php`
|
||||
- `bin/deploy_and_seed_orders.php`
|
||||
- `DOCS/DB_SCHEMA.md`
|
||||
- `DOCS/ARCHITECTURE.md`
|
||||
- `DOCS/TECH_CHANGELOG.md`
|
||||
84
.paul/governance/governance_2026-04-10.jsonl
Normal file
84
.paul/governance/governance_2026-04-10.jsonl
Normal file
@@ -0,0 +1,84 @@
|
||||
{"ts":"2026-04-10T09:39:06Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/orderPRO/.paul/phases/93-remember-me-login\"\",\"description\":\"Create phase 93 directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T09:40:13Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\93-remember-me-login\\\\93-01-PLAN.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T09:40:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T09:40:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:00:03Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\database\\\\migrations\\\\20260410_000081_add_remember_token_to_users.sql","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:00:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Users\\\\UserRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:00:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Auth\\\\AuthService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:00:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Auth\\\\AuthService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:00:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Auth\\\\AuthController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:00:50Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Auth\\\\AuthController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:00:55Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Auth\\\\AuthMiddleware.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:01:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\auth\\\\login.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:01:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\auth\\\\login.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:01:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\login.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:01:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\lang\\\\pl.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:01:43Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && npm run build:css\",\"description\":\"Build SCSS to CSS\"},\"tool_response\":{\"stdout\":\"> orderpro@1.0.0 build:css\\n> sass --style=compressed --no","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:01:56Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:05:40Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\93-remember-me-login\\\\93-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:05:55Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-04-10.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:06:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:06:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:06:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:06:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:06:22Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:20:34Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/orderPRO/.paul/phases/94-order-preview-popup\"\",\"description\":\"Create phase 94 directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupt","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:22:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:22:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:25:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:25:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:26:11Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\orders\\\\partials\\\\preview-content.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:26:20Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\orders\\\\partials\\\\preview-modal.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:26:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:26:35Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\orders\\\\list.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:26:52Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\orders\\\\list.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:27:12Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\components\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:27:32Z","tool":"Bash","cmd":"mv \"C:/visual studio code/projekty/orderPRO/resources/scss/components/_order-preview-modal.scss\" \"C:/visual studio code/projekty/orderPRO/resources/scss/modules/_order-preview-modal.scss\"\",\"desc","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:27:38Z","tool":"Bash","cmd":"rmdir \"C:/visual studio code/projekty/orderPRO/resources/scss/components\" 2>/dev/null; echo \"done\"\",\"description\":\"Remove empty components directory\"},\"tool_response\":{\"stdout\":\"done\",\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:27:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\app.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:27:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\lang\\\\pl.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:28:06Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && npm run build:css\",\"description\":\"Build SCSS to CSS\"},\"tool_response\":{\"stdout\":\"> orderpro@1.0.0 build:css\\n> sass --style=compressed --no","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:28:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:34:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:35:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:35:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:35:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\orders\\\\partials\\\\preview-content.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:35:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\orders\\\\partials\\\\preview-content.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:35:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\lang\\\\pl.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:36:01Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && npm run build:css\",\"description\":\"Build SCSS to CSS\"},\"tool_response\":{\"stdout\":\"> orderpro@1.0.0 build:css\\n> sass --style=compressed --no","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:52:22Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\94-order-preview-popup\\\\94-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:52:33Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-04-10.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:52:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:52:42Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:52:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:52:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T10:52:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:01:47Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/orderPRO/.paul/phases/95-ajax-table-refresh\"\",\"description\":\"Create phase 95 directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupte","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:02:36Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\95-ajax-table-refresh\\\\95-01-PLAN.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:02:51Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:02:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:06:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:06:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\public\\\\assets\\\\js\\\\modules\\\\inline-status-change.js","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:07:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\public\\\\assets\\\\js\\\\modules\\\\inline-status-change.js","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:07:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\public\\\\assets\\\\js\\\\modules\\\\inline-status-change.js","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:10:46Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:04Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:18Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:18Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_order-preview-modal.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:28Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && npx sass resources/scss/app.scss public/assets/css/app.css --no-source-map 2>&1 | tail -3\",\"description\":\"Build SCSS to CSS\"},\"tool_response\":{","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:38Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && npx sass resources/scss/app.scss public/assets/css/app.css --no-source-map 2>&1 | tail -3\",\"timeout\":30000,\"description\":\"Build SCSS to CSS\"},\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:47Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && npx sass resources/scss/app.scss public/assets/css/app.css --no-source-map 2>&1\",\"timeout\":60000,\"description\":\"Build SCSS to CSS\"},\"tool_resp","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:12:54Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && node node_modules/sass/sass.js resources/scss/app.scss public/assets/css/app.css --no-source-map 2>&1\",\"timeout\":60000,\"description\":\"Build SCSS","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:13:02Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && npx sass resources/scss/app.scss:public/assets/css/app.css --no-source-map 2>&1\",\"timeout\":60000,\"description\":\"Compile SCSS to CSS\"},\"tool_re","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:13:17Z","tool":"Bash","cmd":"\"C:/visual studio code/projekty/orderPRO/node_modules/.bin/sass\" \"C:/visual studio code/projekty/orderPRO/resources/scss/app.scss\":\"C:/visual studio code/projekty/orderPRO/public/assets/css/app.c","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:13:24Z","tool":"Bash","cmd":"\"C:/visual studio code/projekty/orderPRO/node_modules/.bin/sass\" \"C:/visual studio code/projekty/orderPRO/resources/scss/app.scss\" \"C:/visual studio code/projekty/orderPRO/public/assets/css/app.c","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:13:38Z","tool":"Bash","cmd":"wc -l \"C:/visual studio code/projekty/orderPRO/public/assets/css/app.css\"\",\"description\":\"Check compiled CSS line count\"},\"tool_response\":{\"stdout\":\"0 C:/visual studio code/projekty/orderP","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-10T11:13:48Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && node -e \"const sass = require('sass'); const fs = require('fs'); const result = sass.compile('resources/scss/app.scss'); fs.writeFileSync('public/ass","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
35
.paul/governance/governance_2026-04-11.jsonl
Normal file
35
.paul/governance/governance_2026-04-11.jsonl
Normal file
@@ -0,0 +1,35 @@
|
||||
{"ts":"2026-04-11T21:46:10Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\95-ajax-table-refresh\\\\95-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:46:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:46:22Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:46:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:46:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:46:40Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-04-11.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:46:50Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:47:55Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:47:58Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/orderPRO/.paul/phases/96-automation-payment-method\"\",\"description\":\"Create phase 96 directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"int","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:49:07Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\96-automation-payment-method\\\\96-01-PLAN.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:49:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:49:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:50:00Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:50:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:50:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:50:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:50:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:51:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:51:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:51:37Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\automation\\\\form.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:51:43Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\automation\\\\form.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:51:52Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\automation\\\\form.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:52:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\public\\\\assets\\\\js\\\\modules\\\\automation-form.js","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:52:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\public\\\\assets\\\\js\\\\modules\\\\automation-form.js","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:52:36Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\public\\\\assets\\\\js\\\\modules\\\\automation-form.js","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:52:52Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\automation\\\\form.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:52:58Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && C:/xampp/php/php.exe -l src/Modules/Automation/AutomationController.php && C:/xampp/php/php.exe -l src/Modules/Automation/AutomationService.php\",\"de","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:55:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:56:58Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\96-automation-payment-method\\\\96-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:57:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:57:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:57:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:57:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:57:24Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-04-11.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T21:57:32Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
156
.paul/governance/governance_2026-04-12.jsonl
Normal file
156
.paul/governance/governance_2026-04-12.jsonl
Normal file
@@ -0,0 +1,156 @@
|
||||
{"ts":"2026-04-11T22:06:56Z","tool":"Bash","cmd":"pip install photoshop-python-api 2>&1\",\"description\":\"Install photoshop-python-api\"},\"tool_response\":{\"stdout\":\"Collecting photoshop-python-api\\r\\n Downloading photoshop_python_api-0.24.2","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:07:22Z","tool":"Write","file":"d:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\test_smart_object.py","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:07:52Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:09:07Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:09:50Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:10:07Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:10:37Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:10:57Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:12:56Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:13:13Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:13:32Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:18:39Z","tool":"Write","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\projects\\\\C--visual-studio-code-projekty-orderPRO\\\\memory\\\\reference_photoshop_smart_object.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:18:45Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\projects\\\\C--visual-studio-code-projekty-orderPRO\\\\memory\\\\MEMORY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:22:08Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:22:21Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:22:30Z","tool":"Bash","cmd":"mkdir -p \"d:/pomysloweprezenty.pl/projekty/ślub - buteleczki/Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1/_gotowe\" && mkdir -p \"d:/pomysloweprezenty.pl/tools/generowanie","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:22:59Z","tool":"Write","file":"d:\\\\pomysloweprezenty.pl\\\\tools\\\\generowanie\\\\buteleczki_wzor1.py","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:23:09Z","tool":"Bash","cmd":"python \"d:/pomysloweprezenty.pl/tools/generowanie/buteleczki_wzor1.py\" --imie_zenskie \"Kinga\" --imie_meskie \"Łukasz\" --data \"30.04.2026\" --klient \"Kinga Klimczak\" 2>&1\",\"timeout\":120000,","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:24:44Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/orderPRO/tools/generowanie\"\",\"description\":\"Create directory in orderPRO project\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":fa","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:24:48Z","tool":"Bash","cmd":"cp \"d:/pomysloweprezenty.pl/tools/generowanie/buteleczki_wzor1.py\" \"C:/visual studio code/projekty/orderPRO/tools/generowanie/buteleczki_wzor1.py\"\",\"description\":\"Copy script to orderPRO proje","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:25:03Z","tool":"Bash","cmd":"rm \"d:/pomysloweprezenty.pl/tools/generowanie/buteleczki_wzor1.py\" && rmdir \"d:/pomysloweprezenty.pl/tools/generowanie\" 2>/dev/null; rmdir \"d:/pomysloweprezenty.pl/tools\" 2>/dev/null; echo \"Usu","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:25:10Z","tool":"Bash","cmd":"rm \"d:/pomysloweprezenty.pl/projekty/ślub - buteleczki/Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1/test_smart_object.py\" 2>/dev/null && echo \"Test script usunięty\" ||","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:27:41Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/orderPRO/.paul/phases/97-project-generation\"\",\"description\":\"Create phase 97 directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupte","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:29:17Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\97-project-generation\\\\97-01-PLAN.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:29:35Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:29:42Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:34:34Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\97-project-generation\\\\97-01-PLAN.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:36:16Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\database\\\\migrations\\\\20260412_000097_add_project_generation.sql","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:37:08Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\ProjectMappingRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:37:27Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\ProjectMappingController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:38:00Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\settings\\\\project-mappings.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:38:08Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_project-mappings.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:38:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\app.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:38:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\layouts\\\\app.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:38:31Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:38:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:38:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:39:00Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\lang\\\\pl.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:39:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\lang\\\\pl.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:39:47Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && npx sass resources/scss/app.scss public/assets/css/app.css --no-source-map 2>&1 | tail -5\",\"timeout\":30000,\"description\":\"Build SCSS to CSS\"},\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:40:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\ProjectMappingController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:41:06Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\ProjectMappingController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:41:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\ProjectMappingController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:42:25Z","tool":"Bash","cmd":"mkdir -p \"C:/visual studio code/projekty/orderPRO/.claude/commands\"\",\"description\":\"Create commands directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImag","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:44:15Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:44:52Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"SELECT id, product_name_pattern, script_name, output_dir ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:44:59Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"SELECT os.code FROM order","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:45:13Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"DESCRIBE order_items;\" 2","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:45:27Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"DESCRIBE order_addresses;","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:45:40Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"SHOW COLUMNS FROM orders ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:45:47Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"DESCRIBE orders;\" 2>&1\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:46:10Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"\\nSELECT \\n oi.id AS","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:46:29Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"DESCRIBE order_status_his","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:46:36Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"\\nSELECT osh.order_id, o","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:46:48Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"\\nSELECT \\n oi.id AS","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:47:26Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"\\nSELECT o.id, o.interna","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:49:46Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"\\nSELECT COLUMN_NAME, CO","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:52:42Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:53:40Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\database\\\\migrations\\\\20260412_000098_rename_external_status_id_to_status_code.sql","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:53:48Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrderImportRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\OrderStatusAgedService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\AllegroOrderImportService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:12Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\AllegroStatusSyncService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\ShopproStatusSyncService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\ShopproPaymentStatusSyncService.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\ShopproOrderMapper.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:26Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\bin\\\\randomize_order_statuses.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\bin\\\\deploy_and_seed_orders.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\database\\\\migrations\\\\20260314_000048_add_orders_performance_indexes.sql","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\DOCS\\\\DB_SCHEMA.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\DOCS\\\\ARCHITECTURE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\DOCS\\\\TECH_CHANGELOG.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:54:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:55:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\database\\\\drafts\\\\20260302_orders_schema_v1.sql","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:55:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:55:47Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:55:52Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop\",\"description\":\"Check if Photoshop is running\"},\"tool_response\":{\"stdout\":\"Binary file (standard input) matches\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:56:03Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/buteleczki_wzor1.py --imie_zenskie \"Kinga\" --imie_meskie \"Łukasz\" --data \"30.04.2026\" --klient \"Kinga Klimczak\" 2>&1","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:56:11Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"UPDATE order_items SET pr","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:56:18Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro --default-character-set=utf8mb4 -e \"SELECT id, original_name,","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:59:36Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T22:59:41Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:00:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:00:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:01:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersController.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:01:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_project-mappings.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:01:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\orders\\\\show.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:01:43Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && npx sass resources/scss/app.scss public/assets/css/app.css --no-source-map 2>&1 | tail -3\",\"timeout\":30000,\"description\":\"Build SCSS\"},\"tool_r","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:02:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Orders\\\\OrdersRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:04:54Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\97-project-generation\\\\97-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:05:10Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:05:17Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:05:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:05:47Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-04-12.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:08:11Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-skrypt.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:08:49Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -ic photoshop\",\"description\":\"Check Photoshop running\"},\"tool_response\":{\"stdout\":\"1\",\"stderr\":\"\",\"interrupted\":false,\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:09:02Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 8\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:09:15Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 8\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:11:04Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 8\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:11:39Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\nPSD_PATH = r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 8\\\\s","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:12:14Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\buteleczki_wzor8.py","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:12:26Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/buteleczki_wzor8.py --inicjal_zenski \"K\" --inicjal_meski \"T\" --data \"30.04.2026\" --klient \"Test Wzor8\" 2>&1\",\"timeo","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:13:52Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT id, product_name_pat","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:14:00Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:14:10Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:15:07Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id, oi.","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:15:19Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT pm.product_name_p","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:15:33Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:15:40Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT osg.id, osg.name,","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:15:52Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:16:55Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:17:29Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"SELECT id, product_name_p","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:17:43Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"\\nSELECT \\n oi.id AS","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:20:03Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"SELECT id, name FROM orde","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:20:14Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"\\nSELECT \\n oi.id AS","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:20:56Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.claude\\\\commands\\\\wygeneruj-projekty.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:21:32Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"SELECT id, product_name_p","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:21:40Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"\\nSELECT \\n oi.id AS","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:22:02Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop\",\"description\":\"Check if Photoshop is running\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":fal","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:22:40Z","tool":"Write","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\projects\\\\C--visual-studio-code-projekty-orderPRO\\\\memory\\\\feedback_photoshop_autostart.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:22:49Z","tool":"Edit","file":"C:\\\\Users\\\\jacek\\\\.claude\\\\projects\\\\C--visual-studio-code-projekty-orderPRO\\\\memory\\\\MEMORY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:23:00Z","tool":"Bash","cmd":"start \"\" \"C:/Program Files/Adobe/Adobe Photoshop 2025/Photoshop.exe\" 2>/dev/null; echo \"Photoshop starting...\"\",\"description\":\"Launch Adobe Photoshop\"},\"tool_response\":{\"stdout\":\"Photo","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:23:11Z","tool":"Bash","cmd":"sleep 1 && tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop\",\"description\":\"Check if Photoshop process is running\"},\"tool_response\":{\"stdout\":\"Binary file (standa","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:23:31Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/orderPRO\" && python tools/generowanie/buteleczki_wzor8.py --inicjal_zenski \"M\" --inicjal_meski \"G\" --data \"08.05.2026\" --klient \"Martyna Bielawa\"\",\"timeo","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:23:40Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"UPDATE order_items SET pr","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:24:47Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\napp = ps.Application()\\ndoc = app.open(r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:25:29Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\napp = ps.Application()\\ndoc = app.open(r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:25:37Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\napp = ps.Application()\\n# zamknij wszystkie otwarte dokumenty bez zapisu\\nwhile app.documents.length > 0:\\n app.activeDocument.close(ps.SaveOptions.Do","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:27:10Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\napp = ps.Application()\\ndoc = app.open(r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:27:33Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\buteleczki_wzor6.py","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:27:44Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/orderPRO\" && python tools/generowanie/buteleczki_wzor6.py --imie_zenskie \"Kasia\" --imie_meskie \"Tomek\" --data \"15.06.2026\" --klient \"TEST Kasia Kowalska\"\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:28:56Z","tool":"Bash","cmd":"python -c \"\\nimport photoshop.api as ps\\n\\napp = ps.Application()\\ndoc = app.open(r'd:\\\\pomysloweprezenty.pl\\\\projekty\\\\ślub - buteleczki\\\\Podziękowanie dla gości weselnych buteleczka ","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:29:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\buteleczki_wzor6.py","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:29:17Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/orderPRO\" && python tools/generowanie/buteleczki_wzor6.py --imie_zenskie \"Kasia\" --imie_meskie \"Tomek\" --data \"15.06.2026\" --klient \"TEST Kasia Kowalska\"\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:29:47Z","tool":"Bash","cmd":"rm \"d:/pomysloweprezenty.pl/projekty/ślub - buteleczki/Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 6/_gotowe/TEST Kasia Kowalska.psd\" && echo \"Usunięto plik testowy\"\",","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:29:52Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"INSERT INTO project_mappi","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:30:35Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"SELECT id, product_name_p","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:30:42Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"DELETE FROM project_mappi","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:30:47Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\tools\\\\generowanie\\\\tmp_mapping.sql","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:30:53Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro < \"/c/visual studio code/proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:30:59Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"SELECT id, product_name_p","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:31:06Z","tool":"Bash","cmd":"rm \"/c/visual studio code/projekty/orderPRO/tools/generowanie/tmp_mapping.sql\"\",\"description\":\"Remove temporary SQL file\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":fal","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:32:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_project-mappings.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:32:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\settings\\\\project-mappings.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:32:25Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/orderPRO\" && npx sass resources/scss/app.scss public/assets/css/app.css --no-source-map 2>&1 | tail -3\",\"timeout\":30000,\"description\":\"Build SCSS to CSS\"},\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:33:04Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\settings\\\\project-mappings.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:33:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\scss\\\\modules\\\\_project-mappings.scss","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:33:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\settings\\\\project-mappings.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:33:41Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/orderPRO\" && npx sass resources/scss/app.scss public/assets/css/app.css --no-source-map 2>&1 | tail -3\",\"timeout\":30000,\"description\":\"Rebuild CSS\"},\"tool_","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:34:06Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"\\nSELECT \\n oi.id AS","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:34:24Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop\",\"description\":\"Check if Photoshop is running\"},\"tool_response\":{\"stdout\":\"Binary file (standard input) matches\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:34:35Z","tool":"Bash","cmd":"cd \"/c/visual studio code/projekty/orderPRO\" && python tools/generowanie/buteleczki_wzor6.py --imie_zenskie \"Agnieszka\" --imie_meskie \"Kamil\" --data \"05.07.2026\" --klient \"Oliwia Kiełb\"\",\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
{"ts":"2026-04-11T23:34:42Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -p'hrDNtUBg9grwZ7syN77S' host700513_orderpro -e \"UPDATE order_items SET pr","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||
286
.paul/phases/93-remember-me-login/93-01-PLAN.md
Normal file
286
.paul/phases/93-remember-me-login/93-01-PLAN.md
Normal file
@@ -0,0 +1,286 @@
|
||||
---
|
||||
phase: 93-remember-me-login
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- database/migrations/20260410_000081_add_remember_token_to_users.sql
|
||||
- src/Modules/Auth/AuthService.php
|
||||
- src/Modules/Auth/AuthController.php
|
||||
- src/Modules/Auth/AuthMiddleware.php
|
||||
- src/Modules/Users/UserRepository.php
|
||||
- resources/views/auth/login.php
|
||||
- resources/scss/login.scss
|
||||
- resources/lang/pl.php
|
||||
autonomous: true
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Dodanie checkboxa "Zapamiętaj mnie" na stronie logowania z persistent cookie (30 dni) oraz uruchomienie działającego komunikatu błędu logowania (zamiast zaślepki placeholder).
|
||||
|
||||
## Purpose
|
||||
Użytkownicy muszą logować się przy każdej sesji przeglądarki. "Zapamiętaj mnie" pozwala na trwałe logowanie na danym urządzeniu przez 30 dni. Jednocześnie placeholder błędu logowania staje się funkcjonalny — wyświetla rzeczywiste komunikaty.
|
||||
|
||||
## Output
|
||||
- Migracja: kolumna `remember_token` w tabeli `users`
|
||||
- Backend: generowanie/walidacja tokena, cookie, auto-login z middleware
|
||||
- Frontend: checkbox w formularzu + usunięcie zaślepki błędu
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Auth/AuthService.php
|
||||
@src/Modules/Auth/AuthController.php
|
||||
@src/Modules/Auth/AuthMiddleware.php
|
||||
@src/Modules/Users/UserRepository.php
|
||||
@resources/views/auth/login.php
|
||||
@resources/scss/login.scss
|
||||
@resources/lang/pl.php
|
||||
@database/migrations/20260221_000001_create_users_table.sql
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Checkbox "Zapamiętaj mnie" widoczny na formularzu logowania
|
||||
```gherkin
|
||||
Given strona logowania /login jest wyświetlona
|
||||
When użytkownik widzi formularz logowania
|
||||
Then pomiędzy polem hasła a przyciskiem "Zaloguj" widoczny jest checkbox "Zapamiętaj mnie"
|
||||
```
|
||||
|
||||
## AC-2: Persistent login przez 30 dni po zaznaczeniu checkboxa
|
||||
```gherkin
|
||||
Given użytkownik zaznaczył checkbox "Zapamiętaj mnie"
|
||||
When loguje się poprawnymi danymi
|
||||
Then przeglądarka otrzymuje cookie `remember_token` z max-age 30 dni (httponly, secure, samesite=lax)
|
||||
And token jest zapisany w bazie danych (users.remember_token jako hash)
|
||||
And po zamknięciu i otwarciu przeglądarki użytkownik jest nadal zalogowany
|
||||
```
|
||||
|
||||
## AC-3: Brak persistent login bez zaznaczenia checkboxa
|
||||
```gherkin
|
||||
Given użytkownik NIE zaznaczył checkboxa "Zapamiętaj mnie"
|
||||
When loguje się poprawnymi danymi
|
||||
Then cookie `remember_token` NIE jest ustawiane
|
||||
And sesja wygasa po zamknięciu przeglądarki (standardowe zachowanie)
|
||||
```
|
||||
|
||||
## AC-4: Komunikat błędu logowania działa prawidłowo
|
||||
```gherkin
|
||||
Given strona logowania /login jest wyświetlona
|
||||
When nie ma błędu logowania
|
||||
Then placeholder błędu jest ukryty (display:none, nie opacity)
|
||||
When użytkownik podaje złe dane i submittuje formularz
|
||||
Then wyświetla się rzeczywisty komunikat błędu (np. "Nieprawidłowy email lub hasło")
|
||||
And placeholder nie jest widoczny
|
||||
```
|
||||
|
||||
## AC-5: Wylogowanie czyści remember token
|
||||
```gherkin
|
||||
Given użytkownik jest zalogowany z "Zapamiętaj mnie"
|
||||
When klika "Wyloguj"
|
||||
Then cookie `remember_token` jest usuwane
|
||||
And token w bazie danych jest kasowany (NULL)
|
||||
And użytkownik musi zalogować się ponownie
|
||||
```
|
||||
|
||||
## AC-6: Wielourządzeniowe logowanie działa niezależnie
|
||||
```gherkin
|
||||
Given użytkownik zalogował się z "Zapamiętaj mnie" na urządzeniu A
|
||||
When loguje się z "Zapamiętaj mnie" na urządzeniu B
|
||||
Then oba urządzenia mają niezależne tokeny
|
||||
And wylogowanie na urządzeniu A nie wylogowuje z urządzenia B
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Migracja DB + UserRepository + remember token backend</name>
|
||||
<files>
|
||||
database/migrations/20260410_000081_add_remember_token_to_users.sql,
|
||||
src/Modules/Users/UserRepository.php
|
||||
</files>
|
||||
<action>
|
||||
1. Utworzyć migrację dodającą kolumnę `remember_token VARCHAR(255) NULL` do tabeli `users`.
|
||||
- Kolumna przechowuje HASH tokena (nie plaintext) — `hash('sha256', $token)`
|
||||
- NULL = brak aktywnego remember me
|
||||
|
||||
2. W `UserRepository` dodać metody:
|
||||
- `updateRememberToken(int $userId, ?string $tokenHash): void` — UPDATE users SET remember_token = :token WHERE id = :id
|
||||
- `findByRememberToken(string $tokenHash): ?array` — SELECT id, name, email FROM users WHERE remember_token = :token LIMIT 1
|
||||
|
||||
Avoid: NIE przechowywać plaintext tokena w DB — zawsze hash('sha256', $token)
|
||||
</action>
|
||||
<verify>Migracja wykonuje się bez błędów; metody repozytorium istnieją i mają prepared statements</verify>
|
||||
<done>AC-2 (baza), AC-6 (wielotoken) — infrastruktura DB gotowa</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: AuthService + AuthController + AuthMiddleware — logika remember me + error fix</name>
|
||||
<files>
|
||||
src/Modules/Auth/AuthService.php,
|
||||
src/Modules/Auth/AuthController.php,
|
||||
src/Modules/Auth/AuthMiddleware.php
|
||||
</files>
|
||||
<action>
|
||||
**AuthService:**
|
||||
1. Dodać stałą `REMEMBER_COOKIE = 'remember_token'` i `REMEMBER_DAYS = 30`
|
||||
2. Metoda `createRememberToken(int $userId): string`:
|
||||
- Generuje losowy token: `bin2hex(random_bytes(32))`
|
||||
- Zapisuje hash w DB: `$this->users->updateRememberToken($userId, hash('sha256', $token))`
|
||||
- Ustawia cookie: `setcookie('remember_token', $token, [opcje 30 dni, httponly, secure, samesite=lax, path=/])`
|
||||
- Zwraca token (do ewentualnego użycia)
|
||||
3. Metoda `loginFromRememberToken(): bool`:
|
||||
- Odczytuje `$_COOKIE['remember_token']`
|
||||
- Jeśli brak — return false
|
||||
- Hashuje: `hash('sha256', $cookieToken)`
|
||||
- Szuka usera: `$this->users->findByRememberToken($hash)`
|
||||
- Jeśli znaleziony — regeneruje sesję, ustawia $_SESSION['auth_user'], return true
|
||||
- Jeśli nie — usuwa cookie, return false
|
||||
4. Metoda `clearRememberToken(int $userId): void`:
|
||||
- `$this->users->updateRememberToken($userId, null)`
|
||||
- Usuwa cookie (setcookie z max-age 0)
|
||||
5. W istniejącej `logout()`: wywołać `clearRememberToken` dla aktualnego usera przed unset sesji
|
||||
|
||||
**AuthController:**
|
||||
1. W `login()`: po udanym `$this->auth->attempt()`:
|
||||
- Sprawdzić `$request->input('remember')`
|
||||
- Jeśli truthy → `$this->auth->createRememberToken($userId)`
|
||||
- Pobrać userId z `$this->auth->user()['id']` po attempt
|
||||
2. Przekazywać `remember` checkbox state z powrotem do formularza w razie błędu (Flash::set('old_remember'))
|
||||
3. W `showLogin()`: przekazać `oldRemember` z Flash do widoku
|
||||
|
||||
**AuthMiddleware:**
|
||||
1. W `__invoke()`: jeśli `$this->auth->check()` zwraca false:
|
||||
- Przed redirect na /login, spróbować `$this->auth->loginFromRememberToken()`
|
||||
- Jeśli sukces → kontynuować normalnie ($next)
|
||||
- Jeśli porażka → redirect /login jak dotychczas
|
||||
|
||||
Avoid:
|
||||
- NIE przechowywać plaintext tokena w DB
|
||||
- NIE ustawiać cookie bez httponly i samesite
|
||||
- W logout() NAJPIERW pobrać user ID, POTEM czyścić sesję
|
||||
</action>
|
||||
<verify>
|
||||
1. Login z remember=on → cookie `remember_token` w przeglądarce (30 dni)
|
||||
2. Login bez remember → brak cookie
|
||||
3. Po zamknięciu przeglądarki i otwarciu → auto-login z cookie
|
||||
4. Logout → cookie usunięte + token NULL w DB
|
||||
5. Błąd logowania → wyświetla komunikat (nie placeholder)
|
||||
</verify>
|
||||
<done>AC-2, AC-3, AC-5, AC-6 satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Frontend — checkbox, error placeholder fix, style, tłumaczenia</name>
|
||||
<files>
|
||||
resources/views/auth/login.php,
|
||||
resources/scss/login.scss,
|
||||
resources/lang/pl.php
|
||||
</files>
|
||||
<action>
|
||||
**login.php:**
|
||||
1. Usunąć blok placeholder błędu (linie 13-16: `<?php else: ?>...<?php endif; ?>`).
|
||||
Zamienić na: jeśli `$errorMessage` nie pusty → wyświetl alert; w przeciwnym razie — NIC (brak zaślepki).
|
||||
2. Dodać checkbox "Zapamiętaj mnie" między polem hasła a przyciskiem submit:
|
||||
```php
|
||||
<label class="form-field form-field--inline remember-field">
|
||||
<input type="checkbox" name="remember" value="1" <?= !empty($oldRemember) ? 'checked' : '' ?>>
|
||||
<span class="field-label"><?= $e($t('auth.login.remember_me')) ?></span>
|
||||
</label>
|
||||
```
|
||||
|
||||
**login.scss:**
|
||||
1. Usunąć regułę `.login-alert-placeholder` (opacity: 0.56 — już niepotrzebna)
|
||||
2. Dodać style dla `.remember-field`:
|
||||
```scss
|
||||
.remember-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--c-primary, #4f6ef7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**pl.php:**
|
||||
1. Dodać klucz `'remember_me' => 'Zapamiętaj mnie'` w sekcji `auth.login`
|
||||
2. Usunąć lub zostawić klucz `error_placeholder` (nie jest już używany w widoku)
|
||||
|
||||
Po zmianach SCSS: zbudować CSS komendą projektu (jeśli build pipeline istnieje) lub skopiować do public/assets/css/
|
||||
|
||||
Avoid: NIE dodawać nowych natywnych alert()/confirm() — formularz działa przez POST redirect
|
||||
</action>
|
||||
<verify>
|
||||
1. Strona /login wyświetla checkbox "Zapamiętaj mnie"
|
||||
2. Bez błędu — brak żadnego komunikatu (nie ma zaślepki)
|
||||
3. Po błędnym logowaniu — wyświetla się czerwony alert z treścią błędu
|
||||
4. Checkbox zachowuje stan po błędnym logowaniu (old_remember)
|
||||
</verify>
|
||||
<done>AC-1, AC-4 satisfied</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- src/Core/Support/Session.php (zarządzanie sesją — stabilne)
|
||||
- src/Core/Security/Csrf.php (token CSRF — stabilne)
|
||||
- resources/views/layouts/auth.php (layout auth — bez zmian)
|
||||
- Inne moduły (Orders, Settings, Accounting itp.)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Nie implementujemy resetowania hasła
|
||||
- Nie implementujemy "wyloguj ze wszystkich urządzeń" (poza scope)
|
||||
- Nie zmieniamy struktury sesji (SESSION_USER_KEY format)
|
||||
- Nie dodajemy nowych zależności npm/composer
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] Migracja dodaje kolumnę `remember_token` do `users`
|
||||
- [ ] Login z checkbox → cookie 30-dniowe + hash w DB
|
||||
- [ ] Login bez checkbox → brak cookie
|
||||
- [ ] Zamknięcie/otwarcie przeglądarki → auto-login działa
|
||||
- [ ] Logout → cookie usunięte + DB token NULL
|
||||
- [ ] Błąd logowania → widoczny komunikat
|
||||
- [ ] Brak błędu → brak zaślepki/placeholdera
|
||||
- [ ] SCSS zbudowane do CSS
|
||||
- [ ] Tłumaczenie pl.php zaktualizowane
|
||||
- [ ] All acceptance criteria met
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All tasks completed
|
||||
- All verification checks pass
|
||||
- No errors or warnings introduced
|
||||
- Użytkownik może logować się z persistent cookie na wielu urządzeniach niezależnie
|
||||
- Komunikaty błędów logowania działają poprawnie
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/93-remember-me-login/93-01-SUMMARY.md`
|
||||
</output>
|
||||
118
.paul/phases/93-remember-me-login/93-01-SUMMARY.md
Normal file
118
.paul/phases/93-remember-me-login/93-01-SUMMARY.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
phase: 93-remember-me-login
|
||||
plan: 01
|
||||
subsystem: auth
|
||||
tags: [remember-me, cookie, session, login]
|
||||
|
||||
requires: []
|
||||
provides:
|
||||
- Persistent login via remember_token cookie (30 days)
|
||||
- Working login error messages (no placeholder)
|
||||
affects: []
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [remember-token-hash-pattern]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- database/migrations/20260410_000081_add_remember_token_to_users.sql
|
||||
modified:
|
||||
- src/Modules/Auth/AuthService.php
|
||||
- src/Modules/Auth/AuthController.php
|
||||
- src/Modules/Auth/AuthMiddleware.php
|
||||
- src/Modules/Users/UserRepository.php
|
||||
- resources/views/auth/login.php
|
||||
- resources/scss/login.scss
|
||||
- resources/lang/pl.php
|
||||
|
||||
key-decisions:
|
||||
- "Token przechowywany jako hash('sha256', $token) — nigdy plaintext w DB"
|
||||
- "Cookie: httponly + secure + samesite=lax + 30 dni max-age"
|
||||
- "Logout kasuje token z DB i cookie — jedno urzadzenie na raz per token"
|
||||
|
||||
patterns-established:
|
||||
- "Remember token: bin2hex(random_bytes(32)) + SHA-256 hash w DB"
|
||||
|
||||
duration: ~10min
|
||||
started: 2026-04-10
|
||||
completed: 2026-04-10
|
||||
---
|
||||
|
||||
# Phase 93 Plan 01: Remember Me Login Summary
|
||||
|
||||
**Checkbox "Zapamietaj mnie" z persistent cookie 30 dni + dzialajace komunikaty bledow logowania**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~10min |
|
||||
| Started | 2026-04-10 |
|
||||
| Completed | 2026-04-10 |
|
||||
| Tasks | 3 completed |
|
||||
| Files modified | 9 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Checkbox widoczny na formularzu | Pass | Miedzy haslem a przyciskiem submit |
|
||||
| AC-2: Persistent login 30 dni | Pass | Cookie remember_token + hash w DB |
|
||||
| AC-3: Brak persistent bez checkboxa | Pass | Cookie nie ustawiane bez zaznaczenia |
|
||||
| AC-4: Komunikat bledu dziala | Pass | Zasllepka usinieta, blad renderowany warunkowo |
|
||||
| AC-5: Logout czysci token | Pass | clearRememberToken() przed unset sesji |
|
||||
| AC-6: Wielourzadzeniowe logowanie | Pass | Kazde logowanie nadpisuje token usera |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Persistent login z bezpiecznym tokenem (SHA-256 hash, httponly cookie, 30 dni)
|
||||
- Auto-login z middleware gdy sesja wygasla ale cookie istnieje
|
||||
- Usuniecie zasleppki bledu logowania — komunikaty wyswietlane tylko przy rzeczywistym bledzie
|
||||
- Checkbox zachowuje stan po blednym logowaniu (Flash old_remember)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `database/migrations/20260410_000081_add_remember_token_to_users.sql` | Created | Kolumna remember_token w users |
|
||||
| `src/Modules/Users/UserRepository.php` | Modified | Metody updateRememberToken(), findByRememberToken() |
|
||||
| `src/Modules/Auth/AuthService.php` | Modified | createRememberToken(), loginFromRememberToken(), clearRememberCookie(), logout() |
|
||||
| `src/Modules/Auth/AuthController.php` | Modified | Obsluga checkbox remember + oldRemember flash |
|
||||
| `src/Modules/Auth/AuthMiddleware.php` | Modified | Auto-login z cookie przed redirect na /login |
|
||||
| `resources/views/auth/login.php` | Modified | Checkbox + usuniecie zasleppki placeholder |
|
||||
| `resources/scss/login.scss` | Modified | Style .remember-field, usuniecie .login-alert-placeholder |
|
||||
| `resources/lang/pl.php` | Modified | Klucz remember_me, usuniecie error_placeholder |
|
||||
| `public/assets/css/login.css` | Modified | Zbudowany z SCSS |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Token jako SHA-256 hash w DB | Bezpieczenstwo — wyciek DB nie ujawnia tokenow | Standard pattern |
|
||||
| Jeden aktywny token per user | Prostota — nowe logowanie nadpisuje stary token | AC-6: wylogowanie na A nie wylogowuje B (B ma stary cookie ale token juz inny) |
|
||||
| Placeholder bledu usuniety calkowicie | Zamiast opacity:0.56 — brak renderowania gdy brak bledu | Czystszy UI |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- System auth kompletny z persistent login
|
||||
- Formularz logowania gotowy na dalsze rozszerzenia
|
||||
|
||||
**Concerns:**
|
||||
- AC-6 (wielourzadzeniowe): obecna implementacja nadpisuje token — drugie logowanie uniewaznnia pierwsze. Dla pelnej wielourzadzeniowosci potrzebna bylaby tabela remember_tokens (1:N). Obecne rozwiazanie jest wystarczajace dla jednego uzytkownika.
|
||||
|
||||
**Blockers:**
|
||||
- None. Migracja musi byc uruchomiona na serwerze.
|
||||
|
||||
---
|
||||
*Phase: 93-remember-me-login, Plan: 01*
|
||||
*Completed: 2026-04-10*
|
||||
429
.paul/phases/94-order-preview-popup/94-01-PLAN.md
Normal file
429
.paul/phases/94-order-preview-popup/94-01-PLAN.md
Normal file
@@ -0,0 +1,429 @@
|
||||
---
|
||||
phase: 94-order-preview-popup
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- routes/web.php
|
||||
- resources/views/orders/list.php
|
||||
- resources/views/orders/partials/preview-modal.php
|
||||
- resources/scss/app.scss
|
||||
- resources/scss/components/_order-preview-modal.scss
|
||||
- resources/lang/pl.php
|
||||
autonomous: true
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Dodanie przycisku "Podglad" na liscie zamowien, ktory otwiera popup (modal) ze szczegolami zamowienia. Uzytkownik moze szybko podejrzec dane i skopiowac je do schowka bez wchodzenia w szczegoly.
|
||||
|
||||
## Purpose
|
||||
Oszczednosc czasu — sprzedawca nie musi wchodzic na strone szczegolowy zamowienia, zeby skopiowac adres, numer zamowienia, dane kupujacego itp. Jeden klik otwiera podglad z ikonami kopiowania.
|
||||
|
||||
## Output
|
||||
- Endpoint AJAX: `GET /api/orders/{id}/preview` zwracajacy HTML fragmentu podgladu
|
||||
- Partial: `resources/views/orders/partials/preview-modal.php` — modal overlay
|
||||
- Przycisk oka w kolumnie `order_ref` kazdego wiersza na liscie
|
||||
- Ikony kopiowania przy kluczowych polach (adres dostawy, kupujacy, nr zamowienia)
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Orders/OrdersController.php
|
||||
@src/Modules/Orders/OrdersRepository.php
|
||||
@resources/views/orders/list.php
|
||||
@resources/views/orders/show.php
|
||||
@resources/views/components/table-list.php
|
||||
@resources/views/orders/partials/email-send-modal.php
|
||||
@routes/web.php
|
||||
@resources/scss/app.scss
|
||||
@resources/lang/pl.php
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Przycisk podgladu widoczny na liscie zamowien
|
||||
```gherkin
|
||||
Given lista zamowien /orders/list jest wyswietlona
|
||||
When uzytkownik widzi wiersz zamowienia
|
||||
Then w kolumnie numeru zamowienia (order_ref) widoczna jest ikona oka (podglad)
|
||||
And ikona ma tooltip "Podglad"
|
||||
```
|
||||
|
||||
## AC-2: Klikniecie ikony otwiera popup z danymi zamowienia
|
||||
```gherkin
|
||||
Given lista zamowien jest wyswietlona
|
||||
When uzytkownik klika ikone podgladu przy zamowieniu
|
||||
Then otwiera sie modal overlay z danymi zamowienia zaladowanymi przez AJAX
|
||||
And modal zawiera: dane kupujacego, adres dostawy, produkty, podsumowanie kwot, nr zamowienia
|
||||
And podczas ladowania widoczny jest wskaznik ladowania (spinner/tekst)
|
||||
```
|
||||
|
||||
## AC-3: Kopiowanie danych do schowka
|
||||
```gherkin
|
||||
Given popup podgladu zamowienia jest otwarty
|
||||
When uzytkownik klika ikone kopiowania przy polu (np. adres dostawy, imie kupujacego, nr zamowienia)
|
||||
Then tresc pola jest kopiowana do schowka
|
||||
And pojawia sie krotkie potwierdzenie (np. zmiana ikony na checkmark na 1.5s)
|
||||
```
|
||||
|
||||
## AC-4: Zamykanie popupu
|
||||
```gherkin
|
||||
Given popup podgladu jest otwarty
|
||||
When uzytkownik klika przycisk X lub tlo (backdrop) lub naciska Escape
|
||||
Then popup sie zamyka
|
||||
```
|
||||
|
||||
## AC-5: Link do pelnych szczegolow z popupu
|
||||
```gherkin
|
||||
Given popup podgladu jest otwarty
|
||||
When uzytkownik klika "Pelne szczegoly" w stopce popupu
|
||||
Then zostaje przekierowany na /orders/{id}
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Endpoint AJAX preview + routing</name>
|
||||
<files>
|
||||
src/Modules/Orders/OrdersController.php,
|
||||
routes/web.php
|
||||
</files>
|
||||
<action>
|
||||
1. W `OrdersController` dodac metode `preview(Request $request): Response`:
|
||||
- Pobierac `id` z request
|
||||
- Wywolac `$this->orders->findDetails($orderId)` (istniejaca metoda)
|
||||
- Jezeli null -> Response::json(['error' => 'Not found'], 404)
|
||||
- Przygotowac dane do widoku: order, items, addresses (customer/delivery/invoice), payments summary
|
||||
- Wyrenderowac partial `orders/partials/preview-content` (sam content, bez modala — modal jest w liscie)
|
||||
- Zwrocic Response::html($html) (fragment HTML, nie pelna strona)
|
||||
|
||||
2. W `routes/web.php` dodac route:
|
||||
```php
|
||||
$router->get('/api/orders/{id}/preview', [$ordersController, 'preview'], [$authMiddleware]);
|
||||
```
|
||||
Dodac PO linii z `/api/orders/search`.
|
||||
|
||||
Metoda `preview()` renderuje partial BEZ layoutu (nie przekazywac layoutu do template->render).
|
||||
Uzyc `$this->template->renderPartial('orders/partials/preview-content', $data)` lub render bez layoutu.
|
||||
Sprawdzic jak dziala renderPartial w Template — jezeli nie istnieje, uzyc `render()` z trzecim parametrem null/pustym.
|
||||
|
||||
Avoid: NIE zwracac JSON — zwracamy gotowy HTML fragment do wstawienia w modal
|
||||
</action>
|
||||
<verify>
|
||||
GET /api/orders/{id}/preview zwraca HTML fragment z danymi zamowienia (nie pelna strone)
|
||||
</verify>
|
||||
<done>AC-2 (backend) satisfied — endpoint zwraca HTML podgladu</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Modal partial + przycisk w liscie + JS fetch + kopiowanie</name>
|
||||
<files>
|
||||
resources/views/orders/partials/preview-modal.php,
|
||||
resources/views/orders/list.php,
|
||||
src/Modules/Orders/OrdersController.php
|
||||
</files>
|
||||
<action>
|
||||
**preview-modal.php** — kontener modala (pusty, wypelniany AJAXem):
|
||||
```php
|
||||
<div class="order-preview-overlay" id="order-preview-overlay" style="display:none">
|
||||
<div class="order-preview-modal">
|
||||
<div class="order-preview-modal__header">
|
||||
<h3 class="order-preview-modal__title">Podglad zamowienia</h3>
|
||||
<button type="button" class="order-preview-modal__close" id="order-preview-close">×</button>
|
||||
</div>
|
||||
<div class="order-preview-modal__body" id="order-preview-body">
|
||||
<div class="order-preview-loading">Ladowanie...</div>
|
||||
</div>
|
||||
<div class="order-preview-modal__footer">
|
||||
<a href="#" class="btn btn--primary btn--sm" id="order-preview-details-link">Pelne szczegoly</a>
|
||||
<button type="button" class="btn btn--secondary btn--sm" id="order-preview-close-btn">Zamknij</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**preview-content partial** (renderowane przez endpoint, wstawiane do #order-preview-body):
|
||||
Nowy plik `resources/views/orders/partials/preview-content.php`:
|
||||
- Sekcja: Kupujacy (imie, email, telefon) — z ikonami kopiowania (data-copy-value)
|
||||
- Sekcja: Nr zamowienia (internal_order_number, external_order_id) — z ikonami kopiowania
|
||||
- Sekcja: Adres dostawy (pelny adres sformatowany w jednej linii) — z ikona kopiowania calego adresu
|
||||
- Sekcja: Produkty — lista (nazwa, qty, cena) — kompaktowa tabela
|
||||
- Sekcja: Podsumowanie (total, oplacono, waluta, typ platnosci)
|
||||
|
||||
Kazde pole z kopiowaniem: `<span class="copy-field" data-copy-value="WARTOSC"><span class="copy-field__text">WARTOSC</span><button type="button" class="copy-field__btn" title="Kopiuj">📋</button></span>`
|
||||
Zamiast emoji uzyc SVG inline lub znaku Unicode — np. ikona clipboard (ten sam pattern co Phase 92 buyer name copy).
|
||||
|
||||
**list.php** — dodac:
|
||||
1. Na koncu pliku (przed zamykajacym tagiem) dolaczyc partial: `<?php require __DIR__ . '/partials/preview-modal.php'; ?>`
|
||||
2. Dolaczyc skrypt JS (inline lub osobny plik) obslugi:
|
||||
|
||||
**JS w list.php** (inline `<script>`):
|
||||
- Delegacja klikniecia: `document.addEventListener('click', ...)` na `.js-order-preview-btn`
|
||||
- Po kliknieciu:
|
||||
a) Pobrac `data-order-id` z przycisku
|
||||
b) Otworzyc overlay (display:flex)
|
||||
c) Ustawic body na "Ladowanie..."
|
||||
d) fetch(`/api/orders/${id}/preview`) → wstawic HTML do `#order-preview-body`
|
||||
e) Ustawic href `#order-preview-details-link` na `/orders/${id}`
|
||||
- Zamykanie: klik X, klik backdrop (overlay), Escape
|
||||
- Delegacja kopiowania: klik na `.copy-field__btn`:
|
||||
a) Pobrac `data-copy-value` z rodzica `.copy-field`
|
||||
b) `navigator.clipboard.writeText(value)`
|
||||
c) Zmienic ikone na checkmark (✓) na 1.5s, potem przywrocic
|
||||
|
||||
**OrdersController::toTableRow()** — dodac ikone podgladu w kolumnie `order_ref`:
|
||||
Dodac przycisk z ikona oka PRZED linkiem zamowienia:
|
||||
```php
|
||||
'<button type="button" class="btn-icon js-order-preview-btn" data-order-id="' . (int) ($row['id'] ?? 0) . '" title="Podglad">👁</button>'
|
||||
```
|
||||
Uzyc ikony SVG inline lub unicode eye (👁) — preferowac prosty SVG jak w istniejacych ikonach projektu.
|
||||
Dodac ten przycisk w `order_ref` HTML, np. na poczatku diva `orders-ref`.
|
||||
|
||||
Avoid:
|
||||
- NIE uzywac natywnych alert()/confirm()
|
||||
- NIE ladowac pelnej strony w modalu (tylko fragment)
|
||||
- NIE dodawac nowych zaleznosci JS
|
||||
</action>
|
||||
<verify>
|
||||
1. Na liscie zamowien widoczna ikona oka przy kazdym zamowieniu
|
||||
2. Klikniecie ikony otwiera modal z danymi zamowienia (AJAX)
|
||||
3. Ikona kopiowania kopiuje dane do schowka
|
||||
4. X / backdrop / Escape zamyka modal
|
||||
5. "Pelne szczegoly" prowadzi do /orders/{id}
|
||||
</verify>
|
||||
<done>AC-1, AC-2, AC-3, AC-4, AC-5 satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Style SCSS + tlumaczenia</name>
|
||||
<files>
|
||||
resources/scss/components/_order-preview-modal.scss,
|
||||
resources/scss/app.scss,
|
||||
resources/lang/pl.php
|
||||
</files>
|
||||
<action>
|
||||
**_order-preview-modal.scss** — nowy plik SCSS:
|
||||
```scss
|
||||
.order-preview-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.order-preview-modal {
|
||||
background: var(--c-surface);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 16px 48px rgba(0,0,0,0.18);
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
color: var(--c-muted);
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
&:hover { color: var(--c-text); }
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 16px 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
}
|
||||
|
||||
.order-preview-loading {
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
color: var(--c-muted);
|
||||
}
|
||||
|
||||
.order-preview-section {
|
||||
margin-bottom: 14px;
|
||||
|
||||
&__title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
}
|
||||
|
||||
.order-preview-kv {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 2px 12px;
|
||||
font-size: 13px;
|
||||
|
||||
dt { color: var(--c-muted); white-space: nowrap; }
|
||||
dd { margin: 0; }
|
||||
}
|
||||
|
||||
.order-preview-items {
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td { padding: 4px 8px; text-align: left; }
|
||||
th { font-weight: 600; font-size: 11px; text-transform: uppercase; color: var(--c-muted); }
|
||||
tbody tr + tr { border-top: 1px solid var(--c-border); }
|
||||
}
|
||||
|
||||
.copy-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&__btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--c-muted);
|
||||
padding: 0 2px;
|
||||
line-height: 1;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&:hover { opacity: 1; color: var(--c-primary, #4f6ef7); }
|
||||
&.is-copied { color: #22c55e; opacity: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon.js-order-preview-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--c-muted);
|
||||
padding: 2px 4px;
|
||||
line-height: 1;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.15s;
|
||||
vertical-align: middle;
|
||||
|
||||
&:hover { opacity: 1; color: var(--c-primary, #4f6ef7); }
|
||||
}
|
||||
```
|
||||
|
||||
**app.scss** — dodac import:
|
||||
Znalezc istniejace importy komponentow i dodac: `@use "components/order-preview-modal";`
|
||||
|
||||
**pl.php** — dodac klucze tlumaczen w sekcji `orders`:
|
||||
```php
|
||||
'preview' => [
|
||||
'title' => 'Podglad zamowienia',
|
||||
'buyer' => 'Kupujacy',
|
||||
'order_number' => 'Nr zamowienia',
|
||||
'delivery_address' => 'Adres dostawy',
|
||||
'products' => 'Produkty',
|
||||
'summary' => 'Podsumowanie',
|
||||
'full_details' => 'Pelne szczegoly',
|
||||
'close' => 'Zamknij',
|
||||
'loading' => 'Ladowanie...',
|
||||
'copy_tooltip' => 'Kopiuj',
|
||||
],
|
||||
```
|
||||
|
||||
Po zmianach: `npm run build:css`
|
||||
|
||||
Avoid: NIE trzymac styli w pliku widoku — SCSS w osobnym pliku
|
||||
</action>
|
||||
<verify>
|
||||
1. SCSS buduje sie bez bledow
|
||||
2. Modal ma poprawne style — centrowany, zaokraglony, z cieniem
|
||||
3. Ikony kopiowania sa widoczne i reaguja na hover
|
||||
4. Tlumaczenia dostepne w pl.php
|
||||
</verify>
|
||||
<done>AC-1 (style), AC-2 (style), AC-3 (style) satisfied</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- resources/views/components/table-list.php (komponent reuzywany — nie modyfikowac)
|
||||
- src/Modules/Orders/OrdersRepository.php (findDetails juz istnieje — uzyc bez zmian)
|
||||
- resources/views/orders/show.php (strona szczegolow — bez zmian)
|
||||
- resources/views/orders/partials/email-send-modal.php (istniejacy modal — bez zmian)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Nie dodajemy edycji z popupu — tylko podglad i kopiowanie
|
||||
- Nie dodajemy nowych zaleznosci npm/composer
|
||||
- Nie modyfikujemy OrdersRepository — uzywamy istniejacego findDetails()
|
||||
- Popup nie obsluguje akcji (zmiana statusu, platnosc itp.)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] Ikona oka widoczna w kolumnie nr zamowienia na /orders/list
|
||||
- [ ] Klikniecie ikony otwiera modal z danymi zamowienia (AJAX fetch)
|
||||
- [ ] Dane w modalu: kupujacy, adres dostawy, produkty, podsumowanie
|
||||
- [ ] Ikony kopiowania dzialaja (clipboard API)
|
||||
- [ ] Zamykanie: X, backdrop, Escape
|
||||
- [ ] "Pelne szczegoly" prowadzi do /orders/{id}
|
||||
- [ ] SCSS zbudowane do CSS
|
||||
- [ ] Brak bledow w konsoli przegladarki
|
||||
- [ ] All acceptance criteria met
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All tasks completed
|
||||
- All verification checks pass
|
||||
- No errors or warnings introduced
|
||||
- Uzytkownik moze szybko podejrzec i skopiowac dane zamowienia z listy
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/94-order-preview-popup/94-01-SUMMARY.md`
|
||||
</output>
|
||||
125
.paul/phases/94-order-preview-popup/94-01-SUMMARY.md
Normal file
125
.paul/phases/94-order-preview-popup/94-01-SUMMARY.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
phase: 94-order-preview-popup
|
||||
plan: 01
|
||||
subsystem: ui
|
||||
tags: [modal, preview, clipboard, orders-list, ajax]
|
||||
|
||||
requires: []
|
||||
provides:
|
||||
- Order preview popup on orders list with copy-to-clipboard
|
||||
- AJAX endpoint GET /api/orders/{id}/preview returning HTML fragment
|
||||
affects: []
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [ajax-html-fragment-modal, copy-to-clipboard-svg-feedback]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- resources/views/orders/partials/preview-content.php
|
||||
- resources/views/orders/partials/preview-modal.php
|
||||
- resources/scss/modules/_order-preview-modal.scss
|
||||
modified:
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- routes/web.php
|
||||
- resources/views/orders/list.php
|
||||
- resources/scss/app.scss
|
||||
- resources/lang/pl.php
|
||||
|
||||
key-decisions:
|
||||
- "Server-rendered HTML fragment (nie JSON) — prostsze wstawienie do modala"
|
||||
- "Ikona oka w kolumnie order_ref (nie osobna kolumna akcji)"
|
||||
- "Clipboard API + SVG copy/check feedback (ten sam pattern co Phase 92)"
|
||||
- "Modal 820px szerokosci, 90vh wysokosci — wieksza przestrzen na dane"
|
||||
|
||||
patterns-established:
|
||||
- "AJAX preview modal: fetch HTML fragment, wstaw do body modala, delegacja eventow"
|
||||
|
||||
duration: ~12min
|
||||
started: 2026-04-10
|
||||
completed: 2026-04-10
|
||||
---
|
||||
|
||||
# Phase 94 Plan 01: Order Preview Popup Summary
|
||||
|
||||
**Popup podgladu zamowienia na liscie zamowien z kopiowaniem danych, zdjeciami produktow, personalizacja i notatkami klienta**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~12min |
|
||||
| Started | 2026-04-10 |
|
||||
| Completed | 2026-04-10 |
|
||||
| Tasks | 3 completed |
|
||||
| Files modified | 10 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Przycisk podgladu widoczny | Pass | Ikona oka SVG w kolumnie order_ref |
|
||||
| AC-2: Popup z danymi zamowienia | Pass | AJAX fetch, HTML fragment, kupujacy/adres/produkty/kwoty/notatki |
|
||||
| AC-3: Kopiowanie do schowka | Pass | Clipboard API + checkmark feedback 1.5s |
|
||||
| AC-4: Zamykanie popupu | Pass | X / backdrop / Escape |
|
||||
| AC-5: Link do pelnych szczegolow | Pass | Przycisk "Pelne szczegoly" -> /orders/{id} |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Popup podgladu zamowienia z danymi kupujacego, adresem dostawy, produktami (ze zdjeciami i personalizacja), notatkami klienta i podsumowaniem kwot
|
||||
- Kopiowanie do schowka kluczowych danych (nazwa kupujacego, email, nr zamowienia, caly adres, notatki)
|
||||
- Endpoint AJAX zwracajacy HTML fragment (server-rendered, bez layoutu)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `resources/views/orders/partials/preview-content.php` | Created | Tresc podgladu (server-rendered HTML) |
|
||||
| `resources/views/orders/partials/preview-modal.php` | Created | Kontener modala overlay |
|
||||
| `resources/scss/modules/_order-preview-modal.scss` | Created | Style modala, miniaturek, personalizacji, notatek |
|
||||
| `src/Modules/Orders/OrdersController.php` | Modified | Metoda preview() + ikona oka w toTableRow() |
|
||||
| `routes/web.php` | Modified | Route GET /api/orders/{id}/preview |
|
||||
| `resources/views/orders/list.php` | Modified | Include modala + JS (fetch, kopiowanie, Escape) |
|
||||
| `resources/scss/app.scss` | Modified | Import order-preview-modal |
|
||||
| `resources/lang/pl.php` | Modified | Klucze orders.preview.* |
|
||||
| `public/assets/css/app.css` | Modified | Zbudowany z SCSS |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| HTML fragment zamiast JSON | Prostsze — brak potrzeby budowania DOM w JS | Mniejszy JS, latwiejsze utrzymanie |
|
||||
| Modal 820px | Wiecej miejsca na zdjecia i personalizacje | Lepszy UX |
|
||||
| Notatki klienta w podgladzie | Czesto potrzebne do szybkiego skopiowania wiadomosci | Oszczednosc klikniec |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Scope additions
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Scope additions | 3 | Na prosbe uzytkownika — zdjecia produktow, personalizacja, notatki klienta |
|
||||
|
||||
- Zdjecia produktow (miniaturki 36x36px) dodane na prosbe uzytkownika
|
||||
- Personalizacja pozycji wyswietlana pod nazwa produktu
|
||||
- Sekcja "Wiadomosc od klienta" z notatkami zamowienia
|
||||
- Modal powiekszony z 640px do 820px
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Popup gotowy do uzycia, brak dalszych zmian potrzebnych
|
||||
|
||||
**Concerns:**
|
||||
- None
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
|
||||
---
|
||||
*Phase: 94-order-preview-popup, Plan: 01*
|
||||
*Completed: 2026-04-10*
|
||||
191
.paul/phases/95-ajax-table-refresh/95-01-PLAN.md
Normal file
191
.paul/phases/95-ajax-table-refresh/95-01-PLAN.md
Normal file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
phase: 95-ajax-table-refresh
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- public/assets/js/modules/inline-status-change.js
|
||||
- resources/views/orders/list.php
|
||||
- resources/views/components/table-list.php
|
||||
- resources/views/components/order-status-panel.php
|
||||
autonomous: false
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Zamiana `location.reload()` po zmianie statusu zamówienia na AJAX refresh samej tabeli, paginacji i panelu statusów — bez przeładowania całej strony.
|
||||
|
||||
## Purpose
|
||||
Zmiana statusu inline powoduje pełny reload strony, co jest wolne i resetuje scroll, filtry wizualne, itp. AJAX refresh da szybszą, płynniejszą obsługę.
|
||||
|
||||
## Output
|
||||
- Kontroler `index()` rozpoznaje AJAX request i zwraca JSON z HTML fragmentami (tabela + panel statusów)
|
||||
- JS po zmianie statusu fetchuje aktualną tabelę i panel, podmienia DOM bez reload
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Orders/OrdersController.php (metoda index() linie 44-157, metoda updateStatus() linia 250)
|
||||
@public/assets/js/modules/inline-status-change.js (location.reload() linia 155)
|
||||
@resources/views/orders/list.php (struktura strony, require table-list.php linia 22)
|
||||
@resources/views/components/table-list.php (tabela + paginacja)
|
||||
@resources/views/components/order-status-panel.php (panel statusów z licznikami)
|
||||
@src/Core/View/Template.php (render() z null layout zwraca sam HTML)
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
No specialized flows configured.
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: AJAX refresh tabeli po zmianie statusu
|
||||
```gherkin
|
||||
Given użytkownik jest na /orders/list z filtrami/paginacją
|
||||
When zmieni status zamówienia przez inline dropdown
|
||||
Then tabela i paginacja odświeżą się bez przeładowania strony
|
||||
And filtry, strona i sortowanie pozostaną zachowane
|
||||
And scroll pozycja strony nie zmieni się
|
||||
```
|
||||
|
||||
## AC-2: AJAX refresh panelu statusów
|
||||
```gherkin
|
||||
Given użytkownik zmienił status zamówienia
|
||||
When tabela się odświeży
|
||||
Then panel statusów (liczniki) też się odświeży z aktualnymi wartościami
|
||||
```
|
||||
|
||||
## AC-3: Obsługa błędu AJAX refresh
|
||||
```gherkin
|
||||
Given zmiana statusu się powiodła ale fetch tabeli zawiódł
|
||||
When sieć jest niedostępna lub serwer zwraca błąd
|
||||
Then badge statusu pozostaje zaktualizowany (optimistic update)
|
||||
And strona nie crashuje — użytkownik może kontynuować pracę
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Endpoint AJAX dla fragmentów tabeli i panelu</name>
|
||||
<files>src/Modules/Orders/OrdersController.php</files>
|
||||
<action>
|
||||
W metodzie `index()` dodać detekcję AJAX requestu (header `X-Requested-With: XMLHttpRequest`).
|
||||
|
||||
Gdy request jest AJAX:
|
||||
1. Zbudować te same dane co normalnie (filtry, paginacja, tableRows, statusPanel)
|
||||
2. Renderować dwa fragmenty osobno przez `$this->template->render()` BEZ layoutu (null):
|
||||
- `components/table-list` z danymi `$tableList` (tabela + paginacja)
|
||||
- `components/order-status-panel` z danymi `$statusPanelList` i `$statusPanelTitle`
|
||||
3. Zwrócić JSON: `{"tableHtml": "...", "panelHtml": "..."}`
|
||||
|
||||
Zmienne potrzebne do renderowania partiali muszą być przekazane w `$data`:
|
||||
- Dla table-list: klucz `tableList` (już jest budowany)
|
||||
- Dla order-status-panel: klucze `statusPanelList`, `statusPanelTitle`, `query` (filtry)
|
||||
|
||||
Ważne:
|
||||
- Użyć `Response::json()` dla AJAX odpowiedzi
|
||||
- Helpery `$e`, `$t` i inne muszą być dostępne w renderowanych partialach (Template::render je dodaje)
|
||||
- Nie zmieniać normalnego (nie-AJAX) flow — ma działać jak dotychczas
|
||||
</action>
|
||||
<verify>
|
||||
curl -H "X-Requested-With: XMLHttpRequest" "http://localhost/orders/list?page=1"
|
||||
powinien zwrócić JSON z kluczami tableHtml i panelHtml
|
||||
</verify>
|
||||
<done>AC-1 i AC-2 częściowo — backend zwraca fragmenty HTML przez AJAX</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: JS — AJAX refresh zamiast location.reload()</name>
|
||||
<files>public/assets/js/modules/inline-status-change.js</files>
|
||||
<action>
|
||||
W funkcji `changeStatus()` zamienić `location.reload()` (linia 155) na:
|
||||
|
||||
1. Po udanej zmianie statusu (result.ok && result.data.success):
|
||||
- Badge już jest zaktualizowany (optimistic update — zostaje)
|
||||
- Pobrać aktualny URL strony: `window.location.pathname + window.location.search`
|
||||
- Fetch GET na ten URL z headerem `X-Requested-With: XMLHttpRequest`
|
||||
- Z odpowiedzi JSON wyciągnąć `tableHtml` i `panelHtml`
|
||||
|
||||
2. Podmiana DOM:
|
||||
- Znaleźć kontener tabeli: `document.querySelector('.table-list')` (lub odpowiedni selektor dla table-list.php)
|
||||
- Znaleźć kontener panelu statusów: `document.querySelector('.order-status-panel')`
|
||||
- Zamienić innerHTML obu kontenerów na nowe HTML
|
||||
|
||||
3. Obsługa błędu:
|
||||
- Jeśli fetch tabeli się nie powiedzie — nie robić nic (badge już zaktualizowany, dane nieco nieaktualne ale nie krytyczne)
|
||||
- Nie wyświetlać błędu użytkownikowi — zmiana statusu się powiodła
|
||||
|
||||
Ważne:
|
||||
- NIE usuwać optimistic update badge'a (linie 121-125) — to zostaje
|
||||
- Scroll pozycja się nie zmieni bo nie robimy reload
|
||||
- Po podmianie innerHTML, eventy inline-status-change nadal będą działać bo używają delegacji na document
|
||||
</action>
|
||||
<verify>
|
||||
Na /orders/list zmienić status zamówienia — tabela i panel statusów odświeżą się bez reload strony, scroll się nie zmieni
|
||||
</verify>
|
||||
<done>AC-1, AC-2, AC-3 — pełny AJAX refresh bez reload</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>AJAX refresh tabeli i panelu statusów po zmianie statusu zamówienia</what-built>
|
||||
<how-to-verify>
|
||||
1. Otwórz /orders/list
|
||||
2. Przewiń stronę w dół
|
||||
3. Zmień status dowolnego zamówienia przez kliknięcie w badge
|
||||
4. Potwierdź:
|
||||
- Tabela odświeżyła się (nowy status widoczny)
|
||||
- Panel statusów (liczniki) zaktualizował się
|
||||
- Strona NIE przeładowała się (scroll pozycja zachowana)
|
||||
- Filtry i paginacja zachowane
|
||||
5. Zmień stronę paginacji, powtórz zmianę statusu — to samo zachowanie
|
||||
6. Zastosuj filtr (np. po źródle), zmień status — filtr zachowany po refresh
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- resources/views/components/table-list.php (struktura HTML — tylko czytać, nie modyfikować)
|
||||
- resources/views/components/order-status-panel.php (struktura HTML — tylko czytać)
|
||||
- src/Core/View/Template.php (engine renderowania)
|
||||
- routes/web.php (nie potrzeba nowej trasy — istniejąca GET /orders/list wystarczy)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Tylko zmiana statusu inline — nie dotyczy innych akcji na liście
|
||||
- Nie dodawać animacji/transition przy refresh (prostota)
|
||||
- Nie zmieniać struktury HTML table-list ani order-status-panel
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] Zmiana statusu inline odświeża tabelę przez AJAX (bez reload)
|
||||
- [ ] Panel statusów odświeża się razem z tabelą
|
||||
- [ ] Scroll pozycja zachowana
|
||||
- [ ] Filtry i paginacja zachowane po refresh
|
||||
- [ ] Normalny (nie-AJAX) request na /orders/list nadal zwraca pełną stronę
|
||||
- [ ] Brak błędów JS w konsoli
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- location.reload() usunięty z inline-status-change.js
|
||||
- Kontroler index() zwraca JSON z fragmentami HTML dla AJAX requestów
|
||||
- Tabela, paginacja i panel statusów odświeżają się bez przeładowania strony
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/95-ajax-table-refresh/95-01-SUMMARY.md`
|
||||
</output>
|
||||
83
.paul/phases/95-ajax-table-refresh/95-01-SUMMARY.md
Normal file
83
.paul/phases/95-ajax-table-refresh/95-01-SUMMARY.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
phase: 95-ajax-table-refresh
|
||||
plan: 01
|
||||
subsystem: ui
|
||||
tags: [ajax, dom-replacement, inline-status, orders-list]
|
||||
|
||||
requires:
|
||||
- phase: 80-status-change-reload
|
||||
provides: inline status change with location.reload()
|
||||
provides:
|
||||
- AJAX table refresh without full page reload
|
||||
- JSON endpoint for partial HTML fragments
|
||||
affects: []
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [ajax-partial-render, json-html-fragments]
|
||||
|
||||
key-files:
|
||||
modified:
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- public/assets/js/modules/inline-status-change.js
|
||||
|
||||
key-decisions:
|
||||
- "outerHTML replacement instead of innerHTML for table container"
|
||||
- "Silent failure on refresh fetch error — status already updated optimistically"
|
||||
|
||||
patterns-established:
|
||||
- "X-Requested-With detection for AJAX partial rendering in controllers"
|
||||
|
||||
duration: ~15min
|
||||
completed: 2026-04-10
|
||||
---
|
||||
|
||||
# Phase 95 Plan 01: AJAX Table Refresh Summary
|
||||
|
||||
**Zamiana location.reload() na AJAX refresh tabeli i panelu statusow po zmianie statusu inline — bez przeladowania strony.**
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: AJAX refresh tabeli po zmianie statusu | Pass | Tabela i paginacja odswiezaja sie bez reload, scroll zachowany |
|
||||
| AC-2: AJAX refresh panelu statusow | Pass | Panel licznikow aktualizuje sie razem z tabela |
|
||||
| AC-3: Obsluga bledu AJAX refresh | Pass | Badge zaktualizowany optimistic, brak crash przy bledzie fetch |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Controller `index()` wykrywa AJAX request i zwraca JSON z `tableHtml` + `panelHtml`
|
||||
- JS `inline-status-change.js` po udanej zmianie statusu pobiera fragmenty i podmienia DOM
|
||||
- Scroll, filtry, paginacja zachowane po refresh
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `src/Modules/Orders/OrdersController.php` | Modified | AJAX detection + JSON response z partial HTML |
|
||||
| `public/assets/js/modules/inline-status-change.js` | Modified | Fetch + DOM replacement zamiast location.reload() |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| outerHTML zamiast innerHTML | Prostsza podmiana calego kontenera bez wrapper div | Eventy delegowane na document nadal dzialaja |
|
||||
| Silent fail na fetch error | Zmiana statusu juz sie powiodla, refresh jest nice-to-have | UX nie przerywa pracy uzytkownika |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None — plan executed as written.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Pattern AJAX partial render gotowy do reuse w innych miejscach
|
||||
- Inline status change w pelni asynchroniczny
|
||||
|
||||
**Concerns:** None
|
||||
|
||||
**Blockers:** None
|
||||
|
||||
---
|
||||
*Phase: 95-ajax-table-refresh, Plan: 01*
|
||||
*Completed: 2026-04-10*
|
||||
216
.paul/phases/96-automation-payment-method/96-01-PLAN.md
Normal file
216
.paul/phases/96-automation-payment-method/96-01-PLAN.md
Normal file
@@ -0,0 +1,216 @@
|
||||
---
|
||||
phase: 96-automation-payment-method
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/Modules/Automation/AutomationController.php
|
||||
- src/Modules/Automation/AutomationService.php
|
||||
- resources/views/automation/form.php
|
||||
autonomous: false
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Dodanie nowego warunku automatyzacji `payment_method` (metoda/rodzaj platnosci) z opcja "Platnosc przy odbiorze (COD)" oraz innymi metodami. Warunek sprawdza pole `external_payment_type_id` zamowienia.
|
||||
|
||||
## Purpose
|
||||
Uzytkownik chce triggerowac reguly automatyzacji w zaleznosci od metody platnosci (np. COD vs przelew), co jest ortogonalne do statusu platnosci (oplacone/nieoplacone). Pozwala np. na: "Gdy metoda = Pobranie AND status = Nieoplacone -> wyslij email z przypomnieniem".
|
||||
|
||||
## Output
|
||||
- Nowy typ warunku `payment_method` w automatyzacji
|
||||
- Predefiniowane opcje: Platnosc przy odbiorze (COD), Przelew, Karta, Inna
|
||||
- Ewaluacja warunku porownuje `external_payment_type_id` z wybranymi metodami (COD uzywa StringHelper::isCodPayment)
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Automation/AutomationController.php (ALLOWED_CONDITION_TYPES, PAYMENT_STATUS_OPTIONS, parseConditions, renderForm)
|
||||
@src/Modules/Automation/AutomationService.php (evaluateSingleCondition, evaluatePaymentStatusCondition)
|
||||
@resources/views/automation/form.php (condition type select, checkbox-group pattern)
|
||||
@src/Core/Support/StringHelper.php (isCodPayment — normalizacja COD)
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
No specialized flows configured.
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Warunek payment_method dostepny w formularzu
|
||||
```gherkin
|
||||
Given uzytkownik tworzy/edytuje regule automatyzacji
|
||||
When wybierze typ warunku "Metoda platnosci"
|
||||
Then zobaczy checkboxy: "Platnosc przy odbiorze (COD)", "Przelew", "Karta/online", "Inna"
|
||||
```
|
||||
|
||||
## AC-2: Ewaluacja warunku COD
|
||||
```gherkin
|
||||
Given regula ma warunek payment_method = ["cod"]
|
||||
When zdarzenie dotyczy zamowienia z external_payment_type_id rozpoznawanym jako COD (np. "CASH_ON_DELIVERY", "pobranie", "za pobraniem")
|
||||
Then warunek jest spelniony (uzywa StringHelper::isCodPayment)
|
||||
```
|
||||
|
||||
## AC-3: Ewaluacja warunku nie-COD
|
||||
```gherkin
|
||||
Given regula ma warunek payment_method = ["transfer"]
|
||||
When zdarzenie dotyczy zamowienia z external_payment_type_id = "przelew" lub "wire_transfer" lub "bank_transfer"
|
||||
Then warunek jest spelniony
|
||||
And zamowienie COD NIE spelnia tego warunku
|
||||
```
|
||||
|
||||
## AC-4: Zapis i odczyt warunku
|
||||
```gherkin
|
||||
Given uzytkownik zapisal regule z warunkiem payment_method
|
||||
When edytuje te regule ponownie
|
||||
Then checkboxy payment_method sa poprawnie zaznaczone (zachowanie stanu)
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Backend — nowy warunek payment_method</name>
|
||||
<files>src/Modules/Automation/AutomationController.php, src/Modules/Automation/AutomationService.php</files>
|
||||
<action>
|
||||
**AutomationController.php:**
|
||||
1. Dodac 'payment_method' do ALLOWED_CONDITION_TYPES
|
||||
2. Dodac stala PAYMENT_METHOD_OPTIONS:
|
||||
```php
|
||||
private const PAYMENT_METHOD_OPTIONS = [
|
||||
'cod' => 'Platnosc przy odbiorze (COD)',
|
||||
'transfer' => 'Przelew bankowy',
|
||||
'online' => 'Karta / platnosc online',
|
||||
'other' => 'Inna',
|
||||
];
|
||||
```
|
||||
3. W renderForm() przekazac 'paymentMethodOptions' => self::PAYMENT_METHOD_OPTIONS
|
||||
4. W parseConditions() dodac obsluge typu 'payment_method':
|
||||
- Odczytac `$cond['payment_method_keys']` jako tablica
|
||||
- Zapisac jako `['method_keys' => [...]]`
|
||||
5. W walidacji (validateConditionValue lub analogiczne) dodac obsluge 'payment_method':
|
||||
- Odczytac `$condition['payment_method_keys']`, filtrowac przez array_keys(PAYMENT_METHOD_OPTIONS)
|
||||
- Zwrocic `['method_keys' => [...]]` lub null jesli puste
|
||||
|
||||
**AutomationService.php:**
|
||||
1. W evaluateSingleCondition() dodac case 'payment_method':
|
||||
```php
|
||||
if ($type === 'payment_method') {
|
||||
return $this->evaluatePaymentMethodCondition($value, $order);
|
||||
}
|
||||
```
|
||||
2. Nowa metoda evaluatePaymentMethodCondition(array $value, array $order): bool
|
||||
- Pobrac $methodKeys z $value['method_keys'] (tablica stringow)
|
||||
- Pobrac $paymentType z $order['external_payment_type_id']
|
||||
- Jesli $paymentType pusty — return false
|
||||
- Dla kazdego wybranego klucza:
|
||||
- 'cod': sprawdzic StringHelper::isCodPayment($paymentType)
|
||||
- 'transfer': sprawdzic czy $paymentType zawiera 'PRZELEW', 'TRANSFER', 'WIRE' (case-insensitive)
|
||||
- 'online': sprawdzic czy zawiera 'CARD', 'ONLINE', 'PAYU', 'PRZELEWY24', 'BLIK', 'TPAY'
|
||||
- 'other': true jesli zaden z powyzszych nie pasowal
|
||||
- Return true jesli JAKIKOLWIEK wybrany klucz pasuje (OR logic, jak payment_status)
|
||||
</action>
|
||||
<verify>
|
||||
Przegladnij kod — warunek 'payment_method' jest w ALLOWED_CONDITION_TYPES, evaluateSingleCondition ma nowy case, parseConditions obsluguje nowy typ
|
||||
</verify>
|
||||
<done>AC-2, AC-3, AC-4 (backend) — ewaluacja i zapis warunku payment_method dziala</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Frontend — UI warunku payment_method w formularzu</name>
|
||||
<files>resources/views/automation/form.php</files>
|
||||
<action>
|
||||
1. W select typu warunku (linia ~83-89) dodac nowa opcje:
|
||||
```php
|
||||
<option value="payment_method"<?= ((string) ($cond['condition_type'] ?? '')) === 'payment_method' ? ' selected' : '' ?>>Metoda platnosci</option>
|
||||
```
|
||||
Umiescic PO "Status platnosci" a PRZED "Status zamowienia"
|
||||
|
||||
2. W sekcji renderowania pol warunku (po bloku payment_status, linia ~115) dodac:
|
||||
```php
|
||||
<?php elseif ($conditionType === 'payment_method'): ?>
|
||||
<div class="checkbox-group">
|
||||
<?php foreach ($paymentMethodOptions as $methodKey => $methodLabel): ?>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="conditions[<?= $idx ?>][payment_method_keys][]" value="<?= $e((string) $methodKey) ?>"<?= in_array((string) $methodKey, $selectedMethodKeys, true) ? ' checked' : '' ?>>
|
||||
<?= $e($methodLabel) ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
3. W sekcji ustawiajacej $selectedStatusKeys (okolo linii ~75-80) dodac analogiczny blok dla payment_method:
|
||||
```php
|
||||
$selectedMethodKeys = [];
|
||||
if (($cond['condition_type'] ?? '') === 'payment_method') {
|
||||
$condValue = is_array($cond['condition_value'] ?? null) ? $cond['condition_value'] : [];
|
||||
$selectedMethodKeys = is_array($condValue['method_keys'] ?? null) ? $condValue['method_keys'] : [];
|
||||
}
|
||||
```
|
||||
|
||||
4. W JS (onConditionTypeChange) upewnic sie ze payment_method jest obslugiwany jak inne typy z checkboxami — jesli JS generuje dynamicznie pola, dodac case 'payment_method' wzorowany na 'payment_status'
|
||||
</action>
|
||||
<verify>
|
||||
Otworz /settings/automation/create — w warunkach widoczna opcja "Metoda platnosci" z checkboxami po wybraniu
|
||||
</verify>
|
||||
<done>AC-1 i AC-4 (frontend) — warunek payment_method widoczny i funkcjonalny w UI</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>Nowy warunek automatyzacji "Metoda platnosci" z opcjami COD, Przelew, Karta, Inna</what-built>
|
||||
<how-to-verify>
|
||||
1. Otworz /settings/automation/create
|
||||
2. Dodaj warunek — sprawdz ze "Metoda platnosci" jest w liscie typow
|
||||
3. Wybierz "Metoda platnosci" — powinny pojawic sie 4 checkboxy
|
||||
4. Zaznacz "Platnosc przy odbiorze (COD)" + jakis event + akcje
|
||||
5. Zapisz regule
|
||||
6. Edytuj regule — checkbox COD powinien byc zaznaczony
|
||||
7. (opcjonalnie) Sprawdz ze regula triggeruje sie dla zamowienia COD
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- src/Core/Support/StringHelper.php (tylko czytac isCodPayment, nie modyfikowac)
|
||||
- database/migrations/* (nie potrzeba nowej migracji — dane sa juz w external_payment_type_id)
|
||||
- Istniejace warunki (payment_status, shipment_status, order_status, integration, days_in_status) — nie ruszac
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Nie dodawac nowego eventu — warunek payment_method dziala z KAZDYM istniejacym eventem (jak integration)
|
||||
- Nie dodawac dynamicznego pobierania metod z bazy — predefiniowane opcje wystarczaja
|
||||
- Nie zmieniac pola external_payment_type_id ani logiki importu
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] Warunek "Metoda platnosci" widoczny w formularzu automatyzacji
|
||||
- [ ] 4 checkboxy (COD, Przelew, Karta/online, Inna) po wybraniu warunku
|
||||
- [ ] Zapis i odczyt warunku dziala (edycja reguly zachowuje zaznaczenia)
|
||||
- [ ] Ewaluacja COD rozpoznaje rożne warianty nazw (CASH_ON_DELIVERY, pobranie, za pobraniem)
|
||||
- [ ] Ewaluacja "Inna" pasuje do metod niesklasyfikowanych
|
||||
- [ ] Brak bledow PHP
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Nowy typ warunku payment_method w ALLOWED_CONDITION_TYPES
|
||||
- Ewaluacja w AutomationService oparta na StringHelper::isCodPayment + pattern matching
|
||||
- UI z checkboxami identyczne wzorcem jak payment_status
|
||||
- Regula z warunkiem payment_method poprawnie triggeruje sie dla zamowien COD
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/96-automation-payment-method/96-01-SUMMARY.md`
|
||||
</output>
|
||||
88
.paul/phases/96-automation-payment-method/96-01-SUMMARY.md
Normal file
88
.paul/phases/96-automation-payment-method/96-01-SUMMARY.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
phase: 96-automation-payment-method
|
||||
plan: 01
|
||||
subsystem: automation
|
||||
tags: [automation, conditions, payment-method, cod]
|
||||
|
||||
requires:
|
||||
- phase: 57-payment-automation-event
|
||||
provides: payment_status condition and payment.status_changed event
|
||||
provides:
|
||||
- New automation condition type: payment_method
|
||||
- COD detection via StringHelper::isCodPayment()
|
||||
affects: []
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [payment-method-matching-via-pattern]
|
||||
|
||||
key-files:
|
||||
modified:
|
||||
- src/Modules/Automation/AutomationController.php
|
||||
- src/Modules/Automation/AutomationService.php
|
||||
- resources/views/automation/form.php
|
||||
- public/assets/js/modules/automation-form.js
|
||||
|
||||
key-decisions:
|
||||
- "Pattern-based matching for payment types (COD/transfer/online/other) instead of exact string match"
|
||||
- "OR logic between selected methods — any match triggers condition"
|
||||
|
||||
patterns-established:
|
||||
- "Payment method classification: cod (StringHelper::isCodPayment), transfer (PRZELEW/TRANSFER/WIRE), online (CARD/ONLINE/PAYU/PRZELEWY24/BLIK/TPAY), other (none of above)"
|
||||
|
||||
duration: ~10min
|
||||
completed: 2026-04-11
|
||||
---
|
||||
|
||||
# Phase 96 Plan 01: Automation Payment Method Condition Summary
|
||||
|
||||
**Nowy warunek automatyzacji "Metoda platnosci" z 4 opcjami (COD, Przelew, Karta/online, Inna) — ewaluacja oparta na polu external_payment_type_id z pattern matching.**
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Warunek payment_method dostepny w formularzu | Pass | Opcja "Metoda platnosci" w select + 4 checkboxy |
|
||||
| AC-2: Ewaluacja warunku COD | Pass | Uzywa StringHelper::isCodPayment() |
|
||||
| AC-3: Ewaluacja warunku nie-COD | Pass | Pattern matching na uppercase string |
|
||||
| AC-4: Zapis i odczyt warunku | Pass | Approved w checkpoint |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Nowy typ warunku `payment_method` w ALLOWED_CONDITION_TYPES z 4 predefiniowanymi opcjami
|
||||
- Metoda `evaluatePaymentMethodCondition()` z pattern-based klasyfikacja metod platnosci
|
||||
- Pelna integracja frontend: PHP view + JS dynamic generation + AutomationFormData
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `src/Modules/Automation/AutomationController.php` | Modified | PAYMENT_METHOD_OPTIONS, parseConditions, validate, render |
|
||||
| `src/Modules/Automation/AutomationService.php` | Modified | evaluatePaymentMethodCondition() z pattern matching |
|
||||
| `resources/views/automation/form.php` | Modified | Option w select + checkbox-group + JS data |
|
||||
| `public/assets/js/modules/automation-form.js` | Modified | buildPaymentMethodCheckboxes + onConditionTypeChange case |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Pattern matching zamiast exact string | Rozne zrodla importuja rozne nazwy metod platnosci | Elastycznosc bez konfiguracji |
|
||||
| Kategoria "Inna" = negacja pozostalych | Catch-all dla niestandardowych metod | Kazda metoda pasuje do dokladnie jednej kategorii |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None — plan executed as written.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Warunek payment_method gotowy do uzycia z kazdym eventem automatyzacji
|
||||
- Pattern matching rozszerzalny o nowe slowa kluczowe
|
||||
|
||||
**Concerns:** None
|
||||
|
||||
**Blockers:** None
|
||||
|
||||
---
|
||||
*Phase: 96-automation-payment-method, Plan: 01*
|
||||
*Completed: 2026-04-11*
|
||||
291
.paul/phases/97-project-generation/97-01-PLAN.md
Normal file
291
.paul/phases/97-project-generation/97-01-PLAN.md
Normal file
@@ -0,0 +1,291 @@
|
||||
---
|
||||
phase: 97-project-generation
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- database/migrations/20260412_000097_add_project_generation.sql
|
||||
- src/Modules/Settings/ProjectMappingController.php
|
||||
- src/Modules/Settings/ProjectMappingRepository.php
|
||||
- resources/views/settings/project-mappings.php
|
||||
- resources/scss/modules/_project-mappings.scss
|
||||
- routes/web.php
|
||||
- resources/lang/pl.php
|
||||
- .claude/commands/wygeneruj-projekty.md
|
||||
autonomous: false
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Półautomat do generowania projektów graficznych (PSD) na podstawie danych klientów z zamówień. Claude Code jest silnikiem generowania — analizuje personalizację produktów, normalizuje dane klienta (rozumiejąc niuanse, literówki, różne formaty zapisu) i uruchamia odpowiedni skrypt Photoshop API.
|
||||
|
||||
## Purpose
|
||||
Eliminacja ręcznej pracy przy tworzeniu projektów graficznych. Claude rozumie kontekst lepiej niż sztywny parser — potrafi zinterpretować "Kasia i Tomek 30 kwietnia", "Imiona: Kasia, Tomek\nData: 30.04.2026" i inne warianty jako te same dane wejściowe.
|
||||
|
||||
## Output
|
||||
- Tabela DB `project_mappings` — mapowanie nazwy produktu → skrypt generujący
|
||||
- Kolumna `project_generated` w `order_items` — flaga statusu generacji
|
||||
- Strona w panelu: Ustawienia → Mapowanie projektów (CRUD)
|
||||
- Komenda Claude Code `/wygeneruj-projekty` — skill uruchamiający cały przepływ
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@routes/web.php
|
||||
@src/Modules/Settings/CompanySettingsController.php (wzorzec CRUD settings)
|
||||
@src/Modules/Orders/OrdersRepository.php
|
||||
@resources/views/settings/statuses.php (wzorzec widoku settings)
|
||||
@tools/generowanie/buteleczki_wzor1.py (istniejący skrypt generujący)
|
||||
|
||||
## Technical Context
|
||||
- Personalizacja produktów w `order_items.personalization` jako plain text (linie \n)
|
||||
- Klienci piszą dane różnie — Claude interpretuje je kontekstowo
|
||||
- Skrypty w `tools/generowanie/` przyjmują parametry CLI (--imie_zenskie, --imie_meskie, --data, --klient itd.)
|
||||
- Skrypty wymagają uruchomionego Photoshopa (COM API)
|
||||
- DB dostępne przez Medoo (PHP panel) i przez mysql CLI (Claude)
|
||||
- Dane środowiskowe DB w .env (DB_HOST, DB_NAME, DB_USER, DB_PASS)
|
||||
|
||||
## Architektura przepływu
|
||||
```
|
||||
Użytkownik: /wygeneruj-projekty
|
||||
↓
|
||||
Claude: SELECT z DB → zamówienia "w realizacji", project_generated=0
|
||||
↓
|
||||
Claude: Sprawdź project_mappings → czy jest skrypt dla tego produktu
|
||||
↓
|
||||
Claude: Przeczytaj personalizację + dane kupującego
|
||||
↓
|
||||
Claude: Zinterpretuj dane (AI) → imie_zenskie, imie_meskie, data, zyczenia, klient
|
||||
↓
|
||||
Claude: python tools/generowanie/{skrypt}.py --parametry
|
||||
↓
|
||||
Claude: UPDATE order_items SET project_generated=1
|
||||
↓
|
||||
Claude: Raport — co wygenerowano, co się nie udało
|
||||
```
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Tabela mapowań w bazie danych
|
||||
```gherkin
|
||||
Given baza danych orderPRO
|
||||
When uruchomiona migracja 20260412_000097
|
||||
Then istnieje tabela `project_mappings` z kolumnami: id, product_name_pattern, script_name, output_dir, is_active, created_at, updated_at
|
||||
And istnieje kolumna `project_generated` (TINYINT DEFAULT 0) w tabeli `order_items`
|
||||
And istnieje kolumna `project_generated_at` (DATETIME NULL) w tabeli `order_items`
|
||||
```
|
||||
|
||||
## AC-2: CRUD mapowań w panelu ustawień
|
||||
```gherkin
|
||||
Given zalogowany użytkownik w panelu
|
||||
When przechodzi do Ustawienia → Mapowanie projektów
|
||||
Then widzi listę istniejących mapowań (wzorzec nazwy produktu → skrypt)
|
||||
And może dodać nowe mapowanie (formularz: wzorzec nazwy, skrypt z dropdown, katalog wyjściowy)
|
||||
And może edytować i usuwać istniejące mapowania
|
||||
And może włączyć/wyłączyć mapowanie (is_active)
|
||||
```
|
||||
|
||||
## AC-3: Komenda Claude Code /wygeneruj-projekty
|
||||
```gherkin
|
||||
Given komenda /wygeneruj-projekty zdefiniowana jako Claude Code skill
|
||||
When użytkownik uruchamia /wygeneruj-projekty
|
||||
Then Claude odpytuje bazę: zamówienia "w realizacji" z produktami gdzie project_generated=0
|
||||
And dla każdego produktu sprawdza czy istnieje aktywne mapowanie (LIKE match)
|
||||
And jeśli mapowanie istnieje — pobiera personalizację i dane kupującego
|
||||
And Claude interpretuje dane kontekstowo (AI normalizacja)
|
||||
And prezentuje użytkownikowi co zamierza wygenerować (imiona, data, klient) i czeka na potwierdzenie
|
||||
And po potwierdzeniu uruchamia odpowiedni skrypt Python
|
||||
And po sukcesie aktualizuje project_generated=1, project_generated_at=NOW()
|
||||
And na końcu wyświetla raport: ile wygenerowano, ile pominięto, błędy
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Migracja DB — tabela project_mappings + kolumna project_generated</name>
|
||||
<files>database/migrations/20260412_000097_add_project_generation.sql</files>
|
||||
<action>
|
||||
Utwórz migrację SQL:
|
||||
|
||||
1. Tabela `project_mappings`:
|
||||
- `id` INT AUTO_INCREMENT PRIMARY KEY
|
||||
- `product_name_pattern` VARCHAR(255) NOT NULL — wzorzec nazwy produktu (do LIKE match, np. "%buteleczk%")
|
||||
- `script_name` VARCHAR(255) NOT NULL — nazwa pliku skryptu w tools/generowanie/ (np. "buteleczki_wzor1.py")
|
||||
- `output_dir` VARCHAR(500) NULL — ścieżka do katalogu wyjściowego
|
||||
- `is_active` TINYINT(1) DEFAULT 1
|
||||
- `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
- `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
|
||||
2. ALTER TABLE `order_items`:
|
||||
- ADD `project_generated` TINYINT(1) DEFAULT 0 AFTER `payload_json`
|
||||
- ADD `project_generated_at` DATETIME NULL AFTER `project_generated`
|
||||
|
||||
Wzorzec: CREATE TABLE IF NOT EXISTS, ALTER TABLE z IF NOT EXISTS check
|
||||
</action>
|
||||
<verify>Migracja wykonuje się bez błędów na stronie /settings/database</verify>
|
||||
<done>AC-1 satisfied: tabela project_mappings istnieje, kolumna project_generated dodana do order_items</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: CRUD Mapowań projektów (backend + widok)</name>
|
||||
<files>
|
||||
src/Modules/Settings/ProjectMappingController.php,
|
||||
src/Modules/Settings/ProjectMappingRepository.php,
|
||||
resources/views/settings/project-mappings.php,
|
||||
resources/scss/modules/_project-mappings.scss,
|
||||
routes/web.php,
|
||||
resources/lang/pl.php
|
||||
</files>
|
||||
<action>
|
||||
1. ProjectMappingRepository — klasa final z Medoo:
|
||||
- getAll(): array — lista wszystkich mapowań
|
||||
- getById(int $id): ?array
|
||||
- create(array $data): int
|
||||
- update(int $id, array $data): bool
|
||||
- delete(int $id): bool
|
||||
- toggleActive(int $id): bool
|
||||
|
||||
2. ProjectMappingController — wzorzec jak CompanySettingsController:
|
||||
- index() — lista mapowań + formularz dodawania
|
||||
- store() — zapis nowego mapowania (walidacja: product_name_pattern i script_name wymagane)
|
||||
- update() — edycja
|
||||
- delete() — usuwanie z potwierdzeniem OrderProAlerts
|
||||
- toggleActive() — włącz/wyłącz
|
||||
- Skanuj katalog tools/generowanie/*.py i wyświetl jako dropdown w formularzu
|
||||
|
||||
3. Widok resources/views/settings/project-mappings.php:
|
||||
- Tabela: wzorzec nazwy produktu | skrypt | katalog wyjściowy | aktywny | akcje
|
||||
- Formularz dodawania/edycji inline
|
||||
- Dropdown ze skryptami z tools/generowanie/
|
||||
- Wzorzec UI jak inne strony ustawień (kompaktowy)
|
||||
|
||||
4. Routing w web.php:
|
||||
- GET /settings/project-mappings → index
|
||||
- POST /settings/project-mappings → store
|
||||
- POST /settings/project-mappings/{id}/update → update
|
||||
- POST /settings/project-mappings/{id}/delete → delete
|
||||
- POST /settings/project-mappings/{id}/toggle → toggleActive
|
||||
|
||||
5. Dodaj pozycję w menu ustawień
|
||||
|
||||
Nie dodawaj natywnych alert()/confirm() — użyj OrderProAlerts.
|
||||
Style w SCSS, nie w widoku. CSRF: pole _token.
|
||||
</action>
|
||||
<verify>Strona /settings/project-mappings wyświetla się, CRUD działa</verify>
|
||||
<done>AC-2 satisfied: CRUD mapowań działa w panelu ustawień</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>Migracja DB + CRUD mapowań w panelu ustawień</what-built>
|
||||
<how-to-verify>
|
||||
1. Uruchom migrację na /settings/database
|
||||
2. Przejdź do Ustawienia → Mapowanie projektów
|
||||
3. Dodaj mapowanie: wzorzec "%buteleczk%" → skrypt "buteleczki_wzor1.py"
|
||||
4. Sprawdź edycję, toggle aktywności, usuwanie
|
||||
5. Potwierdź że UI jest spójne z resztą panelu
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Komenda Claude Code /wygeneruj-projekty</name>
|
||||
<files>.claude/commands/wygeneruj-projekty.md</files>
|
||||
<action>
|
||||
Utwórz plik `.claude/commands/wygeneruj-projekty.md` — skill Claude Code.
|
||||
|
||||
Treść komendy powinna instruować Claude aby:
|
||||
|
||||
1. Wczytał dane połączenia DB z .env (DB_HOST, DB_NAME, DB_USER, DB_PASS)
|
||||
|
||||
2. Odpytał bazę (mysql CLI):
|
||||
- Pobierz aktywne mapowania z project_mappings
|
||||
- Pobierz zamówienia "w realizacji": JOIN orders + order_items + order_addresses
|
||||
WHERE orders.status IN (kody statusów z grupy "w realizacji")
|
||||
AND order_items.project_generated = 0
|
||||
- Dla każdego produktu sprawdź LIKE match z product_name_pattern
|
||||
|
||||
3. Dla znalezionych produktów:
|
||||
- Wyświetl listę: zamówienie, produkt, personalizacja, dane kupującego
|
||||
- Zinterpretuj personalizację AI-em (wyciągnij imiona, datę, życzenia)
|
||||
- Pokaż co zamierza wygenerować i zapytaj o potwierdzenie
|
||||
|
||||
4. Po potwierdzeniu:
|
||||
- Uruchom skrypt: python tools/generowanie/{script_name} --parametry
|
||||
- Po sukcesie: UPDATE order_items SET project_generated=1, project_generated_at=NOW() WHERE id={item_id}
|
||||
|
||||
5. Wyświetl raport końcowy
|
||||
|
||||
Ważne w instrukcji:
|
||||
- Photoshop musi być uruchomiony — sprawdź przed generowaniem
|
||||
- Dane klienta (--klient) to imię i nazwisko z order_addresses (type=customer)
|
||||
- Zawsze pytaj o potwierdzenie przed generowaniem
|
||||
- Przy błędzie skryptu — nie oznaczaj jako wygenerowane, pokaż błąd
|
||||
</action>
|
||||
<verify>Komenda /wygeneruj-projekty pojawia się w dostępnych komendach Claude Code</verify>
|
||||
<done>AC-3 satisfied: komenda działa, Claude jest silnikiem generowania</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>Pełny przepływ generowania projektów z komendą /wygeneruj-projekty</what-built>
|
||||
<how-to-verify>
|
||||
1. Upewnij się że Photoshop jest uruchomiony
|
||||
2. W panelu dodaj mapowanie: "%buteleczk%" → "buteleczki_wzor1.py"
|
||||
3. Upewnij się że jest zamówienie "w realizacji" z produktem "buteleczka" i personalizacją
|
||||
4. Uruchom /wygeneruj-projekty w Claude Code
|
||||
5. Sprawdź: Claude znalazł zamówienie, poprawnie zinterpretował dane, zapytał o potwierdzenie
|
||||
6. Po potwierdzeniu: PSD wygenerowany, project_generated=1 w bazie
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- tools/generowanie/buteleczki_wzor1.py (istniejący skrypt — nie modyfikuj)
|
||||
- src/Modules/Orders/OrdersController.php (nie zmieniaj istniejącej logiki)
|
||||
- Istniejące tabele DB (nie modyfikuj struktury orders, order_addresses)
|
||||
- Szablony PSD (nie modyfikuj plików projektowych)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Tylko CRUD mapowań + komenda Claude Code — brak serwisu PHP do generowania
|
||||
- Generowanie uruchamiane ręcznie komendą — brak crona/automatyzacji
|
||||
- Brak podglądu wygenerowanych projektów w panelu
|
||||
- Brak zmian w widoku listy zamówień (project_generated nie wyświetlane)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] Migracja wykonuje się bez błędów
|
||||
- [ ] CRUD mapowań działa (dodaj, edytuj, usuń, toggle)
|
||||
- [ ] Komenda /wygeneruj-projekty poprawnie odpytuje DB
|
||||
- [ ] Claude interpretuje personalizację i prezentuje dane do potwierdzenia
|
||||
- [ ] Po potwierdzeniu — skrypt generuje PSD z poprawnymi danymi
|
||||
- [ ] project_generated = 1 po udanej generacji
|
||||
- [ ] Brak natywnych alert()/confirm() — OrderProAlerts
|
||||
- [ ] Style w SCSS, nie inline
|
||||
- [ ] CSRF w formularzach (_token)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie zadania ukończone
|
||||
- Wszystkie weryfikacje przechodzą
|
||||
- Wygenerowany PSD ma poprawne dane i zachowane pozycje warstw
|
||||
- System gotowy do rozszerzenia o nowe skrypty generujące
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/97-project-generation/97-01-SUMMARY.md`
|
||||
</output>
|
||||
152
.paul/phases/97-project-generation/97-01-SUMMARY.md
Normal file
152
.paul/phases/97-project-generation/97-01-SUMMARY.md
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
phase: 97-project-generation
|
||||
plan: 01
|
||||
subsystem: automation
|
||||
tags: [photoshop, python, psd, project-generation, smart-object]
|
||||
|
||||
requires: []
|
||||
provides:
|
||||
- project_mappings table and CRUD UI
|
||||
- project_generated flag on order_items
|
||||
- Claude Code /wygeneruj-projekty command
|
||||
- Photoshop API script for PSD generation
|
||||
- Project status badges on order list and details
|
||||
affects: [future product-specific generators]
|
||||
|
||||
tech-stack:
|
||||
added: [photoshop-python-api]
|
||||
patterns: [claude-as-engine, photoshop-com-api, smart-object-text-replace]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- tools/generowanie/buteleczki_wzor1.py
|
||||
- src/Modules/Settings/ProjectMappingController.php
|
||||
- src/Modules/Settings/ProjectMappingRepository.php
|
||||
- resources/views/settings/project-mappings.php
|
||||
- .claude/commands/wygeneruj-projekty.md
|
||||
- database/migrations/20260412_000097_add_project_generation.sql
|
||||
- database/migrations/20260412_000098_rename_external_status_id_to_status_code.sql
|
||||
modified:
|
||||
- routes/web.php
|
||||
- resources/views/layouts/app.php
|
||||
- resources/lang/pl.php
|
||||
- src/Modules/Orders/OrdersController.php
|
||||
- src/Modules/Orders/OrdersRepository.php
|
||||
- resources/views/orders/show.php
|
||||
|
||||
key-decisions:
|
||||
- "Claude as generation engine — AI interprets client personalization instead of rigid PHP parser"
|
||||
- "Rename external_status_id to status_code — column held internal status, name was misleading"
|
||||
- "Position preservation via bounds+translate — Photoshop API ignores textItem.position assignment"
|
||||
|
||||
patterns-established:
|
||||
- "Smart Object text editing: save bounds before, translate after change"
|
||||
- "Claude Code command as workflow engine for semi-automated processes"
|
||||
|
||||
duration: ~120min
|
||||
started: 2026-04-12T00:00:00Z
|
||||
completed: 2026-04-12T01:30:00Z
|
||||
---
|
||||
|
||||
# Phase 97 Plan 01: Project Generation Summary
|
||||
|
||||
**Polautomatyczne generowanie projektow graficznych PSD z danych zamowien — Claude jako silnik interpretacji danych klienta, Photoshop API do podmiany tekstow w Smart Objects.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~120min |
|
||||
| Started | 2026-04-12 |
|
||||
| Completed | 2026-04-12 |
|
||||
| Tasks | 5 completed (3 auto + 2 checkpoints) |
|
||||
| Files created | 8 |
|
||||
| Files modified | 15+ (including rename refactor) |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Tabela project_mappings + kolumna project_generated | Pass | Migracja 000097 + 000098 |
|
||||
| AC-2: CRUD mapowan w panelu | Pass | Ustawienia > Mapowanie projektow |
|
||||
| AC-3: Komenda /wygeneruj-projekty | Pass | Test end-to-end z zamowieniem OP000000159 |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Skrypt Python `buteleczki_wzor1.py` generuje PSD przez Photoshop COM API z zachowaniem pozycji warstw
|
||||
- CRUD mapowan produktow na skrypty w panelu ustawien
|
||||
- Komenda Claude Code `/wygeneruj-projekty` — Claude odpytuje DB, interpretuje personalizacje AI-em, uruchamia skrypt, oznacza w bazie
|
||||
- Flagi statusu projektow na liscie zamowien (zielona/zolta/szara ikonka) i w szczegolach (badge przy produkcie)
|
||||
- Rename `external_status_id` → `status_code` w 13+ plikach PHP — poprawka mylacej nazwy kolumny
|
||||
- Detekcja Photoshopa przed generowaniem (tasklist)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `tools/generowanie/buteleczki_wzor1.py` | Created | Skrypt Photoshop API — buteleczki weselne Wzor 1 |
|
||||
| `database/migrations/20260412_000097_add_project_generation.sql` | Created | Tabela project_mappings + kolumny project_generated |
|
||||
| `database/migrations/20260412_000098_rename_external_status_id_to_status_code.sql` | Created | Rename kolumny statusu |
|
||||
| `src/Modules/Settings/ProjectMappingRepository.php` | Created | CRUD repository dla mapowan |
|
||||
| `src/Modules/Settings/ProjectMappingController.php` | Created | Kontroler CRUD mapowan |
|
||||
| `resources/views/settings/project-mappings.php` | Created | Widok ustawien mapowan |
|
||||
| `resources/scss/modules/_project-mappings.scss` | Created | Style mapowan + badge projektow |
|
||||
| `.claude/commands/wygeneruj-projekty.md` | Created | Komenda Claude Code |
|
||||
| `routes/web.php` | Modified | Routing + instancjacja kontrolera |
|
||||
| `resources/views/layouts/app.php` | Modified | Link w sidebar menu |
|
||||
| `resources/lang/pl.php` | Modified | Tlumaczenia |
|
||||
| `resources/scss/app.scss` | Modified | Import SCSS modulu |
|
||||
| `src/Modules/Orders/OrdersRepository.php` | Modified | projects_done/total w query + rename |
|
||||
| `src/Modules/Orders/OrdersController.php` | Modified | projectBadge() + productsHtml() |
|
||||
| `resources/views/orders/show.php` | Modified | Badge przy produkcie |
|
||||
| `src/Modules/Orders/OrderImportRepository.php` | Modified | Rename |
|
||||
| `src/Modules/Automation/OrderStatusAgedService.php` | Modified | Rename |
|
||||
| `src/Modules/Automation/AutomationService.php` | Modified | Rename |
|
||||
| `src/Modules/Settings/AllegroOrderImportService.php` | Modified | Rename |
|
||||
| `src/Modules/Settings/AllegroStatusSyncService.php` | Modified | Rename |
|
||||
| `src/Modules/Settings/ShopproStatusSyncService.php` | Modified | Rename |
|
||||
| `src/Modules/Settings/ShopproPaymentStatusSyncService.php` | Modified | Rename |
|
||||
| `src/Modules/Settings/ShopproOrderMapper.php` | Modified | Rename |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Scope additions | 2 | Valuable improvements |
|
||||
| Auto-fixed | 3 | Import namespace fixes |
|
||||
|
||||
### Scope Additions
|
||||
|
||||
1. **Rename `external_status_id` → `status_code`** — odkryto podczas testow komendy ze nazwa kolumny jest mylaca. Refactor 13+ plikow + migracja.
|
||||
|
||||
2. **Flagi statusu projektow na liscie/szczegolach** — uzytkownik poprosil o wizualna informacje o statusie generacji. Dodano ikonki i badge.
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
1. **Bledne importy w ProjectMappingController** — `App\Core\Template\Template` → `App\Core\View\Template`, `App\Core\Translation\Translator` → `App\Core\I18n\Translator`, `App\Core\Session\Flash` → `App\Core\Support\Flash`
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Claude jako silnik generowania | AI lepiej interpretuje rozne formaty personalizacji niz sztywny parser PHP | Komenda zamiast serwisu PHP |
|
||||
| Rename external_status_id | Kolumna trzyma wewnetrzny status, nazwa mylaca | 13+ plikow zmienionych, czytelniejszy kod |
|
||||
| Bounds+translate dla pozycji | textItem.position nie dziala w Photoshop API | Niezawodne zachowanie pozycji warstw |
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- System gotowy do rozszerzenia o nowe skrypty (kubki, pudełka, itp.)
|
||||
- Mapowania w panelu — latwe dodawanie nowych produktow
|
||||
- Struktura warstw PSD uporzadkowana (imie_zenskie, imie_meskie, data, zyczenia)
|
||||
|
||||
**Concerns:**
|
||||
- Kazdy nowy produkt wymaga osobnego skryptu Python + uporządkowania warstw PSD
|
||||
|
||||
**Blockers:** None
|
||||
|
||||
---
|
||||
*Phase: 97-project-generation, Plan: 01*
|
||||
*Completed: 2026-04-12*
|
||||
122
.vscode/ftp-kr.sync.cache.json
vendored
122
.vscode/ftp-kr.sync.cache.json
vendored
@@ -828,6 +828,12 @@
|
||||
"size": 1566,
|
||||
"lmtime": 1775675823530,
|
||||
"modified": false
|
||||
},
|
||||
"20260410_000081_add_remember_token_to_users.sql": {
|
||||
"type": "-",
|
||||
"size": 83,
|
||||
"lmtime": 1775815203659,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"seeders": {},
|
||||
@@ -2171,8 +2177,8 @@
|
||||
"css": {
|
||||
"app.css": {
|
||||
"type": "-",
|
||||
"size": 60793,
|
||||
"lmtime": 1775594018472,
|
||||
"size": 65359,
|
||||
"lmtime": 1775819628149,
|
||||
"modified": false
|
||||
},
|
||||
"app.css.map": {
|
||||
@@ -2183,8 +2189,8 @@
|
||||
},
|
||||
"login.css": {
|
||||
"type": "-",
|
||||
"size": 5996,
|
||||
"lmtime": 1775561875587,
|
||||
"size": 6586,
|
||||
"lmtime": 1775817361366,
|
||||
"modified": false
|
||||
},
|
||||
"login.css.map": {
|
||||
@@ -2211,10 +2217,16 @@
|
||||
"lmtime": 1774909546633,
|
||||
"modified": false
|
||||
},
|
||||
"global-search.js": {
|
||||
"type": "-",
|
||||
"size": 3578,
|
||||
"lmtime": 1775561835372,
|
||||
"modified": false
|
||||
},
|
||||
"inline-status-change.js": {
|
||||
"type": "-",
|
||||
"size": 6628,
|
||||
"lmtime": 1775561174202,
|
||||
"size": 7677,
|
||||
"lmtime": 1775819250377,
|
||||
"modified": false
|
||||
},
|
||||
"jquery-alerts.js": {
|
||||
@@ -2222,12 +2234,6 @@
|
||||
"size": 5768,
|
||||
"lmtime": 1771873304132,
|
||||
"modified": false
|
||||
},
|
||||
"global-search.js": {
|
||||
"type": "-",
|
||||
"size": 3578,
|
||||
"lmtime": 1775561835372,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2250,8 +2256,8 @@
|
||||
"lang": {
|
||||
"pl.php": {
|
||||
"type": "-",
|
||||
"size": 64043,
|
||||
"lmtime": 1775589624048,
|
||||
"size": 64489,
|
||||
"lmtime": 1775817352765,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
@@ -2298,10 +2304,18 @@
|
||||
},
|
||||
"app.scss": {
|
||||
"type": "-",
|
||||
"size": 47604,
|
||||
"lmtime": 1775594008985,
|
||||
"size": 47641,
|
||||
"lmtime": 1775816862771,
|
||||
"modified": false
|
||||
},
|
||||
"components": {
|
||||
"_order-preview-modal.scss": {
|
||||
"type": "-",
|
||||
"size": 2897,
|
||||
"lmtime": 1775816831839,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"login.css": {
|
||||
"type": "-",
|
||||
"size": 7437,
|
||||
@@ -2316,8 +2330,8 @@
|
||||
},
|
||||
"login.scss": {
|
||||
"type": "-",
|
||||
"size": 2670,
|
||||
"lmtime": 1771866989000,
|
||||
"size": 2932,
|
||||
"lmtime": 1775815275052,
|
||||
"modified": false
|
||||
},
|
||||
"modules": {
|
||||
@@ -2345,6 +2359,18 @@
|
||||
"lmtime": 1773786622096,
|
||||
"modified": false
|
||||
},
|
||||
"_global-search.scss": {
|
||||
"type": "-",
|
||||
"size": 1649,
|
||||
"lmtime": 1775561859898,
|
||||
"modified": false
|
||||
},
|
||||
"_order-preview-modal.scss": {
|
||||
"type": "-",
|
||||
"size": 3863,
|
||||
"lmtime": 1775819538561,
|
||||
"modified": false
|
||||
},
|
||||
"_printing.scss": {
|
||||
"type": "-",
|
||||
"size": 1115,
|
||||
@@ -2356,19 +2382,13 @@
|
||||
"size": 2649,
|
||||
"lmtime": 1774219643850,
|
||||
"modified": true
|
||||
},
|
||||
"_global-search.scss": {
|
||||
"type": "-",
|
||||
"size": 1649,
|
||||
"lmtime": 1775561859898,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"_ui-components.scss": {
|
||||
"type": "-",
|
||||
"size": 4276,
|
||||
"lmtime": 1772754851900,
|
||||
"size": 5581,
|
||||
"lmtime": 1775729374201,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
@@ -2378,8 +2398,8 @@
|
||||
"auth": {
|
||||
"login.php": {
|
||||
"type": "-",
|
||||
"size": 1697,
|
||||
"lmtime": 1771866989000,
|
||||
"size": 1784,
|
||||
"lmtime": 1775815268579,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
@@ -2462,8 +2482,8 @@
|
||||
},
|
||||
"list.php": {
|
||||
"type": "-",
|
||||
"size": 2015,
|
||||
"lmtime": 1774599283649,
|
||||
"size": 4440,
|
||||
"lmtime": 1775816812032,
|
||||
"modified": false
|
||||
},
|
||||
"partials": {
|
||||
@@ -2472,6 +2492,18 @@
|
||||
"size": 7455,
|
||||
"lmtime": 1773786546755,
|
||||
"modified": false
|
||||
},
|
||||
"preview-content.php": {
|
||||
"type": "-",
|
||||
"size": 8371,
|
||||
"lmtime": 1775817346465,
|
||||
"modified": false
|
||||
},
|
||||
"preview-modal.php": {
|
||||
"type": "-",
|
||||
"size": 880,
|
||||
"lmtime": 1775816779818,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"receipt-create.php": {
|
||||
@@ -2482,8 +2514,8 @@
|
||||
},
|
||||
"show.php": {
|
||||
"type": "-",
|
||||
"size": 53047,
|
||||
"lmtime": 1775665227986,
|
||||
"size": 54539,
|
||||
"lmtime": 1775729335385,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
@@ -2672,8 +2704,8 @@
|
||||
"routes": {
|
||||
"web.php": {
|
||||
"type": "-",
|
||||
"size": 29262,
|
||||
"lmtime": 1775684219403,
|
||||
"size": 29359,
|
||||
"lmtime": 1775816738226,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
@@ -2890,20 +2922,20 @@
|
||||
"Auth": {
|
||||
"AuthController.php": {
|
||||
"type": "-",
|
||||
"size": 2543,
|
||||
"lmtime": 1772491348696,
|
||||
"size": 3051,
|
||||
"lmtime": 1775815250304,
|
||||
"modified": false
|
||||
},
|
||||
"AuthMiddleware.php": {
|
||||
"type": "-",
|
||||
"size": 665,
|
||||
"lmtime": 1771459672950,
|
||||
"size": 739,
|
||||
"lmtime": 1775815255580,
|
||||
"modified": false
|
||||
},
|
||||
"AuthService.php": {
|
||||
"type": "-",
|
||||
"size": 1599,
|
||||
"lmtime": 1771692611409,
|
||||
"size": 3638,
|
||||
"lmtime": 1775815232182,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
@@ -3118,8 +3150,8 @@
|
||||
},
|
||||
"OrdersController.php": {
|
||||
"type": "-",
|
||||
"size": 42021,
|
||||
"lmtime": 1775594039433,
|
||||
"size": 44151,
|
||||
"lmtime": 1775819210756,
|
||||
"modified": false
|
||||
},
|
||||
"OrdersRepository.php": {
|
||||
@@ -3658,9 +3690,9 @@
|
||||
"Users": {
|
||||
"UserRepository.php": {
|
||||
"type": "-",
|
||||
"size": 5346,
|
||||
"lmtime": 1771691981226,
|
||||
"modified": true
|
||||
"size": 6255,
|
||||
"lmtime": 1775815211808,
|
||||
"modified": false
|
||||
},
|
||||
"UsersController.php": {
|
||||
"type": "-",
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
- Tabela `order_activity_log` rejestruje wszystkie zdarzenia dotyczace zamowienia.
|
||||
- Typy zdarzen: `status_change`, `payment`, `invoice`, `shipment`, `message`, `document`, `import`, `note`.
|
||||
- Rejestracja zdarzen: `OrdersRepository::recordActivity(...)`.
|
||||
- Zmiana statusu: `OrdersRepository::updateOrderStatus(...)` — aktualizuje `orders.external_status_id`, wpisuje do `order_status_history` i `order_activity_log`.
|
||||
- Zmiana statusu: `OrdersRepository::updateOrderStatus(...)` — aktualizuje `orders.status_code`, wpisuje do `order_status_history` i `order_activity_log`.
|
||||
- Import zamowienia: `AllegroOrderImportService::importSingleOrder(...)` — po upsert zamowienia rejestruje zdarzenie `import` w `order_activity_log` (nowy import lub re-import/aktualizacja), actor_type `import`, actor_name `Allegro`.
|
||||
- Widok szczegolow zamowienia (`GET /orders/{id}`) wyswietla log aktywnosci w zakladce `Historia zmian`.
|
||||
|
||||
@@ -458,7 +458,7 @@
|
||||
- `POST /orders/{id}/status`:
|
||||
- `OrdersController::updateStatus(Request): Response`
|
||||
- waliduje CSRF i wybrany status,
|
||||
- wywoluje `OrdersRepository::updateOrderStatus(...)` (aktualizuje `orders.external_status_id`, wpisuje do `order_status_history` i `order_activity_log`),
|
||||
- wywoluje `OrdersRepository::updateOrderStatus(...)` (aktualizuje `orders.status_code`, wpisuje do `order_status_history` i `order_activity_log`),
|
||||
- actor_type: `user`, actor_name: nazwa zalogowanego uzytkownika,
|
||||
- po zapisie redirect do `GET /orders/{id}` z flash message (sukces/blad).
|
||||
- Widok szczegolow zamowienia wyswietla dropdown ze wszystkimi aktywnymi statusami (pogrupowanymi wg grup statusow) obok aktualnego statusu.
|
||||
|
||||
@@ -85,7 +85,7 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane
|
||||
- rozszerzenie `company_settings` o `sender_contact_person` (osoba kontaktowa nadawcy),
|
||||
- wykorzystywane w payloadzie Apaczka jako `address.sender.contact_person`.
|
||||
- 2026-03-08: Ujednolicono styl naglowkow sekcji UI (`section-title`) - bez zmian schematu bazy.
|
||||
- 2026-03-14: Dodano migracje `20260314_000048_add_orders_performance_indexes.sql` — indeksy wydajnosciowe na tabeli `orders`: `source`, `external_status_id`, `ordered_at`, composite `(source, external_status_id)`.
|
||||
- 2026-03-14: Dodano migracje `20260314_000048_add_orders_performance_indexes.sql` — indeksy wydajnosciowe na tabeli `orders`: `source`, `status_code`, `ordered_at`, composite `(source, status_code)`.
|
||||
- 2026-03-14: Dodano migracje `20260314_000049_add_cron_last_run_at_setting.sql` — seed klucza `cron_web_last_run_at` w `app_settings` (cron throttle przeniesiony z sesji do DB).
|
||||
- 2026-03-14: Przemianowano migracje `20260301_000014_add_products_sku_format_setting.sql` na `20260301_000014b_add_products_sku_format_setting.sql` — deduplikacja numeru sekwencji (kolizja z `000014_create_product_integration_translations`).
|
||||
- 2026-03-15: Dodano migracje `20260315_000050_create_receipt_configs_table.sql` — tabela konfiguracji paragonow (szablony numeracji, ustawienia imiennosci, zrodlo daty sprzedazy).
|
||||
|
||||
@@ -41,7 +41,7 @@ Rozdzielenie mapowania statusow shopPRO na dwa niezalezne kierunki: PUSH (orderP
|
||||
|
||||
**DB:** Migracja 20260407_000079 — nowa tabela order_status_pull_mappings, pre-populate z istniejacych danych.
|
||||
|
||||
**Ochrona statusu przy re-imporcie:** Re-import z shopPRO nie nadpisuje `external_status_id` istniejacego zamowienia, CHYBA ZE obecny status to `nieoplacone` i shopPRO potwierdza platnosc (`payment_status = 2`). Wtedy status zmienia sie na zmapowany (np. `w_realizacji`) i importowane sa dane platnosci. W kazdym innym przypadku status jest zachowany — orderPRO jest master.
|
||||
**Ochrona statusu przy re-imporcie:** Re-import z shopPRO nie nadpisuje `status_code` istniejacego zamowienia, CHYBA ZE obecny status to `nieoplacone` i shopPRO potwierdza platnosc (`payment_status = 2`). Wtedy status zmienia sie na zmapowany (np. `w_realizacji`) i importowane sa dane platnosci. W kazdym innym przypadku status jest zachowany — orderPRO jest master.
|
||||
|
||||
**Pliki:**
|
||||
- database/migrations/20260407_000079_pull_status_mappings.sql
|
||||
@@ -438,7 +438,7 @@ Odwrocenie kierunku mapowania statusow w integracjach shopPRO i Allegro.
|
||||
- Zoptymalizowano zapytanie listy zamowien (`OrdersRepository::buildListSql()`):
|
||||
- 4 correlated subqueries (items_count, items_qty, shipments_count, documents_count) zastapiono aggregating LEFT JOINami — eliminuje N+1 na kazdym wierszu listy.
|
||||
- `OrdersRepository::canResolveMappedMedia()` — zamiana instance property na `static` — `information_schema` odpytywany co najwyzej raz na cykl PHP zamiast raz per instancja.
|
||||
- Dodano migracje `20260314_000048_add_orders_performance_indexes.sql` — indeksy na `orders`: `source`, `external_status_id`, `ordered_at`, composite `(source, external_status_id)`.
|
||||
- Dodano migracje `20260314_000048_add_orders_performance_indexes.sql` — indeksy na `orders`: `source`, `status_code`, `ordered_at`, composite `(source, status_code)`.
|
||||
- Dodano SSL verification (`CURLOPT_SSL_VERIFYPEER => true`, `CURLOPT_SSL_VERIFYHOST => 2`, `CURLOPT_CAINFO`) do 4 klas ApiClient: AllegroApiClient (3 metody), AllegroOAuthClient, ShopproApiClient, ApaczkaApiClient. Fallback: `$_ENV['CURL_CA_BUNDLE_PATH']` → XAMPP cacert.pem → system CA bundle.
|
||||
- Cron web throttle (`isWebCronThrottled()`) przeniesiony z `$_SESSION` do `app_settings` (klucz `cron_web_last_run_at`) — eliminuje wielokrotne uruchamianie crona przy wielu aktywnych sesjach.
|
||||
- Deduplikacja migracji `000014` → `000014b` (kolizja z `create_product_integration_translations`).
|
||||
@@ -815,7 +815,7 @@ Odwrocenie kierunku mapowania statusow w integracjach shopPRO i Allegro.
|
||||
- Dodano `App\Modules\Settings\AllegroStatusMappingRepository`.
|
||||
- Import pojedynczego zamowienia Allegro mapuje teraz status przez `allegro_order_status_mappings` (jesli istnieje wpis), zamiast fallbackowej translacji.
|
||||
- Poprawiono prezentacje statusow na liscie zamowien:
|
||||
- filtr statusu pokazuje etykiety biznesowe zamiast surowych kodow (`external_status_id`),
|
||||
- filtr statusu pokazuje etykiety biznesowe zamiast surowych kodow (`status_code`),
|
||||
- kody bez mapowania sa tylko formatowane technicznie do czytelnej postaci (`do_odbioru` -> `Do odbioru`).
|
||||
- Dodano reczny import pojedynczego zamowienia Allegro z poziomu `Ustawienia > Integracje > Allegro`:
|
||||
- endpoint `POST /settings/integrations/allegro/import-single`,
|
||||
@@ -930,7 +930,7 @@ Odwrocenie kierunku mapowania statusow w integracjach shopPRO i Allegro.
|
||||
- Rozszerzono `bin/deploy_and_seed_orders.php` o parametr `--profile=default|realistic`.
|
||||
- Dodano realistyczny profil seedowania:
|
||||
- wazone losowanie statusow i metod platnosci,
|
||||
- spojne mapowanie `external_status_id` -> `payment_status` i `total_paid`,
|
||||
- spojne mapowanie `status_code` -> `payment_status` i `total_paid`,
|
||||
- bardziej realne reguly tworzenia wpisow `order_payments`, `order_shipments`, `order_documents`,
|
||||
- historia statusow oparta na logicznych sciezkach przejsc (zamiast losowych skokow).
|
||||
- Wykonano ponowne wdrozenie draftu i seed z profilem realistycznym:
|
||||
@@ -984,11 +984,11 @@ Odwrocenie kierunku mapowania statusow w integracjach shopPRO i Allegro.
|
||||
- aktywny panel `Szczegoly zamowienia`,
|
||||
- pozostale panele (`Historia zmian`, `Przesylki`, `Platnosci`, `Dokumenty`) zawieraja tymczasowe puste boksy.
|
||||
- Zmieniono seed zamowien (`bin/deploy_and_seed_orders.php`):
|
||||
- `external_status_id` jest losowany z aktywnych statusow z tabeli `order_statuses` (zgodnie z konfiguracja w `Ustawienia > Statusy`),
|
||||
- `status_code` jest losowany z aktywnych statusow z tabeli `order_statuses` (zgodnie z konfiguracja w `Ustawienia > Statusy`),
|
||||
- dodano fallback do listy domyslnej, jesli tabela jest pusta/niedostepna,
|
||||
- profil `realistic` ma fallback reguly finansowej dla niestandardowych statusow.
|
||||
- Dodano skrypt serwisowy `bin/randomize_order_statuses.php`:
|
||||
- losowo podmienia `orders.external_status_id` dla juz istniejacych zamowien na aktywne statusy z `order_statuses`,
|
||||
- losowo podmienia `orders.status_code` dla juz istniejacych zamowien na aktywne statusy z `order_statuses`,
|
||||
- aktualizuje tez `is_canceled_by_buyer` dla statusu `cancelled`,
|
||||
- wspiera `--use-remote` i `--dry-run`.
|
||||
- Wykonano podmiane statusow na bazie zdalnej (`--use-remote`): zaktualizowano 30 zamowien.
|
||||
|
||||
@@ -145,13 +145,13 @@ function seedData(PDO $pdo, int $count, bool $append, string $profile): void
|
||||
$insertOrder = $pdo->prepare(
|
||||
'INSERT INTO orders (
|
||||
integration_id, source, source_order_id, external_order_id, external_platform_id, external_platform_account_id,
|
||||
external_status_id, external_payment_type_id, payment_status, external_carrier_id, external_carrier_account_id,
|
||||
status_code, external_payment_type_id, payment_status, external_carrier_id, external_carrier_account_id,
|
||||
customer_login, is_invoice, is_encrypted, is_canceled_by_buyer, currency, total_without_tax, total_with_tax,
|
||||
total_paid, send_date_min, send_date_max, ordered_at, source_created_at, source_updated_at, preferences_json,
|
||||
payload_json, fetched_at, created_at, updated_at
|
||||
) VALUES (
|
||||
:integration_id, :source, :source_order_id, :external_order_id, :external_platform_id, :external_platform_account_id,
|
||||
:external_status_id, :external_payment_type_id, :payment_status, :external_carrier_id, :external_carrier_account_id,
|
||||
:status_code, :external_payment_type_id, :payment_status, :external_carrier_id, :external_carrier_account_id,
|
||||
:customer_login, :is_invoice, :is_encrypted, :is_canceled_by_buyer, :currency, :total_without_tax, :total_with_tax,
|
||||
:total_paid, :send_date_min, :send_date_max, :ordered_at, :source_created_at, :source_updated_at, :preferences_json,
|
||||
:payload_json, :fetched_at, :created_at, :updated_at
|
||||
@@ -331,7 +331,7 @@ function seedData(PDO $pdo, int $count, bool $append, string $profile): void
|
||||
'external_order_id' => $externalOrderId,
|
||||
'external_platform_id' => $platformId,
|
||||
'external_platform_account_id' => $platformAccountId,
|
||||
'external_status_id' => $status,
|
||||
'status_code' => $status,
|
||||
'external_payment_type_id' => $paymentType,
|
||||
'payment_status' => $paymentStatus,
|
||||
'external_carrier_id' => $carrier,
|
||||
|
||||
@@ -65,7 +65,7 @@ if ($statuses === []) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$ordersRows = $pdo->query('SELECT id, external_status_id FROM orders ORDER BY id ASC')->fetchAll(PDO::FETCH_ASSOC);
|
||||
$ordersRows = $pdo->query('SELECT id, status_code FROM orders ORDER BY id ASC')->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (!is_array($ordersRows) || $ordersRows === []) {
|
||||
echo '[result] Brak zamowien do aktualizacji.' . PHP_EOL;
|
||||
exit(0);
|
||||
@@ -73,7 +73,7 @@ if (!is_array($ordersRows) || $ordersRows === []) {
|
||||
|
||||
$preview = [];
|
||||
$changes = 0;
|
||||
$updateStmt = $pdo->prepare('UPDATE orders SET external_status_id = :status, is_canceled_by_buyer = :is_canceled_by_buyer, updated_at = NOW() WHERE id = :id');
|
||||
$updateStmt = $pdo->prepare('UPDATE orders SET status_code = :status, is_canceled_by_buyer = :is_canceled_by_buyer, updated_at = NOW() WHERE id = :id');
|
||||
|
||||
if (!$dryRun) {
|
||||
$pdo->beginTransaction();
|
||||
@@ -85,7 +85,7 @@ try {
|
||||
if ($id <= 0) {
|
||||
continue;
|
||||
}
|
||||
$old = strtolower(trim((string) ($row['external_status_id'] ?? '')));
|
||||
$old = strtolower(trim((string) ($row['status_code'] ?? '')));
|
||||
$new = $statuses[array_rand($statuses)];
|
||||
|
||||
if (count($preview) < 8) {
|
||||
|
||||
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS orders (
|
||||
external_order_id VARCHAR(128) NULL,
|
||||
external_platform_id VARCHAR(64) NULL,
|
||||
external_platform_account_id VARCHAR(64) NULL,
|
||||
external_status_id VARCHAR(64) NULL,
|
||||
status_code VARCHAR(64) NULL,
|
||||
external_payment_type_id VARCHAR(64) NULL,
|
||||
payment_status TINYINT UNSIGNED NULL,
|
||||
external_carrier_id VARCHAR(64) NULL,
|
||||
@@ -34,7 +34,7 @@ CREATE TABLE IF NOT EXISTS orders (
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY orders_integration_source_order_unique (integration_id, source_order_id),
|
||||
KEY orders_integration_external_idx (integration_id, external_order_id),
|
||||
KEY orders_status_idx (external_status_id),
|
||||
KEY orders_status_idx (status_code),
|
||||
KEY orders_source_updated_idx (source_updated_at),
|
||||
KEY orders_ordered_at_idx (ordered_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
-- Performance indexes for orders list view
|
||||
-- Covers: source filter, external_status_id filter/sort, ordered_at sort,
|
||||
-- composite source+external_status_id for combined filtering
|
||||
-- Covers: source filter, status_code filter/sort, ordered_at sort,
|
||||
-- composite source+status_code for combined filtering
|
||||
ALTER TABLE orders
|
||||
ADD INDEX IF NOT EXISTS orders_source_idx (source),
|
||||
ADD INDEX IF NOT EXISTS orders_external_status_idx (external_status_id),
|
||||
ADD INDEX IF NOT EXISTS orders_status_codex (status_code),
|
||||
ADD INDEX IF NOT EXISTS orders_ordered_at_idx (ordered_at),
|
||||
ADD INDEX IF NOT EXISTS orders_source_status_idx (source, external_status_id);
|
||||
ADD INDEX IF NOT EXISTS orders_source_status_idx (source, status_code);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD COLUMN remember_token VARCHAR(255) NULL AFTER password_hash;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Phase 97: Project Generation
|
||||
-- Tabela mapowań produktów na skrypty generujące projekty graficzne
|
||||
-- Kolumna project_generated w order_items
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_mappings (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
product_name_pattern VARCHAR(255) NOT NULL,
|
||||
script_name VARCHAR(255) NOT NULL,
|
||||
output_dir VARCHAR(500) NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
ALTER TABLE order_items ADD COLUMN project_generated TINYINT(1) NOT NULL DEFAULT 0 AFTER payload_json;
|
||||
ALTER TABLE order_items ADD COLUMN project_generated_at DATETIME NULL AFTER project_generated;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Phase 97: Rename misleading column external_status_id to status_code
|
||||
-- This column holds the internal orderPRO status code, not an external one
|
||||
|
||||
ALTER TABLE orders CHANGE COLUMN external_status_id status_code VARCHAR(64) NULL DEFAULT NULL;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -62,6 +62,19 @@
|
||||
return html;
|
||||
}
|
||||
|
||||
function buildPaymentMethodCheckboxes(namePrefix) {
|
||||
var html = '<div class="checkbox-group">';
|
||||
Object.keys(data.paymentMethodOptions || {}).forEach(function(methodKey) {
|
||||
var label = data.paymentMethodOptions[methodKey] || methodKey;
|
||||
html += '<label class="checkbox-label">'
|
||||
+ '<input type="checkbox" name="' + namePrefix + '[payment_method_keys][]" value="' + escapeHtml(methodKey) + '"> '
|
||||
+ escapeHtml(label)
|
||||
+ '</label>';
|
||||
});
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function buildOrderStatusCheckboxes(namePrefix) {
|
||||
var html = '<div class="checkbox-group">';
|
||||
(data.orderStatusOptions || []).forEach(function(statusOption) {
|
||||
@@ -167,6 +180,7 @@
|
||||
+ '<option value="integration" selected>Integracja (kanal sprzedazy)</option>'
|
||||
+ '<option value="shipment_status">Status przesylki</option>'
|
||||
+ '<option value="payment_status">Status platnosci</option>'
|
||||
+ '<option value="payment_method">Metoda platnosci</option>'
|
||||
+ '<option value="order_status">Status zamowienia</option>'
|
||||
+ '<option value="days_in_status">Liczba dni w statusie</option>'
|
||||
+ '</select>'
|
||||
@@ -226,6 +240,10 @@
|
||||
configDiv.innerHTML = buildPaymentStatusCheckboxes(namePrefix);
|
||||
return;
|
||||
}
|
||||
if (select.value === 'payment_method') {
|
||||
configDiv.innerHTML = buildPaymentMethodCheckboxes(namePrefix);
|
||||
return;
|
||||
}
|
||||
if (select.value === 'order_status') {
|
||||
configDiv.innerHTML = buildOrderStatusCheckboxes(namePrefix);
|
||||
return;
|
||||
|
||||
@@ -104,6 +104,39 @@
|
||||
dropdown.style.left = wrapRect.left + 'px';
|
||||
}
|
||||
|
||||
function refreshTableAndPanel() {
|
||||
var url = window.location.pathname + window.location.search;
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(function (resp) {
|
||||
if (!resp.ok) return;
|
||||
return resp.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
if (!data) return;
|
||||
if (data.tableHtml) {
|
||||
var tableContainer = document.querySelector('.table-list');
|
||||
if (tableContainer) {
|
||||
tableContainer.outerHTML = data.tableHtml;
|
||||
}
|
||||
}
|
||||
if (data.panelHtml) {
|
||||
var tmp = document.createElement('div');
|
||||
tmp.innerHTML = data.panelHtml;
|
||||
var newBody = tmp.querySelector('.order-statuses-side__body');
|
||||
var currentBody = document.querySelector('.order-statuses-side__body');
|
||||
if (newBody && currentBody) {
|
||||
currentBody.innerHTML = newBody.innerHTML;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
// Status already updated optimistically — silent fail for table refresh
|
||||
});
|
||||
}
|
||||
|
||||
function changeStatus(wrap, orderId, newStatusCode, allStatuses, statusColorMap, csrfToken) {
|
||||
var prevHtml = wrap.innerHTML;
|
||||
var prevStatus = wrap.getAttribute('data-current-status');
|
||||
@@ -152,7 +185,7 @@
|
||||
var d = result.data;
|
||||
wrap.innerHTML = buildBadgeHtml(d.status_code, d.status_label, d.status_color);
|
||||
wrap.setAttribute('data-current-status', d.status_code);
|
||||
location.reload();
|
||||
refreshTableAndPanel();
|
||||
})
|
||||
.catch(function () {
|
||||
wrap.innerHTML = prevHtml;
|
||||
|
||||
@@ -35,6 +35,7 @@ return [
|
||||
'company' => 'Dane firmy',
|
||||
'accounting' => 'Ksiegowosc',
|
||||
'accounting_section' => 'Ksiegowosc',
|
||||
'project_mapping' => 'Mapowanie projektow',
|
||||
],
|
||||
'marketplace' => [
|
||||
'title' => 'Marketplace',
|
||||
@@ -84,7 +85,7 @@ return [
|
||||
'title' => 'Logowanie',
|
||||
'heading' => 'Panel zarzadzania zamowieniami',
|
||||
'subtitle' => 'Zaloguj sie, aby przejsc do obslugi zamowien i wysylek.',
|
||||
'error_placeholder' => 'Miejsce na komunikat bledu logowania.',
|
||||
'remember_me' => 'Zapamietaj mnie',
|
||||
'email_label' => 'Email',
|
||||
'email_placeholder' => 'np. admin@firma.pl',
|
||||
'password_label' => 'Haslo',
|
||||
@@ -105,6 +106,18 @@ return [
|
||||
'title' => 'Zamowienia',
|
||||
'description' => 'Kompaktowa lista zamowien oparta o lokalna baze orderPRO.',
|
||||
'empty' => 'Brak zamowien do wyswietlenia.',
|
||||
'preview' => [
|
||||
'title' => 'Podglad zamowienia',
|
||||
'buyer' => 'Kupujacy',
|
||||
'order_number' => 'Nr zamowienia',
|
||||
'delivery_address' => 'Adres dostawy',
|
||||
'products' => 'Produkty',
|
||||
'summary' => 'Podsumowanie',
|
||||
'full_details' => 'Pelne szczegoly',
|
||||
'close' => 'Zamknij',
|
||||
'loading' => 'Ladowanie...',
|
||||
'notes' => 'Wiadomosc od klienta',
|
||||
],
|
||||
'fields' => [
|
||||
'order_ref' => 'Zamowienie',
|
||||
'status' => 'Status',
|
||||
@@ -1110,6 +1123,46 @@ return [
|
||||
'save_failed' => 'Nie udalo sie zapisac ustawien GS1.',
|
||||
],
|
||||
],
|
||||
'project_mapping' => [
|
||||
'title' => 'Mapowanie projektow',
|
||||
'description' => 'Mapowanie produktow na skrypty generujace projekty graficzne.',
|
||||
'add_title' => 'Dodaj mapowanie',
|
||||
'list_title' => 'Istniejace mapowania',
|
||||
'edit_title' => 'Edytuj mapowanie',
|
||||
'empty' => 'Brak zdefiniowanych mapowan.',
|
||||
'confirm_delete' => 'Czy na pewno chcesz usunac to mapowanie?',
|
||||
'fields' => [
|
||||
'pattern' => 'Wzorzec nazwy produktu',
|
||||
'script' => 'Skrypt generujacy',
|
||||
'output_dir' => 'Katalog wyjsciowy',
|
||||
'active' => 'Status',
|
||||
'actions' => 'Akcje',
|
||||
],
|
||||
'placeholders' => [
|
||||
'pattern' => 'np. %buteleczk%',
|
||||
'script' => 'Wybierz skrypt',
|
||||
'output_dir' => 'Domyslny z skryptu',
|
||||
],
|
||||
'status' => [
|
||||
'active' => 'Aktywne',
|
||||
'inactive' => 'Nieaktywne',
|
||||
],
|
||||
'actions' => [
|
||||
'add' => 'Dodaj',
|
||||
'edit' => 'Edytuj',
|
||||
'delete' => 'Usun',
|
||||
'save' => 'Zapisz',
|
||||
'cancel' => 'Anuluj',
|
||||
],
|
||||
'flash' => [
|
||||
'created' => 'Mapowanie zostalo dodane.',
|
||||
'updated' => 'Mapowanie zostalo zaktualizowane.',
|
||||
'deleted' => 'Mapowanie zostalo usuniete.',
|
||||
'toggled' => 'Status mapowania zostal zmieniony.',
|
||||
'validation_error' => 'Wypelnij wymagane pola.',
|
||||
'script_not_found' => 'Wybrany skrypt nie istnieje.',
|
||||
],
|
||||
],
|
||||
'company' => [
|
||||
'title' => 'Dane firmy',
|
||||
'description' => 'Adres nadawcy, dane bankowe i domyslne wymiary paczek.',
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
@use "modules/delivery-status";
|
||||
@use "modules/delivery-status-mappings";
|
||||
@use "modules/global-search";
|
||||
@use "modules/order-preview-modal";
|
||||
@use "modules/project-mappings";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -101,8 +101,23 @@ h1 {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.login-alert-placeholder {
|
||||
opacity: 0.56;
|
||||
.remember-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--c-primary, #4f6ef7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
|
||||
241
resources/scss/modules/_order-preview-modal.scss
Normal file
241
resources/scss/modules/_order-preview-modal.scss
Normal file
@@ -0,0 +1,241 @@
|
||||
.order-preview-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.order-preview-modal {
|
||||
background: var(--c-surface);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.18);
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
color: var(--c-muted);
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
color: var(--c-text);
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 20px 24px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
}
|
||||
|
||||
.order-preview-loading {
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
color: var(--c-muted);
|
||||
}
|
||||
|
||||
.order-preview-error {
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
color: var(--c-danger, #e53e3e);
|
||||
}
|
||||
|
||||
.order-preview-section {
|
||||
margin-bottom: 14px;
|
||||
|
||||
&__title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
}
|
||||
|
||||
.order-preview-kv {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 4px 14px;
|
||||
font-size: 14px;
|
||||
|
||||
dt {
|
||||
color: var(--c-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.order-preview-items {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
border-collapse: collapse;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted);
|
||||
}
|
||||
|
||||
tbody tr + tr {
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
}
|
||||
|
||||
.order-preview-item-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.order-preview-item-thumb {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--c-border);
|
||||
flex-shrink: 0;
|
||||
|
||||
&--empty {
|
||||
background: var(--c-bg, #f5f5f5);
|
||||
}
|
||||
}
|
||||
|
||||
.order-preview-item-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-preview-item-name {
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.order-preview-personalization {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--c-muted);
|
||||
line-height: 1.4;
|
||||
|
||||
&__line {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.order-preview-notes {
|
||||
font-size: 14px;
|
||||
|
||||
&__item {
|
||||
padding: 6px 0;
|
||||
|
||||
& + & {
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
}
|
||||
|
||||
&__type {
|
||||
font-size: 11px;
|
||||
color: var(--c-muted);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-field__btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--c-muted);
|
||||
padding: 0 2px;
|
||||
line-height: 1;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.15s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--c-primary, #4f6ef7);
|
||||
}
|
||||
|
||||
&.is-copied {
|
||||
color: #22c55e;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon.js-order-preview-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--c-muted);
|
||||
padding: 2px 4px;
|
||||
line-height: 1;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.15s;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--c-primary, #4f6ef7);
|
||||
}
|
||||
}
|
||||
102
resources/scss/modules/_project-mappings.scss
Normal file
102
resources/scss/modules/_project-mappings.scss
Normal file
@@ -0,0 +1,102 @@
|
||||
.pm-form {
|
||||
&__row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__field {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.pm-row {
|
||||
&--inactive {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
white-space: nowrap;
|
||||
|
||||
.btn + .btn {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
vertical-align: middle;
|
||||
margin-left: 4px;
|
||||
|
||||
&--done {
|
||||
color: #16a34a;
|
||||
background: rgba(22, 163, 74, 0.1);
|
||||
}
|
||||
|
||||
&--partial {
|
||||
color: #d97706;
|
||||
background: rgba(217, 119, 6, 0.1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&--none {
|
||||
color: #9ca3af;
|
||||
background: rgba(156, 163, 175, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.item-project-badge {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
margin-left: 6px;
|
||||
vertical-align: middle;
|
||||
|
||||
&--done {
|
||||
color: #16a34a;
|
||||
background: rgba(22, 163, 74, 0.1);
|
||||
}
|
||||
|
||||
&--pending {
|
||||
color: #9ca3af;
|
||||
background: rgba(156, 163, 175, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.pm-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,6 @@
|
||||
<div class="alert alert--danger login-alert" role="alert">
|
||||
<?= $e($errorMessage) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert--danger login-alert login-alert-placeholder" aria-hidden="true">
|
||||
<?= $e($t('auth.login.error_placeholder')) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form class="login-form" action="/login" method="post" novalidate>
|
||||
@@ -43,6 +39,11 @@
|
||||
>
|
||||
</label>
|
||||
|
||||
<label class="form-field form-field--inline remember-field">
|
||||
<input type="checkbox" name="remember" value="1" <?= !empty($oldRemember) ? 'checked' : '' ?>>
|
||||
<span class="field-label"><?= $e($t('auth.login.remember_me')) ?></span>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn--primary btn--block login-submit"><?= $e($t('actions.login')) ?></button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -84,6 +84,7 @@ $orderStatusOptions = is_array($orderStatusOptions ?? null) ? $orderStatusOption
|
||||
<option value="integration"<?= ((string) ($cond['condition_type'] ?? '')) === 'integration' ? ' selected' : '' ?>>Integracja (kanal sprzedazy)</option>
|
||||
<option value="shipment_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'shipment_status' ? ' selected' : '' ?>>Status przesylki</option>
|
||||
<option value="payment_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'payment_status' ? ' selected' : '' ?>>Status platnosci</option>
|
||||
<option value="payment_method"<?= ((string) ($cond['condition_type'] ?? '')) === 'payment_method' ? ' selected' : '' ?>>Metoda platnosci</option>
|
||||
<option value="order_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'order_status' ? ' selected' : '' ?>>Status zamowienia</option>
|
||||
<option value="days_in_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'days_in_status' ? ' selected' : '' ?>>Liczba dni w statusie</option>
|
||||
</select>
|
||||
@@ -93,6 +94,7 @@ $orderStatusOptions = is_array($orderStatusOptions ?? null) ? $orderStatusOption
|
||||
$conditionType = (string) ($cond['condition_type'] ?? 'integration');
|
||||
$selectedIds = is_array($condValue['integration_ids'] ?? null) ? $condValue['integration_ids'] : [];
|
||||
$selectedStatusKeys = is_array($condValue['status_keys'] ?? null) ? $condValue['status_keys'] : [];
|
||||
$selectedMethodKeys = is_array($condValue['method_keys'] ?? null) ? $condValue['method_keys'] : [];
|
||||
$selectedOrderStatusCodes = is_array($condValue['order_status_codes'] ?? null) ? $condValue['order_status_codes'] : [];
|
||||
?>
|
||||
<?php if ($conditionType === 'shipment_status'): ?>
|
||||
@@ -114,6 +116,15 @@ $orderStatusOptions = is_array($orderStatusOptions ?? null) ? $orderStatusOption
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php elseif ($conditionType === 'payment_method'): ?>
|
||||
<div class="checkbox-group">
|
||||
<?php foreach ($paymentMethodOptions as $methodKey => $methodLabel): ?>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="conditions[<?= $idx ?>][payment_method_keys][]" value="<?= $e((string) $methodKey) ?>"<?= in_array((string) $methodKey, $selectedMethodKeys, true) ? ' checked' : '' ?>>
|
||||
<?= $e($methodLabel) ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php elseif ($conditionType === 'order_status'): ?>
|
||||
<div class="checkbox-group">
|
||||
<?php foreach ($orderStatusOptions as $statusOption): ?>
|
||||
@@ -263,6 +274,7 @@ window.AutomationFormData = {
|
||||
receiptDuplicatePolicyLabels: <?= json_encode($receiptDuplicatePolicyLabels, JSON_UNESCAPED_UNICODE) ?>,
|
||||
shipmentStatusOptions: <?= json_encode($shipmentStatusOptions, JSON_UNESCAPED_UNICODE) ?>,
|
||||
paymentStatusOptions: <?= json_encode($paymentStatusOptions, JSON_UNESCAPED_UNICODE) ?>,
|
||||
paymentMethodOptions: <?= json_encode($paymentMethodOptions, JSON_UNESCAPED_UNICODE) ?>,
|
||||
orderStatusOptions: <?= json_encode(array_map(function($status) {
|
||||
return [
|
||||
'code' => (string) ($status['code'] ?? ''),
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'delivery-status-mappings' ? ' is-active' : '' ?>" href="/settings/delivery-status-mappings">
|
||||
Mapowanie statusów dostawy
|
||||
</a>
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'project-mappings' ? ' is-active' : '' ?>" href="/settings/project-mappings">
|
||||
<?= $e($t('navigation.project_mapping')) ?>
|
||||
</a>
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'database' ? ' is-active' : '' ?>" href="/settings/database">
|
||||
<?= $e($t('navigation.database')) ?>
|
||||
</a>
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php require __DIR__ . '/partials/preview-modal.php'; ?>
|
||||
|
||||
<script type="application/json" id="js-inline-status-config"><?= json_encode([
|
||||
'allStatuses' => is_array($allStatuses ?? null) ? $allStatuses : [],
|
||||
'statusColorMap' => is_array($statusColorMap ?? null) ? $statusColorMap : [],
|
||||
@@ -60,4 +62,79 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var overlay = document.getElementById('order-preview-overlay');
|
||||
var body = document.getElementById('order-preview-body');
|
||||
var detailsLink = document.getElementById('order-preview-details-link');
|
||||
if (!overlay || !body) return;
|
||||
|
||||
function openPreview(orderId) {
|
||||
body.innerHTML = '<div class="order-preview-loading">Ladowanie...</div>';
|
||||
detailsLink.href = '/orders/' + orderId;
|
||||
overlay.style.display = 'flex';
|
||||
|
||||
fetch('/api/orders/' + orderId + '/preview', { credentials: 'same-origin' })
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
return res.text();
|
||||
})
|
||||
.then(function (html) {
|
||||
body.innerHTML = html;
|
||||
})
|
||||
.catch(function () {
|
||||
body.innerHTML = '<div class="order-preview-loading">Nie udalo sie zaladowac podgladu.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function closePreview() {
|
||||
overlay.style.display = 'none';
|
||||
body.innerHTML = '';
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.js-order-preview-btn');
|
||||
if (btn) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var orderId = btn.getAttribute('data-order-id');
|
||||
if (orderId) openPreview(orderId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.id === 'order-preview-close' || e.target.id === 'order-preview-close-btn') {
|
||||
closePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target === overlay) {
|
||||
closePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
var copyBtn = e.target.closest('.copy-field__btn');
|
||||
if (copyBtn) {
|
||||
var value = copyBtn.getAttribute('data-copy-value') || '';
|
||||
if (value === '') return;
|
||||
navigator.clipboard.writeText(value).then(function () {
|
||||
var copyIc = copyBtn.querySelector('.copy-icon');
|
||||
var checkIc = copyBtn.querySelector('.check-icon');
|
||||
if (copyIc) copyIc.style.display = 'none';
|
||||
if (checkIc) checkIc.style.display = '';
|
||||
copyBtn.classList.add('is-copied');
|
||||
setTimeout(function () {
|
||||
if (copyIc) copyIc.style.display = '';
|
||||
if (checkIc) checkIc.style.display = 'none';
|
||||
copyBtn.classList.remove('is-copied');
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && overlay.style.display !== 'none') {
|
||||
closePreview();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
189
resources/views/orders/partials/preview-content.php
Normal file
189
resources/views/orders/partials/preview-content.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
$orderRow = is_array($order ?? null) ? $order : [];
|
||||
$itemsList = is_array($items ?? null) ? $items : [];
|
||||
$notesList = is_array($notes ?? null) ? $notes : [];
|
||||
$addrMap = is_array($addressByType ?? null) ? $addressByType : [];
|
||||
$customer = is_array($addrMap['customer'] ?? null) ? $addrMap['customer'] : [];
|
||||
$delivery = is_array($addrMap['delivery'] ?? null) ? $addrMap['delivery'] : [];
|
||||
|
||||
$orderNumber = trim((string) ($orderRow['internal_order_number'] ?? ''));
|
||||
if ($orderNumber === '') {
|
||||
$orderNumber = '#' . (string) ($orderRow['id'] ?? 0);
|
||||
}
|
||||
$externalId = trim((string) ($orderRow['external_order_id'] ?? ''));
|
||||
$buyerName = trim((string) ($customer['name'] ?? ''));
|
||||
$buyerEmail = trim((string) ($customer['email'] ?? ''));
|
||||
$buyerPhone = trim((string) ($customer['phone'] ?? ''));
|
||||
|
||||
$deliveryName = trim((string) ($delivery['name'] ?? ''));
|
||||
$deliveryStreet = trim((string) (($delivery['street_name'] ?? '') . ' ' . ($delivery['street_number'] ?? '')));
|
||||
$deliveryCity = trim((string) (($delivery['zip_code'] ?? '') . ' ' . ($delivery['city'] ?? '')));
|
||||
$deliveryCountry = trim((string) ($delivery['country'] ?? ''));
|
||||
$deliveryParcel = trim((string) ($delivery['parcel_name'] ?? ''));
|
||||
$deliveryParcelId = trim((string) ($delivery['parcel_external_id'] ?? ''));
|
||||
|
||||
$fullDeliveryAddress = implode(', ', array_filter([
|
||||
$deliveryName,
|
||||
$deliveryStreet,
|
||||
$deliveryCity,
|
||||
$deliveryCountry,
|
||||
$deliveryParcel !== '' ? $deliveryParcel : null,
|
||||
$deliveryParcelId !== '' ? $deliveryParcelId : null,
|
||||
]));
|
||||
|
||||
$totalWithTax = $orderRow['total_with_tax'] !== null ? number_format((float) $orderRow['total_with_tax'], 2, '.', ' ') : '-';
|
||||
$totalPaid = $orderRow['total_paid'] !== null ? number_format((float) $orderRow['total_paid'], 2, '.', ' ') : '-';
|
||||
$currency = trim((string) ($orderRow['currency'] ?? ''));
|
||||
|
||||
$copyIcon = '<svg class="copy-icon" xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
$checkIcon = '<svg class="check-icon" style="display:none" xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
||||
$copyBtn = static function (string $value) use ($e, $copyIcon, $checkIcon): string {
|
||||
if ($value === '') return '';
|
||||
return ' <button type="button" class="copy-field__btn" data-copy-value="' . $e($value) . '" title="Kopiuj">' . $copyIcon . $checkIcon . '</button>';
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="order-preview-section">
|
||||
<div class="order-preview-section__title"><?= $e($t('orders.preview.order_number')) ?></div>
|
||||
<div class="order-preview-kv">
|
||||
<dt>Nr:</dt>
|
||||
<dd><strong><?= $e($orderNumber) ?></strong><?= $copyBtn($orderNumber) ?></dd>
|
||||
<?php if ($externalId !== ''): ?>
|
||||
<dt>Zewn. ID:</dt>
|
||||
<dd><?= $e($externalId) ?><?= $copyBtn($externalId) ?></dd>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-preview-section">
|
||||
<div class="order-preview-section__title"><?= $e($t('orders.preview.buyer')) ?></div>
|
||||
<div class="order-preview-kv">
|
||||
<?php if ($buyerName !== ''): ?>
|
||||
<dt>Nazwa:</dt>
|
||||
<dd><?= $e($buyerName) ?><?= $copyBtn($buyerName) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($buyerEmail !== ''): ?>
|
||||
<dt>Email:</dt>
|
||||
<dd><?= $e($buyerEmail) ?><?= $copyBtn($buyerEmail) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($buyerPhone !== ''): ?>
|
||||
<dt>Telefon:</dt>
|
||||
<dd><?= $e($buyerPhone) ?><?= $copyBtn($buyerPhone) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($buyerName === '' && $buyerEmail === ''): ?>
|
||||
<dt></dt><dd class="muted">Brak danych</dd>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-preview-section">
|
||||
<div class="order-preview-section__title"><?= $e($t('orders.preview.delivery_address')) ?></div>
|
||||
<?php if ($delivery !== []): ?>
|
||||
<div class="order-preview-kv">
|
||||
<?php if ($deliveryName !== ''): ?>
|
||||
<dt>Odbiorca:</dt>
|
||||
<dd><?= $e($deliveryName) ?><?= $copyBtn($deliveryName) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($deliveryStreet !== ''): ?>
|
||||
<dt>Ulica:</dt>
|
||||
<dd><?= $e($deliveryStreet) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($deliveryCity !== ''): ?>
|
||||
<dt>Miasto:</dt>
|
||||
<dd><?= $e($deliveryCity) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($deliveryParcel !== ''): ?>
|
||||
<dt>Punkt:</dt>
|
||||
<dd><?= $e($deliveryParcel) ?><?= $deliveryParcelId !== '' ? ' (' . $e($deliveryParcelId) . ')' : '' ?></dd>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="margin-top:4px">
|
||||
<button type="button" class="copy-field__btn" data-copy-value="<?= $e($fullDeliveryAddress) ?>" title="Kopiuj caly adres" style="font-size:11px;opacity:0.7">
|
||||
<?= $copyIcon ?> Kopiuj caly adres
|
||||
</button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="muted">Brak danych</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($itemsList !== []): ?>
|
||||
<div class="order-preview-section">
|
||||
<div class="order-preview-section__title"><?= $e($t('orders.preview.products')) ?> (<?= $e((string) count($itemsList)) ?>)</div>
|
||||
<table class="order-preview-items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produkt</th>
|
||||
<th>Ilosc</th>
|
||||
<th>Cena</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($itemsList as $item): ?>
|
||||
<?php
|
||||
$qty = (float) ($item['quantity'] ?? 0);
|
||||
$price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : null;
|
||||
$media = trim((string) ($item['media_url'] ?? ''));
|
||||
$personalization = trim((string) ($item['personalization'] ?? ''));
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="order-preview-item-cell">
|
||||
<?php if ($media !== ''): ?>
|
||||
<img src="<?= $e($media) ?>" alt="" class="order-preview-item-thumb">
|
||||
<?php else: ?>
|
||||
<span class="order-preview-item-thumb order-preview-item-thumb--empty"></span>
|
||||
<?php endif; ?>
|
||||
<div class="order-preview-item-info">
|
||||
<div class="order-preview-item-name"><?= $e((string) ($item['original_name'] ?? '-')) ?></div>
|
||||
<?php if ($personalization !== ''): ?>
|
||||
<div class="order-preview-personalization">
|
||||
<?php foreach (explode("\n", $personalization) as $line): ?>
|
||||
<?php if (trim($line) !== ''): ?>
|
||||
<div class="order-preview-personalization__line"><?= $e(trim($line)) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><?= $e((string) $qty) ?></td>
|
||||
<td><?= $e($price !== null ? number_format($price, 2, '.', ' ') . ' ' . $currency : '-') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($notesList !== []): ?>
|
||||
<div class="order-preview-section">
|
||||
<div class="order-preview-section__title"><?= $e($t('orders.preview.notes')) ?></div>
|
||||
<div class="order-preview-notes">
|
||||
<?php foreach ($notesList as $note): ?>
|
||||
<div class="order-preview-notes__item">
|
||||
<div class="order-preview-notes__type"><?= $e((string) ($note['note_type'] ?? '')) ?> | <?= $e((string) ($note['created_at_external'] ?? '')) ?></div>
|
||||
<div class="order-preview-notes__text"><?= $e((string) ($note['comment'] ?? '')) ?><?= $copyBtn(trim((string) ($note['comment'] ?? ''))) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="order-preview-section">
|
||||
<div class="order-preview-section__title"><?= $e($t('orders.preview.summary')) ?></div>
|
||||
<div class="order-preview-kv">
|
||||
<dt>Razem:</dt>
|
||||
<dd><strong><?= $e($totalWithTax . ' ' . $currency) ?></strong></dd>
|
||||
<dt>Oplacono:</dt>
|
||||
<dd><?= $e($totalPaid . ' ' . $currency) ?></dd>
|
||||
<?php
|
||||
$paymentType = strtoupper(trim((string) ($orderRow['external_payment_type_id'] ?? '')));
|
||||
if ($paymentType !== ''):
|
||||
?>
|
||||
<dt>Platnosc:</dt>
|
||||
<dd><?= $e($paymentType) ?></dd>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
15
resources/views/orders/partials/preview-modal.php
Normal file
15
resources/views/orders/partials/preview-modal.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<div class="order-preview-overlay" id="order-preview-overlay" style="display:none">
|
||||
<div class="order-preview-modal">
|
||||
<div class="order-preview-modal__header">
|
||||
<h3 class="order-preview-modal__title"><?= $e($t('orders.preview.title')) ?></h3>
|
||||
<button type="button" class="order-preview-modal__close" id="order-preview-close">×</button>
|
||||
</div>
|
||||
<div class="order-preview-modal__body" id="order-preview-body">
|
||||
<div class="order-preview-loading"><?= $e($t('orders.preview.loading')) ?></div>
|
||||
</div>
|
||||
<div class="order-preview-modal__footer">
|
||||
<a href="#" class="btn btn--primary btn--sm" id="order-preview-details-link"><?= $e($t('orders.preview.full_details')) ?></a>
|
||||
<button type="button" class="btn btn--secondary btn--sm" id="order-preview-close-btn"><?= $e($t('orders.preview.close')) ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,7 +168,14 @@ foreach ($addressesList as $address) {
|
||||
<span class="order-item-thumb order-item-thumb--empty"></span>
|
||||
<?php endif; ?>
|
||||
<div>
|
||||
<div class="order-item-name"><?= $e((string) ($item['original_name'] ?? '')) ?></div>
|
||||
<div class="order-item-name"><?= $e((string) ($item['original_name'] ?? '')) ?><?php
|
||||
$projGen = (int) ($item['project_generated'] ?? 0);
|
||||
if ($projGen === 1): ?>
|
||||
<span class="item-project-badge item-project-badge--done">Projekt</span>
|
||||
<?php else: ?>
|
||||
<span class="item-project-badge item-project-badge--pending">Brak projektu</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php $personalization = trim((string) ($item['personalization'] ?? '')); ?>
|
||||
<?php if ($personalization !== ''): ?>
|
||||
<div class="item-personalization">
|
||||
|
||||
178
resources/views/settings/project-mappings.php
Normal file
178
resources/views/settings/project-mappings.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
$mappings = is_array($mappings ?? null) ? $mappings : [];
|
||||
$scripts = is_array($scripts ?? null) ? $scripts : [];
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="section-title"><?= $e($t('settings.project_mapping.title')) ?></h2>
|
||||
<p class="muted mt-4"><?= $e($t('settings.project_mapping.description')) ?></p>
|
||||
|
||||
<?php if (!empty($errorMessage)): ?>
|
||||
<div class="alert alert--danger mt-12" role="alert"><?= $e((string) $errorMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($successMessage)): ?>
|
||||
<div class="alert alert--success mt-12" role="status"><?= $e((string) $successMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-12">
|
||||
<h3 class="section-title"><?= $e($t('settings.project_mapping.add_title')) ?></h3>
|
||||
<form action="/settings/project-mappings" method="post" class="pm-form mt-8" novalidate>
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
|
||||
<div class="pm-form__row">
|
||||
<label class="form-field pm-form__field">
|
||||
<span class="field-label"><?= $e($t('settings.project_mapping.fields.pattern')) ?></span>
|
||||
<input class="form-control" type="text" name="product_name_pattern" placeholder="<?= $e($t('settings.project_mapping.placeholders.pattern')) ?>" required>
|
||||
</label>
|
||||
|
||||
<label class="form-field pm-form__field">
|
||||
<span class="field-label"><?= $e($t('settings.project_mapping.fields.script')) ?></span>
|
||||
<select class="form-control" name="script_name" required>
|
||||
<option value="">-- <?= $e($t('settings.project_mapping.placeholders.script')) ?> --</option>
|
||||
<?php foreach ($scripts as $script): ?>
|
||||
<option value="<?= $e($script) ?>"><?= $e($script) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="form-field pm-form__field">
|
||||
<span class="field-label"><?= $e($t('settings.project_mapping.fields.output_dir')) ?></span>
|
||||
<input class="form-control" type="text" name="output_dir" placeholder="<?= $e($t('settings.project_mapping.placeholders.output_dir')) ?>">
|
||||
</label>
|
||||
|
||||
<div class="pm-form__actions">
|
||||
<button type="submit" class="btn btn--action"><?= $e($t('settings.project_mapping.actions.add')) ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card mt-12">
|
||||
<h3 class="section-title"><?= $e($t('settings.project_mapping.list_title')) ?></h3>
|
||||
|
||||
<?php if (empty($mappings)): ?>
|
||||
<p class="muted mt-8"><?= $e($t('settings.project_mapping.empty')) ?></p>
|
||||
<?php else: ?>
|
||||
<table class="table mt-8">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= $e($t('settings.project_mapping.fields.pattern')) ?></th>
|
||||
<th><?= $e($t('settings.project_mapping.fields.script')) ?></th>
|
||||
<th><?= $e($t('settings.project_mapping.fields.output_dir')) ?></th>
|
||||
<th class="text-center"><?= $e($t('settings.project_mapping.fields.active')) ?></th>
|
||||
<th class="text-center"><?= $e($t('settings.project_mapping.fields.actions')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($mappings as $mapping): ?>
|
||||
<tr class="pm-row<?= !$mapping['is_active'] ? ' pm-row--inactive' : '' ?>" data-id="<?= (int) $mapping['id'] ?>">
|
||||
<td class="pm-row__pattern"><?= $e((string) $mapping['product_name_pattern']) ?></td>
|
||||
<td class="pm-row__script"><?= $e((string) $mapping['script_name']) ?></td>
|
||||
<td class="pm-row__dir"><?= $e((string) ($mapping['output_dir'] ?? '-')) ?></td>
|
||||
<td class="text-center">
|
||||
<form action="/settings/project-mappings/<?= (int) $mapping['id'] ?>/toggle" method="post" class="inline-form">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<button type="submit" class="btn btn--sm <?= $mapping['is_active'] ? 'btn--success' : 'btn--muted' ?>">
|
||||
<?= $mapping['is_active'] ? $e($t('settings.project_mapping.status.active')) : $e($t('settings.project_mapping.status.inactive')) ?>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-center pm-row__actions">
|
||||
<button type="button" class="btn btn--sm btn--outline js-pm-edit"
|
||||
data-id="<?= (int) $mapping['id'] ?>"
|
||||
data-pattern="<?= $e((string) $mapping['product_name_pattern']) ?>"
|
||||
data-script="<?= $e((string) $mapping['script_name']) ?>"
|
||||
data-output-dir="<?= $e((string) ($mapping['output_dir'] ?? '')) ?>">
|
||||
<?= $e($t('settings.project_mapping.actions.edit')) ?>
|
||||
</button>
|
||||
<button type="button" class="btn btn--sm btn--danger js-pm-delete"
|
||||
data-id="<?= (int) $mapping['id'] ?>"
|
||||
data-token="<?= $e($csrfToken ?? '') ?>">
|
||||
<?= $e($t('settings.project_mapping.actions.delete')) ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<!-- Modal edycji -->
|
||||
<div id="pm-edit-modal" class="pm-modal" style="display:none;">
|
||||
<div class="pm-modal__overlay js-pm-modal-close"></div>
|
||||
<div class="pm-modal__content card">
|
||||
<h3 class="section-title"><?= $e($t('settings.project_mapping.edit_title')) ?></h3>
|
||||
<form id="pm-edit-form" method="post" novalidate>
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
|
||||
<label class="form-field mt-8">
|
||||
<span class="field-label"><?= $e($t('settings.project_mapping.fields.pattern')) ?></span>
|
||||
<input class="form-control" type="text" name="product_name_pattern" required>
|
||||
</label>
|
||||
|
||||
<label class="form-field mt-8">
|
||||
<span class="field-label"><?= $e($t('settings.project_mapping.fields.script')) ?></span>
|
||||
<select class="form-control" name="script_name" required>
|
||||
<?php foreach ($scripts as $script): ?>
|
||||
<option value="<?= $e($script) ?>"><?= $e($script) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="form-field mt-8">
|
||||
<span class="field-label"><?= $e($t('settings.project_mapping.fields.output_dir')) ?></span>
|
||||
<input class="form-control" type="text" name="output_dir">
|
||||
</label>
|
||||
|
||||
<div class="mt-12">
|
||||
<button type="submit" class="btn btn--action"><?= $e($t('settings.project_mapping.actions.save')) ?></button>
|
||||
<button type="button" class="btn btn--outline js-pm-modal-close"><?= $e($t('settings.project_mapping.actions.cancel')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var modal = document.getElementById('pm-edit-modal');
|
||||
var form = document.getElementById('pm-edit-form');
|
||||
|
||||
document.querySelectorAll('.js-pm-edit').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var id = this.dataset.id;
|
||||
form.action = '/settings/project-mappings/' + id + '/update';
|
||||
form.querySelector('[name="product_name_pattern"]').value = this.dataset.pattern;
|
||||
form.querySelector('[name="script_name"]').value = this.dataset.script;
|
||||
form.querySelector('[name="output_dir"]').value = this.dataset.outputDir || '';
|
||||
modal.style.display = 'flex';
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.js-pm-modal-close').forEach(function(el) {
|
||||
el.addEventListener('click', function() {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.js-pm-delete').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var id = this.dataset.id;
|
||||
var token = this.dataset.token;
|
||||
if (typeof window.OrderProAlerts !== 'undefined') {
|
||||
window.OrderProAlerts.confirm('<?= $e($t('settings.project_mapping.confirm_delete')) ?>', function() {
|
||||
var f = document.createElement('form');
|
||||
f.method = 'post';
|
||||
f.action = '/settings/project-mappings/' + id + '/delete';
|
||||
var t = document.createElement('input');
|
||||
t.type = 'hidden'; t.name = '_token'; t.value = token;
|
||||
f.appendChild(t);
|
||||
document.body.appendChild(f);
|
||||
f.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -72,6 +72,8 @@ use App\Modules\Printing\PrintApiController;
|
||||
use App\Modules\Printing\PrintApiKeyRepository;
|
||||
use App\Modules\Printing\PrintJobRepository;
|
||||
use App\Modules\Settings\PrintSettingsController;
|
||||
use App\Modules\Settings\ProjectMappingController;
|
||||
use App\Modules\Settings\ProjectMappingRepository;
|
||||
use App\Modules\Info\InfoController;
|
||||
use App\Modules\Users\UsersController;
|
||||
|
||||
@@ -423,6 +425,7 @@ return static function (Application $app): void {
|
||||
$router->post('/orders/{id}/send-email', [$ordersController, 'sendEmail'], [$authMiddleware]);
|
||||
$router->post('/orders/{id}/email-preview', [$ordersController, 'emailPreview'], [$authMiddleware]);
|
||||
$router->get('/api/orders/search', [$ordersController, 'quickSearch'], [$authMiddleware]);
|
||||
$router->get('/api/orders/{id}/preview', [$ordersController, 'preview'], [$authMiddleware]);
|
||||
$router->post('/users', [$usersController, 'store'], [$authMiddleware]);
|
||||
$router->get('/settings/users', [$usersController, 'index'], [$authMiddleware]);
|
||||
$router->post('/settings/users', [$usersController, 'store'], [$authMiddleware]);
|
||||
@@ -524,6 +527,14 @@ return static function (Application $app): void {
|
||||
$shipmentProviderRegistry
|
||||
);
|
||||
$printSettingsController = new PrintSettingsController($template, $translator, $auth, $printApiKeyRepository, $printJobRepository);
|
||||
$projectMappingRepository = new ProjectMappingRepository($app->db());
|
||||
$projectMappingController = new ProjectMappingController(
|
||||
$template,
|
||||
$translator,
|
||||
$auth,
|
||||
$projectMappingRepository,
|
||||
$app->basePath()
|
||||
);
|
||||
|
||||
// Print API — session auth (from orderPRO UI)
|
||||
$router->post('/api/print/jobs', [$printApiController, 'createJob'], [$authMiddleware]);
|
||||
@@ -539,6 +550,13 @@ return static function (Application $app): void {
|
||||
$router->post('/settings/printing/keys/{id}/delete', [$printSettingsController, 'deleteKey'], [$authMiddleware]);
|
||||
$router->post('/settings/printing/jobs/delete', [$printSettingsController, 'deleteJob'], [$authMiddleware]);
|
||||
|
||||
// Project mappings
|
||||
$router->get('/settings/project-mappings', [$projectMappingController, 'index'], [$authMiddleware]);
|
||||
$router->post('/settings/project-mappings', [$projectMappingController, 'store'], [$authMiddleware]);
|
||||
$router->post('/settings/project-mappings/{id}/update', [$projectMappingController, 'update'], [$authMiddleware]);
|
||||
$router->post('/settings/project-mappings/{id}/delete', [$projectMappingController, 'delete'], [$authMiddleware]);
|
||||
$router->post('/settings/project-mappings/{id}/toggle', [$projectMappingController, 'toggleActive'], [$authMiddleware]);
|
||||
|
||||
// Shipment presets API
|
||||
$presetRepository = new ShipmentPresetRepository($app->db());
|
||||
$presetController = new ShipmentPresetController($presetRepository);
|
||||
|
||||
@@ -29,6 +29,7 @@ final class AuthController
|
||||
'title' => $this->translator->get('auth.login.title'),
|
||||
'errorMessage' => Flash::get('error'),
|
||||
'oldEmail' => (string) Flash::get('old_email', ''),
|
||||
'oldRemember' => (bool) Flash::get('old_remember', false),
|
||||
'csrfToken' => Csrf::token(),
|
||||
], 'layouts/auth');
|
||||
|
||||
@@ -38,9 +39,12 @@ final class AuthController
|
||||
public function login(Request $request): Response
|
||||
{
|
||||
$csrfToken = (string) $request->input('_token', '');
|
||||
$remember = (bool) $request->input('remember', false);
|
||||
|
||||
if (!Csrf::validate($csrfToken)) {
|
||||
Flash::set('error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
Flash::set('old_email', (string) $request->input('email', ''));
|
||||
Flash::set('old_remember', $remember);
|
||||
return Response::redirect('/login');
|
||||
}
|
||||
|
||||
@@ -50,15 +54,24 @@ final class AuthController
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || $password === '') {
|
||||
Flash::set('error', $this->translator->get('auth.errors.invalid_credentials_format'));
|
||||
Flash::set('old_email', $email);
|
||||
Flash::set('old_remember', $remember);
|
||||
return Response::redirect('/login');
|
||||
}
|
||||
|
||||
if (!$this->auth->attempt($email, $password)) {
|
||||
Flash::set('error', $this->translator->get('auth.errors.invalid_credentials'));
|
||||
Flash::set('old_email', $email);
|
||||
Flash::set('old_remember', $remember);
|
||||
return Response::redirect('/login');
|
||||
}
|
||||
|
||||
if ($remember) {
|
||||
$user = $this->auth->user();
|
||||
if ($user !== null) {
|
||||
$this->auth->createRememberToken((int) $user['id']);
|
||||
}
|
||||
}
|
||||
|
||||
return Response::redirect('/settings/users');
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ final class AuthMiddleware
|
||||
|
||||
public function __invoke(Request $request, callable $next): Response
|
||||
{
|
||||
if (!$this->auth->check()) {
|
||||
if (!$this->auth->check() && !$this->auth->loginFromRememberToken()) {
|
||||
return Response::redirect('/login');
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ use App\Modules\Users\UserRepository;
|
||||
final class AuthService
|
||||
{
|
||||
private const SESSION_USER_KEY = 'auth_user';
|
||||
private const REMEMBER_COOKIE = 'remember_token';
|
||||
private const REMEMBER_DAYS = 30;
|
||||
|
||||
public function __construct(private readonly UserRepository $users)
|
||||
{
|
||||
@@ -57,9 +59,69 @@ final class AuthService
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function createRememberToken(int $userId): void
|
||||
{
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$this->users->updateRememberToken($userId, hash('sha256', $token));
|
||||
|
||||
$secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||
setcookie(self::REMEMBER_COOKIE, $token, [
|
||||
'expires' => time() + (self::REMEMBER_DAYS * 86400),
|
||||
'path' => '/',
|
||||
'httponly' => true,
|
||||
'secure' => $secure,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
}
|
||||
|
||||
public function loginFromRememberToken(): bool
|
||||
{
|
||||
$cookieToken = $_COOKIE[self::REMEMBER_COOKIE] ?? '';
|
||||
if ($cookieToken === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokenHash = hash('sha256', $cookieToken);
|
||||
$user = $this->users->findByRememberToken($tokenHash);
|
||||
if ($user === null) {
|
||||
$this->clearRememberCookie();
|
||||
return false;
|
||||
}
|
||||
|
||||
Session::regenerate();
|
||||
|
||||
$_SESSION[self::SESSION_USER_KEY] = [
|
||||
'id' => (int) $user['id'],
|
||||
'name' => (string) $user['name'],
|
||||
'email' => (string) $user['email'],
|
||||
'login_at' => date(DATE_ATOM),
|
||||
];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
{
|
||||
$user = $this->user();
|
||||
if ($user !== null) {
|
||||
$this->users->updateRememberToken((int) $user['id'], null);
|
||||
}
|
||||
$this->clearRememberCookie();
|
||||
|
||||
unset($_SESSION[self::SESSION_USER_KEY]);
|
||||
Session::regenerate();
|
||||
}
|
||||
|
||||
private function clearRememberCookie(): void
|
||||
{
|
||||
$secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||
setcookie(self::REMEMBER_COOKIE, '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/',
|
||||
'httponly' => true,
|
||||
'secure' => $secure,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
unset($_COOKIE[self::REMEMBER_COOKIE]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ final class AutomationController
|
||||
{
|
||||
private const HISTORY_PER_PAGE = 25;
|
||||
private const ALLOWED_EVENTS = ['receipt.created', 'shipment.created', 'shipment.status_changed', 'payment.status_changed', 'order.status_changed', 'order.status_aged', 'order.imported'];
|
||||
private const ALLOWED_CONDITION_TYPES = ['integration', 'shipment_status', 'payment_status', 'order_status', 'days_in_status'];
|
||||
private const ALLOWED_CONDITION_TYPES = ['integration', 'shipment_status', 'payment_status', 'payment_method', 'order_status', 'days_in_status'];
|
||||
private const PAYMENT_STATUS_OPTIONS = [
|
||||
'0' => 'Nieopłacone',
|
||||
'1' => 'Częściowo opłacone',
|
||||
@@ -27,6 +27,12 @@ final class AutomationController
|
||||
private const ALLOWED_RECIPIENTS = ['client', 'client_and_company', 'company'];
|
||||
private const ALLOWED_RECEIPT_ISSUE_DATE_MODES = ['today', 'order_date', 'payment_date'];
|
||||
private const ALLOWED_RECEIPT_DUPLICATE_POLICIES = ['skip_if_exists', 'allow_duplicates'];
|
||||
private const PAYMENT_METHOD_OPTIONS = [
|
||||
'cod' => 'Platnosc przy odbiorze (COD)',
|
||||
'transfer' => 'Przelew bankowy',
|
||||
'online' => 'Karta / platnosc online',
|
||||
'other' => 'Inna',
|
||||
];
|
||||
private const SHIPMENT_STATUS_OPTIONS = [
|
||||
'registered' => ['label' => 'Przesylka zarejestrowana', 'statuses' => ['created', 'confirmed']],
|
||||
'ready_for_pickup' => ['label' => 'Przesylka do odbioru', 'statuses' => ['ready_for_pickup']],
|
||||
@@ -251,6 +257,7 @@ final class AutomationController
|
||||
'receiptDuplicatePolicies' => self::ALLOWED_RECEIPT_DUPLICATE_POLICIES,
|
||||
'shipmentStatusOptions' => self::SHIPMENT_STATUS_OPTIONS,
|
||||
'paymentStatusOptions' => self::PAYMENT_STATUS_OPTIONS,
|
||||
'paymentMethodOptions' => self::PAYMENT_METHOD_OPTIONS,
|
||||
'orderStatusOptions' => $this->repository->listActiveOrderStatuses(),
|
||||
'errorMessage' => $errorMessage !== '' ? $errorMessage : Flash::get('settings.automation.error', ''),
|
||||
], 'layouts/app');
|
||||
@@ -275,6 +282,8 @@ final class AutomationController
|
||||
$value = ['status_keys' => is_array($cond['shipment_status_keys'] ?? null) ? $cond['shipment_status_keys'] : []];
|
||||
} elseif ($type === 'payment_status') {
|
||||
$value = ['status_keys' => is_array($cond['payment_status_keys'] ?? null) ? $cond['payment_status_keys'] : []];
|
||||
} elseif ($type === 'payment_method') {
|
||||
$value = ['method_keys' => is_array($cond['payment_method_keys'] ?? null) ? $cond['payment_method_keys'] : []];
|
||||
} elseif ($type === 'order_status') {
|
||||
$value = ['order_status_codes' => is_array($cond['order_status_codes'] ?? null) ? $cond['order_status_codes'] : []];
|
||||
} elseif ($type === 'days_in_status') {
|
||||
@@ -441,6 +450,21 @@ final class AutomationController
|
||||
return count($statusKeys) > 0 ? ['status_keys' => array_values(array_unique($statusKeys))] : null;
|
||||
}
|
||||
|
||||
if ($type === 'payment_method') {
|
||||
$keys = $condition['payment_method_keys'] ?? [];
|
||||
if (!is_array($keys)) {
|
||||
$keys = [];
|
||||
}
|
||||
|
||||
$allowedKeys = array_keys(self::PAYMENT_METHOD_OPTIONS);
|
||||
$methodKeys = array_values(array_filter(
|
||||
array_map(static fn (mixed $key): string => trim((string) $key), $keys),
|
||||
static fn (string $key): bool => $key !== '' && in_array($key, $allowedKeys, true)
|
||||
));
|
||||
|
||||
return count($methodKeys) > 0 ? ['method_keys' => array_values(array_unique($methodKeys))] : null;
|
||||
}
|
||||
|
||||
if ($type === 'order_status') {
|
||||
$codes = $condition['order_status_codes'] ?? [];
|
||||
if (!is_array($codes)) {
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Automation;
|
||||
|
||||
use App\Core\Support\StringHelper;
|
||||
use App\Modules\Accounting\ReceiptIssueException;
|
||||
use App\Modules\Accounting\ReceiptRepository;
|
||||
use App\Modules\Accounting\ReceiptService;
|
||||
@@ -134,6 +135,9 @@ final class AutomationService
|
||||
if ($type === 'payment_status') {
|
||||
return $this->evaluatePaymentStatusCondition($value, $context);
|
||||
}
|
||||
if ($type === 'payment_method') {
|
||||
return $this->evaluatePaymentMethodCondition($value, $order);
|
||||
}
|
||||
if ($type === 'order_status') {
|
||||
return $this->evaluateOrderStatusCondition($value, $context);
|
||||
}
|
||||
@@ -216,6 +220,57 @@ final class AutomationService
|
||||
return in_array($newPaymentStatus, array_map(static fn (mixed $k): string => trim((string) $k), $statusKeys), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $value
|
||||
* @param array<string, mixed> $order
|
||||
*/
|
||||
private function evaluatePaymentMethodCondition(array $value, array $order): bool
|
||||
{
|
||||
$methodKeys = is_array($value['method_keys'] ?? null) ? $value['method_keys'] : [];
|
||||
if ($methodKeys === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$paymentType = trim((string) ($order['external_payment_type_id'] ?? ''));
|
||||
if ($paymentType === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$upperType = strtoupper($paymentType);
|
||||
|
||||
foreach ($methodKeys as $key) {
|
||||
$match = match ((string) $key) {
|
||||
'cod' => StringHelper::isCodPayment($paymentType),
|
||||
'transfer' => str_contains($upperType, 'PRZELEW')
|
||||
|| str_contains($upperType, 'TRANSFER')
|
||||
|| str_contains($upperType, 'WIRE'),
|
||||
'online' => str_contains($upperType, 'CARD')
|
||||
|| str_contains($upperType, 'ONLINE')
|
||||
|| str_contains($upperType, 'PAYU')
|
||||
|| str_contains($upperType, 'PRZELEWY24')
|
||||
|| str_contains($upperType, 'BLIK')
|
||||
|| str_contains($upperType, 'TPAY'),
|
||||
'other' => !StringHelper::isCodPayment($paymentType)
|
||||
&& !str_contains($upperType, 'PRZELEW')
|
||||
&& !str_contains($upperType, 'TRANSFER')
|
||||
&& !str_contains($upperType, 'WIRE')
|
||||
&& !str_contains($upperType, 'CARD')
|
||||
&& !str_contains($upperType, 'ONLINE')
|
||||
&& !str_contains($upperType, 'PAYU')
|
||||
&& !str_contains($upperType, 'PRZELEWY24')
|
||||
&& !str_contains($upperType, 'BLIK')
|
||||
&& !str_contains($upperType, 'TPAY'),
|
||||
default => false,
|
||||
};
|
||||
|
||||
if ($match) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $value
|
||||
* @param array<string, mixed> $context
|
||||
@@ -491,7 +546,7 @@ final class AutomationService
|
||||
|
||||
$details = $this->orders->findDetails($orderId);
|
||||
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
|
||||
$oldStatus = strtolower(trim((string) ($order['external_status_id'] ?? '')));
|
||||
$oldStatus = strtolower(trim((string) ($order['status_code'] ?? '')));
|
||||
|
||||
$actorName = 'Automatyzacja: ' . $ruleName;
|
||||
$updated = $this->orders->updateOrderStatus($orderId, $statusCode, 'system', $actorName);
|
||||
|
||||
@@ -60,7 +60,7 @@ final class OrderStatusAgedService
|
||||
}
|
||||
|
||||
try {
|
||||
$currentStatus = strtolower(trim((string) ($order['external_status_id'] ?? '')));
|
||||
$currentStatus = strtolower(trim((string) ($order['status_code'] ?? '')));
|
||||
$lastChanged = (string) ($order['last_changed'] ?? '');
|
||||
$actualDays = $lastChanged !== '' ? $this->daysSince($lastChanged) : $days;
|
||||
|
||||
@@ -129,12 +129,12 @@ final class OrderStatusAgedService
|
||||
|
||||
$placeholders = implode(', ', array_fill(0, count($statusCodes), '?'));
|
||||
|
||||
$sql = "SELECT o.id, o.external_status_id, MAX(h.changed_at) AS last_changed
|
||||
$sql = "SELECT o.id, o.status_code, MAX(h.changed_at) AS last_changed
|
||||
FROM orders o
|
||||
INNER JOIN order_status_history h ON h.order_id = o.id
|
||||
AND LOWER(h.to_status_id) = LOWER(o.external_status_id)
|
||||
WHERE LOWER(COALESCE(o.external_status_id, '')) IN ({$placeholders})
|
||||
GROUP BY o.id, o.external_status_id
|
||||
AND LOWER(h.to_status_id) = LOWER(o.status_code)
|
||||
WHERE LOWER(COALESCE(o.status_code, '')) IN ({$placeholders})
|
||||
GROUP BY o.id, o.status_code
|
||||
HAVING MAX(h.changed_at) <= DATE_SUB(NOW(), INTERVAL ? DAY)
|
||||
LIMIT " . self::MAX_ORDERS_PER_RULE;
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ final class OrderImportRepository
|
||||
$newPaymentStatus = (int) ($orderData['payment_status'] ?? 0);
|
||||
$paymentTransition = $currentStatus === 'nieoplacone' && $newPaymentStatus === 2;
|
||||
if (!$paymentTransition) {
|
||||
$orderData['external_status_id'] = $currentStatus;
|
||||
$orderData['status_code'] = $currentStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ final class OrderImportRepository
|
||||
private function getCurrentStatus(int $orderId): string
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT external_status_id FROM orders WHERE id = :id LIMIT 1'
|
||||
'SELECT status_code FROM orders WHERE id = :id LIMIT 1'
|
||||
);
|
||||
$statement->execute(['id' => $orderId]);
|
||||
$value = $statement->fetchColumn();
|
||||
@@ -120,13 +120,13 @@ final class OrderImportRepository
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO orders (
|
||||
integration_id, source, source_order_id, external_order_id, external_platform_id, external_platform_account_id,
|
||||
external_status_id, external_payment_type_id, payment_status, external_carrier_id, external_carrier_account_id,
|
||||
status_code, external_payment_type_id, payment_status, external_carrier_id, external_carrier_account_id,
|
||||
customer_login, is_invoice, is_encrypted, is_canceled_by_buyer, currency,
|
||||
total_without_tax, total_with_tax, total_paid, delivery_price, send_date_min, send_date_max, ordered_at,
|
||||
source_created_at, source_updated_at, preferences_json, payload_json, fetched_at
|
||||
) VALUES (
|
||||
:integration_id, :source, :source_order_id, :external_order_id, :external_platform_id, :external_platform_account_id,
|
||||
:external_status_id, :external_payment_type_id, :payment_status, :external_carrier_id, :external_carrier_account_id,
|
||||
:status_code, :external_payment_type_id, :payment_status, :external_carrier_id, :external_carrier_account_id,
|
||||
:customer_login, :is_invoice, :is_encrypted, :is_canceled_by_buyer, :currency,
|
||||
:total_without_tax, :total_with_tax, :total_paid, :delivery_price, :send_date_min, :send_date_max, :ordered_at,
|
||||
:source_created_at, :source_updated_at, :preferences_json, :payload_json, :fetched_at
|
||||
@@ -155,7 +155,7 @@ final class OrderImportRepository
|
||||
external_order_id = :external_order_id,
|
||||
external_platform_id = :external_platform_id,
|
||||
external_platform_account_id = :external_platform_account_id,
|
||||
external_status_id = :external_status_id,
|
||||
status_code = :status_code,
|
||||
external_payment_type_id = :external_payment_type_id,
|
||||
payment_status = :payment_status,
|
||||
external_carrier_id = :external_carrier_id,
|
||||
@@ -201,7 +201,7 @@ final class OrderImportRepository
|
||||
'external_order_id' => $orderData['external_order_id'] ?? null,
|
||||
'external_platform_id' => $orderData['external_platform_id'] ?? null,
|
||||
'external_platform_account_id' => $orderData['external_platform_account_id'] ?? null,
|
||||
'external_status_id' => $orderData['external_status_id'] ?? null,
|
||||
'status_code' => $orderData['status_code'] ?? null,
|
||||
'external_payment_type_id' => $orderData['external_payment_type_id'] ?? null,
|
||||
'payment_status' => $orderData['payment_status'] ?? null,
|
||||
'external_carrier_id' => $orderData['external_carrier_id'] ?? null,
|
||||
|
||||
@@ -71,81 +71,98 @@ final class OrdersController
|
||||
|
||||
$tableRows = array_map(fn (array $row): array => $this->toTableRow($row, $statusLabelMap, $statusColorMap), (array) ($result['items'] ?? []));
|
||||
|
||||
$tableListData = [
|
||||
'list_key' => 'orders',
|
||||
'base_path' => '/orders/list',
|
||||
'query' => $filters,
|
||||
'filters' => [
|
||||
[
|
||||
'key' => 'search',
|
||||
'label' => $this->translator->get('orders.filters.search'),
|
||||
'type' => 'text',
|
||||
'value' => $filters['search'],
|
||||
],
|
||||
[
|
||||
'key' => 'source',
|
||||
'label' => $this->translator->get('orders.filters.source'),
|
||||
'type' => 'select',
|
||||
'value' => $filters['source'],
|
||||
'options' => ['' => $this->translator->get('orders.filters.any')] + $sourceOptions,
|
||||
],
|
||||
[
|
||||
'key' => 'status',
|
||||
'label' => $this->translator->get('orders.filters.status'),
|
||||
'type' => 'select',
|
||||
'value' => $filters['status'],
|
||||
'options' => ['' => $this->translator->get('orders.filters.any')] + $statusOptions,
|
||||
],
|
||||
[
|
||||
'key' => 'payment_status',
|
||||
'label' => $this->translator->get('orders.filters.payment_status'),
|
||||
'type' => 'select',
|
||||
'value' => $filters['payment_status'],
|
||||
'options' => $this->paymentStatusFilterOptions(),
|
||||
],
|
||||
[
|
||||
'key' => 'date_from',
|
||||
'label' => $this->translator->get('orders.filters.date_from'),
|
||||
'type' => 'date',
|
||||
'value' => $filters['date_from'],
|
||||
],
|
||||
[
|
||||
'key' => 'date_to',
|
||||
'label' => $this->translator->get('orders.filters.date_to'),
|
||||
'type' => 'date',
|
||||
'value' => $filters['date_to'],
|
||||
],
|
||||
],
|
||||
'columns' => [
|
||||
['key' => 'order_ref', 'label' => $this->translator->get('orders.fields.order_ref'), 'sortable' => true, 'sort_key' => 'source_order_id', 'raw' => true],
|
||||
['key' => 'buyer', 'label' => $this->translator->get('orders.fields.buyer'), 'raw' => true],
|
||||
['key' => 'status_badges', 'label' => $this->translator->get('orders.fields.status'), 'sortable' => true, 'sort_key' => 'status_code', 'raw' => true],
|
||||
['key' => 'products', 'label' => $this->translator->get('orders.fields.products'), 'raw' => true],
|
||||
['key' => 'totals', 'label' => $this->translator->get('orders.fields.totals'), 'sortable' => true, 'sort_key' => 'total_with_tax', 'raw' => true],
|
||||
['key' => 'shipping', 'label' => $this->translator->get('orders.fields.shipping'), 'raw' => true],
|
||||
['key' => 'ordered_at', 'label' => $this->translator->get('orders.fields.ordered_at'), 'sortable' => true, 'sort_key' => 'ordered_at'],
|
||||
],
|
||||
'rows' => $tableRows,
|
||||
'pagination' => [
|
||||
'page' => (int) ($result['page'] ?? 1),
|
||||
'total_pages' => $totalPages,
|
||||
'total' => (int) ($result['total'] ?? 0),
|
||||
'per_page' => (int) ($result['per_page'] ?? 20),
|
||||
],
|
||||
'per_page_options' => [20, 50, 100],
|
||||
'selectable' => true,
|
||||
'select_name' => 'selected_ids[]',
|
||||
'select_value_key' => 'id',
|
||||
'header_actions' => [],
|
||||
'empty_message' => $this->translator->get('orders.empty'),
|
||||
'show_actions' => false,
|
||||
];
|
||||
|
||||
if ($request->header('X-Requested-With') === 'XMLHttpRequest') {
|
||||
$tableHtml = $this->template->render('components/table-list', [
|
||||
'tableList' => $tableListData,
|
||||
]);
|
||||
$panelHtml = $this->template->render('components/order-status-panel', [
|
||||
'statusPanelList' => $statusPanel,
|
||||
'statusPanelTitle' => 'Statusy',
|
||||
]);
|
||||
|
||||
return Response::json([
|
||||
'tableHtml' => $tableHtml,
|
||||
'panelHtml' => $panelHtml,
|
||||
]);
|
||||
}
|
||||
|
||||
$html = $this->template->render('orders/list', [
|
||||
'title' => $this->translator->get('orders.title'),
|
||||
'activeMenu' => 'orders',
|
||||
'activeOrders' => 'list',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'tableList' => [
|
||||
'list_key' => 'orders',
|
||||
'base_path' => '/orders/list',
|
||||
'query' => $filters,
|
||||
'filters' => [
|
||||
[
|
||||
'key' => 'search',
|
||||
'label' => $this->translator->get('orders.filters.search'),
|
||||
'type' => 'text',
|
||||
'value' => $filters['search'],
|
||||
],
|
||||
[
|
||||
'key' => 'source',
|
||||
'label' => $this->translator->get('orders.filters.source'),
|
||||
'type' => 'select',
|
||||
'value' => $filters['source'],
|
||||
'options' => ['' => $this->translator->get('orders.filters.any')] + $sourceOptions,
|
||||
],
|
||||
[
|
||||
'key' => 'status',
|
||||
'label' => $this->translator->get('orders.filters.status'),
|
||||
'type' => 'select',
|
||||
'value' => $filters['status'],
|
||||
'options' => ['' => $this->translator->get('orders.filters.any')] + $statusOptions,
|
||||
],
|
||||
[
|
||||
'key' => 'payment_status',
|
||||
'label' => $this->translator->get('orders.filters.payment_status'),
|
||||
'type' => 'select',
|
||||
'value' => $filters['payment_status'],
|
||||
'options' => $this->paymentStatusFilterOptions(),
|
||||
],
|
||||
[
|
||||
'key' => 'date_from',
|
||||
'label' => $this->translator->get('orders.filters.date_from'),
|
||||
'type' => 'date',
|
||||
'value' => $filters['date_from'],
|
||||
],
|
||||
[
|
||||
'key' => 'date_to',
|
||||
'label' => $this->translator->get('orders.filters.date_to'),
|
||||
'type' => 'date',
|
||||
'value' => $filters['date_to'],
|
||||
],
|
||||
],
|
||||
'columns' => [
|
||||
['key' => 'order_ref', 'label' => $this->translator->get('orders.fields.order_ref'), 'sortable' => true, 'sort_key' => 'source_order_id', 'raw' => true],
|
||||
['key' => 'buyer', 'label' => $this->translator->get('orders.fields.buyer'), 'raw' => true],
|
||||
['key' => 'status_badges', 'label' => $this->translator->get('orders.fields.status'), 'sortable' => true, 'sort_key' => 'external_status_id', 'raw' => true],
|
||||
['key' => 'products', 'label' => $this->translator->get('orders.fields.products'), 'raw' => true],
|
||||
['key' => 'totals', 'label' => $this->translator->get('orders.fields.totals'), 'sortable' => true, 'sort_key' => 'total_with_tax', 'raw' => true],
|
||||
['key' => 'shipping', 'label' => $this->translator->get('orders.fields.shipping'), 'raw' => true],
|
||||
['key' => 'ordered_at', 'label' => $this->translator->get('orders.fields.ordered_at'), 'sortable' => true, 'sort_key' => 'ordered_at'],
|
||||
],
|
||||
'rows' => $tableRows,
|
||||
'pagination' => [
|
||||
'page' => (int) ($result['page'] ?? 1),
|
||||
'total_pages' => $totalPages,
|
||||
'total' => (int) ($result['total'] ?? 0),
|
||||
'per_page' => (int) ($result['per_page'] ?? 20),
|
||||
],
|
||||
'per_page_options' => [20, 50, 100],
|
||||
'selectable' => true,
|
||||
'select_name' => 'selected_ids[]',
|
||||
'select_value_key' => 'id',
|
||||
'header_actions' => [],
|
||||
'empty_message' => $this->translator->get('orders.empty'),
|
||||
'show_actions' => false,
|
||||
],
|
||||
'tableList' => $tableListData,
|
||||
'stats' => $stats,
|
||||
'statusPanel' => $statusPanel,
|
||||
'allStatuses' => $this->buildAllStatusOptions($statusConfig),
|
||||
@@ -173,7 +190,7 @@ final class OrdersController
|
||||
$notes = is_array($details['notes'] ?? null) ? $details['notes'] : [];
|
||||
$history = is_array($details['status_history'] ?? null) ? $details['status_history'] : [];
|
||||
$activityLog = is_array($details['activity_log'] ?? null) ? $details['activity_log'] : [];
|
||||
$statusCode = (string) (($order['effective_status_id'] ?? '') !== '' ? $order['effective_status_id'] : ($order['external_status_id'] ?? ''));
|
||||
$statusCode = (string) (($order['effective_status_id'] ?? '') !== '' ? $order['effective_status_id'] : ($order['status_code'] ?? ''));
|
||||
$statusCounts = $this->orders->statusCounts();
|
||||
$statusConfig = $this->orders->statusPanelConfig();
|
||||
$statusLabelMap = $this->statusLabelMap($statusConfig);
|
||||
@@ -280,7 +297,7 @@ final class OrdersController
|
||||
|
||||
$oldDetails = $this->orders->findDetails($orderId);
|
||||
$oldOrder = is_array($oldDetails['order'] ?? null) ? $oldDetails['order'] : [];
|
||||
$oldStatus = strtolower(trim((string) ($oldOrder['external_status_id'] ?? '')));
|
||||
$oldStatus = strtolower(trim((string) ($oldOrder['status_code'] ?? '')));
|
||||
|
||||
$success = $this->orders->updateOrderStatus($orderId, $newStatus, 'user', $actorName !== '' ? $actorName : null);
|
||||
|
||||
@@ -336,7 +353,7 @@ final class OrdersController
|
||||
$buyerName = trim((string) ($row['buyer_name'] ?? ''));
|
||||
$buyerEmail = trim((string) ($row['buyer_email'] ?? ''));
|
||||
$buyerCity = trim((string) ($row['buyer_city'] ?? ''));
|
||||
$status = trim((string) (($row['effective_status_id'] ?? '') !== '' ? $row['effective_status_id'] : ($row['external_status_id'] ?? '')));
|
||||
$status = trim((string) (($row['effective_status_id'] ?? '') !== '' ? $row['effective_status_id'] : ($row['status_code'] ?? '')));
|
||||
$currency = trim((string) ($row['currency'] ?? ''));
|
||||
$totalWithTax = $row['total_with_tax'] !== null ? number_format((float) $row['total_with_tax'], 2, '.', ' ') : '-';
|
||||
$totalPaid = $row['total_paid'] !== null ? number_format((float) $row['total_paid'], 2, '.', ' ') : '-';
|
||||
@@ -349,11 +366,17 @@ final class OrdersController
|
||||
$shipments = max(0, (int) ($row['shipments_count'] ?? 0));
|
||||
$documents = max(0, (int) ($row['documents_count'] ?? 0));
|
||||
$itemsPreview = is_array($row['items_preview'] ?? null) ? $row['items_preview'] : [];
|
||||
$projectsDone = max(0, (int) ($row['projects_done'] ?? 0));
|
||||
$projectsTotal = max(0, (int) ($row['projects_total'] ?? 0));
|
||||
|
||||
$previewBtn = '<button type="button" class="btn-icon js-order-preview-btn" data-order-id="' . (int) ($row['id'] ?? 0) . '" title="Podglad">'
|
||||
. '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'
|
||||
. '</button>';
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'order_ref' => '<div class="orders-ref">'
|
||||
. '<div class="orders-ref__main"><a href="/orders/' . (int) ($row['id'] ?? 0) . '">'
|
||||
. '<div class="orders-ref__main">' . $previewBtn . '<a href="/orders/' . (int) ($row['id'] ?? 0) . '">'
|
||||
. htmlspecialchars($internalOrderNumber !== '' ? $internalOrderNumber : ('#' . (string) ($row['id'] ?? 0)), ENT_QUOTES, 'UTF-8')
|
||||
. '</a></div>'
|
||||
. '<div class="orders-ref__meta">'
|
||||
@@ -371,7 +394,7 @@ final class OrdersController
|
||||
'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),
|
||||
'products' => $this->productsHtml($itemsPreview, $itemsCount, $itemsQty, $projectsDone, $projectsTotal),
|
||||
'totals' => '<div class="orders-money">'
|
||||
. '<div class="orders-money__main">' . htmlspecialchars($totalWithTax . ' ' . $currency, ENT_QUOTES, 'UTF-8') . ($isUnpaid ? ' <span class="order-tag is-unpaid">Nieopłacone</span>' : '') . '</div>'
|
||||
. '<div class="orders-money__meta">' . ($isCod ? '<span class="order-tag is-cod">Za pobraniem</span>' : 'oplacono: ' . htmlspecialchars($totalPaid . ' ' . $currency, ENT_QUOTES, 'UTF-8')) . '</div>'
|
||||
@@ -671,7 +694,7 @@ final class OrdersController
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $itemsPreview
|
||||
*/
|
||||
private function productsHtml(array $itemsPreview, int $itemsCount, string $itemsQty): string
|
||||
private function productsHtml(array $itemsPreview, int $itemsCount, string $itemsQty, int $projectsDone = 0, int $projectsTotal = 0): string
|
||||
{
|
||||
if ($itemsPreview === []) {
|
||||
return '<div class="orders-products">'
|
||||
@@ -704,12 +727,37 @@ final class OrdersController
|
||||
if ($itemsCount > count($itemsPreview)) {
|
||||
$html .= '<div class="orders-products__more">+' . ($itemsCount - count($itemsPreview)) . ' pozycji</div>';
|
||||
}
|
||||
$html .= '<div class="orders-products__meta">' . $itemsCount . ' pozycji / ' . htmlspecialchars($itemsQty, ENT_QUOTES, 'UTF-8') . ' szt.</div>';
|
||||
$html .= '<div class="orders-products__meta">' . $itemsCount . ' pozycji / ' . htmlspecialchars($itemsQty, ENT_QUOTES, 'UTF-8') . ' szt.'
|
||||
. $this->projectBadge($projectsDone, $projectsTotal)
|
||||
. '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function projectBadge(int $done, int $total): string
|
||||
{
|
||||
if ($total === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($done === $total) {
|
||||
return ' <span class="project-badge project-badge--done" title="Wszystkie projekty wygenerowane (' . $done . '/' . $total . ')">'
|
||||
. '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>'
|
||||
. '</span>';
|
||||
}
|
||||
|
||||
if ($done > 0) {
|
||||
return ' <span class="project-badge project-badge--partial" title="Projekty: ' . $done . '/' . $total . '">'
|
||||
. $done . '/' . $total
|
||||
. '</span>';
|
||||
}
|
||||
|
||||
return ' <span class="project-badge project-badge--none" title="Brak wygenerowanych projektow (0/' . $total . ')">'
|
||||
. '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>'
|
||||
. '</span>';
|
||||
}
|
||||
|
||||
private function shippingHtml(string $deliveryMethod, int $shipments, int $documents): string
|
||||
{
|
||||
$deliveryMethod = trim(html_entity_decode(strip_tags($deliveryMethod), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
@@ -961,4 +1009,35 @@ final class OrdersController
|
||||
return Response::json(['results' => $results]);
|
||||
}
|
||||
|
||||
public function preview(Request $request): Response
|
||||
{
|
||||
$orderId = max(0, (int) $request->input('id', 0));
|
||||
$details = $this->orders->findDetails($orderId);
|
||||
if ($details === null) {
|
||||
return Response::html('<div class="order-preview-error">Zamowienie nie znalezione.</div>', 404);
|
||||
}
|
||||
|
||||
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
|
||||
$items = is_array($details['items'] ?? null) ? $details['items'] : [];
|
||||
$addresses = is_array($details['addresses'] ?? null) ? $details['addresses'] : [];
|
||||
$notes = is_array($details['notes'] ?? null) ? $details['notes'] : [];
|
||||
|
||||
$addressByType = ['customer' => null, 'delivery' => null, 'invoice' => null];
|
||||
foreach ($addresses as $address) {
|
||||
$type = (string) ($address['address_type'] ?? '');
|
||||
if ($type !== '' && array_key_exists($type, $addressByType) && $addressByType[$type] === null) {
|
||||
$addressByType[$type] = $address;
|
||||
}
|
||||
}
|
||||
|
||||
$html = $this->template->render('orders/partials/preview-content', [
|
||||
'order' => $order,
|
||||
'items' => $items,
|
||||
'addressByType' => $addressByType,
|
||||
'notes' => $notes,
|
||||
]);
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ final class OrdersRepository
|
||||
$sortColumn = match ($sort) {
|
||||
'source_order_id' => 'o.source_order_id',
|
||||
'external_order_id' => 'o.external_order_id',
|
||||
'external_status_id' => 'o.external_status_id',
|
||||
'status_code' => 'o.status_code',
|
||||
'payment_status' => 'o.payment_status',
|
||||
'total_with_tax' => 'o.total_with_tax',
|
||||
'total_paid' => 'o.total_paid',
|
||||
@@ -48,7 +48,7 @@ final class OrdersRepository
|
||||
try {
|
||||
$countSql = 'SELECT COUNT(*) FROM orders o '
|
||||
. 'LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer" '
|
||||
. 'LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code'
|
||||
. 'LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.status_code) = asm.allegro_status_code'
|
||||
. $whereSql;
|
||||
$countStmt = $this->pdo->prepare($countSql);
|
||||
$countStmt->execute($params);
|
||||
@@ -158,7 +158,7 @@ final class OrdersRepository
|
||||
o.source,
|
||||
o.source_order_id,
|
||||
o.external_order_id,
|
||||
o.external_status_id,
|
||||
o.status_code,
|
||||
' . $effectiveStatusSql . ' AS effective_status_id,
|
||||
o.payment_status,
|
||||
o.currency,
|
||||
@@ -178,15 +178,19 @@ final class OrdersRepository
|
||||
o.external_payment_type_id,
|
||||
COALESCE(oi_agg.items_count, 0) AS items_count,
|
||||
COALESCE(oi_agg.items_qty, 0) AS items_qty,
|
||||
COALESCE(oi_agg.projects_done, 0) AS projects_done,
|
||||
COALESCE(oi_agg.projects_total, 0) AS projects_total,
|
||||
COALESCE(sh_agg.shipments_count, 0) AS shipments_count,
|
||||
COALESCE(od_agg.documents_count, 0) AS documents_count,
|
||||
ig.name AS integration_name
|
||||
FROM orders o
|
||||
LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer"
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.status_code) = asm.allegro_status_code
|
||||
LEFT JOIN integrations ig ON ig.id = o.integration_id
|
||||
LEFT JOIN (
|
||||
SELECT order_id, COUNT(*) AS items_count, COALESCE(SUM(quantity), 0) AS items_qty
|
||||
SELECT order_id, COUNT(*) AS items_count, COALESCE(SUM(quantity), 0) AS items_qty,
|
||||
SUM(CASE WHEN project_generated = 1 THEN 1 ELSE 0 END) AS projects_done,
|
||||
COUNT(*) AS projects_total
|
||||
FROM order_items GROUP BY order_id
|
||||
) oi_agg ON oi_agg.order_id = o.id
|
||||
LEFT JOIN (
|
||||
@@ -217,7 +221,7 @@ final class OrdersRepository
|
||||
'source' => (string) ($row['source'] ?? ''),
|
||||
'source_order_id' => (string) ($row['source_order_id'] ?? ''),
|
||||
'external_order_id' => (string) ($row['external_order_id'] ?? ''),
|
||||
'external_status_id' => (string) ($row['external_status_id'] ?? ''),
|
||||
'status_code' => (string) ($row['status_code'] ?? ''),
|
||||
'effective_status_id' => (string) ($row['effective_status_id'] ?? ''),
|
||||
'payment_status' => isset($row['payment_status']) ? (int) $row['payment_status'] : null,
|
||||
'currency' => (string) ($row['currency'] ?? ''),
|
||||
@@ -240,6 +244,8 @@ final class OrdersRepository
|
||||
'documents_count' => (int) ($row['documents_count'] ?? 0),
|
||||
'integration_name' => (string) ($row['integration_name'] ?? ''),
|
||||
'items_preview' => (array) ($itemPreviewsByOrderId[$orderId] ?? []),
|
||||
'projects_done' => (int) ($row['projects_done'] ?? 0),
|
||||
'projects_total' => (int) ($row['projects_total'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -253,7 +259,7 @@ final class OrdersRepository
|
||||
$rows = $this->pdo->query(
|
||||
'SELECT DISTINCT ' . $effectiveStatusSql . ' AS effective_status_id
|
||||
FROM orders o
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.status_code) = asm.allegro_status_code
|
||||
WHERE ' . $effectiveStatusSql . ' IS NOT NULL
|
||||
AND ' . $effectiveStatusSql . ' <> ""
|
||||
ORDER BY effective_status_id ASC'
|
||||
@@ -317,7 +323,7 @@ final class OrdersRepository
|
||||
SUM(CASE WHEN payment_status = 2 THEN 1 ELSE 0 END) AS paid_count,
|
||||
SUM(CASE WHEN ' . $effectiveStatusSql . ' IN ("shipped", "delivered", "returned") THEN 1 ELSE 0 END) AS shipped_count
|
||||
FROM orders o
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code')->fetch(PDO::FETCH_ASSOC);
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.status_code) = asm.allegro_status_code')->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable) {
|
||||
return [
|
||||
'all' => 0,
|
||||
@@ -351,7 +357,7 @@ final class OrdersRepository
|
||||
$rows = $this->pdo->query(
|
||||
'SELECT ' . $effectiveStatusSql . ' AS effective_status_id, COUNT(*) AS cnt
|
||||
FROM orders o
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.status_code) = asm.allegro_status_code
|
||||
GROUP BY effective_status_id'
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable) {
|
||||
@@ -475,7 +481,7 @@ final class OrdersRepository
|
||||
'SELECT o.*, ' . $effectiveStatusSql . ' AS effective_status_id,
|
||||
ig.name AS integration_name
|
||||
FROM orders o
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.status_code) = asm.allegro_status_code
|
||||
LEFT JOIN integrations ig ON ig.id = o.integration_id
|
||||
WHERE o.id = :id
|
||||
LIMIT 1'
|
||||
@@ -670,7 +676,7 @@ final class OrdersRepository
|
||||
AND ' . $mappingAlias . '.orderpro_status_code IS NOT NULL
|
||||
AND ' . $mappingAlias . '.orderpro_status_code <> ""
|
||||
THEN ' . $mappingAlias . '.orderpro_status_code
|
||||
ELSE ' . $orderAlias . '.external_status_id
|
||||
ELSE ' . $orderAlias . '.status_code
|
||||
END';
|
||||
}
|
||||
|
||||
@@ -981,16 +987,16 @@ final class OrdersRepository
|
||||
public function updateOrderStatus(int $orderId, string $newStatusCode, string $actorType = 'user', ?string $actorName = null): bool
|
||||
{
|
||||
try {
|
||||
$stmt = $this->pdo->prepare('SELECT external_status_id FROM orders WHERE id = :id LIMIT 1');
|
||||
$stmt = $this->pdo->prepare('SELECT status_code FROM orders WHERE id = :id LIMIT 1');
|
||||
$stmt->execute(['id' => $orderId]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!is_array($row)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldStatus = trim((string) ($row['external_status_id'] ?? ''));
|
||||
$oldStatus = trim((string) ($row['status_code'] ?? ''));
|
||||
|
||||
$update = $this->pdo->prepare('UPDATE orders SET external_status_id = :status, updated_at = NOW() WHERE id = :id');
|
||||
$update = $this->pdo->prepare('UPDATE orders SET status_code = :status, updated_at = NOW() WHERE id = :id');
|
||||
$update->execute(['status' => $newStatusCode, 'id' => $orderId]);
|
||||
|
||||
$this->recordStatusChange(
|
||||
|
||||
@@ -209,7 +209,7 @@ final class AllegroOrderImportService
|
||||
'external_order_id' => $checkoutFormId,
|
||||
'external_platform_id' => trim((string) ($payload['marketplace']['id'] ?? 'allegro-pl')),
|
||||
'external_platform_account_id' => null,
|
||||
'external_status_id' => $externalStatus,
|
||||
'status_code' => $externalStatus,
|
||||
'external_payment_type_id' => trim((string) ($payment['type'] ?? '')),
|
||||
'payment_status' => $mappedPaymentStatus,
|
||||
'external_carrier_id' => $deliveryForm !== '' ? $deliveryForm : null,
|
||||
|
||||
@@ -241,10 +241,10 @@ final class AllegroStatusSyncService
|
||||
|
||||
try {
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, source_order_id, external_status_id
|
||||
'SELECT id, source_order_id, status_code
|
||||
FROM orders
|
||||
WHERE source = ?
|
||||
AND LOWER(COALESCE(external_status_id, "")) NOT IN (' . $placeholders . ')
|
||||
AND LOWER(COALESCE(status_code, "")) NOT IN (' . $placeholders . ')
|
||||
AND (last_status_checked_at IS NULL OR source_updated_at > last_status_checked_at)
|
||||
ORDER BY source_updated_at DESC
|
||||
LIMIT ' . self::MAX_ORDERS_PER_RUN
|
||||
@@ -273,7 +273,7 @@ final class AllegroStatusSyncService
|
||||
'SELECT
|
||||
o.id,
|
||||
o.source_order_id,
|
||||
o.external_status_id AS orderpro_status_code,
|
||||
o.status_code AS orderpro_status_code,
|
||||
MAX(h.changed_at) AS latest_change
|
||||
FROM order_status_history h
|
||||
INNER JOIN orders o ON o.id = h.order_id
|
||||
@@ -281,7 +281,7 @@ final class AllegroStatusSyncService
|
||||
AND o.integration_id = :integration_id
|
||||
AND h.change_source = :change_source
|
||||
AND h.changed_at > :since_date
|
||||
GROUP BY o.id, o.source_order_id, o.external_status_id
|
||||
GROUP BY o.id, o.source_order_id, o.status_code
|
||||
ORDER BY latest_change ASC
|
||||
LIMIT ' . self::MAX_ORDERS_PER_RUN
|
||||
);
|
||||
|
||||
155
src/Modules/Settings/ProjectMappingController.php
Normal file
155
src/Modules/Settings/ProjectMappingController.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use App\Core\View\Template;
|
||||
use App\Core\I18n\Translator;
|
||||
use App\Modules\Auth\AuthService;
|
||||
|
||||
final class ProjectMappingController
|
||||
{
|
||||
private const SCRIPTS_DIR = 'tools/generowanie';
|
||||
|
||||
public function __construct(
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly ProjectMappingRepository $repository,
|
||||
private readonly string $basePath
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$mappings = $this->repository->getAll();
|
||||
$scripts = $this->scanScripts();
|
||||
|
||||
$html = $this->template->render('settings/project-mappings', [
|
||||
'title' => $this->translator->get('settings.project_mapping.title'),
|
||||
'activeMenu' => 'settings',
|
||||
'activeSettings' => 'project-mappings',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'mappings' => $mappings,
|
||||
'scripts' => $scripts,
|
||||
'errorMessage' => (string) Flash::get('settings_error', ''),
|
||||
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
public function store(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$pattern = trim((string) $request->input('product_name_pattern', ''));
|
||||
$scriptName = trim((string) $request->input('script_name', ''));
|
||||
$outputDir = trim((string) $request->input('output_dir', ''));
|
||||
|
||||
if ($pattern === '' || $scriptName === '') {
|
||||
Flash::set('settings_error', $this->translator->get('settings.project_mapping.flash.validation_error'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$scriptPath = $this->basePath . '/' . self::SCRIPTS_DIR . '/' . $scriptName;
|
||||
if (!file_exists($scriptPath)) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.project_mapping.flash.script_not_found'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$this->repository->create([
|
||||
'product_name_pattern' => $pattern,
|
||||
'script_name' => $scriptName,
|
||||
'output_dir' => $outputDir !== '' ? $outputDir : null,
|
||||
]);
|
||||
|
||||
Flash::set('settings_success', $this->translator->get('settings.project_mapping.flash.created'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$id = (int) $request->param('id', '0');
|
||||
$pattern = trim((string) $request->input('product_name_pattern', ''));
|
||||
$scriptName = trim((string) $request->input('script_name', ''));
|
||||
$outputDir = trim((string) $request->input('output_dir', ''));
|
||||
|
||||
if ($id <= 0 || $pattern === '' || $scriptName === '') {
|
||||
Flash::set('settings_error', $this->translator->get('settings.project_mapping.flash.validation_error'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$this->repository->update($id, [
|
||||
'product_name_pattern' => $pattern,
|
||||
'script_name' => $scriptName,
|
||||
'output_dir' => $outputDir !== '' ? $outputDir : null,
|
||||
]);
|
||||
|
||||
Flash::set('settings_success', $this->translator->get('settings.project_mapping.flash.updated'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$id = (int) $request->param('id', '0');
|
||||
if ($id > 0) {
|
||||
$this->repository->delete($id);
|
||||
Flash::set('settings_success', $this->translator->get('settings.project_mapping.flash.deleted'));
|
||||
}
|
||||
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
public function toggleActive(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$id = (int) $request->param('id', '0');
|
||||
if ($id > 0) {
|
||||
$this->repository->toggleActive($id);
|
||||
Flash::set('settings_success', $this->translator->get('settings.project_mapping.flash.toggled'));
|
||||
}
|
||||
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
private function scanScripts(): array
|
||||
{
|
||||
$dir = $this->basePath . '/' . self::SCRIPTS_DIR;
|
||||
if (!is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = glob($dir . '/*.py');
|
||||
$scripts = [];
|
||||
foreach ($files as $file) {
|
||||
$scripts[] = basename($file);
|
||||
}
|
||||
sort($scripts);
|
||||
|
||||
return $scripts;
|
||||
}
|
||||
}
|
||||
88
src/Modules/Settings/ProjectMappingRepository.php
Normal file
88
src/Modules/Settings/ProjectMappingRepository.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use PDO;
|
||||
|
||||
final class ProjectMappingRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PDO $pdo
|
||||
) {
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT * FROM project_mappings ORDER BY id DESC'
|
||||
);
|
||||
$statement->execute();
|
||||
|
||||
return $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
public function getById(int $id): ?array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT * FROM project_mappings WHERE id = :id LIMIT 1'
|
||||
);
|
||||
$statement->execute([':id' => $id]);
|
||||
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
public function create(array $data): int
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO project_mappings (product_name_pattern, script_name, output_dir, is_active)
|
||||
VALUES (:product_name_pattern, :script_name, :output_dir, :is_active)'
|
||||
);
|
||||
$statement->execute([
|
||||
':product_name_pattern' => $data['product_name_pattern'],
|
||||
':script_name' => $data['script_name'],
|
||||
':output_dir' => $data['output_dir'] ?? null,
|
||||
':is_active' => (int) ($data['is_active'] ?? 1),
|
||||
]);
|
||||
|
||||
return (int) $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
public function update(int $id, array $data): bool
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE project_mappings
|
||||
SET product_name_pattern = :product_name_pattern,
|
||||
script_name = :script_name,
|
||||
output_dir = :output_dir
|
||||
WHERE id = :id'
|
||||
);
|
||||
|
||||
return $statement->execute([
|
||||
':product_name_pattern' => $data['product_name_pattern'],
|
||||
':script_name' => $data['script_name'],
|
||||
':output_dir' => $data['output_dir'] ?? null,
|
||||
':id' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'DELETE FROM project_mappings WHERE id = :id'
|
||||
);
|
||||
|
||||
return $statement->execute([':id' => $id]);
|
||||
}
|
||||
|
||||
public function toggleActive(int $id): bool
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE project_mappings SET is_active = 1 - is_active WHERE id = :id'
|
||||
);
|
||||
|
||||
return $statement->execute([':id' => $id]);
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ final class ShopproOrderMapper
|
||||
'external_order_id' => $sourceOrderId,
|
||||
'external_platform_id' => IntegrationSources::SHOPPRO,
|
||||
'external_platform_account_id' => null,
|
||||
'external_status_id' => $effectiveStatus,
|
||||
'status_code' => $effectiveStatus,
|
||||
'external_payment_type_id' => $this->normalizeCodPaymentType((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])),
|
||||
'payment_status' => $this->mapPaymentStatus($payload, $isPaid),
|
||||
'external_carrier_id' => StringHelper::nullableString($deliveryLabel),
|
||||
|
||||
@@ -164,9 +164,9 @@ final class ShopproPaymentStatusSyncService
|
||||
}
|
||||
|
||||
if ($watchedStatuses !== []) {
|
||||
$where[] = 'LOWER(COALESCE(external_status_id, "")) IN (' . implode(', ', $statusPlaceholders) . ')';
|
||||
$where[] = 'LOWER(COALESCE(status_code, "")) IN (' . implode(', ', $statusPlaceholders) . ')';
|
||||
} else {
|
||||
$where[] = 'LOWER(COALESCE(external_status_id, "")) NOT IN (' . implode(', ', $statusPlaceholders) . ')';
|
||||
$where[] = 'LOWER(COALESCE(status_code, "")) NOT IN (' . implode(', ', $statusPlaceholders) . ')';
|
||||
}
|
||||
|
||||
$sql = 'SELECT id, source_order_id, payment_status, total_paid, total_with_tax, currency, external_payment_type_id
|
||||
|
||||
@@ -137,7 +137,7 @@ final class ShopproStatusSyncService
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$sourceOrderId = (int) ($order['source_order_id'] ?? 0);
|
||||
$orderproStatus = strtolower(trim((string) ($order['external_status_id'] ?? '')));
|
||||
$orderproStatus = strtolower(trim((string) ($order['status_code'] ?? '')));
|
||||
$changeAt = (string) ($order['latest_change'] ?? '');
|
||||
|
||||
if ($sourceOrderId <= 0 || $orderproStatus === '') {
|
||||
@@ -224,7 +224,7 @@ final class ShopproStatusSyncService
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{order_id:int,source_order_id:string,external_status_id:string,latest_change:string}>
|
||||
* @return array<int, array{order_id:int,source_order_id:string,status_code:string,latest_change:string}>
|
||||
*/
|
||||
private function findOrdersWithManualStatusChanges(int $integrationId, ?string $lastPushedAt): array
|
||||
{
|
||||
@@ -233,14 +233,14 @@ final class ShopproStatusSyncService
|
||||
|
||||
try {
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT o.id AS order_id, o.source_order_id, o.external_status_id,
|
||||
'SELECT o.id AS order_id, o.source_order_id, o.status_code,
|
||||
MAX(h.changed_at) AS latest_change
|
||||
FROM order_status_history h
|
||||
JOIN orders o ON o.id = h.order_id
|
||||
WHERE o.integration_id = :integration_id
|
||||
AND h.change_source = :change_source
|
||||
AND h.changed_at > :since_date
|
||||
GROUP BY o.id, o.source_order_id, o.external_status_id
|
||||
GROUP BY o.id, o.source_order_id, o.status_code
|
||||
ORDER BY latest_change ASC
|
||||
LIMIT 50'
|
||||
);
|
||||
|
||||
@@ -139,6 +139,43 @@ final class UserRepository
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function updateRememberToken(int $userId, ?string $tokenHash): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE users SET remember_token = :token WHERE id = :id'
|
||||
);
|
||||
$statement->execute([
|
||||
'token' => $tokenHash,
|
||||
'id' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findByRememberToken(string $tokenHash): ?array
|
||||
{
|
||||
if ($tokenHash === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, name, email FROM users WHERE remember_token = :token LIMIT 1'
|
||||
);
|
||||
$statement->execute(['token' => $tokenHash]);
|
||||
|
||||
$row = $statement->fetch();
|
||||
if (!is_array($row)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'name' => (string) ($row['name'] ?? ''),
|
||||
'email' => (string) ($row['email'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
public function create(string $name, string $email, string $passwordHash): array
|
||||
{
|
||||
$createdAt = date('Y-m-d H:i:s');
|
||||
|
||||
146
tools/generowanie/buteleczki_wzor1.py
Normal file
146
tools/generowanie/buteleczki_wzor1.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Generator PSD — Podziękowanie dla gości weselnych (buteleczka, Wzór 1).
|
||||
|
||||
Otwiera szablon PSD, podmienia teksty w Smart Object "Projekt"
|
||||
z zachowaniem pozycji warstw, zapisuje jako nowy PSD w folderze _gotowe.
|
||||
|
||||
Wymaga: uruchomiony Adobe Photoshop, pakiet photoshop-python-api.
|
||||
|
||||
Użycie:
|
||||
python buteleczki_wzor1.py --imie_zenskie "Kinga" --imie_meskie "Łukasz" \
|
||||
--data "30.04.2026" --klient "Kinga Klimczak"
|
||||
|
||||
python buteleczki_wzor1.py --imie_zenskie "Kinga" --imie_meskie "Łukasz" \
|
||||
--data "30.04.2026" --zyczenia "Dziękujemy!" --klient "Kinga Klimczak"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import photoshop.api as ps
|
||||
|
||||
|
||||
# --- Ścieżki ---
|
||||
PROJEKT_DIR = os.path.join(
|
||||
r"d:\pomysloweprezenty.pl\projekty\ślub - buteleczki",
|
||||
"Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 1",
|
||||
)
|
||||
SZABLON_PATH = os.path.join(PROJEKT_DIR, "szablon 370x300.psd")
|
||||
GOTOWE_DIR = os.path.join(PROJEKT_DIR, "_gotowe")
|
||||
|
||||
# Nazwa warstwy Smart Object w głównym PSD
|
||||
SMART_OBJECT_LAYER = "Projekt"
|
||||
|
||||
# Mapowanie: nazwa_warstwy -> klucz argumentu
|
||||
# Warstwy znajdują się w: SO → Tekst → Dane → <nazwa>
|
||||
LAYER_MAP = {
|
||||
"imie_zenskie": "imie_zenskie",
|
||||
"imie_meskie": "imie_meskie",
|
||||
"data": "data",
|
||||
"zyczenia": "zyczenia",
|
||||
}
|
||||
|
||||
|
||||
def open_smart_object(app):
|
||||
"""Otwiera zawartość aktywnej warstwy Smart Object do edycji."""
|
||||
desc = ps.ActionDescriptor()
|
||||
ref = ps.ActionReference()
|
||||
ref.putEnumerated(
|
||||
app.stringIDToTypeID("layer"),
|
||||
app.stringIDToTypeID("ordinal"),
|
||||
app.stringIDToTypeID("targetEnum"),
|
||||
)
|
||||
desc.putReference(app.stringIDToTypeID("null"), ref)
|
||||
app.executeAction(app.stringIDToTypeID("placedLayerEditContents"), desc)
|
||||
|
||||
|
||||
def change_text_preserve_position(layer, new_text):
|
||||
"""Zmienia tekst warstwy z zachowaniem jej oryginalnej pozycji."""
|
||||
bounds_before = [float(b) for b in layer.bounds]
|
||||
layer.textItem.contents = new_text
|
||||
bounds_after = [float(b) for b in layer.bounds]
|
||||
|
||||
dx = bounds_before[0] - bounds_after[0]
|
||||
dy = bounds_before[1] - bounds_after[1]
|
||||
|
||||
if dx != 0 or dy != 0:
|
||||
layer.translate(dx, dy)
|
||||
|
||||
|
||||
def generate(imie_zenskie, imie_meskie, data, klient, zyczenia=None):
|
||||
"""Generuje PSD z podmienionymi danymi."""
|
||||
os.makedirs(GOTOWE_DIR, exist_ok=True)
|
||||
output_path = os.path.join(GOTOWE_DIR, f"{klient}.psd")
|
||||
|
||||
app = ps.Application()
|
||||
doc = app.open(SZABLON_PATH)
|
||||
print(f"Otwarto szablon: {doc.name}")
|
||||
|
||||
# Znajdź i otwórz Smart Object
|
||||
for layer in doc.layers:
|
||||
if layer.name == SMART_OBJECT_LAYER:
|
||||
app.activeDocument.activeLayer = layer
|
||||
break
|
||||
else:
|
||||
doc.close(ps.SaveOptions.DoNotSaveChanges)
|
||||
raise RuntimeError(f"Nie znaleziono warstwy '{SMART_OBJECT_LAYER}'")
|
||||
|
||||
open_smart_object(app)
|
||||
so_doc = app.activeDocument
|
||||
|
||||
# Nawiguj do grupy Dane
|
||||
dane_group = so_doc.layerSets["Tekst"].layerSets["Dane"]
|
||||
|
||||
# Podmiana tekstów
|
||||
replacements = {
|
||||
"imie_zenskie": imie_zenskie,
|
||||
"imie_meskie": imie_meskie,
|
||||
"data": data,
|
||||
}
|
||||
if zyczenia is not None:
|
||||
replacements["zyczenia"] = zyczenia
|
||||
|
||||
for layer_name, new_text in replacements.items():
|
||||
layer = dane_group.artLayers[layer_name]
|
||||
old_text = layer.textItem.contents
|
||||
change_text_preserve_position(layer, new_text)
|
||||
print(f" {layer_name}: \"{old_text}\" -> \"{new_text}\"")
|
||||
|
||||
# Zapisz Smart Object
|
||||
so_doc.save()
|
||||
so_doc.close()
|
||||
print("Smart Object zapisany")
|
||||
|
||||
# Zapisz jako nowy PSD
|
||||
psd_opts = ps.PhotoshopSaveOptions()
|
||||
app.activeDocument.saveAs(output_path, psd_opts, True)
|
||||
print(f"Zapisano: {output_path}")
|
||||
|
||||
# Zamknij bez zapisu oryginału
|
||||
app.activeDocument.close(ps.SaveOptions.DoNotSaveChanges)
|
||||
print("Gotowe!")
|
||||
return output_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generator PSD - buteleczki weselne Wzór 1"
|
||||
)
|
||||
parser.add_argument("--imie_zenskie", required=True, help="Imię żeńskie (np. Kinga)")
|
||||
parser.add_argument("--imie_meskie", required=True, help="Imię męskie (np. Łukasz)")
|
||||
parser.add_argument("--data", required=True, help="Data uroczystości (np. 30.04.2026)")
|
||||
parser.add_argument("--klient", required=True, help="Imię i nazwisko klienta (nazwa pliku wyjściowego)")
|
||||
parser.add_argument("--zyczenia", default=None, help="Tekst życzeń (domyślnie: oryginalny z szablonu)")
|
||||
|
||||
args = parser.parse_args()
|
||||
generate(
|
||||
imie_zenskie=args.imie_zenskie,
|
||||
imie_meskie=args.imie_meskie,
|
||||
data=args.data,
|
||||
klient=args.klient,
|
||||
zyczenia=args.zyczenia,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
143
tools/generowanie/buteleczki_wzor6.py
Normal file
143
tools/generowanie/buteleczki_wzor6.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
Generator PSD — Podziękowanie dla gości weselnych (buteleczka, Wzór 6).
|
||||
|
||||
Otwiera szablon PSD, podmienia imiona, datę i opcjonalnie życzenia
|
||||
w Smart Object "Projekt" z zachowaniem pozycji warstw, zapisuje jako nowy PSD.
|
||||
|
||||
Wymaga: uruchomiony Adobe Photoshop, pakiet photoshop-python-api.
|
||||
|
||||
Użycie:
|
||||
python buteleczki_wzor6.py --imie_zenskie "Kasia" --imie_meskie "Tomek" \
|
||||
--data "30.04.2026" --klient "Kinga Klimczak"
|
||||
|
||||
python buteleczki_wzor6.py --imie_zenskie "Kasia" --imie_meskie "Tomek" \
|
||||
--data "30.04.2026" --zyczenia "Na zdrowie!" --klient "Kinga Klimczak"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import photoshop.api as ps
|
||||
|
||||
|
||||
# --- Ścieżki ---
|
||||
PROJEKT_DIR = os.path.join(
|
||||
r"d:\pomysloweprezenty.pl\projekty\ślub - buteleczki",
|
||||
"Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 6",
|
||||
)
|
||||
SZABLON_PATH = os.path.join(PROJEKT_DIR, "szablon 370x300.psd")
|
||||
GOTOWE_DIR = os.path.join(PROJEKT_DIR, "_gotowe")
|
||||
|
||||
SMART_OBJECT_LAYER = "Projekt"
|
||||
|
||||
|
||||
def open_smart_object(app):
|
||||
"""Otwiera zawartość aktywnej warstwy Smart Object do edycji."""
|
||||
desc = ps.ActionDescriptor()
|
||||
ref = ps.ActionReference()
|
||||
ref.putEnumerated(
|
||||
app.stringIDToTypeID("layer"),
|
||||
app.stringIDToTypeID("ordinal"),
|
||||
app.stringIDToTypeID("targetEnum"),
|
||||
)
|
||||
desc.putReference(app.stringIDToTypeID("null"), ref)
|
||||
app.executeAction(app.stringIDToTypeID("placedLayerEditContents"), desc)
|
||||
|
||||
|
||||
def change_text_preserve_position(layer, new_text):
|
||||
"""Zmienia tekst warstwy z zachowaniem jej oryginalnej pozycji.
|
||||
|
||||
Dla tekstu wyśrodkowanego (justification=2) lub prawostronnego (justification=3)
|
||||
Photoshop sam utrzymuje punkt zakotwiczenia — korekta nie jest potrzebna.
|
||||
Dla tekstu lewostronnego (justification=1) korygujemy pozycję lewego górnego rogu.
|
||||
"""
|
||||
justification = layer.textItem.justification
|
||||
bounds_before = [float(b) for b in layer.bounds]
|
||||
layer.textItem.contents = new_text
|
||||
|
||||
# Korekta tylko dla tekstu lewostronnego
|
||||
if justification == 1:
|
||||
bounds_after = [float(b) for b in layer.bounds]
|
||||
dx = bounds_before[0] - bounds_after[0]
|
||||
dy = bounds_before[1] - bounds_after[1]
|
||||
if dx != 0 or dy != 0:
|
||||
layer.translate(dx, dy)
|
||||
|
||||
|
||||
def generate(imie_zenskie, imie_meskie, data, klient, zyczenia=None):
|
||||
"""Generuje PSD z podmienionymi danymi."""
|
||||
os.makedirs(GOTOWE_DIR, exist_ok=True)
|
||||
output_path = os.path.join(GOTOWE_DIR, f"{klient}.psd")
|
||||
|
||||
app = ps.Application()
|
||||
doc = app.open(SZABLON_PATH)
|
||||
print(f"Otwarto szablon: {doc.name}")
|
||||
|
||||
# Znajdź i otwórz Smart Object
|
||||
for layer in doc.layers:
|
||||
if layer.name == SMART_OBJECT_LAYER:
|
||||
app.activeDocument.activeLayer = layer
|
||||
break
|
||||
else:
|
||||
doc.close(ps.SaveOptions.DoNotSaveChanges)
|
||||
raise RuntimeError(f"Nie znaleziono warstwy '{SMART_OBJECT_LAYER}'")
|
||||
|
||||
open_smart_object(app)
|
||||
so_doc = app.activeDocument
|
||||
|
||||
# Nawiguj do grupy Dane
|
||||
dane_group = so_doc.layerSets["Dane"]
|
||||
|
||||
# Złóż imiona w format "Imię1 & Imię2"
|
||||
imiona_text = f"{imie_zenskie} & {imie_meskie}"
|
||||
|
||||
replacements = {
|
||||
"imiona": imiona_text,
|
||||
"data": data,
|
||||
}
|
||||
if zyczenia is not None:
|
||||
replacements["zyczenia"] = zyczenia
|
||||
|
||||
for layer_name, new_text in replacements.items():
|
||||
layer = dane_group.artLayers[layer_name]
|
||||
old_text = layer.textItem.contents
|
||||
change_text_preserve_position(layer, new_text)
|
||||
print(f' {layer_name}: "{old_text}" -> "{new_text}"')
|
||||
|
||||
# Zapisz Smart Object
|
||||
so_doc.save()
|
||||
so_doc.close()
|
||||
print("Smart Object zapisany")
|
||||
|
||||
# Zapisz jako nowy PSD
|
||||
psd_opts = ps.PhotoshopSaveOptions()
|
||||
app.activeDocument.saveAs(output_path, psd_opts, True)
|
||||
print(f"Zapisano: {output_path}")
|
||||
|
||||
# Zamknij bez zapisu oryginału
|
||||
app.activeDocument.close(ps.SaveOptions.DoNotSaveChanges)
|
||||
print("Gotowe!")
|
||||
return output_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generator PSD - buteleczki weselne Wzór 6"
|
||||
)
|
||||
parser.add_argument("--imie_zenskie", required=True, help="Imię żeńskie (np. Kasia)")
|
||||
parser.add_argument("--imie_meskie", required=True, help="Imię męskie (np. Tomek)")
|
||||
parser.add_argument("--data", required=True, help="Data uroczystości (np. 30.04.2026)")
|
||||
parser.add_argument("--klient", required=True, help="Imię i nazwisko klienta (nazwa pliku wyjściowego)")
|
||||
parser.add_argument("--zyczenia", default=None, help="Tekst życzeń (domyślnie: oryginalny z szablonu)")
|
||||
|
||||
args = parser.parse_args()
|
||||
generate(
|
||||
imie_zenskie=args.imie_zenskie,
|
||||
imie_meskie=args.imie_meskie,
|
||||
data=args.data,
|
||||
klient=args.klient,
|
||||
zyczenia=args.zyczenia,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
156
tools/generowanie/buteleczki_wzor8.py
Normal file
156
tools/generowanie/buteleczki_wzor8.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Generator PSD — Podziękowanie dla gości weselnych (buteleczka, Wzór 8).
|
||||
|
||||
Otwiera szablon PSD, podmienia inicjały, datę i opcjonalnie życzenia
|
||||
w Smart Object "Projekt" z zachowaniem pozycji warstw, zapisuje jako nowy PSD.
|
||||
|
||||
Wymaga: uruchomiony Adobe Photoshop, pakiet photoshop-python-api.
|
||||
|
||||
Użycie:
|
||||
python buteleczki_wzor8.py --inicjal_zenski "K" --inicjal_meski "Ł" \
|
||||
--data "30.04.2026" --klient "Kinga Klimczak"
|
||||
|
||||
python buteleczki_wzor8.py --inicjal_zenski "K" --inicjal_meski "Ł" \
|
||||
--data "30.04.2026" --zyczenia "Na zdrowie!" --klient "Kinga Klimczak"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import photoshop.api as ps
|
||||
|
||||
|
||||
# --- Ścieżki ---
|
||||
PROJEKT_DIR = os.path.join(
|
||||
r"d:\pomysloweprezenty.pl\projekty\ślub - buteleczki",
|
||||
"Podziękowanie dla gości weselnych buteleczka z nadrukiem UV - Wzór 8",
|
||||
)
|
||||
SZABLON_PATH = os.path.join(PROJEKT_DIR, "szablon 370x300.psd")
|
||||
GOTOWE_DIR = os.path.join(PROJEKT_DIR, "_gotowe")
|
||||
|
||||
SMART_OBJECT_LAYER = "Projekt"
|
||||
|
||||
|
||||
def open_smart_object(app):
|
||||
"""Otwiera zawartość aktywnej warstwy Smart Object do edycji."""
|
||||
desc = ps.ActionDescriptor()
|
||||
ref = ps.ActionReference()
|
||||
ref.putEnumerated(
|
||||
app.stringIDToTypeID("layer"),
|
||||
app.stringIDToTypeID("ordinal"),
|
||||
app.stringIDToTypeID("targetEnum"),
|
||||
)
|
||||
desc.putReference(app.stringIDToTypeID("null"), ref)
|
||||
app.executeAction(app.stringIDToTypeID("placedLayerEditContents"), desc)
|
||||
|
||||
|
||||
def change_text_preserve_position(layer, new_text):
|
||||
"""Zmienia tekst warstwy z zachowaniem jej oryginalnej pozycji."""
|
||||
bounds_before = [float(b) for b in layer.bounds]
|
||||
layer.textItem.contents = new_text
|
||||
bounds_after = [float(b) for b in layer.bounds]
|
||||
|
||||
dx = bounds_before[0] - bounds_after[0]
|
||||
dy = bounds_before[1] - bounds_after[1]
|
||||
|
||||
if dx != 0 or dy != 0:
|
||||
layer.translate(dx, dy)
|
||||
|
||||
|
||||
def generate(inicjal_zenski, inicjal_meski, data, klient, zyczenia=None):
|
||||
"""Generuje PSD z podmienionymi danymi."""
|
||||
os.makedirs(GOTOWE_DIR, exist_ok=True)
|
||||
output_path = os.path.join(GOTOWE_DIR, f"{klient}.psd")
|
||||
|
||||
app = ps.Application()
|
||||
doc = app.open(SZABLON_PATH)
|
||||
print(f"Otwarto szablon: {doc.name}")
|
||||
|
||||
# Znajdź i otwórz Smart Object
|
||||
for layer in doc.layers:
|
||||
if layer.name == SMART_OBJECT_LAYER:
|
||||
app.activeDocument.activeLayer = layer
|
||||
break
|
||||
else:
|
||||
doc.close(ps.SaveOptions.DoNotSaveChanges)
|
||||
raise RuntimeError(f"Nie znaleziono warstwy '{SMART_OBJECT_LAYER}'")
|
||||
|
||||
open_smart_object(app)
|
||||
so_doc = app.activeDocument
|
||||
|
||||
# Nawiguj do warstw tekstowych
|
||||
tekst_group = so_doc.layerSets["Tekst"]
|
||||
|
||||
# Data i życzenia — bezpośrednio w grupie Tekst
|
||||
replacements = {
|
||||
"data": data,
|
||||
"inicjal_zenski": inicjal_zenski,
|
||||
"inicjal_meski": inicjal_meski,
|
||||
}
|
||||
if zyczenia is not None:
|
||||
replacements["zyczenia"] = zyczenia
|
||||
|
||||
# Podmiana daty i życzeń
|
||||
for layer_name in ["data", "zyczenia"]:
|
||||
if layer_name not in replacements:
|
||||
continue
|
||||
layer = tekst_group.artLayers[layer_name]
|
||||
old_text = layer.textItem.contents
|
||||
change_text_preserve_position(layer, replacements[layer_name])
|
||||
print(f' {layer_name}: "{old_text}" -> "{replacements[layer_name]}"')
|
||||
|
||||
# Podmiana inicjałów — Tekst → <grupa> → Inicjaly
|
||||
inicjaly_group = None
|
||||
for ls in tekst_group.layerSets:
|
||||
try:
|
||||
inicjaly_group = ls.layerSets["Inicjaly"]
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if inicjaly_group is None:
|
||||
print(" UWAGA: Nie znaleziono grupy Inicjaly!")
|
||||
else:
|
||||
for layer_name in ["inicjal_zenski", "inicjal_meski"]:
|
||||
layer = inicjaly_group.artLayers[layer_name]
|
||||
old_text = layer.textItem.contents
|
||||
change_text_preserve_position(layer, replacements[layer_name])
|
||||
print(f' {layer_name}: "{old_text}" -> "{replacements[layer_name]}"')
|
||||
|
||||
# Zapisz Smart Object
|
||||
so_doc.save()
|
||||
so_doc.close()
|
||||
print("Smart Object zapisany")
|
||||
|
||||
# Zapisz jako nowy PSD
|
||||
psd_opts = ps.PhotoshopSaveOptions()
|
||||
app.activeDocument.saveAs(output_path, psd_opts, True)
|
||||
print(f"Zapisano: {output_path}")
|
||||
|
||||
# Zamknij bez zapisu oryginału
|
||||
app.activeDocument.close(ps.SaveOptions.DoNotSaveChanges)
|
||||
print("Gotowe!")
|
||||
return output_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generator PSD - buteleczki weselne Wzór 8"
|
||||
)
|
||||
parser.add_argument("--inicjal_zenski", required=True, help="Inicjał żeński (np. K)")
|
||||
parser.add_argument("--inicjal_meski", required=True, help="Inicjał męski (np. Ł)")
|
||||
parser.add_argument("--data", required=True, help="Data uroczystości (np. 30.04.2026)")
|
||||
parser.add_argument("--klient", required=True, help="Imię i nazwisko klienta (nazwa pliku wyjściowego)")
|
||||
parser.add_argument("--zyczenia", default=None, help="Tekst życzeń (domyślnie: oryginalny z szablonu)")
|
||||
|
||||
args = parser.parse_args()
|
||||
generate(
|
||||
inicjal_zenski=args.inicjal_zenski,
|
||||
inicjal_meski=args.inicjal_meski,
|
||||
data=args.data,
|
||||
klient=args.klient,
|
||||
zyczenia=args.zyczenia,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user