Compare commits

...

5 Commits

21 changed files with 541 additions and 481 deletions

View File

@ -33,6 +33,10 @@
--color-slate-800: #1e293b; --color-slate-800: #1e293b;
--color-slate-900: #0f172a; --color-slate-900: #0f172a;
--color-slate-950: #020617; --color-slate-950: #020617;
--color-pr-green : #3AEA83;
--color-pr-blue : #69C8EA;
--color-pr-red : #F76276;
} }
html, html,
@ -82,9 +86,13 @@ html[dir="rtl"] body {
:root { :root {
--radius: 0.5rem; --radius: 0.5rem;
--color-green: #3AEA83;
--color-blue: #69C8EA;
--color-red: #F76276;
/* primary colors */ /* primary colors */
--color-pr-gray : #3F415A; --color-pr-gray : #3F415A;
--color-pr-green :#3AEA83; --color-pr-green : var(--color-green);
/* Light theme colors */ /* Light theme colors */
--background: #ffffff; --background: #ffffff;
@ -201,7 +209,7 @@ html[dir="rtl"] body {
--color-dark-950: #020617; --color-dark-950: #020617;
/* Login specific colors */ /* Login specific colors */
--color-login-primary: #3aea83; --color-login-primary: var(--color-green);
--color-login-dark-start: #464861; --color-login-dark-start: #464861;
--color-login-dark-end: #111628; --color-login-dark-end: #111628;
} }
@ -441,3 +449,53 @@ html[dir="rtl"] body {
background: linear-gradient(to bottom, rgba(16, 185, 129, 0.5), rgba(16, 185, 129, 0.9)); 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); border-color: rgba(30, 41, 59, 0.6);
} }
:root {
--form-control-color: #3F415A;
--form-control-disabled: ##5F6284;
--form-background: #3AEA83;
}
input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
margin: 0;
font: inherit;
color: #5F6284;
background-color: transparent;
width: 1.15em;
height: 1.15em;
border: 1px solid #5F6284;
border-radius: 0.15em;
transform: translateY(-0.075em);
display: grid;
place-content: center;
cursor: pointer;
}
input[type="checkbox"]::before {
content: "";
width: 0.65em;
height: 0.65em;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
transform: scale(0);
transform-origin: bottom left;
transition: 120ms transform ease-in-out;
box-shadow: inset 1em 1em var(--form-control-color);
}
input[type="checkbox"]:checked::before {
transform: scale(1);
}
input[type="checkbox"]:checked {
background-color: #3AEA83 ;
border: 1px solid transparent;
}
input[type="checkbox"]:disabled {
--form-control-color: var(--form-control-disabled);
color: var(--form-control-disabled);
cursor: not-allowed;
}

View File

@ -192,7 +192,7 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
type="submit" type="submit"
disabled={isLoading || isConnectionError} disabled={isLoading || isConnectionError}
size="lg" size="lg"
className="w-full font-persian bg-[var(--color-login-primary)] hover:bg-[var(--color-login-primary)]/90 text-slate-800 font-bold" className="w-full font-persian bg-[var(--color-login-primary)] hover:bg-[var(--color-login-primary)]/90 text-slate-800 text-base font-semibold"
> >
{isLoading ? ( {isLoading ? (
<> <>
@ -213,6 +213,7 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
<LoginSidebar> <LoginSidebar>
<LoginBranding <LoginBranding
brandName="پتروشیمی بندر امام" brandName="پتروشیمی بندر امام"
engSub="Inception by Fara"
companyName="توسعه‌یافته توسط شرکت رهپویان دانش و فناوری فرا" companyName="توسعه‌یافته توسط شرکت رهپویان دانش و فناوری فرا"
logo={<img src="/brand2.svg"/>} logo={<img src="/brand2.svg"/>}
/> />

View File

@ -75,12 +75,12 @@ export function LoginHeader({
return ( return (
<div className={cn(" space-y-4 flex text-right flex-col", className)}> <div className={cn(" space-y-4 flex text-right flex-col", className)}>
<div className="space-y-2"> <div className="space-y-2">
<h1 className="text-white text-lg font-medium font-persian">{title}</h1> <h1 className="text-white text-base font-medium font-persian">{title}</h1>
<h2 className="text-white text-2xl sm:text-3xl font-bold font-persian leading-relaxed"> <h2 className="text-white text-3xl sm:text-3xl font-bold font-persian leading-relaxed">
{subtitle} {subtitle}
</h2> </h2>
{description && ( {description && (
<p className="text-slate-300 text-sm font-persian leading-relaxed mx-auto"> <p className="text-slate-300 text-sm text-[#ACACAC] font-persian leading-relaxed mx-auto">
{description} {description}
</p> </p>
)} )}
@ -94,6 +94,7 @@ interface LoginBrandingProps {
companyName: string; companyName: string;
logo?: React.ReactNode; logo?: React.ReactNode;
className?: string; className?: string;
engSub ?: string;
} }
export function LoginBranding({ export function LoginBranding({
@ -101,6 +102,7 @@ export function LoginBranding({
companyName, companyName,
logo, logo,
className, className,
engSub
}: LoginBrandingProps) { }: LoginBrandingProps) {
return ( return (
<> <>
@ -116,7 +118,8 @@ export function LoginBranding({
{/* Bottom Section */} {/* Bottom Section */}
<div className="flex flex-col gap-2 mb-4 items-end justify-end"> <div className="flex flex-col gap-2 mb-4 items-end justify-end">
{logo && <div className="flex items-center">{logo}</div>} {logo && <div className="flex items-center">{logo}</div>}
<div className="text-slate-800 text-sm font-persian leading-relaxed max-w-xs"> <h3 className="text-[#3F415A] text-sm font-persian font-light leading-relaxed max-w-xs">{engSub}</h3>
<div className="text-[#3F415A] text-sm font-persian leading-relaxed font-light max-w-xs">
{companyName} {companyName}
</div> </div>
{/* Logo */} {/* Logo */}

View File

@ -26,17 +26,17 @@ const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
<div className="info-box-content"> <div className="info-box-content">
<div className="info-row"> <div className="info-row">
<div className="info-label">درآمد:</div> <div className="info-label">درآمد:</div>
<div className="info-value revenue">{formatNumber(company?.revenue || 0)}</div> <div className="info-value revenue text-[12px]">{formatNumber(company?.revenue || 0)}</div>
<div className="info-unit">میلیون ریال</div> <div className="info-unit">میلیون ریال</div>
</div> </div>
<div className="info-row"> <div className="info-row">
<div className="info-label">هزینه:</div> <div className="info-label">هزینه:</div>
<div className="info-value cost">{formatNumber(company?.cost || 0)}</div> <div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
<div className="info-unit">میلیون ریال</div> <div className="info-unit">میلیون ریال</div>
</div> </div>
<div className="info-row"> <div className="info-row">
<div className="info-label">ظرفیت:</div> <div className="info-label">ظرفیت:</div>
<div className="info-value capacity">{formatNumber(company?.capacity || 0)}</div> <div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
<div className="info-unit">تن در سال</div> <div className="info-unit">تن در سال</div>
</div> </div>
</div> </div>
@ -60,7 +60,7 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
]; ];
return ( return (
<div className="w-full h-[500px] rounded-xl p-4"> <div className="w-full h-[500px] rounded-xl">
<div dir="ltr" className="company-grid-container"> <div dir="ltr" className="company-grid-container">
{displayCompanies.map((company, index) => { {displayCompanies.map((company, index) => {
const gp = gridPositions.find(v => v.name === company.name) ; const gp = gridPositions.find(v => v.name === company.name) ;
@ -121,9 +121,9 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
border: 1px solid #3F415A; border: 1px solid #3F415A;
border-radius: 10px; border-radius: 10px;
height: max-content; height: max-content;
align-self : center; align-self : center;
justify-self : center; justify-self : center;
padding : .2rem 0 ; padding : .2rem 1.2rem;
background-color: transparent; background-color: transparent;
} }
@ -131,16 +131,15 @@ padding : .2rem 0 ;
.info-box-content { .info-box-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-around; justify-content: center;
} }
.info-row { .info-row {
position : relative; position : relative;
margin: 0rem 1rem; margin: .1rem 0;
display: flex; display: flex;
gap : 1rem; gap : .5rem;
justify-content : space-between; justify-content : space-between;
padding: 0rem .8rem;
direction: rtl; direction: rtl;
&:has(.info-value.revenue) {border-bottom: 1px solid #3AEA83;} &:has(.info-value.revenue) {border-bottom: 1px solid #3AEA83;}
@ -150,15 +149,16 @@ padding : .2rem 0 ;
.info-label { .info-label {
color: #FFFFFF; color: #FFFFFF;
font-size: 12px; font-size: 11px;
font-weight: 400; font-weight: 300;
text-align: right; text-align: right;
margin : auto 0;
} }
.info-value { .info-value {
color: #34D399; color: #34D399;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 500;
text-align: right; text-align: right;
margin-bottom : .5rem; margin-bottom : .5rem;
} }
@ -169,10 +169,10 @@ padding : .2rem 0 ;
.info-unit { .info-unit {
position: absolute; position: absolute;
left: 12px; left: 0;
bottom: 0; bottom: 2px;
color: #9CA3AF; color: #ACACAC;
font-size: 8px; font-size: 6px;
font-weight: 400; font-weight: 400;
} }
`}</style> `}</style>

View File

@ -21,7 +21,7 @@ export function DashboardCustomBarChart({
if (loading) { if (loading) {
return ( return (
<div className="w-full"> <div className="w-full">
<h3 className="text-lg font-bold text-white font-persian mb-4 text-center border-b-2 border-gray-500/20 pb-3"> <h3 className="text-sm font-bold text-white font-persian mb-4 text-right border-b-2 border-gray-500/20 pb-3">
{title} {title}
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
@ -40,7 +40,7 @@ export function DashboardCustomBarChart({
return ( return (
<div className="w-full"> <div className="w-full">
<h3 className="text-lg font-bold text-white font-persian mb-4 text-center border-b-2 border-gray-500/20"> <h3 className="text-sm font-bold text-white font-persian mb-6 py-2 px-4 text-right border-b-2 border-gray-500/20">
{title} {title}
</h3> </h3>
<div className="px-4"> <div className="px-4">
@ -51,19 +51,19 @@ export function DashboardCustomBarChart({
return ( return (
<div key={index} className="relative"> <div key={index} className="relative">
{/* Bar container */} {/* Bar container */}
<div className="relative min-h-6 h-10 rounded-lg overflow-hidden"> <div className="flex-row-reverse items-center gap-2 flex min-h-6 h-10 rounded-lg overflow-hidden">
{/* Animated bar */} {/* Animated bar */}
<div <div
className={`absolute left-0 h-auto gap-2 top-0 ${item.color} rounded-lg transition-all duration-1000 ease-out flex items-center justify-between px-2`} className={`h-auto gap-2 ${item.color} rounded-lg transition-all duration-1000 ease-out flex items-center justify-end px-2`}
style={{ width: `${widthPercentage}%` }} style={{ width: `${widthPercentage}%` }}
> >
<span className="text-white font-bold text-base"> <span className="text-[#3F415A] text-left font-persian font-medium text-sm py-1 w-max">
{formatNumber(item.value)}
</span>
<span className="text-[#3F415A] font-persian font-medium text-sm w-max">
{item.label} {item.label}
</span> </span>
</div> </div>
<span className="text-white font-bold text-base">
{formatNumber(item.value)}
</span>
</div> </div>
</div> </div>
); );

View File

@ -26,7 +26,7 @@ import {
DollarSign, DollarSign,
Minus, Minus,
CheckCircle, CheckCircle,
BookOpen, Book,
} from "lucide-react"; } from "lucide-react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
import { CustomBarChart } from "~/components/ui/custom-bar-chart"; import { CustomBarChart } from "~/components/ui/custom-bar-chart";
@ -42,6 +42,8 @@ import {
} from "recharts"; } from "recharts";
import { ChartContainer } from "~/components/ui/chart"; import { ChartContainer } from "~/components/ui/chart";
import { formatNumber } from "~/lib/utils"; import { formatNumber } from "~/lib/utils";
import { MetricCard } from "~/components/ui/metric-card";
import { BaseCard } from "~/components/ui/base-card";
export function DashboardHome() { export function DashboardHome() {
const [dashboardData, setDashboardData] = useState<any | null>(null); const [dashboardData, setDashboardData] = useState<any | null>(null);
@ -310,60 +312,30 @@ export function DashboardHome() {
return ( return (
<DashboardLayout> <DashboardLayout>
<div className="p-3 pb-0 grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 p-3 pb-0 gap-4">
{/* Top Cards Row - Redesigned to match other components */} {/* Top Cards Row - Redesigned to match other components */}
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3"> <div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
{/* Ideas Card */} {/* Ideas Card */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"> <BaseCard title="ایده‌های فناوری و نوآوری">
<CardContent className="py-4 px-0"> <div className="flex items-center gap-2 justify-center flex-row-reverse">
<div className="flex flex-col justify-between gap-2"> <ChartContainer
<div className="flex justify-between items-center border-b-2 border-gray-500/20 pb-2"> config={chartConfig}
<h3 className="text-lg font-bold text-white font-persian px-6"> className="aspect-square w-[6rem] h-auto"
ایدههای فناوری و نوآوری >
</h3> <RadialBarChart
</div> data={[
<div className="flex items-center gap-2 justify-center flex-row-reverse"> {
<ChartContainer browser: "ideas",
config={chartConfig} visitors:
className="w-full h-full max-h-20 max-w-40" parseFloat(
>
<RadialBarChart
data={[
{
browser: "ideas",
visitors:
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea || "0",
) > 0
? Math.round(
(parseFloat(
dashboardData.topData
?.ongoing_innovation_technology_ideas ||
"0",
) /
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea ||
"1",
)) *
100,
)
: 0,
fill: "green",
},
]}
startAngle={90}
endAngle={
90 +
((parseFloat(
dashboardData.topData dashboardData.topData
?.registered_innovation_technology_idea || "0", ?.registered_innovation_technology_idea || "0",
) > 0 ) > 0
? Math.round( ? Math.round(
(parseFloat( (parseFloat(
dashboardData.topData dashboardData.topData
?.ongoing_innovation_technology_ideas || "0", ?.ongoing_innovation_technology_ideas ||
"0",
) / ) /
parseFloat( parseFloat(
dashboardData.topData dashboardData.topData
@ -372,203 +344,141 @@ export function DashboardHome() {
)) * )) *
100, 100,
) )
: 0) / : 0,
100) * fill: "green",
360 },
} ]}
innerRadius={35} startAngle={90}
outerRadius={55} endAngle={
> 90 +
<PolarGrid ((parseFloat(
gridType="circle" dashboardData.topData
radialLines={false} ?.registered_innovation_technology_idea || "0",
stroke="none" ) > 0
className="first:fill-red-400 last:fill-background" ? Math.round(
polarRadius={[38, 31]} (parseFloat(
/>
<RadialBar
dataKey="visitors"
background
cornerRadius={5}
/>
<PolarRadiusAxis
tick={false}
tickLine={false}
axisLine={false}
>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-lg font-bold"
>
%
{formatNumber(
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea ||
"0",
) > 0
? Math.round(
(parseFloat(
dashboardData.topData
?.ongoing_innovation_technology_ideas ||
"0",
) /
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea ||
"1",
)) *
100,
)
: 0,
)}
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
<div className="font-bold font-persian text-center">
<div className="flex flex-col justify-between items-center gap-2">
<span className="flex font-bold items-center gap-1">
<div className="font-light">ثبت شده :</div>
{formatNumber(
dashboardData.topData
?.registered_innovation_technology_idea || "0",
)}
</span>
<span className="flex items-center gap-1 font-bold">
<div className="font-light">در حال اجرا :</div>
{formatNumber(
dashboardData.topData
?.ongoing_innovation_technology_ideas || "0",
)}
</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Revenue Card */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
<CardContent className="py-4 px-0">
<div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 pb-2">
<h3 className="text-lg font-bold text-white font-persian px-6">
افزایش درآمد مبتنی بر فناوری و نوآوری
</h3>
</div>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-4xl font-bold text-green-400">
{formatNumber(
dashboardData.topData
?.technology_innovation_based_revenue_growth || "0",
)}
</p>
<div className="text-xs text-gray-400 font-persian">
میلیون ریال
</div>
</div>
<span className="text-6xl font-thin text-gray-600">/</span>
<div className="text-center">
<p className="text-4xl font-bold text-green-400">
{formatNumber(
Math.round(
dashboardData.topData dashboardData.topData
?.technology_innovation_based_revenue_growth_percent, ?.ongoing_innovation_technology_ideas || "0",
) || "0", ) /
)} parseFloat(
% dashboardData.topData
</p> ?.registered_innovation_technology_idea ||
<div className="text-xs text-gray-400 font-persian"> "1",
درصد به کل درآمد )) *
</div> 100,
</div> )
</div> : 0) /
100) *
360
}
innerRadius={35}
outerRadius={55}
>
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-red-400 last:fill-[#111628]"
polarRadius={[38, 31]}
/>
<RadialBar
dataKey="visitors"
background
cornerRadius={5}
/>
<PolarRadiusAxis
tick={false}
tickLine={false}
axisLine={false}
>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-lg font-bold"
>
%
{formatNumber(
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea ||
"0",
) > 0
? Math.round(
(parseFloat(
dashboardData.topData
?.ongoing_innovation_technology_ideas ||
"0",
) /
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea ||
"1",
)) *
100,
)
: 0,
)}
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
<div className="font-bold font-persian text-center">
<div className="flex flex-col justify-between items-center gap-2">
<span className="flex font-bold items-center gap-1 text-base">
<div className="font-light text-sm">ثبت شده :</div>
{formatNumber(
dashboardData.topData
?.registered_innovation_technology_idea || "0",
)}
</span>
<span className="flex items-center gap-1 font-bold text-base">
<div className="font-light text-sm">در حال اجرا :</div>
{formatNumber(
dashboardData.topData
?.ongoing_innovation_technology_ideas || "0",
)}
</span>
</div> </div>
</div> </div>
</CardContent> </div>
</Card> </BaseCard>
{/* Revenue Card */}
<MetricCard
title="افزایش درآمد مبتنی بر فناوری و نوآوری"
value={dashboardData.topData?.technology_innovation_based_revenue_growth || "0"}
percentValue={Math.round(dashboardData.topData?.technology_innovation_based_revenue_growth_percent) || "0"}
percentLabel="درصد به کل درآمد"
/>
{/* Cost Reduction Card */} {/* Cost Reduction Card */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"> <MetricCard
<CardContent className="py-4 px-0"> title="کاهش هزینه ها مبتنی بر فناوری و نوآوری"
<div className="flex flex-col justify-between gap-2"> value={Math.round(parseFloat(dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(/,/g, "") || "0") / 1000000)}
<div className="flex justify-between items-center border-b-2 border-gray-500/20 pb-2"> percentValue={Math.round(dashboardData.topData?.technology_innovation_based_cost_reduction_percent) || "0"}
<h3 className="text-lg font-bold text-white font-persian px-6"> percentLabel="درصد به کل هزینه"
کاهش هزینه ها مبتنی بر فناوری و نوآوری />
</h3>
</div>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-4xl font-bold text-green-400">
{formatNumber(
Math.round(
parseFloat(
dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(
/,/g,
"",
) || "0",
) / 1000000,
),
)}
</p>
<div className="text-xs text-gray-400 font-persian">
میلیون ریال
</div>
</div>
<span className="text-6xl font-thin text-gray-600">/</span>
<div className="text-center">
<p className="text-4xl font-bold text-green-400">
{formatNumber(
Math.round(
dashboardData.topData
?.technology_innovation_based_cost_reduction_percent,
) || "0",
)}
%
</p>
<div className="text-xs text-gray-400 font-persian">
درصد به کل هزینه
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Budget Ratio Card */} {/* Budget Ratio Card */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"> <BaseCard title="نسبت تحقق بودجه فناوی و نوآوری">
<CardContent className="py-4 px-0"> <div className="flex items-center gap-2 justify-center flex-row-reverse">
<div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 pb-2">
<h3 className="text-lg font-bold text-white font-persian px-6">
نسبت تحقق بودجه فناوی و نوآوری
</h3>
</div>
<div className="flex items-center gap-2 justify-center flex-row-reverse">
<ChartContainer <ChartContainer
config={chartConfig} config={chartConfig}
className="w-full h-full max-h-20 max-w-40" className="aspect-square w-[6rem] h-auto"
> >
<RadialBarChart <RadialBarChart
data={[ data={[
@ -596,7 +506,7 @@ export function DashboardHome() {
gridType="circle" gridType="circle"
radialLines={false} radialLines={false}
stroke="none" stroke="none"
className="first:fill-red-400 last:fill-background" className="first:fill-red-400 last:fill-[#111628]"
polarRadius={[38, 31]} polarRadius={[38, 31]}
/> />
<RadialBar <RadialBar
@ -643,8 +553,8 @@ export function DashboardHome() {
</ChartContainer> </ChartContainer>
<div className="font-bold font-persian text-center"> <div className="font-bold font-persian text-center">
<div className="flex flex-col justify-between items-center gap-2"> <div className="flex flex-col justify-between items-center gap-2">
<span className="flex font-bold items-center gap-1 mr-auto"> <span className="flex font-bold items-center text-base gap-1 mr-auto">
<div className="font-light">مصوب :</div> <div className="font-light text-sm">مصوب :</div>
{formatNumber( {formatNumber(
Math.round( Math.round(
parseFloat( parseFloat(
@ -656,8 +566,8 @@ export function DashboardHome() {
), ),
)} )}
</span> </span>
<span className="flex items-center gap-1 font-bold mr-auto"> <span className="flex items-center gap-1 text-base font-bold mr-auto">
<div className="font-light">جذب شده :</div> <div className="font-light text-sm">جذب شده :</div>
{formatNumber( {formatNumber(
Math.round( Math.round(
parseFloat( parseFloat(
@ -672,10 +582,8 @@ export function DashboardHome() {
</div> </div>
</div> </div>
</div> </div>
</div> </BaseCard>
</CardContent> </div>
</Card>
</div>
{/* Main Content with Tabs */} {/* Main Content with Tabs */}
<Tabs <Tabs
@ -696,12 +604,12 @@ export function DashboardHome() {
</TabsList> </TabsList>
</div> </div>
<TabsContent value="charts" className="w-ful h-full"> <TabsContent value="charts" className="h-full">
<InteractiveBarChart data={companyChartData} /> <InteractiveBarChart data={companyChartData} />
</TabsContent> </TabsContent>
<TabsContent value="canvas" className="w-ful h-full"> <TabsContent value="canvas" className="w-ful h-full">
<div className="p-4 h-full"> <div className="p-4 h-full w-full">
<D3ImageInfo <D3ImageInfo
companies={ companies={
companyChartData.map((item) => { companyChartData.map((item) => {
@ -735,7 +643,7 @@ export function DashboardHome() {
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"> <Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-center gap-1 px-4"> <div className="flex items-center justify-center gap-1 px-4">
<CardTitle className="text-white text-lg min-w-[120px]"> <CardTitle className="text-white text-sm min-w-[100px]">
شدت فناوری شدت فناوری
</CardTitle> </CardTitle>
<p className="text-base text-left"> <p className="text-base text-left">
@ -757,8 +665,8 @@ export function DashboardHome() {
</Card> </Card>
{/* Program Status */} {/* Program Status */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"> <Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm">
<CardContent className="py-6 px-0"> <CardContent className="py-4 px-0">
<DashboardCustomBarChart <DashboardCustomBarChart
title="وضعیت برنامه‌های فناوری و نوآوری" title="وضعیت برنامه‌های فناوری و نوآوری"
loading={loading} loading={loading}
@ -768,21 +676,21 @@ export function DashboardHome() {
value: parseFloat( value: parseFloat(
dashboardData?.leftData?.executed_project || "0", dashboardData?.leftData?.executed_project || "0",
), ),
color: "bg-green-400", color: "bg-pr-green",
}, },
{ {
label: "در حال اجرا", label: "در حال اجرا",
value: parseFloat( value: parseFloat(
dashboardData?.leftData?.in_progress_project || "0", dashboardData?.leftData?.in_progress_project || "0",
), ),
color: "bg-blue-400", color: "bg-pr-blue",
}, },
{ {
label: "برنامه‌ریزی شده", label: "برنامه‌ریزی شده",
value: parseFloat( value: parseFloat(
dashboardData?.leftData?.planned_project || "0", dashboardData?.leftData?.planned_project || "0",
), ),
color: "bg-red-400", color: "bg-pr-red",
}, },
]} ]}
/> />
@ -790,9 +698,9 @@ export function DashboardHome() {
</Card> </Card>
{/* Publications */} {/* Publications */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"> <Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm">
<CardHeader className="pb-2 border-b-2 border-gray-500/20"> <CardHeader className="pb-2 border-b-2 border-gray-500/20">
<CardTitle className="text-white text-lg"> <CardTitle className="text-white text-sm">
انتشارات فناوری و نوآوری انتشارات فناوری و نوآوری
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -800,10 +708,10 @@ export function DashboardHome() {
<div className="grid grid-cols-2 grid-rows-2 gap-4 justify-center"> <div className="grid grid-cols-2 grid-rows-2 gap-4 justify-center">
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-blue-400" /> <Book className="w-4 h-4 text-blue-400" />
<span className="text-base">کتاب:</span> <span className="text-base">کتاب:</span>
</div> </div>
<span className="text-xl font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.printed_books_count || "0", dashboardData.leftData?.printed_books_count || "0",
)} )}
@ -811,10 +719,10 @@ export function DashboardHome() {
</div> </div>
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-purple-400" /> <Book className="w-4 h-4 text-purple-400" />
<span className="text-base">پتنت:</span> <span className="text-sm">پتنت:</span>
</div> </div>
<span className="text-xl font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.registered_patents_count || "0", dashboardData.leftData?.registered_patents_count || "0",
)} )}
@ -822,10 +730,10 @@ export function DashboardHome() {
</div> </div>
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-yellow-400" /> <Book className="w-4 h-4 text-yellow-400" />
<span className="text-base">گزارش:</span> <span className="text-sm">گزارش:</span>
</div> </div>
<span className="text-xl font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.published_reports_count || "0", dashboardData.leftData?.published_reports_count || "0",
)} )}
@ -833,10 +741,10 @@ export function DashboardHome() {
</div> </div>
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-green-400" /> <Book className="w-4 h-4 text-green-400" />
<span className="text-base">مقاله:</span> <span className="text-sm">مقاله:</span>
</div> </div>
<span className="text-xl font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.printed_articles_count || "0", dashboardData.leftData?.printed_articles_count || "0",
)} )}
@ -847,9 +755,9 @@ export function DashboardHome() {
</Card> </Card>
{/* Promotion */} {/* Promotion */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"> <Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm">
<CardHeader className="pb-2 border-b-2 border-gray-500/20"> <CardHeader className="pb-2 border-b-2 border-gray-500/20">
<CardTitle className="text-white text-lg"> <CardTitle className="text-white text-sm">
ترویج فناوری و نوآوری ترویج فناوری و نوآوری
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -857,10 +765,10 @@ export function DashboardHome() {
<div className="grid grid-cols-2 grid-rows-2 gap-4 justify-center"> <div className="grid grid-cols-2 grid-rows-2 gap-4 justify-center">
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-purple-400" /> <Book className="w-4 h-4 text-purple-400" />
<span className="text-base">کنفرانس:</span> <span className="text-sm">کنفرانس:</span>
</div> </div>
<span className="text-xl font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.attended_conferences_count || "0", dashboardData.leftData?.attended_conferences_count || "0",
)} )}
@ -868,10 +776,10 @@ export function DashboardHome() {
</div> </div>
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-blue-400" /> <Book className="w-4 h-4 text-blue-400" />
<span className="text-base">شرکت در رویداد:</span> <span className="text-sm">شرکت در رویداد:</span>
</div> </div>
<span className="text-xl font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.attended_events_count || "0", dashboardData.leftData?.attended_events_count || "0",
)} )}
@ -879,10 +787,10 @@ export function DashboardHome() {
</div> </div>
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-yellow-400" /> <Book className="w-4 h-4 text-yellow-400" />
<span className="text-base">نمایشگاه:</span> <span className="text-sm">نمایشگاه:</span>
</div> </div>
<span className="text-xl font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.attended_exhibitions_count || "0", dashboardData.leftData?.attended_exhibitions_count || "0",
)} )}
@ -890,10 +798,10 @@ export function DashboardHome() {
</div> </div>
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-green-400" /> <Book className="w-4 h-4 text-green-400" />
<span className="text-base">برگزاری رویداد:</span> <span className="text-sm">برگزاری رویداد:</span>
</div> </div>
<span className="text-xl font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.organized_events_count || "0", dashboardData.leftData?.organized_events_count || "0",
)} )}
@ -903,8 +811,9 @@ export function DashboardHome() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</div> </div>
</DashboardLayout> </DashboardLayout>
); );
} }

View File

@ -42,15 +42,15 @@ export function InteractiveBarChart({
data: CompanyChartDatum[]; data: CompanyChartDatum[];
}) { }) {
return ( return (
<Card className="py-0 bg-transparent mt-20 border-none h-full"> <Card className="py-0 bg-transparent mt-8 border-none h-full">
<CardContent className="px-2 sm:p-6 bg-transparent"> <CardContent className="p-2 bg-transparent">
<ChartContainer config={chartConfig} className="aspect-auto h-96 w-full"> <ChartContainer config={chartConfig} className="aspect-auto h-96">
<BarChart <BarChart
accessibilityLayer accessibilityLayer
data={data} data={data}
margin={{ left: 12, right: 12 }} margin={{ left: 12, right: 12 }}
barGap={15} barGap={25}
barSize={8} barSize={9}
> >
<CartesianGrid vertical={false} stroke="#475569" /> <CartesianGrid vertical={false} stroke="#475569" />
<XAxis <XAxis
@ -59,21 +59,21 @@ export function InteractiveBarChart({
axisLine={false} axisLine={false}
tickMargin={8} tickMargin={8}
minTickGap={32} minTickGap={32}
tick={{ fill: "#94a3b8", fontSize: 12 }} style={{ fill: "#ffffff", fontSize: 16 }}
/> />
<YAxis <YAxis
domain={[0, 100]}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
tickMargin={8} tickMargin={25}
tick={{ fill: "#94a3b8", fontSize: 12 }} style={{ fill: "#ACACAC", fontSize: 11 }}
tickFormatter={(value) => `${formatNumber(Math.round(value))}%`} tickFormatter={(value) => `${formatNumber(Math.round(value))}%`}
/> />
<Bar dataKey="capacity" fill={chartConfig.capacity.color} radius={[8, 8, 0, 0]}> <Bar dataKey="capacity" fill={chartConfig.capacity.color} radius={[8, 8, 0, 0]}>
<LabelList <LabelList
dataKey="capacity" dataKey="capacity"
position="top" position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }} offset={15}
style={{ fill: "#ffffff", fontSize: "16px", fontWeight: "bold" }}
formatter={(v: number) => `${formatNumber(Math.round(v))}%`} formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
/> />
</Bar> </Bar>
@ -81,7 +81,7 @@ export function InteractiveBarChart({
<LabelList <LabelList
dataKey="revenue" dataKey="revenue"
position="top" position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }} style={{ fill: "#ffffff", fontSize: "16px", fontWeight: "bold" }}
formatter={(v: number) => `${formatNumber(Math.round(v))}%`} formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
/> />
</Bar> </Bar>
@ -89,7 +89,7 @@ export function InteractiveBarChart({
<LabelList <LabelList
dataKey="cost" dataKey="cost"
position="top" position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }} style={{ fill: "#ffffff", fontSize: "16px", fontWeight: "bold" }}
formatter={(v: number) => `${formatNumber(Math.round(v))}%`} formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
/> />
</Bar> </Bar>
@ -97,27 +97,27 @@ export function InteractiveBarChart({
</ChartContainer> </ChartContainer>
{/* Legend below chart */} {/* Legend below chart */}
<div className="flex justify-center gap-8 mt-4"> <div className="flex justify-center gap-8 mt-10">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className="w-6 h-2 rounded" className="w-6 h-2 rounded"
style={{ backgroundColor: chartConfig.capacity.color }} style={{ backgroundColor: chartConfig.capacity.color }}
></div> ></div>
<span className="text-sm text-white">{chartConfig.capacity.label}</span> <span className="text-xs text-white">{chartConfig.capacity.label}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className="w-6 h-2 rounded" className="w-6 h-2 rounded"
style={{ backgroundColor: chartConfig.cost.color }} style={{ backgroundColor: chartConfig.cost.color }}
></div> ></div>
<span className="text-sm text-white">{chartConfig.cost.label}</span> <span className="text-xs text-white">{chartConfig.cost.label}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className="w-6 h-2 rounded" className="w-6 h-2 rounded"
style={{ backgroundColor: chartConfig.revenue.color }} style={{ backgroundColor: chartConfig.revenue.color }}
></div> ></div>
<span className="text-sm text-white">{chartConfig.revenue.label}</span> <span className="text-xs text-white">{chartConfig.revenue.label}</span>
</div> </div>
</div> </div>

View File

@ -12,6 +12,7 @@ import {
Zap, Zap,
} from "lucide-react"; } from "lucide-react";
import moment from "moment-jalaali"; import moment from "moment-jalaali";
import { formatNumber } from "~/lib/utils";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
@ -201,12 +202,7 @@ export function DigitalInnovationPage() {
setDetailsDialogOpen(true); setDetailsDialogOpen(true);
}; };
const formatNumber = (value: string | number) => { // ...existing code...
if (!value) return "0";
const numericValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numericValue)) return "0";
return new Intl.NumberFormat("fa-IR").format(numericValue);
};
const statsCards: StatsCard[] = [ const statsCards: StatsCard[] = [
{ {

View File

@ -1,5 +1,6 @@
// import moment from "moment-jalaali"; // import moment from "moment-jalaali";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { formatNumber } from "~/lib/utils";
import { import {
Bar, Bar,
BarChart, BarChart,
@ -182,14 +183,14 @@ export function GreenInnovationPage() {
useState<GreenInnovationData | null>(null); useState<GreenInnovationData | null>(null);
const [recycleParams, setRecycleParams] = useState<RecycleParams>({ const [recycleParams, setRecycleParams] = useState<RecycleParams>({
water: { water: {
icon: <Key className="text-emerald-400" size={"18px"} />, icon: <Key className="text-success" size={"18px"} />,
label: "آب", label: "آب",
value: 0, value: 0,
suffix: "لیتر", suffix: "لیتر",
percent: 0, percent: 0,
}, },
food: { food: {
icon: <Sparkle className="text-emerald-400" size={"18px"} />, icon: <Sparkle className="text-success" size={"18px"} />,
label: "خوراک", label: "خوراک",
value: 0, value: 0,
suffix: "تن", suffix: "تن",
@ -197,14 +198,14 @@ export function GreenInnovationPage() {
}, },
power: { power: {
icon: <Zap className="text-emerald-400" size={"18px"} />, icon: <Zap className="text-success" size={"18px"} />,
label: "برق", label: "برق",
value: 0, value: 0,
suffix: "میلیون مگاوات", suffix: "میلیون مگاوات",
percent: 0, percent: 0,
}, },
oil: { oil: {
icon: <Flame className="text-emerald-400" size={"18px"} />, icon: <Flame className="text-success" size={"18px"} />,
label: "سوخت", label: "سوخت",
value: 0, value: 0,
suffix: "متر مربع", suffix: "متر مربع",
@ -256,11 +257,7 @@ export function GreenInnovationPage() {
setDetailsDialogOpen(true); setDetailsDialogOpen(true);
}; };
const formatNumber = (value: string | number) => { // ...existing code...
const numericValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numericValue)) return "0";
return new Intl.NumberFormat("fa-IR").format(numericValue);
};
const fetchProjects = async (reset = false) => { const fetchProjects = async (reset = false) => {
if (fetchingRef.current) { if (fetchingRef.current) {

View File

@ -1,3 +1,4 @@
// ...existing code...
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
@ -39,7 +40,7 @@ import {
XAxis, XAxis,
} from "recharts"; } from "recharts";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { formatCurrency } from "~/lib/utils"; import { formatCurrency, formatNumber } from "~/lib/utils";
import DashboardLayout from "../layout"; import DashboardLayout from "../layout";
interface innovationBuiltInDate { interface innovationBuiltInDate {
@ -275,12 +276,7 @@ export function InnovationBuiltInsidePage() {
}, 500); }, 500);
}; };
const formatNumber = (value: string | number) => { // ...existing code...
if (!value) return "0";
const numericValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numericValue)) return "0";
return new Intl.NumberFormat("fa-IR").format(numericValue);
};
const fetchProjects = async (reset = false) => { const fetchProjects = async (reset = false) => {
if (fetchingRef.current) { if (fetchingRef.current) {

View File

@ -552,15 +552,15 @@ export function ManageIdeasTechPage() {
const getImportanceColor = (importance: string) => { const getImportanceColor = (importance: string) => {
switch (importance?.toLowerCase()) { switch (importance?.toLowerCase()) {
case "تایید شده": case "تایید شده":
return "#3AEA83"; // سبز return "var(--success)"; // سبز
case "در حال بررسی": case "در حال بررسی":
return "#69C8EA"; // آبی return "var(--info)"; // آبی
case "رد شده": case "رد شده":
return "#F76276"; // قرمز return "var(--destructive)"; // قرمز
case "اجرا شده": case "اجرا شده":
return "#FBBF24"; // زرد/نارنجی return "var(--warning)"; // زرد/نارنجی
default: default:
return "#6B7280"; // خاکستری پیش‌فرض return "var(--muted)"; // خاکستری پیش‌فرض
} }
}; };
@ -574,9 +574,9 @@ export function ManageIdeasTechPage() {
case "remaining_time": { case "remaining_time": {
const days = calculateRemainingDays(item.end_date); const days = calculateRemainingDays(item.end_date);
if (days == null) { if (days == null) {
return <span className="text-gray-300">-</span>; return <span className="text-muted-foreground">-</span>;
} }
const color = days > 0 ? "#3AEA83" : days < 0 ? "#F76276" : undefined; const color = days > 0 ? "var(--success)" : days < 0 ? "var(--destructive)" : undefined;
return ( return (
<span <span
dir="ltr" dir="ltr"
@ -589,32 +589,32 @@ export function ManageIdeasTechPage() {
} }
case "idea_income": case "idea_income":
return ( return (
<span className="font-medium text-emerald-400"> <span className="font-medium text-success">
{formatCurrency(String(value))} {formatCurrency(String(value))}
</span> </span>
); );
case "personnel_number": case "personnel_number":
// case "idea_originality": // case "idea_originality":
return ( return (
<span className="text-gray-300"> <span className="text-muted-foreground">
{toPersianDigits(value as any)}{" "} {toPersianDigits(value as any)}{" "}
</span> </span>
); );
case "idea_registration_date": case "idea_registration_date":
return ( return (
<span className="text-gray-300">{formatDate(String(value))}</span> <span className="text-muted-foreground">{formatDate(String(value))}</span>
); );
case "project_no": case "project_no":
return ( return (
<Badge <Badge
variant="outline" variant="outline"
className="font-mono text-emerald-400 border-emerald-500/50" className="font-mono text-success border-success/50"
> >
{String(value)} {String(value)}
</Badge> </Badge>
); );
case "idea_title": case "idea_title":
return <span className="font-medium text-white">{String(value)}</span>; return <span className="font-medium text-foreground">{String(value)}</span>;
case "idea_status": case "idea_status":
return ( return (
<Badge <Badge
@ -631,7 +631,7 @@ export function ManageIdeasTechPage() {
); );
default: default:
return ( return (
<span className="text-gray-300"> <span className="text-muted-foreground">
{(value && String(value)) || "-"} {(value && String(value)) || "-"}
</span> </span>
); );
@ -648,12 +648,12 @@ export function ManageIdeasTechPage() {
<CardContent className="p-0"> <CardContent className="p-0">
<div className="relative"> <div className="relative">
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(100vh-200px)]"> <Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(100vh-200px)]">
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]"> <TableHeader className="sticky top-0 z-50 bg-muted">
<TableRow className="bg-[#3F415A]"> <TableRow className="bg-muted">
{columns.map((column) => ( {columns.map((column) => (
<TableHead <TableHead
key={column.key} key={column.key}
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium bg-[#3F415A] sticky top-0 z-20" className="text-right font-persian whitespace-nowrap text-foreground font-medium bg-muted sticky top-0 z-20"
style={{ width: column.width }} style={{ width: column.width }}
> >
{column.sortable ? ( {column.sortable ? (
@ -690,12 +690,12 @@ export function ManageIdeasTechPage() {
{columns.map((column) => ( {columns.map((column) => (
<TableCell <TableCell
key={column.key} key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2" className="text-right whitespace-nowrap border-success/20 py-1 px-2"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" /> <div className="w-2.5 h-2.5 bg-muted rounded-full animate-pulse" />
<div <div
className="h-2.5 bg-gray-600 rounded animate-pulse" className="h-2.5 bg-muted rounded animate-pulse"
style={{ width: `${Math.random() * 60 + 40}%` }} style={{ width: `${Math.random() * 60 + 40}%` }}
/> />
</div> </div>
@ -709,7 +709,7 @@ export function ManageIdeasTechPage() {
colSpan={columns.length} colSpan={columns.length}
className="text-center py-8" className="text-center py-8"
> >
<span className="text-gray-400 font-persian"> <span className="text-muted-foreground font-persian">
هیچ پروژهای یافت نشد هیچ پروژهای یافت نشد
</span> </span>
</TableCell> </TableCell>
@ -723,7 +723,7 @@ export function ManageIdeasTechPage() {
{columns.map((column) => ( {columns.map((column) => (
<TableCell <TableCell
key={column.key} key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2" className="text-right whitespace-nowrap border-success/20 py-1 px-2"
> >
{renderCellContent(project, column)} {renderCellContent(project, column)}
</TableCell> </TableCell>
@ -740,8 +740,8 @@ export function ManageIdeasTechPage() {
{loadingMore && ( {loadingMore && (
<div className="flex items-center justify-center py-1"> <div className="flex items-center justify-center py-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" /> <RefreshCw className="w-4 h-4 animate-spin text-success" />
<span className="font-persian text-gray-300 text-xs"></span> <span className="font-persian text-muted-foreground text-xs"></span>
</div> </div>
</div> </div>
)} )}
@ -749,8 +749,8 @@ export function ManageIdeasTechPage() {
</CardContent> </CardContent>
{/* Footer */} {/* Footer */}
<div className="p-4 bg-gray-700/50"> <div className="p-4 bg-muted/50">
<div className="flex items-center justify-between text-sm text-gray-300 font-persian"> <div className="flex items-center justify-between text-sm text-muted-foreground font-persian">
<span>کل پروژهها: {formatNumber(actualTotalCount)}</span> <span>کل پروژهها: {formatNumber(actualTotalCount)}</span>
</div> </div>
</div> </div>

View File

@ -13,6 +13,7 @@ import {
} from "~/components/ui/table"; } from "~/components/ui/table";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { formatCurrency } from "~/lib/utils"; import { formatCurrency } from "~/lib/utils";
import { formatNumber } from "~/lib/utils";
import { DashboardLayout } from "../layout"; import { DashboardLayout } from "../layout";
interface ProjectData { interface ProjectData {
@ -352,12 +353,7 @@ export function ProjectManagementPage() {
fetchTotalCount(); fetchTotalCount();
}; };
const formatNumber = (value: string | number) => { // ...existing code...
if (value === undefined || value === null || value === "") return "0";
const numericValue = typeof value === "string" ? Number(value) : value;
if (Number.isNaN(numericValue)) return "0";
return new Intl.NumberFormat("fa-IR").format(numericValue as number);
};
const toPersianDigits = (input: string | number): string => { const toPersianDigits = (input: string | number): string => {
const str = String(input); const str = String(input);

View File

@ -20,6 +20,7 @@ import apiService from "~/lib/api";
import { Skeleton } from "~/components/ui/skeleton"; import { Skeleton } from "~/components/ui/skeleton";
import { formatNumber } from "~/lib/utils"; import { formatNumber } from "~/lib/utils";
import { ChartContainer } from "../ui/chart"; import { ChartContainer } from "../ui/chart";
import { TruncatedText } from "../ui/truncatedText";
interface StrategicAlignmentData { interface StrategicAlignmentData {
strategic_theme: string; strategic_theme: string;
@ -131,7 +132,7 @@ export function StrategicAlignmentPopup({
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full max-w-4xl bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] text-white border-none"> <DialogContent className="w-full max-w-4xl bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] text-white border-none">
<DialogHeader className="border-b-3 mb-10 py-2 w-full pb-4 border-b-2 border-gray-500/20"> <DialogHeader className="border-b-3 mb-10 py-2 w-full pb-4 border-b-2 border-gray-500/20">
<DialogTitle className="ml-auto ">میزان انطباق راهبردی</DialogTitle> <DialogTitle className="ml-auto text-sm text-white ">میزان انطباق راهبردی</DialogTitle>
</DialogHeader> </DialogHeader>
{loading ? ( {loading ? (
@ -153,23 +154,39 @@ export function StrategicAlignmentPopup({
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
tickMargin={10} tickMargin={10}
tick={{ fill: "#94a3b8", fontSize: 12 }} interval={0}
style={{ fill: "#94a3b8", fontSize: 14 }}
tick={(props) => {
const { x, y, payload } = props;
return (
<g transform={`translate(${x},${y})`}>
<foreignObject width={80} height={20} x={-45} y={0}>
<TruncatedText
maxWords={2}
text={payload.value}
/>
</foreignObject>
</g>
);
}}
/> />
<YAxis <YAxis
domain={[0, 100]} domain={[0, 100]}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
tickMargin={8} tickMargin={20}
tick={{ fill: "#94a3b8", fontSize: 12 }} tick={{ fill: "#94a3b8", fontSize: 12 }}
tickFormatter={(value) => tickFormatter={(value) =>
`${formatNumber(Math.round(value))}%` `${formatNumber(Math.round(value))}`
} }
label={{ label={{
value: "تعداد برنامه ها" , value: "تعداد برنامه ها" ,
angle: -90, angle: -90,
position: "insideLeft", position: "insideLeft",
fill: "#94a3b8", fill: "#94a3b8",
fontSize: 14, fontSize: 11,
offset: 0, offset: 0,
dy: 0, dy: 0,
style: { textAnchor: "middle" }, style: { textAnchor: "middle" },
@ -183,12 +200,14 @@ export function StrategicAlignmentPopup({
<LabelList <LabelList
dataKey="percentage" dataKey="percentage"
position="top" position="top"
offset={15}
style={{ style={{
fill: "#ffffff", fill: "#ffffff",
fontSize: "12px", fontSize: "16px",
fontWeight: "bold", fontWeight: "bold",
}} }}
formatter={(v: number) => `${formatNumber(Math.round(v))}%`} formatter={(v: number) => `${formatNumber(Math.round(v))}`}
/> />
</Bar> </Bar>

View File

@ -0,0 +1,33 @@
import { useId } from "react";
interface CheckboxProps {
checked: boolean;
disabled?: boolean;
onChange?: (checked: boolean) => void;
className?: string;
id ?:string;
}
export default function CustomCheckBox({
checked,
disabled = false,
onChange,
className = "",
id
}: CheckboxProps) {
const handleChange = (e: any) => {
onChange?.(e.target.checked);
};
return (
<input
id={id}
type="checkbox"
checked={checked}
disabled={disabled}
onChange={handleChange}
className={`form-checkbox ${className}`}
/>
);
}

View File

@ -0,0 +1,42 @@
import { cn } from "~/lib/utils";
import { Card, CardContent, CardHeader, CardTitle } from "./card";
interface BaseCardProps {
title?: string;
className?: string;
headerClassName?: string;
contentClassName?: string;
children: React.ReactNode;
withHeader?: boolean;
}
export function BaseCard({
title,
className,
headerClassName,
contentClassName,
children,
withHeader = false,
}: BaseCardProps) {
return (
<Card
className={cn(
"bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm py-4 grid items-center",
className
)}
>
{withHeader && title ? (
<CardHeader className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}>
<CardTitle className="text-white text-sm text-right font-persian px-4">{title}</CardTitle>
</CardHeader>
) : title ? (
<div className="border-b-2 border-gray-500/20 pb-2">
<h3 className="text-sm font-bold text-white text-right font-persian px-4">{title}</h3>
</div>
) : null}
<CardContent className={cn("py-2 px-4", contentClassName)}>
{children}
</CardContent>
</Card>
);
}

View File

@ -44,8 +44,8 @@ const DialogContent = React.forwardRef<
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> <DialogPrimitive.Close className="absolute left-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4 cursor-pointer" /> <X className="h-6 w-6 cursor-pointer" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>

View File

@ -3,6 +3,7 @@ import { cn } from "~/lib/utils";
import { Eye, EyeOff, AlertCircle, CheckCircle2 } from "lucide-react"; import { Eye, EyeOff, AlertCircle, CheckCircle2 } from "lucide-react";
import { Input } from "./input"; import { Input } from "./input";
import { Label } from "./label"; import { Label } from "./label";
import CustomCheckbox from "./CustomCheckBox";
interface BaseFieldProps { interface BaseFieldProps {
label?: string; label?: string;
@ -65,12 +66,6 @@ export function TextField({
)} )}
<div className="relative"> <div className="relative">
{leftIcon && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground">
{leftIcon}
</div>
)}
<Input <Input
id={id} id={id}
type={type} type={type}
@ -82,39 +77,14 @@ export function TextField({
maxLength={maxLength} maxLength={maxLength}
minLength={minLength} minLength={minLength}
className={cn( className={cn(
"w-full h-12 px-4 font-persian text-right transition-all duration-200", "w-full h-12 outline-none bg-white text-base text-[#5F6284] px-4 font-persian text-right transition-all duration-200",
leftIcon && "pr-10",
(rightIcon || hasError || hasSuccess) && "pl-10",
hasError &&
"border-destructive focus:border-destructive focus:ring-destructive/20",
hasSuccess &&
"border-green-500 focus:border-green-500 focus:ring-green-500/20",
className, className,
)} )}
style={{boxShadow : "none"}}
/> />
{(rightIcon || hasError || hasSuccess) && (
<div className="absolute left-3 top-1/2 transform -translate-y-1/2">
{hasError ? (
<AlertCircle className="h-4 w-4 text-destructive" />
) : hasSuccess ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
rightIcon && (
<span className="text-muted-foreground">{rightIcon}</span>
)
)}
</div>
)}
</div> </div>
{error && (
<p className="text-sm text-destructive font-persian flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
{error}
</p>
)}
{helper && !error && ( {helper && !error && (
<p className="text-sm text-muted-foreground font-persian">{helper}</p> <p className="text-sm text-muted-foreground font-persian">{helper}</p>
)} )}
@ -217,17 +187,19 @@ export function PasswordField({
autoComplete={autoComplete} autoComplete={autoComplete}
minLength={minLength} minLength={minLength}
className={cn( className={cn(
"w-full h-12 px-4 pl-10 font-persian text-right transition-all duration-200", "w-full h-12 px-4 pl-10 bg-white text-base text-[#5F6284] font-persian text-right transition-all duration-200",
hasError && hasError &&
"border-destructive focus:border-destructive focus:ring-destructive/20", "border-destructive focus:border-destructive focus:ring-destructive/20",
className, className,
)} )}
style={{boxShadow : "none"}}
/> />
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors" className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-black transition-colors"
tabIndex={-1} tabIndex={-1}
> >
{showPassword ? ( {showPassword ? (
@ -318,26 +290,17 @@ export function CheckboxField({
return ( return (
<div className={cn("space-y-2", containerClassName)}> <div className={cn("space-y-2", containerClassName)}>
<div className="flex items-center gap-2"> <div className="flex flex-row-reverse items-center gap-2">
<input <CustomCheckbox
id={id} id={id}
type="checkbox"
checked={checked} checked={checked}
onChange={(e) => onChange(e.target.checked)} onChange={onChange}
disabled={disabled} />
className={cn(
sizes[size],
"text-[var(--color-login-primary)] bg-background border-input rounded focus:ring-[var(--color-login-primary)] focus:ring-2 accent-[var(--color-login-primary)] transition-all duration-200",
disabled && "opacity-50 cursor-not-allowed",
error && "border-destructive focus:ring-destructive",
className,
)}
/>
{label && ( {label && (
<Label <Label
htmlFor={id} htmlFor={id}
className={cn( className={cn(
"text-sm font-persian cursor-pointer", "text-sm font-persian font-light text-white cursor-pointer",
error ? "text-destructive" : "text-foreground", error ? "text-destructive" : "text-foreground",
disabled && "opacity-50 cursor-not-allowed", disabled && "opacity-50 cursor-not-allowed",
required && required &&

View File

@ -1,39 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { FunnelChart } from './funnel-chart';
const mockData = [
{ name: "تعداد کل", value: 250, label: "تعداد کل" },
{ name: "نمونه موفق", value: 130, label: "نمونه موفق" },
{ name: "محصولات موفق", value: 70, label: "محصولات موفق" },
{ name: "بهبود یا تغییر موفق", value: 80, label: "بهبود یا تغییر موفق" },
{ name: "محصول جدید", value: 50, label: "محصول جدید" },
];
describe('FunnelChart', () => {
it('renders funnel chart with correct data', () => {
render(<FunnelChart data={mockData} title="قيف فرآیند پروژه ها" />);
expect(screen.getByText('قيف فرآیند پروژه ها')).toBeInTheDocument();
expect(screen.getByText('۱۰۰%')).toBeInTheDocument();
expect(screen.getByText('۲۵%')).toBeInTheDocument();
expect(screen.getByText('ابتدا فرآیند')).toBeInTheDocument();
expect(screen.getByText('انتها فرآیند')).toBeInTheDocument();
});
it('displays funnel data values correctly', () => {
render(<FunnelChart data={mockData} />);
expect(screen.getByText('۲۵۰')).toBeInTheDocument();
expect(screen.getByText('۱۳۰')).toBeInTheDocument();
expect(screen.getByText('۷۰')).toBeInTheDocument();
expect(screen.getByText('۸۰')).toBeInTheDocument();
expect(screen.getByText('۵۰')).toBeInTheDocument();
});
it('renders without title when not provided', () => {
render(<FunnelChart data={mockData} />);
expect(screen.queryByText('قيف فرآیند پروژه ها')).not.toBeInTheDocument();
});
});

View File

@ -0,0 +1,48 @@
import { formatNumber } from "~/lib/utils";
import { BaseCard } from "./base-card";
interface MetricCardProps {
title: string;
value: string | number;
percentValue?: string | number;
valueLabel?: string;
percentLabel?: string;
}
export function MetricCard({
title,
value,
percentValue,
valueLabel = "میلیون ریال",
percentLabel = "درصد به کل",
}: MetricCardProps) {
return (
<BaseCard title={title}>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl font-bold text-green-400">
{formatNumber(value)}
</p>
<div className="text-xs text-gray-400 font-persian">
{valueLabel}
</div>
</div>
{percentValue !== undefined && (
<>
<span className="text-5xl font-thin text-gray-600">/</span>
<div className="text-center">
<p className="text-3xl font-bold text-green-400">
{formatNumber(percentValue)}%
</p>
<div className="text-xs text-gray-400 font-persian">
{percentLabel}
</div>
</div>
</>
)}
</div>
</div>
</BaseCard>
);
}

View File

@ -0,0 +1,31 @@
import * as React from "react"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"
interface TruncatedTextProps {
text: string
maxWords?: number
}
export function TruncatedText({ text, maxWords = 4 }: TruncatedTextProps) {
const words = text.trim().split(/\s+/)
console.log(words)
const shouldTruncate = words.length > maxWords
const displayText = shouldTruncate ? words.slice(0, maxWords).join(" ") + " ..." : text
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className={`${words.length >= 4 ? "cursor-help" : ""} text-foreground`}>
{displayText}
</span>
</TooltipTrigger>
{shouldTruncate && (
<TooltipContent className="max-w-xs">
{text}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)
}

View File

@ -1,9 +1,16 @@
module.exports = { module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: { theme: {
extend: { extend: {
colors: { colors: {
green: '#3AEA83',
blue: '#69C8EA',
red: '#F76276',
} }
}, },
}, },
plugins: [],
}; };