inogen/app/components/dashboard/project-management/project-management-page.tsx

522 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback, useRef } from "react";
import { DashboardLayout } from "../layout";
import { Card, CardContent } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import {
ChevronUp,
ChevronDown,
RefreshCw,
Calendar,
DollarSign,
Users,
Target,
} from "lucide-react";
import apiService from "~/lib/api";
import toast from "react-hot-toast";
interface ProjectData {
WorkflowID: number;
ValueP1215S1887ValueID: number;
ValueP1215S1887StageID: number;
project_no: string;
title: string;
strategic_theme: string;
value_technology_and_innovation: string;
type_of_innovation: string;
innovation: string;
person_executing: string;
excellent_observer: string;
observer: string;
moderator: string;
start_date: string;
end_date: string | null;
done_date: string | null;
approved_budget: string;
budget_spent: string;
}
interface SortConfig {
field: string;
direction: "asc" | "desc";
}
const columns = [
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "120px" },
{ key: "title", label: "عنوان پروژه", sortable: true, width: "200px" },
{
key: "strategic_theme",
label: "ماموریت راهبردی",
sortable: true,
width: "160px",
},
{
key: "value_technology_and_innovation",
label: "ارزش فناوری و نوآوری",
sortable: true,
width: "200px",
},
{
key: "type_of_innovation",
label: "انواع نوآوری",
sortable: true,
width: "140px",
},
{
key: "innovation",
label: "نوآوری",
sortable: true,
width: "120px",
},
{
key: "person_executing",
label: "مجری",
sortable: true,
width: "140px",
},
{
key: "excellent_observer",
label: "ناظر عالی",
sortable: true,
width: "140px",
},
{
key: "observer",
label: "ناظر",
sortable: true,
width: "140px",
},
{
key: "moderator",
label: "مدیر پروژه",
sortable: true,
width: "140px",
},
{
key: "start_date",
label: "تاریخ شروع",
sortable: true,
width: "120px",
},
{
key: "end_date",
label: "تاریخ پایان نهایی",
sortable: true,
width: "140px",
},
{
key: "done_date",
label: "تاریخ انجام نهایی",
sortable: true,
width: "140px",
},
{
key: "approved_budget",
label: "بودجه مصوب",
sortable: true,
width: "150px",
},
{
key: "budget_spent",
label: "بودجه هزینه شده",
sortable: true,
width: "150px",
},
];
export function ProjectManagementPage() {
const [projects, setProjects] = useState<ProjectData[]>([]);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(20);
const [hasMore, setHasMore] = useState(true);
const [totalCount, setTotalCount] = useState(0);
const [actualTotalCount, setActualTotalCount] = useState(0);
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "start_date",
direction: "asc",
});
const observerRef = useRef<HTMLDivElement>(null);
const fetchProjects = async (reset = false) => {
try {
if (reset) {
setLoading(true);
setCurrentPage(1);
} else {
setLoadingMore(true);
}
const pageToFetch = reset ? 1 : currentPage;
const response = await apiService.select({
ProcessName: "project",
OutputFields: [
"project_no",
"title",
"strategic_theme",
"value_technology_and_innovation",
"type_of_innovation",
"innovation",
"person_executing",
"excellent_observer",
"observer",
"moderator",
"start_date",
"end_date",
"done_date",
"approved_budget",
"budget_spent",
],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [],
});
if (response.state === 0) {
// Parse the JSON string from the API response
const dataString = response.data;
if (dataString && typeof dataString === "string") {
try {
const parsedData = JSON.parse(dataString);
if (Array.isArray(parsedData)) {
if (reset) {
setProjects(parsedData);
setTotalCount(parsedData.length);
} else {
setProjects((prev) => [...prev, ...parsedData]);
setTotalCount((prev) => prev + parsedData.length);
}
// Check if there are more items to load
setHasMore(parsedData.length === pageSize);
} else {
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} catch (parseError) {
console.error("Error parsing project data:", parseError);
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} else {
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} else {
toast.error(response.message || "خطا در دریافت اطلاعات پروژه‌ها");
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} catch (error) {
console.error("Error fetching projects:", error);
toast.error("خطا در دریافت اطلاعات پروژه‌ها");
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
} finally {
setLoading(false);
setLoadingMore(false);
}
};
const loadMore = useCallback(() => {
if (!loadingMore && hasMore) {
setCurrentPage((prev) => prev + 1);
}
}, [loadingMore, hasMore]);
useEffect(() => {
fetchProjects(true);
fetchTotalCount();
}, [sortConfig]);
useEffect(() => {
if (currentPage > 1) {
fetchProjects(false);
}
}, [currentPage]);
// Infinite scroll observer
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !loadingMore) {
loadMore();
}
},
{ threshold: 0.1 },
);
if (observerRef.current) {
observer.observe(observerRef.current);
}
return () => {
if (observerRef.current) {
observer.unobserve(observerRef.current);
}
};
}, [loadMore, hasMore, loadingMore]);
const handleSort = (field: string) => {
setSortConfig((prev) => ({
field,
direction:
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
}));
setCurrentPage(1);
setProjects([]);
setHasMore(true);
};
const fetchTotalCount = async () => {
try {
const response = await apiService.select({
ProcessName: "project",
OutputFields: ["count(project_no)"],
Conditions: [],
});
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
try {
const parsedData = JSON.parse(dataString);
if (Array.isArray(parsedData) && parsedData[0]) {
setActualTotalCount(parsedData[0].project_no_count || 0);
}
} catch (parseError) {
console.error("Error parsing count data:", parseError);
}
}
}
} catch (error) {
console.error("Error fetching total count:", error);
}
};
const handleRefresh = () => {
setCurrentPage(1);
setProjects([]);
setHasMore(true);
fetchProjects(true);
fetchTotalCount();
};
const formatCurrency = (amount: string | number) => {
if (!amount) return "0 ریال";
// Remove commas and convert to number
const numericAmount =
typeof amount === "string"
? parseFloat(amount.replace(/,/g, ""))
: amount;
if (isNaN(numericAmount)) return "0 ریال";
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
};
const formatDate = (dateString: string | null) => {
if (!dateString || dateString === "null" || dateString.trim() === "")
return "-";
try {
return new Intl.DateTimeFormat("fa-IR").format(new Date(dateString));
} catch {
return "-";
}
};
const renderCellContent = (item: ProjectData, column: any) => {
const value = item[column.key as keyof ProjectData];
switch (column.key) {
case "approved_budget":
case "budget_spent":
return (
<span className="font-medium text-emerald-400">
{formatCurrency(String(value))}
</span>
);
case "start_date":
case "end_date":
case "done_date":
return (
<span className="text-gray-300">{formatDate(String(value))}</span>
);
case "project_no":
return (
<Badge
variant="outline"
className="font-mono text-emerald-400 border-emerald-500/50"
>
{String(value)}
</Badge>
);
case "title":
return <span className="font-medium text-white">{String(value)}</span>;
default:
return <span className="text-gray-300">{String(value) || "-"}</span>;
}
};
const totalPages = Math.ceil(totalCount / pageSize);
return (
<DashboardLayout title="مدیریت پروژه‌ها">
<div className="p-6 space-y-6">
{/* Actions */}
<div className="flex justify-end">
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={loading}
>
<RefreshCw
className={`w-4 h-4 ml-2 ${loading ? "animate-spin" : ""}`}
/>
بروزرسانی
</Button>
</div>
{/* Data Table */}
<Card className="bg-gray-800/90 backdrop-blur-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="relative">
<div className="overflow-auto max-h-[calc(100vh-250px)]">
<Table>
<TableHeader>
<TableRow className="bg-[#3F415A] ">
{columns.map((column) => (
<TableHead
key={column.key}
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium"
style={{ width: column.width }}
>
{column.sortable ? (
<button
onClick={() => handleSort(column.key)}
className="flex items-center gap-2 hover:text-emerald-400 transition-colors"
>
<span>{column.label}</span>
{sortConfig.field === column.key ? (
sortConfig.direction === "asc" ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)
) : (
<div className="w-4 h-4" />
)}
</button>
) : (
column.label
)}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center py-8"
>
<div className="flex items-center justify-center gap-2">
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" />
<span className="font-persian text-gray-300">
در حال بارگذاری...
</span>
</div>
</TableCell>
</TableRow>
) : projects.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center py-8"
>
<span className="text-gray-400 font-persian">
هیچ پروژهای یافت نشد
</span>
</TableCell>
</TableRow>
) : (
projects.map((project, index) => (
<TableRow key={`${project.project_no}-${index}`}>
{columns.map((column) => (
<TableCell
key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20"
>
{renderCellContent(project, column)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
{/* Infinite scroll trigger */}
<div
ref={observerRef}
className="h-4 flex items-center justify-center"
>
{loadingMore && (
<div className="flex items-center gap-2 py-4">
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" />
<span className="text-sm text-gray-300 font-persian">
در حال بارگذاری...
</span>
</div>
)}
{!hasMore && projects.length > 0 && (
<div className="py-4">
<span className="text-sm text-gray-400 font-persian">
همه دادهها نمایش داده شد
</span>
</div>
)}
</div>
</CardContent>
{/* Footer */}
<div className="p-4 bg-gray-700/50">
<div className="flex items-center justify-between text-sm text-gray-300 font-persian">
<span>
نمایش {projects.length} از {actualTotalCount} پروژه
</span>
<span>کل پروژهها: {actualTotalCount}</span>
</div>
</div>
</Card>
</div>
</DashboardLayout>
);
}