Task #96: Show dependency counts in admin app/service/user lists

Goal: make the admin delete dialog show its dependency warning on
the FIRST click (matching the existing Groups UX), instead of
needing a 409 round-trip from the DELETE endpoint to populate the
warning.

Changes:
- lib/api-spec/openapi.yaml: added optional count fields to App
  (groupCount, restrictionCount, openCount), Service (orderCount),
  and UserProfile (noteCount, orderCount, conversationCount,
  messageCount).
- artifacts/api-server/src/routes/apps.ts: GET /admin/apps now
  batch-loads groupCount/restrictionCount/openCount via grouped
  COUNT queries on group_apps, app_permissions, and app_opens.
- artifacts/api-server/src/routes/services.ts: GET /services now
  batch-loads orderCount from service_orders.
- artifacts/api-server/src/routes/users.ts: GET /users now batch-
  loads noteCount/orderCount/conversationCount/messageCount from
  notes, service_orders, conversations.created_by, and
  messages.sender_id.
- artifacts/tx-os/src/pages/admin.tsx: Apps, Services, and Users
  delete buttons pre-populate their conflict-state from the row's
  count fields so the DeletionWarningDialog shows the warning on
  the first click. The lazy 409 fallback still works as a safety
  net for any caller without counts.
- replit.md: documented the new admin-panel delete UX.

Tests:
- Added artifacts/api-server/tests/list-dependency-counts.test.mjs
  with 6 tests verifying each list endpoint exposes the new count
  fields with the expected non-zero values when dependents exist
  AND zero values when they do not (apps, services, users).
- Existing artifacts/api-server/tests/delete-force-warnings.test.mjs
  (9 tests) still passes — DELETE behavior unchanged.
- Verified end-to-end with a Playwright browser test: clicking the
  service delete icon ONCE opens the confirmation modal with the
  dependency warning ("orders", count >= 1) immediately.

Notes / drift:
- Count fields were added to the shared App/Service/UserProfile
  schemas (not list-only sub-schemas) so the same shape is returned
  from list and detail endpoints. Code review flagged this as
  acceptable but broader than strictly necessary; left as is to
  keep the API consistent.

Out of scope (already pre-existing on main, tracked as follow-ups):
- artifacts/api-server/tests/apps-open.test.mjs has a syntax error.
- artifacts/api-server/tests/app-permissions-unique.test.mjs has
  two failing assertions about the composite primary key.
- artifacts/api-server/src/routes/executive-meetings.ts has 3 pre-
  existing typecheck errors around the font-settings scope column.
- The `test` workflow exits with ECONNREFUSED when the api-server
  isn't already up — needs a readiness check.

Replit-Task-Id: bd14fe73-9961-431b-ab5a-ab70f116e8c7
This commit is contained in:
riyadhafraa
2026-04-29 08:48:29 +00:00
parent 03ba2cf99f
commit c0cf2115dd
10 changed files with 740 additions and 8 deletions
@@ -151,6 +151,23 @@ export interface UserProfile {
roles: string[];
groups: GroupSummary[];
createdAt: string;
/** Dependent record counts. Populated only by the admin list endpoint
(GET /users) so the delete dialog can warn before the first click.
Omitted from single-user responses (GET /users/:id, PATCH, etc).
*/
noteCount?: number;
/** Number of service orders owned by this user. Populated only by the
admin list endpoint (GET /users).
*/
orderCount?: number;
/** Number of conversations created by this user. Populated only by the
admin list endpoint (GET /users).
*/
conversationCount?: number;
/** Number of messages authored by this user. Populated only by the
admin list endpoint (GET /users).
*/
messageCount?: number;
}
export interface UpdateUserBody {
@@ -314,6 +331,20 @@ export interface App {
sortOrder: number;
createdAt: string;
updatedAt: string;
/** Number of groups granting access to this app. Populated only by the
admin list endpoint (GET /admin/apps) so the delete dialog can warn
before the first click. Omitted from non-admin and single-app
responses.
*/
groupCount?: number;
/** Number of legacy permission restrictions for this app. Populated
only by the admin list endpoint (GET /admin/apps).
*/
restrictionCount?: number;
/** Number of recorded app-open events for this app. Populated only by
the admin list endpoint (GET /admin/apps).
*/
openCount?: number;
}
export interface CreateAppBody {
@@ -364,6 +395,12 @@ export interface Service {
sortOrder: number;
createdAt: string;
updatedAt: string;
/** Number of service_orders rows referencing this service. Populated
only by the list endpoint (GET /services) so the admin delete
dialog can warn before the first click. Omitted from single-service
responses.
*/
orderCount?: number;
}
export interface CreateServiceBody {