query($sql); $products = []; while ($row = $db->fetch_assoc($result)) { $products[] = (int)$row['product']; } return $products; } public function structure(): void { selfserve_schema_bootstrap::ensureTables(); $this->setTable('department_selfserve_tasks'); } /** * Add a task * @param int $department The department id * @param int $lane The lane id * @param int $product The product id * @param int|null $question_id Optional question id that gates this task. Persisted in legacy `condition_id` column. * @param string $task The task text * @param string $description The task description * @param int $order_priority The order priority of the task (lower numbers are shown first) * @param selfserve_lane_services[]|string[]|null $services The services that the task enables (stored as JSON array of service names). May be an array of enum cases or names. * @param array|string|null $buttons Optional buttons on the department's machine dynamic image to be enabled by this task (stored as JSON array of button IDs). Accepts program integers plus "reset" and "start". * @param int|null $dynamic_images_vehicle_type Optional vehicle type selection override for the machine UI. Integer >= 0 or null. * @return department_selfserve_tasks_o * @throws Exception If the object was not created successfully */ public function add( int $department, int $lane, int $product, int|null $question_id, string $task, string $description, int $order_priority = 0, ?array $services = null, array|string|null $buttons = null, int|null $dynamic_images_vehicle_type = null, ?int $machine_type_id = null, ?selfserve_task_gate_type $gate_type = null, ?int $gate_ref_id = null, ): self { global /** @var db $db */ $db; // Sanitize the input $department = (int)$department; $lane = (int)$lane; $product = (int)$product; if (!is_null($question_id)) { $question_id = (int)$question_id; } if (!is_null($machine_type_id)) { $machine_type_id = (int)$machine_type_id; } if (!is_null($gate_ref_id)) { $gate_ref_id = (int)$gate_ref_id; } $task = $db->escape_string($task); $description = $db->escape_string($description); $order_priority = (int)$order_priority; // If the services array is not null, validate and normalize to array of service names (strings) $services_names = null; if (!is_null($services)) { $services_names = []; foreach ($services as $service) { if ($service instanceof selfserve_lane_services) { $services_names[] = $service->name; // store by name continue; } if (is_string($service)) { $name = strtoupper(trim($service)); $valid = false; foreach (selfserve_lane_services::cases() as $case) { if ($case->name === $name) { $valid = true; break; } } if (!$valid) { throw new Exception("Invalid service: " . $service); } $services_names[] = $name; continue; } throw new Exception("Invalid service type"); } } // Normalize/validate buttons $buttons_ids = null; if (!is_null($buttons)) { $buttons_ids = self::normalizeButtonsInput($buttons); } // Normalize/validate dynamic_images_vehicle_type (nullable, integer >= 0) if (!is_null($dynamic_images_vehicle_type)) { $dynamic_images_vehicle_type = (int)$dynamic_images_vehicle_type; if ($dynamic_images_vehicle_type < 0) { throw new Exception('dynamic_images_vehicle_type must be an integer >= 0'); } } if ($gate_type === null) { $resolvedGate = self::resolveLegacyGateDefinition($question_id); $gate_type = $resolvedGate['gate_type']; $gate_ref_id = $resolvedGate['gate_ref_id']; } elseif ($gate_type === selfserve_task_gate_type::ALWAYS) { $gate_ref_id = null; } elseif ($gate_ref_id === null || $gate_ref_id <= 0) { throw new Exception('gate_ref_id must be provided for CONDITION and QUESTION task gate types'); } // Add the object $tmp_id = self::add_object([ 'department' => $department, 'lane' => $lane, 'product' => $product, ...(!is_null($machine_type_id) ? ['machine_type_id' => $machine_type_id] : []), ...(!is_null($question_id) ? ['condition_id' => $question_id] : []), // Legacy column name; contains the gating question id 'gate_type' => $gate_type->value, 'gate_ref_id' => $gate_ref_id, 'task' => $task, 'description' => $description, 'order_priority' => $order_priority, 'services' => $services_names, 'buttons' => $buttons_ids, ...(!is_null($dynamic_images_vehicle_type) ? ['dynamic_images_vehicle_type' => $dynamic_images_vehicle_type] : []), ]); $this->id = $tmp_id; self::getObjectProperties(); self::objectChanged(); return $this; } public function getObjectProperties(): void { $this->department = new object_property($this->table, $this->id, 'department', 'int', false); $this->lane = new object_property($this->table, $this->id, 'lane', 'int', false); $this->product = new object_property($this->table, $this->id, 'product', 'int', false); $this->machine_type_id = new object_property($this->table, $this->id, 'machine_type_id', 'int', false); $this->condition_id = new object_property($this->table, $this->id, 'condition_id', 'int', true); $this->gate_type = new object_property($this->table, $this->id, 'gate_type', 'string', false); $this->gate_ref_id = new object_property($this->table, $this->id, 'gate_ref_id', 'int', true); $this->task = new object_property($this->table, $this->id, 'task', 'string', false); $this->description = new object_property($this->table, $this->id, 'description', 'string', false); $this->order_priority = new object_property($this->table, $this->id, 'order_priority', 'int', false); $this->services = new object_property($this->table, $this->id, 'services', 'json', false); $this->buttons = new object_property($this->table, $this->id, 'buttons', 'json', false); $this->dynamic_images_vehicle_type = new object_property($this->table, $this->id, 'dynamic_images_vehicle_type', 'int', false); $this->created_at = new object_property($this->table, $this->id, 'created_at', 'timestamp', false); $this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'timestamp', false); $this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'timestamp', false); } public function objectChanged(): void { //TODO: Add cache invalidation } public function asArray(): array { return [ 'id' => (int)$this->id, 'department' => (int)$this->department->value(), 'lane' => (int)$this->lane->value(), 'product' => (int)$this->product->value(), 'machine_type_id' => is_null($this->machine_type_id->value()) ? null : (int)$this->machine_type_id->value(), 'condition_id' => is_null($this->condition_id->value()) ? null : (int)$this->condition_id->value(), 'gate_type' => (string)($this->gate_type->value() ?? selfserve_task_gate_type::ALWAYS->value), 'gate_ref_id' => is_null($this->gate_ref_id->value()) ? null : (int)$this->gate_ref_id->value(), 'task' => (string)$this->task->value(), 'description' => (string)$this->description->value(), 'order_priority' => (int)$this->order_priority->value(), 'services' => (array)$this->services->value(), 'buttons' => (array)$this->buttons->value(), 'dynamic_images_vehicle_type' => (function($v){ return $v === null ? null : (int)$v; })($this->dynamic_images_vehicle_type->value()), // Timestamps 'created_at' => (string)$this->created_at->value(), 'updated_at' => (string)$this->updated_at->value(), ]; } /** * Adapter for canonical naming. * @return int|null Question id that gates this task. */ public function getQuestionId(): ?int { $value = $this->condition_id->value(); return is_null($value) ? null : (int)$value; } /** * Adapter for canonical naming while persisting to legacy `condition_id` column. * @param int|null $question_id * @return void */ public function setQuestionId(?int $question_id): void { $this->condition_id->set(is_null($question_id) ? null : (int)$question_id); $resolvedGate = self::resolveLegacyGateDefinition($question_id); $this->gate_type->set($resolvedGate['gate_type']->value); $this->gate_ref_id->set($resolvedGate['gate_ref_id']); } public function getTasksForMachineType(int $machineTypeId): array { return self::getFieldsWhere([ 'machine_type_id' => $machineTypeId, 'deleted_at' => null, ], ['id', 'department', 'lane', 'product', 'machine_type_id', 'condition_id', 'gate_type', 'gate_ref_id', 'task', 'description', 'order_priority', 'services', 'buttons', 'dynamic_images_vehicle_type', 'created_at', 'updated_at']); } public function getLegacyTasksForLaneProduct(int $departmentId, int $laneId, int $productId): array { return self::getFieldsWhere([ 'department' => $departmentId, 'lane' => $laneId, 'product' => $productId, 'deleted_at' => null, ], ['id', 'department', 'lane', 'product', 'machine_type_id', 'condition_id', 'gate_type', 'gate_ref_id', 'task', 'description', 'order_priority', 'services', 'buttons', 'dynamic_images_vehicle_type', 'created_at', 'updated_at']); } /** * Normalize external gate_type inputs. * @param mixed $input * @return selfserve_task_gate_type * @throws Exception */ public static function normalizeGateTypeInput(mixed $input): selfserve_task_gate_type { if ($input instanceof selfserve_task_gate_type) { return $input; } if (is_string($input)) { $normalized = strtoupper(trim($input)); $gateType = selfserve_task_gate_type::tryFrom($normalized); if ($gateType !== null) { return $gateType; } } throw new Exception('Invalid gate_type. Expected ALWAYS, CONDITION, or QUESTION.'); } /** * Resolve a typed gate from legacy `condition_id` input. * @param int|null $legacyGateId * @return array{gate_type:selfserve_task_gate_type,gate_ref_id:int|null} */ public static function resolveLegacyGateDefinition(?int $legacyGateId): array { if ($legacyGateId === null || $legacyGateId <= 0) { return [ 'gate_type' => selfserve_task_gate_type::ALWAYS, 'gate_ref_id' => null, ]; } $condition = (new department_selfserve_conditions_o())->select((int)$legacyGateId); if ($condition->exists()) { return [ 'gate_type' => selfserve_task_gate_type::CONDITION, 'gate_ref_id' => (int)$legacyGateId, ]; } return [ 'gate_type' => selfserve_task_gate_type::QUESTION, 'gate_ref_id' => (int)$legacyGateId, ]; } /** * Normalize input for dynamic_images_vehicle_type into a nullable non-negative integer. * Accepts int, string (numeric), null, or empty string (treated as null). * @param mixed $input * @return int|null * @throws Exception on invalid format or negative values */ public static function normalizeVehicleTypeInput(mixed $input): ?int { if ($input === null) { return null; } if (is_string($input)) { $trim = trim($input); if ($trim === '' || strtolower($trim) === 'null') { return null; } if (ctype_digit($trim)) { $val = (int)$trim; } elseif (is_numeric($trim) && (int)$trim == $trim) { $val = (int)$trim; } else { throw new Exception('Invalid dynamic_images_vehicle_type value'); } } elseif (is_int($input)) { $val = $input; } else { throw new Exception('Invalid dynamic_images_vehicle_type value'); } if ($val < 0) { throw new Exception('dynamic_images_vehicle_type must be >= 0'); } return $val; } /** * Normalize mixed input for buttons into an array of mapped button IDs. * Accepts: * - array of ints/strings * - JSON array string * - comma-separated string * @param mixed $input * @return array * @throws Exception */ public static function normalizeButtonsInput(mixed $input): array { $raw = $input; if (is_string($raw)) { $decoded = json_decode($raw, true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { $raw = $decoded; } else { $raw = array_filter(array_map(function ($s) { return trim((string)$s); }, explode(',', $raw)), fn($s) => $s !== ''); } } if (!is_array($raw)) { throw new Exception('Invalid format for buttons. Expected array, JSON array, or comma-separated string of mapped button ids.'); } $ids = []; foreach ($raw as $btn) { if (is_string($btn)) { $trimmed = trim($btn); $specialButton = strtolower($trimmed); if ($specialButton === 'reset' || $specialButton === 'start' || $specialButton === 'program_picker') { $ids[] = $specialButton; continue; } } if (is_int($btn)) { $val = $btn; } elseif (is_string($btn) && ctype_digit(trim($btn))) { $val = (int)trim($btn); } elseif (is_numeric($btn) && (int)$btn == $btn) { $val = (int)$btn; } else { throw new Exception('Invalid button id: ' . (is_scalar($btn) ? (string)$btn : gettype($btn))); } if ($val < 0) { throw new Exception('Button id must be >= 0: ' . $val); } $ids[] = $val; } // de-duplicate while preserving order $deduped = []; $seen = []; foreach ($ids as $id) { $key = (is_int($id) ? 'int:' : 'string:') . (string)$id; if (isset($seen[$key])) { continue; } $seen[$key] = true; $deduped[] = $id; } return $deduped; } }