Compare commits

...

20 Commits

Author SHA1 Message Date
mahmoodsht
e297522e5c آماده سازی برای دو پتروشیمی دیگر 2025-11-03 12:48:22 +03:30
mahmoodsht
69e702d368 پتروشیمی آپادانا 2025-11-02 16:50:52 +03:30
mahmoodsht
46c81cf8ea اصلاحات درخواستی 2025-11-01 19:29:09 +03:30
MehrdadAdabi
d12c3f1108 chore:clean up function 2025-10-25 19:04:33 +03:30
MehrdadAdabi
5ba67aa240 fix: change download excel 2025-10-25 19:03:19 +03:30
MehrdadAdabi
344d2a36f4 fix: change excel package 2025-10-24 22:13:49 +03:30
MehrdadAdabi
66457e9ef6 completed download excel file on project-managment page 2025-10-24 11:53:28 +03:30
MehrdadAdabi
ec461d178b change format number to fa 2025-10-21 16:23:30 +03:30
mahmoodsht
5bb9776ef0 اصلاح مکان کادرها و متن ها 2025-10-18 13:19:13 +03:30
mahmoodsht
a45ddda0f3 مخفی سازی موقت بخش های غیر فعال 2025-10-18 12:19:34 +03:30
MehrdadAdabi
ac1081cdd2 fix: bugs and chnage some logic 2025-10-17 20:05:59 +03:30
mahmoodsht
c31eba3c19 توسعه نوآوری در فرآیند 2025-10-17 17:19:54 +03:30
MehrdadAdabi
5d550217db Merge branch 'main' of http://git.sepehrdata.com/Saeed0920/inogen 2025-10-14 16:39:57 +03:30
MehrdadAdabi
b1db9e8685 fix: change date-picker logic 2025-10-14 16:39:29 +03:30
0fed828d77 update: the d3 and dashboard-custom-bar 2025-10-14 16:19:08 +03:30
mahmoodsht
7603703fa5 شماتیک پیش فرض 2025-10-14 11:13:05 +03:30
MehrdadAdabi
0fd6e4c78d fix: change next or prev btn 2025-10-13 18:44:38 +03:30
MehrdadAdabi
082856170a Merge branch 'main' of http://git.sepehrdata.com/Saeed0920/inogen 2025-10-13 18:32:35 +03:30
MehrdadAdabi
0dd1fe2ec2 Merge branch 'main' of http://git.sepehrdata.com/Saeed0920/inogen 2025-10-13 02:44:54 +03:30
8749cebe7c fix:the style in dashboard and remove the default token! 2025-10-12 14:02:07 +03:30
32 changed files with 6748 additions and 540 deletions

View File

@ -160,9 +160,9 @@ This document describes the exact implementation of the login page based on the
onChange={(e) => setRememberMe(e.target.checked)} onChange={(e) => setRememberMe(e.target.checked)}
className="w-4 h-4 text-[#4FD1C7] bg-white border-gray-300 rounded focus:ring-[#4FD1C7] focus:ring-2 accent-[#4FD1C7]" className="w-4 h-4 text-[#4FD1C7] bg-white border-gray-300 rounded focus:ring-[#4FD1C7] focus:ring-2 accent-[#4FD1C7]"
/> />
<Label htmlFor="remember" className="text-white text-sm font-persian cursor-pointer"> // <Label htmlFor="remember" className="text-white text-sm font-persian cursor-pointer">
همیشه متصل بمانم // همیشه متصل بمانم
</Label> // </Label>
</div> </div>
{/* Submit Button */} {/* Submit Button */}

View File

@ -176,7 +176,7 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
/> />
{/* Remember Me Checkbox */} {/* Remember Me Checkbox */}
<div className="flex justify-end"> {/* <div className="flex justify-end">
<CheckboxField <CheckboxField
id="remember" id="remember"
label="همیشه متصل بمان" label="همیشه متصل بمان"
@ -185,7 +185,7 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
disabled={isLoading} disabled={isLoading}
size="md" size="md"
/> />
</div> </div> */}
{/* Login Button */} {/* Login Button */}
<Button <Button
@ -212,7 +212,9 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
{/* Right Side - Branding */} {/* Right Side - Branding */}
<LoginSidebar> <LoginSidebar>
<LoginBranding <LoginBranding
brandName="پتروشیمی بندر امام" brandName="پتروشیمی آپادانا"
// brandName="پتروشیمی نوری"
// brandName="پتروشیمی بندر امام"
engSub="Inception by Fara" engSub="Inception by Fara"
companyName="توسعه‌یافته توسط شرکت رهپویان دانش و فناوری فرا" companyName="توسعه‌یافته توسط شرکت رهپویان دانش و فناوری فرا"
logo={<img src="/brand2.svg"/>} logo={<img src="/brand2.svg"/>}

View File

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
interface LoginLayoutProps { interface LoginLayoutProps {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
@ -106,14 +107,25 @@ export function LoginBranding({
}: LoginBrandingProps) { }: LoginBrandingProps) {
return ( return (
<> <>
{/* Top Logo */} <div className="flex justify-end">
<div className="flex justify-end"> <div className="text-slate-800 font-persian">
<div className="text-slate-800 font-persian"> <div className="text-lg font-bold leading-tight">
<div className="text-lg font-bold leading-tight"> <img
<img src="/brand.svg" /> src="/brand.svg?v=1"
</div> alt="Brand Logo"
</div> className="w-auto h-16" // اضافه کردن سایز مشخص
</div> onError={(e) => {
e.target.style.display = 'none';
console.log('Image failed to load');
}}
/>
</div>
</div>
</div>
{/* 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">

View File

@ -1,3 +1,6 @@
//این فایل مخصوص
//شماتیک آپادانا
import React from "react"; import React from "react";
import { formatNumber } from "~/lib/utils"; import { formatNumber } from "~/lib/utils";
@ -8,10 +11,10 @@ export type CompanyInfo = {
costReduction: number; costReduction: number;
revenue?: number; revenue?: number;
capacity?: number; capacity?: number;
costI : number, costI: number;
capacityI : number, capacityI: number;
revenueI : number, revenueI: number;
cost : number | string, cost: number | string;
}; };
export type D3ImageInfoProps = { export type D3ImageInfoProps = {
@ -20,9 +23,11 @@ export type D3ImageInfoProps = {
height?: number; height?: number;
}; };
const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => { const InfoBox = ({ company, style }: { company: CompanyInfo; style: any }) => {
// const hideCapacity = company.name === "واحد 300"; // اگر واحد 300 بود ظرفیت مخفی شود
const hideCapacity = false;
return ( return (
<div className={`info-box`} style={style}> <div className={`info-box`} style={style}>
<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>
@ -31,58 +36,78 @@ const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
</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 text-[12px]">{formatNumber(company?.cost || 0)}</div> {hideCapacity ? (
<div className="info-value cost2 text-[12px]">{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"> {!hideCapacity && (
<div className="info-label">ظرفیت:</div> <div className="info-row">
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div> <div className="info-label">ظرفیت:</div>
<div className="info-unit">تن در سال</div> <div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
</div> <div className="info-unit">تن در سال</div>
</div>
)}
</div> </div>
</div> </div>
); );
}; };
export function D3ImageInfo({ companies }: D3ImageInfoProps) { export function D3ImageInfo({ companies }: D3ImageInfoProps) {
// Ensure we have exactly 6 companies // واحدهای جدید - 4 واحد
const displayCompanies = companies; const sample = [
{ id: "واحد 100", name: "واحد 100", imageUrl: "/abniro.png" },
// Positions inside a 5x4 grid (col, row) { id: "واحد 200", name: "واحد 200", imageUrl: "/besparan.png" },
// Layout keeps same visual logic: left/middle/right on two bands with spacing grid around { id: "واحد 300", name: "واحد 300", imageUrl: "/khwarazmi.png" },
const gridPositions = [ { id: "واحد 400", name: "واحد 400", imageUrl: "/faravash1.png" }
{ col: 2, row: 2 , colI : 1 , rowI : 2 , name : "بسپاران"}, // left - top band
{ col: 3, row: 2 , colI : 3 , rowI : 1 , name : "خوارزمی"}, // middle top (image sits in row 2, info box goes to row 1)
{ col: 4, row: 2 ,colI : 5 , rowI : 2 , name : "فراورش 1"}, // right - top band
{ col: 2, row: 3 , colI : 1 , rowI : 3 , name : "کیمیا"}, // left - bottom band
{ col: 3, row: 3 , colI : 3, rowI : 4 , name : "آب نیرو"}, // middle bottom (image sits in row 3, info box goes to row 4)
{ col: 4, row: 3 , colI : 5 , rowI : 3 , name : "فراورش 2"}, // right - bottom band
]; ];
const merged = sample.map(company => {
const found = companies.find(item => item.id === company.id);
return found
? found
: { ...company, cost: 0, capacity: 0, revenue: 0, costReduction: 0, costI: 0, capacityI: 0, revenueI: 0 };
});
const displayCompanies = merged;
console.log(displayCompanies);
// موقعیت‌های جدید برای چیدمان لوزی شکل (3 ردیف - 1-2-1)
// گرید 5x4 نگه داشته شده اما موقعیت‌ها تغییر کرده
const gridPositions = [
{ col: 2, row: 1, colI: 1, rowI: 1, name: "واحد 100" }, // ردیف اول - ستون اول
{ col: 4, row: 1, colI: 5, rowI: 1, name: "واحد 200" }, // ردیف اول - ستون دوم
{ col: 2, row: 3, colI: 1, rowI: 3, name: "واحد 300" }, // ردیف دوم - ستون اول
{ col: 4, row: 3, colI: 5, rowI: 3, name: "واحد 400" }, // ردیف دوم - ستون دوم
];
return ( return (
<div className="w-full h-[500px] rounded-xl"> <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);
return ( return (
<> <React.Fragment key={company.id}>
<div <div
key={company.id} className={`company-item`}
className={`company-item`} style={{ gridColumn: gp?.col, gridRow: gp?.row }}
style={{ gridColumn: gp.col, gridRow: gp.row }} >
> <div className="company-image-container">
<div className="company-image-containe"> <img
<img src={company.imageUrl}
src={company.imageUrl} alt={company.name}
alt={company.name} className="company-image"
className="company-image" />
/> </div>
{company.name}
</div> </div>
<InfoBox company={company} style={{ gridColumn: gp?.colI, gridRow: gp?.rowI }} />
{company.name} </React.Fragment>
</div> );
<InfoBox company={company} key={index +10} style={{ gridColumn: gp?.colI , gridRow: gp?.rowI }} />
</>);
})} })}
</div> </div>
@ -114,20 +139,20 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
.company-image { .company-image {
object-fit: contain; object-fit: contain;
height : 100px; height: 100px;
} }
.info-box { .info-box {
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 1.2rem; padding: .2rem 1.2rem;
min-width: 8rem;
background-color: transparent; background-color: transparent;
} }
.info-box-content { .info-box-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -135,16 +160,20 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
} }
.info-row { .info-row {
position : relative; position: relative;
margin: .1rem 0; margin: .1rem 0;
display: flex; display: flex;
gap : .5rem; gap: .5rem;
justify-content : space-between; justify-content: space-between;
direction: rtl; direction: rtl;
}
&:has(.info-value.revenue) {border-bottom: 1px solid #3AEA83;} .info-row:has(.info-value.revenue) {
&:has(.info-value.cost) {border-bottom: 1px solid #F76276;} border-bottom: 1px solid #3AEA83;
}
.info-row:has(.info-value.cost) {
border-bottom: 1px solid #F76276;
} }
.info-label { .info-label {
@ -152,7 +181,7 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
font-size: 11px; font-size: 11px;
font-weight: 300; font-weight: 300;
text-align: right; text-align: right;
margin : auto 0; margin: auto 0;
} }
.info-value { .info-value {
@ -160,11 +189,12 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
text-align: right; text-align: right;
margin-bottom : .5rem; margin-bottom: .5rem;
} }
.info-value.revenue { color: #fff;} .info-value.revenue { color: #fff; }
.info-value.cost { color: #fff; } .info-value.cost { color: #fff; }
.info-value.cost2 { color: #fff; }
.info-value.capacity { color: #fff; } .info-value.capacity { color: #fff; }
.info-unit { .info-unit {
@ -178,4 +208,4 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
`}</style> `}</style>
</div> </div>
); );
} }

View File

@ -0,0 +1,213 @@
//این فایل مخصوص
//شماتیک بندر امام
import React from "react";
import { formatNumber } from "~/lib/utils";
export type CompanyInfo = {
id: string;
imageUrl: string;
name: string;
costReduction: number;
revenue?: number;
capacity?: number;
costI : number,
capacityI : number,
revenueI : number,
cost : number | string,
};
export type D3ImageInfoProps = {
companies: CompanyInfo[];
width?: number;
height?: number;
};
const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
const hideCapacity = company.name === "خوارزمی"; // اگر خوارزمی بود ظرفیت مخفی شود
return (
<div className={`info-box`} style={style}>
<div className="info-box-content">
<div className="info-row">
<div className="info-label">درآمد:</div>
<div className="info-value revenue text-[12px]">{formatNumber(company?.revenue || 0)}</div>
<div className="info-unit">میلیون ریال</div>
</div>
<div className="info-row">
<div className="info-label">هزینه:</div>
{
(hideCapacity ?
<div className="info-value cost2 text-[12px]">{formatNumber(company?.cost || 0)}</div>
:
<div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
)
}
<div className="info-unit">میلیون ریال</div>
</div>
{!hideCapacity && (
<div className="info-row">
<div className="info-label">ظرفیت:</div>
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
<div className="info-unit">تن در سال</div>
</div>
)}
</div>
</div>
);
};
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
// Ensure we have exactly 6 companies
const sample = [
{ id: "آب نیرو", name: "آب نیرو", imageUrl: "/abniro.png" },
{ id: "بسپاران", name: "بسپاران", imageUrl: "/besparan.png" },
{ id: "خوارزمی", name: "خوارزمی", imageUrl: "/khwarazmi.png" },
{ id: "فراورش 1", name: "فراورش 1", imageUrl: "/faravash1.png" },
{ id: "فراورش 2", name: "فراورش 2", imageUrl: "/faravash2.png" },
{ id: "کیمیا", name: "کیمیا", imageUrl: "/kimia.png" }
];
const merged = sample.map(company => {
const found = companies.find(item => item.id == company.id);
return found
? found
: { ...company, cost: 0, capacity: 0, revenue: 0 };
});
const displayCompanies = merged;
console.log(displayCompanies)
// Positions inside a 5x4 grid (col, row)
// Layout keeps same visual logic: left/middle/right on two bands with spacing grid around
const gridPositions = [
{ col: 2, row: 2 , colI : 1 , rowI : 2 , name : "بسپاران"}, // left - top band
{ col: 3, row: 2 , colI : 3 , rowI : 1 , name : "خوارزمی"}, // middle top (image sits in row 2, info box goes to row 1)
{ col: 4, row: 2 ,colI : 5 , rowI : 2 , name : "فراورش 1"}, // right - top band
{ col: 2, row: 3 , colI : 1 , rowI : 3 , name : "کیمیا"}, // left - bottom band
{ col: 3, row: 3 , colI : 3, rowI : 4 , name : "آب نیرو"}, // middle bottom (image sits in row 3, info box goes to row 4)
{ col: 4, row: 3 , colI : 5 , rowI : 3 , name : "فراورش 2"}, // right - bottom band
];
return (
<div className="w-full h-[500px] rounded-xl">
<div dir="ltr" className="company-grid-container">
{displayCompanies.map((company, index) => {
const gp = gridPositions.find(v => v.name === company.name) ;
return (
<>
<div
key={company.id}
className={`company-item`}
style={{ gridColumn: gp.col, gridRow: gp.row }}
>
<div className="company-image-containe">
<img
src={company.imageUrl}
alt={company.name}
className="company-image"
/>
</div>
{company.name}
</div>
<InfoBox company={company} key={index +10} style={{ gridColumn: gp?.colI , gridRow: gp?.rowI }} />
</>);
})}
</div>
<style jsx>{`
.company-grid-container {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 5px;
width: 100%;
height: 500px;
}
.company-item {
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.company-image-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.company-image {
object-fit: contain;
height : 100px;
}
.info-box {
border: 1px solid #3F415A;
border-radius: 10px;
height: max-content;
align-self : center;
justify-self : center;
padding : .2rem 1.2rem;
min-width : 8rem;
background-color: transparent;
}
.info-box-content {
display: flex;
flex-direction: column;
justify-content: center;
}
.info-row {
position : relative;
margin: .1rem 0;
display: flex;
gap : .5rem;
justify-content : space-between;
direction: rtl;
&:has(.info-value.revenue) {border-bottom: 1px solid #3AEA83;}
&:has(.info-value.cost) {border-bottom: 1px solid #F76276;}
}
.info-label {
color: #FFFFFF;
font-size: 11px;
font-weight: 300;
text-align: right;
margin : auto 0;
}
.info-value {
color: #34D399;
font-size: 14px;
font-weight: 500;
text-align: right;
margin-bottom : .5rem;
}
.info-value.revenue { color: #fff;}
.info-value.cost { color: #fff; }
.info-value.cost2 { color: #fff; }
.info-value.capacity { color: #fff; }
.info-unit {
position: absolute;
left: 0;
bottom: 2px;
color: #ACACAC;
font-size: 6px;
font-weight: 400;
}
`}</style>
</div>
);
}

View File

@ -0,0 +1,211 @@
//این فایل مخصوص
//شماتیک نوری
import React from "react";
import { formatNumber } from "~/lib/utils";
export type CompanyInfo = {
id: string;
imageUrl: string;
name: string;
costReduction: number;
revenue?: number;
capacity?: number;
costI: number;
capacityI: number;
revenueI: number;
cost: number | string;
};
export type D3ImageInfoProps = {
companies: CompanyInfo[];
width?: number;
height?: number;
};
const InfoBox = ({ company, style }: { company: CompanyInfo; style: any }) => {
// const hideCapacity = company.name === "واحد 300"; // اگر واحد 300 بود ظرفیت مخفی شود
const hideCapacity = false;
return (
<div className={`info-box`} style={style}>
<div className="info-box-content">
<div className="info-row">
<div className="info-label">درآمد:</div>
<div className="info-value revenue text-[12px]">{formatNumber(company?.revenue || 0)}</div>
<div className="info-unit">میلیون ریال</div>
</div>
<div className="info-row">
<div className="info-label">هزینه:</div>
{hideCapacity ? (
<div className="info-value cost2 text-[12px]">{formatNumber(company?.cost || 0)}</div>
) : (
<div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
)}
<div className="info-unit">میلیون ریال</div>
</div>
{!hideCapacity && (
<div className="info-row">
<div className="info-label">ظرفیت:</div>
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
<div className="info-unit">تن در سال</div>
</div>
)}
</div>
</div>
);
};
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
// واحدهای جدید - 4 واحد
const sample = [
{ id: "واحد 100", name: "واحد 100", imageUrl: "/abniro.png" },
{ id: "واحد 200", name: "واحد 200", imageUrl: "/besparan.png" },
{ id: "واحد 300", name: "واحد 300", imageUrl: "/khwarazmi.png" },
{ id: "واحد 400", name: "واحد 400", imageUrl: "/faravash1.png" }
];
const merged = sample.map(company => {
const found = companies.find(item => item.id === company.id);
return found
? found
: { ...company, cost: 0, capacity: 0, revenue: 0, costReduction: 0, costI: 0, capacityI: 0, revenueI: 0 };
});
const displayCompanies = merged;
console.log(displayCompanies);
// موقعیت‌های جدید برای چیدمان لوزی شکل (3 ردیف - 1-2-1)
// گرید 5x4 نگه داشته شده اما موقعیت‌ها تغییر کرده
const gridPositions = [
{ col: 2, row: 1, colI: 1, rowI: 1, name: "واحد 100" }, // ردیف اول - ستون اول
{ col: 4, row: 1, colI: 5, rowI: 1, name: "واحد 200" }, // ردیف اول - ستون دوم
{ col: 2, row: 3, colI: 1, rowI: 3, name: "واحد 300" }, // ردیف دوم - ستون اول
{ col: 4, row: 3, colI: 5, rowI: 3, name: "واحد 400" }, // ردیف دوم - ستون دوم
];
return (
<div className="w-full h-[500px] rounded-xl">
<div dir="ltr" className="company-grid-container">
{displayCompanies.map((company, index) => {
const gp = gridPositions.find(v => v.name === company.name);
return (
<React.Fragment key={company.id}>
<div
className={`company-item`}
style={{ gridColumn: gp?.col, gridRow: gp?.row }}
>
<div className="company-image-container">
<img
src={company.imageUrl}
alt={company.name}
className="company-image"
/>
</div>
{company.name}
</div>
<InfoBox company={company} style={{ gridColumn: gp?.colI, gridRow: gp?.rowI }} />
</React.Fragment>
);
})}
</div>
<style jsx>{`
.company-grid-container {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 5px;
width: 100%;
height: 500px;
}
.company-item {
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.company-image-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.company-image {
object-fit: contain;
height: 100px;
}
.info-box {
border: 1px solid #3F415A;
border-radius: 10px;
height: max-content;
align-self: center;
justify-self: center;
padding: .2rem 1.2rem;
min-width: 8rem;
background-color: transparent;
}
.info-box-content {
display: flex;
flex-direction: column;
justify-content: center;
}
.info-row {
position: relative;
margin: .1rem 0;
display: flex;
gap: .5rem;
justify-content: space-between;
direction: rtl;
}
.info-row:has(.info-value.revenue) {
border-bottom: 1px solid #3AEA83;
}
.info-row:has(.info-value.cost) {
border-bottom: 1px solid #F76276;
}
.info-label {
color: #FFFFFF;
font-size: 11px;
font-weight: 300;
text-align: right;
margin: auto 0;
}
.info-value {
color: #34D399;
font-size: 14px;
font-weight: 500;
text-align: right;
margin-bottom: .5rem;
}
.info-value.revenue { color: #fff; }
.info-value.cost { color: #fff; }
.info-value.cost2 { color: #fff; }
.info-value.capacity { color: #fff; }
.info-unit {
position: absolute;
left: 0;
bottom: 2px;
color: #ACACAC;
font-size: 6px;
font-weight: 400;
}
`}</style>
</div>
);
}

View File

@ -1,5 +1,10 @@
import React from "react"; import React from "react";
import { formatNumber } from "~/lib/utils"; import { formatNumber } from "~/lib/utils";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "~/components/ui/tooltip"
interface DataItem { interface DataItem {
label: string; label: string;
@ -54,12 +59,27 @@ export function DashboardCustomBarChart({
<div className="flex-row-reverse items-center gap-2 flex 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={`h-auto gap-2 ${item.color} rounded-lg transition-all duration-1000 ease-out flex items-center justify-end px-2`} className={`h-auto gap-2 overflow-hidden ${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-[#3F415A] text-left font-persian font-medium text-sm py-1 w-max"> { widthPercentage > 20 ? (
<span className="text-[#3F415A] min-w-max text-left font-persian font-medium text-sm py-1 w-max">
{item.label} {item.label}
</span> </span>
) : (
<Tooltip>
<TooltipTrigger className={`${item.color}`} asChild>
<span className="text-[#3F415A] text-left font-persian font-medium text-sm py-1">
<span className="invisible">""</span>
</span>
</TooltipTrigger>
<TooltipContent className={`${item.color} ${item.color.replace("bg","fill")}`}>
<p className="font-persian text-sm">{item.label}</p>
</TooltipContent>
</Tooltip>
) }
</div> </div>
<span className="text-white font-bold text-base"> <span className="text-white font-bold text-base">
{formatNumber(item.value)} {formatNumber(item.value)}

View File

@ -1,4 +1,3 @@
import jalaali from "jalaali-js";
import { Book, CheckCircle } from "lucide-react"; import { Book, CheckCircle } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
@ -16,6 +15,7 @@ import { ChartContainer } from "~/components/ui/chart";
import { MetricCard } from "~/components/ui/metric-card"; import { MetricCard } from "~/components/ui/metric-card";
import { Progress } from "~/components/ui/progress"; import { Progress } from "~/components/ui/progress";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils"; import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import type { CalendarDate } from "~/types/util.type";
@ -25,7 +25,6 @@ import { InteractiveBarChart } from "./interactive-bar-chart";
import { DashboardLayout } from "./layout"; import { DashboardLayout } from "./layout";
export function DashboardHome() { export function DashboardHome() {
const { jy } = jalaali.toJalaali(new Date());
const [dashboardData, setDashboardData] = useState<any | null>(null); const [dashboardData, setDashboardData] = useState<any | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -42,19 +41,22 @@ export function DashboardHome() {
}[] }[]
>([]); >([]);
const [date, setDate] = useState<CalendarDate>({ const [date, setDate] = useStoredDate();
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
useEffect(() => { useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => { const handler = (date: CalendarDate) => {
if (date) setDate(date); if (date) setDate(date);
}); };
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []); }, []);
useEffect(() => { useEffect(() => {
fetchDashboardData(); if (date?.end && date?.start) fetchDashboardData();
}, [date]); }, [date]);
const fetchDashboardData = async () => { const fetchDashboardData = async () => {
@ -62,12 +64,6 @@ export function DashboardHome() {
setLoading(true); setLoading(true);
setError(null); setError(null);
// First authenticate if needed
const token = localStorage.getItem("auth_token");
if (!token) {
await apiService.login("inogen_admin", "123456");
}
// Fetch top cards data // Fetch top cards data
const topCardsResponse = await apiService.call({ const topCardsResponse = await apiService.call({
main_page_first_function: { main_page_first_function: {
@ -618,7 +614,7 @@ export function DashboardHome() {
{/* Main Content with Tabs */} {/* Main Content with Tabs */}
<Tabs <Tabs
defaultValue="charts" defaultValue="canvas"
className="grid overflow-hidden rounded-lg grid-rows-[max-content] items-center col-span-2 row-start-2 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]" className="grid overflow-hidden rounded-lg grid-rows-[max-content] items-center col-span-2 row-start-2 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]"
> >
<div className="flex items-center border-b border-gray-600 justify-between gap-2"> <div className="flex items-center border-b border-gray-600 justify-between gap-2">
@ -645,16 +641,39 @@ export function DashboardHome() {
<TabsContent value="canvas" className="w-ful h-full"> <TabsContent value="canvas" className="w-ful h-full">
<div className="p-4 h-full w-full"> <div className="p-4 h-full w-full">
<D3ImageInfo <D3ImageInfo
//پتروشیمی بندر امام
// companies={companyChartData.map((item) => {
// const imageMap: Record<string, string> = {
// بسپاران: "/besparan.png",
// خوارزمی: "/khwarazmi.png",
// "فراورش 1": "/faravash1.png",
// "فراورش 2": "/faravash2.png",
// کیمیا: "/kimia.png",
// "آب نیرو": "/abniro.png",
// };
//پتروشیمی آپادانا
companies={companyChartData.map((item) => { companies={companyChartData.map((item) => {
const imageMap: Record<string, string> = { const imageMap: Record<string, string> = {
بسپاران: "/besparan.png", "واحد 100": "/abniro.png" ,
خوارزمی: "/khwarazmi.png", "واحد 200": "/besparan.png" ,
"فراورش 1": "/faravash1.png", "واحد 300": "/khwarazmi.png" ,
"فراورش 2": "/faravash2.png", "واحد 400": "/faravash1.png"
کیمیا: "/kimia.png",
"آب نیرو": "/abniro.png",
}; };
//پتروشیمی نوری
// companies={companyChartData.map((item) => {
// const imageMap: Record<string, string> = {
// "واحد 100": "/abniro.png" ,
// "واحد 200": "/besparan.png" ,
// "واحد 300": "/khwarazmi.png" ,
// "واحد 400": "/faravash1.png"
// };
return { return {
id: item.category, id: item.category,
name: item.category, name: item.category,

View File

@ -1,20 +1,22 @@
import { saveAs } from "file-saver";
import jalaali from "jalaali-js"; import jalaali from "jalaali-js";
import { import {
Calendar, Calendar,
ChevronLeft, ChevronLeft,
FileChartColumnIncreasing,
Menu, Menu,
PanelLeft, PanelLeft,
Server, Server,
Settings,
User, User,
} from "lucide-react"; } from "lucide-react";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Link } from "react-router"; import { useLocation } from "react-router";
import XLSX from "xlsx-js-style";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Calendar as CustomCalendar } from "~/components/ui/Calendar"; import { Calendar as CustomCalendar } from "~/components/ui/Calendar";
import { useAuth } from "~/contexts/auth-context"; import { useAuth } from "~/contexts/auth-context";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { cn, EventBus } from "~/lib/utils"; import { cn, EventBus, handleDataValue } from "~/lib/utils";
interface HeaderProps { interface HeaderProps {
onToggleSidebar?: () => void; onToggleSidebar?: () => void;
@ -65,7 +67,116 @@ const monthList: Array<MonthItem> = [
id: "month-4", id: "month-4",
label: "زمستان", label: "زمستان",
start: "10/01", start: "10/01",
end: "12/29", end: "12/30",
},
];
const columns: Array<any> = [
{ key: "title", label: "عنوان پروژه", sortable: true, width: "300px" },
{
key: "importance_project",
label: "میزان اهمیت",
sortable: true,
width: "160px",
},
{
key: "strategic_theme",
label: "مضمون راهبردی",
sortable: true,
width: "200px",
},
{
key: "value_technology_and_innovation",
label: "ارزش فناوری و نوآوری",
sortable: true,
width: "220px",
},
{
key: "type_of_innovation",
label: "انواع نوآوری",
sortable: true,
width: "160px",
},
{
key: "innovation",
label: "میزان نوآوری",
sortable: true,
width: "140px",
},
{
key: "person_executing",
label: "مسئول اجرا",
sortable: true,
width: "180px",
},
{
key: "excellent_observer",
label: "ناطر عالی",
sortable: true,
width: "180px",
},
{ key: "observer", label: "ناظر پروژه", sortable: true, width: "180px" },
{ key: "moderator", label: "مجری", sortable: true, width: "180px" },
{
key: "executive_phase",
label: "فاز اجرایی",
sortable: true,
width: "160px",
},
{
key: "start_date",
label: "تاریخ شروع",
sortable: true,
width: "120px",
},
{
key: "remaining_time",
label: "زمان باقی مانده",
sortable: true,
width: "140px",
computed: true,
},
{
key: "end_date",
label: "تاریخ پایان (برنامه‌ریزی)",
sortable: true,
width: "160px",
},
{
key: "renewed_duration",
label: "مدت زمان تمدید",
sortable: true,
width: "140px",
},
{
key: "done_date",
label: "تاریخ پایان (واقعی)",
sortable: true,
width: "160px",
},
{
key: "deviation_from_program",
label: "متوسط انحراف برنامه‌ای",
sortable: true,
width: "160px",
},
{
key: "approved_budget",
label: "بودجه مصوب",
sortable: true,
width: "150px",
},
{
key: "budget_spent",
label: "بودجه صرف شده",
sortable: true,
width: "150px",
},
{
key: "cost_deviation",
label: "متوسط انحراف هزینه‌ای",
sortable: true,
width: "160px",
}, },
]; ];
@ -82,22 +193,54 @@ export function Header({
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState<boolean>(false); const [isProfileMenuOpen, setIsProfileMenuOpen] = useState<boolean>(false);
const [isNotificationOpen, setIsNotificationOpen] = useState<boolean>(false); const [isNotificationOpen, setIsNotificationOpen] = useState<boolean>(false);
const [openCalendar, setOpenCalendar] = useState<boolean>(false); const [openCalendar, setOpenCalendar] = useState<boolean>(false);
const [excelLoading, setExcelLoading] = useState<boolean>(false);
const location = useLocation();
const projectManagerRoute = "/dashboard/project-management";
const [currentYear, setCurrentYear] = useState<SelectedDate>({ const [currentYear, setCurrentYear] = useState<SelectedDate>({
since: jy, since: jy,
until: jy, until: jy,
}); });
const [selectedDate, setSelectedDate] = useState<CurrentDay>({ const [selectedDate, setSelectedDate] = useState<CurrentDay>({});
sinceMonth: "بهار",
fromMonth: "زمستان", useEffect(() => {
start: `${currentYear.since}/01/01`, const storedDate = localStorage.getItem("dateSelected");
end: `${currentYear.until}/12/30`, if (storedDate) {
}); const parsedDate = JSON.parse(storedDate);
setSelectedDate(parsedDate);
const sinceYear = parsedDate.start
? parseInt(parsedDate.start.split("/")[0], 10)
: jy;
const untilYear = parsedDate.end
? parseInt(parsedDate.end.split("/")[0], 10)
: jy;
setCurrentYear({ since: sinceYear, until: untilYear });
} else {
const defaultDate = {
sinceMonth: "بهار",
fromMonth: "زمستان",
start: `${jy}/01/01`,
end: `${jy}/12/30`,
};
setSelectedDate(defaultDate);
localStorage.setItem("dateSelected", JSON.stringify(defaultDate));
setCurrentYear({ since: jy, until: jy });
}
}, []);
const redirectHandler = async () => { const redirectHandler = async () => {
try { try {
const getData = await apiService.post("/GenerateSsoCode"); const getData = await apiService.post("/GenerateSsoCode");
const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`;
//بندر امام
// const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`;
//آپادانا
const url = `https://APADANA-IATM-bpms.pelekan.org/redirect/${getData.data}`;
//نوری
// const url = `https://NOPC-IATM-bpms.pelekan.org/redirect/${getData.data}`;
window.open(url, "_blank"); window.open(url, "_blank");
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@ -119,6 +262,7 @@ export function Header({
start: `${newSince}/${selectedDate.start?.split("/").slice(1).join("/")}`, start: `${newSince}/${selectedDate.start?.split("/").slice(1).join("/")}`,
}; };
setSelectedDate(updatedDate); setSelectedDate(updatedDate);
localStorage.setItem("dateSelected", JSON.stringify(updatedDate));
EventBus.emit("dateSelected", updatedDate); EventBus.emit("dateSelected", updatedDate);
}; };
@ -132,6 +276,7 @@ export function Header({
sinceMonth: val.label, sinceMonth: val.label,
}; };
setSelectedDate(data); setSelectedDate(data);
localStorage.setItem("dateSelected", JSON.stringify(data));
EventBus.emit("dateSelected", data); EventBus.emit("dateSelected", data);
}; };
@ -150,6 +295,7 @@ export function Header({
end: `${newUntil}/${selectedDate.end?.split("/").slice(1).join("/")}`, end: `${newUntil}/${selectedDate.end?.split("/").slice(1).join("/")}`,
}; };
setSelectedDate(updatedDate); setSelectedDate(updatedDate);
localStorage.setItem("dateSelected", JSON.stringify(updatedDate));
EventBus.emit("dateSelected", updatedDate); EventBus.emit("dateSelected", updatedDate);
}; };
@ -163,6 +309,7 @@ export function Header({
fromMonth: val.label, fromMonth: val.label,
}; };
setSelectedDate(data); setSelectedDate(data);
localStorage.setItem("dateSelected", JSON.stringify(data));
EventBus.emit("dateSelected", data); EventBus.emit("dateSelected", data);
toggleCalendar(); toggleCalendar();
}; };
@ -186,6 +333,66 @@ export function Header({
}; };
}, []); }, []);
const exportToExcel = async () => {
let arr = [];
const data: any = await fetchExcelData();
for (let i = 0; i < data.length; i++) {
let obj: Record<string, any> = {};
const project = data[i];
Object.entries(project).forEach(([pKey, pValue]: [any, any]) => {
Object.values(columns).forEach((col) => {
if (pKey === col?.key) {
``;
obj[col?.label] = handleDataValue(
pValue?.includes(",") ? pValue.replaceAll(",", "") : pValue
);
}
});
});
arr.push(obj);
}
const worksheet = XLSX.utils.json_to_sheet(arr);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "People");
const excelBuffer = XLSX.write(workbook, {
bookType: "xlsx",
type: "array",
});
const blob = new Blob([excelBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
saveAs(blob, "reports.xls");
};
const fetchExcelData = async () => {
setExcelLoading(true);
const fetchableColumns = columns.filter((c) => !c.computed);
const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key);
const response = await apiService.select({
ProcessName: "project",
OutputFields: outputFields,
Conditions: [
["start_date", ">=", selectedDate?.start || null, "and"],
["start_date", "<=", selectedDate?.end || null],
],
});
const parsedData = JSON.parse(response.data);
setExcelLoading(false);
return parsedData;
};
const handleDownloadFile = () => {
if (excelLoading) return null;
else exportToExcel();
};
return ( return (
<header <header
className={cn( className={cn(
@ -241,12 +448,16 @@ export function Header({
<div className="flex flex-row gap-1.5 w-max"> <div className="flex flex-row gap-1.5 w-max">
<span className="text-md">از</span> <span className="text-md">از</span>
<span className="text-md">{selectedDate?.sinceMonth}</span> <span className="text-md">{selectedDate?.sinceMonth}</span>
<span className="text-md">{currentYear.since}</span> <span className="text-md">
{handleDataValue(currentYear.since)}
</span>
</div> </div>
<div className="flex flex-row gap-1.5 w-max"> <div className="flex flex-row gap-1.5 w-max">
<span className="text-md">تا</span> <span className="text-md">تا</span>
<span className="text-md">{selectedDate?.fromMonth}</span> <span className="text-md">{selectedDate?.fromMonth}</span>
<span className="text-md">{currentYear.until}</span> <span className="text-md">
{handleDataValue(currentYear.until)}
</span>
</div> </div>
</div> </div>
) : ( ) : (
@ -258,9 +469,9 @@ export function Header({
<div className="flex flex-row gap-2.5 absolute top-14 right-[-40px] p-2.5 !pt-3.5 w-80 rounded-3xl overflow-hidden bg-pr-gray border-2 border-[#5F6284]"> <div className="flex flex-row gap-2.5 absolute top-14 right-[-40px] p-2.5 !pt-3.5 w-80 rounded-3xl overflow-hidden bg-pr-gray border-2 border-[#5F6284]">
<CustomCalendar <CustomCalendar
title="از" title="از"
nextYearHandler={nextFromYearHandler} nextYearHandler={prevFromYearHandler}
prevYearHandler={prevFromYearHandler} prevYearHandler={nextFromYearHandler}
currentYear={currentYear?.since} currentYear={handleDataValue(currentYear?.since)}
monthList={monthList} monthList={monthList}
selectedDate={selectedDate?.sinceMonth} selectedDate={selectedDate?.sinceMonth}
selectDateHandler={selectFromDateHandler} selectDateHandler={selectFromDateHandler}
@ -268,9 +479,9 @@ export function Header({
<span className="w-0.5 h-[12.5rem] border border-[#5F6284] block "></span> <span className="w-0.5 h-[12.5rem] border border-[#5F6284] block "></span>
<CustomCalendar <CustomCalendar
title="تا" title="تا"
nextYearHandler={nextUntilYearHandler} nextYearHandler={prevUntilYearHandler}
prevYearHandler={prevUntilYearHandler} prevYearHandler={nextUntilYearHandler}
currentYear={currentYear?.until} currentYear={handleDataValue(currentYear?.until)}
monthList={monthList} monthList={monthList}
selectedDate={selectedDate?.fromMonth} selectedDate={selectedDate?.fromMonth}
selectDateHandler={selectUntilDateHandler} selectDateHandler={selectUntilDateHandler}
@ -285,9 +496,23 @@ export function Header({
{/* User Menu */} {/* User Menu */}
<div className="relative"> <div className="relative">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{location.pathname === projectManagerRoute ? (
<div className="flex justify-end w-full mb-0 pl-2">
<span
className={`flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian ${excelLoading ? "!cursor-not-allowed !opacity-10" : ""}`}
onClick={handleDownloadFile}
>
<FileChartColumnIncreasing className="h-4 w-4" />
دانلود فایل اکسل
</span>
</div>
) : (
""
)}
{user?.id === 2041 && ( {user?.id === 2041 && (
<button <button
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian" className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
onClick={redirectHandler} onClick={redirectHandler}
> >
<Server className="h-4 w-4" /> <Server className="h-4 w-4" />
@ -314,6 +539,7 @@ export function Header({
</div> </div>
</Button> </Button>
</div> </div>
{/* Profile Dropdown */} {/* Profile Dropdown */}
{isProfileMenuOpen && ( {isProfileMenuOpen && (
<div className="absolute left-0 top-full mt-2 w-48 bg-gray-800 border border-emerald-500/30 rounded-lg shadow-lg z-50"> <div className="absolute left-0 top-full mt-2 w-48 bg-gray-800 border border-emerald-500/30 rounded-lg shadow-lg z-50">
@ -325,7 +551,7 @@ export function Header({
{user?.email} {user?.email}
</div> </div>
</div> </div>
<div className="py-1"> {/* <div className="py-1">
<Link <Link
to="/dashboard/profile" to="/dashboard/profile"
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian" className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
@ -333,16 +559,16 @@ export function Header({
> >
<User className="h-4 w-4" /> <User className="h-4 w-4" />
پروفایل کاربری پروفایل کاربری
</Link> </Link>
<Link <Link
to="/dashboard/settings" to="/dashboard/settings"
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian" className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
onClick={() => setIsProfileMenuOpen(false)} onClick={() => setIsProfileMenuOpen(false)}
> >
<Settings className="h-4 w-4" /> <Settings className="h-4 w-4" />
تنظیمات تنظیمات
</Link> </Link>
</div> </div> */}
</div> </div>
)} )}
</div> </div>

View File

@ -1,4 +1,3 @@
import jalaali from "jalaali-js";
import { import {
BrainCircuit, BrainCircuit,
ChevronDown, ChevronDown,
@ -16,6 +15,7 @@ import moment from "moment-jalaali";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, 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";
import { BaseCard } from "~/components/ui/base-card";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Card, CardContent } from "~/components/ui/card"; import { Card, CardContent } from "~/components/ui/card";
import { Checkbox } from "~/components/ui/checkbox"; import { Checkbox } from "~/components/ui/checkbox";
@ -34,6 +34,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils"; import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import type { CalendarDate } from "~/types/util.type";
@ -148,18 +149,13 @@ const columns = [
]; ];
export function DigitalInnovationPage() { export function DigitalInnovationPage() {
const { jy } = jalaali.toJalaali(new Date());
const [projects, setProjects] = useState<DigitalInnovationMetrics[]>([]); const [projects, setProjects] = useState<DigitalInnovationMetrics[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(20); const [pageSize] = useState(20);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
// const [totalCount, setTotalCount] = useState(0); const [date, setDate] = useStoredDate();
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
const [actualTotalCount, setActualTotalCount] = useState(0); const [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false); const [statsLoading, setStatsLoading] = useState(false);
const [rating, setRating] = useState<ListItem[]>([]); const [rating, setRating] = useState<ListItem[]>([]);
@ -364,21 +360,27 @@ export function DigitalInnovationPage() {
}, [hasMore, loading, loadingMore]); }, [hasMore, loading, loadingMore]);
useEffect(() => { useEffect(() => {
fetchTable(true); if (date?.start && date?.end) {
fetchTotalCount(); fetchTable(true);
fetchStats(); fetchTotalCount();
fetchStats();
}
}, [sortConfig, date]); }, [sortConfig, date]);
useEffect(() => { useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => { const handler = (date: CalendarDate) => {
if (date) { if (date) setDate(date);
setDate(date); };
}
}); EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []); }, []);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1 && date?.start && date?.end) {
fetchTable(false); fetchTable(false);
} }
}, [currentPage]); }, [currentPage]);
@ -432,7 +434,7 @@ export function DigitalInnovationPage() {
prev.field === field && prev.direction === "asc" ? "desc" : "asc", prev.field === field && prev.direction === "asc" ? "desc" : "asc",
})); }));
fetchTotalCount(date?.start, date?.end); fetchTotalCount(date?.start, date?.end);
fetchStats(date?.start, date?.end); fetchStats();
setCurrentPage(1); setCurrentPage(1);
setProjects([]); setProjects([]);
setHasMore(true); setHasMore(true);
@ -753,12 +755,12 @@ export function DigitalInnovationPage() {
</div> </div>
{/* Process Impacts Chart */} {/* Process Impacts Chart */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg w-full overflow-hidden h-[18rem]"> <BaseCard className="rounded-xl w-full overflow-hidden">
{/* <CardContent > */} {/* <CardContent > */}
<CustomBarChart <CustomBarChart
title="تاثیرات نوآوری دیجیتال به صورت درصد مقایسه ای" title="تاثیرات نوآوری دیجیتال به صورت درصد مقایسه ای"
loading={statsLoading} loading={statsLoading}
height="100%" // height="100%"
data={[ data={[
{ {
label: DigitalCardLabel.decreasCost, label: DigitalCardLabel.decreasCost,
@ -788,8 +790,7 @@ export function DigitalInnovationPage() {
barHeight="h-5" barHeight="h-5"
showAxisLabels={true} showAxisLabels={true}
/> />
{/* </CardContent> */} </BaseCard>
</Card>
</div> </div>
{/* Data Table */} {/* Data Table */}

View File

@ -1,4 +1,3 @@
// import moment from "moment-jalaali";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { import {
Bar, Bar,
@ -28,7 +27,6 @@ import {
} from "~/components/ui/table"; } from "~/components/ui/table";
import { EventBus, formatNumber } from "~/lib/utils"; import { EventBus, formatNumber } from "~/lib/utils";
import jalaali from "jalaali-js";
import { import {
Building2, Building2,
ChevronDown, ChevronDown,
@ -44,13 +42,17 @@ import {
UsersIcon, UsersIcon,
Zap, Zap,
} from "lucide-react"; } from "lucide-react";
import moment from "moment-jalaali";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { MetricCard } from "~/components/ui/metric-card";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { formatCurrency } from "~/lib/utils"; import { formatCurrency } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import type { CalendarDate } from "~/types/util.type";
import DashboardLayout from "../layout"; import DashboardLayout from "../layout";
// moment.loadPersian({ usePersianDigits: true }); moment.loadPersian({ usePersianDigits: true });
interface GreenInnovationData { interface GreenInnovationData {
WorkflowID: string; WorkflowID: string;
approved_budget: string; approved_budget: string;
@ -159,7 +161,6 @@ const columns = [
]; ];
export function GreenInnovationPage() { export function GreenInnovationPage() {
const { jy } = jalaali.toJalaali(new Date());
const [projects, setProjects] = useState<GreenInnovationData[]>([]); const [projects, setProjects] = useState<GreenInnovationData[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
@ -169,10 +170,8 @@ export function GreenInnovationPage() {
const [totalCount, setTotalCount] = useState(0); const [totalCount, setTotalCount] = useState(0);
const [actualTotalCount, setActualTotalCount] = useState(0); const [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false); const [statsLoading, setStatsLoading] = useState(false);
const [date, setDate] = useState<CalendarDate>({ const [date, setDate] = useStoredDate();
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
const [stats, setStats] = useState<stateCounter>(); const [stats, setStats] = useState<stateCounter>();
const [sortConfig, setSortConfig] = useState<SortConfig>({ const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "start_date", field: "start_date",
@ -362,11 +361,15 @@ export function GreenInnovationPage() {
}; };
useEffect(() => { useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => { const handler = (date: CalendarDate) => {
if (date) { if (date) setDate(date);
setDate(date); };
}
}); EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []); }, []);
const loadMore = useCallback(() => { const loadMore = useCallback(() => {
@ -376,12 +379,14 @@ export function GreenInnovationPage() {
}, [hasMore, loading]); }, [hasMore, loading]);
useEffect(() => { useEffect(() => {
fetchProjects(true); if (date.end && date.start) {
fetchTotalCount(); fetchProjects(true);
fetchTotalCount();
}
}, [sortConfig, date]); }, [sortConfig, date]);
useEffect(() => { useEffect(() => {
fetchStats(); if (date.end && date.start) fetchStats();
}, [selectedProjects, date]); }, [selectedProjects, date]);
useEffect(() => { useEffect(() => {
@ -519,13 +524,13 @@ export function GreenInnovationPage() {
}, },
pollution: { pollution: {
value: formatNumber(parseNum(stats.pollution_reduction)), value: parseNum(stats.pollution_reduction),
percent: formatNumber(parseNum(stats.pollution_reduction_percent)), percent: parseNum(stats.pollution_reduction_percent),
}, },
waste: { waste: {
value: formatNumber(parseNum(stats.waste_reduction)), value: parseNum(stats.waste_reduction),
percent: formatNumber(parseNum(stats.waste_reductionn_percent)), percent: parseNum(stats.waste_reductionn_percent),
}, },
avarage: stats.average_project_score, avarage: stats.average_project_score,
countInnovationGreenProjects: stats.count_innovation_green_projects, countInnovationGreenProjects: stats.count_innovation_green_projects,
@ -543,7 +548,6 @@ export function GreenInnovationPage() {
setStatsLoading(false); setStatsLoading(false);
} }
}; };
const setPageData = (normalized: any) => { const setPageData = (normalized: any) => {
setSustainabilityStats((prev) => ({ setSustainabilityStats((prev) => ({
...prev, ...prev,
@ -747,39 +751,14 @@ export function GreenInnovationPage() {
</Card> </Card>
)) ))
: Object.entries(sustainabilityStats).map(([key, value]) => ( : Object.entries(sustainabilityStats).map(([key, value]) => (
<Card <MetricCard
key={key} key={key}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50" title={value.title}
> value={Math.round(value.total.value || 0)}
<CardContent className="p-0 h-full"> valueLabel={value.total?.description}
<div className="flex flex-col justify-between gap-2 h-full"> percentValue={value.percent?.value || 0}
<div className="flex justify-between items-center border-b-2 border-gray-500/20 "> percentLabel={value.percent?.description}
<h3 className="text-lg font-bold text-white font-persian p-4"> />
{value.title}
</h3>
</div>
<div className="flex items-center justify-between p-6 flex-row-reverse">
<div className="flex flex-col">
<span className="text-3xl font-bold text-pr-green mb-1 font-persian">
% {value.percent?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.percent?.description}
</span>
</div>
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
<div className="flex flex-col">
<span className="text-3xl font-bold text-pr-green mb-1 font-persian">
{value.total?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.total?.description}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
))} ))}
</div> </div>

View File

@ -19,7 +19,6 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import jalaali from "jalaali-js";
import { import {
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
@ -40,6 +39,8 @@ import {
ResponsiveContainer, ResponsiveContainer,
XAxis, XAxis,
} from "recharts"; } from "recharts";
import { MetricCard } from "~/components/ui/metric-card";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils"; import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import type { CalendarDate } from "~/types/util.type";
@ -179,7 +180,6 @@ const dialogChartData = [
]; ];
export function InnovationBuiltInsidePage() { export function InnovationBuiltInsidePage() {
const { jy } = jalaali.toJalaali(new Date());
const [projects, setProjects] = useState<innovationBuiltInDate[]>([]); const [projects, setProjects] = useState<innovationBuiltInDate[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
@ -194,10 +194,8 @@ export function InnovationBuiltInsidePage() {
field: "start_date", field: "start_date",
direction: "asc", direction: "asc",
}); });
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`, const [date, setDate] = useStoredDate();
end: `${jy}/12/30`,
});
const [tblAvarage, setTblAvarage] = useState<number>(0); const [tblAvarage, setTblAvarage] = useState<number>(0);
const [selectedProjects, setSelectedProjects] = const [selectedProjects, setSelectedProjects] =
useState<Set<string | number>>(); useState<Set<string | number>>();
@ -428,19 +426,23 @@ export function InnovationBuiltInsidePage() {
}, [hasMore, loading]); }, [hasMore, loading]);
useEffect(() => { useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => { const handler = (date: CalendarDate) => {
if (date) { if (date) setDate(date);
setDate(date); };
}
}); EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []); }, []);
useEffect(() => { useEffect(() => {
fetchProjects(true); if (date.start && date.end) fetchProjects(true);
}, [sortConfig, date]); }, [sortConfig, date]);
useEffect(() => { useEffect(() => {
fetchStats(); if (date.end && date.start) fetchStats();
}, [selectedProjects, date]); }, [selectedProjects, date]);
useEffect(() => { useEffect(() => {
@ -526,15 +528,13 @@ export function InnovationBuiltInsidePage() {
const stats = data[0]; const stats = data[0];
const normalized: any = { const normalized: any = {
currencySaving: { currencySaving: {
value: formatNumber(parseNum(stats?.foreign_currency_saving)), value: parseNum(stats?.foreign_currency_saving),
percent: formatNumber( percent: parseNum(stats?.foreign_currency_saving_percent),
parseNum(stats?.foreign_currency_saving_percent)
),
}, },
investmentAmount: { investmentAmount: {
value: formatNumber(parseNum(stats?.investment_amount)), value: parseNum(stats?.investment_amount),
percent: formatNumber(parseNum(stats?.investment_amount_percent)), percent: parseNum(stats?.investment_amount_percent),
}, },
technology: { technology: {
@ -724,7 +724,7 @@ export function InnovationBuiltInsidePage() {
return ( return (
<DashboardLayout title="نوآوری ساخت داخل"> <DashboardLayout title="نوآوری ساخت داخل">
<div className="space-y-4 justify-between gap-8 grid pl-3.5 sm:grid-cols-1 xl:grid-cols-[35%_65%]"> <div className="space-y-4 justify-between gap-8 grid pl-6 sm:grid-cols-1 xl:grid-cols-[35%_65%]">
{/* Stats Cards */} {/* Stats Cards */}
<div className="flex w-full mb-0"> <div className="flex w-full mb-0">
<div className="flex flex-col w-full justify-between gap-2"> <div className="flex flex-col w-full justify-between gap-2">
@ -758,39 +758,47 @@ export function InnovationBuiltInsidePage() {
</Card> </Card>
)) ))
: Object.entries(sustainabilityStats).map(([key, value]) => ( : Object.entries(sustainabilityStats).map(([key, value]) => (
<Card <MetricCard
key={key} key={key}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50" title={value.title}
> value={Math.round(value.total.value || 0)}
<CardContent className="p-0 h-full"> valueLabel={value.total?.description}
<div className="flex flex-col justify-between gap-2 h-full"> percentValue={value.percent?.value || 0}
<div className="flex justify-between items-center border-b-2 border-gray-500/20 "> percentLabel={value.percent?.description}
<h3 className="text-lg font-semibold text-white p-4"> />
{value.title} // <Card
</h3> // key={key}
</div> // className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
<div className="flex items-center justify-between p-6 flex-row-reverse"> // >
<div className="flex flex-col"> // <CardContent className="p-0 h-full">
<span className="text-3xl font-bold text-pr-green mb-1 font-persian"> // <div className="flex flex-col justify-between gap-2 h-full">
% {value.percent?.value} // <div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
</span> // <h3 className="text-lg font-semibold text-white p-4">
<span className="text-sm text-gray-400 font-persian"> // {value.title}
{value.percent?.description} // </h3>
</span> // </div>
</div> // <div className="flex items-center justify-between p-6 flex-row-reverse">
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" /> // <div className="flex flex-col">
<div className="flex flex-col"> // <span className="text-3xl font-bold text-pr-green mb-1 font-persian">
<span className="text-3xl font-bold text-pr-green mb-1 font-persian"> // % {value.percent?.value}
{value.total?.value} // </span>
</span> // <span className="text-sm text-gray-400 font-persian">
<span className="text-sm text-gray-400 font-persian"> // {value.percent?.description}
{value.total?.description} // </span>
</span> // </div>
</div> // <b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
</div> // <div className="flex flex-col">
</div> // <span className="text-3xl font-bold text-pr-green mb-1 font-persian">
</CardContent> // {value.total?.value}
</Card> // </span>
// <span className="text-sm text-gray-400 font-persian">
// {value.total?.description}
// </span>
// </div>
// </div>
// </div>
// </CardContent>
// </Card>
))} ))}
{statsLoading ? ( {statsLoading ? (

View File

@ -1,4 +1,3 @@
import jalaali from "jalaali-js";
import { import {
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
@ -43,6 +42,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils"; import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import type { CalendarDate } from "~/types/util.type";
@ -261,7 +261,6 @@ const VerticalBarChart = memo<{
const MemoizedVerticalBarChart = VerticalBarChart; const MemoizedVerticalBarChart = VerticalBarChart;
export function ManageIdeasTechPage() { export function ManageIdeasTechPage() {
const { jy } = jalaali.toJalaali(new Date());
const [ideas, setIdeas] = useState<IdeaData[]>([]); const [ideas, setIdeas] = useState<IdeaData[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
@ -276,10 +275,7 @@ export function ManageIdeasTechPage() {
field: "idea_title", field: "idea_title",
direction: "asc", direction: "asc",
}); });
const [date, setDate] = useState<CalendarDate>({ const [date, setDate] = useStoredDate();
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
// People ranking state // People ranking state
const [peopleRanking, setPeopleRanking] = useState<PersonRanking[]>([]); const [peopleRanking, setPeopleRanking] = useState<PersonRanking[]>([]);
@ -409,19 +405,25 @@ export function ManageIdeasTechPage() {
}, [hasMore, loading, loadingMore]); }, [hasMore, loading, loadingMore]);
useEffect(() => { useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => { const handler = (date: CalendarDate) => {
if (date) { if (date) setDate(date);
setDate(date); };
}
}); EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []); }, []);
useEffect(() => { useEffect(() => {
fetchIdeas(true); if (date.end && date.start) {
fetchTotalCount(); fetchIdeas(true);
fetchPeopleRanking(); fetchTotalCount();
fetchChartData(); fetchPeopleRanking();
fetchStatsData(); fetchChartData();
fetchStatsData();
}
}, [sortConfig, date]); }, [sortConfig, date]);
useEffect(() => { useEffect(() => {

View File

@ -1,4 +1,3 @@
import jalaali from "jalaali-js";
import { import {
Building2, Building2,
ChevronDown, ChevronDown,
@ -35,6 +34,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils"; import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import type { CalendarDate } from "~/types/util.type";
@ -67,9 +67,11 @@ interface ProjectStats {
percent_reduction_value_currency: string; percent_reduction_value_currency: string;
percent_sum_stopping_production: string; percent_sum_stopping_production: string;
percent_throat_removal: string; percent_throat_removal: string;
percent_operating_cost_before_innovation: string;
sum_reducing_breakdowns: number; sum_reducing_breakdowns: number;
sum_reduction_value_currency: number; sum_reduction_value_currency: number;
sum_stopping_production: number; sum_stopping_production: number;
sum_operating_cost_reduction: number;
} }
interface SortConfig { interface SortConfig {
@ -94,9 +96,11 @@ interface InnovationStats {
currencyReductionSum: number; // مجموع کاهش ارز بری (میلیون ریال) currencyReductionSum: number; // مجموع کاهش ارز بری (میلیون ریال)
frequentFailuresReductionSum: number; // مجموع کاهش خرابی های پرتکرار frequentFailuresReductionSum: number; // مجموع کاهش خرابی های پرتکرار
percentProductionStops: number | string; // درصد مقایسه‌ای جلوگیری از توقفات تولید percentProductionStops: number | string; // درصد مقایسه‌ای جلوگیری از توقفات تولید
reductionCostOprationSum: number; // مجموع کاهش هزینه عملیاتی
percentBottleneckRemoval: number | string; // درصد مقایسه‌ای رفع گلوگاه percentBottleneckRemoval: number | string; // درصد مقایسه‌ای رفع گلوگاه
percentCurrencyReduction: number | string; // درصد مقایسه‌ای کاهش ارز بری percentCurrencyReduction: number | string; // درصد مقایسه‌ای کاهش ارز بری
percentFailuresReduction: number | string; // درصد مقایسه‌ای کاهش خرابی‌های پرتکرار percentFailuresReduction: number | string; // درصد مقایسه‌ای کاهش خرابی‌های پرتکرار
percentOperatingCostBeforeInnovation: number | string; // درصد مقایسه‌ای کاهش هزینه عملیاتی
} }
const columns = [ const columns = [
@ -119,24 +123,20 @@ const columns = [
]; ];
export function ProcessInnovationPage() { export function ProcessInnovationPage() {
const { jy } = jalaali.toJalaali(new Date());
const [projects, setProjects] = useState<ProcessInnovationData[]>([]); const [projects, setProjects] = useState<ProcessInnovationData[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(20); const [pageSize] = useState(20);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
// const [totalCount, setTotalCount] = useState(0); const [date, setDate] = useStoredDate();
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
const [actualTotalCount, setActualTotalCount] = useState(0); const [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false); const [statsLoading, setStatsLoading] = useState(false);
const [stats, setStats] = useState<InnovationStats>({ const [stats, setStats] = useState<InnovationStats>({
totalProjects: 0, totalProjects: 0,
averageScore: 0, averageScore: 0,
productionStopsPreventionSum: 0, productionStopsPreventionSum: 0,
reductionCostOprationSum: 0,
bottleneckRemovalCount: 0, bottleneckRemovalCount: 0,
currencyReductionSum: 0, currencyReductionSum: 0,
frequentFailuresReductionSum: 0, frequentFailuresReductionSum: 0,
@ -144,6 +144,7 @@ export function ProcessInnovationPage() {
percentBottleneckRemoval: 0, percentBottleneckRemoval: 0,
percentCurrencyReduction: 0, percentCurrencyReduction: 0,
percentFailuresReduction: 0, percentFailuresReduction: 0,
percentOperatingCostBeforeInnovation: 0,
}); });
const [sortConfig, setSortConfig] = useState<SortConfig>({ const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "start_date", field: "start_date",
@ -159,7 +160,7 @@ export function ProcessInnovationPage() {
const [stateCard, setStateCard] = useState({ const [stateCard, setStateCard] = useState({
productionstopsprevention: { productionstopsprevention: {
id: "productionstopsprevention", id: "productionstopsprevention",
title: "جلوگیری از توقفات تولید", title: "توقفات تولید",
value: formatNumber( value: formatNumber(
stats.productionStopsPreventionSum.toFixed?.(1) ?? stats.productionStopsPreventionSum.toFixed?.(1) ??
stats.productionStopsPreventionSum stats.productionStopsPreventionSum
@ -170,7 +171,7 @@ export function ProcessInnovationPage() {
}, },
bottleneckremoval: { bottleneckremoval: {
id: "bottleneckremoval", id: "bottleneckremoval",
title: "رفع گلوگاه", title: "گلوگاه ها",
value: formatNumber(stats.bottleneckRemovalCount), value: formatNumber(stats.bottleneckRemovalCount),
description: "تعداد رفع گلوگاه", description: "تعداد رفع گلوگاه",
icon: Funnel, icon: Funnel,
@ -178,7 +179,7 @@ export function ProcessInnovationPage() {
}, },
currencyreduction: { currencyreduction: {
id: "currencyreduction", id: "currencyreduction",
title: "کاهش ارز بری", title: "ارز بری",
value: formatNumber( value: formatNumber(
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
), ),
@ -186,14 +187,25 @@ export function ProcessInnovationPage() {
icon: DollarSign, icon: DollarSign,
color: "text-pr-green", color: "text-pr-green",
}, },
decreaseCurrencyOperation: {
id: "decreaseCurrencyOperation",
title: "هزینه های عملیاتی",
value: formatNumber(
stats.reductionCostOprationSum.toFixed?.(0) ??
stats.reductionCostOprationSum
),
description: "میلیون ریال کاهش یافته",
icon: DollarSign,
color: "text-pr-green",
},
frequentfailuresreduction: { frequentfailuresreduction: {
id: "frequentfailuresreduction", id: "frequentfailuresreduction",
title: "کاهش خرابی های پرتکرار", title: "خرابی های پرتکرار",
value: formatNumber( value: formatNumber(
stats.frequentFailuresReductionSum.toFixed?.(1) ?? stats.frequentFailuresReductionSum.toFixed?.(1) ??
stats.frequentFailuresReductionSum stats.frequentFailuresReductionSum
), ),
description: "مجموع درصد کاهش خرابی", description: "خرابی پر تکرار کاهش یافته",
icon: Wrench, icon: Wrench,
color: "text-pr-green", color: "text-pr-green",
}, },
@ -202,15 +214,6 @@ export function ProcessInnovationPage() {
const observerRef = useRef<HTMLDivElement>(null); const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false); const fetchingRef = useRef(false);
// Selection handlers
// const handleSelectAll = () => {
// if (selectedProjects.size === projects.length) {
// setSelectedProjects(new Set());
// } else {
// setSelectedProjects(new Set(projects.map((p) => p.project_no)));
// }
// };
const handleSelectProject = (projectNo: string) => { const handleSelectProject = (projectNo: string) => {
const newSelected = new Set(selectedProjects); const newSelected = new Set(selectedProjects);
if (newSelected.has(projectNo)) { if (newSelected.has(projectNo)) {
@ -337,20 +340,26 @@ export function ProcessInnovationPage() {
}, [hasMore, loading]); }, [hasMore, loading]);
useEffect(() => { useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => { const handler = (date: CalendarDate) => {
if (date) { if (date) setDate(date);
setDate(date); };
}
}); EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []); }, []);
useEffect(() => { useEffect(() => {
fetchProjects(true); if (date?.start && date?.end) {
fetchTotalCount(); fetchProjects(true);
fetchTotalCount();
}
}, [sortConfig, date]); }, [sortConfig, date]);
useEffect(() => { useEffect(() => {
fetchStats(); if (date?.start && date?.end) fetchStats();
}, [selectedProjects, date]); }, [selectedProjects, date]);
useEffect(() => { useEffect(() => {
@ -471,10 +480,13 @@ export function ProcessInnovationPage() {
totalProjects: parseNum(stats?.count_innovation_process_projects), totalProjects: parseNum(stats?.count_innovation_process_projects),
averageScore: parseFloat(data[0].average_project_score), averageScore: parseFloat(data[0].average_project_score),
productionStopsPreventionSum: parseNum(stats?.sum_stopping_production), productionStopsPreventionSum: parseNum(stats?.sum_stopping_production),
reductionCostOprationSum: parseNum(stats?.sum_operating_cost_reduction),
bottleneckRemovalCount: parseNum(stats?.count_throat_removal), bottleneckRemovalCount: parseNum(stats?.count_throat_removal),
currencyReductionSum: parseNum(stats?.sum_reduction_value_currency), currencyReductionSum: parseNum(stats?.sum_reduction_value_currency),
frequentFailuresReductionSum: parseNum(stats?.sum_reducing_breakdowns), frequentFailuresReductionSum: parseNum(stats?.sum_reducing_breakdowns),
percentProductionStops: stats?.percent_sum_stopping_production, percentProductionStops: stats?.percent_sum_stopping_production,
percentOperatingCostBeforeInnovation:
stats?.percent_operating_cost_before_innovation,
percentBottleneckRemoval: stats?.percent_throat_removal, percentBottleneckRemoval: stats?.percent_throat_removal,
percentCurrencyReduction: stats?.percent_reduction_value_currency, percentCurrencyReduction: stats?.percent_reduction_value_currency,
percentFailuresReduction: stats?.percent_reducing_breakdowns, percentFailuresReduction: stats?.percent_reducing_breakdowns,
@ -497,6 +509,10 @@ export function ProcessInnovationPage() {
...prev.currencyreduction, ...prev.currencyreduction,
value: formatNumber(normalized.currencyReductionSum), value: formatNumber(normalized.currencyReductionSum),
}, },
decreaseCurrencyOperation: {
...prev.decreaseCurrencyOperation,
value: formatNumber(normalized.reductionCostOprationSum),
},
})); }));
setStats(normalized); setStats(normalized);
} catch (error) { } catch (error) {
@ -623,13 +639,14 @@ export function ProcessInnovationPage() {
<div className="flex gap-4"> <div className="flex gap-4">
<div className="space-y-4 w-full"> <div className="space-y-4 w-full">
{/* Stats Grid */} {/* Stats Grid */}
<div className="grid grid-cols-2 gap-3"> <div className="h-full">
{loading || statsLoading {loading || statsLoading ? (
? // Loading skeleton for stats cards - matching new design // Skeleton cards
Array.from({ length: 4 }).map((_, index) => ( <div className="flex flex-wrap justify-between gap-3">
{Array.from({ length: 6 }).map((_, index) => (
<BaseCard <BaseCard
key={`skeleton-${index}`} key={`skeleton-${index}`}
className="rounded-2xl overflow-hidden" className="rounded-2xl overflow-hidden w-full sm:w-[48%] md:w-[30%]"
> >
<div className="flex flex-col justify-between gap-2"> <div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20"> <div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
@ -637,7 +654,7 @@ export function ProcessInnovationPage() {
className="h-6 bg-gray-600 rounded animate-pulse" className="h-6 bg-gray-600 rounded animate-pulse"
style={{ width: "60%" }} style={{ width: "60%" }}
/> />
<div className="p-3 rounded-full w-fit"> <div className="p-3 rounded-full w-fit">
<div className="w-6 h-6 bg-gray-600 rounded animate-pulse" /> <div className="w-6 h-6 bg-gray-600 rounded animate-pulse" />
</div> </div>
</div> </div>
@ -653,42 +670,112 @@ export function ProcessInnovationPage() {
</div> </div>
</div> </div>
</BaseCard> </BaseCard>
)) ))}
: Object.entries(stateCard).map(([key, card]) => { </div>
// map percent values for each card key ) : (
const percentMap: Record< <div className="flex flex-col h-full gap-5">
string, <div className="flex flex-row gap-4 h-full">
number | string | undefined <BaseCard
> = { key={stateCard.productionstopsprevention.id}
productionstopsprevention: stats.percentProductionStops, title={stateCard.productionstopsprevention.title}
bottleneckremoval: stats.percentBottleneckRemoval, className="border-gray-700/50 w-full"
currencyreduction: stats.percentCurrencyReduction, icon={stateCard.productionstopsprevention.icon}
frequentfailuresreduction: stats.percentFailuresReduction, >
}; <div className="flex items-center justify-center flex-col">
const percentValue = percentMap[key]; <div className="flex items-center gap-4">
<div className="text-center">
return ( <p className="text-3xl text-pr-green font-bold mb-1">
<BaseCard {stateCard.productionstopsprevention.value}
key={card.id} </p>
title={card.title} <div className="text-[11px] text-[#ACACAC] font-light font-persian">
className="border-gray-700/50" {stateCard.productionstopsprevention.description}
icon={card.icon}
>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{card.value}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{card.description}
</div>
</div> </div>
</div> </div>
</div> </div>
</BaseCard> </div>
); </BaseCard>
})}
<BaseCard
key={stateCard.frequentfailuresreduction.id}
title={stateCard.frequentfailuresreduction.title}
className="border-gray-700/50 w-full"
icon={stateCard.frequentfailuresreduction.icon}
>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{stateCard.frequentfailuresreduction.value}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{stateCard.frequentfailuresreduction.description}
</div>
</div>
</div>
</div>
</BaseCard>
</div>
<div className="flex flex-row gap-4 h-full">
<BaseCard
key={stateCard.currencyreduction.id}
title={stateCard.currencyreduction.title}
className="border-gray-700/50 w-full"
icon={stateCard.currencyreduction.icon}
>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{stateCard.currencyreduction.value}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{stateCard.currencyreduction.description}
</div>
</div>
</div>
</div>
</BaseCard>
<BaseCard
key={stateCard.decreaseCurrencyOperation.id}
title={stateCard.decreaseCurrencyOperation.title}
className="border-gray-700/50 w-full"
icon={stateCard.decreaseCurrencyOperation.icon}
>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{stateCard.decreaseCurrencyOperation.value}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{stateCard.decreaseCurrencyOperation.description}
</div>
</div>
</div>
</div>
</BaseCard>
<BaseCard
key={stateCard.bottleneckremoval.id}
title={stateCard.bottleneckremoval.title}
className="border-gray-700/50 w-full"
icon={stateCard.bottleneckremoval.icon}
>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{stateCard.bottleneckremoval.value}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{stateCard.bottleneckremoval.description}
</div>
</div>
</div>
</div>
</BaseCard>
</div>
</div>
)}
</div> </div>
</div> </div>
@ -705,7 +792,7 @@ export function ProcessInnovationPage() {
loading={statsLoading} loading={statsLoading}
data={[ data={[
{ {
label: "کاهش توقفات تولید", label: "توقفات تولید",
value: Number(stats.percentProductionStops) || 0, value: Number(stats.percentProductionStops) || 0,
labelColor: "text-white", labelColor: "text-white",
}, },
@ -715,17 +802,23 @@ export function ProcessInnovationPage() {
labelColor: "text-white", labelColor: "text-white",
}, },
{ {
label: "کاهش ارز بری", label: "ارز بری",
value: Number(stats.percentCurrencyReduction) || 0, value: Number(stats.percentCurrencyReduction) || 0,
labelColor: "text-white", labelColor: "text-white",
}, },
{ {
label: "کاهش خرابی پر تکرار", label: "خرابی پر تکرار",
value: Number(stats.percentFailuresReduction) || 0, value: Number(stats.percentFailuresReduction) || 0,
labelColor: "text-white", labelColor: "text-white",
}, },
{
label: "هزینه های عملیاتی",
value:
Number(stats.percentOperatingCostBeforeInnovation) || 0,
labelColor: "text-white",
},
]} ]}
barHeight="h-6" barHeight="h-5"
showAxisLabels={true} showAxisLabels={true}
/> />
</BaseCard> </BaseCard>

View File

@ -14,7 +14,6 @@ import {
PopoverTrigger, PopoverTrigger,
} from "~/components/ui/popover"; } from "~/components/ui/popover";
import jalaali from "jalaali-js";
import { import {
CartesianGrid, CartesianGrid,
Legend, Legend,
@ -42,6 +41,7 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import { Tooltip as TooltipSh, TooltipTrigger } from "~/components/ui/tooltip"; import { Tooltip as TooltipSh, TooltipTrigger } from "~/components/ui/tooltip";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatNumber, handleDataValue } from "~/lib/utils"; import { EventBus, formatNumber, handleDataValue } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import type { CalendarDate } from "~/types/util.type";
@ -199,7 +199,6 @@ export default function Timeline(valueTimeLine: string) {
export function ProductInnovationPage() { export function ProductInnovationPage() {
// const [showPopup, setShowPopup] = useState(false); // const [showPopup, setShowPopup] = useState(false);
const { jy } = jalaali.toJalaali(new Date());
const [projects, setProjects] = useState<ProductInnovationData[]>([]); const [projects, setProjects] = useState<ProductInnovationData[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
@ -264,10 +263,8 @@ export function ProductInnovationPage() {
}, },
}); });
const [date, setDate] = useState<CalendarDate>({ const [date, setDate] = useStoredDate();
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
const observerRef = useRef<HTMLDivElement>(null); const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false); const fetchingRef = useRef(false);
@ -288,9 +285,7 @@ export function ProductInnovationPage() {
// Fetch popup stats // Fetch popup stats
const statsResponse = await apiService.call({ const statsResponse = await apiService.call({
innovation_product_popup_function1: { innovation_product_popup_function1: {
project_id: project.project_id, project_id: project.project_id
start_date: startDate || null,
end_date: endDate || null,
}, },
}); });
@ -520,11 +515,11 @@ export function ProductInnovationPage() {
}, []); }, []);
useEffect(() => { useEffect(() => {
fetchProjects(true); if (date.end && date.start) fetchProjects(true);
}, [sortConfig, date]); }, [sortConfig, date]);
useEffect(() => { useEffect(() => {
fetchStats(); if (date.end && date.start) fetchStats();
}, [date]); }, [date]);
useEffect(() => { useEffect(() => {

View File

@ -1,7 +1,8 @@
import jalaali from "jalaali-js"; import { saveAs } from "file-saver";
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react"; import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
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 XLSX from "xlsx-js-style";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { Card, CardContent } from "~/components/ui/card"; import { Card, CardContent } from "~/components/ui/card";
import { import {
@ -13,8 +14,14 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils"; import {
EventBus,
formatCurrency,
formatNumber,
handleDataValue,
} from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import type { CalendarDate } from "~/types/util.type";
import { DashboardLayout } from "../layout"; import { DashboardLayout } from "../layout";
@ -154,7 +161,6 @@ const columns: ColumnDef[] = [
]; ];
export function ProjectManagementPage() { export function ProjectManagementPage() {
const { jy } = jalaali.toJalaali(new Date());
const [projects, setProjects] = useState<ProjectData[]>([]); const [projects, setProjects] = useState<ProjectData[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
@ -171,10 +177,12 @@ export function ProjectManagementPage() {
const fetchingRef = useRef(false); const fetchingRef = useRef(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null); const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
const [date, setDate] = useState<CalendarDate>({ // const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`, // start: `${jy}/01/01`,
end: `${jy}/12/30`, // end: `${jy}/12/30`,
}); // });
const [date, setDate] = useStoredDate();
const fetchProjects = async (reset = false) => { const fetchProjects = async (reset = false) => {
// Prevent concurrent API calls // Prevent concurrent API calls
@ -275,11 +283,15 @@ export function ProjectManagementPage() {
}; };
useEffect(() => { useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => { const handler = (date: CalendarDate) => {
if (date) { if (date) setDate(date);
setDate(date); };
}
}); EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []); }, []);
const loadMore = useCallback(() => { const loadMore = useCallback(() => {
if (hasMore && !loading && !loadingMore && !fetchingRef.current) { if (hasMore && !loading && !loadingMore && !fetchingRef.current) {
@ -288,8 +300,10 @@ export function ProjectManagementPage() {
}, [hasMore, loading, loadingMore]); }, [hasMore, loading, loadingMore]);
useEffect(() => { useEffect(() => {
fetchProjects(true); if (date.end && date.start) {
fetchTotalCount(); fetchProjects(true);
fetchTotalCount();
}
}, [sortConfig, date]); }, [sortConfig, date]);
useEffect(() => { useEffect(() => {
@ -783,13 +797,88 @@ export function ProjectManagementPage() {
} }
}; };
const totalPages = Math.ceil(totalCount / pageSize); // const totalPages = Math.ceil(totalCount / pageSize);
const exportToExcel = async () => {
let arr = [];
const data = await fetchExcelData();
debugger;
for (let i = 0; i < data.length; i++) {
let obj: Record<string, any> = {};
const project = data[i];
Object.entries(project).forEach(([pKey, pValue]) => {
Object.values(columns).forEach((col) => {
if (pKey === col.key) {
``;
obj[col.label] = handleDataValue(
pValue?.includes(",") ? pValue.replaceAll(",", "") : pValue
);
}
});
});
arr.push(obj);
}
// تبدیل داده‌ها به worksheet
const worksheet = XLSX.utils.json_to_sheet(arr);
// ساخت workbook
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "People");
// تبدیل به فایل باینری
const excelBuffer = XLSX.write(workbook, {
bookType: "xlsx",
type: "array",
});
const blob = new Blob([excelBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
saveAs(blob, "people.xls");
};
const fetchExcelData = async () => {
const fetchableColumns = columns.filter((c) => !c.computed);
const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key);
const sortCol = columns.find((c) => c.key === sortConfig.field);
const sortField = sortCol?.computed
? undefined
: (sortCol?.apiField ?? sortCol?.key);
const response = await apiService.select({
ProcessName: "project",
OutputFields: outputFields,
Sorts: sortField ? [[sortField, sortConfig.direction]] : [],
Conditions: [
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
});
const parsedData = JSON.parse(response.data);
return parsedData;
};
return ( return (
<DashboardLayout title="مدیریت پروژه‌ها"> <DashboardLayout title="مدیریت پروژه‌ها">
<div className="space-y-6"> <div className="space-y-6">
{/* <div className="flex justify-end w-full mb-0 pl-2">
<Button
className="flex w-max justify-center rounded-xl mb-4 border-gray-500/20 border-2 cursor-pointer transition-all hover:bg-[#3F415A]/50 bg-[#3F415A] py-3 text-center items-center gap-3 "
variant="ghost"
size="sm"
onClick={exportToExcel}
>
<FileChartColumnIncreasing />
دانلود فایل اکسل
</Button>
</div> */}
{/* Data Table */} {/* Data Table */}
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden"> <Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
{/* <div onClick={exportToExcel}>DownloadExcle</div> */}
<CardContent className="p-0"> <CardContent className="p-0">
<div className="relative"> <div className="relative">
<div <div

View File

@ -110,12 +110,12 @@ const menuItems: MenuItem[] = [
]; ];
const bottomMenuItems: MenuItem[] = [ const bottomMenuItems: MenuItem[] = [
{ // {
id: "settings", // id: "settings",
label: "تنظیمات", // label: "تنظیمات",
icon: Settings, // icon: Settings,
href: "/dashboard/settings", // href: "/dashboard/settings",
}, // },
{ {
id: "logout", id: "logout",
label: "خروج", label: "خروج",

View File

@ -1,4 +1,3 @@
import jalaali from "jalaali-js";
import { useEffect, useReducer, useRef, useState } from "react"; import { useEffect, useReducer, useRef, useState } from "react";
import { import {
Bar, Bar,
@ -12,6 +11,7 @@ import {
} from "recharts"; } from "recharts";
import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog";
import { Skeleton } from "~/components/ui/skeleton"; import { Skeleton } from "~/components/ui/skeleton";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils"; import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import type { CalendarDate } from "~/types/util.type";
@ -118,7 +118,6 @@ export function StrategicAlignmentPopup({
open, open,
onOpenChange, onOpenChange,
}: StrategicAlignmentPopupProps) { }: StrategicAlignmentPopupProps) {
const { jy } = jalaali.toJalaali(new Date());
const [data, setData] = useState<StrategicAlignmentData[]>([]); const [data, setData] = useState<StrategicAlignmentData[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const contentRef = useRef<HTMLDivElement | null>(null); const contentRef = useRef<HTMLDivElement | null>(null);
@ -128,10 +127,8 @@ export function StrategicAlignmentPopup({
dropDownItems: [], dropDownItems: [],
}); });
const [date, setDate] = useState<CalendarDate>({ const [date, setDate] = useStoredDate();
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
useEffect(() => { useEffect(() => {
if (open) { if (open) {
fetchData(); fetchData();
@ -139,11 +136,15 @@ export function StrategicAlignmentPopup({
}, [open]); }, [open]);
useEffect(() => { useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => { const handler = (date: CalendarDate) => {
if (date) { if (date) setDate(date);
setDate(date); };
}
}); EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []); }, []);
const fetchData = async () => { const fetchData = async () => {

View File

@ -12,6 +12,7 @@ import {
} from "recharts"; } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { CustomBarChart } from "~/components/ui/custom-bar-chart"; import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils"; import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import type { CalendarDate } from "~/types/util.type";
@ -63,17 +64,23 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
const [counts, setCounts] = useState<EcosystemCounts | null>(null); const [counts, setCounts] = useState<EcosystemCounts | null>(null);
const [processData, setProcessData] = useState<ProcessActorsData[]>([]); const [processData, setProcessData] = useState<ProcessActorsData[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [date, setDate] = useState<CalendarDate>(); // const [date, setDate] = useState<CalendarDate>();
const [date, setDate] = useStoredDate();
useEffect(() => { useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => { const handler = (date: CalendarDate) => {
if (date) { if (date) setDate(date);
setDate(date); };
}
}); EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []); }, []);
useEffect(() => { useEffect(() => {
fetchCounts(); if (date.end && date.start) fetchCounts();
}, [date]); }, [date]);
const fetchCounts = async () => { const fetchCounts = async () => {
@ -183,7 +190,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
{ label: "شتابدهنده", value: parseNumber(counts.accelerator_count) }, { label: "شتابدهنده", value: parseNumber(counts.accelerator_count) },
{ label: "دانشگاه", value: parseNumber(counts.university_count) }, { label: "دانشگاه", value: parseNumber(counts.university_count) },
{ label: "صندوق های مالی", value: parseNumber(counts.fund_count) }, { label: "صندوق های مالی", value: parseNumber(counts.fund_count) },
{ label: "شرکت", value: parseNumber(counts.company_count) }, { label: "تامین کننده", value: parseNumber(counts.company_count) },
] ]
: []; : [];

View File

@ -1,12 +1,19 @@
import * as d3 from "d3"; import * as d3 from "d3";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useStoredDate } from "~/hooks/useStoredDate";
import { EventBus } from "~/lib/utils"; import { EventBus } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import type { CalendarDate } from "~/types/util.type";
import { useAuth } from "../../contexts/auth-context"; import { useAuth } from "../../contexts/auth-context";
import apiService from "../../lib/api"; import apiService from "../../lib/api";
const API_BASE_URL = const API_BASE_URL =
import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api"; //بندر امام
// import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
//آپادانا
import.meta.env.VITE_API_URL || "https://APADANA-IATM-back.pelekan.org/api";
//نوری
// import.meta.env.VITE_API_URL || "https://NOPC-IATM-back.pelekan.org/api";
export interface Node { export interface Node {
id: string; id: string;
@ -73,13 +80,19 @@ export function NetworkGraph({
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { token } = useAuth(); const { token } = useAuth();
const [date, setDate] = useState<CalendarDate>(); // const [date, setDate] = useState<CalendarDate>();
const [date, setDate] = useStoredDate();
useEffect(() => { useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => { const handler = (date: CalendarDate) => {
if (date) { if (date) setDate(date);
setDate(date); };
}
}); EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -102,8 +115,8 @@ export function NetworkGraph({
return await apiService.call<any>({ return await apiService.call<any>({
get_values_workflow_function: { get_values_workflow_function: {
stage_id: stage_id, stage_id: stage_id,
start_date: date?.start || null, // start_date: date?.start || null,
end_date: date?.end || null, // end_date: date?.end || null,
}, },
}); });
}, },
@ -120,7 +133,10 @@ export function NetworkGraph({
setIsLoading(true); setIsLoading(true);
try { try {
const res = await apiService.call<any[]>({ const res = await apiService.call<any[]>({
graph_production_function: {}, graph_production_function: {
start_date: date.start || null,
end_date: date.end || null,
},
}); });
if (aborted) return; if (aborted) return;
@ -133,7 +149,9 @@ export function NetworkGraph({
// نود مرکزی // نود مرکزی
const centerNode: Node = { const centerNode: Node = {
id: "center", id: "center",
label: "پتروشیمی بندر امام", // label: "پتروشیمی بندر امام",
// label: "پتروشیمی نوری",
label: "پتروشیمی آپادانا",
category: "center", category: "center",
stageid: 0, stageid: 0,
isCenter: true, isCenter: true,
@ -188,7 +206,7 @@ export function NetworkGraph({
aborted = true; aborted = true;
controller.abort(); controller.abort();
}; };
}, [isMounted, token, getImageUrl]); }, [isMounted, token, getImageUrl, date]);
useEffect(() => { useEffect(() => {
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) if (!isMounted || !svgRef.current || isLoading || nodes.length === 0)
@ -232,7 +250,7 @@ export function NetworkGraph({
مشاور: "#10B981", مشاور: "#10B981",
"دانش بنیان": "#F59E0B", "دانش بنیان": "#F59E0B",
استارتاپ: "#EF4444", استارتاپ: "#EF4444",
شرکت: "#8B5CF6", "تامین کننده": "#8B5CF6",
صندوق: "#06B6D4", صندوق: "#06B6D4",
شتابدهنده: "#9333EA", شتابدهنده: "#9333EA",
"مرکز نوآوری": "#F472B6", "مرکز نوآوری": "#F472B6",
@ -357,41 +375,90 @@ export function NetworkGraph({
nodeGroup.each(function (d) { nodeGroup.each(function (d) {
const group = d3.select(this); const group = d3.select(this);
if (d.isCenter) { // if (d.isCenter) {
const rect = group // const rect = group
.append("rect") // .append("rect")
.attr("width", 200) // .attr("width", 200)
.attr("height", 80) // .attr("height", 80)
.attr("x", -100) // نصف عرض جدید منفی // .attr("x", -100) // نصف عرض جدید منفی
.attr("y", -40) // نصف ارتفاع جدید منفی // .attr("y", -40) // نصف ارتفاع جدید منفی
.attr("rx", 8) // .attr("rx", 8)
.attr("ry", 8) // .attr("ry", 8)
.attr("fill", categoryToColor[d.category] || "#94A3B8") // .attr("fill", categoryToColor[d.category] || "#94A3B8")
.attr("stroke", "#FFFFFF") // .attr("stroke", "#FFFFFF")
.attr("stroke-width", 3) // .attr("stroke-width", 3)
.style("pointer-events", "none"); // .style("pointer-events", "none");
if (d.imageUrl || d.isCenter) { // if (d.imageUrl || d.isCenter) {
const pattern = defs // const pattern = defs
.append("pattern") // .append("pattern")
.attr("id", `image-${d.id}`) // .attr("id", `image-${d.id}`)
.attr("x", 0) // .attr("x", 0)
.attr("y", 0) // .attr("y", 0)
.attr("width", 1) // .attr("width", 1)
.attr("height", 1); // .attr("height", 1);
pattern // pattern
.append("image") // .append("image")
.attr("x", 0) // .attr("x", 0)
.attr("y", 0) // .attr("y", 0)
.attr("width", 200) // ← هم‌اندازه با مستطیل // .attr("width", 200) // ← هم‌اندازه با مستطیل
.attr("height", 80) // .attr("height", 80)
.attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl) // .attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
.attr("preserveAspectRatio", "xMidYMid slice"); // .attr("preserveAspectRatio", "xMidYMid slice");
rect.attr("fill", `url(#image-${d.id})`); // rect.attr("fill", `url(#image-${d.id})`);
} // }
} else { // }
// راه حل ساده‌تر - ابعاد ثابت با حفظ نسبت
if (d.isCenter) {
//آپادانا
const fixedWidth = 198;
const fixedHeight = 200; // یا می‌توانید براساس نسبت تصویر محاسبه کنید
//بندر امام
// const fixedWidth = 100;
// const fixedHeight = 80; // یا می‌توانید براساس نسبت تصویر محاسبه کنید
//نوری
// const fixedWidth = 100;
// const fixedHeight = 80; // یا می‌توانید براساس نسبت تصویر محاسبه کنید
const rect = group
.append("rect")
.attr("width", fixedWidth)
.attr("height", fixedHeight)
.attr("x", -fixedWidth / 2)
.attr("y", -fixedHeight / 2)
.attr("rx", 8)
.attr("ry", 8)
.attr("fill", categoryToColor[d.category] || "#94A3B8")
.attr("stroke", "#FFFFFF")
.attr("stroke-width", 3)
.style("pointer-events", "none");
const pattern = defs
.append("pattern")
.attr("id", `image-${d.id}`)
.attr("x", 0)
.attr("y", 0)
.attr("width", 1)
.attr("height", 1);
pattern
.append("image")
.attr("x", 0)
.attr("y", 0)
.attr("width", fixedWidth)
.attr("height", fixedHeight)
.attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
.attr("preserveAspectRatio", "xMidYMid meet"); // حفظ نسبت تصویر
rect.attr("fill", `url(#image-${d.id})`);
}
else {
const circle = group const circle = group
.append("circle") .append("circle")
.attr("r", 25) .attr("r", 25)
@ -431,16 +498,31 @@ export function NetworkGraph({
}); });
const labels = nodeGroup const labels = nodeGroup
.append("text") .append("text")
.text((d) => d.label) .text((d) => d.label)
.attr("text-anchor", "middle") .attr("text-anchor", "middle")
.attr("dy", (d) => (d.isCenter ? 50 : 45)) .attr("dy", (d) => {
.attr("font-size", (d) => (d.isCenter ? "14px" : "12px")) if (d.isCenter) {
.attr("font-weight", "bold")
.attr("fill", "#F9FAFB") //آپادانا
.attr("stroke", "rgba(17, 24, 39, 0.95)") const centerNodeHeight = 200; // ارتفاع نود مرکزی
.attr("stroke-width", 4)
.attr("paint-order", "stroke"); //بندر امام
// const centerNodeHeight = 80; // ارتفاع نود مرکزی
//نوری
// const centerNodeHeight = 80; // ارتفاع نود مرکزی
return centerNodeHeight / 2 + 20; // نصف ارتفاع + فاصله 20px
}
return 45; // برای نودهای دیگر
})
.attr("font-size", (d) => (d.isCenter ? "14px" : "12px"))
.attr("font-weight", "bold")
.attr("fill", "#F9FAFB")
.attr("stroke", "rgba(17, 24, 39, 0.95)")
.attr("stroke-width", 4)
.attr("paint-order", "stroke");
nodeGroup nodeGroup
.on("mouseenter", function (event, d) { .on("mouseenter", function (event, d) {
@ -484,35 +566,37 @@ export function NetworkGraph({
onLoadingChange?.(true); onLoadingChange?.(true);
try { try {
const res = await callAPI(d.stageid); if (date.start && date.end) {
const responseData = JSON.parse(res.data); const res = await callAPI(d.stageid);
const fieldValues = const responseData = JSON.parse(res.data);
JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || []; const fieldValues =
JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || [];
const filteredFields = fieldValues.filter( const filteredFields = fieldValues.filter(
(field: any) => (field: any) =>
!["image", "img", "full_name", "about_collaboration"].includes( !["image", "img", "full_name", "about_collaboration"].includes(
field.F.toLowerCase() field.F.toLowerCase()
) )
); );
const descriptionField = fieldValues.find( const descriptionField = fieldValues.find(
(field: any) => (field: any) =>
field.F.toLowerCase().includes("description") || field.F.toLowerCase().includes("description") ||
field.F.toLowerCase().includes("about_collaboration") || field.F.toLowerCase().includes("about_collaboration") ||
field.F.toLowerCase().includes("about") field.F.toLowerCase().includes("about")
); );
const companyDetails: CompanyDetails = { const companyDetails: CompanyDetails = {
id: d.id, id: d.id,
label: d.label, label: d.label,
category: d.category, category: d.category,
stageid: d.stageid, stageid: d.stageid,
fields: filteredFields, fields: filteredFields,
description: descriptionField?.V || undefined, description: descriptionField?.V || undefined,
}; };
onNodeClick(companyDetails); onNodeClick(companyDetails);
}
} catch (error) { } catch (error) {
console.error("Failed to fetch company details:", error); console.error("Failed to fetch company details:", error);
// Keep the basic details already shown // Keep the basic details already shown
@ -541,7 +625,7 @@ export function NetworkGraph({
return () => { return () => {
simulation.stop(); simulation.stop();
}; };
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI]); }, [nodes, links, isLoading, isMounted, onNodeClick, callAPI, date]);
if (error) { if (error) {
return ( return (

View File

@ -7,7 +7,7 @@ interface BaseCardProps {
headerClassName?: string; headerClassName?: string;
contentClassName?: string; contentClassName?: string;
children: React.ReactNode; children: React.ReactNode;
icon ?: React.ComponentType<{ className?: string }>; icon?: React.ComponentType<{ className?: string }>;
withHeader?: boolean; withHeader?: boolean;
} }
@ -18,32 +18,44 @@ export function BaseCard({
contentClassName, contentClassName,
children, children,
withHeader = false, withHeader = false,
icon : Icon, icon: Icon,
}: BaseCardProps) { }: BaseCardProps) {
return ( return (
<Card <Card
className={cn( className={cn(
"bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm py-4 grid items-center", "bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm py-2 pb-0 grid items-center",
className className
)} )}
> >
{Icon && title ? ( {Icon && title ? (
<CardHeader className={cn("border-b-2 border-gray-500/20 py-2 px-0 pb-4", headerClassName)}> <CardHeader
<CardTitle className="text-white text-sm text-right font-persian px-4 my-auto items-center flex w-full justify-between">{title} {<Icon />} </CardTitle> className={cn(
"border-b-2 border-gray-500/20 py-2 px-0 pb-4",
headerClassName
)}
>
<CardTitle className="text-white text-sm text-right font-persian px-4 my-auto items-center flex w-full justify-between">
{title} {<Icon />}{" "}
</CardTitle>
</CardHeader> </CardHeader>
) : ) : withHeader && title ? (
withHeader && title ? ( <CardHeader
<CardHeader className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}> 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> >
<CardTitle className="text-white text-sm text-right font-persian px-4">
{title}
</CardTitle>
</CardHeader> </CardHeader>
) : title ? ( ) : title ? (
<div className="border-b-2 border-gray-500/20 pb-2"> <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> <h3 className="text-sm font-bold text-white text-right font-persian px-4">
{title}
</h3>
</div> </div>
) : null} ) : null}
<CardContent className={cn("py-2 px-4", contentClassName)}> <CardContent className={cn("py-2 px-4 ", contentClassName)}>
{children} {children}
</CardContent> </CardContent>
</Card> </Card>
); );
} }

View File

@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm ", "rounded-lg border bg-card text-card-foreground shadow-sm",
className className
)} )}
{...props} {...props}

View File

@ -39,7 +39,7 @@ export function CustomBarChart({
// Loading skeleton // Loading skeleton
if (loading) { if (loading) {
return ( return (
<div className={`space-y-6 p-4 ${className}`} style={{ height }}> <div className={`space-y-6 p-4 pt-0 ${className}`} style={{ height }}>
{title && ( {title && (
<div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div> <div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div>
)} )}
@ -71,7 +71,7 @@ export function CustomBarChart({
<div className={`space-y-6 ${className}`} style={{ height }}> <div className={`space-y-6 ${className}`} style={{ height }}>
{title && ( {title && (
<div className="border-b-[#3F415A] border-b-2"> <div className="border-b-[#3F415A] border-b-2">
<h3 className="text-sm font-semibold text-white font-persian text-right p-4"> <h3 className="text-sm font-semibold text-white font-persian text-right px-4 pb-3">
{title} {title}
</h3> </h3>
</div> </div>

View File

@ -17,32 +17,32 @@ export function MetricCard({
percentLabel = "درصد به کل", percentLabel = "درصد به کل",
}: MetricCardProps) { }: MetricCardProps) {
return ( return (
<BaseCard title={title}> <BaseCard title={title} className="h-full">
<div className="flex items-center justify-center flex-col"> <div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4 h-full">
<div className="text-center"> <div className="text-center">
<p className="text-3xl font-bold text-green-400"> <p className="text-3xl font-bold text-green-400">
{formatNumber(value)} {formatNumber(value)}
</p> </p>
<div className="text-xs text-gray-400 font-persian"> <div className="text-xs text-gray-400 font-persian">
{valueLabel} {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>
</div> </div>
</BaseCard> {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

@ -6,24 +6,46 @@ import { cn, formatNumber } from "~/lib/utils"
const Progress = React.forwardRef< const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>, React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => ( >(({ className, value, ...props }, ref) => {
<ProgressPrimitive.Root // Dynamic scaling logic based on value ranges
ref={ref} const getScaledValue = (inputValue: number) => {
className={cn( const numValue = Number(inputValue);
"relative h-4 w-full overflow-hidden rounded-full bg-pr-gray", if (numValue <= 1) {
className return numValue * 100;
)} }
{...props} else if (numValue <= 10) {
> return (numValue / 10) * 100;
<span className="left-0 text-sm absolute z-10 px-2 text-[#5F6284]">۰%</span> } else if (numValue <= 50) {
<span className="w-full right-0 text-sm absolute z-10 px-2 text-[#5F6284]" return (numValue / 50) * 100;
>{formatNumber(Math.ceil(value || 0 * 10) / 10)}%</span> }
<ProgressPrimitive.Indicator else {
className="h-full w-full flex-1 bg-pr-green transition-all" return numValue
style={{ transform: `translateX(-${15 - (value || 0)}%)` }} }
/> };
</ProgressPrimitive.Root>
)) const scaledValue = getScaledValue(Number(value) || 0);
const displayValue = Number(value) || 0;
return (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-pr-gray",
className
)}
{...props}
>
<span className="left-0 text-sm absolute z-10 px-2 text-[#5F6284]">۰%</span>
<span className="w-full right-0 text-sm absolute z-10 px-2 text-[#5F6284]">
{formatNumber(displayValue.toFixed(2))}%
</span>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-pr-green transition-all z-20"
style={{ transform: `translateX(-${100-scaledValue}%)` }}
/>
</ProgressPrimitive.Root>
)
})
Progress.displayName = ProgressPrimitive.Root.displayName Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress } export { Progress }

View File

@ -52,7 +52,7 @@ function TooltipContent({
{...props} {...props}
> >
{children} {children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> <TooltipPrimitive.Arrow className={cn("bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]",className)} />
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
) )

View File

@ -0,0 +1,27 @@
import jalaali from "jalaali-js";
import { useEffect, useState } from "react";
import type { CalendarDate } from "~/types/util.type";
const { jy } = jalaali.toJalaali(new Date());
export function useStoredDate(): [
CalendarDate,
React.Dispatch<React.SetStateAction<CalendarDate>>,
] {
const [date, setDate] = useState<CalendarDate>({});
useEffect(() => {
const storedDate = localStorage.getItem("dateSelected");
if (storedDate) {
setDate(JSON.parse(storedDate));
} else {
setDate({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
}
}, [jy]);
return [date, setDate];
}

View File

@ -162,10 +162,24 @@ class ApiService {
// Innovation process function call wrapper // Innovation process function call wrapper
public async call<T = any>(payload: any) { public async call<T = any>(payload: any) {
//بندر امام
const url = "https://inogen-back.pelekan.org/api/call"; const url = "https://inogen-back.pelekan.org/api/call";
//آپادانا
const url = "https://APADANA-IATM-back.pelekan.org/api/call";
//نوری
const url = "https://NOPC-IATM-back.pelekan.org/api/call";
return this.postAbsolute<T>(url, payload); return this.postAbsolute<T>(url, payload);
} }
const API_BASE_URL =
//بندر امام
// import.meta.env.VITE_API_URL || "https://inogen-bpms-back.pelekan.org/api";
//آپادانا
import.meta.env.VITE_API_URL || "https://APADANA-IATM-back.pelekan.org/api";
//نوری
// import.meta.env.VITE_API_URL || "https://NOPC-IATM-back.pelekan.org/api";
// GET request // GET request
public async get<T = any>(endpoint: string): Promise<ApiResponse<T>> { public async get<T = any>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { return this.request<T>(endpoint, {

View File

@ -1,28 +1,33 @@
import type { Route } from "./+types/ecosystem"; import moment from "moment-jalaali";
import React from "react"; import React from "react";
import { ProtectedRoute } from "~/components/auth/protected-route"; import { ProtectedRoute } from "~/components/auth/protected-route";
import { DashboardLayout } from "~/components/dashboard/layout"; import { DashboardLayout } from "~/components/dashboard/layout";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { InfoPanel } from "~/components/ecosystem/info-panel";
import { NetworkGraph } from "~/components/ecosystem/network-graph";
import { Card, CardContent } from "~/components/ui/card";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "~/components/ui/dialog"; } from "~/components/ui/dialog";
import { NetworkGraph } from "~/components/ecosystem/network-graph";
import { InfoPanel } from "~/components/ecosystem/info-panel";
import { useAuth } from "~/contexts/auth-context"; import { useAuth } from "~/contexts/auth-context";
import moment from "moment-jalaali"; import type { Route } from "./+types/ecosystem";
// Get API base URL at module level to avoid process.env access in browser // Get API base URL at module level to avoid process.env access in browser
const API_BASE_URL = const API_BASE_URL =
import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api"; //بندر امام
// import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
//آپادانا
import.meta.env.VITE_API_URL || "https://APADANA-IATM-back.pelekan.org/api";
//نوری
// import.meta.env.VITE_API_URL || "https://NOPC-IATM-back.pelekan.org/api";
// Import the CompanyDetails type // Import the CompanyDetails type
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
import { formatNumber } from "~/lib/utils";
import { Hexagon } from "lucide-react"; import { Hexagon } from "lucide-react";
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
export function meta({}: Route.MetaArgs) { export function meta({}: Route.MetaArgs) {
return [ return [
@ -89,7 +94,10 @@ export default function EcosystemPage() {
<div className="lg:col-span-8 h-full"> <div className="lg:col-span-8 h-full">
<Card className="h-full overflow-hidden bg-transparent border-[#3F415A]"> <Card className="h-full overflow-hidden bg-transparent border-[#3F415A]">
<CardContent className="p-0 h-full bg-transparent"> <CardContent className="p-0 h-full bg-transparent">
<NetworkGraph onNodeClick={handleNodeClick} onLoadingChange={handleLoadingChange} /> <NetworkGraph
onNodeClick={handleNodeClick}
onLoadingChange={handleLoadingChange}
/>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -224,9 +232,11 @@ export default function EcosystemPage() {
</span> </span>
<span className="text-right min-w-1/3"> <span className="text-right min-w-1/3">
<span className="font-persian text-sm font-normal text-right"> <span className="font-persian text-sm font-normal text-right">
{handleValue(field.V)} {handleValue(field.V)}
{field.U && <span className="mr-1">({field.U})</span>} {field.U && (
</span> <span className="mr-1">({field.U})</span>
)}
</span>
</span> </span>
</div> </div>
))} ))}

View File

@ -1,6 +1,6 @@
export interface CalendarDate { export interface CalendarDate {
start: string; start?: string;
end: string; end?: string;
sinceMonth?: string; sinceMonth?: string;
untilMonth?: string; untilMonth?: string;
} }

5128
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"d3": "^7.9.0", "d3": "^7.9.0",
"file-saver": "^2.0.5",
"graphology": "^0.26.0", "graphology": "^0.26.0",
"isbot": "^5.1.27", "isbot": "^5.1.27",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
@ -35,11 +36,13 @@
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-router": "^7.7.0", "react-router": "^7.7.0",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"tailwind-merge": "^3.3.1" "tailwind-merge": "^3.3.1",
"xlsx-js-style": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@react-router/dev": "^7.7.0", "@react-router/dev": "^7.7.0",
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@types/file-saver": "^2.0.7",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19.1.2", "@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2", "@types/react-dom": "^19.1.2",