feat(pm): polish UI/UX, align drawer with warehouse pattern, and connect real API data

- Refactor TodayPage into dedicated single-project site supervisor dashboard
- Mirror warehouse panel menu accordion pattern for project switching
- Separate high-priority QC emergency banner from checklist filter chips
- Add smooth scrolling to emergency section when tapping critical KPI card
- Connect all dashboard data (projects, KPIs, checklists, upcoming tasks) to real API endpoints
- Remove all hardcoded mock arrays and fallbacks
- Add clean loading skeleton with spinner and empty states
- Dynamically format user display name and role-based titles in header
This commit is contained in:
Behrad
2026-10-22 14:32:24 +03:30
parent 4f7e58f7b7
commit 50c5443240
5 changed files with 872 additions and 289 deletions
+74 -64
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Folder, Plus, RefreshCw, Users, MapPin, Calendar } from "lucide-react";
import { Folder, Plus, RefreshCw, ChevronLeft } from "lucide-react";
import { createProject, getProjects } from "../lib/api";
import type { CreateProjectPayload, Project } from "../lib/api";
import {
@@ -13,7 +13,6 @@ import {
Modal,
PageHeader,
PanelsMenu,
ProgressBar,
btnPrimary,
btnSecondary,
inputCls,
@@ -34,73 +33,84 @@ function ProjectCard({ project }: { project: Project }) {
};
const currentStatus = project.status ? statusLabels[project.status.toUpperCase()] : null;
const progressVal =
project.progress !== undefined
? Math.round(project.progress)
: project.stats.total > 0
? Math.round(
Math.max(
0,
((project.stats.total - project.stats.overdue - project.stats.failed_qc) /
project.stats.total) *
100
)
)
: 0;
return (
<Link to={`/projects/${targetId}`} className="block">
<Card className="p-4 active:bg-slate-100/60 transition-all hover:border-slate-300">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="min-w-0 truncate text-sm font-bold text-slate-800">{project.name}</p>
{currentStatus && <Chip tone={currentStatus.tone}>{currentStatus.label}</Chip>}
</div>
<p dir="ltr" className="mt-0.5 text-right font-mono text-[11px] text-slate-400">
{project.identifier}
</p>
</div>
</div>
{project.location && (
<div className="mt-2 flex flex-wrap items-center gap-3 text-xs text-slate-500">
<span className="inline-flex items-center gap-1">
<MapPin size={13} className="text-slate-400" />
{project.location}
</span>
</div>
)}
{(project.planned_start_jalali || project.planned_end_jalali || project.actual_start_jalali) && (
<div className="mt-2 flex flex-wrap items-center gap-2 text-[11px] text-slate-500">
{(project.planned_start_jalali || project.planned_end_jalali) && (
<span className="inline-flex items-center gap-1">
<Calendar size={12} className="text-slate-400" />
<span>برنامه:</span>
<span className="font-medium text-slate-700">
{project.planned_start_jalali || "—"} تا {project.planned_end_jalali || "—"}
<Link to={`/projects/${targetId}`} className="group block">
<Card className="p-3 shadow-xs transition-all duration-150 hover:border-slate-300 hover:shadow-sm active:bg-slate-50/90">
<div className="flex items-center justify-between gap-3">
{/* بلوک محتوایی ۲ سطره در سمت راست */}
<div className="min-w-0 flex-1 space-y-2">
{/* سطر ۱: نام پروژه و بج پیل وضعیت */}
<div className="flex items-center gap-2 min-w-0">
<p className="min-w-0 truncate text-xs font-bold text-slate-800 group-hover:text-sky-700 transition-colors">
{project.name}
</p>
{project.stats.failed_qc > 0 ? (
<span className="inline-flex shrink-0 items-center rounded-full bg-red-50 border border-red-200/60 px-2 py-0.5 text-[10px] font-bold text-red-600">
{project.stats.failed_qc} مردود
</span>
</span>
)}
{project.actual_start_jalali && (
<span className="inline-flex items-center gap-1 rounded bg-emerald-50 px-1.5 py-0.5 text-emerald-700">
<span>واقعی:</span>
<span>{project.actual_start_jalali}</span>
{project.actual_end_jalali && <span> تا {project.actual_end_jalali}</span>}
</span>
)}
</div>
)}
{project.progress !== undefined && (
<div className="mt-3">
<div className="flex items-center justify-between text-[11px] text-slate-500 mb-1">
<span>پیشرفت پروژه</span>
<span className="font-bold tabular-nums text-slate-700">{Math.round(project.progress)}٪</span>
) : currentStatus ? (
<Chip tone={currentStatus.tone} className="text-[10px] py-0 px-2 shrink-0">
{currentStatus.label}
</Chip>
) : (
<span className="inline-flex shrink-0 items-center rounded-full bg-emerald-50 border border-emerald-200/60 px-2 py-0.5 text-[10px] font-medium text-emerald-700">
در حال اجرا
</span>
)}
</div>
<ProgressBar value={project.progress} />
</div>
)}
<div className="mt-3 flex flex-wrap items-center gap-1.5">
<Chip tone="sky">کل: {project.stats.total}</Chip>
<Chip tone="amber">انتظار QC: {project.stats.awaiting_qc}</Chip>
{project.stats.failed_qc > 0 && <Chip tone="red">مردود: {project.stats.failed_qc}</Chip>}
{project.stats.overdue > 0 && <Chip tone="red">عقب‌افتاده: {project.stats.overdue}</Chip>}
{project.member_count !== undefined && project.member_count > 0 && (
<span className="ms-auto inline-flex items-center gap-1 text-[11px] text-slate-400">
<Users size={12} />
{project.member_count} عضو
</span>
)}
{/* سطر ۲: پیل تعداد فعالیت در راست — نوار پیشرفت باریک با درصد در چپ */}
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-1.5 min-w-0">
<span className="inline-flex shrink-0 items-center rounded-md bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-600">
{project.stats.total} فعالیت
</span>
{project.stats.awaiting_qc > 0 && (
<span className="inline-flex shrink-0 items-center rounded-md bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700">
{project.stats.awaiting_qc} منتظر QC
</span>
)}
{project.location && (
<span className="truncate text-[10px] text-slate-400 hidden sm:inline">
• {project.location}
</span>
)}
</div>
<div className="flex items-center gap-2 w-28 sm:w-32 shrink-0 justify-end">
<div dir="rtl" className="h-1.5 flex-1 overflow-hidden rounded-full bg-slate-100">
<div
className={`h-full rounded-full transition-all duration-300 ${
progressVal >= 100 ? "bg-emerald-500" : "bg-sky-500"
}`}
style={{ width: `${progressVal}%` }}
/>
</div>
<span className="shrink-0 text-left text-[10px] font-bold tabular-nums text-slate-500 w-7">
{progressVal}٪
</span>
</div>
</div>
</div>
{/* فلش تعاملی در منتهی‌الیه چپ با تراز عمودی کامل روی کل ارتفاع کارت */}
<div className="flex h-7 w-7 shrink-0 items-center justify-center self-center text-slate-400 transition-all group-hover:text-sky-600 group-hover:bg-sky-50 group-active:-translate-x-0.5 rounded-lg">
<ChevronLeft size={18} />
</div>
</div>
</Card>
</Link>