feat: contineu developing page innterbal-innovation

This commit is contained in:
MehrdadAdabi 2025-09-04 13:34:58 +03:30
parent 49f018e56f
commit 73f960b56a
2 changed files with 362 additions and 187 deletions

View File

@ -1168,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>

View File

@ -23,28 +23,27 @@ import apiService from "~/lib/api";
import toast from "react-hot-toast";
import {
FilterIcon,
Key,
Sparkle,
Zap,
Flame,
Building2,
PickaxeIcon,
UsersIcon,
UserIcon,
RefreshCw,
ChevronUp,
ChevronDown,
Handshake,
CodeXml,
SquareUser,
Users,
User,
} from "lucide-react";
import DashboardLayout from "../layout";
import { LineChart, CartesianGrid, Legend, Line, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, Customized } from "recharts";
moment.loadPersian({ usePersianDigits: true });
interface GreenInnovationData {
WorkflowID: string;
interface innovationBuiltInDate {
WorkflowID: number;
approved_budget: string;
done_date: string | null;
observer: string;
project_description: string;
project_id: string;
project_id: number | null;
project_no: string;
project_rating: string;
project_status: string;
@ -52,6 +51,25 @@ interface GreenInnovationData {
title: string;
}
interface DialogInfo {
WorkflowID: number
collaboration_model: string; // مشارکت استراتژیک
complexity_level: string; // فناوری سطح متوسط
developer_team_role: string; // نظارت
number_employees_involved: number | null; // ممکن است null باشد
participants_full_name: string; // مریم احمدی
project_description: string; // توضیحات پروژه
project_id: string; // "20922"
project_no: string; // "13"
project_rating: string; // "68" (اگر میخوای عدد باشد: number)
project_status: string; // پروپوزال
role_company_staff: string | null; // ممکن است null باشد
technology_maturity_level: string; // مرحله بلوغ
title: string; // عنوان پروژه
technology_params?: Array<TechnologyParameter>
}
interface SortConfig {
field: string;
direction: "asc" | "desc";
@ -112,28 +130,11 @@ interface InnovationStats {
resolved_bottleneck_count: number;
}
interface Params {
icon: any;
label: string;
value: number;
suffix: string;
percent: number;
}
interface RecycleParams {
water: Params;
food: Params;
power: Params;
oil: Params;
}
interface stateCounter {
totalProjects: number;
}
interface ChartDataItem {
name: string;
pv: any; // actual value
amt: number; // max value or target
interface TechnologyParameter {
WorkflowID: number;
domestic_technology_parameter_value: string;
foreign_technology_parameter_value: string;
technology_parameter_title: string;
}
enum projectStatus {
@ -164,8 +165,16 @@ const columns = [
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" },
];
const data = [
{ name: 'افول', value: 10 },
{ name: 'رشد', value: 20 },
{ name: 'بلوغ', value: 15 },
{ name: 'معرفی', value: 25 },
];
export function InnovationBuiltInsidePage() {
const [projects, setProjects] = useState<GreenInnovationData[]>([]);
const [projects, setProjects] = useState<innovationBuiltInDate[]>([]);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
@ -174,7 +183,7 @@ export function InnovationBuiltInsidePage() {
const [totalCount, setTotalCount] = useState(0);
const [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false);
const [stats, setStats] = useState<stateCounter>();
// const [stats, setStats] = useState<stateCounter>();
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "start_date",
direction: "asc",
@ -185,11 +194,9 @@ export function InnovationBuiltInsidePage() {
);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] =
useState<GreenInnovationData | null>(null);
useState<DialogInfo>();
const [innovationMetric, setInnovationMetric] = useState({
})
const [sustainabilityStats, setSustainabilityStats] = useState<StatsCard>({
currencySaving: {
@ -242,6 +249,8 @@ export function InnovationBuiltInsidePage() {
})
const [showDialogItems, setShowDialogItems] = useState<boolean>(false)
const [countOfHighTech, setCountOfHighTech] = useState(0)
const observerRef = useRef<HTMLDivElement>(null);
@ -257,11 +266,20 @@ export function InnovationBuiltInsidePage() {
setSelectedProjects(newSelected);
};
const handleProjectDetails = (project: GreenInnovationData) => {
setSelectedProjectDetails(project);
const handleProjectDetails = async (project: DialogInfo) => {
setShowDialogItems(true)
setDetailsDialogOpen(true);
setSelectedProjectDetails(project);
await fetchDialogTbl(project.WorkflowID)
setTimeout(() => {
setShowDialogItems(false)
calculateProgressBar(+project.project_rating)
}, 500);
};
const formatNumber = (value: string | number) => {
if (!value) return "0";
const numericValue = typeof value === "string" ? parseFloat(value) : value;
@ -292,10 +310,13 @@ export function InnovationBuiltInsidePage() {
"project_status",
"project_rating",
"project_description",
"start_date",
"done_date",
"approved_budget",
"observer"
"complexity_level",
"collaboration_model",
"developer_team_role",
"role_company_staff",
"number_employees_involved",
"participants_full_name",
"technology_maturity_level"
],
Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [["type_of_innovation", "=", "نوآوری ساخت داخل"]],
@ -360,6 +381,43 @@ export function InnovationBuiltInsidePage() {
}
};
const fetchDialogTbl = async (tblId: number) => {
try {
const response = await apiService.select({
ProcessName: "technology_parameter",
OutputFields: [
"technology_parameter_title",
"domestic_technology_parameter_value",
"foreign_technology_parameter_value"
],
Conditions: [["project_id", "=", tblId]],
});
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
const parsedData = JSON.parse(dataString);
setSelectedProjectDetails((prev: any) => ({
...prev,
technology_params: parsedData
}));
}
}
} catch (error) {
console.error("Error fetching projects:", error);
toast.error("خطا در دریافت اطلاعات پروژه‌ها");
}
};
const calculateProgressBar = (val: number) => {
const pointer = document.getElementsByClassName('progressBarPointer')[0] as HTMLElement;
if (!pointer) return;
const leftValue = val !== 0 ? `calc(${val}% - 100px)` : `calc(0% - 40px)`;
pointer.style.left = leftValue;
};
const loadMore = useCallback(() => {
if (!loadingMore && hasMore && !loading) {
setCurrentPage((prev) => prev + 1);
@ -368,7 +426,6 @@ export function InnovationBuiltInsidePage() {
useEffect(() => {
fetchProjects(true);
fetchTotalCount();
fetchStats();
}, [sortConfig, selectedProjects]);
@ -418,34 +475,7 @@ export function InnovationBuiltInsidePage() {
setHasMore(true);
};
const fetchTotalCount = async () => {
try {
const response = await apiService.select({
ProcessName: "project",
OutputFields: ["count(project_no)"],
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
});
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
try {
const parsedData = JSON.parse(dataString);
if (Array.isArray(parsedData) && parsedData[0]) {
const count = parsedData[0].project_no_count || 0;
// Keep stats in sync if backend stats not yet loaded
setStats((prev) => ({ ...prev, totalProjects: count }));
}
} catch (parseError) {
console.error("Error parsing count data:", parseError);
}
}
}
} catch (error) {
console.error("Error fetching total count:", error);
}
};
// Fetch aggregated stats from backend call API (innovation_process_function)
const fetchStats = async () => {
try {
setStatsLoading(true);
@ -561,8 +591,8 @@ export function InnovationBuiltInsidePage() {
setCountOfHighTech(normalized.countOfHighTech)
};
const renderCellContent = (item: GreenInnovationData, column: any) => {
const value = item[column.key as keyof GreenInnovationData];
const renderCellContent = (item: innovationBuiltInDate, column: any) => {
const value = item[column.key as keyof innovationBuiltInDate];
switch (column.key) {
case "select":
@ -654,12 +684,6 @@ export function InnovationBuiltInsidePage() {
return el;
};
// const [chartData, setChartData] = useState<Array<ChartDataItem>>([
// { name: recycleParams.water.label, pv: 70, amt: 80 },
// { name: recycleParams.power.label, pv: 45, amt: 60 },
// { name: recycleParams.oil.label, pv: 90, amt: 75 },
// { name: recycleParams.food.label, pv: 30, amt: 50 },
// ]);
return (
<DashboardLayout title="نوآوری ساخت داخل">
@ -807,8 +831,6 @@ export function InnovationBuiltInsidePage() {
</Card>
}
{
loading ? <div className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-30 rounded-lg backdrop-blur-sm border border-gray-700/50 animate-pulse mt-4">
<div className="h-full flex flex-row justify-between p-6 items-center w-4/5 m-auto">
@ -959,110 +981,283 @@ export function InnovationBuiltInsidePage() {
</div>
</div>
</Card>
</div>
{/* Project Details Dialog */}
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-4xl overflow-y-auto">
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-5xl overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
شرح پروژه
</DialogTitle>
</DialogHeader>
<div className="space-y-4 flex justify-between text-right px-6">
{/* Project Description */}
<div className="flex-[4] border-l-2 border-gray-600">
<h2 className="font-bold">{selectedProjectDetails?.title}</h2>
<p className="text-gray-300 font-persian px-1 mt-2">
{selectedProjectDetails?.project_description || "-"}
</p>
<div className="flex justify-between text-right px-4">
<div className="flex-[4] border-l-2 border-gray-600 pl-8">
{
showDialogItems ? <div className="animate-pulse flex flex-col gap-2">
<div className="flex flex-col gap-2 mt-2">
<div className="h-4 w-full bg-gray-600 rounded"></div>
<div className="h-4 w-5/6 bg-gray-600 rounded"></div>
<div className="h-4 w-11/12 bg-gray-600 rounded"></div>
</div>
</div> : <div>
<h2 className="font-bold">{selectedProjectDetails?.title}</h2>
<p className="text-gray-300 font-persian mt-2 text-justify">
{selectedProjectDetails?.project_description || "-"}
</p>
</div>
}
<h2 className="font-bold my-6">دانش فنی محصول جدید</h2>
{
showDialogItems ? <div className="newProductTechKnowledge flex flex-col gap-4 h-max w-full animate-pulse">
<div className="w-max relative">
<div className="range flex flex-row-reverse rounded-xl overflow-hidden justify-center">
<div className="bg-gray-500 w-28 h-10 p-4 text-center flex items-center justify-center">
</div>
<div className="bg-gray-400 w-28 h-10 p-4 text-center flex items-center justify-center">
</div>
<div className="bg-gray-300 w-28 h-10 p-4 text-center flex items-center justify-center">
</div>
</div>
<div className="progressBarPointer absolute z-[200] top-0.5 left-0.5">
<div className="flex flex-col justify-center items-center">
<span className="block w-0.5 h-14 bg-gray-400"></span>
<span className="text-gray-400 border border-gray-400 p-1 px-2 text-xs rounded-lg">
سطح تکنولوژی
</span>
</div>
</div>
</div>
</div> : <div className="newProductTechKnowledge flex flex-col gap-4 h-max w-full ">
<div className="w-max relative transition-all duration-300">
<div className="range flex flex-row-reverse rounded-xl overflow-hidden justify-center">
<div className="bg-emerald-700 w-28 h-10 p-4 text-center text-sm flex items-center justify-center">
<span className="text-gray-700">سطح پایین</span>
</div>
<div className="bg-emerald-600 w-28 h-10 p-4 text-center text-sm flex items-center justify-center">
<span className="text-gray-700">سطح متوسط</span>
</div>
<div className="bg-emerald-400 w-28 h-10 p-4 text-center text-sm flex items-center justify-center">
<span className="text-gray-700">سطح بالا</span>
</div>
</div>
<div className='progressBarPointer absolute z-[200] top-0.5 transition-all duration-300'>
<div className="flex flex-col justify-center items-center">
<span className="block w-0.5 h-14 bg-white"></span>
<span className="text-white border border-white p-1 px-2 text-xs rounded-lg"> سطح تکنولوژی</span>
</div>
</div>
</div>
</div>
}
<div className="flex flex-col gap-5 mt-20">
<h2 className="font-bold">مشارکت در پروژه</h2>
{
showDialogItems ? <div className="space-y-3">
{[...Array(4)].map((_, i) => (
<div key={i} className="flex flex-row gap-2 items-center">
<div className="h-5 w-5 bg-gray-500 rounded-full"></div>
<div className="flex flex-row justify-between w-full gap-2">
<div className="h-4 w-24 bg-gray-600 rounded"></div>
<div className="h-4 w-20 bg-gray-500 rounded"></div>
</div>
</div>
))}
</div> : <div className="flex flex-col gap-5">
<div className="content flex flex-col gap-3 w-5/6">
<div className="flex flex-row gap-2 w-full">
<Handshake className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full">
<span>مدل همکاری:</span>
<span>{selectedProjectDetails?.collaboration_model}</span>
</div>
</div>
<div className="flex flex-row gap-2 w-full">
<CodeXml className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full">
<span>نقش تیم توسعه دهنده:</span>
<span>{selectedProjectDetails?.developer_team_role}</span>
</div>
</div>
<div className="flex flex-row gap-2 w-full">
<SquareUser className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full">
<span>نقش کارکنان شرکت: </span>
<span>نظارت کننده</span>
</div>
</div>
<div className="flex flex-row gap-2 w-full">
<Users className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full">
<span>تعداد کارکنان درگیر: </span>
<span className="persian-font">{selectedProjectDetails?.number_employees_involved ?? 0}</span>
</div>
</div>
</div>
<div className="flex flex-row gap-2 w-full">
<User className="text-emerald-500 w-5" />
<div className="flex flex-col justify-between w-full gap-1">
<span>لیست کارکنان : </span>
<span className="pr-1">{selectedProjectDetails?.participants_full_name}</span>
</div>
</div>
</div>
}
</div>
</div>
{/* Project Details */}
<div className="flex flex-[3] gap-2 flex-col px-4">
<div className="font-bold text-right ">جزئیات پروژه</div>
<div className="px-4 pl-0 flex flex-col gap-4 w-1/2">
{
showDialogItems ? <div className="flex flex-col gap-2 animate-pulse">
{[...Array(5)].map((_, rowIndex) => (
<div key={`skeleton-${rowIndex}`}>
<div className="h-6 bg-gray-700 rounded"></div>
</div>
))}
</div> : <div className="tbl rounded-xl overflow-hidden">
<div className="grid grid-cols-3 bg-[#3F415A] text-white">
<span className="text-md text-center p-3">شاخص مقایسه با نمونه خارجی</span>
<span className="text-md text-center p-3">نمونه داخلی</span>
<span className="text-md text-center p-3">نمونه خارجی</span>
</div>
<div className="flex flex-col divide-y divide-gray-600 overflow-auto max-h-[6rem]">
{
selectedProjectDetails?.technology_params?.map((el, index) => {
return <div className="grid grid-cols-3 py-3" key={`technology-${index}-${el.WorkflowID}`}>
<span className="text-center">{el.technology_parameter_title}</span>
<div className="flex flex-row items-center gap-1 justify-center">
<span>{el.domestic_technology_parameter_value}</span>
<span className="text-sm text-gray-400">میلیون لیتر</span>
</div>
<div className="flex flex-row items-center gap-1 justify-center">
<span>{el.foreign_technology_parameter_value}</span>
<span className="text-sm text-gray-400">میلیون لیتر</span>
</div>
</div>
})
}
</div>
</div>
}
<div style={{ width: '100%', height: 400 }}>
{
showDialogItems ? <div className="relative h-96 w-full bg-gray-700 rounded-lg overflow-hidden animate-pulse">
{[...Array(4)].map((_, i) => (
<div
key={i}
className="absolute left-0 w-full h-1 bg-gray-600 rounded"
style={{
top: `${20 + i * 20}%`,
transform: `scaleX(${0.8 + i * 0.05})`,
}}
/>
))}
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute bg-gray-500 rounded-full"
style={{
width: '10px',
height: '10px',
left: `${10 + i * 18}%`,
top: `${30 + (i % 2) * 20}%`,
}}
/>
))}
</div> : <ResponsiveContainer width="100%" height={420}>
<LineChart
data={data}
margin={{ top: 20, right: 20, left: 20, bottom: 80 }}
>
<XAxis
dataKey="name"
interval={0}
padding={{ right: 10 }}
axisLine={false}
tick={{ fill: "white" }}
/>
<Line type="monotone" dataKey="value" stroke="#ff4d6d" strokeWidth={2} dot={false} />
<ReferenceLine x="بلوغ" stroke="#00bc7c" strokeWidth={2} />
<Customized
component={(props: any) => {
const { xAxisMap, width, height } = props;
const xAxes = Object.values(xAxisMap || {});
if (!xAxes.length) return null;
const xAxis: any = xAxes[0];
const ticks = xAxis?.ticks || [];
const tick = ticks.find((t: any) => {
return t && (t.value === "بلوغ" || (t.payload && t.payload.value === "بلوغ"));
});
const x = (tick && tick.coordinate) ?? width / 2 + 80
const rectWidth = 140;
const rectHeight = 28;
const rectX = x - rectWidth / 2;
const axisHeight = xAxis?.height ?? 40;
const rectY = (height - axisHeight) - 30;
return (
<g>
<rect
x={rectX}
y={rectY}
width={rectWidth}
height={rectHeight}
fill="none"
stroke="#00bc7c"
strokeWidth={2}
rx={6}
/>
<text
x={x}
y={rectY + rectHeight / 2}
fill="white"
fontSize={12}
textAnchor="middle"
alignmentBaseline="middle"
>
سطح بلوغ تکنولوژی
</text>
</g>
);
}}
/>
</LineChart>
</ResponsiveContainer>
}
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
<Building2 className="h-4 text-green-500" />
زمان شروع:
</h4>
<span className="text-white font-bold font-persian">
{selectedProjectDetails?.start_date
? moment(
selectedProjectDetails?.start_date,
"YYYY-MM-DD"
).format("YYYY/MM/DD")
: "-"}
</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">
<PickaxeIcon className="h-4 text-green-500" />
زمان پایان:
</h4>
<span className="text-white font-bold font-persian">
{selectedProjectDetails?.done_date
? moment(
selectedProjectDetails?.done_date,
"YYYY-MM-DD"
).format("YYYY/MM/DD")
: "-"}
</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">
<UsersIcon className="h-4 text-green-500" />
هزینه برآورد شده:
</h4>
<span className="text-white font-bold font-persian">
{formatNumber(
Number(
selectedProjectDetails?.approved_budget.replaceAll(
",",
""
)
)
) || "-"}
</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">
<UserIcon 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">
<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>
</Dialog>
</DashboardLayout>
</DashboardLayout >
);
}