feat(28-shipment-tracking-ui): badge'e statusow dostawy, linki sledzenia, ustawienia interwalu trackingu
- Kolorowe badge'e statusow dostawy w tabelach paczek (show.php + prepare.php) - Link sledzenia z carrier detection (InPost, Apaczka, Orlen, Allegro, Google fallback) - Sekcja Status dostawy w boksie Platnosc i wysylka - Ustawienie interwalu trackingu crona (5-120 min) w zakladce Ustawienia - Tekstowe mapowania statusow Apaczka API (NEW, CONFIRMED, etc.) - Fix: use-statements ApaczkaShipmentService (pre-existing bug) - Fix: pickup date normalization (next day po 16:00) - Fix: przycisk Pobierz etykiete (POST zamiast link do prepare) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -370,6 +370,31 @@ final class CronRepository
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateScheduleInterval(string $jobType, int $intervalSeconds): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE cron_schedules
|
||||
SET interval_seconds = :interval_seconds,
|
||||
updated_at = NOW()
|
||||
WHERE job_type = :job_type'
|
||||
);
|
||||
$statement->execute([
|
||||
'interval_seconds' => max(1, $intervalSeconds),
|
||||
'job_type' => trim($jobType),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getScheduleInterval(string $jobType): ?int
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT interval_seconds FROM cron_schedules WHERE job_type = :job_type LIMIT 1'
|
||||
);
|
||||
$statement->execute(['job_type' => trim($jobType)]);
|
||||
$value = $statement->fetchColumn();
|
||||
|
||||
return $value !== false ? (int) $value : null;
|
||||
}
|
||||
|
||||
private function getSettingValue(string $key): ?string
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -55,6 +55,9 @@ final class CronSettingsController
|
||||
$pastPage = 1;
|
||||
}
|
||||
|
||||
$trackingIntervalSeconds = $this->cronRepository->getScheduleInterval('shipment_tracking_sync');
|
||||
$trackingIntervalMinutes = $trackingIntervalSeconds !== null ? (int) ($trackingIntervalSeconds / 60) : 15;
|
||||
|
||||
$html = $this->template->render('settings/cron', [
|
||||
'title' => $this->translator->get('settings.cron.title'),
|
||||
'activeMenu' => 'settings',
|
||||
@@ -63,6 +66,7 @@ final class CronSettingsController
|
||||
'csrfToken' => Csrf::token(),
|
||||
'runOnWeb' => $runOnWeb,
|
||||
'webLimit' => $webLimit,
|
||||
'trackingIntervalMinutes' => $trackingIntervalMinutes,
|
||||
'schedules' => $schedules,
|
||||
'futureJobs' => $futureJobs,
|
||||
'pastJobs' => $pastJobs,
|
||||
@@ -91,9 +95,13 @@ final class CronSettingsController
|
||||
$webLimitRaw = (int) $request->input('cron_web_limit', $this->webLimitDefault);
|
||||
$webLimit = max(1, min(100, $webLimitRaw));
|
||||
|
||||
$trackingMinutesRaw = (int) $request->input('tracking_interval_minutes', 15);
|
||||
$trackingMinutes = max(5, min(120, $trackingMinutesRaw));
|
||||
|
||||
try {
|
||||
$this->cronRepository->upsertSetting('cron_run_on_web', $runOnWeb ? '1' : '0');
|
||||
$this->cronRepository->upsertSetting('cron_web_limit', (string) $webLimit);
|
||||
$this->cronRepository->updateScheduleInterval('shipment_tracking_sync', $trackingMinutes * 60);
|
||||
Flash::set('settings_success', $this->translator->get('settings.cron.flash.saved'));
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.cron.flash.save_failed') . ' ' . $exception->getMessage());
|
||||
|
||||
@@ -7,8 +7,8 @@ use App\Modules\Orders\OrdersRepository;
|
||||
use App\Modules\Settings\ApaczkaApiClient;
|
||||
use App\Modules\Settings\ApaczkaIntegrationRepository;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
use AppCorexceptionsIntegrationConfigException;
|
||||
use AppCorexceptionsShipmentException;
|
||||
use App\Core\Exceptions\IntegrationConfigException;
|
||||
use App\Core\Exceptions\ShipmentException;
|
||||
use Throwable;
|
||||
|
||||
final class ApaczkaShipmentService implements ShipmentProviderInterface
|
||||
@@ -117,6 +117,9 @@ final class ApaczkaShipmentService implements ShipmentProviderInterface
|
||||
}
|
||||
|
||||
$carrierLabel = trim((string) ($serviceDefinition['name'] ?? ''));
|
||||
if ($carrierLabel === '') {
|
||||
$carrierLabel = (string) ($this->packages->resolveCarrierName('apaczka', $deliveryMethodId) ?? '');
|
||||
}
|
||||
|
||||
$packageId = $this->packages->create([
|
||||
'order_id' => $orderId,
|
||||
@@ -724,6 +727,13 @@ final class ApaczkaShipmentService implements ShipmentProviderInterface
|
||||
$pickupDate = date('Y-m-d');
|
||||
}
|
||||
$pickupDate = $this->normalizeCourierPickupDate($pickupDate);
|
||||
|
||||
// If pickup is today and current time past safe cutoff, move to next business day
|
||||
if ($pickupDate === date('Y-m-d') && (int) date('H') >= 16) {
|
||||
$nextDay = date('Y-m-d', strtotime('+1 day', strtotime($pickupDate)));
|
||||
$pickupDate = $this->normalizeCourierPickupDate($nextDay);
|
||||
}
|
||||
|
||||
$hoursFrom = trim((string) ($formData['pickup_hours_from'] ?? ''));
|
||||
if (preg_match('/^\d{2}:\d{2}$/', $hoursFrom) !== 1) {
|
||||
$hoursFrom = '09:00';
|
||||
@@ -748,6 +758,11 @@ final class ApaczkaShipmentService implements ShipmentProviderInterface
|
||||
$ts = time();
|
||||
}
|
||||
|
||||
$today = date('Y-m-d');
|
||||
if (date('Y-m-d', $ts) < $today) {
|
||||
$ts = time();
|
||||
}
|
||||
|
||||
// Apaczka rejects Sunday as pickup date.
|
||||
$weekday = (int) date('N', $ts);
|
||||
if ($weekday === 7) {
|
||||
|
||||
@@ -131,6 +131,18 @@ final class DeliveryStatus
|
||||
'8' => self::PROBLEM,
|
||||
'9' => self::READY_FOR_PICKUP,
|
||||
'10' => self::IN_TRANSIT,
|
||||
'NEW' => self::CREATED,
|
||||
'PENDING' => self::CREATED,
|
||||
'CONFIRMED' => self::CONFIRMED,
|
||||
'PICKED_UP' => self::IN_TRANSIT,
|
||||
'IN_TRANSIT' => self::IN_TRANSIT,
|
||||
'OUT_FOR_DELIVERY' => self::OUT_FOR_DELIVERY,
|
||||
'DELIVERED' => self::DELIVERED,
|
||||
'RETURNED' => self::RETURNED,
|
||||
'CANCELLED' => self::CANCELLED,
|
||||
'ERROR' => self::PROBLEM,
|
||||
'WAITING_FOR_PICKUP' => self::READY_FOR_PICKUP,
|
||||
'REDIRECT' => self::IN_TRANSIT,
|
||||
];
|
||||
|
||||
private const APACZKA_DESCRIPTIONS = [
|
||||
@@ -145,6 +157,18 @@ final class DeliveryStatus
|
||||
'8' => 'Błąd zamówienia',
|
||||
'9' => 'Oczekuje na odbiór w punkcie',
|
||||
'10' => 'Przekierowana',
|
||||
'NEW' => 'Zamówienie utworzone',
|
||||
'PENDING' => 'Oczekuje na przetworzenie',
|
||||
'CONFIRMED' => 'Zamówienie potwierdzone',
|
||||
'PICKED_UP' => 'Odebrana przez kuriera',
|
||||
'IN_TRANSIT' => 'W transporcie',
|
||||
'OUT_FOR_DELIVERY' => 'W doręczeniu',
|
||||
'DELIVERED' => 'Doręczona',
|
||||
'RETURNED' => 'Zwrócona do nadawcy',
|
||||
'CANCELLED' => 'Anulowana',
|
||||
'ERROR' => 'Błąd zamówienia',
|
||||
'WAITING_FOR_PICKUP' => 'Oczekuje na odbiór w punkcie',
|
||||
'REDIRECT' => 'Przekierowana',
|
||||
];
|
||||
|
||||
private const ALLEGRO_MAP = [
|
||||
@@ -200,4 +224,61 @@ final class DeliveryStatus
|
||||
{
|
||||
return in_array($status, self::TERMINAL_STATUSES, true);
|
||||
}
|
||||
|
||||
public static function trackingUrl(string $provider, string $trackingNumber, string $carrierId = ''): ?string
|
||||
{
|
||||
$number = trim($trackingNumber);
|
||||
if ($number === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$encoded = rawurlencode($number);
|
||||
|
||||
if ($provider === 'inpost') {
|
||||
return 'https://inpost.pl/sledzenie-przesylek?number=' . $encoded;
|
||||
}
|
||||
|
||||
if ($provider === 'allegro_wza') {
|
||||
return 'https://allegro.pl/przesylka/' . $encoded;
|
||||
}
|
||||
|
||||
if ($carrierId !== '') {
|
||||
$url = self::matchCarrierByName($encoded, strtolower(trim($carrierId)));
|
||||
if ($url !== null) {
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
return 'https://www.google.com/search?q=' . $encoded . '+sledzenie+przesylki';
|
||||
}
|
||||
|
||||
private static function matchCarrierByName(string $encoded, string $carrier): ?string
|
||||
{
|
||||
if (str_contains($carrier, 'dpd')) {
|
||||
return 'https://tracktrace.dpd.com.pl/parcelDetails?p1=' . $encoded;
|
||||
}
|
||||
if (str_contains($carrier, 'dhl')) {
|
||||
return 'https://www.dhl.com/pl-pl/home/sledzenie-przesylki.html?tracking-id=' . $encoded;
|
||||
}
|
||||
if (str_contains($carrier, 'inpost') || str_contains($carrier, 'paczkomat')) {
|
||||
return 'https://inpost.pl/sledzenie-przesylek?number=' . $encoded;
|
||||
}
|
||||
if (str_contains($carrier, 'orlen') || str_contains($carrier, 'ruch')) {
|
||||
return 'https://www.orlenpaczka.pl/sledz-paczke/?numer=' . $encoded;
|
||||
}
|
||||
if (str_contains($carrier, 'poczta') || str_contains($carrier, 'pocztex')) {
|
||||
return 'https://emonitoring.poczta-polska.pl/?numer=' . $encoded;
|
||||
}
|
||||
if (str_contains($carrier, 'ups')) {
|
||||
return 'https://www.ups.com/track?tracknum=' . $encoded;
|
||||
}
|
||||
if (str_contains($carrier, 'fedex')) {
|
||||
return 'https://www.fedex.com/fedextrack/?trknbr=' . $encoded;
|
||||
}
|
||||
if (str_contains($carrier, 'gls')) {
|
||||
return 'https://gls-group.com/PL/pl/sledzenie-paczek?match=' . $encoded;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +181,24 @@ final class ShipmentPackageRepository
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
|
||||
public function resolveCarrierName(string $provider, string $serviceId): ?string
|
||||
{
|
||||
if ($serviceId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT provider_service_name FROM carrier_delivery_method_mappings
|
||||
WHERE provider = :provider AND provider_service_id = :service_id
|
||||
LIMIT 1'
|
||||
);
|
||||
$statement->execute(['provider' => $provider, 'service_id' => $serviceId]);
|
||||
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||
$name = trim((string) ($row['provider_service_name'] ?? ''));
|
||||
|
||||
return $name !== '' ? $name : null;
|
||||
}
|
||||
|
||||
private function nullStr(string $value): ?string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
|
||||
Reference in New Issue
Block a user