getMessage(), $e->status, $e->errorCode); } $clientId = (int)$tenant['client_id']; $userId = (int)($tenant['client_user_id'] ?? 0); $tenantRole = (string)($tenant['role'] ?? 'editor'); $db = dbnToolsDb(); $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')); $action = (string)($_GET['action'] ?? ($method === 'POST' ? '' : 'list')); try { switch ($action) { case 'list': dbnToolsRequireMethod('GET'); listCats($db, $clientId); break; case 'create': dbnToolsRequireMethod('POST'); createCat($db, $clientId, $userId, $tenantRole); break; case 'update': dbnToolsRequireMethod('POST'); updateCat($db, $clientId, $userId, $tenantRole); break; case 'delete': dbnToolsRequireMethod('POST'); deleteCat($db, $clientId, $userId, $tenantRole); break; case 'reorder': dbnToolsRequireMethod('POST'); reorderCats($db, $clientId); break; default: dbnToolsError('Unknown action.', 400, 'unknown_action'); } } catch (DbnToolsHttpException $e) { dbnToolsError($e->getMessage(), $e->status, $e->errorCode, $e->extra); } catch (Throwable $e) { error_log('[dbn-dms/categories] ' . $e->getMessage()); dbnToolsError('Category operation failed.', 500, 'op_failed'); } function listCats(PDO $db, int $clientId): void { dbnDmsSeedDefaultCategoriesIfEmpty($clientId); $stmt = $db->prepare( 'SELECT id, slug, label, color, icon, sort_order, is_system, created_at FROM client_categories WHERE client_id = ? ORDER BY sort_order ASC, label ASC' ); $stmt->execute([$clientId]); // Also fetch usage counts. $counts = $db->prepare( "SELECT category, COUNT(*) AS n FROM client_documents WHERE client_id = ? AND deleted_at IS NULL GROUP BY category" ); $counts->execute([$clientId]); $countMap = []; foreach ($counts->fetchAll() as $r) { $countMap[(string)$r['category']] = (int)$r['n']; } $rows = $stmt->fetchAll(); foreach ($rows as &$r) { $r['doc_count'] = $countMap[(string)$r['slug']] ?? 0; $r['is_system'] = (int)$r['is_system'] === 1; } dbnToolsRespond(['ok' => true, 'categories' => $rows]); } function createCat(PDO $db, int $clientId, int $userId, string $tenantRole): void { if (!in_array($tenantRole, ['editor','admin','owner'], true)) { dbnToolsError('Forbidden.', 403, 'forbidden'); } $input = dbnToolsJsonInput(10_000); $slug = strtolower(trim((string)($input['slug'] ?? ''))); $slug = preg_replace('/[^a-z0-9\-_]/', '', $slug) ?: ''; $label = trim((string)($input['label'] ?? '')); $color = trim((string)($input['color'] ?? '')); $icon = trim((string)($input['icon'] ?? '')); if ($slug === '' || mb_strlen($slug, 'UTF-8') > 50) { dbnToolsError('Slug is required (lowercase, 1–50, [a-z0-9-_]).', 422, 'invalid_slug'); } if ($label === '' || mb_strlen($label, 'UTF-8') > 100) { dbnToolsError('Label is required (1–100).', 422, 'invalid_label'); } if ($color !== '' && !preg_match('/^#[0-9a-fA-F]{6}$/', $color)) { dbnToolsError('Invalid color.', 422, 'invalid_color'); } try { $stmt = $db->prepare( 'INSERT INTO client_categories (client_id, slug, label, color, icon, sort_order, is_system, created_at) VALUES (?, ?, ?, ?, ?, 999, 0, NOW())' ); $stmt->execute([ $clientId, $slug, $label, $color !== '' ? $color : null, $icon !== '' ? substr($icon, 0, 40) : null, ]); } catch (PDOException $e) { if ((int)$e->errorInfo[1] === 1062) { dbnToolsError('A category with this slug already exists.', 409, 'duplicate_slug'); } throw $e; } $id = (int)$db->lastInsertId(); dbnDmsLogAudit($clientId, $userId, 'category_create', ['slug' => $slug]); dbnToolsRespond(['ok' => true, 'id' => $id], 201); } function updateCat(PDO $db, int $clientId, int $userId, string $tenantRole): void { if (!in_array($tenantRole, ['editor','admin','owner'], true)) { dbnToolsError('Forbidden.', 403, 'forbidden'); } $input = dbnToolsJsonInput(10_000); $id = (int)($input['id'] ?? 0); if ($id <= 0) { dbnToolsError('id is required.', 400, 'missing_id'); } $fields = []; $params = []; if (array_key_exists('label', $input)) { $label = trim((string)$input['label']); if ($label === '' || mb_strlen($label, 'UTF-8') > 100) { dbnToolsError('Invalid label.', 422, 'invalid_label'); } $fields[] = 'label = ?'; $params[] = $label; } if (array_key_exists('color', $input)) { $color = trim((string)$input['color']); if ($color !== '' && !preg_match('/^#[0-9a-fA-F]{6}$/', $color)) { dbnToolsError('Invalid color.', 422, 'invalid_color'); } $fields[] = 'color = ?'; $params[] = $color !== '' ? $color : null; } if (array_key_exists('icon', $input)) { $icon = trim((string)$input['icon']); $fields[] = 'icon = ?'; $params[] = $icon !== '' ? substr($icon, 0, 40) : null; } if (array_key_exists('sort_order', $input)) { $fields[] = 'sort_order = ?'; $params[] = (int)$input['sort_order']; } if (!$fields) { dbnToolsError('Nothing to update.', 400, 'no_fields'); } $params[] = $id; $params[] = $clientId; $stmt = $db->prepare('UPDATE client_categories SET ' . implode(', ', $fields) . ' WHERE id = ? AND client_id = ?'); $stmt->execute($params); dbnDmsLogAudit($clientId, $userId, 'category_update', ['id' => $id]); dbnToolsRespond(['ok' => true]); } function deleteCat(PDO $db, int $clientId, int $userId, string $tenantRole): void { if (!in_array($tenantRole, ['admin','owner'], true)) { dbnToolsError('Forbidden.', 403, 'forbidden'); } $input = dbnToolsJsonInput(2_000); $id = (int)($input['id'] ?? 0); if ($id <= 0) { dbnToolsError('id is required.', 400, 'missing_id'); } $row = $db->prepare('SELECT slug, is_system FROM client_categories WHERE id = ? AND client_id = ?'); $row->execute([$id, $clientId]); $existing = $row->fetch(); if (!$existing) { dbnToolsError('Not found.', 404, 'not_found'); } if ((int)$existing['is_system'] === 1) { dbnToolsError('System categories cannot be deleted.', 422, 'is_system'); } // Reassign any docs in this category to uncategorized. $db->prepare("UPDATE client_documents SET category = 'uncategorized' WHERE client_id = ? AND category = ?") ->execute([$clientId, $existing['slug']]); $del = $db->prepare('DELETE FROM client_categories WHERE id = ? AND client_id = ?'); $del->execute([$id, $clientId]); dbnDmsLogAudit($clientId, $userId, 'category_delete', ['id' => $id, 'slug' => $existing['slug']]); dbnToolsRespond(['ok' => true]); } function reorderCats(PDO $db, int $clientId): void { $input = dbnToolsJsonInput(20_000); $ids = $input['ids'] ?? []; if (!is_array($ids)) { dbnToolsError('ids array is required.', 400, 'missing_ids'); } $ids = array_values(array_filter(array_map('intval', $ids), fn($v) => $v > 0)); $upd = $db->prepare('UPDATE client_categories SET sort_order = ? WHERE id = ? AND client_id = ?'); foreach ($ids as $i => $id) { $upd->execute([$i, $id, $clientId]); } dbnToolsRespond(['ok' => true, 'reordered' => count($ids)]); }