# Cross-channel conversation workspace design

Date: 2026-08-14  
Branch: `dev-chen`  
Status: approved product direction; implementation must preserve Zoom compatibility

## 1. Objective

Give Phone Call and Showroom F2F recordings the workflow already demonstrated on
Zoom meetings without copying a separate implementation into each channel.

All three channels will share:

- the existing analysis tabs (`AI analysis`, `Customer`, `Sales performance`,
  and `Meeting report`);
- the 90%-viewport recording workspace with an independently scrolling Ask AI
  rail;
- AI-proposed, human-reviewed action plans;
- per-step editing, assignee selection, priority, scheduling, agree/disagree,
  and rejection feedback;
- approval into the existing Sales Dashboard Action Items / Checklist;
- AI-suggested and human-confirmed pipeline matches;
- confirmed product and commission value;
- explainable opportunity signals; and
- the existing Super Admin versus ordinary-user visibility boundary.

The Sales Checklist remains one cross-channel workspace. It groups work by Lead,
not by recording or channel. Every generated task retains a visible source badge,
source time, and link back to its original Zoom meeting, Phone Call, or Showroom
F2F recording.

## 2. Decisions already made

1. Use the compatibility-first approach. Keep existing Zoom tables, routes, and
   public behavior working while extracting shared components and services.
2. Do not create independent Phone Call and Showroom task dashboards.
3. Group Sales Checklist rows by Lead across all channels.
4. Preserve source provenance per task. A merged Lead group must never hide
   whether a task came from Zoom, Phone Call, Showroom F2F, or manual entry.
5. AI output is advisory. Pipeline product and commission become facts only after
   a human confirms an existing pipeline Engagement.
6. Do not invent conversion probabilities or claimed uplift percentages.

## 3. Current-state facts

### Already shared

`RecordingDetail.vue` already renders the normalized details for all three
channels. Phone Call and Showroom adapters already supply transcript and the
unified conversation-analysis schema. Therefore these tabs are already at
parity:

- Overview
- Transcript
- AI analysis
- Customer
- Sales performance
- Meeting report

### Still Zoom-specific

The later workflow is structurally tied to Zoom today:

- `ZoomMeetingActionPlan` and `zoom_meeting_action_plans`;
- `ZoomMeetingOpportunityLink` and `zoom_meeting_opportunity_links`;
- Zoom-only action-plan and opportunity controllers/routes;
- `ZoomRecordingChatContextBuilder` and the Zoom-only Ask AI endpoint;
- type checks in `RecordingDetail.vue` that explicitly reject Call/F2F;
- `SalesWorkQueue` relations and queries typed to Zoom plans; and
- list ordering and opportunity presenters implemented only for Zoom meetings.

The work is therefore a compatibility-preserving domain generalization, not a
copy-and-paste UI change.

## 4. Approaches considered

### A. Copy the Zoom implementation twice

Fast initially, but it creates three repositories, three state machines, and
three authorization surfaces that will drift. Rejected.

### B. Compatibility-first shared core (selected)

Add a common source identity to the existing workflow storage, keep the old
Zoom identifiers and routes as compatibility aliases, and move behavior behind
channel-neutral services and components. Phone Call and Showroom then connect to
the same core. This has the lowest migration and regression risk.

### C. Immediate hard rename to a new Conversation domain

Cleanest final naming, but it would replace existing Zoom tables, model types,
foreign-like references, endpoints, and tests in one migration. Rejected for
this iteration because rollback and regression risk are unnecessarily high.

## 5. Shared source contract

Introduce a small channel-neutral source value/adapter layer. It must expose the
same contract for `ZoomMeeting`, `CallRecording`, and `F2fRecording`:

```text
source_type          zoom | phone_call | showroom_f2f
source_id            internal numeric record id
source_uuid          public record uuid
source_label         Zoom | Phone Call | Showroom F2F
source_title         safe human-readable title
occurred_at          meeting/call/showroom timestamp
lead_id              linked Lead id or null
owner_admin_id       owning salesperson Admin id (admins.id) or null
transcript           source transcript or null
analysis             unified conversation analysis or null
detail_url           authorized Manage URL to reopen the source
```

Use integer constants for persisted source types and statuses, following the
project status-column convention. UI/API payloads may expose stable string
codes.

Define every integer source-type constant exactly once on the source contract
class consumed by the registry. `ConversationActionPlan`,
`ConversationOpportunityLink`, and their migrations reference those constants
and never redefine them.

The adapter is the only place allowed to know channel-specific column names
such as `called_at` versus `recorded_at`. Shared services must not branch on
model internals.

`source_type` is deliberately not an Eloquent morph-class name. Resolve sources
through a `ConversationSourceRegistry`: one adapter per integer source type,
with `find()` for an individual request and `findMany()` for queue payloads.
Queue consumers must collect IDs by source type and batch-load at most once per
type; resolving a source lazily per task is forbidden. This avoids both Laravel
morph-map ambiguity and an N+1 query in Lead-grouped checklists.

## 6. Persistence and Zoom compatibility

### Action plans

Evolve the existing action-plan storage rather than create two copied tables:

- alter the legacy `zoom_meeting_id` column to nullable; its existing unique
  index remains and MySQL permits multiple non-Zoom rows with null there;
- add nullable `source_type` and `source_id` columns plus a nullable JSON
  `provenance` snapshot column;
- backfill every existing row deterministically as `zoom + zoom_meeting_id`;
- alter both new columns to NOT NULL only after the backfill;
- then add a unique `(source_type, source_id)` index;
- retain `zoom_meeting_id` during the compatibility period;
- for Zoom writes, populate both identities;
- for Phone Call and Showroom writes, leave `zoom_meeting_id` null; and
- keep the existing row IDs, so `lead_action_items.source_action_plan_id`
  remains valid without rewriting generated tasks.

Every repository create/restore/update path must write both shared identity
columns. For Zoom, `source_id` and `zoom_meeting_id` must refer to the same row.
No post-migration plan may carry a partial or null shared identity.

Split this into an additive expansion migration (nullable columns and backfill)
and a tightening migration (NOT NULL plus the composite unique index). The
expansion may land with the source contract, but the tightening migration runs
only after every create, restore, and update path dual-writes the shared
identity. Its preflight must refuse to run while any existing row has a null or
partial shared identity. The opportunity-link migration follows the same
ordering.

Add an immutable `provenance` snapshot to the plan, populated from the source
adapter when a draft is created and frozen on approval. It contains only the
channel code/label, public source UUID, safe title, and occurrence timestamp;
it never stores a URL or contact data. This lets approved tasks keep an honest
source label and time if the recording is later deleted. The viewer-specific
URL is always derived at response time after authorization.

Create a channel-neutral `ConversationActionPlan` model/service contract mapped
to the legacy table. `ZoomMeetingActionPlan` must extend
`ConversationActionPlan`, add a Zoom-only source scope, and retain its legacy
`meeting()` relation. Shared relations and consumers move to the parent type;
legacy Zoom call sites continue receiving the subclass and cannot accidentally
query Call/F2F rows.

The existing integer status state machine, JSON step schema, soft-delete
behavior, row locking, approval transaction, and `(source_action_plan_id,
source_step_key)` idempotency boundary must remain unchanged.

### Opportunity links

Apply the same compatibility pattern to `zoom_meeting_opportunity_links`:

- make `zoom_meeting_id` nullable while retaining its unique index;
- add the shared columns nullable, backfill all Zoom rows, alter both to NOT
  NULL, then add unique `(source_type, source_id)` across active and
  soft-deleted history;
- retain `zoom_meeting_id` for legacy Zoom reads/writes; and
- expose a neutral `ConversationOpportunityLink` service/model contract, with
  `ZoomMeetingOpportunityLink extends ConversationOpportunityLink` plus a
  Zoom-only source scope.

AI suggestion columns and human-confirmed columns remain separate. A suggestion
must never populate `engagement_id`, `reviewed_by`, or `reviewed_at`.

### Migration safety and rollback window

- The expansion migration must run up/down on a scratch database before any
  non-Zoom row exists.
- After the first Phone Call/F2F plan or link is written, dropping the shared
  identity would destroy its only source reference and restoring
  `zoom_meeting_id` to NOT NULL would fail. From that point, schema rollback is
  intentionally blocked by a preflight exception; rollback means disabling the
  feature flags and reverting application code while leaving additive columns
  in place. Tests must cover both the safe pre-write down and the explicit
  post-write refusal.
- Backfill must be deterministic and safe to rerun.
- Repository writes must restore-and-reuse soft-deleted rows under a transaction
  and `lockForUpdate()`; a unique index includes soft-deleted rows.
- Existing Zoom row IDs, plan UUIDs, action-item links, and opportunity link
  UUIDs must not change.
- No production data is copied into demo fixtures.
- Before any future removal of the Zoom compatibility subclasses, replace the
  model-constant default in the committed `2026_08_05_100001` migration with the
  required integer literal and run the migration-constant guard. This cleanup
  is not part of the current iteration.

## 7. Shared backend services

Extract channel-neutral services while retaining thin legacy wrappers where
needed:

1. `ConversationActionPlanDraftService`
   - reads the unified `meeting_report.next_steps` / recommended-action source;
   - requires completed analysis and a linked Lead;
   - preserves the current reanalysis rules and approved-plan immutability;
   - generates no draft for an unlinked source.

2. `ConversationActionPlanRepository`
   - owns replace, edit, feedback, approve, and materialization transactions;
   - accepts the shared source contract;
   - keeps per-step assignee, priority, schedule, feedback, and idempotency rules.

3. `ConversationRecordingChatContextBuilder`
   - builds the same bounded, injection-safe context for all channels;
   - preserves the `<RECORDING_CONTEXT>` boundary protection and `JSON_HEX_TAG`
     rules;
   - includes only the current source's transcript and analysis;
   - explicitly excludes caller/callee/customer phone fields,
     `deepgram_json`, media/source-audio URLs, credentials, raw provider data,
     imported lineage identifiers, and `ai_analysis_zh` from provider context;
   - never grants access merely because a UUID exists.

4. `ConversationOpportunityCandidateBuilder` and repository
   - searches only pipeline Engagements belonging to the linked Lead;
   - records AI suggestions separately from confirmation;
   - derives project, stage, and commission only from the confirmed Engagement;
   - handles retired soft-deleted Engagements explicitly.

5. `ConversationOpportunitySignals`
   - reads interest/stage only from analysis;
   - reads product/value only from a confirmed opportunity link;
   - reports data completeness, not a fabricated probability.

6. `ConversationSourcePresenter`
   - produces the source badge, timestamp, title, and authorized source URL used
     by review screens and task queues.

Existing Zoom-named services may remain temporarily as wrappers delegating to
the shared service. They must not contain a second implementation.

## 8. Analysis pipeline integration

After a successful analysis persist:

- Zoom continues its existing draft/opportunity hooks through compatibility
  wrappers;
- `AnalyzeCallRecording` invokes shared draft and opportunity suggestion sync;
- `AnalyzeF2fRecording` invokes the same shared sync; and
- retries remain idempotent.

Automatic retry/sync must not resurrect an opportunity suggestion a reviewer
has rejected. A `REJECTED` link is skipped by background sync. Only an explicit,
audited reviewer action to regenerate the suggestion may return it to
`UNREVIEWED`; this is distinct from a queue retry.

Translation-only writes must not regenerate or replace action plans. Reanalysis
may replace an unapproved draft according to the existing Zoom rule, but it must
never mutate an approved plan or generated task.

Provide a bounded, idempotent backfill command for already-analyzed, linked Call
and F2F records. It must support dry-run and channel/ID filters so the demo can
be prepared without processing every historical recording.

## 9. Routes, requests, and authorization

Use channel-specific public routes for source lookup and permission clarity,
but delegate them to shared controllers/services. Examples:

```text
/manage/zoom/recordings/{uuid}/ai-chat
/manage/calls/recordings/{uuid}/ai-chat
/manage/f2f/showroom/recordings/{uuid}/ai-chat
```

Action-plan and opportunity endpoints may use the plan/link UUID once loaded,
but every request must authorize through its resolved source.

Authorization rules:

- Source detail and Ask AI preserve each channel's current module boundary:
  Zoom requires the `VIEW_ZOOM` module permission plus
  `LeadVisibility::allowsLeadOrOwner` (both are part of today's reachable
  boundary — the route enforces `VIEW_ZOOM`, the query enforces lead/owner
  scope); Phone Call requires `VIEW_CALLS`; Showroom requires `VIEW_F2F`. This
  iteration must not silently narrow the existing Phone/F2F list/detail
  visibility by adding a new owner or Lead scope.
- Super Admin alone can review/approve team action plans, regenerate a rejected
  AI suggestion, and confirm/reject pipeline matches across all three channels.
- A non-Super-Admin receives only tasks assigned to that user in the Sales
  Checklist. Team checklist payloads and review endpoints are unavailable.
- UUID possession never bypasses source visibility.
- Server responses must omit restricted data, not merely hide controls in Vue.
- Team payloads must continue to omit phone/contact details where the existing
  privacy boundary requires it; My Tasks keeps its existing contact actions.
- A task assignee lacking the source channel's module permission still receives
  the immutable source label/title/time, but the server returns no source URL;
  the UI must not render a link that ends in 403.

Retain and expand the existing ordinary-admin and Super-Admin boundary tests for
all three source types.

## 10. Shared UI architecture

### Recording workspace modal

Extract a shared `RecordingWorkspaceModal` around `RecordingDetail`:

- approximately 90% viewport width and height on desktop;
- normalized identity header at the same vertical start as Ask AI;
- independently scrolling left analysis area and right Ask AI conversation;
- composer fixed at the bottom of the Ask AI rail;
- modal Close row aligned with the bottom of the rail;
- mobile order remains content, Ask AI, then Close; and
- channel-specific badges/actions enter through narrow slots or normalized
  metadata, not copied layouts.

Zoom, Phone Call, and F2F hosts become thin adapters. Call and F2F retain their
existing edit/link behavior and channel-specific metadata.

### Capability-driven RecordingDetail

Remove type-coded availability such as `type === 'zoom'`. Hosts pass explicit
capabilities and endpoint URLs:

```text
can_chat
chat_url
can_review_action_plan
action_plan_urls
can_review_opportunity
opportunity_urls
```

`RecordingChatTab` and shared review components remain endpoint-agnostic. Chat
keeps the existing keyboard contract: Enter sends, Shift+Enter inserts a
newline, and IME composition Enter never sends.

### Action-plan review

Move the reusable Vue implementation out of the Zoom namespace. The modal must
show a source header:

```text
Phone Call · Outbound call · 14 Aug 2026 10:30 · Open recording
```

The same header pattern applies to Zoom and Showroom. All existing per-step
controls and AI revision chat behavior remain.

### Channel lists

Phone Call and Showroom lists gain the same honest opportunity columns where
their current table layout permits:

- Intent (AI-read)
- Buying stage
- Pipeline product (confirmed only)
- Commission (confirmed only)
- Data completeness
- Action-plan/review entry

The labels must preserve the current disclaimer: interest and stage are an AI
reading of the conversation, not a prediction of purchase.

## 11. Unified Sales Checklist and provenance

`SalesWorkQueue` becomes source-neutral and remains the single source for Hub,
Sales Dashboard, and AI Agent follow-through counts.

Grouping rules:

- top-level unit is Lead;
- generated tasks from approved Zoom, Phone Call, and Showroom plans for that
  Lead appear in the same Lead group;
- the existing manual Action Item path and all headline counting semantics stay
  unchanged in this iteration; do not remove `source_action_plan_id IS NOT NULL`
  or admit null-plan rows into `SalesWorkQueue`;
- a Lead must not split across pagination;
- existing urgency/priority ordering remains;
- schedule filters apply to tasks as well as group selection; and
- source provenance is attached per task, not only once at group level.

Generated task response payload (computed per viewer):

```json
{
  "provenance": {
    "type": "phone_call",
    "label": "Phone Call",
    "title": "Outbound call",
    "occurred_at": "2026-08-14T10:30:00+08:00",
    "url": "/manage/calls/history?detail=..."
  }
}
```

The response `url` is derived at response time from the source adapter after
authorization and is never persisted. The stored provenance snapshot contains
only type, label, title, and `occurred_at`, as defined in section 6.

`provenance` is intentionally distinct from `CallRecording.source`, which means
Dowayai/API/manual-upload origin rather than conversation channel. If the
source later becomes inaccessible, the task remains visible to its assignee
using the plan's immutable provenance snapshot, but its URL is omitted; no link
may lead to a 403 dead end.

The queue continues grouping generated items by non-null
`source_action_plan_id`. `sourcePlan` and approval-time sort helpers move to the
neutral parent model. After plans are loaded, their sources are batch-resolved
through the registry by type so adding provenance costs at most one source query
per represented channel, never one query per task.

Completed-state calculations continue to use active, non-soft-deleted generated
items. An approved plan with zero active items is `no_active_steps`, never
vacuously `completed`.

## 12. Feature flags and AI prompts

Introduce channel-neutral runtime feature configuration backed by the existing
global `settings` table. The four release controls are Action Plans / Sales
Checklist, per-recording Ask AI, Pipeline Product / Commission, and Lead AI
Sales Coach. An absent or malformed database value means disabled. All four
ship disabled.

Only a Super Admin can change these controls, from the existing Manage → AI
Prompts/Settings page. The write endpoint is Super-Admin-gated explicitly —
never inherited from the page's broader integrations permission, which ordinary
legacy admins hold — and accepts exactly the four fixed keys. Non-Super-Admin
viewers of the page receive no release-control state in the server response.
Saving `1` or `0` in the database must take effect on the next request without
an environment edit, config-cache rebuild, or deploy, and equally on the next
queued job without a worker restart: background draft/suggestion sync reads the
same runtime resolver, so a long-lived queue worker must observe both enable
and disable on its next job. The
old Zoom environment keys no longer control these release gates; old Inertia
property names may remain response aliases during compatibility, but their
values come from the runtime resolver. This makes a merged deployment safely
off by default while still directly activatable in the UI.

Turning a flag off must be verified in the running app with literal `false`; do
not rely on a PHP cast assumption. The disabled state must continue to return
404 before validation for every mutating feature endpoint.

Shared requests and controllers must read this neutral flag resolver. A copied
`ZoomRecordingChatRequest` that still hard-codes
`features.zoom_recording_chat_demo` is not acceptable for Call/F2F endpoints;
the disabled-before-validation 404 behavior must remain identical on all three.

Use shared prompt keys for cross-channel context where the prompt semantics are
identical. Prompt/model selection stays in the existing AI Prompts database
configuration and must never be hard-coded in channel services.

If separate prompt keys are retained for evaluation, their bodies must be
generated from one canonical prompt resource or share an explicitly tested
contract so behavior cannot silently drift.

## 13. Error and empty states

- Unlinked source: show `Link Lead before creating an action plan or pipeline
  match`; Ask AI may still answer about the recording if source visibility and
  content are valid.
- No transcript and no analysis: Ask AI returns the existing honest empty-context
  response and does not call a provider.
- AI provider unavailable: preserve the user's draft/retry path and log the
  provider failure through `AiRequest`; do not expose credentials or raw provider
  errors.
- No pipeline candidates: show that no existing Engagement is available; never
  manufacture a project.
- Confirmed Engagement retired: show `Confirmed pipeline item retired` and keep
  historical provenance.
- Source deleted after approval: generated tasks and audit history survive;
  source presentation becomes unavailable without breaking checklist queries.

## 14. Testing and review gates

### Backend

- migration up/down before cross-channel writes, post-write down refusal, and
  deterministic backfill;
- exactly one source identity per plan/link;
- every create/restore/update path writes non-null `source_type` and `source_id`;
- legacy Zoom read/write compatibility;
- Call/F2F analysis hooks and retry idempotency;
- draft replacement versus approved immutability;
- approval transaction, row lock, and unique step idempotency;
- AI suggestion versus human confirmation separation;
- confirmed commission derived from the real Engagement only;
- source authorization for Super Admin and ordinary users;
- automatic retry preserves rejected opportunity decisions;
- source deletion/retirement behavior; and
- one work-queue consistency test across Zoom, Call, and F2F sources.

### Frontend

- all three hosts render the same 90% workspace and independent scroll owners;
- source switching resets Ask AI context and endpoint;
- Phone/F2F never post to a Zoom endpoint;
- mobile content/rail/footer order;
- Enter, Shift+Enter, IME, button, and duplicate-send behavior;
- action-plan controls and provenance links;
- confirmed-only product/commission display; and
- ordinary-user payloads do not leak team-only/contact data.

### End-to-end

For each channel, rehearse:

1. open an analyzed, linked recording;
2. ask a grounded question in the right rail;
3. inspect and edit the AI action plan;
4. assign different steps to sales users;
5. agree/disagree and record a rejection reason;
6. confirm a pipeline Engagement;
7. verify real product and commission display;
8. approve once and retry approval to prove idempotency;
9. open Sales Dashboard and find the Lead group;
10. verify every task's source badge and original-record link;
11. complete a task and confirm all shared counts change; and
12. repeat as ordinary Admin/Sales to prove scoped visibility.

Each implementation part requires its own specification review and code review.
Final integration requires a separate authorization/security review and a full
browser walkthrough. Pushing `dev-chen` and opening a ready PR happen only at
the very end, after the final release review passes and all four database
release gates are verified off; nothing is deployed or merged.

## 15. Delivery sequence

1. Shared source contract and additive compatibility migrations (nullable
   columns plus deterministic backfill only).
2. Shared action-plan repository/service, all-path dual writes, Zoom adapter
   regression pass, then the preflighted NOT NULL/unique tightening migration.
3. Phone Call integration, including Ask AI, draft generation, review, pipeline
   match, list signals, and source provenance.
4. Showroom F2F integration through the same shared services.
5. Source-neutral SalesWorkQueue and Lead-grouped provenance UI.
6. Shared recording workspace modal migration for all three hosts.
7. Authorization, concurrency, migration, build, and browser verification.

Phone Call is integrated before Showroom to prove the abstraction against one
new source. Showroom must reuse that proven path; any second copied
implementation blocks the review.

## 16. Non-goals

- No automatic approval or automatic pipeline confirmation.
- No independent Phone Call or Showroom task dashboard.
- No new conversion probability or predicted uplift.
- No replacement of the existing unified conversation-analysis schema.
- No unrelated redesign of Call History, Showroom, Lead Discussion, or the
  manual Action Item path.
- No production enablement, deployment, or merge. Push and a ready PR occur
  only as the final hand-off step after every release gate is verified off.

## 17. Acceptance criteria

The work is complete when an authorized user can open any supported Zoom, Phone
Call, or Showroom recording in the same workspace, chat against that exact
recording, review and approve an AI plan, confirm an existing pipeline item, and
see the resulting tasks grouped under the correct Lead in the single Sales
Checklist with honest source provenance. Existing Zoom behavior and IDs remain
compatible, ordinary users cannot cross their visibility boundary, and all
tests plus the three-channel browser walkthrough pass.
