Compare commits
No commits in common. "main" and "feat/calendar" have entirely different histories.
main
...
feat/calen
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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,9 +212,7 @@ 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"/>}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
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;
|
||||||
|
|
@ -107,25 +106,14 @@ export function LoginBranding({
|
||||||
}: LoginBrandingProps) {
|
}: LoginBrandingProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-end">
|
{/* Top Logo */}
|
||||||
<div className="text-slate-800 font-persian">
|
<div className="flex justify-end">
|
||||||
<div className="text-lg font-bold leading-tight">
|
<div className="text-slate-800 font-persian">
|
||||||
<img
|
<div className="text-lg font-bold leading-tight">
|
||||||
src="/brand.svg?v=1"
|
<img src="/brand.svg" />
|
||||||
alt="Brand Logo"
|
</div>
|
||||||
className="w-auto h-16" // اضافه کردن سایز مشخص
|
</div>
|
||||||
onError={(e) => {
|
</div>
|
||||||
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">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
//این فایل مخصوص
|
|
||||||
//شماتیک آپادانا
|
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { formatNumber } from "~/lib/utils";
|
import { formatNumber } from "~/lib/utils";
|
||||||
|
|
||||||
|
|
@ -11,10 +8,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 = {
|
||||||
|
|
@ -23,11 +20,9 @@ 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>
|
||||||
|
|
@ -36,78 +31,58 @@ 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>
|
||||||
{hideCapacity ? (
|
<div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
|
||||||
<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>
|
||||||
{!hideCapacity && (
|
<div className="info-row">
|
||||||
<div className="info-row">
|
<div className="info-label">ظرفیت:</div>
|
||||||
<div className="info-label">ظرفیت:</div>
|
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
|
||||||
<div className="info-value capacity text-[12px]">{formatNumber(company?.capacity || 0)}</div>
|
<div className="info-unit">تن در سال</div>
|
||||||
<div className="info-unit">تن در سال</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||||
// واحدهای جدید - 4 واحد
|
// Ensure we have exactly 6 companies
|
||||||
const sample = [
|
const displayCompanies = companies;
|
||||||
{ 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" }
|
|
||||||
];
|
|
||||||
|
|
||||||
|
// Positions inside a 5x4 grid (col, row)
|
||||||
|
// Layout keeps same visual logic: left/middle/right on two bands with spacing grid around
|
||||||
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 = [
|
const gridPositions = [
|
||||||
{ col: 2, row: 1, colI: 1, rowI: 1, name: "واحد 100" }, // ردیف اول - ستون اول
|
{ col: 2, row: 2 , colI : 1 , rowI : 2 , name : "بسپاران"}, // left - top band
|
||||||
{ col: 4, row: 1, colI: 5, rowI: 1, name: "واحد 200" }, // ردیف اول - ستون دوم
|
{ col: 3, row: 2 , colI : 3 , rowI : 1 , name : "خوارزمی"}, // middle top (image sits in row 2, info box goes to row 1)
|
||||||
{ col: 2, row: 3, colI: 1, rowI: 3, name: "واحد 300" }, // ردیف دوم - ستون اول
|
{ col: 4, row: 2 ,colI : 5 , rowI : 2 , name : "فراورش 1"}, // right - top band
|
||||||
{ col: 4, row: 3, colI: 5, rowI: 3, name: "واحد 400" }, // ردیف دوم - ستون دوم
|
{ 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 (
|
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
|
||||||
className={`company-item`}
|
key={company.id}
|
||||||
style={{ gridColumn: gp?.col, gridRow: gp?.row }}
|
className={`company-item`}
|
||||||
>
|
style={{ gridColumn: gp.col, gridRow: gp.row }}
|
||||||
<div className="company-image-container">
|
>
|
||||||
<img
|
<div className="company-image-containe">
|
||||||
src={company.imageUrl}
|
<img
|
||||||
alt={company.name}
|
src={company.imageUrl}
|
||||||
className="company-image"
|
alt={company.name}
|
||||||
/>
|
className="company-image"
|
||||||
</div>
|
/>
|
||||||
{company.name}
|
|
||||||
</div>
|
</div>
|
||||||
<InfoBox company={company} style={{ gridColumn: gp?.colI, gridRow: gp?.rowI }} />
|
|
||||||
</React.Fragment>
|
{company.name}
|
||||||
);
|
</div>
|
||||||
|
<InfoBox company={company} key={index +10} style={{ gridColumn: gp?.colI , gridRow: gp?.rowI }} />
|
||||||
|
</>);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -139,20 +114,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;
|
||||||
|
|
@ -160,20 +135,16 @@ 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;
|
||||||
}
|
|
||||||
|
|
||||||
.info-row:has(.info-value.revenue) {
|
&:has(.info-value.revenue) {border-bottom: 1px solid #3AEA83;}
|
||||||
border-bottom: 1px solid #3AEA83;
|
&:has(.info-value.cost) {border-bottom: 1px solid #F76276;}
|
||||||
}
|
|
||||||
|
|
||||||
.info-row:has(.info-value.cost) {
|
|
||||||
border-bottom: 1px solid #F76276;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-label {
|
.info-label {
|
||||||
|
|
@ -181,7 +152,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 {
|
||||||
|
|
@ -189,12 +160,11 @@ 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 {
|
||||||
|
|
@ -208,4 +178,4 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
//این فایل مخصوص
|
|
||||||
//شماتیک بندر امام
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
//این فایل مخصوص
|
|
||||||
//شماتیک نوری
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
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;
|
||||||
|
|
@ -59,27 +54,12 @@ 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 overflow-hidden ${item.color} rounded-lg transition-all duration-1000 ease-out flex items-center justify-end px-2`}
|
className={`h-auto gap-2 ${item.color} rounded-lg transition-all duration-1000 ease-out flex items-center justify-end px-2`}
|
||||||
style={{ width: `${widthPercentage}%` }}
|
style={{ width: `${widthPercentage}%` }}
|
||||||
>
|
>
|
||||||
{ widthPercentage > 20 ? (
|
<span className="text-[#3F415A] text-left font-persian font-medium text-sm py-1 w-max">
|
||||||
<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)}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
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";
|
||||||
|
|
@ -15,7 +16,6 @@ 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,6 +25,7 @@ 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);
|
||||||
|
|
@ -41,22 +42,19 @@ export function DashboardHome() {
|
||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const [date, setDate] = useStoredDate();
|
const [date, setDate] = useState<CalendarDate>({
|
||||||
|
start: `${jy}/01/01`,
|
||||||
|
end: `${jy}/12/30`,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
if (date) setDate(date);
|
if (date) setDate(date);
|
||||||
};
|
});
|
||||||
|
|
||||||
EventBus.on("dateSelected", handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date?.end && date?.start) fetchDashboardData();
|
fetchDashboardData();
|
||||||
}, [date]);
|
}, [date]);
|
||||||
|
|
||||||
const fetchDashboardData = async () => {
|
const fetchDashboardData = async () => {
|
||||||
|
|
@ -64,6 +62,12 @@ 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: {
|
||||||
|
|
@ -614,7 +618,7 @@ export function DashboardHome() {
|
||||||
|
|
||||||
{/* Main Content with Tabs */}
|
{/* Main Content with Tabs */}
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultValue="canvas"
|
defaultValue="charts"
|
||||||
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">
|
||||||
|
|
@ -641,39 +645,16 @@ 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> = {
|
||||||
"واحد 100": "/abniro.png" ,
|
بسپاران: "/besparan.png",
|
||||||
"واحد 200": "/besparan.png" ,
|
خوارزمی: "/khwarazmi.png",
|
||||||
"واحد 300": "/khwarazmi.png" ,
|
"فراورش 1": "/faravash1.png",
|
||||||
"واحد 400": "/faravash1.png"
|
"فراورش 2": "/faravash2.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,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,20 @@
|
||||||
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 { useLocation } from "react-router";
|
import { Link } 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, handleDataValue } from "~/lib/utils";
|
import { cn, EventBus } from "~/lib/utils";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
onToggleSidebar?: () => void;
|
onToggleSidebar?: () => void;
|
||||||
|
|
@ -67,116 +65,7 @@ const monthList: Array<MonthItem> = [
|
||||||
id: "month-4",
|
id: "month-4",
|
||||||
label: "زمستان",
|
label: "زمستان",
|
||||||
start: "10/01",
|
start: "10/01",
|
||||||
end: "12/30",
|
end: "12/29",
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
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",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -193,54 +82,22 @@ 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: "بهار",
|
||||||
useEffect(() => {
|
fromMonth: "زمستان",
|
||||||
const storedDate = localStorage.getItem("dateSelected");
|
start: `${currentYear.since}/01/01`,
|
||||||
if (storedDate) {
|
end: `${currentYear.until}/12/30`,
|
||||||
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);
|
||||||
|
|
@ -262,7 +119,6 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -276,7 +132,6 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -295,7 +150,6 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -309,7 +163,6 @@ 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();
|
||||||
};
|
};
|
||||||
|
|
@ -333,66 +186,6 @@ 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(
|
||||||
|
|
@ -448,16 +241,12 @@ 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">
|
<span className="text-md">{currentYear.since}</span>
|
||||||
{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">
|
<span className="text-md">{currentYear.until}</span>
|
||||||
{handleDataValue(currentYear.until)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -469,9 +258,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={prevFromYearHandler}
|
nextYearHandler={nextFromYearHandler}
|
||||||
prevYearHandler={nextFromYearHandler}
|
prevYearHandler={prevFromYearHandler}
|
||||||
currentYear={handleDataValue(currentYear?.since)}
|
currentYear={currentYear?.since}
|
||||||
monthList={monthList}
|
monthList={monthList}
|
||||||
selectedDate={selectedDate?.sinceMonth}
|
selectedDate={selectedDate?.sinceMonth}
|
||||||
selectDateHandler={selectFromDateHandler}
|
selectDateHandler={selectFromDateHandler}
|
||||||
|
|
@ -479,9 +268,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={prevUntilYearHandler}
|
nextYearHandler={nextUntilYearHandler}
|
||||||
prevYearHandler={nextUntilYearHandler}
|
prevYearHandler={prevUntilYearHandler}
|
||||||
currentYear={handleDataValue(currentYear?.until)}
|
currentYear={currentYear?.until}
|
||||||
monthList={monthList}
|
monthList={monthList}
|
||||||
selectedDate={selectedDate?.fromMonth}
|
selectedDate={selectedDate?.fromMonth}
|
||||||
selectDateHandler={selectUntilDateHandler}
|
selectDateHandler={selectUntilDateHandler}
|
||||||
|
|
@ -496,23 +285,9 @@ 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" />
|
||||||
|
|
@ -539,7 +314,6 @@ 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">
|
||||||
|
|
@ -551,7 +325,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"
|
||||||
|
|
@ -559,16 +333,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>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import jalaali from "jalaali-js";
|
||||||
import {
|
import {
|
||||||
BrainCircuit,
|
BrainCircuit,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
|
@ -15,7 +16,6 @@ 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,7 +34,6 @@ 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";
|
||||||
|
|
@ -149,13 +148,18 @@ 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 [date, setDate] = useStoredDate();
|
// const [totalCount, setTotalCount] = useState(0);
|
||||||
|
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[]>([]);
|
||||||
|
|
@ -360,27 +364,21 @@ export function DigitalInnovationPage() {
|
||||||
}, [hasMore, loading, loadingMore]);
|
}, [hasMore, loading, loadingMore]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date?.start && date?.end) {
|
fetchTable(true);
|
||||||
fetchTable(true);
|
fetchTotalCount();
|
||||||
fetchTotalCount();
|
fetchStats();
|
||||||
fetchStats();
|
|
||||||
}
|
|
||||||
}, [sortConfig, date]);
|
}, [sortConfig, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
if (date) setDate(date);
|
if (date) {
|
||||||
};
|
setDate(date);
|
||||||
|
}
|
||||||
EventBus.on("dateSelected", handler);
|
});
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPage > 1 && date?.start && date?.end) {
|
if (currentPage > 1) {
|
||||||
fetchTable(false);
|
fetchTable(false);
|
||||||
}
|
}
|
||||||
}, [currentPage]);
|
}, [currentPage]);
|
||||||
|
|
@ -434,7 +432,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();
|
fetchStats(date?.start, date?.end);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
|
|
@ -755,12 +753,12 @@ export function DigitalInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Process Impacts Chart */}
|
{/* Process Impacts Chart */}
|
||||||
<BaseCard className="rounded-xl w-full overflow-hidden">
|
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg w-full overflow-hidden h-[18rem]">
|
||||||
{/* <CardContent > */}
|
{/* <CardContent > */}
|
||||||
<CustomBarChart
|
<CustomBarChart
|
||||||
title="تاثیرات نوآوری دیجیتال به صورت درصد مقایسه ای"
|
title="تاثیرات نوآوری دیجیتال به صورت درصد مقایسه ای"
|
||||||
loading={statsLoading}
|
loading={statsLoading}
|
||||||
// height="100%"
|
height="100%"
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
label: DigitalCardLabel.decreasCost,
|
label: DigitalCardLabel.decreasCost,
|
||||||
|
|
@ -790,7 +788,8 @@ export function DigitalInnovationPage() {
|
||||||
barHeight="h-5"
|
barHeight="h-5"
|
||||||
showAxisLabels={true}
|
showAxisLabels={true}
|
||||||
/>
|
/>
|
||||||
</BaseCard>
|
{/* </CardContent> */}
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Data Table */}
|
{/* Data Table */}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// import moment from "moment-jalaali";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Bar,
|
Bar,
|
||||||
|
|
@ -27,6 +28,7 @@ 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,
|
||||||
|
|
@ -42,17 +44,13 @@ 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;
|
||||||
|
|
@ -161,6 +159,7 @@ 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);
|
||||||
|
|
@ -170,8 +169,10 @@ 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] = useStoredDate();
|
const [date, setDate] = useState<CalendarDate>({
|
||||||
|
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",
|
||||||
|
|
@ -361,15 +362,11 @@ export function GreenInnovationPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
if (date) setDate(date);
|
if (date) {
|
||||||
};
|
setDate(date);
|
||||||
|
}
|
||||||
EventBus.on("dateSelected", handler);
|
});
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
|
|
@ -379,14 +376,12 @@ export function GreenInnovationPage() {
|
||||||
}, [hasMore, loading]);
|
}, [hasMore, loading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.end && date.start) {
|
fetchProjects(true);
|
||||||
fetchProjects(true);
|
fetchTotalCount();
|
||||||
fetchTotalCount();
|
|
||||||
}
|
|
||||||
}, [sortConfig, date]);
|
}, [sortConfig, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.end && date.start) fetchStats();
|
fetchStats();
|
||||||
}, [selectedProjects, date]);
|
}, [selectedProjects, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -524,13 +519,13 @@ export function GreenInnovationPage() {
|
||||||
},
|
},
|
||||||
|
|
||||||
pollution: {
|
pollution: {
|
||||||
value: parseNum(stats.pollution_reduction),
|
value: formatNumber(parseNum(stats.pollution_reduction)),
|
||||||
percent: parseNum(stats.pollution_reduction_percent),
|
percent: formatNumber(parseNum(stats.pollution_reduction_percent)),
|
||||||
},
|
},
|
||||||
|
|
||||||
waste: {
|
waste: {
|
||||||
value: parseNum(stats.waste_reduction),
|
value: formatNumber(parseNum(stats.waste_reduction)),
|
||||||
percent: parseNum(stats.waste_reductionn_percent),
|
percent: formatNumber(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,
|
||||||
|
|
@ -548,6 +543,7 @@ export function GreenInnovationPage() {
|
||||||
setStatsLoading(false);
|
setStatsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPageData = (normalized: any) => {
|
const setPageData = (normalized: any) => {
|
||||||
setSustainabilityStats((prev) => ({
|
setSustainabilityStats((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -751,14 +747,39 @@ export function GreenInnovationPage() {
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
||||||
<MetricCard
|
<Card
|
||||||
key={key}
|
key={key}
|
||||||
title={value.title}
|
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
|
||||||
value={Math.round(value.total.value || 0)}
|
>
|
||||||
valueLabel={value.total?.description}
|
<CardContent className="p-0 h-full">
|
||||||
percentValue={value.percent?.value || 0}
|
<div className="flex flex-col justify-between gap-2 h-full">
|
||||||
percentLabel={value.percent?.description}
|
<div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
|
||||||
/>
|
<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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "~/components/ui/table";
|
} from "~/components/ui/table";
|
||||||
|
|
||||||
|
import jalaali from "jalaali-js";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
|
|
@ -39,8 +40,6 @@ 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";
|
||||||
|
|
@ -180,6 +179,7 @@ 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,8 +194,10 @@ export function InnovationBuiltInsidePage() {
|
||||||
field: "start_date",
|
field: "start_date",
|
||||||
direction: "asc",
|
direction: "asc",
|
||||||
});
|
});
|
||||||
|
const [date, setDate] = useState<CalendarDate>({
|
||||||
const [date, setDate] = useStoredDate();
|
start: `${jy}/01/01`,
|
||||||
|
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>>();
|
||||||
|
|
@ -426,23 +428,19 @@ export function InnovationBuiltInsidePage() {
|
||||||
}, [hasMore, loading]);
|
}, [hasMore, loading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
if (date) setDate(date);
|
if (date) {
|
||||||
};
|
setDate(date);
|
||||||
|
}
|
||||||
EventBus.on("dateSelected", handler);
|
});
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.start && date.end) fetchProjects(true);
|
fetchProjects(true);
|
||||||
}, [sortConfig, date]);
|
}, [sortConfig, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.end && date.start) fetchStats();
|
fetchStats();
|
||||||
}, [selectedProjects, date]);
|
}, [selectedProjects, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -528,13 +526,15 @@ export function InnovationBuiltInsidePage() {
|
||||||
const stats = data[0];
|
const stats = data[0];
|
||||||
const normalized: any = {
|
const normalized: any = {
|
||||||
currencySaving: {
|
currencySaving: {
|
||||||
value: parseNum(stats?.foreign_currency_saving),
|
value: formatNumber(parseNum(stats?.foreign_currency_saving)),
|
||||||
percent: parseNum(stats?.foreign_currency_saving_percent),
|
percent: formatNumber(
|
||||||
|
parseNum(stats?.foreign_currency_saving_percent)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
investmentAmount: {
|
investmentAmount: {
|
||||||
value: parseNum(stats?.investment_amount),
|
value: formatNumber(parseNum(stats?.investment_amount)),
|
||||||
percent: parseNum(stats?.investment_amount_percent),
|
percent: formatNumber(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-6 sm:grid-cols-1 xl:grid-cols-[35%_65%]">
|
<div className="space-y-4 justify-between gap-8 grid pl-3.5 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,47 +758,39 @@ export function InnovationBuiltInsidePage() {
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
||||||
<MetricCard
|
<Card
|
||||||
key={key}
|
key={key}
|
||||||
title={value.title}
|
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
|
||||||
value={Math.round(value.total.value || 0)}
|
>
|
||||||
valueLabel={value.total?.description}
|
<CardContent className="p-0 h-full">
|
||||||
percentValue={value.percent?.value || 0}
|
<div className="flex flex-col justify-between gap-2 h-full">
|
||||||
percentLabel={value.percent?.description}
|
<div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
|
||||||
/>
|
<h3 className="text-lg font-semibold text-white p-4">
|
||||||
// <Card
|
{value.title}
|
||||||
// key={key}
|
</h3>
|
||||||
// className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
|
</div>
|
||||||
// >
|
<div className="flex items-center justify-between p-6 flex-row-reverse">
|
||||||
// <CardContent className="p-0 h-full">
|
<div className="flex flex-col">
|
||||||
// <div className="flex flex-col justify-between gap-2 h-full">
|
<span className="text-3xl font-bold text-pr-green mb-1 font-persian">
|
||||||
// <div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
|
% {value.percent?.value}
|
||||||
// <h3 className="text-lg font-semibold text-white p-4">
|
</span>
|
||||||
// {value.title}
|
<span className="text-sm text-gray-400 font-persian">
|
||||||
// </h3>
|
{value.percent?.description}
|
||||||
// </div>
|
</span>
|
||||||
// <div className="flex items-center justify-between p-6 flex-row-reverse">
|
</div>
|
||||||
// <div className="flex flex-col">
|
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
|
||||||
// <span className="text-3xl font-bold text-pr-green mb-1 font-persian">
|
<div className="flex flex-col">
|
||||||
// % {value.percent?.value}
|
<span className="text-3xl font-bold text-pr-green mb-1 font-persian">
|
||||||
// </span>
|
{value.total?.value}
|
||||||
// <span className="text-sm text-gray-400 font-persian">
|
</span>
|
||||||
// {value.percent?.description}
|
<span className="text-sm text-gray-400 font-persian">
|
||||||
// </span>
|
{value.total?.description}
|
||||||
// </div>
|
</span>
|
||||||
// <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">
|
</div>
|
||||||
// {value.total?.value}
|
</CardContent>
|
||||||
// </span>
|
</Card>
|
||||||
// <span className="text-sm text-gray-400 font-persian">
|
|
||||||
// {value.total?.description}
|
|
||||||
// </span>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </CardContent>
|
|
||||||
// </Card>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{statsLoading ? (
|
{statsLoading ? (
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import jalaali from "jalaali-js";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
|
|
@ -42,7 +43,6 @@ 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,6 +261,7 @@ 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);
|
||||||
|
|
@ -275,7 +276,10 @@ export function ManageIdeasTechPage() {
|
||||||
field: "idea_title",
|
field: "idea_title",
|
||||||
direction: "asc",
|
direction: "asc",
|
||||||
});
|
});
|
||||||
const [date, setDate] = useStoredDate();
|
const [date, setDate] = useState<CalendarDate>({
|
||||||
|
start: `${jy}/01/01`,
|
||||||
|
end: `${jy}/12/30`,
|
||||||
|
});
|
||||||
|
|
||||||
// People ranking state
|
// People ranking state
|
||||||
const [peopleRanking, setPeopleRanking] = useState<PersonRanking[]>([]);
|
const [peopleRanking, setPeopleRanking] = useState<PersonRanking[]>([]);
|
||||||
|
|
@ -405,25 +409,19 @@ export function ManageIdeasTechPage() {
|
||||||
}, [hasMore, loading, loadingMore]);
|
}, [hasMore, loading, loadingMore]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
if (date) setDate(date);
|
if (date) {
|
||||||
};
|
setDate(date);
|
||||||
|
}
|
||||||
EventBus.on("dateSelected", handler);
|
});
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.end && date.start) {
|
fetchIdeas(true);
|
||||||
fetchIdeas(true);
|
fetchTotalCount();
|
||||||
fetchTotalCount();
|
fetchPeopleRanking();
|
||||||
fetchPeopleRanking();
|
fetchChartData();
|
||||||
fetchChartData();
|
fetchStatsData();
|
||||||
fetchStatsData();
|
|
||||||
}
|
|
||||||
}, [sortConfig, date]);
|
}, [sortConfig, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import jalaali from "jalaali-js";
|
||||||
import {
|
import {
|
||||||
Building2,
|
Building2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
|
@ -34,7 +35,6 @@ 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,11 +67,9 @@ 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 {
|
||||||
|
|
@ -96,11 +94,9 @@ 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 = [
|
||||||
|
|
@ -123,20 +119,24 @@ 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 [date, setDate] = useStoredDate();
|
// const [totalCount, setTotalCount] = useState(0);
|
||||||
|
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,7 +144,6 @@ 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",
|
||||||
|
|
@ -160,7 +159,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
|
||||||
|
|
@ -171,7 +170,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,
|
||||||
|
|
@ -179,7 +178,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
|
||||||
),
|
),
|
||||||
|
|
@ -187,25 +186,14 @@ 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",
|
||||||
},
|
},
|
||||||
|
|
@ -214,6 +202,15 @@ 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)) {
|
||||||
|
|
@ -340,26 +337,20 @@ export function ProcessInnovationPage() {
|
||||||
}, [hasMore, loading]);
|
}, [hasMore, loading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
if (date) setDate(date);
|
if (date) {
|
||||||
};
|
setDate(date);
|
||||||
|
}
|
||||||
EventBus.on("dateSelected", handler);
|
});
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date?.start && date?.end) {
|
fetchProjects(true);
|
||||||
fetchProjects(true);
|
fetchTotalCount();
|
||||||
fetchTotalCount();
|
|
||||||
}
|
|
||||||
}, [sortConfig, date]);
|
}, [sortConfig, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date?.start && date?.end) fetchStats();
|
fetchStats();
|
||||||
}, [selectedProjects, date]);
|
}, [selectedProjects, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -480,13 +471,10 @@ 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,
|
||||||
|
|
@ -509,10 +497,6 @@ 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) {
|
||||||
|
|
@ -639,14 +623,13 @@ 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="h-full">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{loading || statsLoading ? (
|
{loading || statsLoading
|
||||||
// Skeleton cards
|
? // Loading skeleton for stats cards - matching new design
|
||||||
<div className="flex flex-wrap justify-between gap-3">
|
Array.from({ length: 4 }).map((_, index) => (
|
||||||
{Array.from({ length: 6 }).map((_, index) => (
|
|
||||||
<BaseCard
|
<BaseCard
|
||||||
key={`skeleton-${index}`}
|
key={`skeleton-${index}`}
|
||||||
className="rounded-2xl overflow-hidden w-full sm:w-[48%] md:w-[30%]"
|
className="rounded-2xl overflow-hidden"
|
||||||
>
|
>
|
||||||
<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">
|
||||||
|
|
@ -654,7 +637,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>
|
||||||
|
|
@ -670,112 +653,42 @@ export function ProcessInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
))}
|
))
|
||||||
</div>
|
: Object.entries(stateCard).map(([key, card]) => {
|
||||||
) : (
|
// map percent values for each card key
|
||||||
<div className="flex flex-col h-full gap-5">
|
const percentMap: Record<
|
||||||
<div className="flex flex-row gap-4 h-full">
|
string,
|
||||||
<BaseCard
|
number | string | undefined
|
||||||
key={stateCard.productionstopsprevention.id}
|
> = {
|
||||||
title={stateCard.productionstopsprevention.title}
|
productionstopsprevention: stats.percentProductionStops,
|
||||||
className="border-gray-700/50 w-full"
|
bottleneckremoval: stats.percentBottleneckRemoval,
|
||||||
icon={stateCard.productionstopsprevention.icon}
|
currencyreduction: stats.percentCurrencyReduction,
|
||||||
>
|
frequentfailuresreduction: stats.percentFailuresReduction,
|
||||||
<div className="flex items-center justify-center flex-col">
|
};
|
||||||
<div className="flex items-center gap-4">
|
const percentValue = percentMap[key];
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
|
||||||
{stateCard.productionstopsprevention.value}
|
|
||||||
</p>
|
|
||||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
|
||||||
{stateCard.productionstopsprevention.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseCard>
|
|
||||||
|
|
||||||
<BaseCard
|
return (
|
||||||
key={stateCard.frequentfailuresreduction.id}
|
<BaseCard
|
||||||
title={stateCard.frequentfailuresreduction.title}
|
key={card.id}
|
||||||
className="border-gray-700/50 w-full"
|
title={card.title}
|
||||||
icon={stateCard.frequentfailuresreduction.icon}
|
className="border-gray-700/50"
|
||||||
>
|
icon={card.icon}
|
||||||
<div className="flex items-center justify-center flex-col">
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center justify-center flex-col">
|
||||||
<div className="text-center">
|
<div className="flex items-center gap-4">
|
||||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
<div className="text-center">
|
||||||
{stateCard.frequentfailuresreduction.value}
|
<p className="text-3xl text-pr-green font-bold mb-1">
|
||||||
</p>
|
{card.value}
|
||||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
</p>
|
||||||
{stateCard.frequentfailuresreduction.description}
|
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
||||||
|
{card.description}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseCard>
|
||||||
</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>
|
||||||
|
|
||||||
|
|
@ -792,7 +705,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",
|
||||||
},
|
},
|
||||||
|
|
@ -802,23 +715,17 @@ 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-5"
|
barHeight="h-6"
|
||||||
showAxisLabels={true}
|
showAxisLabels={true}
|
||||||
/>
|
/>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "~/components/ui/popover";
|
} from "~/components/ui/popover";
|
||||||
|
|
||||||
|
import jalaali from "jalaali-js";
|
||||||
import {
|
import {
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
Legend,
|
Legend,
|
||||||
|
|
@ -41,7 +42,6 @@ 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,6 +199,7 @@ 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);
|
||||||
|
|
@ -263,8 +264,10 @@ export function ProductInnovationPage() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [date, setDate] = useStoredDate();
|
const [date, setDate] = useState<CalendarDate>({
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
@ -285,7 +288,9 @@ 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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -515,11 +520,11 @@ export function ProductInnovationPage() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.end && date.start) fetchProjects(true);
|
fetchProjects(true);
|
||||||
}, [sortConfig, date]);
|
}, [sortConfig, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.end && date.start) fetchStats();
|
fetchStats();
|
||||||
}, [date]);
|
}, [date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { saveAs } from "file-saver";
|
import jalaali from "jalaali-js";
|
||||||
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 {
|
||||||
|
|
@ -14,14 +13,8 @@ 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 {
|
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
|
||||||
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";
|
||||||
|
|
||||||
|
|
@ -161,6 +154,7 @@ 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);
|
||||||
|
|
@ -177,12 +171,10 @@ 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
|
||||||
|
|
@ -283,15 +275,11 @@ export function ProjectManagementPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
if (date) setDate(date);
|
if (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) {
|
||||||
|
|
@ -300,10 +288,8 @@ export function ProjectManagementPage() {
|
||||||
}, [hasMore, loading, loadingMore]);
|
}, [hasMore, loading, loadingMore]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.end && date.start) {
|
fetchProjects(true);
|
||||||
fetchProjects(true);
|
fetchTotalCount();
|
||||||
fetchTotalCount();
|
|
||||||
}
|
|
||||||
}, [sortConfig, date]);
|
}, [sortConfig, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -797,88 +783,13 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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: "خروج",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import jalaali from "jalaali-js";
|
||||||
import { useEffect, useReducer, useRef, useState } from "react";
|
import { useEffect, useReducer, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Bar,
|
Bar,
|
||||||
|
|
@ -11,7 +12,6 @@ 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,6 +118,7 @@ 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);
|
||||||
|
|
@ -127,8 +128,10 @@ export function StrategicAlignmentPopup({
|
||||||
dropDownItems: [],
|
dropDownItems: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [date, setDate] = useStoredDate();
|
const [date, setDate] = useState<CalendarDate>({
|
||||||
|
start: `${jy}/01/01`,
|
||||||
|
end: `${jy}/12/30`,
|
||||||
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
fetchData();
|
fetchData();
|
||||||
|
|
@ -136,15 +139,11 @@ export function StrategicAlignmentPopup({
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
if (date) setDate(date);
|
if (date) {
|
||||||
};
|
setDate(date);
|
||||||
|
}
|
||||||
EventBus.on("dateSelected", handler);
|
});
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ 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";
|
||||||
|
|
@ -64,23 +63,17 @@ 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(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
if (date) setDate(date);
|
if (date) {
|
||||||
};
|
setDate(date);
|
||||||
|
}
|
||||||
EventBus.on("dateSelected", handler);
|
});
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (date.end && date.start) fetchCounts();
|
fetchCounts();
|
||||||
}, [date]);
|
}, [date]);
|
||||||
|
|
||||||
const fetchCounts = async () => {
|
const fetchCounts = async () => {
|
||||||
|
|
@ -190,7 +183,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) },
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,12 @@
|
||||||
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;
|
||||||
|
|
@ -80,19 +73,13 @@ 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(() => {
|
||||||
const handler = (date: CalendarDate) => {
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
if (date) setDate(date);
|
if (date) {
|
||||||
};
|
setDate(date);
|
||||||
|
}
|
||||||
EventBus.on("dateSelected", handler);
|
});
|
||||||
|
|
||||||
return () => {
|
|
||||||
EventBus.off("dateSelected", handler);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -115,8 +102,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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -133,10 +120,7 @@ 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;
|
||||||
|
|
||||||
|
|
@ -149,9 +133,7 @@ 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,
|
||||||
|
|
@ -206,7 +188,7 @@ export function NetworkGraph({
|
||||||
aborted = true;
|
aborted = true;
|
||||||
controller.abort();
|
controller.abort();
|
||||||
};
|
};
|
||||||
}, [isMounted, token, getImageUrl, date]);
|
}, [isMounted, token, getImageUrl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0)
|
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0)
|
||||||
|
|
@ -250,7 +232,7 @@ export function NetworkGraph({
|
||||||
مشاور: "#10B981",
|
مشاور: "#10B981",
|
||||||
"دانش بنیان": "#F59E0B",
|
"دانش بنیان": "#F59E0B",
|
||||||
استارتاپ: "#EF4444",
|
استارتاپ: "#EF4444",
|
||||||
"تامین کننده": "#8B5CF6",
|
شرکت: "#8B5CF6",
|
||||||
صندوق: "#06B6D4",
|
صندوق: "#06B6D4",
|
||||||
شتابدهنده: "#9333EA",
|
شتابدهنده: "#9333EA",
|
||||||
"مرکز نوآوری": "#F472B6",
|
"مرکز نوآوری": "#F472B6",
|
||||||
|
|
@ -375,90 +357,41 @@ 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)
|
||||||
|
|
@ -498,31 +431,16 @@ const fixedHeight = 200; // یا میتوانید براساس نسبت تص
|
||||||
});
|
});
|
||||||
|
|
||||||
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) => {
|
.attr("dy", (d) => (d.isCenter ? 50 : 45))
|
||||||
if (d.isCenter) {
|
.attr("font-size", (d) => (d.isCenter ? "14px" : "12px"))
|
||||||
|
.attr("font-weight", "bold")
|
||||||
//آپادانا
|
.attr("fill", "#F9FAFB")
|
||||||
const centerNodeHeight = 200; // ارتفاع نود مرکزی
|
.attr("stroke", "rgba(17, 24, 39, 0.95)")
|
||||||
|
.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) {
|
||||||
|
|
@ -566,37 +484,35 @@ const fixedHeight = 200; // یا میتوانید براساس نسبت تص
|
||||||
onLoadingChange?.(true);
|
onLoadingChange?.(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (date.start && date.end) {
|
const res = await callAPI(d.stageid);
|
||||||
const res = await callAPI(d.stageid);
|
const responseData = JSON.parse(res.data);
|
||||||
const responseData = JSON.parse(res.data);
|
const fieldValues =
|
||||||
const fieldValues =
|
JSON.parse(responseData?.getvalue)?.[0]?.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
|
||||||
|
|
@ -625,7 +541,7 @@ const fixedHeight = 200; // یا میتوانید براساس نسبت تص
|
||||||
return () => {
|
return () => {
|
||||||
simulation.stop();
|
simulation.stop();
|
||||||
};
|
};
|
||||||
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI, date]);
|
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -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,44 +18,32 @@ 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-2 pb-0 grid items-center",
|
"bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm py-4 grid items-center",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{Icon && title ? (
|
{Icon && title ? (
|
||||||
<CardHeader
|
<CardHeader className={cn("border-b-2 border-gray-500/20 py-2 px-0 pb-4", headerClassName)}>
|
||||||
className={cn(
|
<CardTitle className="text-white text-sm text-right font-persian px-4 my-auto items-center flex w-full justify-between">{title} {<Icon />} </CardTitle>
|
||||||
"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 ? (
|
) :
|
||||||
<CardHeader
|
withHeader && title ? (
|
||||||
className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}
|
<CardHeader className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}>
|
||||||
>
|
<CardTitle className="text-white text-sm text-right font-persian px-4">{title}</CardTitle>
|
||||||
<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">
|
<h3 className="text-sm font-bold text-white text-right font-persian px-4">{title}</h3>
|
||||||
{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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export function CustomBarChart({
|
||||||
// Loading skeleton
|
// Loading skeleton
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-6 p-4 pt-0 ${className}`} style={{ height }}>
|
<div className={`space-y-6 p-4 ${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 px-4 pb-3">
|
<h3 className="text-sm font-semibold text-white font-persian text-right p-4">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,32 +17,32 @@ export function MetricCard({
|
||||||
percentLabel = "درصد به کل",
|
percentLabel = "درصد به کل",
|
||||||
}: MetricCardProps) {
|
}: MetricCardProps) {
|
||||||
return (
|
return (
|
||||||
<BaseCard title={title} className="h-full">
|
<BaseCard title={title}>
|
||||||
<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 h-full">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-3xl font-bold text-green-400">
|
|
||||||
{formatNumber(value)}
|
|
||||||
</p>
|
|
||||||
<div className="text-xs text-gray-400 font-persian">
|
|
||||||
{valueLabel}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{percentValue !== undefined && (
|
|
||||||
<>
|
|
||||||
<span className="text-5xl font-thin text-gray-600">/</span>
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-3xl font-bold text-green-400">
|
<p className="text-3xl font-bold text-green-400">
|
||||||
{formatNumber(percentValue)}%
|
{formatNumber(value)}
|
||||||
</p>
|
</p>
|
||||||
<div className="text-xs text-gray-400 font-persian">
|
<div className="text-xs text-gray-400 font-persian">
|
||||||
{percentLabel}
|
{valueLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
{percentValue !== undefined && (
|
||||||
)}
|
<>
|
||||||
</div>
|
<span className="text-5xl font-thin text-gray-600">/</span>
|
||||||
</div>
|
<div className="text-center">
|
||||||
</BaseCard>
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,46 +6,24 @@ 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) => (
|
||||||
// Dynamic scaling logic based on value ranges
|
<ProgressPrimitive.Root
|
||||||
const getScaledValue = (inputValue: number) => {
|
ref={ref}
|
||||||
const numValue = Number(inputValue);
|
className={cn(
|
||||||
if (numValue <= 1) {
|
"relative h-4 w-full overflow-hidden rounded-full bg-pr-gray",
|
||||||
return numValue * 100;
|
className
|
||||||
}
|
)}
|
||||||
else if (numValue <= 10) {
|
{...props}
|
||||||
return (numValue / 10) * 100;
|
>
|
||||||
} else if (numValue <= 50) {
|
<span className="left-0 text-sm absolute z-10 px-2 text-[#5F6284]">۰%</span>
|
||||||
return (numValue / 50) * 100;
|
<span className="w-full right-0 text-sm absolute z-10 px-2 text-[#5F6284]"
|
||||||
}
|
>{formatNumber(Math.ceil(value || 0 * 10) / 10)}%</span>
|
||||||
else {
|
<ProgressPrimitive.Indicator
|
||||||
return numValue
|
className="h-full w-full flex-1 bg-pr-green transition-all"
|
||||||
}
|
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 }
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ function TooltipContent({
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<TooltipPrimitive.Arrow className={cn("bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]",className)} />
|
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
</TooltipPrimitive.Content>
|
</TooltipPrimitive.Content>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
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];
|
|
||||||
}
|
|
||||||
|
|
@ -162,24 +162,10 @@ 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, {
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,28 @@
|
||||||
import moment from "moment-jalaali";
|
import type { Route } from "./+types/ecosystem";
|
||||||
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 { InfoPanel } from "~/components/ecosystem/info-panel";
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
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 type { Route } from "./+types/ecosystem";
|
import moment from "moment-jalaali";
|
||||||
|
|
||||||
// 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 { Hexagon } from "lucide-react";
|
|
||||||
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
|
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
|
||||||
|
import { formatNumber } from "~/lib/utils";
|
||||||
|
import { Hexagon } from "lucide-react";
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
|
|
@ -94,10 +89,7 @@ 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
|
<NetworkGraph onNodeClick={handleNodeClick} onLoadingChange={handleLoadingChange} />
|
||||||
onNodeClick={handleNodeClick}
|
|
||||||
onLoadingChange={handleLoadingChange}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -232,11 +224,9 @@ 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 && (
|
{field.U && <span className="mr-1">({field.U})</span>}
|
||||||
<span className="mr-1">({field.U})</span>
|
</span>
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -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
5128
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -26,7 +26,6 @@
|
||||||
"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",
|
||||||
|
|
@ -36,13 +35,11 @@
|
||||||
"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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user