feat/digital-innovation (#1)

Co-authored-by: MehrdadAdabi <126083584+mehrdadAdabi@users.noreply.github.com>
Co-authored-by: mehrdad <admehrdad148113@gmail.com>
Reviewed-on: https://git.pelekan.org/Saeed0920/inogen/pulls/1
This commit is contained in:
Saeed AB 2025-08-27 13:36:31 +03:30
parent b5c734ebce
commit 20ee1f54bb
10 changed files with 8087 additions and 110 deletions

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,6 @@ import {
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Funnel, Wrench, CirclePause, DollarSign } from "lucide-react"; import { Funnel, Wrench, CirclePause, DollarSign } from "lucide-react";
import ProjectDetail from "../projects/project-detail";
moment.loadPersian({ usePersianDigits: true }); moment.loadPersian({ usePersianDigits: true });
interface ProcessInnovationData { interface ProcessInnovationData {
@ -150,7 +149,6 @@ export function ProcessInnovationPage() {
}; };
const handleProjectDetails = (project: ProcessInnovationData) => { const handleProjectDetails = (project: ProcessInnovationData) => {
console.log(project);
setSelectedProjectDetails(project); setSelectedProjectDetails(project);
setDetailsDialogOpen(true); setDetailsDialogOpen(true);
}; };
@ -169,7 +167,7 @@ export function ProcessInnovationPage() {
title: "جلوگیری از توقفات تولید", title: "جلوگیری از توقفات تولید",
value: formatNumber( value: formatNumber(
stats.productionStopsPreventionSum.toFixed?.(1) ?? stats.productionStopsPreventionSum.toFixed?.(1) ??
stats.productionStopsPreventionSum, stats.productionStopsPreventionSum,
), ),
description: "تن افزایش یافته", description: "تن افزایش یافته",
icon: <CirclePause />, icon: <CirclePause />,
@ -199,7 +197,7 @@ export function ProcessInnovationPage() {
title: "کاهش خرابی های پرتکرار", title: "کاهش خرابی های پرتکرار",
value: formatNumber( value: formatNumber(
stats.frequentFailuresReductionSum.toFixed?.(1) ?? stats.frequentFailuresReductionSum.toFixed?.(1) ??
stats.frequentFailuresReductionSum, stats.frequentFailuresReductionSum,
), ),
description: "مجموع درصد کاهش خرابی", description: "مجموع درصد کاهش خرابی",
icon: <Wrench />, icon: <Wrench />,
@ -402,7 +400,7 @@ export function ProcessInnovationPage() {
if (typeof payload === "string") { if (typeof payload === "string") {
try { try {
payload = JSON.parse(payload); payload = JSON.parse(payload);
} catch {} } catch { }
} }
const parseNum = (v: unknown): number => { const parseNum = (v: unknown): number => {
@ -575,73 +573,73 @@ export function ProcessInnovationPage() {
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{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-2">
<div className="flex flex-col justify-between gap-2"> <div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20"> <div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
<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="p-3 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 className="flex items-center justify-center flex-col p-1">
<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> </div>
</CardContent> <div className="flex items-center justify-center flex-col p-1">
</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>
))
: statsCards.map((card) => ( : statsCards.map((card) => (
<Card <Card
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-2">
<div className="flex flex-col justify-between gap-2"> <div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20"> <div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
<h3 className="text-lg font-bold text-white font-persian"> <h3 className="text-lg font-bold text-white font-persian">
{card.title} {card.title}
</h3> </h3>
<div <div
className={`p-3 gird placeitems-center rounded-full w-fit `} className={`p-3 gird placeitems-center rounded-full w-fit `}
> >
{card.icon} {card.icon}
</div>
</div>
<div className="flex items-center justify-center flex-col p-1">
<p
className={`text-3xl font-bold ${card.color} mb-1`}
>
{card.value}
</p>
<p className="text-sm text-gray-300 font-persian">
{card.description}
</p>
</div> </div>
</div> </div>
</CardContent> <div className="flex items-center justify-center flex-col p-1">
</Card> <p
))} className={`text-3xl font-bold ${card.color} mb-1`}
>
{card.value}
</p>
<p className="text-sm text-gray-300 font-persian">
{card.description}
</p>
</div>
</div>
</CardContent>
</Card>
))}
</div> </div>
</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-2xl w-full overflow-hidden">
<CardContent className="p-4"> <CardContent >
<CustomBarChart <CustomBarChart
title="تاثیرات فرآیندی به صورت درصد مقایسه ای" title="تاثیرات فرآیندی به صورت درصد مقایسه ای"
loading={statsLoading} loading={statsLoading}
@ -842,8 +840,8 @@ export function ProcessInnovationPage() {
<div className="font-bold"> <div className="font-bold">
{formatNumber( {formatNumber(
((stats.averageScore ?? 0) as number).toFixed?.(1) ?? ((stats.averageScore ?? 0) as number).toFixed?.(1) ??
stats.averageScore ?? stats.averageScore ??
0, 0,
)} )}
</div> </div>
</div> </div>
@ -883,9 +881,9 @@ export function ProcessInnovationPage() {
<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>
@ -898,9 +896,9 @@ export function ProcessInnovationPage() {
<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>

View File

@ -149,7 +149,7 @@ export function Sidebar({
React.useEffect(() => { React.useEffect(() => {
const autoExpandParents = () => { const autoExpandParents = () => {
const newExpandedItems: string[] = []; const newExpandedItems: string[] = [];
menuItems.forEach((item) => { menuItems.forEach((item) => {
if (item.children) { if (item.children) {
const hasActiveChild = item.children.some( const hasActiveChild = item.children.some(
@ -160,10 +160,10 @@ export function Sidebar({
} }
} }
}); });
setExpandedItems(newExpandedItems); setExpandedItems(newExpandedItems);
}; };
autoExpandParents(); autoExpandParents();
}, [location.pathname]); }, [location.pathname]);
@ -200,8 +200,8 @@ export function Sidebar({
const renderMenuItem = (item: MenuItem, level = 0) => { const renderMenuItem = (item: MenuItem, level = 0) => {
const isActive = isActiveRoute(item.href, item.children); const isActive = isActiveRoute(item.href, item.children);
const isExpanded = expandedItems.includes(item.id) || const isExpanded = expandedItems.includes(item.id) ||
(item.children && item.children.some(child => (item.children && item.children.some(child =>
child.href && location.pathname === child.href child.href && location.pathname === child.href
)); ));
const hasChildren = item.children && item.children.length > 0; const hasChildren = item.children && item.children.length > 0;
@ -265,14 +265,14 @@ export function Sidebar({
</div> </div>
</Link> </Link>
) : ( ) : (
<button <button
className={cn( className={cn(
"w-full text-right", "w-full text-right",
// Disable pointer cursor when child is active (cannot collapse) // Disable pointer cursor when child is active (cannot collapse)
item.children && item.children.some(child => item.children && item.children.some(child =>
child.href && location.pathname === child.href child.href && location.pathname === child.href
) && "cursor-not-allowed" ) && "cursor-not-allowed"
)} )}
onClick={handleClick} onClick={handleClick}
> >
<div <div
@ -313,7 +313,7 @@ export function Sidebar({
"w-4 h-4 transition-transform duration-200", "w-4 h-4 transition-transform duration-200",
isExpanded ? "rotate-180" : "rotate-0", isExpanded ? "rotate-180" : "rotate-0",
// Show different color when child is active (cannot collapse) // Show different color when child is active (cannot collapse)
item.children && item.children.some(child => item.children && item.children.some(child =>
child.href && location.pathname === child.href child.href && location.pathname === child.href
) ? "text-emerald-400" : "text-current" ) ? "text-emerald-400" : "text-current"
)} )}
@ -361,7 +361,7 @@ export function Sidebar({
{!isCollapsed ? ( {!isCollapsed ? (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<GalleryVerticalEnd <GalleryVerticalEnd
color="black" color="black"
size={32} size={32}
strokeWidth={1} strokeWidth={1}
className="bg-green-400 p-1.5 rounded-lg" className="bg-green-400 p-1.5 rounded-lg"

View File

@ -38,7 +38,7 @@ export function CustomBarChart({
// Loading skeleton // Loading skeleton
if (loading) { if (loading) {
return ( return (
<div className={`space-y-6 ${className}`} style={{ height }}> <div className={`space-y-6 p-4 ${className}`} style={{ height }}>
{title && ( {title && (
<div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div> <div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div>
)} )}
@ -68,13 +68,15 @@ export function CustomBarChart({
return ( return (
<div className={`space-y-6 ${className}`} style={{ height }}> <div className={`space-y-6 ${className}`} style={{ height }}>
{title && ( <div className="border-b">
<h3 className="text-xl font-bold text-white font-persian text-right mb-4"> {title && (
{title} <h3 className="text-xl font-bold text-white font-persian text-right p-4">
</h3> {title}
)} </h3>
)}
</div>
<div className="space-y-4"> <div className="space-y-4 px-4 pb-4">
{data.map((item, index) => { {data.map((item, index) => {
const percentage = const percentage =
globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0; globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0;
@ -84,9 +86,8 @@ 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 ${ className={`font-persian text-sm min-w-[160px] text-right ${item.labelColor || "text-white"
item.labelColor || "text-white" }`}
}`}
> >
{item.label} {item.label}
</span> </span>
@ -96,9 +97,8 @@ 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 ${ className={`${barHeight} rounded-full transition-all duration-700 ease-out relative ${item.color || "bg-emerald-400"
item.color || "bg-emerald-400" }`}
}`}
style={{ style={{
width: `${Math.min(percentage, 100)}%`, width: `${Math.min(percentage, 100)}%`,
}} }}
@ -110,19 +110,18 @@ export function CustomBarChart({
{/* Value Label */} {/* Value Label */}
<span <span
className={`font-bold text-sm min-w-[60px] text-left ${ className={`font-bold text-sm min-w-[60px] text-left ${item.color?.includes("emerald")
item.color?.includes("emerald") ? "text-emerald-400"
? "text-emerald-400" : item.color?.includes("blue")
: item.color?.includes("blue") ? "text-blue-400"
? "text-blue-400" : item.color?.includes("purple")
: item.color?.includes("purple") ? "text-purple-400"
? "text-purple-400" : item.color?.includes("red")
: item.color?.includes("red") ? "text-red-400"
? "text-red-400" : item.color?.includes("yellow")
: item.color?.includes("yellow") ? "text-yellow-400"
? "text-yellow-400" : "text-emerald-400"
: "text-emerald-400" }`}
}`}
> >
{item.valuePrefix || ""} {item.valuePrefix || ""}
{formatNumber(parseFloat(displayValue))} {formatNumber(parseFloat(displayValue))}

View File

@ -11,7 +11,7 @@ const Table = React.forwardRef<HTMLTableElement, TableProps>(
<div className={cn("relative w-full", containerClassName)}> <div className={cn("relative w-full", containerClassName)}>
<table <table
ref={ref} ref={ref}
className={cn("w-full caption-bottom text-sm", className)} className={cn("w-full caption-bottom text-sm h-full", className)}
{...props} {...props}
/> />
</div> </div>

View File

@ -4,7 +4,14 @@ export default [
route("login", "routes/login.tsx"), route("login", "routes/login.tsx"),
route("dashboard", "routes/dashboard.tsx"), route("dashboard", "routes/dashboard.tsx"),
route("dashboard/project-management", "routes/project-management.tsx"), route("dashboard/project-management", "routes/project-management.tsx"),
route("dashboard/innovation-basket/process-innovation", "routes/innovation-basket.process-innovation.tsx"), route(
"dashboard/innovation-basket/process-innovation",
"routes/innovation-basket.process-innovation.tsx"
),
route(
"/dashboard/innovation-basket/digital-innovation",
"routes/digital-innovation-page.tsx"
),
route("projects", "routes/projects.tsx"), route("projects", "routes/projects.tsx"),
route("dashboard/ecosystem", "routes/ecosystem.tsx"), route("dashboard/ecosystem", "routes/ecosystem.tsx"),
route("404", "routes/404.tsx"), route("404", "routes/404.tsx"),

View File

@ -0,0 +1,17 @@
import { ProtectedRoute } from "~/components/auth/protected-route";
import DigitalInnovationPage from "~/components/dashboard/project-management/digital-innovation-page";
export function meta() {
return [
{ title: "نوآوری در فرآیند - سیستم مدیریت فناوری و نوآوری" },
{ name: "description", content: "مدیریت پروژه‌های نوآوری در فرآیند" },
];
}
export default function ProcessInnovation() {
return (
<ProtectedRoute requireAuth={true}>
<DigitalInnovationPage />
</ProtectedRoute>
);
}

View File

@ -2,7 +2,7 @@ import type { Route } from "./+types/project-management";
import { ProjectManagementPage } from "~/components/dashboard/project-management/project-management-page"; import { ProjectManagementPage } from "~/components/dashboard/project-management/project-management-page";
import { ProtectedRoute } from "~/components/auth/protected-route"; import { ProtectedRoute } from "~/components/auth/protected-route";
export function meta({}: Route.MetaArgs) { export function meta({ }: Route.MetaArgs) {
return [ return [
{ title: "مدیریت پروژه‌ها - سیستم مدیریت فناوری و نوآوری" }, { title: "مدیریت پروژه‌ها - سیستم مدیریت فناوری و نوآوری" },
{ name: "description", content: "مدیریت و نظارت بر پروژه‌های فناوری و نوآوری" }, { name: "description", content: "مدیریت و نظارت بر پروژه‌های فناوری و نوآوری" },

6859
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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",
"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"
}, },