feat: Enhance InPost service selection and handling in Allegro settings
- Added a check for available InPost services and display a message if none are found. - Updated the InPost service selection dropdown to include additional data attributes for better handling in JavaScript. - Improved JavaScript event handling for InPost service selection to correctly populate hidden fields with selected service data. feat: Introduce Cash on Delivery (COD) functionality in shipment preparation - Added a new input field for specifying the COD amount and currency in the shipment preparation view. - Updated the shipment creation logic to handle COD amounts correctly when creating shipments. refactor: Update OrdersController to include shipment package repository - Modified the OrdersController to accept a ShipmentPackageRepository for better management of shipment-related data. - Enhanced order details to include shipment package information. fix: Ensure internal order numbers are generated upon order creation - Updated the OrderImportRepository to generate and store internal order numbers when a new order is created. feat: Implement status synchronization for Allegro orders - Introduced a new service for syncing order statuses from Allegro to the internal system. - Added logic to fetch and process orders needing status updates, handling errors gracefully. chore: Clean up InPost service definitions in AllegroIntegrationController - Removed hardcoded InPost service definitions and replaced them with dynamic fetching based on available services. feat: Add activity logging for shipment actions - Implemented activity logging for various shipment actions, including creation, label downloads, and errors, to improve traceability and auditing.
This commit is contained in:
1042
.vscode/ftp-kr.sync.cache.json
vendored
1042
.vscode/ftp-kr.sync.cache.json
vendored
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@
|
||||
- 2026-03-05: Dodano tabele `apaczka_integration_settings` pod konfiguracje klucza API Apaczka.
|
||||
- 2026-03-05: Dodano tabele `inpost_integration_settings` pod konfiguracje integracji InPost ShipX.
|
||||
- 2026-03-06: Dodano kolumne `carrier` do tabeli `allegro_delivery_method_mappings` (default 'allegro') - umozliwia mapowanie na roznych przewoznikow (Allegro, InPost).
|
||||
- 2026-03-06: Wdrozono migracje `20260302_000019_add_internal_order_number_to_orders.sql` - kolumna `internal_order_number` VARCHAR(11) UNIQUE w tabeli `orders`, format `OPXXXXXXXXX` (np. `OP000000001`); backfill istniejacych rekordow; UI: lista i szczegoly zamowien wyswietlaja numer wewnetrzny jako glowny identyfikator.
|
||||
- 2026-03-04: Poprawiono prezentacje daty zamowienia na liscie (`fallback ordered_at -> source_created_at -> source_updated_at -> fetched_at`) - bez zmian schematu.
|
||||
|
||||
## Tabele
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
# Tech Changelog
|
||||
|
||||
## 2026-03-06
|
||||
- Fix: synchronizacja statusow Allegro nie aktualizowala zamowien.
|
||||
- Przyczyna: Allegro API nie zmienia `updatedAt` przy zmianie `fulfillment.status`.
|
||||
Cursor-based sync (`AllegroOrdersSyncService`) pomijal takie zamowienia.
|
||||
- Rozwiazanie: `AllegroStatusSyncService` przepisany na podejscie direct-query:
|
||||
odpytuje baze o zamowienia Allegro w nie-finalnych statusach i re-importuje je
|
||||
przez `AllegroOrderImportService::importSingleOrder()`.
|
||||
- `AllegroStatusSyncService` nie zalezy juz od `AllegroOrdersSyncService`.
|
||||
- Dodano `ensureDefaultSchedulesExist()` w `AllegroIntegrationController`,
|
||||
aby harmonogramy cron byly tworzone automatycznie.
|
||||
- Rozszerzono zakladke `Formy dostawy` o wybor przewoznika (Allegro / InPost) per wiersz:
|
||||
- nowa kolumna `carrier` w tabeli `allegro_delivery_method_mappings`,
|
||||
- select przewoznika determinuje dostepne uslugi (Allegro z API, InPost statyczna lista),
|
||||
|
||||
@@ -262,32 +262,83 @@ a {
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--c-border);
|
||||
border-right-color: #243041;
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
transition: width 0.22s ease, min-width 0.22s ease;
|
||||
border-right: 1px solid #243041;
|
||||
background: #111a28;
|
||||
padding: 18px 14px;
|
||||
padding: 18px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar.is-collapsed {
|
||||
width: 52px;
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
.sidebar__brand {
|
||||
margin: 4px 10px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 4px 4px 16px;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar__brand-text {
|
||||
color: #e9f0ff;
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.02em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.sidebar__brand-text strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sidebar__brand strong {
|
||||
font-weight: 700;
|
||||
.sidebar__collapse-btn {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid #2a3a54;
|
||||
border-radius: 6px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.sidebar__collapse-btn:hover {
|
||||
background: #1b2a3f;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.sidebar__collapse-icon {
|
||||
display: block;
|
||||
transition: transform 0.22s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar.is-collapsed .sidebar__collapse-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.sidebar__nav {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sidebar__link {
|
||||
@@ -310,23 +361,29 @@ a {
|
||||
|
||||
.sidebar__group {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.sidebar__group-toggle {
|
||||
list-style: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
padding: 9px 10px;
|
||||
color: #cbd5e1;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar__group-toggle::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar__group:hover .sidebar__group-toggle {
|
||||
.sidebar__group:hover .sidebar__group-toggle,
|
||||
.sidebar__group-toggle:hover {
|
||||
color: #f8fafc;
|
||||
background: #1b2a3f;
|
||||
}
|
||||
@@ -336,32 +393,78 @@ a {
|
||||
background: #2e4f93;
|
||||
}
|
||||
|
||||
.sidebar__icon {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.sidebar__label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar__toggle-arrow {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
opacity: 0.5;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
details[open] > .sidebar__group-toggle .sidebar__toggle-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.sidebar__group-links {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding-left: 8px;
|
||||
gap: 2px;
|
||||
padding-left: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar__sublink {
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
padding: 7px 10px 7px 8px;
|
||||
text-decoration: none;
|
||||
color: #cbd5e1;
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sidebar__sublink::before {
|
||||
content: "";
|
||||
flex-shrink: 0;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: rgba(148, 163, 184, 0.3);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.sidebar__sublink:hover {
|
||||
color: #f8fafc;
|
||||
color: #e2e8f0;
|
||||
background: #1b2a3f;
|
||||
}
|
||||
|
||||
.sidebar__sublink:hover::before {
|
||||
background: rgba(148, 163, 184, 0.65);
|
||||
}
|
||||
.sidebar__sublink.is-active {
|
||||
color: #ffffff;
|
||||
background: #2e4f93;
|
||||
background: rgba(46, 79, 147, 0.55);
|
||||
}
|
||||
.sidebar__sublink.is-active::before {
|
||||
background: #93c5fd;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -1165,6 +1268,16 @@ a {
|
||||
background: #fffbeb;
|
||||
color: #92400e;
|
||||
}
|
||||
.order-tag.is-cod {
|
||||
border-color: #f9a8d4;
|
||||
background: #fdf2f8;
|
||||
color: #9d174d;
|
||||
}
|
||||
.order-tag.is-unpaid {
|
||||
border-color: #fca5a5;
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.orders-mini {
|
||||
font-size: 14px;
|
||||
@@ -1541,6 +1654,30 @@ a {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.payment-summary {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.payment-summary__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.payment-summary__label {
|
||||
width: 150px;
|
||||
flex-shrink: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.payment-summary__value {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.order-kv dt {
|
||||
color: #64748b;
|
||||
}
|
||||
@@ -2052,17 +2189,23 @@ a {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
flex-direction: column;
|
||||
}
|
||||
.sidebar {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #243041;
|
||||
padding: 14px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.sidebar__brand {
|
||||
margin: 0 0 10px;
|
||||
font-size: 22px;
|
||||
}
|
||||
.sidebar__collapse-btn {
|
||||
display: none;
|
||||
}
|
||||
.sidebar__nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -184,6 +184,9 @@ return [
|
||||
'document' => 'Dokument',
|
||||
'import' => 'Import',
|
||||
'note' => 'Notatka',
|
||||
'shipment_created' => 'Przesylka WZA',
|
||||
'shipment_label_downloaded' => 'Etykieta pobrana',
|
||||
'shipment_error' => 'Blad przesylki',
|
||||
],
|
||||
'actors' => [
|
||||
'system' => 'System',
|
||||
|
||||
@@ -23,32 +23,85 @@ a {
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--c-border);
|
||||
border-right-color: #243041;
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
transition: width 0.22s ease, min-width 0.22s ease;
|
||||
border-right: 1px solid #243041;
|
||||
background: #111a28;
|
||||
padding: 18px 14px;
|
||||
padding: 18px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar.is-collapsed {
|
||||
width: 52px;
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
.sidebar__brand {
|
||||
margin: 4px 10px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 4px 4px 16px;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar__brand-text {
|
||||
color: #e9f0ff;
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.02em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__brand strong {
|
||||
font-weight: 700;
|
||||
.sidebar__collapse-btn {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid #2a3a54;
|
||||
border-radius: 6px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #1b2a3f;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__collapse-icon {
|
||||
display: block;
|
||||
transition: transform 0.22s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar.is-collapsed .sidebar__collapse-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.sidebar__nav {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sidebar__link {
|
||||
@@ -71,23 +124,29 @@ a {
|
||||
|
||||
.sidebar__group {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.sidebar__group-toggle {
|
||||
list-style: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
padding: 9px 10px;
|
||||
color: #cbd5e1;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar__group-toggle::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar__group:hover .sidebar__group-toggle {
|
||||
.sidebar__group:hover .sidebar__group-toggle,
|
||||
.sidebar__group-toggle:hover {
|
||||
color: #f8fafc;
|
||||
background: #1b2a3f;
|
||||
}
|
||||
@@ -97,32 +156,83 @@ a {
|
||||
background: #2e4f93;
|
||||
}
|
||||
|
||||
.sidebar__icon {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.sidebar__label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar__toggle-arrow {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
opacity: 0.5;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
details[open] > .sidebar__group-toggle .sidebar__toggle-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.sidebar__group-links {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding-left: 8px;
|
||||
gap: 2px;
|
||||
padding-left: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar__sublink {
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
padding: 7px 10px 7px 8px;
|
||||
text-decoration: none;
|
||||
color: #cbd5e1;
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
|
||||
.sidebar__sublink:hover {
|
||||
color: #f8fafc;
|
||||
background: #1b2a3f;
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
flex-shrink: 0;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: rgba(148, 163, 184, 0.3);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.sidebar__sublink.is-active {
|
||||
color: #ffffff;
|
||||
background: #2e4f93;
|
||||
&:hover {
|
||||
color: #e2e8f0;
|
||||
background: #1b2a3f;
|
||||
|
||||
&::before {
|
||||
background: rgba(148, 163, 184, 0.65);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: #ffffff;
|
||||
background: rgba(46, 79, 147, 0.55);
|
||||
|
||||
&::before {
|
||||
background: #93c5fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -944,6 +1054,18 @@ a {
|
||||
background: #fffbeb;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
&.is-cod {
|
||||
border-color: #f9a8d4;
|
||||
background: #fdf2f8;
|
||||
color: #9d174d;
|
||||
}
|
||||
|
||||
&.is-unpaid {
|
||||
border-color: #fca5a5;
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
.orders-mini {
|
||||
@@ -1316,6 +1438,30 @@ a {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.payment-summary {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.payment-summary__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.payment-summary__label {
|
||||
width: 150px;
|
||||
flex-shrink: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.payment-summary__value {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.order-kv dt {
|
||||
color: #64748b;
|
||||
}
|
||||
@@ -1853,13 +1999,16 @@ a {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #243041;
|
||||
padding: 14px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.sidebar__brand {
|
||||
@@ -1867,6 +2016,10 @@ a {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.sidebar__collapse-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar__nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -14,13 +14,32 @@
|
||||
<?php $currentMenu = (string) ($activeMenu ?? ''); ?>
|
||||
<?php $currentSettings = (string) ($activeSettings ?? ''); ?>
|
||||
<?php $currentOrders = (string) ($activeOrders ?? ''); ?>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar__brand"><?= $e($t('brand.name_prefix')) ?><strong><?= $e($t('brand.name_suffix')) ?></strong></div>
|
||||
<div class="app-shell" id="js-app-shell">
|
||||
<aside class="sidebar" id="js-sidebar">
|
||||
<div class="sidebar__brand">
|
||||
<span class="sidebar__brand-text"><?= $e($t('brand.name_prefix')) ?><strong><?= $e($t('brand.name_suffix')) ?></strong></span>
|
||||
<button class="sidebar__collapse-btn" id="js-sidebar-collapse" title="Zwiń menu" aria-label="Zwiń menu">
|
||||
<svg class="sidebar__collapse-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M15 18l-6-6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar__nav" aria-label="<?= $e($t('navigation.main_menu')) ?>">
|
||||
<details class="sidebar__group<?= $currentMenu === 'orders' ? ' is-active' : '' ?>"<?= $currentMenu === 'orders' ? ' open' : '' ?>>
|
||||
<summary class="sidebar__group-toggle"><?= $e($t('navigation.orders')) ?></summary>
|
||||
<summary class="sidebar__group-toggle">
|
||||
<span class="sidebar__icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 5H7a2 2 0 00-2 2v13a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/>
|
||||
<rect x="9" y="3" width="6" height="4" rx="1"/>
|
||||
<path d="M9 12h6M9 16h4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="sidebar__label"><?= $e($t('navigation.orders')) ?></span>
|
||||
<svg class="sidebar__toggle-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<div class="sidebar__group-links">
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'orders' && $currentOrders === 'list' ? ' is-active' : '' ?>" href="/orders/list">
|
||||
<?= $e($t('navigation.orders_list')) ?>
|
||||
@@ -29,7 +48,18 @@
|
||||
</details>
|
||||
|
||||
<details class="sidebar__group<?= $currentMenu === 'settings' ? ' is-active' : '' ?>"<?= $currentMenu === 'settings' ? ' open' : '' ?>>
|
||||
<summary class="sidebar__group-toggle"><?= $e($t('navigation.settings')) ?></summary>
|
||||
<summary class="sidebar__group-toggle">
|
||||
<span class="sidebar__icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 2v2M12 20v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M2 12h2M20 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="sidebar__label"><?= $e($t('navigation.settings')) ?></span>
|
||||
<svg class="sidebar__toggle-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<div class="sidebar__group-links">
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'users' ? ' is-active' : '' ?>" href="/settings/users">
|
||||
<?= $e($t('navigation.users')) ?>
|
||||
@@ -77,5 +107,42 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/js/modules/jquery-alerts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/jquery-alerts.js') ?: 0 ?>"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var STORAGE_KEY = 'sidebarCollapsed';
|
||||
var sidebar = document.getElementById('js-sidebar');
|
||||
var collapseBtn = document.getElementById('js-sidebar-collapse');
|
||||
if (!sidebar || !collapseBtn) return;
|
||||
|
||||
function setCollapsed(collapsed) {
|
||||
sidebar.classList.toggle('is-collapsed', collapsed);
|
||||
collapseBtn.setAttribute('title', collapsed ? 'Rozwiń menu' : 'Zwiń menu');
|
||||
collapseBtn.setAttribute('aria-label', collapsed ? 'Rozwiń menu' : 'Zwiń menu');
|
||||
if (collapsed) {
|
||||
sidebar.querySelectorAll('details[open]').forEach(function (det) {
|
||||
det.removeAttribute('open');
|
||||
});
|
||||
}
|
||||
try { localStorage.setItem(STORAGE_KEY, collapsed ? '1' : '0'); } catch (e) {}
|
||||
}
|
||||
|
||||
try {
|
||||
if (localStorage.getItem(STORAGE_KEY) === '1') setCollapsed(true);
|
||||
} catch (e) {}
|
||||
|
||||
collapseBtn.addEventListener('click', function () {
|
||||
setCollapsed(!sidebar.classList.contains('is-collapsed'));
|
||||
});
|
||||
|
||||
sidebar.querySelectorAll('details > summary').forEach(function (summary) {
|
||||
summary.addEventListener('click', function (e) {
|
||||
if (sidebar.classList.contains('is-collapsed')) {
|
||||
e.preventDefault();
|
||||
setCollapsed(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,20 +11,6 @@
|
||||
<h2 class="section-title"><?= $e($t('orders.title')) ?></h2>
|
||||
<p class="muted mt-12"><?= $e($t('orders.description')) ?></p>
|
||||
</div>
|
||||
<div class="orders-stats">
|
||||
<div class="orders-stat">
|
||||
<span class="orders-stat__label"><?= $e($t('orders.stats.all')) ?></span>
|
||||
<strong class="orders-stat__value"><?= $e((string) ((int) ($stats['all'] ?? 0))) ?></strong>
|
||||
</div>
|
||||
<div class="orders-stat">
|
||||
<span class="orders-stat__label"><?= $e($t('orders.stats.paid')) ?></span>
|
||||
<strong class="orders-stat__value"><?= $e((string) ((int) ($stats['paid'] ?? 0))) ?></strong>
|
||||
</div>
|
||||
<div class="orders-stat">
|
||||
<span class="orders-stat__label"><?= $e($t('orders.stats.shipped')) ?></span>
|
||||
<strong class="orders-stat__value"><?= $e((string) ((int) ($stats['shipped'] ?? 0))) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($errorMessage)): ?>
|
||||
<div class="alert alert--warning mt-12" role="alert">
|
||||
|
||||
@@ -4,6 +4,7 @@ $itemsList = is_array($items ?? null) ? $items : [];
|
||||
$addressesList = is_array($addresses ?? null) ? $addresses : [];
|
||||
$paymentsList = is_array($payments ?? null) ? $payments : [];
|
||||
$shipmentsList = is_array($shipments ?? null) ? $shipments : [];
|
||||
$packagesList = is_array($packages ?? null) ? $packages : [];
|
||||
$documentsList = is_array($documents ?? null) ? $documents : [];
|
||||
$notesList = is_array($notes ?? null) ? $notes : [];
|
||||
$historyList = is_array($history ?? null) ? $history : [];
|
||||
@@ -36,7 +37,7 @@ foreach ($addressesList as $address) {
|
||||
<div class="order-details-head">
|
||||
<div>
|
||||
<a href="/orders/list" class="order-back-link">← <?= $e($t('navigation.orders_list')) ?></a>
|
||||
<h2 class="section-title mt-12"><?= $e($t('orders.details.title')) ?> #<?= $e((string) ($orderId ?? 0)) ?></h2>
|
||||
<h2 class="section-title mt-12"><?= $e($t('orders.details.title')) ?> <?= $e((string) ($orderRow['internal_order_number'] ?? ('#' . ($orderId ?? 0)))) ?></h2>
|
||||
<div class="order-details-sub mt-12">
|
||||
<span><?= $e(ucfirst((string) ($orderRow['source'] ?? ''))) ?> <?= $e((string) ($orderRow['external_order_id'] ?? '')) ?></span>
|
||||
</div>
|
||||
@@ -95,7 +96,7 @@ foreach ($addressesList as $address) {
|
||||
<section class="card mt-16 order-details-tabs">
|
||||
<button type="button" class="order-details-tab is-active" data-order-tab-target="details"><?= $e($t('orders.details.tabs.details')) ?></button>
|
||||
<button type="button" class="order-details-tab" data-order-tab-target="history"><?= $e($t('orders.details.tabs.history')) ?> (<?= $e((string) count($activityLogList)) ?>)</button>
|
||||
<button type="button" class="order-details-tab" data-order-tab-target="shipments"><?= $e($t('orders.details.tabs.shipments')) ?> (<?= $e((string) count($shipmentsList)) ?>)</button>
|
||||
<button type="button" class="order-details-tab" data-order-tab-target="shipments"><?= $e($t('orders.details.tabs.shipments')) ?> (<?= $e((string) (count($shipmentsList) + count($packagesList))) ?>)</button>
|
||||
<button type="button" class="order-details-tab" data-order-tab-target="payments"><?= $e($t('orders.details.tabs.payments')) ?> (<?= $e((string) count($paymentsList)) ?>)</button>
|
||||
<button type="button" class="order-details-tab" data-order-tab-target="documents"><?= $e($t('orders.details.tabs.documents')) ?> (<?= $e((string) count($documentsList)) ?>)</button>
|
||||
</section>
|
||||
@@ -160,6 +161,7 @@ foreach ($addressesList as $address) {
|
||||
<h3 class="section-title"><?= $e($t('orders.details.order_info')) ?></h3>
|
||||
<dl class="order-kv mt-12">
|
||||
<dt><?= $e($t('orders.details.fields.status')) ?></dt><dd><?= $e((string) ($statusLabel ?? '-')) ?></dd>
|
||||
<dt>Nr zamowienia</dt><dd><strong><?= $e((string) ($orderRow['internal_order_number'] ?? '-')) ?></strong></dd>
|
||||
<dt><?= $e($t('orders.details.fields.source_order_id')) ?></dt><dd><?= $e((string) ($orderRow['source_order_id'] ?? '-')) ?></dd>
|
||||
<dt><?= $e($t('orders.details.fields.external_order_id')) ?></dt><dd><?= $e((string) ($orderRow['external_order_id'] ?? '-')) ?></dd>
|
||||
<dt><?= $e($t('orders.details.fields.ordered_at')) ?></dt><dd><?= $e((string) ($orderRow['ordered_at'] ?? '-')) ?></dd>
|
||||
@@ -172,6 +174,23 @@ foreach ($addressesList as $address) {
|
||||
<h3 class="section-title"><?= $e($t('orders.details.payment_shipping')) ?></h3>
|
||||
<dl class="order-kv mt-12">
|
||||
<dt><?= $e($t('orders.details.fields.payment_status')) ?></dt><dd><?= $e((string) ($orderRow['payment_status'] ?? '-')) ?></dd>
|
||||
<dt>Typ platnosci</dt>
|
||||
<dd>
|
||||
<?php
|
||||
$paymentTypeRaw = strtoupper(trim((string) ($orderRow['external_payment_type_id'] ?? '')));
|
||||
$paymentTypeLabels = [
|
||||
'CASH_ON_DELIVERY' => 'Za pobraniem',
|
||||
'ONLINE' => 'Platnosc online',
|
||||
'TRANSFER' => 'Przelew',
|
||||
];
|
||||
$paymentTypeLabel = $paymentTypeLabels[$paymentTypeRaw] ?? ($paymentTypeRaw !== '' ? $paymentTypeRaw : '-');
|
||||
?>
|
||||
<?php if ($paymentTypeRaw === 'CASH_ON_DELIVERY'): ?>
|
||||
<span class="order-tag is-cod"><?= $e($paymentTypeLabel) ?></span>
|
||||
<?php else: ?>
|
||||
<?= $e($paymentTypeLabel) ?>
|
||||
<?php endif; ?>
|
||||
</dd>
|
||||
<dt><?= $e($t('orders.details.fields.total_with_tax')) ?></dt><dd><?= $e((string) ($orderRow['total_with_tax'] ?? '-')) ?></dd>
|
||||
<dt><?= $e($t('orders.details.fields.total_paid')) ?></dt><dd><?= $e((string) ($orderRow['total_paid'] ?? '-')) ?></dd>
|
||||
<dt><?= $e($t('orders.details.fields.carrier')) ?></dt><dd><?= $e((string) ($orderRow['external_carrier_id'] ?? '-')) ?></dd>
|
||||
@@ -292,16 +311,183 @@ foreach ($addressesList as $address) {
|
||||
</div>
|
||||
|
||||
<div class="order-tab-panel" data-order-tab-panel="shipments">
|
||||
<?php if ($packagesList !== []): ?>
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title">Wygenerowane przesylki (WZA)</h3>
|
||||
<div class="table-wrap mt-12">
|
||||
<table class="table table--details">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th>Nr sledzenia</th>
|
||||
<th>Przewoznik</th>
|
||||
<th>Etykieta</th>
|
||||
<th>Utworzono</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($packagesList as $pkg): ?>
|
||||
<?php
|
||||
$pkgStatus = (string) ($pkg['status'] ?? 'draft');
|
||||
$pkgTracking = trim((string) ($pkg['tracking_number'] ?? ''));
|
||||
$pkgCarrier = trim((string) ($pkg['carrier_id'] ?? ''));
|
||||
$pkgLabelPath = trim((string) ($pkg['label_path'] ?? ''));
|
||||
$pkgError = trim((string) ($pkg['error_message'] ?? ''));
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $e((string) ($pkg['id'] ?? '')) ?></td>
|
||||
<td>
|
||||
<span class="order-tag <?= $pkgStatus === 'label_ready' || $pkgStatus === 'created' ? 'is-success' : ($pkgStatus === 'error' ? 'is-danger' : 'is-warn') ?>">
|
||||
<?= $e($pkgStatus) ?>
|
||||
</span>
|
||||
<?php if ($pkgError !== ''): ?>
|
||||
<div class="muted mt-4" style="font-size:0.75rem"><?= $e($pkgError) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $e($pkgTracking !== '' ? $pkgTracking : '-') ?></td>
|
||||
<td><?= $e($pkgCarrier !== '' ? $pkgCarrier : '-') ?></td>
|
||||
<td>
|
||||
<?php if ($pkgLabelPath !== ''): ?>
|
||||
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/prepare" class="btn btn--sm btn--secondary">Pobierz</a>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-nowrap"><?= $e((string) ($pkg['created_at'] ?? '')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($shipmentsList !== []): ?>
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title">Wysylki z Allegro</h3>
|
||||
<div class="table-wrap mt-12">
|
||||
<table class="table table--details">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nr sledzenia</th>
|
||||
<th>Przewoznik</th>
|
||||
<th>Data nadania</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($shipmentsList as $sh): ?>
|
||||
<tr>
|
||||
<td><?= $e(trim((string) ($sh['tracking_number'] ?? '-'))) ?></td>
|
||||
<td><?= $e(trim((string) ($sh['carrier_provider_id'] ?? '-'))) ?></td>
|
||||
<td class="text-nowrap"><?= $e(trim((string) ($sh['posted_at'] ?? '-'))) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($packagesList === [] && $shipmentsList === []): ?>
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title"><?= $e($t('orders.details.tabs.shipments')) ?></h3>
|
||||
<div class="order-empty-placeholder mt-12"></div>
|
||||
<p class="muted mt-12">Brak przesylek dla tego zamowienia.</p>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="order-tab-panel" data-order-tab-panel="payments">
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title"><?= $e($t('orders.details.tabs.payments')) ?></h3>
|
||||
<div class="order-empty-placeholder mt-12"></div>
|
||||
|
||||
<?php
|
||||
$paymentStatusNum = isset($orderRow['payment_status']) ? (int) $orderRow['payment_status'] : null;
|
||||
$paymentStatusLabels = [0 => 'Nieopłacone', 1 => 'Częściowo opłacone', 2 => 'Opłacone', 3 => 'Zwrócone'];
|
||||
$paymentStatusClasses = [0 => 'is-danger', 1 => 'is-warn', 2 => 'is-success', 3 => 'is-neutral'];
|
||||
$paymentTypeLabels = [
|
||||
'ONLINE' => 'Płatność online',
|
||||
'CASH_ON_DELIVERY' => 'Za pobraniem',
|
||||
'TRANSFER' => 'Przelew bankowy',
|
||||
];
|
||||
$providerLabels = [
|
||||
'AF' => 'Allegro Pay',
|
||||
'PAYU' => 'PayU',
|
||||
'PRZELEWY24' => 'Przelewy24',
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="payment-summary mt-12">
|
||||
<div class="payment-summary__row">
|
||||
<span class="payment-summary__label">Status płatności</span>
|
||||
<span class="payment-summary__value">
|
||||
<?php if ($paymentStatusNum !== null && isset($paymentStatusLabels[$paymentStatusNum])): ?>
|
||||
<span class="order-tag <?= $e($paymentStatusClasses[$paymentStatusNum] ?? 'is-neutral') ?>">
|
||||
<?= $e($paymentStatusLabels[$paymentStatusNum]) ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="muted">—</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="payment-summary__row">
|
||||
<span class="payment-summary__label">Kwota do zapłaty</span>
|
||||
<span class="payment-summary__value">
|
||||
<?= $e($orderRow['total_with_tax'] !== null ? number_format((float) $orderRow['total_with_tax'], 2, '.', ' ') . ' ' . ($orderRow['currency'] ?? '') : '—') ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="payment-summary__row">
|
||||
<span class="payment-summary__label">Kwota opłacona</span>
|
||||
<span class="payment-summary__value">
|
||||
<?= $e($orderRow['total_paid'] !== null ? number_format((float) $orderRow['total_paid'], 2, '.', ' ') . ' ' . ($orderRow['currency'] ?? '') : '—') ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($paymentsList === []): ?>
|
||||
<p class="muted mt-16">Brak zarejestrowanych płatności.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap mt-16">
|
||||
<table class="table table--details">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Data płatności</th>
|
||||
<th>Typ</th>
|
||||
<th>Dostawca</th>
|
||||
<th>Kwota</th>
|
||||
<th>ID płatności</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($paymentsList as $payment): ?>
|
||||
<?php
|
||||
$ptRaw = strtoupper(trim((string) ($payment['payment_type_id'] ?? '')));
|
||||
$ptLabel = $paymentTypeLabels[$ptRaw] ?? ($ptRaw !== '' ? $ptRaw : '—');
|
||||
$providerRaw = strtoupper(trim((string) ($payment['comment'] ?? '')));
|
||||
$providerLabel = $providerLabels[$providerRaw] ?? ($providerRaw !== '' ? $providerRaw : '—');
|
||||
$amount = $payment['amount'] !== null
|
||||
? number_format((float) $payment['amount'], 2, '.', ' ') . ' ' . ($payment['currency'] ?? '')
|
||||
: '—';
|
||||
$payDate = (string) ($payment['payment_date'] ?? '');
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-nowrap"><?= $e($payDate !== '' ? $payDate : '—') ?></td>
|
||||
<td>
|
||||
<?php if ($ptRaw === 'CASH_ON_DELIVERY'): ?>
|
||||
<span class="order-tag is-cod"><?= $e($ptLabel) ?></span>
|
||||
<?php else: ?>
|
||||
<?= $e($ptLabel) ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $e($providerLabel) ?></td>
|
||||
<td class="text-nowrap"><strong><?= $e($amount) ?></strong></td>
|
||||
<td class="muted" style="font-size:11px"><?= $e((string) ($payment['source_payment_id'] ?? '—')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -378,14 +378,29 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
|
||||
<?php // InPost simple select ?>
|
||||
<div class="dm-inpost-panel" style="<?= $currentCarrier !== 'inpost' ? 'display:none' : '' ?>">
|
||||
<select class="form-control dm-inpost-select">
|
||||
<option value="">-- <?= $e($t('settings.allegro.delivery.fields.no_mapping')) ?> --</option>
|
||||
<?php foreach ($dmInpostServices as $inSvc): ?>
|
||||
<option value="<?= $e((string) ($inSvc['id'] ?? '')) ?>"<?= $currentCarrier === 'inpost' && $currentAllegroId === (string) ($inSvc['id'] ?? '') ? ' selected' : '' ?>>
|
||||
<?= $e((string) ($inSvc['name'] ?? '')) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if ($dmInpostServices === []): ?>
|
||||
<div class="muted">Brak uslug InPost (sprawdz polaczenie z Allegro).</div>
|
||||
<?php else: ?>
|
||||
<select class="form-control dm-inpost-select">
|
||||
<option value="">-- <?= $e($t('settings.allegro.delivery.fields.no_mapping')) ?> --</option>
|
||||
<?php foreach ($dmInpostServices as $inSvc): ?>
|
||||
<?php
|
||||
$inSvcId = is_array($inSvc['id'] ?? null) ? $inSvc['id'] : [];
|
||||
$inSvcMethodId = trim((string) ($inSvcId['deliveryMethodId'] ?? ''));
|
||||
$inSvcCredentialsId = trim((string) ($inSvcId['credentialsId'] ?? ''));
|
||||
$inSvcCarrierId = trim((string) ($inSvc['carrierId'] ?? ''));
|
||||
$inSvcName = trim((string) ($inSvc['name'] ?? ''));
|
||||
?>
|
||||
<option
|
||||
value="<?= $e($inSvcMethodId) ?>"
|
||||
data-credentials-id="<?= $e($inSvcCredentialsId) ?>"
|
||||
data-carrier-id="<?= $e($inSvcCarrierId) ?>"
|
||||
<?= $currentCarrier === 'inpost' && $currentAllegroId === $inSvcMethodId ? 'selected' : '' ?>>
|
||||
<?= $e($inSvcName) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php // Empty state ?>
|
||||
@@ -501,8 +516,8 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
|
||||
inpostSelect.addEventListener('change', function () {
|
||||
var opt = inpostSelect.options[inpostSelect.selectedIndex];
|
||||
if (hiddenMethodId) hiddenMethodId.value = inpostSelect.value;
|
||||
if (hiddenCredentialsId) hiddenCredentialsId.value = '';
|
||||
if (hiddenCarrierId) hiddenCarrierId.value = '';
|
||||
if (hiddenCredentialsId) hiddenCredentialsId.value = opt ? (opt.getAttribute('data-credentials-id') || '') : '';
|
||||
if (hiddenCarrierId) hiddenCarrierId.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
|
||||
if (hiddenServiceName) hiddenServiceName.value = opt ? opt.textContent.trim() : '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ $pointId = trim((string) ($receiver['parcel_external_id'] ?? ''));
|
||||
$pointName = trim((string) ($receiver['parcel_name'] ?? ''));
|
||||
$totalWithTax = (float) ($orderRow['total_with_tax'] ?? 0);
|
||||
$currency = strtoupper(trim((string) ($orderRow['currency'] ?? 'PLN')));
|
||||
$isCod = strtoupper(trim((string) ($orderRow['external_payment_type_id'] ?? ''))) === 'CASH_ON_DELIVERY';
|
||||
$defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
@@ -171,14 +173,30 @@ $currency = strtoupper(trim((string) ($orderRow['currency'] ?? 'PLN')));
|
||||
</div>
|
||||
|
||||
<div id="shipment-inpost-panel" style="<?= $preselectedCarrier !== 'inpost' ? 'display:none' : '' ?>">
|
||||
<select class="form-control" id="shipment-inpost-select">
|
||||
<option value="">-- Wybierz usluge InPost --</option>
|
||||
<?php foreach ($inpostSvcList as $inSvc): ?>
|
||||
<option value="<?= $e((string) ($inSvc['id'] ?? '')) ?>"<?= $mappedCarrier === 'inpost' && $mappedMethodId === (string) ($inSvc['id'] ?? '') ? ' selected' : '' ?>>
|
||||
<?= $e((string) ($inSvc['name'] ?? '')) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if ($inpostSvcList === []): ?>
|
||||
<div class="muted">Brak uslug InPost (sprawdz polaczenie z Allegro).</div>
|
||||
<?php else: ?>
|
||||
<select class="form-control" id="shipment-inpost-select">
|
||||
<option value="">-- Wybierz usluge InPost --</option>
|
||||
<?php foreach ($inpostSvcList as $inSvc): ?>
|
||||
<?php
|
||||
$inSvcId = is_array($inSvc['id'] ?? null) ? $inSvc['id'] : [];
|
||||
$inSvcMethodId = trim((string) ($inSvcId['deliveryMethodId'] ?? ''));
|
||||
$inSvcCredentialsId = trim((string) ($inSvcId['credentialsId'] ?? ''));
|
||||
$inSvcCarrierId = trim((string) ($inSvc['carrierId'] ?? ''));
|
||||
$inSvcName = trim((string) ($inSvc['name'] ?? ''));
|
||||
$inSvcSelected = $mappedCarrier === 'inpost' && $mappedMethodId === $inSvcMethodId;
|
||||
?>
|
||||
<option
|
||||
value="<?= $e($inSvcMethodId) ?>"
|
||||
data-credentials-id="<?= $e($inSvcCredentialsId) ?>"
|
||||
data-carrier-id="<?= $e($inSvcCarrierId) ?>"
|
||||
<?= $inSvcSelected ? 'selected' : '' ?>>
|
||||
<?= $e($inSvcName) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div id="shipment-empty-panel" class="muted" style="<?= $preselectedCarrier !== '' ? 'display:none' : '' ?>">Wybierz przewoznika</div>
|
||||
@@ -230,6 +248,14 @@ $currency = strtoupper(trim((string) ($orderRow['currency'] ?? 'PLN')));
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Pobranie (<?= $e($currency) ?>)<?= $isCod ? ' <span class="order-tag is-cod" style="font-size:0.7rem;vertical-align:middle">ZA POBRANIEM</span>' : '' ?></span>
|
||||
<input class="form-control" type="number" name="cod_amount" step="0.01" min="0" value="<?= $e($defaultCodAmount) ?>">
|
||||
<input type="hidden" name="cod_currency" value="<?= $e($currency) ?>">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="form-field">
|
||||
<span class="field-label">Punkt nadania (opcjonalnie)</span>
|
||||
<input class="form-control" type="text" name="sender_point_id" maxlength="64" placeholder="np. KRA010">
|
||||
@@ -482,13 +508,15 @@ $currency = strtoupper(trim((string) ($orderRow['currency'] ?? 'PLN')));
|
||||
|
||||
// --- InPost select ---
|
||||
if (inpostSelect) {
|
||||
inpostSelect.addEventListener('change', function () {
|
||||
function syncInpostFields() {
|
||||
var opt = inpostSelect.options[inpostSelect.selectedIndex];
|
||||
hiddenInput.value = inpostSelect.value;
|
||||
credentialsInput.value = '';
|
||||
carrierInput.value = '';
|
||||
});
|
||||
credentialsInput.value = opt ? (opt.getAttribute('data-credentials-id') || '') : '';
|
||||
carrierInput.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
|
||||
}
|
||||
inpostSelect.addEventListener('change', syncInpostFields);
|
||||
if (carrierSelect.value === 'inpost' && inpostSelect.value !== '') {
|
||||
hiddenInput.value = inpostSelect.value;
|
||||
syncInpostFields();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,7 +601,7 @@ $currency = strtoupper(trim((string) ($orderRow['currency'] ?? 'PLN')));
|
||||
fetch('/orders/' + oId + '/shipment/' + pkgId + '/status')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.status === 'created') {
|
||||
if (data.status === 'created' || data.status === 'label_ready') {
|
||||
window.location.reload();
|
||||
} else if (data.status === 'error') {
|
||||
if (btn) {
|
||||
|
||||
@@ -39,7 +39,8 @@ return static function (Application $app): void {
|
||||
|
||||
$authController = new AuthController($template, $auth, $translator);
|
||||
$usersController = new UsersController($template, $translator, $auth, $app->users());
|
||||
$ordersController = new OrdersController($template, $translator, $auth, $app->orders());
|
||||
$shipmentPackageRepositoryForOrders = new ShipmentPackageRepository($app->db());
|
||||
$ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders);
|
||||
$settingsController = new SettingsController($template, $translator, $auth, $app->migrator(), $app->orderStatuses());
|
||||
$allegroIntegrationRepository = new AllegroIntegrationRepository(
|
||||
$app->db(),
|
||||
|
||||
@@ -297,7 +297,8 @@ final class Application
|
||||
'allegro_status_sync' => new AllegroStatusSyncHandler(
|
||||
new AllegroStatusSyncService(
|
||||
$repository,
|
||||
$ordersSyncService
|
||||
$orderImportService,
|
||||
$this->db
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
@@ -106,7 +106,12 @@ final class OrderImportRepository
|
||||
);
|
||||
$statement->execute($this->orderParams($orderData));
|
||||
|
||||
return (int) $this->pdo->lastInsertId();
|
||||
$newId = (int) $this->pdo->lastInsertId();
|
||||
$this->pdo->prepare(
|
||||
"UPDATE orders SET internal_order_number = CONCAT('OP', LPAD(id, 9, '0')) WHERE id = :id AND (internal_order_number IS NULL OR internal_order_number = '')"
|
||||
)->execute(['id' => $newId]);
|
||||
|
||||
return $newId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Core\I18n\Translator;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\View\Template;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use App\Modules\Shipments\ShipmentPackageRepository;
|
||||
|
||||
final class OrdersController
|
||||
{
|
||||
@@ -16,7 +17,8 @@ final class OrdersController
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly OrdersRepository $orders
|
||||
private readonly OrdersRepository $orders,
|
||||
private readonly ?ShipmentPackageRepository $shipmentPackages = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -153,6 +155,10 @@ final class OrdersController
|
||||
|
||||
$allStatuses = $this->buildAllStatusOptions($statusConfig);
|
||||
|
||||
$packages = $this->shipmentPackages !== null
|
||||
? $this->shipmentPackages->findByOrderId($orderId)
|
||||
: [];
|
||||
|
||||
$flashSuccess = (string) ($_SESSION['order_flash_success'] ?? '');
|
||||
$flashError = (string) ($_SESSION['order_flash_error'] ?? '');
|
||||
unset($_SESSION['order_flash_success'], $_SESSION['order_flash_error']);
|
||||
@@ -169,6 +175,7 @@ final class OrdersController
|
||||
'addresses' => $addresses,
|
||||
'payments' => $payments,
|
||||
'shipments' => $shipments,
|
||||
'packages' => $packages,
|
||||
'documents' => $documents,
|
||||
'notes' => $notes,
|
||||
'history' => $resolvedHistory,
|
||||
@@ -222,6 +229,7 @@ final class OrdersController
|
||||
*/
|
||||
private function toTableRow(array $row, array $statusLabelMap): array
|
||||
{
|
||||
$internalOrderNumber = trim((string) ($row['internal_order_number'] ?? ''));
|
||||
$sourceOrderId = trim((string) ($row['source_order_id'] ?? ''));
|
||||
$externalOrderId = trim((string) ($row['external_order_id'] ?? ''));
|
||||
$source = trim((string) ($row['source'] ?? ''));
|
||||
@@ -232,6 +240,10 @@ final class OrdersController
|
||||
$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, '.', ' ') : '-';
|
||||
$paymentType = strtoupper(trim((string) ($row['external_payment_type_id'] ?? '')));
|
||||
$isCod = $paymentType === 'CASH_ON_DELIVERY';
|
||||
$paymentStatus = isset($row['payment_status']) ? (int) $row['payment_status'] : null;
|
||||
$isUnpaid = !$isCod && $paymentStatus === 0;
|
||||
$itemsCount = max(0, (int) ($row['items_count'] ?? 0));
|
||||
$itemsQty = $this->formatQuantity((float) ($row['items_qty'] ?? 0));
|
||||
$shipments = max(0, (int) ($row['shipments_count'] ?? 0));
|
||||
@@ -241,10 +253,10 @@ final class OrdersController
|
||||
return [
|
||||
'order_ref' => '<div class="orders-ref">'
|
||||
. '<div class="orders-ref__main"><a href="/orders/' . (int) ($row['id'] ?? 0) . '">'
|
||||
. htmlspecialchars($sourceOrderId !== '' ? $sourceOrderId : ('#' . (string) ($row['id'] ?? 0)), ENT_QUOTES, 'UTF-8')
|
||||
. htmlspecialchars($internalOrderNumber !== '' ? $internalOrderNumber : ('#' . (string) ($row['id'] ?? 0)), ENT_QUOTES, 'UTF-8')
|
||||
. '</a></div>'
|
||||
. '<div class="orders-ref__meta">'
|
||||
. '<span>' . htmlspecialchars($externalOrderId, ENT_QUOTES, 'UTF-8') . '</span>'
|
||||
. '<span>' . htmlspecialchars($sourceOrderId !== '' ? $sourceOrderId : $externalOrderId, ENT_QUOTES, 'UTF-8') . '</span>'
|
||||
. '<span>' . htmlspecialchars($source, ENT_QUOTES, 'UTF-8') . '</span>'
|
||||
. '</div>'
|
||||
. '</div>',
|
||||
@@ -260,8 +272,8 @@ final class OrdersController
|
||||
. '</div>',
|
||||
'products' => $this->productsHtml($itemsPreview, $itemsCount, $itemsQty),
|
||||
'totals' => '<div class="orders-money">'
|
||||
. '<div class="orders-money__main">' . htmlspecialchars($totalWithTax . ' ' . $currency, ENT_QUOTES, 'UTF-8') . '</div>'
|
||||
. '<div class="orders-money__meta">oplacono: ' . htmlspecialchars($totalPaid . ' ' . $currency, ENT_QUOTES, 'UTF-8') . '</div>'
|
||||
. '<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>'
|
||||
. '</div>',
|
||||
'shipping' => $this->shippingHtml(
|
||||
trim((string) ($row['external_carrier_id'] ?? '')),
|
||||
|
||||
@@ -99,6 +99,7 @@ final class OrdersRepository
|
||||
|
||||
$listSql = 'SELECT
|
||||
o.id,
|
||||
o.internal_order_number,
|
||||
o.source,
|
||||
o.source_order_id,
|
||||
o.external_order_id,
|
||||
@@ -119,6 +120,7 @@ final class OrdersRepository
|
||||
a.email AS buyer_email,
|
||||
a.city AS buyer_city,
|
||||
o.external_carrier_id,
|
||||
o.external_payment_type_id,
|
||||
(SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = o.id) AS items_count,
|
||||
(SELECT COALESCE(SUM(oi.quantity), 0) FROM order_items oi WHERE oi.order_id = o.id) AS items_qty,
|
||||
(SELECT COUNT(*) FROM order_shipments sh WHERE sh.order_id = o.id) AS shipments_count,
|
||||
@@ -156,6 +158,7 @@ final class OrdersRepository
|
||||
$orderId = (int) ($row['id'] ?? 0);
|
||||
return [
|
||||
'id' => $orderId,
|
||||
'internal_order_number' => (string) ($row['internal_order_number'] ?? ''),
|
||||
'source' => (string) ($row['source'] ?? ''),
|
||||
'source_order_id' => (string) ($row['source_order_id'] ?? ''),
|
||||
'external_order_id' => (string) ($row['external_order_id'] ?? ''),
|
||||
@@ -172,6 +175,7 @@ final class OrdersRepository
|
||||
'is_invoice' => (int) ($row['is_invoice'] ?? 0) === 1,
|
||||
'is_canceled_by_buyer' => (int) ($row['is_canceled_by_buyer'] ?? 0) === 1,
|
||||
'external_carrier_id' => (string) ($row['external_carrier_id'] ?? ''),
|
||||
'external_payment_type_id' => (string) ($row['external_payment_type_id'] ?? ''),
|
||||
'buyer_name' => (string) ($row['buyer_name'] ?? ''),
|
||||
'buyer_email' => (string) ($row['buyer_email'] ?? ''),
|
||||
'buyer_city' => (string) ($row['buyer_city'] ?? ''),
|
||||
|
||||
@@ -73,6 +73,7 @@ final class AllegroIntegrationController
|
||||
if (trim((string) ($settings['redirect_uri'] ?? '')) === '') {
|
||||
$settings['redirect_uri'] = $defaultRedirectUri;
|
||||
}
|
||||
$this->ensureDefaultSchedulesExist();
|
||||
$importIntervalSeconds = $this->currentImportIntervalSeconds();
|
||||
$statusSyncDirection = $this->currentStatusSyncDirection();
|
||||
$statusSyncIntervalMinutes = $this->currentStatusSyncIntervalMinutes();
|
||||
@@ -99,7 +100,10 @@ final class AllegroIntegrationController
|
||||
'orderDeliveryMethods' => $this->deliveryMappings !== null ? $this->deliveryMappings->getDistinctOrderDeliveryMethods() : [],
|
||||
'allegroDeliveryServices' => $deliveryServicesData[0],
|
||||
'allegroDeliveryServicesError' => $deliveryServicesData[1],
|
||||
'inpostDeliveryServices' => $this->inpostServicesList(),
|
||||
'inpostDeliveryServices' => array_values(array_filter(
|
||||
$deliveryServicesData[0],
|
||||
static fn(array $svc) => stripos((string) ($svc['carrierId'] ?? ''), 'inpost') !== false
|
||||
)),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
@@ -146,6 +150,7 @@ final class AllegroIntegrationController
|
||||
'orders_fetch_enabled' => (string) $request->input('orders_fetch_enabled', '0') === '1',
|
||||
'orders_fetch_start_date' => $ordersFetchStartDate,
|
||||
]);
|
||||
$this->ensureDefaultSchedulesExist();
|
||||
Flash::set('settings_success', $this->translator->get('settings.allegro.flash.saved'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set(
|
||||
@@ -544,26 +549,6 @@ final class AllegroIntegrationController
|
||||
return Response::redirect('/settings/integrations/allegro?tab=delivery');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: string, name: string}>
|
||||
*/
|
||||
private function inpostServicesList(): array
|
||||
{
|
||||
return [
|
||||
['id' => 'inpost_locker_standard', 'name' => 'Paczkomat Standard'],
|
||||
['id' => 'inpost_locker_economy', 'name' => 'Paczkomat Economy'],
|
||||
['id' => 'inpost_locker_allegro', 'name' => 'Allegro Paczkomat InPost'],
|
||||
['id' => 'inpost_courier_standard', 'name' => 'Kurier InPost'],
|
||||
['id' => 'inpost_courier_express_1000', 'name' => 'Kurier InPost Express 10:00'],
|
||||
['id' => 'inpost_courier_express_1200', 'name' => 'Kurier InPost Express 12:00'],
|
||||
['id' => 'inpost_courier_express_1700', 'name' => 'Kurier InPost Express 17:00'],
|
||||
['id' => 'inpost_courier_palette', 'name' => 'Kurier InPost Paleta'],
|
||||
['id' => 'inpost_courier_c2c', 'name' => 'Kurier InPost C2C'],
|
||||
['id' => 'inpost_courier_local_standard', 'name' => 'Kurier InPost Lokalny'],
|
||||
['id' => 'inpost_courier_local_express', 'name' => 'Kurier InPost Lokalny Express'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
* @return array{0: array<int, array<string, mixed>>, 1: string}
|
||||
@@ -862,4 +847,32 @@ final class AllegroIntegrationController
|
||||
self::STATUS_SYNC_DIRECTION_ORDERPRO_TO_ALLEGRO,
|
||||
];
|
||||
}
|
||||
|
||||
private function ensureDefaultSchedulesExist(): void
|
||||
{
|
||||
try {
|
||||
if ($this->findImportSchedule() === []) {
|
||||
$this->cronRepository->upsertSchedule(
|
||||
self::ORDERS_IMPORT_JOB_TYPE,
|
||||
self::ORDERS_IMPORT_DEFAULT_INTERVAL_SECONDS,
|
||||
self::ORDERS_IMPORT_DEFAULT_PRIORITY,
|
||||
self::ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS,
|
||||
self::ORDERS_IMPORT_DEFAULT_PAYLOAD,
|
||||
true
|
||||
);
|
||||
}
|
||||
if ($this->findStatusSyncSchedule() === []) {
|
||||
$this->cronRepository->upsertSchedule(
|
||||
self::STATUS_SYNC_JOB_TYPE,
|
||||
self::STATUS_SYNC_DEFAULT_INTERVAL_MINUTES * 60,
|
||||
self::ORDERS_IMPORT_DEFAULT_PRIORITY,
|
||||
self::ORDERS_IMPORT_DEFAULT_MAX_ATTEMPTS,
|
||||
null,
|
||||
true
|
||||
);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// non-critical: schedules will be created when user explicitly saves import settings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,12 +368,10 @@ final class AllegroOrderImportService
|
||||
$pickupAddress = is_array($pickupPoint['address'] ?? null) ? $pickupPoint['address'] : [];
|
||||
if ($deliveryAddress !== [] || $pickupAddress !== []) {
|
||||
$isPickupPointDelivery = $pickupAddress !== [];
|
||||
$name = $isPickupPointDelivery
|
||||
? $this->nullableString((string) ($pickupPoint['name'] ?? ''))
|
||||
: $this->fallbackName($deliveryAddress, 'Dostawa');
|
||||
if ($name === null) {
|
||||
$name = 'Dostawa';
|
||||
}
|
||||
|
||||
// Always use recipient's personal data from delivery.address for name/phone/email.
|
||||
// For pickup points, delivery.address still holds the recipient's data (not the machine location).
|
||||
$name = $this->fallbackName($deliveryAddress, 'Dostawa');
|
||||
|
||||
$street = $isPickupPointDelivery
|
||||
? $this->nullableString((string) ($pickupAddress['street'] ?? ''))
|
||||
@@ -388,10 +386,13 @@ final class AllegroOrderImportService
|
||||
? $this->nullableString((string) ($pickupAddress['countryCode'] ?? ''))
|
||||
: $this->nullableString((string) ($deliveryAddress['countryCode'] ?? ''));
|
||||
|
||||
$deliveryPhone = trim((string) ($deliveryAddress['phoneNumber'] ?? ''));
|
||||
$buyerPhone = trim((string) ($buyer['phoneNumber'] ?? ''));
|
||||
|
||||
$result[] = [
|
||||
'address_type' => 'delivery',
|
||||
'name' => $name,
|
||||
'phone' => $this->nullableString((string) ($deliveryAddress['phoneNumber'] ?? '')),
|
||||
'phone' => $this->nullableString($deliveryPhone !== '' ? $deliveryPhone : $buyerPhone),
|
||||
'email' => $this->nullableString((string) ($deliveryAddress['email'] ?? $buyer['email'] ?? '')),
|
||||
'street_name' => $street,
|
||||
'street_number' => null,
|
||||
|
||||
@@ -4,15 +4,21 @@ declare(strict_types=1);
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Modules\Cron\CronRepository;
|
||||
use PDO;
|
||||
use Throwable;
|
||||
|
||||
final class AllegroStatusSyncService
|
||||
{
|
||||
private const DIRECTION_ALLEGRO_TO_ORDERPRO = 'allegro_to_orderpro';
|
||||
private const DIRECTION_ORDERPRO_TO_ALLEGRO = 'orderpro_to_allegro';
|
||||
|
||||
private const FINAL_STATUSES = ['anulowane', 'cancelled', 'returned', 'zwrocone'];
|
||||
private const MAX_ORDERS_PER_RUN = 50;
|
||||
|
||||
public function __construct(
|
||||
private readonly CronRepository $cronRepository,
|
||||
private readonly AllegroOrdersSyncService $ordersSyncService
|
||||
private readonly AllegroOrderImportService $orderImportService,
|
||||
private readonly PDO $pdo
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -38,16 +44,60 @@ final class AllegroStatusSyncService
|
||||
];
|
||||
}
|
||||
|
||||
$ordersResult = $this->ordersSyncService->sync([
|
||||
'max_pages' => 3,
|
||||
'page_limit' => 50,
|
||||
'max_orders' => 100,
|
||||
]);
|
||||
$orders = $this->findOrdersNeedingStatusSync();
|
||||
|
||||
return [
|
||||
$result = [
|
||||
'ok' => true,
|
||||
'direction' => $direction,
|
||||
'orders_sync' => $ordersResult,
|
||||
'processed' => 0,
|
||||
'failed' => 0,
|
||||
'errors' => [],
|
||||
];
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$sourceOrderId = (string) ($order['source_order_id'] ?? '');
|
||||
|
||||
try {
|
||||
$this->orderImportService->importSingleOrder($sourceOrderId);
|
||||
$result['processed']++;
|
||||
} catch (Throwable $exception) {
|
||||
$result['failed']++;
|
||||
$errors = is_array($result['errors']) ? $result['errors'] : [];
|
||||
if (count($errors) < 20) {
|
||||
$errors[] = [
|
||||
'source_order_id' => $sourceOrderId,
|
||||
'error' => $exception->getMessage(),
|
||||
];
|
||||
}
|
||||
$result['errors'] = $errors;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function findOrdersNeedingStatusSync(): array
|
||||
{
|
||||
$placeholders = implode(',', array_fill(0, count(self::FINAL_STATUSES), '?'));
|
||||
|
||||
try {
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, source_order_id, external_status_id
|
||||
FROM orders
|
||||
WHERE source = ?
|
||||
AND LOWER(COALESCE(external_status_id, "")) NOT IN (' . $placeholders . ')
|
||||
ORDER BY source_updated_at DESC
|
||||
LIMIT ' . self::MAX_ORDERS_PER_RUN
|
||||
);
|
||||
$params = array_merge(['allegro'], self::FINAL_STATUSES);
|
||||
$statement->execute($params);
|
||||
|
||||
return $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,17 +104,11 @@ final class AllegroShipmentService
|
||||
|
||||
$codAmount = (float) ($formData['cod_amount'] ?? 0);
|
||||
if ($codAmount > 0) {
|
||||
$cod = [
|
||||
// Allegro WZA manages COD funds internally – iban/ownerName are not accepted
|
||||
$apiPayload['input']['cashOnDelivery'] = [
|
||||
'amount' => number_format($codAmount, 2, '.', ''),
|
||||
'currency' => strtoupper(trim((string) ($formData['cod_currency'] ?? 'PLN'))),
|
||||
];
|
||||
if (trim($company['bank_owner_name']) !== '') {
|
||||
$cod['ownerName'] = $company['bank_owner_name'];
|
||||
}
|
||||
if (trim($company['bank_account']) !== '') {
|
||||
$cod['iban'] = $company['bank_account'];
|
||||
}
|
||||
$apiPayload['input']['cashOnDelivery'] = $cod;
|
||||
}
|
||||
|
||||
$credentialsId = trim((string) ($formData['credentials_id'] ?? ''));
|
||||
@@ -266,14 +260,28 @@ final class AllegroShipmentService
|
||||
$filePath = $dir . '/' . $filename;
|
||||
file_put_contents($filePath, $binary);
|
||||
|
||||
$relativePath = 'labels/' . $filename;
|
||||
$this->packages->update($packageId, [
|
||||
$updateFields = [
|
||||
'status' => 'label_ready',
|
||||
'label_path' => $relativePath,
|
||||
]);
|
||||
'label_path' => 'labels/' . $filename,
|
||||
];
|
||||
|
||||
// Refresh tracking number if not yet saved (may not have been available at creation time)
|
||||
if (trim((string) ($package['tracking_number'] ?? '')) === '') {
|
||||
try {
|
||||
$details = $this->apiClient->getShipmentDetails($env, $accessToken, $shipmentId);
|
||||
$trackingNumber = trim((string) ($details['waybill'] ?? ''));
|
||||
if ($trackingNumber !== '') {
|
||||
$updateFields['tracking_number'] = $trackingNumber;
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// non-critical – label is still saved
|
||||
}
|
||||
}
|
||||
|
||||
$this->packages->update($packageId, $updateFields);
|
||||
|
||||
return [
|
||||
'label_path' => $relativePath,
|
||||
'label_path' => 'labels/' . $filename,
|
||||
'full_path' => $filePath,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -68,6 +68,11 @@ final class ShipmentController
|
||||
$deliveryServicesError = $exception->getMessage();
|
||||
}
|
||||
|
||||
$inpostServices = array_values(array_filter(
|
||||
$deliveryServices,
|
||||
static fn(array $svc) => stripos((string) ($svc['carrierId'] ?? ''), 'inpost') !== false
|
||||
));
|
||||
|
||||
$flashSuccess = (string) ($_SESSION['shipment_flash_success'] ?? '');
|
||||
$flashError = (string) ($_SESSION['shipment_flash_error'] ?? '');
|
||||
unset($_SESSION['shipment_flash_success'], $_SESSION['shipment_flash_error']);
|
||||
@@ -96,32 +101,12 @@ final class ShipmentController
|
||||
'flashSuccess' => $flashSuccess,
|
||||
'flashError' => $flashError,
|
||||
'deliveryMapping' => $deliveryMapping,
|
||||
'inpostServices' => $this->inpostServicesList(),
|
||||
'inpostServices' => $inpostServices,
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: string, name: string}>
|
||||
*/
|
||||
private function inpostServicesList(): array
|
||||
{
|
||||
return [
|
||||
['id' => 'inpost_locker_standard', 'name' => 'Paczkomat Standard'],
|
||||
['id' => 'inpost_locker_economy', 'name' => 'Paczkomat Economy'],
|
||||
['id' => 'inpost_locker_allegro', 'name' => 'Allegro Paczkomat InPost'],
|
||||
['id' => 'inpost_courier_standard', 'name' => 'Kurier InPost'],
|
||||
['id' => 'inpost_courier_express_1000', 'name' => 'Kurier InPost Express 10:00'],
|
||||
['id' => 'inpost_courier_express_1200', 'name' => 'Kurier InPost Express 12:00'],
|
||||
['id' => 'inpost_courier_express_1700', 'name' => 'Kurier InPost Express 17:00'],
|
||||
['id' => 'inpost_courier_palette', 'name' => 'Kurier InPost Paleta'],
|
||||
['id' => 'inpost_courier_c2c', 'name' => 'Kurier InPost C2C'],
|
||||
['id' => 'inpost_courier_local_standard', 'name' => 'Kurier InPost Lokalny'],
|
||||
['id' => 'inpost_courier_local_express', 'name' => 'Kurier InPost Lokalny Express'],
|
||||
];
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$orderId = max(0, (int) $request->input('id', 0));
|
||||
@@ -135,6 +120,10 @@ final class ShipmentController
|
||||
return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
|
||||
}
|
||||
|
||||
$user = $this->auth->user();
|
||||
$actorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : null;
|
||||
$actorName = ($actorName !== null && $actorName !== '') ? $actorName : null;
|
||||
|
||||
try {
|
||||
$result = $this->shipmentService->createShipment($orderId, [
|
||||
'delivery_method_id' => (string) $request->input('delivery_method_id', ''),
|
||||
@@ -163,9 +152,25 @@ final class ShipmentController
|
||||
]);
|
||||
|
||||
$packageId = (int) ($result['package_id'] ?? 0);
|
||||
$this->ordersRepository->recordActivity(
|
||||
$orderId,
|
||||
'shipment_created',
|
||||
'Zlecono utworzenie przesylki WZA (ID paczki: ' . $packageId . ')',
|
||||
['package_id' => $packageId, 'command_id' => $result['command_id'] ?? null],
|
||||
'user',
|
||||
$actorName
|
||||
);
|
||||
$_SESSION['shipment_flash_success'] = 'Komenda tworzenia przesylki wyslana. Sprawdz status.';
|
||||
return Response::redirect('/orders/' . $orderId . '/shipment/prepare?check=' . $packageId);
|
||||
} catch (Throwable $exception) {
|
||||
$this->ordersRepository->recordActivity(
|
||||
$orderId,
|
||||
'shipment_error',
|
||||
'Blad tworzenia przesylki: ' . $exception->getMessage(),
|
||||
null,
|
||||
'user',
|
||||
$actorName
|
||||
);
|
||||
$_SESSION['shipment_flash_error'] = 'Blad tworzenia przesylki: ' . $exception->getMessage();
|
||||
return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
|
||||
}
|
||||
@@ -181,6 +186,16 @@ final class ShipmentController
|
||||
|
||||
try {
|
||||
$result = $this->shipmentService->checkCreationStatus($packageId);
|
||||
|
||||
if (($result['status'] ?? '') === 'created') {
|
||||
try {
|
||||
$this->shipmentService->downloadLabel($packageId, $this->storagePath);
|
||||
$result['status'] = 'label_ready';
|
||||
} catch (Throwable) {
|
||||
// label generation failed – return created so user can retry manually
|
||||
}
|
||||
}
|
||||
|
||||
return Response::json($result);
|
||||
} catch (Throwable $exception) {
|
||||
return Response::json(['status' => 'error', 'error' => $exception->getMessage()]);
|
||||
@@ -201,6 +216,10 @@ final class ShipmentController
|
||||
return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
|
||||
}
|
||||
|
||||
$user = $this->auth->user();
|
||||
$actorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : null;
|
||||
$actorName = ($actorName !== null && $actorName !== '') ? $actorName : null;
|
||||
|
||||
try {
|
||||
$result = $this->shipmentService->downloadLabel($packageId, $this->storagePath);
|
||||
$fullPath = (string) ($result['full_path'] ?? '');
|
||||
@@ -210,6 +229,15 @@ final class ShipmentController
|
||||
$contentType = $labelFormat === 'ZPL' ? 'application/octet-stream' : 'application/pdf';
|
||||
$filename = basename($fullPath);
|
||||
|
||||
$this->ordersRepository->recordActivity(
|
||||
$orderId,
|
||||
'shipment_label_downloaded',
|
||||
'Pobrano etykiete dla przesylki #' . $packageId,
|
||||
['package_id' => $packageId, 'filename' => $filename],
|
||||
'user',
|
||||
$actorName
|
||||
);
|
||||
|
||||
return new Response(
|
||||
(string) file_get_contents($fullPath),
|
||||
200,
|
||||
@@ -222,6 +250,14 @@ final class ShipmentController
|
||||
|
||||
$_SESSION['shipment_flash_success'] = 'Etykieta pobrana.';
|
||||
} catch (Throwable $exception) {
|
||||
$this->ordersRepository->recordActivity(
|
||||
$orderId,
|
||||
'shipment_error',
|
||||
'Blad pobierania etykiety (paczka #' . $packageId . '): ' . $exception->getMessage(),
|
||||
null,
|
||||
'user',
|
||||
$actorName
|
||||
);
|
||||
$_SESSION['shipment_flash_error'] = 'Blad pobierania etykiety: ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user