fix logout and gurad ,also fix the table on project management ,fix the style in process-innovation page
This commit is contained in:
parent
26e024f9ac
commit
fa9aa8eedd
|
|
@ -70,7 +70,16 @@ export function GlobalRouteGuard({ children }: GlobalRouteGuardProps) {
|
|||
|
||||
// Case 1: User accessing protected route without authentication
|
||||
if (isProtectedRoute && !isAuthenticated) {
|
||||
toast.error("برای دسترسی به این صفحه باید وارد شوید");
|
||||
// if just logged out, don't show another auth-required toast
|
||||
const justLoggedOut = sessionStorage.getItem("justLoggedOut") === "1";
|
||||
if (!justLoggedOut) {
|
||||
toast.remove("auth-required");
|
||||
toast.error("برای دسترسی به این صفحه باید وارد شوید", {
|
||||
id: "auth-required",
|
||||
});
|
||||
} else {
|
||||
sessionStorage.removeItem("justLoggedOut");
|
||||
}
|
||||
|
||||
// Save the intended destination for after login
|
||||
const returnTo = encodeURIComponent(currentPath + location.search);
|
||||
|
|
@ -80,7 +89,10 @@ export function GlobalRouteGuard({ children }: GlobalRouteGuardProps) {
|
|||
|
||||
// Case 2: User accessing protected route with expired/invalid token
|
||||
if (isProtectedRoute && isAuthenticated && (!token || !token.accessToken)) {
|
||||
toast.error("جلسه کاری شما منقضی شده است. لطفاً دوباره وارد شوید");
|
||||
toast.remove("session-expired");
|
||||
toast.error("جلسه کاری شما منقضی شده است. لطفاً دوباره وارد شوید", {
|
||||
id: "session-expired",
|
||||
});
|
||||
|
||||
// Clear invalid auth data
|
||||
localStorage.removeItem("auth_user");
|
||||
|
|
@ -125,7 +137,10 @@ export function GlobalRouteGuard({ children }: GlobalRouteGuardProps) {
|
|||
navigate("/404", { replace: true });
|
||||
} else {
|
||||
// If user is not authenticated, redirect to login
|
||||
toast.error("صفحه مورد نظر یافت نشد. لطفاً وارد شوید");
|
||||
toast.remove("not-found-login");
|
||||
toast.error("صفحه مورد نظر یافت نشد. لطفاً وارد شوید", {
|
||||
id: "not-found-login",
|
||||
});
|
||||
navigate("/login", { replace: true });
|
||||
}
|
||||
return;
|
||||
|
|
@ -137,7 +152,10 @@ export function GlobalRouteGuard({ children }: GlobalRouteGuardProps) {
|
|||
try {
|
||||
const isValid = await validateToken();
|
||||
if (!isValid) {
|
||||
toast.error("جلسه کاری شما منقضی شده است");
|
||||
toast.remove("session-expired-soft");
|
||||
toast.error("جلسه کاری شما منقضی شده است", {
|
||||
id: "session-expired-soft",
|
||||
});
|
||||
navigate("/unauthorized?reason=token-expired", { replace: true });
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -168,30 +186,7 @@ export function GlobalRouteGuard({ children }: GlobalRouteGuardProps) {
|
|||
navigate,
|
||||
]);
|
||||
|
||||
// Validate token periodically for authenticated users
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !token?.accessToken) return;
|
||||
|
||||
const validateTokenPeriodically = async () => {
|
||||
try {
|
||||
const isValid = await validateToken();
|
||||
if (!isValid) {
|
||||
toast.error("جلسه کاری شما منقضی شده است. لطفاً دوباره وارد شوید");
|
||||
navigate("/login", { replace: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Token validation error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Validate token immediately
|
||||
validateTokenPeriodically();
|
||||
|
||||
// Set up periodic validation (every 5 minutes)
|
||||
const interval = setInterval(validateTokenPeriodically, 5 * 60 * 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isAuthenticated, token, validateToken, navigate]);
|
||||
// Note: periodic validation is handled in the block above and the auth context; avoid duplicating it here.
|
||||
|
||||
// Show loading screen while checking authentication
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from "react";
|
||||
import { useAuth } from "~/contexts/auth-context";
|
||||
import { Navigate, useLocation } from "react-router";
|
||||
import toast from "react-hot-toast";
|
||||
import { useLocation } from "react-router";
|
||||
import { LoadingPage } from "~/components/ui/loading";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
|
|
@ -49,31 +48,37 @@ export function ProtectedRoute({
|
|||
);
|
||||
}
|
||||
|
||||
// If authentication is required but user is not authenticated
|
||||
if (requireAuth && !isAuthenticated) {
|
||||
toast.error("برای دسترسی به این صفحه باید وارد شوید");
|
||||
|
||||
// Save the current location so we can redirect back after login
|
||||
const currentPath = location.pathname + location.search;
|
||||
const loginPath = `${redirectTo}?returnTo=${encodeURIComponent(currentPath)}`;
|
||||
|
||||
return <Navigate to={loginPath} replace />;
|
||||
}
|
||||
|
||||
// If authentication is required but token is missing/invalid
|
||||
if (requireAuth && isAuthenticated && (!token || !token.accessToken)) {
|
||||
toast.error("جلسه کاری شما منقضی شده است. لطفاً دوباره وارد شوید");
|
||||
|
||||
// Clear any stored authentication data
|
||||
localStorage.removeItem("auth_user");
|
||||
localStorage.removeItem("auth_token");
|
||||
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
// If user is authenticated but trying to access login page
|
||||
if (!requireAuth && isAuthenticated && location.pathname === "/login") {
|
||||
return <Navigate to="/dashboard" replace />;
|
||||
// If access is not allowed, render fallback and let the global route guard handle navigation/toasts
|
||||
if (
|
||||
(requireAuth && !isAuthenticated) ||
|
||||
(requireAuth && isAuthenticated && (!token || !token.accessToken)) ||
|
||||
(!requireAuth && isAuthenticated && location.pathname === "/login")
|
||||
) {
|
||||
return (
|
||||
fallback || (
|
||||
<div
|
||||
className="min-h-screen flex items-center justify-center"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(135deg, var(--color-login-dark-start) 0%, var(--color-login-dark-end) 100%)",
|
||||
}}
|
||||
>
|
||||
<div className="text-center space-y-6 max-w-md mx-auto p-8">
|
||||
<div className="flex justify-center">
|
||||
<div className="w-8 h-8 border-2 border-[var(--color-login-primary)] border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-medium font-persian text-white">
|
||||
در حال انتقال...
|
||||
</h2>
|
||||
<p className="text-sm font-persian leading-relaxed text-gray-300">
|
||||
لطفاً منتظر بمانید
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// If all checks pass, render the protected content
|
||||
|
|
|
|||
|
|
@ -468,7 +468,6 @@ export function ProcessInnovationPage() {
|
|||
onClick={() => handleProjectDetails(item)}
|
||||
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 ml-1" />
|
||||
جزئیات بیشتر
|
||||
</Button>
|
||||
);
|
||||
|
|
@ -482,7 +481,7 @@ export function ProcessInnovationPage() {
|
|||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="font-mono text-emerald-400 border-emerald-500/50"
|
||||
className="font-mono"
|
||||
>
|
||||
{String(value)}
|
||||
</Badge>
|
||||
|
|
@ -495,9 +494,7 @@ export function ProcessInnovationPage() {
|
|||
variant="outline"
|
||||
className="font-medium border-2"
|
||||
style={{
|
||||
color: getStatusColor(String(value)),
|
||||
borderColor: getStatusColor(String(value)),
|
||||
backgroundColor: `${getStatusColor(String(value))}20`,
|
||||
border:"none",
|
||||
}}
|
||||
>
|
||||
{String(value)}
|
||||
|
|
@ -593,7 +590,7 @@ export function ProcessInnovationPage() {
|
|||
</div>
|
||||
|
||||
{/* Chart Container */}
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 flex flex-col">
|
||||
{/* Chart Item 1 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-white font-persian text-sm min-w-[140px] text-right">کاهش توقفات تولید</span>
|
||||
|
|
@ -661,10 +658,10 @@ export function ProcessInnovationPage() {
|
|||
{formatNumber(((stats.percentFailuresReduction ?? 0) as number).toFixed?.(1) ?? (stats.percentFailuresReduction ?? 0))}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center ml-[50px] gap-3">
|
||||
<span className="min-w-[140px]"></span>
|
||||
|
||||
{/* Percentage Scale */}
|
||||
<div className="flex justify-between mt-6 pt-4 border-t border-gray-700">
|
||||
<div className="flex w-full justify-between pt-4 border-t border-gray-700">
|
||||
<span className="text-gray-400 text-xs">۰٪</span>
|
||||
{
|
||||
(() => {
|
||||
|
|
@ -684,6 +681,10 @@ export function ProcessInnovationPage() {
|
|||
})()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Percentage Scale */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
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,
|
||||
|
|
@ -42,30 +41,40 @@ interface ProjectData {
|
|||
}
|
||||
|
||||
interface SortConfig {
|
||||
field: string;
|
||||
field: string; // uses column.key
|
||||
direction: "asc" | "desc";
|
||||
}
|
||||
|
||||
const columns = [
|
||||
type ColumnDef = {
|
||||
key: string; // UI key
|
||||
label: string;
|
||||
sortable: boolean;
|
||||
width: string;
|
||||
apiField?: string; // API field name; defaults to key
|
||||
computed?: boolean; // not fetched from API
|
||||
};
|
||||
|
||||
const columns: ColumnDef[] = [
|
||||
{ key: "title", label: "عنوان پروژه", sortable: true, width: "200px" },
|
||||
{ key: "importance_project", label: "میزان اهمیت", sortable: true, width: "150px" },
|
||||
{ key: "importance_project", label: "میزان اهمیت", sortable: true, width: "150px" },
|
||||
{ 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: "execution_phase", label: "فاز اجرایی", sortable: true, width: "140px" }, // API فعلاً نداره، باید اضافه شه
|
||||
{ key: "moderator", label: "مجری", sortable: true, width: "140px" },
|
||||
{ key: "executive_phase", label: "فاز اجرایی", sortable: true, width: "140px" },
|
||||
{ key: "start_date", label: "تاریخ شروع", sortable: true, width: "120px" },
|
||||
{ key: "remaining_time", label: "زمان باقی مانده", sortable: true, width: "140px" }, // API فعلاً نداره
|
||||
{ key: "planned_end_date", label: "تاریخ پایان (برنامهریزی)", sortable: true, width: "160px" }, // API نداره
|
||||
{ key: "extension_duration", label: "مدت زمان تمدید", sortable: true, width: "140px" }, // API نداره
|
||||
{ key: "end_date", label: "تاریخ پایان (واقعی)", sortable: true, width: "160px" },
|
||||
{ key: "avg_schedule_deviation", label: "متوسط انحراف برنامهای", sortable: true, width: "160px" }, // API نداره
|
||||
{ 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: "avg_cost_deviation", label: "متوسط انحراف هزینهای", sortable: true, width: "160px" }, // API نداره
|
||||
{ key: "cost_deviation", label: "متوسط انحراف هزینهای", sortable: true, width: "160px" }
|
||||
];
|
||||
|
||||
|
||||
|
|
@ -103,28 +112,16 @@ export function ProjectManagementPage() {
|
|||
|
||||
const pageToFetch = reset ? 1 : currentPage;
|
||||
|
||||
const fetchableColumns = columns.filter((c) => !c.computed);
|
||||
const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key);
|
||||
const sortCol = columns.find((c) => c.key === sortConfig.field);
|
||||
const sortField = sortCol?.computed ? undefined : (sortCol?.apiField ?? sortCol?.key);
|
||||
|
||||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
OutputFields: [
|
||||
"project_no",
|
||||
"title",
|
||||
"importance_project",
|
||||
"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",
|
||||
],
|
||||
OutputFields: outputFields,
|
||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||
Sorts: [[sortConfig.field, sortConfig.direction]],
|
||||
Sorts: sortField ? [[sortField, sortConfig.direction]] : [],
|
||||
Conditions: [],
|
||||
});
|
||||
|
||||
|
|
@ -376,11 +373,10 @@ export function ProjectManagementPage() {
|
|||
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
};
|
||||
|
||||
const calculateRemainingDays = (start: string | null, end: string | null): number | null => {
|
||||
if (!start || !end) return null; // if either missing
|
||||
const startDate = parseToDate(start);
|
||||
const calculateRemainingDays = (end: string | null): number | null => {
|
||||
if (!end) return null; // if either missing
|
||||
const endDate = parseToDate(end);
|
||||
if (!startDate || !endDate) return null;
|
||||
if (!endDate) return null;
|
||||
const today = getTodayMidnight();
|
||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
const diff = Math.round((endDate.getTime() - today.getTime()) / MS_PER_DAY);
|
||||
|
|
@ -437,19 +433,20 @@ export function ProjectManagementPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const renderCellContent = (item: ProjectData, column: any) => {
|
||||
const value = item[column.key as keyof ProjectData];
|
||||
const renderCellContent = (item: ProjectData, column: ColumnDef) => {
|
||||
const apiField = column.apiField ?? column.key;
|
||||
const value = (item as any)[apiField];
|
||||
|
||||
switch (column.key) {
|
||||
case "remaining_time": {
|
||||
const days = calculateRemainingDays(item.start_date, item.end_date);
|
||||
const days = calculateRemainingDays(item.end_date);
|
||||
if (days == null) {
|
||||
return <span className="text-gray-300">-</span>;
|
||||
}
|
||||
const color = days > 0 ? "#3AEA83" : days < 0 ? "#F76276" : undefined;
|
||||
return (
|
||||
<span className="font-medium" style={{ color }}>
|
||||
{toPersianDigits(days)}
|
||||
<span dir="ltr" className="font-medium flex justify-end gap-1 items-center" style={{ color }}>
|
||||
<span>روز</span> {toPersianDigits(days)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -472,6 +469,23 @@ export function ProjectManagementPage() {
|
|||
{formatCurrency(String(value))}
|
||||
</span>
|
||||
);
|
||||
case "renewed_duration": {
|
||||
const raw = value as any;
|
||||
const numeric = typeof raw === "string" ? Number(raw) : (raw as number);
|
||||
if (numeric === undefined || numeric === null || Number.isNaN(numeric)) {
|
||||
return <span className="text-gray-300">-</span>;
|
||||
}
|
||||
return (
|
||||
<span dir="ltr" className="font-medium flex justify-end gap-1 items-center text-gray-300">
|
||||
<span>روز</span> {toPersianDigits(numeric)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
case "deviation_from_program":
|
||||
case "cost_deviation":
|
||||
return (
|
||||
<span className="text-gray-300">{formatNumber(value as any)}</span>
|
||||
);
|
||||
case "start_date":
|
||||
case "end_date":
|
||||
case "done_date":
|
||||
|
|
@ -504,7 +518,7 @@ export function ProjectManagementPage() {
|
|||
</Badge>
|
||||
);
|
||||
default:
|
||||
return <span className="text-gray-300">{String(value) || "-"}</span>;
|
||||
return <span className="text-gray-300">{(value && String(value)) || "-"}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||
} else {
|
||||
// Token is invalid, clear auth data
|
||||
clearAuthData();
|
||||
toast.error("جلسه کاری شما منقضی شده است");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing saved user data:", error);
|
||||
|
|
@ -126,7 +125,6 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||
const isValid = await validateToken();
|
||||
if (!isValid) {
|
||||
clearAuthData();
|
||||
toast.error("جلسه کاری شما منقضی شده است. لطفاً دوباره وارد شوید");
|
||||
}
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
|
|
@ -210,8 +208,12 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||
} catch (error) {
|
||||
console.error("Logout error:", error);
|
||||
} finally {
|
||||
// mark logout event to suppress next auth-required toast from guard
|
||||
try {
|
||||
sessionStorage.setItem("justLoggedOut", "1");
|
||||
} catch {}
|
||||
clearAuthData();
|
||||
toast.success("با موفقیت خارج شدید");
|
||||
toast.success("با موفقیت خارج شدید", { id: "logout-success" });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -81,20 +81,22 @@ class ApiService {
|
|||
} catch (error) {
|
||||
console.error("API request failed:", error);
|
||||
|
||||
// Handle network errors
|
||||
// Handle network errors (propagate up; UI decides how to toast)
|
||||
if (error instanceof TypeError && error.message.includes("fetch")) {
|
||||
toast.error(
|
||||
"خطا در اتصال به سرور. لطفاً اتصال اینترنت خود را بررسی کنید",
|
||||
);
|
||||
throw new Error("شبکه در دسترس نیست");
|
||||
const err = Object.assign(new Error("شبکه در دسترس نیست"), {
|
||||
code: "NETWORK_ERROR",
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Handle authentication errors
|
||||
if (error instanceof Error && error.message.includes("401")) {
|
||||
toast.error("جلسه کاری شما منقضی شده است. لطفاً دوباره وارد شوید");
|
||||
this.clearToken();
|
||||
localStorage.removeItem("auth_token");
|
||||
localStorage.removeItem("auth_user");
|
||||
try {
|
||||
sessionStorage.setItem("sessionExpired", "1");
|
||||
} catch {}
|
||||
window.location.href = "/login";
|
||||
throw error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { PublicRoute } from "~/components/auth/protected-route";
|
|||
import { useAuth } from "~/contexts/auth-context";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { LoadingPage } from "~/components/ui/loading";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user