Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

106 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Scratchpad — Mobile Remaining Features (10, 11, 15)
## Key Discoveries
### Feature 10: Documents
- `GET /api/v1/tickets/{id}/documents` exists, returns `IDocument[]` via `createSuccessResponse`
- No upload endpoint in API v1 — uploads use server action `uploadDocument()` via FormData
- **Need to create:** `POST /api/v1/tickets/{id}/documents` for mobile upload
- Document download: `GET /api/documents/download/{fileId}` (different base path, not v1)
- Documents have: `document_name`, `type_name`, `type_icon`, `file_size`, `mime_type`, `created_by_full_name`, `updated_at`
- `getTicketDocuments()` already returns enriched data with joins
- `StorageService.uploadFile()` handles actual storage (Local or S3 provider)
- Auto-files into `/Tickets/Attachments` folder
- Document types resolved from MIME via `getDocumentTypeId()`
### Feature 11: Inventory/Products (Materials)
- Products live in `service_catalog` table with `item_kind = 'product'`
- Product-ticket link via `ticket_materials` table (quantity, rate, currency, description)
- **No API v1 endpoint for materials** — web app uses server actions
- **Need to create:** `GET /api/v1/tickets/{id}/materials` and `POST /api/v1/tickets/{id}/materials`
- Web component: `TicketMaterialsCard` — searchable product picker with multi-currency prices
- Key actions: `searchServiceCatalogForPicker()`, `getServicePrices()`, `listTicketMaterials()`, `addTicketMaterial()`
- Material fields: `service_id`, `quantity`, `rate`, `currency_code`, `description`, `is_billed`
- Products have SKU, vendor, manufacturer fields
- `GET /api/v1/products` exists for listing/searching products
### Feature 15: Contact & Client Avatars
- `getContactAvatarUrl(contactId, tenant)` in `@alga-psa/formatting/avatarUtils`
- `getClientLogoUrl(clientId, tenant)` in same file
- Both use `getEntityImageUrl()` which queries `document_associations` + `documents` + `external_files`
- Returns URL like `/api/documents/view/{fileId}?t={timestamp}` or null
- Comments already use this pattern: batch-fetch user avatar URLs, map into response
- `TicketService.getById()` has `contact_name_id` and `client_id` available from the ticket
- **Simple addition:** call both functions after ticket query, add to response
- Mobile `TicketDetail` type uses `& Record<string, unknown>` so new fields work without type changes
## Decisions
- Feature 15 is the quickest win (server change only + minor mobile UI)
- Feature 10 needs a new upload endpoint — can follow the pattern from `ApiAssetController`
- Feature 11 needs two new endpoints (list + create materials)
- For mobile, simplified product picker without multi-currency (use `default_rate`)
## Key File Paths
- `server/src/lib/api/services/TicketService.ts` — main ticket service
- `server/src/lib/api/controllers/ApiTicketController.ts` — ticket API controller
- `packages/formatting/src/avatarUtils.ts` — avatar/logo URL helpers
- `packages/documents/src/actions/documentActions.ts` — document upload action
- `packages/tickets/src/actions/materialCatalogActions.ts` — material actions
- `ee/mobile/src/screens/TicketDetailScreen.tsx` — mobile ticket detail
- `ee/mobile/src/api/tickets.ts` — mobile ticket API
## Progress Log
- F001 complete: `TicketService.getById()` now resolves `contact_avatar_url` via `getContactAvatarUrl()` when `contact_name_id` is present; implemented together with the rest of Feature 15 to avoid partial dead code.
- F002 complete: the same `Promise.all` enrichment in `TicketService.getById()` now resolves `client_logo_url` via `getClientLogoUrl()` when `client_id` is present, matching the PRDs server contract.
- F003 complete: ticket detail responses now include both `contact_avatar_url` and `client_logo_url` alongside the existing enriched ticket payload and documents list.
- F004 complete: `TicketDetailScreen` now renders an `Avatar` plus name row for the contact field inside `KeyValue`, with the API key passed through for protected image fetches.
- F005 complete: the client `KeyValue` field now uses the same avatar row pattern so logos render beside the client name instead of plain text only.
- F006 complete: null `contact_avatar_url`/`client_logo_url` now fall back cleanly because the screen always renders the shared `Avatar` component and `KeyValue` accepts rich value content instead of string-only text.
- F010 complete: `POST /api/v1/tickets/{id}/documents` now exists via the ticket documents route plus `ApiTicketController.uploadDocument()`, using API-key auth and ticket update permission checks before delegating to the service upload path.
- F011 complete: the upload controller reads `multipart/form-data` with `req.formData()`, extracts the `file` field, and `TicketService.uploadTicketDocument()` persists the payload through `StorageService.validateFileUpload()` and `StorageService.uploadFile()`.
- F012 complete: `TicketService.uploadTicketDocument()` now inserts a `documents` row and a matching `document_associations` row with `entity_type: 'ticket'` inside one transaction, after resolving the folder path and document type.
- F013 complete: after upload, the service reloads the enriched `IDocument` via `getDocumentById()` and the controller returns that object in the API success payload with HTTP 201.
- F014 complete: `ee/mobile/src/api/documents.ts` now exposes `getTicketDocuments()` as the mobile wrapper around `GET /api/v1/tickets/{ticketId}/documents`, including the required `x-api-key` header and typed `TicketDocument[]` response.
- F015 complete: `uploadTicketDocument()` now posts `FormData` to the same ticket documents endpoint, and `ApiClient.request()` was updated so multipart bodies bypass JSON encoding and omit the JSON content type header.
- F016 complete: `DocumentsSection` is now mounted in `TicketDetailScreen` below the description card and loads ticket documents into a dedicated card section on first render.
- F017 complete: each document row now renders the document name plus a secondary metadata line composed from the resolved type label, formatted file size, and formatted `updated_at` timestamp.
- F018 complete: the documents card header now includes a neutral `Badge` showing the current document count next to the attach action, satisfying the count badge requirement.
- F019 complete: tapping a document row now downloads the file to Expo FileSystem with the API key header and immediately hands the cached URI off to `Linking.openURL()` for the platform handler.
- F020 complete: the attach UI now exposes a Camera option backed by `expo-image-picker`, including explicit camera permission checks and upload of the captured asset through the document API wrapper.
- F021 complete: the same attach affordance now exposes a File option backed by `expo-document-picker`, allowing arbitrary picked files to be posted as ticket documents.
- F022 complete: while an upload request is in flight, `DocumentsSection` switches on an `ActivityIndicator` plus localized uploading text so technicians get explicit progress feedback instead of a dead UI.
- F023 complete: successful uploads now call `loadDocuments()` before clearing the upload state, so the section refreshes immediately and the new attachment appears without reopening the screen.
- F024 complete: when the ticket has no attachments, the documents card now renders a dedicated localized empty-state message instead of an empty container.
- F025 complete: document loading/open/upload failures now surface localized errors in the section, with a specific camera-permission denial path and fallback server/network error messaging for rejected uploads.
- F030 complete: `GET /api/v1/tickets/{id}/materials` now exists through the new ticket materials route plus `ApiTicketController.getMaterials()`, and the service exposes `getTicketMaterials()` for the shared list path.
- F031 complete: `getTicketMaterials()` and `getTicketMaterialById()` both join `service_catalog` so material payloads include `service_name` and `sku` directly from the product catalog.
- F032 complete: `POST /api/v1/tickets/{id}/materials` now exists via `ApiTicketController.addMaterial()` and the dedicated ticket materials route, returning a created material payload with HTTP 201.
- F033 complete: create requests now pass through `createTicketMaterialSchema` for body validation and are revalidated in the service for positive quantity, non-negative rate, and product-backed `service_id` enforcement before insert.
- F034 complete: `addTicketMaterial()` now looks up the ticket first and copies `client_id` from the ticket row into `ticket_materials`, so mobile callers do not send client context explicitly.
- F035 complete: `ee/mobile/src/api/materials.ts` now exposes `getTicketMaterials()` as the typed wrapper around `GET /api/v1/tickets/{ticketId}/materials`.
- F036 complete: the same mobile API module now exposes `addTicketMaterial()` for posting `{ service_id, quantity, rate, currency_code, description? }` to the ticket materials endpoint.
- F037 complete: `listProducts()` now wraps the existing `GET /api/v1/products` endpoint with mobile-friendly search and limit parameters, reusing the catalogs `default_rate` and `sku` fields for picker display and defaults.
- F038 complete: `MaterialsSection` is now mounted in `TicketDetailScreen` as a dedicated ticket detail card that loads and manages material data independently of comments/documents.
- F039 complete: each material row now renders the product name, SKU, localized quantity/rate line, and a billed or unbilled badge directly in the ticket detail UI.
- F040 complete: the materials card now exposes an `Add Product` action that opens `EntityPickerModal`, loads product results from `/api/v1/products`, and supports search-driven filtering.
- F041 complete: selecting a product now closes the picker and opens a dedicated modal with quantity and rate inputs so the technician can confirm billable details before creation.
- F042 complete: the rate input is seeded from the selected products `default_rate`, converted from minor units into a currency input string so the default price is editable rather than blank.
- F043 complete: saving from the material modal now posts the selected product, quantity, and rate, then closes the modal and reloads the materials list so the new row is visible immediately.
- F044 complete: when a ticket has no materials, the section now renders a dedicated localized empty-state message instead of an empty list shell.
- F045 complete: materials loading, product search, input validation, and add failures now surface localized or server-provided errors in the section/modal instead of failing silently.
- T010/T011 complete: added `server/src/test/unit/api/ticketDocuments.service.test.ts` to exercise `TicketService.getTicketDocuments()` directly for both populated and empty ticket attachment lists, closing the remaining gap in document list coverage.
- T001-T005 complete: `server/src/test/unit/api/ticketService.avatarUrls.test.ts` covers populated contact/client image URLs, null helper fallbacks, and the no-contact/no-client branch so the ticket detail API enrichment is exercised end to end at the service layer.
- F050/T006-T008 complete: `ee/mobile/src/screens/TicketDetailScreen.avatars.test.ts` verifies both avatar image rendering paths and the initials fallback path for contact/client rows in the mobile detail screen.
- T012-T016 complete: `ticketDocumentUpload.service.test.ts` and `ticketDocumentsUpload.contract.test.ts` cover document creation, ticket association creation, response reloading, missing-file rejection, and the shared authenticated controller flow for upload requests.
- F052/F053/T017-T026/T051-T052 complete: `ee/mobile/src/api/documents.test.ts` and `ee/mobile/src/features/ticketDetail/components/DocumentsSection.test.ts` cover the document API wrappers, list metadata, count badge, download/open action, camera/file uploads, upload progress, refresh-after-upload, empty state, upload failure messaging, and camera-permission denial behavior.
- T030-T035 complete: `server/src/test/unit/api/ticketMaterials.service.test.ts` and `ticketMaterials.contract.test.ts` exercise list-with-join behavior, empty lists, successful material creation, ticket-derived `client_id`, validation failures, invalid `service_id`, and the controllers authenticated validation path.
- F055/F056/T036-T043/T053-T054 complete: `ee/mobile/src/api/materials.test.ts` and `ee/mobile/src/features/ticketDetail/components/MaterialsSection.test.ts` cover material API wrappers, rendered product metadata and billed badges, picker search, SKU display, quantity/rate modal defaults, create-and-refresh flow, empty state, and add-failure messaging.
- F051/F054/F057/T050 complete: `cd ee/mobile && npx vitest run` now passes with the new avatar/document/material suites included. The only follow-up needed was mocking `DocumentsSection` and `MaterialsSection` inside the rich-text screen tests so those suites stay isolated from Expo native file-system bindings.
- F058/T055 complete: the final full mobile suite run now passes at `45` test files / `169` tests, confirming the pre-existing suite still passes alongside the new avatar, document, and material coverage.
## Commands / Runbooks
- Server targeted test: `cd server && npx vitest run src/test/unit/api/ticketService.avatarUrls.test.ts`
- Mobile targeted test: `cd ee/mobile && npx vitest run src/screens/TicketDetailScreen.avatars.test.ts`
- Server targeted tests: `cd server && npx vitest run src/test/unit/api/ticketMaterials.service.test.ts src/test/unit/api/ticketMaterials.contract.test.ts src/test/unit/api/ticketDocumentsUpload.contract.test.ts src/test/unit/api/ticketDocumentUpload.service.test.ts src/test/unit/api/ticketService.avatarUrls.test.ts`
- Mobile targeted tests: `cd ee/mobile && npx vitest run src/api/materials.test.ts src/features/ticketDetail/components/MaterialsSection.test.ts src/api/documents.test.ts src/features/ticketDetail/components/DocumentsSection.test.ts src/screens/TicketDetailScreen.avatars.test.ts`