From 50c54432408700bbda2619e53c28d7ba3fbe09e2 Mon Sep 17 00:00:00 2001 From: Behrad Date: Thu, 22 Oct 2026 14:32:24 +0330 Subject: [PATCH] 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 --- pm/frontend/src/components/ui.tsx | 197 ++++++- pm/frontend/src/lib/api.ts | 5 + pm/frontend/src/lib/roles.ts | 54 +- pm/frontend/src/pages/ProjectsPage.tsx | 138 ++--- pm/frontend/src/pages/TodayPage.tsx | 767 ++++++++++++++++++------- 5 files changed, 872 insertions(+), 289 deletions(-) diff --git a/pm/frontend/src/components/ui.tsx b/pm/frontend/src/components/ui.tsx index 508a4b3..620cd64 100644 --- a/pm/frontend/src/components/ui.tsx +++ b/pm/frontend/src/components/ui.tsx @@ -13,7 +13,7 @@ import type { LucideIcon } from "lucide-react"; import { toast } from "sonner"; import { ApiError } from "../lib/api"; import { useAuthStore } from "../store/auth"; -import { visibleTabs } from "../lib/roles"; +import { visibleTabs, getRoleTitle } from "../lib/roles"; export type Tone = "red" | "amber" | "green" | "sky" | "slate"; @@ -69,28 +69,27 @@ export function PageHeader({ }) { const user = useAuthStore((s) => s.user); const fullName = user?.display_name || user?.username || "کاربر"; - const username = user?.username || "-"; - const role = - user?.role_label || - ((user?.role as string) === "project_supervisor" || (user?.role as string) === "supervisor" - ? "سرپرست پروژه" - : (user?.role as string) === "admin" - ? "مدیر ارشد" - : "مدیر پروژه"); - const userSub = user ? `${fullName} | نام کاربری: ${username} | نقش: ${role}` : null; + const role = getRoleTitle(user?.role, user?.role_label); + const userSub = user ? ( + + {fullName} + • + {role} + + ) : null; const displaySub = sub !== undefined ? sub : userSub; return ( -
+
{menu &&
{menu}
} {onBack && ( )}
@@ -114,13 +113,26 @@ const ERP_URL = type PanelMenuItem = | { kind: "divider" } | { kind: "link"; label: string; href: string } - | { kind: "current"; label: string } + | { + kind: "current"; + label: string; + children?: { label: string; active: boolean; onClick: () => void }[]; + } | { kind: "button"; label: string; onClick: () => void }; -export function PanelsMenu() { +export function PanelsMenu({ + projects, + activeProjectId, + onSelectProject, +}: { + projects?: { id?: string | number; op_project_id?: number; name: string }[]; + activeProjectId?: string | number; + onSelectProject?: (id: string) => void; +} = {}) { const navigate = useNavigate(); const clear = useAuthStore((s) => s.clear); const [open, setOpen] = useState(false); + const [expanded, setExpanded] = useState(true); const ref = useRef(null); useEffect(() => { @@ -194,7 +206,23 @@ export function PanelsMenu() { } if (hasPm) { - items.push({ kind: "current", label: "مدیریت پروژه" }); + const projectChildren = + projects && projects.length > 0 + ? projects.map((p) => { + const pId = String(p.id ?? p.op_project_id ?? ""); + return { + label: p.name, + active: String(activeProjectId) === pId, + onClick: () => onSelectProject?.(pId), + }; + }) + : undefined; + + items.push({ + kind: "current", + label: "مدیریت پروژه", + children: projectChildren, + }); } if (canManageOrg) { @@ -211,7 +239,7 @@ export function PanelsMenu() { onClick={() => setOpen((o) => !o)} aria-label="منو" title="منو" - className="flex h-11 w-11 items-center justify-center rounded-full text-slate-600 active:bg-slate-100" + className="flex h-10 w-10 items-center justify-center rounded-xl text-slate-600 hover:bg-slate-100 active:scale-95 transition-all" > {open ? : } @@ -240,12 +268,51 @@ export function PanelsMenu() { {it.label} ) : it.kind === "current" ? ( -
- {it.label} - ✓ فعلی +
+
+
+ {it.label} + ✓ فعلی +
+ {it.children && it.children.length > 0 && ( + + )} +
+ {it.children && it.children.length > 0 && expanded && ( +
+ {it.children.map((c, j) => ( + + ))} +
+ )}
) : ( + ); + })} +
+ ); +} + // ---------------------------------------- // کارت و چیپ // ---------------------------------------- diff --git a/pm/frontend/src/lib/api.ts b/pm/frontend/src/lib/api.ts index ac4ca60..27cc8bc 100644 --- a/pm/frontend/src/lib/api.ts +++ b/pm/frontend/src/lib/api.ts @@ -209,8 +209,10 @@ export type DefectSeverity = "low" | "medium" | "high"; export interface DashboardTask { id?: string; + project_id?: string; op_wp_id?: number | null; subject: string; + description?: string; due_date: string | null; percentage_done: number; project_name: string; @@ -248,6 +250,9 @@ export interface DashboardToday { export interface ProjectStats { total: number; active: number; + completed?: number; + in_progress?: number; + blocked?: number; awaiting_qc: number; failed_qc: number; overdue: number; diff --git a/pm/frontend/src/lib/roles.ts b/pm/frontend/src/lib/roles.ts index caaa07b..7e19c65 100644 --- a/pm/frontend/src/lib/roles.ts +++ b/pm/frontend/src/lib/roles.ts @@ -34,9 +34,61 @@ export type Permission = (typeof Perm)[keyof typeof Perm]; export const roleLabels: Record = { project_manager: "مدیر پروژه", - project_supervisor: "سرپرست پروژه", + project_supervisor: "سرپرست کارگاه", }; +/** + * نگاشت تمیز کدهای نقش به عناوین فارسی استاندارد سیستم: + * - site_supervisor / project_supervisor / supervisor -> "سرپرست کارگاه" + * - project_manager / pm -> "مدیر پروژه" + * - admin / ceo -> "مدیر ارشد" + */ +export function getRoleTitle(role?: string | null, customLabel?: string | null): string { + const normalized = (role ?? "").toLowerCase().trim(); + switch (normalized) { + case "site_supervisor": + case "project_supervisor": + case "supervisor": + return "سرپرست کارگاه"; + case "project_manager": + case "pm": + return "مدیر پروژه"; + case "admin": + case "ceo": + case "administrator": + return "مدیر ارشد"; + case "procurement_manager": + return "مدیر تدارکات"; + } + + if (customLabel && customLabel.trim()) { + if (customLabel.trim() === "سرپرست پروژه") { + return "سرپرست کارگاه"; + } + return customLabel.trim(); + } + + if (!role) return "کاربر سیستم"; + return roleLabels[normalized as Role] || "کاربر سیستم"; +} + +/** + * تعیین عنوان پویای بالای هدر صفحه بر اساس نقش کاربر: + * - سرپرست کارگاه -> "میز کار سرپرست کارگاه" + * - مدیر پروژه / سایر -> "مدیریت پروژه" + */ +export function getDashboardHeaderTitle(role?: string | null): string { + const normalized = (role ?? "").toLowerCase().trim(); + if ( + normalized === "site_supervisor" || + normalized === "project_supervisor" || + normalized === "supervisor" + ) { + return "میز کار سرپرست کارگاه"; + } + return "مدیریت پروژه"; +} + export function hasPermission(user: User | null | undefined, permission: Permission | string): boolean { return user?.permissions?.includes(permission) ?? false; } diff --git a/pm/frontend/src/pages/ProjectsPage.tsx b/pm/frontend/src/pages/ProjectsPage.tsx index dd91793..cde1158 100644 --- a/pm/frontend/src/pages/ProjectsPage.tsx +++ b/pm/frontend/src/pages/ProjectsPage.tsx @@ -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 ( - - -
-
-
-

{project.name}

- {currentStatus && {currentStatus.label}} -
-

- {project.identifier} -

-
-
- - {project.location && ( -
- - - {project.location} - -
- )} - - {(project.planned_start_jalali || project.planned_end_jalali || project.actual_start_jalali) && ( -
- {(project.planned_start_jalali || project.planned_end_jalali) && ( - - - برنامه: - - {project.planned_start_jalali || "—"} تا {project.planned_end_jalali || "—"} + + +
+ {/* بلوک محتوایی ۲ سطره در سمت راست */} +
+ {/* سطر ۱: نام پروژه و بج پیل وضعیت */} +
+

+ {project.name} +

+ {project.stats.failed_qc > 0 ? ( + + {project.stats.failed_qc} مردود - - )} - {project.actual_start_jalali && ( - - واقعی: - {project.actual_start_jalali} - {project.actual_end_jalali && تا {project.actual_end_jalali}} - - )} -
- )} - - {project.progress !== undefined && ( -
-
- پیشرفت پروژه - {Math.round(project.progress)}٪ + ) : currentStatus ? ( + + {currentStatus.label} + + ) : ( + + در حال اجرا + + )}
- -
- )} -
- کل: {project.stats.total} - انتظار QC: {project.stats.awaiting_qc} - {project.stats.failed_qc > 0 && مردود: {project.stats.failed_qc}} - {project.stats.overdue > 0 && عقب‌افتاده: {project.stats.overdue}} - {project.member_count !== undefined && project.member_count > 0 && ( - - - {project.member_count} عضو - - )} + {/* سطر ۲: پیل تعداد فعالیت در راست — نوار پیشرفت باریک با درصد در چپ */} +
+
+ + {project.stats.total} فعالیت + + {project.stats.awaiting_qc > 0 && ( + + {project.stats.awaiting_qc} منتظر QC + + )} + {project.location && ( + + • {project.location} + + )} +
+ +
+
+
= 100 ? "bg-emerald-500" : "bg-sky-500" + }`} + style={{ width: `${progressVal}%` }} + /> +
+ + {progressVal}٪ + +
+
+
+ + {/* فلش تعاملی در منتهی‌الیه چپ با تراز عمودی کامل روی کل ارتفاع کارت */} +
+ +
diff --git a/pm/frontend/src/pages/TodayPage.tsx b/pm/frontend/src/pages/TodayPage.tsx index a225441..e4898a2 100644 --- a/pm/frontend/src/pages/TodayPage.tsx +++ b/pm/frontend/src/pages/TodayPage.tsx @@ -1,103 +1,244 @@ -import { useState } from "react"; +import { useState, useMemo, useRef, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { CheckCircle2, ChevronDown, ChevronUp, Folder, RefreshCw } from "lucide-react"; -import { dashboardTaskKey, devLogin, getDashboardToday, getDevRoles } from "../lib/api"; +import { + AlertCircle, + AlertTriangle, + Building2, + Calendar, + CheckCircle2, + ChevronDown, + ChevronLeft, + ChevronUp, + ClipboardCheck, + RefreshCw, + type LucideIcon, +} from "lucide-react"; +import { dashboardTaskKey, devLogin, getDashboardToday, getDevRoles, getProjects } from "../lib/api"; import type { DashboardTask, Project } from "../lib/api"; -import { Card, CardSkeleton, Chip, EmptyState, ErrorState, PageHeader, PanelsMenu, ProgressBar } from "../components/ui"; -import { formatDate, isOverdue } from "../lib/format"; -import { roleLabels, scopeLabel } from "../lib/roles"; +import { + Card, + CardSkeleton, + Chip, + EmptyState, + ErrorState, + PageHeader, + PanelsMenu, + type SegmentedOption, +} from "../components/ui"; +import { formatDate } from "../lib/format"; +import { getDashboardHeaderTitle, getRoleTitle } from "../lib/roles"; import { useAuthStore } from "../store/auth"; -type SectionKey = "overdue" | "due_today" | "awaiting_qc" | "failed_qc" | "blocked" | "upcoming"; +type FieldTab = "all" | "pending_approval" | "in_progress"; -interface SectionMeta { - key: SectionKey; - title: string; - tone: "red" | "amber" | "sky" | "slate"; -} - -/** - * بخش‌های داشبورد به تفکیک نقش: - * مدیر پروژه نگاه مدیریتی دارد (تأخیرها و QC همه پروژه‌ها) و سرپرست پروژه نگاه - * عملیاتی (کار امروز و کاری که باید همین حالا اجرا/بازرسی شود). - */ -const managerSections: SectionMeta[] = [ - { key: "overdue", title: "عقب‌افتاده", tone: "red" }, - { key: "due_today", title: "کارهای امروز", tone: "sky" }, - { key: "awaiting_qc", title: "منتظر QC", tone: "amber" }, - { key: "failed_qc", title: "QC مردود", tone: "red" }, - { key: "blocked", title: "مسدود شده", tone: "slate" }, - { key: "upcoming", title: "هفته آینده", tone: "sky" }, -]; - -const supervisorSections: SectionMeta[] = [ - { key: "due_today", title: "کارهای امروز", tone: "sky" }, - { key: "overdue", title: "عقب‌افتاده", tone: "red" }, - { key: "awaiting_qc", title: "منتظر اجرای بازرسی", tone: "amber" }, - { key: "failed_qc", title: "بازرسی مردود", tone: "red" }, - { key: "blocked", title: "مسدود شده (پیش‌نیاز ناتمام)", tone: "slate" }, - { key: "upcoming", title: "هفته آینده", tone: "sky" }, -]; - -function TaskRow({ task, showBlockedBy = false }: { task: DashboardTask; showBlockedBy?: boolean }) { - const overdue = isOverdue(task.due_date); - // شناسهٔ پایدار: UUID تسک بومی، وگرنه op_wp_id میراث +/** کارت اکشن فوری برای موارد QC مردود (طرح اضطراری با دکمه تمام‌عرض) */ +function QcFailedEmergencyCard({ + task, + projectName, +}: { + task: DashboardTask; + projectName: string; +}) { const taskKey = dashboardTaskKey(task); return ( - +
-

{task.subject}

- - {formatDate(task.due_date)} +

+ {task.subject} +

+ + عدم انطباق بحرانی
-
- - {task.percentage_done}٪ -
-
- {task.project_name} - {showBlockedBy && task.blocked_by && ( - مسدود توسط: {task.blocked_by} - )} -
- - ); -} -function SummaryBox({ tone, label, value }: { tone: "red" | "amber" | "green"; label: string; value: number }) { - const cls = { - red: "border-red-100 bg-red-50 text-red-600", - amber: "border-amber-100 bg-amber-50 text-amber-600", - green: "border-emerald-100 bg-emerald-50 text-emerald-600", - }[tone]; - return ( -
-

{value}

-

{label}

+
+
+ پروژه فعال + + {projectName} + +
+
+ تاریخ بازرسی + + {formatDate(task.due_date)} + +
+
+ پیشرفت فعلی + + {task.percentage_done}٪ + +
+
+ +
+ + مشاهده عدم انطباق و ثبت اقدام اصلاحی + + +
); } -/** کاشی مدیریتی یک پروژه — فقط در داشبورد مدیر پروژه */ -function ProjectTile({ project }: { project: Project }) { +/** ردیف فشرده و مدرن چک‌لیست بازرسی روزانه (ارتفاع ۵۶ تا ۶۴ پیکسل) */ +function TodayChecklistRow({ + task, + projectName, + phase, +}: { + task: DashboardTask; + projectName: string; + phase?: string; +}) { + const taskKey = dashboardTaskKey(task); + + const statusMeta = + task.qc_status === "pre_qc_pending" || task.qc_status === "exec_qc_pending" + ? { label: "در انتظار بازرسی", bg: "bg-amber-50 text-amber-700 border-amber-200/60" } + : task.qc_status === "ready" + ? { label: "آماده اجرا / تایید", bg: "bg-sky-50 text-sky-700 border-sky-200/60" } + : task.qc_status === "in_progress" + ? { label: "در حال بررسی", bg: "bg-slate-100 text-slate-700 border-slate-200" } + : task.qc_status === "final_passed" + ? { label: "تأیید نهایی", bg: "bg-emerald-50 text-emerald-700 border-emerald-200/60" } + : { label: "آماده اقدام", bg: "bg-sky-50 text-sky-700 border-sky-200/60" }; + return ( - -
-

{project.name}

-
- {project.stats.total} فعالیت - {project.stats.overdue > 0 && {project.stats.overdue} عقب‌افتاده} - {project.stats.awaiting_qc > 0 && {project.stats.awaiting_qc} منتظر QC} - {project.stats.failed_qc > 0 && {project.stats.failed_qc} مردود} +
+ {/* سمت راست: آیکون وضعیت + عنوان چک‌لیست (با شکست تا ۲ خط بدون truncate سخت) و مرحله */} +
+
+ +
+
+

+ {task.subject} +

+
+ {projectName} + {phase && ( + <> + • + {phase} + + )} +
+
+
+ + {/* سمت چپ: پیل فشرده وضعیت + دکمه آیکونی کوچک برای بازکردن بازرسی (تراز عمودی مرکز) */} +
+ + {statusMeta.label} + + + + +
+
+ ); +} + +/** کارت پیش‌نمایش وظایف و نقاط عطف هفته آینده */ +function UpcomingTaskCard({ + task, + projectName, +}: { + task: DashboardTask; + projectName: string; +}) { + const taskKey = dashboardTaskKey(task); + return ( + +
+
+

+ {task.subject} +

+ +
+ + {projectName} + + {task.due_date && ( + + + موعد: {formatDate(task.due_date)} + + )} + {task.percentage_done > 0 && ( + + {task.percentage_done}٪ + + )} +
+
+ +
+
); } -/** انتخابگر نقش — فقط در فاز توسعه (سرور آن را با PM_DEV_ROLE_SWITCH کنترل می‌کند) */ +/** کارت شاخص آماری با عناوین کوتاه و بدون بریدگی متن */ +function SummaryBox({ + tone, + label, + value, + icon: Icon, + onClick, +}: { + tone: "red" | "amber" | "green"; + label: string; + value: number; + icon?: LucideIcon; + onClick?: () => void; +}) { + const cls = { + red: "border-red-200/80 bg-red-50/60 text-red-700 hover:border-red-300", + amber: "border-amber-200/80 bg-amber-50/60 text-amber-700 hover:border-amber-300", + green: "border-emerald-200/80 bg-emerald-50/60 text-emerald-700 hover:border-emerald-300", + }[tone]; + + const iconCls = { + red: "text-red-500", + amber: "text-amber-500", + green: "text-emerald-500", + }[tone]; + + return ( + + ); +} + + +/** انتخابگر نقش در حالت توسعه */ function DevRoleSwitcher() { const user = useAuthStore((s) => s.user); const setAuth = useAuthStore((s) => s.setAuth); @@ -140,7 +281,7 @@ function DevRoleSwitcher() { > {roles.map((r) => ( ))} @@ -150,28 +291,170 @@ function DevRoleSwitcher() { export default function TodayPage() { const queryClient = useQueryClient(); const user = useAuthStore((s) => s.user); - const [showUpcoming, setShowUpcoming] = useState(false); + + // تب فعال عملیاتی چک‌لیست‌های روزانه: همه اقدامات | در انتظار تایید / بازرسی | در حال بررسی + const [activeTab, setActiveTab] = useState("all"); + const [showUpcoming, setShowUpcoming] = useState(null); + const emergencyRef = useRef(null); + + const handleCriticalClick = () => { + if (emergencyRef.current) { + emergencyRef.current.scrollIntoView({ behavior: "smooth", block: "start" }); + } + }; + + // واکشی داده‌های داشبورد امروز و لیست پروژه‌ها از اندپوینت‌های واقعی const todayQuery = useQuery({ queryKey: ["today"], queryFn: getDashboardToday }); + const projectsQuery = useQuery({ queryKey: ["projects"], queryFn: getProjects }); const data = todayQuery.data; const refresh = () => { void queryClient.invalidateQueries({ queryKey: ["today"] }); + void queryClient.invalidateQueries({ queryKey: ["projects"] }); }; - const isManager = user?.role === "project_manager"; - const role = data?.role ?? user?.role ?? "project_supervisor"; - const sections = isManager ? managerSections : supervisorSections; - const projects = data?.projects ?? []; + // لیست کلی پروژه‌ها مستقیماً از داده‌های API + const allProjects = useMemo(() => { + return projectsQuery.data ?? data?.projects ?? []; + }, [projectsQuery.data, data?.projects]); - const totalItems = data - ? Object.values(data.sections).reduce((sum, list) => sum + list.length, 0) - : 0; + // مدیریت شناسه پروژه فعال با ذخیره‌سازی محلی + const [activeProjectId, setActiveProjectId] = useState(() => { + return localStorage.getItem("pm_active_project_id") || ""; + }); + + const activeProject = useMemo(() => { + if (allProjects.length === 0) return null; + const found = allProjects.find( + (p) => String(p.id || (p as Project).op_project_id) === String(activeProjectId) + ); + return found || allProjects[0]; + }, [allProjects, activeProjectId]); + + // همگام‌سازی خودکار شناسه در صورت نامعتبر بودن مقدار محلی + useEffect(() => { + if (activeProject) { + const currentId = String(activeProject.id || (activeProject as Project).op_project_id); + if (currentId !== activeProjectId) { + setActiveProjectId(currentId); + localStorage.setItem("pm_active_project_id", currentId); + } + } + }, [activeProject, activeProjectId]); + + const currentPhase = + activeProject?.description?.trim() || + (activeProject?.status === "ACTIVE" ? "در حال ساخت / اجرا" : activeProject?.status) || + "در حال اجرا"; + + const handleSelectProject = (id: string) => { + setActiveProjectId(id); + localStorage.setItem("pm_active_project_id", id); + try { + const raw = localStorage.getItem("procurement-auth-v1"); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed?.state?.user) { + parsed.state.user.project_id = id; + localStorage.setItem("procurement-auth-v1", JSON.stringify(parsed)); + } + } + } catch {} + }; + + // وظایف و چک‌لیست‌های عملیاتی کارگاه از اندپوینت واقعی dashboard/today + const failedQcTasks = useMemo(() => { + const list = data?.sections?.failed_qc ?? []; + if (!activeProject?.id) return list; + return list.filter((t) => !t.project_id || String(t.project_id) === String(activeProject.id)); + }, [data?.sections?.failed_qc, activeProject?.id]); + + const overdueTasks = useMemo(() => { + const list = data?.sections?.overdue ?? []; + if (!activeProject?.id) return list; + return list.filter((t) => !t.project_id || String(t.project_id) === String(activeProject.id)); + }, [data?.sections?.overdue, activeProject?.id]); + + const todayChecklists = useMemo(() => { + if (!data?.sections) return []; + const dueToday = data.sections.due_today ?? []; + const awaitingQc = data.sections.awaiting_qc ?? []; + const combined = [...dueToday, ...awaitingQc]; + if (!activeProject?.id) return combined; + return combined.filter((t) => !t.project_id || String(t.project_id) === String(activeProject.id)); + }, [data?.sections, activeProject?.id]); + + const upcomingTasks = useMemo(() => { + const list = data?.sections?.upcoming ?? []; + if (!activeProject?.id) return list; + return list.filter((t) => !t.project_id || String(t.project_id) === String(activeProject.id)); + }, [data?.sections?.upcoming, activeProject?.id]); + + // فیلتر چک‌لیست‌های عملیاتی کارگاه + const pendingApprovalList = useMemo(() => { + return todayChecklists.filter((t) => t.qc_status !== "in_progress"); + }, [todayChecklists]); + + const inProgressList = useMemo(() => { + return todayChecklists.filter((t) => t.qc_status === "in_progress"); + }, [todayChecklists]); + + const displayedChecklists = useMemo(() => { + if (activeTab === "pending_approval") return pendingApprovalList; + if (activeTab === "in_progress") return inProgressList; + return todayChecklists; + }, [activeTab, todayChecklists, pendingApprovalList, inProgressList]); + + // محاسبه شاخص‌های سلامت کارگاه بر اساس داده‌های واقعی سرور + const criticalCount = activeProject?.stats + ? (activeProject.stats.failed_qc ?? 0) + (activeProject.stats.overdue ?? 0) + : failedQcTasks.length + overdueTasks.length; + + const pendingApprovalCount = pendingApprovalList.length; + + const verifiedChecklistsCount = activeProject?.stats + ? (activeProject.stats.completed ?? 0) + : (data?.summary?.ok ?? 0); + + // تب‌های فیلتر چک‌لیست‌های روزانه کارگاه + const filterTabs = useMemo[]>(() => [ + { id: "all", label: "همه اقدامات", count: todayChecklists.length }, + { + id: "pending_approval", + label: "در انتظار تایید / بازرسی", + count: pendingApprovalList.length, + tone: pendingApprovalList.length > 0 ? "amber" : undefined, + }, + { + id: "in_progress", + label: "در حال بررسی", + count: inProgressList.length, + }, + ], [todayChecklists.length, pendingApprovalList.length, inProgressList.length]); + + const activeRole = data?.role || user?.role || "site_supervisor"; + const roleTitle = getRoleTitle(activeRole, user?.role_label || data?.role_label); + const headerTitle = getDashboardHeaderTitle(activeRole); + const userDisplayName = user?.display_name || user?.username || "کاربر سیستم"; return ( <> } + title={headerTitle} + sub={ + + {userDisplayName} + • + {roleTitle} + + } + menu={ + + } right={ @@ -179,143 +462,237 @@ export default function TodayPage() { type="button" onClick={refresh} aria-label="به‌روزرسانی" - className="flex h-11 w-11 items-center justify-center rounded-full text-slate-500 active:bg-slate-100" + className="flex h-10 w-10 items-center justify-center rounded-full text-slate-500 hover:text-slate-700 active:bg-slate-100" > - + } /> -
- {/* هویت نقش و محدوده دسترسی */} -
- {roleLabels[role]} - {scopeLabel(user)} - - {isManager - ? "نمای مدیریتی همه پروژه‌ها" - : "نمای عملیاتی پروژه‌های اختصاص‌یافته"} - -
- - {todayQuery.isPending ? ( -
+
+ {todayQuery.isPending || projectsQuery.isPending ? ( +
+
+ + در حال بارگذاری اطلاعات پروژه و وظایف... +
+
-
-
-
+
+
+
-
- ) : todayQuery.isError ? ( + ) : todayQuery.isError || projectsQuery.isError ? ( - ) : data ? ( + ) : allProjects.length === 0 ? ( + + + + ) : !activeProject ? ( + + + + ) : ( <> + {/* ---------------------------------------------------- */} + {/* ۱. نمایش وضعیت پروژه و کارگاه فعال */} + {/* ---------------------------------------------------- */} +
+
+
+ + پروژه فعال: {activeProject.name} +
+
+ +

+ مرحله: + {currentPhase} +

+
+ + {/* ---------------------------------------------------- */} + {/* ۲. کارت‌های شاخص سلامت کارگاه با عناوین کوتاه و بدون بریدگی */} + {/* ---------------------------------------------------- */}
setActiveTab("pending_approval")} + /> + setActiveTab("all")} /> -
- {/* نمای مدیریتی پروژه‌ها — فقط مدیر پروژه */} - {isManager && projects.length > 0 && ( - -
-

وضعیت پروژه‌ها

- - همه پروژه‌ها - + {/* ---------------------------------------------------- */} + {/* ۳. بخش اقدام فوری: QC مردود و عدم انطباق (مستقل و رندر مشروط) */} + {/* ---------------------------------------------------- */} + {failedQcTasks.length > 0 && ( +
+
+

+ + نیازمند اقدام فوری (عدم انطباق و QC مردود) +

+ {failedQcTasks.length} مورد
+
- {projects.map((p) => ( - + {failedQcTasks.map((t) => ( + ))}
- - )} - - {/* ناظر بدون پروژه اختصاص‌یافته */} - {!isManager && (data?.scope?.project_count ?? user?.scope?.project_count ?? 0) === 0 && totalItems === 0 && ( - - - - )} - - {totalItems === 0 && (isManager || (data?.scope?.project_count ?? user?.scope?.project_count ?? 0) > 0) ? ( - - - - ) : ( -
- {sections.map(({ key, title, tone }) => { - const items = data.sections[key]; - if (!items || items.length === 0) return null; - const isUpcoming = key === "upcoming"; - - if (isUpcoming && !showUpcoming) { - return ( - - ); - } - - return ( - -
-

{title}

- - {items.length} مورد - {isUpcoming && ( - - )} - -
-
- {items.map((t) => ( - - ))} -
-
- ); - })}
)} + + {/* ---------------------------------------------------- */} + {/* ۴. چیپ‌های فیلتر عملیاتی چک‌لیست‌های روزانه */} + {/* ---------------------------------------------------- */} +
+ {filterTabs.map((opt) => { + const active = opt.id === activeTab; + return ( + + ); + })} +
+ + {/* ---------------------------------------------------- */} + {/* ۵. بخش چک‌لیست‌ها و تأیدیه‌های امروز کارگاه (طرح سطری فشرده) */} + {/* ---------------------------------------------------- */} +
+
+

+ + چک‌لیست‌ها و تأیدیه‌های امروز +

+ {displayedChecklists.length} مورد +
+ + {displayedChecklists.length > 0 ? ( +
+ {displayedChecklists.map((t) => ( + + ))} +
+ ) : ( + + + + )} +
+ + {/* ---------------------------------------------------- */} + {/* ۶. بخش برنامه آتی و نقاط عطف کارگاه (هفته آینده) */} + {/* ---------------------------------------------------- */} + + + + {(showUpcoming ?? true) && ( + upcomingTasks.length > 0 ? ( +
+ {upcomingTasks.map((t) => ( + + ))} +
+ ) : ( +
+ هیچ برنامه یا فعالیت آتی برای هفته آینده در این پروژه ثبت نشده است. +
+ ) + )} +
- ) : null} + )}
+ ); }