Crm Sales Call Workbench
blockcommunicationLive CRM sales call console with audio waveform dialer, real-time AI live transcription, objection handling battlecards, and CRM note logger.
Installation
$pnpm dlx uipkge-ng@latest add crm-sales-call-workbench$npx uipkge-ng@latest add crm-sales-call-workbench$yarn dlx uipkge-ng@latest add crm-sales-call-workbench$bunx uipkge-ng@latest add crm-sales-call-workbenchManifest:
https://uipkge.dev/r/angular/crm-sales-call-workbench.jsonInstalls to:src/app/components/blocks/crm-sales-call-workbench/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
CrmSalesCallActionIteminterface 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 & 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 & 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 & 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 & 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
export { UiCrmSalesCallWorkbenchComponent, type CrmSalesCallActionItem } from './crm-sales-call-workbench.component'
Raw manifest:https://uipkge.dev/r/angular/crm-sales-call-workbench.json