Merge branch 'dashboard_charts'
This commit is contained in:
commit
41e2787601
|
|
@ -125,7 +125,6 @@ export default [
|
|||
index("routes/home.tsx"), // /
|
||||
route("login", "routes/login.tsx"), // /login
|
||||
route("dashboard", "routes/dashboard.tsx"), // /dashboard
|
||||
route("dashboard/projects", "routes/dashboard.projects.tsx"), // /dashboard/projects
|
||||
route("404", "routes/404.tsx"), // /404
|
||||
route("unauthorized", "routes/unauthorized.tsx"), // /unauthorized
|
||||
route("*", "routes/$.tsx"), // Catch-all for 404s
|
||||
|
|
@ -257,8 +256,6 @@ useEffect(() => {
|
|||
```tsx
|
||||
// app/components/dashboard/dashboard-layout.tsx
|
||||
<nav className="hidden md:flex items-center space-x-8 space-x-reverse">
|
||||
<NavigationLink to="/dashboard" label="داشبورد" />
|
||||
<NavigationLink to="/dashboard/projects" label="پروژهها" />
|
||||
</nav>
|
||||
```
|
||||
|
||||
|
|
@ -298,7 +295,6 @@ Both shadcn/ui components and React Router navigation are fully responsive:
|
|||
{/* Mobile-friendly navigation */}
|
||||
<nav className="hidden md:flex items-center space-x-8 space-x-reverse">
|
||||
<NavigationLink to="/dashboard" label="داشبورد" />
|
||||
<NavigationLink to="/dashboard/projects" label="پروژهها" />
|
||||
</nav>
|
||||
|
||||
{/* Mobile logo for small screens */}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,26 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import * as d3 from "d3";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
|
||||
export type D3ImageInfoProps = {
|
||||
imageUrl?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
width?: number; // fallback width if container size not measured yet
|
||||
height?: number; // fallback height
|
||||
export type CompanyInfo = {
|
||||
id: string;
|
||||
imageUrl: string;
|
||||
name: string;
|
||||
costReduction: number; // absolute value
|
||||
revenue?: number;
|
||||
capacity?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* D3ImageInfo
|
||||
* - Renders an image and an information box beside it using D3 within an SVG.
|
||||
* - Includes a clickable "show" chip that opens a popup dialog with more details.
|
||||
*/
|
||||
export function D3ImageInfo({
|
||||
imageUrl = "/placeholder.svg",
|
||||
title = "عنوان آیتم",
|
||||
description = "توضیحات تکمیلی در مورد این آیتم در این قسمت نمایش داده میشود.",
|
||||
width = 800,
|
||||
height = 360,
|
||||
}: D3ImageInfoProps) {
|
||||
export type D3ImageInfoProps = {
|
||||
companies: CompanyInfo[]; // exactly 6 items
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export function D3ImageInfo({ companies, width = 900, height = 400 }: D3ImageInfoProps) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Redraw helper
|
||||
const draw = () => {
|
||||
if (!containerRef.current || !svgRef.current) return;
|
||||
|
||||
|
|
@ -42,200 +28,130 @@ export function D3ImageInfo({
|
|||
const svg = d3.select(svgRef.current);
|
||||
|
||||
const W = Math.max(480, container.clientWidth || width);
|
||||
const H = Math.max(260, height);
|
||||
const H = Math.max(300, height);
|
||||
|
||||
svg.attr("width", W).attr("height", H);
|
||||
|
||||
// Clear previous content
|
||||
svg.selectAll("*").remove();
|
||||
|
||||
// Layout
|
||||
const padding = 16;
|
||||
const imageAreaWidth = Math.min(300, Math.max(220, W * 0.35));
|
||||
const infoAreaX = padding + imageAreaWidth + padding;
|
||||
const infoAreaWidth = W - infoAreaX - padding;
|
||||
const padding = 10;
|
||||
const cols = 3;
|
||||
const rows = 2;
|
||||
const boxWidth = (W - padding * (cols + 1)) / cols;
|
||||
const boxHeight = (H - padding * (rows + 1)) / rows;
|
||||
|
||||
// Image area (with rounded border)
|
||||
const imgGroup = svg
|
||||
.append("g")
|
||||
.attr("transform", `translate(${padding}, ${padding})`);
|
||||
const group = svg.append("g").attr("transform", `translate(${padding}, ${padding})`);
|
||||
|
||||
const imgW = imageAreaWidth;
|
||||
const imgH = H - 2 * padding;
|
||||
companies.forEach((company, i) => {
|
||||
const col = i % cols;
|
||||
const row = Math.floor(i / cols);
|
||||
const x = col * (boxWidth + padding);
|
||||
const y = row * (boxHeight + padding);
|
||||
|
||||
// Frame
|
||||
imgGroup
|
||||
.append("rect")
|
||||
.attr("width", imgW)
|
||||
.attr("height", imgH)
|
||||
.attr("rx", 10)
|
||||
.attr("ry", 10)
|
||||
.attr("fill", "#1F2937") // gray-800
|
||||
.attr("stroke", "#4B5563") // gray-600
|
||||
.attr("stroke-width", 1.5);
|
||||
const companyGroup = group.append("g").attr("transform", `translate(${x}, ${y})`);
|
||||
|
||||
// Image
|
||||
imgGroup
|
||||
.append("image")
|
||||
.attr("href", imageUrl)
|
||||
.attr("x", 4)
|
||||
.attr("y", 4)
|
||||
.attr("width", imgW - 8)
|
||||
.attr("height", imgH - 8)
|
||||
.attr("preserveAspectRatio", "xMidYMid slice")
|
||||
.attr("clip-path", null);
|
||||
// Draw background box
|
||||
companyGroup
|
||||
.append("rect")
|
||||
.attr("width", boxWidth)
|
||||
.attr("height", boxHeight)
|
||||
.attr("rx", 10)
|
||||
.attr("ry", 10)
|
||||
.attr("fill", "transparent")
|
||||
|
||||
// Info area
|
||||
const infoGroup = svg
|
||||
.append("g")
|
||||
.attr("transform", `translate(${infoAreaX}, ${padding})`);
|
||||
// Draw image
|
||||
const imgSize = Math.min(boxWidth, boxHeight) * 0.5;
|
||||
companyGroup
|
||||
.append("image")
|
||||
.attr("href", company.imageUrl)
|
||||
.attr("x", 10)
|
||||
.attr("y", 10)
|
||||
.attr("width", imgSize)
|
||||
.attr("height", imgSize)
|
||||
.attr("preserveAspectRatio", "xMidYMid slice")
|
||||
.style("background", "transparent");
|
||||
|
||||
// Info container
|
||||
infoGroup
|
||||
.append("rect")
|
||||
.attr("width", Math.max(220, infoAreaWidth))
|
||||
.attr("height", imgH)
|
||||
.attr("rx", 12)
|
||||
.attr("ry", 12)
|
||||
.attr("fill", "url(#infoGradient)")
|
||||
.attr("stroke", "#6B7280") // gray-500
|
||||
.attr("stroke-width", 1);
|
||||
// Adjust positions to match picture
|
||||
// Position image slightly left and info box to right with spacing
|
||||
const infoX = imgSize + 30;
|
||||
const infoY = 10;
|
||||
const infoWidth = 120;
|
||||
const infoHeight = imgSize;
|
||||
|
||||
// Background gradient
|
||||
const defs = svg.append("defs");
|
||||
const gradient = defs
|
||||
.append("linearGradient")
|
||||
.attr("id", "infoGradient")
|
||||
.attr("x1", "0%")
|
||||
.attr("y1", "0%")
|
||||
.attr("x2", "0%")
|
||||
.attr("y2", "100%");
|
||||
const infoGroup = companyGroup.append("g");
|
||||
|
||||
gradient.append("stop").attr("offset", "0%").attr("stop-color", "#111827"); // gray-900
|
||||
gradient
|
||||
.append("stop")
|
||||
.attr("offset", "100%")
|
||||
.attr("stop-color", "#374151"); // gray-700
|
||||
infoGroup
|
||||
.append("rect")
|
||||
.attr("x", infoX)
|
||||
.attr("y", infoY)
|
||||
.attr("width", infoWidth)
|
||||
.attr("height", infoHeight)
|
||||
.attr("rx", 10)
|
||||
.attr("ry", 10)
|
||||
.attr("fill", "transparent")
|
||||
.attr("stroke", "#3F415A")
|
||||
.attr("stroke-width", 1);
|
||||
|
||||
// Title
|
||||
infoGroup
|
||||
.append("text")
|
||||
.attr("x", 16)
|
||||
.attr("y", 36)
|
||||
.attr("fill", "#F9FAFB") // gray-50
|
||||
.attr("font-weight", 700)
|
||||
.attr("font-size", 18)
|
||||
.text(title);
|
||||
|
||||
// Description (wrapped)
|
||||
const wrapText = (text: string, maxWidth: number) => {
|
||||
const words = text.split(/\s+/).reverse();
|
||||
const lines: string[] = [];
|
||||
let line: string[] = [];
|
||||
let t = "";
|
||||
while (words.length) {
|
||||
const word = words.pop()!;
|
||||
const test = (t + " " + word).trim();
|
||||
// Approximate measure using character count
|
||||
const tooLong = test.length * 8 > maxWidth; // 8px avg char width
|
||||
if (tooLong && t.length) {
|
||||
lines.push(t);
|
||||
t = word;
|
||||
} else {
|
||||
t = test;
|
||||
}
|
||||
}
|
||||
if (t) lines.push(t);
|
||||
return lines;
|
||||
};
|
||||
|
||||
const descMaxWidth = Math.max(200, infoAreaWidth - 32);
|
||||
const descLines = wrapText(description, descMaxWidth);
|
||||
|
||||
descLines.forEach((line, i) => {
|
||||
// Add text inside info box
|
||||
const lineHeight = 20;
|
||||
infoGroup
|
||||
.append("text")
|
||||
.attr("x", 16)
|
||||
.attr("y", 70 + i * 22)
|
||||
.attr("fill", "#E5E7EB") // gray-200
|
||||
.attr("x", imgSize + 10 )
|
||||
.attr("y", infoY + imgSize + 10)
|
||||
.attr("fill", "#FFFFFF")
|
||||
.attr("font-weight", "700")
|
||||
.attr("font-size", 14)
|
||||
.text(line);
|
||||
.text(company.name);
|
||||
|
||||
infoGroup
|
||||
.append("text")
|
||||
.attr("x", infoX + imgSize)
|
||||
.attr("y", infoY + lineHeight )
|
||||
.attr("fill", "#FFFFFF")
|
||||
.attr("font-size", 12)
|
||||
.text(`درآمد: ${formatNumber(company?.revenue || "0")}`);
|
||||
infoGroup
|
||||
.append("text")
|
||||
.attr("x", infoX + imgSize -20 )
|
||||
.attr("y", infoY + lineHeight +5 )
|
||||
.attr("fill", "#ACACAC")
|
||||
.attr("font-size", 6)
|
||||
.text(`میلیون ریال`);
|
||||
|
||||
|
||||
infoGroup
|
||||
.append("text")
|
||||
.attr("x", infoX + imgSize)
|
||||
.attr("y", infoY + lineHeight *2 )
|
||||
.attr("fill", "#FFFFFF")
|
||||
.attr("font-size", 12)
|
||||
.text(`هزینه: ${formatNumber(company?.cost || "0")} میلیون ریال`);
|
||||
|
||||
infoGroup
|
||||
.append("text")
|
||||
.attr("x", infoX + imgSize)
|
||||
.attr("y", infoY + lineHeight * 3 )
|
||||
.attr("fill", "#FFFFFF")
|
||||
.attr("font-size", 12)
|
||||
.text(`ظرفیت: ${formatNumber(company?.capacity || "0")} تن در سال`);
|
||||
|
||||
// Remove click handlers and popup
|
||||
companyGroup.style("cursor", "default");
|
||||
});
|
||||
|
||||
// Show button-like chip
|
||||
const chipY = Math.min(imgH - 48, 70 + descLines.length * 22 + 16);
|
||||
const chip = infoGroup
|
||||
.append("g")
|
||||
.attr("class", "show-chip")
|
||||
.style("cursor", "pointer");
|
||||
|
||||
const chipW = 120;
|
||||
const chipH = 36;
|
||||
|
||||
chip
|
||||
.append("rect")
|
||||
.attr("x", 16)
|
||||
.attr("y", chipY)
|
||||
.attr("width", chipW)
|
||||
.attr("height", chipH)
|
||||
.attr("rx", 8)
|
||||
.attr("ry", 8)
|
||||
.attr("fill", "#3B82F6") // blue-500
|
||||
.attr("stroke", "#60A5FA")
|
||||
.attr("stroke-width", 1.5)
|
||||
.attr("opacity", 0.95);
|
||||
|
||||
chip
|
||||
.append("text")
|
||||
.attr("x", 16 + chipW / 2)
|
||||
.attr("y", chipY + chipH / 2 + 5)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("fill", "#FFFFFF")
|
||||
.attr("font-weight", 700)
|
||||
.text("نمایش");
|
||||
|
||||
// Hover & click
|
||||
chip
|
||||
.on("mouseenter", function () {
|
||||
d3.select(this).select("rect").attr("fill", "#2563EB"); // blue-600
|
||||
})
|
||||
.on("mouseleave", function () {
|
||||
d3.select(this).select("rect").attr("fill", "#3B82F6"); // blue-500
|
||||
})
|
||||
.on("click", () => setOpen(true));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
draw();
|
||||
const ro = new ResizeObserver(() => draw());
|
||||
if (containerRef.current) ro.observe(containerRef.current);
|
||||
draw();
|
||||
return () => ro.disconnect();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [imageUrl, title, description]);
|
||||
}, [companies, width, height]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<div ref={containerRef} className="w-full h-[380px]">
|
||||
<div ref={containerRef} className="w-full h-[400px]">
|
||||
<svg ref={svgRef} className="block w-full h-full"></svg>
|
||||
</div>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-[520px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-persian">{title}</DialogTitle>
|
||||
<DialogDescription className="font-persian">
|
||||
{description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="mt-4">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={title}
|
||||
className="w-full h-60 object-cover rounded-md border border-gray-700"
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { DashboardLayout } from "./layout";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Progress } from "~/components/ui/progress";
|
||||
|
|
@ -47,6 +47,11 @@ export function DashboardHome() {
|
|||
const [dashboardData, setDashboardData] = useState<any | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Chart and schematic data from select API
|
||||
const [companyChartData, setCompanyChartData] = useState<
|
||||
{ category: string; capacity: number; revenue: number; cost: number }[]
|
||||
>([]);
|
||||
const [totalIncreasedCapacity, setTotalIncreasedCapacity] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardData();
|
||||
|
|
@ -90,6 +95,63 @@ export function DashboardHome() {
|
|||
chartData: leftCardsResponseData?.chartData || [],
|
||||
};
|
||||
setDashboardData(realData);
|
||||
|
||||
// Fetch company aggregates for chart and schematic (select API)
|
||||
const selectPayload = {
|
||||
ProcessName: "project",
|
||||
OutputFields: [
|
||||
"related_company",
|
||||
"sum(pre_innovation_fee)",
|
||||
"sum(innovation_cost_reduction)",
|
||||
"sum(pre_project_production_capacity)",
|
||||
"sum(increased_capacity_after_innovation)",
|
||||
"sum(pre_project_income)",
|
||||
"sum(increased_income_after_innovation)",
|
||||
],
|
||||
GroupBy: ["related_company"],
|
||||
};
|
||||
|
||||
const selectResp = await apiService.select(selectPayload);
|
||||
const selectDataRaw = ((): any => {
|
||||
try {
|
||||
return typeof selectResp?.data === "string"
|
||||
? JSON.parse(selectResp.data)
|
||||
: selectResp?.data;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
|
||||
const rows: any[] = Array.isArray(selectDataRaw) ? selectDataRaw : [];
|
||||
let incCapacityTotal = 0;
|
||||
const chartRows = rows.map((r) => {
|
||||
const rel = r?.related_company ?? "-";
|
||||
const preFee = Number(r?.pre_innovation_fee_sum ?? 0);
|
||||
const costRed = Number(r?.innovation_cost_reduction_sum ?? 0);
|
||||
const preCap = Number(r?.pre_project_production_capacity_sum ?? 0);
|
||||
const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0);
|
||||
const preInc = Number(r?.pre_project_income_sum ?? 0);
|
||||
const incInc = Number(r?.increased_income_after_innovation_sum ?? 0);
|
||||
|
||||
incCapacityTotal += incCap;
|
||||
|
||||
const capacityPct = preCap > 0 ? (incCap / preCap) * 100 : 0;
|
||||
const revenuePct = preInc > 0 ? (incInc / preInc) * 100 : 0;
|
||||
const costPct = preFee > 0 ? (costRed / preFee) * 100 : 0;
|
||||
|
||||
return {
|
||||
category: rel,
|
||||
capacity: isFinite(capacityPct) ? capacityPct : 0,
|
||||
revenue: isFinite(revenuePct) ? revenuePct : 0,
|
||||
cost: isFinite(costPct) ? costPct : 0,
|
||||
costI : costRed,
|
||||
capacityI : incCap,
|
||||
revenueI : incInc
|
||||
};
|
||||
});
|
||||
|
||||
setCompanyChartData(chartRows);
|
||||
setTotalIncreasedCapacity(incCapacityTotal);
|
||||
} catch (error) {
|
||||
console.error("Error fetching dashboard data:", error);
|
||||
const errorMessage =
|
||||
|
|
@ -634,15 +696,33 @@ export function DashboardHome() {
|
|||
</div>
|
||||
|
||||
<TabsContent value="charts" className="w-ful h-full">
|
||||
<InteractiveBarChart />
|
||||
<InteractiveBarChart data={companyChartData} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="canvas" className="w-ful h-full">
|
||||
<div className="p-4">
|
||||
<D3ImageInfo
|
||||
imageUrl="/main-circle.png"
|
||||
title="نمای شماتیک"
|
||||
description=":"
|
||||
companies={
|
||||
companyChartData.map((item) => {
|
||||
const imageMap: Record<string, string> = {
|
||||
"بسپاران": "/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,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -1,70 +1,52 @@
|
|||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, LabelList } from "recharts";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
XAxis,
|
||||
YAxis,
|
||||
LabelList,
|
||||
} from "recharts";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "~/components/ui/chart";
|
||||
|
||||
export const description = "An interactive bar chart";
|
||||
|
||||
const chartData = [
|
||||
{ category: "کیمیا", ideas: 12, revenue: 850, cost: 320 },
|
||||
{ category: "ُفرآروزش", ideas: 19, revenue: 1200, cost: 450 },
|
||||
{ category: "خوارزمی", ideas: 15, revenue: 1400, cost: 520 },
|
||||
];
|
||||
export type CompanyChartDatum = {
|
||||
category: string; // related_company
|
||||
capacity: number; // percentage
|
||||
revenue: number; // percentage
|
||||
cost: number; // percentage
|
||||
};
|
||||
|
||||
const chartConfig = {
|
||||
ideas: {
|
||||
label: "ایدهها",
|
||||
capacity: {
|
||||
label: "افزایش ظرفیت",
|
||||
color: "#60A5FA", // Blue-400
|
||||
},
|
||||
revenue: {
|
||||
label: "درآمد (میلیون)",
|
||||
label: "افزایش درآمد",
|
||||
color: "#4ADE80", // Green-400
|
||||
},
|
||||
cost: {
|
||||
label: "کاهش هزینه (میلیون)",
|
||||
label: "کاهش هزینه",
|
||||
color: "#F87171", // Red-400
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export function InteractiveBarChart() {
|
||||
const [activeChart, setActiveChart] =
|
||||
React.useState<keyof typeof chartConfig>("ideas");
|
||||
|
||||
const total = React.useMemo(
|
||||
() => ({
|
||||
ideas: chartData.reduce((acc, curr) => acc + curr.ideas, 0),
|
||||
revenue: chartData.reduce((acc, curr) => acc + curr.revenue, 0),
|
||||
cost: chartData.reduce((acc, curr) => acc + curr.cost, 0),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
export function InteractiveBarChart({
|
||||
data,
|
||||
}: {
|
||||
data: CompanyChartDatum[];
|
||||
}) {
|
||||
return (
|
||||
<Card className="py-0 bg-transparent mt-20 border-none h-full">
|
||||
<CardContent className="px-2 sm:p-6 bg-transparent">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-auto h-96 w-full"
|
||||
>
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-96 w-full">
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
data={data}
|
||||
margin={{ left: 12, right: 12 }}
|
||||
barCategoryGap="42%"
|
||||
>
|
||||
<CartesianGrid vertical={false} stroke="#475569" />
|
||||
|
|
@ -84,41 +66,58 @@ export function InteractiveBarChart() {
|
|||
tick={{ fill: "#94a3b8", fontSize: 12 }}
|
||||
tickFormatter={(value) => `${value}%`}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="ideas"
|
||||
fill={chartConfig.ideas.color}
|
||||
radius={[8, 8, 0, 0]}
|
||||
>
|
||||
<Bar dataKey="capacity" fill={chartConfig.capacity.color} radius={[8, 8, 0, 0]}>
|
||||
<LabelList
|
||||
dataKey="ideas"
|
||||
dataKey="capacity"
|
||||
position="top"
|
||||
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
||||
formatter={(v: number) => `${Math.round(v)}%`}
|
||||
/>
|
||||
</Bar>
|
||||
<Bar
|
||||
dataKey="revenue"
|
||||
fill={chartConfig.revenue.color}
|
||||
radius={[8, 8, 0, 0]}
|
||||
>
|
||||
<Bar dataKey="revenue" fill={chartConfig.revenue.color} radius={[8, 8, 0, 0]}>
|
||||
<LabelList
|
||||
dataKey="revenue"
|
||||
position="top"
|
||||
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
||||
formatter={(v: number) => `${Math.round(v)}%`}
|
||||
/>
|
||||
</Bar>
|
||||
<Bar
|
||||
dataKey="cost"
|
||||
fill={chartConfig.cost.color}
|
||||
radius={[8, 8, 0, 0]}
|
||||
>
|
||||
<Bar dataKey="cost" fill={chartConfig.cost.color} radius={[8, 8, 0, 0]}>
|
||||
<LabelList
|
||||
dataKey="cost"
|
||||
position="top"
|
||||
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
||||
formatter={(v: number) => `${Math.round(v)}%`}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
|
||||
{/* Legend below chart */}
|
||||
<div className="flex justify-center gap-8 mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-6 h-2 rounded"
|
||||
style={{ backgroundColor: chartConfig.capacity.color }}
|
||||
></div>
|
||||
<span className="text-sm text-white">{chartConfig.capacity.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-6 h-2 rounded"
|
||||
style={{ backgroundColor: chartConfig.cost.color }}
|
||||
></div>
|
||||
<span className="text-sm text-white">{chartConfig.cost.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-6 h-2 rounded"
|
||||
style={{ backgroundColor: chartConfig.revenue.color }}
|
||||
></div>
|
||||
<span className="text-sm text-white">{chartConfig.revenue.label}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,354 +0,0 @@
|
|||
import React from "react";
|
||||
import { DashboardLayout } from "../layout";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import {
|
||||
ArrowRight,
|
||||
Calendar,
|
||||
User,
|
||||
Users,
|
||||
DollarSign,
|
||||
Clock,
|
||||
FileText,
|
||||
Edit,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
|
||||
interface ProjectDetailProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
// Mock project data
|
||||
const mockProject = {
|
||||
id: 1,
|
||||
name: "پروژه توسعه اپلیکیشن موبایل",
|
||||
manager: "علی احمدی",
|
||||
team: "تیم توسعه موبایل",
|
||||
status: "در حال انجام",
|
||||
priority: "بالا",
|
||||
startDate: "1403/01/15",
|
||||
endDate: "1403/06/30",
|
||||
budget: "500,000,000",
|
||||
progress: 65,
|
||||
description: "این پروژه شامل توسعه یک اپلیکیشن موبایل کراس پلتفرم برای مدیریت پروژهها و وظایف میباشد. اپلیکیشن باید قابلیتهای مختلفی از جمله مدیریت کاربران، گزارشگیری و نوتیفیکیشن را داشته باشد.",
|
||||
teamMembers: [
|
||||
{ id: 1, name: "علی احمدی", role: "مدیر پروژه", avatar: "AA" },
|
||||
{ id: 2, name: "سارا کریمی", role: "توسعهدهنده فرانتاند", avatar: "SK" },
|
||||
{ id: 3, name: "محمد رضایی", role: "توسعهدهنده بکاند", avatar: "MR" },
|
||||
{ id: 4, name: "فاطمه موسوی", role: "طراح UI/UX", avatar: "FM" },
|
||||
],
|
||||
milestones: [
|
||||
{ id: 1, title: "تحلیل نیازمندیها", status: "تکمیل شده", date: "1403/01/30" },
|
||||
{ id: 2, title: "طراحی رابط کاربری", status: "تکمیل شده", date: "1403/02/15" },
|
||||
{ id: 3, title: "توسعه بکاند", status: "در حال انجام", date: "1403/04/01" },
|
||||
{ id: 4, title: "توسعه فرانتاند", status: "در حال انجام", date: "1403/05/01" },
|
||||
{ id: 5, title: "تست و رفع باگ", status: "در انتظار", date: "1403/06/01" },
|
||||
{ id: 6, title: "انتشار نهایی", status: "در انتظار", date: "1403/06/30" },
|
||||
],
|
||||
tasks: [
|
||||
{ id: 1, title: "پیادهسازی سیستم احراز هویت", assignee: "محمد رضایی", status: "در حال انجام", priority: "بالا" },
|
||||
{ id: 2, title: "طراحی صفحه داشبورد", assignee: "سارا کریمی", status: "تکمیل شده", priority: "متوسط" },
|
||||
{ id: 3, title: "توسعه API گزارشگیری", assignee: "محمد رضایی", status: "در انتظار", priority: "بالا" },
|
||||
{ id: 4, title: "طراحی آیکونهای اپلیکیشن", assignee: "فاطمه موسوی", status: "در حال انجام", priority: "پایین" },
|
||||
],
|
||||
};
|
||||
|
||||
const statusColors = {
|
||||
"در حال انجام": "info",
|
||||
"تکمیل شده": "success",
|
||||
"در انتظار": "warning",
|
||||
"لغو شده": "destructive",
|
||||
} as const;
|
||||
|
||||
const priorityColors = {
|
||||
"بالا": "destructive",
|
||||
"متوسط": "warning",
|
||||
"پایین": "secondary",
|
||||
} as const;
|
||||
|
||||
export function ProjectDetail({ projectId }: ProjectDetailProps) {
|
||||
const project = mockProject; // In real app, fetch by projectId
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<Button variant="ghost" size="sm" className="font-persian p-0 h-auto">
|
||||
پروژهها
|
||||
</Button>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
<span className="font-persian text-gray-900 dark:text-white">
|
||||
جزئیات پروژه
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Project Header */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white font-persian">
|
||||
{project.name}
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 font-persian mt-2">
|
||||
{project.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="font-persian">
|
||||
<Edit className="w-4 h-4 ml-2" />
|
||||
ویرایش پروژه
|
||||
</Button>
|
||||
<Button variant="destructive" className="font-persian">
|
||||
<Trash2 className="w-4 h-4 ml-2" />
|
||||
حذف پروژه
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-2 bg-blue-100 rounded-lg dark:bg-blue-900">
|
||||
<Calendar className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">تاریخ شروع</p>
|
||||
<p className="text-lg font-bold text-gray-900 dark:text-white font-persian">
|
||||
{project.startDate}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-2 bg-green-100 rounded-lg dark:bg-green-900">
|
||||
<Clock className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">تاریخ پایان</p>
|
||||
<p className="text-lg font-bold text-gray-900 dark:text-white font-persian">
|
||||
{project.endDate}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-2 bg-yellow-100 rounded-lg dark:bg-yellow-900">
|
||||
<DollarSign className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">بودجه</p>
|
||||
<p className="text-lg font-bold text-gray-900 dark:text-white font-persian">
|
||||
{project.budget} ریال
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-2 bg-purple-100 rounded-lg dark:bg-purple-900">
|
||||
<FileText className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">پیشرفت</p>
|
||||
<p className="text-lg font-bold text-gray-900 dark:text-white font-persian">
|
||||
{project.progress}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Project Details */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Progress Bar */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="font-persian">پیشرفت پروژه</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-gray-500 dark:text-gray-400 font-persian">
|
||||
{project.progress}% تکمیل شده
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={statusColors[project.status as keyof typeof statusColors]} className="font-persian">
|
||||
{project.status}
|
||||
</Badge>
|
||||
<Badge variant={priorityColors[project.priority as keyof typeof priorityColors]} className="font-persian">
|
||||
اولویت {project.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3 dark:bg-gray-700">
|
||||
<div
|
||||
className="bg-gradient-to-r from-blue-500 to-blue-600 h-3 rounded-full transition-all duration-500"
|
||||
style={{ width: `${project.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Milestones */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="font-persian">مراحل پروژه</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{project.milestones.map((milestone) => (
|
||||
<div key={milestone.id} className="flex items-center justify-between p-3 border rounded-lg dark:border-gray-700">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ${
|
||||
milestone.status === "تکمیل شده"
|
||||
? "bg-green-500"
|
||||
: milestone.status === "در حال انجام"
|
||||
? "bg-blue-500"
|
||||
: "bg-gray-300"
|
||||
}`}
|
||||
></div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||
{milestone.title}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">
|
||||
{milestone.date}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
variant={statusColors[milestone.status as keyof typeof statusColors]}
|
||||
className="font-persian"
|
||||
>
|
||||
{milestone.status}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Tasks */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="font-persian">وظایف اخیر</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{project.tasks.map((task) => (
|
||||
<div key={task.id} className="flex items-center justify-between p-3 border rounded-lg dark:border-gray-700">
|
||||
<div className="text-right">
|
||||
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||
{task.title}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">
|
||||
مسئول: {task.assignee}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={statusColors[task.status as keyof typeof statusColors]} className="font-persian">
|
||||
{task.status}
|
||||
</Badge>
|
||||
<Badge variant={priorityColors[task.priority as keyof typeof priorityColors]} className="font-persian">
|
||||
{task.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Project Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="font-persian">اطلاعات پروژه</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<User className="w-4 h-4 text-gray-500" />
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">مدیر پروژه</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||
{project.manager}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Users className="w-4 h-4 text-gray-500" />
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">تیم</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||
{project.team}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Calendar className="w-4 h-4 text-gray-500" />
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">مدت زمان</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||
{project.startDate} تا {project.endDate}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Team Members */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="font-persian">اعضای تیم</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{project.teamMembers.map((member) => (
|
||||
<div key={member.id} className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full flex items-center justify-center text-white font-medium text-sm">
|
||||
{member.avatar}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium text-gray-900 dark:text-white font-persian">
|
||||
{member.name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 font-persian">
|
||||
{member.role}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectDetail;
|
||||
|
|
@ -1,663 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import { DashboardLayout } from "../layout";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/ui/select";
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Filter,
|
||||
MoreHorizontal,
|
||||
Edit,
|
||||
Trash2,
|
||||
Eye,
|
||||
Calendar,
|
||||
User,
|
||||
DollarSign,
|
||||
Clock,
|
||||
} from "lucide-react";
|
||||
|
||||
// Mock data for projects
|
||||
const mockProjects = [
|
||||
{
|
||||
id: 1,
|
||||
name: "پروژه توسعه اپلیکیشن موبایل",
|
||||
manager: "علی احمدی",
|
||||
team: "تیم توسعه موبایل",
|
||||
status: "در حال انجام",
|
||||
priority: "بالا",
|
||||
startDate: "1403/01/15",
|
||||
endDate: "1403/06/30",
|
||||
budget: "500,000,000",
|
||||
progress: 65,
|
||||
description: "توسعه اپلیکیشن موبایل برای مدیریت پروژهها",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "پیادهسازی سیستم مدیریت محتوا",
|
||||
manager: "فاطمه کریمی",
|
||||
team: "تیم بکاند",
|
||||
status: "تکمیل شده",
|
||||
priority: "متوسط",
|
||||
startDate: "1402/10/01",
|
||||
endDate: "1403/02/15",
|
||||
budget: "750,000,000",
|
||||
progress: 100,
|
||||
description: "توسعه سیستم مدیریت محتوای وب",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "بهینهسازی پایگاه داده",
|
||||
manager: "محمد رضایی",
|
||||
team: "تیم دیتابیس",
|
||||
status: "در انتظار",
|
||||
priority: "بالا",
|
||||
startDate: "1403/03/01",
|
||||
endDate: "1403/05/30",
|
||||
budget: "300,000,000",
|
||||
progress: 0,
|
||||
description: "بهینهسازی عملکرد پایگاه دادههای موجود",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "راهاندازی سیستم مانیتورینگ",
|
||||
manager: "سارا موسوی",
|
||||
team: "تیم DevOps",
|
||||
status: "در حال انجام",
|
||||
priority: "متوسط",
|
||||
startDate: "1403/02/01",
|
||||
endDate: "1403/04/15",
|
||||
budget: "400,000,000",
|
||||
progress: 30,
|
||||
description: "پیادهسازی سیستم نظارت و مانیتورینگ",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "توسعه پنل مدیریت",
|
||||
manager: "رضا نوری",
|
||||
team: "تیم فرانتاند",
|
||||
status: "لغو شده",
|
||||
priority: "پایین",
|
||||
startDate: "1402/12/01",
|
||||
endDate: "1403/03/01",
|
||||
budget: "200,000,000",
|
||||
progress: 25,
|
||||
description: "توسعه پنل مدیریت برای ادمینها",
|
||||
},
|
||||
];
|
||||
|
||||
const statusColors = {
|
||||
"در حال انجام": "info",
|
||||
"تکمیل شده": "success",
|
||||
"در انتظار": "warning",
|
||||
"لغو شده": "destructive",
|
||||
} as const;
|
||||
|
||||
const priorityColors = {
|
||||
بالا: "destructive",
|
||||
متوسط: "warning",
|
||||
پایین: "secondary",
|
||||
} as const;
|
||||
|
||||
export function ProjectsPage() {
|
||||
const [projects, setProjects] = useState(mockProjects);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filterStatus, setFilterStatus] = useState("همه");
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||
const [editingProject, setEditingProject] = useState<any>(null);
|
||||
const [newProject, setNewProject] = useState({
|
||||
name: "",
|
||||
manager: "",
|
||||
team: "",
|
||||
status: "در انتظار",
|
||||
priority: "متوسط",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
budget: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const filteredProjects = projects.filter((project) => {
|
||||
const matchesSearch =
|
||||
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
project.manager.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus =
|
||||
filterStatus === "همه" || project.status === filterStatus;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const handleAddProject = () => {
|
||||
const id = Math.max(...projects.map((p) => p.id)) + 1;
|
||||
setProjects([...projects, { ...newProject, id, progress: 0 }]);
|
||||
setNewProject({
|
||||
name: "",
|
||||
manager: "",
|
||||
team: "",
|
||||
status: "در انتظار",
|
||||
priority: "متوسط",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
budget: "",
|
||||
description: "",
|
||||
});
|
||||
setIsAddDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleEditProject = (project: any) => {
|
||||
setEditingProject(project);
|
||||
setNewProject(project);
|
||||
setIsAddDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleUpdateProject = () => {
|
||||
setProjects(
|
||||
projects.map((p) =>
|
||||
p.id === editingProject.id
|
||||
? {
|
||||
...newProject,
|
||||
id: editingProject.id,
|
||||
progress: editingProject.progress,
|
||||
}
|
||||
: p,
|
||||
),
|
||||
);
|
||||
setEditingProject(null);
|
||||
setNewProject({
|
||||
name: "",
|
||||
manager: "",
|
||||
team: "",
|
||||
status: "در انتظار",
|
||||
priority: "متوسط",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
budget: "",
|
||||
description: "",
|
||||
});
|
||||
setIsAddDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleDeleteProject = (id: number) => {
|
||||
setProjects(projects.filter((p) => p.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white font-persian">
|
||||
مدیریت پروژهها
|
||||
</h1>
|
||||
<p className="text-gray-300 font-persian mt-1">
|
||||
مدیریت و پیگیری پروژههای فناوری و نوآوری
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="font-persian">
|
||||
<Plus className="w-4 h-4 ml-2" />
|
||||
پروژه جدید
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-persian">
|
||||
{editingProject ? "ویرایش پروژه" : "پروژه جدید"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="font-persian">
|
||||
{editingProject
|
||||
? "اطلاعات پروژه را ویرایش کنید."
|
||||
: "اطلاعات پروژه جدید را وارد کنید."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name" className="font-persian">
|
||||
نام پروژه
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newProject.name}
|
||||
onChange={(e) =>
|
||||
setNewProject({ ...newProject, name: e.target.value })
|
||||
}
|
||||
className="font-persian"
|
||||
placeholder="نام پروژه را وارد کنید"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="manager" className="font-persian">
|
||||
مدیر پروژه
|
||||
</Label>
|
||||
<Input
|
||||
id="manager"
|
||||
value={newProject.manager}
|
||||
onChange={(e) =>
|
||||
setNewProject({ ...newProject, manager: e.target.value })
|
||||
}
|
||||
className="font-persian"
|
||||
placeholder="نام مدیر پروژه"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="team" className="font-persian">
|
||||
تیم
|
||||
</Label>
|
||||
<Input
|
||||
id="team"
|
||||
value={newProject.team}
|
||||
onChange={(e) =>
|
||||
setNewProject({ ...newProject, team: e.target.value })
|
||||
}
|
||||
className="font-persian"
|
||||
placeholder="نام تیم"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label className="font-persian">وضعیت</Label>
|
||||
<Select
|
||||
value={newProject.status}
|
||||
onValueChange={(value) =>
|
||||
setNewProject({ ...newProject, status: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="font-persian">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="در انتظار">در انتظار</SelectItem>
|
||||
<SelectItem value="در حال انجام">
|
||||
در حال انجام
|
||||
</SelectItem>
|
||||
<SelectItem value="تکمیل شده">تکمیل شده</SelectItem>
|
||||
<SelectItem value="لغو شده">لغو شده</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label className="font-persian">اولویت</Label>
|
||||
<Select
|
||||
value={newProject.priority}
|
||||
onValueChange={(value) =>
|
||||
setNewProject({ ...newProject, priority: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="font-persian">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="بالا">بالا</SelectItem>
|
||||
<SelectItem value="متوسط">متوسط</SelectItem>
|
||||
<SelectItem value="پایین">پایین</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="startDate" className="font-persian">
|
||||
تاریخ شروع
|
||||
</Label>
|
||||
<Input
|
||||
id="startDate"
|
||||
value={newProject.startDate}
|
||||
onChange={(e) =>
|
||||
setNewProject({
|
||||
...newProject,
|
||||
startDate: e.target.value,
|
||||
})
|
||||
}
|
||||
className="font-persian"
|
||||
placeholder="1403/01/01"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="endDate" className="font-persian">
|
||||
تاریخ پایان
|
||||
</Label>
|
||||
<Input
|
||||
id="endDate"
|
||||
value={newProject.endDate}
|
||||
onChange={(e) =>
|
||||
setNewProject({
|
||||
...newProject,
|
||||
endDate: e.target.value,
|
||||
})
|
||||
}
|
||||
className="font-persian"
|
||||
placeholder="1403/06/01"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="budget" className="font-persian">
|
||||
بودجه (ریال)
|
||||
</Label>
|
||||
<Input
|
||||
id="budget"
|
||||
value={newProject.budget}
|
||||
onChange={(e) =>
|
||||
setNewProject({ ...newProject, budget: e.target.value })
|
||||
}
|
||||
className="font-persian"
|
||||
placeholder="500,000,000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description" className="font-persian">
|
||||
توضیحات
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={newProject.description}
|
||||
onChange={(e) =>
|
||||
setNewProject({
|
||||
...newProject,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
className="font-persian"
|
||||
placeholder="توضیحات پروژه"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsAddDialogOpen(false)}
|
||||
className="font-persian"
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
onClick={
|
||||
editingProject ? handleUpdateProject : handleAddProject
|
||||
}
|
||||
className="font-persian"
|
||||
>
|
||||
{editingProject ? "ویرایش" : "ایجاد"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-2 bg-blue-100 rounded-lg dark:bg-blue-900">
|
||||
<Calendar className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">
|
||||
کل پروژهها
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{projects.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-2 bg-green-100 rounded-lg dark:bg-green-900">
|
||||
<Clock className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">
|
||||
در حال انجام
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{projects.filter((p) => p.status === "در حال انجام").length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-2 bg-teal-100 rounded-lg dark:bg-teal-900">
|
||||
<User className="w-5 h-5 text-teal-600 dark:text-teal-400" />
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">
|
||||
تکمیل شده
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{projects.filter((p) => p.status === "تکمیل شده").length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="p-2 bg-yellow-100 rounded-lg dark:bg-yellow-900">
|
||||
<DollarSign className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 font-persian">
|
||||
در انتظار
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{projects.filter((p) => p.status === "در انتظار").length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters and Search */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
||||
<CardTitle className="font-persian">لیست پروژهها</CardTitle>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||
<div className="relative">
|
||||
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
placeholder="جستجو در پروژهها..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pr-10 font-persian w-full sm:w-64"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select value={filterStatus} onValueChange={setFilterStatus}>
|
||||
<SelectTrigger className="w-full sm:w-40">
|
||||
<Filter className="w-4 h-4 ml-2" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="همه">همه وضعیتها</SelectItem>
|
||||
<SelectItem value="در حال انجام">در حال انجام</SelectItem>
|
||||
<SelectItem value="تکمیل شده">تکمیل شده</SelectItem>
|
||||
<SelectItem value="در انتظار">در انتظار</SelectItem>
|
||||
<SelectItem value="لغو شده">لغو شده</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="font-persian">نام پروژه</TableHead>
|
||||
<TableHead className="font-persian">مدیر پروژه</TableHead>
|
||||
<TableHead className="font-persian">تیم</TableHead>
|
||||
<TableHead className="font-persian">وضعیت</TableHead>
|
||||
<TableHead className="font-persian">اولویت</TableHead>
|
||||
<TableHead className="font-persian">تاریخ شروع</TableHead>
|
||||
<TableHead className="font-persian">پیشرفت</TableHead>
|
||||
<TableHead className="font-persian">عملیات</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredProjects.map((project) => (
|
||||
<TableRow key={project.id}>
|
||||
<TableCell className="font-medium font-persian">
|
||||
{project.name}
|
||||
</TableCell>
|
||||
<TableCell className="font-persian">
|
||||
{project.manager}
|
||||
</TableCell>
|
||||
<TableCell className="font-persian">
|
||||
{project.team}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
statusColors[
|
||||
project.status as keyof typeof statusColors
|
||||
]
|
||||
}
|
||||
className="font-persian"
|
||||
>
|
||||
{project.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
priorityColors[
|
||||
project.priority as keyof typeof priorityColors
|
||||
]
|
||||
}
|
||||
className="font-persian"
|
||||
>
|
||||
{project.priority}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-persian">
|
||||
{project.startDate}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all"
|
||||
style={{ width: `${project.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 min-w-[3rem]">
|
||||
{project.progress}%
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel className="font-persian">
|
||||
عملیات
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="font-persian">
|
||||
<Eye className="ml-2 h-4 w-4" />
|
||||
مشاهده جزئیات
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="font-persian"
|
||||
onClick={() => handleEditProject(project)}
|
||||
>
|
||||
<Edit className="ml-2 h-4 w-4" />
|
||||
ویرایش
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="font-persian text-red-600"
|
||||
onClick={() => handleDeleteProject(project.id)}
|
||||
>
|
||||
<Trash2 className="ml-2 h-4 w-4" />
|
||||
حذف
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{filteredProjects.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500 dark:text-gray-400 font-persian">
|
||||
هیچ پروژهای یافت نشد.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectsPage;
|
||||
|
|
@ -269,22 +269,6 @@ class ApiService {
|
|||
return this.get("/projects");
|
||||
}
|
||||
|
||||
public async getProject(id: number) {
|
||||
return this.get(`/projects/${id}`);
|
||||
}
|
||||
|
||||
public async createProject(data: any) {
|
||||
return this.post("/projects", data);
|
||||
}
|
||||
|
||||
public async updateProject(id: number, data: any) {
|
||||
return this.put(`/projects/${id}`, data);
|
||||
}
|
||||
|
||||
public async deleteProject(id: number) {
|
||||
return this.delete(`/projects/${id}`);
|
||||
}
|
||||
|
||||
// Dashboard methods
|
||||
public async getDashboardStats() {
|
||||
return this.get("/dashboard/stats");
|
||||
|
|
|
|||
|
|
@ -6,18 +6,16 @@ export default [
|
|||
route("dashboard/project-management", "routes/project-management.tsx"),
|
||||
route(
|
||||
"dashboard/innovation-basket/process-innovation",
|
||||
"routes/innovation-basket.process-innovation.tsx"
|
||||
"routes/innovation-basket.process-innovation.tsx",
|
||||
),
|
||||
route(
|
||||
"dashboard/innovation-basket/green-innovation",
|
||||
"routes/green-innovation.tsx"
|
||||
"routes/green-innovation.tsx",
|
||||
),
|
||||
route(
|
||||
"/dashboard/innovation-basket/digital-innovation",
|
||||
"routes/digital-innovation-page.tsx"
|
||||
"routes/digital-innovation-page.tsx",
|
||||
),
|
||||
|
||||
route("projects", "routes/projects.tsx"),
|
||||
route("dashboard/ecosystem", "routes/ecosystem.tsx"),
|
||||
route("404", "routes/404.tsx"),
|
||||
route("unauthorized", "routes/unauthorized.tsx"),
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import type { Route } from "./+types/projects";
|
||||
import { ProjectsPage } from "~/components/dashboard/projects/projects-page";
|
||||
import { ProtectedRoute } from "~/components/auth/protected-route";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "پروژهها - سیستم مدیریت فناوری و نوآوری" },
|
||||
{ name: "description", content: "مدیریت پروژههای فناوری و نوآوری" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Projects() {
|
||||
return (
|
||||
<ProtectedRoute requireAuth={true}>
|
||||
<ProjectsPage />
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
BIN
public/abniro.png
Normal file
BIN
public/abniro.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
BIN
public/besparan.png
Normal file
BIN
public/besparan.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
BIN
public/faravash1.png
Normal file
BIN
public/faravash1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
BIN
public/faravash2.png
Normal file
BIN
public/faravash2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
BIN
public/khwarazmi.png
Normal file
BIN
public/khwarazmi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
BIN
public/kimia.png
Normal file
BIN
public/kimia.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
Loading…
Reference in New Issue
Block a user