Compare commits
1 Commits
main
...
hotfix-scr
| Author | SHA1 | Date | |
|---|---|---|---|
| 31fa601ab2 |
|
|
@ -160,9 +160,9 @@ This document describes the exact implementation of the login page based on the
|
|||
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]"
|
||||
/>
|
||||
// <Label htmlFor="remember" className="text-white text-sm font-persian cursor-pointer">
|
||||
// همیشه متصل بمانم
|
||||
// </Label>
|
||||
<Label htmlFor="remember" className="text-white text-sm font-persian cursor-pointer">
|
||||
همیشه متصل بمانم
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ html[dir="rtl"] body {
|
|||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-dark-blue: var(--dark-blue);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
|
|
@ -126,7 +125,6 @@ html[dir="rtl"] body {
|
|||
--border: #e5e5e5;
|
||||
--input: #e5e5e5;
|
||||
--ring: #22c55e;
|
||||
--dark-blue: #33364d;
|
||||
|
||||
/* Primary color scale */
|
||||
--color-primary-50: #f0fdf4;
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
|
|||
/>
|
||||
|
||||
{/* Remember Me Checkbox */}
|
||||
{/* <div className="flex justify-end">
|
||||
<div className="flex justify-end">
|
||||
<CheckboxField
|
||||
id="remember"
|
||||
label="همیشه متصل بمان"
|
||||
|
|
@ -185,7 +185,7 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
|
|||
disabled={isLoading}
|
||||
size="md"
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* Login Button */}
|
||||
<Button
|
||||
|
|
@ -212,9 +212,7 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
|
|||
{/* Right Side - Branding */}
|
||||
<LoginSidebar>
|
||||
<LoginBranding
|
||||
// brandName="پتروشیمی آپادانا"
|
||||
brandName="پتروشیمی نوری"
|
||||
// brandName="پتروشیمی بندر امام"
|
||||
brandName="پتروشیمی بندر امام"
|
||||
engSub="Inception by Fara"
|
||||
companyName="توسعهیافته توسط شرکت رهپویان دانش و فناوری فرا"
|
||||
logo={<img src="/brand2.svg"/>}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from "react";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
|
||||
interface LoginLayoutProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
|
|
@ -107,26 +106,14 @@ export function LoginBranding({
|
|||
}: LoginBrandingProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-end">
|
||||
{/* Top Logo */}
|
||||
<div className="flex justify-end">
|
||||
<div className="text-slate-800 font-persian">
|
||||
<div className="text-lg font-bold leading-tight">
|
||||
<img
|
||||
src="/brand.svg?v=1"
|
||||
alt="Brand Logo"
|
||||
className="w-auto h-8"
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
console.log('Image failed to load');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-lg font-bold leading-tight">
|
||||
<img src="/brand.svg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Bottom Section */}
|
||||
<div className="flex flex-col gap-2 mb-4 items-end justify-end">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
//این فایل مخصوص
|
||||
//شماتیک نوری
|
||||
import React from "react";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
|
||||
|
|
@ -27,12 +25,17 @@ const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
|
|||
<div className={`info-box`} style={style}>
|
||||
<div className="info-box-content">
|
||||
<div className="info-row">
|
||||
<div className="info-label">هزینه عملیاتی:</div>
|
||||
<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>
|
||||
<div className="info-value cost text-[12px]">{formatNumber(company?.cost || 0)}</div>
|
||||
<div className="info-unit">میلیون ریال</div>
|
||||
</div>
|
||||
<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-unit">تن در سال</div>
|
||||
</div>
|
||||
|
|
@ -42,41 +45,25 @@ const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
|
|||
};
|
||||
|
||||
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||
const sample = [
|
||||
{ id: "PX", name: "PX", imageUrl: "/abniro.png" },
|
||||
{ id: "LLTE&HLTE", name: "LLTE&HLTE", imageUrl: "/besparan.png" },
|
||||
{ id: "BTX", name: "BTX", imageUrl: "/khwarazmi.png" },
|
||||
{ id: "Utility", name: "Utility", imageUrl: "/faravash1.png" },
|
||||
{ id: "Reforming", name: "Reforming", imageUrl: "/faravash2.png" },
|
||||
{ id: "Storage Tank", name: "Storage Tank", 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;
|
||||
// Ensure we have exactly 6 companies
|
||||
const displayCompanies = companies;
|
||||
|
||||
// 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 : "LLTE&HLTE"}, // left - top band
|
||||
{ col: 3, row: 2 , colI : 3 , rowI : 1 , name : "BTX"}, // middle top (image sits in row 2, info box goes to row 1)
|
||||
{ col: 4, row: 2 ,colI : 5 , rowI : 2 , name : "Utility"}, // right - top band
|
||||
{ col: 2, row: 3 , colI : 1 , rowI : 3 , name : "Storage Tank"}, // left - bottom band
|
||||
{ col: 3, row: 3 , colI : 3, rowI : 4 , name : "PX"}, // middle bottom (image sits in row 3, info box goes to row 4)
|
||||
{ col: 4, row: 3 , colI : 5 , rowI : 3 , name : "Reforming"}, // right - bottom band
|
||||
{ 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);
|
||||
const gp = gridPositions.find(v => v.name === company.name) ;
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -137,10 +124,10 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
|||
align-self : center;
|
||||
justify-self : center;
|
||||
padding : .2rem 1.2rem;
|
||||
min-width : 8rem;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
.info-box-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -154,12 +141,11 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
|||
gap : .5rem;
|
||||
justify-content : space-between;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.info-row:has(.info-value.cost) {
|
||||
border-bottom: 1px solid #F76276;
|
||||
}
|
||||
&:has(.info-value.revenue) {border-bottom: 1px solid #3AEA83;}
|
||||
&:has(.info-value.cost) {border-bottom: 1px solid #F76276;}
|
||||
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #FFFFFF;
|
||||
|
|
@ -177,6 +163,7 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
|||
margin-bottom : .5rem;
|
||||
}
|
||||
|
||||
.info-value.revenue { color: #fff;}
|
||||
.info-value.cost { color: #fff; }
|
||||
.info-value.capacity { color: #fff; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 { formatNumber } from "~/lib/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "~/components/ui/tooltip"
|
||||
|
||||
interface DataItem {
|
||||
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">
|
||||
{/* Animated bar */}
|
||||
<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}%` }}
|
||||
>
|
||||
{ widthPercentage > 20 ? (
|
||||
<span className="text-[#3F415A] min-w-max text-left font-persian font-medium text-sm py-1 w-max">
|
||||
<span className="text-[#3F415A] text-left font-persian font-medium text-sm py-1 w-max">
|
||||
{item.label}
|
||||
</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>
|
||||
<span className="text-white font-bold text-base">
|
||||
{formatNumber(item.value)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,38 @@
|
|||
import { Book, CheckCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { DashboardLayout } from "./layout";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Progress } from "~/components/ui/progress";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
LineChart,
|
||||
Line,
|
||||
} from "recharts";
|
||||
import apiService from "~/lib/api";
|
||||
import toast from "react-hot-toast";
|
||||
import {
|
||||
Calendar,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Target,
|
||||
Lightbulb,
|
||||
DollarSign,
|
||||
Minus,
|
||||
CheckCircle,
|
||||
Book,
|
||||
} from "lucide-react";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
|
||||
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
|
||||
import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart";
|
||||
import { InteractiveBarChart } from "./interactive-bar-chart";
|
||||
import { D3ImageInfo } from "./d3-image-info";
|
||||
import {
|
||||
Label,
|
||||
PolarGrid,
|
||||
|
|
@ -8,21 +40,10 @@ import {
|
|||
RadialBar,
|
||||
RadialBarChart,
|
||||
} from "recharts";
|
||||
import { BaseCard } from "~/components/ui/base-card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { ChartContainer } from "~/components/ui/chart";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
import { MetricCard } from "~/components/ui/metric-card";
|
||||
import { Progress } from "~/components/ui/progress";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
||||
import apiService from "~/lib/api";
|
||||
import { EventBus, formatNumber } from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import { D3ImageInfo } from "./d3-image-info";
|
||||
import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart";
|
||||
import { InteractiveBarChart } from "./interactive-bar-chart";
|
||||
import { DashboardLayout } from "./layout";
|
||||
import { BaseCard } from "~/components/ui/base-card";
|
||||
|
||||
export function DashboardHome() {
|
||||
const [dashboardData, setDashboardData] = useState<any | null>(null);
|
||||
|
|
@ -30,54 +51,35 @@ export function DashboardHome() {
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
// Chart and schematic data from select API
|
||||
const [companyChartData, setCompanyChartData] = useState<
|
||||
{
|
||||
category: string;
|
||||
capacity: number;
|
||||
revenue: number;
|
||||
cost: number;
|
||||
costI: number;
|
||||
capacityI: number;
|
||||
revenueI: number;
|
||||
}[]
|
||||
{ category: string; capacity: number; revenue: number; cost: number , costI : number,
|
||||
capacityI : number,
|
||||
revenueI : number }[]
|
||||
>([]);
|
||||
|
||||
const [date, setDate] = useStoredDate();
|
||||
const [totalIncreasedCapacity, setTotalIncreasedCapacity] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (date: CalendarDate) => {
|
||||
if (date) setDate(date);
|
||||
};
|
||||
|
||||
EventBus.on("dateSelected", handler);
|
||||
|
||||
return () => {
|
||||
EventBus.off("dateSelected", handler);
|
||||
};
|
||||
fetchDashboardData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (date?.end && date?.start) fetchDashboardData();
|
||||
}, [date]);
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// First authenticate if needed
|
||||
const token = localStorage.getItem("auth_token");
|
||||
if (!token) {
|
||||
await apiService.login("inogen_admin", "123456");
|
||||
}
|
||||
|
||||
// Fetch top cards data
|
||||
const topCardsResponse = await apiService.call({
|
||||
main_page_first_function: {
|
||||
start_date: date.start || null,
|
||||
end_date: date.end || null,
|
||||
},
|
||||
main_page_first_function: {},
|
||||
});
|
||||
|
||||
// Fetch left section data
|
||||
const leftCardsResponse = await apiService.call({
|
||||
main_page_second_function: {
|
||||
start_date: date.start || null,
|
||||
end_date: date.end || null,
|
||||
},
|
||||
main_page_second_function: {},
|
||||
});
|
||||
|
||||
const topCardsResponseData = JSON.parse(topCardsResponse?.data);
|
||||
|
|
@ -110,10 +112,6 @@ export function DashboardHome() {
|
|||
"sum(pre_project_income)",
|
||||
"sum(increased_income_after_innovation)",
|
||||
],
|
||||
Conditions: [
|
||||
["start_date", ">=", date.start || null, "and"],
|
||||
["start_date", "<=", date.end || null],
|
||||
],
|
||||
GroupBy: ["related_company"],
|
||||
};
|
||||
|
||||
|
|
@ -132,30 +130,12 @@ export function DashboardHome() {
|
|||
let incCapacityTotal = 0;
|
||||
const chartRows = rows.map((r) => {
|
||||
const rel = r?.related_company ?? "-";
|
||||
const preFee =
|
||||
Number(r?.pre_innovation_fee_sum ?? 0) >= 0
|
||||
? r?.pre_innovation_fee_sum
|
||||
: 0;
|
||||
const costRed =
|
||||
Number(r?.innovation_cost_reduction_sum ?? 0) >= 0
|
||||
? r?.innovation_cost_reduction_sum
|
||||
: 0;
|
||||
const preCap =
|
||||
Number(r?.pre_project_production_capacity_sum ?? 0) >= 0
|
||||
? r?.pre_project_production_capacity_sum
|
||||
: 0;
|
||||
const incCap =
|
||||
Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0
|
||||
? r?.increased_capacity_after_innovation_sum
|
||||
: 0;
|
||||
const preInc =
|
||||
Number(r?.pre_project_income_sum ?? 0) >= 0
|
||||
? r?.pre_project_income_sum
|
||||
: 0;
|
||||
const incInc =
|
||||
Number(r?.increased_income_after_innovation_sum ?? 0) >= 0
|
||||
? r?.increased_income_after_innovation_sum
|
||||
: 0;
|
||||
const preFee = Number(r?.pre_innovation_fee_sum ?? 0) >= 0 ? r?.pre_innovation_fee_sum : 0;
|
||||
const costRed = Number(r?.innovation_cost_reduction_sum ?? 0) >= 0 ? r?.innovation_cost_reduction_sum : 0;
|
||||
const preCap = Number(r?.pre_project_production_capacity_sum ?? 0) >= 0 ? r?.pre_project_production_capacity_sum : 0;
|
||||
const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0 ? r?.increased_capacity_after_innovation_sum : 0;
|
||||
const preInc = Number(r?.pre_project_income_sum ?? 0) >= 0 ? r?.pre_project_income_sum : 0;
|
||||
const incInc = Number(r?.increased_income_after_innovation_sum ?? 0) >= 0 ? r?.increased_income_after_innovation_sum : 0;
|
||||
|
||||
incCapacityTotal += incCap;
|
||||
|
||||
|
|
@ -167,14 +147,14 @@ export function DashboardHome() {
|
|||
capacity: isFinite(capacityPct) ? capacityPct : 0,
|
||||
revenue: isFinite(revenuePct) ? revenuePct : 0,
|
||||
cost: isFinite(costPct) ? costPct : 0,
|
||||
costI: costRed,
|
||||
capacityI: incCap,
|
||||
revenueI: incInc,
|
||||
costI : costRed,
|
||||
capacityI : incCap,
|
||||
revenueI : incInc
|
||||
};
|
||||
});
|
||||
|
||||
setCompanyChartData(chartRows);
|
||||
// setTotalIncreasedCapacity(incCapacityTotal);
|
||||
setTotalIncreasedCapacity(incCapacityTotal);
|
||||
} catch (error) {
|
||||
console.error("Error fetching dashboard data:", error);
|
||||
const errorMessage =
|
||||
|
|
@ -187,24 +167,25 @@ export function DashboardHome() {
|
|||
};
|
||||
|
||||
// RadialBarChart data for ideas visualization
|
||||
// const getIdeasChartData = () => {
|
||||
// if (!dashboardData?.topData)
|
||||
// return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
|
||||
const getIdeasChartData = () => {
|
||||
if (!dashboardData?.topData)
|
||||
return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
|
||||
|
||||
// const registered = parseFloat(
|
||||
// dashboardData.topData.registered_innovation_technology_idea || "0"
|
||||
// );
|
||||
// const ongoing = parseFloat(
|
||||
// dashboardData.topData.ongoing_innovation_technology_ideas || "0"
|
||||
// );
|
||||
// const percentage = registered > 0 ? (ongoing / registered) * 100 : 0;
|
||||
const registered = parseFloat(
|
||||
dashboardData.topData.registered_innovation_technology_idea || "0",
|
||||
);
|
||||
const ongoing = parseFloat(
|
||||
dashboardData.topData.ongoing_innovation_technology_ideas || "0",
|
||||
);
|
||||
const percentage =
|
||||
registered > 0 ? (ongoing / registered) * 100 : 0;
|
||||
|
||||
// return [
|
||||
// { browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
|
||||
// ];
|
||||
// };
|
||||
return [
|
||||
{ browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
|
||||
];
|
||||
};
|
||||
|
||||
// const chartData = getIdeasChartData();
|
||||
const chartData = getIdeasChartData();
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
|
|
@ -253,7 +234,7 @@ export function DashboardHome() {
|
|||
style={{ height: `${Math.random() * 80 + 20}%` }}
|
||||
></div>
|
||||
<div
|
||||
className="w-full bg-pr-red rounded-t-sm"
|
||||
className="w-full bg-red-400/30 rounded-t-sm"
|
||||
style={{ height: `${Math.random() * 80 + 20}%` }}
|
||||
></div>
|
||||
</div>
|
||||
|
|
@ -270,7 +251,7 @@ export function DashboardHome() {
|
|||
if (loading) {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="grid grid-cols-3 gap-4 animate-pulse">
|
||||
<div className="p-3 pb-0 grid grid-cols-3 gap-4 animate-pulse">
|
||||
{/* Top Cards Row */}
|
||||
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
|
||||
<SkeletonCard />
|
||||
|
|
@ -331,7 +312,7 @@ export function DashboardHome() {
|
|||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-3 p-3 pb-0 gap-4">
|
||||
{/* Top Cards Row - Redesigned to match other components */}
|
||||
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
|
||||
{/* Ideas Card */}
|
||||
|
|
@ -348,22 +329,23 @@ export function DashboardHome() {
|
|||
visitors:
|
||||
parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea || "0"
|
||||
?.registered_innovation_technology_idea || "0",
|
||||
) > 0
|
||||
? Math.round(
|
||||
(parseFloat(
|
||||
dashboardData.topData
|
||||
?.ongoing_innovation_technology_ideas || "0"
|
||||
?.ongoing_innovation_technology_ideas ||
|
||||
"0",
|
||||
) /
|
||||
parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea ||
|
||||
"1"
|
||||
"1",
|
||||
)) *
|
||||
100
|
||||
100,
|
||||
)
|
||||
: 0,
|
||||
fill: "var(--color-green)",
|
||||
fill: "green",
|
||||
},
|
||||
]}
|
||||
startAngle={90}
|
||||
|
|
@ -371,18 +353,19 @@ export function DashboardHome() {
|
|||
90 +
|
||||
((parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea || "0"
|
||||
?.registered_innovation_technology_idea || "0",
|
||||
) > 0
|
||||
? Math.round(
|
||||
(parseFloat(
|
||||
dashboardData.topData
|
||||
?.ongoing_innovation_technology_ideas || "0"
|
||||
?.ongoing_innovation_technology_ideas || "0",
|
||||
) /
|
||||
parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea || "1"
|
||||
?.registered_innovation_technology_idea ||
|
||||
"1",
|
||||
)) *
|
||||
100
|
||||
100,
|
||||
)
|
||||
: 0) /
|
||||
100) *
|
||||
|
|
@ -398,7 +381,11 @@ export function DashboardHome() {
|
|||
className="first:fill-pr-red last:fill-[#24273A]"
|
||||
polarRadius={[38, 31]}
|
||||
/>
|
||||
<RadialBar dataKey="visitors" background cornerRadius={5} />
|
||||
<RadialBar
|
||||
dataKey="visitors"
|
||||
background
|
||||
cornerRadius={5}
|
||||
/>
|
||||
<PolarRadiusAxis
|
||||
tick={false}
|
||||
tickLine={false}
|
||||
|
|
@ -424,22 +411,22 @@ export function DashboardHome() {
|
|||
parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea ||
|
||||
"0"
|
||||
"0",
|
||||
) > 0
|
||||
? Math.round(
|
||||
(parseFloat(
|
||||
dashboardData.topData
|
||||
?.ongoing_innovation_technology_ideas ||
|
||||
"0"
|
||||
"0",
|
||||
) /
|
||||
parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea ||
|
||||
"1"
|
||||
"1",
|
||||
)) *
|
||||
100
|
||||
100,
|
||||
)
|
||||
: 0
|
||||
: 0,
|
||||
)}
|
||||
</tspan>
|
||||
</text>
|
||||
|
|
@ -456,14 +443,14 @@ export function DashboardHome() {
|
|||
<div className="font-light text-sm">ثبت شده :</div>
|
||||
{formatNumber(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea || "0"
|
||||
?.registered_innovation_technology_idea || "0",
|
||||
)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 font-bold text-base">
|
||||
<div className="font-light text-sm">در حال اجرا :</div>
|
||||
{formatNumber(
|
||||
dashboardData.topData
|
||||
?.ongoing_innovation_technology_ideas || "0"
|
||||
?.ongoing_innovation_technology_ideas || "0",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -473,34 +460,16 @@ export function DashboardHome() {
|
|||
{/* Revenue Card */}
|
||||
<MetricCard
|
||||
title="افزایش درآمد مبتنی بر فناوری و نوآوری"
|
||||
value={
|
||||
dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll(
|
||||
",",
|
||||
""
|
||||
) || "0"
|
||||
}
|
||||
percentValue={
|
||||
dashboardData.topData
|
||||
?.technology_innovation_based_revenue_growth_percent
|
||||
}
|
||||
value={dashboardData.topData?.technology_innovation_based_revenue_growth || "0"}
|
||||
percentValue={dashboardData.topData?.technology_innovation_based_revenue_growth_percent}
|
||||
percentLabel="درصد به کل درآمد"
|
||||
/>
|
||||
|
||||
{/* Cost Reduction Card */}
|
||||
<MetricCard
|
||||
title="کاهش هزینه ها مبتنی بر فناوری و نوآوری"
|
||||
value={Math.round(
|
||||
parseFloat(
|
||||
dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(
|
||||
/,/g,
|
||||
""
|
||||
) || "0"
|
||||
)
|
||||
)}
|
||||
percentValue={
|
||||
dashboardData.topData
|
||||
?.technology_innovation_based_cost_reduction_percent || "0"
|
||||
}
|
||||
value={Math.round(parseFloat(dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(/,/g, "") || "0") / 1000000)}
|
||||
percentValue={dashboardData.topData?.technology_innovation_based_cost_reduction_percent || "0"}
|
||||
percentLabel="درصد به کل هزینه"
|
||||
/>
|
||||
|
||||
|
|
@ -517,9 +486,9 @@ export function DashboardHome() {
|
|||
browser: "budget",
|
||||
visitors: parseFloat(
|
||||
dashboardData.topData
|
||||
?.innovation_budget_achievement_percent || "0"
|
||||
?.innovation_budget_achievement_percent || "0",
|
||||
),
|
||||
fill: "var(--color-green)",
|
||||
fill: "green",
|
||||
},
|
||||
]}
|
||||
startAngle={90}
|
||||
|
|
@ -540,7 +509,11 @@ export function DashboardHome() {
|
|||
className="first:fill-pr-red last:fill-[#24273A]"
|
||||
polarRadius={[38, 31]}
|
||||
/>
|
||||
<RadialBar dataKey="visitors" background cornerRadius={5} />
|
||||
<RadialBar
|
||||
dataKey="visitors"
|
||||
background
|
||||
cornerRadius={5}
|
||||
/>
|
||||
<PolarRadiusAxis
|
||||
tick={false}
|
||||
tickLine={false}
|
||||
|
|
@ -566,8 +539,8 @@ export function DashboardHome() {
|
|||
Math.round(
|
||||
dashboardData.topData
|
||||
?.innovation_budget_achievement_percent ||
|
||||
0
|
||||
)
|
||||
0,
|
||||
),
|
||||
)}
|
||||
</tspan>
|
||||
</text>
|
||||
|
|
@ -587,10 +560,10 @@ export function DashboardHome() {
|
|||
parseFloat(
|
||||
dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
|
||||
/,/g,
|
||||
""
|
||||
) || "0"
|
||||
)
|
||||
)
|
||||
"",
|
||||
) || "0",
|
||||
) / 1000000000,
|
||||
),
|
||||
)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-base font-bold mr-auto">
|
||||
|
|
@ -600,10 +573,10 @@ export function DashboardHome() {
|
|||
parseFloat(
|
||||
dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
|
||||
/,/g,
|
||||
""
|
||||
) || "0"
|
||||
)
|
||||
)
|
||||
"",
|
||||
) || "0",
|
||||
) / 1000000000,
|
||||
),
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -614,21 +587,18 @@ export function DashboardHome() {
|
|||
|
||||
{/* Main Content with 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)]"
|
||||
>
|
||||
<div className="flex items-center border-b border-gray-600 justify-between gap-2">
|
||||
<p className="p-6 font-persian font-semibold text-lg ">
|
||||
تحقق ارزش ها
|
||||
</p>
|
||||
<TabsList className="bg-transparent py-2 m-6 border-[1px] border-[#5F6284]">
|
||||
<TabsList className="bg-transparent py-2 border m-6 border-gray-600">
|
||||
<TabsTrigger value="canvas" className="cursor-pointer">
|
||||
شماتیک
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="charts"
|
||||
className=" text-white cursor-pointer font-light "
|
||||
>
|
||||
<TabsTrigger value="charts" className=" text-white cursor-pointer font-light ">
|
||||
مقایسه ای
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
|
@ -641,41 +611,17 @@ export function DashboardHome() {
|
|||
<TabsContent value="canvas" className="w-ful h-full">
|
||||
<div className="p-4 h-full w-full">
|
||||
<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) => {
|
||||
// const imageMap: Record<string, string> = {
|
||||
// "واحد 100": "/abniro.png" ,
|
||||
// "واحد 200": "/besparan.png" ,
|
||||
// "واحد 300": "/khwarazmi.png" ,
|
||||
// "واحد 400": "/faravash1.png"
|
||||
// };
|
||||
|
||||
//پتروشیمی نوری
|
||||
companies={companyChartData.map((item) => {
|
||||
companies={
|
||||
companyChartData.map((item) => {
|
||||
const imageMap: Record<string, string> = {
|
||||
"LLTE&HLTE": "/besparan.png",
|
||||
BTX: "/khwarazmi.png",
|
||||
Utility: "/faravash1.png",
|
||||
Reforming: "/faravash2.png",
|
||||
"Storage Tank": "/kimia.png",
|
||||
PX: "/abniro.png",
|
||||
"بسپاران": "/besparan.png",
|
||||
"خوارزمی": "/khwarazmi.png",
|
||||
"فراورش 1": "/faravash1.png",
|
||||
"فراورش 2": "/faravash2.png",
|
||||
"کیمیا": "/kimia.png",
|
||||
"آب نیرو": "/abniro.png",
|
||||
};
|
||||
|
||||
|
||||
|
||||
return {
|
||||
id: item.category,
|
||||
name: item.category,
|
||||
|
|
@ -684,7 +630,8 @@ export function DashboardHome() {
|
|||
capacity: item?.capacityI || 0,
|
||||
revenue: item?.revenueI || 0,
|
||||
};
|
||||
})}
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
|
@ -702,7 +649,7 @@ export function DashboardHome() {
|
|||
|
||||
<Progress
|
||||
value={parseFloat(
|
||||
dashboardData.leftData?.technology_intensity
|
||||
dashboardData.leftData?.technology_intensity,
|
||||
)}
|
||||
className="h-4 flex-1"
|
||||
/>
|
||||
|
|
@ -720,21 +667,21 @@ export function DashboardHome() {
|
|||
{
|
||||
label: "اجرا شده",
|
||||
value: parseFloat(
|
||||
dashboardData?.leftData?.executed_project || "0"
|
||||
dashboardData?.leftData?.executed_project || "0",
|
||||
),
|
||||
color: "bg-pr-green",
|
||||
},
|
||||
{
|
||||
label: "در حال اجرا",
|
||||
value: parseFloat(
|
||||
dashboardData?.leftData?.in_progress_project || "0"
|
||||
dashboardData?.leftData?.in_progress_project || "0",
|
||||
),
|
||||
color: "bg-pr-blue",
|
||||
},
|
||||
{
|
||||
label: "برنامهریزی شده",
|
||||
value: parseFloat(
|
||||
dashboardData?.leftData?.planned_project || "0"
|
||||
dashboardData?.leftData?.planned_project || "0",
|
||||
),
|
||||
color: "bg-pr-red",
|
||||
},
|
||||
|
|
@ -759,7 +706,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.printed_books_count || "0"
|
||||
dashboardData.leftData?.printed_books_count || "0",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -770,7 +717,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.registered_patents_count || "0"
|
||||
dashboardData.leftData?.registered_patents_count || "0",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -781,7 +728,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.published_reports_count || "0"
|
||||
dashboardData.leftData?.published_reports_count || "0",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -792,7 +739,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.printed_articles_count || "0"
|
||||
dashboardData.leftData?.printed_articles_count || "0",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -816,7 +763,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.attended_conferences_count || "0"
|
||||
dashboardData.leftData?.attended_conferences_count || "0",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -827,7 +774,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.attended_events_count || "0"
|
||||
dashboardData.leftData?.attended_events_count || "0",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -838,7 +785,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.attended_exhibitions_count || "0"
|
||||
dashboardData.leftData?.attended_exhibitions_count || "0",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -849,7 +796,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.organized_events_count || "0"
|
||||
dashboardData.leftData?.organized_events_count || "0",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -859,6 +806,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
import { saveAs } from "file-saver";
|
||||
import jalaali from "jalaali-js";
|
||||
import {
|
||||
Calendar,
|
||||
ChevronLeft,
|
||||
FileChartColumnIncreasing,
|
||||
Menu,
|
||||
PanelLeft,
|
||||
Server,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import XLSX from "xlsx-js-style";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Calendar as CustomCalendar } from "~/components/ui/Calendar";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useAuth } from "~/contexts/auth-context";
|
||||
import { Link } from "react-router";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
PanelLeft,
|
||||
|
||||
Settings,
|
||||
User,
|
||||
|
||||
Menu,
|
||||
ChevronDown,
|
||||
Server,
|
||||
ChevronLeft ,
|
||||
|
||||
} from "lucide-react";
|
||||
import apiService from "~/lib/api";
|
||||
import { cn, EventBus, handleDataValue } from "~/lib/utils";
|
||||
|
||||
interface HeaderProps {
|
||||
onToggleSidebar?: () => void;
|
||||
|
|
@ -25,161 +24,6 @@ interface HeaderProps {
|
|||
titleIcon?: React.ComponentType<{ className?: string }> | null;
|
||||
}
|
||||
|
||||
interface MonthItem {
|
||||
id: string;
|
||||
label: string;
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
interface CurrentDay {
|
||||
start?: string;
|
||||
end?: string;
|
||||
sinceMonth?: string;
|
||||
fromMonth?: string;
|
||||
}
|
||||
|
||||
interface SelectedDate {
|
||||
since?: number;
|
||||
until?: number;
|
||||
}
|
||||
|
||||
const monthList: Array<MonthItem> = [
|
||||
{
|
||||
id: "month-1",
|
||||
label: "بهار",
|
||||
start: "01/01",
|
||||
end: "03/31",
|
||||
},
|
||||
{
|
||||
id: "month-2",
|
||||
label: "تابستان",
|
||||
start: "04/01",
|
||||
end: "06/31",
|
||||
},
|
||||
{
|
||||
id: "month-3",
|
||||
label: "پاییز",
|
||||
start: "07/01",
|
||||
end: "09/31",
|
||||
},
|
||||
{
|
||||
id: "month-4",
|
||||
label: "زمستان",
|
||||
start: "10/01",
|
||||
end: "12/30",
|
||||
},
|
||||
];
|
||||
|
||||
const columns: Array<any> = [
|
||||
{ key: "title", label: "عنوان پروژه", sortable: true, width: "300px" },
|
||||
{
|
||||
key: "importance_project",
|
||||
label: "میزان اهمیت",
|
||||
sortable: true,
|
||||
width: "160px",
|
||||
},
|
||||
{
|
||||
key: "strategic_theme",
|
||||
label: "مضمون راهبردی",
|
||||
sortable: true,
|
||||
width: "200px",
|
||||
},
|
||||
{
|
||||
key: "value_technology_and_innovation",
|
||||
label: "ارزش فناوری و نوآوری",
|
||||
sortable: true,
|
||||
width: "220px",
|
||||
},
|
||||
{
|
||||
key: "type_of_innovation",
|
||||
label: "انواع نوآوری",
|
||||
sortable: true,
|
||||
width: "160px",
|
||||
},
|
||||
{
|
||||
key: "innovation",
|
||||
label: "میزان نوآوری",
|
||||
sortable: true,
|
||||
width: "140px",
|
||||
},
|
||||
{
|
||||
key: "person_executing",
|
||||
label: "مسئول اجرا",
|
||||
sortable: true,
|
||||
width: "180px",
|
||||
},
|
||||
{
|
||||
key: "excellent_observer",
|
||||
label: "ناطر عالی",
|
||||
sortable: true,
|
||||
width: "180px",
|
||||
},
|
||||
{ key: "observer", label: "ناظر پروژه", sortable: true, width: "180px" },
|
||||
{ key: "moderator", label: "مجری", sortable: true, width: "180px" },
|
||||
{
|
||||
key: "executive_phase",
|
||||
label: "فاز اجرایی",
|
||||
sortable: true,
|
||||
width: "160px",
|
||||
},
|
||||
{
|
||||
key: "start_date",
|
||||
label: "تاریخ شروع",
|
||||
sortable: true,
|
||||
width: "120px",
|
||||
},
|
||||
{
|
||||
key: "remaining_time",
|
||||
label: "زمان باقی مانده",
|
||||
sortable: true,
|
||||
width: "140px",
|
||||
computed: true,
|
||||
},
|
||||
{
|
||||
key: "end_date",
|
||||
label: "تاریخ پایان (برنامهریزی)",
|
||||
sortable: true,
|
||||
width: "160px",
|
||||
},
|
||||
{
|
||||
key: "renewed_duration",
|
||||
label: "مدت زمان تمدید",
|
||||
sortable: true,
|
||||
width: "140px",
|
||||
},
|
||||
{
|
||||
key: "done_date",
|
||||
label: "تاریخ پایان (واقعی)",
|
||||
sortable: true,
|
||||
width: "160px",
|
||||
},
|
||||
{
|
||||
key: "deviation_from_program",
|
||||
label: "متوسط انحراف برنامهای",
|
||||
sortable: true,
|
||||
width: "160px",
|
||||
},
|
||||
{
|
||||
key: "approved_budget",
|
||||
label: "بودجه مصوب",
|
||||
sortable: true,
|
||||
width: "150px",
|
||||
},
|
||||
{
|
||||
key: "budget_spent",
|
||||
label: "بودجه صرف شده",
|
||||
sortable: true,
|
||||
width: "150px",
|
||||
},
|
||||
{
|
||||
key: "cost_deviation",
|
||||
label: "متوسط انحراف هزینهای",
|
||||
sortable: true,
|
||||
width: "160px",
|
||||
},
|
||||
];
|
||||
|
||||
export function Header({
|
||||
onToggleSidebar,
|
||||
className,
|
||||
|
|
@ -187,217 +31,25 @@ export function Header({
|
|||
titleIcon,
|
||||
}: HeaderProps) {
|
||||
const { user } = useAuth();
|
||||
const { jy } = jalaali.toJalaali(new Date());
|
||||
|
||||
const calendarRef = useRef<HTMLDivElement>(null);
|
||||
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState<boolean>(false);
|
||||
const [isNotificationOpen, setIsNotificationOpen] = 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>({
|
||||
since: jy,
|
||||
until: jy,
|
||||
});
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState<CurrentDay>({});
|
||||
|
||||
useEffect(() => {
|
||||
const storedDate = localStorage.getItem("dateSelected");
|
||||
if (storedDate) {
|
||||
const parsedDate = JSON.parse(storedDate);
|
||||
setSelectedDate(parsedDate);
|
||||
|
||||
const sinceYear = parsedDate.start
|
||||
? parseInt(parsedDate.start.split("/")[0], 10)
|
||||
: jy;
|
||||
const untilYear = parsedDate.end
|
||||
? parseInt(parsedDate.end.split("/")[0], 10)
|
||||
: jy;
|
||||
|
||||
setCurrentYear({ since: sinceYear, until: untilYear });
|
||||
} else {
|
||||
const defaultDate = {
|
||||
sinceMonth: "بهار",
|
||||
fromMonth: "زمستان",
|
||||
start: `${jy}/01/01`,
|
||||
end: `${jy}/12/30`,
|
||||
};
|
||||
setSelectedDate(defaultDate);
|
||||
localStorage.setItem("dateSelected", JSON.stringify(defaultDate));
|
||||
setCurrentYear({ since: jy, until: jy });
|
||||
}
|
||||
}, []);
|
||||
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
|
||||
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
||||
|
||||
const redirectHandler = async () => {
|
||||
try {
|
||||
const getData = await apiService.post("/GenerateSsoCode");
|
||||
|
||||
//بندر امام
|
||||
// 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}`;
|
||||
|
||||
const getData = await apiService.post('/GenerateSsoCode')
|
||||
//const url = `http://localhost:3000/redirect/${getData.data}`;
|
||||
const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`;
|
||||
window.open(url, "_blank");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const changeSinceYear = (delta: number) => {
|
||||
if (!currentYear) return;
|
||||
|
||||
const newSince = (currentYear.since ?? 0) + delta;
|
||||
|
||||
if (newSince > (currentYear.until ?? Infinity) || newSince < 0) return;
|
||||
|
||||
const updatedYear = { ...currentYear, since: newSince };
|
||||
setCurrentYear(updatedYear);
|
||||
|
||||
const updatedDate = {
|
||||
...selectedDate,
|
||||
start: `${newSince}/${selectedDate.start?.split("/").slice(1).join("/")}`,
|
||||
};
|
||||
setSelectedDate(updatedDate);
|
||||
localStorage.setItem("dateSelected", JSON.stringify(updatedDate));
|
||||
EventBus.emit("dateSelected", updatedDate);
|
||||
};
|
||||
|
||||
const nextFromYearHandler = () => changeSinceYear(1);
|
||||
const prevFromYearHandler = () => changeSinceYear(-1);
|
||||
|
||||
const selectFromDateHandler = (val: MonthItem) => {
|
||||
const data = {
|
||||
...selectedDate,
|
||||
start: `${currentYear.since}/${val.start}`,
|
||||
sinceMonth: val.label,
|
||||
};
|
||||
setSelectedDate(data);
|
||||
localStorage.setItem("dateSelected", JSON.stringify(data));
|
||||
EventBus.emit("dateSelected", data);
|
||||
};
|
||||
|
||||
const changeUntilYear = (delta: number) => {
|
||||
if (!currentYear) return;
|
||||
|
||||
const newUntil = (currentYear.until ?? 0) + delta;
|
||||
|
||||
if (newUntil < (currentYear.since ?? 0)) return;
|
||||
|
||||
const updatedYear = { ...currentYear, until: newUntil };
|
||||
setCurrentYear(updatedYear);
|
||||
|
||||
const updatedDate = {
|
||||
...selectedDate,
|
||||
end: `${newUntil}/${selectedDate.end?.split("/").slice(1).join("/")}`,
|
||||
};
|
||||
setSelectedDate(updatedDate);
|
||||
localStorage.setItem("dateSelected", JSON.stringify(updatedDate));
|
||||
EventBus.emit("dateSelected", updatedDate);
|
||||
};
|
||||
|
||||
const nextUntilYearHandler = () => changeUntilYear(1);
|
||||
const prevUntilYearHandler = () => changeUntilYear(-1);
|
||||
|
||||
const selectUntilDateHandler = (val: MonthItem) => {
|
||||
const data = {
|
||||
...selectedDate,
|
||||
end: `${currentYear.until}/${val.end}`,
|
||||
fromMonth: val.label,
|
||||
};
|
||||
setSelectedDate(data);
|
||||
localStorage.setItem("dateSelected", JSON.stringify(data));
|
||||
EventBus.emit("dateSelected", data);
|
||||
toggleCalendar();
|
||||
};
|
||||
|
||||
const toggleCalendar = () => {
|
||||
setOpenCalendar(!openCalendar);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
calendarRef.current &&
|
||||
!calendarRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setOpenCalendar(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
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 (
|
||||
<header
|
||||
className={cn(
|
||||
"backdrop-blur-sm border-b border-gray-400/30 h-16 flex items-center justify-between px-4 lg:px-6 shadow-sm relative z-30",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Left Section */}
|
||||
|
|
@ -425,70 +77,16 @@ export function Header({
|
|||
<PanelLeft />
|
||||
)}
|
||||
{title.includes("-") ? (
|
||||
<div className="flex row items-center gap-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="flex items-center gap-1">
|
||||
{title.split("-")[0]}
|
||||
<ChevronLeft className="inline-block w-4 h-4" />
|
||||
{title.split("-")[1]}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
</span>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
)}
|
||||
|
||||
</h1>
|
||||
|
||||
<div ref={calendarRef} className="flex flex-col gap-3 relative">
|
||||
<div
|
||||
onClick={toggleCalendar}
|
||||
className="flex flex-row w-full gap-2 items-center border border-pr-gray p-1.5 rounded-md px-2.5 min-w-64 cursor-pointer hover:bg-pr-gray/50 transition-all duration-300"
|
||||
>
|
||||
<Calendar size={20} />
|
||||
{selectedDate ? (
|
||||
<div className="flex flex-row justify-between w-full min-w-36 font-bold gap-1">
|
||||
<div className="flex flex-row gap-1.5 w-max">
|
||||
<span className="text-md">از</span>
|
||||
<span className="text-md">{selectedDate?.sinceMonth}</span>
|
||||
<span className="text-md">
|
||||
{handleDataValue(currentYear.since)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1.5 w-max">
|
||||
<span className="text-md">تا</span>
|
||||
<span className="text-md">{selectedDate?.fromMonth}</span>
|
||||
<span className="text-md">
|
||||
{handleDataValue(currentYear.until)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
"تاریخ مورد نظر خود را انتخاب نمایید"
|
||||
)}
|
||||
</div>
|
||||
|
||||
{openCalendar && (
|
||||
<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
|
||||
title="از"
|
||||
nextYearHandler={prevFromYearHandler}
|
||||
prevYearHandler={nextFromYearHandler}
|
||||
currentYear={handleDataValue(currentYear?.since)}
|
||||
monthList={monthList}
|
||||
selectedDate={selectedDate?.sinceMonth}
|
||||
selectDateHandler={selectFromDateHandler}
|
||||
/>
|
||||
<span className="w-0.5 h-[12.5rem] border border-[#5F6284] block "></span>
|
||||
<CustomCalendar
|
||||
title="تا"
|
||||
nextYearHandler={prevUntilYearHandler}
|
||||
prevYearHandler={nextUntilYearHandler}
|
||||
currentYear={handleDataValue(currentYear?.until)}
|
||||
monthList={monthList}
|
||||
selectedDate={selectedDate?.fromMonth}
|
||||
selectDateHandler={selectUntilDateHandler}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Section */}
|
||||
|
|
@ -496,29 +94,14 @@ export function Header({
|
|||
{/* User Menu */}
|
||||
<div className="relative">
|
||||
<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 && (
|
||||
<button
|
||||
{
|
||||
user?.id === 2041 && <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"
|
||||
onClick={redirectHandler}
|
||||
>
|
||||
onClick={redirectHandler}>
|
||||
<Server className="h-4 w-4" />
|
||||
ورود به میزکار مدیریت
|
||||
</button>
|
||||
)}
|
||||
ورود به میزکار مدیریت</button>
|
||||
}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -526,6 +109,7 @@ export function Header({
|
|||
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
|
||||
className="flex items-center gap-2 text-gray-300"
|
||||
>
|
||||
|
||||
<div className="hidden sm:block text-right">
|
||||
<div className="text-sm font-medium font-persian">
|
||||
{user?.name} {user?.family}
|
||||
|
|
@ -539,7 +123,6 @@ export function Header({
|
|||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Profile Dropdown */}
|
||||
{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">
|
||||
|
|
@ -551,7 +134,7 @@ export function Header({
|
|||
{user?.email}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="py-1">
|
||||
<div className="py-1">
|
||||
<Link
|
||||
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"
|
||||
|
|
@ -568,7 +151,7 @@ export function Header({
|
|||
<Settings className="h-4 w-4" />
|
||||
تنظیمات
|
||||
</Link>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,22 +27,19 @@ const chartConfig = {
|
|||
},
|
||||
revenue: {
|
||||
label: "افزایش درآمد",
|
||||
color: "#4ADE80",
|
||||
color: "#4ADE80", // Green-400
|
||||
},
|
||||
cost: {
|
||||
// آپادانا بندرامام
|
||||
//label: "کاهش هزینه",
|
||||
label: "هزینه عملیاتی", // نوری
|
||||
color: "#F87171",
|
||||
label: "کاهش هزینه",
|
||||
color: "#F87171", // Red-400
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
|
||||
export function InteractiveBarChart({
|
||||
data,
|
||||
showRevenue = "نوری"!="نوری", // آپادانا بندرامام
|
||||
}: {
|
||||
data: CompanyChartDatum[];
|
||||
showRevenue?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Card className="py-0 bg-transparent mt-8 border-none h-full">
|
||||
|
|
@ -69,67 +66,31 @@ export function InteractiveBarChart({
|
|||
axisLine={false}
|
||||
tickMargin={25}
|
||||
style={{ fill: "#ACACAC", fontSize: 11 }}
|
||||
tickFormatter={(value) =>
|
||||
`${formatNumber(Math.round(value))}%`
|
||||
}
|
||||
tickFormatter={(value) => `${formatNumber(Math.round(value))}%`}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="capacity"
|
||||
fill={chartConfig.capacity.color}
|
||||
radius={[8, 8, 0, 0]}
|
||||
>
|
||||
<Bar dataKey="capacity" fill={chartConfig.capacity.color} radius={[8, 8, 0, 0]}>
|
||||
<LabelList
|
||||
dataKey="capacity"
|
||||
position="top"
|
||||
offset={15}
|
||||
style={{
|
||||
fill: "#ffffff",
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
formatter={(v: number) =>
|
||||
`${formatNumber(Math.round(v))}%`
|
||||
}
|
||||
style={{ fill: "#ffffff", fontSize: "16px", fontWeight: "bold" }}
|
||||
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
|
||||
/>
|
||||
</Bar>
|
||||
|
||||
{showRevenue && (
|
||||
<Bar
|
||||
dataKey="revenue"
|
||||
fill={chartConfig.revenue.color}
|
||||
radius={[8, 8, 0, 0]}
|
||||
>
|
||||
<Bar dataKey="revenue" fill={chartConfig.revenue.color} radius={[8, 8, 0, 0]}>
|
||||
<LabelList
|
||||
dataKey="revenue"
|
||||
position="top"
|
||||
style={{
|
||||
fill: "#ffffff",
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
formatter={(v: number) =>
|
||||
`${formatNumber(Math.round(v))}%`
|
||||
}
|
||||
style={{ fill: "#ffffff", fontSize: "16px", fontWeight: "bold" }}
|
||||
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
|
||||
/>
|
||||
</Bar>
|
||||
)}
|
||||
|
||||
<Bar
|
||||
dataKey="cost"
|
||||
fill={chartConfig.cost.color}
|
||||
radius={[8, 8, 0, 0]}
|
||||
>
|
||||
<Bar dataKey="cost" fill={chartConfig.cost.color} radius={[8, 8, 0, 0]}>
|
||||
<LabelList
|
||||
dataKey="cost"
|
||||
position="top"
|
||||
style={{
|
||||
fill: "#ffffff",
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
formatter={(v: number) =>
|
||||
`${formatNumber(Math.round(v))}%`
|
||||
}
|
||||
style={{ fill: "#ffffff", fontSize: "16px", fontWeight: "bold" }}
|
||||
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
|
|
@ -142,32 +103,23 @@ export function InteractiveBarChart({
|
|||
className="w-6 h-2 rounded"
|
||||
style={{ backgroundColor: chartConfig.capacity.color }}
|
||||
></div>
|
||||
<span className="text-xs text-white">
|
||||
{chartConfig.capacity.label}
|
||||
</span>
|
||||
<span className="text-xs text-white">{chartConfig.capacity.label}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-6 h-2 rounded"
|
||||
style={{ backgroundColor: chartConfig.cost.color }}
|
||||
></div>
|
||||
<span className="text-xs text-white">
|
||||
{chartConfig.cost.label}
|
||||
</span>
|
||||
<span className="text-xs text-white">{chartConfig.cost.label}</span>
|
||||
</div>
|
||||
|
||||
{showRevenue && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-6 h-2 rounded"
|
||||
style={{ backgroundColor: chartConfig.revenue.color }}
|
||||
></div>
|
||||
<span className="text-xs text-white">
|
||||
{chartConfig.revenue.label}
|
||||
</span>
|
||||
<span className="text-xs text-white">{chartConfig.revenue.label}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useState } from "react";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Header } from "./header";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { Header } from "./header";
|
||||
import { StrategicAlignmentPopup } from "./strategic-alignment-popup";
|
||||
import apiService from "~/lib/api";
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: React.ReactNode;
|
||||
|
|
@ -17,14 +18,9 @@ export function DashboardLayout({
|
|||
}: DashboardLayoutProps) {
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||
const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] =
|
||||
useState(false);
|
||||
const [currentTitle, setCurrentTitle] = useState<string | undefined>(
|
||||
title ?? "صفحه اول"
|
||||
);
|
||||
const [currentTitleIcon, setCurrentTitleIcon] = useState<
|
||||
React.ComponentType<{ className?: string }> | null | undefined
|
||||
>(undefined);
|
||||
const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] = useState(false);
|
||||
const [currentTitle, setCurrentTitle] = useState<string | undefined>(title ?? "صفحه اول");
|
||||
const [currentTitleIcon, setCurrentTitleIcon] = useState<React.ComponentType<{ className?: string }> | null | undefined>(undefined);
|
||||
|
||||
const toggleSidebarCollapse = () => {
|
||||
setIsSidebarCollapsed(!isSidebarCollapsed);
|
||||
|
|
@ -34,6 +30,8 @@ export function DashboardLayout({
|
|||
setIsMobileSidebarOpen(!isMobileSidebarOpen);
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-screen flex overflow-hidden bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)] relative overflow-x-hidden"
|
||||
|
|
@ -57,20 +55,19 @@ export function DashboardLayout({
|
|||
"fixed inset-y-0 right-0 z-50 flex flex-col lg:static lg:inset-auto lg:translate-x-0 transition-transform duration-300 ease-in-out",
|
||||
isMobileSidebarOpen
|
||||
? "translate-x-0"
|
||||
: "translate-x-full lg:translate-x-0"
|
||||
: "translate-x-full lg:translate-x-0",
|
||||
)}
|
||||
>
|
||||
<Sidebar
|
||||
isCollapsed={isSidebarCollapsed}
|
||||
onToggleCollapse={toggleSidebarCollapse}
|
||||
className="h-full flex-shrink-0 relative z-10"
|
||||
onStrategicAlignmentClick={() =>
|
||||
setIsStrategicAlignmentPopupOpen(true)
|
||||
}
|
||||
onStrategicAlignmentClick={() => setIsStrategicAlignmentPopupOpen(true)}
|
||||
onTitleChange={(info) => {
|
||||
setCurrentTitle(info.title);
|
||||
setCurrentTitleIcon(info.icon ?? null);
|
||||
}}
|
||||
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -88,18 +85,15 @@ export function DashboardLayout({
|
|||
<main
|
||||
className={cn(
|
||||
"flex-1 overflow-x-hidden overflow-y-auto focus:outline-none transition-all duration-300 min-w-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="relative h-full min-w-0 w-full z-10 overflow-x-hidden p-5">
|
||||
<div className="relative h-full min-w-0 w-full z-10 overflow-x-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<StrategicAlignmentPopup
|
||||
open={isStrategicAlignmentPopupOpen}
|
||||
onOpenChange={setIsStrategicAlignmentPopupOpen}
|
||||
/>
|
||||
<StrategicAlignmentPopup open={isStrategicAlignmentPopupOpen} onOpenChange={setIsStrategicAlignmentPopupOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import {
|
|||
Zap,
|
||||
} from "lucide-react";
|
||||
import moment from "moment-jalaali";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { BaseCard } from "~/components/ui/base-card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import { Checkbox } from "~/components/ui/checkbox";
|
||||
|
|
@ -34,10 +34,8 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
||||
import apiService from "~/lib/api";
|
||||
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import { formatCurrency } from "~/lib/utils";
|
||||
import { DashboardLayout } from "../layout";
|
||||
|
||||
moment.loadPersian({ usePersianDigits: true });
|
||||
|
|
@ -155,7 +153,7 @@ export function DigitalInnovationPage() {
|
|||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize] = useState(20);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [date, setDate] = useStoredDate();
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [actualTotalCount, setActualTotalCount] = useState(0);
|
||||
const [statsLoading, setStatsLoading] = useState(false);
|
||||
const [rating, setRating] = useState<ListItem[]>([]);
|
||||
|
|
@ -215,7 +213,7 @@ export function DigitalInnovationPage() {
|
|||
value: formatNumber(stats.reduceCosts.toFixed?.(1) ?? stats.reduceCosts),
|
||||
description: "میلیون ریال کاهش یافته",
|
||||
icon: <TrendingDown />,
|
||||
color: "text-pr-green",
|
||||
color: "text-emerald-400",
|
||||
},
|
||||
{
|
||||
id: "bottleneck-removal",
|
||||
|
|
@ -223,7 +221,7 @@ export function DigitalInnovationPage() {
|
|||
value: formatNumber(stats.increasedRevenue),
|
||||
description: "میلیون ریال افزایش یافته",
|
||||
icon: <TrendingUp />,
|
||||
color: "text-pr-green",
|
||||
color: "text-emerald-400",
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -234,7 +232,7 @@ export function DigitalInnovationPage() {
|
|||
),
|
||||
description: "هزار تن صرفه جوریی شده",
|
||||
icon: <Database />,
|
||||
color: "text-pr-green",
|
||||
color: "text-emerald-400",
|
||||
},
|
||||
{
|
||||
id: "frequent-failures-reduction",
|
||||
|
|
@ -245,7 +243,7 @@ export function DigitalInnovationPage() {
|
|||
),
|
||||
description: "مگاوات کاهش یافته",
|
||||
icon: <Zap />,
|
||||
color: "text-pr-green",
|
||||
color: "text-emerald-400",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -284,11 +282,7 @@ export function DigitalInnovationPage() {
|
|||
"reduce_costs_percent",
|
||||
],
|
||||
Sorts: [[sortConfig.field, sortConfig.direction]],
|
||||
Conditions: [
|
||||
["type_of_innovation", "=", "نوآوری دیجیتال", "and"],
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]],
|
||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||
});
|
||||
|
||||
|
|
@ -301,16 +295,16 @@ export function DigitalInnovationPage() {
|
|||
if (reset) {
|
||||
setProjects(parsedData);
|
||||
// calculateAverage(parsedData);
|
||||
// setTotalCount(parsedData.length);
|
||||
setTotalCount(parsedData.length);
|
||||
} else {
|
||||
setProjects((prev) => [...prev, ...parsedData]);
|
||||
// setTotalCount((prev) => prev + parsedData.length);
|
||||
setTotalCount((prev) => prev + parsedData.length);
|
||||
}
|
||||
setHasMore(parsedData.length === pageSize);
|
||||
} else {
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
// setTotalCount(0);
|
||||
setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -318,14 +312,14 @@ export function DigitalInnovationPage() {
|
|||
console.error("Error parsing project data:", parseError);
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
// setTotalCount(0);
|
||||
setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
} else {
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
// setTotalCount(0);
|
||||
setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -333,7 +327,7 @@ export function DigitalInnovationPage() {
|
|||
toast.error(response.message || "خطا در دریافت اطلاعات پروژهها");
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
// setTotalCount(0);
|
||||
setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -342,7 +336,7 @@ export function DigitalInnovationPage() {
|
|||
toast.error("خطا در دریافت اطلاعات پروژهها");
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
// setTotalCount(0);
|
||||
setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
} finally {
|
||||
|
|
@ -360,27 +354,13 @@ export function DigitalInnovationPage() {
|
|||
}, [hasMore, loading, loadingMore]);
|
||||
|
||||
useEffect(() => {
|
||||
if (date?.start && date?.end) {
|
||||
fetchTable(true);
|
||||
fetchTotalCount();
|
||||
fetchStats();
|
||||
}
|
||||
}, [sortConfig, date]);
|
||||
}, [sortConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (date: CalendarDate) => {
|
||||
if (date) setDate(date);
|
||||
};
|
||||
|
||||
EventBus.on("dateSelected", handler);
|
||||
|
||||
return () => {
|
||||
EventBus.off("dateSelected", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > 1 && date?.start && date?.end) {
|
||||
if (currentPage > 1) {
|
||||
fetchTable(false);
|
||||
}
|
||||
}, [currentPage]);
|
||||
|
|
@ -390,8 +370,7 @@ export function DigitalInnovationPage() {
|
|||
const scrollContainer = scrollContainerRef.current;
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current)
|
||||
return;
|
||||
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return;
|
||||
|
||||
// Clear previous timeout
|
||||
if (scrollTimeoutRef.current) {
|
||||
|
|
@ -411,9 +390,7 @@ export function DigitalInnovationPage() {
|
|||
};
|
||||
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener("scroll", handleScroll, {
|
||||
passive: true,
|
||||
});
|
||||
scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
|
@ -433,23 +410,19 @@ export function DigitalInnovationPage() {
|
|||
direction:
|
||||
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
|
||||
}));
|
||||
fetchTotalCount(date?.start, date?.end);
|
||||
fetchTotalCount();
|
||||
fetchStats();
|
||||
setCurrentPage(1);
|
||||
setProjects([]);
|
||||
setHasMore(true);
|
||||
};
|
||||
|
||||
const fetchTotalCount = async (startDate?: string, endDate?: string) => {
|
||||
const fetchTotalCount = async () => {
|
||||
try {
|
||||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
OutputFields: ["count(project_no)"],
|
||||
Conditions: [
|
||||
["type_of_innovation", "=", "نوآوری دیجیتال", "and"],
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]],
|
||||
});
|
||||
|
||||
if (response.state === 0) {
|
||||
|
|
@ -476,12 +449,11 @@ export function DigitalInnovationPage() {
|
|||
try {
|
||||
setStatsLoading(true);
|
||||
const raw = await apiService.call<any>({
|
||||
innovation_digital_function: {
|
||||
start_date: date?.start || null,
|
||||
end_date: date?.end || null,
|
||||
},
|
||||
innovation_digital_function: {},
|
||||
});
|
||||
|
||||
|
||||
|
||||
// let payload: DigitalInnovationMetrics = raw?.data;
|
||||
// console.log("*-*-*-*" +payload);
|
||||
// if (typeof payload === "string") {
|
||||
|
|
@ -510,6 +482,8 @@ export function DigitalInnovationPage() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const parseNum = (v: unknown): number => {
|
||||
if (v == null) return 0;
|
||||
if (typeof v === "number") return v;
|
||||
|
|
@ -557,33 +531,33 @@ export function DigitalInnovationPage() {
|
|||
// fetchStats();
|
||||
// };
|
||||
|
||||
// const renderProgress = useMemo(() => {
|
||||
// const total = 10;
|
||||
// for (let i = 0; i < rating.length; i++) {
|
||||
// const currentElm = rating[i];
|
||||
// currentElm.house = [];
|
||||
// const greenBoxes = Math.floor((total * currentElm.development) / 100);
|
||||
// const partialPercent =
|
||||
// (total * currentElm.development) / 100 - greenBoxes;
|
||||
// for (let j = 0; j < greenBoxes; j++) {
|
||||
// currentElm.house.push({
|
||||
// index: j,
|
||||
// color: "!bg-emerald-400",
|
||||
// });
|
||||
// }
|
||||
// if (partialPercent != 0 && greenBoxes != 10)
|
||||
// currentElm.house.push({
|
||||
// index: greenBoxes + 1,
|
||||
// style: `linear-gradient(
|
||||
// to right,
|
||||
// oklch(76.5% 0.177 163.223) 0%,
|
||||
// oklch(76.5% 0.177 163.223) ${partialPercent * 100}%,
|
||||
// oklch(55.1% 0.027 264.364) ${partialPercent * 100}%,
|
||||
// oklch(55.1% 0.027 264.364) 100%
|
||||
// )`,
|
||||
// });
|
||||
// }
|
||||
// }, [rating]);
|
||||
const renderProgress = useMemo(() => {
|
||||
const total = 10;
|
||||
for (let i = 0; i < rating.length; i++) {
|
||||
const currentElm = rating[i];
|
||||
currentElm.house = [];
|
||||
const greenBoxes = Math.floor((total * currentElm.development) / 100);
|
||||
const partialPercent =
|
||||
(total * currentElm.development) / 100 - greenBoxes;
|
||||
for (let j = 0; j < greenBoxes; j++) {
|
||||
currentElm.house.push({
|
||||
index: j,
|
||||
color: "!bg-emerald-400",
|
||||
});
|
||||
}
|
||||
if (partialPercent != 0 && greenBoxes != 10)
|
||||
currentElm.house.push({
|
||||
index: greenBoxes + 1,
|
||||
style: `linear-gradient(
|
||||
to right,
|
||||
oklch(76.5% 0.177 163.223) 0%,
|
||||
oklch(76.5% 0.177 163.223) ${partialPercent * 100}%,
|
||||
oklch(55.1% 0.027 264.364) ${partialPercent * 100}%,
|
||||
oklch(55.1% 0.027 264.364) 100%
|
||||
)`,
|
||||
});
|
||||
}
|
||||
}, [rating]);
|
||||
|
||||
const statusColor = (status: projectStatus): any => {
|
||||
let el = null;
|
||||
|
|
@ -627,14 +601,14 @@ export function DigitalInnovationPage() {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleProjectDetails(item)}
|
||||
className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
|
||||
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
|
||||
>
|
||||
جزئیات بیشتر
|
||||
</Button>
|
||||
);
|
||||
case "amount_currency_reduction":
|
||||
return (
|
||||
<span className="font-medium text-pr-green">
|
||||
<span className="font-medium text-emerald-400">
|
||||
{formatCurrency(String(value))}
|
||||
</span>
|
||||
);
|
||||
|
|
@ -645,9 +619,7 @@ export function DigitalInnovationPage() {
|
|||
</Badge>
|
||||
);
|
||||
case "title":
|
||||
return (
|
||||
<span className="font-light text-sm text-white">{String(value)}</span>
|
||||
);
|
||||
return <span className="font-medium text-white">{String(value)}</span>;
|
||||
case "project_status":
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -682,7 +654,7 @@ export function DigitalInnovationPage() {
|
|||
|
||||
return (
|
||||
<DashboardLayout title="نوآوری دیجیتال">
|
||||
<div className="space-y-4 grid justify-between gap-7 pl-6 sm:grid-cols-1 xl:grid-cols-[40%_60%]">
|
||||
<div className="p-6 space-y-4 grid justify-between gap-8 sm:grid-cols-1 xl:grid-cols-[40%_60%]">
|
||||
{/* Stats Cards */}
|
||||
<div className="flex flex-col gap-6 w-full mb-0">
|
||||
<div className="space-y-6 w-full">
|
||||
|
|
@ -755,49 +727,50 @@ export function DigitalInnovationPage() {
|
|||
</div>
|
||||
|
||||
{/* 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-full ">
|
||||
{/* <CardContent > */}
|
||||
<CustomBarChart
|
||||
title="تاثیرات نوآوری دیجیتال به صورت درصد مقایسه ای"
|
||||
loading={statsLoading}
|
||||
// height="100%"
|
||||
height="100%"
|
||||
data={[
|
||||
{
|
||||
label: DigitalCardLabel.decreasCost,
|
||||
value: stats.reduceCostsPercent || 0,
|
||||
color: "bg-pr-green",
|
||||
color: "bg-emerald-400",
|
||||
labelColor: "text-white",
|
||||
},
|
||||
{
|
||||
label: DigitalCardLabel.increaseRevenue,
|
||||
value: stats.increasedRevenuePercent || 0,
|
||||
color: "bg-pr-green",
|
||||
color: "bg-emerald-400",
|
||||
labelColor: "text-white",
|
||||
},
|
||||
{
|
||||
label: DigitalCardLabel.performance,
|
||||
value: stats.resourceProductivityPercent || 0,
|
||||
color: "bg-pr-green",
|
||||
color: "bg-emerald-400",
|
||||
labelColor: "text-white",
|
||||
},
|
||||
{
|
||||
label: DigitalCardLabel.decreaseEnergy,
|
||||
value: stats.reduceEnergyConsumptionPercent || 0,
|
||||
color: "bg-pr-green",
|
||||
color: "bg-emerald-400",
|
||||
labelColor: "text-white",
|
||||
},
|
||||
]}
|
||||
barHeight="h-5"
|
||||
showAxisLabels={true}
|
||||
/>
|
||||
</BaseCard>
|
||||
{/* </CardContent> */}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Data Table */}
|
||||
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden w-full h-max">
|
||||
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden w-full h-[39.7rem]">
|
||||
<CardContent className="p-0">
|
||||
<div className="relative h-full">
|
||||
<Table containerClassName="overflow-auto custom-scrollbar w-full h-[calc(100vh-160px)] ">
|
||||
<Table containerClassName="overflow-auto custom-scrollbar w-full h-[36.8rem] ">
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#3F415A]">
|
||||
{columns.map((column) => (
|
||||
|
|
@ -976,13 +949,13 @@ export function DigitalInnovationPage() {
|
|||
|
||||
{/* Project Details Dialog */}
|
||||
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
||||
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-6xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-5xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
|
||||
شرح پروژه
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="body grid grid-cols-[40%_20%_40%] pb-6">
|
||||
<div className="body grid grid-cols-[40%_20%_40%]">
|
||||
<div className="border-l-2 border-l-gray-600 px-6">
|
||||
<span className="title text-lg font-bold">
|
||||
{dialogInfo?.title}
|
||||
|
|
@ -1034,7 +1007,7 @@ export function DigitalInnovationPage() {
|
|||
</div>
|
||||
<div className="digitalAbilityDevelopment flex flex-col gap-10 border-l-2 border-l-gray-600 px-5">
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-lg font-bold">
|
||||
<span className="text-md font-bold">
|
||||
توسعه قابلیت های دیجیتال:{" "}
|
||||
</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
|
|
@ -1097,10 +1070,10 @@ export function DigitalInnovationPage() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col px-6 gap-4">
|
||||
<div className="flex flex-col pr-7 gap-4">
|
||||
<div className="costBoard mx-auto w-full">
|
||||
<div className="board o border border-gray-600 rounded-xl overflow-hidden flex flex-col">
|
||||
<span className="text-sm bg-[#3F415A] text-white w-full p-2.5 pr-4 ">
|
||||
<span className="title bg-[#3F415A] text-white w-full p-2.5 pr-4 ">
|
||||
کاهش هزینه ها
|
||||
</span>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// import moment from "moment-jalaali";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
|
|
@ -25,7 +27,6 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
import { EventBus, formatNumber } from "~/lib/utils";
|
||||
|
||||
import {
|
||||
Building2,
|
||||
|
|
@ -42,17 +43,12 @@ import {
|
|||
UsersIcon,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import moment from "moment-jalaali";
|
||||
import toast from "react-hot-toast";
|
||||
import { MetricCard } from "~/components/ui/metric-card";
|
||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
||||
import apiService from "~/lib/api";
|
||||
import { formatCurrency } from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import DashboardLayout from "../layout";
|
||||
|
||||
moment.loadPersian({ usePersianDigits: true });
|
||||
|
||||
// moment.loadPersian({ usePersianDigits: true });
|
||||
interface GreenInnovationData {
|
||||
WorkflowID: string;
|
||||
approved_budget: string;
|
||||
|
|
@ -170,8 +166,6 @@ export function GreenInnovationPage() {
|
|||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [actualTotalCount, setActualTotalCount] = useState(0);
|
||||
const [statsLoading, setStatsLoading] = useState(false);
|
||||
const [date, setDate] = useStoredDate();
|
||||
|
||||
const [stats, setStats] = useState<stateCounter>();
|
||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||
field: "start_date",
|
||||
|
|
@ -294,11 +288,7 @@ export function GreenInnovationPage() {
|
|||
"observer",
|
||||
],
|
||||
Sorts: [[sortConfig.field, sortConfig.direction]],
|
||||
Conditions: [
|
||||
["type_of_innovation", "=", "نوآوری سبز", "and"],
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
|
||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||
});
|
||||
if (response.state === 0) {
|
||||
|
|
@ -360,18 +350,6 @@ export function GreenInnovationPage() {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (date: CalendarDate) => {
|
||||
if (date) setDate(date);
|
||||
};
|
||||
|
||||
EventBus.on("dateSelected", handler);
|
||||
|
||||
return () => {
|
||||
EventBus.off("dateSelected", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (hasMore && !loading) {
|
||||
setCurrentPage((prev) => prev + 1);
|
||||
|
|
@ -379,15 +357,13 @@ export function GreenInnovationPage() {
|
|||
}, [hasMore, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (date.end && date.start) {
|
||||
fetchProjects(true);
|
||||
fetchTotalCount();
|
||||
}
|
||||
}, [sortConfig, date]);
|
||||
}, [sortConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (date.end && date.start) fetchStats();
|
||||
}, [selectedProjects, date]);
|
||||
fetchStats();
|
||||
}, [selectedProjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > 1) {
|
||||
|
|
@ -440,11 +416,7 @@ export function GreenInnovationPage() {
|
|||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
OutputFields: ["count(project_no)"],
|
||||
Conditions: [
|
||||
["type_of_innovation", "=", "نوآوری سبز", "and"],
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
|
||||
});
|
||||
if (response.state === 0) {
|
||||
const dataString = response.data;
|
||||
|
|
@ -476,8 +448,6 @@ export function GreenInnovationPage() {
|
|||
selectedProjects.size > 0
|
||||
? Array.from(selectedProjects).join(" , ")
|
||||
: "",
|
||||
start_date: date?.start || null,
|
||||
end_date: date?.end || null,
|
||||
},
|
||||
});
|
||||
let payload: any = raw?.data;
|
||||
|
|
@ -524,13 +494,13 @@ export function GreenInnovationPage() {
|
|||
},
|
||||
|
||||
pollution: {
|
||||
value: parseNum(stats.pollution_reduction),
|
||||
percent: parseNum(stats.pollution_reduction_percent),
|
||||
value: formatNumber(parseNum(stats.pollution_reduction)),
|
||||
percent: formatNumber(parseNum(stats.pollution_reduction_percent)),
|
||||
},
|
||||
|
||||
waste: {
|
||||
value: parseNum(stats.waste_reduction),
|
||||
percent: parseNum(stats.waste_reductionn_percent),
|
||||
value: formatNumber(parseNum(stats.waste_reduction)),
|
||||
percent: formatNumber(parseNum(stats.waste_reductionn_percent)),
|
||||
},
|
||||
avarage: stats.average_project_score,
|
||||
countInnovationGreenProjects: stats.count_innovation_green_projects,
|
||||
|
|
@ -548,6 +518,7 @@ export function GreenInnovationPage() {
|
|||
setStatsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const setPageData = (normalized: any) => {
|
||||
setSustainabilityStats((prev) => ({
|
||||
...prev,
|
||||
|
|
@ -631,14 +602,14 @@ export function GreenInnovationPage() {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleProjectDetails(item)}
|
||||
className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
|
||||
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
|
||||
>
|
||||
جزئیات بیشتر
|
||||
</Button>
|
||||
);
|
||||
case "amount_currency_reduction":
|
||||
return (
|
||||
<span className="font-medium text-pr-green">
|
||||
<span className="font-medium text-emerald-400">
|
||||
{formatCurrency(String(value))}
|
||||
</span>
|
||||
);
|
||||
|
|
@ -649,9 +620,7 @@ export function GreenInnovationPage() {
|
|||
</Badge>
|
||||
);
|
||||
case "title":
|
||||
return (
|
||||
<span className="font-light text-sm text-white">{String(value)}</span>
|
||||
);
|
||||
return <span className="font-medium text-white">{String(value)}</span>;
|
||||
case "project_status":
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -717,7 +686,7 @@ export function GreenInnovationPage() {
|
|||
|
||||
return (
|
||||
<DashboardLayout title="نوآوری سبز">
|
||||
<div className="space-y-4 h-[23.5rem]">
|
||||
<div className="p-6 space-y-4 h-[23.5rem]">
|
||||
{/* Stats Cards */}
|
||||
<div className="flex gap-6 mb-5 md:flex-col xl:flex-row">
|
||||
<div className="flex flex-col justify-between xl:w-1/2 sm:w-full sm:gap-2">
|
||||
|
|
@ -751,14 +720,39 @@ export function GreenInnovationPage() {
|
|||
</Card>
|
||||
))
|
||||
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
||||
<MetricCard
|
||||
<Card
|
||||
key={key}
|
||||
title={value.title}
|
||||
value={Math.round(value.total.value || 0)}
|
||||
valueLabel={value.total?.description}
|
||||
percentValue={value.percent?.value || 0}
|
||||
percentLabel={value.percent?.description}
|
||||
/>
|
||||
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
|
||||
>
|
||||
<CardContent className="p-0 h-full">
|
||||
<div className="flex flex-col justify-between gap-2 h-full">
|
||||
<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-emerald-400 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-emerald-400 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>
|
||||
|
||||
|
|
@ -819,10 +813,7 @@ export function GreenInnovationPage() {
|
|||
<div className="params flex flex-col gap-3.5">
|
||||
{Object.entries(recycleParams).map((el, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="param flex flex-row justify-between items-center"
|
||||
>
|
||||
<div key={index} className="param flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row gap-2">
|
||||
{el[1].icon}
|
||||
<span className="font-normal text-sm font-persian">
|
||||
|
|
@ -904,7 +895,7 @@ export function GreenInnovationPage() {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
<Card className="w-1/2 bg-pr-gray backdrop-blur-sm rounded-lg overflow-hidden">
|
||||
<Card className="w-1/3 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg overflow-hidden">
|
||||
<CardContent className="p-0">
|
||||
<div className="border-b-2 border-gray-500/20">
|
||||
<div className="flex flex-row justify-between w-full p-4">
|
||||
|
|
@ -955,7 +946,7 @@ export function GreenInnovationPage() {
|
|||
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden">
|
||||
<CardContent className="p-0">
|
||||
<div className="relative">
|
||||
<Table containerClassName="overflow-auto custom-scrollbar h-full">
|
||||
<Table containerClassName="overflow-auto custom-scrollbar h-[25rem]">
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#3F415A]">
|
||||
{columns.map((column) => (
|
||||
|
|
@ -1119,7 +1110,7 @@ export function GreenInnovationPage() {
|
|||
شرح پروژه
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 flex justify-between text-right p-6">
|
||||
<div className="space-y-4 flex justify-between text-right px-6">
|
||||
{/* Project Description */}
|
||||
<div className="flex-[4] border-l-2 border-gray-600">
|
||||
<h2 className="font-bold">{selectedProjectDetails?.title}</h2>
|
||||
|
|
@ -1165,7 +1156,7 @@ export function GreenInnovationPage() {
|
|||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
|
||||
<UsersIcon className="h-4 text-green-500" />
|
||||
هزینه برآورد شده(میلیون ریال):
|
||||
هزینه برآورد شده:
|
||||
</h4>
|
||||
<span className="text-white font-bold font-persian">
|
||||
{formatNumber(
|
||||
|
|
|
|||
|
|
@ -39,11 +39,8 @@ import {
|
|||
ResponsiveContainer,
|
||||
XAxis,
|
||||
} from "recharts";
|
||||
import { MetricCard } from "~/components/ui/metric-card";
|
||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
||||
import apiService from "~/lib/api";
|
||||
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import { formatCurrency, formatNumber } from "~/lib/utils";
|
||||
import DashboardLayout from "../layout";
|
||||
|
||||
interface innovationBuiltInDate {
|
||||
|
|
@ -155,8 +152,8 @@ enum projectStatus {
|
|||
|
||||
const columns = [
|
||||
{ key: "select", label: "", sortable: false, width: "50px" },
|
||||
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "120px" },
|
||||
{ key: "title", label: "عنوان پروژه", sortable: true, width: "300px" },
|
||||
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" },
|
||||
{ key: "title", label: "عنوان پروژه", sortable: true, width: "400px" },
|
||||
{
|
||||
key: "project_status",
|
||||
label: "وضعیت پروژه",
|
||||
|
|
@ -167,7 +164,7 @@ const columns = [
|
|||
key: "project_rating",
|
||||
label: "امتیاز پروژه",
|
||||
sortable: true,
|
||||
width: "120px",
|
||||
width: "140px",
|
||||
},
|
||||
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" },
|
||||
];
|
||||
|
|
@ -194,8 +191,6 @@ export function InnovationBuiltInsidePage() {
|
|||
field: "start_date",
|
||||
direction: "asc",
|
||||
});
|
||||
|
||||
const [date, setDate] = useStoredDate();
|
||||
const [tblAvarage, setTblAvarage] = useState<number>(0);
|
||||
const [selectedProjects, setSelectedProjects] =
|
||||
useState<Set<string | number>>();
|
||||
|
|
@ -315,11 +310,7 @@ export function InnovationBuiltInsidePage() {
|
|||
"technology_maturity_level",
|
||||
],
|
||||
Sorts: [[sortConfig.field, sortConfig.direction]],
|
||||
Conditions: [
|
||||
["type_of_innovation", "=", "نوآوری ساخت داخل", "and"],
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
Conditions: [["type_of_innovation", "=", "نوآوری ساخت داخل"]],
|
||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||
});
|
||||
if (response.state === 0) {
|
||||
|
|
@ -426,24 +417,12 @@ export function InnovationBuiltInsidePage() {
|
|||
}, [hasMore, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (date: CalendarDate) => {
|
||||
if (date) setDate(date);
|
||||
};
|
||||
|
||||
EventBus.on("dateSelected", handler);
|
||||
|
||||
return () => {
|
||||
EventBus.off("dateSelected", handler);
|
||||
};
|
||||
}, []);
|
||||
fetchProjects(true);
|
||||
}, [sortConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (date.start && date.end) fetchProjects(true);
|
||||
}, [sortConfig, date]);
|
||||
|
||||
useEffect(() => {
|
||||
if (date.end && date.start) fetchStats();
|
||||
}, [selectedProjects, date]);
|
||||
fetchStats();
|
||||
}, [selectedProjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > 1) {
|
||||
|
|
@ -501,8 +480,6 @@ export function InnovationBuiltInsidePage() {
|
|||
selectedProjects && selectedProjects?.size > 0
|
||||
? Array.from(selectedProjects).join(" , ")
|
||||
: "",
|
||||
start_date: date?.start || null,
|
||||
end_date: date?.end || null,
|
||||
},
|
||||
});
|
||||
let payload: any = raw?.data;
|
||||
|
|
@ -528,13 +505,15 @@ export function InnovationBuiltInsidePage() {
|
|||
const stats = data[0];
|
||||
const normalized: any = {
|
||||
currencySaving: {
|
||||
value: parseNum(stats?.foreign_currency_saving),
|
||||
percent: parseNum(stats?.foreign_currency_saving_percent),
|
||||
value: formatNumber(parseNum(stats?.foreign_currency_saving)),
|
||||
percent: formatNumber(
|
||||
parseNum(stats?.foreign_currency_saving_percent)
|
||||
),
|
||||
},
|
||||
|
||||
investmentAmount: {
|
||||
value: parseNum(stats?.investment_amount),
|
||||
percent: parseNum(stats?.investment_amount_percent),
|
||||
value: formatNumber(parseNum(stats?.investment_amount)),
|
||||
percent: formatNumber(parseNum(stats?.investment_amount_percent)),
|
||||
},
|
||||
|
||||
technology: {
|
||||
|
|
@ -645,14 +624,14 @@ export function InnovationBuiltInsidePage() {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleProjectDetails(item)}
|
||||
className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
|
||||
className="text-emerald-500 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto cursor-pointer"
|
||||
>
|
||||
جزئیات بیشتر
|
||||
</Button>
|
||||
);
|
||||
case "amount_currency_reduction":
|
||||
return (
|
||||
<span className="font-medium text-pr-green">
|
||||
<span className="font-medium text-emerald-500">
|
||||
{formatCurrency(String(value))}
|
||||
</span>
|
||||
);
|
||||
|
|
@ -663,9 +642,7 @@ export function InnovationBuiltInsidePage() {
|
|||
</Badge>
|
||||
);
|
||||
case "title":
|
||||
return (
|
||||
<span className="font-light text-sm text-white">{String(value)}</span>
|
||||
);
|
||||
return <span className="font-medium text-white">{String(value)}</span>;
|
||||
case "project_status":
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -724,10 +701,10 @@ export function InnovationBuiltInsidePage() {
|
|||
|
||||
return (
|
||||
<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="p-6 space-y-4 justify-between gap-8 grid sm:grid-cols-1 xl:grid-cols-[40%_60%]">
|
||||
{/* Stats Cards */}
|
||||
<div className="flex w-full mb-0">
|
||||
<div className="flex flex-col w-full justify-between gap-2">
|
||||
<div className="flex gap-6 w-full mb-0">
|
||||
<div className="flex flex-col justify-between w-full gap-6">
|
||||
{statsLoading
|
||||
? // Loading skeleton for stats cards - matching new design
|
||||
Array.from({ length: 2 }).map((_, index) => (
|
||||
|
|
@ -758,47 +735,39 @@ export function InnovationBuiltInsidePage() {
|
|||
</Card>
|
||||
))
|
||||
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
||||
<MetricCard
|
||||
<Card
|
||||
key={key}
|
||||
title={value.title}
|
||||
value={Math.round(value.total.value || 0)}
|
||||
valueLabel={value.total?.description}
|
||||
percentValue={value.percent?.value || 0}
|
||||
percentLabel={value.percent?.description}
|
||||
/>
|
||||
// <Card
|
||||
// key={key}
|
||||
// className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
|
||||
// >
|
||||
// <CardContent className="p-0 h-full">
|
||||
// <div className="flex flex-col justify-between gap-2 h-full">
|
||||
// <div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
|
||||
// <h3 className="text-lg font-semibold text-white 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>
|
||||
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
|
||||
>
|
||||
<CardContent className="p-0 h-full">
|
||||
<div className="flex flex-col justify-between gap-2 h-full">
|
||||
<div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
|
||||
<h3 className="text-lg font-semibold text-white 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-emerald-500 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-emerald-500 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>
|
||||
))}
|
||||
|
||||
{statsLoading ? (
|
||||
|
|
@ -898,7 +867,7 @@ export function InnovationBuiltInsidePage() {
|
|||
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden w-full h-max">
|
||||
<CardContent className="p-0">
|
||||
<div className="relative ">
|
||||
<Table containerClassName="overflow-auto custom-scrollbar h-[calc(100vh-160px)]">
|
||||
<Table containerClassName="overflow-auto custom-scrollbar h-[calc(90vh-15px)]">
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#3F415A]">
|
||||
{columns.map((column) => (
|
||||
|
|
@ -1061,7 +1030,7 @@ export function InnovationBuiltInsidePage() {
|
|||
|
||||
{/* Project Details Dialog */}
|
||||
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
||||
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-5xl overflow-y-auto">
|
||||
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-6xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
|
||||
شرح پروژه
|
||||
|
|
@ -1123,6 +1092,7 @@ export function InnovationBuiltInsidePage() {
|
|||
<div className="flex flex-col justify-center items-center">
|
||||
<span className="block w-0.5 h-14 bg-white"></span>
|
||||
<span className="text-white border border-white p-1 px-2 text-xs rounded-lg">
|
||||
{" "}
|
||||
سطح تکنولوژی
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -1286,7 +1256,7 @@ export function InnovationBuiltInsidePage() {
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<ResponsiveContainer width="100%" height={420}>
|
||||
<LineChart
|
||||
data={dialogChartData}
|
||||
margin={{ top: 20, right: 70, left: 30, bottom: 80 }}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,8 @@ import moment from "moment-jalaali";
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { BaseCard } from "~/components/ui/base-card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import { BaseCard } from "~/components/ui/base-card";
|
||||
import { Checkbox } from "~/components/ui/checkbox";
|
||||
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
|
||||
import {
|
||||
|
|
@ -34,11 +33,10 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
||||
import apiService from "~/lib/api";
|
||||
import { EventBus, formatNumber } from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
import { DashboardLayout } from "../layout";
|
||||
import { Card , CardContent} from "~/components/ui/card";
|
||||
|
||||
moment.loadPersian({ usePersianDigits: true });
|
||||
interface ProcessInnovationData {
|
||||
|
|
@ -67,11 +65,9 @@ interface ProjectStats {
|
|||
percent_reduction_value_currency: string;
|
||||
percent_sum_stopping_production: string;
|
||||
percent_throat_removal: string;
|
||||
percent_operating_cost_before_innovation: string;
|
||||
sum_reducing_breakdowns: number;
|
||||
sum_reduction_value_currency: number;
|
||||
sum_stopping_production: number;
|
||||
sum_operating_cost_reduction: number;
|
||||
}
|
||||
|
||||
interface SortConfig {
|
||||
|
|
@ -96,11 +92,9 @@ interface InnovationStats {
|
|||
currencyReductionSum: number; // مجموع کاهش ارز بری (میلیون ریال)
|
||||
frequentFailuresReductionSum: number; // مجموع کاهش خرابی های پرتکرار
|
||||
percentProductionStops: number | string; // درصد مقایسهای جلوگیری از توقفات تولید
|
||||
reductionCostOprationSum: number; // مجموع کاهش هزینه عملیاتی
|
||||
percentBottleneckRemoval: number | string; // درصد مقایسهای رفع گلوگاه
|
||||
percentCurrencyReduction: number | string; // درصد مقایسهای کاهش ارز بری
|
||||
percentFailuresReduction: number | string; // درصد مقایسهای کاهش خرابیهای پرتکرار
|
||||
percentOperatingCostBeforeInnovation: number | string; // درصد مقایسهای کاهش هزینه عملیاتی
|
||||
}
|
||||
|
||||
const columns = [
|
||||
|
|
@ -129,14 +123,13 @@ export function ProcessInnovationPage() {
|
|||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize] = useState(20);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [date, setDate] = useStoredDate();
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [actualTotalCount, setActualTotalCount] = useState(0);
|
||||
const [statsLoading, setStatsLoading] = useState(false);
|
||||
const [stats, setStats] = useState<InnovationStats>({
|
||||
totalProjects: 0,
|
||||
averageScore: 0,
|
||||
productionStopsPreventionSum: 0,
|
||||
reductionCostOprationSum: 0,
|
||||
bottleneckRemovalCount: 0,
|
||||
currencyReductionSum: 0,
|
||||
frequentFailuresReductionSum: 0,
|
||||
|
|
@ -144,7 +137,6 @@ export function ProcessInnovationPage() {
|
|||
percentBottleneckRemoval: 0,
|
||||
percentCurrencyReduction: 0,
|
||||
percentFailuresReduction: 0,
|
||||
percentOperatingCostBeforeInnovation: 0,
|
||||
});
|
||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||
field: "start_date",
|
||||
|
|
@ -160,60 +152,58 @@ export function ProcessInnovationPage() {
|
|||
const [stateCard, setStateCard] = useState({
|
||||
productionstopsprevention: {
|
||||
id: "productionstopsprevention",
|
||||
title: "توقفات تولید",
|
||||
title: "جلوگیری از توقفات تولید",
|
||||
value: formatNumber(
|
||||
stats.productionStopsPreventionSum.toFixed?.(1) ??
|
||||
stats.productionStopsPreventionSum
|
||||
),
|
||||
description: "تن افزایش یافته",
|
||||
icon: CirclePause,
|
||||
color: "text-pr-green",
|
||||
color: "text-emerald-400",
|
||||
},
|
||||
bottleneckremoval: {
|
||||
id: "bottleneckremoval",
|
||||
title: "گلوگاه ها",
|
||||
title: "رفع گلوگاه",
|
||||
value: formatNumber(stats.bottleneckRemovalCount),
|
||||
description: "تعداد رفع گلوگاه",
|
||||
icon: Funnel,
|
||||
color: "text-pr-green",
|
||||
color: "text-emerald-400",
|
||||
},
|
||||
currencyreduction: {
|
||||
id: "currencyreduction",
|
||||
title: "ارز بری",
|
||||
title: "کاهش ارز بری",
|
||||
value: formatNumber(
|
||||
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
|
||||
),
|
||||
description: "دلار کاهش یافته",
|
||||
icon: DollarSign,
|
||||
color: "text-pr-green",
|
||||
},
|
||||
decreaseCurrencyOperation: {
|
||||
id: "decreaseCurrencyOperation",
|
||||
title: "هزینه های عملیاتی",
|
||||
value: formatNumber(
|
||||
stats.reductionCostOprationSum.toFixed?.(0) ??
|
||||
stats.reductionCostOprationSum
|
||||
),
|
||||
description: "میلیون ریال کاهش یافته",
|
||||
icon: DollarSign,
|
||||
color: "text-pr-green",
|
||||
icon: DollarSign ,
|
||||
color: "text-emerald-400",
|
||||
},
|
||||
frequentfailuresreduction: {
|
||||
id: "frequentfailuresreduction",
|
||||
title: "خرابی های پرتکرار",
|
||||
title: "کاهش خرابی های پرتکرار",
|
||||
value: formatNumber(
|
||||
stats.frequentFailuresReductionSum.toFixed?.(1) ??
|
||||
stats.frequentFailuresReductionSum
|
||||
),
|
||||
description: "خرابی پر تکرار کاهش یافته",
|
||||
description: "مجموع درصد کاهش خرابی",
|
||||
icon: Wrench,
|
||||
color: "text-pr-green",
|
||||
color: "text-emerald-400",
|
||||
},
|
||||
});
|
||||
|
||||
const observerRef = useRef<HTMLDivElement>(null);
|
||||
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 newSelected = new Set(selectedProjects);
|
||||
if (newSelected.has(projectNo)) {
|
||||
|
|
@ -266,11 +256,7 @@ export function ProcessInnovationPage() {
|
|||
"observer",
|
||||
],
|
||||
Sorts: [["start_date", "asc"]],
|
||||
Conditions: [
|
||||
["type_of_innovation", "=", "نوآوری در فرآیند", "and"],
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]],
|
||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||
});
|
||||
|
||||
|
|
@ -282,16 +268,16 @@ export function ProcessInnovationPage() {
|
|||
if (Array.isArray(parsedData)) {
|
||||
if (reset) {
|
||||
setProjects(parsedData);
|
||||
// setTotalCount(parsedData.length);
|
||||
setTotalCount(parsedData.length);
|
||||
} else {
|
||||
setProjects((prev) => [...prev, ...parsedData]);
|
||||
// setTotalCount((prev) => prev + parsedData.length);
|
||||
setTotalCount((prev) => prev + parsedData.length);
|
||||
}
|
||||
setHasMore(parsedData.length === pageSize);
|
||||
} else {
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
// setTotalCount(0);
|
||||
setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -299,14 +285,14 @@ export function ProcessInnovationPage() {
|
|||
console.error("Error parsing project data:", parseError);
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
// setTotalCount(0);
|
||||
setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
} else {
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
// setTotalCount(0);
|
||||
setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -314,7 +300,7 @@ export function ProcessInnovationPage() {
|
|||
toast.error(response.message || "خطا در دریافت اطلاعات پروژهها");
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
// setTotalCount(0);
|
||||
setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -323,7 +309,7 @@ export function ProcessInnovationPage() {
|
|||
toast.error("خطا در دریافت اطلاعات پروژهها");
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
// setTotalCount(0);
|
||||
setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
} finally {
|
||||
|
|
@ -340,27 +326,13 @@ export function ProcessInnovationPage() {
|
|||
}, [hasMore, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (date: CalendarDate) => {
|
||||
if (date) setDate(date);
|
||||
};
|
||||
|
||||
EventBus.on("dateSelected", handler);
|
||||
|
||||
return () => {
|
||||
EventBus.off("dateSelected", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (date?.start && date?.end) {
|
||||
fetchProjects(true);
|
||||
fetchTotalCount();
|
||||
}
|
||||
}, [sortConfig, date]);
|
||||
}, [sortConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (date?.start && date?.end) fetchStats();
|
||||
}, [selectedProjects, date]);
|
||||
fetchStats();
|
||||
}, [selectedProjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > 1) {
|
||||
|
|
@ -410,11 +382,7 @@ export function ProcessInnovationPage() {
|
|||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
OutputFields: ["count(project_no)"],
|
||||
Conditions: [
|
||||
["type_of_innovation", "=", "نوآوری در فرآیند", "and"],
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]],
|
||||
});
|
||||
|
||||
if (response.state === 0) {
|
||||
|
|
@ -448,8 +416,6 @@ export function ProcessInnovationPage() {
|
|||
selectedProjects.size > 0
|
||||
? Array.from(selectedProjects).join(" , ")
|
||||
: "",
|
||||
start_date: date?.start || null,
|
||||
end_date: date?.end || null,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -480,13 +446,10 @@ export function ProcessInnovationPage() {
|
|||
totalProjects: parseNum(stats?.count_innovation_process_projects),
|
||||
averageScore: parseFloat(data[0].average_project_score),
|
||||
productionStopsPreventionSum: parseNum(stats?.sum_stopping_production),
|
||||
reductionCostOprationSum: parseNum(stats?.sum_operating_cost_reduction),
|
||||
bottleneckRemovalCount: parseNum(stats?.count_throat_removal),
|
||||
currencyReductionSum: parseNum(stats?.sum_reduction_value_currency),
|
||||
frequentFailuresReductionSum: parseNum(stats?.sum_reducing_breakdowns),
|
||||
percentProductionStops: stats?.percent_sum_stopping_production,
|
||||
percentOperatingCostBeforeInnovation:
|
||||
stats?.percent_operating_cost_before_innovation,
|
||||
percentBottleneckRemoval: stats?.percent_throat_removal,
|
||||
percentCurrencyReduction: stats?.percent_reduction_value_currency,
|
||||
percentFailuresReduction: stats?.percent_reducing_breakdowns,
|
||||
|
|
@ -509,10 +472,6 @@ export function ProcessInnovationPage() {
|
|||
...prev.currencyreduction,
|
||||
value: formatNumber(normalized.currencyReductionSum),
|
||||
},
|
||||
decreaseCurrencyOperation: {
|
||||
...prev.decreaseCurrencyOperation,
|
||||
value: formatNumber(normalized.reductionCostOprationSum),
|
||||
},
|
||||
}));
|
||||
setStats(normalized);
|
||||
} catch (error) {
|
||||
|
|
@ -565,7 +524,7 @@ export function ProcessInnovationPage() {
|
|||
<Checkbox
|
||||
checked={selectedProjects.has(item.project_id)}
|
||||
onCheckedChange={() => handleSelectProject(item.project_id)}
|
||||
className="data-[state=checked]:bg-pr-green data-[state=checked]:border-pr-green"
|
||||
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
||||
/>
|
||||
);
|
||||
case "details":
|
||||
|
|
@ -574,14 +533,14 @@ export function ProcessInnovationPage() {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleProjectDetails(item)}
|
||||
className="text-pr-green underline-offset-4 underline font-normal p-2 h-auto"
|
||||
className="text-pr-green hover:text-emerald-300 underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
|
||||
>
|
||||
جزئیات بیشتر
|
||||
</Button>
|
||||
);
|
||||
case "amount_currency_reduction":
|
||||
return (
|
||||
<span className="font-medium text-pr-green">
|
||||
<span className="font-medium text-emerald-400">
|
||||
{formatCurrency(String(value))}
|
||||
</span>
|
||||
);
|
||||
|
|
@ -592,11 +551,7 @@ export function ProcessInnovationPage() {
|
|||
</Badge>
|
||||
);
|
||||
case "title":
|
||||
return (
|
||||
<span className="font-normal text-sm text-white">
|
||||
{String(value)}
|
||||
</span>
|
||||
);
|
||||
return <span className="font-normal text-sm text-white">{String(value)}</span>;
|
||||
case "project_status":
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -612,10 +567,7 @@ export function ProcessInnovationPage() {
|
|||
);
|
||||
case "project_rating":
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-base font-semibold text-center border-none"
|
||||
>
|
||||
<Badge variant="outline" className="text-base font-semibold text-center border-none">
|
||||
{formatNumber(String(value))}
|
||||
</Badge>
|
||||
);
|
||||
|
|
@ -634,27 +586,23 @@ export function ProcessInnovationPage() {
|
|||
|
||||
return (
|
||||
<DashboardLayout title="نوآوری در فرآیند">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="p-6 py-2 space-y-4">
|
||||
{/* Stats Cards */}
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-6">
|
||||
<div className="space-y-4 w-full">
|
||||
{/* Stats Grid */}
|
||||
<div className="h-full">
|
||||
{loading || statsLoading ? (
|
||||
// Skeleton cards
|
||||
<div className="flex flex-wrap justify-between gap-3">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<BaseCard
|
||||
key={`skeleton-${index}`}
|
||||
className="rounded-2xl overflow-hidden w-full sm:w-[48%] md:w-[30%]"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{loading || statsLoading
|
||||
? // Loading skeleton for stats cards - matching new design
|
||||
Array.from({ length: 4 }).map((_, index) => (
|
||||
<BaseCard key={`skeleton-${index}`} className="rounded-2xl overflow-hidden">
|
||||
<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="h-6 bg-gray-600 rounded animate-pulse"
|
||||
style={{ width: "60%" }}
|
||||
/>
|
||||
<div className="p-3 rounded-full w-fit">
|
||||
<div className="p-3 bg-emerald-500/20 rounded-full w-fit">
|
||||
<div className="w-6 h-6 bg-gray-600 rounded animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -670,112 +618,39 @@ export function ProcessInnovationPage() {
|
|||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full gap-5">
|
||||
<div className="flex flex-row gap-4 h-full">
|
||||
<BaseCard
|
||||
key={stateCard.productionstopsprevention.id}
|
||||
title={stateCard.productionstopsprevention.title}
|
||||
className="border-gray-700/50 w-full"
|
||||
icon={stateCard.productionstopsprevention.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.productionstopsprevention.value}
|
||||
</p>
|
||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
||||
{stateCard.productionstopsprevention.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
))
|
||||
: Object.entries(stateCard).map(([key, card]) => {
|
||||
// map percent values for each card key
|
||||
const percentMap: Record<string, number | string | undefined> = {
|
||||
productionstopsprevention: stats.percentProductionStops,
|
||||
bottleneckremoval: stats.percentBottleneckRemoval,
|
||||
currencyreduction: stats.percentCurrencyReduction,
|
||||
frequentfailuresreduction: stats.percentFailuresReduction,
|
||||
};
|
||||
const percentValue = percentMap[key];
|
||||
|
||||
return (
|
||||
<BaseCard
|
||||
key={stateCard.frequentfailuresreduction.id}
|
||||
title={stateCard.frequentfailuresreduction.title}
|
||||
className="border-gray-700/50 w-full"
|
||||
icon={stateCard.frequentfailuresreduction.icon}
|
||||
key={card.id}
|
||||
title={card.title}
|
||||
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="text-center">
|
||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
||||
{stateCard.frequentfailuresreduction.value}
|
||||
{(card.value)}
|
||||
</p>
|
||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
||||
{stateCard.frequentfailuresreduction.description}
|
||||
{card.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 h-full">
|
||||
<BaseCard
|
||||
key={stateCard.currencyreduction.id}
|
||||
title={stateCard.currencyreduction.title}
|
||||
className="border-gray-700/50 w-full"
|
||||
icon={stateCard.currencyreduction.icon}
|
||||
>
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-center">
|
||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
||||
{stateCard.currencyreduction.value}
|
||||
</p>
|
||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
||||
{stateCard.currencyreduction.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
<BaseCard
|
||||
key={stateCard.decreaseCurrencyOperation.id}
|
||||
title={stateCard.decreaseCurrencyOperation.title}
|
||||
className="border-gray-700/50 w-full"
|
||||
icon={stateCard.decreaseCurrencyOperation.icon}
|
||||
>
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-center">
|
||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
||||
{stateCard.decreaseCurrencyOperation.value}
|
||||
</p>
|
||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
||||
{stateCard.decreaseCurrencyOperation.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
<BaseCard
|
||||
key={stateCard.bottleneckremoval.id}
|
||||
title={stateCard.bottleneckremoval.title}
|
||||
className="border-gray-700/50 w-full"
|
||||
icon={stateCard.bottleneckremoval.icon}
|
||||
>
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-center">
|
||||
<p className="text-3xl text-pr-green font-bold mb-1">
|
||||
{stateCard.bottleneckremoval.value}
|
||||
</p>
|
||||
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
|
||||
{stateCard.bottleneckremoval.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -786,13 +661,13 @@ export function ProcessInnovationPage() {
|
|||
- Nice Max: 75 (گرد و خوانا)
|
||||
- Ticks: [0, 20, 40, 60, 75]
|
||||
این باعث میشود نمودار زیباتر و خواناتر باشد */}
|
||||
<BaseCard className="rounded-xl w-full overflow-hidden">
|
||||
<BaseCard className="rounded-2xl w-full overflow-hidden">
|
||||
<CustomBarChart
|
||||
title="تاثیرات فرآیندی به صورت درصد مقایسه ای"
|
||||
loading={statsLoading}
|
||||
data={[
|
||||
{
|
||||
label: "توقفات تولید",
|
||||
label: "کاهش توقفات تولید",
|
||||
value: Number(stats.percentProductionStops) || 0,
|
||||
labelColor: "text-white",
|
||||
},
|
||||
|
|
@ -802,23 +677,17 @@ export function ProcessInnovationPage() {
|
|||
labelColor: "text-white",
|
||||
},
|
||||
{
|
||||
label: "ارز بری",
|
||||
label: "کاهش ارز بری",
|
||||
value: Number(stats.percentCurrencyReduction) || 0,
|
||||
labelColor: "text-white",
|
||||
},
|
||||
{
|
||||
label: "خرابی پر تکرار",
|
||||
label: "کاهش خرابی پر تکرار",
|
||||
value: Number(stats.percentFailuresReduction) || 0,
|
||||
labelColor: "text-white",
|
||||
},
|
||||
{
|
||||
label: "هزینه های عملیاتی",
|
||||
value:
|
||||
Number(stats.percentOperatingCostBeforeInnovation) || 0,
|
||||
labelColor: "text-white",
|
||||
},
|
||||
]}
|
||||
barHeight="h-5"
|
||||
barHeight="h-6"
|
||||
showAxisLabels={true}
|
||||
/>
|
||||
</BaseCard>
|
||||
|
|
@ -828,7 +697,7 @@ export function ProcessInnovationPage() {
|
|||
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
|
||||
<CardContent className="p-0">
|
||||
<div className="relative">
|
||||
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(90vh-420px)]">
|
||||
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(90vh-400px)]">
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#3F415A]">
|
||||
{columns.map((column) => (
|
||||
|
|
@ -839,7 +708,14 @@ export function ProcessInnovationPage() {
|
|||
>
|
||||
{column.key === "select" ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<span></span>
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedProjects.size === projects.length &&
|
||||
projects.length > 0
|
||||
}
|
||||
onCheckedChange={handleSelectAll}
|
||||
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
||||
/>
|
||||
</div>
|
||||
) : column.sortable ? (
|
||||
<button
|
||||
|
|
@ -875,7 +751,7 @@ export function ProcessInnovationPage() {
|
|||
{columns.map((column) => (
|
||||
<TableCell
|
||||
key={column.key}
|
||||
className="text-right whitespace-nowrap border-pr-green py-1 px-2"
|
||||
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" />
|
||||
|
|
@ -908,7 +784,7 @@ export function ProcessInnovationPage() {
|
|||
{columns.map((column) => (
|
||||
<TableCell
|
||||
key={column.key}
|
||||
className={`text-right whitespace-nowrap border-pr-green py-1 px-2 ${column.key === "select" ? "flex justify-center items-center" : ""}`}
|
||||
className={`text-right whitespace-nowrap border-emerald-500/20 py-1 px-2 ${column.key === "select" ? "flex justify-center items-center" : ""}`}
|
||||
>
|
||||
{renderCellContent(project, column)}
|
||||
</TableCell>
|
||||
|
|
@ -925,7 +801,7 @@ export function ProcessInnovationPage() {
|
|||
{loadingMore && (
|
||||
<div className="flex items-center justify-center py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshCw className="w-4 h-4 animate-spin text-pr-green" />
|
||||
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" />
|
||||
<span className="font-persian text-gray-300 text-xs"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -970,12 +846,10 @@ export function ProcessInnovationPage() {
|
|||
شرح پروژه
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 flex justify-between text-right p-6">
|
||||
<div className="space-y-4 flex justify-between text-right px-6">
|
||||
{/* Project Description */}
|
||||
<div className="flex-[4] border-l-2 border-gray-600">
|
||||
<h2 className="font-bold text-base">
|
||||
{selectedProjectDetails?.title}
|
||||
</h2>
|
||||
<h2 className="font-bold text-base">{selectedProjectDetails?.title}</h2>
|
||||
<p className="text-white font-normal text-base font-persian px-2 mt-2">
|
||||
{selectedProjectDetails?.project_description || "-"}
|
||||
</p>
|
||||
|
|
@ -987,7 +861,7 @@ export function ProcessInnovationPage() {
|
|||
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
||||
<Building2 className="h-4 text-pr-green text-sm font-light" />
|
||||
<Building2 className="h-4 text-green-500 text-sm font-light" />
|
||||
زمان شروع:
|
||||
</h4>
|
||||
<span className="text-white font-normal text-base font-persian">
|
||||
|
|
@ -1002,7 +876,7 @@ export function ProcessInnovationPage() {
|
|||
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
||||
<PickaxeIcon className="h-4 text-pr-green text-sm font-light" />
|
||||
<PickaxeIcon className="h-4 text-green-500 text-sm font-light" />
|
||||
زمان پایان:
|
||||
</h4>
|
||||
<span className="text-white font-normal text-base font-persian">
|
||||
|
|
@ -1017,8 +891,8 @@ export function ProcessInnovationPage() {
|
|||
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
||||
<UsersIcon className="h-4 text-pr-green text-sm font-light" />
|
||||
هزینه برآورد شده(میلیون ریال):
|
||||
<UsersIcon className="h-4 text-green-500 text-sm font-light" />
|
||||
هزینه برآورد شده:
|
||||
</h4>
|
||||
<span className="text-white font-normal text-base font-persian">
|
||||
{selectedProjectDetails?.approved_budget
|
||||
|
|
@ -1035,7 +909,7 @@ export function ProcessInnovationPage() {
|
|||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
|
||||
<UserIcon className="h-4 text-pr-green text-sm font-light" />
|
||||
<UserIcon className="h-4 text-green-500 text-sm font-light" />
|
||||
نفر مرتبط:
|
||||
</h4>
|
||||
<span className="text-white font-normal text-base font-persian">
|
||||
|
|
|
|||
|
|
@ -1,37 +1,46 @@
|
|||
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
|
||||
import {
|
||||
ArrowDownCircle,
|
||||
ArrowUpCircle,
|
||||
Building2,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
CirclePause,
|
||||
DollarSign,
|
||||
Funnel,
|
||||
Loader2,
|
||||
PickaxeIcon,
|
||||
RefreshCw,
|
||||
TrendingUp,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { Bar, BarChart, LabelList } from "recharts";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { BaseCard } from "~/components/ui/base-card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import { Checkbox } from "~/components/ui/checkbox";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { MetricCard } from "~/components/ui/metric-card";
|
||||
import { BaseCard } from "~/components/ui/base-card";
|
||||
import { Checkbox } from "~/components/ui/checkbox";
|
||||
import { Bar, BarChart, LabelList } from "recharts"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "~/components/ui/popover";
|
||||
PopoverContent,
|
||||
} from "~/components/ui/popover"
|
||||
|
||||
import {
|
||||
CartesianGrid,
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { FunnelChart } from "~/components/ui/funnel-chart";
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog";
|
||||
import { FunnelChart } from "~/components/ui/funnel-chart";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -40,12 +49,12 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
import { Tooltip as TooltipSh, TooltipTrigger } from "~/components/ui/tooltip";
|
||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
||||
import apiService from "~/lib/api";
|
||||
import { EventBus, formatNumber, handleDataValue } from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import { formatNumber, handleDataValue } from "~/lib/utils";
|
||||
import { DashboardLayout } from "../layout";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import { Tooltip as TooltipSh, TooltipTrigger, TooltipContent } from "~/components/ui/tooltip";
|
||||
|
||||
|
||||
interface ProjectData {
|
||||
project_no: string;
|
||||
|
|
@ -130,16 +139,15 @@ const columns = [
|
|||
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" },
|
||||
];
|
||||
|
||||
export default function Timeline(valueTimeLine: string) {
|
||||
|
||||
export default function Timeline( valueTimeLine : string) {
|
||||
const stages = ["تجاری سازی", "توسعه", "تحلیل بازار", "ثبت ایده"];
|
||||
const currentStage = stages
|
||||
?.toReversed()
|
||||
?.findIndex((x: string) => x == valueTimeLine);
|
||||
const currentStage = stages?.toReversed()?.findIndex((x : string) => x == valueTimeLine)
|
||||
const per = () => {
|
||||
const main = stages?.findIndex((x) => x == "ثبت ایده");
|
||||
console.log("yay ", 25 * main + 12.5);
|
||||
return 25 * main + 12.5;
|
||||
};
|
||||
const main = stages?.findIndex((x) => x == "ثبت ایده")
|
||||
console.log( 'yay ' , 25 * main + 12.5);
|
||||
return 25 * main + 12.5
|
||||
}
|
||||
return (
|
||||
<div className="w-full p-4">
|
||||
{/* Year labels */}
|
||||
|
|
@ -152,17 +160,12 @@ export default function Timeline(valueTimeLine: string) {
|
|||
{/* Timeline bar */}
|
||||
<div className="relative rounded-lg flex mb-4 items-center">
|
||||
{stages.map((stage, index) => (
|
||||
<div
|
||||
key={stage}
|
||||
className="flex-1 flex flex-col items-center relative"
|
||||
>
|
||||
<div key={stage} className="flex-1 flex flex-col items-center relative">
|
||||
<TooltipSh>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`w-full py-2 text-center transition-colors duration-300 ${
|
||||
index <= currentStage
|
||||
? "bg-[#3D7968] text-white"
|
||||
: "bg-[#3AEA83] text-slate-600"
|
||||
index <= currentStage ? "bg-[#3D7968] text-white" : "bg-[#3AEA83] text-slate-600"
|
||||
}`}
|
||||
>
|
||||
<span className="mt-1 text-sm">{stage}</span>
|
||||
|
|
@ -173,32 +176,25 @@ export default function Timeline(valueTimeLine: string) {
|
|||
))}
|
||||
|
||||
{/* Vertical line showing current position */}
|
||||
{valueTimeLine?.length > 0 && (
|
||||
<>
|
||||
{" "}
|
||||
<div
|
||||
{ valueTimeLine?.length > 0 && ( <> <div
|
||||
className={`absolute top-0 h-[150%] bottom-0 w-[2px] bg-white rounded-full`}
|
||||
style={{
|
||||
left: `${(currentStage + 0.5) * (100 / stages.length)}%`,
|
||||
}}
|
||||
style={{ left: `${(currentStage + 0.5) * (100 / stages.length)}%` }}
|
||||
/>
|
||||
<div
|
||||
className="absolute top-15 h-[max-content] translate-x-[-50%] text-xs text-gray-300 border-gray-400 rounded-md border px-2 bottom-0"
|
||||
style={{
|
||||
left: `${(currentStage + 0.5) * (100 / stages.length)}%`,
|
||||
}}
|
||||
>
|
||||
وضعیت فعلی
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
style={{ left: `${(currentStage + 0.5) * (100 / stages.length)}%` }}
|
||||
>وضعیت فعلی</div>
|
||||
</> ) }
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function ProductInnovationPage() {
|
||||
// const [showPopup, setShowPopup] = useState(false);
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
const [projects, setProjects] = useState<ProductInnovationData[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
|
|
@ -241,11 +237,11 @@ export function ProductInnovationPage() {
|
|||
revenueNewProducts: {
|
||||
id: "revenueNewProducts",
|
||||
title: "سهم از درآمد برای محصولات جدید",
|
||||
value: 0,
|
||||
value: "0",
|
||||
description: "میلیون ریال",
|
||||
descriptionPercent: "درصد به کل درآمد",
|
||||
color: "text-[#3AEA83]",
|
||||
percent: 0,
|
||||
percent : "0"
|
||||
},
|
||||
newProductExports: {
|
||||
id: "newProductExports",
|
||||
|
|
@ -263,22 +259,19 @@ export function ProductInnovationPage() {
|
|||
},
|
||||
});
|
||||
|
||||
const [date, setDate] = useStoredDate();
|
||||
|
||||
const observerRef = useRef<HTMLDivElement>(null);
|
||||
const fetchingRef = useRef(false);
|
||||
|
||||
|
||||
|
||||
const handleProjectDetails = async (project: ProductInnovationData) => {
|
||||
setSelectedProjectDetails(project);
|
||||
console.log(project)
|
||||
setDetailsDialogOpen(true);
|
||||
await fetchPopupData(project, date?.start, date?.end);
|
||||
await fetchPopupData(project);
|
||||
};
|
||||
|
||||
const fetchPopupData = async (
|
||||
project: ProductInnovationData,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
) => {
|
||||
const fetchPopupData = async (project: ProductInnovationData) => {
|
||||
try {
|
||||
setPopupLoading(true);
|
||||
|
||||
|
|
@ -286,26 +279,25 @@ export function ProductInnovationPage() {
|
|||
const statsResponse = await apiService.call({
|
||||
innovation_product_popup_function1: {
|
||||
project_id: project.project_id
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
if (statsResponse.state === 0) {
|
||||
const statsData = JSON.parse(statsResponse.data);
|
||||
if (
|
||||
statsData.innovation_product_popup_function1 &&
|
||||
statsData.innovation_product_popup_function1[0]
|
||||
) {
|
||||
setPopupStats(
|
||||
JSON.parse(statsData.innovation_product_popup_function1)[0]
|
||||
);
|
||||
if (statsData.innovation_product_popup_function1 && statsData.innovation_product_popup_function1[0]) {
|
||||
setPopupStats(JSON.parse(statsData.innovation_product_popup_function1)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch export chart data
|
||||
const chartResponse = await apiService.select({
|
||||
ProcessName: "export_product_innovation",
|
||||
OutputFields: ["product_title", "full_season", "sum(export_revenue)"],
|
||||
GroupBy: ["product_title", "full_season"],
|
||||
OutputFields: [
|
||||
"product_title",
|
||||
"full_season",
|
||||
"sum(export_revenue)"
|
||||
],
|
||||
GroupBy: ["product_title", "full_season"]
|
||||
});
|
||||
if (chartResponse.state === 0) {
|
||||
const chartData = JSON.parse(chartResponse.data);
|
||||
|
|
@ -313,13 +305,14 @@ export function ProductInnovationPage() {
|
|||
// Set all data for line chart
|
||||
|
||||
// Filter data for the selected project (bar chart)
|
||||
const filteredData = chartData.filter(
|
||||
(item) => item.product_title === project?.title
|
||||
const filteredData = chartData.filter(item =>
|
||||
item.product_title === project?.title
|
||||
);
|
||||
setAllExportData(chartData);
|
||||
setExportChartData(filteredData);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching popup data:", error);
|
||||
} finally {
|
||||
|
|
@ -365,14 +358,10 @@ export function ProductInnovationPage() {
|
|||
"knowledge_based_certificate_obtained",
|
||||
"knowledge_based_certificate_number",
|
||||
"certificate_obtain_date",
|
||||
"issuing_authority",
|
||||
"issuing_authority"
|
||||
],
|
||||
Sorts: [["start_date", "asc"]],
|
||||
Conditions: [
|
||||
["type_of_innovation", "=", "نوآوری در محصول", "and"],
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
Conditions: [["type_of_innovation", "=", "نوآوری در محصول"]],
|
||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||
});
|
||||
|
||||
|
|
@ -438,12 +427,8 @@ export function ProductInnovationPage() {
|
|||
const fetchStats = async () => {
|
||||
try {
|
||||
setStatsLoading(true);
|
||||
|
||||
const raw = await apiService.call<any>({
|
||||
innovation_product_function: {
|
||||
start_date: date?.start || null,
|
||||
end_date: date?.end || null,
|
||||
},
|
||||
innovation_product_function: {},
|
||||
});
|
||||
|
||||
let payload: any = JSON.parse(raw?.data);
|
||||
|
|
@ -459,25 +444,21 @@ export function ProductInnovationPage() {
|
|||
return 0;
|
||||
};
|
||||
|
||||
const data: Array<any> = JSON.parse(payload?.innovation_product_function);
|
||||
const data: Array<any> = JSON.parse(
|
||||
payload?.innovation_product_function
|
||||
);
|
||||
const stats = data[0];
|
||||
const normalized: ProductInnovationStats = {
|
||||
new_products_revenue_share: parseNum(stats?.new_products_revenue_share),
|
||||
new_products_revenue_share_percent: parseNum(
|
||||
stats?.new_products_revenue_share_percent
|
||||
),
|
||||
new_products_revenue_share_percent: parseNum(stats?.new_products_revenue_share_percent),
|
||||
import_impact: parseNum(stats?.import_impact),
|
||||
new_products_export: parseNum(stats?.new_products_export),
|
||||
all_funnel: parseNum(stats?.all_funnel),
|
||||
successful_sample_funnel: parseNum(stats?.successful_sample_funnel),
|
||||
successful_products_funnel: parseNum(stats?.successful_products_funnel),
|
||||
successful_improvement_or_change_funnel: parseNum(
|
||||
stats?.successful_improvement_or_change_funnel
|
||||
),
|
||||
successful_improvement_or_change_funnel: parseNum(stats?.successful_improvement_or_change_funnel),
|
||||
new_product_funnel: parseNum(stats?.new_product_funnel),
|
||||
count_innovation_construction_inside_projects: parseNum(
|
||||
stats?.count_innovation_construction_inside_projects
|
||||
),
|
||||
count_innovation_construction_inside_projects: parseNum(stats?.count_innovation_construction_inside_projects),
|
||||
average_project_score: parseNum(stats?.average_project_score),
|
||||
};
|
||||
|
||||
|
|
@ -485,8 +466,8 @@ export function ProductInnovationPage() {
|
|||
...prev,
|
||||
revenueNewProducts: {
|
||||
...prev.revenueNewProducts,
|
||||
value: normalized.new_products_revenue_share,
|
||||
percent: normalized.new_products_revenue_share_percent,
|
||||
value: formatNumber(normalized?.new_products_revenue_share),
|
||||
percent: formatNumber(normalized?.new_products_revenue_share_percent),
|
||||
},
|
||||
impactOnImports: {
|
||||
...prev.impactOnImports,
|
||||
|
|
@ -507,21 +488,13 @@ export function ProductInnovationPage() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||
if (date) {
|
||||
setDate(date);
|
||||
}
|
||||
});
|
||||
fetchProjects(true);
|
||||
}, [sortConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (date.end && date.start) fetchProjects(true);
|
||||
}, [sortConfig, date]);
|
||||
|
||||
useEffect(() => {
|
||||
if (date.end && date.start) fetchStats();
|
||||
}, [date]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > 1) {
|
||||
fetchProjects(false);
|
||||
|
|
@ -565,42 +538,39 @@ export function ProductInnovationPage() {
|
|||
setHasMore(true);
|
||||
};
|
||||
|
||||
// const formatCurrency = (amount: string | number) => {
|
||||
// if (!amount) return "0 ریال";
|
||||
// const numericAmount =
|
||||
// typeof amount === "string"
|
||||
// ? parseFloat(amount.replace(/,/g, ""))
|
||||
// : amount;
|
||||
// if (isNaN(numericAmount)) return "0 ریال";
|
||||
// return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
|
||||
// };
|
||||
|
||||
const formatCurrency = (amount: string | number) => {
|
||||
if (!amount) return "0 ریال";
|
||||
const numericAmount =
|
||||
typeof amount === "string"
|
||||
? parseFloat(amount.replace(/,/g, ""))
|
||||
: amount;
|
||||
if (isNaN(numericAmount)) return "0 ریال";
|
||||
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
|
||||
};
|
||||
|
||||
// Transform data for line chart
|
||||
const transformDataForLineChart = (data: any[]) => {
|
||||
const seasons = [...new Set(data.map((item) => item.full_season))];
|
||||
const products = [...new Set(data.map((item) => item.product_title))];
|
||||
return seasons.map((season) => {
|
||||
const seasons = [...new Set(data.map(item => item.full_season))];
|
||||
const products = [...new Set(data.map(item => item.product_title))];
|
||||
return seasons.map(season => {
|
||||
const seasonData: any = { season };
|
||||
products.forEach((product) => {
|
||||
const productData = data.find(
|
||||
(item) =>
|
||||
products.forEach(product => {
|
||||
const productData = data.find(item =>
|
||||
item.product_title === product && item.full_season === season
|
||||
);
|
||||
seasonData[product] =
|
||||
productData?.export_revenue_sum > 0 && productData
|
||||
? Math.round(productData?.export_revenue_sum)
|
||||
: 0;
|
||||
seasonData[product] = productData?.export_revenue_sum > 0 && productData ? Math.round(productData?.export_revenue_sum) : 0;
|
||||
});
|
||||
return seasonData;
|
||||
});
|
||||
};
|
||||
|
||||
// const getRatingColor = (rating: string | number) => {
|
||||
// const numRating = typeof rating === "string" ? parseInt(rating) : rating;
|
||||
// if (numRating >= 150) return "text-emerald-400";
|
||||
// if (numRating >= 100) return "text-blue-400";
|
||||
// return "text-red-400";
|
||||
// };
|
||||
const getRatingColor = (rating: string | number) => {
|
||||
const numRating = typeof rating === "string" ? parseInt(rating) : rating;
|
||||
if (numRating >= 150) return "text-emerald-400";
|
||||
if (numRating >= 100) return "text-blue-400";
|
||||
return "text-red-400";
|
||||
};
|
||||
|
||||
const statusColor = (status: projectStatus): any => {
|
||||
let el = null;
|
||||
|
|
@ -645,26 +615,23 @@ export function ProductInnovationPage() {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
handleProjectDetails(item);
|
||||
}}
|
||||
className="text-emerald-400 underline underline-offset-4 font-ligth text-sm p-2 h-auto"
|
||||
handleProjectDetails(item)}}
|
||||
className="text-emerald-400 underline underline-offset-4 font-ligth text-base hover:bg-emerald-500/20 p-2 h-auto"
|
||||
>
|
||||
جزئیات بیشتر
|
||||
</Button>
|
||||
);
|
||||
case "project_no":
|
||||
return (
|
||||
<Badge variant="outline" className="font-mono text-sm font-light">
|
||||
<Badge variant="outline" className="font-mono text-base font-light">
|
||||
{String(value)}
|
||||
</Badge>
|
||||
);
|
||||
case "title":
|
||||
return (
|
||||
<span className="font-light text-sm text-white">{String(value)}</span>
|
||||
);
|
||||
return <span className="font-light text-base text-white">{String(value)}</span>;
|
||||
case "project_status":
|
||||
return (
|
||||
<div className="flex items-center text-sm font-light gap-1">
|
||||
<div className="flex items-center text-base font-light gap-1">
|
||||
<Badge
|
||||
variant={statusColor(value as projectStatus)}
|
||||
className="font-semibold text-base border-2 p-0 block w-2 h-2 rounded-full"
|
||||
|
|
@ -685,11 +652,7 @@ export function ProductInnovationPage() {
|
|||
</Badge>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<span className="text-white text-sm font-light">
|
||||
{String(value) || "-"}
|
||||
</span>
|
||||
);
|
||||
return <span className="text-white text-base font-light">{String(value) || "-"}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -704,15 +667,14 @@ export function ProductInnovationPage() {
|
|||
})
|
||||
.map((item) => ({
|
||||
label: item.full_season,
|
||||
value:
|
||||
item.export_revenue_sum < 0 ? 0 : Math.round(item.export_revenue_sum),
|
||||
value: item.export_revenue_sum < 0 ? 0 : Math.round(item.export_revenue_sum) ,
|
||||
}));
|
||||
|
||||
return (
|
||||
<DashboardLayout title="نوآوری در محصول">
|
||||
<div className=" flex w-full gap-4">
|
||||
<div className="p-6 space-y-4 flex justify-center gap-4">
|
||||
{/* Stats Cards */}
|
||||
<div className="flex flex-col flex-1 gap-6">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="space-y-6 w-full">
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 grid-rows-2 gap-5 h-full">
|
||||
|
|
@ -754,30 +716,21 @@ export function ProductInnovationPage() {
|
|||
<div className="col-span-2">
|
||||
<MetricCard
|
||||
title={stateCard.revenueNewProducts.title}
|
||||
value={stateCard.revenueNewProducts.value}
|
||||
percentValue={stateCard.revenueNewProducts.percent}
|
||||
value={formatNumber(stateCard.revenueNewProducts.value)}
|
||||
percentValue={formatNumber(stateCard.revenueNewProducts.percent)}
|
||||
valueLabel={stateCard.revenueNewProducts.description}
|
||||
percentLabel={
|
||||
stateCard.revenueNewProducts.descriptionPercent
|
||||
}
|
||||
percentLabel={stateCard.revenueNewProducts.descriptionPercent}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Second card */}
|
||||
<div>
|
||||
<BaseCard
|
||||
title={stateCard.newProductExports.title}
|
||||
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"
|
||||
>
|
||||
<BaseCard title={stateCard.newProductExports.title} className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold mb-1 text-pr-green">
|
||||
{stateCard.newProductExports.value}
|
||||
</p>
|
||||
<div className="text-xs text-gray-400 font-persian">
|
||||
{stateCard.newProductExports.description}
|
||||
</div>
|
||||
<p className="text-3xl font-bold mb-1 text-pr-green">{stateCard.newProductExports.value}</p>
|
||||
<div className="text-xs text-gray-400 font-persian">{stateCard.newProductExports.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -786,19 +739,12 @@ export function ProductInnovationPage() {
|
|||
|
||||
{/* Third card - basic BaseCard */}
|
||||
<div>
|
||||
<BaseCard
|
||||
title={stateCard.impactOnImports.title}
|
||||
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"
|
||||
>
|
||||
<BaseCard title={stateCard.impactOnImports.title} className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold mb-1 text-pr-red">
|
||||
{stateCard.impactOnImports.value}
|
||||
</p>
|
||||
<div className="text-xs text-gray-400 font-persian">
|
||||
{stateCard.impactOnImports.description}
|
||||
</div>
|
||||
<p className="text-3xl font-bold mb-1 text-pr-red">{stateCard.impactOnImports.value}</p>
|
||||
<div className="text-xs text-gray-400 font-persian">{stateCard.impactOnImports.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -810,7 +756,7 @@ export function ProductInnovationPage() {
|
|||
</div>
|
||||
|
||||
{/* Funnel Chart */}
|
||||
<Card className=" bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-full 2xl:h-auto pb-8 backdrop-blur-sm rounded-2xl w-full overflow-hidden">
|
||||
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-full backdrop-blur-sm rounded-2xl w-full overflow-hidden">
|
||||
<CardContent className="px-0 py-4">
|
||||
<FunnelChart
|
||||
title="قيف فرآیند پروژه ها"
|
||||
|
|
@ -847,7 +793,7 @@ export function ProductInnovationPage() {
|
|||
</div>
|
||||
|
||||
{/* Data Table */}
|
||||
<Card className="bg-transparent flex-2 rounded-2xl overflow-hidden">
|
||||
<Card className="bg-transparent rounded-2xl overflow-hidden">
|
||||
<CardContent className="p-0">
|
||||
<div className="relative">
|
||||
<Table containerClassName="overflow-auto custom-scrollbar backdrop max-h-[calc(100vh-200px)]">
|
||||
|
|
@ -956,10 +902,7 @@ export function ProductInnovationPage() {
|
|||
<div className="flex gap-4 text-sm text-gray-300 font-persian justify-between sm:flex-col xl:flex-row">
|
||||
<div className="text-center gap-2 items-center xl:w-1/3 pr-36 sm:w-full">
|
||||
<div className="text-sm font-semibold text-white">
|
||||
کل پروژه ها :
|
||||
{formatNumber(
|
||||
stats?.count_innovation_construction_inside_projects
|
||||
)}
|
||||
کل پروژه ها :{formatNumber(stats?.count_innovation_construction_inside_projects)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -974,9 +917,7 @@ export function ProductInnovationPage() {
|
|||
<div className="text-bold text-sm text-white">میانگین :</div>
|
||||
<div className="font-bold text-sm text-white">
|
||||
{formatNumber(
|
||||
((stats.average_project_score ?? 0) as number).toFixed?.(
|
||||
1
|
||||
) ?? 0
|
||||
((stats.average_project_score ?? 0) as number).toFixed?.(1) ?? 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1000,47 +941,29 @@ export function ProductInnovationPage() {
|
|||
<div className="space-y-4">
|
||||
{/* Stats Cards */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-bold text-base">
|
||||
{selectedProjectDetails?.title}
|
||||
</h3>
|
||||
<p className="py-2">
|
||||
{selectedProjectDetails?.project_description}
|
||||
</p>
|
||||
<h3 className="font-bold text-base">{selectedProjectDetails?.title}</h3>
|
||||
<p className="py-2">{selectedProjectDetails?.project_description}</p>
|
||||
</div>
|
||||
<Timeline
|
||||
valueTimeLine={selectedProjectDetails?.current_status}
|
||||
/>
|
||||
<Timeline valueTimeLine={selectedProjectDetails?.current_status} />
|
||||
|
||||
{/* Technical Knowledge */}
|
||||
<div className=" rounded-lg py-2 mb-0">
|
||||
<h3 className="text-sm text-white font-semibold mb-2">
|
||||
دانش فنی محصول جدید
|
||||
</h3>
|
||||
<h3 className="text-sm text-white font-semibold mb-2">دانش فنی محصول جدید</h3>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-white font-light">
|
||||
توسعه درونزا
|
||||
</span>
|
||||
<span className="text-sm text-white font-light">توسعه درونزا</span>
|
||||
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedProjectDetails?.developed_technology_type ===
|
||||
"توسعه درونزا"
|
||||
}
|
||||
checked={selectedProjectDetails?.developed_technology_type === "توسعه درونزا"}
|
||||
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-white font-light">
|
||||
همکاری فناورانه
|
||||
</span>
|
||||
<span className="text-sm text-white font-light">همکاری فناورانه</span>
|
||||
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedProjectDetails?.developed_technology_type ===
|
||||
"همکاری فناوری"
|
||||
}
|
||||
checked={selectedProjectDetails?.developed_technology_type === "همکاری فناوری"}
|
||||
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1048,14 +971,11 @@ export function ProductInnovationPage() {
|
|||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-white font-light">سایر</span>
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedProjectDetails?.developed_technology_type ===
|
||||
"سایر"
|
||||
}
|
||||
checked={selectedProjectDetails?.developed_technology_type === "سایر"}
|
||||
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Standards */}
|
||||
|
|
@ -1064,20 +984,15 @@ export function ProductInnovationPage() {
|
|||
استانداردهای ملی و بینالمللی اخذ شده
|
||||
</h3>
|
||||
|
||||
{selectedProjectDetails?.obtained_standard_title &&
|
||||
selectedProjectDetails?.obtained_standard_title.length > 0 ? (
|
||||
{selectedProjectDetails?.obtained_standard_title && selectedProjectDetails?.obtained_standard_title.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{(Array.isArray(
|
||||
selectedProjectDetails?.obtained_standard_title
|
||||
)
|
||||
{(Array.isArray(selectedProjectDetails?.obtained_standard_title)
|
||||
? selectedProjectDetails?.obtained_standard_title
|
||||
: [selectedProjectDetails?.obtained_standard_title]
|
||||
).map((standard, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-emerald-500 rounded-full"></div>
|
||||
<span className="text-sm text-white font-light">
|
||||
{standard}
|
||||
</span>
|
||||
<span className="text-sm text-white font-light">{standard}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -1086,12 +1001,11 @@ export function ProductInnovationPage() {
|
|||
هیچ استانداردی ثبت نشده است.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Knowledge-based Certificate Button */}
|
||||
<div className="justify-self-centerr grid py-1 mx-auto">
|
||||
{selectedProjectDetails?.knowledge_based_certificate_obtained ===
|
||||
"خیر" ? (
|
||||
{selectedProjectDetails?.knowledge_based_certificate_obtained === "خیر" ? (
|
||||
<div className=" border border-pr-red mx-auto rounded-lg p-2 text-center">
|
||||
<button className="text-pr-red font-bold text-sm">
|
||||
گواهی دانشبنیان ندارد
|
||||
|
|
@ -1122,14 +1036,10 @@ export function ProductInnovationPage() {
|
|||
</p>
|
||||
<p className="text-sm text-white">
|
||||
<span className="font-bold">تاریخ اخذ: </span>
|
||||
{handleDataValue(
|
||||
selectedProjectDetails?.certificate_obtain_date
|
||||
) || "—"}
|
||||
{handleDataValue(selectedProjectDetails?.certificate_obtain_date) || "—"}
|
||||
</p>
|
||||
<p className="text-sm text-white">
|
||||
<span className="font-bold">
|
||||
مرجع صادرکننده:{" "}
|
||||
</span>
|
||||
<span className="font-bold">مرجع صادرکننده: </span>
|
||||
{selectedProjectDetails?.issuing_authority || "—"}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -1171,32 +1081,16 @@ export function ProductInnovationPage() {
|
|||
<div className="rounded-lg pt-4 grid grid-cols-2 gap-4 w-full">
|
||||
<MetricCard
|
||||
title="میزان صادارت محصول جدید"
|
||||
value={Math.round(
|
||||
popupStats?.new_products_export > 0
|
||||
? popupStats?.new_products_export
|
||||
: 0
|
||||
)}
|
||||
percentValue={Math.round(
|
||||
popupStats?.new_products_export_percent > 0
|
||||
? popupStats?.new_products_export_percent
|
||||
: 0
|
||||
)}
|
||||
value={Math.round(popupStats?.new_products_export > 0 ? popupStats?.new_products_export : 0)}
|
||||
percentValue={Math.round(popupStats?.new_products_export_percent > 0 ? popupStats?.new_products_export_percent : 0)}
|
||||
valueLabel="میلیون ریال"
|
||||
percentLabel="درصد به کل صادرات"
|
||||
/>
|
||||
|
||||
<MetricCard
|
||||
title="تاثیر در واردات"
|
||||
value={Math.round(
|
||||
popupStats?.import_impact > 0
|
||||
? popupStats?.import_impact
|
||||
: 0
|
||||
)}
|
||||
percentValue={Math.round(
|
||||
popupStats?.import_impact_percent > 0
|
||||
? popupStats?.import_impact_percent
|
||||
: 0
|
||||
)}
|
||||
value={Math.round(popupStats?.import_impact > 0 ? popupStats?.import_impact : 0)}
|
||||
percentValue={Math.round(popupStats?.import_impact_percent > 0 ? popupStats?.import_impact_percent : 0)}
|
||||
valueLabel="میلیون ریال"
|
||||
percentLabel="درصد صرفه جویی"
|
||||
/>
|
||||
|
|
@ -1204,9 +1098,7 @@ export function ProductInnovationPage() {
|
|||
|
||||
{/* Export Revenue Bar Chart */}
|
||||
<div className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] rounded-lg px-6 py-4">
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
ظرفیت صادر شده
|
||||
</h3>
|
||||
<h3 className="text-sm font-semibold text-white">ظرفیت صادر شده</h3>
|
||||
<div className="h-60">
|
||||
{exportChartData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
|
@ -1224,149 +1116,52 @@ export function ProductInnovationPage() {
|
|||
axisLine={false}
|
||||
stroke="#C3C3C3"
|
||||
tickMargin={8}
|
||||
tickFormatter={(value: string) =>
|
||||
`${value.split(" ")[0]} ${formatNumber(value.split(" ")[1]).replaceAll("٬", "")}`
|
||||
}
|
||||
tickFormatter={(value: string) => `${value.split(" ")[0]} ${formatNumber(value.split(" ")[1]).replaceAll('٬','')}`}
|
||||
fontSize={11}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
stroke="#9CA3AF"
|
||||
fontSize={11}
|
||||
tick={{ dx: -50 }}
|
||||
tickFormatter={(value: number) =>
|
||||
`${formatNumber(value)} میلیون`
|
||||
}
|
||||
/>
|
||||
<YAxis tickLine={false} axisLine={false} stroke="#9CA3AF" fontSize={11} tick={{ dx: -50 }} tickFormatter={(value: number) => `${formatNumber(value)} میلیون`} />
|
||||
<Bar dataKey="value" fill="#10B981" radius={10}>
|
||||
<LabelList
|
||||
formatter={(value: number) =>
|
||||
`${formatNumber(value)}`
|
||||
}
|
||||
position="top"
|
||||
offset={15}
|
||||
fill="F9FAFB"
|
||||
className="fill-foreground"
|
||||
fontSize={16}
|
||||
/>
|
||||
<LabelList formatter={(value: number) => `${formatNumber(value)}`} position="top" offset={15} fill="F9FAFB" className="fill-foreground" fontSize={16} />
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-400">
|
||||
دادهای برای نمایش وجود ندارد
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-full text-gray-400">دادهای برای نمایش وجود ندارد</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export Revenue Line Chart */}
|
||||
<div className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] rounded-lg px-6 py-4">
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
ظرفیت صادر شده
|
||||
</h3>
|
||||
<h3 className="text-sm font-semibold text-white">ظرفیت صادر شده</h3>
|
||||
<div className="h-60">
|
||||
{allExportData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart
|
||||
className="aspect-auto w-full"
|
||||
data={transformDataForLineChart(allExportData)}
|
||||
margin={{ top: 20, right: 30, left: 10, bottom: 50 }}
|
||||
>
|
||||
<LineChart className="aspect-auto w-full" data={transformDataForLineChart(allExportData)} margin={{ top: 20, right: 30, left: 10, bottom: 50 }}>
|
||||
<CartesianGrid vertical={false} stroke="#374151" />
|
||||
<XAxis
|
||||
dataKey="season"
|
||||
stroke="#9CA3AF"
|
||||
fontSize={11}
|
||||
tick={({ x, y, payload }) => (
|
||||
<XAxis dataKey="season" stroke="#9CA3AF" fontSize={11} tick={({ x, y, payload }) => (
|
||||
<g transform={`translate(${x},${y + 10})`}>
|
||||
<text
|
||||
x={-40}
|
||||
y={15}
|
||||
dy={0}
|
||||
textAnchor="end"
|
||||
fill="#9CA3AF"
|
||||
fontSize={11}
|
||||
transform="rotate(-45)"
|
||||
>
|
||||
{(payload as any).value}
|
||||
</text>
|
||||
<text x={-40} y={15} dy={0} textAnchor="end" fill="#9CA3AF" fontSize={11} transform="rotate(-45)">{(payload as any).value}</text>
|
||||
</g>
|
||||
)}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
stroke="#9CA3AF"
|
||||
fontSize={11}
|
||||
tick={{ dx: -50 }}
|
||||
tickFormatter={(value) =>
|
||||
`${formatNumber(value)} میلیون`
|
||||
}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number) =>
|
||||
`${formatNumber(value)} میلیون`
|
||||
}
|
||||
contentStyle={{
|
||||
backgroundColor: "#1F2937",
|
||||
border: "1px solid #374151",
|
||||
borderRadius: "6px",
|
||||
padding: "6px 10px",
|
||||
fontSize: "11px",
|
||||
color: "#F9FAFB",
|
||||
}}
|
||||
/>
|
||||
<Legend
|
||||
layout="vertical"
|
||||
verticalAlign="middle"
|
||||
align="right"
|
||||
iconType={"plainline"}
|
||||
className="!flex"
|
||||
wrapperStyle={{
|
||||
fontSize: 11,
|
||||
paddingLeft: 12,
|
||||
gap: 10,
|
||||
}}
|
||||
/>
|
||||
{[
|
||||
...new Set(
|
||||
allExportData.map((item) => item.product_title)
|
||||
),
|
||||
]
|
||||
.slice(0, 5)
|
||||
.map((product, index) => {
|
||||
const colors = [
|
||||
"#10B981",
|
||||
"#EF4444",
|
||||
"#3B82F6",
|
||||
"#F59E0B",
|
||||
"#8B5CF6",
|
||||
];
|
||||
return (
|
||||
<Line
|
||||
key={product}
|
||||
type="linear"
|
||||
dot={false}
|
||||
activeDot={{ r: 5 }}
|
||||
dataKey={product}
|
||||
stroke={colors[index % colors.length]}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
);
|
||||
)} />
|
||||
<YAxis tickLine={false} axisLine={false} stroke="#9CA3AF" fontSize={11} tick={{ dx: -50 }} tickFormatter={(value) => `${formatNumber(value)} میلیون`} />
|
||||
<Tooltip formatter={(value: number) => `${formatNumber(value)} میلیون`} contentStyle={{ backgroundColor: "#1F2937", border: "1px solid #374151", borderRadius: "6px", padding: "6px 10px", fontSize: "11px", color: "#F9FAFB" }} />
|
||||
<Legend layout="vertical" verticalAlign="middle" align="right" iconType={"plainline"} className="!flex" wrapperStyle={{ fontSize: 11, paddingLeft: 12, gap: 10 }} />
|
||||
{[...new Set(allExportData.map((item) => item.product_title))].slice(0, 5).map((product, index) => {
|
||||
const colors = ["#10B981", "#EF4444", "#3B82F6", "#F59E0B", "#8B5CF6"];
|
||||
return <Line key={product} type="linear" dot={false} activeDot={{ r: 5 }} dataKey={product} stroke={colors[index % colors.length]} strokeWidth={2} />;
|
||||
})}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-400">
|
||||
دادهای برای نمایش وجود ندارد
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-full text-gray-400">دادهای برای نمایش وجود ندارد</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { saveAs } from "file-saver";
|
||||
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import XLSX from "xlsx-js-style";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import {
|
||||
|
|
@ -14,15 +12,9 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
||||
import apiService from "~/lib/api";
|
||||
import {
|
||||
EventBus,
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
handleDataValue,
|
||||
} from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import { formatCurrency } from "~/lib/utils";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
import { DashboardLayout } from "../layout";
|
||||
|
||||
interface ProjectData {
|
||||
|
|
@ -177,12 +169,6 @@ export function ProjectManagementPage() {
|
|||
const fetchingRef = useRef(false);
|
||||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
// const [date, setDate] = useState<CalendarDate>({
|
||||
// start: `${jy}/01/01`,
|
||||
// end: `${jy}/12/30`,
|
||||
// });
|
||||
|
||||
const [date, setDate] = useStoredDate();
|
||||
|
||||
const fetchProjects = async (reset = false) => {
|
||||
// Prevent concurrent API calls
|
||||
|
|
@ -214,10 +200,7 @@ export function ProjectManagementPage() {
|
|||
OutputFields: outputFields,
|
||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||
Sorts: sortField ? [[sortField, sortConfig.direction]] : [],
|
||||
Conditions: [
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
Conditions: [],
|
||||
});
|
||||
|
||||
if (response.state === 0) {
|
||||
|
|
@ -282,17 +265,6 @@ export function ProjectManagementPage() {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (date: CalendarDate) => {
|
||||
if (date) setDate(date);
|
||||
};
|
||||
|
||||
EventBus.on("dateSelected", handler);
|
||||
|
||||
return () => {
|
||||
EventBus.off("dateSelected", handler);
|
||||
};
|
||||
}, []);
|
||||
const loadMore = useCallback(() => {
|
||||
if (hasMore && !loading && !loadingMore && !fetchingRef.current) {
|
||||
setCurrentPage((prev) => prev + 1);
|
||||
|
|
@ -300,11 +272,9 @@ export function ProjectManagementPage() {
|
|||
}, [hasMore, loading, loadingMore]);
|
||||
|
||||
useEffect(() => {
|
||||
if (date.end && date.start) {
|
||||
fetchProjects(true);
|
||||
fetchTotalCount();
|
||||
}
|
||||
}, [sortConfig, date]);
|
||||
}, [sortConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > 1) {
|
||||
|
|
@ -317,8 +287,7 @@ export function ProjectManagementPage() {
|
|||
const scrollContainer = scrollContainerRef.current;
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current)
|
||||
return;
|
||||
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return;
|
||||
|
||||
// Clear previous timeout
|
||||
if (scrollTimeoutRef.current) {
|
||||
|
|
@ -338,9 +307,7 @@ export function ProjectManagementPage() {
|
|||
};
|
||||
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener("scroll", handleScroll, {
|
||||
passive: true,
|
||||
});
|
||||
scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
|
@ -370,10 +337,7 @@ export function ProjectManagementPage() {
|
|||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
OutputFields: ["count(project_no)"],
|
||||
Conditions: [
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
Conditions: [],
|
||||
});
|
||||
|
||||
if (response.state === 0) {
|
||||
|
|
@ -394,14 +358,14 @@ export function ProjectManagementPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// const handleRefresh = () => {
|
||||
// fetchingRef.current = false; // Reset fetching state on refresh
|
||||
// setCurrentPage(1);
|
||||
// setProjects([]);
|
||||
// setHasMore(true);
|
||||
// fetchProjects(true);
|
||||
// fetchTotalCount();
|
||||
// };
|
||||
const handleRefresh = () => {
|
||||
fetchingRef.current = false; // Reset fetching state on refresh
|
||||
setCurrentPage(1);
|
||||
setProjects([]);
|
||||
setHasMore(true);
|
||||
fetchProjects(true);
|
||||
fetchTotalCount();
|
||||
};
|
||||
|
||||
// ...existing code...
|
||||
|
||||
|
|
@ -666,7 +630,7 @@ export function ProjectManagementPage() {
|
|||
.filter((v) => v !== null) as number[];
|
||||
res["remaining_time"] = remainingValues.length
|
||||
? Math.round(
|
||||
remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length
|
||||
remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length,
|
||||
)
|
||||
: null;
|
||||
|
||||
|
|
@ -680,7 +644,7 @@ export function ProjectManagementPage() {
|
|||
const num = Number(
|
||||
String(raw)
|
||||
.toString()
|
||||
.replace(/[^0-9.-]/g, "")
|
||||
.replace(/[^0-9.-]/g, ""),
|
||||
);
|
||||
return Number.isFinite(num) ? num : NaN;
|
||||
})
|
||||
|
|
@ -797,94 +761,16 @@ export function ProjectManagementPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
return (
|
||||
<DashboardLayout title="مدیریت پروژهها">
|
||||
<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> */}
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Data Table */}
|
||||
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
|
||||
{/* <div onClick={exportToExcel}>DownloadExcle</div> */}
|
||||
<CardContent className="p-0">
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="relative overflow-auto custom-scrollbar max-h-[calc(100vh-120px)]"
|
||||
>
|
||||
<div ref={scrollContainerRef} className="relative overflow-auto custom-scrollbar max-h-[calc(100vh-120px)]">
|
||||
<Table className="table-fixed">
|
||||
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]">
|
||||
<TableRow className="bg-[#3F415A]">
|
||||
|
|
|
|||
|
|
@ -1,16 +1,25 @@
|
|||
import {
|
||||
Box,
|
||||
Building2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
FolderKanban,
|
||||
GalleryVerticalEnd,
|
||||
House,
|
||||
LightbulbIcon,
|
||||
ListTodo,
|
||||
Globe,
|
||||
LayoutDashboard,
|
||||
Leaf,
|
||||
Lightbulb,
|
||||
LogOut,
|
||||
Radar,
|
||||
MonitorSmartphone,
|
||||
Package,
|
||||
Settings,
|
||||
Star,
|
||||
Workflow,
|
||||
DiscAlbum,
|
||||
LucideLightbulb
|
||||
House,
|
||||
ListTodo,
|
||||
LightbulbIcon,
|
||||
Radar
|
||||
} from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { Link, useLocation } from "react-router";
|
||||
|
|
@ -40,6 +49,7 @@ interface MenuItem {
|
|||
}
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
|
||||
{
|
||||
id: "dashboard",
|
||||
label: "صفحه اصلی",
|
||||
|
|
@ -98,24 +108,31 @@ const menuItems: MenuItem[] = [
|
|||
{
|
||||
id: "ideas",
|
||||
label: "ایدههای فناوری و نوآوری",
|
||||
icon: LucideLightbulb,
|
||||
icon: House,
|
||||
href: "/dashboard/manage-ideas-tech",
|
||||
},
|
||||
{
|
||||
id: "top-innovations",
|
||||
label: "نوآور برتر",
|
||||
icon: Star,
|
||||
href: "/dashboard/top-innovations",
|
||||
},
|
||||
{
|
||||
id: "strategic-alignment",
|
||||
label: "میزان انطباق راهبردی",
|
||||
icon: null,
|
||||
href: "#", // This is not a route, it opens a popup
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
const bottomMenuItems: MenuItem[] = [
|
||||
// {
|
||||
// id: "settings",
|
||||
// label: "تنظیمات",
|
||||
// icon: Settings,
|
||||
// href: "/dashboard/settings",
|
||||
// },
|
||||
{
|
||||
id: "settings",
|
||||
label: "تنظیمات",
|
||||
icon: Settings,
|
||||
href: "/dashboard/settings",
|
||||
},
|
||||
{
|
||||
id: "logout",
|
||||
label: "خروج",
|
||||
|
|
@ -155,10 +172,7 @@ export function Sidebar({
|
|||
// Update header title based on current route
|
||||
// If a child route is active, use that child's label prefixed by parent label
|
||||
let activeTitle: string | undefined = undefined;
|
||||
let activeIcon:
|
||||
| React.ComponentType<{ className?: string }>
|
||||
| null
|
||||
| undefined = undefined;
|
||||
let activeIcon: React.ComponentType<{ className?: string }> | null | undefined = undefined;
|
||||
menuItems.forEach((item) => {
|
||||
if (item.children) {
|
||||
const activeChild = item.children.find(
|
||||
|
|
@ -176,10 +190,7 @@ export function Sidebar({
|
|||
}
|
||||
});
|
||||
if (onTitleChange) {
|
||||
onTitleChange({
|
||||
title: activeTitle ?? "صفحه اول",
|
||||
icon: activeIcon ?? null,
|
||||
});
|
||||
onTitleChange({ title: activeTitle ?? "صفحه اول", icon: activeIcon ?? null });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -250,7 +261,7 @@ export function Sidebar({
|
|||
<button
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"flex items-center justify-center w-full px-2 rounded-none mt-4 transition-all duration-200 group"
|
||||
"flex items-center justify-center w-full px-2 rounded-none mt-4 transition-all duration-200 group",
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
|
|
@ -260,7 +271,8 @@ export function Sidebar({
|
|||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -415,42 +427,18 @@ export function Sidebar({
|
|||
<div className="flex items-center justify-start">
|
||||
{!isCollapsed ? (
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
{/* آپادانا بندرامام */}
|
||||
{"نوری" == "نوری" ? (
|
||||
<img
|
||||
src="/brand.svg"
|
||||
alt="logo"
|
||||
className="bg-green-400 p-1.5 rounded-lg w-auto max-h-[30px] object-contain"
|
||||
/>
|
||||
|
||||
|
||||
) : (<GalleryVerticalEnd
|
||||
<GalleryVerticalEnd
|
||||
color="black"
|
||||
size={32}
|
||||
strokeWidth={1}
|
||||
className="bg-green-400 p-1.5 rounded-lg"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="font-persian">
|
||||
|
||||
{/* آپادانا بندرامام */}
|
||||
{"نوری" == "نوری" ? (
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">
|
||||
پتروشیمی نوری
|
||||
اینوژن بندر امام
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">نسخه ۰.۱</div>
|
||||
</div>
|
||||
|
||||
):(
|
||||
|
||||
<div className="text-sm font-semibold text-white">
|
||||
داشبورد مدیریت فناوری و نوآوری
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center w-full">
|
||||
|
|
|
|||
|
|
@ -1,80 +1,33 @@
|
|||
import { useEffect, useReducer, useRef, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
LabelList,
|
||||
ResponsiveContainer,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
LabelList,
|
||||
Cell,
|
||||
} from "recharts";
|
||||
import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
||||
import apiService from "~/lib/api";
|
||||
import { EventBus, formatNumber } from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
import { ChartContainer } from "../ui/chart";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuButton,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { TruncatedText } from "../ui/truncatedText";
|
||||
|
||||
interface StrategicAlignmentData {
|
||||
strategic_theme: string;
|
||||
operational_fee_count: number;
|
||||
operational_fee_sum: number;
|
||||
percentage?: number;
|
||||
}
|
||||
|
||||
interface DropDownConfig {
|
||||
isOpen: boolean;
|
||||
selectedValue: string;
|
||||
dropDownItems: Array<string>;
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: "OPEN" }
|
||||
| { type: "CLOSE" }
|
||||
| { type: "SETVALUE"; value: Array<string> }
|
||||
| { type: "SELECT"; value: string };
|
||||
|
||||
// const DropDownItems = [
|
||||
// {
|
||||
// id: 0,
|
||||
// key: "همه مضامین",
|
||||
// Value: "همه مضامین",
|
||||
// },
|
||||
// {
|
||||
// id: 1,
|
||||
// key: "ارزش های هم افزایی نوآورانه",
|
||||
// Value: "همه مضامین",
|
||||
// },
|
||||
// {
|
||||
// id: 2,
|
||||
// key: "ارزش های خودکفایی نوآوورانه",
|
||||
// Value: "همه مضامین",
|
||||
// },
|
||||
// {
|
||||
// id: 3,
|
||||
// key: "ارزش های فناوری های نوین",
|
||||
// Value: "همه مضامین",
|
||||
// },
|
||||
// {
|
||||
// id: 4,
|
||||
// key: "ارزش های توسعه منابع انسانی",
|
||||
// Value: "همه مضامین",
|
||||
// },
|
||||
// {
|
||||
// id: 5,
|
||||
// key: "ارزش های نوآوری سبز",
|
||||
// Value: "همه مضامین",
|
||||
// },
|
||||
// ];
|
||||
|
||||
interface StrategicAlignmentPopupProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
|
|
@ -88,10 +41,11 @@ const chartConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
const maxHeight = 150;
|
||||
const barHeights = () => Math.floor(Math.random() * maxHeight);
|
||||
const maxHeight = 150;
|
||||
const barHeights = () => Math.floor(Math.random() * maxHeight);
|
||||
|
||||
const ChartSkeleton = () => (
|
||||
|
||||
<div className="flex justify-center h-96 w-full p-4">
|
||||
{/* Chart bars */}
|
||||
<div className=" w-full flex items-end gap-10">
|
||||
|
|
@ -120,14 +74,6 @@ export function StrategicAlignmentPopup({
|
|||
}: StrategicAlignmentPopupProps) {
|
||||
const [data, setData] = useState<StrategicAlignmentData[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
isOpen: false,
|
||||
selectedValue: "همه مضامین",
|
||||
dropDownItems: [],
|
||||
});
|
||||
|
||||
const [date, setDate] = useStoredDate();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
|
|
@ -135,132 +81,33 @@ export function StrategicAlignmentPopup({
|
|||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (date: CalendarDate) => {
|
||||
if (date) setDate(date);
|
||||
};
|
||||
|
||||
EventBus.on("dateSelected", handler);
|
||||
|
||||
return () => {
|
||||
EventBus.off("dateSelected", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
OutputFields: ["strategic_theme", "count(operational_fee)"],
|
||||
GroupBy: ["strategic_theme"],
|
||||
Conditions: [
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
});
|
||||
|
||||
const responseData =
|
||||
typeof response.data === "string"
|
||||
? JSON.parse(response.data)
|
||||
: response.data;
|
||||
|
||||
setBarItems(responseData);
|
||||
const dropDownItems = responseData.map(
|
||||
(item: any) => item.strategic_theme
|
||||
);
|
||||
|
||||
setDropDownValues(["همه مضامین", ...dropDownItems]);
|
||||
} catch (error) {
|
||||
console.error("Error fetching strategic alignment data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDropDownItems = async (item: string) => {
|
||||
try {
|
||||
if (item !== "همه مضامین") {
|
||||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
OutputFields: [
|
||||
"value_technology_and_innovation",
|
||||
"count(operational_fee)",
|
||||
"strategic_theme",
|
||||
"sum(operational_fee) as operational_fee_sum",
|
||||
],
|
||||
Conditions: [
|
||||
["strategic_theme", "=", item, "and"],
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
GroupBy: ["value_technology_and_innovation"],
|
||||
GroupBy: ["strategic_theme"],
|
||||
});
|
||||
|
||||
const responseData =
|
||||
typeof response.data === "string"
|
||||
? JSON.parse(response.data)
|
||||
: response.data;
|
||||
setBarItems(responseData);
|
||||
} else fetchData();
|
||||
} catch (error) {
|
||||
console.error("Error fetching strategic alignment data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
function reducer(state: DropDownConfig, action: Action): DropDownConfig {
|
||||
switch (action.type) {
|
||||
case "OPEN":
|
||||
return { ...state, isOpen: true };
|
||||
case "CLOSE":
|
||||
return { ...state, isOpen: false };
|
||||
case "SETVALUE":
|
||||
return { ...state, dropDownItems: action.value };
|
||||
case "SELECT":
|
||||
return { ...state, selectedValue: action.value };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMenuHandler = () => {
|
||||
dispatch({
|
||||
type: "OPEN",
|
||||
});
|
||||
};
|
||||
|
||||
const selectItem = (item: string) => {
|
||||
dispatch({
|
||||
type: "SELECT",
|
||||
value: item,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: "CLOSE",
|
||||
});
|
||||
|
||||
fetchDropDownItems(item);
|
||||
};
|
||||
|
||||
const setDropDownValues = (items: Array<string>) => {
|
||||
dispatch({
|
||||
type: "SETVALUE",
|
||||
value: items,
|
||||
});
|
||||
};
|
||||
|
||||
const setBarItems = (responseData: any) => {
|
||||
const processedData = responseData
|
||||
.map((item: any) => ({
|
||||
strategic_theme:
|
||||
item.strategic_theme || item.value_technology_and_innovation || "N/A",
|
||||
operational_fee_count: Math.max(0, Number(item.operational_fee_count)),
|
||||
strategic_theme: item.strategic_theme || "N/A",
|
||||
operational_fee_sum: Math.max(0, Number(item.operational_fee_sum)),
|
||||
}))
|
||||
.filter((item: StrategicAlignmentData) => item.strategic_theme !== "");
|
||||
|
||||
const total = processedData.reduce(
|
||||
(acc: number, item: StrategicAlignmentData) =>
|
||||
acc + item.operational_fee_count,
|
||||
acc + item.operational_fee_sum,
|
||||
0
|
||||
);
|
||||
|
||||
|
|
@ -269,72 +116,23 @@ export function StrategicAlignmentPopup({
|
|||
...item,
|
||||
percentage:
|
||||
total > 0
|
||||
? Math.round((item.operational_fee_count / total) * 100)
|
||||
? Math.round((item.operational_fee_sum / total) * 100)
|
||||
: 0,
|
||||
})
|
||||
);
|
||||
setData(dataWithPercentage || []);
|
||||
};
|
||||
|
||||
const dialogHandler = (status: boolean) => {
|
||||
if (onOpenChange) onOpenChange(status);
|
||||
dispatch({
|
||||
type: "SELECT",
|
||||
value: "همه مضامین",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
contentRef.current &&
|
||||
!contentRef.current.contains(event.target as Node)
|
||||
) {
|
||||
dispatch({
|
||||
type: "CLOSE",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching strategic alignment data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={dialogHandler}>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-full max-w-4xl bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] text-white border-none">
|
||||
<DialogHeader className="mb-10 w-full border-b-2 border-gray-500/20">
|
||||
<div>
|
||||
<div className="flex">
|
||||
<DropdownMenu
|
||||
modal={true}
|
||||
open={state.isOpen}
|
||||
onOpenChange={toggleMenuHandler}
|
||||
>
|
||||
<DropdownMenuButton>{state.selectedValue}</DropdownMenuButton>
|
||||
|
||||
<DropdownMenuContent
|
||||
ref={contentRef}
|
||||
forceMount={true}
|
||||
className="w-56"
|
||||
>
|
||||
{state.dropDownItems.map((item: string, key: number) => (
|
||||
<div
|
||||
onClick={() => selectItem(item)}
|
||||
key={`${key}-${item}`}
|
||||
>
|
||||
<DropdownMenuItem selected={state.selectedValue === item}>
|
||||
{item}
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<DialogHeader className="mb-10 py-2 w-full pb-4 border-b-2 border-gray-500/20">
|
||||
<DialogTitle className="ml-auto text-sm text-white ">میزان انطباق راهبردی</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
|
|
@ -342,10 +140,7 @@ export function StrategicAlignmentPopup({
|
|||
) : (
|
||||
<>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-96 w-full"
|
||||
>
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-96 w-full">
|
||||
<BarChart
|
||||
data={data}
|
||||
margin={{ left: 12, right: 12 }}
|
||||
|
|
@ -366,7 +161,10 @@ export function StrategicAlignmentPopup({
|
|||
return (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
<foreignObject width={80} height={20} x={-45} y={0}>
|
||||
<TruncatedText maxWords={2} text={payload.value} />
|
||||
<TruncatedText
|
||||
maxWords={2}
|
||||
text={payload.value}
|
||||
/>
|
||||
</foreignObject>
|
||||
</g>
|
||||
);
|
||||
|
|
@ -381,8 +179,10 @@ export function StrategicAlignmentPopup({
|
|||
tickFormatter={(value) =>
|
||||
`${formatNumber(Math.round(value))}`
|
||||
}
|
||||
|
||||
|
||||
label={{
|
||||
value: "تعداد برنامه ها",
|
||||
value: "تعداد برنامه ها" ,
|
||||
angle: -90,
|
||||
position: "insideLeft",
|
||||
fill: "#94a3b8",
|
||||
|
|
@ -395,24 +195,21 @@ export function StrategicAlignmentPopup({
|
|||
|
||||
<Bar dataKey="percentage" radius={[8, 8, 0, 0]}>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={chartConfig.percentage.color}
|
||||
/>
|
||||
<Cell key={`cell-${index}`} fill={chartConfig.percentage.color} />
|
||||
))}
|
||||
<LabelList
|
||||
dataKey="percentage"
|
||||
position="top"
|
||||
offset={15}
|
||||
|
||||
style={{
|
||||
fill: "#ffffff",
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
formatter={(v: number) =>
|
||||
`${formatNumber(Math.round(v))}`
|
||||
}
|
||||
formatter={(v: number) => `${formatNumber(Math.round(v))}`}
|
||||
/>
|
||||
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
|
|
@ -10,12 +11,9 @@ import {
|
|||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
|
||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
||||
import apiService from "~/lib/api";
|
||||
import { EventBus, formatNumber } from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
|
||||
export interface CompanyDetails {
|
||||
id: string;
|
||||
|
|
@ -64,50 +62,27 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
const [counts, setCounts] = useState<EcosystemCounts | null>(null);
|
||||
const [processData, setProcessData] = useState<ProcessActorsData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
// const [date, setDate] = useState<CalendarDate>();
|
||||
const [date, setDate] = useStoredDate();
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (date: CalendarDate) => {
|
||||
if (date) setDate(date);
|
||||
};
|
||||
|
||||
EventBus.on("dateSelected", handler);
|
||||
|
||||
return () => {
|
||||
EventBus.off("dateSelected", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (date.end && date.start) fetchCounts();
|
||||
}, [date]);
|
||||
|
||||
const fetchCounts = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [countsRes, processRes] = await Promise.all([
|
||||
apiService.call<EcosystemCounts>({
|
||||
ecosystem_count_function: {
|
||||
start_date: date?.start || null,
|
||||
end_date: date?.end || null,
|
||||
},
|
||||
ecosystem_count_function: {},
|
||||
}),
|
||||
apiService.call<ProcessActorsResponse[]>({
|
||||
process_creating_actors_function: {
|
||||
start_date: date?.start || null,
|
||||
end_date: date?.end || null,
|
||||
},
|
||||
process_creating_actors_function: {},
|
||||
}),
|
||||
]);
|
||||
|
||||
setCounts(
|
||||
JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0]
|
||||
JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0],
|
||||
);
|
||||
|
||||
// Process the years data and fill missing years
|
||||
const processedData = processYearsData(
|
||||
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors)
|
||||
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors),
|
||||
);
|
||||
setProcessData(processedData);
|
||||
} catch (err) {
|
||||
|
|
@ -116,6 +91,8 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchCounts();
|
||||
}, []);
|
||||
|
||||
// Helper function to safely parse numbers
|
||||
const parseNumber = (value: string | undefined): number => {
|
||||
|
|
@ -126,7 +103,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
|
||||
// Helper function to process years data and fill missing years
|
||||
const processYearsData = (
|
||||
data: ProcessActorsResponse[]
|
||||
data: ProcessActorsResponse[],
|
||||
): ProcessActorsData[] => {
|
||||
if (!data || data.length === 0) return [];
|
||||
|
||||
|
|
@ -144,7 +121,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
acc[item.start_year] = item.total_count;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
|
||||
for (let year = minYear; year <= maxYear; year++) {
|
||||
|
|
@ -190,7 +167,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
{ label: "شتابدهنده", value: parseNumber(counts.accelerator_count) },
|
||||
{ label: "دانشگاه", value: parseNumber(counts.university_count) },
|
||||
{ label: "صندوق های مالی", value: parseNumber(counts.fund_count) },
|
||||
{ label: "تامین کننده", value: parseNumber(counts.company_count) },
|
||||
{ label: "شرکت", value: parseNumber(counts.company_count) },
|
||||
]
|
||||
: [];
|
||||
|
||||
|
|
@ -279,7 +256,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute w-2 h-2 bg-pr-green rounded-full animate-pulse"
|
||||
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse"
|
||||
style={{
|
||||
left: `${20 + i * 25}%`,
|
||||
top: `${30 + Math.random() * 40}%`,
|
||||
|
|
@ -310,7 +287,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
{/* Actor Count Skeleton */}
|
||||
<CardHeader className="text-center pt-0 pb-4">
|
||||
<div className="w-36 h-5 rounded animate-pulse mx-auto mb-2"></div>
|
||||
<div className="w-16 h-8 bg-pr-green bg-opacity-30 rounded animate-pulse mx-auto"></div>
|
||||
<div className="w-16 h-8 bg-green-400 bg-opacity-30 rounded animate-pulse mx-auto"></div>
|
||||
</CardHeader>
|
||||
|
||||
{/* Bar Chart Skeleton */}
|
||||
|
|
@ -385,7 +362,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute w-2 h-2 bg-pr-green rounded-full animate-pulse"
|
||||
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse"
|
||||
style={{
|
||||
left: `${20 + i * 25}%`,
|
||||
top: `${30 + Math.random() * 40}%`,
|
||||
|
|
@ -401,7 +378,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
<CardContent className="pt-0 pb-6">
|
||||
<div className="bg-[rgba(255,255,255,0.1)] rounded-lg p-4 text-center">
|
||||
<div className="w-28 h-4 bg-gray-600 rounded animate-pulse mx-auto mb-1"></div>
|
||||
<div className="w-12 h-6 bg-pr-green bg-opacity-30 rounded animate-pulse mx-auto"></div>
|
||||
<div className="w-12 h-6 bg-green-400 bg-opacity-30 rounded animate-pulse mx-auto"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -455,7 +432,6 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
<CardContent className="flex-1 px-6 border-b-2 border-[#3F415A]">
|
||||
<div className="w-full">
|
||||
<CustomBarChart
|
||||
hasPercent={false}
|
||||
data={barData.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
|
|
@ -485,13 +461,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
margin={{ top: 25, right: 30, left: 0, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="fillDesktop"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#3AEA83" stopOpacity={1} />
|
||||
<stop offset="100%" stopColor="#3AEA83" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
|
|
@ -530,14 +500,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
activeDot={({ cx, cy, payload }) => (
|
||||
<g>
|
||||
{/* Small circle */}
|
||||
<circle
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={5}
|
||||
fill="#3AEA83"
|
||||
stroke="#fff"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<circle cx={cx} cy={cy} r={5} fill="#3AEA83" stroke="#fff" strokeWidth={2} />
|
||||
{/* Year label above point */}
|
||||
<text
|
||||
x={cx}
|
||||
|
|
@ -553,7 +516,8 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
)}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</ResponsiveContainer>
|
||||
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-400 font-persian">
|
||||
دادهای برای نمایش وجود ندارد
|
||||
|
|
@ -561,6 +525,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,11 @@
|
|||
import React, { useEffect, useRef, useState, useCallback } from "react";
|
||||
import * as d3 from "d3";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useStoredDate } from "~/hooks/useStoredDate";
|
||||
import { EventBus } from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import { useAuth } from "../../contexts/auth-context";
|
||||
import apiService from "../../lib/api";
|
||||
import { useAuth } from "../../contexts/auth-context";
|
||||
|
||||
// Get API base URL at module level to avoid process.env access in browser
|
||||
const API_BASE_URL =
|
||||
//بندر امام
|
||||
// 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.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
|
|
@ -52,9 +44,9 @@ export interface CompanyDetails {
|
|||
|
||||
export interface NetworkGraphProps {
|
||||
onNodeClick?: (node: CompanyDetails) => void;
|
||||
onLoadingChange?: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
// Helper to robustly parse backend response
|
||||
function parseApiResponse(raw: any): any[] {
|
||||
let data = raw;
|
||||
try {
|
||||
|
|
@ -64,14 +56,12 @@ function parseApiResponse(raw: any): any[] {
|
|||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
// Check if we're in browser environment
|
||||
function isBrowser(): boolean {
|
||||
return typeof window !== "undefined";
|
||||
}
|
||||
|
||||
export function NetworkGraph({
|
||||
onNodeClick,
|
||||
onLoadingChange,
|
||||
}: NetworkGraphProps) {
|
||||
export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [links, setLinks] = useState<Link[]>([]);
|
||||
|
|
@ -80,21 +70,7 @@ export function NetworkGraph({
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
const { token } = useAuth();
|
||||
|
||||
// const [date, setDate] = useState<CalendarDate>();
|
||||
|
||||
const [date, setDate] = useStoredDate();
|
||||
useEffect(() => {
|
||||
const handler = (date: CalendarDate) => {
|
||||
if (date) setDate(date);
|
||||
};
|
||||
|
||||
EventBus.on("dateSelected", handler);
|
||||
|
||||
return () => {
|
||||
EventBus.off("dateSelected", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Ensure component only renders on client side
|
||||
useEffect(() => {
|
||||
if (isBrowser()) {
|
||||
const timer = setTimeout(() => setIsMounted(true), 100);
|
||||
|
|
@ -102,27 +78,7 @@ export function NetworkGraph({
|
|||
}
|
||||
}, []);
|
||||
|
||||
const getImageUrl = useCallback(
|
||||
(stageid: number) => {
|
||||
if (!token?.accessToken) return null;
|
||||
return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`;
|
||||
},
|
||||
[token?.accessToken]
|
||||
);
|
||||
|
||||
const callAPI = useCallback(
|
||||
async (stage_id: number) => {
|
||||
return await apiService.call<any>({
|
||||
get_values_workflow_function: {
|
||||
stage_id: stage_id,
|
||||
// start_date: date?.start || null,
|
||||
// end_date: date?.end || null,
|
||||
},
|
||||
});
|
||||
},
|
||||
[date]
|
||||
);
|
||||
|
||||
// Fetch data from API
|
||||
useEffect(() => {
|
||||
if (!isMounted) return;
|
||||
|
||||
|
|
@ -133,45 +89,28 @@ export function NetworkGraph({
|
|||
setIsLoading(true);
|
||||
try {
|
||||
const res = await apiService.call<any[]>({
|
||||
graph_production_function: {
|
||||
start_date: date.start || null,
|
||||
end_date: date.end || null,
|
||||
},
|
||||
graph_production_function: {},
|
||||
});
|
||||
if (aborted) return;
|
||||
|
||||
const data = parseApiResponse(JSON.parse(res.data)?.graph_production);
|
||||
console.log(
|
||||
"All available fields in first item:",
|
||||
Object.keys(data[0] || {})
|
||||
Object.keys(data[0] || {}),
|
||||
);
|
||||
|
||||
// نود مرکزی
|
||||
// Create center node
|
||||
const centerNode: Node = {
|
||||
id: "center",
|
||||
// label: "پتروشیمی بندر امام",
|
||||
label: "پتروشیمی نوری",
|
||||
// label: "پتروشیمی آپادانا",
|
||||
label: "پتروشیمی بندر امام", //مرکز زیست بوم
|
||||
category: "center",
|
||||
stageid: 0,
|
||||
isCenter: true,
|
||||
};
|
||||
|
||||
// دستهبندیها
|
||||
const categories = Array.from(
|
||||
new Set(data.map((item: any) => item.category))
|
||||
);
|
||||
|
||||
const categoryNodes: Node[] = categories.map((cat, index) => ({
|
||||
id: `cat-${index}`,
|
||||
label: cat,
|
||||
category: cat,
|
||||
stageid: -1,
|
||||
}));
|
||||
|
||||
// نودهای نهایی
|
||||
const finalNodes: Node[] = data.map((item: any) => ({
|
||||
id: `node-${item.stageid}`,
|
||||
// Create ecosystem nodes
|
||||
const ecosystemNodes: Node[] = data.map((item: any) => ({
|
||||
id: String(item.stageid),
|
||||
label: item.title,
|
||||
category: item.category,
|
||||
stageid: item.stageid,
|
||||
|
|
@ -179,16 +118,13 @@ export function NetworkGraph({
|
|||
rawData: item,
|
||||
}));
|
||||
|
||||
// لینکها: مرکز → دستهبندیها → نودهای نهایی
|
||||
const graphLinks: Link[] = [
|
||||
...categoryNodes.map((cat) => ({ source: "center", target: cat.id })),
|
||||
...finalNodes.map((node) => {
|
||||
const catIndex = categories.indexOf(node.category);
|
||||
return { source: `cat-${catIndex}`, target: node.id };
|
||||
}),
|
||||
];
|
||||
// Create links (all nodes connected to center)
|
||||
const graphLinks: Link[] = ecosystemNodes.map((node) => ({
|
||||
source: "center",
|
||||
target: node.id,
|
||||
}));
|
||||
|
||||
setNodes([centerNode, ...categoryNodes, ...finalNodes]);
|
||||
setNodes([centerNode, ...ecosystemNodes]);
|
||||
setLinks(graphLinks);
|
||||
} catch (err: any) {
|
||||
if (err.name !== "AbortError") {
|
||||
|
|
@ -206,19 +142,43 @@ export function NetworkGraph({
|
|||
aborted = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, [isMounted, token, getImageUrl, date]);
|
||||
}, [isMounted, token]);
|
||||
|
||||
// Get image URL for a node
|
||||
const getImageUrl = useCallback(
|
||||
(stageid: number) => {
|
||||
if (!token?.accessToken) return null;
|
||||
return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`;
|
||||
},
|
||||
[token?.accessToken],
|
||||
);
|
||||
|
||||
// Import apiService for the onClick handler
|
||||
const callAPI = useCallback(async (stage_id: number) => {
|
||||
return await apiService.call<any>({
|
||||
get_values_workflow_function: {
|
||||
stage_id: stage_id,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Initialize D3 graph
|
||||
useEffect(() => {
|
||||
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0)
|
||||
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = d3.select(svgRef.current);
|
||||
const width = svgRef.current.clientWidth;
|
||||
const height = svgRef.current.clientHeight;
|
||||
|
||||
// Clear previous content
|
||||
svg.selectAll("*").remove();
|
||||
|
||||
// Create defs for patterns and filters
|
||||
const defs = svg.append("defs");
|
||||
|
||||
// Add glow filter for hover effect
|
||||
const filter = defs
|
||||
.append("filter")
|
||||
.attr("id", "glow")
|
||||
|
|
@ -236,27 +196,33 @@ export function NetworkGraph({
|
|||
feMerge.append("feMergeNode").attr("in", "coloredBlur");
|
||||
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
|
||||
|
||||
const container = svg.append("g");
|
||||
|
||||
// Create zoom behavior
|
||||
const zoom = d3
|
||||
.zoom<SVGSVGElement, unknown>()
|
||||
.scaleExtent([0.3, 2.5])
|
||||
.on("zoom", (event) => container.attr("transform", event.transform));
|
||||
.scaleExtent([0.8, 2.5]) // Limit zoom out to 1x, zoom in to 2.5x
|
||||
.on("zoom", (event) => {
|
||||
container.attr("transform", event.transform);
|
||||
});
|
||||
|
||||
svg.call(zoom);
|
||||
|
||||
// Create container group
|
||||
const container = svg.append("g");
|
||||
|
||||
// Category colors
|
||||
const categoryToColor: Record<string, string> = {
|
||||
دانشگاه: "#3B82F6",
|
||||
مشاور: "#10B981",
|
||||
"دانش بنیان": "#F59E0B",
|
||||
استارتاپ: "#EF4444",
|
||||
"تامین کننده": "#8B5CF6",
|
||||
شرکت: "#8B5CF6",
|
||||
صندوق: "#06B6D4",
|
||||
شتابدهنده: "#9333EA",
|
||||
"مرکز نوآوری": "#F472B6",
|
||||
center: "#34D399",
|
||||
};
|
||||
|
||||
// Create force simulation
|
||||
const simulation = d3
|
||||
.forceSimulation<Node>(nodes)
|
||||
.force(
|
||||
|
|
@ -265,21 +231,16 @@ export function NetworkGraph({
|
|||
.forceLink<Node, Link>(links)
|
||||
.id((d) => d.id)
|
||||
.distance(150)
|
||||
.strength(0.2)
|
||||
.strength(0.1),
|
||||
)
|
||||
.force("charge", d3.forceManyBody().strength(-300))
|
||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||
.force(
|
||||
"radial",
|
||||
d3.forceRadial((d) => (d.isCenter ? 0 : 300), width / 2, height / 2)
|
||||
)
|
||||
.force(
|
||||
"collision",
|
||||
d3.forceCollide().radius((d) => (d.isCenter ? 50 : 35))
|
||||
d3.forceCollide().radius((d) => (d.isCenter ? 40 : 30)),
|
||||
);
|
||||
|
||||
// Initial zoom to show entire graph
|
||||
const initialScale = 0.6;
|
||||
const initialScale = 0.85;
|
||||
const initialTranslate = [
|
||||
width / 2 - (width / 2) * initialScale,
|
||||
height / 2 - (height / 2) * initialScale,
|
||||
|
|
@ -288,69 +249,37 @@ export function NetworkGraph({
|
|||
zoom.transform,
|
||||
d3.zoomIdentity
|
||||
.translate(initialTranslate[0], initialTranslate[1])
|
||||
.scale(initialScale)
|
||||
.scale(initialScale),
|
||||
);
|
||||
|
||||
// Fix center node
|
||||
// Fix center node position
|
||||
const centerNode = nodes.find((n) => n.isCenter);
|
||||
const categoryNodes = nodes.filter((n) => !n.isCenter && n.stageid === -1);
|
||||
|
||||
if (centerNode) {
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
centerNode.fx = centerX;
|
||||
centerNode.fy = centerY;
|
||||
|
||||
const baseRadius = 450; // شعاع پایه
|
||||
const variation = 100; // تغییر طول یکی در میان
|
||||
const angleStep = (2 * Math.PI) / categoryNodes.length;
|
||||
|
||||
categoryNodes.forEach((catNode, i) => {
|
||||
const angle = i * angleStep;
|
||||
const radius = baseRadius + (i % 2 === 0 ? -variation : variation);
|
||||
catNode.fx = centerX + radius * Math.cos(angle);
|
||||
catNode.fy = centerY + radius * Math.sin(angle);
|
||||
});
|
||||
centerNode.fx = width / 2;
|
||||
centerNode.fy = height / 2;
|
||||
}
|
||||
|
||||
// نودهای نهایی **هیچ fx/fy نداشته باشند**
|
||||
// فقط forceLink آنها را به دستهها متصل نگه میدارد
|
||||
|
||||
// const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1);
|
||||
|
||||
// categoryNodes.forEach((catNode) => {
|
||||
// const childNodes = finalNodes.filter(n => n.category === catNode.category);
|
||||
// const childCount = childNodes.length;
|
||||
// const radius = 100; // فاصله از دسته
|
||||
// const angleStep = (2 * Math.PI) / childCount;
|
||||
|
||||
// childNodes.forEach((node, i) => {
|
||||
// const angle = i * angleStep;
|
||||
// node.fx = catNode.fx! + radius * Math.cos(angle);
|
||||
// node.fy = catNode.fy! + radius * Math.sin(angle);
|
||||
// });
|
||||
// });
|
||||
|
||||
// Curved links
|
||||
// Create links
|
||||
const link = container
|
||||
.selectAll(".link")
|
||||
.data(links)
|
||||
.enter()
|
||||
.append("path")
|
||||
.append("line")
|
||||
.attr("class", "link")
|
||||
.attr("stroke", "#E2E8F0")
|
||||
.attr("stroke-width", 2)
|
||||
.attr("stroke-opacity", 0.6)
|
||||
.attr("fill", "none");
|
||||
.attr("stroke-opacity", 0.6);
|
||||
|
||||
// Create node groups
|
||||
const nodeGroup = container
|
||||
.selectAll(".node")
|
||||
.data(nodes)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "node")
|
||||
.style("cursor", (d) => (d.stageid === -1 ? "default" : "pointer"));
|
||||
.style("cursor", "pointer");
|
||||
|
||||
// Add drag behavior
|
||||
const drag = d3
|
||||
.drag<SVGGElement, Node>()
|
||||
.on("start", (event, d) => {
|
||||
|
|
@ -372,65 +301,18 @@ export function NetworkGraph({
|
|||
|
||||
nodeGroup.call(drag);
|
||||
|
||||
// Add node circles/rectangles
|
||||
nodeGroup.each(function (d) {
|
||||
const group = d3.select(this);
|
||||
|
||||
// if (d.isCenter) {
|
||||
// const rect = group
|
||||
// .append("rect")
|
||||
// .attr("width", 200)
|
||||
// .attr("height", 80)
|
||||
// .attr("x", -100) // نصف عرض جدید منفی
|
||||
// .attr("y", -40) // نصف ارتفاع جدید منفی
|
||||
// .attr("rx", 8)
|
||||
// .attr("ry", 8)
|
||||
// .attr("fill", categoryToColor[d.category] || "#94A3B8")
|
||||
// .attr("stroke", "#FFFFFF")
|
||||
// .attr("stroke-width", 3)
|
||||
// .style("pointer-events", "none");
|
||||
|
||||
// if (d.imageUrl || d.isCenter) {
|
||||
// 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", 200) // ← هماندازه با مستطیل
|
||||
// .attr("height", 80)
|
||||
// .attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
|
||||
// .attr("preserveAspectRatio", "xMidYMid slice");
|
||||
|
||||
// rect.attr("fill", `url(#image-${d.id})`);
|
||||
// }
|
||||
// }
|
||||
// راه حل سادهتر - ابعاد ثابت با حفظ نسبت
|
||||
if (d.isCenter) {
|
||||
|
||||
//آپادانا
|
||||
// const fixedWidth = 196;
|
||||
// const fixedHeight = 200; // یا میتوانید براساس نسبت تصویر محاسبه کنید
|
||||
|
||||
//بندر امام
|
||||
// const fixedWidth = 100;
|
||||
// const fixedHeight = 80; // یا میتوانید براساس نسبت تصویر محاسبه کنید
|
||||
|
||||
//نوری
|
||||
const fixedWidth = 186;
|
||||
const fixedHeight = 70; // یا میتوانید براساس نسبت تصویر محاسبه کنید
|
||||
|
||||
if (d.isCenter) {
|
||||
// Center node as rectangle
|
||||
const rect = group
|
||||
.append("rect")
|
||||
.attr("width", fixedWidth)
|
||||
.attr("height", fixedHeight)
|
||||
.attr("x", -fixedWidth / 2)
|
||||
.attr("y", -fixedHeight / 2)
|
||||
.attr("width", 150)
|
||||
.attr("height", 60)
|
||||
.attr("x", -75)
|
||||
.attr("y", -30)
|
||||
.attr("rx", 8)
|
||||
.attr("ry", 8)
|
||||
.attr("fill", categoryToColor[d.category] || "#94A3B8")
|
||||
|
|
@ -438,6 +320,8 @@ const fixedHeight = 70; // یا میتوانید براساس نسبت تصو
|
|||
.attr("stroke-width", 3)
|
||||
.style("pointer-events", "none");
|
||||
|
||||
// Add center image if available
|
||||
if (d.imageUrl || d.isCenter) {
|
||||
const pattern = defs
|
||||
.append("pattern")
|
||||
.attr("id", `image-${d.id}`)
|
||||
|
|
@ -450,22 +334,23 @@ const fixedHeight = 70; // یا میتوانید براساس نسبت تصو
|
|||
.append("image")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
.attr("width", fixedWidth)
|
||||
.attr("height", fixedHeight)
|
||||
.attr("width", 150)
|
||||
.attr("height", 60)
|
||||
.attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
|
||||
.attr("preserveAspectRatio", "xMidYMid meet"); // حفظ نسبت تصویر
|
||||
.attr("preserveAspectRatio", "xMidYMid slice");
|
||||
|
||||
rect.attr("fill", `url(#image-${d.id})`);
|
||||
|
||||
}
|
||||
else {
|
||||
}
|
||||
} else {
|
||||
// Regular nodes as circles
|
||||
const circle = group
|
||||
.append("circle")
|
||||
.attr("r", 25)
|
||||
.attr("fill", categoryToColor[d.category] || "#fff")
|
||||
.attr("fill", categoryToColor[d.category] || "8#fff")
|
||||
.attr("stroke", "#FFFFFF")
|
||||
.attr("stroke-width", 3);
|
||||
|
||||
// Add node image if available
|
||||
if (d.imageUrl) {
|
||||
const pattern = defs
|
||||
.append("pattern")
|
||||
|
|
@ -482,8 +367,10 @@ const fixedHeight = 70; // یا میتوانید براساس نسبت تصو
|
|||
.attr("width", 50)
|
||||
.attr("height", 50)
|
||||
.attr("href", d.imageUrl)
|
||||
.attr("backgroundColor", "#fff")
|
||||
.attr("preserveAspectRatio", "xMidYMid slice");
|
||||
|
||||
// Create circular clip path
|
||||
defs
|
||||
.append("clipPath")
|
||||
.attr("id", `clip-${d.id}`)
|
||||
|
|
@ -497,26 +384,12 @@ const fixedHeight = 70; // یا میتوانید براساس نسبت تصو
|
|||
}
|
||||
});
|
||||
|
||||
// Add labels below nodes
|
||||
const labels = nodeGroup
|
||||
.append("text")
|
||||
.text((d) => d.label)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dy", (d) => {
|
||||
if (d.isCenter) {
|
||||
|
||||
//آپادانا
|
||||
// const centerNodeHeight = 200; // ارتفاع نود مرکزی
|
||||
|
||||
//بندر امام
|
||||
// const centerNodeHeight = 80; // ارتفاع نود مرکزی
|
||||
|
||||
//نوری
|
||||
const centerNodeHeight = 80; // ارتفاع نود مرکزی
|
||||
|
||||
return centerNodeHeight / 2 + 20; // نصف ارتفاع + فاصله 20px
|
||||
}
|
||||
return 45; // برای نودهای دیگر
|
||||
})
|
||||
.attr("dy", (d) => (d.isCenter ? 50 : 45))
|
||||
.attr("font-size", (d) => (d.isCenter ? "14px" : "12px"))
|
||||
.attr("font-weight", "bold")
|
||||
.attr("fill", "#F9FAFB")
|
||||
|
|
@ -524,6 +397,7 @@ const fixedHeight = 70; // یا میتوانید براساس نسبت تصو
|
|||
.attr("stroke-width", 4)
|
||||
.attr("paint-order", "stroke");
|
||||
|
||||
// Add hover effects
|
||||
nodeGroup
|
||||
.on("mouseenter", function (event, d) {
|
||||
if (d.isCenter) return;
|
||||
|
|
@ -545,45 +419,34 @@ const fixedHeight = 70; // یا میتوانید براساس نسبت تصو
|
|||
.attr("stroke-width", 3);
|
||||
});
|
||||
|
||||
// Add click handlers
|
||||
nodeGroup.on("click", async function (event, d) {
|
||||
event.stopPropagation();
|
||||
|
||||
// جلوگیری از کلیک روی مرکز و دستهبندیها
|
||||
if (d.isCenter || d.stageid === -1) return;
|
||||
// Don't handle center node clicks
|
||||
if (d.isCenter) return;
|
||||
|
||||
if (onNodeClick && d.stageid) {
|
||||
// Open dialog immediately with basic info
|
||||
const basicDetails: CompanyDetails = {
|
||||
id: d.id,
|
||||
label: d.label,
|
||||
category: d.category,
|
||||
stageid: d.stageid,
|
||||
fields: [],
|
||||
};
|
||||
onNodeClick(basicDetails);
|
||||
|
||||
// Start loading
|
||||
onLoadingChange?.(true);
|
||||
|
||||
try {
|
||||
if (date.start && date.end) {
|
||||
// Fetch detailed company data
|
||||
const res = await callAPI(d.stageid);
|
||||
|
||||
const responseData = JSON.parse(res.data);
|
||||
const fieldValues =
|
||||
JSON.parse(responseData?.getvalue)?.[0]?.FieldValues || [];
|
||||
|
||||
// Filter out image fields and find description
|
||||
const filteredFields = fieldValues.filter(
|
||||
(field: any) =>
|
||||
!["image", "img", "full_name", "about_collaboration"].includes(
|
||||
field.F.toLowerCase()
|
||||
)
|
||||
field.F.toLowerCase(),
|
||||
),
|
||||
);
|
||||
|
||||
const descriptionField = fieldValues.find(
|
||||
(field: any) =>
|
||||
field.F.toLowerCase().includes("description") ||
|
||||
field.F.toLowerCase().includes("about_collaboration") ||
|
||||
field.F.toLowerCase().includes("about")
|
||||
field.F.toLowerCase().includes("about"),
|
||||
);
|
||||
|
||||
const companyDetails: CompanyDetails = {
|
||||
|
|
@ -596,37 +459,39 @@ const fixedHeight = 70; // یا میتوانید براساس نسبت تصو
|
|||
};
|
||||
|
||||
onNodeClick(companyDetails);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch company details:", error);
|
||||
// Keep the basic details already shown
|
||||
} finally {
|
||||
// Stop loading
|
||||
onLoadingChange?.(false);
|
||||
// Fallback to basic info
|
||||
const basicDetails: CompanyDetails = {
|
||||
id: d.id,
|
||||
label: d.label,
|
||||
category: d.category,
|
||||
stageid: d.stageid,
|
||||
fields: [],
|
||||
};
|
||||
onNodeClick(basicDetails);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update positions on simulation tick
|
||||
simulation.on("tick", () => {
|
||||
link.attr("d", (d: any) => {
|
||||
const sx = (d.source as Node).x!;
|
||||
const sy = (d.source as Node).y!;
|
||||
const tx = (d.target as Node).x!;
|
||||
const ty = (d.target as Node).y!;
|
||||
const dx = tx - sx;
|
||||
const dy = ty - sy;
|
||||
const dr = Math.sqrt(dx * dx + dy * dy) * 1.2; // منحنی
|
||||
return `M${sx},${sy}A${dr},${dr} 0 0,1 ${tx},${ty}`;
|
||||
});
|
||||
link
|
||||
.attr("x1", (d) => (d.source as Node).x!)
|
||||
.attr("y1", (d) => (d.source as Node).y!)
|
||||
.attr("x2", (d) => (d.target as Node).x!)
|
||||
.attr("y2", (d) => (d.target as Node).y!);
|
||||
|
||||
nodeGroup.attr("transform", (d) => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
simulation.stop();
|
||||
};
|
||||
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI, date]);
|
||||
}, [nodes, links, isLoading, isMounted, onNodeClick, callAPI]);
|
||||
|
||||
// Show error message
|
||||
if (error) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800">
|
||||
|
|
@ -640,6 +505,7 @@ const fixedHeight = 70; // یا میتوانید براساس نسبت تصو
|
|||
);
|
||||
}
|
||||
|
||||
// Don't render on server side
|
||||
if (!isMounted) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center bg-transparent">
|
||||
|
|
@ -653,11 +519,14 @@ const fixedHeight = 70; // یا میتوانید براساس نسبت تصو
|
|||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full h-full relative bg-transparent">
|
||||
{/* Skeleton Graph Container */}
|
||||
<div className="w-full h-full flex items-center justify-center relative">
|
||||
{/* Center Node Skeleton */}
|
||||
<div className="w-12 h-12 rounded-lg bg-gray-600 animate-pulse relative z-10">
|
||||
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-gray-500 to-gray-600 animate-pulse"></div>
|
||||
</div>
|
||||
|
||||
{/* Outer Ring Nodes Skeleton */}
|
||||
{Array.from({ length: 8 }).map((_, i) => {
|
||||
const angle = (i * 2 * Math.PI) / 8;
|
||||
const radius = 120;
|
||||
|
|
@ -678,25 +547,40 @@ const fixedHeight = 70; // یا میتوانید براساس نسبت تصو
|
|||
<div
|
||||
className="absolute w-16 h-3 bg-gray-600 rounded animate-pulse"
|
||||
style={{
|
||||
transform: `rotate(${(i * 360) / 8}deg) translateX(32px)`,
|
||||
transformOrigin: "left center",
|
||||
left: "50%",
|
||||
top: "40px",
|
||||
transform: "translateX(-50%)",
|
||||
animationDelay: `${i * 200 + 100}ms`,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="absolute w-0.5 bg-gray-600 animate-pulse opacity-30"
|
||||
style={{
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
height: `${radius - 16}px`,
|
||||
transformOrigin: "top",
|
||||
transform: `translateX(-50%) rotate(${angle + Math.PI}rad)`,
|
||||
animationDelay: `${i * 100}ms`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2">
|
||||
<div className="text-white font-persian text-sm animate-pulse">
|
||||
در حال بارگذاری نمودار...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<svg
|
||||
ref={svgRef}
|
||||
className="w-full h-full bg-transparent"
|
||||
style={{ cursor: "grab" }}
|
||||
/>
|
||||
<div className="w-full h-full relative bg-transparent overflow-hidden">
|
||||
<svg ref={svgRef} className="w-full h-full" style={{ minHeight: 500 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
interface MonthItem {
|
||||
id: string;
|
||||
label: string;
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
// interface CurrentDay {
|
||||
// start: string;
|
||||
// end: string;
|
||||
// month: string;
|
||||
// }
|
||||
|
||||
interface CalendarProps {
|
||||
title: string;
|
||||
nextYearHandler: () => void;
|
||||
prevYearHandler: () => void;
|
||||
currentYear?: number;
|
||||
monthList: Array<MonthItem>;
|
||||
selectedDate?: string;
|
||||
selectDateHandler: (item: MonthItem) => void;
|
||||
}
|
||||
|
||||
export const Calendar: React.FC<CalendarProps> = ({
|
||||
title,
|
||||
nextYearHandler,
|
||||
prevYearHandler,
|
||||
currentYear,
|
||||
monthList,
|
||||
selectedDate,
|
||||
selectDateHandler,
|
||||
}) => {
|
||||
return (
|
||||
<div className="filter-box bg-pr-gray w-full px-1">
|
||||
<header className="flex flex-row border-b border-[#5F6284] pb-1.5 justify-center">
|
||||
<span className="font-light">{title}</span>
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<ChevronRight
|
||||
className="inline-block w-6 h-6 cursor-pointer"
|
||||
onClick={nextYearHandler}
|
||||
/>
|
||||
<span className="font-light">{currentYear}</span>
|
||||
<ChevronLeft
|
||||
className="inline-block w-6 h-6 cursor-pointer"
|
||||
onClick={prevYearHandler}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div className="content flex flex-col gap-2 text-center pt-1 cursor-pointer">
|
||||
{monthList.map((item, index) => (
|
||||
<span
|
||||
key={`${item.id}-${index}`}
|
||||
className={`text-lg hover:bg-[#33364D] p-1 rounded-xl transition-all duration-300 ${
|
||||
selectedDate === item.label ? `bg-[#33364D]` : ""
|
||||
}`}
|
||||
onClick={() => selectDateHandler(item)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,7 +7,7 @@ interface BaseCardProps {
|
|||
headerClassName?: string;
|
||||
contentClassName?: string;
|
||||
children: React.ReactNode;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
icon ?: React.ComponentType<{ className?: string }>;
|
||||
withHeader?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -18,42 +18,30 @@ export function BaseCard({
|
|||
contentClassName,
|
||||
children,
|
||||
withHeader = false,
|
||||
icon: Icon,
|
||||
icon : Icon,
|
||||
}: BaseCardProps) {
|
||||
return (
|
||||
<Card
|
||||
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
|
||||
)}
|
||||
>
|
||||
{Icon && title ? (
|
||||
<CardHeader
|
||||
className={cn(
|
||||
"border-b-2 border-gray-500/20 py-2 px-0 pb-4",
|
||||
headerClassName
|
||||
)}
|
||||
>
|
||||
<CardTitle className="text-white text-sm text-right font-persian px-4 my-auto items-center flex w-full justify-between">
|
||||
{title} {<Icon />}{" "}
|
||||
</CardTitle>
|
||||
<CardHeader className={cn("border-b-2 border-gray-500/20 py-2 px-0 pb-4", headerClassName)}>
|
||||
<CardTitle className="text-white text-sm text-right font-persian px-4 my-auto items-center flex w-full justify-between">{title} {<Icon />} </CardTitle>
|
||||
</CardHeader>
|
||||
) : withHeader && title ? (
|
||||
<CardHeader
|
||||
className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}
|
||||
>
|
||||
<CardTitle className="text-white text-sm text-right font-persian px-4">
|
||||
{title}
|
||||
</CardTitle>
|
||||
) :
|
||||
withHeader && title ? (
|
||||
<CardHeader className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}>
|
||||
<CardTitle className="text-white text-sm text-right font-persian px-4">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
) : title ? (
|
||||
<div className="border-b-2 border-gray-500/20 pb-2">
|
||||
<h3 className="text-sm font-bold text-white text-right font-persian px-4">
|
||||
{title}
|
||||
</h3>
|
||||
<h3 className="text-sm font-bold text-white text-right font-persian px-4">{title}</h3>
|
||||
</div>
|
||||
) : null}
|
||||
<CardContent className={cn("py-2 px-4 ", contentClassName)}>
|
||||
<CardContent className={cn("py-2 px-4", contentClassName)}>
|
||||
{children}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const Card = React.forwardRef<
|
|||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm ",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { calculateNiceRange, formatNumber } from "~/lib/utils";
|
||||
import { formatNumber, calculateNiceRange } from "~/lib/utils";
|
||||
|
||||
export interface BarChartData {
|
||||
label: string;
|
||||
|
|
@ -18,7 +18,6 @@ interface CustomBarChartProps {
|
|||
showAxisLabels?: boolean;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
hasPercent?: boolean;
|
||||
}
|
||||
|
||||
export function CustomBarChart({
|
||||
|
|
@ -29,7 +28,6 @@ export function CustomBarChart({
|
|||
showAxisLabels = true,
|
||||
className = "",
|
||||
loading = false,
|
||||
hasPercent = true,
|
||||
}: CustomBarChartProps) {
|
||||
// استفاده از nice numbers برای محاسبه دامنه مناسب
|
||||
const values = data.map((item) => item.maxValue || item.value);
|
||||
|
|
@ -39,7 +37,7 @@ export function CustomBarChart({
|
|||
// Loading skeleton
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={`space-y-6 p-4 pt-0 ${className}`} style={{ height }}>
|
||||
<div className={`space-y-6 p-4 ${className}`} style={{ height }}>
|
||||
{title && (
|
||||
<div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div>
|
||||
)}
|
||||
|
|
@ -71,7 +69,7 @@ export function CustomBarChart({
|
|||
<div className={`space-y-6 ${className}`} style={{ height }}>
|
||||
{title && (
|
||||
<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}
|
||||
</h3>
|
||||
</div>
|
||||
|
|
@ -86,7 +84,7 @@ export function CustomBarChart({
|
|||
return (
|
||||
<div key={index} className="flex items-center gap-3">
|
||||
<span
|
||||
className={`font-persian text-sm font-normal min-w-[120px] text-left ${
|
||||
className={`font-persian text-sm font-normal min-w-[120px] text-right ${
|
||||
item.labelColor || "text-white"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -109,8 +107,7 @@ export function CustomBarChart({
|
|||
<span className={`text-base font-normal text-left text-white`}>
|
||||
{item.valuePrefix || ""}
|
||||
|
||||
{formatNumber(parseFloat(displayValue))}
|
||||
{hasPercent ? "%" : ""}
|
||||
{formatNumber(parseFloat(displayValue))}%
|
||||
{item.valueSuffix || ""}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
|
|
@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef<
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
|
|
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
|
|||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
|
|
@ -59,13 +59,13 @@ const DialogHeader = ({
|
|||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col p-4 space-y-1.5 text-center sm:text-left",
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
|
|
@ -78,8 +78,8 @@ const DialogFooter = ({
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
|
|
@ -93,8 +93,8 @@ const DialogTitle = React.forwardRef<
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
|
|
@ -105,18 +105,18 @@ const DialogDescription = React.forwardRef<
|
|||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
};
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronDown, Circle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
|
|
@ -34,10 +34,11 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
|
|
@ -51,9 +52,9 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
|
|
@ -64,34 +65,32 @@ const DropdownMenuContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden mt-1 rounded-xl border border-gray-500 bg-pr-gray p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
selected?: boolean;
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, selected, ...props }, ref) => (
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"cursor-pointer select-none rounded-md px-2 py-1.5 text-sm outline-none transition-colors hover:bg-dark-blue data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
selected && "bg-dark-blue text-white",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
|
|
@ -113,9 +112,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
|
|
@ -136,13 +135,13 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
|
|
@ -154,8 +153,8 @@ const DropdownMenuLabel = React.forwardRef<
|
|||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
|
|
@ -166,8 +165,8 @@ const DropdownMenuSeparator = React.forwardRef<
|
|||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
|
|
@ -178,43 +177,24 @@ const DropdownMenuShortcut = ({
|
|||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
const DropdownMenuButton = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-2 text-sm outline-none border border-gray-500 p-3 rounded-xl min-w-50 max-w-72 group",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 transition-transform duration-200 group-data-[state=open]:rotate-180" />
|
||||
</DropdownMenuPrimitive.Trigger>
|
||||
));
|
||||
DropdownMenuButton.displayName = "DropdownMenuButton";
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuButton,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ interface FunnelChartProps {
|
|||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
const greenColors = ["#3C9F71","#3BC47A","#3BC47A","#3BD77E","#3AEA83"]
|
||||
|
||||
export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
|
||||
const maxValue = Math.max(...data.map(d => d.value));
|
||||
|
|
@ -51,15 +50,15 @@ export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
|
|||
|
||||
return (
|
||||
<div key={index} className="grid grid-cols-[6rem_1fr] gap-2 w-full">
|
||||
<div className="text-sm font-light text-white font-persian cols-start-1 justify-self-start min-w-[max-content] text-center">
|
||||
<div className="text-sm font-light text-white cols-start-1 justify-self-start font-thin min-w-[max-content] text-center">
|
||||
{item.label}
|
||||
</div>
|
||||
<div className="flex items-center gap-10 w-full cols-start-2 justify-center">
|
||||
<div className="flex items-center gap-10 w-full cols-start-2 flex items-center justify-center w-full">
|
||||
<div className="flex items-center w-full">
|
||||
<div style={{ width: `${(100 - barWidth) / 2}%` }} />
|
||||
<div
|
||||
className="bg-[#3BC47A] h-8 rounded-2xl flex items-center justify-center text-lg relative"
|
||||
style={{ width: `${barWidth}%` ,backgroundColor : `${greenColors[index]}`}}
|
||||
style={{ width: `${barWidth}%` }}
|
||||
>
|
||||
<span className="text-pr-gray text-base font-semibold">
|
||||
{item.value.toLocaleString('fa-IR')}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ export function MetricCard({
|
|||
percentLabel = "درصد به کل",
|
||||
}: MetricCardProps) {
|
||||
return (
|
||||
<BaseCard title={title} className="h-full">
|
||||
<BaseCard title={title}>
|
||||
<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)}
|
||||
|
|
|
|||
|
|
@ -6,27 +6,7 @@ import { cn, formatNumber } from "~/lib/utils"
|
|||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => {
|
||||
// Dynamic scaling logic based on value ranges
|
||||
const getScaledValue = (inputValue: number) => {
|
||||
const numValue = Number(inputValue);
|
||||
if (numValue <= 1) {
|
||||
return numValue * 100;
|
||||
}
|
||||
else if (numValue <= 10) {
|
||||
return (numValue / 10) * 100;
|
||||
} else if (numValue <= 50) {
|
||||
return (numValue / 50) * 100;
|
||||
}
|
||||
else {
|
||||
return numValue
|
||||
}
|
||||
};
|
||||
|
||||
const scaledValue = getScaledValue(Number(value) || 0);
|
||||
const displayValue = Number(value) || 0;
|
||||
|
||||
return (
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
|
|
@ -36,16 +16,14 @@ const Progress = React.forwardRef<
|
|||
{...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>
|
||||
<span className="w-full right-0 text-sm absolute z-10 px-2 text-[#5F6284]"
|
||||
>{formatNumber(Math.ceil(value || 0 * 10) / 10)}%</span>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-pr-green transition-all z-20"
|
||||
style={{ transform: `translateX(-${100-scaledValue}%)` }}
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${15 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
})
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { cn } from "~/lib/utils"
|
|||
|
||||
interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
|
||||
containerClassName?: string
|
||||
containerRef?: React.RefObject<HTMLDivElement | null>
|
||||
containerRef?: React.RefObject<HTMLDivElement>
|
||||
}
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, TableProps>(
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export function TabsTrigger({
|
|||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
isActive
|
||||
? "bg-pr-gray text-foreground shadow-sm"
|
||||
? "bg-gray-700 text-foreground shadow-sm"
|
||||
: "hover:bg-muted/50",
|
||||
className,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ function TooltipContent({
|
|||
{...props}
|
||||
>
|
||||
{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.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,17 +162,10 @@ class ApiService {
|
|||
|
||||
// Innovation process function call wrapper
|
||||
public async call<T = any>(payload: any) {
|
||||
//بندر امام
|
||||
// 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";
|
||||
const url = "https://inogen-back.pelekan.org/api/call";
|
||||
return this.postAbsolute<T>(url, payload);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// GET request
|
||||
public async get<T = any>(endpoint: string): Promise<ApiResponse<T>> {
|
||||
return this.request<T>(endpoint, {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx";
|
||||
import EventEmitter from "events";
|
||||
import moment from "moment-jalaali";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import moment from "moment-jalaali";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
|
|
@ -23,6 +22,8 @@ export const formatCurrency = (amount: string | number) => {
|
|||
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* محاسبه دامنه nice numbers برای محور Y نمودارها
|
||||
* @param values آرایه از مقادیر دادهها
|
||||
|
|
@ -116,7 +117,7 @@ function calculateNiceNumber(value: number, round: boolean): number {
|
|||
}
|
||||
|
||||
export const handleDataValue = (val: any): any => {
|
||||
moment.loadPersian({ usePersianDigits: true });
|
||||
moment.loadPersian({ usePersianDigits: true });
|
||||
if (val == null) return val;
|
||||
if (
|
||||
typeof val === "string" &&
|
||||
|
|
@ -131,6 +132,4 @@ export const handleDataValue = (val: any): any => {
|
|||
return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
export const EventBus = new EventEmitter();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,28 @@
|
|||
import moment from "moment-jalaali";
|
||||
import type { Route } from "./+types/ecosystem";
|
||||
import React from "react";
|
||||
import { ProtectedRoute } from "~/components/auth/protected-route";
|
||||
import { DashboardLayout } from "~/components/dashboard/layout";
|
||||
import { InfoPanel } from "~/components/ecosystem/info-panel";
|
||||
import { NetworkGraph } from "~/components/ecosystem/network-graph";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} 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 type { Route } from "./+types/ecosystem";
|
||||
import moment from "moment-jalaali";
|
||||
|
||||
// Get API base URL at module level to avoid process.env access in browser
|
||||
const API_BASE_URL =
|
||||
//بندر امام
|
||||
// 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.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
|
||||
|
||||
// Import the CompanyDetails type
|
||||
import { Hexagon } from "lucide-react";
|
||||
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
import { Hexagon } from "lucide-react";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
|
|
@ -61,20 +56,10 @@ function handleValue(val: any): any {
|
|||
export default function EcosystemPage() {
|
||||
const [selectedCompany, setSelectedCompany] =
|
||||
React.useState<CompanyDetails | null>(null);
|
||||
const [isDialogLoading, setIsDialogLoading] = React.useState(false);
|
||||
const { token } = useAuth();
|
||||
|
||||
const closeDialog = () => {
|
||||
setSelectedCompany(null);
|
||||
setIsDialogLoading(false);
|
||||
};
|
||||
|
||||
const handleNodeClick = (company: CompanyDetails) => {
|
||||
setSelectedCompany(company);
|
||||
};
|
||||
|
||||
const handleLoadingChange = (loading: boolean) => {
|
||||
setIsDialogLoading(loading);
|
||||
};
|
||||
|
||||
// Construct image URL
|
||||
|
|
@ -85,7 +70,7 @@ export default function EcosystemPage() {
|
|||
return (
|
||||
<ProtectedRoute requireAuth={true}>
|
||||
<DashboardLayout title="زیست بوم فناوری">
|
||||
<div>
|
||||
<div className="p-4 lg:p-6">
|
||||
<div className="grid grid-cols-1 items-start lg:grid-cols-12 gap-4">
|
||||
<div className="lg:col-span-4">
|
||||
<InfoPanel selectedCompany={selectedCompany} />
|
||||
|
|
@ -94,10 +79,7 @@ export default function EcosystemPage() {
|
|||
<div className="lg:col-span-8 h-full">
|
||||
<Card className="h-full overflow-hidden bg-transparent border-[#3F415A]">
|
||||
<CardContent className="p-0 h-full bg-transparent">
|
||||
<NetworkGraph
|
||||
onNodeClick={handleNodeClick}
|
||||
onLoadingChange={handleLoadingChange}
|
||||
/>
|
||||
<NetworkGraph onNodeClick={setSelectedCompany} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
@ -109,7 +91,7 @@ export default function EcosystemPage() {
|
|||
open={!!selectedCompany}
|
||||
onOpenChange={(open) => !open && closeDialog()}
|
||||
>
|
||||
<DialogContent className="font-persian max-w-6xl min-h-max bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]">
|
||||
<DialogContent className="font-persian max-w-6xl max-h-[75vh] overflow-y-auto bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-right border-b-2 border-gray-600 pt-2 pb-4 mr-4 text-sm font-semibold">
|
||||
معرفی
|
||||
|
|
@ -117,44 +99,7 @@ export default function EcosystemPage() {
|
|||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isDialogLoading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 p-4 gap-6">
|
||||
{/* Right Column - Loading Skeleton */}
|
||||
<div className="space-y-4 p-6 border-l-2 border-gray-600">
|
||||
{/* Company Image & Title Skeleton */}
|
||||
<div className="flex justify-between px-10 items-center mb-4">
|
||||
<div className="h-8 bg-gray-600 rounded animate-pulse w-48"></div>
|
||||
<div className="w-12 h-12 bg-gray-600 rounded-2xl animate-pulse"></div>
|
||||
</div>
|
||||
{/* Description Skeleton */}
|
||||
<div className="p-4 rounded-lg space-y-2">
|
||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-full"></div>
|
||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-5/6"></div>
|
||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-4/6"></div>
|
||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-3/6"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Left Column - Loading Skeleton */}
|
||||
<div className="space-y-2">
|
||||
<div className="h-6 bg-gray-600 rounded animate-pulse w-32"></div>
|
||||
<div className="space-y-3 px-2">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex justify-between items-center rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="h-4 w-4 bg-gray-600 rounded animate-pulse"></div>
|
||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-24"></div>
|
||||
</div>
|
||||
<div className="h-4 bg-gray-600 rounded animate-pulse w-20"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid p-4 pb-6 grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Right Column - Description */}
|
||||
<div className="space-y-4 p-6 border-l-2 border-gray-600">
|
||||
{/* Company Image */}
|
||||
|
|
@ -177,7 +122,7 @@ export default function EcosystemPage() {
|
|||
/>
|
||||
) : null}
|
||||
<div
|
||||
className="w-24 h-24 rounded-full bg-gray-600 border-4 border-pr-green flex items-center justify-center"
|
||||
className="w-24 h-24 rounded-full bg-gray-600 border-4 border-green-400 flex items-center justify-center"
|
||||
style={{
|
||||
display:
|
||||
selectedCompany?.stageid && token?.accessToken
|
||||
|
|
@ -233,9 +178,7 @@ export default function EcosystemPage() {
|
|||
<span className="text-right min-w-1/3">
|
||||
<span className="font-persian text-sm font-normal text-right">
|
||||
{handleValue(field.V)}
|
||||
{field.U && (
|
||||
<span className="mr-1">({field.U})</span>
|
||||
)}
|
||||
{field.U && <span className="mr-1">({field.U})</span>}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -248,7 +191,6 @@ export default function EcosystemPage() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DashboardLayout>
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
export interface CalendarDate {
|
||||
start?: string;
|
||||
end?: string;
|
||||
sinceMonth?: string;
|
||||
untilMonth?: string;
|
||||
}
|
||||
5128
package-lock.json
generated
|
|
@ -26,7 +26,6 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"d3": "^7.9.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"graphology": "^0.26.0",
|
||||
"isbot": "^5.1.27",
|
||||
"lucide-react": "^0.525.0",
|
||||
|
|
@ -36,13 +35,11 @@
|
|||
"react-hot-toast": "^2.5.2",
|
||||
"react-router": "^7.7.0",
|
||||
"recharts": "^2.15.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"xlsx-js-style": "^1.2.0"
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.7.0",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
|
@ -1,13 +1,23 @@
|
|||
<svg width="60" height="16" viewBox="0 0 60 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2855_3816)">
|
||||
<path d="M34.9888 10.8078C35.4446 11.1829 35.9075 11.4573 36.4274 11.6275C38.8345 12.4228 41.3485 11.103 41.993 8.71017C42.6304 6.33465 40.9497 3.89315 38.45 3.56322C36.1746 3.26107 34.1164 4.67457 33.6428 6.86602C33.5538 7.26888 33.5538 7.68217 33.5538 8.09545C33.5538 10.6446 33.5538 13.1938 33.5538 15.7429C33.5538 15.7985 33.5538 15.8506 33.5538 15.9062C33.5574 15.9999 33.5253 16.0312 33.4363 15.9791C32.8345 15.6249 32.19 15.3331 31.6239 14.9233C31.0577 14.5135 30.7408 13.9405 30.7194 13.2459C30.698 12.4506 30.7087 11.6553 30.7087 10.8599C30.7087 8.96369 30.7194 7.07092 30.7052 5.17468C30.698 4.16057 31.136 3.4243 32.0369 2.92419C33.5431 2.0872 35.0458 1.24327 36.5414 0.388917C37.4173 -0.107719 38.2755 -0.111192 39.1479 0.388917C40.6434 1.2398 42.1461 2.0872 43.6523 2.92419C44.5639 3.42777 44.9948 4.17793 44.9912 5.19552C44.9805 6.88685 44.9912 8.58167 44.9912 10.273C44.9912 11.0197 44.7348 11.6587 44.1402 12.1449C44.0084 12.2526 43.8624 12.3464 43.7129 12.4297C42.1817 13.2945 40.6506 14.1523 39.1194 15.0171C38.2648 15.4998 37.4173 15.4998 36.5663 15.0136C36.0891 14.7427 35.6084 14.4753 35.1313 14.2044C35.0601 14.1662 34.9888 14.1384 34.9888 14.0308C34.9924 12.9715 34.9888 11.9157 34.9888 10.8113V10.8078Z" fill="#2E3144"/>
|
||||
<path d="M10.7145 11.4608V0.0486193C10.743 0.0486193 10.768 0.0416734 10.7822 0.0486193C11.3448 0.36466 11.9145 0.670282 12.4629 1.00369C13.1929 1.44823 13.5632 2.11851 13.5668 2.94855C13.5774 5.91448 13.5774 8.88387 13.5668 11.8498C13.5632 12.7493 13.1252 13.43 12.324 13.8815C11.505 14.3434 10.6825 14.7983 9.86706 15.2672C9.71039 15.3575 9.66053 15.3262 9.57507 15.1838C7.39585 11.4955 5.21306 7.81072 3.03027 4.12937C2.98754 4.05643 2.94125 3.9835 2.86647 3.85847V15.3123C2.79881 15.3506 2.76677 15.3123 2.73472 15.295C2.17211 14.9755 1.60237 14.6733 1.05757 14.333C0.409496 13.9301 0.0818991 13.3258 0.0142433 12.5826C0.00356083 12.4819 0.00712166 12.3812 0.00712166 12.2804C0 9.41176 0.00712166 6.53961 0 3.66746C0 2.67072 0.4273 1.93792 1.31751 1.44823C2.11869 1.00716 2.91276 0.559147 3.70326 0.100714C3.84926 0.0173625 3.90623 0.0277815 3.99169 0.173646C6.17448 3.86542 8.36083 7.55719 10.5472 11.249C10.5899 11.3219 10.6362 11.3948 10.6825 11.4678C10.6932 11.4643 10.7074 11.4608 10.7181 11.4573L10.7145 11.4608Z" fill="#2E3144"/>
|
||||
<path d="M29.2664 7.69613C29.2664 8.5609 29.2664 9.4222 29.2664 10.287C29.27 11.2351 28.8498 11.9436 28.013 12.4159C26.4534 13.298 24.8937 14.1802 23.327 15.0519C22.5365 15.4895 21.7282 15.493 20.9377 15.0519C19.3709 14.1802 17.8148 13.2946 16.2516 12.4159C15.4112 11.9436 14.9946 11.2281 14.9946 10.287C14.9946 8.5609 14.9946 6.83483 14.9946 5.10877C14.9946 4.16064 15.4148 3.44174 16.2623 2.96594C17.8077 2.0977 19.3495 1.22598 20.8949 0.357734C21.7068 -0.0972257 22.5258 -0.111118 23.3377 0.340369C24.908 1.21903 26.4783 2.10117 28.0415 2.98678C28.8569 3.44868 29.2664 4.14675 29.2664 5.07056C29.2664 5.94575 29.2664 6.82442 29.2664 7.69961V7.69613ZM17.8504 7.68919C17.8326 9.96399 19.759 11.8706 22.1234 11.8672C24.4771 11.8672 26.4035 10.0022 26.4107 7.69961C26.4178 5.35187 24.4593 3.52162 22.1377 3.51814C19.7946 3.51814 17.8504 5.36924 17.8504 7.68919Z" fill="#2E3144"/>
|
||||
<path d="M46.4083 7.6892C46.4083 6.8279 46.4083 5.96313 46.4083 5.10183C46.4083 4.15718 46.8214 3.44522 47.6618 2.96942C49.2214 2.09076 50.781 1.20862 52.3442 0.336907C53.1419 -0.107634 53.9537 -0.111107 54.7514 0.336907C56.3395 1.22252 57.9205 2.11854 59.5051 3.0111C59.6618 3.09792 59.8042 3.20558 59.9395 3.32019C60.0178 3.38618 60.0392 3.42785 59.9324 3.49731C59.0279 4.10508 58.127 4.7198 57.2297 5.33799C57.1193 5.4144 57.0908 5.35188 57.041 5.2859C56.2932 4.28221 55.2891 3.70917 54.0285 3.54594C52.1698 3.30977 50.222 4.44891 49.5632 6.16109C48.7442 8.28655 49.6701 10.5162 51.7674 11.4956C53.5585 12.3291 55.7983 11.763 56.9911 10.1689C57.0908 10.0369 57.1478 10.0196 57.2867 10.1168C58.1342 10.7107 58.9888 11.2941 59.8469 11.8741C60.0072 11.9818 59.9929 12.0408 59.854 12.1416C59.7259 12.2318 59.5977 12.3256 59.4624 12.402C57.9063 13.2772 56.3537 14.1524 54.7977 15.0276C53.9644 15.4964 53.1348 15.4999 52.3015 15.0276C50.7597 14.1594 49.2143 13.2911 47.6724 12.4229C46.825 11.9471 46.4048 11.2316 46.4048 10.28C46.4048 9.41874 46.4048 8.55397 46.4048 7.69267L46.4083 7.6892Z" fill="#2E3144"/>
|
||||
<svg width="76" height="38" viewBox="0 0 76 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1007_3463)">
|
||||
<path d="M21.8536 12.3231C22.9134 12.3231 23.8351 12.2642 24.7464 12.3364C26.3737 12.4645 27.2042 11.1349 27.26 9.99516C27.2997 9.18825 27.3276 8.3725 27.26 7.56853C27.1792 6.60407 27.8745 6.18736 28.4771 5.76623C28.9931 5.40548 29.4811 5.73237 29.4664 6.36847C29.4223 8.29888 29.7472 10.2632 28.9137 12.117C31.0069 12.6471 32.1872 11.7842 32.2505 9.72128C32.2696 9.09696 32.3225 8.46085 32.2343 7.8483C32.0946 6.87206 32.6958 6.33461 33.3294 5.84722C33.9203 5.39223 34.4789 5.68966 34.4818 6.42001C34.4877 7.59651 34.4656 8.77301 34.4377 9.94952C34.4186 10.7211 34.2187 11.4617 33.8821 12.1744C35.355 12.435 36.378 11.5972 36.5089 10.0688C36.5647 9.41059 36.6132 8.73031 36.5059 8.08684C36.3295 7.02225 36.9778 6.46271 37.648 5.88403C37.9229 5.64696 38.2581 5.60132 38.5741 5.74856C38.8622 5.88403 38.7608 6.19619 38.752 6.43621C38.7005 7.86597 38.8872 9.30163 38.5991 10.727C38.4977 11.2306 38.2728 11.6914 38.1596 12.1921C39.4355 12.3923 40.4012 11.7003 40.5526 10.403C40.6541 9.53134 40.6746 8.6346 40.6026 7.76143C40.5203 6.77193 41.1494 6.26834 41.7962 5.81188C42.3739 5.40401 42.8663 5.70292 42.8619 6.40528C42.8546 7.76437 42.9619 9.12641 42.7664 10.4811C42.4209 12.8827 40.757 14.4067 38.361 14.414C31.6037 14.4332 24.8479 14.4258 18.0906 14.414C16.2781 14.4111 14.967 13.5335 14.0277 12.011C13.4809 11.1231 13.644 10.1616 13.6778 9.21917C13.6925 8.80688 14.3481 8.72442 14.7185 9.11315C15.1492 9.56373 15.4579 10.085 15.6696 10.699C16.1091 11.9742 17.2586 12.5322 18.5712 12.2171C19.4988 11.9933 20.3646 10.8801 20.3293 9.95688C20.2925 9.00861 19.2636 7.78351 18.3287 7.75554C16.459 7.69958 14.5833 7.70547 12.7136 7.75848C11.8728 7.78204 11.2392 8.55362 11.1878 9.49747C11.1422 10.3412 11.1672 11.1893 11.1687 12.0345C11.1687 12.5146 11.1363 12.9887 11.0261 13.4569C10.6557 15.034 9.33859 16.1368 7.71725 16.1795C6.68977 16.206 5.66082 16.2075 4.63333 16.1781C2.38874 16.1118 0.483709 14.3021 0.376404 12.061C0.299968 10.484 0.339656 8.89965 0.348475 7.31969C0.351415 6.74984 1.17311 5.74709 1.69934 5.60426C2.1903 5.47027 2.41814 5.64107 2.42255 6.21092C2.43284 7.90279 2.43872 9.59465 2.41226 11.2865C2.40197 11.9418 2.58571 12.5204 2.92527 13.0564C3.36478 13.75 3.99391 14.1667 4.82589 14.1961C5.55939 14.2211 6.29436 14.2197 7.02933 14.2005C8.33757 14.1667 9.02991 13.4496 9.05196 12.1111C9.06518 11.3763 9.02991 10.6401 9.05048 9.90387C9.06371 9.42826 9.1034 8.94824 9.19454 8.48146C9.49734 6.93096 10.763 5.77801 12.3417 5.72942C14.2849 5.66905 16.2341 5.64696 18.1758 5.72206C20.2822 5.80452 21.9859 7.35944 22.4034 9.42973C22.5151 9.98044 22.4548 10.5282 22.2814 11.0612C22.1579 11.4382 22.0271 11.8137 21.8521 12.3276L21.8536 12.3231Z" fill="#3F415A"/>
|
||||
<path d="M54.2581 32.2824C57.7801 32.2824 61.1565 32.3074 64.5315 32.2706C65.9 32.2559 66.8084 31.2958 66.8951 29.8587C66.9466 29.0179 66.8834 28.1698 66.8775 27.3261C66.8731 26.6208 67.5904 25.7211 68.2739 25.5665C68.787 25.4501 69.006 25.6077 69.0045 26.1775C68.9986 27.6853 68.956 29.1917 68.9472 30.6995C68.9442 31.2296 68.5371 31.6404 68.5503 32.2397C69.6454 32.3354 70.7405 32.3722 71.8194 32.2485C72.9395 32.1204 73.7715 31.1368 73.8332 29.9588C73.8803 29.0783 73.8421 28.1948 73.8523 27.3113C73.8597 26.6001 74.4668 25.7932 75.1327 25.5885C75.6795 25.4207 75.9779 25.6077 75.9837 26.182C75.997 27.5425 76.0249 28.9045 75.9632 30.2636C75.9117 31.3989 75.3958 32.3972 74.6696 33.2439C74.1096 33.8962 73.4011 34.2614 72.4559 34.2584C63.9744 34.2305 55.4943 34.2437 47.0128 34.2364C46.7115 34.2364 46.2984 34.4042 46.1323 34.0258C45.9368 33.5811 46.2617 33.2896 46.566 32.9965C47.0613 32.518 47.5729 32.2058 48.3167 32.2809C49.0428 32.3545 49.7836 32.3045 50.5171 32.2839C51.258 32.2633 51.8607 31.9349 52.3134 31.3533C52.8411 30.6774 53.1277 29.9456 52.7029 29.1063C52.2781 28.2684 51.6372 27.6912 50.6612 27.6456C49.8557 27.6088 49.0457 27.6485 48.2387 27.6397C48.0021 27.6368 47.6875 27.7457 47.5832 27.4174C47.4935 27.1361 47.467 26.7989 47.6934 26.581C48.2123 26.0848 48.665 25.4501 49.5396 25.5724C49.8645 25.618 50.1996 25.5782 50.5304 25.5812C53.8495 25.6047 55.803 28.6586 54.4683 31.7361C54.411 31.8672 54.3654 32.0012 54.2581 32.2824Z" fill="#3F415A"/>
|
||||
<path d="M21.6786 35.94C19.0724 35.94 16.4677 35.9445 13.8615 35.94C11.6581 35.9356 10.0235 34.5633 9.64575 32.387C9.56491 31.9202 9.50317 31.4328 9.54139 30.9631C9.68103 29.202 9.67662 27.4336 9.59871 25.6784C9.55462 24.6712 10.0897 24.1927 10.7585 23.7392C11.2891 23.3784 11.6522 23.5875 11.6566 24.2354C11.6684 26.073 11.6522 27.9107 11.6566 29.7483C11.6581 30.3726 11.6713 30.997 11.711 31.6198C11.7463 32.1808 11.9638 32.6829 12.2946 33.1409C12.7532 33.777 13.3044 34.098 14.1423 34.0906C19.1327 34.0523 24.1231 34.0568 29.1136 34.0685C30.2983 34.0715 31.1612 33.6283 31.661 32.5224C32.1063 31.5359 31.9138 30.6539 31.245 29.8602C30.6291 29.1284 29.5678 28.8251 28.7681 29.1093C27.6951 29.4892 27.0836 30.2975 27.0307 31.4093C26.991 32.2353 26.1796 33.1394 25.4284 33.1747C25.1844 33.1865 24.8508 33.2336 24.8405 32.8611C24.8037 31.5035 24.7229 30.1518 25.6019 28.9488C27.0733 26.9344 29.7471 26.3896 31.8594 27.7281C33.9467 29.0504 34.6332 31.7877 33.3631 33.9242C32.5782 35.245 31.392 35.9769 29.825 35.9827C27.11 35.9931 24.3936 35.9857 21.6786 35.9857C21.6786 35.971 21.6786 35.9562 21.6786 35.9415V35.94Z" fill="#3F415A"/>
|
||||
<path d="M54.8078 8.86716C54.949 8.48873 55.0621 8.09705 55.2371 7.7363C55.8265 6.52151 56.7467 5.76614 58.149 5.72785C59.5807 5.68957 60.5876 6.40372 61.3152 7.56991C61.6519 8.10736 61.8077 8.71844 61.8459 9.35012C61.8959 10.1938 61.962 11.039 61.9503 11.8828C61.9429 12.4349 62.159 12.5837 62.6661 12.5586C63.3261 12.5262 63.9905 12.5896 64.6476 12.5424C66.0455 12.4423 66.9319 11.497 66.9686 10.0952C66.9907 9.25 66.9657 8.40333 66.9774 7.55813C66.9877 6.90583 67.7227 5.92075 68.3254 5.71607C68.8134 5.55116 69.0956 5.72932 69.1029 6.26825C69.1279 7.88502 69.125 9.50179 68.9912 11.1171C68.9515 11.5956 68.5899 11.9785 68.6046 12.5513C69.613 12.5513 70.5993 12.6116 71.5768 12.5365C72.9468 12.432 73.8156 11.4425 73.8626 10.0319C73.8905 9.18668 73.8655 8.34001 73.8817 7.49481C73.8935 6.87491 74.5961 5.92664 75.1605 5.73227C75.7044 5.54527 75.9719 5.72638 75.9749 6.34187C75.9793 7.88502 76.0631 9.437 75.8455 10.9684C75.6574 12.2906 75.0209 13.3803 73.8979 14.1607C73.5701 14.3889 73.2246 14.4198 72.8542 14.4198C68.7046 14.4257 64.5564 14.4228 60.4083 14.4493C59.8247 14.4537 59.6689 14.2741 59.691 13.6954C59.7409 12.3363 59.7189 10.9743 59.7115 9.6137C59.7101 9.28534 59.7013 8.94667 59.6219 8.63156C59.4705 8.03816 58.9839 7.63912 58.4562 7.60819C57.9329 7.57727 57.2715 8.01165 57.1009 8.56383C56.9378 9.09097 56.8981 9.6402 56.9025 10.1983C56.9157 11.9638 56.8922 13.7278 56.9143 15.4933C56.9201 15.9968 56.7937 16.2604 56.2425 16.2059C55.9515 16.1765 55.6516 16.175 55.3635 16.2148C54.7843 16.2928 54.6888 15.9321 54.6535 15.4933C54.48 13.2801 54.3757 11.067 54.8078 8.86863V8.86716Z" fill="#3F415A"/>
|
||||
<path d="M0.0367746 31.7038C0.0367746 30.5641 0.0191354 29.4244 0.0397144 28.2862C0.111741 24.2369 4.32604 22.6864 7.06894 24.3738C8.68734 25.3692 9.5546 27.3467 9.11362 29.2109C8.63442 31.2385 7.10863 32.5887 5.11099 32.7271C4.5627 32.7654 4.01001 32.7463 3.46025 32.7419C3.25005 32.7404 2.97811 32.8081 2.87081 32.5755C2.74439 32.3031 2.73116 31.9673 2.96929 31.7524C3.47054 31.2988 3.86007 30.6848 4.69647 30.7305C5.43731 30.7717 6.03557 30.3844 6.4839 29.8028C7.60105 28.3539 7.18065 26.6635 5.51669 25.8802C4.04969 25.1896 2.26813 26.3675 2.17259 28.1271C2.11967 29.1166 2.15054 30.112 2.14907 31.103C2.1476 32.5357 2.13878 33.9699 2.15789 35.4026C2.16377 35.8576 2.03148 36.1079 1.53905 36.0579C1.46702 36.0505 1.39205 36.0579 1.31856 36.0579C-0.29984 36.0829 0.0353046 36.0814 0.0147256 34.7886C-0.00144371 33.7593 0.0117857 32.7301 0.0117857 31.7023H0.0367746V31.7038Z" fill="#3F415A"/>
|
||||
<path d="M53.5321 11.8151C53.5321 12.9916 53.538 14.1681 53.5306 15.3446C53.5247 16.156 53.5159 16.1692 52.6663 16.1707C51.3272 16.1722 51.4683 16.3032 51.4609 15.0207C51.4477 12.8517 51.4271 10.6828 51.4418 8.51532C51.4477 7.60239 51.1243 6.87058 50.454 6.27717C49.6809 5.59542 48.5284 5.51885 47.7038 6.08575C46.7116 6.7675 46.253 7.98082 46.6601 8.96001C47.1026 10.0217 47.8655 10.6916 49.0576 10.7947C49.7749 10.8565 50.4599 11.4382 50.5893 12.0978C50.6569 12.438 50.6613 12.787 50.1645 12.8032C48.9841 12.84 47.817 12.8105 46.7336 12.2377C45.005 11.3248 44.1598 9.49304 44.5082 7.38299C44.8066 5.57333 46.3823 4.03755 48.2021 3.78134C51.0846 3.37493 53.4307 5.35541 53.5071 8.28415C53.538 9.45918 53.5115 10.6372 53.5115 11.8137C53.5189 11.8137 53.5262 11.8137 53.5336 11.8137L53.5321 11.8151Z" fill="#3F415A"/>
|
||||
<path d="M36.9746 29.9427C36.9746 31.7435 36.9614 33.5443 36.9834 35.3452C36.9893 35.8473 36.8585 36.0711 36.3102 36.0696C34.2479 36.0637 34.6506 36.2316 34.6359 34.4882C34.6109 31.5845 34.6256 28.6808 34.6242 25.7771C34.6242 24.8686 35.2166 23.963 36.0191 23.642C36.6218 23.4005 36.9761 23.6361 36.9805 24.3178C36.9908 26.1923 36.9849 28.0668 36.9849 29.9412H36.9775L36.9746 29.9427Z" fill="#3F415A"/>
|
||||
<path d="M46.0442 29.1549C46.0442 30.0722 46.0706 30.9925 46.0383 31.9084C45.9854 33.3588 45.3371 34.5132 44.1774 35.3687C43.6717 35.7412 43.1161 36.0372 42.4664 36.046C41.4389 36.0607 40.4129 36.0578 39.3854 36.0534C39.1796 36.0534 38.9238 36.1079 38.84 35.8207C38.7724 35.5896 38.8474 35.3834 39.0252 35.2259C39.6191 34.6972 40.1321 34.1421 41.0817 34.1451C43.0191 34.1509 43.8569 33.2174 43.8672 31.2723C43.8731 30.0604 43.9495 28.8398 43.8422 27.6367C43.7467 26.5663 44.4346 26.1054 45.1299 25.6666C45.6958 25.3088 46.078 25.593 46.0883 26.288C46.103 27.2436 46.0927 28.1978 46.0927 29.1534C46.078 29.1534 46.0633 29.1534 46.0471 29.1534L46.0442 29.1549Z" fill="#3F415A"/>
|
||||
<path d="M36.6439 3.77984C35.6885 3.77984 34.733 3.79603 33.779 3.77542C32.9191 3.75628 32.8706 3.6856 32.8677 2.79476C32.8657 2.14981 33.1847 1.82096 33.8246 1.8082C34.0084 1.80526 34.195 1.78317 34.3758 1.80673C35.7693 1.98048 36.9747 1.73163 37.8375 0.4712C38.1198 0.0589084 39.6911 -0.126623 40.1086 0.0942477C40.2967 0.194376 40.3276 0.371072 40.3291 0.559548C40.3364 1.44303 40.3394 2.32504 40.3511 3.20852C40.357 3.64142 40.1262 3.77984 39.7293 3.77689C38.7004 3.76806 37.6729 3.77395 36.6439 3.77395V3.77984Z" fill="#3F415A"/>
|
||||
<path d="M67.739 17.8287C67.9345 17.2868 67.4729 16.6478 67.8668 16.2532C68.2564 15.86 68.9164 16.1044 69.4558 16.0971C71.0301 16.0779 72.6059 16.0838 74.1802 16.0941C75.1886 16.1 75.193 16.1103 75.2003 17.1528C75.2048 17.8125 75.2003 18.4736 75.1886 19.1333C75.1856 19.3512 75.1739 19.6472 74.9078 19.6516C74.1008 19.6678 73.1939 20.0551 72.5677 19.204C71.7725 18.1217 70.7376 17.7492 69.4235 17.9288C68.8987 17.9995 68.3358 18.0319 67.739 17.8302V17.8287Z" fill="#3F415A"/>
|
||||
<path d="M64.0935 3.77691C63.4335 3.77691 62.772 3.76365 62.112 3.78132C61.6534 3.7931 61.4167 3.6223 61.455 3.1408C61.4638 3.03184 61.4491 2.91993 61.4476 2.80949C61.4285 1.84944 61.4461 1.82 62.4104 1.80969C63.5849 1.79644 64.7594 1.79791 65.9338 1.80969C66.7114 1.81852 66.7247 1.84503 66.7555 2.61366C66.7614 2.7609 66.7482 2.90815 66.7614 3.05392C66.807 3.55898 66.6144 3.80341 66.0764 3.78132C65.4164 3.75482 64.755 3.77543 64.095 3.77396L64.0935 3.77691Z" fill="#3F415A"/>
|
||||
<path d="M23.9761 17.9435C23.3161 17.9435 22.6561 17.9243 21.9975 17.9494C21.5213 17.967 21.3228 17.7697 21.3522 17.3029C21.364 17.1204 21.361 16.9348 21.3463 16.7522C21.3067 16.2811 21.5272 16.0837 21.9858 16.0852C23.341 16.0911 24.6963 16.0896 26.0531 16.0852C26.4647 16.0837 26.6587 16.2752 26.6469 16.686C26.641 16.8686 26.6337 17.0541 26.6469 17.2367C26.6851 17.7417 26.4867 17.9773 25.9546 17.9538C25.2961 17.9258 24.6361 17.9464 23.9761 17.9464V17.9435Z" fill="#3F415A"/>
|
||||
<path d="M64.7551 23.6361C64.0622 23.6371 63.7134 23.3352 63.7085 22.7306C63.7011 21.7263 63.7702 21.663 64.8682 21.6689C65.7987 21.6733 65.8457 21.7234 65.8531 22.6761C65.858 23.3141 65.492 23.6342 64.7551 23.6361Z" fill="#3F415A"/>
|
||||
<path d="M72.0825 37.0355C72.0825 37.6746 71.7552 37.9961 71.1006 38C70.0481 38.0059 70.0467 38.0044 70.0496 37.0031C70.0525 36.0446 70.0628 36.0343 71.0256 36.0328C72.0678 36.0313 72.084 36.046 72.084 37.037L72.0825 37.0355Z" fill="#3F415A"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2855_3816">
|
||||
<rect width="60" height="16" fill="white"/>
|
||||
<clipPath id="clip0_1007_3463">
|
||||
<rect width="76" height="38" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 12 KiB |
|
|
@ -1,53 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 157.92 68.92">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #40415a;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-1" d="M42.01,27.03h-13.48v19.64h4.26v-4.96h9.22c2.63,0,4.76-2.14,4.76-4.76v-5.16c0-2.62-2.14-4.76-4.76-4.76ZM42.51,36.95c0,.28-.23.51-.51.51h-9.22v-6.17h9.22c.28,0,.51.23.51.51v5.16Z"/>
|
||||
<path class="cls-1" d="M21.74,27.03h-8.72c-2.62,0-4.76,2.14-4.76,4.76v14.88h4.26v-4.96h9.73v4.96h4.26v-14.88c0-2.62-2.14-4.76-4.76-4.76ZM22.25,37.46h-9.73v-5.67c0-.28.23-.51.51-.51h8.72c.28,0,.51.23.51.51v5.67Z"/>
|
||||
<path class="cls-1" d="M62.28,27.03h-8.72c-2.62,0-4.76,2.14-4.76,4.76v14.88h4.26v-4.96h9.73v4.96h4.26v-14.88c0-2.62-2.14-4.76-4.76-4.76ZM62.78,37.46h-9.73v-5.67c0-.28.23-.51.51-.51h8.72c.28,0,.5.23.5.51v5.67Z"/>
|
||||
<path class="cls-1" d="M102.81,27.03h-8.72c-2.63,0-4.76,2.14-4.76,4.76v14.88h4.26v-4.96h9.73v4.96h4.26v-14.88c0-2.62-2.14-4.76-4.76-4.76ZM103.32,37.46h-9.73v-5.67c0-.28.23-.51.51-.51h8.72c.28,0,.5.23.5.51v5.67Z"/>
|
||||
<path class="cls-1" d="M143.35,27.03h-8.72c-2.63,0-4.76,2.14-4.76,4.76v14.88h4.26v-4.96h9.73v4.96h4.26v-14.88c0-2.62-2.14-4.76-4.76-4.76ZM143.86,37.46h-9.73v-5.67c0-.28.23-.51.51-.51h8.72c.28,0,.51.23.51.51v5.67Z"/>
|
||||
<path class="cls-1" d="M82.03,27.03h-12.46v19.64h12.46c2.63,0,4.76-2.14,4.76-4.76v-10.11c0-2.63-2.14-4.76-4.76-4.76ZM82.53,41.91c0,.28-.22.5-.5.5h-8.19v-11.12h8.19c.28,0,.5.22.5.5v10.11Z"/>
|
||||
<polygon class="cls-1" points="123.14 39.14 114.33 27.03 114.31 27.05 114.31 27.03 110.04 27.03 110.04 46.67 114.31 46.67 114.31 34.53 123.14 46.67 127.41 46.67 127.41 27.03 123.14 27.03 123.14 39.14"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-1" d="M74.98,7.74h-5.59c-1.68,0-3.05,1.37-3.05,3.05v9.55h2.73v-3.18h6.24v3.18h2.73v-9.55c0-1.68-1.37-3.05-3.05-3.05ZM75.3,14.43h-6.24v-3.64c0-.18.15-.32.32-.32h5.59c.18,0,.32.15.32.32v3.64Z"/>
|
||||
<polygon class="cls-1" points="88.55 15.51 82.9 7.74 82.88 7.75 82.88 7.74 80.14 7.74 80.14 20.34 82.88 20.34 82.88 12.55 88.55 20.34 91.29 20.34 91.29 7.74 88.55 7.74 88.55 15.51"/>
|
||||
<path class="cls-1" d="M16.91,7.74h-8.65v12.6h2.73v-3.18h5.92c1.68,0,3.05-1.37,3.05-3.05v-3.31c0-1.68-1.37-3.05-3.05-3.05ZM17.24,14.1c0,.18-.15.32-.32.32h-5.92v-3.96h5.92c.18,0,.32.15.32.32v3.31Z"/>
|
||||
<path class="cls-1" d="M43.58,7.74h-8.65v12.6h2.73v-3.65h5.91c.18,0,.32.15.32.32v3.33h2.73v-3.33c0-.62-.18-1.19-.52-1.68.32-.48.52-1.08.52-1.7v-2.84c0-1.69-1.37-3.05-3.05-3.05ZM43.9,13.64c0,.18-.14.32-.32.32h-5.91v-3.49h5.91c.18,0,.32.14.32.32v2.84Z"/>
|
||||
<polygon class="cls-1" points="140.08 10.47 140.08 12.68 146.75 12.68 146.75 15.41 140.08 15.41 140.08 20.34 137.34 20.34 137.34 7.75 148.11 7.75 148.11 10.47 140.08 10.47"/>
|
||||
<polygon class="cls-1" points="21.74 7.74 21.74 20.34 32.5 20.34 32.5 17.61 24.48 17.61 24.48 15.41 31.14 15.41 31.14 12.67 24.48 12.67 24.48 10.47 32.5 10.47 32.5 7.74 24.48 7.74 21.74 7.74"/>
|
||||
<polygon class="cls-1" points="127.54 7.74 124.79 7.74 124.79 20.34 135.56 20.34 135.56 17.61 127.54 17.61 127.54 7.74"/>
|
||||
<rect class="cls-1" x="61.62" y="7.65" width="2.75" height="12.69"/>
|
||||
<path class="cls-1" d="M56.4,20.34h-7.45v-2.7h7.45c.08,0,.14-.06.14-.14v-1.99s-.13-.14-.13-.14h-5.16c-1.56,0-2.84-1.27-2.84-2.84v-1.99c0-.76.29-1.47.83-2.01.54-.54,1.25-.84,2.01-.84h7.42v2.7h-7.42l-.14.14v1.99c0,.08.07.14.14.14h5.16c.76,0,1.47.3,2.01.84.54.54.83,1.25.83,2.01v1.99c0,1.56-1.28,2.84-2.85,2.84Z"/>
|
||||
<path class="cls-1" d="M106.1,20.34h-5c-1.76,0-3.19-1.43-3.19-3.19v-6.26c0-1.76,1.43-3.19,3.19-3.19h7.16v2.7h-7.16c-.27,0-.49.22-.49.49v6.26c0,.27.22.49.49.49h5c.12,0,.22-.1.22-.22v-1.83h-2.56v-2.7h5.27v4.54c0,1.61-1.31,2.92-2.92,2.92Z"/>
|
||||
<path class="cls-1" d="M119.03,20.34h-4.97c-1.76,0-3.19-1.43-3.19-3.19V7.72h2.7v9.43c0,.27.22.49.49.49h4.97c.27,0,.49-.22.49-.49V7.72h2.7v9.43c0,1.76-1.43,3.19-3.19,3.19Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-1" d="M14.9,53.53h-6.22v9.18h1.92v-2.33h4.3c1.2,0,2.18-.99,2.18-2.21v-2.42c0-1.22-.98-2.21-2.18-2.21ZM15.16,58.16c0,.14-.12.26-.26.26h-4.3v-2.95h4.3c.14,0,.26.12.26.26v2.42Z"/>
|
||||
<path class="cls-1" d="M43.32,53.53h-6.22v9.18h1.93v-2.67h4.29c.14,0,.26.12.26.26v2.41h1.93v-2.41c0-.44-.12-.85-.37-1.21h0s0-.03,0-.03c.24-.36.37-.8.37-1.23v-2.08c0-1.22-.98-2.21-2.18-2.21ZM43.58,57.82c0,.15-.11.26-.26.26h-4.29v-2.6h4.29c.15,0,.26.11.26.26v2.08Z"/>
|
||||
<path class="cls-1" d="M111.47,53.53h-4.04c-1.2,0-2.18.99-2.18,2.21v6.97h1.92v-2.33h4.55v2.33h1.93v-6.97c0-1.22-.98-2.21-2.18-2.21ZM111.73,58.43h-4.55v-2.69c0-.14.12-.26.26-.26h4.04c.14,0,.26.12.26.26v2.69Z"/>
|
||||
<path class="cls-1" d="M52.18,53.53h-3.56c-1.2,0-2.18,1-2.18,2.22v4.75c0,1.22.98,2.21,2.18,2.21h3.56c1.2,0,2.18-.99,2.18-2.21v-4.75c0-1.22-.98-2.22-2.18-2.22ZM52.44,60.5c0,.15-.11.26-.26.26h-3.56c-.14,0-.26-.12-.26-.26v-4.75c0-.14.11-.26.26-.26h3.56c.15,0,.26.11.26.26v4.75Z"/>
|
||||
<path class="cls-1" d="M55.57,55.75v4.75c0,1.22.98,2.21,2.18,2.21h5.75v-1.95h-5.75c-.14,0-.26-.12-.26-.26v-4.75c0-.14.11-.26.26-.26h5.75v-1.96h-5.75c-1.2,0-2.18,1-2.18,2.22Z"/>
|
||||
<path class="cls-1" d="M95.94,55.75v4.75c0,1.22.98,2.21,2.18,2.21h5.75v-1.95h-5.75c-.14,0-.26-.12-.26-.26v-4.75c0-.14.12-.26.26-.26h5.75v-1.96h-5.75c-1.2,0-2.18,1-2.18,2.22Z"/>
|
||||
<polygon class="cls-1" points="18.38 62.71 26.1 62.71 26.1 60.76 20.31 60.76 20.31 59.09 25.12 59.09 25.12 57.14 20.31 57.14 20.31 55.48 26.1 55.48 26.1 53.53 18.38 53.53 18.38 62.71"/>
|
||||
<polygon class="cls-1" points="73.97 62.71 81.69 62.71 81.69 60.76 75.9 60.76 75.9 59.09 80.71 59.09 80.71 57.14 75.9 57.14 75.9 55.48 81.69 55.48 81.69 53.53 73.97 53.53 73.97 62.71"/>
|
||||
<polygon class="cls-1" points="117.26 53.53 115.33 53.53 115.33 62.71 123.06 62.71 123.06 60.76 117.26 60.76 117.26 53.53"/>
|
||||
<polygon class="cls-1" points="27.31 55.49 30.54 55.49 30.54 62.71 32.48 62.71 32.48 55.49 35.71 55.49 35.71 53.53 27.31 53.53 27.31 55.49"/>
|
||||
<polygon class="cls-1" points="70.72 57.1 66.65 57.1 66.65 53.46 64.72 53.46 64.72 62.71 66.65 62.71 66.65 59.07 70.72 59.07 70.72 62.71 72.65 62.71 72.65 53.46 70.72 53.46 70.72 57.1"/>
|
||||
<rect class="cls-1" x="92.42" y="53.46" width="1.94" height="9.24"/>
|
||||
<polygon class="cls-1" points="89.01 53.86 87.01 57.37 84.98 53.46 83.04 53.46 83.04 62.71 84.97 62.71 84.97 57.63 86.5 60.59 87.47 60.59 89.05 57.7 89.03 62.71 90.97 62.71 90.97 53.46 89.03 53.46 89.01 53.86"/>
|
||||
<path class="cls-1" d="M142.67,53.53h-3.56c-1.2,0-2.18,1-2.18,2.22v4.75c0,1.22.98,2.21,2.18,2.21h3.56c1.2,0,2.18-.99,2.18-2.21v-4.75c0-1.22-.98-2.22-2.18-2.22ZM142.92,60.5c0,.15-.11.26-.26.26h-3.56c-.14,0-.26-.12-.26-.26v-4.75c0-.14.11-.26.26-.26h3.56c.15,0,.26.11.26.26v4.75Z"/>
|
||||
<path class="cls-1" d="M127.59,55.75v4.75c0,1.22.98,2.21,2.18,2.21h5.75v-1.95h-5.75c-.14,0-.26-.12-.26-.26v-4.75c0-.14.12-.26.26-.26h5.75v-1.96h-5.75c-1.2,0-2.18,1-2.18,2.22Z"/>
|
||||
<rect class="cls-1" x="146.18" y="60.75" width="1.93" height="1.96"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |