663 lines
20 KiB
Vue
663 lines
20 KiB
Vue
<script setup lang="ts">
|
|
import { PIPELINE_STAGES, type PipelineStage } from '~/composables/useSalesPipeline'
|
|
|
|
definePageMeta({ ssr: false })
|
|
usePageTitle('Mission Control \u00b7 Quotes')
|
|
|
|
/* ── Mock agents ── */
|
|
interface AgentStats {
|
|
name: string
|
|
activeDeals: number
|
|
quotesSent: number
|
|
conversionRate: number
|
|
avgResponseTime: string
|
|
pipelineValue: number
|
|
}
|
|
|
|
const agents: AgentStats[] = [
|
|
{ name: 'Ana Ram\u00edrez', activeDeals: 8, quotesSent: 14, conversionRate: 71, avgResponseTime: '1.2d', pipelineValue: 18_400 },
|
|
{ name: 'Marco Villanueva', activeDeals: 4, quotesSent: 6, conversionRate: 83, avgResponseTime: '0.8d', pipelineValue: 22_100 },
|
|
{ name: 'Luc\u00eda Fern\u00e1ndez', activeDeals: 6, quotesSent: 4, conversionRate: 42, avgResponseTime: '2.4d', pipelineValue: 4_700 },
|
|
]
|
|
|
|
/* ── Mock deals spread across pipeline ── */
|
|
interface MockDeal {
|
|
id: string
|
|
customerName: string
|
|
productLine: string
|
|
currentStage: PipelineStage
|
|
agent: string
|
|
daysAtStage: number
|
|
premium: number
|
|
formCompletion: number
|
|
createdDaysAgo: number
|
|
}
|
|
|
|
const mockDeals: MockDeal[] = [
|
|
{ id: 'd01', customerName: 'Mar\u00eda Elena P\u00e9rez', productLine: 'Auto', currentStage: 'customer', agent: 'Ana Ram\u00edrez', daysAtStage: 1, premium: 1200, formCompletion: 45, createdDaysAgo: 1 },
|
|
{ id: 'd02', customerName: 'Carlos Mendoza', productLine: 'Life', currentStage: 'customer', agent: 'Luc\u00eda Fern\u00e1ndez', daysAtStage: 3, premium: 3200, formCompletion: 20, createdDaysAgo: 3 },
|
|
{ id: 'd03', customerName: 'Laura Castillo', productLine: 'Health', currentStage: 'get_quotes', agent: 'Ana Ram\u00edrez', daysAtStage: 2, premium: 2800, formCompletion: 60, createdDaysAgo: 5 },
|
|
{ id: 'd04', customerName: 'Sof\u00eda Rojas Delgado', productLine: 'Auto', currentStage: 'get_quotes', agent: 'Luc\u00eda Fern\u00e1ndez', daysAtStage: 4, premium: 1500, formCompletion: 50, createdDaysAgo: 7 },
|
|
{ id: 'd05', customerName: 'Andr\u00e9s Vargas', productLine: 'General Risk', currentStage: 'get_quotes', agent: 'Marco Villanueva', daysAtStage: 1, premium: 8500, formCompletion: 75, createdDaysAgo: 4 },
|
|
{ id: 'd06', customerName: 'Patricia Herrera', productLine: 'Auto', currentStage: 'waiting_carriers', agent: 'Ana Ram\u00edrez', daysAtStage: 6, premium: 1100, formCompletion: 100, createdDaysAgo: 10 },
|
|
{ id: 'd07', customerName: 'Fernando L\u00f3pez', productLine: 'Life', currentStage: 'waiting_carriers', agent: 'Luc\u00eda Fern\u00e1ndez', daysAtStage: 8, premium: 4200, formCompletion: 100, createdDaysAgo: 12 },
|
|
{ id: 'd08', customerName: 'Roberto Jim\u00e9nez Mora', productLine: 'Health', currentStage: 'waiting_carriers', agent: 'Marco Villanueva', daysAtStage: 3, premium: 3100, formCompletion: 100, createdDaysAgo: 8 },
|
|
{ id: 'd09', customerName: 'Gabriela Torres', productLine: 'Auto', currentStage: 'present_quotes', agent: 'Ana Ram\u00edrez', daysAtStage: 2, premium: 1400, formCompletion: 100, createdDaysAgo: 14 },
|
|
{ id: 'd10', customerName: 'Diego Salazar', productLine: 'Life', currentStage: 'present_quotes', agent: 'Marco Villanueva', daysAtStage: 1, premium: 5600, formCompletion: 100, createdDaysAgo: 9 },
|
|
{ id: 'd11', customerName: 'Isabel Moreno', productLine: 'General Risk', currentStage: 'waiting_client', agent: 'Ana Ram\u00edrez', daysAtStage: 7, premium: 2200, formCompletion: 100, createdDaysAgo: 16 },
|
|
{ id: 'd12', customerName: 'Alejandro Rios', productLine: 'Auto', currentStage: 'waiting_client', agent: 'Luc\u00eda Fern\u00e1ndez', daysAtStage: 12, premium: 900, formCompletion: 100, createdDaysAgo: 20 },
|
|
{ id: 'd13', customerName: 'Valentina Cruz', productLine: 'Health', currentStage: 'solicitud', agent: 'Ana Ram\u00edrez', daysAtStage: 3, premium: 2700, formCompletion: 68, createdDaysAgo: 18 },
|
|
{ id: 'd14', customerName: 'Jorge Navarro', productLine: 'Life', currentStage: 'solicitud', agent: 'Marco Villanueva', daysAtStage: 2, premium: 6800, formCompletion: 85, createdDaysAgo: 15 },
|
|
{ id: 'd15', customerName: 'Camila Fuentes', productLine: 'Auto', currentStage: 'emission', agent: 'Ana Ram\u00edrez', daysAtStage: 1, premium: 1600, formCompletion: 92, createdDaysAgo: 22 },
|
|
{ id: 'd16', customerName: 'Ra\u00fal Espinoza', productLine: 'General Risk', currentStage: 'emission', agent: 'Marco Villanueva', daysAtStage: 2, premium: 7200, formCompletion: 100, createdDaysAgo: 25 },
|
|
{ id: 'd17', customerName: 'M\u00f3nica Delgado', productLine: 'Life', currentStage: 'waiting_carriers', agent: 'Ana Ram\u00edrez', daysAtStage: 11, premium: 3800, formCompletion: 100, createdDaysAgo: 15 },
|
|
{ id: 'd18', customerName: 'Enrique Paredes', productLine: 'Health', currentStage: 'customer', agent: 'Luc\u00eda Fern\u00e1ndez', daysAtStage: 6, premium: 2100, formCompletion: 30, createdDaysAgo: 6 },
|
|
]
|
|
|
|
/* ── KPI computations ── */
|
|
const totalActiveDeals = computed(() => mockDeals.length)
|
|
|
|
const totalPipelinePremium = computed(() =>
|
|
mockDeals.reduce((sum, d) => sum + d.premium, 0),
|
|
)
|
|
|
|
const stageCounts = computed(() => {
|
|
const counts: Record<PipelineStage, number> = {} as any
|
|
for (const s of PIPELINE_STAGES) counts[s.id] = 0
|
|
for (const d of mockDeals) counts[d.currentStage]++
|
|
return counts
|
|
})
|
|
|
|
const bottleneckStage = computed(() => {
|
|
let max = 0
|
|
let stage = PIPELINE_STAGES[0]
|
|
for (const s of PIPELINE_STAGES) {
|
|
if (stageCounts.value[s.id] > max) {
|
|
max = stageCounts.value[s.id]
|
|
stage = s
|
|
}
|
|
}
|
|
return stage.label
|
|
})
|
|
|
|
const maxStageCount = computed(() =>
|
|
Math.max(...PIPELINE_STAGES.map(s => stageCounts.value[s.id]), 1),
|
|
)
|
|
|
|
/* ── Funnel colors ── */
|
|
const stageColors: Record<PipelineStage, string> = {
|
|
customer: '#01696f',
|
|
get_quotes: '#0d8a8f',
|
|
waiting_carriers: '#f59e0b',
|
|
present_quotes: '#3b82f6',
|
|
waiting_client: '#f59e0b',
|
|
solicitud: '#8b5cf6',
|
|
emission: '#10b981',
|
|
}
|
|
|
|
/* ── Bottleneck deals (stuck > 5 days) ── */
|
|
const stuckDeals = computed(() =>
|
|
mockDeals
|
|
.filter(d => d.daysAtStage > 5)
|
|
.sort((a, b) => b.daysAtStage - a.daysAtStage),
|
|
)
|
|
|
|
function severityColor(days: number) {
|
|
if (days > 10) return '#ef4444'
|
|
if (days > 5) return '#f59e0b'
|
|
return '#6b7280'
|
|
}
|
|
|
|
function severityBg(days: number) {
|
|
if (days > 10) return 'rgba(239,68,68,0.08)'
|
|
if (days > 5) return 'rgba(245,158,11,0.08)'
|
|
return 'transparent'
|
|
}
|
|
|
|
/* ── Stage breakdown tab ── */
|
|
const activeStageTab = ref<PipelineStage>('customer')
|
|
|
|
const stageDeals = computed(() =>
|
|
mockDeals.filter(d => d.currentStage === activeStageTab.value),
|
|
)
|
|
|
|
function stageLabelFor(id: PipelineStage) {
|
|
return PIPELINE_STAGES.find(s => s.id === id)?.label ?? id
|
|
}
|
|
|
|
function formatCurrency(v: number) {
|
|
return '$' + v.toLocaleString('en-US')
|
|
}
|
|
|
|
const toast = useToast()
|
|
const nudgeConfirmDeal = ref<MockDeal | null>(null)
|
|
|
|
function onNudge(deal: MockDeal) {
|
|
nudgeConfirmDeal.value = deal
|
|
}
|
|
|
|
function confirmNudge() {
|
|
if (nudgeConfirmDeal.value) {
|
|
toast.add({ title: `Nudge sent to ${nudgeConfirmDeal.value.customerName}`, color: 'success' })
|
|
}
|
|
nudgeConfirmDeal.value = null
|
|
}
|
|
|
|
function cancelNudge() {
|
|
nudgeConfirmDeal.value = null
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="mc-page">
|
|
<!-- Header -->
|
|
<div class="mc-header">
|
|
<h1 class="mc-title">Mission Control</h1>
|
|
<p class="mc-subtitle">Pipeline analytics & agent performance</p>
|
|
</div>
|
|
|
|
<!-- KPI Strip -->
|
|
<div class="mc-kpi-strip">
|
|
<div class="mc-card mc-kpi-card">
|
|
<span class="mc-kpi-label">Total Active Deals</span>
|
|
<span class="mc-kpi-value">{{ totalActiveDeals }}</span>
|
|
</div>
|
|
<div class="mc-card mc-kpi-card">
|
|
<span class="mc-kpi-label">Quotes This Month</span>
|
|
<span class="mc-kpi-value">24</span>
|
|
</div>
|
|
<div class="mc-card mc-kpi-card">
|
|
<span class="mc-kpi-label">Avg Days in Pipeline</span>
|
|
<span class="mc-kpi-value">8.3</span>
|
|
</div>
|
|
<div class="mc-card mc-kpi-card">
|
|
<span class="mc-kpi-label">Conversion Rate</span>
|
|
<span class="mc-kpi-value">62%</span>
|
|
</div>
|
|
<div class="mc-card mc-kpi-card">
|
|
<span class="mc-kpi-label">Pipeline Premium</span>
|
|
<span class="mc-kpi-value">{{ formatCurrency(totalPipelinePremium) }}</span>
|
|
</div>
|
|
<div class="mc-card mc-kpi-card">
|
|
<span class="mc-kpi-label">Bottleneck Stage</span>
|
|
<span class="mc-kpi-value mc-kpi-value--bottleneck">{{ bottleneckStage }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pipeline Funnel -->
|
|
<div class="mc-card mc-section">
|
|
<h2 class="mc-section-title">Pipeline Funnel</h2>
|
|
<div class="mc-funnel">
|
|
<div
|
|
v-for="stage in PIPELINE_STAGES"
|
|
:key="stage.id"
|
|
class="mc-funnel-row"
|
|
>
|
|
<span class="mc-funnel-label">{{ stage.label }}</span>
|
|
<div class="mc-funnel-bar-track">
|
|
<div
|
|
class="mc-funnel-bar"
|
|
:style="{
|
|
width: Math.max((stageCounts[stage.id] / maxStageCount) * 100, 4) + '%',
|
|
background: stageColors[stage.id],
|
|
}"
|
|
/>
|
|
</div>
|
|
<span class="mc-funnel-count">{{ stageCounts[stage.id] }}</span>
|
|
<span class="mc-funnel-pct">
|
|
{{ totalActiveDeals ? Math.round((stageCounts[stage.id] / totalActiveDeals) * 100) : 0 }}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Agent Performance -->
|
|
<div class="mc-card mc-section">
|
|
<h2 class="mc-section-title">Agent Performance</h2>
|
|
<div class="mc-table-wrap">
|
|
<table class="mc-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Agent</th>
|
|
<th>Active Deals</th>
|
|
<th>Quotes Sent</th>
|
|
<th>Conversion</th>
|
|
<th>Avg Response</th>
|
|
<th>Pipeline Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="a in agents" :key="a.name">
|
|
<td class="mc-agent-name">{{ a.name }}</td>
|
|
<td>{{ a.activeDeals }}</td>
|
|
<td>{{ a.quotesSent }}</td>
|
|
<td>
|
|
<span
|
|
class="mc-badge"
|
|
:style="{ background: a.conversionRate >= 70 ? 'rgba(16,185,129,0.1)' : a.conversionRate >= 50 ? 'rgba(245,158,11,0.1)' : 'rgba(239,68,68,0.1)', color: a.conversionRate >= 70 ? '#059669' : a.conversionRate >= 50 ? '#d97706' : '#dc2626' }"
|
|
>
|
|
{{ a.conversionRate }}%
|
|
</span>
|
|
</td>
|
|
<td>{{ a.avgResponseTime }}</td>
|
|
<td>{{ formatCurrency(a.pipelineValue) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bottleneck Analysis -->
|
|
<div class="mc-card mc-section">
|
|
<h2 class="mc-section-title">Bottleneck Analysis</h2>
|
|
<p class="mc-section-desc">Deals stuck at a stage for more than 5 days</p>
|
|
<div v-if="stuckDeals.length === 0" class="mc-empty">No bottlenecks detected.</div>
|
|
<div v-else class="mc-stuck-list">
|
|
<div
|
|
v-for="deal in stuckDeals"
|
|
:key="deal.id"
|
|
class="mc-stuck-row"
|
|
:style="{ background: severityBg(deal.daysAtStage) }"
|
|
>
|
|
<div class="mc-stuck-info">
|
|
<span class="mc-stuck-name">{{ deal.customerName }}</span>
|
|
<span class="mc-stuck-meta">
|
|
<span
|
|
class="mc-stuck-days"
|
|
:style="{ color: severityColor(deal.daysAtStage) }"
|
|
>
|
|
{{ deal.daysAtStage }}d
|
|
</span>
|
|
at {{ stageLabelFor(deal.currentStage) }}
|
|
</span>
|
|
</div>
|
|
<span class="mc-stuck-agent">{{ deal.agent }}</span>
|
|
<button class="mc-btn-nudge" @click="onNudge(deal)">Nudge</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stage Breakdown -->
|
|
<div class="mc-card mc-section">
|
|
<h2 class="mc-section-title">Stage Breakdown</h2>
|
|
<div class="mc-tab-bar">
|
|
<button
|
|
v-for="stage in PIPELINE_STAGES"
|
|
:key="stage.id"
|
|
class="mc-tab"
|
|
:class="{ 'mc-tab--active': activeStageTab === stage.id }"
|
|
@click="activeStageTab = stage.id"
|
|
>
|
|
{{ stage.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="stageDeals.length === 0" class="mc-empty">
|
|
<UIcon name="i-heroicons-inbox" style="width: 20px; height: 20px; color: #c0c0bc;" />
|
|
<p>No deals at this stage</p>
|
|
</div>
|
|
<div v-else class="mc-table-wrap">
|
|
<table class="mc-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Customer</th>
|
|
<th>Product Line</th>
|
|
<th>Days at Stage</th>
|
|
<th>Agent</th>
|
|
<th>Premium</th>
|
|
<th>Form Completion</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="deal in stageDeals" :key="deal.id">
|
|
<td class="mc-agent-name">{{ deal.customerName }}</td>
|
|
<td>{{ deal.productLine }}</td>
|
|
<td>
|
|
<span :style="{ color: deal.daysAtStage > 5 ? severityColor(deal.daysAtStage) : 'inherit', fontWeight: deal.daysAtStage > 5 ? 600 : 400 }">
|
|
{{ deal.daysAtStage }}d
|
|
</span>
|
|
</td>
|
|
<td>{{ deal.agent }}</td>
|
|
<td>{{ formatCurrency(deal.premium) }}</td>
|
|
<td>
|
|
<div class="mc-progress-wrap">
|
|
<div class="mc-progress-track">
|
|
<div
|
|
class="mc-progress-fill"
|
|
:style="{ width: deal.formCompletion + '%' }"
|
|
/>
|
|
</div>
|
|
<span class="mc-progress-label">{{ deal.formCompletion }}%</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nudge confirmation modal -->
|
|
<Teleport to="body">
|
|
<Transition
|
|
enter-active-class="transition duration-150 ease-out"
|
|
enter-from-class="opacity-0"
|
|
enter-to-class="opacity-100"
|
|
leave-active-class="transition duration-100 ease-in"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div v-if="nudgeConfirmDeal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/30" @click.self="cancelNudge">
|
|
<div class="w-full max-w-sm rounded-xl border border-[var(--card-border)] bg-white p-6 shadow-xl">
|
|
<div class="flex items-start gap-3">
|
|
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg" style="background: rgba(1,105,111,0.08); color: #01696f;">
|
|
<UIcon name="i-heroicons-bell-alert" style="width: 18px; height: 18px;" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-[14px] font-semibold text-[var(--text-primary)]">Send nudge?</p>
|
|
<p class="mt-1 text-[13px] text-[var(--text-muted)]">
|
|
This will send a follow-up reminder to <span class="font-semibold text-[var(--text-primary)]">{{ nudgeConfirmDeal.customerName }}</span> about their pending deal.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="mt-5 flex justify-end gap-2">
|
|
<UButton color="neutral" variant="soft" size="sm" @click="cancelNudge">Cancel</UButton>
|
|
<UButton color="primary" size="sm" @click="confirmNudge">Send nudge</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ── Page ── */
|
|
.mc-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
max-width: 1120px;
|
|
}
|
|
|
|
/* ── Header ── */
|
|
.mc-header {
|
|
margin-bottom: 4px;
|
|
}
|
|
.mc-title {
|
|
font-size: 24px;
|
|
font-weight: 650;
|
|
letter-spacing: -0.015em;
|
|
color: var(--text-primary);
|
|
}
|
|
.mc-subtitle {
|
|
font-size: 14px;
|
|
color: #8a8a86;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* ── Card ── */
|
|
.mc-card {
|
|
background: #fff;
|
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
border-radius: 12px;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
|
|
}
|
|
|
|
/* ── KPI strip ── */
|
|
.mc-kpi-strip {
|
|
display: grid;
|
|
grid-template-columns: repeat(6, 1fr);
|
|
gap: 12px;
|
|
}
|
|
.mc-kpi-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 16px 12px;
|
|
gap: 6px;
|
|
}
|
|
.mc-kpi-label {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
text-align: center;
|
|
}
|
|
.mc-kpi-value {
|
|
font-size: 22px;
|
|
font-weight: 650;
|
|
color: var(--text-primary);
|
|
}
|
|
.mc-kpi-value--bottleneck {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #01696f;
|
|
}
|
|
|
|
/* ── Section ── */
|
|
.mc-section {
|
|
padding: 20px 24px;
|
|
}
|
|
.mc-section-title {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
margin-bottom: 14px;
|
|
}
|
|
.mc-section-desc {
|
|
font-size: 13px;
|
|
color: #8a8a86;
|
|
margin-top: -8px;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
/* ── Funnel ── */
|
|
.mc-funnel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.mc-funnel-row {
|
|
display: grid;
|
|
grid-template-columns: 140px 1fr 36px 44px;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.mc-funnel-label {
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
}
|
|
.mc-funnel-bar-track {
|
|
height: 22px;
|
|
background: rgba(0, 0, 0, 0.03);
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
}
|
|
.mc-funnel-bar {
|
|
height: 100%;
|
|
border-radius: 6px;
|
|
transition: width 0.4s ease;
|
|
min-width: 4px;
|
|
}
|
|
.mc-funnel-count {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
text-align: right;
|
|
color: var(--text-primary);
|
|
}
|
|
.mc-funnel-pct {
|
|
font-size: 12px;
|
|
color: #8a8a86;
|
|
text-align: right;
|
|
}
|
|
|
|
/* ── Table ── */
|
|
.mc-table-wrap {
|
|
overflow-x: auto;
|
|
}
|
|
.mc-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13px;
|
|
}
|
|
.mc-table th {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
font-weight: 500;
|
|
text-align: left;
|
|
padding: 0 12px 10px;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
}
|
|
.mc-table td {
|
|
padding: 10px 12px;
|
|
color: var(--text-primary);
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
}
|
|
.mc-table tbody tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
.mc-agent-name {
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* ── Badge ── */
|
|
.mc-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 2px 8px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ── Stuck list ── */
|
|
.mc-stuck-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.mc-stuck-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 10px 14px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0, 0, 0, 0.04);
|
|
}
|
|
.mc-stuck-info {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
.mc-stuck-name {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
.mc-stuck-meta {
|
|
font-size: 12px;
|
|
color: #8a8a86;
|
|
}
|
|
.mc-stuck-days {
|
|
font-weight: 600;
|
|
}
|
|
.mc-stuck-agent {
|
|
font-size: 12px;
|
|
color: #8a8a86;
|
|
min-width: 120px;
|
|
}
|
|
.mc-btn-nudge {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
padding: 4px 14px;
|
|
border-radius: 6px;
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
background: #fff;
|
|
color: #01696f;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: background 0.15s;
|
|
}
|
|
.mc-btn-nudge:hover {
|
|
background: rgba(1, 105, 111, 0.06);
|
|
}
|
|
|
|
/* ── Tab bar ── */
|
|
.mc-tab-bar {
|
|
display: inline-flex;
|
|
background: rgba(0, 0, 0, 0.04);
|
|
padding: 3px;
|
|
border-radius: 10px;
|
|
gap: 2px;
|
|
margin-bottom: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.mc-tab {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
padding: 5px 12px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: transparent;
|
|
color: #6b6b68;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: all 0.15s;
|
|
}
|
|
.mc-tab--active {
|
|
background: #fff;
|
|
color: var(--text-primary);
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
/* ── Progress bar ── */
|
|
.mc-progress-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.mc-progress-track {
|
|
flex: 1;
|
|
height: 4px;
|
|
background: rgba(0, 0, 0, 0.06);
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
min-width: 60px;
|
|
}
|
|
.mc-progress-fill {
|
|
height: 100%;
|
|
background: #01696f;
|
|
border-radius: 2px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
.mc-progress-label {
|
|
font-size: 12px;
|
|
color: #8a8a86;
|
|
min-width: 32px;
|
|
}
|
|
|
|
/* ── Empty state ── */
|
|
.mc-empty {
|
|
font-size: 13px;
|
|
color: #8a8a86;
|
|
padding: 20px 0;
|
|
text-align: center;
|
|
}
|
|
|
|
/* ── Responsive ── */
|
|
@media (max-width: 900px) {
|
|
.mc-kpi-strip {
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
}
|
|
@media (max-width: 600px) {
|
|
.mc-kpi-strip {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
}
|
|
</style>
|