fix logout and gurad ,also fix the table on project management ,fix the style in process-innovation page

This commit is contained in:
Saeed AB 2025-08-13 02:40:50 +03:30
parent 26e024f9ac
commit fa9aa8eedd
7 changed files with 133 additions and 115 deletions

View File

@ -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) {

View File

@ -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

View File

@ -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>

View File

@ -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,11 +41,20 @@ 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: "strategic_theme", label: "مضمون راهبردی", sortable: true, width: "160px" },
@ -54,18 +62,19 @@ const columns = [
{ 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: "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>;
}
};

View File

@ -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" });
}
};

View File

@ -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;
}

View File

@ -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 [