dashboard_popup (#8)

Add new pop up in dasbhoard

Reviewed-on: https://git.pelekan.org/Saeed0920/inogen/pulls/8
Co-authored-by: saeed0920 <sd.eed1381@gmail.com>
Co-committed-by: saeed0920 <sd.eed1381@gmail.com>
This commit is contained in:
Saeed AB 2025-09-09 02:23:49 +03:30 committed by Saeed AB
parent cc163a19f0
commit a51d904b13
9 changed files with 367 additions and 9 deletions

View File

@ -5,13 +5,17 @@ export type CompanyInfo = {
id: string; id: string;
imageUrl: string; imageUrl: string;
name: string; name: string;
costReduction: number; // absolute value costReduction: number;
revenue?: number; revenue?: number;
capacity?: number; capacity?: number;
costI : number,
capacityI : number,
revenueI : number,
cost : number | string,
}; };
export type D3ImageInfoProps = { export type D3ImageInfoProps = {
companies: CompanyInfo[]; // exactly 6 items companies: CompanyInfo[];
width?: number; width?: number;
height?: number; height?: number;
}; };
@ -59,7 +63,7 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
<div className="w-full h-[500px] rounded-xl p-4"> <div className="w-full h-[500px] rounded-xl p-4">
<div dir="ltr" className="company-grid-container"> <div dir="ltr" className="company-grid-container">
{displayCompanies.map((company, index) => { {displayCompanies.map((company, index) => {
const gp = gridPositions.find(v => v.name === company.name) || { col: (index % 5) + 1, row: Math.floor(index / 5) + 1 }; const gp = gridPositions.find(v => v.name === company.name) ;
return ( return (
<> <>
<div <div
@ -77,7 +81,7 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
{company.name} {company.name}
</div> </div>
<InfoBox company={company} key={index +10} style={{ gridColumn: gp.colI , gridRow: gp.rowI }} /> <InfoBox company={company} key={index +10} style={{ gridColumn: gp?.colI , gridRow: gp?.rowI }} />
</>); </>);
})} })}
</div> </div>

View File

@ -49,7 +49,9 @@ export function DashboardHome() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Chart and schematic data from select API // Chart and schematic data from select API
const [companyChartData, setCompanyChartData] = useState< const [companyChartData, setCompanyChartData] = useState<
{ category: string; capacity: number; revenue: number; cost: number }[] { category: string; capacity: number; revenue: number; cost: number , costI : number,
capacityI : number,
revenueI : number }[]
>([]); >([]);
const [totalIncreasedCapacity, setTotalIncreasedCapacity] = useState<number>(0); const [totalIncreasedCapacity, setTotalIncreasedCapacity] = useState<number>(0);
@ -138,7 +140,6 @@ export function DashboardHome() {
const capacityPct = preCap > 0 ? (incCap / preCap) * 100 : 0; const capacityPct = preCap > 0 ? (incCap / preCap) * 100 : 0;
const revenuePct = preInc > 0 ? (incInc / preInc) * 100 : 0; const revenuePct = preInc > 0 ? (incInc / preInc) * 100 : 0;
const costPct = preFee > 0 ? (costRed / preFee) * 100 : 0; const costPct = preFee > 0 ? (costRed / preFee) * 100 : 0;
console.log(costRed)
return { return {
category: rel, category: rel,
capacity: isFinite(capacityPct) ? capacityPct : 0, capacity: isFinite(capacityPct) ? capacityPct : 0,

View File

@ -2,6 +2,7 @@ import { useState } from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { Sidebar } from "./sidebar"; import { Sidebar } from "./sidebar";
import { Header } from "./header"; import { Header } from "./header";
import { StrategicAlignmentPopup } from "./strategic-alignment-popup";
interface DashboardLayoutProps { interface DashboardLayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -16,6 +17,7 @@ export function DashboardLayout({
}: DashboardLayoutProps) { }: DashboardLayoutProps) {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] = useState(false);
const toggleSidebarCollapse = () => { const toggleSidebarCollapse = () => {
setIsSidebarCollapsed(!isSidebarCollapsed); setIsSidebarCollapsed(!isSidebarCollapsed);
@ -55,6 +57,7 @@ export function DashboardLayout({
isCollapsed={isSidebarCollapsed} isCollapsed={isSidebarCollapsed}
onToggleCollapse={toggleSidebarCollapse} onToggleCollapse={toggleSidebarCollapse}
className="h-full flex-shrink-0 relative z-10" className="h-full flex-shrink-0 relative z-10"
onStrategicAlignmentClick={() => setIsStrategicAlignmentPopupOpen(true)}
/> />
</div> </div>
@ -79,6 +82,7 @@ export function DashboardLayout({
</div> </div>
</main> </main>
</div> </div>
<StrategicAlignmentPopup open={isStrategicAlignmentPopupOpen} onOpenChange={setIsStrategicAlignmentPopupOpen} />
</div> </div>
); );
} }

View File

@ -25,18 +25,20 @@ interface SidebarProps {
isCollapsed?: boolean; isCollapsed?: boolean;
onToggleCollapse?: () => void; onToggleCollapse?: () => void;
className?: string; className?: string;
onStrategicAlignmentClick?: () => void;
} }
interface MenuItem { interface MenuItem {
id: string; id: string;
label: string; label: string;
icon: React.ComponentType<{ className?: string }>; icon: React.ComponentType<{ className?: string }> | null;
href?: string; href?: string;
children?: MenuItem[]; children?: MenuItem[];
badge?: string | number; badge?: string | number;
} }
const menuItems: MenuItem[] = [ const menuItems: MenuItem[] = [
{ {
id: "dashboard", id: "dashboard",
label: "صفحه اصلی", label: "صفحه اصلی",
@ -104,6 +106,12 @@ const menuItems: MenuItem[] = [
icon: Star, icon: Star,
href: "/dashboard/top-innovations", href: "/dashboard/top-innovations",
}, },
{
id: "strategic-alignment",
label: "میزان انطباق راهبردی",
icon: null,
href: "#", // This is not a route, it opens a popup
},
]; ];
const bottomMenuItems: MenuItem[] = [ const bottomMenuItems: MenuItem[] = [
@ -125,6 +133,7 @@ export function Sidebar({
isCollapsed = false, isCollapsed = false,
onToggleCollapse, onToggleCollapse,
className, className,
onStrategicAlignmentClick,
}: SidebarProps) { }: SidebarProps) {
const location = useLocation(); const location = useLocation();
const [expandedItems, setExpandedItems] = useState<string[]>([]); const [expandedItems, setExpandedItems] = useState<string[]>([]);
@ -196,16 +205,42 @@ export function Sidebar({
const ItemIcon = item.icon; const ItemIcon = item.icon;
const handleClick = () => { const handleClick = () => {
if (item.id === "logout") { if (item.id === "strategic-alignment") {
onStrategicAlignmentClick?.();
} else if (item.id === "logout") {
logout(); logout();
} else if (hasChildren) { } else if (hasChildren) {
toggleExpanded(item.id); toggleExpanded(item.id);
} }
}; };
if (item.id === "strategic-alignment") {
return (
<button
className={cn(
"w-full text-right",
)}
onClick={handleClick}
>
<div
className={cn(
"flex items-center justify-center w-full px-2 rounded-lg mt-4 transition-all duration-200 group",
)}
>
<div className="flex justify-center rounded-xl border-gray-500/20 border-2 cursor-pointer transition-all hover:bg-[#3F415A]/50 bg-[#3F415A] py-2 text-center items-center gap-3 min-w-0 flex-1">
<span className="font-persian text-sm font-medium truncate">
{item.label}
</span>
</div>
</div>
</button>
)
}
return ( return (
<div key={item.id} className="relative"> <div key={item.id} className="relative">
{item.href && item.id !== "logout" ? ( {item.href && item.href !== "#" ? (
<Link to={item.href} className="block"> <Link to={item.href} className="block">
<div <div
className={cn( className={cn(

View File

@ -0,0 +1,203 @@
import React, { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LabelList,
Cell,
} from "recharts";
import apiService from "~/lib/api";
import { Skeleton } from "~/components/ui/skeleton";
import { formatNumber } from "~/lib/utils";
import { ChartContainer } from "../ui/chart";
interface StrategicAlignmentData {
strategic_theme: string;
operational_fee_sum: number;
percentage?: number;
}
interface StrategicAlignmentPopupProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
// ✅ Chart config for shadcn/ui
const chartConfig = {
percentage: {
label: "",
color: "#3AEA83",
},
};
const maxHeight = 150;
const barHeights = () => Math.floor(Math.random() * maxHeight);
const ChartSkeleton = () => (
<div className="flex justify-center h-96 w-full p-4">
{/* Chart bars */}
<div className=" w-full flex items-end gap-10">
{[...Array(9)].map((_, i) => (
<div key={i} className="flex flex-col items-center gap-1">
<Skeleton
className="w-10 bg-gray-700 rounded-md"
style={{ height: `${barHeights()}px` }}
/>
</div>
))}
</div>
{/* Left space for Y-axis label */}
<div className="flex flex-col justify-between mr-2">
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
</div>
</div>
);
export function StrategicAlignmentPopup({
open,
onOpenChange,
}: StrategicAlignmentPopupProps) {
const [data, setData] = useState<StrategicAlignmentData[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (open) {
fetchData();
}
}, [open]);
const fetchData = async () => {
setLoading(true);
try {
const response = await apiService.select({
ProcessName: "project",
OutputFields: [
"strategic_theme",
"sum(operational_fee) as operational_fee_sum",
],
GroupBy: ["strategic_theme"],
});
const responseData =
typeof response.data === "string"
? JSON.parse(response.data)
: response.data;
const processedData = responseData
.map((item: any) => ({
strategic_theme: item.strategic_theme || "N/A",
operational_fee_sum: Math.max(0, Number(item.operational_fee_sum)),
}))
.filter((item: StrategicAlignmentData) => item.strategic_theme !== "");
const total = processedData.reduce(
(acc: number, item: StrategicAlignmentData) =>
acc + item.operational_fee_sum,
0
);
const dataWithPercentage = processedData.map(
(item: StrategicAlignmentData) => ({
...item,
percentage:
total > 0
? Math.round((item.operational_fee_sum / total) * 100)
: 0,
})
);
setData(dataWithPercentage || []);
} catch (error) {
console.error("Error fetching strategic alignment data:", error);
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full max-w-4xl bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] text-white border-none">
<DialogHeader className="border-b-3 mb-10 py-2 w-full pb-4 border-b-2 border-gray-500/20">
<DialogTitle className="ml-auto ">میزان انطباق راهبردی</DialogTitle>
</DialogHeader>
{loading ? (
<ChartSkeleton />
) : (
<>
<ResponsiveContainer width="100%" height={400}>
<ChartContainer config={chartConfig} className="aspect-auto h-96 w-full">
<BarChart
data={data}
margin={{ left: 12, right: 12 }}
barGap={15}
barSize={30}
accessibilityLayer
>
<CartesianGrid vertical={false} stroke="#475569" />
<XAxis
dataKey="strategic_theme"
tickLine={false}
axisLine={false}
tickMargin={10}
tick={{ fill: "#94a3b8", fontSize: 12 }}
/>
<YAxis
domain={[0, 100]}
tickLine={false}
axisLine={false}
tickMargin={8}
tick={{ fill: "#94a3b8", fontSize: 12 }}
tickFormatter={(value) =>
`${formatNumber(Math.round(value))}%`
}
label={{
value: "تعداد برنامه ها" ,
angle: -90,
position: "insideLeft",
fill: "#94a3b8",
fontSize: 14,
offset: 0,
dy: 0,
style: { textAnchor: "middle" },
}}
/>
<Bar dataKey="percentage" radius={[8, 8, 0, 0]}>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={chartConfig.percentage.color} />
))}
<LabelList
dataKey="percentage"
position="top"
style={{
fill: "#ffffff",
fontSize: "12px",
fontWeight: "bold",
}}
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
/>
</Bar>
</BarChart>
</ChartContainer>
</ResponsiveContainer>
</>
)}
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,13 @@
import { cn } from "~/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "~/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -17,6 +17,7 @@
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-router/node": "^7.7.0", "@react-router/node": "^7.7.0",
"@react-router/serve": "^7.7.1", "@react-router/serve": "^7.7.1",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",

View File

@ -32,6 +32,9 @@ importers:
'@radix-ui/react-tabs': '@radix-ui/react-tabs':
specifier: ^1.1.13 specifier: ^1.1.13
version: 1.1.13(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) version: 1.1.13(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-tooltip':
specifier: ^1.2.8
version: 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@react-router/node': '@react-router/node':
specifier: ^7.7.0 specifier: ^7.7.0
version: 7.8.2(react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.9.2) version: 7.8.2(react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.9.2)
@ -755,6 +758,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-tooltip@1.2.8':
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.1': '@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies: peerDependencies:
@ -3119,6 +3135,26 @@ snapshots:
'@types/react': 19.1.12 '@types/react': 19.1.12
'@types/react-dom': 19.1.9(@types/react@19.1.12) '@types/react-dom': 19.1.9(@types/react@19.1.12)
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1)
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
optionalDependencies:
'@types/react': 19.1.12
'@types/react-dom': 19.1.9(@types/react@19.1.12)
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.12)(react@19.1.1)': '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.12)(react@19.1.1)':
dependencies: dependencies:
react: 19.1.1 react: 19.1.1