'Nieopłacone', '1' => 'Częściowo opłacone', '2' => 'Opłacone', ]; private const ALLOWED_ACTION_TYPES = ['send_email', 'issue_receipt', 'update_shipment_status', 'update_order_status']; 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 SHIPMENT_STATUS_OPTIONS = [ 'registered' => ['label' => 'Przesylka zarejestrowana', 'statuses' => ['created', 'confirmed']], 'ready_for_pickup' => ['label' => 'Przesylka do odbioru', 'statuses' => ['ready_for_pickup']], 'dropped_at_point' => ['label' => 'Przesylka nadana w punkcie', 'statuses' => ['confirmed', 'in_transit']], 'picked_up' => ['label' => 'Przesylka odebrana', 'statuses' => ['delivered']], 'cancelled' => ['label' => 'Przesylka anulowana', 'statuses' => ['cancelled']], 'unclaimed' => ['label' => 'Przesylka nieodebrana', 'statuses' => ['problem']], 'picked_up_return' => ['label' => 'Przesylka odebrana (zwrot)', 'statuses' => ['returned']], ]; public function __construct( private readonly Template $template, private readonly Translator $translator, private readonly AuthService $auth, private readonly AutomationRepository $repository, private readonly AutomationExecutionLogRepository $executionLogs, private readonly ReceiptConfigRepository $receiptConfigs ) { } public function index(Request $request): Response { $rules = $this->repository->findAll(); $historyFilters = $this->extractHistoryFilters($request); $historyPage = max(1, (int) $request->input('history_page', 1)); $historyTotal = $this->executionLogs->count($historyFilters); $historyTotalPages = max(1, (int) ceil($historyTotal / self::HISTORY_PER_PAGE)); if ($historyPage > $historyTotalPages) { $historyPage = $historyTotalPages; } $historyEntries = $this->executionLogs->paginate($historyFilters, $historyPage, self::HISTORY_PER_PAGE); $activeTab = $this->resolveActiveTab($request, $historyFilters); $html = $this->template->render('automation/index', [ 'title' => 'Zadania automatyczne', 'activeMenu' => 'settings', 'activeSettings' => 'automation', 'user' => $this->auth->user(), 'csrfToken' => Csrf::token(), 'rules' => $rules, 'activeTab' => $activeTab, 'historyEntries' => $historyEntries, 'historyFilters' => $historyFilters, 'historyEventTypes' => array_values(array_unique(array_merge(self::ALLOWED_EVENTS, $this->executionLogs->listEventTypes()))), 'historyRuleOptions' => $this->repository->listRuleOptions(), 'historyPagination' => [ 'page' => $historyPage, 'per_page' => self::HISTORY_PER_PAGE, 'total' => $historyTotal, 'total_pages' => $historyTotalPages, ], 'successMessage' => Flash::get('settings.automation.success', ''), 'errorMessage' => Flash::get('settings.automation.error', ''), ], 'layouts/app'); return Response::html($html); } public function create(Request $request): Response { return $this->renderForm(null); } public function edit(Request $request): Response { $id = (int) $request->input('id', '0'); $rule = $id > 0 ? $this->repository->findById($id) : null; if ($rule === null) { Flash::set('settings.automation.error', 'Nie znaleziono reguly'); return Response::redirect('/settings/automation'); } return $this->renderForm($rule); } public function store(Request $request): Response { $error = $this->validateCsrf($request); if ($error !== null) { return $error; } $validationError = $this->validateInput($request); if ($validationError !== null) { return $this->renderForm($this->buildRuleFromRequest($request), $validationError); } try { $this->repository->create( $this->extractRuleData($request), $this->extractConditions($request), $this->extractActions($request) ); Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo utworzone'); } catch (Throwable) { return $this->renderForm($this->buildRuleFromRequest($request), 'Blad zapisu zadania automatycznego'); } return Response::redirect('/settings/automation'); } public function update(Request $request): Response { $error = $this->validateCsrf($request); if ($error !== null) { return $error; } $id = (int) $request->input('id', '0'); if ($id <= 0) { Flash::set('settings.automation.error', 'Nieprawidlowy identyfikator'); return Response::redirect('/settings/automation'); } $validationError = $this->validateInput($request); if ($validationError !== null) { return $this->renderForm($this->buildRuleFromRequest($request, $id), $validationError); } try { $this->repository->update( $id, $this->extractRuleData($request), $this->extractConditions($request), $this->extractActions($request) ); Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo zaktualizowane'); } catch (Throwable) { return $this->renderForm($this->buildRuleFromRequest($request, $id), 'Blad aktualizacji zadania automatycznego'); } return Response::redirect('/settings/automation'); } public function destroy(Request $request): Response { $error = $this->validateCsrf($request); if ($error !== null) { return $error; } $id = (int) $request->input('id', '0'); if ($id <= 0) { Flash::set('settings.automation.error', 'Nieprawidlowy identyfikator'); return Response::redirect('/settings/automation'); } try { $this->repository->delete($id); Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo usuniete'); } catch (Throwable) { Flash::set('settings.automation.error', 'Blad usuwania zadania automatycznego'); } return Response::redirect('/settings/automation'); } public function duplicate(Request $request): Response { $error = $this->validateCsrf($request); if ($error !== null) { return $error; } $id = (int) $request->input('id', '0'); if ($id <= 0) { Flash::set('settings.automation.error', 'Nieprawidlowy identyfikator'); return Response::redirect('/settings/automation'); } try { $this->repository->duplicate($id); Flash::set('settings.automation.success', 'Zadanie zostalo zduplikowane'); } catch (Throwable) { Flash::set('settings.automation.error', 'Blad duplikowania zadania'); } return Response::redirect('/settings/automation'); } public function toggleStatus(Request $request): Response { $error = $this->validateCsrf($request); if ($error !== null) { return $error; } $id = (int) $request->input('id', '0'); if ($id <= 0) { Flash::set('settings.automation.error', 'Nieprawidlowy identyfikator'); return Response::redirect('/settings/automation'); } try { $this->repository->toggleActive($id); Flash::set('settings.automation.success', 'Status zadania zostal zmieniony'); } catch (Throwable) { Flash::set('settings.automation.error', 'Blad zmiany statusu'); } return Response::redirect('/settings/automation'); } private function renderForm(?array $rule, string $errorMessage = ''): Response { $html = $this->template->render('automation/form', [ 'title' => $rule !== null && isset($rule['id']) ? 'Edytuj zadanie automatyczne' : 'Nowe zadanie automatyczne', 'activeMenu' => 'settings', 'activeSettings' => 'automation', 'user' => $this->auth->user(), 'csrfToken' => Csrf::token(), 'rule' => $rule, 'integrations' => $this->repository->listOrderIntegrations(), 'emailTemplates' => $this->repository->listEmailTemplates(), 'eventTypes' => self::ALLOWED_EVENTS, 'conditionTypes' => self::ALLOWED_CONDITION_TYPES, 'actionTypes' => self::ALLOWED_ACTION_TYPES, 'recipientOptions' => self::ALLOWED_RECIPIENTS, 'receiptConfigs' => $this->listActiveReceiptConfigs(), 'receiptIssueDateModes' => self::ALLOWED_RECEIPT_ISSUE_DATE_MODES, 'receiptDuplicatePolicies' => self::ALLOWED_RECEIPT_DUPLICATE_POLICIES, 'shipmentStatusOptions' => self::SHIPMENT_STATUS_OPTIONS, 'paymentStatusOptions' => self::PAYMENT_STATUS_OPTIONS, 'orderStatusOptions' => $this->repository->listActiveOrderStatuses(), 'errorMessage' => $errorMessage !== '' ? $errorMessage : Flash::get('settings.automation.error', ''), ], 'layouts/app'); return Response::html($html); } private function buildRuleFromRequest(Request $request, ?int $id = null): array { $raw = $request->input('conditions', []); $conditions = []; if (is_array($raw)) { foreach ($raw as $cond) { if (!is_array($cond)) { continue; } $type = (string) ($cond['type'] ?? ''); $value = []; if ($type === 'integration') { $value = ['integration_ids' => is_array($cond['integration_ids'] ?? null) ? $cond['integration_ids'] : []]; } elseif ($type === 'shipment_status') { $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 === 'order_status') { $value = ['order_status_codes' => is_array($cond['order_status_codes'] ?? null) ? $cond['order_status_codes'] : []]; } elseif ($type === 'days_in_status') { $value = ['days' => max(1, (int) ($cond['days'] ?? 0))]; } $conditions[] = ['condition_type' => $type, 'condition_value' => $value]; } } $rawActions = $request->input('actions', []); $actions = []; if (is_array($rawActions)) { foreach ($rawActions as $act) { if (!is_array($act)) { continue; } $type = (string) ($act['type'] ?? ''); $config = $act; unset($config['type']); $actions[] = ['action_type' => $type, 'action_config' => $config]; } } $rule = [ 'name' => trim((string) $request->input('name', '')), 'event_type' => (string) $request->input('event_type', ''), 'is_active' => $request->input('is_active', null) !== null ? 1 : 0, 'conditions' => $conditions, 'actions' => $actions, ]; if ($id !== null) { $rule['id'] = $id; } return $rule; } private function validateCsrf(Request $request): ?Response { if (!Csrf::validate((string) $request->input('_token', ''))) { Flash::set('settings.automation.error', 'Nieprawidlowy token CSRF'); return Response::redirect('/settings/automation'); } return null; } private function validateInput(Request $request): ?string { $name = trim((string) $request->input('name', '')); if ($name === '' || mb_strlen($name) > 128) { return 'Nazwa jest wymagana (maks. 128 znakow)'; } $eventType = (string) $request->input('event_type', ''); if (!in_array($eventType, self::ALLOWED_EVENTS, true)) { return 'Nieprawidlowy typ zdarzenia'; } $conditions = $this->extractConditions($request); if (count($conditions) === 0) { return 'Wymagany jest co najmniej jeden warunek'; } $actions = $this->extractActions($request); if (count($actions) === 0) { return 'Wymagana jest co najmniej jedna akcja'; } return null; } /** * @return array */ private function extractRuleData(Request $request): array { return [ 'name' => trim((string) $request->input('name', '')), 'event_type' => (string) $request->input('event_type', ''), 'is_active' => $request->input('is_active', null) !== null ? 1 : 0, ]; } /** * @return list}> */ private function extractConditions(Request $request): array { $raw = $request->input('conditions', []); if (!is_array($raw)) { return []; } $result = []; foreach ($raw as $condition) { if (!is_array($condition)) { continue; } $type = (string) ($condition['type'] ?? ''); if (!in_array($type, self::ALLOWED_CONDITION_TYPES, true)) { continue; } $value = $this->parseConditionValue($type, $condition); if ($value === null) { continue; } $result[] = ['type' => $type, 'value' => $value]; } return $result; } /** * @param array $condition * @return array|null */ private function parseConditionValue(string $type, array $condition): ?array { if ($type === 'integration') { $ids = $condition['integration_ids'] ?? []; if (!is_array($ids)) { $ids = []; } $integrationIds = array_values(array_filter( array_map('intval', $ids), static fn (int $id): bool => $id > 0 )); return count($integrationIds) > 0 ? ['integration_ids' => $integrationIds] : null; } if ($type === 'shipment_status') { $keys = $condition['shipment_status_keys'] ?? []; if (!is_array($keys)) { $keys = []; } $allowedKeys = array_keys(self::SHIPMENT_STATUS_OPTIONS); $statusKeys = 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($statusKeys) > 0 ? ['status_keys' => array_values(array_unique($statusKeys))] : null; } if ($type === 'payment_status') { $keys = $condition['payment_status_keys'] ?? []; if (!is_array($keys)) { $keys = []; } $allowedKeys = array_map('strval', array_keys(self::PAYMENT_STATUS_OPTIONS)); $statusKeys = 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($statusKeys) > 0 ? ['status_keys' => array_values(array_unique($statusKeys))] : null; } if ($type === 'order_status') { $codes = $condition['order_status_codes'] ?? []; if (!is_array($codes)) { $codes = []; } $availableCodes = array_map( static fn (array $row): string => strtolower(trim((string) ($row['code'] ?? ''))), $this->repository->listActiveOrderStatuses() ); $statusCodes = array_values(array_filter( array_map(static fn (mixed $code): string => strtolower(trim((string) $code)), $codes), static fn (string $code): bool => $code !== '' && in_array($code, $availableCodes, true) )); return count($statusCodes) > 0 ? ['order_status_codes' => array_values(array_unique($statusCodes))] : null; } if ($type === 'days_in_status') { $days = (int) ($condition['days'] ?? 0); return $days >= 1 ? ['days' => $days] : null; } return null; } /** * @return list}> */ private function extractActions(Request $request): array { $raw = $request->input('actions', []); if (!is_array($raw)) { return []; } $result = []; foreach ($raw as $action) { if (!is_array($action)) { continue; } $type = (string) ($action['type'] ?? ''); if (!in_array($type, self::ALLOWED_ACTION_TYPES, true)) { continue; } $config = $this->parseActionConfig($type, $action); if ($config === null) { continue; } $result[] = ['type' => $type, 'config' => $config]; } return $result; } /** * @param array $action * @return array|null */ private function parseActionConfig(string $type, array $action): ?array { if ($type === 'send_email') { $templateId = (int) ($action['template_id'] ?? 0); $recipient = (string) ($action['recipient'] ?? ''); if ($templateId <= 0 || !in_array($recipient, self::ALLOWED_RECIPIENTS, true)) { return null; } return ['template_id' => $templateId, 'recipient' => $recipient]; } if ($type === 'issue_receipt') { $configId = (int) ($action['receipt_config_id'] ?? 0); $issueDateMode = (string) ($action['issue_date_mode'] ?? ''); $duplicatePolicy = (string) ($action['duplicate_policy'] ?? ''); if ( $configId <= 0 || !in_array($issueDateMode, self::ALLOWED_RECEIPT_ISSUE_DATE_MODES, true) || !in_array($duplicatePolicy, self::ALLOWED_RECEIPT_DUPLICATE_POLICIES, true) ) { return null; } return [ 'receipt_config_id' => $configId, 'issue_date_mode' => $issueDateMode, 'duplicate_policy' => $duplicatePolicy, ]; } if ($type === 'update_shipment_status') { $statusKey = trim((string) ($action['shipment_status_key'] ?? '')); if (!array_key_exists($statusKey, self::SHIPMENT_STATUS_OPTIONS)) { return null; } return ['status_key' => $statusKey]; } if ($type === 'update_order_status') { $statusCode = trim((string) ($action['order_status_code'] ?? '')); if ($statusCode === '') { return null; } $availableCodes = array_map( static fn (array $row): string => trim((string) ($row['code'] ?? '')), $this->repository->listActiveOrderStatuses() ); if (!in_array($statusCode, $availableCodes, true)) { return null; } return ['status_code' => $statusCode]; } return null; } /** * @return list */ private function listActiveReceiptConfigs(): array { $all = $this->receiptConfigs->listAll(); $result = []; foreach ($all as $config) { if ((int) ($config['is_active'] ?? 0) !== 1) { continue; } $configId = (int) ($config['id'] ?? 0); if ($configId <= 0) { continue; } $result[] = [ 'id' => $configId, 'name' => (string) ($config['name'] ?? ''), 'number_format' => (string) ($config['number_format'] ?? ''), ]; } return $result; } /** * @return array{event_type:string,execution_status:string,rule_id:int,order_id:int,date_from:string,date_to:string} */ private function extractHistoryFilters(Request $request): array { return [ 'event_type' => trim((string) $request->input('history_event_type', '')), 'execution_status' => trim((string) $request->input('history_status', '')), 'rule_id' => max(0, (int) $request->input('history_rule_id', 0)), 'order_id' => max(0, (int) $request->input('history_order_id', 0)), 'date_from' => trim((string) $request->input('history_date_from', '')), 'date_to' => trim((string) $request->input('history_date_to', '')), ]; } /** * @param array{event_type:string,execution_status:string,rule_id:int,order_id:int,date_from:string,date_to:string} $historyFilters */ private function resolveActiveTab(Request $request, array $historyFilters): string { $activeTab = trim((string) $request->input('tab', 'settings')); if ($activeTab === 'history') { return 'history'; } if ((int) $request->input('history_page', 0) > 1) { return 'history'; } if ($this->hasHistoryFilters($historyFilters)) { return 'history'; } return 'settings'; } /** * @param array{event_type:string,execution_status:string,rule_id:int,order_id:int,date_from:string,date_to:string} $historyFilters */ private function hasHistoryFilters(array $historyFilters): bool { return $historyFilters['event_type'] !== '' || $historyFilters['execution_status'] !== '' || $historyFilters['rule_id'] > 0 || $historyFilters['order_id'] > 0 || $historyFilters['date_from'] !== '' || $historyFilters['date_to'] !== ''; } }