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 (
{/* Chart title skeleton */}
{/* Chart area skeleton */}
{/* Y-axis labels */}
{Array.from({ length: 4 }).map((_, i) => (
))}
{/* Bars skeleton */}
{Array.from({ length: 4 }).map((_, i) => (
{/* Bar */}
{/* X-axis label */}
))}
);
}
if (!chartData.length) {
return (
وضعیت ایده ها
هیچ دادهای یافت نشد
);
}
// 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 (
toPersianDigits(value)}
label={{
value: "تعداد برنامه ها",
angle: -90,
position: "insideLeft",
fill: "#94a3b8",
fontSize: 11,
offset: 0,
dy: 0,
style: { textAnchor: "middle" },
}}
/>
`${formatNumber(Math.round(v))}`}
/>
);
}
);
const MemoizedVerticalBarChart = VerticalBarChart;
export function ManageIdeasTechPage() {
const [ideas, setIdeas] = useState([]);
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(null);
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
const [sortConfig, setSortConfig] = useState({
field: "idea_title",
direction: "asc",
});
const [date, setDate] = useStoredDate();
// People ranking state
const [peopleRanking, setPeopleRanking] = useState([]);
const [loadingPeople, setLoadingPeople] = useState(false);
// Chart state
const [chartData, setChartData] = useState([]);
const [loadingChart, setLoadingChart] = useState(false);
// Stats state
const [statsData, setStatsData] = useState(null);
const [loadingStats, setLoadingStats] = useState(false);
const observerRef = useRef(null);
const fetchingRef = useRef(false);
const scrollTimeoutRef = useRef(null);
const scrollContainerRef = useRef(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 = {
"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 = {};
const seenStatuses = new Set();
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 {String(value)};
case "idea_registration_date":
return (
{formatDate(String(value))}
);
case "idea_status":
return (
{!!value ? String(value) : "-"}
);
case "increased_revenue":
return (
{formatCurrency(String(value || "0")).replace("ریال", "")}
);
case "details":
return (
);
default:
return (
{(value && String(value)) || "-"}
);
}
};
return (
{/* People Ranking Table */}
رتبه بندی نوآوران
رتبه
ایده پرداز
امتیاز
{loadingPeople ? (
Array.from({ length: 10 }).map((_, index) => (
{Array.from({ length: 5 }).map(
(_, starIndex) => (
)
)}
))
) : peopleRanking.length === 0 ? (
هیچ دادهای یافت نشد
) : (
peopleRanking.map((person) => (
{toPersianDigits(person.ranking)}
{person.full_name}
{Array.from({ length: 5 }).map(
(_, starIndex) => (
)
)}
))
)}
کل افراد: {toPersianDigits(peopleRanking.length)}
{/* Main Ideas Table */}
لیست ایده ها
{columns.map((column) => (
{column.sortable ? (
) : (
column.label
)}
))}
{loading ? (
Array.from({ length: 20 }).map((_, index) => (
{columns.map((column) => (
))}
))
) : ideas.length === 0 ? (
هیچ ایدهای یافت نشد
) : (
ideas.map((idea, index) => (
{columns.map((column) => (
{renderCellContent(idea, column)}
))}
))
)}
{/* Infinite scroll trigger */}
{/* Footer */}
کل ایدهها: {toPersianDigits(actualTotalCount)}
{loadingMore && (
)}
{/* Chart Section */}
{loadingStats ? (
) : (
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}
>
ثبت شده :
{formatNumber(
statsData?.registered_innovation_technology_idea ||
"0"
)}
در حال اجرا :
{formatNumber(
statsData?.ongoing_innovation_technology_ideas || "0"
)}
)}
{loadingStats ? (
) : (
)}
{/* Details Dialog */}
);
}