592 lines
18 KiB
TypeScript
592 lines
18 KiB
TypeScript
import { saveAs } from "file-saver";
|
||
import jalaali from "jalaali-js";
|
||
import {
|
||
Calendar,
|
||
ChevronLeft,
|
||
FileChartColumnIncreasing,
|
||
Menu,
|
||
PanelLeft,
|
||
Server,
|
||
User,
|
||
} from "lucide-react";
|
||
import React, { useEffect, useRef, useState } from "react";
|
||
import { useLocation } from "react-router";
|
||
import XLSX from "xlsx-js-style";
|
||
import { Button } from "~/components/ui/button";
|
||
import { Calendar as CustomCalendar } from "~/components/ui/Calendar";
|
||
import { useAuth } from "~/contexts/auth-context";
|
||
import apiService from "~/lib/api";
|
||
import { cn, EventBus, handleDataValue } from "~/lib/utils";
|
||
|
||
interface HeaderProps {
|
||
onToggleSidebar?: () => void;
|
||
className?: string;
|
||
title?: string;
|
||
titleIcon?: React.ComponentType<{ className?: string }> | null;
|
||
}
|
||
|
||
interface MonthItem {
|
||
id: string;
|
||
label: string;
|
||
start: string;
|
||
end: string;
|
||
}
|
||
|
||
interface CurrentDay {
|
||
start?: string;
|
||
end?: string;
|
||
sinceMonth?: string;
|
||
fromMonth?: string;
|
||
}
|
||
|
||
interface SelectedDate {
|
||
since?: number;
|
||
until?: number;
|
||
}
|
||
|
||
const monthList: Array<MonthItem> = [
|
||
{
|
||
id: "month-1",
|
||
label: "بهار",
|
||
start: "01/01",
|
||
end: "03/31",
|
||
},
|
||
{
|
||
id: "month-2",
|
||
label: "تابستان",
|
||
start: "04/01",
|
||
end: "06/31",
|
||
},
|
||
{
|
||
id: "month-3",
|
||
label: "پاییز",
|
||
start: "07/01",
|
||
end: "09/31",
|
||
},
|
||
{
|
||
id: "month-4",
|
||
label: "زمستان",
|
||
start: "10/01",
|
||
end: "12/30",
|
||
},
|
||
];
|
||
|
||
const columns: Array<any> = [
|
||
{ key: "title", label: "عنوان پروژه", sortable: true, width: "300px" },
|
||
{
|
||
key: "importance_project",
|
||
label: "میزان اهمیت",
|
||
sortable: true,
|
||
width: "160px",
|
||
},
|
||
{
|
||
key: "strategic_theme",
|
||
label: "مضمون راهبردی",
|
||
sortable: true,
|
||
width: "200px",
|
||
},
|
||
{
|
||
key: "value_technology_and_innovation",
|
||
label: "ارزش فناوری و نوآوری",
|
||
sortable: true,
|
||
width: "220px",
|
||
},
|
||
{
|
||
key: "type_of_innovation",
|
||
label: "انواع نوآوری",
|
||
sortable: true,
|
||
width: "160px",
|
||
},
|
||
{
|
||
key: "innovation",
|
||
label: "میزان نوآوری",
|
||
sortable: true,
|
||
width: "140px",
|
||
},
|
||
{
|
||
key: "person_executing",
|
||
label: "مسئول اجرا",
|
||
sortable: true,
|
||
width: "180px",
|
||
},
|
||
{
|
||
key: "excellent_observer",
|
||
label: "ناطر عالی",
|
||
sortable: true,
|
||
width: "180px",
|
||
},
|
||
{ key: "observer", label: "ناظر پروژه", sortable: true, width: "180px" },
|
||
{ key: "moderator", label: "مجری", sortable: true, width: "180px" },
|
||
{
|
||
key: "executive_phase",
|
||
label: "فاز اجرایی",
|
||
sortable: true,
|
||
width: "160px",
|
||
},
|
||
{
|
||
key: "start_date",
|
||
label: "تاریخ شروع",
|
||
sortable: true,
|
||
width: "120px",
|
||
},
|
||
{
|
||
key: "remaining_time",
|
||
label: "زمان باقی مانده",
|
||
sortable: true,
|
||
width: "140px",
|
||
computed: true,
|
||
},
|
||
{
|
||
key: "end_date",
|
||
label: "تاریخ پایان (برنامهریزی)",
|
||
sortable: true,
|
||
width: "160px",
|
||
},
|
||
{
|
||
key: "renewed_duration",
|
||
label: "مدت زمان تمدید",
|
||
sortable: true,
|
||
width: "140px",
|
||
},
|
||
{
|
||
key: "done_date",
|
||
label: "تاریخ پایان (واقعی)",
|
||
sortable: true,
|
||
width: "160px",
|
||
},
|
||
{
|
||
key: "deviation_from_program",
|
||
label: "متوسط انحراف برنامهای",
|
||
sortable: true,
|
||
width: "160px",
|
||
},
|
||
{
|
||
key: "approved_budget",
|
||
label: "بودجه مصوب",
|
||
sortable: true,
|
||
width: "150px",
|
||
},
|
||
{
|
||
key: "budget_spent",
|
||
label: "بودجه صرف شده",
|
||
sortable: true,
|
||
width: "150px",
|
||
},
|
||
{
|
||
key: "cost_deviation",
|
||
label: "متوسط انحراف هزینهای",
|
||
sortable: true,
|
||
width: "160px",
|
||
},
|
||
];
|
||
|
||
export function Header({
|
||
onToggleSidebar,
|
||
className,
|
||
title = "صفحه اول",
|
||
titleIcon,
|
||
}: HeaderProps) {
|
||
const { user } = useAuth();
|
||
const { jy } = jalaali.toJalaali(new Date());
|
||
|
||
const calendarRef = useRef<HTMLDivElement>(null);
|
||
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState<boolean>(false);
|
||
const [isNotificationOpen, setIsNotificationOpen] = useState<boolean>(false);
|
||
const [openCalendar, setOpenCalendar] = useState<boolean>(false);
|
||
const [excelLoading, setExcelLoading] = useState<boolean>(false);
|
||
const location = useLocation();
|
||
const projectManagerRoute = "/dashboard/project-management";
|
||
const [currentYear, setCurrentYear] = useState<SelectedDate>({
|
||
since: jy,
|
||
until: jy,
|
||
});
|
||
|
||
const [selectedDate, setSelectedDate] = useState<CurrentDay>({});
|
||
|
||
useEffect(() => {
|
||
const storedDate = localStorage.getItem("dateSelected");
|
||
if (storedDate) {
|
||
const parsedDate = JSON.parse(storedDate);
|
||
setSelectedDate(parsedDate);
|
||
|
||
const sinceYear = parsedDate.start
|
||
? parseInt(parsedDate.start.split("/")[0], 10)
|
||
: jy;
|
||
const untilYear = parsedDate.end
|
||
? parseInt(parsedDate.end.split("/")[0], 10)
|
||
: jy;
|
||
|
||
setCurrentYear({ since: sinceYear, until: untilYear });
|
||
} else {
|
||
const defaultDate = {
|
||
sinceMonth: "بهار",
|
||
fromMonth: "زمستان",
|
||
start: `${jy}/01/01`,
|
||
end: `${jy}/12/30`,
|
||
};
|
||
setSelectedDate(defaultDate);
|
||
localStorage.setItem("dateSelected", JSON.stringify(defaultDate));
|
||
setCurrentYear({ since: jy, until: jy });
|
||
}
|
||
}, []);
|
||
|
||
const redirectHandler = async () => {
|
||
try {
|
||
const getData = await apiService.post("/GenerateSsoCode");
|
||
|
||
//بندر امام
|
||
// const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`;
|
||
//آپادانا
|
||
const url = `https://APADANA-IATM-bpms.pelekan.org/redirect/${getData.data}`;
|
||
//نوری
|
||
// const url = `https://NOPC-IATM-bpms.pelekan.org/redirect/${getData.data}`;
|
||
|
||
window.open(url, "_blank");
|
||
} catch (error) {
|
||
console.log(error);
|
||
}
|
||
};
|
||
|
||
const changeSinceYear = (delta: number) => {
|
||
if (!currentYear) return;
|
||
|
||
const newSince = (currentYear.since ?? 0) + delta;
|
||
|
||
if (newSince > (currentYear.until ?? Infinity) || newSince < 0) return;
|
||
|
||
const updatedYear = { ...currentYear, since: newSince };
|
||
setCurrentYear(updatedYear);
|
||
|
||
const updatedDate = {
|
||
...selectedDate,
|
||
start: `${newSince}/${selectedDate.start?.split("/").slice(1).join("/")}`,
|
||
};
|
||
setSelectedDate(updatedDate);
|
||
localStorage.setItem("dateSelected", JSON.stringify(updatedDate));
|
||
EventBus.emit("dateSelected", updatedDate);
|
||
};
|
||
|
||
const nextFromYearHandler = () => changeSinceYear(1);
|
||
const prevFromYearHandler = () => changeSinceYear(-1);
|
||
|
||
const selectFromDateHandler = (val: MonthItem) => {
|
||
const data = {
|
||
...selectedDate,
|
||
start: `${currentYear.since}/${val.start}`,
|
||
sinceMonth: val.label,
|
||
};
|
||
setSelectedDate(data);
|
||
localStorage.setItem("dateSelected", JSON.stringify(data));
|
||
EventBus.emit("dateSelected", data);
|
||
};
|
||
|
||
const changeUntilYear = (delta: number) => {
|
||
if (!currentYear) return;
|
||
|
||
const newUntil = (currentYear.until ?? 0) + delta;
|
||
|
||
if (newUntil < (currentYear.since ?? 0)) return;
|
||
|
||
const updatedYear = { ...currentYear, until: newUntil };
|
||
setCurrentYear(updatedYear);
|
||
|
||
const updatedDate = {
|
||
...selectedDate,
|
||
end: `${newUntil}/${selectedDate.end?.split("/").slice(1).join("/")}`,
|
||
};
|
||
setSelectedDate(updatedDate);
|
||
localStorage.setItem("dateSelected", JSON.stringify(updatedDate));
|
||
EventBus.emit("dateSelected", updatedDate);
|
||
};
|
||
|
||
const nextUntilYearHandler = () => changeUntilYear(1);
|
||
const prevUntilYearHandler = () => changeUntilYear(-1);
|
||
|
||
const selectUntilDateHandler = (val: MonthItem) => {
|
||
const data = {
|
||
...selectedDate,
|
||
end: `${currentYear.until}/${val.end}`,
|
||
fromMonth: val.label,
|
||
};
|
||
setSelectedDate(data);
|
||
localStorage.setItem("dateSelected", JSON.stringify(data));
|
||
EventBus.emit("dateSelected", data);
|
||
toggleCalendar();
|
||
};
|
||
|
||
const toggleCalendar = () => {
|
||
setOpenCalendar(!openCalendar);
|
||
};
|
||
|
||
useEffect(() => {
|
||
const handleClickOutside = (event: MouseEvent) => {
|
||
if (
|
||
calendarRef.current &&
|
||
!calendarRef.current.contains(event.target as Node)
|
||
) {
|
||
setOpenCalendar(false);
|
||
}
|
||
};
|
||
document.addEventListener("mousedown", handleClickOutside);
|
||
return () => {
|
||
document.removeEventListener("mousedown", handleClickOutside);
|
||
};
|
||
}, []);
|
||
|
||
const exportToExcel = async () => {
|
||
let arr = [];
|
||
const data: any = await fetchExcelData();
|
||
for (let i = 0; i < data.length; i++) {
|
||
let obj: Record<string, any> = {};
|
||
const project = data[i];
|
||
|
||
Object.entries(project).forEach(([pKey, pValue]: [any, any]) => {
|
||
Object.values(columns).forEach((col) => {
|
||
if (pKey === col?.key) {
|
||
``;
|
||
obj[col?.label] = handleDataValue(
|
||
pValue?.includes(",") ? pValue.replaceAll(",", "") : pValue
|
||
);
|
||
}
|
||
});
|
||
});
|
||
|
||
arr.push(obj);
|
||
}
|
||
|
||
const worksheet = XLSX.utils.json_to_sheet(arr);
|
||
|
||
const workbook = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(workbook, worksheet, "People");
|
||
|
||
const excelBuffer = XLSX.write(workbook, {
|
||
bookType: "xlsx",
|
||
type: "array",
|
||
});
|
||
|
||
const blob = new Blob([excelBuffer], {
|
||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||
});
|
||
saveAs(blob, "reports.xls");
|
||
};
|
||
|
||
const fetchExcelData = async () => {
|
||
setExcelLoading(true);
|
||
const fetchableColumns = columns.filter((c) => !c.computed);
|
||
const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key);
|
||
|
||
const response = await apiService.select({
|
||
ProcessName: "project",
|
||
OutputFields: outputFields,
|
||
Conditions: [
|
||
["start_date", ">=", selectedDate?.start || null, "and"],
|
||
["start_date", "<=", selectedDate?.end || null],
|
||
],
|
||
});
|
||
const parsedData = JSON.parse(response.data);
|
||
setExcelLoading(false);
|
||
return parsedData;
|
||
};
|
||
|
||
const handleDownloadFile = () => {
|
||
if (excelLoading) return null;
|
||
else exportToExcel();
|
||
};
|
||
|
||
return (
|
||
<header
|
||
className={cn(
|
||
"backdrop-blur-sm border-b border-gray-400/30 h-16 flex items-center justify-between px-4 lg:px-6 shadow-sm relative z-30",
|
||
className
|
||
)}
|
||
>
|
||
{/* Left Section */}
|
||
<div className="flex items-center gap-4">
|
||
{/* Mobile Menu Toggle */}
|
||
{onToggleSidebar && (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={onToggleSidebar}
|
||
className="lg:hidden"
|
||
>
|
||
<Menu className="h-5 w-5" />
|
||
</Button>
|
||
)}
|
||
|
||
{/* Page Title */}
|
||
<h1 className="text-xl flex items-center justify-center gap-4 font-bold text-white font-persian">
|
||
{/* Right-side icon for current page */}
|
||
{titleIcon ? (
|
||
<div className="flex items-center gap-2 mr-4">
|
||
{React.createElement(titleIcon, { className: "w-5 h-5 " })}
|
||
</div>
|
||
) : (
|
||
<PanelLeft />
|
||
)}
|
||
{title.includes("-") ? (
|
||
<div className="flex row items-center gap-4">
|
||
<div className="flex items-center gap-1">
|
||
{title.split("-")[0]}
|
||
<ChevronLeft className="inline-block w-4 h-4" />
|
||
{title.split("-")[1]}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
title
|
||
)}
|
||
</h1>
|
||
|
||
<div ref={calendarRef} className="flex flex-col gap-3 relative">
|
||
<div
|
||
onClick={toggleCalendar}
|
||
className="flex flex-row w-full gap-2 items-center border border-pr-gray p-1.5 rounded-md px-2.5 min-w-64 cursor-pointer hover:bg-pr-gray/50 transition-all duration-300"
|
||
>
|
||
<Calendar size={20} />
|
||
{selectedDate ? (
|
||
<div className="flex flex-row justify-between w-full min-w-36 font-bold gap-1">
|
||
<div className="flex flex-row gap-1.5 w-max">
|
||
<span className="text-md">از</span>
|
||
<span className="text-md">{selectedDate?.sinceMonth}</span>
|
||
<span className="text-md">
|
||
{handleDataValue(currentYear.since)}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-row gap-1.5 w-max">
|
||
<span className="text-md">تا</span>
|
||
<span className="text-md">{selectedDate?.fromMonth}</span>
|
||
<span className="text-md">
|
||
{handleDataValue(currentYear.until)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
"تاریخ مورد نظر خود را انتخاب نمایید"
|
||
)}
|
||
</div>
|
||
|
||
{openCalendar && (
|
||
<div className="flex flex-row gap-2.5 absolute top-14 right-[-40px] p-2.5 !pt-3.5 w-80 rounded-3xl overflow-hidden bg-pr-gray border-2 border-[#5F6284]">
|
||
<CustomCalendar
|
||
title="از"
|
||
nextYearHandler={prevFromYearHandler}
|
||
prevYearHandler={nextFromYearHandler}
|
||
currentYear={handleDataValue(currentYear?.since)}
|
||
monthList={monthList}
|
||
selectedDate={selectedDate?.sinceMonth}
|
||
selectDateHandler={selectFromDateHandler}
|
||
/>
|
||
<span className="w-0.5 h-[12.5rem] border border-[#5F6284] block "></span>
|
||
<CustomCalendar
|
||
title="تا"
|
||
nextYearHandler={prevUntilYearHandler}
|
||
prevYearHandler={nextUntilYearHandler}
|
||
currentYear={handleDataValue(currentYear?.until)}
|
||
monthList={monthList}
|
||
selectedDate={selectedDate?.fromMonth}
|
||
selectDateHandler={selectUntilDateHandler}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Section */}
|
||
<div className="flex items-center gap-2">
|
||
{/* User Menu */}
|
||
<div className="relative">
|
||
<div className="flex items-center gap-2">
|
||
{location.pathname === projectManagerRoute ? (
|
||
<div className="flex justify-end w-full mb-0 pl-2">
|
||
<span
|
||
className={`flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian ${excelLoading ? "!cursor-not-allowed !opacity-10" : ""}`}
|
||
onClick={handleDownloadFile}
|
||
>
|
||
<FileChartColumnIncreasing className="h-4 w-4" />
|
||
دانلود فایل اکسل
|
||
</span>
|
||
</div>
|
||
) : (
|
||
""
|
||
)}
|
||
|
||
{user?.id === 2041 && (
|
||
<button
|
||
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
|
||
onClick={redirectHandler}
|
||
>
|
||
<Server className="h-4 w-4" />
|
||
ورود به میزکار مدیریت
|
||
</button>
|
||
)}
|
||
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
|
||
className="flex items-center gap-2 text-gray-300"
|
||
>
|
||
<div className="hidden sm:block text-right">
|
||
<div className="text-sm font-medium font-persian">
|
||
{user?.name} {user?.family}
|
||
</div>
|
||
<div className="text-xs text-gray-400 font-persian">
|
||
{user?.username}
|
||
</div>
|
||
</div>
|
||
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 rounded-lg flex items-center justify-center">
|
||
<User className="h-4 w-4" />
|
||
</div>
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Profile Dropdown */}
|
||
{isProfileMenuOpen && (
|
||
<div className="absolute left-0 top-full mt-2 w-48 bg-gray-800 border border-emerald-500/30 rounded-lg shadow-lg z-50">
|
||
<div className="p-3 border-b border-emerald-500/30">
|
||
<div className="text-sm font-medium text-white font-persian">
|
||
{user?.name} {user?.family}
|
||
</div>
|
||
<div className="text-xs text-gray-400 font-persian">
|
||
{user?.email}
|
||
</div>
|
||
</div>
|
||
{/* <div className="py-1">
|
||
<Link
|
||
to="/dashboard/profile"
|
||
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
|
||
onClick={() => setIsProfileMenuOpen(false)}
|
||
>
|
||
<User className="h-4 w-4" />
|
||
پروفایل کاربری
|
||
</Link>
|
||
<Link
|
||
to="/dashboard/settings"
|
||
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
|
||
onClick={() => setIsProfileMenuOpen(false)}
|
||
>
|
||
<Settings className="h-4 w-4" />
|
||
تنظیمات
|
||
</Link>
|
||
</div> */}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Click outside to close dropdowns */}
|
||
{(isProfileMenuOpen || isNotificationOpen) && (
|
||
<div
|
||
className="fixed inset-0 z-40"
|
||
onClick={() => {
|
||
setIsProfileMenuOpen(false);
|
||
setIsNotificationOpen(false);
|
||
}}
|
||
/>
|
||
)}
|
||
</header>
|
||
);
|
||
}
|
||
|
||
export default Header;
|