Files
CRM_Montzar/apps/web/src/views/ReportsView.vue
T
s.esmaeili 1ae87c97b7 feat(ui): standardize design system, modals, sidebar, and form inputs
- Implement unified design system tokens, 10px radius, and IRANSans typography
- Modernize sidebar with Gemini-style collapsed expander, square 1:1 items, and elevated profile flyout
- Standardize all modal dialogs with 16px padding, fixed header/footer, 16px medium title, and sticky bottom actions
- Refactor DealFormView and CustomFieldsSection with standard UiField, UiInput, and UiSelect components
- Add draggable bottom-left QuickCreate floating action button
2026-09-12 01:33:07 +03:30

1051 lines
27 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import { formatMoney } from '@/lib/money';
import {
dateToJalali,
formatJalaliNumeric,
formatLocalDateInput,
jalaliToDate,
toFaDigits,
} from '@/lib/jalali';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import UiIcon from '@/components/UiIcon.vue';
import {
UiButton,
UiCard,
UiPageHeader,
UiChart,
CHART_PALETTE,
type UiChartOption,
} from '@/components/ui';
import type { OrganizationDto, ReportsDashboardDto } from '@mento/shared';
import { brand } from '@mento/shared';
type PeriodKey = 'today' | 'week' | 'month' | 'quarter' | 'year' | 'custom';
const PERIODS: Array<{ key: PeriodKey; label: string }> = [
{ key: 'today', label: 'امروز' },
{ key: 'week', label: 'هفته' },
{ key: 'month', label: 'ماه' },
{ key: 'quarter', label: 'سه‌ماهه' },
{ key: 'year', label: 'سال' },
];
const loading = ref(false);
const error = ref('');
const data = ref<ReportsDashboardDto | null>(null);
const org = ref<OrganizationDto | null>(null);
const period = ref<PeriodKey>('month');
const from = ref('');
const to = ref('');
const showCustomDates = ref(false);
const currency = computed(
() => data.value?.currency || org.value?.baseCurrency || 'IRR',
);
function money(n: number | null | undefined) {
if (n == null || Number.isNaN(n)) return '—';
return formatMoney(n, currency.value);
}
function num(n: number | null | undefined) {
if (n == null || Number.isNaN(n)) return '—';
return toFaDigits(Math.round(n).toLocaleString('en-US'));
}
function formatKpi(value: number, format: 'money' | 'number') {
return format === 'money' ? money(value) : num(value);
}
function formatChange(p: number | null | undefined) {
if (p == null) return '—';
const sign = p > 0 ? '+' : '';
return `${sign}${toFaDigits(p)}٪`;
}
function rangeLabel() {
if (!from.value || !to.value) return '—';
try {
const a = formatJalaliNumeric(new Date(from.value));
const b = formatJalaliNumeric(new Date(to.value));
return `${a} – ${b}`;
} catch {
return '—';
}
}
function applyPeriod(key: PeriodKey) {
period.value = key;
showCustomDates.value = key === 'custom';
if (key === 'custom') return;
const now = new Date();
const j = dateToJalali(now);
to.value = formatLocalDateInput(now);
if (key === 'today') {
from.value = formatLocalDateInput(now);
} else if (key === 'week') {
const d = new Date(now);
d.setDate(d.getDate() - 6);
from.value = formatLocalDateInput(d);
} else if (key === 'month') {
from.value = formatLocalDateInput(jalaliToDate(j.jy, j.jm, 1));
} else if (key === 'quarter') {
let jm = j.jm - 2;
let jy = j.jy;
if (jm < 1) {
jm += 12;
jy -= 1;
}
from.value = formatLocalDateInput(jalaliToDate(jy, jm, 1));
} else if (key === 'year') {
from.value = formatLocalDateInput(jalaliToDate(j.jy, 1, 1));
}
void load();
}
function openCustom() {
period.value = 'custom';
showCustomDates.value = true;
}
async function load() {
loading.value = true;
error.value = '';
try {
const [dash, orgRes] = await Promise.all([
api.get<ReportsDashboardDto>('/reports/dashboard', {
params: {
from: from.value || undefined,
to: to.value || undefined,
},
}),
api.get<OrganizationDto>('/organizations/current'),
]);
data.value = dash.data;
org.value = orgRes.data;
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
(e.response?.data as { message?: string })?.message ??
'بارگذاری گزارش ناموفق بود.',
)
: 'بارگذاری گزارش ناموفق بود.';
data.value = null;
} finally {
loading.value = false;
}
}
function exportReport() {
if (!data.value) return;
const d = data.value;
const lines: string[] = [
`گزارشات ${brand.nameFa}`,
`بازه,${rangeLabel()}`,
'',
'شاخص,مقدار,تغییر٪',
...d.kpis.map(
(k) =>
`${k.label},${k.value},${k.changePercent ?? ''}`,
),
'',
'روند فروش',
'برچسب,مبلغ',
...d.salesTrend.map((p) => `${p.label},${p.value}`),
'',
'منابع',
'منبع,تعداد,درصد',
...d.sources.map((s) => `${s.label},${s.count},${s.percent}`),
'',
'پرفروش‌ها',
'نام,تعداد,مبلغ',
...d.topProducts.map((p) => `${p.name},${p.quantity},${p.amount}`),
'',
'تیم',
'نام,فعالیت,فروش',
...d.team.map(
(t) => `${t.fullName},${t.activitiesDone},${t.wonAmount}`,
),
];
const blob = new Blob(['\ufeff' + lines.join('\n')], {
type: 'text/csv;charset=utf-8',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${brand.filePrefix}-report-${from.value || 'from'}-${to.value || 'to'}.csv`;
a.click();
URL.revokeObjectURL(url);
}
/* ——— ECharts Options ——— */
const salesTrendOption = computed<UiChartOption>(() => {
const trend = data.value?.salesTrend ?? [];
return {
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const item = Array.isArray(params) ? params[0] : params;
if (!item) return '';
return `${item.name}<br/><strong>${money(item.value)}</strong>`;
},
},
grid: { left: '3%', right: '4%', bottom: '8%', top: '12%', containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: trend.map((p) => p.label),
axisLine: { lineStyle: { color: '#cbd5e1' } },
axisLabel: { color: '#64748b', fontFamily: 'IRANSans' },
},
yAxis: {
type: 'value',
axisLine: { show: false },
splitLine: { lineStyle: { color: '#f1f5f9', type: 'dashed' } },
axisLabel: {
color: '#64748b',
fontFamily: 'IRANSans',
formatter: (v: number) => num(v),
},
},
series: [
{
name: 'فروش',
type: 'line',
smooth: true,
showSymbol: false,
symbolSize: 6,
itemStyle: { color: '#14b8a6' },
lineStyle: { width: 3, color: '#0d9488' },
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(20, 184, 166, 0.35)' },
{ offset: 1, color: 'rgba(20, 184, 166, 0.01)' },
],
},
},
data: trend.map((p) => p.value),
},
],
};
});
const sourcesOption = computed<UiChartOption>(() => {
const sources = data.value?.sources ?? [];
return {
tooltip: {
trigger: 'item',
formatter: (p: any) => `${p.name}: <strong>${num(p.value)}</strong> (${toFaDigits(p.percent)}٪)`,
},
legend: {
bottom: '0%',
left: 'center',
textStyle: { fontFamily: 'IRANSans', fontSize: 11, color: '#475569' },
},
color: CHART_PALETTE,
series: [
{
name: 'منابع مشتریان',
type: 'pie',
radius: ['45%', '70%'],
center: ['50%', '42%'],
avoidLabelOverlap: true,
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { show: false },
data: sources.map((s) => ({ name: s.label, value: s.count })),
},
],
};
});
const monthlyOption = computed<UiChartOption>(() => {
const rows = data.value?.monthlyComparison ?? [];
return {
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const item = Array.isArray(params) ? params[0] : params;
if (!item) return '';
return `${item.name}<br/><strong>${money(item.value)}</strong>`;
},
},
grid: { left: '3%', right: '4%', bottom: '8%', top: '12%', containLabel: true },
xAxis: {
type: 'category',
data: rows.map((r) => r.label),
axisLine: { lineStyle: { color: '#cbd5e1' } },
axisLabel: { color: '#64748b', fontFamily: 'IRANSans' },
},
yAxis: {
type: 'value',
axisLine: { show: false },
splitLine: { lineStyle: { color: '#f1f5f9', type: 'dashed' } },
axisLabel: { color: '#64748b', fontFamily: 'IRANSans', formatter: (v: number) => num(v) },
},
series: [
{
name: 'فروش ماهانه',
type: 'bar',
barWidth: '38%',
itemStyle: { color: '#14b8a6', borderRadius: [6, 6, 0, 0] },
data: rows.map((r) => r.value),
},
],
};
});
const topProductsOption = computed<UiChartOption>(() => {
const prods = (data.value?.topProducts ?? []).slice(0, 5).reverse();
return {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: any) => {
const item = Array.isArray(params) ? params[0] : params;
if (!item) return '';
return `${item.name}: <strong>${num(item.value)} عدد</strong>`;
},
},
grid: { left: '3%', right: '8%', bottom: '4%', top: '4%', containLabel: true },
xAxis: {
type: 'value',
axisLine: { show: false },
splitLine: { lineStyle: { color: '#f1f5f9' } },
axisLabel: { color: '#64748b', fontFamily: 'IRANSans', formatter: (v: number) => num(v) },
},
yAxis: {
type: 'category',
data: prods.map((p) => p.name),
axisLine: { lineStyle: { color: '#cbd5e1' } },
axisLabel: { color: '#334155', fontFamily: 'IRANSans' },
},
series: [
{
name: 'تعداد فروش',
type: 'bar',
barWidth: '45%',
itemStyle: { color: '#0f766e', borderRadius: [0, 6, 6, 0] },
data: prods.map((p) => p.quantity),
},
],
};
});
const categoryOption = computed<UiChartOption>(() => {
const cats = data.value?.salesByCategory ?? [];
return {
tooltip: {
trigger: 'item',
formatter: (p: any) => `${p.name}: <strong>${money(p.value)}</strong> (${toFaDigits(p.percent)}٪)`,
},
legend: {
bottom: '0%',
left: 'center',
textStyle: { fontFamily: 'IRANSans', fontSize: 11, color: '#475569' },
},
color: CHART_PALETTE,
series: [
{
name: 'دسته‌بندی',
type: 'pie',
radius: ['45%', '70%'],
center: ['50%', '42%'],
avoidLabelOverlap: true,
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { show: false },
data: cats.map((c) => ({ name: c.label, value: c.amount ?? 0 })),
},
],
};
});
const statusOption = computed<UiChartOption>(() => {
const st = data.value?.customerStatus ?? [];
return {
tooltip: {
trigger: 'item',
formatter: (p: any) => `${p.name}: <strong>${num(p.value)}</strong> (${toFaDigits(p.percent)}٪)`,
},
legend: {
bottom: '0%',
left: 'center',
textStyle: { fontFamily: 'IRANSans', fontSize: 11, color: '#475569' },
},
color: ['#10b981', '#f59e0b', '#3b82f6', '#94a3b8', '#ef4444'],
series: [
{
name: 'وضعیت مشتریان',
type: 'pie',
radius: ['45%', '70%'],
center: ['50%', '42%'],
avoidLabelOverlap: true,
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { show: false },
data: st.map((s) => ({ name: s.label, value: s.count })),
},
],
};
});
const growthOption = computed<UiChartOption>(() => {
const gr = data.value?.customerGrowth ?? [];
return {
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const item = Array.isArray(params) ? params[0] : params;
if (!item) return '';
return `${item.name}<br/>رشد: <strong>${num(item.value)} مشتری</strong>`;
},
},
grid: { left: '3%', right: '4%', bottom: '8%', top: '12%', containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: gr.map((p) => p.label),
axisLine: { lineStyle: { color: '#cbd5e1' } },
axisLabel: { color: '#64748b', fontFamily: 'IRANSans' },
},
yAxis: {
type: 'value',
axisLine: { show: false },
splitLine: { lineStyle: { color: '#f1f5f9', type: 'dashed' } },
axisLabel: { color: '#64748b', fontFamily: 'IRANSans', formatter: (v: number) => num(v) },
},
series: [
{
name: 'رشد مشتریان',
type: 'line',
smooth: true,
showSymbol: false,
symbolSize: 6,
itemStyle: { color: '#3b82f6' },
lineStyle: { width: 3, color: '#2563eb' },
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(59, 130, 246, 0.3)' },
{ offset: 1, color: 'rgba(59, 130, 246, 0.01)' },
],
},
},
data: gr.map((p) => p.value),
},
],
};
});
const activityMax = computed(() =>
Math.max(
...(data.value?.activities.byType.map((t) => t.count) ?? [0]),
1,
),
);
onMounted(() => {
applyPeriod('month');
});
</script>
<template>
<section class="crm-page">
<UiPageHeader
title="گزارشات و داشبورد"
lead="نمای جامع عملکرد فروش، مشتریان، فعالیت‌ها و تیم"
>
<template #actions>
<UiButton
variant="primary"
:disabled="!data"
@click="exportReport"
>
<UiIcon name="document" :size="16" />
خروجی اکسل / CSV
</UiButton>
</template>
</UiPageHeader>
<!-- Toolbar Filters -->
<div class="crm-toolbar filter-toolbar">
<div class="period-chips">
<button
v-for="p in PERIODS"
:key="p.key"
type="button"
class="chip"
:class="{ on: period === p.key }"
@click="applyPeriod(p.key)"
>
{{ p.label }}
</button>
</div>
<button type="button" class="range-btn" @click="openCustom">
<UiIcon name="calendar" :size="16" />
<span>{{ rangeLabel() }}</span>
</button>
</div>
<!-- Custom date inputs -->
<form v-if="showCustomDates" class="custom-range" @submit.prevent="load">
<JalaliDateInput v-model="from" placeholder="از تاریخ" />
<JalaliDateInput v-model="to" placeholder="تا تاریخ" />
<UiButton variant="primary" type="submit" :disabled="loading">اعمال</UiButton>
</form>
<p v-if="error" class="error">{{ error }}</p>
<div v-else-if="loading" class="loading-wrap">
<div class="spinner"></div>
<span>در حال بارگذاری گزارشات...</span>
</div>
<template v-else-if="data">
<!-- KPI row -->
<div class="kpis">
<article v-for="k in data.kpis" :key="k.key" class="kpi-card">
<div class="kpi-top">
<span class="kpi-ico">
<UiIcon
:name="
k.key === 'new_customers'
? 'users'
: k.key === 'invoice_count'
? 'document'
: k.key === 'sales_amount'
? 'trend'
: 'dollar'
"
:size="18"
/>
</span>
<span class="kpi-label">{{ k.label }}</span>
</div>
<strong class="kpi-val">{{ formatKpi(k.value, k.format) }}</strong>
<div class="kpi-change" :class="{ down: (k.changePercent ?? 0) < 0, flat: k.changePercent == null }">
<span>{{ formatChange(k.changePercent) }}</span>
<small>{{ k.changeLabel }}</small>
</div>
</article>
</div>
<div class="charts-grid">
<!-- Sales trend (wide) -->
<UiCard class="chart-card span-2" title="روند فروش">
<UiChart
v-if="data.salesTrend?.length"
:option="salesTrendOption"
height="260px"
/>
<p v-else class="empty-msg">داده‌ای برای روند فروش نیست.</p>
</UiCard>
<!-- Customer sources -->
<UiCard class="chart-card" title="منابع مشتریان">
<UiChart
v-if="data.sources?.length"
:option="sourcesOption"
height="260px"
/>
<p v-else class="empty-msg">منبعی ثبت نشده است.</p>
</UiCard>
<!-- Monthly comparison -->
<UiCard class="chart-card" title="مقایسه فروش ماه‌های اخیر">
<UiChart
v-if="data.monthlyComparison?.length"
:option="monthlyOption"
height="260px"
/>
<p v-else class="empty-msg">داده‌ای نیست.</p>
</UiCard>
<!-- Top products -->
<UiCard class="chart-card" title="پرفروش‌ترین محصولات">
<UiChart
v-if="data.topProducts?.length"
:option="topProductsOption"
height="260px"
/>
<p v-else class="empty-msg">فروش محصولی در این بازه نیست.</p>
</UiCard>
<!-- Sales by category -->
<UiCard class="chart-card" title="فروش بر اساس دسته‌بندی">
<UiChart
v-if="data.salesByCategory?.length"
:option="categoryOption"
height="260px"
/>
<p v-else class="empty-msg">دسته‌بندی فروشی نیست.</p>
</UiCard>
<!-- Customer status -->
<UiCard class="chart-card" title="وضعیت مشتریان">
<UiChart
v-if="data.customerStatus?.length"
:option="statusOption"
height="260px"
/>
<p v-else class="empty-msg">مشتری ثبت نشده است.</p>
</UiCard>
<!-- Customer growth -->
<UiCard class="chart-card" title="رشد مشتریان">
<UiChart
v-if="data.customerGrowth?.length"
:option="growthOption"
height="260px"
/>
<p v-else class="empty-msg">داده‌ای نیست.</p>
</UiCard>
<!-- Team performance -->
<UiCard class="chart-card" title="عملکرد تیم">
<ul v-if="data.team?.length" class="team-list">
<li v-for="u in data.team.slice(0, 8)" :key="u.userId">
<span class="avatar">{{ u.fullName.slice(0, 1) }}</span>
<div class="team-meta">
<strong>{{ u.fullName }}</strong>
<small>{{ num(u.activitiesDone) }} فعالیت انجام‌شده</small>
</div>
<em class="team-won">{{ money(u.wonAmount) }}</em>
</li>
</ul>
<p v-else class="empty-msg">داده تیمی نیست.</p>
</UiCard>
<!-- Activities stats -->
<UiCard class="chart-card" title="فعالیت‌ها">
<div class="act-stats">
<div class="act-stat-box">
<span>باز</span>
<strong>{{ num(data.activities.open) }}</strong>
</div>
<div class="act-stat-box done">
<span>انجام‌شده</span>
<strong>{{ num(data.activities.done) }}</strong>
</div>
<div class="act-stat-box overdue">
<span>عقب‌افتاده</span>
<strong>{{ num(data.activities.overdue) }}</strong>
</div>
</div>
<ul v-if="data.activities.byType.length" class="act-bars">
<li v-for="t in data.activities.byType" :key="t.type">
<span class="act-bar-label">{{ t.label }}</span>
<div class="hb-track">
<div
class="hb-fill"
:style="{
width: `${Math.max(6, Math.round((t.count / activityMax) * 100))}%`,
}"
/>
</div>
<em>{{ num(t.count) }}</em>
</li>
</ul>
<p v-else class="empty-msg">فعالیتی در بازه نیست.</p>
</UiCard>
<!-- AI Insights -->
<UiCard class="chart-card span-full" title="تحلیل هوشمند">
<template #header>
<div class="insights-head">
<UiIcon name="sparkle" :size="18" class="sparkle-ico" />
<h3 class="card-title">تحلیل هوشمند و پیش‌بینی</h3>
</div>
</template>
<ul v-if="data.insights?.length" class="insight-list">
<li v-for="(line, i) in data.insights" :key="i">
<UiIcon name="check" :size="16" class="check-ico" />
<span>{{ line }}</span>
</li>
</ul>
<p v-else class="empty-msg">با ثبت فروش و مشتری، تحلیل‌های هوشمند به طور خودکار نمایش داده می‌شوند.</p>
<div class="suggest-box">
<UiIcon name="lightbulb" :size="18" />
<span>پیشنهاد: روی دسته‌ها و منابع مشتری با نرخ رشد و حاشیه سود بالاتر تمرکز کنید.</span>
</div>
</UiCard>
</div>
</template>
</section>
</template>
<style scoped>
.filter-toolbar {
display: flex;
align-items: center;
gap: var(--sp-3);
flex-wrap: wrap;
}
.period-chips {
display: inline-flex;
align-items: center;
gap: 2px;
background: var(--surface-subtle);
border: 1px solid var(--line);
padding: 3px;
border-radius: var(--radius);
}
.chip {
border: 0;
background: transparent;
padding: 0 var(--sp-3);
min-height: calc(var(--control-h-md) - 6px);
border-radius: calc(var(--radius) - 2px);
font: inherit;
font-size: var(--fs-xs);
font-weight: var(--fw-semibold);
color: var(--muted);
cursor: pointer;
transition: all var(--transition-fast);
}
.chip.on {
background: var(--surface);
color: var(--accent-deep);
box-shadow: var(--shadow-xs);
font-weight: var(--fw-bold);
}
.range-btn {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
min-height: var(--control-h-md);
padding: 0 var(--sp-3);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
color: var(--ink-secondary);
font: inherit;
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
cursor: pointer;
}
.custom-range {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-3);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
flex-wrap: wrap;
}
.loading-wrap {
display: flex;
align-items: center;
justify-content: center;
gap: var(--sp-3);
padding: var(--sp-10);
color: var(--muted);
font-size: var(--fs-base);
}
/* KPI Cards */
.kpis {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--sp-4);
}
.kpi-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: var(--sp-4);
box-shadow: var(--shadow-xs);
display: flex;
flex-direction: column;
gap: var(--sp-2);
}
.kpi-top {
display: flex;
align-items: center;
gap: var(--sp-2);
}
.kpi-ico {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: var(--radius-sm);
background: var(--accent-soft);
color: var(--accent-deep);
}
.kpi-label {
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
color: var(--muted);
}
.kpi-val {
font-size: var(--fs-xl);
font-weight: var(--fw-extrabold);
color: var(--ink);
line-height: var(--lh-tight);
}
.kpi-change {
display: flex;
align-items: center;
gap: var(--sp-2);
font-size: var(--fs-xs);
font-weight: var(--fw-semibold);
color: var(--success);
}
.kpi-change.down {
color: var(--danger);
}
.kpi-change.flat {
color: var(--muted);
}
.kpi-change small {
color: var(--muted);
font-weight: normal;
}
/* Charts Grid */
.charts-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--sp-4);
}
.chart-card.span-2 {
grid-column: span 2;
}
.chart-card.span-full {
grid-column: 1 / -1;
}
.empty-msg {
text-align: center;
padding: var(--sp-6);
color: var(--muted);
font-size: var(--fs-sm);
}
/* Team List */
.team-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--sp-2);
}
.team-list li {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-2) 0;
border-bottom: 1px solid var(--line-light);
}
.team-list li:last-child {
border-bottom: none;
}
.avatar {
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--accent-soft);
color: var(--accent-deep);
display: grid;
place-items: center;
font-weight: var(--fw-bold);
font-size: var(--fs-xs);
}
.team-meta {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.team-meta strong {
font-size: var(--fs-sm);
color: var(--ink);
}
.team-meta small {
font-size: var(--fs-xs);
color: var(--muted);
}
.team-won {
font-style: normal;
font-weight: var(--fw-bold);
font-size: var(--fs-sm);
color: var(--accent-deep);
}
/* Activity Stats */
.act-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--sp-2);
margin-bottom: var(--sp-4);
}
.act-stat-box {
background: var(--surface-subtle);
border-radius: var(--radius-sm);
padding: var(--sp-2);
text-align: center;
display: flex;
flex-direction: column;
gap: 2px;
}
.act-stat-box span {
font-size: var(--fs-xs);
color: var(--muted);
}
.act-stat-box strong {
font-size: var(--fs-base);
color: var(--ink);
}
.act-stat-box.done strong {
color: var(--success);
}
.act-stat-box.overdue strong {
color: var(--danger);
}
.act-bars {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--sp-2);
}
.act-bars li {
display: flex;
align-items: center;
gap: var(--sp-2);
font-size: var(--fs-xs);
}
.act-bar-label {
flex: 0 0 70px;
color: var(--ink-secondary);
}
.hb-track {
flex: 1;
height: 8px;
background: var(--surface-subtle);
border-radius: var(--radius-full);
overflow: hidden;
}
.hb-fill {
height: 100%;
background: var(--accent);
border-radius: var(--radius-full);
}
/* Insights */
.insights-head {
display: flex;
align-items: center;
gap: var(--sp-2);
}
.sparkle-ico {
color: var(--accent);
}
.card-title {
margin: 0;
font-size: var(--fs-md);
font-weight: var(--fw-bold);
color: var(--ink);
}
.insight-list {
list-style: none;
margin: 0 0 var(--sp-4);
padding: 0;
display: flex;
flex-direction: column;
gap: var(--sp-2);
}
.insight-list li {
display: flex;
align-items: center;
gap: var(--sp-2);
font-size: var(--fs-sm);
color: var(--ink-secondary);
background: var(--surface-subtle);
padding: var(--sp-2) var(--sp-3);
border-radius: var(--radius-sm);
}
.check-ico {
color: var(--success);
flex-shrink: 0;
}
.suggest-box {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-3);
background: var(--accent-subtle);
border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
border-radius: var(--radius);
color: var(--accent-deep);
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
}
@media (max-width: 900px) {
.charts-grid {
grid-template-columns: 1fr;
}
.chart-card.span-2 {
grid-column: auto;
}
}
</style>