Merge branch 'main' of http://git.sepehrdata.com/Saeed0920/inogen
This commit is contained in:
commit
f1b4d313eb
|
|
@ -441,13 +441,38 @@ export function DigitalInnovationPage() {
|
||||||
innovation_digital_function: {},
|
innovation_digital_function: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
let payload: DigitalInnovationMetrics = raw?.data;
|
|
||||||
if (typeof payload === "string") {
|
|
||||||
|
// let payload: DigitalInnovationMetrics = raw?.data;
|
||||||
|
// console.log("*-*-*-*" +payload);
|
||||||
|
// if (typeof payload === "string") {
|
||||||
|
// try {
|
||||||
|
// payload = JSON.parse(payload).innovation_digital_function;
|
||||||
|
|
||||||
|
// } catch {}
|
||||||
|
// }
|
||||||
|
|
||||||
|
let payload: DigitalInnovationMetrics | null = null;
|
||||||
|
|
||||||
|
if (raw?.data) {
|
||||||
try {
|
try {
|
||||||
payload = JSON.parse(payload);
|
// مرحله اول: data رو از string به object تبدیل کن
|
||||||
} catch {}
|
const parsedData = JSON.parse(raw.data);
|
||||||
|
|
||||||
|
// مرحله دوم: innovation_digital_function رو که خودش string هست parse کن
|
||||||
|
const arr = JSON.parse(parsedData.innovation_digital_function);
|
||||||
|
|
||||||
|
// مرحله سوم: اولین خانه آرایه رو بردار
|
||||||
|
if (Array.isArray(arr) && arr.length > 0) {
|
||||||
|
payload = arr[0];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error parsing API response:", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const parseNum = (v: unknown): number => {
|
const parseNum = (v: unknown): number => {
|
||||||
if (v == null) return 0;
|
if (v == null) return 0;
|
||||||
if (typeof v === "number") return v;
|
if (typeof v === "number") return v;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import moment from "moment-jalaali";
|
// import moment from "moment-jalaali";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Bar,
|
Bar,
|
||||||
|
|
@ -47,7 +47,7 @@ import apiService from "~/lib/api";
|
||||||
import { formatCurrency } from "~/lib/utils";
|
import { formatCurrency } from "~/lib/utils";
|
||||||
import DashboardLayout from "../layout";
|
import DashboardLayout from "../layout";
|
||||||
|
|
||||||
moment.loadPersian({ usePersianDigits: true });
|
// moment.loadPersian({ usePersianDigits: true });
|
||||||
interface GreenInnovationData {
|
interface GreenInnovationData {
|
||||||
WorkflowID: string;
|
WorkflowID: string;
|
||||||
approved_budget: string;
|
approved_budget: string;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import moment from "moment-jalaali";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
|
@ -43,8 +42,6 @@ import apiService from "~/lib/api";
|
||||||
import { formatCurrency } from "~/lib/utils";
|
import { formatCurrency } from "~/lib/utils";
|
||||||
import DashboardLayout from "../layout";
|
import DashboardLayout from "../layout";
|
||||||
|
|
||||||
moment.loadPersian({ usePersianDigits: true });
|
|
||||||
|
|
||||||
interface innovationBuiltInDate {
|
interface innovationBuiltInDate {
|
||||||
WorkflowID: number;
|
WorkflowID: number;
|
||||||
approved_budget: string;
|
approved_budget: string;
|
||||||
|
|
@ -194,9 +191,8 @@ export function InnovationBuiltInsidePage() {
|
||||||
direction: "asc",
|
direction: "asc",
|
||||||
});
|
});
|
||||||
const [tblAvarage, setTblAvarage] = useState<number>(0);
|
const [tblAvarage, setTblAvarage] = useState<number>(0);
|
||||||
const [selectedProjects, setSelectedProjects] = useState<
|
const [selectedProjects, setSelectedProjects] =
|
||||||
Set<string | number>
|
useState<Set<string | number>>();
|
||||||
>(new Set());
|
|
||||||
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
||||||
const [selectedProjectDetails, setSelectedProjectDetails] =
|
const [selectedProjectDetails, setSelectedProjectDetails] =
|
||||||
useState<DialogInfo>();
|
useState<DialogInfo>();
|
||||||
|
|
@ -426,7 +422,6 @@ export function InnovationBuiltInsidePage() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProjects(true);
|
fetchProjects(true);
|
||||||
fetchStats();
|
|
||||||
}, [sortConfig]);
|
}, [sortConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -486,7 +481,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
const raw = await apiService.call<any>({
|
const raw = await apiService.call<any>({
|
||||||
innovation_construction_inside_function: {
|
innovation_construction_inside_function: {
|
||||||
project_ids:
|
project_ids:
|
||||||
selectedProjects.size > 0
|
selectedProjects && selectedProjects?.size > 0
|
||||||
? Array.from(selectedProjects).join(" , ")
|
? Array.from(selectedProjects).join(" , ")
|
||||||
: "",
|
: "",
|
||||||
},
|
},
|
||||||
|
|
@ -622,7 +617,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
case "select":
|
case "select":
|
||||||
return (
|
return (
|
||||||
<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 cursor-pointer"
|
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1039,7 +1034,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
|
|
||||||
{/* Project Details Dialog */}
|
{/* Project Details Dialog */}
|
||||||
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
||||||
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-5xl overflow-y-auto">
|
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-6xl overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
|
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
|
||||||
شرح پروژه
|
شرح پروژه
|
||||||
|
|
@ -1192,12 +1187,12 @@ export function InnovationBuiltInsidePage() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="tbl rounded-xl overflow-hidden">
|
<div className="tbl rounded-xl overflow-hidden">
|
||||||
<div className="grid grid-cols-3 bg-[#3F415A] text-white">
|
<div className="grid grid-cols-3 bg-[#3F415A] text-white p-3 items-center">
|
||||||
<span className="text-md text-center p-3">
|
<span className="text-md text-center">
|
||||||
شاخص مقایسه با نمونه خارجی
|
شاخص مقایسه با نمونه خارجی
|
||||||
</span>
|
</span>
|
||||||
<span className="text-md text-center p-3">نمونه داخلی</span>
|
<span className="text-md text-center">نمونه داخلی</span>
|
||||||
<span className="text-md text-center p-3">نمونه خارجی</span>
|
<span className="text-md text-center">نمونه خارجی</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col divide-y divide-gray-600 overflow-auto max-h-[6rem]">
|
<div className="flex flex-col divide-y divide-gray-600 overflow-auto max-h-[6rem]">
|
||||||
{selectedProjectDetails?.technology_params?.map(
|
{selectedProjectDetails?.technology_params?.map(
|
||||||
|
|
@ -1268,7 +1263,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
<ResponsiveContainer width="100%" height={420}>
|
<ResponsiveContainer width="100%" height={420}>
|
||||||
<LineChart
|
<LineChart
|
||||||
data={dialogChartData}
|
data={dialogChartData}
|
||||||
margin={{ top: 20, right: 20, left: 20, bottom: 80 }}
|
margin={{ top: 20, right: 70, left: 30, bottom: 80 }}
|
||||||
>
|
>
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="name"
|
dataKey="name"
|
||||||
|
|
@ -1323,7 +1318,7 @@ export function InnovationBuiltInsidePage() {
|
||||||
axisOffsetX -
|
axisOffsetX -
|
||||||
15;
|
15;
|
||||||
|
|
||||||
const rectWidth = 140;
|
const rectWidth = 130;
|
||||||
const rectHeight = 28;
|
const rectHeight = 28;
|
||||||
const rectX = x - rectWidth / 2;
|
const rectX = x - rectWidth / 2;
|
||||||
const axisHeight = xAxis?.height ?? 40;
|
const axisHeight = xAxis?.height ?? 40;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -219,21 +219,15 @@ export function Sidebar({
|
||||||
if (item.id === "strategic-alignment") {
|
if (item.id === "strategic-alignment") {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
key={item.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full text-right",
|
"flex items-center justify-center w-full px-2 rounded-lg mt-4 transition-all duration-200 group",
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex justify-center rounded-xl border-gray-500/20 border-2 cursor-pointer transition-all hover:bg-[#3F415A]/50 bg-[#3F415A] py-2 text-center items-center gap-3 min-w-0 flex-1">
|
||||||
className={cn(
|
<span className="font-persian text-sm font-medium truncate">
|
||||||
"flex items-center justify-center w-full px-2 rounded-lg mt-4 transition-all duration-200 group",
|
{item.label}
|
||||||
)}
|
</span>
|
||||||
>
|
|
||||||
<div className="flex justify-center rounded-xl border-gray-500/20 border-2 cursor-pointer transition-all hover:bg-[#3F415A]/50 bg-[#3F415A] py-2 text-center items-center gap-3 min-w-0 flex-1">
|
|
||||||
<span className="font-persian text-sm font-medium truncate">
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
39
app/components/ui/funnel-chart.test.tsx
Normal file
39
app/components/ui/funnel-chart.test.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { FunnelChart } from './funnel-chart';
|
||||||
|
|
||||||
|
const mockData = [
|
||||||
|
{ name: "تعداد کل", value: 250, label: "تعداد کل" },
|
||||||
|
{ name: "نمونه موفق", value: 130, label: "نمونه موفق" },
|
||||||
|
{ name: "محصولات موفق", value: 70, label: "محصولات موفق" },
|
||||||
|
{ name: "بهبود یا تغییر موفق", value: 80, label: "بهبود یا تغییر موفق" },
|
||||||
|
{ name: "محصول جدید", value: 50, label: "محصول جدید" },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('FunnelChart', () => {
|
||||||
|
it('renders funnel chart with correct data', () => {
|
||||||
|
render(<FunnelChart data={mockData} title="قيف فرآیند پروژه ها" />);
|
||||||
|
|
||||||
|
expect(screen.getByText('قيف فرآیند پروژه ها')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('۱۰۰%')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('۲۵%')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('ابتدا فرآیند')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('انتها فرآیند')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays funnel data values correctly', () => {
|
||||||
|
render(<FunnelChart data={mockData} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('۲۵۰')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('۱۳۰')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('۷۰')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('۸۰')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('۵۰')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders without title when not provided', () => {
|
||||||
|
render(<FunnelChart data={mockData} />);
|
||||||
|
|
||||||
|
expect(screen.queryByText('قيف فرآیند پروژه ها')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
95
app/components/ui/funnel-chart.tsx
Normal file
95
app/components/ui/funnel-chart.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import React from "react";
|
||||||
|
import { formatNumber } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface FunnelData {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
percentage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FunnelChartProps {
|
||||||
|
data: FunnelData[];
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FunnelChart({ data, title, className = "" }: FunnelChartProps) {
|
||||||
|
const maxValue = Math.max(...data.map(d => d.value));
|
||||||
|
const toPercent = (value: number) => {
|
||||||
|
if (!maxValue || maxValue <= 0) return 0;
|
||||||
|
return Math.round((value / maxValue) * 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`w-full ${className}`}>
|
||||||
|
{title && (
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-4 py-2 text-right border-b-2 border-gray-400/20">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-2 space-y-2">
|
||||||
|
{/* Start Process Line */}
|
||||||
|
<div className="flex items-center w-full gap-10 mt-6">
|
||||||
|
<div className="text-lg text-gray-600 min-w-[max-content]">ابتدا فرآیند</div>
|
||||||
|
<div className="flex items-center w-full gap-4">
|
||||||
|
<div className="w-full h-0.5 bg-gray-600 relative">
|
||||||
|
<div className="text-2xl text-white absolute left-1/2 -translate-x-1/2 top-[-1rem] -translate-y-1/2">۱۰۰%</div>
|
||||||
|
<div className="absolute -top-1 left-0 w-1 h-3 bg-gray-600"></div>
|
||||||
|
<div className="absolute -top-1 right-0 w-1 h-3 bg-gray-600"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Funnel Bars */}
|
||||||
|
<div className="flex flex-col items-center space-y-1 gap-2 w-full max-w-md">
|
||||||
|
{data.map((item, index) => {
|
||||||
|
const widthPercentage = toPercent(item.value);
|
||||||
|
const barWidth = Math.max(20, widthPercentage); // Minimum 20% width
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className="grid grid-cols-[6rem_1fr] gap-2 w-full">
|
||||||
|
<div className="text-lg text-white cols-start-1 justify-self-start font-thin min-w-[max-content] text-center">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-10 w-full cols-start-2 flex items-center justify-center w-full">
|
||||||
|
<div className="flex items-center w-full">
|
||||||
|
<div style={{ width: `${(100 - barWidth) / 2}%` }} />
|
||||||
|
<div
|
||||||
|
className="bg-[#3BC47A] h-8 rounded-2xl flex items-center justify-center text-lg relative"
|
||||||
|
style={{ width: `${barWidth}%` }}
|
||||||
|
>
|
||||||
|
<span className="text-[#3F415A] font-semibold">
|
||||||
|
{item.value.toLocaleString('fa-IR')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: `${(100 - barWidth) / 2}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* End Process Line */}
|
||||||
|
<div className="flex items-center w-full gap-10">
|
||||||
|
<div className="text-lg text-gray-600 min-w-[max-content]">انتها فرآیند</div>
|
||||||
|
<div className="flex items-center w-full gap-4">
|
||||||
|
{(() => {
|
||||||
|
const lastValue = data[data.length - 1]?.value ?? 0;
|
||||||
|
const percent = toPercent(lastValue);
|
||||||
|
return (
|
||||||
|
<div style={{ width: `${percent}%` }} className={`mx-auto h-0.5 bg-gray-600 relative ${percent === 0 ? "hidden" : ""}`}>
|
||||||
|
<div className="text-2xl text-white absolute left-1/2 -translate-x-1/2 bottom-[-2.5rem] -translate-y-1">{formatNumber(percent)}%</div>
|
||||||
|
<div className="absolute -top-1 left-0 w-1 h-3 bg-gray-600"></div>
|
||||||
|
<div className="absolute -top-1 right-0 w-1 h-3 bg-gray-600"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
app/components/ui/popover.tsx
Normal file
48
app/components/ui/popover.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import moment from "moment-jalaali";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
|
|
@ -20,3 +21,23 @@ export const formatCurrency = (amount: string | number) => {
|
||||||
if (isNaN(numericAmount)) return "0 ریال";
|
if (isNaN(numericAmount)) return "0 ریال";
|
||||||
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
|
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const handleDataValue = (val: any): any => {
|
||||||
|
moment.loadPersian({ usePersianDigits: true });
|
||||||
|
if (val == null) return val;
|
||||||
|
if (
|
||||||
|
typeof val === "string" &&
|
||||||
|
/^\d{4}[-/]\d{2}[-/]\d{2}( \d{2}:\d{2}(:\d{2})?)?$/.test(val)
|
||||||
|
) {
|
||||||
|
return moment(val, "YYYY-MM-DD HH:mm:ss").format("YYYY/MM/DD");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof val === "number" ||
|
||||||
|
(typeof val === "string" && /^-?\d+$/.test(val))
|
||||||
|
) {
|
||||||
|
return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ export default [
|
||||||
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(
|
||||||
|
"dashboard/innovation-basket/product-innovation",
|
||||||
|
"routes/innovation-basket.product-innovation.tsx"
|
||||||
),
|
),
|
||||||
route(
|
route(
|
||||||
"dashboard/innovation-basket/green-innovation",
|
"dashboard/innovation-basket/green-innovation",
|
||||||
|
|
|
||||||
17
app/routes/innovation-basket.product-innovation.tsx
Normal file
17
app/routes/innovation-basket.product-innovation.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { ProductInnovationPage } from "~/components/dashboard/project-management/product-innovation-page";
|
||||||
|
import { ProtectedRoute } from "~/components/auth/protected-route";
|
||||||
|
|
||||||
|
export function meta() {
|
||||||
|
return [
|
||||||
|
{ title: "نوآوری محصول - سیستم مدیریت فناوری و نوآوری" },
|
||||||
|
{ name: "description", content: "مدیریت پروژههای نوآوری محصول" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductInnovation() {
|
||||||
|
return (
|
||||||
|
<ProtectedRoute requireAuth={true}>
|
||||||
|
<ProductInnovationPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ importers:
|
||||||
'@radix-ui/react-label':
|
'@radix-ui/react-label':
|
||||||
specifier: ^2.0.2
|
specifier: ^2.0.2
|
||||||
version: 2.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 2.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-popover':
|
||||||
|
specifier: ^1.1.15
|
||||||
|
version: 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
'@radix-ui/react-progress':
|
'@radix-ui/react-progress':
|
||||||
specifier: ^1.1.7
|
specifier: ^1.1.7
|
||||||
version: 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
|
@ -645,6 +648,19 @@ packages:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-popover@1.1.15':
|
||||||
|
resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-popper@1.2.8':
|
'@radix-ui/react-popper@1.2.8':
|
||||||
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
|
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -3009,6 +3025,29 @@ snapshots:
|
||||||
'@types/react': 19.1.12
|
'@types/react': 19.1.12
|
||||||
'@types/react-dom': 19.1.9(@types/react@19.1.12)
|
'@types/react-dom': 19.1.9(@types/react@19.1.12)
|
||||||
|
|
||||||
|
'@radix-ui/react-popover@1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
aria-hidden: 1.2.6
|
||||||
|
react: 19.1.1
|
||||||
|
react-dom: 19.1.1(react@19.1.1)
|
||||||
|
react-remove-scroll: 2.7.1(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.12
|
||||||
|
'@types/react-dom': 19.1.9(@types/react@19.1.12)
|
||||||
|
|
||||||
'@radix-ui/react-popper@1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
'@radix-ui/react-popper@1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/react-dom': 2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
'@floating-ui/react-dom': 2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user