Compare commits
13 Commits
86f5622bdd
...
19b3409572
| Author | SHA1 | Date | |
|---|---|---|---|
| 19b3409572 | |||
| 2c393eddf0 | |||
|
|
ec6235f00c | ||
|
|
73f960b56a | ||
|
|
49f018e56f | ||
|
|
8df1fbc422 | ||
|
|
957b05cdbd | ||
| b53051b77f | |||
| 31a344e3a1 | |||
|
|
41e2787601 | ||
| 12e85fdb08 | |||
|
|
28f22dd0d3 | ||
|
|
69cea7a06c |
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React from "react";
|
||||||
import * as d3 from "d3";
|
|
||||||
import { formatNumber } from "~/lib/utils";
|
import { formatNumber } from "~/lib/utils";
|
||||||
|
|
||||||
export type CompanyInfo = {
|
export type CompanyInfo = {
|
||||||
|
|
@ -17,141 +16,162 @@ export type D3ImageInfoProps = {
|
||||||
height?: number;
|
height?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function D3ImageInfo({ companies, width = 900, height = 400 }: D3ImageInfoProps) {
|
const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
|
||||||
|
|
||||||
const draw = () => {
|
|
||||||
if (!containerRef.current || !svgRef.current) return;
|
|
||||||
|
|
||||||
const container = containerRef.current;
|
|
||||||
const svg = d3.select(svgRef.current);
|
|
||||||
|
|
||||||
const W = Math.max(480, container.clientWidth || width);
|
|
||||||
const H = Math.max(300, height);
|
|
||||||
|
|
||||||
svg.attr("width", W).attr("height", H);
|
|
||||||
|
|
||||||
svg.selectAll("*").remove();
|
|
||||||
|
|
||||||
const padding = 10;
|
|
||||||
const cols = 3;
|
|
||||||
const rows = 2;
|
|
||||||
const boxWidth = (W - padding * (cols + 1)) / cols;
|
|
||||||
const boxHeight = (H - padding * (rows + 1)) / rows;
|
|
||||||
|
|
||||||
const group = svg.append("g").attr("transform", `translate(${padding}, ${padding})`);
|
|
||||||
|
|
||||||
companies.forEach((company, i) => {
|
|
||||||
const col = i % cols;
|
|
||||||
const row = Math.floor(i / cols);
|
|
||||||
const x = col * (boxWidth + padding);
|
|
||||||
const y = row * (boxHeight + padding);
|
|
||||||
|
|
||||||
const companyGroup = group.append("g").attr("transform", `translate(${x}, ${y})`);
|
|
||||||
|
|
||||||
// Draw background box
|
|
||||||
companyGroup
|
|
||||||
.append("rect")
|
|
||||||
.attr("width", boxWidth)
|
|
||||||
.attr("height", boxHeight)
|
|
||||||
.attr("rx", 10)
|
|
||||||
.attr("ry", 10)
|
|
||||||
.attr("fill", "transparent")
|
|
||||||
|
|
||||||
// Draw image
|
|
||||||
const imgSize = Math.min(boxWidth, boxHeight) * 0.5;
|
|
||||||
companyGroup
|
|
||||||
.append("image")
|
|
||||||
.attr("href", company.imageUrl)
|
|
||||||
.attr("x", 10)
|
|
||||||
.attr("y", 10)
|
|
||||||
.attr("width", imgSize)
|
|
||||||
.attr("height", imgSize)
|
|
||||||
.attr("preserveAspectRatio", "xMidYMid slice")
|
|
||||||
.style("background", "transparent");
|
|
||||||
|
|
||||||
// Adjust positions to match picture
|
|
||||||
// Position image slightly left and info box to right with spacing
|
|
||||||
const infoX = imgSize + 30;
|
|
||||||
const infoY = 10;
|
|
||||||
const infoWidth = 120;
|
|
||||||
const infoHeight = imgSize;
|
|
||||||
|
|
||||||
const infoGroup = companyGroup.append("g");
|
|
||||||
|
|
||||||
infoGroup
|
|
||||||
.append("rect")
|
|
||||||
.attr("x", infoX)
|
|
||||||
.attr("y", infoY)
|
|
||||||
.attr("width", infoWidth)
|
|
||||||
.attr("height", infoHeight)
|
|
||||||
.attr("rx", 10)
|
|
||||||
.attr("ry", 10)
|
|
||||||
.attr("fill", "transparent")
|
|
||||||
.attr("stroke", "#3F415A")
|
|
||||||
.attr("stroke-width", 1);
|
|
||||||
|
|
||||||
// Add text inside info box
|
|
||||||
const lineHeight = 20;
|
|
||||||
infoGroup
|
|
||||||
.append("text")
|
|
||||||
.attr("x", imgSize + 10 )
|
|
||||||
.attr("y", infoY + imgSize + 10)
|
|
||||||
.attr("fill", "#FFFFFF")
|
|
||||||
.attr("font-weight", "700")
|
|
||||||
.attr("font-size", 14)
|
|
||||||
.text(company.name);
|
|
||||||
|
|
||||||
infoGroup
|
|
||||||
.append("text")
|
|
||||||
.attr("x", infoX + imgSize)
|
|
||||||
.attr("y", infoY + lineHeight )
|
|
||||||
.attr("fill", "#FFFFFF")
|
|
||||||
.attr("font-size", 12)
|
|
||||||
.text(`درآمد: ${formatNumber(company?.revenue || "0")}`);
|
|
||||||
infoGroup
|
|
||||||
.append("text")
|
|
||||||
.attr("x", infoX + imgSize -20 )
|
|
||||||
.attr("y", infoY + lineHeight +5 )
|
|
||||||
.attr("fill", "#ACACAC")
|
|
||||||
.attr("font-size", 6)
|
|
||||||
.text(`میلیون ریال`);
|
|
||||||
|
|
||||||
|
|
||||||
infoGroup
|
|
||||||
.append("text")
|
|
||||||
.attr("x", infoX + imgSize)
|
|
||||||
.attr("y", infoY + lineHeight *2 )
|
|
||||||
.attr("fill", "#FFFFFF")
|
|
||||||
.attr("font-size", 12)
|
|
||||||
.text(`هزینه: ${formatNumber(company?.cost || "0")} میلیون ریال`);
|
|
||||||
|
|
||||||
infoGroup
|
|
||||||
.append("text")
|
|
||||||
.attr("x", infoX + imgSize)
|
|
||||||
.attr("y", infoY + lineHeight * 3 )
|
|
||||||
.attr("fill", "#FFFFFF")
|
|
||||||
.attr("font-size", 12)
|
|
||||||
.text(`ظرفیت: ${formatNumber(company?.capacity || "0")} تن در سال`);
|
|
||||||
|
|
||||||
// Remove click handlers and popup
|
|
||||||
companyGroup.style("cursor", "default");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
draw();
|
|
||||||
const ro = new ResizeObserver(() => draw());
|
|
||||||
if (containerRef.current) ro.observe(containerRef.current);
|
|
||||||
return () => ro.disconnect();
|
|
||||||
}, [companies, width, height]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className={`info-box`} style={style}>
|
||||||
<div ref={containerRef} className="w-full h-[400px]">
|
<div className="info-box-content">
|
||||||
<svg ref={svgRef} className="block w-full h-full"></svg>
|
<div className="info-row">
|
||||||
|
<div className="info-label">درآمد:</div>
|
||||||
|
<div className="info-value revenue">{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">{formatNumber(company?.cost || 0)}</div>
|
||||||
|
<div className="info-unit">میلیون ریال</div>
|
||||||
|
</div>
|
||||||
|
<div className="info-row">
|
||||||
|
<div className="info-label">ظرفیت:</div>
|
||||||
|
<div className="info-value capacity">{formatNumber(company?.capacity || 0)}</div>
|
||||||
|
<div className="info-unit">تن در سال</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||||
|
// 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 : "بسپاران"}, // 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 p-4">
|
||||||
|
<div dir="ltr" className="company-grid-container">
|
||||||
|
{displayCompanies.map((company, index) => {
|
||||||
|
const gp = gridPositions.find(v => v.name === company.name) || { col: (index % 5) + 1, row: Math.floor(index / 5) + 1 };
|
||||||
|
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 0 ;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.info-box-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
position : relative;
|
||||||
|
margin: 0rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap : 1rem;
|
||||||
|
justify-content : space-between;
|
||||||
|
padding: 0rem .8rem;
|
||||||
|
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: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: #34D399;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: right;
|
||||||
|
margin-bottom : .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value.revenue { color: #fff;}
|
||||||
|
.info-value.cost { color: #fff; }
|
||||||
|
.info-value.capacity { color: #fff; }
|
||||||
|
|
||||||
|
.info-unit {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
bottom: 0;
|
||||||
|
color: #9CA3AF;
|
||||||
|
font-size: 8px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export function DashboardCustomBarChart({
|
||||||
<span className="text-white font-bold text-base">
|
<span className="text-white font-bold text-base">
|
||||||
{formatNumber(item.value)}
|
{formatNumber(item.value)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-white font-persian font-medium text-sm w-max">
|
<span className="text-[#3F415A] font-persian font-medium text-sm w-max">
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -126,19 +126,19 @@ export function DashboardHome() {
|
||||||
let incCapacityTotal = 0;
|
let incCapacityTotal = 0;
|
||||||
const chartRows = rows.map((r) => {
|
const chartRows = rows.map((r) => {
|
||||||
const rel = r?.related_company ?? "-";
|
const rel = r?.related_company ?? "-";
|
||||||
const preFee = Number(r?.pre_innovation_fee_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);
|
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);
|
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);
|
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);
|
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);
|
const incInc = Number(r?.increased_income_after_innovation_sum ?? 0) > 0 ? r?.increased_income_after_innovation_sum : 0;
|
||||||
|
|
||||||
incCapacityTotal += incCap;
|
incCapacityTotal += incCap;
|
||||||
|
|
||||||
const capacityPct = preCap > 0 ? (incCap / preCap) * 100 : 0;
|
const capacityPct = preCap > 0 ? (incCap / preCap) * 100 : 0;
|
||||||
const revenuePct = preInc > 0 ? (incInc / preInc) * 100 : 0;
|
const revenuePct = preInc > 0 ? (incInc / preInc) * 100 : 0;
|
||||||
const costPct = preFee > 0 ? (costRed / preFee) * 100 : 0;
|
const costPct = preFee > 0 ? (costRed / preFee) * 100 : 0;
|
||||||
|
console.log(costRed)
|
||||||
return {
|
return {
|
||||||
category: rel,
|
category: rel,
|
||||||
capacity: isFinite(capacityPct) ? capacityPct : 0,
|
capacity: isFinite(capacityPct) ? capacityPct : 0,
|
||||||
|
|
@ -475,7 +475,7 @@ export function DashboardHome() {
|
||||||
<div className="flex items-center justify-center flex-col">
|
<div className="flex items-center justify-center flex-col">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-5xl font-bold text-green-400">
|
<p className="text-4xl font-bold text-green-400">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.technology_innovation_based_revenue_growth || "0",
|
?.technology_innovation_based_revenue_growth || "0",
|
||||||
|
|
@ -487,7 +487,7 @@ export function DashboardHome() {
|
||||||
</div>
|
</div>
|
||||||
<span className="text-6xl font-thin text-gray-600">/</span>
|
<span className="text-6xl font-thin text-gray-600">/</span>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-5xl font-bold text-green-400">
|
<p className="text-4xl font-bold text-green-400">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
Math.round(
|
Math.round(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
|
|
@ -518,7 +518,7 @@ export function DashboardHome() {
|
||||||
<div className="flex items-center justify-center flex-col">
|
<div className="flex items-center justify-center flex-col">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-5xl font-bold text-green-400">
|
<p className="text-4xl font-bold text-green-400">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
Math.round(
|
Math.round(
|
||||||
parseFloat(
|
parseFloat(
|
||||||
|
|
@ -536,7 +536,7 @@ export function DashboardHome() {
|
||||||
</div>
|
</div>
|
||||||
<span className="text-6xl font-thin text-gray-600">/</span>
|
<span className="text-6xl font-thin text-gray-600">/</span>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-5xl font-bold text-green-400">
|
<p className="text-4xl font-bold text-green-400">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
Math.round(
|
Math.round(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
|
|
@ -642,7 +642,7 @@ export function DashboardHome() {
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div className="font-bold font-persian text-center">
|
<div className="font-bold font-persian text-center">
|
||||||
<div className="flex flex-col justify-between items-center gap-2">
|
<div className="flex flex-col justify-between items-center gap-2">
|
||||||
<span className="flex font-bold items-center gap-1">
|
<span className="flex font-bold items-center gap-1 mr-auto">
|
||||||
<div className="font-light">مصوب :</div>
|
<div className="font-light">مصوب :</div>
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
Math.round(
|
Math.round(
|
||||||
|
|
@ -655,7 +655,7 @@ export function DashboardHome() {
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1 font-bold">
|
<span className="flex items-center gap-1 font-bold mr-auto">
|
||||||
<div className="font-light">جذب شده :</div>
|
<div className="font-light">جذب شده :</div>
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
Math.round(
|
Math.round(
|
||||||
|
|
@ -700,7 +700,7 @@ export function DashboardHome() {
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="canvas" className="w-ful h-full">
|
<TabsContent value="canvas" className="w-ful h-full">
|
||||||
<div className="p-4">
|
<div className="p-4 h-full">
|
||||||
<D3ImageInfo
|
<D3ImageInfo
|
||||||
companies={
|
companies={
|
||||||
companyChartData.map((item) => {
|
companyChartData.map((item) => {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
} from "~/components/ui/chart";
|
} from "~/components/ui/chart";
|
||||||
|
import { formatNumber } from "~/lib/utils";
|
||||||
|
|
||||||
export type CompanyChartDatum = {
|
export type CompanyChartDatum = {
|
||||||
category: string; // related_company
|
category: string; // related_company
|
||||||
|
|
@ -22,7 +23,7 @@ export type CompanyChartDatum = {
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
capacity: {
|
capacity: {
|
||||||
label: "افزایش ظرفیت",
|
label: "افزایش ظرفیت",
|
||||||
color: "#60A5FA", // Blue-400
|
color: "#69C8EA",
|
||||||
},
|
},
|
||||||
revenue: {
|
revenue: {
|
||||||
label: "افزایش درآمد",
|
label: "افزایش درآمد",
|
||||||
|
|
@ -34,6 +35,7 @@ const chartConfig = {
|
||||||
},
|
},
|
||||||
} satisfies ChartConfig;
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
|
||||||
export function InteractiveBarChart({
|
export function InteractiveBarChart({
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -47,7 +49,8 @@ export function InteractiveBarChart({
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={data}
|
data={data}
|
||||||
margin={{ left: 12, right: 12 }}
|
margin={{ left: 12, right: 12 }}
|
||||||
barCategoryGap="42%"
|
barGap={15}
|
||||||
|
barSize={8}
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} stroke="#475569" />
|
<CartesianGrid vertical={false} stroke="#475569" />
|
||||||
<XAxis
|
<XAxis
|
||||||
|
|
@ -64,14 +67,14 @@ export function InteractiveBarChart({
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickMargin={8}
|
tickMargin={8}
|
||||||
tick={{ fill: "#94a3b8", fontSize: 12 }}
|
tick={{ fill: "#94a3b8", fontSize: 12 }}
|
||||||
tickFormatter={(value) => `${value}%`}
|
tickFormatter={(value) => `${formatNumber(Math.round(value))}%`}
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="capacity" fill={chartConfig.capacity.color} radius={[8, 8, 0, 0]}>
|
<Bar dataKey="capacity" fill={chartConfig.capacity.color} radius={[8, 8, 0, 0]}>
|
||||||
<LabelList
|
<LabelList
|
||||||
dataKey="capacity"
|
dataKey="capacity"
|
||||||
position="top"
|
position="top"
|
||||||
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
||||||
formatter={(v: number) => `${Math.round(v)}%`}
|
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
|
||||||
/>
|
/>
|
||||||
</Bar>
|
</Bar>
|
||||||
<Bar dataKey="revenue" fill={chartConfig.revenue.color} radius={[8, 8, 0, 0]}>
|
<Bar dataKey="revenue" fill={chartConfig.revenue.color} radius={[8, 8, 0, 0]}>
|
||||||
|
|
@ -79,7 +82,7 @@ export function InteractiveBarChart({
|
||||||
dataKey="revenue"
|
dataKey="revenue"
|
||||||
position="top"
|
position="top"
|
||||||
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
||||||
formatter={(v: number) => `${Math.round(v)}%`}
|
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
|
||||||
/>
|
/>
|
||||||
</Bar>
|
</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]}>
|
||||||
|
|
@ -87,7 +90,7 @@ export function InteractiveBarChart({
|
||||||
dataKey="cost"
|
dataKey="cost"
|
||||||
position="top"
|
position="top"
|
||||||
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
||||||
formatter={(v: number) => `${Math.round(v)}%`}
|
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
|
||||||
/>
|
/>
|
||||||
</Bar>
|
</Bar>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,7 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "~/components/ui/dialog";
|
} from "~/components/ui/dialog";
|
||||||
import {
|
import { ChevronUp, ChevronDown, RefreshCw } from "lucide-react";
|
||||||
ChevronUp,
|
|
||||||
ChevronDown,
|
|
||||||
RefreshCw,
|
|
||||||
Building2,
|
|
||||||
PickaxeIcon,
|
|
||||||
UserIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import {
|
import {
|
||||||
|
|
@ -69,6 +61,7 @@ interface DigitalInnovationMetrics {
|
||||||
reduce_energy_consumption_percent: string;
|
reduce_energy_consumption_percent: string;
|
||||||
resource_productivity: string;
|
resource_productivity: string;
|
||||||
resource_productivity_percent: string;
|
resource_productivity_percent: string;
|
||||||
|
average_project_score?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalized interface for digital innovation stats
|
// Normalized interface for digital innovation stats
|
||||||
|
|
@ -82,6 +75,8 @@ interface DigitalInnovationStats {
|
||||||
reduceEnergyConsumptionPercent: number;
|
reduceEnergyConsumptionPercent: number;
|
||||||
resourceProductivity: number;
|
resourceProductivity: number;
|
||||||
resourceProductivityPercent: number;
|
resourceProductivityPercent: number;
|
||||||
|
avarageProjectScore: number;
|
||||||
|
countInnovationDigitalProjects: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DigitalCardLabel {
|
enum DigitalCardLabel {
|
||||||
|
|
@ -168,6 +163,8 @@ export function DigitalInnovationPage() {
|
||||||
reduceEnergyConsumptionPercent: 0,
|
reduceEnergyConsumptionPercent: 0,
|
||||||
resourceProductivity: 0,
|
resourceProductivity: 0,
|
||||||
resourceProductivityPercent: 0,
|
resourceProductivityPercent: 0,
|
||||||
|
avarageProjectScore: 0,
|
||||||
|
countInnovationDigitalProjects: 0,
|
||||||
});
|
});
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||||
field: "start_date",
|
field: "start_date",
|
||||||
|
|
@ -177,7 +174,7 @@ export function DigitalInnovationPage() {
|
||||||
new Set()
|
new Set()
|
||||||
);
|
);
|
||||||
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
||||||
const [avarage, setAvarage] = useState<number>(0);
|
// const [avarage, setAvarage] = useState<number>(0);
|
||||||
const observerRef = useRef<HTMLDivElement>(null);
|
const observerRef = useRef<HTMLDivElement>(null);
|
||||||
const fetchingRef = useRef(false);
|
const fetchingRef = useRef(false);
|
||||||
|
|
||||||
|
|
@ -288,7 +285,6 @@ export function DigitalInnovationPage() {
|
||||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(JSON.parse(response.data));
|
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
const dataString = response.data;
|
const dataString = response.data;
|
||||||
if (dataString && typeof dataString === "string") {
|
if (dataString && typeof dataString === "string") {
|
||||||
|
|
@ -297,7 +293,7 @@ export function DigitalInnovationPage() {
|
||||||
if (Array.isArray(parsedData)) {
|
if (Array.isArray(parsedData)) {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setProjects(parsedData);
|
setProjects(parsedData);
|
||||||
calculateAverage(parsedData);
|
// calculateAverage(parsedData);
|
||||||
setTotalCount(parsedData.length);
|
setTotalCount(parsedData.length);
|
||||||
} else {
|
} else {
|
||||||
setProjects((prev) => [...prev, ...parsedData]);
|
setProjects((prev) => [...prev, ...parsedData]);
|
||||||
|
|
@ -422,8 +418,6 @@ export function DigitalInnovationPage() {
|
||||||
const parsedData = JSON.parse(dataString);
|
const parsedData = JSON.parse(dataString);
|
||||||
if (Array.isArray(parsedData) && parsedData[0]) {
|
if (Array.isArray(parsedData) && parsedData[0]) {
|
||||||
const count = parsedData[0].project_no_count || 0;
|
const count = parsedData[0].project_no_count || 0;
|
||||||
setActualTotalCount(count);
|
|
||||||
// Keep stats in sync if backend stats not yet loaded
|
|
||||||
setStats((prev) => ({ ...prev, totalProjects: count }));
|
setStats((prev) => ({ ...prev, totalProjects: count }));
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
|
|
@ -440,7 +434,7 @@ export function DigitalInnovationPage() {
|
||||||
const fetchStats = async () => {
|
const fetchStats = async () => {
|
||||||
try {
|
try {
|
||||||
setStatsLoading(true);
|
setStatsLoading(true);
|
||||||
const raw = await apiService.callInnovationProcess<any>({
|
const raw = await apiService.call<any>({
|
||||||
innovation_digital_function: {},
|
innovation_digital_function: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -474,8 +468,12 @@ export function DigitalInnovationPage() {
|
||||||
resourceProductivityPercent: parseNum(
|
resourceProductivityPercent: parseNum(
|
||||||
payload?.resource_productivity_percent
|
payload?.resource_productivity_percent
|
||||||
),
|
),
|
||||||
|
avarageProjectScore: parseNum(payload?.average_project_score),
|
||||||
|
countInnovationDigitalProjects: parseNum(
|
||||||
|
payload?.count_innovation_digital_projects
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
setActualTotalCount(normalized.countInnovationDigitalProjects);
|
||||||
setStats(normalized);
|
setStats(normalized);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching stats:", error);
|
console.error("Error fetching stats:", error);
|
||||||
|
|
@ -532,7 +530,7 @@ export function DigitalInnovationPage() {
|
||||||
}
|
}
|
||||||
}, [rating]);
|
}, [rating]);
|
||||||
|
|
||||||
const ststusColor = (status: projectStatus): any => {
|
const statusColor = (status: projectStatus): any => {
|
||||||
let el = null;
|
let el = null;
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case projectStatus.contract:
|
case projectStatus.contract:
|
||||||
|
|
@ -597,7 +595,7 @@ export function DigitalInnovationPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Badge
|
<Badge
|
||||||
variant={ststusColor(value)}
|
variant={statusColor(value)}
|
||||||
className="font-medium border-2 p-0 block w-2 h-2 rounded-full"
|
className="font-medium border-2 p-0 block w-2 h-2 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
border: "none",
|
border: "none",
|
||||||
|
|
@ -625,41 +623,33 @@ export function DigitalInnovationPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateAverage = (data: Array<ProcessInnovationData>) => {
|
|
||||||
let number = 0;
|
|
||||||
data.map(
|
|
||||||
(item: ProcessInnovationData) => (number = number + +item.project_rating)
|
|
||||||
);
|
|
||||||
setAvarage(number / data.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="نوآوری دیجیتال">
|
<DashboardLayout title="نوآوری دیجیتال">
|
||||||
<div className="flex flex-row gap-8 justify-between p-6 space-y-4 h-full">
|
<div className="grid grid-cols-2 gap-8 justify-between p-6 space-y-4 h-full">
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-6 w-full mb-0">
|
||||||
<div className="space-y-6 w-full">
|
<div className="space-y-6 w-full">
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-5">
|
||||||
{loading || statsLoading
|
{loading || statsLoading
|
||||||
? // Loading skeleton for stats cards - matching new design
|
? // Loading skeleton for stats cards - matching new design
|
||||||
Array.from({ length: 4 }).map((_, index) => (
|
Array.from({ length: 4 }).map((_, index) => (
|
||||||
<Card
|
<Card
|
||||||
key={`skeleton-${index}`}
|
key={`skeleton-${index}`}
|
||||||
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl overflow-hidden"
|
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl overflow-hidden"
|
||||||
>
|
>
|
||||||
<CardContent className="p-2">
|
<CardContent className="p-0">
|
||||||
<div className="flex flex-col justify-between gap-2">
|
<div className="flex flex-col justify-between gap-2">
|
||||||
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
|
<div className="flex justify-between items-center border-b-2 px-6 py-4 border-gray-500/20">
|
||||||
<div
|
<div
|
||||||
className="h-6 bg-gray-600 rounded animate-pulse"
|
className="h-6 bg-gray-600 rounded animate-pulse"
|
||||||
style={{ width: "60%" }}
|
style={{ width: "60%" }}
|
||||||
/>
|
/>
|
||||||
<div className="p-3 bg-emerald-500/20 rounded-full w-fit">
|
<div className="bg-emerald-500/20 rounded-full w-fit">
|
||||||
<div className="w-6 h-6 bg-gray-600 rounded animate-pulse" />
|
<div className="w-6 h-6 bg-gray-600 rounded animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center flex-col p-1">
|
<div className="flex items-center justify-center flex-col p-4">
|
||||||
<div
|
<div
|
||||||
className="h-8 bg-gray-600 rounded mb-1 animate-pulse"
|
className="h-8 bg-gray-600 rounded mb-1 animate-pulse"
|
||||||
style={{ width: "40%" }}
|
style={{ width: "40%" }}
|
||||||
|
|
@ -678,19 +668,19 @@ export function DigitalInnovationPage() {
|
||||||
key={card.id}
|
key={card.id}
|
||||||
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"
|
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"
|
||||||
>
|
>
|
||||||
<CardContent className="p-2">
|
<CardContent className="p-0">
|
||||||
<div className="flex flex-col justify-between gap-2">
|
<div className="flex flex-col justify-between gap-2">
|
||||||
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
|
<div className="flex justify-between items-center border-b-2 px-6 border-gray-500/20">
|
||||||
<h3 className="text-lg font-bold text-white font-persian">
|
<h3 className="text-lg font-bold text-white font-persian py-4">
|
||||||
{card.title}
|
{card.title}
|
||||||
</h3>
|
</h3>
|
||||||
<div
|
<div
|
||||||
className={`p-3 gird placeitems-center rounded-full w-fit `}
|
className={`gird placeitems-center rounded-full w-fit`}
|
||||||
>
|
>
|
||||||
{card.icon}
|
{card.icon}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center flex-col p-1">
|
<div className="flex items-center justify-center flex-col p-2 pb-4">
|
||||||
<p
|
<p
|
||||||
className={`text-3xl font-bold ${card.color} mb-1`}
|
className={`text-3xl font-bold ${card.color} mb-1`}
|
||||||
>
|
>
|
||||||
|
|
@ -708,11 +698,12 @@ export function DigitalInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Process Impacts Chart */}
|
{/* Process Impacts Chart */}
|
||||||
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl w-full overflow-hidden">
|
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg w-full overflow-hidden">
|
||||||
{/* <CardContent > */}
|
{/* <CardContent > */}
|
||||||
<CustomBarChart
|
<CustomBarChart
|
||||||
title="تاثیرات نوآوری دیجیتال به صورت درصد مقایسه ای"
|
title="تاثیرات نوآوری دیجیتال به صورت درصد مقایسه ای"
|
||||||
loading={statsLoading}
|
loading={statsLoading}
|
||||||
|
height="100%"
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
label: DigitalCardLabel.decreasCost,
|
label: DigitalCardLabel.decreasCost,
|
||||||
|
|
@ -747,10 +738,10 @@ export function DigitalInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Data Table */}
|
{/* Data Table */}
|
||||||
<Card className="bg-transparent backdrop-blur-sm rounded-2xl 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">
|
<CardContent className="p-0">
|
||||||
<div className="relative">
|
<div className="relative h-full">
|
||||||
<Table containerClassName="overflow-auto custom-scrollbar h-[calc(90vh-270px)]">
|
<Table containerClassName="overflow-auto custom-scrollbar w-full h-[36.8rem] ">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-[#3F415A]">
|
<TableRow className="bg-[#3F415A]">
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
|
|
@ -864,30 +855,29 @@ export function DigitalInnovationPage() {
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="p-2 px-4 bg-gray-700/50">
|
<div className="p-2 px-4 bg-gray-700/50">
|
||||||
<div className="grid grid-cols-6 gap-4 text-sm text-gray-300 font-persian">
|
<div className="flex flex-row gap-4 text-sm text-gray-300 font-persian justify-between">
|
||||||
<div className="text-center gap-2 items-center flex">
|
<div className="text-center gap-2 items-center w-1/3 pr-16">
|
||||||
<div className="text-base text-gray-401 mb-1">
|
<div className="text-base text-gray-401 mb-1">
|
||||||
کل پروژه ها :{formatNumber(actualTotalCount)}
|
کل پروژه ها : {formatNumber(actualTotalCount)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Project number column - empty */}
|
|
||||||
<div></div>
|
<div className="flex items-center flex-row gap-4 status w-3/5 justify-center">
|
||||||
{/* Title column - empty */}
|
<div className="flex flex-row-reverse">
|
||||||
<div></div>
|
<span className="block w-7 h-2.5 bg-violet-500 rounded-tl-xl rounded-bl-xl"></span>
|
||||||
{/* Project status column - empty */}
|
<span className="block w-7 h-2.5 bg-purple-500 "></span>
|
||||||
<div className="flex items-center flex-row-reverse status ">
|
<span className="block w-7 h-2.5 bg-cyan-300 "></span>
|
||||||
<span className="block w-7 h-2.5 bg-violet-500 rounded-tl-xl rounded-bl-xl"></span>
|
<span className="block w-7 h-2.5 bg-pink-400 rounded-tr-xl rounded-br-xl"></span>
|
||||||
<span className="block w-7 h-2.5 bg-purple-500 "></span>
|
|
||||||
<span className="block w-7 h-2.5 bg-cyan-300 "></span>
|
|
||||||
<span className="block w-7 h-2.5 bg-pink-400 rounded-tr-xl rounded-br-xl"></span>
|
|
||||||
</div>
|
|
||||||
{/* Project rating column - show average */}
|
|
||||||
<div className="flex justify-center items-center gap-2">
|
|
||||||
<div className="text-base text-gray-400 mb-1">
|
|
||||||
میانگین امتیاز :
|
|
||||||
</div>
|
</div>
|
||||||
<div className="font-bold">
|
<div className="flex justify-center items-center gap-2">
|
||||||
{formatNumber(((avarage ?? 0) as number).toFixed?.(1) ?? 0)}
|
<div className="text-base text-gray-400 mb-1">میانگین :</div>
|
||||||
|
<div className="font-bold">
|
||||||
|
{formatNumber(
|
||||||
|
((stats.avarageProjectScore ?? 0) as number).toFixed?.(
|
||||||
|
1
|
||||||
|
) ?? 0
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,7 @@ import {
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Radar,
|
|
||||||
Cog,
|
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
@ -96,28 +95,12 @@ interface InnovationStats {
|
||||||
pollution_reduction: number;
|
pollution_reduction: number;
|
||||||
pollution_reduction_percent: number;
|
pollution_reduction_percent: number;
|
||||||
waste_reduction: number;
|
waste_reduction: number;
|
||||||
waste_reduction_percent: number;
|
waste_reductionn_percent: number;
|
||||||
water_recovery_reduction: number;
|
water_recovery_reduction: number;
|
||||||
water_recovery_reduction_percent: number;
|
water_recovery_reduction_percent: number;
|
||||||
}
|
average_project_score: number;
|
||||||
|
count_innovation_green_projects: number;
|
||||||
interface GreenInnovationState {
|
standard_regulations: string
|
||||||
water: {
|
|
||||||
value: any;
|
|
||||||
percent: number;
|
|
||||||
};
|
|
||||||
food: {
|
|
||||||
value: any;
|
|
||||||
percent: number;
|
|
||||||
};
|
|
||||||
power: {
|
|
||||||
value: any;
|
|
||||||
percent: number;
|
|
||||||
};
|
|
||||||
oil: {
|
|
||||||
value: any;
|
|
||||||
percent: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
|
|
@ -144,6 +127,15 @@ interface ChartDataItem {
|
||||||
amt: number; // max value or target
|
amt: number; // max value or target
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum projectStatus {
|
||||||
|
propozal = "پروپوزال",
|
||||||
|
contract = "پیشنویس قرارداد",
|
||||||
|
inprogress = "در حال انجام",
|
||||||
|
stop = "متوقف شده",
|
||||||
|
mafasa = "مرحله مفاصا",
|
||||||
|
finish = "پایان یافته",
|
||||||
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: "select", label: "", sortable: false, width: "50px" },
|
{ key: "select", label: "", sortable: false, width: "50px" },
|
||||||
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" },
|
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" },
|
||||||
|
|
@ -178,9 +170,11 @@ export function GreenInnovationPage() {
|
||||||
field: "start_date",
|
field: "start_date",
|
||||||
direction: "asc",
|
direction: "asc",
|
||||||
});
|
});
|
||||||
|
const [tblAvarage, setTblAvarage] = useState<number>(0);
|
||||||
const [selectedProjects, setSelectedProjects] = useState<Set<string>>(
|
const [selectedProjects, setSelectedProjects] = useState<Set<string>>(
|
||||||
new Set()
|
new Set()
|
||||||
);
|
);
|
||||||
|
const [standartRegulation, setStandardRegulation] = useState<Array<string>>([])
|
||||||
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
||||||
const [selectedProjectDetails, setSelectedProjectDetails] =
|
const [selectedProjectDetails, setSelectedProjectDetails] =
|
||||||
useState<GreenInnovationData | null>(null);
|
useState<GreenInnovationData | null>(null);
|
||||||
|
|
@ -199,13 +193,7 @@ export function GreenInnovationPage() {
|
||||||
suffix: "تن",
|
suffix: "تن",
|
||||||
percent: 0,
|
percent: 0,
|
||||||
},
|
},
|
||||||
oil: {
|
|
||||||
icon: <Flame className="text-emerald-400" size={"18px"} />,
|
|
||||||
label: "سوخت",
|
|
||||||
value: 0,
|
|
||||||
suffix: "متر مربع",
|
|
||||||
percent: 0,
|
|
||||||
},
|
|
||||||
power: {
|
power: {
|
||||||
icon: <Zap className="text-emerald-400" size={"18px"} />,
|
icon: <Zap className="text-emerald-400" size={"18px"} />,
|
||||||
label: "برق",
|
label: "برق",
|
||||||
|
|
@ -213,6 +201,13 @@ export function GreenInnovationPage() {
|
||||||
suffix: "میلیون مگاوات",
|
suffix: "میلیون مگاوات",
|
||||||
percent: 0,
|
percent: 0,
|
||||||
},
|
},
|
||||||
|
oil: {
|
||||||
|
icon: <Flame className="text-emerald-400" size={"18px"} />,
|
||||||
|
label: "سوخت",
|
||||||
|
value: 0,
|
||||||
|
suffix: "متر مربع",
|
||||||
|
percent: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const [sustainabilityStats, setSustainabilityStats] = useState<StatsCard>({
|
const [sustainabilityStats, setSustainabilityStats] = useState<StatsCard>({
|
||||||
pollution: {
|
pollution: {
|
||||||
|
|
@ -273,9 +268,8 @@ export function GreenInnovationPage() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fetchingRef.current = true;
|
fetchingRef.current = true;
|
||||||
|
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setLoading(true);
|
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
} else {
|
} else {
|
||||||
setLoadingMore(true);
|
setLoadingMore(true);
|
||||||
|
|
@ -402,6 +396,9 @@ export function GreenInnovationPage() {
|
||||||
};
|
};
|
||||||
}, [loadMore, hasMore, loadingMore]);
|
}, [loadMore, hasMore, loadingMore]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
}, [])
|
||||||
const handleSort = (field: string) => {
|
const handleSort = (field: string) => {
|
||||||
fetchingRef.current = false;
|
fetchingRef.current = false;
|
||||||
setSortConfig((prev) => ({
|
setSortConfig((prev) => ({
|
||||||
|
|
@ -428,7 +425,6 @@ export function GreenInnovationPage() {
|
||||||
const parsedData = JSON.parse(dataString);
|
const parsedData = JSON.parse(dataString);
|
||||||
if (Array.isArray(parsedData) && parsedData[0]) {
|
if (Array.isArray(parsedData) && parsedData[0]) {
|
||||||
const count = parsedData[0].project_no_count || 0;
|
const count = parsedData[0].project_no_count || 0;
|
||||||
setActualTotalCount(count);
|
|
||||||
// Keep stats in sync if backend stats not yet loaded
|
// Keep stats in sync if backend stats not yet loaded
|
||||||
setStats((prev) => ({ ...prev, totalProjects: count }));
|
setStats((prev) => ({ ...prev, totalProjects: count }));
|
||||||
}
|
}
|
||||||
|
|
@ -458,7 +454,7 @@ export function GreenInnovationPage() {
|
||||||
if (typeof payload === "string") {
|
if (typeof payload === "string") {
|
||||||
try {
|
try {
|
||||||
payload = JSON.parse(payload);
|
payload = JSON.parse(payload);
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
const parseNum = (v: unknown): any => {
|
const parseNum = (v: unknown): any => {
|
||||||
if (v == null) return 0;
|
if (v == null) return 0;
|
||||||
|
|
@ -475,7 +471,6 @@ export function GreenInnovationPage() {
|
||||||
payload?.innovation_green_function
|
payload?.innovation_green_function
|
||||||
);
|
);
|
||||||
const stats = data[0];
|
const stats = data[0];
|
||||||
|
|
||||||
const normalized: any = {
|
const normalized: any = {
|
||||||
food: {
|
food: {
|
||||||
value: formatNumber(parseNum(stats?.feed_recovery_reduction)),
|
value: formatNumber(parseNum(stats?.feed_recovery_reduction)),
|
||||||
|
|
@ -504,9 +499,15 @@ export function GreenInnovationPage() {
|
||||||
|
|
||||||
waste: {
|
waste: {
|
||||||
value: formatNumber(parseNum(stats.waste_reduction)),
|
value: formatNumber(parseNum(stats.waste_reduction)),
|
||||||
percent: formatNumber(parseNum(stats.waste_reduction_percent)),
|
percent: formatNumber(parseNum(stats.waste_reductionn_percent)),
|
||||||
},
|
},
|
||||||
|
avarage: stats.average_project_score,
|
||||||
|
countInnovationGreenProjects: stats.count_innovation_green_projects,
|
||||||
|
standardRegulation: stats.standard_regulations.replace('\r', '').split('\n')
|
||||||
};
|
};
|
||||||
|
setStandardRegulation(normalized.standardRegulation)
|
||||||
|
setActualTotalCount(normalized.countInnovationGreenProjects);
|
||||||
|
setTblAvarage(normalized.avarage);
|
||||||
setPageData(normalized);
|
setPageData(normalized);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching stats:", error);
|
console.error("Error fetching stats:", error);
|
||||||
|
|
@ -589,7 +590,7 @@ export function GreenInnovationPage() {
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedProjects.has(item.project_id)}
|
checked={selectedProjects.has(item.project_id)}
|
||||||
onCheckedChange={() => handleSelectProject(item.project_id)}
|
onCheckedChange={() => handleSelectProject(item.project_id)}
|
||||||
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "details":
|
case "details":
|
||||||
|
|
@ -598,7 +599,7 @@ export function GreenInnovationPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleProjectDetails(item)}
|
onClick={() => handleProjectDetails(item)}
|
||||||
className="text-emerald-400 hover:text-emerald-300 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>
|
</Button>
|
||||||
|
|
@ -619,15 +620,16 @@ export function GreenInnovationPage() {
|
||||||
return <span className="font-medium text-white">{String(value)}</span>;
|
return <span className="font-medium text-white">{String(value)}</span>;
|
||||||
case "project_status":
|
case "project_status":
|
||||||
return (
|
return (
|
||||||
<Badge
|
<div className="flex items-center gap-1">
|
||||||
variant="outline"
|
<Badge
|
||||||
className="font-medium border-2"
|
variant={statusColor(value)}
|
||||||
style={{
|
className="font-medium border-2 p-0 block w-2 h-2 rounded-full"
|
||||||
border: "none",
|
style={{
|
||||||
}}
|
border: "none",
|
||||||
>
|
}}
|
||||||
|
></Badge>
|
||||||
{String(value)}
|
{String(value)}
|
||||||
</Badge>
|
</div>
|
||||||
);
|
);
|
||||||
case "project_rating":
|
case "project_rating":
|
||||||
return (
|
return (
|
||||||
|
|
@ -648,6 +650,30 @@ export function GreenInnovationPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const statusColor = (status: projectStatus): any => {
|
||||||
|
let el = null;
|
||||||
|
switch (status) {
|
||||||
|
case projectStatus.contract:
|
||||||
|
el = "teal";
|
||||||
|
break;
|
||||||
|
case projectStatus.finish:
|
||||||
|
el = "info";
|
||||||
|
break;
|
||||||
|
case projectStatus.stop:
|
||||||
|
el = "warning";
|
||||||
|
break;
|
||||||
|
case projectStatus.inprogress:
|
||||||
|
el = "teal";
|
||||||
|
break;
|
||||||
|
case projectStatus.mafasa:
|
||||||
|
el = "destructive";
|
||||||
|
break;
|
||||||
|
case projectStatus.propozal:
|
||||||
|
el = "info";
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
const [chartData, setChartData] = useState<Array<ChartDataItem>>([
|
const [chartData, setChartData] = useState<Array<ChartDataItem>>([
|
||||||
{ name: recycleParams.water.label, pv: 70, amt: 80 },
|
{ name: recycleParams.water.label, pv: 70, amt: 80 },
|
||||||
{ name: recycleParams.power.label, pv: 45, amt: 60 },
|
{ name: recycleParams.power.label, pv: 45, amt: 60 },
|
||||||
|
|
@ -657,81 +683,80 @@ export function GreenInnovationPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="نوآوری سبز">
|
<DashboardLayout title="نوآوری سبز">
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4 h-[23.5rem]">
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="flex gap-6 mb-6">
|
<div className="flex gap-6 mb-5">
|
||||||
<div className="flex flex-col gap-11 h-full w-1/2">
|
<div className="flex flex-col justify-between w-1/2">
|
||||||
{/* Stats Grid */}
|
|
||||||
{loading || statsLoading
|
{loading || statsLoading
|
||||||
? // Loading skeleton for stats cards - matching new design
|
? // Loading skeleton for stats cards - matching new design
|
||||||
Array.from({ length: 2 }).map((_, index) => (
|
Array.from({ length: 2 }).map((_, index) => (
|
||||||
<Card
|
<Card
|
||||||
key={`skeleton-${index}`}
|
key={`skeleton-${index}`}
|
||||||
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl overflow-hidden"
|
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg overflow-hidden"
|
||||||
>
|
>
|
||||||
<CardContent className="p-0 h-48">
|
<CardContent className="p-0 h-[11.5rem]">
|
||||||
<div className="flex flex-col gap-2 h-full">
|
<div className="flex flex-col gap-2 h-full">
|
||||||
<div className="border-b-2 border-gray-500/20 p-2.5">
|
<div className="border-b-2 border-gray-500/20 p-2.5">
|
||||||
<div
|
<div
|
||||||
className="h-6 bg-gray-600 rounded animate-pulse"
|
className="h-6 bg-gray-600 rounded animate-pulse"
|
||||||
style={{ width: "60%" }}
|
style={{ width: "60%" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center flex-col p-2.5 mt-4">
|
|
||||||
<div
|
|
||||||
className="h-8 bg-gray-600 rounded mb-1 animate-pulse"
|
|
||||||
style={{ width: "40%" }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="h-4 bg-gray-600 rounded animate-pulse"
|
|
||||||
style={{ width: "80%" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
<div className="flex items-center justify-center flex-col p-2.5 mt-4">
|
||||||
</Card>
|
<div
|
||||||
))
|
className="h-8 bg-gray-600 rounded mb-1 animate-pulse"
|
||||||
|
style={{ width: "40%" }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="h-4 bg-gray-600 rounded animate-pulse"
|
||||||
|
style={{ width: "80%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
: Object.entries(sustainabilityStats).map(([key, value]) => (
|
||||||
<Card
|
<Card
|
||||||
key={key}
|
key={key}
|
||||||
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"
|
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">
|
<CardContent className="p-0 h-full">
|
||||||
<div className="flex flex-col justify-between gap-2 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 ">
|
<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">
|
<h3 className="text-lg font-bold text-white font-persian p-4">
|
||||||
{value.title}
|
{value.title}
|
||||||
</h3>
|
</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>
|
</div>
|
||||||
<div className="flex items-center justify-between p-6 flex-row-reverse">
|
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-3xl font-bold text-emerald-400 mb-1 font-persian">
|
<span className="text-3xl font-bold text-emerald-400 mb-1 font-persian">
|
||||||
% {value.percent?.value}
|
{value.total?.value}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-400 font-persian">
|
<span className="text-sm text-gray-400 font-persian">
|
||||||
{value.percent?.description}
|
{value.total?.description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</CardContent>
|
||||||
))}
|
</Card>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Process Impacts Chart */}
|
{/* Process Impacts Chart */}
|
||||||
|
|
||||||
{statsLoading ? (
|
{statsLoading ? (
|
||||||
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] 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-lg w-full overflow-hidden">
|
||||||
<CardContent className="p-0 h-full">
|
<CardContent className="p-0 h-full">
|
||||||
<div className="border-b-2 border-gray-500/20">
|
<div className="border-b-2 border-gray-500/20">
|
||||||
<div className="w-full p-4 px-6">
|
<div className="w-full p-4 px-6">
|
||||||
|
|
@ -739,7 +764,7 @@ export function GreenInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="content grid gap-6 h-max p-8 box-border items-center justify-between sm:grid-cols-1 xl:grid-cols-[30%_70%]">
|
<div className="content grid gap-6 h-full p-8 box-border items-center justify-between sm:grid-cols-1 xl:grid-cols-[30%_70%]">
|
||||||
<div className="params flex flex-col gap-3.5">
|
<div className="params flex flex-col gap-3.5">
|
||||||
{[...Array(3)].map((_, paramIndex) => (
|
{[...Array(3)].map((_, paramIndex) => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -758,7 +783,7 @@ export function GreenInnovationPage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-72 w-full flex items-end justify-between px-6">
|
<div className="h-[17rem] w-full min-w-[35rem] flex items-end justify-between px-6">
|
||||||
{[...Array(8)].map((_, barIndex) => (
|
{[...Array(8)].map((_, barIndex) => (
|
||||||
<div
|
<div
|
||||||
key={barIndex}
|
key={barIndex}
|
||||||
|
|
@ -774,14 +799,14 @@ export function GreenInnovationPage() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] 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-lg w-full overflow-hidden">
|
||||||
<CardContent className="p-0 h-full overflow-hidden">
|
<CardContent className="p-0 h-full overflow-hidden">
|
||||||
<div className="border-b-2 border-gray-500/20">
|
<div className="border-b-2 border-gray-500/20">
|
||||||
<div className="w-full p-4 px-6">
|
<div className="w-full p-4 px-6">
|
||||||
<span>بازیافت و بازیابی منابع</span>
|
<span>بازیافت و بازیابی منابع</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="content grid gap-6 h-max p-8 box-border items-center justify-between sm:grid-cols-1 sm:overflow-auto xl:overflow-hidden xl:grid-cols-[30%_70%]">
|
<div className="content grid gap-9 h-max p-8 box-border items-center justify-between sm:grid-cols-1 sm:overflow-x-scroll xl:overflow-hidden xl:grid-cols-[30%_70%]">
|
||||||
<div className="params flex flex-col gap-3.5">
|
<div className="params flex flex-col gap-3.5">
|
||||||
{Object.entries(recycleParams).map((el, index) => {
|
{Object.entries(recycleParams).map((el, index) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -805,7 +830,7 @@ export function GreenInnovationPage() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-72 w-[35rem]">
|
<div className="h-64 w-full">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart
|
<BarChart
|
||||||
width={500}
|
width={500}
|
||||||
|
|
@ -844,7 +869,7 @@ export function GreenInnovationPage() {
|
||||||
fontSize: 14, // سایز فونت
|
fontSize: 14, // سایز فونت
|
||||||
dx: -30, // جابجایی افقی (اعداد نزدیکتر یا دورتر از محور)
|
dx: -30, // جابجایی افقی (اعداد نزدیکتر یا دورتر از محور)
|
||||||
}}
|
}}
|
||||||
tickFormatter={(val) => `${val}%`}
|
tickFormatter={(val) => `${formatNumber(val)}%`}
|
||||||
/>
|
/>
|
||||||
<Bar
|
<Bar
|
||||||
dataKey="pv"
|
dataKey="pv"
|
||||||
|
|
@ -855,7 +880,7 @@ export function GreenInnovationPage() {
|
||||||
position: "top",
|
position: "top",
|
||||||
fill: "#fff",
|
fill: "#fff",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
formatter: (value) => `${value}%`,
|
formatter: (value: any) => `${formatNumber(value)}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
|
|
@ -866,10 +891,10 @@ export function GreenInnovationPage() {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card className="w-1/3 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl 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">
|
<CardContent className="p-0">
|
||||||
<div className="border-b-2 border-gray-500/20">
|
<div className="border-b-2 border-gray-500/20">
|
||||||
<div className="flex flex-row justify-between w-full p-4 px-6">
|
<div className="flex flex-row justify-between w-full p-4">
|
||||||
{statsLoading ? (
|
{statsLoading ? (
|
||||||
<>
|
<>
|
||||||
<span className="h-4 w-28 bg-gray-500/40 rounded animate-pulse"></span>
|
<span className="h-4 w-28 bg-gray-500/40 rounded animate-pulse"></span>
|
||||||
|
|
@ -884,34 +909,31 @@ export function GreenInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="flex flex-col gap-3 p-4 overflow-y-scroll h-[20rem]">
|
||||||
className={`flex flex-col gap-3 p-4 max-h-[22rem] ${
|
|
||||||
statsLoading ? "overflow-y-hidden" : "overflow-y-scroll"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{statsLoading
|
{statsLoading
|
||||||
? Array.from({ length: 10 }).map((_, index) => (
|
? Array.from({ length: 10 }).map((_, index) => (
|
||||||
<div key={index} className="flex gap-2 items-center">
|
<div key={`skeleton-${index}`} className="flex gap-2 items-center">
|
||||||
<span className="h-4 w-4 bg-gray-500/40 rounded-full animate-pulse"></span>
|
<span className="h-4 w-4 bg-gray-500/40 rounded-full animate-pulse"></span>
|
||||||
<span className="h-3 w-32 bg-gray-500/40 rounded animate-pulse"></span>
|
<span className="h-3 w-32 bg-gray-500/40 rounded animate-pulse"></span>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
: Array.from({ length: 10 }).map((_, index) => (
|
: standartRegulation.map((item, index) => (
|
||||||
<div key={`${index}-1`} className="flex gap-2">
|
<div key={`${item}-${index}-1`} className="flex flex-row flex-1 gap-2">
|
||||||
<LoaderCircle
|
<LoaderCircle
|
||||||
size={"18px"}
|
size={"20px"}
|
||||||
className="text-emerald-400"
|
height={"20px"}
|
||||||
/>
|
className="text-emerald-400 w-5 h-5 shrink-0"
|
||||||
<span>استاندارد Iso 2005</span>
|
/>
|
||||||
</div>
|
<span className="text-sm truncate">{item}</span>
|
||||||
))}
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Data Table */}
|
{/* Data Table */}
|
||||||
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
|
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(90vh-400px)]">
|
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(90vh-400px)]">
|
||||||
|
|
@ -920,13 +942,13 @@ export function GreenInnovationPage() {
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={column.key}
|
key={column.key}
|
||||||
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium sticky top-0 z-20 bg-[#3F415A]"
|
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium sticky top-0 z-20 bg-[#3F415A] "
|
||||||
style={{ width: column.width }}
|
style={{ width: column.width }}
|
||||||
>
|
>
|
||||||
{column.sortable ? (
|
{column.sortable ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSort(column.key)}
|
onClick={() => handleSort(column.key)}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
<span>{column.label}</span>
|
<span>{column.label}</span>
|
||||||
{sortConfig.field === column.key ? (
|
{sortConfig.field === column.key ? (
|
||||||
|
|
@ -1015,8 +1037,34 @@ export function GreenInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="p-2 px-4 bg-gray-700/50">
|
<div className="p-2 px-4 bg-gray-700/50">
|
||||||
|
<div className="flex flex-row gap-4 text-sm text-gray-300 font-persian justify-between">
|
||||||
|
<div className="text-center gap-2 items-center w-1/3 pr-36">
|
||||||
|
<div className="text-base text-gray-401 mb-1">
|
||||||
|
کل پروژه ها :{formatNumber(actualTotalCount)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center flex-row gap-20 status justify-center w-2/3">
|
||||||
|
<div className="flex flex-row-reverse">
|
||||||
|
<span className="block w-7 h-2.5 bg-violet-500 rounded-tl-xl rounded-bl-xl"></span>
|
||||||
|
<span className="block w-7 h-2.5 bg-purple-500 "></span>
|
||||||
|
<span className="block w-7 h-2.5 bg-cyan-300 "></span>
|
||||||
|
<span className="block w-7 h-2.5 bg-pink-400 rounded-tr-xl rounded-br-xl"></span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center items-center gap-2">
|
||||||
|
<div className="text-base text-gray-400 mb-1">میانگین :</div>
|
||||||
|
<div className="font-bold">
|
||||||
|
{formatNumber(
|
||||||
|
((tblAvarage ?? 0) as number).toFixed?.(1) ?? 0
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="p-2 px-4 bg-gray-700/50">
|
||||||
<div className="grid grid-cols-6 gap-4 text-sm text-gray-300 font-persian">
|
<div className="grid grid-cols-6 gap-4 text-sm text-gray-300 font-persian">
|
||||||
<div className="text-center gap-2 items-center flex">
|
<div className="text-center gap-2 items-center flex">
|
||||||
<div className="text-base text-gray-401 mb-1">
|
<div className="text-base text-gray-401 mb-1">
|
||||||
|
|
@ -1025,13 +1073,7 @@ export function GreenInnovationPage() {
|
||||||
{formatNumber(stats?.totalProjects || actualTotalCount)}
|
{formatNumber(stats?.totalProjects || actualTotalCount)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Project number column - empty */}
|
|
||||||
<div></div>
|
|
||||||
{/* Title column - empty */}
|
|
||||||
<div></div>
|
|
||||||
{/* Project status column - empty */}
|
|
||||||
<div></div>
|
|
||||||
{/* Project rating column - show average */}
|
|
||||||
<div className="flex justify-center items-center gap-2">
|
<div className="flex justify-center items-center gap-2">
|
||||||
<div className="text-base text-gray-400 mb-1">
|
<div className="text-base text-gray-400 mb-1">
|
||||||
{" "}
|
{" "}
|
||||||
|
|
@ -1039,16 +1081,14 @@ export function GreenInnovationPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="font-bold">
|
<div className="font-bold">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
((stats?.averageScore ?? 0) as number).toFixed?.(1) ??
|
((tblAvarage ?? 0) as number).toFixed?.(1) ??
|
||||||
stats?.averageScore ??
|
tblAvarage ??
|
||||||
0
|
0
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Details column - show total count */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1081,9 +1121,9 @@ export function GreenInnovationPage() {
|
||||||
<span className="text-white font-bold font-persian">
|
<span className="text-white font-bold font-persian">
|
||||||
{selectedProjectDetails?.start_date
|
{selectedProjectDetails?.start_date
|
||||||
? moment(
|
? moment(
|
||||||
selectedProjectDetails?.start_date,
|
selectedProjectDetails?.start_date,
|
||||||
"YYYY-MM-DD"
|
"YYYY-MM-DD"
|
||||||
).format("YYYY/MM/DD")
|
).format("YYYY/MM/DD")
|
||||||
: "-"}
|
: "-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1096,9 +1136,9 @@ export function GreenInnovationPage() {
|
||||||
<span className="text-white font-bold font-persian">
|
<span className="text-white font-bold font-persian">
|
||||||
{selectedProjectDetails?.done_date
|
{selectedProjectDetails?.done_date
|
||||||
? moment(
|
? moment(
|
||||||
selectedProjectDetails?.done_date,
|
selectedProjectDetails?.done_date,
|
||||||
"YYYY-MM-DD"
|
"YYYY-MM-DD"
|
||||||
).format("YYYY/MM/DD")
|
).format("YYYY/MM/DD")
|
||||||
: "-"}
|
: "-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1128,26 +1168,6 @@ export function GreenInnovationPage() {
|
||||||
{selectedProjectDetails?.observer || "-"}
|
{selectedProjectDetails?.observer || "-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
|
|
||||||
<Radar className="h-4 text-green-500" />
|
|
||||||
حوزه کاری :
|
|
||||||
</h4>
|
|
||||||
<span className="text-white font-bold font-persian">
|
|
||||||
{selectedProjectDetails?.observer || "-"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
|
|
||||||
<Cog className="h-4 text-green-500" />
|
|
||||||
صنعت :
|
|
||||||
</h4>
|
|
||||||
<span className="text-white font-bold font-persian">
|
|
||||||
{selectedProjectDetails?.observer || "-"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -32,7 +32,7 @@ export function CustomBarChart({
|
||||||
}: CustomBarChartProps) {
|
}: CustomBarChartProps) {
|
||||||
// Calculate the maximum value across all data points for consistent scaling
|
// Calculate the maximum value across all data points for consistent scaling
|
||||||
const globalMaxValue = Math.max(
|
const globalMaxValue = Math.max(
|
||||||
...data.map((item) => item.maxValue || item.value),
|
...data.map((item) => item.maxValue || item.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Loading skeleton
|
// Loading skeleton
|
||||||
|
|
@ -43,7 +43,7 @@ export function CustomBarChart({
|
||||||
<div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div>
|
<div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 flex flex-col gap-4">
|
||||||
{Array.from({ length: 4 }).map((_, index) => (
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
<div key={index} className="flex items-center gap-3">
|
<div key={index} className="flex items-center gap-3">
|
||||||
{/* Label skeleton */}
|
{/* Label skeleton */}
|
||||||
|
|
@ -86,8 +86,9 @@ export function CustomBarChart({
|
||||||
<div key={index} className="flex items-center gap-3">
|
<div key={index} className="flex items-center gap-3">
|
||||||
{/* Label */}
|
{/* Label */}
|
||||||
<span
|
<span
|
||||||
className={`font-persian text-sm min-w-[160px] text-right ${item.labelColor || "text-white"
|
className={`font-persian text-sm min-w-[160px] text-right ${
|
||||||
}`}
|
item.labelColor || "text-white"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -97,8 +98,9 @@ export function CustomBarChart({
|
||||||
className={`flex-1 flex items-center bg-gray-700 rounded-full relative overflow-hidden ${barHeight}`}
|
className={`flex-1 flex items-center bg-gray-700 rounded-full relative overflow-hidden ${barHeight}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`${barHeight} rounded-full transition-all duration-700 ease-out relative ${item.color || "bg-emerald-400"
|
className={`${barHeight} rounded-full transition-all duration-700 ease-out relative ${
|
||||||
}`}
|
item.color || "bg-emerald-400"
|
||||||
|
}`}
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.min(percentage, 100)}%`,
|
width: `${Math.min(percentage, 100)}%`,
|
||||||
}}
|
}}
|
||||||
|
|
@ -110,21 +112,22 @@ export function CustomBarChart({
|
||||||
|
|
||||||
{/* Value Label */}
|
{/* Value Label */}
|
||||||
<span
|
<span
|
||||||
className={`font-bold text-sm min-w-[60px] text-left ${item.color?.includes("emerald")
|
className={`font-bold text-sm min-w-[60px] text-left ${
|
||||||
? "text-emerald-400"
|
item.color?.includes("emerald")
|
||||||
: item.color?.includes("blue")
|
? "text-emerald-400"
|
||||||
? "text-blue-400"
|
: item.color?.includes("blue")
|
||||||
: item.color?.includes("purple")
|
? "text-blue-400"
|
||||||
? "text-purple-400"
|
: item.color?.includes("purple")
|
||||||
: item.color?.includes("red")
|
? "text-purple-400"
|
||||||
? "text-red-400"
|
: item.color?.includes("red")
|
||||||
: item.color?.includes("yellow")
|
? "text-red-400"
|
||||||
? "text-yellow-400"
|
: item.color?.includes("yellow")
|
||||||
: "text-emerald-400"
|
? "text-yellow-400"
|
||||||
}`}
|
: "text-emerald-400"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{item.valuePrefix || ""}
|
{item.valuePrefix || ""}
|
||||||
{formatNumber(parseFloat(displayValue))}
|
{formatNumber(parseFloat(displayValue))}%
|
||||||
{item.valueSuffix || ""}
|
{item.valueSuffix || ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ const DialogContent = React.forwardRef<
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4 cursor-pointer" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,19 @@ export default [
|
||||||
route("dashboard/project-management", "routes/project-management.tsx"),
|
route("dashboard/project-management", "routes/project-management.tsx"),
|
||||||
route(
|
route(
|
||||||
"dashboard/innovation-basket/process-innovation",
|
"dashboard/innovation-basket/process-innovation",
|
||||||
"routes/innovation-basket.process-innovation.tsx",
|
"routes/innovation-basket.process-innovation.tsx"
|
||||||
),
|
),
|
||||||
route(
|
route(
|
||||||
"dashboard/innovation-basket/green-innovation",
|
"dashboard/innovation-basket/green-innovation",
|
||||||
"routes/green-innovation.tsx",
|
"routes/green-innovation.tsx"
|
||||||
|
),
|
||||||
|
route(
|
||||||
|
"/dashboard/innovation-basket/internal-innovation",
|
||||||
|
"routes/innovation-built-insider-page.tsx"
|
||||||
),
|
),
|
||||||
route(
|
route(
|
||||||
"/dashboard/innovation-basket/digital-innovation",
|
"/dashboard/innovation-basket/digital-innovation",
|
||||||
"routes/digital-innovation-page.tsx",
|
"routes/digital-innovation-page.tsx"
|
||||||
),
|
),
|
||||||
route("dashboard/ecosystem", "routes/ecosystem.tsx"),
|
route("dashboard/ecosystem", "routes/ecosystem.tsx"),
|
||||||
route("404", "routes/404.tsx"),
|
route("404", "routes/404.tsx"),
|
||||||
|
|
|
||||||
17
app/routes/innovation-built-insider-page.tsx
Normal file
17
app/routes/innovation-built-insider-page.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { ProtectedRoute } from "~/components/auth/protected-route";
|
||||||
|
import InnovationBuiltInsidePage from "~/components/dashboard/project-management/innovation-built-inside-page";
|
||||||
|
|
||||||
|
export function meta() {
|
||||||
|
return [
|
||||||
|
{ title: "نوآوری در فرآیند - سیستم مدیریت فناوری و نوآوری" },
|
||||||
|
{ name: "description", content: "مدیریت پروژههای نوآوری در فرآیند" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InnovationBuiltInside() {
|
||||||
|
return (
|
||||||
|
<ProtectedRoute requireAuth={true}>
|
||||||
|
<InnovationBuiltInsidePage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
);
|
||||||
|
}
|
||||||
4964
package-lock.json
generated
Normal file
4964
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -4,7 +4,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "react-router build",
|
"build": "react-router build",
|
||||||
"dev": "react-router dev --port 3000",
|
"dev": "react-router dev --port 3001",
|
||||||
"start": "react-router-serve ./build/server/index.js",
|
"start": "react-router-serve ./build/server/index.js",
|
||||||
"typecheck": "react-router typegen && tsc"
|
"typecheck": "react-router typegen && tsc"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user