add new page (#9)
Reviewed-on: https://git.pelekan.org/Saeed0920/inogen/pulls/9 Co-authored-by: saeed0920 <sd.eed1381@gmail.com> Co-committed-by: saeed0920 <sd.eed1381@gmail.com>
This commit is contained in:
parent
5917c53b1e
commit
ef2cf01495
File diff suppressed because it is too large
Load Diff
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,10 @@ export default [
|
|||
route(
|
||||
"dashboard/innovation-basket/process-innovation",
|
||||
"routes/innovation-basket.process-innovation.tsx"
|
||||
),
|
||||
route(
|
||||
"dashboard/innovation-basket/product-innovation",
|
||||
"routes/innovation-basket.product-innovation.tsx"
|
||||
),
|
||||
route(
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user