$data * @return int|null Inserted ID or null on failure */ public function insert(array $data): ?int { $result = DB::insert( 'INSERT INTO helpdesk_handovers ' . '(tenant_id, debitor_no, debitor_name, product_code, status, schema_snapshot, field_values, created_by) ' . 'VALUES (?, ?, ?, ?, ?, ?, ?, ?)', (string) ($data['tenant_id'] ?? 0), (string) ($data['debitor_no'] ?? ''), (string) ($data['debitor_name'] ?? ''), (string) ($data['product_code'] ?? ''), (string) ($data['status'] ?? 'draft'), $data['schema_snapshot'] ?? null, $data['field_values'] ?? null, (string) ($data['created_by'] ?? 0) ); if ($result === false) { return null; } return (int) $result; } public function findById(int $tenantId, int $id): ?array { if ($id <= 0) { return null; } $row = DB::selectOne( 'SELECT * FROM helpdesk_handovers WHERE id = ? AND tenant_id = ? LIMIT 1', (string) $id, (string) $tenantId ); return $this->normalizeRow($row); } /** * @param array $filters * @return array{total: int, rows: list>} */ public function listPaged(int $tenantId, array $filters): array { $search = trim((string) ($filters['search'] ?? '')); $status = trim((string) ($filters['status'] ?? '')); $productCode = trim((string) ($filters['product_code'] ?? '')); [$limit, $offset] = RepoQuery::sanitizeLimitOffset($filters, 20, 1, 100, 0); [$order, $dir] = RepoQuery::sanitizeOrder( $filters, ['id', 'debitor_name', 'product_code', 'status', 'created_at', 'created_by'], 'created_at', 'desc' ); $where = ['h.tenant_id = ?']; $params = [(string) $tenantId]; RepoQuery::addLikeFilter($where, $params, ['h.debitor_name', 'h.debitor_no', 'h.product_code'], $search); if ($status !== '' && $status !== 'all') { $where[] = 'h.status = ?'; $params[] = $status; } if ($productCode !== '') { $where[] = 'h.product_code = ?'; $params[] = $productCode; } $whereSql = ' WHERE ' . implode(' AND ', $where); $total = (int) (DB::selectValue('SELECT COUNT(*) FROM helpdesk_handovers h' . $whereSql, ...$params) ?? 0); $rows = DB::select( 'SELECT h.*, u.display_name AS created_by_name FROM helpdesk_handovers h' . ' LEFT JOIN users u ON u.id = h.created_by' . $whereSql . sprintf(' ORDER BY h.`%s` %s LIMIT ? OFFSET ?', $order, $dir), ...array_merge($params, [(string) $limit, (string) $offset]) ); $normalized = []; if (is_array($rows)) { foreach ($rows as $row) { $item = $this->normalizeRow($row); if ($item !== null) { $normalized[] = $item; } } } return [ 'total' => $total, 'rows' => $normalized, ]; } public function updateFieldValues(int $tenantId, int $id, string $fieldValuesJson, int $updatedBy): bool { $result = DB::update( 'UPDATE helpdesk_handovers SET field_values = ?, updated_by = ? WHERE id = ? AND tenant_id = ?', $fieldValuesJson, (string) $updatedBy, (string) $id, (string) $tenantId ); return $result !== false; } public function updateStatus(int $tenantId, int $id, string $status, int $updatedBy): bool { $result = DB::update( 'UPDATE helpdesk_handovers SET status = ?, updated_by = ? WHERE id = ? AND tenant_id = ?', $status, (string) $updatedBy, (string) $id, (string) $tenantId ); return $result !== false; } private function normalizeRow(mixed $row): ?array { if (!is_array($row)) { return null; } $item = $row['helpdesk_handovers'] ?? $row; if (!is_array($item) || !isset($item['id'])) { return null; } return $item; } }