Compare commits

...

2 Commits

Author SHA1 Message Date
mahmoodsht
41e2787601 Merge branch 'dashboard_charts' 2025-08-31 10:08:07 +03:30
86f5622bdd delete the useless files ,also update the chart and canvas 2025-08-31 04:12:05 +03:30
15 changed files with 257 additions and 1319 deletions

View File

@ -125,7 +125,6 @@ export default [
index("routes/home.tsx"), // / index("routes/home.tsx"), // /
route("login", "routes/login.tsx"), // /login route("login", "routes/login.tsx"), // /login
route("dashboard", "routes/dashboard.tsx"), // /dashboard route("dashboard", "routes/dashboard.tsx"), // /dashboard
route("dashboard/projects", "routes/dashboard.projects.tsx"), // /dashboard/projects
route("404", "routes/404.tsx"), // /404 route("404", "routes/404.tsx"), // /404
route("unauthorized", "routes/unauthorized.tsx"), // /unauthorized route("unauthorized", "routes/unauthorized.tsx"), // /unauthorized
route("*", "routes/$.tsx"), // Catch-all for 404s route("*", "routes/$.tsx"), // Catch-all for 404s
@ -257,8 +256,6 @@ useEffect(() => {
```tsx ```tsx
// app/components/dashboard/dashboard-layout.tsx // app/components/dashboard/dashboard-layout.tsx
<nav className="hidden md:flex items-center space-x-8 space-x-reverse"> <nav className="hidden md:flex items-center space-x-8 space-x-reverse">
<NavigationLink to="/dashboard" label="داشبورد" />
<NavigationLink to="/dashboard/projects" label="پروژه‌ها" />
</nav> </nav>
``` ```
@ -298,7 +295,6 @@ Both shadcn/ui components and React Router navigation are fully responsive:
{/* Mobile-friendly navigation */} {/* Mobile-friendly navigation */}
<nav className="hidden md:flex items-center space-x-8 space-x-reverse"> <nav className="hidden md:flex items-center space-x-8 space-x-reverse">
<NavigationLink to="/dashboard" label="داشبورد" /> <NavigationLink to="/dashboard" label="داشبورد" />
<NavigationLink to="/dashboard/projects" label="پروژه‌ها" />
</nav> </nav>
{/* Mobile logo for small screens */} {/* Mobile logo for small screens */}

View File

@ -1,40 +1,26 @@
"use client";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3"; import * as d3 from "d3";
import { import { formatNumber } from "~/lib/utils";
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
export type D3ImageInfoProps = { export type CompanyInfo = {
imageUrl?: string; id: string;
title?: string; imageUrl: string;
description?: string; name: string;
width?: number; // fallback width if container size not measured yet costReduction: number; // absolute value
height?: number; // fallback height revenue?: number;
capacity?: number;
}; };
/** export type D3ImageInfoProps = {
* D3ImageInfo companies: CompanyInfo[]; // exactly 6 items
* - Renders an image and an information box beside it using D3 within an SVG. width?: number;
* - Includes a clickable "show" chip that opens a popup dialog with more details. height?: number;
*/ };
export function D3ImageInfo({
imageUrl = "/placeholder.svg", export function D3ImageInfo({ companies, width = 900, height = 400 }: D3ImageInfoProps) {
title = "عنوان آیتم",
description = "توضیحات تکمیلی در مورد این آیتم در این قسمت نمایش داده می‌شود.",
width = 800,
height = 360,
}: D3ImageInfoProps) {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const svgRef = useRef<SVGSVGElement | null>(null); const svgRef = useRef<SVGSVGElement | null>(null);
const [open, setOpen] = useState(false);
// Redraw helper
const draw = () => { const draw = () => {
if (!containerRef.current || !svgRef.current) return; if (!containerRef.current || !svgRef.current) return;
@ -42,200 +28,130 @@ export function D3ImageInfo({
const svg = d3.select(svgRef.current); const svg = d3.select(svgRef.current);
const W = Math.max(480, container.clientWidth || width); 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); svg.attr("width", W).attr("height", H);
// Clear previous content
svg.selectAll("*").remove(); svg.selectAll("*").remove();
// Layout const padding = 10;
const padding = 16; const cols = 3;
const imageAreaWidth = Math.min(300, Math.max(220, W * 0.35)); const rows = 2;
const infoAreaX = padding + imageAreaWidth + padding; const boxWidth = (W - padding * (cols + 1)) / cols;
const infoAreaWidth = W - infoAreaX - padding; const boxHeight = (H - padding * (rows + 1)) / rows;
// Image area (with rounded border) const group = svg.append("g").attr("transform", `translate(${padding}, ${padding})`);
const imgGroup = svg
.append("g")
.attr("transform", `translate(${padding}, ${padding})`);
const imgW = imageAreaWidth; companies.forEach((company, i) => {
const imgH = H - 2 * padding; const col = i % cols;
const row = Math.floor(i / cols);
const x = col * (boxWidth + padding);
const y = row * (boxHeight + padding);
// Frame const companyGroup = group.append("g").attr("transform", `translate(${x}, ${y})`);
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);
// Image // Draw background box
imgGroup companyGroup
.append("image") .append("rect")
.attr("href", imageUrl) .attr("width", boxWidth)
.attr("x", 4) .attr("height", boxHeight)
.attr("y", 4) .attr("rx", 10)
.attr("width", imgW - 8) .attr("ry", 10)
.attr("height", imgH - 8) .attr("fill", "transparent")
.attr("preserveAspectRatio", "xMidYMid slice")
.attr("clip-path", null);
// Info area // Draw image
const infoGroup = svg const imgSize = Math.min(boxWidth, boxHeight) * 0.5;
.append("g") companyGroup
.attr("transform", `translate(${infoAreaX}, ${padding})`); .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 // Adjust positions to match picture
infoGroup // Position image slightly left and info box to right with spacing
.append("rect") const infoX = imgSize + 30;
.attr("width", Math.max(220, infoAreaWidth)) const infoY = 10;
.attr("height", imgH) const infoWidth = 120;
.attr("rx", 12) const infoHeight = imgSize;
.attr("ry", 12)
.attr("fill", "url(#infoGradient)")
.attr("stroke", "#6B7280") // gray-500
.attr("stroke-width", 1);
// Background gradient const infoGroup = companyGroup.append("g");
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%");
gradient.append("stop").attr("offset", "0%").attr("stop-color", "#111827"); // gray-900 infoGroup
gradient .append("rect")
.append("stop") .attr("x", infoX)
.attr("offset", "100%") .attr("y", infoY)
.attr("stop-color", "#374151"); // gray-700 .attr("width", infoWidth)
.attr("height", infoHeight)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill", "transparent")
.attr("stroke", "#3F415A")
.attr("stroke-width", 1);
// Title // Add text inside info box
infoGroup const lineHeight = 20;
.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) => {
infoGroup infoGroup
.append("text") .append("text")
.attr("x", 16) .attr("x", imgSize + 10 )
.attr("y", 70 + i * 22) .attr("y", infoY + imgSize + 10)
.attr("fill", "#E5E7EB") // gray-200 .attr("fill", "#FFFFFF")
.attr("font-weight", "700")
.attr("font-size", 14) .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(() => { useEffect(() => {
draw();
const ro = new ResizeObserver(() => draw()); const ro = new ResizeObserver(() => draw());
if (containerRef.current) ro.observe(containerRef.current); if (containerRef.current) ro.observe(containerRef.current);
draw();
return () => ro.disconnect(); return () => ro.disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps }, [companies, width, height]);
}, [imageUrl, title, description]);
return ( return (
<div className="w-full h-full"> <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> <svg ref={svgRef} className="block w-full h-full"></svg>
</div> </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> </div>
); );
} }

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { DashboardLayout } from "./layout"; import { DashboardLayout } from "./layout";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Progress } from "~/components/ui/progress"; import { Progress } from "~/components/ui/progress";
@ -47,6 +47,11 @@ export function DashboardHome() {
const [dashboardData, setDashboardData] = useState<any | null>(null); const [dashboardData, setDashboardData] = useState<any | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); 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(() => { useEffect(() => {
fetchDashboardData(); fetchDashboardData();
@ -90,6 +95,63 @@ export function DashboardHome() {
chartData: leftCardsResponseData?.chartData || [], chartData: leftCardsResponseData?.chartData || [],
}; };
setDashboardData(realData); 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) { } catch (error) {
console.error("Error fetching dashboard data:", error); console.error("Error fetching dashboard data:", error);
const errorMessage = const errorMessage =
@ -634,15 +696,33 @@ export function DashboardHome() {
</div> </div>
<TabsContent value="charts" className="w-ful h-full"> <TabsContent value="charts" className="w-ful h-full">
<InteractiveBarChart /> <InteractiveBarChart data={companyChartData} />
</TabsContent> </TabsContent>
<TabsContent value="canvas" className="w-ful h-full"> <TabsContent value="canvas" className="w-ful h-full">
<div className="p-4"> <div className="p-4">
<D3ImageInfo <D3ImageInfo
imageUrl="/main-circle.png" companies={
title="نمای شماتیک" companyChartData.map((item) => {
description=":" 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> </div>
</TabsContent> </TabsContent>

View File

@ -1,70 +1,52 @@
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, LabelList } from "recharts";
import React, { useState, useEffect } from "react";
import { import {
Card, Bar,
CardContent, BarChart,
CardDescription, CartesianGrid,
CardHeader, XAxis,
CardTitle, YAxis,
} from "~/components/ui/card"; LabelList,
} from "recharts";
import { Card, CardContent } from "~/components/ui/card";
import { import {
type ChartConfig, type ChartConfig,
ChartContainer, ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "~/components/ui/chart"; } from "~/components/ui/chart";
export const description = "An interactive bar chart"; export type CompanyChartDatum = {
category: string; // related_company
const chartData = [ capacity: number; // percentage
{ category: "کیمیا", ideas: 12, revenue: 850, cost: 320 }, revenue: number; // percentage
{ category: "ُفرآروزش", ideas: 19, revenue: 1200, cost: 450 }, cost: number; // percentage
{ category: "خوارزمی", ideas: 15, revenue: 1400, cost: 520 }, };
];
const chartConfig = { const chartConfig = {
ideas: { capacity: {
label: "ایده‌ها", label: "افزایش ظرفیت",
color: "#60A5FA", // Blue-400 color: "#60A5FA", // Blue-400
}, },
revenue: { revenue: {
label: "درآمد (میلیون)", label: "افزایش درآمد",
color: "#4ADE80", // Green-400 color: "#4ADE80", // Green-400
}, },
cost: { cost: {
label: "کاهش هزینه (میلیون)", label: "کاهش هزینه",
color: "#F87171", // Red-400 color: "#F87171", // Red-400
}, },
} satisfies ChartConfig; } satisfies ChartConfig;
export function InteractiveBarChart() { export function InteractiveBarChart({
const [activeChart, setActiveChart] = data,
React.useState<keyof typeof chartConfig>("ideas"); }: {
data: CompanyChartDatum[];
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),
}),
[],
);
return ( return (
<Card className="py-0 bg-transparent mt-20 border-none h-full"> <Card className="py-0 bg-transparent mt-20 border-none h-full">
<CardContent className="px-2 sm:p-6 bg-transparent"> <CardContent className="px-2 sm:p-6 bg-transparent">
<ChartContainer <ChartContainer config={chartConfig} className="aspect-auto h-96 w-full">
config={chartConfig}
className="aspect-auto h-96 w-full"
>
<BarChart <BarChart
accessibilityLayer accessibilityLayer
data={chartData} data={data}
margin={{ margin={{ left: 12, right: 12 }}
left: 12,
right: 12,
}}
barCategoryGap="42%" barCategoryGap="42%"
> >
<CartesianGrid vertical={false} stroke="#475569" /> <CartesianGrid vertical={false} stroke="#475569" />
@ -84,41 +66,58 @@ export function InteractiveBarChart() {
tick={{ fill: "#94a3b8", fontSize: 12 }} tick={{ fill: "#94a3b8", fontSize: 12 }}
tickFormatter={(value) => `${value}%`} tickFormatter={(value) => `${value}%`}
/> />
<Bar <Bar dataKey="capacity" fill={chartConfig.capacity.color} radius={[8, 8, 0, 0]}>
dataKey="ideas"
fill={chartConfig.ideas.color}
radius={[8, 8, 0, 0]}
>
<LabelList <LabelList
dataKey="ideas" dataKey="capacity"
position="top" position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }} style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
formatter={(v: number) => `${Math.round(v)}%`}
/> />
</Bar> </Bar>
<Bar <Bar dataKey="revenue" fill={chartConfig.revenue.color} radius={[8, 8, 0, 0]}>
dataKey="revenue"
fill={chartConfig.revenue.color}
radius={[8, 8, 0, 0]}
>
<LabelList <LabelList
dataKey="revenue" dataKey="revenue"
position="top" position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }} style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
formatter={(v: number) => `${Math.round(v)}%`}
/> />
</Bar> </Bar>
<Bar <Bar dataKey="cost" fill={chartConfig.cost.color} radius={[8, 8, 0, 0]}>
dataKey="cost"
fill={chartConfig.cost.color}
radius={[8, 8, 0, 0]}
>
<LabelList <LabelList
dataKey="cost" dataKey="cost"
position="top" position="top"
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }} style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
formatter={(v: number) => `${Math.round(v)}%`}
/> />
</Bar> </Bar>
</BarChart> </BarChart>
</ChartContainer> </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> </CardContent>
</Card> </Card>
); );

View File

@ -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;

View File

@ -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;

View File

@ -269,22 +269,6 @@ class ApiService {
return this.get("/projects"); 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 // Dashboard methods
public async getDashboardStats() { public async getDashboardStats() {
return this.get("/dashboard/stats"); return this.get("/dashboard/stats");

View File

@ -6,18 +6,16 @@ export default [
route("dashboard/project-management", "routes/project-management.tsx"), route("dashboard/project-management", "routes/project-management.tsx"),
route( route(
"dashboard/innovation-basket/process-innovation", "dashboard/innovation-basket/process-innovation",
"routes/innovation-basket.process-innovation.tsx" "routes/innovation-basket.process-innovation.tsx",
), ),
route( route(
"dashboard/innovation-basket/green-innovation", "dashboard/innovation-basket/green-innovation",
"routes/green-innovation.tsx" "routes/green-innovation.tsx",
), ),
route( route(
"/dashboard/innovation-basket/digital-innovation", "/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("dashboard/ecosystem", "routes/ecosystem.tsx"),
route("404", "routes/404.tsx"), route("404", "routes/404.tsx"),
route("unauthorized", "routes/unauthorized.tsx"), route("unauthorized", "routes/unauthorized.tsx"),

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
public/besparan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
public/faravash1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
public/faravash2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
public/khwarazmi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
public/kimia.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB