-
+
ERP
diff --git a/fe-user/src/pages/UserDashboardPage.tsx b/fe-user/src/pages/UserDashboardPage.tsx
new file mode 100644
index 0000000..d7b5738
--- /dev/null
+++ b/fe-user/src/pages/UserDashboardPage.tsx
@@ -0,0 +1,203 @@
+// "Tổng quan" cho user — overview KPI cá nhân + recent contracts. Khác Inbox
+// (chờ duyệt) — Dashboard cho user nhìn nhanh tất cả HĐ liên quan.
+import { useQuery } from '@tanstack/react-query'
+import { useNavigate } from 'react-router-dom'
+import { FileText, CheckCircle2, AlertTriangle, Pencil, Clock, Inbox, Coins } from 'lucide-react'
+import { PageHeader } from '@/components/PageHeader'
+import { PhaseBadge } from '@/components/PhaseBadge'
+import { SlaTimer } from '@/components/SlaTimer'
+import { EmptyState } from '@/components/EmptyState'
+import { Button } from '@/components/ui/Button'
+import { useAuth } from '@/contexts/AuthContext'
+import { api } from '@/lib/api'
+import type { Paged } from '@/types/master'
+import type { ContractListItem } from '@/types/contracts'
+import { ContractTypeLabel } from '@/types/forms'
+
+type MyDashboard = {
+ draftsInProgress: number
+ pendingMyApproval: number
+ dueSoon: number
+ overdue: number
+ draftsTotalValue: number
+}
+
+const fmtMoney = (v: number) => {
+ if (v >= 1_000_000_000) return (v / 1_000_000_000).toFixed(1) + ' tỷ'
+ if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + ' tr'
+ return v.toLocaleString('vi-VN')
+}
+
+function StatCard({
+ icon: Icon,
+ label,
+ value,
+ hint,
+ tone = 'default',
+ onClick,
+}: {
+ icon: React.ComponentType<{ className?: string }>
+ label: string
+ value: React.ReactNode
+ hint?: string
+ tone?: 'default' | 'warn' | 'good' | 'danger'
+ onClick?: () => void
+}) {
+ const toneClass =
+ tone === 'warn' ? 'text-amber-600 bg-amber-50' :
+ tone === 'good' ? 'text-emerald-600 bg-emerald-50' :
+ tone === 'danger' ? 'text-red-600 bg-red-50' :
+ 'text-brand-600 bg-brand-50'
+ return (
+
+ )
+}
+
+export function UserDashboardPage() {
+ const navigate = useNavigate()
+ const { user } = useAuth()
+
+ const stats = useQuery({
+ queryKey: ['my-dashboard'],
+ queryFn: async () => (await api.get
('/reports/my-dashboard')).data,
+ })
+
+ const recent = useQuery({
+ queryKey: ['my-contracts-recent'],
+ queryFn: async () =>
+ (await api.get>('/contracts', { params: { page: 1, pageSize: 5 } })).data,
+ })
+
+ const s = stats.data
+
+ return (
+
+
+
+
+ Của tôi
+
+ navigate('/my-contracts')}
+ />
+ navigate('/inbox')}
+ />
+ navigate('/inbox')}
+ />
+ navigate('/inbox')}
+ />
+ navigate('/my-contracts')}
+ />
+
+
+
+
+
+
+
+ HĐ gần đây
+
+
+
+
+ {recent.isLoading && (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ )}
+
+ {!recent.isLoading && (recent.data?.items?.length ?? 0) === 0 && (
+
+ navigate('/contracts/new')}>
+
+ Tạo HĐ mới
+
+ }
+ />
+
+ )}
+
+
+ {recent.data?.items?.map(c => (
+ -
+
+
+ ))}
+
+
+
+ )
+}