+ {/* سطر ۱: نام پروژه و بج پیل وضعیت */}
+
+
+ {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}
+ )}
+
>
);
}