664 lines
24 KiB
TypeScript
664 lines
24 KiB
TypeScript
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;
|