1495 lines
56 KiB
TypeScript
1495 lines
56 KiB
TypeScript
import {
|
||
ChevronDown,
|
||
ChevronUp,
|
||
Download,
|
||
Hexagon,
|
||
RefreshCw,
|
||
Star,
|
||
TrendingUp,
|
||
} from "lucide-react";
|
||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||
import toast from "react-hot-toast";
|
||
import {
|
||
Bar,
|
||
BarChart,
|
||
CartesianGrid,
|
||
Label,
|
||
LabelList,
|
||
PolarGrid,
|
||
PolarRadiusAxis,
|
||
RadialBar,
|
||
RadialBarChart,
|
||
ResponsiveContainer,
|
||
XAxis,
|
||
YAxis,
|
||
} from "recharts";
|
||
import { BaseCard } from "~/components/ui/base-card";
|
||
import { Button } from "~/components/ui/button";
|
||
import { Card, CardContent } from "~/components/ui/card";
|
||
import { ChartContainer, type ChartConfig } from "~/components/ui/chart";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "~/components/ui/dialog";
|
||
import { MetricCard } from "~/components/ui/metric-card";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from "~/components/ui/table";
|
||
import { useStoredDate } from "~/hooks/useStoredDate";
|
||
import apiService from "~/lib/api";
|
||
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
|
||
import type { CalendarDate } from "~/types/util.type";
|
||
import { DashboardLayout } from "../layout";
|
||
|
||
interface IdeaData {
|
||
idea_title: string;
|
||
idea_registration_date: string;
|
||
idea_status: string;
|
||
increased_revenue: string;
|
||
full_name: string;
|
||
personnel_number: string;
|
||
management: string;
|
||
deputy: string;
|
||
innovator_team_members: string;
|
||
innovation_type: string;
|
||
idea_originality: string;
|
||
idea_axis: string;
|
||
idea_description: string;
|
||
idea_current_status_description: string;
|
||
idea_execution_benefits: string;
|
||
process_improvements: string;
|
||
}
|
||
|
||
interface PersonRanking {
|
||
full_name: string;
|
||
full_name_count: number;
|
||
ranking: number;
|
||
stars: number;
|
||
}
|
||
|
||
interface IdeaStatusData {
|
||
idea_status: string;
|
||
idea_status_count: number;
|
||
}
|
||
|
||
interface IdeaStatsData {
|
||
registered_innovation_technology_idea: string;
|
||
ongoing_innovation_technology_ideas: string;
|
||
increased_revenue_from_ideas: string;
|
||
increased_revenue_from_ideas_percent: string;
|
||
}
|
||
|
||
interface SortConfig {
|
||
field: string;
|
||
direction: "asc" | "desc";
|
||
}
|
||
|
||
type ColumnDef = {
|
||
key: string;
|
||
label: string;
|
||
sortable: boolean;
|
||
width: string;
|
||
};
|
||
|
||
const columns: ColumnDef[] = [
|
||
{ key: "idea_title", label: "عنوان ایده", sortable: true, width: "250px" },
|
||
{
|
||
key: "idea_registration_date",
|
||
label: "تاریخ ثبت ایده",
|
||
sortable: true,
|
||
width: "180px",
|
||
},
|
||
{ key: "idea_status", label: "وضعیت ایده", sortable: true, width: "150px" },
|
||
{
|
||
key: "increased_revenue",
|
||
label: "درآمد حاصل از ایده",
|
||
sortable: true,
|
||
width: "180px",
|
||
},
|
||
{ key: "details", label: "جزئیات بیشتر", sortable: false, width: "120px" },
|
||
];
|
||
|
||
// Memoized Vertical Bar Chart Component
|
||
const VerticalBarChart = memo<{
|
||
chartData: IdeaStatusData[];
|
||
loadingChart: boolean;
|
||
chartConfig: ChartConfig;
|
||
getChartStatusColor: (status: string) => string;
|
||
toPersianDigits: (input: string | number) => string;
|
||
formatNumber: (value: number) => string;
|
||
}>(
|
||
({
|
||
chartData,
|
||
loadingChart,
|
||
chartConfig,
|
||
getChartStatusColor,
|
||
toPersianDigits,
|
||
formatNumber,
|
||
}) => {
|
||
if (loadingChart) {
|
||
return (
|
||
<div className="p-6 space-y-4">
|
||
{/* Chart title skeleton */}
|
||
<div className="h-5 bg-gray-600 rounded animate-pulse w-32 mx-auto"></div>
|
||
|
||
{/* Chart area skeleton */}
|
||
<div className="relative h-48 bg-gradient-to-t from-gray-700/30 to-transparent rounded p-4">
|
||
{/* Y-axis labels */}
|
||
<div className="absolute left-2 top-4 space-y-6">
|
||
{Array.from({ length: 4 }).map((_, i) => (
|
||
<div
|
||
key={i}
|
||
className="h-3 bg-gray-600 rounded animate-pulse w-6"
|
||
></div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Bars skeleton */}
|
||
<div className="flex items-end justify-center gap-4 h-full pt-4 pb-8 ml-8">
|
||
{Array.from({ length: 4 }).map((_, i) => (
|
||
<div key={i} className="flex flex-col items-center gap-2">
|
||
{/* Bar */}
|
||
<div
|
||
className="bg-gray-600 rounded-t animate-pulse w-12"
|
||
style={{ height: `${Math.random() * 60 + 40}%` }}
|
||
></div>
|
||
{/* X-axis label */}
|
||
<div className="h-3 bg-gray-600 rounded animate-pulse w-16"></div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!chartData.length) {
|
||
return (
|
||
<div className="p-6 text-center">
|
||
<h3 className="text-lg font-persian font-semibold text-white mb-4">
|
||
وضعیت ایده ها
|
||
</h3>
|
||
<p className="text-gray-400 font-persian">هیچ دادهای یافت نشد</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Prepare data for recharts
|
||
const rechartData = useMemo(
|
||
() =>
|
||
chartData.map((item) => ({
|
||
status: item.idea_status,
|
||
count: item.idea_status_count,
|
||
fill: getChartStatusColor(item.idea_status),
|
||
})),
|
||
[chartData, getChartStatusColor]
|
||
);
|
||
|
||
return (
|
||
<ResponsiveContainer width="100%">
|
||
<ChartContainer config={chartConfig} className="w-full">
|
||
<BarChart
|
||
margin={{ top: 25, left: 12, right: 12, bottom: 12 }}
|
||
barGap={15}
|
||
barSize={45}
|
||
accessibilityLayer
|
||
data={rechartData}
|
||
>
|
||
<CartesianGrid vertical={false} stroke="#475569" />
|
||
<XAxis
|
||
dataKey="status"
|
||
axisLine={false}
|
||
tickLine={false}
|
||
tick={{
|
||
fill: "#fff",
|
||
fontSize: 14,
|
||
fontFamily: "inherit",
|
||
}}
|
||
interval={0}
|
||
angle={0}
|
||
tickMargin={10}
|
||
textAnchor="middle"
|
||
/>
|
||
<YAxis
|
||
tickMargin={20}
|
||
axisLine={false}
|
||
tickLine={false}
|
||
tick={{
|
||
fill: "#9CA3AF",
|
||
fontSize: 12,
|
||
fontFamily: "inherit",
|
||
}}
|
||
tickFormatter={(value) => toPersianDigits(value)}
|
||
label={{
|
||
value: "تعداد برنامه ها",
|
||
angle: -90,
|
||
position: "insideLeft",
|
||
fill: "#94a3b8",
|
||
fontSize: 11,
|
||
offset: 0,
|
||
dy: 0,
|
||
style: { textAnchor: "middle" },
|
||
}}
|
||
/>
|
||
<Bar dataKey="count" radius={[4, 4, 0, 0]}>
|
||
<LabelList
|
||
dataKey="count"
|
||
position="top"
|
||
offset={12}
|
||
style={{
|
||
fill: "#ffffff",
|
||
fontSize: "16px",
|
||
fontWeight: "bold",
|
||
}}
|
||
formatter={(v: number) => `${formatNumber(Math.round(v))}`}
|
||
/>
|
||
</Bar>
|
||
</BarChart>
|
||
</ChartContainer>
|
||
</ResponsiveContainer>
|
||
);
|
||
}
|
||
);
|
||
|
||
const MemoizedVerticalBarChart = VerticalBarChart;
|
||
|
||
export function ManageIdeasTechPage() {
|
||
const [ideas, setIdeas] = useState<IdeaData[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [loadingMore, setLoadingMore] = useState(false);
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
const [pageSize] = useState(10);
|
||
const [hasMore, setHasMore] = useState(true);
|
||
const [totalCount, setTotalCount] = useState(0);
|
||
const [actualTotalCount, setActualTotalCount] = useState(0);
|
||
const [selectedIdea, setSelectedIdea] = useState<IdeaData | null>(null);
|
||
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
|
||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||
field: "idea_title",
|
||
direction: "asc",
|
||
});
|
||
const [date, setDate] = useStoredDate();
|
||
|
||
// People ranking state
|
||
const [peopleRanking, setPeopleRanking] = useState<PersonRanking[]>([]);
|
||
const [loadingPeople, setLoadingPeople] = useState(false);
|
||
|
||
// Chart state
|
||
const [chartData, setChartData] = useState<IdeaStatusData[]>([]);
|
||
const [loadingChart, setLoadingChart] = useState(false);
|
||
|
||
// Stats state
|
||
const [statsData, setStatsData] = useState<IdeaStatsData | null>(null);
|
||
const [loadingStats, setLoadingStats] = useState(false);
|
||
|
||
const observerRef = useRef<HTMLDivElement>(null);
|
||
const fetchingRef = useRef(false);
|
||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||
|
||
const fetchIdeas = async (reset = false) => {
|
||
if (fetchingRef.current) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
fetchingRef.current = true;
|
||
|
||
if (reset) {
|
||
setLoading(true);
|
||
setCurrentPage(1);
|
||
} else {
|
||
setLoadingMore(true);
|
||
}
|
||
|
||
const pageToFetch = reset ? 1 : currentPage;
|
||
|
||
const response = await apiService.select({
|
||
ProcessName: "idea",
|
||
OutputFields: [
|
||
"idea_title",
|
||
"idea_registration_date",
|
||
"idea_status",
|
||
"increased_revenue",
|
||
"full_name",
|
||
"personnel_number",
|
||
"management",
|
||
"deputy",
|
||
"innovator_team_members",
|
||
"innovation_type",
|
||
"idea_originality",
|
||
"idea_axis",
|
||
"idea_description",
|
||
"idea_current_status_description",
|
||
"idea_execution_benefits",
|
||
"process_improvements",
|
||
],
|
||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||
Sorts: [[sortConfig.field, sortConfig.direction]],
|
||
Conditions: [
|
||
["idea_registration_date", ">=", date?.start || null, "and"],
|
||
["idea_registration_date", "<=", date?.end || null],
|
||
],
|
||
});
|
||
|
||
if (response.state === 0) {
|
||
const dataString = response.data;
|
||
if (dataString && typeof dataString === "string") {
|
||
try {
|
||
const parsedData = JSON.parse(dataString);
|
||
if (Array.isArray(parsedData)) {
|
||
if (reset) {
|
||
setIdeas(parsedData);
|
||
setTotalCount(parsedData.length);
|
||
} else {
|
||
setIdeas((prev) => [...prev, ...parsedData]);
|
||
setTotalCount((prev) => prev + parsedData.length);
|
||
}
|
||
setHasMore(parsedData.length === pageSize);
|
||
} else {
|
||
if (reset) {
|
||
setIdeas([]);
|
||
setTotalCount(0);
|
||
}
|
||
setHasMore(false);
|
||
}
|
||
} catch (parseError) {
|
||
console.error("Error parsing idea data:", parseError);
|
||
if (reset) {
|
||
setIdeas([]);
|
||
setTotalCount(0);
|
||
}
|
||
setHasMore(false);
|
||
}
|
||
} else {
|
||
if (reset) {
|
||
setIdeas([]);
|
||
setTotalCount(0);
|
||
}
|
||
setHasMore(false);
|
||
}
|
||
} else {
|
||
toast.error(response.message || "خطا در دریافت اطلاعات ایدهها");
|
||
if (reset) {
|
||
setIdeas([]);
|
||
setTotalCount(0);
|
||
}
|
||
setHasMore(false);
|
||
}
|
||
} catch (error) {
|
||
console.error("Error fetching ideas:", error);
|
||
toast.error("خطا در دریافت اطلاعات ایدهها");
|
||
if (reset) {
|
||
setIdeas([]);
|
||
setTotalCount(0);
|
||
}
|
||
setHasMore(false);
|
||
} finally {
|
||
setLoading(false);
|
||
setLoadingMore(false);
|
||
fetchingRef.current = false;
|
||
}
|
||
};
|
||
|
||
const loadMore = useCallback(() => {
|
||
if (hasMore && !loading && !loadingMore && !fetchingRef.current) {
|
||
setCurrentPage((prev) => prev + 1);
|
||
}
|
||
}, [hasMore, loading, loadingMore]);
|
||
|
||
useEffect(() => {
|
||
const handler = (date: CalendarDate) => {
|
||
if (date) setDate(date);
|
||
};
|
||
|
||
EventBus.on("dateSelected", handler);
|
||
|
||
return () => {
|
||
EventBus.off("dateSelected", handler);
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (date.end && date.start) {
|
||
fetchIdeas(true);
|
||
fetchTotalCount();
|
||
fetchPeopleRanking();
|
||
fetchChartData();
|
||
fetchStatsData();
|
||
}
|
||
}, [sortConfig, date]);
|
||
|
||
useEffect(() => {
|
||
if (currentPage > 1) {
|
||
fetchIdeas(false);
|
||
}
|
||
}, [currentPage]);
|
||
|
||
// Infinite scroll observer with debouncing
|
||
useEffect(() => {
|
||
const scrollContainer = scrollContainerRef.current;
|
||
|
||
const handleScroll = () => {
|
||
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current)
|
||
return;
|
||
|
||
if (scrollTimeoutRef.current) {
|
||
clearTimeout(scrollTimeoutRef.current);
|
||
}
|
||
|
||
scrollTimeoutRef.current = setTimeout(() => {
|
||
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
||
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
|
||
|
||
if (scrollPercentage >= 0.95) {
|
||
loadMore();
|
||
}
|
||
}, 150);
|
||
};
|
||
|
||
if (scrollContainer) {
|
||
scrollContainer.addEventListener("scroll", handleScroll, {
|
||
passive: true,
|
||
});
|
||
}
|
||
|
||
return () => {
|
||
if (scrollContainer) {
|
||
scrollContainer.removeEventListener("scroll", handleScroll);
|
||
}
|
||
if (scrollTimeoutRef.current) {
|
||
clearTimeout(scrollTimeoutRef.current);
|
||
}
|
||
};
|
||
}, [loadMore, hasMore, loadingMore]);
|
||
|
||
const handleSort = (field: string) => {
|
||
fetchingRef.current = false;
|
||
setSortConfig((prev) => ({
|
||
field,
|
||
direction:
|
||
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
|
||
}));
|
||
setCurrentPage(1);
|
||
setIdeas([]);
|
||
setHasMore(true);
|
||
};
|
||
|
||
const fetchTotalCount = async () => {
|
||
try {
|
||
const response = await apiService.select({
|
||
ProcessName: "idea",
|
||
OutputFields: ["count(idea_title)"],
|
||
Conditions: [
|
||
["idea_registration_date", ">=", date?.start || null, "and"],
|
||
["idea_registration_date", "<=", date?.end || null],
|
||
],
|
||
});
|
||
|
||
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]) {
|
||
setActualTotalCount(parsedData[0].idea_title_count || 0);
|
||
}
|
||
} catch (parseError) {
|
||
console.error("Error parsing count data:", parseError);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Error fetching total count:", error);
|
||
}
|
||
};
|
||
|
||
const fetchPeopleRanking = async () => {
|
||
try {
|
||
setLoadingPeople(true);
|
||
|
||
const response = await apiService.select({
|
||
ProcessName: "idea",
|
||
OutputFields: ["full_name", "count(full_name)"],
|
||
GroupBy: ["full_name"],
|
||
Conditions: [
|
||
["idea_registration_date", ">=", date?.start || null, "and"],
|
||
["idea_registration_date", "<=", date?.end || null],
|
||
],
|
||
});
|
||
|
||
if (response.state === 0) {
|
||
const dataString = response.data;
|
||
if (dataString && typeof dataString === "string") {
|
||
try {
|
||
const parsedData = JSON.parse(dataString);
|
||
if (Array.isArray(parsedData)) {
|
||
// Calculate rankings and stars
|
||
const counts = parsedData.map((item) => item.full_name_count);
|
||
const maxCount = Math.max(...counts);
|
||
const minCount = Math.min(...counts);
|
||
|
||
// Sort by count first (highest first)
|
||
const sortedData = parsedData.sort(
|
||
(a, b) => b.full_name_count - a.full_name_count
|
||
);
|
||
|
||
const rankedPeople = [];
|
||
let currentRank = 1;
|
||
let sum = 1;
|
||
|
||
for (let i = 0; i < sortedData.length; i++) {
|
||
const item = sortedData[i];
|
||
|
||
// If this is not the first person and their count is different from previous
|
||
if (
|
||
i > 0 &&
|
||
sortedData[i - 1].full_name_count !== item.full_name_count
|
||
) {
|
||
currentRank = sum + 1; // New rank based on position
|
||
sum++;
|
||
}
|
||
const normalizedScore =
|
||
maxCount === minCount
|
||
? 1
|
||
: (item.full_name_count - minCount) / (maxCount - minCount);
|
||
const stars = Math.max(1, Math.round(normalizedScore * 5));
|
||
|
||
rankedPeople.push({
|
||
full_name: item.full_name,
|
||
full_name_count: item.full_name_count,
|
||
ranking: currentRank,
|
||
stars: stars,
|
||
});
|
||
}
|
||
setPeopleRanking(rankedPeople);
|
||
}
|
||
} catch (parseError) {
|
||
console.error("Error parsing people ranking data:", parseError);
|
||
}
|
||
}
|
||
} else {
|
||
toast.error(
|
||
response.message || "خطا در دریافت اطلاعات رتبهبندی افراد"
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error("Error fetching people ranking:", error);
|
||
toast.error("خطا در دریافت اطلاعات رتبهبندی افراد");
|
||
} finally {
|
||
setLoadingPeople(false);
|
||
}
|
||
};
|
||
|
||
const fetchChartData = async () => {
|
||
try {
|
||
setLoadingChart(true);
|
||
|
||
const response = await apiService.select({
|
||
ProcessName: "idea",
|
||
OutputFields: ["idea_status", "count(idea_status)"],
|
||
GroupBy: ["idea_status"],
|
||
Conditions: [
|
||
["idea_registration_date", ">=", date?.start || null, "and"],
|
||
["idea_registration_date", "<=", date?.end || null],
|
||
],
|
||
});
|
||
|
||
if (response.state === 0) {
|
||
const dataString = response.data;
|
||
if (dataString && typeof dataString === "string") {
|
||
try {
|
||
const parsedData: IdeaStatusData[] = JSON.parse(dataString);
|
||
if (Array.isArray(parsedData)) {
|
||
setChartData(parsedData?.reverse());
|
||
}
|
||
} catch (parseError) {
|
||
console.error("Error parsing chart data:", parseError);
|
||
}
|
||
}
|
||
} else {
|
||
toast.error(response.message || "خطا در دریافت اطلاعات نمودار");
|
||
}
|
||
} catch (error) {
|
||
console.error("Error fetching chart data:", error);
|
||
toast.error("خطا در دریافت اطلاعات نمودار");
|
||
} finally {
|
||
setLoadingChart(false);
|
||
}
|
||
};
|
||
|
||
const fetchStatsData = async () => {
|
||
try {
|
||
setLoadingStats(true);
|
||
|
||
const response = await apiService.call({
|
||
idea_page_function: {
|
||
start_date: date?.start || null,
|
||
end_date: date?.end || null,
|
||
},
|
||
});
|
||
|
||
if (response.state === 0) {
|
||
const dataString = response.data;
|
||
if (dataString && typeof dataString === "string") {
|
||
try {
|
||
const parsedData: IdeaStatsData = JSON.parse(dataString);
|
||
setStatsData(parsedData);
|
||
} catch (parseError) {
|
||
console.error("Error parsing stats data:", parseError);
|
||
}
|
||
}
|
||
} else {
|
||
toast.error(response.message || "خطا در دریافت آمار ایدهها");
|
||
}
|
||
} catch (error) {
|
||
console.error("Error fetching stats data:", error);
|
||
toast.error("خطا در دریافت آمار ایدهها");
|
||
} finally {
|
||
setLoadingStats(false);
|
||
}
|
||
};
|
||
|
||
const toPersianDigits = useCallback((input: string | number): string => {
|
||
const str = String(input);
|
||
const map: Record<string, string> = {
|
||
"0": "۰",
|
||
"1": "۱",
|
||
"2": "۲",
|
||
"3": "۳",
|
||
"4": "۴",
|
||
"5": "۵",
|
||
"6": "۶",
|
||
"7": "۷",
|
||
"8": "۸",
|
||
"9": "۹",
|
||
};
|
||
return str.replace(/[0-9]/g, (d) => map[d] ?? d);
|
||
}, []);
|
||
|
||
const formatDate = (dateString: string | null) => {
|
||
if (!dateString || dateString === "null" || dateString.trim() === "") {
|
||
return "-";
|
||
}
|
||
|
||
const raw = String(dateString).trim();
|
||
const jalaliPattern = /^(\d{4})[\/](\d{1,2})[\/](\d{1,2})$/;
|
||
const jalaliMatch = raw.match(jalaliPattern);
|
||
if (jalaliMatch) {
|
||
const [, y, m, d] = jalaliMatch;
|
||
const mm = m.padStart(2, "0");
|
||
const dd = d.padStart(2, "0");
|
||
return toPersianDigits(`${y}/${mm}/${dd}`);
|
||
}
|
||
|
||
try {
|
||
const parsed = new Date(raw);
|
||
if (isNaN(parsed.getTime())) return "-";
|
||
return new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
|
||
year: "numeric",
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
}).format(parsed);
|
||
} catch {
|
||
return "-";
|
||
}
|
||
};
|
||
|
||
// Chart configuration for shadcn/ui
|
||
const chartConfig: ChartConfig = useMemo(
|
||
() => ({
|
||
count: {
|
||
label: "تعداد",
|
||
},
|
||
}),
|
||
[]
|
||
);
|
||
|
||
// Color palette for idea status
|
||
// Specific colors for idea statuses
|
||
const getChartStatusColor = useCallback((status: string) => {
|
||
switch (status) {
|
||
case "اجرا شده":
|
||
return "#69C8EA";
|
||
case "تایید شده":
|
||
return "#3AEA83";
|
||
case "در حال بررسی":
|
||
return "#EAD069";
|
||
case "رد شده":
|
||
return "#F76276";
|
||
default:
|
||
return "#6B7280";
|
||
}
|
||
}, []);
|
||
|
||
const statusColorPalette = [
|
||
"#3AEA83",
|
||
"#69C8EA",
|
||
"#F76276",
|
||
"#FFD700",
|
||
"#A757FF",
|
||
"#E884CE",
|
||
"#C3BF8B",
|
||
"#FB7185",
|
||
];
|
||
|
||
// Build a mapping of status value -> color based on loaded ideas
|
||
const statusColorMap = useMemo(() => {
|
||
const map: Record<string, string> = {};
|
||
const seenStatuses = new Set<string>();
|
||
|
||
ideas.forEach((idea) => {
|
||
const status = String(idea.idea_status || "").trim();
|
||
if (status && !seenStatuses.has(status)) {
|
||
seenStatuses.add(status);
|
||
}
|
||
});
|
||
|
||
const statusArray = Array.from(seenStatuses).sort();
|
||
statusArray.forEach((status, index) => {
|
||
map[status] = statusColorPalette[index % statusColorPalette.length];
|
||
});
|
||
|
||
return map;
|
||
}, [ideas]);
|
||
|
||
const getStatusColor = (status: string) => {
|
||
const statusValue = String(status || "").trim();
|
||
return statusColorMap[statusValue] || "#6B7280";
|
||
};
|
||
|
||
const handleShowDetails = (idea: IdeaData) => {
|
||
setSelectedIdea(idea);
|
||
setIsDetailsOpen(true);
|
||
};
|
||
|
||
const renderCellContent = (item: IdeaData, column: ColumnDef) => {
|
||
const value = (item as any)[column.key];
|
||
|
||
switch (column.key) {
|
||
case "idea_title":
|
||
return <span className="text-sm text-white">{String(value)}</span>;
|
||
case "idea_registration_date":
|
||
return (
|
||
<span className="text-white text-sm">
|
||
{formatDate(String(value))}
|
||
</span>
|
||
);
|
||
case "idea_status":
|
||
return (
|
||
<span className="flex items-center justify-end flex-row-reverse gap-1 w-full">
|
||
<span className="text-white text-sm">
|
||
{!!value ? String(value) : "-"}
|
||
</span>
|
||
<span
|
||
style={{
|
||
backgroundColor: getStatusColor(String(value)),
|
||
display: !value ? "none" : "block",
|
||
}}
|
||
className="inline-block w-2 h-2 rounded-full"
|
||
/>
|
||
</span>
|
||
);
|
||
case "increased_revenue":
|
||
return (
|
||
<span className="text-sm text-white w-full">
|
||
{formatCurrency(String(value || "0")).replace("ریال", "")}
|
||
</span>
|
||
);
|
||
case "details":
|
||
return (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleShowDetails(item)}
|
||
className="underline text-pr-green underline-offset-4 text-sm"
|
||
>
|
||
جزئیات بیشتر
|
||
</Button>
|
||
);
|
||
default:
|
||
return (
|
||
<span className="text-white text-sm">
|
||
{(value && String(value)) || "-"}
|
||
</span>
|
||
);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<DashboardLayout title="مدیریت ایده های فناوری و نوآوری">
|
||
<div className="space-y-6 h-full">
|
||
<div className="grid grid-cols-1 grid-rows-2 lg:grid-cols-3 gap-4 h-full">
|
||
{/* People Ranking Table */}
|
||
<div className="lg:col-span-1">
|
||
<h3 className="text-base text-center mb-2 font-persian font-bold text-foreground">
|
||
رتبه بندی نوآوران
|
||
</h3>
|
||
<Card className="bg-transparent border-none rounded-xl overflow-hidden">
|
||
<CardContent className="p-0 ">
|
||
<div className="relative max-h-[calc(100vh-200px)] overflow-auto custom-scrollbar">
|
||
<Table>
|
||
<TableHeader className="sticky top-0 z-50 bg-pr-gray">
|
||
<TableRow className="bg-pr-gray">
|
||
<TableHead className="text-center font-persian font-semibold text-white bg-pr-gray sticky top-0 z-20 w-16">
|
||
رتبه
|
||
</TableHead>
|
||
<TableHead className="text-right font-persian text-white font-semibold bg-pr-gray sticky top-0 z-20">
|
||
ایده پرداز
|
||
</TableHead>
|
||
<TableHead className="text-center font-persian font-semibold bg-pr-gray text-white sticky top-0 z-20 w-24">
|
||
امتیاز
|
||
</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{loadingPeople ? (
|
||
Array.from({ length: 10 }).map((_, index) => (
|
||
<TableRow
|
||
key={`skeleton-${index}`}
|
||
className="text-sm leading-tight h-12"
|
||
>
|
||
<TableCell className="text-center py-2 px-2">
|
||
<div className="w-6 h-6 bg-muted rounded-full animate-pulse mx-auto" />
|
||
</TableCell>
|
||
<TableCell className="text-right py-2 px-4">
|
||
<div className="h-4 bg-muted rounded animate-pulse w-3/4" />
|
||
</TableCell>
|
||
<TableCell className="text-center py-2 px-2">
|
||
<div className="flex items-center justify-center gap-1">
|
||
{Array.from({ length: 5 }).map(
|
||
(_, starIndex) => (
|
||
<div
|
||
key={starIndex}
|
||
className="w-3 h-3 bg-muted rounded animate-pulse"
|
||
/>
|
||
)
|
||
)}
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
) : peopleRanking.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell colSpan={3} className="text-center py-8">
|
||
<span className="text-muted-foreground font-persian">
|
||
هیچ دادهای یافت نشد
|
||
</span>
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
peopleRanking.map((person) => (
|
||
<TableRow
|
||
key={person.full_name}
|
||
className="text-sm leading-tight h-10 not-last:border-b-pr-gray border-border"
|
||
>
|
||
<TableCell className="text-center py-2 px-2">
|
||
<div className="flex items-center justify-center text-white text-sm mx-auto">
|
||
{toPersianDigits(person.ranking)}
|
||
</div>
|
||
</TableCell>
|
||
<TableCell className="text-right py-2 px-4">
|
||
<span className="font-persian font-medium text-foreground">
|
||
{person.full_name}
|
||
</span>
|
||
</TableCell>
|
||
<TableCell className="text-center py-4 px-2">
|
||
<div className="flex mx-4 flex-row-reverse items-center justify-center gap-1">
|
||
{Array.from({ length: 5 }).map(
|
||
(_, starIndex) => (
|
||
<Star
|
||
key={starIndex}
|
||
className={`w-5 h-5 ${
|
||
starIndex < person.stars
|
||
? "text-pr-green fill-pr-green"
|
||
: "text-pr-gray"
|
||
}`}
|
||
/>
|
||
)
|
||
)}
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
|
||
<div className="p-4 bg-pr-gray">
|
||
<div className="text-sm text-white font-semibold font-persian text-right mr-16">
|
||
کل افراد: {toPersianDigits(peopleRanking.length)}
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Main Ideas Table */}
|
||
<div className="col-span-2 row-span-1">
|
||
<h3 className="text-base text-center mb-2 font-persian font-bold text-foreground">
|
||
لیست ایده ها
|
||
</h3>
|
||
<Card className="bg-transparent backdrop-blur-sm rounded-xl overflow-hidden">
|
||
<CardContent className="p-0">
|
||
<div className="relative">
|
||
<Table
|
||
containerRef={scrollContainerRef}
|
||
containerClassName="overflow-auto custom-scrollbar max-h-[calc(50vh-120px)]"
|
||
>
|
||
<TableHeader className="sticky top-0 z-50 bg-pr-gray">
|
||
<TableRow className="bg-pr-gray">
|
||
{columns.map((column) => (
|
||
<TableHead
|
||
key={column.key}
|
||
className="font-persian whitespace-nowrap text-white text-sm text-center font-semibold bg-pr-gray sticky top-0 z-20"
|
||
style={{ width: column.width }}
|
||
>
|
||
{column.sortable ? (
|
||
<button
|
||
onClick={() => handleSort(column.key)}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<span>{column.label}</span>
|
||
{column.key === "increased_revenue" && (
|
||
<span className="text-[#ACACAC] text-right font-light text-[8px]">
|
||
میلیون <br />
|
||
ریال
|
||
</span>
|
||
)}
|
||
{sortConfig.field === column.key ? (
|
||
sortConfig.direction === "asc" ? (
|
||
<ChevronUp className="w-4 h-4" />
|
||
) : (
|
||
<ChevronDown className="w-4 h-4" />
|
||
)
|
||
) : (
|
||
<div className="w-4 h-4" />
|
||
)}
|
||
</button>
|
||
) : (
|
||
column.label
|
||
)}
|
||
</TableHead>
|
||
))}
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{loading ? (
|
||
Array.from({ length: 20 }).map((_, index) => (
|
||
<TableRow
|
||
key={`skeleton-${index}`}
|
||
className="text-sm leading-tight h-12"
|
||
>
|
||
{columns.map((column) => (
|
||
<TableCell
|
||
key={column.key}
|
||
className="text-right whitespace-nowrap border-success/20 py-2 px-4"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-3 h-3 bg-muted rounded-full animate-pulse" />
|
||
<div
|
||
className="h-3 bg-muted rounded animate-pulse"
|
||
style={{
|
||
width: `${Math.random() * 60 + 40}%`,
|
||
}}
|
||
/>
|
||
</div>
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
))
|
||
) : ideas.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell
|
||
colSpan={columns.length}
|
||
className="text-center py-8"
|
||
>
|
||
<span className="text-muted-foreground font-persian">
|
||
هیچ ایدهای یافت نشد
|
||
</span>
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
ideas.map((idea, index) => (
|
||
<TableRow
|
||
key={`${idea.idea_title}-${index}`}
|
||
className="text-sm leading-tight h-12 not-last:border-b-[1px] border-b-pr-gray"
|
||
>
|
||
{columns.map((column) => (
|
||
<TableCell
|
||
key={column.key}
|
||
className="text-right text-sm text-white font-normal whitespace-nowrap py-2 px-4"
|
||
>
|
||
{renderCellContent(idea, column)}
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
|
||
{/* Infinite scroll trigger */}
|
||
<div ref={observerRef} className="h-auto"></div>
|
||
</CardContent>
|
||
|
||
{/* Footer */}
|
||
<div className="p-4 bg-pr-gray">
|
||
<div className="flex items-center justify-between font-semibold text-sm text-white font-persian">
|
||
<span>کل ایدهها: {toPersianDigits(actualTotalCount)}</span>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
{loadingMore && (
|
||
<div className="flex items-center justify-center py-2">
|
||
<div className="flex items-center gap-2">
|
||
<RefreshCw className="w-4 h-4 animate-spin text-success" />
|
||
<span className="font-persian text-muted-foreground text-sm"></span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
{/* Chart Section */}
|
||
<BaseCard
|
||
icon={TrendingUp}
|
||
className="col-span-1 mt-12 row-start-2 col-start-3 row-span-1"
|
||
title="نمودار ایدهها"
|
||
>
|
||
<MemoizedVerticalBarChart
|
||
chartData={chartData}
|
||
loadingChart={loadingChart}
|
||
chartConfig={chartConfig}
|
||
getChartStatusColor={getChartStatusColor}
|
||
toPersianDigits={toPersianDigits}
|
||
formatNumber={formatNumber}
|
||
/>
|
||
</BaseCard>
|
||
<div className="col-span-1 col-start-2 mt-12 row-start-2 flex flex-col-reverse justify-end gap-2 row-span-1">
|
||
<BaseCard title="ایدههای فناوری و نوآوری">
|
||
{loadingStats ? (
|
||
<div className="flex items-center gap-2 justify-center flex-row-reverse">
|
||
<div className="w-[6rem] h-[6rem] bg-gray-600 rounded-full animate-pulse"></div>
|
||
<div className="font-bold font-persian text-center">
|
||
<div className="flex flex-col justify-between items-center gap-2">
|
||
<div className="flex items-center gap-1 text-base">
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse w-16"></div>
|
||
<div className="h-5 bg-gray-600 rounded animate-pulse w-8"></div>
|
||
</div>
|
||
<div className="flex items-center gap-1 text-base">
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse w-20"></div>
|
||
<div className="h-5 bg-gray-600 rounded animate-pulse w-8"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center gap-2 justify-center flex-row-reverse">
|
||
<ChartContainer
|
||
config={chartConfig}
|
||
className="aspect-square w-[6rem] h-auto"
|
||
>
|
||
<RadialBarChart
|
||
data={[
|
||
{
|
||
browser: "ideas",
|
||
visitors:
|
||
parseFloat(
|
||
statsData?.registered_innovation_technology_idea ||
|
||
"0"
|
||
) > 0
|
||
? Math.round(
|
||
(parseFloat(
|
||
statsData?.registered_innovation_technology_idea ||
|
||
"0"
|
||
) /
|
||
parseFloat(
|
||
statsData?.registered_innovation_technology_idea ||
|
||
"1"
|
||
)) *
|
||
100
|
||
)
|
||
: 0,
|
||
fill: "var(--color-green)",
|
||
},
|
||
]}
|
||
startAngle={90}
|
||
endAngle={
|
||
90 +
|
||
((parseFloat(
|
||
statsData?.registered_innovation_technology_idea ||
|
||
"0"
|
||
) > 0
|
||
? Math.round(
|
||
(parseFloat(
|
||
statsData?.ongoing_innovation_technology_ideas ||
|
||
"0"
|
||
) /
|
||
parseFloat(
|
||
statsData?.registered_innovation_technology_idea ||
|
||
"1"
|
||
)) *
|
||
100
|
||
)
|
||
: 0) /
|
||
100) *
|
||
360
|
||
}
|
||
innerRadius={35}
|
||
outerRadius={55}
|
||
>
|
||
<PolarGrid
|
||
gridType="circle"
|
||
radialLines={false}
|
||
stroke="none"
|
||
className="first:fill-pr-red last:fill-[#24273A]"
|
||
polarRadius={[38, 31]}
|
||
/>
|
||
<RadialBar
|
||
dataKey="visitors"
|
||
background
|
||
cornerRadius={5}
|
||
/>
|
||
<PolarRadiusAxis
|
||
tick={false}
|
||
tickLine={false}
|
||
axisLine={false}
|
||
>
|
||
<Label
|
||
content={({ viewBox }) => {
|
||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||
return (
|
||
<text
|
||
x={viewBox.cx}
|
||
y={viewBox.cy}
|
||
textAnchor="middle"
|
||
dominantBaseline="middle"
|
||
>
|
||
<tspan
|
||
x={viewBox.cx}
|
||
y={viewBox.cy}
|
||
className="fill-foreground text-lg font-bold"
|
||
>
|
||
%
|
||
{formatNumber(
|
||
parseFloat(
|
||
statsData?.registered_innovation_technology_idea ||
|
||
"0"
|
||
) > 0
|
||
? Math.round(
|
||
(parseFloat(
|
||
statsData?.ongoing_innovation_technology_ideas ||
|
||
"0"
|
||
) /
|
||
parseFloat(
|
||
statsData?.registered_innovation_technology_idea ||
|
||
"1"
|
||
)) *
|
||
100
|
||
)
|
||
: 0
|
||
)}
|
||
</tspan>
|
||
</text>
|
||
);
|
||
}
|
||
}}
|
||
/>
|
||
</PolarRadiusAxis>
|
||
</RadialBarChart>
|
||
</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 text-base">
|
||
<div className="font-light text-sm">ثبت شده :</div>
|
||
{formatNumber(
|
||
statsData?.registered_innovation_technology_idea ||
|
||
"0"
|
||
)}
|
||
</span>
|
||
<span className="flex items-center gap-1 font-bold text-base">
|
||
<div className="font-light text-sm">در حال اجرا :</div>
|
||
{formatNumber(
|
||
statsData?.ongoing_innovation_technology_ideas || "0"
|
||
)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</BaseCard>
|
||
|
||
{loadingStats ? (
|
||
<div className="bg-gradient-to-br from-[#444a64] to-[#252942] rounded-xl p-4 border border-[#3F415A]">
|
||
<div className="space-y-3">
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse w-24"></div>
|
||
<div className="space-y-2">
|
||
<div className="h-8 bg-gray-600 rounded animate-pulse w-16"></div>
|
||
<div className="h-3 bg-gray-600 rounded animate-pulse w-20"></div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse w-6"></div>
|
||
<div className="h-3 bg-gray-600 rounded animate-pulse w-16"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<MetricCard
|
||
title="درآمد افزایش یافته"
|
||
value={
|
||
statsData?.increased_revenue_from_ideas?.replaceAll(
|
||
",",
|
||
""
|
||
) || "0"
|
||
}
|
||
percentValue={statsData?.increased_revenue_from_ideas_percent}
|
||
percentLabel="درصد به کل درآمد"
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Details Dialog */}
|
||
<Dialog open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
|
||
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-6xl max-h-[90vh] overflow-y-auto">
|
||
<DialogHeader className="border-b border-gray-600/30 pb-4 mb-6">
|
||
<DialogTitle className="text-right font-persian text-xl text-white flex items-center justify-between">
|
||
<span>عنوان ایده: میکروکاتالیزورهای دما بالا</span>
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
{selectedIdea && (
|
||
<div className="flex w-full justify-center gap-4">
|
||
<div className="flex gap-4 flex-col text-right font-persian w-full border-l-2 border-l-pr-gray px-4 pb-4">
|
||
{/* مشخصات ایده پردازان Section */}
|
||
<div className="">
|
||
<h3 className="text-base font-bold text-white mb-2">
|
||
مشخصات ایده پردازان
|
||
</h3>
|
||
<div className="flex flex-col gap-4 mr-5">
|
||
<div className="grid grid-cols-3 items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light">
|
||
نام ایده پرداز:
|
||
</span>
|
||
</div>
|
||
<span className="text-white font-normal text-sm mr-10">
|
||
{selectedIdea.full_name || "-"}
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light">
|
||
شماره پرسنلی:
|
||
</span>
|
||
</div>
|
||
<span className="text-white font-normal text-sm mr-10">
|
||
{toPersianDigits(selectedIdea.personnel_number) ||
|
||
"۱۳۰۶۵۸۰۶"}
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light">
|
||
مدیریت:
|
||
</span>
|
||
</div>
|
||
<span className="text-white font-normal text-sm mr-10">
|
||
{selectedIdea.management || "مدیریت توسعه"}
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light">
|
||
معاونت:
|
||
</span>
|
||
</div>
|
||
<span className="text-white font-normal text-sm mr-10">
|
||
{selectedIdea.deputy || "توسعه"}
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 items-center gap-2 col-span-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light">
|
||
اعضای تیم:
|
||
</span>
|
||
</div>
|
||
<span className="text-white font-normal text-sm mr-10">
|
||
{selectedIdea.innovator_team_members ||
|
||
"رضا حسین پور, محمد رضا شیاطی, محمد مددی"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* مشخصات ایده Section */}
|
||
<div className="">
|
||
<h3 className="text-base font-bold text-white mb-2">
|
||
مشخصات ایده
|
||
</h3>
|
||
<div className="flex flex-col gap-4 mr-5">
|
||
<div className="grid grid-cols-3 items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light">
|
||
تاریخ ثبت ایده:
|
||
</span>
|
||
</div>
|
||
<span className="text-white font-normal text-sm mr-10">
|
||
{formatDate(selectedIdea.idea_registration_date) ||
|
||
"-"}
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light">
|
||
نوع نوآوری:
|
||
</span>
|
||
</div>
|
||
<span className="text-white font-normal text-sm mr-10">
|
||
{selectedIdea.innovation_type || "-"}
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light">
|
||
اصالت ایده:
|
||
</span>
|
||
</div>
|
||
<span className="text-white font-normal text-sm mr-10">
|
||
{selectedIdea.idea_originality || "-"}
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light min-w-max">
|
||
محور ایده:
|
||
</span>
|
||
</div>
|
||
<span className="text-white font-normal text-sm mr-10">
|
||
{selectedIdea.idea_axis || "-"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* نتایج و خروجی ها Section */}
|
||
<div>
|
||
<h3 className="text-base font-bold text-white mb-2">
|
||
نتایج و خروجی ها
|
||
</h3>
|
||
<div className="flex flex-col gap-4 mr-5">
|
||
<div className="grid grid-cols-3 items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light">
|
||
درآمد حاصل:
|
||
</span>
|
||
</div>
|
||
<span className="text-white text-sm font-normal mr-10">
|
||
{formatNumber(selectedIdea.increased_revenue) || "-"}
|
||
<span className="text-[11px] mr-2 font-light">
|
||
میلیون ریال
|
||
</span>
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light">
|
||
مقاله چاپ شده:
|
||
</span>
|
||
</div>
|
||
<span className="text-white font-normal cursor-pointer text-sm flex items-center gap-2 mr-10">
|
||
<Download className="h-4 w-4" />
|
||
دانلود
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||
<span className="text-white text-sm text-light">
|
||
پتنت ثبت شده:
|
||
</span>
|
||
</div>
|
||
<span className="text-white cursor-pointer font-normal text-sm flex items-center gap-2 mr-10">
|
||
<Download className="h-4 w-4" />
|
||
دانلود
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="w-full flex flex-col gap-8">
|
||
{/* شرح ایده Section */}
|
||
<div>
|
||
<h3 className="text-base font-bold text-white mb-4">
|
||
شرح ایده
|
||
</h3>
|
||
<div className="">
|
||
<p className="text-white text-sm">
|
||
{selectedIdea.idea_description || "-"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* شرح وضعیت موجود ایده Section */}
|
||
<div>
|
||
<h3 className="text-base font-bold text-white mb-4">
|
||
شرح وضعیت موجود ایده
|
||
</h3>
|
||
<div className="">
|
||
<p className="text-white leading-relaxed text-sm">
|
||
{selectedIdea.idea_current_status_description || "-"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* منافع حاصل از ایده Section */}
|
||
<div>
|
||
<h3 className="text-base font-bold text-white mb-4">
|
||
منافع حاصل از ایده
|
||
</h3>
|
||
<div>
|
||
<p className="text-white leading-relaxed text-sm">
|
||
{selectedIdea.idea_execution_benefits || "-"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* بهبود های فرآیندی ایده Section */}
|
||
<div>
|
||
<h3 className="text-base font-bold text-white mb-4">
|
||
بهبود های فرآیندی ایده
|
||
</h3>
|
||
<div>
|
||
<p className="text-white leading-relaxed text-sm">
|
||
{selectedIdea.process_improvements || "-"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
</DashboardLayout>
|
||
);
|
||
}
|