forked from fa/breadcrumb-the-shire
feat(helpdesk): add handover protocol management
Implement handover protocols as a new entity in the helpdesk module, allowing users to create fillable protocol records from admin-defined software product schemas. Key additions: - DB migration (helpdesk_handovers table) with tenant scope - HandoverService with status workflow (draft/in_progress/completed/archived) - Three-tier permissions (view/create/manage) - Two-step creation wizard (Stripe-style assistant) - Grid.js list page with search and status filter - Edit/detail page with aside metadata and status controls - Reusable core autocomplete lookup component (app-lookup-field) - Debitor lookup data endpoint for autocomplete - Dynamic form rendering from schema snapshots - 11 PHPUnit tests for HandoverService - DE+EN i18n translations (48 keys each) Also includes: PHPStan fixes (dead code removal, stale baseline cleanup), software product edit title improvement, fieldset simplification, and Stripe-style hover for schema preview. Workflow: HD-HANDOVERS-001 (.agents/runs/HD-HANDOVERS-001/) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Module\Helpdesk\Repository;
|
||||
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\Repository\Support\RepoQuery;
|
||||
|
||||
class HandoverRepository
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $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<string, mixed> $filters
|
||||
* @return array{total: int, rows: list<array<string, mixed>>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user