From 173176bbb5968ae3382a49f181f63c04d9b0d034 Mon Sep 17 00:00:00 2001 From: MehrdadAdabi <126083584+mehrdadAdabi@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:11:38 +0330 Subject: [PATCH] feat: completed designed --- app/components/dashboard/dashboard-home.tsx | 478 ++++++------ app/components/dashboard/header.tsx | 259 ++++++- app/components/dashboard/layout.tsx | 30 +- .../green-innovation-page.tsx | 6 + .../product-innovation-page.tsx | 688 +++++++++++------- app/components/ui/Calendar.tsx | 67 ++ app/lib/utils.ts | 29 +- app/types/util.type.ts | 6 + 8 files changed, 1019 insertions(+), 544 deletions(-) create mode 100644 app/components/ui/Calendar.tsx create mode 100644 app/types/util.type.ts diff --git a/app/components/dashboard/dashboard-home.tsx b/app/components/dashboard/dashboard-home.tsx index a3074f2..1b30aae 100644 --- a/app/components/dashboard/dashboard-home.tsx +++ b/app/components/dashboard/dashboard-home.tsx @@ -1,38 +1,6 @@ -import { useState, useEffect } from "react"; -import { DashboardLayout } from "./layout"; -import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; -import { Progress } from "~/components/ui/progress"; -import { Badge } from "~/components/ui/badge"; -import { Button } from "~/components/ui/button"; -import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - LineChart, - Line, -} from "recharts"; -import apiService from "~/lib/api"; +import { Book, CheckCircle } from "lucide-react"; +import { useEffect, useState } from "react"; import toast from "react-hot-toast"; -import { - Calendar, - TrendingUp, - TrendingDown, - Target, - Lightbulb, - DollarSign, - Minus, - CheckCircle, - Book, -} from "lucide-react"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs"; -import { CustomBarChart } from "~/components/ui/custom-bar-chart"; -import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart"; -import { InteractiveBarChart } from "./interactive-bar-chart"; -import { D3ImageInfo } from "./d3-image-info"; import { Label, PolarGrid, @@ -40,10 +8,20 @@ import { RadialBar, RadialBarChart, } from "recharts"; -import { ChartContainer } from "~/components/ui/chart"; -import { formatNumber } from "~/lib/utils"; -import { MetricCard } from "~/components/ui/metric-card"; import { BaseCard } from "~/components/ui/base-card"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { ChartContainer } from "~/components/ui/chart"; +import { MetricCard } from "~/components/ui/metric-card"; +import { Progress } from "~/components/ui/progress"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; +import apiService from "~/lib/api"; +import { EventBus, formatNumber } from "~/lib/utils"; +import type { CalendarDate } from "~/types/util.type"; +import { D3ImageInfo } from "./d3-image-info"; +import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart"; +import { InteractiveBarChart } from "./interactive-bar-chart"; +import { DashboardLayout } from "./layout"; export function DashboardHome() { const [dashboardData, setDashboardData] = useState(null); @@ -51,17 +29,30 @@ export function DashboardHome() { const [error, setError] = useState(null); // Chart and schematic data from select API const [companyChartData, setCompanyChartData] = useState< - { category: string; capacity: number; revenue: number; cost: number , costI : number, - capacityI : number, - revenueI : number }[] + { + category: string; + capacity: number; + revenue: number; + cost: number; + costI: number; + capacityI: number; + revenueI: number; + }[] >([]); - const [totalIncreasedCapacity, setTotalIncreasedCapacity] = useState(0); + // const [totalIncreasedCapacity, setTotalIncreasedCapacity] = + // useState(0); useEffect(() => { fetchDashboardData(); }, []); - const fetchDashboardData = async () => { + useEffect(() => { + EventBus.on("dateSelected", (date: CalendarDate) => { + if (date) fetchDashboardData(date.start, date.end); + }); + }, []); + + const fetchDashboardData = async (startDate?: string, endDate?: string) => { try { setLoading(true); setError(null); @@ -74,12 +65,18 @@ export function DashboardHome() { // Fetch top cards data const topCardsResponse = await apiService.call({ - main_page_first_function: {}, + main_page_first_function: { + start_date: startDate || null, + end_date: endDate || null, + }, }); // Fetch left section data const leftCardsResponse = await apiService.call({ - main_page_second_function: {}, + main_page_second_function: { + start_date: startDate || null, + end_date: endDate || null, + }, }); const topCardsResponseData = JSON.parse(topCardsResponse?.data); @@ -130,12 +127,30 @@ export function DashboardHome() { let incCapacityTotal = 0; const chartRows = rows.map((r) => { const rel = r?.related_company ?? "-"; - const preFee = Number(r?.pre_innovation_fee_sum ?? 0) >= 0 ? r?.pre_innovation_fee_sum : 0; - const costRed = Number(r?.innovation_cost_reduction_sum ?? 0) >= 0 ? r?.innovation_cost_reduction_sum : 0; - const preCap = Number(r?.pre_project_production_capacity_sum ?? 0) >= 0 ? r?.pre_project_production_capacity_sum : 0; - const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0 ? r?.increased_capacity_after_innovation_sum : 0; - const preInc = Number(r?.pre_project_income_sum ?? 0) >= 0 ? r?.pre_project_income_sum : 0; - const incInc = Number(r?.increased_income_after_innovation_sum ?? 0) >= 0 ? r?.increased_income_after_innovation_sum : 0; + const preFee = + Number(r?.pre_innovation_fee_sum ?? 0) >= 0 + ? r?.pre_innovation_fee_sum + : 0; + const costRed = + Number(r?.innovation_cost_reduction_sum ?? 0) >= 0 + ? r?.innovation_cost_reduction_sum + : 0; + const preCap = + Number(r?.pre_project_production_capacity_sum ?? 0) >= 0 + ? r?.pre_project_production_capacity_sum + : 0; + const incCap = + Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0 + ? r?.increased_capacity_after_innovation_sum + : 0; + const preInc = + Number(r?.pre_project_income_sum ?? 0) >= 0 + ? r?.pre_project_income_sum + : 0; + const incInc = + Number(r?.increased_income_after_innovation_sum ?? 0) >= 0 + ? r?.increased_income_after_innovation_sum + : 0; incCapacityTotal += incCap; @@ -147,14 +162,14 @@ export function DashboardHome() { capacity: isFinite(capacityPct) ? capacityPct : 0, revenue: isFinite(revenuePct) ? revenuePct : 0, cost: isFinite(costPct) ? costPct : 0, - costI : costRed, - capacityI : incCap, - revenueI : incInc + costI: costRed, + capacityI: incCap, + revenueI: incInc, }; }); setCompanyChartData(chartRows); - setTotalIncreasedCapacity(incCapacityTotal); + // setTotalIncreasedCapacity(incCapacityTotal); } catch (error) { console.error("Error fetching dashboard data:", error); const errorMessage = @@ -172,20 +187,19 @@ export function DashboardHome() { return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }]; const registered = parseFloat( - dashboardData.topData.registered_innovation_technology_idea || "0", + dashboardData.topData.registered_innovation_technology_idea || "0" ); const ongoing = parseFloat( - dashboardData.topData.ongoing_innovation_technology_ideas || "0", + dashboardData.topData.ongoing_innovation_technology_ideas || "0" ); - const percentage = - registered > 0 ? (ongoing / registered) * 100 : 0; + const percentage = registered > 0 ? (ongoing / registered) * 100 : 0; return [ { browser: "safari", visitors: percentage, fill: "var(--color-safari)" }, ]; }; - const chartData = getIdeasChartData(); + // const chartData = getIdeasChartData(); const chartConfig = { visitors: { @@ -329,20 +343,19 @@ export function DashboardHome() { visitors: parseFloat( dashboardData.topData - ?.registered_innovation_technology_idea || "0", + ?.registered_innovation_technology_idea || "0" ) > 0 ? Math.round( (parseFloat( dashboardData.topData - ?.ongoing_innovation_technology_ideas || - "0", + ?.ongoing_innovation_technology_ideas || "0" ) / parseFloat( dashboardData.topData ?.registered_innovation_technology_idea || - "1", + "1" )) * - 100, + 100 ) : 0, fill: "var(--color-green)", @@ -353,19 +366,18 @@ export function DashboardHome() { 90 + ((parseFloat( dashboardData.topData - ?.registered_innovation_technology_idea || "0", + ?.registered_innovation_technology_idea || "0" ) > 0 ? Math.round( (parseFloat( dashboardData.topData - ?.ongoing_innovation_technology_ideas || "0", + ?.ongoing_innovation_technology_ideas || "0" ) / parseFloat( dashboardData.topData - ?.registered_innovation_technology_idea || - "1", + ?.registered_innovation_technology_idea || "1" )) * - 100, + 100 ) : 0) / 100) * @@ -381,11 +393,7 @@ export function DashboardHome() { className="first:fill-pr-red last:fill-[#24273A]" polarRadius={[38, 31]} /> - + 0 ? Math.round( (parseFloat( dashboardData.topData ?.ongoing_innovation_technology_ideas || - "0", + "0" ) / parseFloat( dashboardData.topData ?.registered_innovation_technology_idea || - "1", + "1" )) * - 100, + 100 ) - : 0, + : 0 )} @@ -443,14 +451,14 @@ export function DashboardHome() {
ثبت شده :
{formatNumber( dashboardData.topData - ?.registered_innovation_technology_idea || "0", + ?.registered_innovation_technology_idea || "0" )}
در حال اجرا :
{formatNumber( dashboardData.topData - ?.ongoing_innovation_technology_ideas || "0", + ?.ongoing_innovation_technology_ideas || "0" )}
@@ -460,130 +468,144 @@ export function DashboardHome() { {/* Revenue Card */} {/* Cost Reduction Card */} {/* Budget Ratio Card */}
- + + + + - - - - - - - -
-
- -
مصوب :
- {formatNumber( - Math.round( - parseFloat( - dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace( - /,/g, - "", - ) || "0", - ), - ), - )} -
- -
جذب شده :
- {formatNumber( - Math.round( - parseFloat( - dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace( - /,/g, - "", - ) || "0", - ), - ), - )} -
-
-
+
+ +
+ {/* Main Content with Tabs */} - + شماتیک - + مقایسه ای @@ -611,27 +636,25 @@ export function DashboardHome() {
{ - const imageMap: Record = { - "بسپاران": "/besparan.png", - "خوارزمی": "/khwarazmi.png", - "فراورش 1": "/faravash1.png", - "فراورش 2": "/faravash2.png", - "کیمیا": "/kimia.png", - "آب نیرو": "/abniro.png", - }; + companies={companyChartData.map((item) => { + const imageMap: Record = { + بسپاران: "/besparan.png", + خوارزمی: "/khwarazmi.png", + "فراورش 1": "/faravash1.png", + "فراورش 2": "/faravash2.png", + کیمیا: "/kimia.png", + "آب نیرو": "/abniro.png", + }; - return { - id: item.category, - name: item.category, - imageUrl: imageMap[item.category] || "/placeholder.png", - cost: item?.costI || 0, - capacity: item?.capacityI || 0, - revenue: item?.revenueI || 0, - }; - }) - } + return { + id: item.category, + name: item.category, + imageUrl: imageMap[item.category] || "/placeholder.png", + cost: item?.costI || 0, + capacity: item?.capacityI || 0, + revenue: item?.revenueI || 0, + }; + })} />
@@ -649,7 +672,7 @@ export function DashboardHome() { @@ -667,21 +690,21 @@ export function DashboardHome() { { label: "اجرا شده", value: parseFloat( - dashboardData?.leftData?.executed_project || "0", + dashboardData?.leftData?.executed_project || "0" ), color: "bg-pr-green", }, { label: "در حال اجرا", value: parseFloat( - dashboardData?.leftData?.in_progress_project || "0", + dashboardData?.leftData?.in_progress_project || "0" ), color: "bg-pr-blue", }, { label: "برنامه‌ریزی شده", value: parseFloat( - dashboardData?.leftData?.planned_project || "0", + dashboardData?.leftData?.planned_project || "0" ), color: "bg-pr-red", }, @@ -706,7 +729,7 @@ export function DashboardHome() { {formatNumber( - dashboardData.leftData?.printed_books_count || "0", + dashboardData.leftData?.printed_books_count || "0" )} @@ -717,7 +740,7 @@ export function DashboardHome() { {formatNumber( - dashboardData.leftData?.registered_patents_count || "0", + dashboardData.leftData?.registered_patents_count || "0" )} @@ -728,7 +751,7 @@ export function DashboardHome() { {formatNumber( - dashboardData.leftData?.published_reports_count || "0", + dashboardData.leftData?.published_reports_count || "0" )} @@ -739,7 +762,7 @@ export function DashboardHome() { {formatNumber( - dashboardData.leftData?.printed_articles_count || "0", + dashboardData.leftData?.printed_articles_count || "0" )} @@ -763,7 +786,7 @@ export function DashboardHome() { {formatNumber( - dashboardData.leftData?.attended_conferences_count || "0", + dashboardData.leftData?.attended_conferences_count || "0" )} @@ -774,7 +797,7 @@ export function DashboardHome() { {formatNumber( - dashboardData.leftData?.attended_events_count || "0", + dashboardData.leftData?.attended_events_count || "0" )} @@ -785,7 +808,7 @@ export function DashboardHome() { {formatNumber( - dashboardData.leftData?.attended_exhibitions_count || "0", + dashboardData.leftData?.attended_exhibitions_count || "0" )} @@ -796,7 +819,7 @@ export function DashboardHome() { {formatNumber( - dashboardData.leftData?.organized_events_count || "0", + dashboardData.leftData?.organized_events_count || "0" )} @@ -804,9 +827,8 @@ export function DashboardHome() { - - - + + ); } diff --git a/app/components/dashboard/header.tsx b/app/components/dashboard/header.tsx index ddc0231..7d980b6 100644 --- a/app/components/dashboard/header.tsx +++ b/app/components/dashboard/header.tsx @@ -1,21 +1,20 @@ -import React, { useEffect, useState } from "react"; -import { useAuth } from "~/contexts/auth-context"; -import { Link } from "react-router"; -import { cn } from "~/lib/utils"; -import { Button } from "~/components/ui/button"; +import jalaali from "jalaali-js"; import { + Calendar, + ChevronLeft, + Menu, PanelLeft, - + Server, Settings, User, - - Menu, - ChevronDown, - Server, - ChevronLeft , - } from "lucide-react"; +import React, { useEffect, useRef, useState } from "react"; +import { Link } from "react-router"; +import { Button } from "~/components/ui/button"; +import { Calendar as CustomCalendar } from "~/components/ui/Calendar"; +import { useAuth } from "~/contexts/auth-context"; import apiService from "~/lib/api"; +import { cn, EventBus } from "~/lib/utils"; interface HeaderProps { onToggleSidebar?: () => void; @@ -24,6 +23,52 @@ interface HeaderProps { titleIcon?: React.ComponentType<{ className?: string }> | null; } +interface MonthItem { + id: string; + label: string; + start: string; + end: string; +} + +interface CurrentDay { + start?: string; + end?: string; + sinceMonth?: string; + fromMonth?: string; +} + +interface SelectedDate { + since?: number; + until?: number; +} + +const monthList: Array = [ + { + id: "month-1", + label: "بهار", + start: "01/01", + end: "03/31", + }, + { + id: "month-2", + label: "تابستان", + start: "04/01", + end: "06/31", + }, + { + id: "month-3", + label: "پاییز", + start: "07/01", + end: "09/31", + }, + { + id: "month-4", + label: "زمستان", + start: "10/01", + end: "12/29", + }, +]; + export function Header({ onToggleSidebar, className, @@ -31,25 +76,111 @@ export function Header({ titleIcon, }: HeaderProps) { const { user } = useAuth(); - const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); - const [isNotificationOpen, setIsNotificationOpen] = useState(false); + const { jy } = jalaali.toJalaali(new Date()); + + const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); + const [isNotificationOpen, setIsNotificationOpen] = useState(false); + const [selectedDate, setSelectedDate] = useState(); + const [openCalendar, setOpenCalendar] = useState(false); + const calendarRef = useRef(null); + const [currentYear, setCurrentYear] = useState({ + since: jy, + until: jy, + }); const redirectHandler = async () => { try { - const getData = await apiService.post('/GenerateSsoCode') - //const url = `http://localhost:3000/redirect/${getData.data}`; - const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`; + const getData = await apiService.post("/GenerateSsoCode"); + const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`; window.open(url, "_blank"); } catch (error) { console.log(error); } - } + }; + + const nextFromYearHandler = () => { + if (currentYear && (currentYear.since ?? 0) < (currentYear.until ?? 0)) { + setCurrentYear((prev) => ({ + ...prev, + since: currentYear?.since! + 1, + })); + } + }; + + const prevFromYearHandler = () => { + setCurrentYear((prev) => ({ + ...prev, + since: currentYear?.since! - 1, + })); + }; + + const selectFromDateHandler = (val: MonthItem) => { + const data = { + start: `${currentYear.since}/${val.start}`, + sinceMonth: val.label, + }; + setSelectedDate((prev) => ({ + ...prev, + ...data, + })); + }; + + const nextUntilYearHandler = () => { + setCurrentYear((prev) => ({ + ...prev, + until: currentYear?.until! + 1, + })); + }; + + const prevUntilYearHandler = () => { + if (currentYear && (currentYear.since ?? 0) < (currentYear.until ?? 0)) { + setCurrentYear((prev) => ({ + ...prev, + until: currentYear?.until! - 1, + })); + } + }; + + const selectUntilDateHandler = (val: MonthItem) => { + const data = { + end: `${currentYear.until}/${val.end}`, + fromMonth: val.label, + }; + setSelectedDate((prev) => ({ + ...prev, + ...data, + })); + toggleCalendar(); + }; + + const toggleCalendar = () => { + setOpenCalendar(!openCalendar); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + calendarRef.current && + !calendarRef.current.contains(event.target as Node) + ) { + setOpenCalendar(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + useEffect(() => { + EventBus.emit("dateSelected", selectedDate); + }, [currentYear, selectedDate]); return (
{/* Left Section */} @@ -69,24 +200,74 @@ export function Header({ {/* Page Title */}

{/* Right-side icon for current page */} - {titleIcon ? ( + {titleIcon ? (
{React.createElement(titleIcon, { className: "w-5 h-5 " })}
) : ( - + + )} + {title.includes("-") ? ( +
+
+ {title.split("-")[0]} + + {title.split("-")[1]} +
+
+ ) : ( + title )} - {title.includes("-") ? ( - - {title.split("-")[0]} - - {title.split("-")[1]} - -) : ( - title -)} -

+ +
+
+ + {selectedDate ? ( +
+
+ از + {selectedDate?.sinceMonth} + {currentYear.since} +
+
+ تا + {selectedDate?.fromMonth} + {currentYear.until} +
+
+ ) : ( + "تاریخ مورد نظر خود را انتخاب نمایید" + )} +
+ + {openCalendar && ( +
+ + + +
+ )} +
{/* Right Section */} @@ -94,14 +275,15 @@ export function Header({ {/* User Menu */}
- - { - user?.id === 2041 && - } + ورود به میزکار مدیریت + + )} diff --git a/app/components/dashboard/layout.tsx b/app/components/dashboard/layout.tsx index 25e9af5..e133218 100644 --- a/app/components/dashboard/layout.tsx +++ b/app/components/dashboard/layout.tsx @@ -1,9 +1,8 @@ import { useState } from "react"; import { cn } from "~/lib/utils"; -import { Sidebar } from "./sidebar"; import { Header } from "./header"; +import { Sidebar } from "./sidebar"; import { StrategicAlignmentPopup } from "./strategic-alignment-popup"; -import apiService from "~/lib/api"; interface DashboardLayoutProps { children: React.ReactNode; @@ -18,9 +17,14 @@ export function DashboardLayout({ }: DashboardLayoutProps) { const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); - const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] = useState(false); - const [currentTitle, setCurrentTitle] = useState(title ?? "صفحه اول"); - const [currentTitleIcon, setCurrentTitleIcon] = useState | null | undefined>(undefined); + const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] = + useState(false); + const [currentTitle, setCurrentTitle] = useState( + title ?? "صفحه اول" + ); + const [currentTitleIcon, setCurrentTitleIcon] = useState< + React.ComponentType<{ className?: string }> | null | undefined + >(undefined); const toggleSidebarCollapse = () => { setIsSidebarCollapsed(!isSidebarCollapsed); @@ -30,8 +34,6 @@ export function DashboardLayout({ setIsMobileSidebarOpen(!isMobileSidebarOpen); }; - - return (
setIsStrategicAlignmentPopupOpen(true)} + onStrategicAlignmentClick={() => + setIsStrategicAlignmentPopupOpen(true) + } onTitleChange={(info) => { setCurrentTitle(info.title); setCurrentTitleIcon(info.icon ?? null); }} - />
@@ -85,7 +88,7 @@ export function DashboardLayout({
@@ -93,7 +96,10 @@ export function DashboardLayout({
- +
); } diff --git a/app/components/dashboard/project-management/green-innovation-page.tsx b/app/components/dashboard/project-management/green-innovation-page.tsx index 574aa75..c24e5b6 100644 --- a/app/components/dashboard/project-management/green-innovation-page.tsx +++ b/app/components/dashboard/project-management/green-innovation-page.tsx @@ -686,6 +686,12 @@ export function GreenInnovationPage() { { name: recycleParams.food.label, pv: 30, amt: 50 }, ]); + // useEffect(() => { + // EventBus.on("dateSelected", (date) => { + // debugger; + // }); + // }, []); + return (
diff --git a/app/components/dashboard/project-management/product-innovation-page.tsx b/app/components/dashboard/project-management/product-innovation-page.tsx index 1ec7c87..ed22582 100644 --- a/app/components/dashboard/project-management/product-innovation-page.tsx +++ b/app/components/dashboard/project-management/product-innovation-page.tsx @@ -1,46 +1,37 @@ -import { - ArrowDownCircle, - ArrowUpCircle, - Building2, - ChevronDown, - ChevronUp, - CirclePause, - DollarSign, - Funnel, - Loader2, - PickaxeIcon, - RefreshCw, - TrendingUp, - UserIcon, - UsersIcon, - Wrench, -} from "lucide-react"; +import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import toast from "react-hot-toast"; +import { Bar, BarChart, LabelList } from "recharts"; import { Badge } from "~/components/ui/badge"; -import { Button } from "~/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; -import { MetricCard } from "~/components/ui/metric-card"; import { BaseCard } from "~/components/ui/base-card"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent } from "~/components/ui/card"; import { Checkbox } from "~/components/ui/checkbox"; -import { Bar, BarChart, LabelList } from "recharts" +import { MetricCard } from "~/components/ui/metric-card"; import { Popover, - PopoverTrigger, PopoverContent, -} from "~/components/ui/popover" + PopoverTrigger, +} from "~/components/ui/popover"; -import { FunnelChart } from "~/components/ui/funnel-chart"; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; +import { + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; import { Dialog, DialogContent, - DialogDescription, - DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; -import { Label } from "~/components/ui/label"; +import { FunnelChart } from "~/components/ui/funnel-chart"; +import { Skeleton } from "~/components/ui/skeleton"; import { Table, TableBody, @@ -49,12 +40,10 @@ import { TableHeader, TableRow, } from "~/components/ui/table"; +import { Tooltip as TooltipSh, TooltipTrigger } from "~/components/ui/tooltip"; import apiService from "~/lib/api"; import { formatNumber, handleDataValue } from "~/lib/utils"; import { DashboardLayout } from "../layout"; -import { Skeleton } from "~/components/ui/skeleton"; -import { Tooltip as TooltipSh, TooltipTrigger, TooltipContent } from "~/components/ui/tooltip"; - interface ProjectData { project_no: string; @@ -139,15 +128,16 @@ const columns = [ { key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" }, ]; - -export default function Timeline( valueTimeLine : string) { - const stages = ["تجاری سازی", "توسعه", "تحلیل بازار", "ثبت ایده"]; - const currentStage = stages?.toReversed()?.findIndex((x : string) => x == valueTimeLine) +export default function Timeline(valueTimeLine: string) { + const stages = ["تجاری سازی", "توسعه", "تحلیل بازار", "ثبت ایده"]; + const currentStage = stages + ?.toReversed() + ?.findIndex((x: string) => x == valueTimeLine); const per = () => { - const main = stages?.findIndex((x) => x == "ثبت ایده") - console.log( 'yay ' , 25 * main + 12.5); - return 25 * main + 12.5 - } + const main = stages?.findIndex((x) => x == "ثبت ایده"); + console.log("yay ", 25 * main + 12.5); + return 25 * main + 12.5; + }; return (
{/* Year labels */} @@ -160,15 +150,20 @@ export default function Timeline( valueTimeLine : string) { {/* Timeline bar */}
{stages.map((stage, index) => ( -
+
- {stage} + {stage}
@@ -176,25 +171,32 @@ export default function Timeline( valueTimeLine : string) { ))} {/* Vertical line showing current position */} - { valueTimeLine?.length > 0 && ( <>
-
وضعیت فعلی
- ) } - -
+ {valueTimeLine?.length > 0 && ( + <> + {" "} +
+
+ وضعیت فعلی +
+ + )} +
); } - - export function ProductInnovationPage() { - const [showPopup, setShowPopup] = useState(false); + const [showPopup, setShowPopup] = useState(false); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); @@ -215,7 +217,7 @@ export function ProductInnovationPage() { new_product_funnel: 0, count_innovation_construction_inside_projects: 0, average_project_score: 0, - }); + }); const [sortConfig, setSortConfig] = useState({ field: "start_date", direction: "asc", @@ -241,7 +243,7 @@ export function ProductInnovationPage() { description: "میلیون ریال", descriptionPercent: "درصد به کل درآمد", color: "text-[#3AEA83]", - percent :0 + percent: 0, }, newProductExports: { id: "newProductExports", @@ -262,11 +264,9 @@ export function ProductInnovationPage() { const observerRef = useRef(null); const fetchingRef = useRef(false); - - const handleProjectDetails = async (project: ProductInnovationData) => { setSelectedProjectDetails(project); - console.log(project) + console.log(project); setDetailsDialogOpen(true); await fetchPopupData(project); }; @@ -278,26 +278,27 @@ export function ProductInnovationPage() { // Fetch popup stats const statsResponse = await apiService.call({ innovation_product_popup_function1: { - project_id: project.project_id - } + project_id: project.project_id, + }, }); if (statsResponse.state === 0) { const statsData = JSON.parse(statsResponse.data); - if (statsData.innovation_product_popup_function1 && statsData.innovation_product_popup_function1[0]) { - setPopupStats(JSON.parse(statsData.innovation_product_popup_function1)[0]); + if ( + statsData.innovation_product_popup_function1 && + statsData.innovation_product_popup_function1[0] + ) { + setPopupStats( + JSON.parse(statsData.innovation_product_popup_function1)[0] + ); } } // Fetch export chart data const chartResponse = await apiService.select({ ProcessName: "export_product_innovation", - OutputFields: [ - "product_title", - "full_season", - "sum(export_revenue)" - ], - GroupBy: ["product_title", "full_season"] + OutputFields: ["product_title", "full_season", "sum(export_revenue)"], + GroupBy: ["product_title", "full_season"], }); if (chartResponse.state === 0) { const chartData = JSON.parse(chartResponse.data); @@ -305,14 +306,13 @@ export function ProductInnovationPage() { // Set all data for line chart // Filter data for the selected project (bar chart) - const filteredData = chartData.filter(item => - item.product_title === project?.title + const filteredData = chartData.filter( + (item) => item.product_title === project?.title ); setAllExportData(chartData); setExportChartData(filteredData); } } - } catch (error) { console.error("Error fetching popup data:", error); } finally { @@ -334,9 +334,9 @@ export function ProductInnovationPage() { try { fetchingRef.current = true; - if (reset) { + if (reset) { setLoading(true); - setCurrentPage(1); + setCurrentPage(1); } else { setLoadingMore(true); } @@ -358,7 +358,7 @@ export function ProductInnovationPage() { "knowledge_based_certificate_obtained", "knowledge_based_certificate_number", "certificate_obtain_date", - "issuing_authority" + "issuing_authority", ], Sorts: [["start_date", "asc"]], Conditions: [["type_of_innovation", "=", "نوآوری در محصول"]], @@ -424,11 +424,15 @@ export function ProductInnovationPage() { } }; - const fetchStats = async () => { + const fetchStats = async (startDate?: string, endDate?: string) => { try { setStatsLoading(true); + const raw = await apiService.call({ - innovation_product_function: {}, + innovation_product_function: { + start_date: startDate, + end_date: endDate, + }, }); let payload: any = JSON.parse(raw?.data); @@ -444,21 +448,25 @@ export function ProductInnovationPage() { return 0; }; - const data: Array = JSON.parse( - payload?.innovation_product_function - ); + const data: Array = JSON.parse(payload?.innovation_product_function); const stats = data[0]; const normalized: ProductInnovationStats = { new_products_revenue_share: parseNum(stats?.new_products_revenue_share), - new_products_revenue_share_percent: parseNum(stats?.new_products_revenue_share_percent), + new_products_revenue_share_percent: parseNum( + stats?.new_products_revenue_share_percent + ), import_impact: parseNum(stats?.import_impact), new_products_export: parseNum(stats?.new_products_export), all_funnel: parseNum(stats?.all_funnel), successful_sample_funnel: parseNum(stats?.successful_sample_funnel), successful_products_funnel: parseNum(stats?.successful_products_funnel), - successful_improvement_or_change_funnel: parseNum(stats?.successful_improvement_or_change_funnel), + successful_improvement_or_change_funnel: parseNum( + stats?.successful_improvement_or_change_funnel + ), new_product_funnel: parseNum(stats?.new_product_funnel), - count_innovation_construction_inside_projects: parseNum(stats?.count_innovation_construction_inside_projects), + count_innovation_construction_inside_projects: parseNum( + stats?.count_innovation_construction_inside_projects + ), average_project_score: parseNum(stats?.average_project_score), }; @@ -466,7 +474,7 @@ export function ProductInnovationPage() { ...prev, revenueNewProducts: { ...prev.revenueNewProducts, - value: normalized.new_products_revenue_share, + value: normalized.new_products_revenue_share, percent: normalized.new_products_revenue_share_percent, }, impactOnImports: { @@ -538,7 +546,6 @@ export function ProductInnovationPage() { setHasMore(true); }; - const formatCurrency = (amount: string | number) => { if (!amount) return "0 ریال"; const numericAmount = @@ -551,15 +558,19 @@ export function ProductInnovationPage() { // Transform data for line chart const transformDataForLineChart = (data: any[]) => { - const seasons = [...new Set(data.map(item => item.full_season))]; - const products = [...new Set(data.map(item => item.product_title))]; - return seasons.map(season => { + const seasons = [...new Set(data.map((item) => item.full_season))]; + const products = [...new Set(data.map((item) => item.product_title))]; + return seasons.map((season) => { const seasonData: any = { season }; - products.forEach(product => { - const productData = data.find(item => - item.product_title === product && item.full_season === season + products.forEach((product) => { + const productData = data.find( + (item) => + item.product_title === product && item.full_season === season ); - seasonData[product] = productData?.export_revenue_sum > 0 && productData ? Math.round(productData?.export_revenue_sum) : 0; + seasonData[product] = + productData?.export_revenue_sum > 0 && productData + ? Math.round(productData?.export_revenue_sum) + : 0; }); return seasonData; }); @@ -615,7 +626,8 @@ export function ProductInnovationPage() { variant="ghost" size="sm" onClick={() => { - handleProjectDetails(item)}} + handleProjectDetails(item); + }} className="text-emerald-400 underline underline-offset-4 font-ligth text-sm p-2 h-auto" > جزئیات بیشتر @@ -628,7 +640,9 @@ export function ProductInnovationPage() { ); case "title": - return {String(value)}; + return ( + {String(value)} + ); case "project_status": return (
@@ -652,23 +666,28 @@ export function ProductInnovationPage() { ); default: - return {String(value) || "-"}; + return ( + + {String(value) || "-"} + + ); } }; const seasonOrder = ["بهار", "تابستان", "پاییز", "زمستان"]; const sortedBarData = exportChartData - .sort((a, b) => { - const getSeasonIndex = (s: string) => { - const [seasonName, year] = s.split(" "); - return parseInt(year) * 4 + seasonOrder.indexOf(seasonName); - }; - return getSeasonIndex(a.full_season) - getSeasonIndex(b.full_season); - }) - .map((item) => ({ - label: item.full_season, - value: item.export_revenue_sum < 0 ? 0 : Math.round(item.export_revenue_sum) , - })); + .sort((a, b) => { + const getSeasonIndex = (s: string) => { + const [seasonName, year] = s.split(" "); + return parseInt(year) * 4 + seasonOrder.indexOf(seasonName); + }; + return getSeasonIndex(a.full_season) - getSeasonIndex(b.full_season); + }) + .map((item) => ({ + label: item.full_season, + value: + item.export_revenue_sum < 0 ? 0 : Math.round(item.export_revenue_sum), + })); return ( @@ -719,18 +738,27 @@ export function ProductInnovationPage() { value={stateCard.revenueNewProducts.value} percentValue={stateCard.revenueNewProducts.percent} valueLabel={stateCard.revenueNewProducts.description} - percentLabel={stateCard.revenueNewProducts.descriptionPercent} + percentLabel={ + stateCard.revenueNewProducts.descriptionPercent + } />
{/* Second card */}
- +
-

{stateCard.newProductExports.value}

-
{stateCard.newProductExports.description}
+

+ {stateCard.newProductExports.value} +

+
+ {stateCard.newProductExports.description} +
@@ -739,12 +767,19 @@ export function ProductInnovationPage() { {/* Third card - basic BaseCard */}
- +
-

{stateCard.impactOnImports.value}

-
{stateCard.impactOnImports.description}
+

+ {stateCard.impactOnImports.value} +

+
+ {stateCard.impactOnImports.description} +
@@ -753,9 +788,9 @@ export function ProductInnovationPage() { )}
-
+
- {/* Funnel Chart */} + {/* Funnel Chart */} - -
+ +
{/* Data Table */} @@ -807,7 +842,7 @@ export function ProductInnovationPage() { > {column.sortable ? (
- + ))} )) @@ -892,8 +927,8 @@ export function ProductInnovationPage() { - - )} + + )} @@ -902,7 +937,10 @@ export function ProductInnovationPage() {
- کل پروژه ها :{formatNumber(stats?.count_innovation_construction_inside_projects)} + کل پروژه ها : + {formatNumber( + stats?.count_innovation_construction_inside_projects + )}
@@ -917,7 +955,9 @@ export function ProductInnovationPage() {
میانگین :‌
{formatNumber( - ((stats.average_project_score ?? 0) as number).toFixed?.(1) ?? 0 + ((stats.average_project_score ?? 0) as number).toFixed?.( + 1 + ) ?? 0 )}
@@ -928,127 +968,158 @@ export function ProductInnovationPage() { {/* Project Details Dialog */} - + - + شرح پروژه - - + +
- {/* right Column - Stats Cards and Details */} -
+ {/* right Column - Stats Cards and Details */} +
{/* Stats Cards */}
-

{selectedProjectDetails?.title}

-

{selectedProjectDetails?.project_description}

+

+ {selectedProjectDetails?.title} +

+

+ {selectedProjectDetails?.project_description} +

- + {/* Technical Knowledge */}
-

دانش فنی محصول جدید

-
-
- توسعه درونزا +

+ دانش فنی محصول جدید +

+
+
+ + توسعه درونزا + - -
+ +
-
- همکاری فناورانه +
+ + همکاری فناورانه + - -
+ +
-
- سایر - -
-
+
+ سایر + +
+
{/* Standards */}
-

- استانداردهای ملی و بین‌المللی اخذ شده -

+

+ استانداردهای ملی و بین‌المللی اخذ شده +

- {selectedProjectDetails?.obtained_standard_title && selectedProjectDetails?.obtained_standard_title.length > 0 ? ( -
- {(Array.isArray(selectedProjectDetails?.obtained_standard_title) - ? selectedProjectDetails?.obtained_standard_title - : [selectedProjectDetails?.obtained_standard_title] - ).map((standard, index) => ( -
-
- {standard} -
- ))} -
- ) : ( -

- هیچ استانداردی ثبت نشده است. -

- )} -
+ {selectedProjectDetails?.obtained_standard_title && + selectedProjectDetails?.obtained_standard_title.length > 0 ? ( +
+ {(Array.isArray( + selectedProjectDetails?.obtained_standard_title + ) + ? selectedProjectDetails?.obtained_standard_title + : [selectedProjectDetails?.obtained_standard_title] + ).map((standard, index) => ( +
+
+ + {standard} + +
+ ))} +
+ ) : ( +

+ هیچ استانداردی ثبت نشده است. +

+ )} +
{/* Knowledge-based Certificate Button */} -
- {selectedProjectDetails?.knowledge_based_certificate_obtained === "خیر" ? ( -
- -
- ) : ( - - - - - - +
+ {selectedProjectDetails?.knowledge_based_certificate_obtained === + "خیر" ? ( +
+ +
+ ) : ( + + + + + + - -
-

- شماره گواهی: - {selectedProjectDetails?.knowledge_based_certificate_number || - "—"} -

-

- تاریخ اخذ: - {handleDataValue(selectedProjectDetails?.certificate_obtain_date) || "—"} -

-

- مرجع صادرکننده: - {selectedProjectDetails?.issuing_authority || "—"} -

-
-
-
-
-
- )} -
+ +
+

+ شماره گواهی: + {selectedProjectDetails?.knowledge_based_certificate_number || + "—"} +

+

+ تاریخ اخذ: + {handleDataValue( + selectedProjectDetails?.certificate_obtain_date + ) || "—"} +

+

+ + مرجع صادرکننده:{" "} + + {selectedProjectDetails?.issuing_authority || "—"} +

+
+
+
+
+
+ )} +
{/* Left Column - Project Description and Charts */} @@ -1081,16 +1152,32 @@ export function ProductInnovationPage() {
0 ? popupStats?.new_products_export : 0)} - percentValue={Math.round(popupStats?.new_products_export_percent > 0 ? popupStats?.new_products_export_percent : 0)} + value={Math.round( + popupStats?.new_products_export > 0 + ? popupStats?.new_products_export + : 0 + )} + percentValue={Math.round( + popupStats?.new_products_export_percent > 0 + ? popupStats?.new_products_export_percent + : 0 + )} valueLabel="میلیون ریال" percentLabel="درصد به کل صادرات" /> 0 ? popupStats?.import_impact : 0)} - percentValue={Math.round(popupStats?.import_impact_percent > 0 ? popupStats?.import_impact_percent : 0)} + value={Math.round( + popupStats?.import_impact > 0 + ? popupStats?.import_impact + : 0 + )} + percentValue={Math.round( + popupStats?.import_impact_percent > 0 + ? popupStats?.import_impact_percent + : 0 + )} valueLabel="میلیون ریال" percentLabel="درصد صرفه جویی" /> @@ -1098,7 +1185,9 @@ export function ProductInnovationPage() { {/* Export Revenue Bar Chart */}
-

ظرفیت صادر شده

+

+ ظرفیت صادر شده +

{exportChartData.length > 0 ? ( @@ -1116,55 +1205,152 @@ export function ProductInnovationPage() { axisLine={false} stroke="#C3C3C3" tickMargin={8} - tickFormatter={(value: string) => `${value.split(" ")[0]} ${formatNumber(value.split(" ")[1]).replaceAll('٬','')}`} + tickFormatter={(value: string) => + `${value.split(" ")[0]} ${formatNumber(value.split(" ")[1]).replaceAll("٬", "")}` + } fontSize={11} /> - `${formatNumber(value)} میلیون`} /> + + `${formatNumber(value)} میلیون` + } + /> - `${formatNumber(value)}`} position="top" offset={15} fill="F9FAFB" className="fill-foreground" fontSize={16} /> + + `${formatNumber(value)}` + } + position="top" + offset={15} + fill="F9FAFB" + className="fill-foreground" + fontSize={16} + /> ) : ( -
داده‌ای برای نمایش وجود ندارد
+
+ داده‌ای برای نمایش وجود ندارد +
)}
{/* Export Revenue Line Chart */}
-

ظرفیت صادر شده

+

+ ظرفیت صادر شده +

{allExportData.length > 0 ? ( - + - ( - - {(payload as any).value} - - )} /> - `${formatNumber(value)} میلیون`} /> - `${formatNumber(value)} میلیون`} contentStyle={{ backgroundColor: "#1F2937", border: "1px solid #374151", borderRadius: "6px", padding: "6px 10px", fontSize: "11px", color: "#F9FAFB" }} /> - - {[...new Set(allExportData.map((item) => item.product_title))].slice(0, 5).map((product, index) => { - const colors = ["#10B981", "#EF4444", "#3B82F6", "#F59E0B", "#8B5CF6"]; - return ; - })} + ( + + + {(payload as any).value} + + + )} + /> + + `${formatNumber(value)} میلیون` + } + /> + + `${formatNumber(value)} میلیون` + } + contentStyle={{ + backgroundColor: "#1F2937", + border: "1px solid #374151", + borderRadius: "6px", + padding: "6px 10px", + fontSize: "11px", + color: "#F9FAFB", + }} + /> + + {[ + ...new Set( + allExportData.map((item) => item.product_title) + ), + ] + .slice(0, 5) + .map((product, index) => { + const colors = [ + "#10B981", + "#EF4444", + "#3B82F6", + "#F59E0B", + "#8B5CF6", + ]; + return ( + + ); + })} ) : ( -
داده‌ای برای نمایش وجود ندارد
+
+ داده‌ای برای نمایش وجود ندارد +
)}
)} - -
-
-
+ +
); } diff --git a/app/components/ui/Calendar.tsx b/app/components/ui/Calendar.tsx new file mode 100644 index 0000000..f05648e --- /dev/null +++ b/app/components/ui/Calendar.tsx @@ -0,0 +1,67 @@ +import { ChevronLeft, ChevronRight } from "lucide-react"; +import React from "react"; + +interface MonthItem { + id: string; + label: string; + start: string; + end: string; +} + +// interface CurrentDay { +// start: string; +// end: string; +// month: string; +// } + +interface CalendarProps { + title: string; + nextYearHandler: () => void; + prevYearHandler: () => void; + currentYear?: number; + monthList: Array; + selectedDate?: string; + selectDateHandler: (item: MonthItem) => void; +} + +export const Calendar: React.FC = ({ + title, + nextYearHandler, + prevYearHandler, + currentYear, + monthList, + selectedDate, + selectDateHandler, +}) => { + return ( +
+
+ {title} +
+ + {currentYear} + +
+
+
+ {monthList.map((item, index) => ( + selectDateHandler(item)} + > + {item.label} + + ))} +
+
+ ); +}; diff --git a/app/lib/utils.ts b/app/lib/utils.ts index 8d032cc..74e5b88 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -1,6 +1,7 @@ import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; +import EventEmitter from "events"; import moment from "moment-jalaali"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -22,8 +23,6 @@ export const formatCurrency = (amount: string | number) => { return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال"; }; - - /** * محاسبه دامنه nice numbers برای محور Y نمودارها * @param values آرایه از مقادیر داده‌ها @@ -46,7 +45,7 @@ export function calculateNiceRange( // پیدا کردن حداکثر مقدار در داده‌ها const dataMax = Math.max(...values); - + // اگر همه مقادیر صفر یا منفی هستند if (dataMax <= 0) { return { niceMax: 100, tickInterval: 20, ticks: [0, 20, 40, 60, 80, 100] }; @@ -57,19 +56,19 @@ export function calculateNiceRange( // محاسبه nice upper limit const niceMax = calculateNiceNumber(maxWithMargin, true); - + // محاسبه فاصله مناسب tick ها بر اساس niceMax const range = niceMax - minValue; const targetTicks = 5; // هدف: 5 tick const roughTickInterval = range / (targetTicks - 1); const niceTickInterval = calculateNiceNumber(roughTickInterval, false); - + // ایجاد آرایه tick ها const ticks: number[] = []; for (let i = minValue; i <= niceMax; i += niceTickInterval) { ticks.push(Math.round(i)); } - + // اطمینان از اینکه niceMax در آرایه tick ها باشد if (ticks[ticks.length - 1] !== niceMax) { ticks.push(niceMax); @@ -90,13 +89,13 @@ export function calculateNiceRange( */ function calculateNiceNumber(value: number, round: boolean): number { if (value <= 0) return 0; - + // پیدا کردن قدرت 10 const exponent = Math.floor(Math.log10(value)); const fraction = value / Math.pow(10, exponent); - + let niceFraction: number; - + if (round) { // برای حداکثر: به سمت بالا گرد می‌کنیم با دقت بیشتر if (fraction <= 1.0) niceFraction = 1; @@ -112,12 +111,12 @@ function calculateNiceNumber(value: number, round: boolean): number { else if (fraction <= 5.0) niceFraction = 5; else niceFraction = 10; } - + return niceFraction * Math.pow(10, exponent); } -export const handleDataValue = (val: any): any => { -moment.loadPersian({ usePersianDigits: true }); +export const handleDataValue = (val: any): any => { + moment.loadPersian({ usePersianDigits: true }); if (val == null) return val; if ( typeof val === "string" && @@ -132,4 +131,6 @@ moment.loadPersian({ usePersianDigits: true }); return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]); } return val; -} +}; + +export const EventBus = new EventEmitter(); diff --git a/app/types/util.type.ts b/app/types/util.type.ts new file mode 100644 index 0000000..da91223 --- /dev/null +++ b/app/types/util.type.ts @@ -0,0 +1,6 @@ +export interface CalendarDate { + start: string; + end: string; + sinceMonth: string; + untilMonth: string; +}