Compare commits
2 Commits
12e85fdb08
...
41e2787601
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41e2787601 | ||
| 86f5622bdd |
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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");
|
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");
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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