removed suspicious comments

This commit is contained in:
laxmanhalaki 2026-02-10 09:54:54 +05:30
parent 08cda349f3
commit 7ae9133b98
9 changed files with 1376 additions and 1614 deletions

View File

@ -412,201 +412,6 @@ function AppRoutes({ onLogout }: AppProps) {
}); });
} }
// Keep the old code below for backward compatibility (local storage fallback)
// This can be removed once API integration is fully tested
/*
// Generate unique ID for the new claim request
const requestId = `RE-REQ-2024-CM-${String(dynamicRequests.length + 2).padStart(3, '0')}`;
// Create full request object
const newRequest = {
id: requestId,
title: `${claimData.activityName} - Claim Request`,
description: claimData.requestDescription,
category: 'Dealer Operations',
subcategory: 'Claim Management',
status: 'pending',
priority: 'standard',
amount: 'TBD',
slaProgress: 0,
slaRemaining: '7 days',
slaEndDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
currentStep: 1,
totalSteps: 8,
templateType: 'claim-management',
templateName: 'Claim Management',
initiator: {
name: 'Current User',
role: 'Regional Marketing Coordinator',
department: 'Marketing',
email: 'current.user@royalenfield.com',
phone: '+91 98765 43290',
avatar: 'CU'
},
department: 'Marketing',
createdAt: new Date().toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true
}),
updatedAt: new Date().toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true
}),
dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
conclusionRemark: '',
claimDetails: {
activityName: claimData.activityName,
activityType: claimData.activityType,
activityDate: claimData.activityDate ? new Date(claimData.activityDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '',
location: claimData.location,
dealerCode: claimData.dealerCode,
dealerName: claimData.dealerName,
dealerEmail: claimData.dealerEmail || 'N/A',
dealerPhone: claimData.dealerPhone || 'N/A',
dealerAddress: claimData.dealerAddress || 'N/A',
requestDescription: claimData.requestDescription,
estimatedBudget: claimData.estimatedBudget || 'TBD',
periodStart: claimData.periodStartDate ? new Date(claimData.periodStartDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '',
periodEnd: claimData.periodEndDate ? new Date(claimData.periodEndDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : ''
},
approvalFlow: claimData.workflowSteps || [
{
step: 1,
approver: `${claimData.dealerName} (Dealer)`,
role: 'Dealer - Document Upload',
status: 'pending',
tatHours: 72,
elapsedHours: 0,
assignedAt: new Date().toISOString(),
comment: null,
timestamp: null,
description: 'Dealer uploads proposal document, cost breakup, timeline for closure, and other supporting documents'
},
{
step: 2,
approver: 'Current User (Initiator)',
role: 'Initiator Evaluation',
status: 'waiting',
tatHours: 48,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Initiator reviews dealer documents and approves or requests modifications'
},
{
step: 3,
approver: 'System Auto-Process',
role: 'IO Confirmation',
status: 'waiting',
tatHours: 1,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Automatic IO (Internal Order) confirmation generated upon initiator approval'
},
{
step: 4,
approver: 'Rajesh Kumar',
role: 'Department Lead Approval',
status: 'waiting',
tatHours: 72,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Department head approves and blocks budget in IO for this activity'
},
{
step: 5,
approver: `${claimData.dealerName} (Dealer)`,
role: 'Dealer - Completion Documents',
status: 'waiting',
tatHours: 120,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Dealer submits activity completion documents and description'
},
{
step: 6,
approver: 'Current User (Initiator)',
role: 'Initiator Verification',
status: 'waiting',
tatHours: 48,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Initiator verifies completion documents and can modify approved amount'
},
{
step: 7,
approver: 'System Auto-Process',
role: 'E-Invoice Generation',
status: 'waiting',
tatHours: 1,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Auto-generate e-invoice based on final approved amount'
},
{
step: 8,
approver: 'Finance Team',
role: 'Credit Note Issuance',
status: 'waiting',
tatHours: 48,
elapsedHours: 0,
assignedAt: null,
comment: null,
timestamp: null,
description: 'Finance team issues credit note to dealer'
}
],
documents: [],
spectators: [],
auditTrail: [
{
type: 'created',
action: 'Request Created',
details: `Claim request for ${claimData.activityName} created`,
user: 'Current User',
timestamp: new Date().toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: true
})
}
],
tags: ['claim-management', 'new-request', claimData.activityType?.toLowerCase().replace(/\s+/g, '-')]
};
// Add to dynamic requests
setDynamicRequests(prev => [...prev, newRequest]);
// Also add to REQUEST_DATABASE for immediate viewing
(REQUEST_DATABASE as any)[requestId] = newRequest;
toast.success('Claim Request Submitted', {
description: 'Your claim management request has been created successfully.',
});
navigate('/my-requests');
*/
}; };
return ( return (

View File

@ -31,7 +31,7 @@ export function AnalyticsConfig() {
}); });
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save configuration
toast.success('Analytics configuration saved successfully'); toast.success('Analytics configuration saved successfully');
}; };

View File

@ -8,7 +8,7 @@ import { toast } from 'sonner';
export type Role = 'Initiator' | 'Approver' | 'Spectator'; export type Role = 'Initiator' | 'Approver' | 'Spectator';
export type KPICard = export type KPICard =
| 'Total Requests' | 'Total Requests'
| 'Open Requests' | 'Open Requests'
| 'Approved Requests' | 'Approved Requests'
@ -59,7 +59,7 @@ export function DashboardConfig() {
}); });
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save dashboard configuration
toast.success('Dashboard layout saved successfully'); toast.success('Dashboard layout saved successfully');
}; };

View File

@ -28,7 +28,7 @@ export function NotificationConfig() {
}); });
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save notification configuration
toast.success('Notification configuration saved successfully'); toast.success('Notification configuration saved successfully');
}; };

View File

@ -23,7 +23,7 @@ export function SharingConfig() {
}); });
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save sharing configuration
toast.success('Sharing policy saved successfully'); toast.success('Sharing policy saved successfully');
}; };

View File

@ -2,18 +2,18 @@ import { useState, useCallback, useEffect, useRef } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { import {
Plus, Plus,
Search, Search,
Users, Users,
Shield, Shield,
Loader2, Loader2,
CheckCircle, CheckCircle,
AlertCircle, AlertCircle,
@ -75,7 +75,7 @@ export function UserManagement() {
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
const [loadingUsers, setLoadingUsers] = useState(false); const [loadingUsers, setLoadingUsers] = useState(false);
const [roleStats, setRoleStats] = useState({ admins: 0, management: 0, users: 0, total: 0, active: 0, inactive: 0 }); const [roleStats, setRoleStats] = useState({ admins: 0, management: 0, users: 0, total: 0, active: 0, inactive: 0 });
// Pagination and filtering // Pagination and filtering
const [roleFilter, setRoleFilter] = useState<'ELEVATED' | 'ALL' | 'ADMIN' | 'MANAGEMENT' | 'USER'>('ELEVATED'); const [roleFilter, setRoleFilter] = useState<'ELEVATED' | 'ALL' | 'ADMIN' | 'MANAGEMENT' | 'USER'>('ELEVATED');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@ -135,14 +135,14 @@ export function UserManagement() {
// We'll search with a broader filter to find the user // We'll search with a broader filter to find the user
const response = await userApi.getUsersByRole('ALL', 1, 1000); const response = await userApi.getUsersByRole('ALL', 1, 1000);
const allUsers = response.data?.data?.users || []; const allUsers = response.data?.data?.users || [];
const foundUser = allUsers.find((u: any) => const foundUser = allUsers.find((u: any) =>
u.email?.toLowerCase() === email.toLowerCase() u.email?.toLowerCase() === email.toLowerCase()
); );
if (foundUser && foundUser.role) { if (foundUser && foundUser.role) {
return foundUser.role as 'USER' | 'MANAGEMENT' | 'ADMIN'; return foundUser.role as 'USER' | 'MANAGEMENT' | 'ADMIN';
} }
return null; // User not found in system, no role assigned return null; // User not found in system, no role assigned
} catch (error) { } catch (error) {
console.error('Failed to fetch user role:', error); console.error('Failed to fetch user role:', error);
@ -156,7 +156,7 @@ export function UserManagement() {
setSearchQuery(user.email); setSearchQuery(user.email);
setSearchResults([]); setSearchResults([]);
setFetchingRole(true); setFetchingRole(true);
try { try {
// Fetch and set the user's current role if they have one // Fetch and set the user's current role if they have one
const currentRole = await fetchUserRole(user.email); const currentRole = await fetchUserRole(user.email);
@ -186,7 +186,7 @@ export function UserManagement() {
try { try {
await userApi.assignRole(selectedUser.email, selectedRole); await userApi.assignRole(selectedUser.email, selectedRole);
setMessage({ setMessage({
type: 'success', type: 'success',
text: `Successfully assigned ${selectedRole} role to ${selectedUser.displayName || selectedUser.email}` text: `Successfully assigned ${selectedRole} role to ${selectedUser.displayName || selectedUser.email}`
@ -200,7 +200,7 @@ export function UserManagement() {
// Refresh the users list // Refresh the users list
await fetchUsers(); await fetchUsers();
await fetchRoleStatistics(); await fetchRoleStatistics();
toast.success(`Role assigned successfully`); toast.success(`Role assigned successfully`);
} catch (error: any) { } catch (error: any) {
console.error('Role assignment failed:', error); console.error('Role assignment failed:', error);
@ -220,7 +220,7 @@ export function UserManagement() {
setLoadingUsers(true); setLoadingUsers(true);
try { try {
const response = await userApi.getUsersByRole(roleFilter, page, limit); const response = await userApi.getUsersByRole(roleFilter, page, limit);
const usersData = response.data?.data?.users || []; const usersData = response.data?.data?.users || [];
const paginationData = response.data?.data?.pagination; const paginationData = response.data?.data?.pagination;
const summaryData = response.data?.data?.summary; const summaryData = response.data?.data?.summary;
@ -234,13 +234,13 @@ export function UserManagement() {
designation: u.designation, designation: u.designation,
isActive: u.isActive !== false // Default to true if not specified isActive: u.isActive !== false // Default to true if not specified
}))); })));
if (paginationData) { if (paginationData) {
setCurrentPage(paginationData.currentPage); setCurrentPage(paginationData.currentPage);
setTotalPages(paginationData.totalPages); setTotalPages(paginationData.totalPages);
setTotalUsers(paginationData.totalUsers); setTotalUsers(paginationData.totalUsers);
} }
// Update summary stats if available // Update summary stats if available
if (summaryData) { if (summaryData) {
setRoleStats(prev => ({ setRoleStats(prev => ({
@ -264,13 +264,13 @@ export function UserManagement() {
try { try {
const response = await userApi.getRoleStatistics(); const response = await userApi.getRoleStatistics();
const statsData = response.data?.data?.statistics || response.data?.statistics || []; const statsData = response.data?.data?.statistics || response.data?.statistics || [];
const stats = { const stats = {
admins: parseInt(statsData.find((s: any) => s.role === 'ADMIN')?.count || '0'), admins: parseInt(statsData.find((s: any) => s.role === 'ADMIN')?.count || '0'),
management: parseInt(statsData.find((s: any) => s.role === 'MANAGEMENT')?.count || '0'), management: parseInt(statsData.find((s: any) => s.role === 'MANAGEMENT')?.count || '0'),
users: parseInt(statsData.find((s: any) => s.role === 'USER')?.count || '0') users: parseInt(statsData.find((s: any) => s.role === 'USER')?.count || '0')
}; };
setRoleStats(prev => ({ setRoleStats(prev => ({
...prev, ...prev,
...stats, ...stats,
@ -317,8 +317,8 @@ export function UserManagement() {
const handleToggleUserStatus = async (userId: string) => { const handleToggleUserStatus = async (userId: string) => {
const user = users.find(u => u.userId === userId); const user = users.find(u => u.userId === userId);
if (!user) return; if (!user) return;
// TODO: Implement backend API for toggling user status
toast.info('User status toggle functionality coming soon'); toast.info('User status toggle functionality coming soon');
}; };
@ -326,13 +326,12 @@ export function UserManagement() {
const handleDeleteUser = async (userId: string) => { const handleDeleteUser = async (userId: string) => {
const user = users.find(u => u.userId === userId); const user = users.find(u => u.userId === userId);
if (!user) return; if (!user) return;
if (user.role === 'ADMIN') { if (user.role === 'ADMIN') {
toast.error('Cannot delete admin user'); toast.error('Cannot delete admin user');
return; return;
} }
// TODO: Implement backend API for deleting users
toast.info('User deletion functionality coming soon'); toast.info('User deletion functionality coming soon');
}; };
@ -515,11 +514,10 @@ export function UserManagement() {
{/* Message */} {/* Message */}
{message && ( {message && (
<div className={`border-2 rounded-lg p-4 ${ <div className={`border-2 rounded-lg p-4 ${message.type === 'success'
message.type === 'success' ? 'border-green-200 bg-green-50'
? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50'
: 'border-red-200 bg-red-50' }`}>
}`}>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
{message.type === 'success' ? ( {message.type === 'success' ? (
<CheckCircle className="w-5 h-5 text-green-600 shrink-0 mt-0.5" /> <CheckCircle className="w-5 h-5 text-green-600 shrink-0 mt-0.5" />
@ -602,7 +600,7 @@ export function UserManagement() {
</div> </div>
<p className="font-medium text-gray-700">No users found</p> <p className="font-medium text-gray-700">No users found</p>
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-gray-500 mt-1">
{roleFilter === 'ELEVATED' {roleFilter === 'ELEVATED'
? 'Assign ADMIN or MANAGEMENT roles to see users here' ? 'Assign ADMIN or MANAGEMENT roles to see users here'
: 'No users match the selected filter' : 'No users match the selected filter'
} }
@ -664,11 +662,10 @@ export function UserManagement() {
variant={currentPage === pageNum ? "default" : "outline"} variant={currentPage === pageNum ? "default" : "outline"}
size="sm" size="sm"
onClick={() => handlePageChange(pageNum)} onClick={() => handlePageChange(pageNum)}
className={`w-9 h-9 p-0 ${ className={`w-9 h-9 p-0 ${currentPage === pageNum
currentPage === pageNum ? 'bg-re-green hover:bg-re-green/90'
? 'bg-re-green hover:bg-re-green/90' : ''
: '' }`}
}`}
> >
{pageNum} {pageNum}
</Button> </Button>

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@ let configLoaded = false;
// Lazy initialization of configuration // Lazy initialization of configuration
async function ensureConfigLoaded() { async function ensureConfigLoaded() {
if (configLoaded) return; if (configLoaded) return;
try { try {
const config = await configService.getConfig(); const config = await configService.getConfig();
WORK_START_HOUR = config.workingHours.START_HOUR; WORK_START_HOUR = config.workingHours.START_HOUR;
@ -30,7 +30,7 @@ async function ensureConfigLoaded() {
} }
// Initialize config on first import (non-blocking) // Initialize config on first import (non-blocking)
ensureConfigLoaded().catch(() => {}); ensureConfigLoaded().catch(() => { });
/** /**
* Check if current time is within working hours * Check if current time is within working hours
@ -40,7 +40,7 @@ ensureConfigLoaded().catch(() => {});
export function isWorkingTime(date: Date = new Date(), priority: string = 'standard'): boolean { export function isWorkingTime(date: Date = new Date(), priority: string = 'standard'): boolean {
const day = date.getDay(); // 0 = Sunday, 6 = Saturday const day = date.getDay(); // 0 = Sunday, 6 = Saturday
const hour = date.getHours(); const hour = date.getHours();
// For standard priority: exclude weekends // For standard priority: exclude weekends
// For express priority: include weekends (calendar days) // For express priority: include weekends (calendar days)
if (priority === 'standard') { if (priority === 'standard') {
@ -48,14 +48,13 @@ export function isWorkingTime(date: Date = new Date(), priority: string = 'stand
return false; return false;
} }
} }
// Working hours check (applies to both priorities) // Working hours check (applies to both priorities)
if (hour < WORK_START_HOUR || hour >= WORK_END_HOUR) { if (hour < WORK_START_HOUR || hour >= WORK_END_HOUR) {
return false; return false;
} }
// TODO: Add holiday check if holiday API is available
return true; return true;
} }
@ -66,12 +65,12 @@ export function isWorkingTime(date: Date = new Date(), priority: string = 'stand
*/ */
export function getNextWorkingTime(date: Date = new Date(), priority: string = 'standard'): Date { export function getNextWorkingTime(date: Date = new Date(), priority: string = 'standard'): Date {
const result = new Date(date); const result = new Date(date);
// If already in working time, return as is // If already in working time, return as is
if (isWorkingTime(result, priority)) { if (isWorkingTime(result, priority)) {
return result; return result;
} }
// For standard priority: skip weekends // For standard priority: skip weekends
if (priority === 'standard') { if (priority === 'standard') {
const day = result.getDay(); const day = result.getDay();
@ -86,13 +85,13 @@ export function getNextWorkingTime(date: Date = new Date(), priority: string = '
return result; return result;
} }
} }
// If before work hours, move to work start // If before work hours, move to work start
if (result.getHours() < WORK_START_HOUR) { if (result.getHours() < WORK_START_HOUR) {
result.setHours(WORK_START_HOUR, 0, 0, 0); result.setHours(WORK_START_HOUR, 0, 0, 0);
return result; return result;
} }
// If after work hours, move to next day work start // If after work hours, move to next day work start
if (result.getHours() >= WORK_END_HOUR) { if (result.getHours() >= WORK_END_HOUR) {
result.setDate(result.getDate() + 1); result.setDate(result.getDate() + 1);
@ -100,7 +99,7 @@ export function getNextWorkingTime(date: Date = new Date(), priority: string = '
// Check if next day is weekend (only for standard priority) // Check if next day is weekend (only for standard priority)
return getNextWorkingTime(result, priority); return getNextWorkingTime(result, priority);
} }
return result; return result;
} }
@ -114,19 +113,19 @@ export function calculateElapsedWorkingHours(startDate: Date, endDate: Date = ne
let current = new Date(startDate); let current = new Date(startDate);
const end = new Date(endDate); const end = new Date(endDate);
let elapsedMinutes = 0; let elapsedMinutes = 0;
// Move minute by minute and count only working minutes // Move minute by minute and count only working minutes
while (current < end) { while (current < end) {
if (isWorkingTime(current, priority)) { if (isWorkingTime(current, priority)) {
elapsedMinutes++; elapsedMinutes++;
} }
current.setMinutes(current.getMinutes() + 1); current.setMinutes(current.getMinutes() + 1);
// Safety: stop if calculating more than 1 year // Safety: stop if calculating more than 1 year
const hoursSoFar = elapsedMinutes / 60; const hoursSoFar = elapsedMinutes / 60;
if (hoursSoFar > 8760) break; if (hoursSoFar > 8760) break;
} }
// Convert minutes to hours (with decimal precision) // Convert minutes to hours (with decimal precision)
return elapsedMinutes / 60; return elapsedMinutes / 60;
} }
@ -140,12 +139,12 @@ export function calculateElapsedWorkingHours(startDate: Date, endDate: Date = ne
export function calculateRemainingWorkingHours(deadline: Date, fromDate: Date = new Date(), priority: string = 'standard'): number { export function calculateRemainingWorkingHours(deadline: Date, fromDate: Date = new Date(), priority: string = 'standard'): number {
const deadlineTime = new Date(deadline).getTime(); const deadlineTime = new Date(deadline).getTime();
const currentTime = new Date(fromDate).getTime(); const currentTime = new Date(fromDate).getTime();
// If deadline has passed // If deadline has passed
if (deadlineTime <= currentTime) { if (deadlineTime <= currentTime) {
return 0; return 0;
} }
// Calculate remaining working hours // Calculate remaining working hours
return calculateElapsedWorkingHours(fromDate, deadline, priority); return calculateElapsedWorkingHours(fromDate, deadline, priority);
} }
@ -160,9 +159,9 @@ export function calculateRemainingWorkingHours(deadline: Date, fromDate: Date =
export function calculateSLAProgress(startDate: Date, deadline: Date, currentDate: Date = new Date(), priority: string = 'standard'): number { export function calculateSLAProgress(startDate: Date, deadline: Date, currentDate: Date = new Date(), priority: string = 'standard'): number {
const totalHours = calculateElapsedWorkingHours(startDate, deadline, priority); const totalHours = calculateElapsedWorkingHours(startDate, deadline, priority);
const elapsedHours = calculateElapsedWorkingHours(startDate, currentDate, priority); const elapsedHours = calculateElapsedWorkingHours(startDate, currentDate, priority);
if (totalHours === 0) return 0; if (totalHours === 0) return 0;
const progress = (elapsedHours / totalHours) * 100; const progress = (elapsedHours / totalHours) * 100;
return Math.min(Math.max(progress, 0), 100); // Clamp between 0 and 100 return Math.min(Math.max(progress, 0), 100); // Clamp between 0 and 100
} }
@ -185,17 +184,17 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date,
const start = new Date(startDate); const start = new Date(startDate);
const end = new Date(deadline); const end = new Date(deadline);
const now = new Date(); const now = new Date();
const isWorking = isWorkingTime(now, priority); const isWorking = isWorkingTime(now, priority);
const elapsedHours = calculateElapsedWorkingHours(start, now, priority); const elapsedHours = calculateElapsedWorkingHours(start, now, priority);
const totalHours = calculateElapsedWorkingHours(start, end, priority); const totalHours = calculateElapsedWorkingHours(start, end, priority);
const remainingHours = Math.max(0, totalHours - elapsedHours); const remainingHours = Math.max(0, totalHours - elapsedHours);
const progress = calculateSLAProgress(start, end, now, priority); const progress = calculateSLAProgress(start, end, now, priority);
let statusText = ''; let statusText = '';
if (!isWorking) { if (!isWorking) {
statusText = priority === 'express' statusText = priority === 'express'
? 'SLA tracking paused (outside working hours)' ? 'SLA tracking paused (outside working hours)'
: 'SLA tracking paused (outside working hours/days)'; : 'SLA tracking paused (outside working hours/days)';
} else if (remainingHours === 0) { } else if (remainingHours === 0) {
statusText = 'SLA deadline reached'; statusText = 'SLA deadline reached';
@ -208,7 +207,7 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date,
} else { } else {
statusText = 'On track'; statusText = 'On track';
} }
return { return {
isWorkingTime: isWorking, isWorkingTime: isWorking,
progress, progress,
@ -231,38 +230,38 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date,
export function formatHoursMinutes(hours: number | null | undefined): string { export function formatHoursMinutes(hours: number | null | undefined): string {
if (hours === null || hours === undefined || hours < 0) return '0 hours'; if (hours === null || hours === undefined || hours < 0) return '0 hours';
if (hours === 0) return '0 hours'; if (hours === 0) return '0 hours';
const WORKING_HOURS_PER_DAY = 8; const WORKING_HOURS_PER_DAY = 8;
// If less than 1 hour, show minutes only // If less than 1 hour, show minutes only
if (hours < 1) { if (hours < 1) {
const m = Math.round(hours * 60); const m = Math.round(hours * 60);
return m > 0 ? `${m}m` : '0 hours'; return m > 0 ? `${m}m` : '0 hours';
} }
// Calculate days and remaining hours (8 hours = 1 day) // Calculate days and remaining hours (8 hours = 1 day)
// Match backend formatTime logic from Re_Backend/src/utils/tatTimeUtils.ts // Match backend formatTime logic from Re_Backend/src/utils/tatTimeUtils.ts
const days = Math.floor(hours / WORKING_HOURS_PER_DAY); const days = Math.floor(hours / WORKING_HOURS_PER_DAY);
const remainingHrs = Math.floor(hours % WORKING_HOURS_PER_DAY); const remainingHrs = Math.floor(hours % WORKING_HOURS_PER_DAY);
const minutes = Math.round((hours % 1) * 60); const minutes = Math.round((hours % 1) * 60);
// If we have days, format with days (matching backend format) // If we have days, format with days (matching backend format)
if (days > 0) { if (days > 0) {
const dayLabel = days === 1 ? 'day' : 'days'; const dayLabel = days === 1 ? 'day' : 'days';
const hourLabel = remainingHrs === 1 ? 'hour' : 'hours'; const hourLabel = remainingHrs === 1 ? 'hour' : 'hours';
const minuteLabel = minutes === 1 ? 'min' : 'm'; const minuteLabel = minutes === 1 ? 'min' : 'm';
if (minutes > 0) { if (minutes > 0) {
return `${days} ${dayLabel} ${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`; return `${days} ${dayLabel} ${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`;
} else { } else {
return `${days} ${dayLabel} ${remainingHrs} ${hourLabel}`; return `${days} ${dayLabel} ${remainingHrs} ${hourLabel}`;
} }
} }
// No days, just hours and minutes // No days, just hours and minutes
const hourLabel = remainingHrs === 1 ? 'hour' : 'hours'; const hourLabel = remainingHrs === 1 ? 'hour' : 'hours';
const minuteLabel = minutes === 1 ? 'min' : 'm'; const minuteLabel = minutes === 1 ? 'min' : 'm';
if (minutes > 0) { if (minutes > 0) {
return `${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`; return `${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`;
} else { } else {
@ -276,13 +275,13 @@ export function formatHoursMinutes(hours: number | null | undefined): string {
export function formatWorkingHours(hours: number): string { export function formatWorkingHours(hours: number): string {
if (hours === 0) return '0h'; if (hours === 0) return '0h';
if (hours < 0) return '0h'; if (hours < 0) return '0h';
const totalMinutes = Math.round(hours * 60); const totalMinutes = Math.round(hours * 60);
const days = Math.floor(totalMinutes / (8 * 60)); // 8 working hours per day const days = Math.floor(totalMinutes / (8 * 60)); // 8 working hours per day
const remainingMinutes = totalMinutes % (8 * 60); const remainingMinutes = totalMinutes % (8 * 60);
const remainingHours = Math.floor(remainingMinutes / 60); const remainingHours = Math.floor(remainingMinutes / 60);
const minutes = remainingMinutes % 60; const minutes = remainingMinutes % 60;
if (days > 0 && remainingHours > 0 && minutes > 0) { if (days > 0 && remainingHours > 0 && minutes > 0) {
return `${days}d ${remainingHours}h ${minutes}m`; return `${days}d ${remainingHours}h ${minutes}m`;
} else if (days > 0 && remainingHours > 0) { } else if (days > 0 && remainingHours > 0) {
@ -306,14 +305,14 @@ export function getTimeUntilNextWorking(priority: string = 'standard'): string {
if (isWorkingTime(new Date(), priority)) { if (isWorkingTime(new Date(), priority)) {
return 'In working hours'; return 'In working hours';
} }
const now = new Date(); const now = new Date();
const next = getNextWorkingTime(now, priority); const next = getNextWorkingTime(now, priority);
const diff = next.getTime() - now.getTime(); const diff = next.getTime() - now.getTime();
const hours = Math.floor(diff / (1000 * 60 * 60)); const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
if (hours > 24) { if (hours > 24) {
const days = Math.floor(hours / 24); const days = Math.floor(hours / 24);
return `Resumes in ${days}d ${hours % 24}h`; return `Resumes in ${days}d ${hours % 24}h`;

View File

@ -57,14 +57,14 @@ export const cookieUtils = {
*/ */
clearAll(): void { clearAll(): void {
const cookieNames = [ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, ID_TOKEN_KEY, USER_DATA_KEY]; const cookieNames = [ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, ID_TOKEN_KEY, USER_DATA_KEY];
cookieNames.forEach(name => { cookieNames.forEach(name => {
// Remove with default path // Remove with default path
this.remove(name); this.remove(name);
// Remove with root path explicitly // Remove with root path explicitly
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
// Remove with domain (if applicable) // Remove with domain (if applicable)
const hostname = window.location.hostname; const hostname = window.location.hostname;
if (hostname !== 'localhost' && hostname !== '127.0.0.1') { if (hostname !== 'localhost' && hostname !== '127.0.0.1') {
@ -75,82 +75,60 @@ export const cookieUtils = {
}, },
}; };
/**
* Token Manager - Handles token storage and retrieval
*
* SECURITY MODES:
* - Production: Tokens stored in httpOnly cookies by backend only
* Frontend does NOT store access/refresh tokens anywhere
* All API requests rely on cookies being sent automatically
*
* - Development: Tokens stored in localStorage for debugging
* Needed because frontend/backend run on different ports
*/
export class TokenManager { export class TokenManager {
/** /**
* Store access token * Store access token
* In production: No-op (backend handles via httpOnly cookies)
* In development: Store in localStorage for Authorization header
*/ */
static setAccessToken(token: string): void { static setAccessToken(token: string): void {
// SECURITY: In production, don't store tokens client-side
// Backend sets httpOnly cookies that are sent automatically
if (isProduction()) { if (isProduction()) {
return; // No-op - rely on httpOnly cookies return; // No-op - rely on httpOnly cookies
} }
// Development only: Store for debugging and cross-port requests // Development only: Store for debugging and cross-port requests
localStorage.setItem(ACCESS_TOKEN_KEY, token); localStorage.setItem(ACCESS_TOKEN_KEY, token);
} }
/** /**
* Get access token * Get access token
* In production: Returns null (cookies are sent automatically) *
* In development: Returns from localStorage
*/ */
static getAccessToken(): string | null { static getAccessToken(): string | null {
// SECURITY: In production, return null - cookies are used instead
if (isProduction()) { if (isProduction()) {
return null; // API calls use cookies via withCredentials: true return null;
} }
// Development: Return from localStorage // Development: Return from localStorage
return localStorage.getItem(ACCESS_TOKEN_KEY); return localStorage.getItem(ACCESS_TOKEN_KEY);
} }
/** /**
* Store refresh token * Store refresh token
* In production: No-op (backend handles via httpOnly cookies)
* In development: Store in localStorage
*/ */
static setRefreshToken(token: string): void { static setRefreshToken(token: string): void {
// SECURITY: In production, don't store tokens client-side // SECURITY: In production, don't store tokens client-side
if (isProduction()) { if (isProduction()) {
return; // No-op - rely on httpOnly cookies return; // No-op - rely on httpOnly cookies
} }
// Development only // Development only
localStorage.setItem(REFRESH_TOKEN_KEY, token); localStorage.setItem(REFRESH_TOKEN_KEY, token);
} }
/** /**
* Get refresh token * Get refresh token
* In production: Returns null (cookies are used)
* In development: Returns from localStorage
*/ */
static getRefreshToken(): string | null { static getRefreshToken(): string | null {
// SECURITY: In production, return null - backend reads from cookie // SECURITY: In production, return null - backend reads from cookie
if (isProduction()) { if (isProduction()) {
return null; return null;
} }
return localStorage.getItem(REFRESH_TOKEN_KEY); return localStorage.getItem(REFRESH_TOKEN_KEY);
} }
/**
* Store ID token (from Okta) - needed for logout
* Stored in sessionStorage (cleared when tab closes)
*/
static setIdToken(token: string): void { static setIdToken(token: string): void {
// ID token is needed for Okta logout, use sessionStorage (more secure than localStorage) // ID token is needed for Okta logout, use sessionStorage (more secure than localStorage)
sessionStorage.setItem(ID_TOKEN_KEY, token); sessionStorage.setItem(ID_TOKEN_KEY, token);
@ -183,18 +161,7 @@ export class TokenManager {
} }
} }
/**
* Clear all tokens and user data
*
* PRODUCTION MODE:
* - Clears user data from localStorage
* - Clears ID token from sessionStorage
* - Backend logout endpoint clears httpOnly cookies
*
* DEVELOPMENT MODE:
* - Clears all localStorage and sessionStorage
* - Clears client-side cookies
*/
static clearAll(): void { static clearAll(): void {
// CRITICAL: Set logout flag in sessionStorage FIRST (before clearing) // CRITICAL: Set logout flag in sessionStorage FIRST (before clearing)
// This flag survives the redirect and prevents auto-authentication // This flag survives the redirect and prevents auto-authentication
@ -204,7 +171,7 @@ export class TokenManager {
} catch (e) { } catch (e) {
console.warn('Could not set logout flags:', e); console.warn('Could not set logout flags:', e);
} }
// Clear user data (stored in both modes) // Clear user data (stored in both modes)
try { try {
localStorage.removeItem(USER_DATA_KEY); localStorage.removeItem(USER_DATA_KEY);
@ -212,7 +179,7 @@ export class TokenManager {
} catch (e) { } catch (e) {
console.warn('Error clearing user data:', e); console.warn('Error clearing user data:', e);
} }
// In production, httpOnly cookies are cleared by backend // In production, httpOnly cookies are cleared by backend
// Only need to clear user data above // Only need to clear user data above
if (isProduction()) { if (isProduction()) {
@ -225,7 +192,7 @@ export class TokenManager {
} }
return; return;
} }
// DEVELOPMENT MODE: Clear everything // DEVELOPMENT MODE: Clear everything
const authKeys = [ const authKeys = [
ACCESS_TOKEN_KEY, ACCESS_TOKEN_KEY,
@ -246,7 +213,7 @@ export class TokenManager {
'persist:auth', 'persist:auth',
'redux-persist', 'redux-persist',
]; ];
authKeys.forEach(key => { authKeys.forEach(key => {
try { try {
localStorage.removeItem(key); localStorage.removeItem(key);
@ -255,14 +222,14 @@ export class TokenManager {
console.warn(`Error removing ${key}:`, e); console.warn(`Error removing ${key}:`, e);
} }
}); });
// Clear ALL localStorage // Clear ALL localStorage
try { try {
localStorage.clear(); localStorage.clear();
} catch (e) { } catch (e) {
console.error('Error clearing localStorage:', e); console.error('Error clearing localStorage:', e);
} }
// Clear ALL sessionStorage except logout flags // Clear ALL sessionStorage except logout flags
try { try {
const keysToKeep = ['__logout_in_progress__', '__force_logout__']; const keysToKeep = ['__logout_in_progress__', '__force_logout__'];
@ -277,7 +244,7 @@ export class TokenManager {
} catch (e) { } catch (e) {
console.error('Error clearing sessionStorage:', e); console.error('Error clearing sessionStorage:', e);
} }
// Clear client-side cookies (development only) // Clear client-side cookies (development only)
cookieUtils.clearAll(); cookieUtils.clearAll();
} }
@ -296,11 +263,7 @@ export class TokenManager {
return !!this.getAccessToken(); return !!this.getAccessToken();
} }
/**
* Check if refresh token exists
* In production: Always returns true if user data exists
* In development: Checks localStorage
*/
static hasRefreshToken(): boolean { static hasRefreshToken(): boolean {
if (isProduction()) { if (isProduction()) {
return !!this.getUserData(); return !!this.getUserData();
@ -318,7 +281,7 @@ export class TokenManager {
window.location.hostname === '' window.location.hostname === ''
); );
} }
/** /**
* Check if we're in production mode * Check if we're in production mode
*/ */