Compare commits
3 Commits
abb8bcc9e4
...
005b3e3e6d
| Author | SHA1 | Date | |
|---|---|---|---|
| 005b3e3e6d | |||
| f1dd7c38ad | |||
| 1f68770efc |
62
app/app.css
62
app/app.css
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"/>}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 gird-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-background"
|
||||||
|
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={[
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 w-full">
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -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[] = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
33
app/components/ui/CustomCheckBox.tsx
Normal file
33
app/components/ui/CustomCheckBox.tsx
Normal 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}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
app/components/ui/base-card.tsx
Normal file
42
app/components/ui/base-card.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 &&
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
48
app/components/ui/metric-card.tsx
Normal file
48
app/components/ui/metric-card.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
app/components/ui/truncatedText.tsx
Normal file
31
app/components/ui/truncatedText.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user