remove the package-lock json ,also fix the api call for project-management and fix some style in dashboard-home

This commit is contained in:
Saeed AB 2025-09-21 16:30:18 +03:30
parent 85ae658c85
commit 97331fdf34
7 changed files with 1421 additions and 8323 deletions

View File

@ -1,11 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
@import url(/font/fontiran.css); @import url(/font/iranfont.css);
@theme { @theme {
--font-sans:
"Vazirmatn", "Inter", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* Teal color scale */ /* Teal color scale */
--color-teal-50: #f0fdfa; --color-teal-50: #f0fdfa;
--color-teal-100: #ccfbf1; --color-teal-100: #ccfbf1;
@ -32,30 +28,35 @@
--color-slate-900: #0f172a; --color-slate-900: #0f172a;
--color-slate-950: #020617; --color-slate-950: #020617;
--color-pr-green : #3AEA83; --color-pr-green: #3aea83;
--color-pr-blue : #69C8EA; --color-pr-blue: #69c8ea;
--color-pr-red : #F76276; --color-pr-red: #f76276;
--color-pr-gray : #3F415A; --color-pr-gray: #3f415a;
} }
html, html,
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
color-scheme: dark; color-scheme: dark;
} }
} }
body { body {
font-family: IRANYekanX !important; font-family: IRANYekanX;
direction: rtl; direction: rtl;
background-color: #cdcdcd; background-color: #cdcdcd;
margin: 0; margin: 0;
} }
h1, h2, h3, h4, h5, h6,input, textarea { h1,
font-family: IRANYekanX !important; h2,
h3,
h4,
h5,
h6,
input,
textarea {
font-family: IRANYekanX;
} }
/* RTL Support */ /* RTL Support */
@ -96,12 +97,12 @@ html[dir="rtl"] body {
:root { :root {
--radius: 0.5rem; --radius: 0.5rem;
--color-green: #3AEA83; --color-green: #3aea83;
--color-blue: #69C8EA; --color-blue: #69c8ea;
--color-red: #F76276; --color-red: #f76276;
/* primary colors */ /* primary colors */
--color-pr-gray : #3F415A; --color-pr-gray: #3f415a;
--color-pr-green: var(--color-green); --color-pr-green: var(--color-green);
/* Light theme colors */ /* Light theme colors */
@ -255,12 +256,11 @@ html[dir="rtl"] body {
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
/* Persian/Farsi font class */ /* Persian/Farsi font class */
.font-persian { .font-persian {
font-family: IRANYekanX; font-family: "IRANYekanX";
} }
/* Custom utility classes */ /* Custom utility classes */
@ -420,9 +420,13 @@ html[dir="rtl"] body {
} }
.custom-scrollbar::-webkit-scrollbar-thumb { .custom-scrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(to bottom, rgba(16, 185, 129, 0.6), rgba(16, 185, 129, 0.9)); /* emerald */ background: linear-gradient(
to bottom,
rgba(16, 185, 129, 0.6),
rgba(16, 185, 129, 0.9)
); /* emerald */
border-radius: 9999px; border-radius: 9999px;
border: .5px solid transparent; border: 0.5px solid transparent;
background-clip: padding-box; background-clip: padding-box;
} }
@ -438,11 +442,10 @@ html[dir="rtl"] body {
.dark .custom-scrollbar::-webkit-scrollbar-thumb { .dark .custom-scrollbar::-webkit-scrollbar-thumb {
} }
:root { :root {
--form-control-color: #3F415A; --form-control-color: #3f415a;
--form-control-disabled: ##5F6284; --form-control-disabled: ##5f6284;
--form-background: #3AEA83; --form-background: #3aea83;
} }
input[type="checkbox"] { input[type="checkbox"] {
@ -450,11 +453,11 @@ input[type="checkbox"] {
appearance: none; appearance: none;
margin: 0; margin: 0;
font: inherit; font: inherit;
color: #5F6284; color: #5f6284;
background-color: transparent; background-color: transparent;
width: 1.15em; width: 1.15em;
height: 1.15em; height: 1.15em;
border: 1px solid #5F6284; border: 1px solid #5f6284;
border-radius: 0.15em; border-radius: 0.15em;
transform: translateY(-0.075em); transform: translateY(-0.075em);
display: grid; display: grid;
@ -478,7 +481,7 @@ input[type="checkbox"]:checked::before {
} }
input[type="checkbox"]:checked { input[type="checkbox"]:checked {
background-color: #3AEA83 ; background-color: #3aea83;
border: 1px solid transparent; border: 1px solid transparent;
} }

View File

@ -595,10 +595,10 @@ export function DashboardHome() {
تحقق ارزش ها تحقق ارزش ها
</p> </p>
<TabsList className="bg-transparent py-2 border m-6 border-gray-600"> <TabsList className="bg-transparent py-2 border m-6 border-gray-600">
<TabsTrigger value="canvas" className=""> <TabsTrigger value="canvas" className="cursor-pointer">
شماتیک شماتیک
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="charts" className=" text-white font-light "> <TabsTrigger value="charts" className=" text-white cursor-pointer font-light ">
مقایسه ای مقایسه ای
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>

View File

@ -97,7 +97,7 @@ export function Header({
{ {
user?.id === 2041 && <button user?.id === 2041 && <button
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian" className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
onClick={redirectHandler}> onClick={redirectHandler}>
<Server className="h-4 w-4" /> <Server className="h-4 w-4" />
ورود به میزکار مدیریت</button> ورود به میزکار مدیریت</button>

View File

@ -289,7 +289,6 @@ export function ProjectManagementPage() {
const { scrollTop, scrollHeight, clientHeight } = scrollContainer; const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
// Trigger load more when scrolled to 90% of the container // Trigger load more when scrolled to 90% of the container
if (scrollPercentage >= 0.9) { if (scrollPercentage >= 0.9) {
loadMore(); loadMore();
@ -554,7 +553,10 @@ export function ProjectManagementPage() {
// Compute counts and totals for each category so footer segments can be proportional // Compute counts and totals for each category so footer segments can be proportional
const categoryStats = useMemo(() => { const categoryStats = useMemo(() => {
const stats: Record<string, { counts: Record<string, number>; total: number }> = {}; const stats: Record<
string,
{ counts: Record<string, number>; total: number }
> = {};
categoryDefs.forEach((cat) => { categoryDefs.forEach((cat) => {
const counts: Record<string, number> = {}; const counts: Record<string, number> = {};
let total = 0; let total = 0;
@ -613,7 +615,9 @@ export function ProjectManagementPage() {
.map((p) => calculateRemainingDays((p as any).end_date)) .map((p) => calculateRemainingDays((p as any).end_date))
.filter((v) => v !== null) as number[]; .filter((v) => v !== null) as number[];
res["remaining_time"] = remainingValues.length res["remaining_time"] = remainingValues.length
? Math.round(remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length) ? Math.round(
remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length,
)
: null; : null;
// For other keys, parse numeric values // For other keys, parse numeric values
@ -623,11 +627,17 @@ export function ProjectManagementPage() {
.map((p) => { .map((p) => {
const raw = (p as any)[k]; const raw = (p as any)[k];
if (raw == null) return NaN; if (raw == null) return NaN;
const num = Number(String(raw).toString().replace(/[^0-9.-]/g, "")); const num = Number(
String(raw)
.toString()
.replace(/[^0-9.-]/g, ""),
);
return Number.isFinite(num) ? num : NaN; return Number.isFinite(num) ? num : NaN;
}) })
.filter((n) => !Number.isNaN(n)); .filter((n) => !Number.isNaN(n));
res[k] = vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null; res[k] = vals.length
? vals.reduce((a, b) => a + b, 0) / vals.length
: null;
}); });
return res; return res;
@ -668,7 +678,9 @@ export function ProjectManagementPage() {
const color = getCategoryColor(column.key, value); const color = getCategoryColor(column.key, value);
return ( return (
<span className="inline-flex items-center justify-end flex-row-reverse gap-2 w-full"> <span className="inline-flex items-center justify-end flex-row-reverse gap-2 w-full">
<span className="text-gray-300">{!!value ? String(value) : "-"}</span> <span className="text-gray-300">
{!!value ? String(value) : "-"}
</span>
<span <span
style={{ style={{
backgroundColor: color, backgroundColor: color,
@ -689,25 +701,30 @@ export function ProjectManagementPage() {
case "deviation_from_program": case "deviation_from_program":
case "cost_deviation": case "cost_deviation":
return ( return (
<span className="text-sm font-normal">{formatNumber(value as any)}</span> <span className="text-sm font-normal">
{formatNumber(value as any)}
</span>
); );
case "start_date": case "start_date":
case "end_date": case "end_date":
case "done_date": case "done_date":
return ( return (
<span className=" text-sm font-normal">{formatDate(String(value))}</span> <span className=" text-sm font-normal">
{formatDate(String(value))}
</span>
); );
case "project_no": case "project_no":
return ( return (
<Badge <Badge variant="teal" className="border-emerald-500/50">
variant="teal"
className="border-emerald-500/50"
>
{String(value)} {String(value)}
</Badge> </Badge>
); );
case "title": case "title":
return <span className="text-sm font-normal text-white">{String(value)}</span>; return (
<span className="text-sm font-normal text-white">
{String(value)}
</span>
);
case "importance_project": case "importance_project":
return ( return (
<Badge <Badge
@ -767,8 +784,7 @@ export function ProjectManagementPage() {
</button> </button>
) : ( ) : (
column.label column.label
) )}
}
</TableHead> </TableHead>
))} ))}
</TableRow> </TableRow>
@ -791,7 +807,9 @@ export function ProjectManagementPage() {
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" /> <div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" />
<div <div
className="h-2.5 bg-gray-600 rounded animate-pulse" className="h-2.5 bg-gray-600 rounded animate-pulse"
style={{ width: `${Math.random() * 60 + 40}%` }} style={{
width: `${Math.random() * 60 + 40}%`,
}}
/> />
</div> </div>
</TableCell> </TableCell>
@ -834,7 +852,10 @@ export function ProjectManagementPage() {
// First column: show total projects text similar to API count // First column: show total projects text similar to API count
if (colIndex === 0) { if (colIndex === 0) {
return ( return (
<TableCell key={column.key} className="p-3 text-sm text-white font-semibold font-persian"> <TableCell
key={column.key}
className="p-3 text-sm text-white font-semibold font-persian"
>
کل پروژهها: {formatNumber(actualTotalCount)} کل پروژهها: {formatNumber(actualTotalCount)}
</TableCell> </TableCell>
); );
@ -860,15 +881,18 @@ export function ProjectManagementPage() {
<div className="w-full bg-gray-800 rounded-sm overflow-hidden h-3 flex"> <div className="w-full bg-gray-800 rounded-sm overflow-hidden h-3 flex">
{order.map((k) => { {order.map((k) => {
const cnt = imp.counts[k] || 0; const cnt = imp.counts[k] || 0;
const widthPercent = imp.total > 0 ? (cnt / imp.total) * 100 : 0; const widthPercent =
imp.total > 0 ? (cnt / imp.total) * 100 : 0;
return ( return (
<div <div
key={k} key={k}
title={`${k} (${cnt})`} title={`${k} (${cnt})`}
className="h-3 flex items-center justify-center text-xs font-medium" className="h-3 flex items-center justify-center text-xs font-medium"
style={{ width: `${widthPercent}%`, backgroundColor: colorFor(k) }} style={{
> width: `${widthPercent}%`,
</div> backgroundColor: colorFor(k),
}}
></div>
); );
})} })}
</div> </div>
@ -884,26 +908,37 @@ export function ProjectManagementPage() {
"executive_phase", "executive_phase",
]; ];
if (categoryLike.includes(column.key)) { if (categoryLike.includes(column.key)) {
const stat = categoryStats[column.key] || { counts: {}, total: 0 }; const stat = categoryStats[column.key] || {
counts: {},
total: 0,
};
const entries = Object.entries(stat.counts); const entries = Object.entries(stat.counts);
return ( return (
<TableCell key={column.key} className="p-1"> <TableCell key={column.key} className="p-1">
<div className="w-full bg-gray-800 rounded-sm overflow-hidden h-3 flex"> <div className="w-full bg-gray-800 rounded-sm overflow-hidden h-3 flex">
{entries.length > 0 ? ( {entries.length > 0 ? (
entries.map(([val, cnt]) => { entries.map(([val, cnt]) => {
let color = categoryColorMaps[column.key]?.[val] || "#6B7280"; let color =
categoryColorMaps[column.key]?.[val] ||
"#6B7280";
if (column.key === "executive_phase") { if (column.key === "executive_phase") {
color = (phaseColors as any)[val] || color; color =
(phaseColors as any)[val] || color;
} }
const widthPercent = stat.total > 0 ? (cnt / stat.total) * 100 : 0; const widthPercent =
stat.total > 0
? (cnt / stat.total) * 100
: 0;
return ( return (
<div <div
key={val} key={val}
title={`${val} (${cnt})`} title={`${val} (${cnt})`}
className="h-3 flex items-center justify-center text-xs font-medium" className="h-3 flex items-center justify-center text-xs font-medium"
style={{ width: `${widthPercent}%`, backgroundColor: color }} style={{
> width: `${widthPercent}%`,
</div> backgroundColor: color,
}}
></div>
); );
}) })
) : ( ) : (
@ -921,10 +956,23 @@ export function ProjectManagementPage() {
// remaining_time: show average days with color (green/red/white) // remaining_time: show average days with color (green/red/white)
if (column.key === "remaining_time") { if (column.key === "remaining_time") {
const avg = numericAverages["remaining_time"] as number | null; const avg = numericAverages["remaining_time"] as
const color = avg == null ? "#9CA3AF" : avg > 0 ? "#3AEA83" : avg < 0 ? "#F76276" : "#FFFFFF"; | number
| null;
const color =
avg == null
? "#9CA3AF"
: avg > 0
? "#3AEA83"
: avg < 0
? "#F76276"
: "#FFFFFF";
return ( return (
<TableCell key={column.key} className="p-2 text-right font-medium" style={{ color }}> <TableCell
key={column.key}
className="p-2 text-right font-medium"
style={{ color }}
>
{avg == null ? "-" : `${formatNumber(avg)} روز`} {avg == null ? "-" : `${formatNumber(avg)} روز`}
</TableCell> </TableCell>
); );
@ -943,10 +991,15 @@ export function ProjectManagementPage() {
const avg = numericAverages[mapped] as number | null; const avg = numericAverages[mapped] as number | null;
let display = "-"; let display = "-";
if (avg != null) { if (avg != null) {
display = mapped.includes("budget") ? formatCurrency(String(Math.round(avg))) : formatNumber(Math.round(avg)); display = mapped.includes("budget")
? formatCurrency(String(Math.round(avg)))
: formatNumber(Math.round(avg));
} }
return ( return (
<TableCell key={column.key} className="p-2 text-right font-medium text-gray-200"> <TableCell
key={column.key}
className="p-2 text-right font-medium text-gray-200"
>
{display} {display}
</TableCell> </TableCell>
); );
@ -973,8 +1026,6 @@ export function ProjectManagementPage() {
)} )}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</DashboardLayout> </DashboardLayout>

6910
package-lock.json generated

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -17,92 +17,100 @@ This set of fonts are used in this project under the license: (.....)
* *
**/ **/
@font-face { @font-face {
font-family: IRANYekanX; font-family: IRANYekanX;
font-style: normal; font-style: normal;
font-weight: 100; font-weight: 100;
src: url('woff/IRANYekanX-Thin.woff') format('woff'), src:
url('woff2/IRANYekanX-Thin.woff2') format('woff2'); url("/font/woff/IRANYekanX-Thin.woff") format("woff"),
url("/font/woff2/IRANYekanX-Thin.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: IRANYekanX; font-family: IRANYekanX;
font-style: normal; font-style: normal;
font-weight: 200; font-weight: 200;
src: url('woff/IRANYekanX-UltraLight.woff') format('woff'), src:
url('woff2/IRANYekanX-UltraLight.woff2') format('woff2'); url("/font/woff/IRANYekanX-UltraLight.woff") format("woff"),
url("/font/woff2/IRANYekanX-UltraLight.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: IRANYekanX; font-family: IRANYekanX;
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: url('woff/IRANYekanX-Light.woff') format('woff'), src:
url('woff2/IRANYekanX-Light.woff2') format('woff2'); url("/font/woff/IRANYekanX-Light.woff") format("woff"),
url("/font/woff2/IRANYekanX-Light.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: IRANYekanX; font-family: IRANYekanX;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: url('woff/IRANYekanX-Medium.woff') format('woff'), src:
url('woff2/IRANYekanX-Medium.woff2') format('woff2'); url("/font/woff/IRANYekanX-Medium.woff") format("woff"),
url("/font/woff2/IRANYekanX-Medium.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: IRANYekanX; font-family: IRANYekanX;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: url('woff/IRANYekanX-DemiBold.woff') format('woff'), src:
url('woff2/IRANYekanX-DemiBold.woff2') format('woff2'); url("/font/woff/IRANYekanX-DemiBold.woff") format("woff"),
url("/font/woff2/IRANYekanX-DemiBold.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: IRANYekanX; font-family: IRANYekanX;
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
src: url('woff/IRANYekanX-ExtraBold.woff') format('woff'), src:
url('woff2/IRANYekanX-ExtraBold.woff2') format('woff2'); url("/font/woff/IRANYekanX-ExtraBold.woff") format("woff"),
url("/font/woff2/IRANYekanX-ExtraBold.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: IRANYekanX; font-family: IRANYekanX;
font-style: normal; font-style: normal;
font-weight: 900; font-weight: 900;
src: url('woff/IRANYekanX-Black.woff') format('woff'), src:
url('woff2/IRANYekanX-Black.woff2') format('woff2'); url("/font/woff/IRANYekanX-Black.woff") format("woff"),
url("/font/woff2/IRANYekanX-Black.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: IRANYekanX; font-family: IRANYekanX;
font-style: normal; font-style: normal;
font-weight: 950; font-weight: 950;
src: url('woff/IRANYekanX-ExtraBlack.woff') format('woff'), src:
url('woff2/IRANYekanX-ExtraBlack.woff2') format('woff2'); url("/font/woff/IRANYekanX-ExtraBlack.woff") format("woff"),
url("/font/woff2/IRANYekanX-ExtraBlack.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: IRANYekanX; font-family: IRANYekanX;
font-style: normal; font-style: normal;
font-weight: 1000; font-weight: 1000;
src: url('woff/IRANYekanX-Heavy.woff') format('woff'), src:
url('woff2/IRANYekanX-Heavy.woff2') format('woff2'); url("/font/woff/IRANYekanX-Heavy.woff") format("woff"),
url("/font/woff2/IRANYekanX-Heavy.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: IRANYekanX; font-family: IRANYekanX;
font-style: normal; font-style: normal;
font-weight: bold; font-weight: bold;
src: url('woff/IRANYekanX-Bold.woff') format('woff'), src:
url('woff2/IRANYekanX-Bold.woff2') format('woff2'); url("/font/woff/IRANYekanX-Bold.woff") format("woff"),
url("/font/woff2/IRANYekanX-Bold.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: IRANYekanX; font-family: IRANYekanX;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
src: url('woff/IRANYekanX-Regular.woff') format('woff'), src:
url('woff2/IRANYekanX-Regular.woff2') format('woff2'); url("/font/woff/IRANYekanX-Regular.woff") format("woff"),
url("/font/woff2/IRANYekanX-Regular.woff2") format("woff2");
} }