From 737a68d886c23cc7258313185b76c761b6764f33 Mon Sep 17 00:00:00 2001 From: saeed0920 Date: Wed, 17 Sep 2025 19:44:02 +0330 Subject: [PATCH] update and fix styles in project-management page --- app/app.css | 22 -- .../project-management-page.tsx | 354 ++++++++++++++++-- 2 files changed, 315 insertions(+), 61 deletions(-) diff --git a/app/app.css b/app/app.css index 4247830..0c8c204 100644 --- a/app/app.css +++ b/app/app.css @@ -246,23 +246,6 @@ html[dir="rtl"] body { @apply bg-background text-foreground; } - /* Scrollbar styling */ - ::-webkit-scrollbar { - width: 6px; - height: 6px; - } - - ::-webkit-scrollbar-track { - @apply bg-neutral-100 dark:bg-neutral-800; - } - - ::-webkit-scrollbar-thumb { - @apply bg-neutral-300 dark:bg-neutral-600 rounded-full; - } - - ::-webkit-scrollbar-thumb:hover { - @apply bg-neutral-400 dark:bg-neutral-500; - } } /* Persian/Farsi font class */ @@ -434,20 +417,15 @@ html[dir="rtl"] body { } .custom-scrollbar:hover::-webkit-scrollbar-thumb { - background: linear-gradient(to bottom, rgba(16, 185, 129, 0.8), rgba(16, 185, 129, 1)); } .dark .custom-scrollbar { - scrollbar-color: rgba(16, 185, 129, 0.6) rgba(30, 41, 59, 0.6); /* thumb track */ } .dark .custom-scrollbar::-webkit-scrollbar-track { - background: rgba(30, 41, 59, 0.6); /* slate-800 */ } .dark .custom-scrollbar::-webkit-scrollbar-thumb { - background: linear-gradient(to bottom, rgba(16, 185, 129, 0.5), rgba(16, 185, 129, 0.9)); - border-color: rgba(30, 41, 59, 0.6); } diff --git a/app/components/dashboard/project-management/project-management-page.tsx b/app/components/dashboard/project-management/project-management-page.tsx index 0c073ac..087f98a 100644 --- a/app/components/dashboard/project-management/project-management-page.tsx +++ b/app/components/dashboard/project-management/project-management-page.tsx @@ -1,5 +1,5 @@ import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState, useMemo } from "react"; import toast from "react-hot-toast"; import { Badge } from "~/components/ui/badge"; import { Card, CardContent } from "~/components/ui/card"; @@ -7,6 +7,7 @@ import { Table, TableBody, TableCell, + TableFooter, TableHead, TableHeader, TableRow, @@ -53,51 +54,51 @@ type ColumnDef = { }; const columns: ColumnDef[] = [ - { key: "title", label: "عنوان پروژه", sortable: true, width: "200px" }, + { key: "title", label: "عنوان پروژه", sortable: true, width: "300px" }, { key: "importance_project", label: "میزان اهمیت", sortable: true, - width: "150px", + width: "160px", }, { key: "strategic_theme", label: "مضمون راهبردی", sortable: true, - width: "160px", + width: "200px", }, { key: "value_technology_and_innovation", label: "ارزش فناوری و نوآوری", sortable: true, - width: "200px", + width: "220px", }, { key: "type_of_innovation", label: "انواع نوآوری", sortable: true, - width: "140px", + width: "160px", }, - { key: "innovation", label: "میزان نوآوری", sortable: true, width: "120px" }, + { key: "innovation", label: "میزان نوآوری", sortable: true, width: "140px" }, { key: "person_executing", label: "مسئول اجرا", sortable: true, - width: "140px", + width: "180px", }, { key: "excellent_observer", label: "ناطر عالی", sortable: true, - width: "140px", + width: "180px", }, - { key: "observer", label: "ناظر پروژه", sortable: true, width: "140px" }, - { key: "moderator", label: "مجری", sortable: true, width: "140px" }, + { key: "observer", label: "ناظر پروژه", sortable: true, width: "180px" }, + { key: "moderator", label: "مجری", sortable: true, width: "180px" }, { key: "executive_phase", label: "فاز اجرایی", sortable: true, - width: "140px", + width: "160px", }, { key: "start_date", label: "تاریخ شروع", sortable: true, width: "120px" }, { @@ -495,6 +496,149 @@ export function ProjectManagementPage() { } }; + // Categories for which we'll generate/display color legends + const categoryDefs = [ + { + key: "strategic_theme", + label: "مضمون راهبردی", + palette: ["#6D53FB", "#7C3AED", "#5B21B6", "#4C1D95", "#A78BFA"], + }, + { + key: "value_technology_and_innovation", + label: "ارزش فناوری و نوآوری", + palette: ["#A757FF", "#C084FC", "#8B5CF6", "#7C3AED", "#D8B4FE"], + }, + { + key: "type_of_innovation", + label: "انواع نوآوری", + palette: ["#E884CE", "#FB7185", "#F472B6", "#F97316", "#FBCFE8"], + }, + { + key: "innovation", + label: "میزان نوآوری", + palette: ["#C3BF8B", "#10B981", "#F59E0B", "#EF4444", "#FDE68A"], + }, + { + key: "executive_phase", + label: "فاز اجرایی", + palette: ["#C3BF8B", "#10B981", "#F59E0B", "#EF4444", "#FDE68A"], + }, + ]; + + // Build a mapping of value -> color for each category based on loaded projects. + // We assign colors deterministically from the category palette in order of appearance. + const categoryColorMaps = useMemo(() => { + const maps: Record> = {}; + categoryDefs.forEach((cat) => { + maps[cat.key] = {}; + const seen = new Map(); + const values: string[] = projects + .map((p) => (p as any)[cat.key]) + .filter((v) => v !== undefined && v !== null && String(v).trim() !== "") + .map((v) => String(v)); + + // preserve order of first appearance + values.forEach((val, idx) => { + if (!seen.has(val)) { + const color = cat.palette[seen.size % cat.palette.length]; + seen.set(val, color); + } + }); + + seen.forEach((color, val) => { + maps[cat.key][val] = color; + }); + }); + return maps; + }, [projects]); + + // Compute counts and totals for each category so footer segments can be proportional + const categoryStats = useMemo(() => { + const stats: Record; total: number }> = {}; + categoryDefs.forEach((cat) => { + const counts: Record = {}; + let total = 0; + projects.forEach((p) => { + const val = String((p as any)[cat.key] ?? "").trim(); + if (val !== "") { + counts[val] = (counts[val] || 0) + 1; + total += 1; + } + }); + stats[cat.key] = { counts, total }; + }); + // also compute executive_phase counts + const execCounts: Record = {}; + let execTotal = 0; + projects.forEach((p) => { + const val = String((p as any)["executive_phase"] ?? "").trim(); + if (val !== "") { + execCounts[val] = (execCounts[val] || 0) + 1; + execTotal += 1; + } + }); + stats["executive_phase"] = { counts: execCounts, total: execTotal }; + + return stats; + }, [projects]); + + // Importance counts (بالا، متوسط، پایین) for footer bar + const importanceCounts = useMemo(() => { + const counts: Record = {}; + let total = 0; + projects.forEach((p) => { + const val = String((p as any).importance_project ?? "").trim(); + if (val !== "") { + counts[val] = (counts[val] || 0) + 1; + total += 1; + } + }); + return { counts, total }; + }, [projects]); + + // Numeric averages for specified columns + const numericAverages = useMemo(() => { + const keys = [ + "remaining_time", + "renewed_duration", + "deviation_from_program", + "approved_budget", + "budget_spent", + "cost_deviation", + ]; + const res: Record = {}; + + // remaining_time is computed from end_date + const remainingValues: number[] = projects + .map((p) => calculateRemainingDays((p as any).end_date)) + .filter((v) => v !== null) as number[]; + res["remaining_time"] = remainingValues.length + ? Math.round(remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length) + : null; + + // For other keys, parse numeric values + keys.forEach((k) => { + if (k === "remaining_time") return; + const vals: number[] = projects + .map((p) => { + const raw = (p as any)[k]; + if (raw == null) return NaN; + const num = Number(String(raw).toString().replace(/[^0-9.-]/g, "")); + return Number.isFinite(num) ? num : NaN; + }) + .filter((n) => !Number.isNaN(n)); + res[k] = vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null; + }); + + return res; + }, [projects]); + + const getCategoryColor = (categoryKey: string, value: unknown) => { + const val = value == null ? "" : String(value); + const map = categoryColorMaps[categoryKey] || {}; + return map[val] ?? "#6B7280"; // fallback gray + }; + const renderCellContent = (item: ProjectData, column: ColumnDef) => { const apiField = column.apiField ?? column.key; const value = (item as any)[apiField]; @@ -509,7 +653,7 @@ export function ProjectManagementPage() { return ( روز {toPersianDigits(days)} @@ -520,55 +664,58 @@ export function ProjectManagementPage() { case "value_technology_and_innovation": case "type_of_innovation": case "innovation": + case "executive_phase": { + const color = getCategoryColor(column.key, value); return ( - {String(value) || "-"} + {!!value ? String(value) : "-"} ); + } case "approved_budget": case "budget_spent": return ( - + {formatCurrency(String(value))} ); case "deviation_from_program": case "cost_deviation": return ( - {formatNumber(value as any)} + {formatNumber(value as any)} ); case "start_date": case "end_date": case "done_date": return ( - {formatDate(String(value))} + {formatDate(String(value))} ); case "project_no": return ( {String(value)} ); case "title": - return {String(value)}; + return {String(value)}; case "importance_project": return ( {String(value)} @@ -576,7 +723,7 @@ export function ProjectManagementPage() { ); default: return ( - + {(value && String(value)) || "-"} ); @@ -592,14 +739,15 @@ export function ProjectManagementPage() {
- +
+
{columns.map((column) => ( {column.sortable ? (
+ + + + {columns.map((column, colIndex) => { + // First column: show total projects text similar to API count + if (colIndex === 0) { + return ( + + کل پروژه‌ها: {formatNumber(actualTotalCount)} + + ); + } + // importance_project: render importance bar with specified colors + if (column.key === "importance_project") { + const imp = importanceCounts; + const order = ["بالا", "متوسط", "پایین"]; + const colorFor = (k: string) => { + switch (k) { + case "بالا": + return "var(--color-pr-green)"; // green + case "متوسط": + return "#69C8EA"; // blue-ish + case "پایین": + return "#F76276"; // red + default: + return "#6B7280"; + } + }; + return ( + +
+ {order.map((k) => { + const cnt = imp.counts[k] || 0; + const widthPercent = imp.total > 0 ? (cnt / imp.total) * 100 : 0; + return ( +
+
+ ); + })} +
+
+ ); + } + + // For category-like columns: strategic_theme, value_technology_and_innovation, innovation, executive_phase + const categoryLike = [ + "strategic_theme", + "value_technology_and_innovation", + "innovation", + "executive_phase", + ]; + if (categoryLike.includes(column.key)) { + const stat = categoryStats[column.key] || { counts: {}, total: 0 }; + const entries = Object.entries(stat.counts); + return ( + +
+ {entries.length > 0 ? ( + entries.map(([val, cnt]) => { + let color = categoryColorMaps[column.key]?.[val] || "#6B7280"; + if (column.key === "executive_phase") { + color = (phaseColors as any)[val] || color; + } + const widthPercent = stat.total > 0 ? (cnt / stat.total) * 100 : 0; + return ( +
+
+ ); + }) + ) : ( +
+ )} +
+ + ); + } + + // remove bar for type_of_innovation (show empty cell) + if (column.key === "type_of_innovation") { + return ; + } + + // remaining_time: show average days with color (green/red/white) + if (column.key === "remaining_time") { + const avg = numericAverages["remaining_time"] as number | null; + const color = avg == null ? "#9CA3AF" : avg > 0 ? "#3AEA83" : avg < 0 ? "#F76276" : "#FFFFFF"; + return ( + + {avg == null ? "-" : `${formatNumber(avg)} روز`} + + ); + } + + // For numeric columns: show average rounded + const numericKeyMap: Record = { + renewed_duration: "renewed_duration", + deviation_from_program: "deviation_from_program", + approved_budget: "approved_budget", + budget_spent: "budget_spent", + cost_deviation: "cost_deviation", + }; + const mapped = (numericKeyMap as any)[column.key]; + if (mapped) { + const avg = numericAverages[mapped] as number | null; + let display = "-"; + if (avg != null) { + display = mapped.includes("budget") ? formatCurrency(String(Math.round(avg))) : formatNumber(Math.round(avg)); + } + return ( + + {display} + + ); + } + + // Default: empty cell to keep alignment + return ; + })} + + + +
{/* Infinite scroll trigger */} @@ -685,7 +966,7 @@ export function ProjectManagementPage() { {loadingMore && (
- +
@@ -693,12 +974,7 @@ export function ProjectManagementPage() {
- {/* Footer */} -
-
- کل پروژه‌ها: {formatNumber(actualTotalCount)} -
-
+