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