Compare commits

...

13 Commits

Author SHA1 Message Date
19b3409572 update the styles in interactive bar chart , also finisht the shematic 2025-09-07 00:05:39 +03:30
2c393eddf0 fix: change designed bug 2025-09-05 19:55:15 +03:30
MehrdadAdabi
ec6235f00c feat: finish internal-innovation 2025-09-04 16:40:26 +03:30
MehrdadAdabi
73f960b56a feat: contineu developing page innterbal-innovation 2025-09-04 13:34:58 +03:30
MehrdadAdabi
49f018e56f feat: add skeleton for innovation built in and change part pf login green innovation 2025-09-03 12:48:02 +03:30
MehrdadAdabi
8df1fbc422 feat: start innvoation-built-in page 2025-09-02 18:55:30 +03:30
MehrdadAdabi
957b05cdbd fix: designed bugs 2025-09-01 10:37:21 +03:30
b53051b77f fix: designed bug 2025-08-31 16:45:20 +03:30
31a344e3a1 fix: chage designed and props 2025-08-31 16:21:47 +03:30
mahmoodsht
41e2787601 Merge branch 'dashboard_charts' 2025-08-31 10:08:07 +03:30
12e85fdb08 fix: desinged bugs 2025-08-31 06:34:29 +03:30
MehrdadAdabi
28f22dd0d3 fix: green inivation designed bugs 2025-08-30 18:21:46 +03:30
MehrdadAdabi
69cea7a06c fix: green innovation designed bugs 2025-08-30 17:17:29 +03:30
13 changed files with 6722 additions and 420 deletions

View File

@ -1,5 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
import React from "react";
import { formatNumber } from "~/lib/utils";
export type CompanyInfo = {
@ -17,141 +16,162 @@ export type D3ImageInfoProps = {
height?: number;
};
export function D3ImageInfo({ companies, width = 900, height = 400 }: D3ImageInfoProps) {
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]);
const InfoBox = ({ company, style }: { company: CompanyInfo; style :any }) => {
return (
<div className="w-full h-full">
<div ref={containerRef} className="w-full h-[400px]">
<svg ref={svgRef} className="block w-full h-full"></svg>
<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">{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>
);
};
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>
);
}

View File

@ -60,7 +60,7 @@ export function DashboardCustomBarChart({
<span className="text-white font-bold text-base">
{formatNumber(item.value)}
</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}
</span>
</div>

View File

@ -126,19 +126,19 @@ 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);
const costRed = Number(r?.innovation_cost_reduction_sum ?? 0);
const preCap = Number(r?.pre_project_production_capacity_sum ?? 0);
const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0);
const preInc = Number(r?.pre_project_income_sum ?? 0);
const incInc = Number(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;
const capacityPct = preCap > 0 ? (incCap / preCap) * 100 : 0;
const revenuePct = preInc > 0 ? (incInc / preInc) * 100 : 0;
const costPct = preFee > 0 ? (costRed / preFee) * 100 : 0;
console.log(costRed)
return {
category: rel,
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 gap-4">
<div className="text-center">
<p className="text-5xl font-bold text-green-400">
<p className="text-4xl font-bold text-green-400">
{formatNumber(
dashboardData.topData
?.technology_innovation_based_revenue_growth || "0",
@ -487,7 +487,7 @@ export function DashboardHome() {
</div>
<span className="text-6xl font-thin text-gray-600">/</span>
<div className="text-center">
<p className="text-5xl font-bold text-green-400">
<p className="text-4xl font-bold text-green-400">
{formatNumber(
Math.round(
dashboardData.topData
@ -518,7 +518,7 @@ export function DashboardHome() {
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-5xl font-bold text-green-400">
<p className="text-4xl font-bold text-green-400">
{formatNumber(
Math.round(
parseFloat(
@ -536,7 +536,7 @@ export function DashboardHome() {
</div>
<span className="text-6xl font-thin text-gray-600">/</span>
<div className="text-center">
<p className="text-5xl font-bold text-green-400">
<p className="text-4xl font-bold text-green-400">
{formatNumber(
Math.round(
dashboardData.topData
@ -642,7 +642,7 @@ export function DashboardHome() {
</ChartContainer>
<div className="font-bold font-persian text-center">
<div className="flex flex-col justify-between items-center gap-2">
<span className="flex font-bold items-center gap-1">
<span className="flex font-bold items-center gap-1 mr-auto">
<div className="font-light">مصوب :</div>
{formatNumber(
Math.round(
@ -655,7 +655,7 @@ export function DashboardHome() {
),
)}
</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>
{formatNumber(
Math.round(
@ -700,7 +700,7 @@ export function DashboardHome() {
</TabsContent>
<TabsContent value="canvas" className="w-ful h-full">
<div className="p-4">
<div className="p-4 h-full">
<D3ImageInfo
companies={
companyChartData.map((item) => {

View File

@ -11,6 +11,7 @@ import {
type ChartConfig,
ChartContainer,
} from "~/components/ui/chart";
import { formatNumber } from "~/lib/utils";
export type CompanyChartDatum = {
category: string; // related_company
@ -22,7 +23,7 @@ export type CompanyChartDatum = {
const chartConfig = {
capacity: {
label: "افزایش ظرفیت",
color: "#60A5FA", // Blue-400
color: "#69C8EA",
},
revenue: {
label: "افزایش درآمد",
@ -34,6 +35,7 @@ const chartConfig = {
},
} satisfies ChartConfig;
export function InteractiveBarChart({
data,
}: {
@ -47,7 +49,8 @@ export function InteractiveBarChart({
accessibilityLayer
data={data}
margin={{ left: 12, right: 12 }}
barCategoryGap="42%"
barGap={15}
barSize={8}
>
<CartesianGrid vertical={false} stroke="#475569" />
<XAxis
@ -64,14 +67,14 @@ export function InteractiveBarChart({
axisLine={false}
tickMargin={8}
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]}>
<LabelList
dataKey="capacity"
position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
formatter={(v: number) => `${Math.round(v)}%`}
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
/>
</Bar>
<Bar dataKey="revenue" fill={chartConfig.revenue.color} radius={[8, 8, 0, 0]}>
@ -79,7 +82,7 @@ export function InteractiveBarChart({
dataKey="revenue"
position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
formatter={(v: number) => `${Math.round(v)}%`}
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
/>
</Bar>
<Bar dataKey="cost" fill={chartConfig.cost.color} radius={[8, 8, 0, 0]}>
@ -87,7 +90,7 @@ export function InteractiveBarChart({
dataKey="cost"
position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
formatter={(v: number) => `${Math.round(v)}%`}
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
/>
</Bar>
</BarChart>

View File

@ -19,15 +19,7 @@ import {
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import {
ChevronUp,
ChevronDown,
RefreshCw,
Building2,
PickaxeIcon,
UserIcon,
UsersIcon,
} from "lucide-react";
import { ChevronUp, ChevronDown, RefreshCw } from "lucide-react";
import apiService from "~/lib/api";
import toast from "react-hot-toast";
import {
@ -69,6 +61,7 @@ interface DigitalInnovationMetrics {
reduce_energy_consumption_percent: string;
resource_productivity: string;
resource_productivity_percent: string;
average_project_score?: number;
}
// Normalized interface for digital innovation stats
@ -82,6 +75,8 @@ interface DigitalInnovationStats {
reduceEnergyConsumptionPercent: number;
resourceProductivity: number;
resourceProductivityPercent: number;
avarageProjectScore: number;
countInnovationDigitalProjects: number;
}
enum DigitalCardLabel {
@ -168,6 +163,8 @@ export function DigitalInnovationPage() {
reduceEnergyConsumptionPercent: 0,
resourceProductivity: 0,
resourceProductivityPercent: 0,
avarageProjectScore: 0,
countInnovationDigitalProjects: 0,
});
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "start_date",
@ -177,7 +174,7 @@ export function DigitalInnovationPage() {
new Set()
);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [avarage, setAvarage] = useState<number>(0);
// const [avarage, setAvarage] = useState<number>(0);
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
@ -288,7 +285,6 @@ export function DigitalInnovationPage() {
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
});
// console.log(JSON.parse(response.data));
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
@ -297,7 +293,7 @@ export function DigitalInnovationPage() {
if (Array.isArray(parsedData)) {
if (reset) {
setProjects(parsedData);
calculateAverage(parsedData);
// calculateAverage(parsedData);
setTotalCount(parsedData.length);
} else {
setProjects((prev) => [...prev, ...parsedData]);
@ -422,8 +418,6 @@ export function DigitalInnovationPage() {
const parsedData = JSON.parse(dataString);
if (Array.isArray(parsedData) && parsedData[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 }));
}
} catch (parseError) {
@ -440,7 +434,7 @@ export function DigitalInnovationPage() {
const fetchStats = async () => {
try {
setStatsLoading(true);
const raw = await apiService.callInnovationProcess<any>({
const raw = await apiService.call<any>({
innovation_digital_function: {},
});
@ -474,8 +468,12 @@ export function DigitalInnovationPage() {
resourceProductivityPercent: parseNum(
payload?.resource_productivity_percent
),
avarageProjectScore: parseNum(payload?.average_project_score),
countInnovationDigitalProjects: parseNum(
payload?.count_innovation_digital_projects
),
};
setActualTotalCount(normalized.countInnovationDigitalProjects);
setStats(normalized);
} catch (error) {
console.error("Error fetching stats:", error);
@ -532,7 +530,7 @@ export function DigitalInnovationPage() {
}
}, [rating]);
const ststusColor = (status: projectStatus): any => {
const statusColor = (status: projectStatus): any => {
let el = null;
switch (status) {
case projectStatus.contract:
@ -597,7 +595,7 @@ export function DigitalInnovationPage() {
return (
<div className="flex items-center gap-1">
<Badge
variant={ststusColor(value)}
variant={statusColor(value)}
className="font-medium border-2 p-0 block w-2 h-2 rounded-full"
style={{
border: "none",
@ -625,22 +623,14 @@ 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 (
<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 */}
<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">
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-2 gap-5">
{loading || statsLoading
? // Loading skeleton for stats cards - matching new design
Array.from({ length: 4 }).map((_, index) => (
@ -648,18 +638,18 @@ export function DigitalInnovationPage() {
key={`skeleton-${index}`}
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 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
className="h-6 bg-gray-600 rounded animate-pulse"
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>
</div>
<div className="flex items-center justify-center flex-col p-1">
<div className="flex items-center justify-center flex-col p-4">
<div
className="h-8 bg-gray-600 rounded mb-1 animate-pulse"
style={{ width: "40%" }}
@ -678,19 +668,19 @@ export function DigitalInnovationPage() {
key={card.id}
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 justify-between items-center border-b-2 mx-4 border-gray-500/20">
<h3 className="text-lg font-bold text-white font-persian">
<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 py-4">
{card.title}
</h3>
<div
className={`p-3 gird placeitems-center rounded-full w-fit `}
className={`gird placeitems-center rounded-full w-fit`}
>
{card.icon}
</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
className={`text-3xl font-bold ${card.color} mb-1`}
>
@ -708,11 +698,12 @@ export function DigitalInnovationPage() {
</div>
{/* 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 > */}
<CustomBarChart
title="تاثیرات نوآوری دیجیتال به صورت درصد مقایسه ای"
loading={statsLoading}
height="100%"
data={[
{
label: DigitalCardLabel.decreasCost,
@ -747,10 +738,10 @@ export function DigitalInnovationPage() {
</div>
{/* 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">
<div className="relative">
<Table containerClassName="overflow-auto custom-scrollbar h-[calc(90vh-270px)]">
<div className="relative h-full">
<Table containerClassName="overflow-auto custom-scrollbar w-full h-[36.8rem] ">
<TableHeader>
<TableRow className="bg-[#3F415A]">
{columns.map((column) => (
@ -864,30 +855,29 @@ export function DigitalInnovationPage() {
{/* Footer */}
<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="text-center gap-2 items-center flex">
<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-16">
<div className="text-base text-gray-401 mb-1">
کل پروژه ها :{formatNumber(actualTotalCount)}
کل پروژه ها : {formatNumber(actualTotalCount)}
</div>
</div>
{/* Project number column - empty */}
<div></div>
{/* Title column - empty */}
<div></div>
{/* Project status column - empty */}
<div className="flex items-center flex-row-reverse status ">
<div className="flex items-center flex-row gap-4 status w-3/5 justify-center">
<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>
{/* 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 className="text-base text-gray-400 mb-1">میانگین :</div>
<div className="font-bold">
{formatNumber(((avarage ?? 0) as number).toFixed?.(1) ?? 0)}
{formatNumber(
((stats.avarageProjectScore ?? 0) as number).toFixed?.(
1
) ?? 0
)}
</div>
</div>
</div>
</div>

View File

@ -41,8 +41,7 @@ import {
UsersIcon,
UserIcon,
RefreshCw,
Radar,
Cog,
ChevronUp,
ChevronDown,
} from "lucide-react";
@ -96,28 +95,12 @@ interface InnovationStats {
pollution_reduction: number;
pollution_reduction_percent: number;
waste_reduction: number;
waste_reduction_percent: number;
waste_reductionn_percent: number;
water_recovery_reduction: number;
water_recovery_reduction_percent: number;
}
interface GreenInnovationState {
water: {
value: any;
percent: number;
};
food: {
value: any;
percent: number;
};
power: {
value: any;
percent: number;
};
oil: {
value: any;
percent: number;
};
average_project_score: number;
count_innovation_green_projects: number;
standard_regulations: string
}
interface Params {
@ -144,6 +127,15 @@ interface ChartDataItem {
amt: number; // max value or target
}
enum projectStatus {
propozal = "پروپوزال",
contract = "پیشنویس قرارداد",
inprogress = "در حال انجام",
stop = "متوقف شده",
mafasa = "مرحله مفاصا",
finish = "پایان یافته",
}
const columns = [
{ key: "select", label: "", sortable: false, width: "50px" },
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" },
@ -178,9 +170,11 @@ export function GreenInnovationPage() {
field: "start_date",
direction: "asc",
});
const [tblAvarage, setTblAvarage] = useState<number>(0);
const [selectedProjects, setSelectedProjects] = useState<Set<string>>(
new Set()
);
const [standartRegulation, setStandardRegulation] = useState<Array<string>>([])
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] =
useState<GreenInnovationData | null>(null);
@ -199,13 +193,7 @@ export function GreenInnovationPage() {
suffix: "تن",
percent: 0,
},
oil: {
icon: <Flame className="text-emerald-400" size={"18px"} />,
label: "سوخت",
value: 0,
suffix: "متر مربع",
percent: 0,
},
power: {
icon: <Zap className="text-emerald-400" size={"18px"} />,
label: "برق",
@ -213,6 +201,13 @@ export function GreenInnovationPage() {
suffix: "میلیون مگاوات",
percent: 0,
},
oil: {
icon: <Flame className="text-emerald-400" size={"18px"} />,
label: "سوخت",
value: 0,
suffix: "متر مربع",
percent: 0,
},
});
const [sustainabilityStats, setSustainabilityStats] = useState<StatsCard>({
pollution: {
@ -273,9 +268,8 @@ export function GreenInnovationPage() {
try {
fetchingRef.current = true;
if (reset) {
setLoading(true);
setCurrentPage(1);
} else {
setLoadingMore(true);
@ -402,6 +396,9 @@ export function GreenInnovationPage() {
};
}, [loadMore, hasMore, loadingMore]);
useEffect(() => {
setLoading(true);
}, [])
const handleSort = (field: string) => {
fetchingRef.current = false;
setSortConfig((prev) => ({
@ -428,7 +425,6 @@ export function GreenInnovationPage() {
const parsedData = JSON.parse(dataString);
if (Array.isArray(parsedData) && parsedData[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 }));
}
@ -458,7 +454,7 @@ export function GreenInnovationPage() {
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch {}
} catch { }
}
const parseNum = (v: unknown): any => {
if (v == null) return 0;
@ -475,7 +471,6 @@ export function GreenInnovationPage() {
payload?.innovation_green_function
);
const stats = data[0];
const normalized: any = {
food: {
value: formatNumber(parseNum(stats?.feed_recovery_reduction)),
@ -504,9 +499,15 @@ export function GreenInnovationPage() {
waste: {
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);
} catch (error) {
console.error("Error fetching stats:", error);
@ -589,7 +590,7 @@ export function GreenInnovationPage() {
<Checkbox
checked={selectedProjects.has(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":
@ -598,7 +599,7 @@ export function GreenInnovationPage() {
variant="ghost"
size="sm"
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>
@ -619,15 +620,16 @@ export function GreenInnovationPage() {
return <span className="font-medium text-white">{String(value)}</span>;
case "project_status":
return (
<div className="flex items-center gap-1">
<Badge
variant="outline"
className="font-medium border-2"
variant={statusColor(value)}
className="font-medium border-2 p-0 block w-2 h-2 rounded-full"
style={{
border: "none",
}}
>
></Badge>
{String(value)}
</Badge>
</div>
);
case "project_rating":
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>>([
{ name: recycleParams.water.label, pv: 70, amt: 80 },
{ name: recycleParams.power.label, pv: 45, amt: 60 },
@ -657,19 +683,18 @@ export function GreenInnovationPage() {
return (
<DashboardLayout title="نوآوری سبز">
<div className="p-6 space-y-4">
<div className="p-6 space-y-4 h-[23.5rem]">
{/* Stats Cards */}
<div className="flex gap-6 mb-6">
<div className="flex flex-col gap-11 h-full w-1/2">
{/* Stats Grid */}
<div className="flex gap-6 mb-5">
<div className="flex flex-col justify-between w-1/2">
{loading || statsLoading
? // Loading skeleton for stats cards - matching new design
Array.from({ length: 2 }).map((_, index) => (
<Card
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="border-b-2 border-gray-500/20 p-2.5">
<div
@ -694,7 +719,7 @@ export function GreenInnovationPage() {
: Object.entries(sustainabilityStats).map(([key, value]) => (
<Card
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">
<div className="flex flex-col justify-between gap-2 h-full">
@ -731,7 +756,7 @@ export function GreenInnovationPage() {
{/* Process Impacts Chart */}
{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">
<div className="border-b-2 border-gray-500/20">
<div className="w-full p-4 px-6">
@ -739,7 +764,7 @@ export function GreenInnovationPage() {
</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">
{[...Array(3)].map((_, paramIndex) => (
<div
@ -758,7 +783,7 @@ export function GreenInnovationPage() {
))}
</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) => (
<div
key={barIndex}
@ -774,14 +799,14 @@ export function GreenInnovationPage() {
</CardContent>
</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">
<div className="border-b-2 border-gray-500/20">
<div className="w-full p-4 px-6">
<span>بازیافت و بازیابی منابع</span>
</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">
{Object.entries(recycleParams).map((el, index) => {
return (
@ -805,7 +830,7 @@ export function GreenInnovationPage() {
})}
</div>
<div className="h-72 w-[35rem]">
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart
width={500}
@ -844,7 +869,7 @@ export function GreenInnovationPage() {
fontSize: 14, // سایز فونت
dx: -30, // جابجایی افقی (اعداد نزدیک‌تر یا دورتر از محور)
}}
tickFormatter={(val) => `${val}%`}
tickFormatter={(val) => `${formatNumber(val)}%`}
/>
<Bar
dataKey="pv"
@ -855,7 +880,7 @@ export function GreenInnovationPage() {
position: "top",
fill: "#fff",
fontWeight: "bold",
formatter: (value) => `${value}%`,
formatter: (value: any) => `${formatNumber(value)}%`,
}}
/>
</BarChart>
@ -866,10 +891,10 @@ export function GreenInnovationPage() {
</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">
<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 ? (
<>
<span className="h-4 w-28 bg-gray-500/40 rounded animate-pulse"></span>
@ -884,25 +909,22 @@ export function GreenInnovationPage() {
</div>
</div>
<div
className={`flex flex-col gap-3 p-4 max-h-[22rem] ${
statsLoading ? "overflow-y-hidden" : "overflow-y-scroll"
}`}
>
<div className="flex flex-col gap-3 p-4 overflow-y-scroll h-[20rem]">
{statsLoading
? 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-3 w-32 bg-gray-500/40 rounded animate-pulse"></span>
</div>
))
: Array.from({ length: 10 }).map((_, index) => (
<div key={`${index}-1`} className="flex gap-2">
: standartRegulation.map((item, index) => (
<div key={`${item}-${index}-1`} className="flex flex-row flex-1 gap-2">
<LoaderCircle
size={"18px"}
className="text-emerald-400"
size={"20px"}
height={"20px"}
className="text-emerald-400 w-5 h-5 shrink-0"
/>
<span>استاندارد Iso 2005</span>
<span className="text-sm truncate">{item}</span>
</div>
))}
</div>
@ -911,7 +933,7 @@ export function GreenInnovationPage() {
</div>
{/* 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">
<div className="relative">
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(90vh-400px)]">
@ -920,13 +942,13 @@ export function GreenInnovationPage() {
{columns.map((column) => (
<TableHead
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 }}
>
{column.sortable ? (
<button
onClick={() => handleSort(column.key)}
className="flex items-center gap-2"
className="flex items-center gap-2 cursor-pointer"
>
<span>{column.label}</span>
{sortConfig.field === column.key ? (
@ -1015,8 +1037,34 @@ export function GreenInnovationPage() {
</div>
</CardContent>
{/* Footer */}
<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="text-center gap-2 items-center flex">
<div className="text-base text-gray-401 mb-1">
@ -1025,13 +1073,7 @@ export function GreenInnovationPage() {
{formatNumber(stats?.totalProjects || actualTotalCount)}
</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="text-base text-gray-400 mb-1">
{" "}
@ -1039,16 +1081,14 @@ export function GreenInnovationPage() {
</div>
<div className="font-bold">
{formatNumber(
((stats?.averageScore ?? 0) as number).toFixed?.(1) ??
stats?.averageScore ??
((tblAvarage ?? 0) as number).toFixed?.(1) ??
tblAvarage ??
0
)}
</div>
</div>
{/* Details column - show total count */}
</div>
</div>
</div> */}
</Card>
</div>
@ -1128,26 +1168,6 @@ export function GreenInnovationPage() {
{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">
<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>
</DialogContent>

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@ export function CustomBarChart({
}: CustomBarChartProps) {
// Calculate the maximum value across all data points for consistent scaling
const globalMaxValue = Math.max(
...data.map((item) => item.maxValue || item.value),
...data.map((item) => item.maxValue || item.value)
);
// 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="space-y-4">
<div className="space-y-4 flex flex-col gap-4">
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="flex items-center gap-3">
{/* Label skeleton */}
@ -86,7 +86,8 @@ export function CustomBarChart({
<div key={index} className="flex items-center gap-3">
{/* Label */}
<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}
@ -97,7 +98,8 @@ export function CustomBarChart({
className={`flex-1 flex items-center bg-gray-700 rounded-full relative overflow-hidden ${barHeight}`}
>
<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={{
width: `${Math.min(percentage, 100)}%`,
@ -110,7 +112,8 @@ export function CustomBarChart({
{/* Value Label */}
<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 ${
item.color?.includes("emerald")
? "text-emerald-400"
: item.color?.includes("blue")
? "text-blue-400"
@ -124,7 +127,7 @@ export function CustomBarChart({
}`}
>
{item.valuePrefix || ""}
{formatNumber(parseFloat(displayValue))}
{formatNumber(parseFloat(displayValue))}%
{item.valueSuffix || ""}
</span>
</div>

View File

@ -45,7 +45,7 @@ const DialogContent = React.forwardRef<
>
{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">
<X className="h-4 w-4" />
<X className="h-4 w-4 cursor-pointer" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>

View File

@ -6,15 +6,19 @@ export default [
route("dashboard/project-management", "routes/project-management.tsx"),
route(
"dashboard/innovation-basket/process-innovation",
"routes/innovation-basket.process-innovation.tsx",
"routes/innovation-basket.process-innovation.tsx"
),
route(
"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(
"/dashboard/innovation-basket/digital-innovation",
"routes/digital-innovation-page.tsx",
"routes/digital-innovation-page.tsx"
),
route("dashboard/ecosystem", "routes/ecosystem.tsx"),
route("404", "routes/404.tsx"),

View 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

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"build": "react-router build",
"dev": "react-router dev --port 3000",
"dev": "react-router dev --port 3001",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},