Menu

Change language

GitHub repo

Crm Sales Call Workbench

blockcommunication

Live CRM sales call console with audio waveform dialer, real-time AI live transcription, objection handling battlecards, and CRM note logger.

Installation

$npx uipkge-ng@latest add crm-sales-call-workbench
Manifest:https://uipkge.dev/r/angular/crm-sales-call-workbench.jsonInstalls to:src/app/components/blocks/crm-sales-call-workbench/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

CrmSalesCallActionItem
interface CrmSalesCallActionItem {
  id: string
  text: string
  completed: boolean
  dueDate: string
  assignee: string
}

Files installed (2)

  • src/app/components/blocks/crm-sales-call-workbench/crm-sales-call-workbench.component.ts39.6 kB
    // Edit this file to change the call, transcript, battlecard and CRM copy. State is local --
    // wire the call controls, "End Call & Log to CRM" and "Log Call & Sync CRM Record" to your
    // telephony / CRM APIs.
    import { Component, DestroyRef, computed, effect, inject, signal, ChangeDetectionStrategy } from '@angular/core'
    import {
      Bot,
      Check,
      CheckCircle2,
      Clock,
      Copy,
      Disc,
      FileText,
      LucideAngularModule,
      Mic,
      MicOff,
      Pause,
      Phone,
      PhoneForwarded,
      PhoneOff,
      Play,
      Plus,
      Radio,
      RefreshCw,
      Save,
      ShieldCheck,
      Zap,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import { UiTextareaComponent } from '@/ui/textarea/textarea.component'
    
    export interface CrmSalesCallActionItem {
      id: string
      text: string
      completed: boolean
      dueDate: string
      assignee: string
    }
    
    // 32-bar audio spectrum waveform bars
    const waveformBars = [
      32, 48, 65, 42, 78, 92, 84, 66, 52, 74, 96, 82, 60, 44, 58, 88, 100, 76, 62, 48, 72, 94, 80, 68, 54, 42, 68, 86, 72,
      52, 38, 28,
    ]
    
    /**
     * CrmSalesCallWorkbench -- the React block, 1:1: a live call header (status dot, running
     * timer, prospect, End Call / Reconnect), a dark audio canvas with a 32-bar waveform and
     * Mute / Hold / Transfer / Record controls, a diarized AI transcript, an objection
     * battlecard, quick deal notes with tag chips, and a next-steps checklist with an add row.
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-crm-sales-call-workbench, [ui-crm-sales-call-workbench]',
      standalone: true,
      // React renders the root <div> itself: keep this wrapper out of the layout.
      host: { class: 'contents' },
      imports: [
        LucideAngularModule,
        UiAvatarComponent,
        UiAvatarFallbackComponent,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiCardDescriptionComponent,
        UiCardContentComponent,
        UiCheckboxComponent,
        UiInputComponent,
        UiSeparatorComponent,
        UiTextareaComponent,
      ],
      template: `
        <div data-slot="crm-sales-call-workbench" class="bg-background text-foreground w-full space-y-4">
          <!-- Top Header Bar -->
          <header class="bg-card rounded-xl border p-4 shadow-xs sm:px-6 sm:py-3.5">
            <div class="flex flex-wrap items-center justify-between gap-4">
              <!-- Call Metadata Strip -->
              <div class="flex flex-wrap items-center gap-3 sm:gap-4">
                <!-- Status Indicator & Timer -->
                <div class="flex flex-wrap items-center gap-2">
                  <span class="relative flex size-3">
                    @if (isLive()) {
                      <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                    }
                    <span [class]="statusDotClass()"></span>
                  </span>
                  <div class="flex items-center gap-1.5 font-mono text-sm font-semibold tracking-tight">
                    <lucide-icon [img]="Clock" class="text-muted-foreground size-3.5" />
                    <span class="tabular-nums">{{ statusLabel() }}</span>
                  </div>
                </div>
    
                <ui-separator orientation="vertical" class="hidden h-5 sm:block" />
    
                <!-- Customer Name & Role -->
                <div class="flex flex-wrap items-center gap-2.5">
                  <ui-avatar class="size-7 border">
                    <ui-avatar-fallback class="bg-primary/10 text-primary text-xs font-bold">DC</ui-avatar-fallback>
                  </ui-avatar>
                  <div class="flex flex-col sm:flex-row sm:items-center sm:gap-2">
                    <span class="text-foreground text-xs font-bold sm:text-sm">David Chen</span>
                    <span class="text-muted-foreground text-xs">VP of Engineering at Acme Corp</span>
                  </div>
                </div>
    
                <ui-separator orientation="vertical" class="hidden h-5 md:block" />
    
                <!-- Phone & Carrier Strip -->
                <div class="hidden items-center gap-2 md:flex">
                  <span ui-badge variant="outline" class="gap-1.5 font-mono text-xs font-medium">
                    <lucide-icon [img]="Phone" class="text-success size-3" />
                    <span>+1 (415) 555-0192</span>
                  </span>
                  <span ui-badge variant="secondary" class="text-xs font-normal">Acme Corp · 500 Seats ($180k ARR)</span>
                </div>
              </div>
    
              <!-- Header Actions: End Call / Sync -->
              <div class="flex flex-wrap items-center gap-2.5">
                <div class="text-muted-foreground hidden items-center gap-1.5 text-xs lg:flex">
                  <lucide-icon [img]="Radio" class="text-success size-3.5" />
                  <span class="font-mono text-xs">Opus HD · 14ms Latency</span>
                </div>
    
                @if (!isCallEnded()) {
                  <button
                    ui-button
                    variant="destructive"
                    size="sm"
                    class="h-8 gap-1.5 px-3 text-xs font-semibold shadow-xs"
                    (click)="endCall()"
                  >
                    <lucide-icon [img]="PhoneOff" class="size-3.5" />
                    <span>End Call &amp; Log to CRM</span>
                  </button>
                } @else {
                  <button
                    ui-button
                    variant="default"
                    size="sm"
                    class="h-8 gap-1.5 px-3 text-xs font-semibold shadow-xs"
                    (click)="reconnectCall()"
                  >
                    <lucide-icon [img]="RefreshCw" class="size-3.5" />
                    <span>Reconnect Call</span>
                  </button>
                }
              </div>
            </div>
          </header>
    
          <!-- 2-Column Call Workspace Grid (60% Left / 40% Right) -->
          <div class="grid grid-cols-1 gap-4 lg:grid-cols-12">
            <!-- Left Panel: Live Audio Visualizer & Real-Time AI Transcript (60% -> 7 Cols) -->
            <div class="space-y-4 lg:col-span-7 xl:col-span-7">
              <!-- Audio Dialing Canvas -->
              <section
                class="relative flex flex-col justify-between overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950 p-5 text-white shadow-lg"
              >
                <!-- Top Overlay: Speaker Status & WebRTC Specs -->
                <div class="flex flex-wrap items-center justify-between gap-3">
                  <!-- Active Speaker Info -->
                  <div
                    class="flex flex-wrap items-center gap-2.5 rounded-lg border border-zinc-800 bg-zinc-900/80 p-2 backdrop-blur-xs"
                  >
                    <div class="relative">
                      <ui-avatar class="border-success/60 size-8 border">
                        <ui-avatar-fallback class="text-success bg-zinc-800 text-xs font-bold">DC</ui-avatar-fallback>
                      </ui-avatar>
                      @if (isLive()) {
                        <span
                          class="bg-success absolute -right-0.5 -bottom-0.5 size-2.5 rounded-full ring-2 ring-zinc-950"
                        ></span>
                      }
                    </div>
                    <div>
                      <div class="flex items-center gap-1.5">
                        <span class="text-xs font-semibold text-zinc-100">David Chen</span>
                        <span
                          ui-badge
                          variant="outline"
                          class="border-success/40 bg-success/20 text-success px-1 py-0 text-xs"
                        >
                          Speaking
                        </span>
                      </div>
                      <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                        <lucide-icon [img]="Mic" class="text-success size-3" />
                        <span>Acme Corp · Audio Level: -14 dB</span>
                      </div>
                    </div>
                  </div>
    
                  <!-- Call Encryption & Recording Status -->
                  <div class="flex flex-wrap items-center gap-2">
                    @if (isRecording()) {
                      <div
                        class="border-destructive/40 bg-destructive/15 text-destructive flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs"
                      >
                        <span class="bg-destructive size-2 animate-pulse rounded-full"></span>
                        <span class="font-mono text-xs font-semibold">REC 08:42</span>
                      </div>
                    }
                    <div
                      class="flex items-center gap-1.5 rounded-lg border border-zinc-800 bg-zinc-900/80 px-2.5 py-1 text-xs text-zinc-300"
                    >
                      <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" />
                      <span class="font-mono text-xs">WebRTC AES-256</span>
                    </div>
                  </div>
                </div>
    
                <!-- Live 32-Bar Audio Waveform Visualizer -->
                <div class="my-6 flex flex-col items-center justify-center space-y-3">
                  <div class="flex h-24 w-full items-center justify-between gap-1 px-1 sm:gap-1.5 sm:px-4">
                    @for (bar of waveformBars; track $index) {
                      <div [class]="barClass($index)" [style]="barStyle(bar, $index)"></div>
                    }
                  </div>
    
                  <!-- Frequency Scale & Stream Diagnostics -->
                  <div
                    class="text-muted-foreground flex w-full items-center justify-between px-2 font-mono text-xs sm:px-6"
                  >
                    <span class="shrink-0 whitespace-nowrap">-48 dB</span>
                    <span class="text-muted-foreground min-w-0 text-center">
                      32-Band AI Audio Spectrum · Real-time Voice Detection
                    </span>
                    <span class="shrink-0 whitespace-nowrap">0 dB</span>
                  </div>
                </div>
    
                <!-- Audio Controls Bar -->
                <div class="flex items-center justify-center pt-2">
                  <div
                    class="flex flex-wrap items-center justify-center gap-2 rounded-full border border-zinc-800 bg-zinc-900/90 p-1.5 shadow-xl sm:px-4 sm:py-2"
                  >
                    <!-- Mute Button -->
                    <button
                      ui-button
                      [variant]="isMuted() ? 'destructive' : 'secondary'"
                      size="sm"
                      class="h-8 gap-1.5 rounded-full px-3 text-xs font-medium"
                      (click)="isMuted.set(!isMuted())"
                    >
                      @if (isMuted()) {
                        <lucide-icon [img]="MicOff" class="size-3.5" />
                      } @else {
                        <lucide-icon [img]="Mic" class="text-success size-3.5" />
                      }
                      <span>{{ isMuted() ? 'Muted' : 'Mute Mic' }}</span>
                    </button>
    
                    <!-- Hold Call Button -->
                    <button
                      ui-button
                      [variant]="isOnHold() ? 'default' : 'secondary'"
                      size="sm"
                      class="h-8 gap-1.5 rounded-full px-3 text-xs font-medium"
                      (click)="isOnHold.set(!isOnHold())"
                    >
                      @if (isOnHold()) {
                        <lucide-icon [img]="Play" class="text-warning size-3.5" />
                      } @else {
                        <lucide-icon [img]="Pause" class="size-3.5" />
                      }
                      <span>{{ isOnHold() ? 'Resume Call' : 'Hold Call' }}</span>
                    </button>
    
                    <!-- Transfer Button -->
                    <button
                      ui-button
                      variant="secondary"
                      size="sm"
                      class="h-8 gap-1.5 rounded-full px-3 text-xs font-medium text-zinc-200"
                    >
                      <lucide-icon [img]="PhoneForwarded" class="text-muted-foreground size-3.5" />
                      <span>Transfer</span>
                    </button>
    
                    <!-- Record Toggle Button -->
                    <button
                      ui-button
                      [variant]="isRecording() ? 'destructive' : 'secondary'"
                      size="sm"
                      class="h-8 gap-1.5 rounded-full px-3 text-xs font-medium"
                      (click)="isRecording.set(!isRecording())"
                    >
                      <!-- [class] (not class=) feeds only the svg, so the spin is not applied twice (host + svg). -->
                      <lucide-icon [img]="Disc" [class]="isRecording() ? 'size-3.5 animate-spin' : 'size-3.5'" />
                      <span>{{ isRecording() ? 'Recording' : 'Record' }}</span>
                    </button>
                  </div>
                </div>
              </section>
    
              <!-- Real-Time AI Live Speech Transcript -->
              <ui-card class="shadow-xs">
                <ui-card-header class="border-b px-4 py-3 sm:px-5">
                  <div class="flex flex-wrap items-center justify-between gap-2">
                    <div class="space-y-0.5">
                      <ui-card-title class="flex flex-wrap items-center gap-2 text-sm font-bold">
                        <lucide-icon [img]="Bot" class="text-primary size-4" />
                        <span>Real-Time AI Live Speech Transcript</span>
                      </ui-card-title>
                      <ui-card-description class="text-xs">
                        Deepgram Nova-2 STT · Real-time diarization &amp; sentiment detection
                      </ui-card-description>
                    </div>
                    <span
                      ui-badge
                      variant="outline"
                      class="border-success/30 bg-success/10 text-success gap-1.5 text-xs font-medium"
                    >
                      <span class="bg-success size-1.5 animate-pulse rounded-full"></span>
                      <span>Live Diarization</span>
                    </span>
                  </div>
                </ui-card-header>
    
                <ui-card-content class="space-y-4 p-4 sm:p-5">
                  <!-- Turn 1: Elena Rostova -->
                  <div class="space-y-1.5">
                    <div class="flex flex-wrap items-center justify-between gap-2">
                      <div class="flex flex-wrap items-center gap-2">
                        <ui-avatar class="size-6">
                          <ui-avatar-fallback class="bg-primary/20 text-primary text-xs font-bold">ER</ui-avatar-fallback>
                        </ui-avatar>
                        <span class="text-foreground text-xs font-bold">Elena Rostova</span>
                        <span ui-badge variant="outline" class="text-xs font-normal">Account Executive (You)</span>
                      </div>
                      <div class="flex flex-wrap items-center gap-2">
                        <span ui-badge variant="outline" class="border-info/30 bg-info/10 text-info text-xs"
                          >Discovery</span
                        >
                        <span class="text-muted-foreground font-mono text-xs">08:12</span>
                      </div>
                    </div>
                    <div class="bg-muted/30 text-foreground rounded-lg border p-3 text-xs leading-relaxed">
                      "Thanks for walking us through your current build setup, David. You mentioned earlier that team
                      velocity slowed down during the last framework migration. How is that impacting your Q3 delivery
                      milestones?"
                    </div>
                  </div>
    
                  <!-- Turn 2: David Chen (Objection) -->
                  <div class="space-y-1.5">
                    <div class="flex flex-wrap items-center justify-between gap-2">
                      <div class="flex flex-wrap items-center gap-2">
                        <ui-avatar class="size-6">
                          <ui-avatar-fallback class="bg-warning/20 text-warning text-xs font-bold">DC</ui-avatar-fallback>
                        </ui-avatar>
                        <span class="text-foreground text-xs font-bold">David Chen</span>
                        <span ui-badge variant="secondary" class="text-xs font-normal">Prospect (Acme Corp)</span>
                      </div>
                      <div class="flex flex-wrap items-center gap-2">
                        <span ui-badge variant="outline" class="border-warning/40 bg-warning/10 text-warning text-xs">
                          Budget Hesitation
                        </span>
                        <span class="text-muted-foreground font-mono text-xs">08:24</span>
                      </div>
                    </div>
                    <div class="bg-muted/30 text-foreground space-y-2 rounded-lg border p-3 text-xs leading-relaxed">
                      <p>
                        "Right now we're spending around 12 engineering hours a week just fixing broken dependency cascades
                        across 40 micro-frontends whenever upstream packages release major semver bumps. We want to
                        standardize, but leadership is wary of signing another 6-figure SaaS vendor contract this quarter."
                      </p>
                      <!-- AI Extracted Entities -->
                      <div class="flex flex-wrap items-center gap-1.5 border-t pt-2 text-xs">
                        <span class="text-muted-foreground font-medium">AI Entities:</span>
                        <span class="bg-warning/10 text-warning rounded px-1.5 py-0.5 font-mono text-xs">
                          40 micro-frontends
                        </span>
                        <span class="bg-warning/10 text-warning rounded px-1.5 py-0.5 font-mono text-xs">
                          12 eng hrs/wk debt
                        </span>
                        <span class="bg-warning/10 text-warning rounded px-1.5 py-0.5 font-mono text-xs">
                          Objection: 6-figure SaaS cost
                        </span>
                      </div>
                    </div>
                  </div>
    
                  <!-- Turn 3: Elena Rostova (Value Prop) -->
                  <div class="space-y-1.5">
                    <div class="flex flex-wrap items-center justify-between gap-2">
                      <div class="flex flex-wrap items-center gap-2">
                        <ui-avatar class="size-6">
                          <ui-avatar-fallback class="bg-primary/20 text-primary text-xs font-bold">ER</ui-avatar-fallback>
                        </ui-avatar>
                        <span class="text-foreground text-xs font-bold">Elena Rostova</span>
                        <span ui-badge variant="outline" class="text-xs font-normal">Account Executive (You)</span>
                      </div>
                      <div class="flex flex-wrap items-center gap-2">
                        <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs">
                          Value Proposition
                        </span>
                        <span class="text-muted-foreground font-mono text-xs">08:31</span>
                      </div>
                    </div>
                    <div class="bg-muted/30 text-foreground rounded-lg border p-3 text-xs leading-relaxed">
                      "That makes total sense. With UIPKGE's registry distribution model, your engineers copy the exact
                      source code directly into your own repositories. You own the code completely, eliminate runtime npm
                      lock-in, and never deal with breaking semver upgrades again."
                    </div>
                  </div>
    
                  <!-- Turn 4: David Chen (Buying Signal) -->
                  <div class="space-y-1.5">
                    <div class="flex flex-wrap items-center justify-between gap-2">
                      <div class="flex flex-wrap items-center gap-2">
                        <ui-avatar class="size-6">
                          <ui-avatar-fallback class="bg-success/20 text-success text-xs font-bold">DC</ui-avatar-fallback>
                        </ui-avatar>
                        <span class="text-foreground text-xs font-bold">David Chen</span>
                        <span ui-badge variant="secondary" class="text-xs font-normal">Prospect (Acme Corp)</span>
                      </div>
                      <div class="flex flex-wrap items-center gap-2">
                        <span
                          ui-badge
                          variant="outline"
                          class="border-success/40 bg-success/15 text-success text-xs font-medium"
                        >
                          Positive Intent
                        </span>
                        <span class="text-muted-foreground font-mono text-xs">08:42</span>
                      </div>
                    </div>
                    <div class="bg-muted/30 text-foreground space-y-2 rounded-lg border p-3 text-xs leading-relaxed">
                      <p>
                        "That would honestly solve our biggest compliance and dependency audit headache. If we can verify
                        SOC2 Type II and get our SecOps team to review your DPA, we'd be ready to proceed with a 500-seat
                        Enterprise rollout next month."
                      </p>
                      <div class="flex flex-wrap items-center justify-between gap-2 border-t pt-2 text-xs">
                        <div class="text-success flex items-center gap-1.5">
                          <lucide-icon [img]="CheckCircle2" class="size-3.5" />
                          <span class="font-medium">Buying Signal: 500 seats ($180k ARR) pending SOC2 &amp; DPA</span>
                        </div>
                        <span class="text-muted-foreground text-xs">AI Confidence: 99.4%</span>
                      </div>
                    </div>
                  </div>
    
                  <!-- Active Live Listening Indicator -->
                  <div class="bg-muted/20 flex items-center justify-between rounded-lg border border-dashed p-3 text-xs">
                    <div class="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
                      <span class="bg-success size-2 rounded-full"></span>
                      <span class="text-foreground font-medium">Real-time speech stream active...</span>
                      <span>listening to David Chen</span>
                    </div>
                    <span class="text-muted-foreground font-mono text-xs">Deepgram Nova-2 STT</span>
                  </div>
                </ui-card-content>
              </ui-card>
            </div>
    
            <!-- Right Panel: AI Battlecards & CRM Note Logger (40% -> 5 Cols) -->
            <div class="space-y-4 lg:col-span-5 xl:col-span-5">
              <!-- Real-Time AI Objection Handling Battlecard -->
              <ui-card class="border-primary/40 from-primary/5 via-card to-card bg-gradient-to-br shadow-xs">
                <ui-card-header class="px-4 pt-4 pb-2 sm:px-5">
                  <div class="flex items-center justify-between">
                    <span
                      ui-badge
                      variant="outline"
                      class="border-primary/30 bg-primary/10 text-primary gap-1.5 text-xs font-semibold"
                    >
                      <lucide-icon [img]="Zap" class="text-primary size-3" />
                      <span>AI Battlecard · Live Trigger</span>
                    </span>
                    <span class="text-muted-foreground font-mono text-xs">Confidence: 98%</span>
                  </div>
                  <ui-card-title class="text-foreground pt-1.5 text-sm font-bold">
                    Competitor / Pricing Objection: Legacy NPM Packages
                  </ui-card-title>
                  <ui-card-description class="text-xs">
                    Triggered by mention of 6-figure SaaS vendor fatigue &amp; dependency cascades
                  </ui-card-description>
                </ui-card-header>
    
                <ui-card-content class="space-y-3 px-4 pb-4 sm:px-5">
                  <!-- AI Counter-Points List -->
                  <div class="bg-card space-y-2.5 rounded-lg border p-3 text-xs">
                    <div class="text-foreground flex items-center gap-1.5 font-bold">
                      <lucide-icon [img]="Zap" class="text-primary size-3.5" />
                      <span>Recommended Response Points:</span>
                    </div>
    
                    <div class="space-y-2 text-xs">
                      <div class="flex items-start gap-2">
                        <span
                          class="bg-primary/20 text-primary mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full text-xs font-bold"
                        >
                          1
                        </span>
                        <p class="text-muted-foreground leading-relaxed">
                          <strong class="text-foreground font-semibold">Zero-Dependency Ownership:</strong> Code is copied
                          directly into Acme's repos via CLI (<code class="text-foreground bg-muted rounded px-1"
                            >npx shadcn-vue add</code
                          >). Zero npm runtime lock-in.
                        </p>
                      </div>
    
                      <div class="flex items-start gap-2">
                        <span
                          class="bg-primary/20 text-primary mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full text-xs font-bold"
                        >
                          2
                        </span>
                        <p class="text-muted-foreground leading-relaxed">
                          <strong class="text-foreground font-semibold">Eliminate Upgrade Cascades:</strong> Reclaim the 12
                          hrs/week engineering maintenance debt David mentioned.
                        </p>
                      </div>
    
                      <div class="flex items-start gap-2">
                        <span
                          class="bg-primary/20 text-primary mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full text-xs font-bold"
                        >
                          3
                        </span>
                        <p class="text-muted-foreground leading-relaxed">
                          <strong class="text-foreground font-semibold">Fast Enterprise ROI:</strong> Predictable team seat
                          license pays for itself in under 3 weeks of reclaimed developer hours.
                        </p>
                      </div>
                    </div>
                  </div>
    
                  <!-- Copy Talking Points Button -->
                  <button
                    ui-button
                    variant="outline"
                    size="sm"
                    class="w-full gap-1.5 text-xs font-semibold"
                    (click)="copyTalkingPoints()"
                  >
                    @if (copiedTalkingPoints()) {
                      <lucide-icon [img]="Check" class="text-success size-3.5" />
                    } @else {
                      <lucide-icon [img]="Copy" class="size-3.5" />
                    }
                    <span>{{
                      copiedTalkingPoints() ? 'Talking Points Copied to Clipboard!' : 'Copy Talking Points to Clipboard'
                    }}</span>
                  </button>
                </ui-card-content>
              </ui-card>
    
              <!-- Quick Deal Notes Textarea -->
              <ui-card class="shadow-xs">
                <ui-card-header class="px-4 pt-4 pb-2 sm:px-5">
                  <div class="flex items-center justify-between">
                    <ui-card-title class="flex items-center gap-1.5 text-sm font-bold">
                      <lucide-icon [img]="FileText" class="text-primary size-4" />
                      <span>Quick Deal Notes</span>
                    </ui-card-title>
                    <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                      <lucide-icon [img]="CheckCircle2" class="text-success size-3.5" />
                      <span>Synced with Salesforce</span>
                    </div>
                  </div>
                  <ui-card-description class="text-xs">
                    Live call documentation synced to Opportunity #ACME-9481
                  </ui-card-description>
                </ui-card-header>
    
                <ui-card-content class="space-y-3 px-4 pb-4 sm:px-5">
                  <ui-textarea
                    [value]="dealNotes()"
                    (valueChange)="dealNotes.set($event)"
                    [rows]="4"
                    class="resize-y text-xs leading-relaxed"
                    placeholder="Enter call notes, key customer requirements, and objections..."
                  />
    
                  <!-- Quick Append Tags -->
                  <div class="space-y-1.5">
                    <span class="text-muted-foreground text-xs font-medium">Quick Tags:</span>
                    <div class="flex flex-wrap gap-1.5">
                      <button
                        ui-button
                        variant="outline"
                        size="sm"
                        class="h-6 px-2 text-xs"
                        (click)="appendTag('SOC2-Security')"
                      >
                        + #soc2-security
                      </button>
                      <button
                        ui-button
                        variant="outline"
                        size="sm"
                        class="h-6 px-2 text-xs"
                        (click)="appendTag('500-Seats')"
                      >
                        + #500-seats
                      </button>
                      <button
                        ui-button
                        variant="outline"
                        size="sm"
                        class="h-6 px-2 text-xs"
                        (click)="appendTag('DPA-Review')"
                      >
                        + #dpa-review
                      </button>
                      <button
                        ui-button
                        variant="outline"
                        size="sm"
                        class="h-6 px-2 text-xs"
                        (click)="appendTag('Q3-Close')"
                      >
                        + #q3-close
                      </button>
                    </div>
                  </div>
    
                  <!-- Opportunity Metrics Strip -->
                  <div class="bg-muted/30 grid grid-cols-3 gap-2 rounded-lg border p-2.5 text-center text-xs">
                    <div>
                      <span class="text-muted-foreground block text-xs">Deal Value</span>
                      <span class="text-foreground font-mono font-bold">$180,000</span>
                    </div>
                    <div class="border-x">
                      <span class="text-muted-foreground block text-xs">Stage</span>
                      <span class="text-foreground font-semibold">Negotiation</span>
                    </div>
                    <div>
                      <span class="text-muted-foreground block text-xs">Win Prob.</span>
                      <span class="text-success font-semibold">85%</span>
                    </div>
                  </div>
                </ui-card-content>
              </ui-card>
    
              <!-- Next Steps Checklist -->
              <ui-card class="shadow-xs">
                <ui-card-header class="px-4 pt-4 pb-2 sm:px-5">
                  <div class="flex items-center justify-between">
                    <ui-card-title class="flex items-center gap-1.5 text-sm font-bold">
                      <lucide-icon [img]="CheckCircle2" class="text-primary size-4" />
                      <span>Next Steps Checklist</span>
                    </ui-card-title>
                    <span ui-badge variant="secondary" class="text-xs"
                      >{{ completedCount() }}/{{ actionItems().length }} Done</span
                    >
                  </div>
                  <ui-card-description class="text-xs">
                    Action items committed during call. Checked items auto-create CRM tasks.
                  </ui-card-description>
                </ui-card-header>
    
                <ui-card-content class="space-y-3 px-4 pb-4 sm:px-5">
                  <div class="space-y-2">
                    @for (item of actionItems(); track item.id) {
                      <div
                        class="bg-muted/20 hover:bg-muted/40 flex items-start gap-2.5 rounded-lg border p-2.5 transition-colors"
                      >
                        <ui-checkbox
                          [id]="item.id"
                          [checked]="item.completed"
                          (checkedChange)="toggleStep(item.id)"
                          class="mt-0.5"
                        />
                        <div class="flex-1 space-y-0.5">
                          <label [attr.for]="item.id" [class]="stepLabelClass(item.completed)">{{ item.text }}</label>
                          <div class="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
                            <span>{{ item.dueDate }}</span>
                            <span>·</span>
                            <span>{{ item.assignee }}</span>
                          </div>
                        </div>
                      </div>
                    }
                  </div>
    
                  <!-- Add Item Row -->
                  <div class="flex flex-wrap items-center gap-2 pt-1">
                    <ui-input
                      [value]="newItemText()"
                      (valueChange)="newItemText.set($event)"
                      placeholder="Add custom action item..."
                      class="h-8 text-xs"
                      (keydown)="onNewItemKeydown($event)"
                    />
                    <button
                      ui-button
                      size="sm"
                      variant="outline"
                      class="h-8 shrink-0 px-3 text-xs"
                      [disabled]="!newItemText().trim()"
                      (click)="addActionItem()"
                    >
                      <lucide-icon [img]="Plus" class="size-3.5" />
                      <span>Add</span>
                    </button>
                  </div>
    
                  <!-- Sync Button -->
                  <div class="border-t pt-2">
                    <button ui-button size="sm" class="w-full gap-1.5 text-xs font-semibold shadow-xs" (click)="syncCrm()">
                      @if (isSyncedCrm()) {
                        <lucide-icon [img]="Check" class="size-3.5" />
                      } @else {
                        <lucide-icon [img]="Save" class="size-3.5" />
                      }
                      <span>{{
                        isSyncedCrm() ? 'Call Summary & Tasks Logged to CRM!' : 'Log Call & Sync CRM Record'
                      }}</span>
                    </button>
                  </div>
                </ui-card-content>
              </ui-card>
            </div>
          </div>
        </div>
      `,
    })
    export class UiCrmSalesCallWorkbenchComponent {
      private readonly timers = new Set<ReturnType<typeof setTimeout>>()
    
      // Interactive Call State
      readonly isMuted = signal(false)
      readonly isOnHold = signal(false)
      readonly isRecording = signal(true)
      readonly isCallEnded = signal(false)
      readonly callDurationSeconds = signal(522) // 08m:42s
      readonly copiedTalkingPoints = signal(false)
      readonly isSyncedCrm = signal(false)
      readonly newItemText = signal('')
    
      // CRM Deal Notes State
      readonly dealNotes = signal(
        'David Chen (VP of Eng) confirmed Acme Corp has 40 micro-frontends facing frequent dependency breakages (~12 eng hrs/week maintenance debt). Highly receptive to UIPKGE zero-dependency registry model. Key decision factor: SOC2 Type II compliance and DPA sign-off. Deal size: 500 seats ($180k ARR). Target close: Q3 end.',
      )
    
      // Next Steps Checklist State
      readonly actionItems = signal<CrmSalesCallActionItem[]>([
        {
          id: 'step-1',
          text: 'Send SOC2 Type II compliance package & Security Whitepaper',
          completed: true,
          dueDate: 'Today · 2:00 PM',
          assignee: 'Elena Rostova',
        },
        {
          id: 'step-2',
          text: 'Schedule DPA legal review with Acme SecOps team',
          completed: true,
          dueDate: 'Tomorrow · 10:00 AM',
          assignee: 'Elena Rostova',
        },
        {
          id: 'step-3',
          text: 'Draft 500-seat Enterprise MSA & customized Order Form',
          completed: false,
          dueDate: 'Thu, Aug 27',
          assignee: 'Legal Ops',
        },
        {
          id: 'step-4',
          text: 'Send post-call recap email with call recording & transcript highlights',
          completed: false,
          dueDate: 'Today · 5:00 PM',
          assignee: 'Elena Rostova',
        },
      ])
    
      readonly isLive = computed(() => !this.isCallEnded() && !this.isOnHold())
      readonly completedCount = computed(() => this.actionItems().filter((i) => i.completed).length)
    
      readonly formattedDuration = computed(() => {
        const seconds = this.callDurationSeconds()
        const mins = Math.floor(seconds / 60)
        const secs = seconds % 60
        return `${mins.toString().padStart(2, '0')}m:${secs.toString().padStart(2, '0')}s`
      })
    
      readonly statusLabel = computed(() =>
        this.isCallEnded()
          ? 'Call Ended'
          : this.isOnHold()
            ? 'Call On Hold'
            : `Live Call in Progress · ${this.formattedDuration()}`,
      )
    
      readonly statusDotClass = computed(() =>
        cn(
          'relative inline-flex size-3 rounded-full',
          this.isCallEnded() ? 'bg-zinc-400' : this.isOnHold() ? 'bg-warning' : 'bg-success',
        ),
      )
    
      protected readonly waveformBars = waveformBars
    
      constructor() {
        // Timer: ticks while the call is live, pauses on hold, stops when the call ends.
        effect((onCleanup) => {
          if (this.isCallEnded() || this.isOnHold()) return
          const interval = setInterval(() => this.callDurationSeconds.update((prev) => prev + 1), 1000)
          onCleanup(() => clearInterval(interval))
        })
        inject(DestroyRef).onDestroy(() => {
          for (const t of this.timers) clearTimeout(t)
          this.timers.clear()
        })
      }
    
      barClass(index: number): string {
        const ended = this.isCallEnded()
        const onHold = this.isOnHold()
        const muted = this.isMuted()
        const isBarMuted = muted && index % 2 === 0
        return cn(
          'w-full max-w-[8px] rounded-full transition-[height] duration-200',
          ended
            ? 'h-2 bg-zinc-800'
            : onHold
              ? 'bg-warning/60 h-3'
              : isBarMuted
                ? 'h-2 bg-zinc-700'
                : 'bg-gradient-to-t from-emerald-500 to-teal-300',
          !ended && !onHold && !muted ? 'animate-pulse' : '',
        )
      }
    
      /** Runtime values per bar (height from the spectrum, a staggered pulse), as React's inline style. */
      barStyle(bar: number, index: number): Record<string, string> {
        return {
          height: this.isCallEnded() ? '8%' : this.isOnHold() ? '16%' : `${bar}%`,
          'animation-delay': `${(index % 8) * 120}ms`,
          'animation-duration': '1.2s',
        }
      }
    
      stepLabelClass(completed: boolean): string {
        return cn(
          'cursor-pointer text-xs font-medium select-none',
          completed ? 'text-muted-foreground line-through' : 'text-foreground',
        )
      }
    
      endCall(): void {
        this.isCallEnded.set(true)
      }
    
      reconnectCall(): void {
        this.isCallEnded.set(false)
        this.callDurationSeconds.set(0)
      }
    
      copyTalkingPoints(): void {
        this.copiedTalkingPoints.set(true)
        this.later(() => this.copiedTalkingPoints.set(false), 2500)
      }
    
      syncCrm(): void {
        this.isSyncedCrm.set(true)
        this.later(() => this.isSyncedCrm.set(false), 3500)
      }
    
      addActionItem(): void {
        const text = this.newItemText().trim()
        if (!text) return
        this.actionItems.update((prev) => [
          ...prev,
          {
            id: `step-${Date.now()}`,
            text,
            completed: false,
            dueDate: 'Next Week',
            assignee: 'Elena Rostova',
          },
        ])
        this.newItemText.set('')
      }
    
      onNewItemKeydown(event: KeyboardEvent): void {
        if (event.key === 'Enter') {
          event.preventDefault()
          this.addActionItem()
        }
      }
    
      toggleStep(id: string): void {
        this.actionItems.update((prev) =>
          prev.map((item) => (item.id === id ? { ...item, completed: !item.completed } : item)),
        )
      }
    
      appendTag(tag: string): void {
        if (!this.dealNotes().includes(tag)) {
          this.dealNotes.update((prev) => `${prev}\n[Tag: ${tag}]`)
        }
      }
    
      private later(fn: () => void, ms: number): void {
        const t = setTimeout(() => {
          this.timers.delete(t)
          fn()
        }, ms)
        this.timers.add(t)
      }
    
      protected readonly Bot = Bot
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Copy = Copy
      protected readonly Disc = Disc
      protected readonly FileText = FileText
      protected readonly Mic = Mic
      protected readonly MicOff = MicOff
      protected readonly Pause = Pause
      protected readonly Phone = Phone
      protected readonly PhoneForwarded = PhoneForwarded
      protected readonly PhoneOff = PhoneOff
      protected readonly Play = Play
      protected readonly Plus = Plus
      protected readonly Radio = Radio
      protected readonly RefreshCw = RefreshCw
      protected readonly Save = Save
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Zap = Zap
    }
    
  • src/app/components/blocks/crm-sales-call-workbench/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/crm-sales-call-workbench.json