Files
CRM_Montzar/apps/web/src/components/dashboard/HomeAlertsSection.vue
T
Mahdi 26792f9a37 feat: migrate CRM screens to Ant Design Vue and expand home dashboard
Preserve the local Ant UI work across views/components and add the new home dashboard chart panels before merging latest main.
2026-11-03 16:51:51 +03:30

201 lines
6.7 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 } from 'vue';
import { RouterLink } from 'vue-router';
import type { Component } from 'vue';
import {
AlertOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
InfoCircleOutlined,
LeftOutlined,
} from '@ant-design/icons-vue';
import { toFaDigits } from '@/lib/jalali';
import type { HomeDashboardAlertDto } from '@mento/shared';
const props = defineProps<{
alerts: HomeDashboardAlertDto[];
}>();
type Tone = HomeDashboardAlertDto['tone'];
type AlertRow = {
key: string;
tone: Tone;
title: string;
subtitle?: string;
link: string;
};
const TONE_ORDER: Record<Tone, number> = {
urgent: 0,
warning: 1,
info: 2,
success: 3,
};
const rows = computed<AlertRow[]>(() =>
[...(props.alerts ?? [])]
.map((a) => {
const parts = a.text.split(/\s*[—\-–]\s*|\n/);
const title = parts[0]?.trim() || a.text;
const subtitle = parts.slice(1).join(' ').trim() || undefined;
return {
key: a.key,
tone: a.tone,
title,
subtitle,
link: a.link || '/',
};
})
.sort((a, b) => TONE_ORDER[a.tone] - TONE_ORDER[b.tone]),
);
const urgentCount = computed(() => rows.value.filter((r) => r.tone === 'urgent').length);
const warningCount = computed(() => rows.value.filter((r) => r.tone === 'warning').length);
function toneIcon(tone: Tone): Component {
if (tone === 'urgent') return ExclamationCircleOutlined;
if (tone === 'warning') return AlertOutlined;
if (tone === 'success') return CheckCircleOutlined;
return InfoCircleOutlined;
}
function toneLabel(tone: Tone) {
if (tone === 'urgent') return 'فوری';
if (tone === 'warning') return 'هشدار';
if (tone === 'success') return 'موفق';
return 'اطلاع';
}
function actionLabel(tone: Tone) {
if (tone === 'urgent') return 'اقدام';
if (tone === 'warning') return 'بررسی';
return 'مشاهده';
}
function rowClass(tone: Tone) {
if (tone === 'urgent') return 'border-rose-200/80 bg-rose-50/50 hover:bg-rose-50';
if (tone === 'warning') return 'border-amber-200/80 bg-amber-50/50 hover:bg-amber-50';
if (tone === 'success') return 'border-emerald-200/70 bg-emerald-50/40 hover:bg-emerald-50/80';
return 'border-slate-200/80 bg-slate-50/70 hover:bg-slate-100/80';
}
function barClass(tone: Tone) {
if (tone === 'urgent') return 'bg-rose-500';
if (tone === 'warning') return 'bg-amber-500';
if (tone === 'success') return 'bg-emerald-500';
return 'bg-blue-500';
}
function iconWrapClass(tone: Tone) {
if (tone === 'urgent') return 'bg-rose-100 text-rose-600';
if (tone === 'warning') return 'bg-amber-100 text-amber-600';
if (tone === 'success') return 'bg-emerald-100 text-emerald-600';
return 'bg-blue-100 text-blue-600';
}
function badgeClass(tone: Tone) {
if (tone === 'urgent') return 'bg-rose-100 text-rose-700';
if (tone === 'warning') return 'bg-amber-100 text-amber-700';
if (tone === 'success') return 'bg-emerald-100 text-emerald-700';
return 'bg-blue-100 text-blue-700';
}
function actionClass(tone: Tone) {
if (tone === 'urgent') return 'text-rose-600 group-hover:text-rose-700';
if (tone === 'warning') return 'text-amber-600 group-hover:text-amber-700';
if (tone === 'success') return 'text-emerald-600 group-hover:text-emerald-700';
return 'text-blue-600 group-hover:text-blue-700';
}
</script>
<template>
<section
v-if="rows.length"
class="bg-white rounded-2xl border border-slate-200/80 p-5 shadow-sm flex flex-col h-[330px]"
>
<header class="flex items-center justify-between gap-3 pb-3 mb-3 border-b border-slate-100 shrink-0">
<div class="flex items-center gap-2.5 min-w-0">
<span class="relative flex h-2.5 w-2.5 shrink-0" aria-hidden="true">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75" />
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-rose-500" />
</span>
<div class="min-w-0">
<h3 class="m-0 font-bold text-slate-800 text-base leading-6">نیازمند توجه</h3>
<p class="m-0 mt-0.5 text-xs text-slate-400 truncate">
موارد با اولویت بالاتر اول آمده‌اند
</p>
</div>
</div>
<div class="flex items-center gap-1.5 shrink-0">
<span
v-if="urgentCount"
class="text-[11px] font-bold bg-rose-50 text-rose-600 border border-rose-200/70 px-2 py-0.5 rounded-full"
>
{{ toFaDigits(urgentCount) }} فوری
</span>
<span
v-if="warningCount"
class="text-[11px] font-bold bg-amber-50 text-amber-700 border border-amber-200/70 px-2 py-0.5 rounded-full"
>
{{ toFaDigits(warningCount) }} هشدار
</span>
<span class="text-[11px] font-semibold text-slate-500 bg-slate-100 px-2 py-0.5 rounded-full">
{{ toFaDigits(rows.length) }} مورد
</span>
</div>
</header>
<div class="flex-1 overflow-y-auto space-y-2 pe-1 min-h-0">
<RouterLink
v-for="item in rows"
:key="item.key"
:to="item.link"
class="group relative overflow-hidden rounded-xl border px-3 py-2.5 flex items-center gap-3 transition-all"
:class="rowClass(item.tone)"
>
<span
class="absolute inset-y-0 right-0 w-1 rounded-s-full"
:class="barClass(item.tone)"
aria-hidden="true"
/>
<span
class="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 text-sm"
:class="iconWrapClass(item.tone)"
aria-hidden="true"
>
<component :is="toneIcon(item.tone)" />
</span>
<div class="min-w-0 flex-1">
<h4 class="m-0 font-bold text-[13px] text-slate-800 leading-5 truncate">
{{ item.title }}
</h4>
<div class="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1">
<span
class="text-[10px] font-bold px-1.5 py-0.5 rounded shrink-0"
:class="badgeClass(item.tone)"
>
{{ toneLabel(item.tone) }}
</span>
<span v-if="item.subtitle" class="text-[11px] text-slate-500 truncate">
{{ item.subtitle }}
</span>
</div>
</div>
<div
class="shrink-0 flex items-center gap-1 text-[11px] font-semibold"
:class="actionClass(item.tone)"
>
<span class="opacity-0 group-hover:opacity-100 transition-opacity hidden sm:inline">
{{ actionLabel(item.tone) }}
</span>
<LeftOutlined class="text-slate-400 group-hover:-translate-x-0.5 transition-transform" />
</div>
</RouterLink>
</div>
</section>
</template>