code sanitized removed mail refernces and url refernces ualong with that routes are secured
This commit is contained in:
parent
80ed407cd8
commit
2fa52b90e3
@ -27,7 +27,6 @@ import { CreateAdminRequest } from '@/pages/CreateAdminRequest/CreateAdminReques
|
|||||||
import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal';
|
import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase';
|
|
||||||
import { AuthCallback } from '@/pages/Auth/AuthCallback';
|
import { AuthCallback } from '@/pages/Auth/AuthCallback';
|
||||||
import { createClaimRequest } from '@/services/dealerClaimApi';
|
import { createClaimRequest } from '@/services/dealerClaimApi';
|
||||||
import { ManagerSelectionModal } from '@/components/modals/ManagerSelectionModal';
|
import { ManagerSelectionModal } from '@/components/modals/ManagerSelectionModal';
|
||||||
@ -193,7 +192,7 @@ function AppRoutes({ onLogout }: AppProps) {
|
|||||||
|
|
||||||
// Regular custom request submission (old flow without API)
|
// Regular custom request submission (old flow without API)
|
||||||
// Generate unique ID for the new custom request
|
// Generate unique ID for the new custom request
|
||||||
const requestId = `RE-REQ-2024-${String(Object.keys(CUSTOM_REQUEST_DATABASE).length + dynamicRequests.length + 1).padStart(3, '0')}`;
|
const requestId = `RE-REQ-2024-${String(dynamicRequests.length + 1).padStart(3, '0')}`;
|
||||||
|
|
||||||
// Create full custom request object
|
// Create full custom request object
|
||||||
const newCustomRequest = {
|
const newCustomRequest = {
|
||||||
@ -217,7 +216,7 @@ function AppRoutes({ onLogout }: AppProps) {
|
|||||||
name: 'Current User',
|
name: 'Current User',
|
||||||
role: requestData.initiatorRole || 'Employee',
|
role: requestData.initiatorRole || 'Employee',
|
||||||
department: requestData.department || 'General',
|
department: requestData.department || 'General',
|
||||||
email: 'current.user@royalenfield.com',
|
email: 'current.user@{{API_DOMAIN}}',
|
||||||
phone: '+91 98765 43290',
|
phone: '+91 98765 43290',
|
||||||
avatar: 'CU'
|
avatar: 'CU'
|
||||||
},
|
},
|
||||||
|
|||||||
@ -45,18 +45,18 @@ export function ApprovalWorkflowStep({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const approverCount = formData.approverCount || 1;
|
const approverCount = formData.approverCount || 1;
|
||||||
const currentApprovers = formData.approvers || [];
|
const currentApprovers = formData.approvers || [];
|
||||||
|
|
||||||
// Ensure we have the correct number of approvers
|
// Ensure we have the correct number of approvers
|
||||||
if (currentApprovers.length < approverCount) {
|
if (currentApprovers.length < approverCount) {
|
||||||
const newApprovers = [...currentApprovers];
|
const newApprovers = [...currentApprovers];
|
||||||
// Fill missing approver slots
|
// Fill missing approver slots
|
||||||
for (let i = currentApprovers.length; i < approverCount; i++) {
|
for (let i = currentApprovers.length; i < approverCount; i++) {
|
||||||
if (!newApprovers[i]) {
|
if (!newApprovers[i]) {
|
||||||
newApprovers[i] = {
|
newApprovers[i] = {
|
||||||
email: '',
|
email: '',
|
||||||
name: '',
|
name: '',
|
||||||
level: i + 1,
|
level: i + 1,
|
||||||
tat: '' as any
|
tat: '' as any
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,7 +71,7 @@ export function ApprovalWorkflowStep({
|
|||||||
const newApprovers = [...formData.approvers];
|
const newApprovers = [...formData.approvers];
|
||||||
const previousEmail = newApprovers[index]?.email;
|
const previousEmail = newApprovers[index]?.email;
|
||||||
const emailChanged = previousEmail !== value;
|
const emailChanged = previousEmail !== value;
|
||||||
|
|
||||||
newApprovers[index] = {
|
newApprovers[index] = {
|
||||||
...newApprovers[index],
|
...newApprovers[index],
|
||||||
email: value,
|
email: value,
|
||||||
@ -94,8 +94,8 @@ export function ApprovalWorkflowStep({
|
|||||||
try {
|
try {
|
||||||
// Check for duplicates in other approver slots (excluding current index)
|
// Check for duplicates in other approver slots (excluding current index)
|
||||||
const isDuplicateApprover = formData.approvers?.some(
|
const isDuplicateApprover = formData.approvers?.some(
|
||||||
(approver: any, idx: number) =>
|
(approver: any, idx: number) =>
|
||||||
idx !== index &&
|
idx !== index &&
|
||||||
(approver.userId === selectedUser.userId || approver.email?.toLowerCase() === selectedUser.email?.toLowerCase())
|
(approver.userId === selectedUser.userId || approver.email?.toLowerCase() === selectedUser.email?.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -196,9 +196,9 @@ export function ApprovalWorkflowStep({
|
|||||||
<div data-testid="approval-workflow-count-field">
|
<div data-testid="approval-workflow-count-field">
|
||||||
<Label className="text-base font-semibold mb-4 block">Number of Approvers *</Label>
|
<Label className="text-base font-semibold mb-4 block">Number of Approvers *</Label>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const currentCount = formData.approverCount || 1;
|
const currentCount = formData.approverCount || 1;
|
||||||
@ -216,14 +216,14 @@ export function ApprovalWorkflowStep({
|
|||||||
<span className="text-2xl font-semibold w-12 text-center" data-testid="approval-workflow-count-display">
|
<span className="text-2xl font-semibold w-12 text-center" data-testid="approval-workflow-count-display">
|
||||||
{formData.approverCount || 1}
|
{formData.approverCount || 1}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const currentCount = formData.approverCount || 1;
|
const currentCount = formData.approverCount || 1;
|
||||||
const newCount = currentCount + 1;
|
const newCount = currentCount + 1;
|
||||||
|
|
||||||
// Validate against system policy
|
// Validate against system policy
|
||||||
if (newCount > systemPolicy.maxApprovalLevels) {
|
if (newCount > systemPolicy.maxApprovalLevels) {
|
||||||
onPolicyViolation([{
|
onPolicyViolation([{
|
||||||
@ -234,7 +234,7 @@ export function ApprovalWorkflowStep({
|
|||||||
}]);
|
}]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFormData('approverCount', newCount);
|
updateFormData('approverCount', newCount);
|
||||||
}}
|
}}
|
||||||
disabled={(formData.approverCount || 1) >= systemPolicy.maxApprovalLevels}
|
disabled={(formData.approverCount || 1) >= systemPolicy.maxApprovalLevels}
|
||||||
@ -282,13 +282,13 @@ export function ApprovalWorkflowStep({
|
|||||||
{Array.from({ length: formData.approverCount || 1 }, (_, index) => {
|
{Array.from({ length: formData.approverCount || 1 }, (_, index) => {
|
||||||
const level = index + 1;
|
const level = index + 1;
|
||||||
const isLast = level === (formData.approverCount || 1);
|
const isLast = level === (formData.approverCount || 1);
|
||||||
|
|
||||||
// Ensure approver exists (should be initialized by useEffect, but provide fallback)
|
// Ensure approver exists (should be initialized by useEffect, but provide fallback)
|
||||||
const approver = formData.approvers[index] || {
|
const approver = formData.approvers[index] || {
|
||||||
email: '',
|
email: '',
|
||||||
name: '',
|
name: '',
|
||||||
level: level,
|
level: level,
|
||||||
tat: '' as any
|
tat: '' as any
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -296,18 +296,16 @@ export function ApprovalWorkflowStep({
|
|||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="w-px h-6 bg-gray-300"></div>
|
<div className="w-px h-6 bg-gray-300"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`p-4 rounded-lg border-2 transition-all ${
|
<div className={`p-4 rounded-lg border-2 transition-all ${approver.email
|
||||||
approver.email
|
? 'border-green-200 bg-green-50'
|
||||||
? 'border-green-200 bg-green-50'
|
|
||||||
: 'border-gray-200 bg-gray-50'
|
: 'border-gray-200 bg-gray-50'
|
||||||
}`}>
|
}`}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${approver.email
|
||||||
approver.email
|
? 'bg-green-600'
|
||||||
? 'bg-green-600'
|
|
||||||
: 'bg-gray-400'
|
: 'bg-gray-400'
|
||||||
}`}>
|
}`}>
|
||||||
<span className="text-white font-semibold">{level}</span>
|
<span className="text-white font-semibold">{level}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -336,7 +334,7 @@ export function ApprovalWorkflowStep({
|
|||||||
<Input
|
<Input
|
||||||
id={`approver-${level}`}
|
id={`approver-${level}`}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="approver@royalenfield.com"
|
placeholder="approver@{{API_DOMAIN}}"
|
||||||
value={approver.email || ''}
|
value={approver.email || ''}
|
||||||
onChange={(e) => handleApproverEmailChange(index, e.target.value)}
|
onChange={(e) => handleApproverEmailChange(index, e.target.value)}
|
||||||
className="h-10 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full"
|
className="h-10 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full"
|
||||||
|
|||||||
@ -73,15 +73,15 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// PRIORITY 1: Check for logout flags in sessionStorage (survives page reload during logout)
|
// PRIORITY 1: Check for logout flags in sessionStorage (survives page reload during logout)
|
||||||
const logoutFlag = sessionStorage.getItem('__logout_in_progress__');
|
const logoutFlag = sessionStorage.getItem('__logout_in_progress__');
|
||||||
const forceLogout = sessionStorage.getItem('__force_logout__');
|
const forceLogout = sessionStorage.getItem('__force_logout__');
|
||||||
|
|
||||||
if (logoutFlag === 'true' || forceLogout === 'true') {
|
if (logoutFlag === 'true' || forceLogout === 'true') {
|
||||||
// Remove flags
|
// Remove flags
|
||||||
sessionStorage.removeItem('__logout_in_progress__');
|
sessionStorage.removeItem('__logout_in_progress__');
|
||||||
sessionStorage.removeItem('__force_logout__');
|
sessionStorage.removeItem('__force_logout__');
|
||||||
|
|
||||||
// Clear all tokens one more time (aggressive)
|
// Clear all tokens one more time (aggressive)
|
||||||
TokenManager.clearAll();
|
TokenManager.clearAll();
|
||||||
|
|
||||||
// Also manually clear everything
|
// Also manually clear everything
|
||||||
try {
|
try {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
@ -89,16 +89,16 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error clearing storage:', e);
|
console.error('Error clearing storage:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set unauthenticated state
|
// Set unauthenticated state
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIORITY 2: Check if URL has logout parameter (from redirect)
|
// PRIORITY 2: Check if URL has logout parameter (from redirect)
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
if (urlParams.has('logout') || urlParams.has('okta_logged_out') || urlParams.has('tanflow_logged_out')) {
|
if (urlParams.has('logout') || urlParams.has('okta_logged_out') || urlParams.has('tanflow_logged_out')) {
|
||||||
@ -127,7 +127,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
window.history.replaceState({}, document.title, newUrl);
|
window.history.replaceState({}, document.title, newUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIORITY 3: Skip auth check if on callback page - let callback handler process first
|
// PRIORITY 3: Skip auth check if on callback page - let callback handler process first
|
||||||
// This is critical for production mode where we need to exchange code for tokens
|
// This is critical for production mode where we need to exchange code for tokens
|
||||||
// before we can verify session with server
|
// before we can verify session with server
|
||||||
@ -136,16 +136,16 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// The callback handler will set isAuthenticated after successful token exchange
|
// The callback handler will set isAuthenticated after successful token exchange
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIORITY 4: Check authentication status
|
// PRIORITY 4: Check authentication status
|
||||||
const token = TokenManager.getAccessToken();
|
const token = TokenManager.getAccessToken();
|
||||||
const refreshToken = TokenManager.getRefreshToken();
|
const refreshToken = TokenManager.getRefreshToken();
|
||||||
const userData = TokenManager.getUserData();
|
const userData = TokenManager.getUserData();
|
||||||
const hasAuthData = token || refreshToken || userData;
|
const hasAuthData = token || refreshToken || userData;
|
||||||
|
|
||||||
// Check if we're in production mode (tokens in httpOnly cookies, not accessible to JS)
|
// Check if we're in production mode (tokens in httpOnly cookies, not accessible to JS)
|
||||||
const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
||||||
|
|
||||||
// In production: Always verify with server (cookies are sent automatically)
|
// In production: Always verify with server (cookies are sent automatically)
|
||||||
// In development: Check local auth data first
|
// In development: Check local auth data first
|
||||||
if (isProductionMode) {
|
if (isProductionMode) {
|
||||||
@ -163,7 +163,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIORITY 5: Only check auth status if we have some auth data AND we're not logging out
|
// PRIORITY 5: Only check auth status if we have some auth data AND we're not logging out
|
||||||
if (!isLoggingOut) {
|
if (!isLoggingOut) {
|
||||||
checkAuthStatus();
|
checkAuthStatus();
|
||||||
@ -211,7 +211,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// Handle callback from OAuth redirect
|
// Handle callback from OAuth redirect
|
||||||
// Use ref to prevent duplicate calls (React StrictMode runs effects twice in dev)
|
// Use ref to prevent duplicate calls (React StrictMode runs effects twice in dev)
|
||||||
const callbackProcessedRef = useRef(false);
|
const callbackProcessedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Skip if already processed or not on callback page
|
// Skip if already processed or not on callback page
|
||||||
if (callbackProcessedRef.current || window.location.pathname !== '/login/callback') {
|
if (callbackProcessedRef.current || window.location.pathname !== '/login/callback') {
|
||||||
@ -220,7 +220,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const handleCallback = async () => {
|
const handleCallback = async () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
// Check if this is a logout redirect (from Tanflow post-logout redirect)
|
// Check if this is a logout redirect (from Tanflow post-logout redirect)
|
||||||
// If it has logout parameters but no code, it's a logout redirect, not a login callback
|
// If it has logout parameters but no code, it's a logout redirect, not a login callback
|
||||||
if ((urlParams.has('logout') || urlParams.has('tanflow_logged_out') || urlParams.has('okta_logged_out')) && !urlParams.get('code')) {
|
if ((urlParams.has('logout') || urlParams.has('tanflow_logged_out') || urlParams.has('okta_logged_out')) && !urlParams.get('code')) {
|
||||||
@ -236,10 +236,10 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
window.location.replace(redirectUrl);
|
window.location.replace(redirectUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as processed immediately to prevent duplicate calls
|
// Mark as processed immediately to prevent duplicate calls
|
||||||
callbackProcessedRef.current = true;
|
callbackProcessedRef.current = true;
|
||||||
|
|
||||||
const code = urlParams.get('code');
|
const code = urlParams.get('code');
|
||||||
const errorParam = urlParams.get('error');
|
const errorParam = urlParams.get('error');
|
||||||
|
|
||||||
@ -248,7 +248,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// Detect provider from sessionStorage
|
// Detect provider from sessionStorage
|
||||||
const authProvider = sessionStorage.getItem('auth_provider');
|
const authProvider = sessionStorage.getItem('auth_provider');
|
||||||
|
|
||||||
// If Tanflow provider, handle it separately (will be handled by TanflowCallback component)
|
// If Tanflow provider, handle it separately (will be handled by TanflowCallback component)
|
||||||
if (authProvider === 'tanflow') {
|
if (authProvider === 'tanflow') {
|
||||||
// Clear the provider flag and let TanflowCallback handle it
|
// Clear the provider flag and let TanflowCallback handle it
|
||||||
@ -277,21 +277,21 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// IMPORTANT: redirectUri must match the one used in initial Okta authorization request
|
// IMPORTANT: redirectUri must match the one used in initial Okta authorization request
|
||||||
// This is the frontend callback URL, NOT the backend URL
|
// This is the frontend callback URL, NOT the backend URL
|
||||||
// Backend will use this same URI when exchanging code with Okta
|
// Backend will use this same URI when exchanging code with Okta
|
||||||
const redirectUri = `${window.location.origin}/login/callback`;
|
const redirectUri = `${window.location.origin}/login/callback`;
|
||||||
|
|
||||||
const result = await exchangeCodeForTokens(code, redirectUri);
|
const result = await exchangeCodeForTokens(code, redirectUri);
|
||||||
|
|
||||||
setUser(result.user);
|
setUser(result.user);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Clear provider flag after successful authentication
|
// Clear provider flag after successful authentication
|
||||||
sessionStorage.removeItem('auth_provider');
|
sessionStorage.removeItem('auth_provider');
|
||||||
|
|
||||||
// Clean URL after success
|
// Clean URL after success
|
||||||
window.history.replaceState({}, document.title, '/');
|
window.history.replaceState({}, document.title, '/');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -317,17 +317,17 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// PRODUCTION MODE: Verify session via httpOnly cookie
|
// PRODUCTION MODE: Verify session via httpOnly cookie
|
||||||
// The cookie is sent automatically with the request (withCredentials: true)
|
// The cookie is sent automatically with the request (withCredentials: true)
|
||||||
if (isProductionMode) {
|
if (isProductionMode) {
|
||||||
const storedUser = TokenManager.getUserData();
|
const storedUser = TokenManager.getUserData();
|
||||||
|
|
||||||
// Try to get current user from server - this validates the httpOnly cookie
|
// Try to get current user from server - this validates the httpOnly cookie
|
||||||
try {
|
try {
|
||||||
const userData = await getCurrentUser();
|
const userData = await getCurrentUser();
|
||||||
@ -368,7 +368,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEVELOPMENT MODE: Check local token
|
// DEVELOPMENT MODE: Check local token
|
||||||
const token = TokenManager.getAccessToken();
|
const token = TokenManager.getAccessToken();
|
||||||
const storedUser = TokenManager.getUserData();
|
const storedUser = TokenManager.getUserData();
|
||||||
@ -454,7 +454,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
// Redirect to Okta login
|
// Redirect to Okta login
|
||||||
const oktaDomain = import.meta.env.VITE_OKTA_DOMAIN || 'https://dev-830839.oktapreview.com';
|
const oktaDomain = import.meta.env.VITE_OKTA_DOMAIN || '{{IDP_DOMAIN}}';
|
||||||
const clientId = import.meta.env.VITE_OKTA_CLIENT_ID || '0oa2jgzvrpdwx2iqd0h8';
|
const clientId = import.meta.env.VITE_OKTA_CLIENT_ID || '0oa2jgzvrpdwx2iqd0h8';
|
||||||
const redirectUri = `${window.location.origin}/login/callback`;
|
const redirectUri = `${window.location.origin}/login/callback`;
|
||||||
const responseType = 'code';
|
const responseType = 'code';
|
||||||
@ -467,14 +467,14 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// Check if we're coming from a logout - if so, add prompt=login to force re-authentication
|
// Check if we're coming from a logout - if so, add prompt=login to force re-authentication
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const isAfterLogout = urlParams.has('logout') || urlParams.has('okta_logged_out') || urlParams.has('tanflow_logged_out');
|
const isAfterLogout = urlParams.has('logout') || urlParams.has('okta_logged_out') || urlParams.has('tanflow_logged_out');
|
||||||
|
|
||||||
let authUrl = `${oktaDomain}/oauth2/default/v1/authorize?` +
|
let authUrl = `${oktaDomain}/oauth2/default/v1/authorize?` +
|
||||||
`client_id=${clientId}&` +
|
`client_id=${clientId}&` +
|
||||||
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
|
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
|
||||||
`response_type=${responseType}&` +
|
`response_type=${responseType}&` +
|
||||||
`scope=${encodeURIComponent(scope)}&` +
|
`scope=${encodeURIComponent(scope)}&` +
|
||||||
`state=${state}`;
|
`state=${state}`;
|
||||||
|
|
||||||
// Add prompt=login if coming from logout to force re-authentication
|
// Add prompt=login if coming from logout to force re-authentication
|
||||||
// This ensures Okta requires login even if a session still exists
|
// This ensures Okta requires login even if a session still exists
|
||||||
if (isAfterLogout) {
|
if (isAfterLogout) {
|
||||||
@ -493,25 +493,25 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// CRITICAL: Get id_token from TokenManager before clearing anything
|
// CRITICAL: Get id_token from TokenManager before clearing anything
|
||||||
// Needed for both Okta and Tanflow logout endpoints
|
// Needed for both Okta and Tanflow logout endpoints
|
||||||
const idToken = TokenManager.getIdToken();
|
const idToken = TokenManager.getIdToken();
|
||||||
|
|
||||||
// Detect which provider was used for login (check sessionStorage or user data)
|
// Detect which provider was used for login (check sessionStorage or user data)
|
||||||
// If auth_provider is set, use it; otherwise check if we have Tanflow id_token pattern
|
// If auth_provider is set, use it; otherwise check if we have Tanflow id_token pattern
|
||||||
const authProvider = sessionStorage.getItem('auth_provider') ||
|
const authProvider = sessionStorage.getItem('auth_provider') ||
|
||||||
(idToken && idToken.includes('tanflow') ? 'tanflow' : null) ||
|
(idToken && idToken.includes('tanflow') ? 'tanflow' : null) ||
|
||||||
'okta'; // Default to OKTA if unknown
|
'okta'; // Default to OKTA if unknown
|
||||||
|
|
||||||
// Set logout flag to prevent auto-authentication after redirect
|
// Set logout flag to prevent auto-authentication after redirect
|
||||||
// This must be set BEFORE clearing storage so it survives
|
// This must be set BEFORE clearing storage so it survives
|
||||||
sessionStorage.setItem('__logout_in_progress__', 'true');
|
sessionStorage.setItem('__logout_in_progress__', 'true');
|
||||||
sessionStorage.setItem('__force_logout__', 'true');
|
sessionStorage.setItem('__force_logout__', 'true');
|
||||||
setIsLoggingOut(true);
|
setIsLoggingOut(true);
|
||||||
|
|
||||||
// Reset auth state FIRST to prevent any re-authentication
|
// Reset auth state FIRST to prevent any re-authentication
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsLoading(true); // Set loading to prevent checkAuthStatus from running
|
setIsLoading(true); // Set loading to prevent checkAuthStatus from running
|
||||||
|
|
||||||
// Call backend logout API to clear server-side session and httpOnly cookies
|
// Call backend logout API to clear server-side session and httpOnly cookies
|
||||||
// IMPORTANT: This MUST be called before clearing local storage to clear httpOnly cookies
|
// IMPORTANT: This MUST be called before clearing local storage to clear httpOnly cookies
|
||||||
try {
|
try {
|
||||||
@ -522,17 +522,17 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
console.warn('🚪 Backend logout failed - httpOnly cookies may not be cleared');
|
console.warn('🚪 Backend logout failed - httpOnly cookies may not be cleared');
|
||||||
// Continue with logout even if API call fails
|
// Continue with logout even if API call fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve logout flags, id_token, and auth_provider BEFORE clearing tokens
|
// Preserve logout flags, id_token, and auth_provider BEFORE clearing tokens
|
||||||
const logoutInProgress = sessionStorage.getItem('__logout_in_progress__');
|
const logoutInProgress = sessionStorage.getItem('__logout_in_progress__');
|
||||||
const forceLogout = sessionStorage.getItem('__force_logout__');
|
const forceLogout = sessionStorage.getItem('__force_logout__');
|
||||||
const storedAuthProvider = sessionStorage.getItem('auth_provider');
|
const storedAuthProvider = sessionStorage.getItem('auth_provider');
|
||||||
|
|
||||||
// Clear all tokens EXCEPT id_token (we need it for provider logout)
|
// Clear all tokens EXCEPT id_token (we need it for provider logout)
|
||||||
// Note: We'll clear id_token after provider logout
|
// Note: We'll clear id_token after provider logout
|
||||||
// Clear tokens (but we'll restore id_token if needed)
|
// Clear tokens (but we'll restore id_token if needed)
|
||||||
TokenManager.clearAll();
|
TokenManager.clearAll();
|
||||||
|
|
||||||
// Restore logout flags and id_token immediately after clearAll
|
// Restore logout flags and id_token immediately after clearAll
|
||||||
if (logoutInProgress) sessionStorage.setItem('__logout_in_progress__', logoutInProgress);
|
if (logoutInProgress) sessionStorage.setItem('__logout_in_progress__', logoutInProgress);
|
||||||
if (forceLogout) sessionStorage.setItem('__force_logout__', forceLogout);
|
if (forceLogout) sessionStorage.setItem('__force_logout__', forceLogout);
|
||||||
@ -542,10 +542,10 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
if (storedAuthProvider) {
|
if (storedAuthProvider) {
|
||||||
sessionStorage.setItem('auth_provider', storedAuthProvider); // Restore for logout
|
sessionStorage.setItem('auth_provider', storedAuthProvider); // Restore for logout
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small delay to ensure sessionStorage is written before redirect
|
// Small delay to ensure sessionStorage is written before redirect
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
// Handle provider-specific logout
|
// Handle provider-specific logout
|
||||||
if (authProvider === 'tanflow' && idToken) {
|
if (authProvider === 'tanflow' && idToken) {
|
||||||
console.log('🚪 Initiating Tanflow logout...');
|
console.log('🚪 Initiating Tanflow logout...');
|
||||||
@ -560,7 +560,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// Fall through to default logout flow
|
// Fall through to default logout flow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OKTA logout or fallback: Clear auth_provider and redirect to login page with flags
|
// OKTA logout or fallback: Clear auth_provider and redirect to login page with flags
|
||||||
console.log('🚪 Using OKTA logout flow or fallback');
|
console.log('🚪 Using OKTA logout flow or fallback');
|
||||||
sessionStorage.removeItem('auth_provider');
|
sessionStorage.removeItem('auth_provider');
|
||||||
@ -590,7 +590,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const getAccessTokenSilently = async (): Promise<string | null> => {
|
const getAccessTokenSilently = async (): Promise<string | null> => {
|
||||||
const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
||||||
|
|
||||||
// In production mode, tokens are in httpOnly cookies
|
// In production mode, tokens are in httpOnly cookies
|
||||||
// We can't access them directly, but API calls will include them automatically
|
// We can't access them directly, but API calls will include them automatically
|
||||||
if (isProductionMode) {
|
if (isProductionMode) {
|
||||||
@ -599,7 +599,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
return 'cookie-based-auth'; // Placeholder - actual auth via cookies
|
return 'cookie-based-auth'; // Placeholder - actual auth via cookies
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to refresh the session
|
// Try to refresh the session
|
||||||
try {
|
try {
|
||||||
await refreshTokenSilently();
|
await refreshTokenSilently();
|
||||||
@ -608,7 +608,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Development mode: tokens in localStorage
|
// Development mode: tokens in localStorage
|
||||||
const token = TokenManager.getAccessToken();
|
const token = TokenManager.getAccessToken();
|
||||||
if (token && !isTokenExpired(token)) {
|
if (token && !isTokenExpired(token)) {
|
||||||
@ -626,17 +626,17 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const refreshTokenSilently = async (): Promise<void> => {
|
const refreshTokenSilently = async (): Promise<void> => {
|
||||||
const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
const isProductionMode = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newToken = await refreshAccessToken();
|
const newToken = await refreshAccessToken();
|
||||||
|
|
||||||
// In production, refresh might not return token (it's in httpOnly cookie)
|
// In production, refresh might not return token (it's in httpOnly cookie)
|
||||||
// but if the call succeeded, the session is valid
|
// but if the call succeeded, the session is valid
|
||||||
if (isProductionMode) {
|
if (isProductionMode) {
|
||||||
// Session refreshed via cookies
|
// Session refreshed via cookies
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newToken) {
|
if (newToken) {
|
||||||
// Token refreshed successfully (development mode)
|
// Token refreshed successfully (development mode)
|
||||||
return;
|
return;
|
||||||
@ -672,7 +672,7 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
|
|||||||
export function _Auth0AuthProvider({ children }: { children: ReactNode }) {
|
export function _Auth0AuthProvider({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Auth0Provider
|
<Auth0Provider
|
||||||
domain="https://dev-830839.oktapreview.com/oauth2/default/v1"
|
domain="{{IDP_DOMAIN}}/oauth2/default/v1"
|
||||||
clientId="0oa2j8slwj5S4bG5k0h8"
|
clientId="0oa2j8slwj5S4bG5k0h8"
|
||||||
authorizationParams={{
|
authorizationParams={{
|
||||||
redirect_uri: window.location.origin + '/login/callback',
|
redirect_uri: window.location.origin + '/login/callback',
|
||||||
|
|||||||
@ -141,7 +141,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
// Create new approver only if it doesn't exist
|
// Create new approver only if it doesn't exist
|
||||||
if (step.isAuto) {
|
if (step.isAuto) {
|
||||||
// System steps
|
// System steps
|
||||||
const systemEmail = step.level === 8 ? 'finance@royalenfield.com' : 'system@royalenfield.com';
|
const systemEmail = step.level === 8 ? 'finance@{{API_DOMAIN}}' : 'system@{{API_DOMAIN}}';
|
||||||
const systemName = step.level === 8 ? 'System/Finance' : 'System';
|
const systemName = step.level === 8 ? 'System/Finance' : 'System';
|
||||||
newApprovers.push({
|
newApprovers.push({
|
||||||
email: systemEmail,
|
email: systemEmail,
|
||||||
|
|||||||
@ -2519,7 +2519,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
stepNumber={selectedStepForEmail?.stepNumber || 4}
|
stepNumber={selectedStepForEmail?.stepNumber || 4}
|
||||||
stepName={selectedStepForEmail?.stepName || 'Activity Creation'}
|
stepName={selectedStepForEmail?.stepName || 'Activity Creation'}
|
||||||
requestNumber={request?.requestNumber || request?.id || request?.request_number}
|
requestNumber={request?.requestNumber || request?.id || request?.request_number}
|
||||||
recipientEmail="system@royalenfield.com"
|
recipientEmail="system@{{API_DOMAIN}}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Additional Approver Review Modal */}
|
{/* Additional Approver Review Modal */}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export function EmailNotificationTemplateModal({
|
|||||||
stepNumber,
|
stepNumber,
|
||||||
stepName,
|
stepName,
|
||||||
requestNumber = 'RE-REQ-2024-CM-101',
|
requestNumber = 'RE-REQ-2024-CM-101',
|
||||||
recipientEmail = 'system@royalenfield.com',
|
recipientEmail = 'system@{{API_DOMAIN}}',
|
||||||
subject,
|
subject,
|
||||||
emailBody,
|
emailBody,
|
||||||
}: EmailNotificationTemplateModalProps) {
|
}: EmailNotificationTemplateModalProps) {
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
import workflowApi, { getPauseDetails } from '@/services/workflowApi';
|
import workflowApi, { getPauseDetails } from '@/services/workflowApi';
|
||||||
import apiClient from '@/services/authApi';
|
import apiClient from '@/services/authApi';
|
||||||
import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase';
|
|
||||||
import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase';
|
|
||||||
import { getSocket } from '@/utils/socket';
|
import { getSocket } from '@/utils/socket';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,19 +28,19 @@ export function useRequestDetails(
|
|||||||
) {
|
) {
|
||||||
// State: Stores the fetched and transformed request data
|
// State: Stores the fetched and transformed request data
|
||||||
const [apiRequest, setApiRequest] = useState<any | null>(null);
|
const [apiRequest, setApiRequest] = useState<any | null>(null);
|
||||||
|
|
||||||
// State: Indicates if data is currently being fetched
|
// State: Indicates if data is currently being fetched
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
// State: Loading state for initial fetch
|
// State: Loading state for initial fetch
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// State: Access denied information
|
// State: Access denied information
|
||||||
const [accessDenied, setAccessDenied] = useState<{ denied: boolean; message: string } | null>(null);
|
const [accessDenied, setAccessDenied] = useState<{ denied: boolean; message: string } | null>(null);
|
||||||
|
|
||||||
// State: Stores the current approval level for the logged-in user
|
// State: Stores the current approval level for the logged-in user
|
||||||
const [currentApprovalLevel, setCurrentApprovalLevel] = useState<any | null>(null);
|
const [currentApprovalLevel, setCurrentApprovalLevel] = useState<any | null>(null);
|
||||||
|
|
||||||
// State: Indicates if the current user is a spectator (view-only access)
|
// State: Indicates if the current user is a spectator (view-only access)
|
||||||
const [isSpectator, setIsSpectator] = useState(false);
|
const [isSpectator, setIsSpectator] = useState(false);
|
||||||
|
|
||||||
@ -103,14 +101,14 @@ export function useRequestDetails(
|
|||||||
const documents = Array.isArray(details.documents) ? details.documents : [];
|
const documents = Array.isArray(details.documents) ? details.documents : [];
|
||||||
const summary = details.summary || {};
|
const summary = details.summary || {};
|
||||||
const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : [];
|
const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : [];
|
||||||
|
|
||||||
// Debug: Log TAT alerts for monitoring
|
// Debug: Log TAT alerts for monitoring
|
||||||
if (tatAlerts.length > 0) {
|
if (tatAlerts.length > 0) {
|
||||||
// TAT alerts loaded - logging removed
|
// TAT alerts loaded - logging removed
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLevel = summary?.currentLevel || wf.currentLevel || 1;
|
const currentLevel = summary?.currentLevel || wf.currentLevel || 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform: Map approval levels to UI format with TAT alerts
|
* Transform: Map approval levels to UI format with TAT alerts
|
||||||
* Each approval level includes:
|
* Each approval level includes:
|
||||||
@ -123,10 +121,10 @@ export function useRequestDetails(
|
|||||||
const levelNumber = a.levelNumber || 0;
|
const levelNumber = a.levelNumber || 0;
|
||||||
const levelStatus = (a.status || '').toString().toUpperCase();
|
const levelStatus = (a.status || '').toString().toUpperCase();
|
||||||
const levelId = a.levelId || a.level_id;
|
const levelId = a.levelId || a.level_id;
|
||||||
|
|
||||||
// Determine display status based on workflow progress
|
// Determine display status based on workflow progress
|
||||||
let displayStatus = statusMap(a.status);
|
let displayStatus = statusMap(a.status);
|
||||||
|
|
||||||
// Future levels that haven't been reached yet show as "waiting"
|
// Future levels that haven't been reached yet show as "waiting"
|
||||||
if (levelNumber > currentLevel && levelStatus !== 'APPROVED' && levelStatus !== 'REJECTED') {
|
if (levelNumber > currentLevel && levelStatus !== 'APPROVED' && levelStatus !== 'REJECTED') {
|
||||||
displayStatus = 'waiting';
|
displayStatus = 'waiting';
|
||||||
@ -135,10 +133,10 @@ export function useRequestDetails(
|
|||||||
else if (levelNumber === currentLevel && levelStatus === 'PENDING') {
|
else if (levelNumber === currentLevel && levelStatus === 'PENDING') {
|
||||||
displayStatus = 'pending';
|
displayStatus = 'pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter: Get TAT alerts that belong to this specific approval level
|
// Filter: Get TAT alerts that belong to this specific approval level
|
||||||
const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId);
|
const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: levelNumber,
|
step: levelNumber,
|
||||||
levelId,
|
levelId,
|
||||||
@ -152,8 +150,8 @@ export function useRequestDetails(
|
|||||||
remainingHours: Number(a.remainingHours || 0),
|
remainingHours: Number(a.remainingHours || 0),
|
||||||
tatPercentageUsed: Number(a.tatPercentageUsed || 0),
|
tatPercentageUsed: Number(a.tatPercentageUsed || 0),
|
||||||
// Calculate actual hours taken if level is completed
|
// Calculate actual hours taken if level is completed
|
||||||
actualHours: a.levelEndTime && a.levelStartTime
|
actualHours: a.levelEndTime && a.levelStartTime
|
||||||
? Math.max(0, (new Date(a.levelEndTime).getTime() - new Date(a.levelStartTime).getTime()) / (1000 * 60 * 60))
|
? Math.max(0, (new Date(a.levelEndTime).getTime() - new Date(a.levelStartTime).getTime()) / (1000 * 60 * 60))
|
||||||
: undefined,
|
: undefined,
|
||||||
comment: a.comments || undefined,
|
comment: a.comments || undefined,
|
||||||
timestamp: a.actionDate || undefined,
|
timestamp: a.actionDate || undefined,
|
||||||
@ -211,11 +209,11 @@ export function useRequestDetails(
|
|||||||
* Filter: Remove TAT breach activities from audit trail
|
* Filter: Remove TAT breach activities from audit trail
|
||||||
* TAT warnings are already shown in approval steps, no need to duplicate in timeline
|
* TAT warnings are already shown in approval steps, no need to duplicate in timeline
|
||||||
*/
|
*/
|
||||||
const filteredActivities = Array.isArray(details.activities)
|
const filteredActivities = Array.isArray(details.activities)
|
||||||
? details.activities.filter((activity: any) => {
|
? details.activities.filter((activity: any) => {
|
||||||
const activityType = (activity.type || '').toLowerCase();
|
const activityType = (activity.type || '').toLowerCase();
|
||||||
return activityType !== 'sla_warning';
|
return activityType !== 'sla_warning';
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -224,7 +222,7 @@ export function useRequestDetails(
|
|||||||
*/
|
*/
|
||||||
let pauseInfo = null;
|
let pauseInfo = null;
|
||||||
const isPaused = (wf as any).isPaused || false;
|
const isPaused = (wf as any).isPaused || false;
|
||||||
|
|
||||||
if (isPaused) {
|
if (isPaused) {
|
||||||
try {
|
try {
|
||||||
pauseInfo = await getPauseDetails(wf.requestId);
|
pauseInfo = await getPauseDetails(wf.requestId);
|
||||||
@ -240,13 +238,13 @@ export function useRequestDetails(
|
|||||||
let proposalDetails = null;
|
let proposalDetails = null;
|
||||||
let completionDetails = null;
|
let completionDetails = null;
|
||||||
let internalOrder = null;
|
let internalOrder = null;
|
||||||
|
|
||||||
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
|
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
|
||||||
try {
|
try {
|
||||||
const claimResponse = await apiClient.get(`/dealer-claims/${wf.requestId}`);
|
const claimResponse = await apiClient.get(`/dealer-claims/${wf.requestId}`);
|
||||||
|
|
||||||
const claimData = claimResponse.data?.data || claimResponse.data;
|
const claimData = claimResponse.data?.data || claimResponse.data;
|
||||||
|
|
||||||
if (claimData) {
|
if (claimData) {
|
||||||
claimDetails = claimData.claimDetails || claimData.claim_details;
|
claimDetails = claimData.claimDetails || claimData.claim_details;
|
||||||
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
proposalDetails = claimData.proposalDetails || claimData.proposal_details;
|
||||||
@ -257,7 +255,7 @@ export function useRequestDetails(
|
|||||||
const invoice = claimData.invoice || null;
|
const invoice = claimData.invoice || null;
|
||||||
const creditNote = claimData.creditNote || claimData.credit_note || null;
|
const creditNote = claimData.creditNote || claimData.credit_note || null;
|
||||||
const completionExpenses = claimData.completionExpenses || claimData.completion_expenses || null;
|
const completionExpenses = claimData.completionExpenses || claimData.completion_expenses || null;
|
||||||
|
|
||||||
// Store new fields in claimDetails for backward compatibility and easy access
|
// Store new fields in claimDetails for backward compatibility and easy access
|
||||||
if (claimDetails) {
|
if (claimDetails) {
|
||||||
(claimDetails as any).budgetTracking = budgetTracking;
|
(claimDetails as any).budgetTracking = budgetTracking;
|
||||||
@ -265,7 +263,7 @@ export function useRequestDetails(
|
|||||||
(claimDetails as any).creditNote = creditNote;
|
(claimDetails as any).creditNote = creditNote;
|
||||||
(claimDetails as any).completionExpenses = completionExpenses;
|
(claimDetails as any).completionExpenses = completionExpenses;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extracted details processed
|
// Extracted details processed
|
||||||
} else {
|
} else {
|
||||||
console.warn('[useRequestDetails] No claimData found in response');
|
console.warn('[useRequestDetails] No claimData found in response');
|
||||||
@ -334,7 +332,7 @@ export function useRequestDetails(
|
|||||||
creditNote: (claimDetails as any)?.creditNote || null,
|
creditNote: (claimDetails as any)?.creditNote || null,
|
||||||
completionExpenses: (claimDetails as any)?.completionExpenses || null,
|
completionExpenses: (claimDetails as any)?.completionExpenses || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setApiRequest(updatedRequest);
|
setApiRequest(updatedRequest);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -352,8 +350,8 @@ export function useRequestDetails(
|
|||||||
const approvalLevelNumber = a.levelNumber || 0;
|
const approvalLevelNumber = a.levelNumber || 0;
|
||||||
// Only show buttons if user is assigned to the CURRENT active level
|
// Only show buttons if user is assigned to the CURRENT active level
|
||||||
// Include PAUSED status - paused level is still the current level
|
// Include PAUSED status - paused level is still the current level
|
||||||
return (st === 'PENDING' || st === 'IN_PROGRESS' || st === 'PAUSED')
|
return (st === 'PENDING' || st === 'IN_PROGRESS' || st === 'PAUSED')
|
||||||
&& approverEmail === userEmail
|
&& approverEmail === userEmail
|
||||||
&& approvalLevelNumber === currentLevel;
|
&& approvalLevelNumber === currentLevel;
|
||||||
});
|
});
|
||||||
setCurrentApprovalLevel(newCurrentLevel || null);
|
setCurrentApprovalLevel(newCurrentLevel || null);
|
||||||
@ -364,8 +362,8 @@ export function useRequestDetails(
|
|||||||
*/
|
*/
|
||||||
const viewerId = (user as any)?.userId;
|
const viewerId = (user as any)?.userId;
|
||||||
if (viewerId) {
|
if (viewerId) {
|
||||||
const isSpec = participants.some((p: any) =>
|
const isSpec = participants.some((p: any) =>
|
||||||
(p.participantType || p.participant_type || '').toUpperCase() === 'SPECTATOR' &&
|
(p.participantType || p.participant_type || '').toUpperCase() === 'SPECTATOR' &&
|
||||||
(p.userId || p.user_id) === viewerId
|
(p.userId || p.user_id) === viewerId
|
||||||
);
|
);
|
||||||
setIsSpectator(isSpec);
|
setIsSpectator(isSpec);
|
||||||
@ -389,11 +387,11 @@ export function useRequestDetails(
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setAccessDenied(null);
|
setAccessDenied(null);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const details = await workflowApi.getWorkflowDetails(requestIdentifier);
|
const details = await workflowApi.getWorkflowDetails(requestIdentifier);
|
||||||
@ -401,7 +399,7 @@ export function useRequestDetails(
|
|||||||
if (mounted) setLoading(false);
|
if (mounted) setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the same transformation logic as refreshDetails
|
// Use the same transformation logic as refreshDetails
|
||||||
const wf = details.workflow || {};
|
const wf = details.workflow || {};
|
||||||
const approvals = Array.isArray(details.approvals) ? details.approvals : [];
|
const approvals = Array.isArray(details.approvals) ? details.approvals : [];
|
||||||
@ -409,7 +407,7 @@ export function useRequestDetails(
|
|||||||
const documents = Array.isArray(details.documents) ? details.documents : [];
|
const documents = Array.isArray(details.documents) ? details.documents : [];
|
||||||
const summary = details.summary || {};
|
const summary = details.summary || {};
|
||||||
const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : [];
|
const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : [];
|
||||||
|
|
||||||
// TAT alerts received - logging removed
|
// TAT alerts received - logging removed
|
||||||
|
|
||||||
const priority = (wf.priority || '').toString().toLowerCase();
|
const priority = (wf.priority || '').toString().toLowerCase();
|
||||||
@ -420,9 +418,9 @@ export function useRequestDetails(
|
|||||||
const levelNumber = a.levelNumber || 0;
|
const levelNumber = a.levelNumber || 0;
|
||||||
const levelStatus = (a.status || '').toString().toUpperCase();
|
const levelStatus = (a.status || '').toString().toUpperCase();
|
||||||
const levelId = a.levelId || a.level_id;
|
const levelId = a.levelId || a.level_id;
|
||||||
|
|
||||||
let displayStatus = statusMap(a.status);
|
let displayStatus = statusMap(a.status);
|
||||||
|
|
||||||
// If paused, show paused status (don't change it)
|
// If paused, show paused status (don't change it)
|
||||||
if (levelStatus === 'PAUSED') {
|
if (levelStatus === 'PAUSED') {
|
||||||
displayStatus = 'paused';
|
displayStatus = 'paused';
|
||||||
@ -431,9 +429,9 @@ export function useRequestDetails(
|
|||||||
} else if (levelNumber === currentLevel && (levelStatus === 'PENDING' || levelStatus === 'IN_PROGRESS')) {
|
} else if (levelNumber === currentLevel && (levelStatus === 'PENDING' || levelStatus === 'IN_PROGRESS')) {
|
||||||
displayStatus = levelStatus === 'IN_PROGRESS' ? 'in-review' : 'pending';
|
displayStatus = levelStatus === 'IN_PROGRESS' ? 'in-review' : 'pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId);
|
const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: levelNumber,
|
step: levelNumber,
|
||||||
levelId,
|
levelId,
|
||||||
@ -448,8 +446,8 @@ export function useRequestDetails(
|
|||||||
tatPercentageUsed: Number(a.tatPercentageUsed || 0),
|
tatPercentageUsed: Number(a.tatPercentageUsed || 0),
|
||||||
// Use backend-calculated elapsedHours (working hours) for completed approvals
|
// Use backend-calculated elapsedHours (working hours) for completed approvals
|
||||||
// Backend already calculates this correctly using calculateElapsedWorkingHours
|
// Backend already calculates this correctly using calculateElapsedWorkingHours
|
||||||
actualHours: a.elapsedHours !== undefined && a.elapsedHours !== null
|
actualHours: a.elapsedHours !== undefined && a.elapsedHours !== null
|
||||||
? Number(a.elapsedHours)
|
? Number(a.elapsedHours)
|
||||||
: undefined,
|
: undefined,
|
||||||
comment: a.comments || undefined,
|
comment: a.comments || undefined,
|
||||||
timestamp: a.actionDate || undefined,
|
timestamp: a.actionDate || undefined,
|
||||||
@ -457,7 +455,7 @@ export function useRequestDetails(
|
|||||||
tatAlerts: levelAlerts,
|
tatAlerts: levelAlerts,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map spectators
|
// Map spectators
|
||||||
const spectators = participants
|
const spectators = participants
|
||||||
.filter((p: any) => (p.participantType || '').toUpperCase() === 'SPECTATOR')
|
.filter((p: any) => (p.participantType || '').toUpperCase() === 'SPECTATOR')
|
||||||
@ -492,18 +490,18 @@ export function useRequestDetails(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Filter out TAT warnings from activities
|
// Filter out TAT warnings from activities
|
||||||
const filteredActivities = Array.isArray(details.activities)
|
const filteredActivities = Array.isArray(details.activities)
|
||||||
? details.activities.filter((activity: any) => {
|
? details.activities.filter((activity: any) => {
|
||||||
const activityType = (activity.type || '').toLowerCase();
|
const activityType = (activity.type || '').toLowerCase();
|
||||||
return activityType !== 'sla_warning';
|
return activityType !== 'sla_warning';
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Fetch pause details only if request is actually paused
|
// Fetch pause details only if request is actually paused
|
||||||
// Use request-level isPaused field from workflow response
|
// Use request-level isPaused field from workflow response
|
||||||
let pauseInfo = null;
|
let pauseInfo = null;
|
||||||
const isPaused = (wf as any).isPaused || false;
|
const isPaused = (wf as any).isPaused || false;
|
||||||
|
|
||||||
if (isPaused) {
|
if (isPaused) {
|
||||||
try {
|
try {
|
||||||
pauseInfo = await getPauseDetails(wf.requestId);
|
pauseInfo = await getPauseDetails(wf.requestId);
|
||||||
@ -519,11 +517,11 @@ export function useRequestDetails(
|
|||||||
let proposalDetails = null;
|
let proposalDetails = null;
|
||||||
let completionDetails = null;
|
let completionDetails = null;
|
||||||
let internalOrder = null;
|
let internalOrder = null;
|
||||||
|
|
||||||
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
|
if (wf.workflowType === 'CLAIM_MANAGEMENT' || wf.templateType === 'claim-management') {
|
||||||
try {
|
try {
|
||||||
const claimResponse = await apiClient.get(`/dealer-claims/${wf.requestId}`);
|
const claimResponse = await apiClient.get(`/dealer-claims/${wf.requestId}`);
|
||||||
|
|
||||||
const claimData = claimResponse.data?.data || claimResponse.data;
|
const claimData = claimResponse.data?.data || claimResponse.data;
|
||||||
if (claimData) {
|
if (claimData) {
|
||||||
claimDetails = claimData.claimDetails || claimData.claim_details;
|
claimDetails = claimData.claimDetails || claimData.claim_details;
|
||||||
@ -535,7 +533,7 @@ export function useRequestDetails(
|
|||||||
const invoice = claimData.invoice || null;
|
const invoice = claimData.invoice || null;
|
||||||
const creditNote = claimData.creditNote || claimData.credit_note || null;
|
const creditNote = claimData.creditNote || claimData.credit_note || null;
|
||||||
const completionExpenses = claimData.completionExpenses || claimData.completion_expenses || null;
|
const completionExpenses = claimData.completionExpenses || claimData.completion_expenses || null;
|
||||||
|
|
||||||
// Store new fields in claimDetails for backward compatibility and easy access
|
// Store new fields in claimDetails for backward compatibility and easy access
|
||||||
if (claimDetails) {
|
if (claimDetails) {
|
||||||
(claimDetails as any).budgetTracking = budgetTracking;
|
(claimDetails as any).budgetTracking = budgetTracking;
|
||||||
@ -543,7 +541,7 @@ export function useRequestDetails(
|
|||||||
(claimDetails as any).creditNote = creditNote;
|
(claimDetails as any).creditNote = creditNote;
|
||||||
(claimDetails as any).completionExpenses = completionExpenses;
|
(claimDetails as any).completionExpenses = completionExpenses;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial load - Extracted details processed
|
// Initial load - Extracted details processed
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -599,9 +597,9 @@ export function useRequestDetails(
|
|||||||
creditNote: (claimDetails as any)?.creditNote || null,
|
creditNote: (claimDetails as any)?.creditNote || null,
|
||||||
completionExpenses: (claimDetails as any)?.completionExpenses || null,
|
completionExpenses: (claimDetails as any)?.completionExpenses || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setApiRequest(mapped);
|
setApiRequest(mapped);
|
||||||
|
|
||||||
// Find current user's approval level
|
// Find current user's approval level
|
||||||
// Only show approve/reject buttons if user is the CURRENT active approver
|
// Only show approve/reject buttons if user is the CURRENT active approver
|
||||||
// Include PAUSED status - when paused, the paused level is still the current level
|
// Include PAUSED status - when paused, the paused level is still the current level
|
||||||
@ -612,8 +610,8 @@ export function useRequestDetails(
|
|||||||
const approvalLevelNumber = a.levelNumber || 0;
|
const approvalLevelNumber = a.levelNumber || 0;
|
||||||
// Only show buttons if user is assigned to the CURRENT active level
|
// Only show buttons if user is assigned to the CURRENT active level
|
||||||
// Include PAUSED status - paused level is still the current level
|
// Include PAUSED status - paused level is still the current level
|
||||||
return (status === 'PENDING' || status === 'IN_PROGRESS' || status === 'PAUSED')
|
return (status === 'PENDING' || status === 'IN_PROGRESS' || status === 'PAUSED')
|
||||||
&& approverEmail === userEmail
|
&& approverEmail === userEmail
|
||||||
&& approvalLevelNumber === currentLevel;
|
&& approvalLevelNumber === currentLevel;
|
||||||
});
|
});
|
||||||
setCurrentApprovalLevel(userCurrentLevel || null);
|
setCurrentApprovalLevel(userCurrentLevel || null);
|
||||||
@ -621,7 +619,7 @@ export function useRequestDetails(
|
|||||||
// Check spectator status
|
// Check spectator status
|
||||||
const viewerId = (user as any)?.userId;
|
const viewerId = (user as any)?.userId;
|
||||||
if (viewerId) {
|
if (viewerId) {
|
||||||
const isSpec = participants.some((p: any) =>
|
const isSpec = participants.some((p: any) =>
|
||||||
(p.participantType || '').toUpperCase() === 'SPECTATOR' && p.userId === viewerId
|
(p.participantType || '').toUpperCase() === 'SPECTATOR' && p.userId === viewerId
|
||||||
);
|
);
|
||||||
setIsSpectator(isSpec);
|
setIsSpectator(isSpec);
|
||||||
@ -633,7 +631,7 @@ export function useRequestDetails(
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
// Check for 403 Forbidden (Access Denied)
|
// Check for 403 Forbidden (Access Denied)
|
||||||
if (error?.response?.status === 403) {
|
if (error?.response?.status === 403) {
|
||||||
const message = error?.response?.data?.message ||
|
const message = error?.response?.data?.message ||
|
||||||
'You do not have permission to view this request. Access is restricted to the initiator, approvers, and spectators of this request.';
|
'You do not have permission to view this request. Access is restricted to the initiator, approvers, and spectators of this request.';
|
||||||
setAccessDenied({ denied: true, message });
|
setAccessDenied({ denied: true, message });
|
||||||
}
|
}
|
||||||
@ -645,7 +643,7 @@ export function useRequestDetails(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => { mounted = false; };
|
return () => { mounted = false; };
|
||||||
}, [requestIdentifier, user]);
|
}, [requestIdentifier, user]);
|
||||||
|
|
||||||
@ -656,23 +654,15 @@ export function useRequestDetails(
|
|||||||
const request = useMemo(() => {
|
const request = useMemo(() => {
|
||||||
// Primary source: API data
|
// Primary source: API data
|
||||||
if (apiRequest) return apiRequest;
|
if (apiRequest) return apiRequest;
|
||||||
|
|
||||||
// Fallback 1: Static custom request database
|
// Fallback: Dynamic requests passed as props
|
||||||
const customRequest = CUSTOM_REQUEST_DATABASE[requestIdentifier];
|
const dynamicRequest = dynamicRequests.find((req: any) =>
|
||||||
if (customRequest) return customRequest;
|
req.id === requestIdentifier ||
|
||||||
|
|
||||||
// Fallback 2: Static claim management database
|
|
||||||
const claimRequest = CLAIM_MANAGEMENT_DATABASE[requestIdentifier];
|
|
||||||
if (claimRequest) return claimRequest;
|
|
||||||
|
|
||||||
// Fallback 3: Dynamic requests passed as props
|
|
||||||
const dynamicRequest = dynamicRequests.find((req: any) =>
|
|
||||||
req.id === requestIdentifier ||
|
|
||||||
req.requestNumber === requestIdentifier ||
|
req.requestNumber === requestIdentifier ||
|
||||||
req.request_number === requestIdentifier
|
req.request_number === requestIdentifier
|
||||||
);
|
);
|
||||||
if (dynamicRequest) return dynamicRequest;
|
if (dynamicRequest) return dynamicRequest;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [requestIdentifier, dynamicRequests, apiRequest]);
|
}, [requestIdentifier, dynamicRequests, apiRequest]);
|
||||||
|
|
||||||
@ -693,9 +683,9 @@ export function useRequestDetails(
|
|||||||
*/
|
*/
|
||||||
const existingParticipants = useMemo(() => {
|
const existingParticipants = useMemo(() => {
|
||||||
if (!request) return [];
|
if (!request) return [];
|
||||||
|
|
||||||
const participants: Array<{ email: string; participantType: string; name?: string }> = [];
|
const participants: Array<{ email: string; participantType: string; name?: string }> = [];
|
||||||
|
|
||||||
// Add initiator
|
// Add initiator
|
||||||
if (request.initiator?.email) {
|
if (request.initiator?.email) {
|
||||||
participants.push({
|
participants.push({
|
||||||
@ -704,7 +694,7 @@ export function useRequestDetails(
|
|||||||
name: request.initiator.name
|
name: request.initiator.name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add approvers from approval flow
|
// Add approvers from approval flow
|
||||||
if (request.approvalFlow && Array.isArray(request.approvalFlow)) {
|
if (request.approvalFlow && Array.isArray(request.approvalFlow)) {
|
||||||
request.approvalFlow.forEach((approval: any) => {
|
request.approvalFlow.forEach((approval: any) => {
|
||||||
@ -717,7 +707,7 @@ export function useRequestDetails(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add spectators
|
// Add spectators
|
||||||
if (request.spectators && Array.isArray(request.spectators)) {
|
if (request.spectators && Array.isArray(request.spectators)) {
|
||||||
request.spectators.forEach((spectator: any) => {
|
request.spectators.forEach((spectator: any) => {
|
||||||
@ -730,20 +720,20 @@ export function useRequestDetails(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add from participants array
|
// Add from participants array
|
||||||
if (request.participants && Array.isArray(request.participants)) {
|
if (request.participants && Array.isArray(request.participants)) {
|
||||||
request.participants.forEach((p: any) => {
|
request.participants.forEach((p: any) => {
|
||||||
const email = (p.userEmail || p.email || '').toLowerCase();
|
const email = (p.userEmail || p.email || '').toLowerCase();
|
||||||
const participantType = (p.participantType || p.participant_type || '').toUpperCase();
|
const participantType = (p.participantType || p.participant_type || '').toUpperCase();
|
||||||
const name = p.userName || p.user_name || p.name;
|
const name = p.userName || p.user_name || p.name;
|
||||||
|
|
||||||
if (email && participantType && !participants.find(x => x.email === email)) {
|
if (email && participantType && !participants.find(x => x.email === email)) {
|
||||||
participants.push({ email, participantType, name });
|
participants.push({ email, participantType, name });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return participants;
|
return participants;
|
||||||
}, [request]);
|
}, [request]);
|
||||||
|
|
||||||
@ -762,12 +752,12 @@ export function useRequestDetails(
|
|||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!requestIdentifier || !apiRequest) return;
|
if (!requestIdentifier || !apiRequest) return;
|
||||||
|
|
||||||
const socket = getSocket();
|
const socket = getSocket();
|
||||||
if (!socket) {
|
if (!socket) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler: Request updated by another user
|
* Handler: Request updated by another user
|
||||||
* Silently refresh to show latest changes
|
* Silently refresh to show latest changes
|
||||||
@ -779,10 +769,10 @@ export function useRequestDetails(
|
|||||||
refreshDetails();
|
refreshDetails();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register listener
|
// Register listener
|
||||||
socket.on('request:updated', handleRequestUpdated);
|
socket.on('request:updated', handleRequestUpdated);
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
return () => {
|
return () => {
|
||||||
socket.off('request:updated', handleRequestUpdated);
|
socket.off('request:updated', handleRequestUpdated);
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
import { TokenManager } from '../utils/tokenManager';
|
import { TokenManager } from '../utils/tokenManager';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const TANFLOW_BASE_URL = import.meta.env.VITE_TANFLOW_BASE_URL || 'https://ssodev.rebridge.co.in/realms/RE';
|
const TANFLOW_BASE_URL = import.meta.env.VITE_TANFLOW_BASE_URL || '{{IDP_DOMAIN}}/realms/RE';
|
||||||
const TANFLOW_CLIENT_ID = import.meta.env.VITE_TANFLOW_CLIENT_ID || 'REFLOW';
|
const TANFLOW_CLIENT_ID = import.meta.env.VITE_TANFLOW_CLIENT_ID || 'REFLOW';
|
||||||
const TANFLOW_REDIRECT_URI = `${window.location.origin}/login/callback`;
|
const TANFLOW_REDIRECT_URI = `${window.location.origin}/login/callback`;
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ export function initiateTanflowLogin(): void {
|
|||||||
// Check if we're coming from a logout - if so, add prompt=login to force re-authentication
|
// Check if we're coming from a logout - if so, add prompt=login to force re-authentication
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const isAfterLogout = urlParams.has('logout') || urlParams.has('tanflow_logged_out');
|
const isAfterLogout = urlParams.has('logout') || urlParams.has('tanflow_logged_out');
|
||||||
|
|
||||||
// Clear any previous logout flags before starting new login
|
// Clear any previous logout flags before starting new login
|
||||||
if (isAfterLogout) {
|
if (isAfterLogout) {
|
||||||
sessionStorage.removeItem('tanflow_logged_out');
|
sessionStorage.removeItem('tanflow_logged_out');
|
||||||
@ -26,26 +26,26 @@ export function initiateTanflowLogin(): void {
|
|||||||
sessionStorage.removeItem('__force_logout__');
|
sessionStorage.removeItem('__force_logout__');
|
||||||
console.log('🚪 Cleared logout flags before initiating Tanflow login');
|
console.log('🚪 Cleared logout flags before initiating Tanflow login');
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = Math.random().toString(36).substring(7);
|
const state = Math.random().toString(36).substring(7);
|
||||||
// Store provider type and state to identify Tanflow callback
|
// Store provider type and state to identify Tanflow callback
|
||||||
sessionStorage.setItem('auth_provider', 'tanflow');
|
sessionStorage.setItem('auth_provider', 'tanflow');
|
||||||
sessionStorage.setItem('tanflow_auth_state', state);
|
sessionStorage.setItem('tanflow_auth_state', state);
|
||||||
|
|
||||||
let authUrl = `${TANFLOW_BASE_URL}/protocol/openid-connect/auth?` +
|
let authUrl = `${TANFLOW_BASE_URL}/protocol/openid-connect/auth?` +
|
||||||
`client_id=${TANFLOW_CLIENT_ID}&` +
|
`client_id=${TANFLOW_CLIENT_ID}&` +
|
||||||
`response_type=code&` +
|
`response_type=code&` +
|
||||||
`scope=openid&` +
|
`scope=openid&` +
|
||||||
`redirect_uri=${encodeURIComponent(TANFLOW_REDIRECT_URI)}&` +
|
`redirect_uri=${encodeURIComponent(TANFLOW_REDIRECT_URI)}&` +
|
||||||
`state=${state}`;
|
`state=${state}`;
|
||||||
|
|
||||||
// Add prompt=login if coming from logout to force re-authentication
|
// Add prompt=login if coming from logout to force re-authentication
|
||||||
// This ensures Tanflow requires login even if a session still exists
|
// This ensures Tanflow requires login even if a session still exists
|
||||||
if (isAfterLogout) {
|
if (isAfterLogout) {
|
||||||
authUrl += `&prompt=login`;
|
authUrl += `&prompt=login`;
|
||||||
console.log('🚪 Adding prompt=login to force re-authentication after logout');
|
console.log('🚪 Adding prompt=login to force re-authentication after logout');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🚪 Initiating Tanflow login', { isAfterLogout, hasPrompt: isAfterLogout });
|
console.log('🚪 Initiating Tanflow login', { isAfterLogout, hasPrompt: isAfterLogout });
|
||||||
window.location.href = authUrl;
|
window.location.href = authUrl;
|
||||||
}
|
}
|
||||||
@ -64,7 +64,7 @@ export async function exchangeTanflowCodeForTokens(
|
|||||||
user: any;
|
user: any;
|
||||||
}> {
|
}> {
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1';
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${API_BASE_URL}/auth/tanflow/token-exchange`,
|
`${API_BASE_URL}/auth/tanflow/token-exchange`,
|
||||||
@ -80,9 +80,9 @@ export async function exchangeTanflowCodeForTokens(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = response.data?.data || response.data;
|
const data = response.data?.data || response.data;
|
||||||
|
|
||||||
// Store tokens
|
// Store tokens
|
||||||
if (data.accessToken) {
|
if (data.accessToken) {
|
||||||
TokenManager.setAccessToken(data.accessToken);
|
TokenManager.setAccessToken(data.accessToken);
|
||||||
@ -96,7 +96,7 @@ export async function exchangeTanflowCodeForTokens(
|
|||||||
if (data.user) {
|
if (data.user) {
|
||||||
TokenManager.setUserData(data.user);
|
TokenManager.setUserData(data.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('❌ Tanflow token exchange failed:', {
|
console.error('❌ Tanflow token exchange failed:', {
|
||||||
@ -114,11 +114,11 @@ export async function exchangeTanflowCodeForTokens(
|
|||||||
export async function refreshTanflowToken(): Promise<string> {
|
export async function refreshTanflowToken(): Promise<string> {
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1';
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1';
|
||||||
const refreshToken = TokenManager.getRefreshToken();
|
const refreshToken = TokenManager.getRefreshToken();
|
||||||
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
throw new Error('No refresh token available');
|
throw new Error('No refresh token available');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${API_BASE_URL}/auth/tanflow/refresh`,
|
`${API_BASE_URL}/auth/tanflow/refresh`,
|
||||||
@ -130,15 +130,15 @@ export async function refreshTanflowToken(): Promise<string> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = response.data?.data || response.data;
|
const data = response.data?.data || response.data;
|
||||||
const accessToken = data.accessToken;
|
const accessToken = data.accessToken;
|
||||||
|
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
TokenManager.setAccessToken(accessToken);
|
TokenManager.setAccessToken(accessToken);
|
||||||
return accessToken;
|
return accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Failed to refresh token');
|
throw new Error('Failed to refresh token');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('❌ Tanflow token refresh failed:', error);
|
console.error('❌ Tanflow token refresh failed:', error);
|
||||||
@ -160,23 +160,23 @@ export function tanflowLogout(idToken: string): void {
|
|||||||
window.location.replace(homeUrl);
|
window.location.replace(homeUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build Tanflow logout URL with redirect back to login callback
|
// Build Tanflow logout URL with redirect back to login callback
|
||||||
// IMPORTANT: Use the base redirect URI (without query params) to match registered URIs
|
// IMPORTANT: Use the base redirect URI (without query params) to match registered URIs
|
||||||
// Tanflow requires exact match with registered "Valid Post Logout Redirect URIs"
|
// Tanflow requires exact match with registered "Valid Post Logout Redirect URIs"
|
||||||
// The same URI used for login should be registered for logout
|
// The same URI used for login should be registered for logout
|
||||||
// Using the base URI ensures it matches what's registered in Tanflow client config
|
// Using the base URI ensures it matches what's registered in Tanflow client config
|
||||||
const postLogoutRedirectUri = TANFLOW_REDIRECT_URI; // Use base URI without query params
|
const postLogoutRedirectUri = TANFLOW_REDIRECT_URI; // Use base URI without query params
|
||||||
|
|
||||||
// Construct logout URL - ensure all parameters are properly encoded
|
// Construct logout URL - ensure all parameters are properly encoded
|
||||||
// Keycloak/Tanflow expects: client_id, id_token_hint, post_logout_redirect_uri
|
// Keycloak/Tanflow expects: client_id, id_token_hint, post_logout_redirect_uri
|
||||||
const logoutUrl = new URL(`${TANFLOW_BASE_URL}/protocol/openid-connect/logout`);
|
const logoutUrl = new URL(`${TANFLOW_BASE_URL}/protocol/openid-connect/logout`);
|
||||||
logoutUrl.searchParams.set('client_id', TANFLOW_CLIENT_ID);
|
logoutUrl.searchParams.set('client_id', TANFLOW_CLIENT_ID);
|
||||||
logoutUrl.searchParams.set('id_token_hint', idToken);
|
logoutUrl.searchParams.set('id_token_hint', idToken);
|
||||||
logoutUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri);
|
logoutUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri);
|
||||||
|
|
||||||
const finalLogoutUrl = logoutUrl.toString();
|
const finalLogoutUrl = logoutUrl.toString();
|
||||||
|
|
||||||
console.log('🚪 Tanflow logout initiated', {
|
console.log('🚪 Tanflow logout initiated', {
|
||||||
hasIdToken: !!idToken,
|
hasIdToken: !!idToken,
|
||||||
idTokenPrefix: idToken ? idToken.substring(0, 20) + '...' : 'none',
|
idTokenPrefix: idToken ? idToken.substring(0, 20) + '...' : 'none',
|
||||||
@ -184,14 +184,14 @@ export function tanflowLogout(idToken: string): void {
|
|||||||
logoutUrlBase: `${TANFLOW_BASE_URL}/protocol/openid-connect/logout`,
|
logoutUrlBase: `${TANFLOW_BASE_URL}/protocol/openid-connect/logout`,
|
||||||
finalLogoutUrl: finalLogoutUrl.replace(idToken.substring(0, 20), '***'),
|
finalLogoutUrl: finalLogoutUrl.replace(idToken.substring(0, 20), '***'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// DO NOT clear auth_provider here - we need it to detect Tanflow callback
|
// DO NOT clear auth_provider here - we need it to detect Tanflow callback
|
||||||
// The logout flags should already be set by AuthContext
|
// The logout flags should already be set by AuthContext
|
||||||
// Just ensure they're there
|
// Just ensure they're there
|
||||||
sessionStorage.setItem('__logout_in_progress__', 'true');
|
sessionStorage.setItem('__logout_in_progress__', 'true');
|
||||||
sessionStorage.setItem('__force_logout__', 'true');
|
sessionStorage.setItem('__force_logout__', 'true');
|
||||||
// Don't set tanflow_logged_out here - it will be set when Tanflow redirects back
|
// Don't set tanflow_logged_out here - it will be set when Tanflow redirects back
|
||||||
|
|
||||||
// Redirect to Tanflow logout endpoint
|
// Redirect to Tanflow logout endpoint
|
||||||
// Tanflow will clear the session and redirect back to post_logout_redirect_uri
|
// Tanflow will clear the session and redirect back to post_logout_redirect_uri
|
||||||
// The redirect will include tanflow_logged_out=true in the query params
|
// The redirect will include tanflow_logged_out=true in the query params
|
||||||
|
|||||||
@ -2,166 +2,7 @@
|
|||||||
// This database is exclusively for claim management requests created via ClaimManagementWizard
|
// This database is exclusively for claim management requests created via ClaimManagementWizard
|
||||||
// Template: Claim Management (8-step workflow)
|
// Template: Claim Management (8-step workflow)
|
||||||
|
|
||||||
export const CLAIM_MANAGEMENT_DATABASE: any = {
|
export const CLAIM_MANAGEMENT_DATABASE: any = {};
|
||||||
'RE-REQ-2024-CM-001': {
|
|
||||||
id: 'RE-REQ-2024-CM-001',
|
|
||||||
title: 'Dealer Marketing Activity Claim - Diwali Festival Campaign',
|
|
||||||
description: 'Claim request for dealer-led Diwali festival marketing campaign including showroom decoration, test ride events, customer engagement activities, and promotional merchandise distribution. Activity conducted at Royal Motors Mumbai dealership.',
|
|
||||||
category: 'Dealer Operations',
|
|
||||||
subcategory: 'Claim Management',
|
|
||||||
status: 'pending',
|
|
||||||
priority: 'standard',
|
|
||||||
amount: 'TBD',
|
|
||||||
slaProgress: 35,
|
|
||||||
slaRemaining: '4 days 12 hours',
|
|
||||||
slaEndDate: 'Oct 16, 2024 5:00 PM',
|
|
||||||
currentStep: 1,
|
|
||||||
totalSteps: 8,
|
|
||||||
template: 'claim-management',
|
|
||||||
templateName: 'Claim Management',
|
|
||||||
initiator: {
|
|
||||||
name: 'Sneha Patil',
|
|
||||||
role: 'Regional Marketing Coordinator',
|
|
||||||
department: 'Marketing - West Zone',
|
|
||||||
email: 'sneha.patil@royalenfield.com',
|
|
||||||
phone: '+91 98765 43250',
|
|
||||||
avatar: 'SP'
|
|
||||||
},
|
|
||||||
department: 'Marketing - West Zone',
|
|
||||||
createdAt: 'Oct 7, 2024 9:30 AM',
|
|
||||||
updatedAt: 'Oct 7, 2024 9:30 AM',
|
|
||||||
dueDate: '2024-10-16T17:00:00Z',
|
|
||||||
conclusionRemark: '',
|
|
||||||
claimDetails: {
|
|
||||||
activityName: 'Diwali Festival Campaign 2024',
|
|
||||||
activityType: 'Marketing Activity',
|
|
||||||
activityDate: 'Oct 5, 2024',
|
|
||||||
location: 'Mumbai, Maharashtra',
|
|
||||||
dealerCode: 'RE-MH-001',
|
|
||||||
dealerName: 'Royal Motors Mumbai',
|
|
||||||
dealerEmail: 'dealer@royalmotorsmumbai.com',
|
|
||||||
dealerPhone: '+91 98765 12345',
|
|
||||||
dealerAddress: '123 Main Street, Andheri West, Mumbai, Maharashtra 400053',
|
|
||||||
requestDescription: 'Marketing campaign for Diwali festival including showroom decoration, test ride events, customer engagement activities, and promotional merchandise distribution at Royal Motors Mumbai dealership.',
|
|
||||||
estimatedBudget: '₹2,45,000',
|
|
||||||
periodStart: 'Oct 1, 2024',
|
|
||||||
periodEnd: 'Oct 10, 2024'
|
|
||||||
},
|
|
||||||
approvalFlow: [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
approver: 'Royal Motors Mumbai (Dealer)',
|
|
||||||
role: 'Dealer - Document Upload',
|
|
||||||
status: 'pending',
|
|
||||||
tatHours: 72,
|
|
||||||
elapsedHours: 12,
|
|
||||||
assignedAt: '2024-10-07T09:30:00Z',
|
|
||||||
comment: null,
|
|
||||||
timestamp: null,
|
|
||||||
description: 'Dealer uploads proposal document, cost breakup, timeline for closure, and other supporting documents'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
approver: 'Sneha Patil (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: 'Royal Motors Mumbai (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: 'Sneha Patil (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: 'Meera Patel',
|
|
||||||
role: 'Finance - Credit Note Issuance',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 48,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null,
|
|
||||||
description: 'Finance team issues credit note to dealer'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{ name: 'Claim_Proposal_Diwali_2024.pdf', size: '1.8 MB', type: 'PDF', uploadedBy: 'Sneha Patil', uploadedAt: 'Oct 7, 2024 9:35 AM' },
|
|
||||||
{ name: 'Cost_Breakup_Detailed.xlsx', size: '450 KB', type: 'Excel', uploadedBy: 'Sneha Patil', uploadedAt: 'Oct 7, 2024 9:38 AM' },
|
|
||||||
{ name: 'Activity_Timeline.pdf', size: '320 KB', type: 'PDF', uploadedBy: 'Sneha Patil', uploadedAt: 'Oct 7, 2024 9:40 AM' }
|
|
||||||
],
|
|
||||||
spectators: [
|
|
||||||
{ name: 'Arjun Menon', role: 'Brand Manager', avatar: 'AM' },
|
|
||||||
{ name: 'Finance Team', role: 'Budget Monitoring', avatar: 'FT' }
|
|
||||||
],
|
|
||||||
auditTrail: [
|
|
||||||
{ type: 'created', action: 'Claim Request Created', details: 'Diwali festival campaign claim initiated using Claim Management template', user: 'Sneha Patil', timestamp: 'Oct 7, 2024 9:30 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Dealer', details: 'Dealer Royal Motors Mumbai assigned for document upload', user: 'System', timestamp: 'Oct 7, 2024 9:31 AM' },
|
|
||||||
{ type: 'status_change', action: 'Workflow Started', details: 'Step 1: Dealer document upload phase initiated', user: 'System', timestamp: 'Oct 7, 2024 9:31 AM' }
|
|
||||||
],
|
|
||||||
tags: ['claim-management', 'dealer-activity', 'marketing', 'diwali-campaign', 'template']
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// API Endpoints for Claim Management (to be implemented with backend)
|
// API Endpoints for Claim Management (to be implemented with backend)
|
||||||
export const CLAIM_MANAGEMENT_API_ENDPOINTS = {
|
export const CLAIM_MANAGEMENT_API_ENDPOINTS = {
|
||||||
|
|||||||
@ -2,720 +2,7 @@
|
|||||||
// This database is exclusively for custom requests created via NewRequestWizard
|
// This database is exclusively for custom requests created via NewRequestWizard
|
||||||
// Users define their own workflow, approvers, spectators, and tagged participants
|
// Users define their own workflow, approvers, spectators, and tagged participants
|
||||||
|
|
||||||
export const CUSTOM_REQUEST_DATABASE: any = {
|
export const CUSTOM_REQUEST_DATABASE: any = {};
|
||||||
'RE-REQ-2024-001': {
|
|
||||||
id: 'RE-REQ-2024-001',
|
|
||||||
title: 'Himalayan 450 Launch Campaign - Digital Media Blitz',
|
|
||||||
description: 'Comprehensive digital marketing campaign for Himalayan 450 adventure motorcycle launch. Includes social media campaigns, influencer partnerships, performance marketing, content creation, and digital advertising across platforms. Target: Reach 10M adventure enthusiasts across India.\n\nEquipment Specifications:\n• 10x MacBook Pro 16-inch (M2 Pro chip)\n• 5x Professional Camera Kits (Canon EOS R5)\n• Video Editing Workstations\n• Social Media Management Tools',
|
|
||||||
category: 'Marketing & Campaigns',
|
|
||||||
subcategory: 'Digital Marketing',
|
|
||||||
status: 'pending',
|
|
||||||
priority: 'express',
|
|
||||||
amount: '₹3,75,00,000',
|
|
||||||
slaProgress: 65,
|
|
||||||
slaRemaining: '8 hours 45 minutes',
|
|
||||||
slaEndDate: 'Oct 9, 2024 5:00 PM',
|
|
||||||
currentStep: 1,
|
|
||||||
totalSteps: 3,
|
|
||||||
template: 'custom',
|
|
||||||
initiator: {
|
|
||||||
name: 'Priya Sharma',
|
|
||||||
role: 'Senior Digital Marketing Manager',
|
|
||||||
department: 'Marketing',
|
|
||||||
email: 'priya.sharma@royalenfield.com',
|
|
||||||
phone: '+91 98765 43210',
|
|
||||||
avatar: 'PS'
|
|
||||||
},
|
|
||||||
department: 'Marketing',
|
|
||||||
createdAt: 'Oct 6, 2024 10:30 AM',
|
|
||||||
updatedAt: 'Oct 7, 2024 2:15 PM',
|
|
||||||
dueDate: '2024-10-09T17:00:00Z',
|
|
||||||
conclusionRemark: '',
|
|
||||||
approvalFlow: [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
approver: 'Rajesh Kumar',
|
|
||||||
role: 'Marketing Director - India',
|
|
||||||
status: 'pending',
|
|
||||||
tatHours: 24,
|
|
||||||
elapsedHours: 22,
|
|
||||||
assignedAt: '2024-10-06T10:30:00Z',
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
approver: 'Amit Desai',
|
|
||||||
role: 'VP Product Marketing',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 48,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
approver: 'Deepika Sharma',
|
|
||||||
role: 'VP Sales & Marketing',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 72,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{ name: 'Himalayan_450_Digital_Strategy.pdf', size: '5.2 MB', type: 'PDF', uploadedBy: 'Priya Sharma', uploadedAt: 'Oct 6, 2024 10:45 AM' },
|
|
||||||
{ name: 'Budget_Breakdown_Q4_2024.xlsx', size: '980 KB', type: 'Excel', uploadedBy: 'Priya Sharma', uploadedAt: 'Oct 6, 2024 11:15 AM' },
|
|
||||||
{ name: 'Influencer_Partnership_List.xlsx', size: '450 KB', type: 'Excel', uploadedBy: 'Marketing Team', uploadedAt: 'Oct 6, 2024 2:30 PM' },
|
|
||||||
{ name: 'Creative_Campaign_Assets.zip', size: '125 MB', type: 'ZIP', uploadedBy: 'Creative Team', uploadedAt: 'Oct 6, 2024 4:15 PM' }
|
|
||||||
],
|
|
||||||
spectators: [
|
|
||||||
{ name: 'Sarah Khan', role: 'Brand Strategy Lead', avatar: 'SK' },
|
|
||||||
{ name: 'Finance Team', role: 'Budget Approval', avatar: 'FT' }
|
|
||||||
],
|
|
||||||
auditTrail: [
|
|
||||||
{ type: 'created', action: 'Request Created', details: 'New digital marketing campaign request submitted', user: 'Priya Sharma', timestamp: 'Oct 6, 2024 10:30 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Rajesh Kumar', details: 'Forwarded to Marketing Director for review', user: 'System', timestamp: 'Oct 6, 2024 10:31 AM' },
|
|
||||||
{ type: 'comment', action: 'Work Note Added', details: 'Reviewed budget allocation and target metrics', user: 'Rajesh Kumar', timestamp: 'Oct 7, 2024 2:15 PM' },
|
|
||||||
{ type: 'reminder', action: 'SLA Reminder', details: 'TAT approaching - 8 hours remaining', user: 'System', timestamp: 'Oct 7, 2024 8:15 AM' }
|
|
||||||
],
|
|
||||||
tags: ['digital-marketing', 'launch-campaign', 'himalayan-450', 'high-priority']
|
|
||||||
},
|
|
||||||
|
|
||||||
'RE-REQ-2024-002': {
|
|
||||||
id: 'RE-REQ-2024-002',
|
|
||||||
title: 'New Laptop Procurement - Design Team Expansion',
|
|
||||||
description: 'Purchase of 10 high-performance laptops for the newly expanded Product Design team. Required specifications: Latest generation processor, 32GB RAM, dedicated graphics card for 3D modeling and rendering work.',
|
|
||||||
category: 'IT & Infrastructure',
|
|
||||||
subcategory: 'Hardware Procurement',
|
|
||||||
status: 'in-review',
|
|
||||||
priority: 'standard',
|
|
||||||
amount: '₹12,50,000',
|
|
||||||
slaProgress: 45,
|
|
||||||
slaRemaining: '2 days 8 hours',
|
|
||||||
slaEndDate: 'Oct 11, 2024 5:00 PM',
|
|
||||||
currentStep: 2,
|
|
||||||
totalSteps: 3,
|
|
||||||
template: 'custom',
|
|
||||||
initiator: {
|
|
||||||
name: 'Vikram Singh',
|
|
||||||
role: 'Head - IT Operations',
|
|
||||||
department: 'Information Technology',
|
|
||||||
email: 'vikram.singh@royalenfield.com',
|
|
||||||
phone: '+91 98765 43221',
|
|
||||||
avatar: 'VS'
|
|
||||||
},
|
|
||||||
department: 'Information Technology',
|
|
||||||
createdAt: 'Oct 5, 2024 9:15 AM',
|
|
||||||
updatedAt: 'Oct 7, 2024 3:45 PM',
|
|
||||||
dueDate: '2024-10-11T17:00:00Z',
|
|
||||||
conclusionRemark: '',
|
|
||||||
approvalFlow: [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
approver: 'Meera Patel',
|
|
||||||
role: 'IT Manager',
|
|
||||||
status: 'approved',
|
|
||||||
tatHours: 24,
|
|
||||||
actualHours: 18,
|
|
||||||
assignedAt: '2024-10-05T09:15:00Z',
|
|
||||||
comment: 'Technical specifications verified. Hardware meets design team requirements.',
|
|
||||||
timestamp: '2024-10-06T03:15:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
approver: 'Anil Kapoor',
|
|
||||||
role: 'Finance Manager',
|
|
||||||
status: 'in-review',
|
|
||||||
tatHours: 48,
|
|
||||||
elapsedHours: 32,
|
|
||||||
assignedAt: '2024-10-06T03:15:00Z',
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
approver: 'Ramesh Kulkarni',
|
|
||||||
role: 'VP Operations',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 72,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{ name: 'Laptop_Specifications.pdf', size: '850 KB', type: 'PDF', uploadedBy: 'Vikram Singh', uploadedAt: 'Oct 5, 2024 9:20 AM' },
|
|
||||||
{ name: 'Vendor_Quotations.xlsx', size: '1.2 MB', type: 'Excel', uploadedBy: 'Procurement Team', uploadedAt: 'Oct 5, 2024 11:45 AM' },
|
|
||||||
{ name: 'Team_Expansion_Plan.pdf', size: '620 KB', type: 'PDF', uploadedBy: 'Design Team', uploadedAt: 'Oct 5, 2024 2:30 PM' }
|
|
||||||
],
|
|
||||||
spectators: [
|
|
||||||
{ name: 'Design Team Lead', role: 'End Users', avatar: 'DT' },
|
|
||||||
{ name: 'Procurement Team', role: 'Vendor Management', avatar: 'PT' }
|
|
||||||
],
|
|
||||||
auditTrail: [
|
|
||||||
{ type: 'created', action: 'Request Created', details: 'Laptop procurement request for design team', user: 'Vikram Singh', timestamp: 'Oct 5, 2024 9:15 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Meera Patel', details: 'IT Manager to verify specifications', user: 'System', timestamp: 'Oct 5, 2024 9:16 AM' },
|
|
||||||
{ type: 'approval', action: 'Approved by Meera Patel', details: 'Technical specifications approved', user: 'Meera Patel', timestamp: 'Oct 6, 2024 3:15 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Anil Kapoor', details: 'Forwarded to Finance for budget approval', user: 'System', timestamp: 'Oct 6, 2024 3:15 AM' }
|
|
||||||
],
|
|
||||||
tags: ['hardware', 'procurement', 'design-team', 'laptops']
|
|
||||||
},
|
|
||||||
|
|
||||||
'RE-REQ-2024-003': {
|
|
||||||
id: 'RE-REQ-2024-003',
|
|
||||||
title: 'Annual Service Center Expansion - Western Region',
|
|
||||||
description: 'Proposal for opening 15 new authorized service centers across tier-2 cities in Western region. Includes infrastructure setup, technician training, spare parts inventory, and marketing support. Expected to improve service accessibility by 35% in the target region.',
|
|
||||||
category: 'Operations & Expansion',
|
|
||||||
subcategory: 'Service Network',
|
|
||||||
status: 'pending',
|
|
||||||
priority: 'standard',
|
|
||||||
amount: '₹8,75,00,000',
|
|
||||||
slaProgress: 78,
|
|
||||||
slaRemaining: '1 day 4 hours',
|
|
||||||
slaEndDate: 'Oct 10, 2024 5:00 PM',
|
|
||||||
currentStep: 1,
|
|
||||||
totalSteps: 4,
|
|
||||||
template: 'custom',
|
|
||||||
initiator: {
|
|
||||||
name: 'Sanjay Reddy',
|
|
||||||
role: 'Regional Service Manager - West',
|
|
||||||
department: 'After Sales Service',
|
|
||||||
email: 'sanjay.reddy@royalenfield.com',
|
|
||||||
phone: '+91 98765 43232',
|
|
||||||
avatar: 'SR'
|
|
||||||
},
|
|
||||||
department: 'After Sales Service',
|
|
||||||
createdAt: 'Oct 3, 2024 8:45 AM',
|
|
||||||
updatedAt: 'Oct 6, 2024 5:45 PM',
|
|
||||||
dueDate: '2024-10-10T17:00:00Z',
|
|
||||||
conclusionRemark: '',
|
|
||||||
approvalFlow: [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
approver: 'Ramesh Kulkarni',
|
|
||||||
role: 'Head - After Sales Service',
|
|
||||||
status: 'pending',
|
|
||||||
tatHours: 72,
|
|
||||||
elapsedHours: 85,
|
|
||||||
assignedAt: '2024-10-03T08:45:00Z',
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
approver: 'Finance Team',
|
|
||||||
role: 'Budget Allocation',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 96,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
approver: 'Legal Team',
|
|
||||||
role: 'Compliance Review',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 120,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 4,
|
|
||||||
approver: 'Deepika Sharma',
|
|
||||||
role: 'VP Sales & Marketing',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 72,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{ name: 'Western_Region_Expansion_Plan.pdf', size: '7.5 MB', type: 'PDF', uploadedBy: 'Sanjay Reddy', uploadedAt: 'Oct 3, 2024 9:00 AM' },
|
|
||||||
{ name: 'Service_Center_Requirements.xlsx', size: '2.8 MB', type: 'Excel', uploadedBy: 'Planning Team', uploadedAt: 'Oct 3, 2024 11:30 AM' },
|
|
||||||
{ name: 'Customer_Demand_Analysis.pptx', size: '4.2 MB', type: 'PowerPoint', uploadedBy: 'Analytics Team', uploadedAt: 'Oct 4, 2024 2:15 PM' },
|
|
||||||
{ name: 'ROI_Projections_Service_Network.xlsx', size: '1.9 MB', type: 'Excel', uploadedBy: 'Finance Team', uploadedAt: 'Oct 5, 2024 10:45 AM' }
|
|
||||||
],
|
|
||||||
spectators: [
|
|
||||||
{ name: 'Regional Managers', role: 'Service Operations', avatar: 'RM' },
|
|
||||||
{ name: 'Training Team', role: 'Technician Development', avatar: 'TT' }
|
|
||||||
],
|
|
||||||
auditTrail: [
|
|
||||||
{ type: 'created', action: 'Request Created', details: 'Service center expansion proposal submitted', user: 'Sanjay Reddy', timestamp: 'Oct 3, 2024 8:45 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Ramesh Kulkarni', details: 'Forwarded to Head of After Sales Service', user: 'System', timestamp: 'Oct 3, 2024 8:46 AM' },
|
|
||||||
{ type: 'reminder', action: 'Reminder Sent', details: 'TAT breach reminder sent to approver', user: 'System', timestamp: 'Oct 6, 2024 5:45 PM' },
|
|
||||||
{ type: 'updated', action: 'Additional Documents', details: 'ROI projections added by finance team', user: 'Finance Team', timestamp: 'Oct 5, 2024 10:45 AM' }
|
|
||||||
],
|
|
||||||
tags: ['service-expansion', 'western-region', 'tier2-cities', 'overdue']
|
|
||||||
},
|
|
||||||
|
|
||||||
'RE-REQ-2024-004': {
|
|
||||||
id: 'RE-REQ-2024-004',
|
|
||||||
title: 'Employee Training Program - Advanced Motorcycle Mechanics',
|
|
||||||
description: 'Comprehensive training program for 50 service center technicians covering advanced diagnostics, electrical systems, fuel injection troubleshooting, and customer service excellence. Program duration: 3 weeks. Includes certification upon completion.',
|
|
||||||
category: 'Human Resources',
|
|
||||||
subcategory: 'Training & Development',
|
|
||||||
status: 'approved',
|
|
||||||
priority: 'standard',
|
|
||||||
amount: '₹18,50,000',
|
|
||||||
slaProgress: 100,
|
|
||||||
slaRemaining: 'Completed',
|
|
||||||
slaEndDate: 'Oct 5, 2024 5:00 PM',
|
|
||||||
currentStep: 3,
|
|
||||||
totalSteps: 3,
|
|
||||||
template: 'custom',
|
|
||||||
initiator: {
|
|
||||||
name: 'Kavita Menon',
|
|
||||||
role: 'Training Manager',
|
|
||||||
department: 'Human Resources',
|
|
||||||
email: 'kavita.menon@royalenfield.com',
|
|
||||||
phone: '+91 98765 43243',
|
|
||||||
avatar: 'KM'
|
|
||||||
},
|
|
||||||
department: 'Human Resources',
|
|
||||||
createdAt: 'Sep 28, 2024 11:00 AM',
|
|
||||||
updatedAt: 'Oct 5, 2024 4:30 PM',
|
|
||||||
dueDate: '2024-10-05T17:00:00Z',
|
|
||||||
submittedDate: '2024-09-28T11:00:00Z',
|
|
||||||
estimatedCompletion: 'Oct 5, 2024',
|
|
||||||
currentApprover: 'Completed',
|
|
||||||
approverLevel: '3 of 3',
|
|
||||||
conclusionRemark: 'All approvals completed. Training program scheduled for November 2024.',
|
|
||||||
approvalFlow: [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
approver: 'Ramesh Kulkarni',
|
|
||||||
role: 'Head - After Sales Service',
|
|
||||||
status: 'approved',
|
|
||||||
tatHours: 48,
|
|
||||||
actualHours: 36,
|
|
||||||
assignedAt: '2024-09-28T11:00:00Z',
|
|
||||||
comment: 'Excellent initiative. Training content approved.',
|
|
||||||
timestamp: 'Sep 29, 2024 11:00 PM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
approver: 'Anil Kapoor',
|
|
||||||
role: 'Finance Manager',
|
|
||||||
status: 'approved',
|
|
||||||
tatHours: 72,
|
|
||||||
actualHours: 48,
|
|
||||||
assignedAt: '2024-09-29T23:00:00Z',
|
|
||||||
comment: 'Budget approved. Cost per participant is reasonable.',
|
|
||||||
timestamp: 'Oct 1, 2024 11:00 PM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
approver: 'Deepika Sharma',
|
|
||||||
role: 'VP Sales & Marketing',
|
|
||||||
status: 'approved',
|
|
||||||
tatHours: 96,
|
|
||||||
actualHours: 72,
|
|
||||||
assignedAt: '2024-10-01T23:00:00Z',
|
|
||||||
comment: 'Final approval granted. Proceed with program execution.',
|
|
||||||
timestamp: 'Oct 5, 2024 4:30 PM'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{ name: 'Training_Curriculum.pdf', size: '3.2 MB', type: 'PDF', uploadedBy: 'Kavita Menon', uploadedAt: 'Sep 28, 2024 11:15 AM' },
|
|
||||||
{ name: 'Trainer_Profiles.pdf', size: '1.8 MB', type: 'PDF', uploadedBy: 'HR Team', uploadedAt: 'Sep 28, 2024 2:45 PM' },
|
|
||||||
{ name: 'Budget_Training_Program.xlsx', size: '680 KB', type: 'Excel', uploadedBy: 'Finance Team', uploadedAt: 'Sep 29, 2024 10:30 AM' }
|
|
||||||
],
|
|
||||||
spectators: [
|
|
||||||
{ name: 'Service Center Managers', role: 'Participant Coordination', avatar: 'SC' },
|
|
||||||
{ name: 'Quality Assurance', role: 'Training Quality', avatar: 'QA' }
|
|
||||||
],
|
|
||||||
auditTrail: [
|
|
||||||
{ type: 'created', action: 'Request Created', details: 'Training program proposal submitted', user: 'Kavita Menon', timestamp: 'Sep 28, 2024 11:00 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Ramesh Kulkarni', details: 'Forwarded to After Sales Service Head', user: 'System', timestamp: 'Sep 28, 2024 11:01 AM' },
|
|
||||||
{ type: 'approval', action: 'Approved by Ramesh Kulkarni', details: 'Level 1 approval completed', user: 'Ramesh Kulkarni', timestamp: 'Sep 29, 2024 11:00 PM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Anil Kapoor', details: 'Forwarded to Finance Manager', user: 'System', timestamp: 'Sep 29, 2024 11:01 PM' },
|
|
||||||
{ type: 'approval', action: 'Approved by Anil Kapoor', details: 'Budget approval completed', user: 'Anil Kapoor', timestamp: 'Oct 1, 2024 11:00 PM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Deepika Sharma', details: 'Forwarded to VP for final approval', user: 'System', timestamp: 'Oct 1, 2024 11:01 PM' },
|
|
||||||
{ type: 'approval', action: 'Approved by Deepika Sharma', details: 'Final approval - Request completed', user: 'Deepika Sharma', timestamp: 'Oct 5, 2024 4:30 PM' },
|
|
||||||
{ type: 'completed', action: 'Request Completed', details: 'All approvals obtained. Training scheduled.', user: 'System', timestamp: 'Oct 5, 2024 4:31 PM' }
|
|
||||||
],
|
|
||||||
tags: ['training', 'technicians', 'approved', 'completed']
|
|
||||||
},
|
|
||||||
|
|
||||||
'RE-REQ-2024-005': {
|
|
||||||
id: 'RE-REQ-2024-005',
|
|
||||||
title: 'Showroom Renovation - Chennai Flagship Store',
|
|
||||||
description: 'Complete renovation of Chennai flagship showroom including modern interior design, interactive display zones, customer lounge upgrade, motorcycle test ride facility, and digital experience center. Project timeline: 8 weeks.',
|
|
||||||
category: 'Infrastructure',
|
|
||||||
subcategory: 'Retail & Showroom',
|
|
||||||
status: 'rejected',
|
|
||||||
priority: 'standard',
|
|
||||||
amount: '₹65,00,000',
|
|
||||||
slaProgress: 100,
|
|
||||||
slaRemaining: 'Rejected',
|
|
||||||
slaEndDate: 'Oct 4, 2024 5:00 PM',
|
|
||||||
currentStep: 2,
|
|
||||||
totalSteps: 4,
|
|
||||||
template: 'custom',
|
|
||||||
initiator: {
|
|
||||||
name: 'Arjun Nair',
|
|
||||||
role: 'Showroom Manager - South',
|
|
||||||
department: 'Retail Operations',
|
|
||||||
email: 'arjun.nair@royalenfield.com',
|
|
||||||
phone: '+91 98765 43254',
|
|
||||||
avatar: 'AN'
|
|
||||||
},
|
|
||||||
department: 'Retail Operations',
|
|
||||||
createdAt: 'Oct 1, 2024 9:30 AM',
|
|
||||||
updatedAt: 'Oct 4, 2024 3:15 PM',
|
|
||||||
dueDate: '2024-10-04T17:00:00Z',
|
|
||||||
submittedDate: '2024-10-01T09:30:00Z',
|
|
||||||
estimatedCompletion: 'N/A',
|
|
||||||
currentApprover: 'Rejected by Anil Kapoor',
|
|
||||||
approverLevel: '2 of 4',
|
|
||||||
conclusionRemark: 'Request rejected due to insufficient budget justification. Please revise with detailed ROI analysis.',
|
|
||||||
approvalFlow: [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
approver: 'Suresh Iyer',
|
|
||||||
role: 'Regional Manager - South',
|
|
||||||
status: 'approved',
|
|
||||||
tatHours: 48,
|
|
||||||
actualHours: 24,
|
|
||||||
assignedAt: '2024-10-01T09:30:00Z',
|
|
||||||
comment: 'Renovation is necessary. Current showroom needs upgrade.',
|
|
||||||
timestamp: 'Oct 2, 2024 9:30 AM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
approver: 'Anil Kapoor',
|
|
||||||
role: 'Finance Manager',
|
|
||||||
status: 'rejected',
|
|
||||||
tatHours: 72,
|
|
||||||
actualHours: 48,
|
|
||||||
assignedAt: '2024-10-02T09:30:00Z',
|
|
||||||
comment: 'Budget allocation not justified. Need detailed ROI analysis and comparison with alternative renovation options. Please revise and resubmit with comprehensive financial projections.',
|
|
||||||
timestamp: 'Oct 4, 2024 3:15 PM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
approver: 'Legal Team',
|
|
||||||
role: 'Compliance & Contracts',
|
|
||||||
status: 'cancelled',
|
|
||||||
tatHours: 96,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 4,
|
|
||||||
approver: 'Ramesh Kulkarni',
|
|
||||||
role: 'VP Operations',
|
|
||||||
status: 'cancelled',
|
|
||||||
tatHours: 72,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{ name: 'Showroom_Renovation_Plan.pdf', size: '12.5 MB', type: 'PDF', uploadedBy: 'Arjun Nair', uploadedAt: 'Oct 1, 2024 9:45 AM' },
|
|
||||||
{ name: 'Interior_Design_Mockups.zip', size: '85 MB', type: 'ZIP', uploadedBy: 'Design Team', uploadedAt: 'Oct 1, 2024 2:30 PM' },
|
|
||||||
{ name: 'Contractor_Quotations.xlsx', size: '2.1 MB', type: 'Excel', uploadedBy: 'Procurement Team', uploadedAt: 'Oct 2, 2024 11:15 AM' }
|
|
||||||
],
|
|
||||||
spectators: [
|
|
||||||
{ name: 'Marketing Team', role: 'Brand Experience', avatar: 'MT' },
|
|
||||||
{ name: 'Customer Experience', role: 'Feedback & Analysis', avatar: 'CX' }
|
|
||||||
],
|
|
||||||
auditTrail: [
|
|
||||||
{ type: 'created', action: 'Request Created', details: 'Showroom renovation request submitted', user: 'Arjun Nair', timestamp: 'Oct 1, 2024 9:30 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Suresh Iyer', details: 'Forwarded to Regional Manager', user: 'System', timestamp: 'Oct 1, 2024 9:31 AM' },
|
|
||||||
{ type: 'approval', action: 'Approved by Suresh Iyer', details: 'Level 1 approval completed', user: 'Suresh Iyer', timestamp: 'Oct 2, 2024 9:30 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Anil Kapoor', details: 'Forwarded to Finance Manager', user: 'System', timestamp: 'Oct 2, 2024 9:31 AM' },
|
|
||||||
{ type: 'rejection', action: 'Rejected by Anil Kapoor', details: 'Budget justification insufficient', user: 'Anil Kapoor', timestamp: 'Oct 4, 2024 3:15 PM' },
|
|
||||||
{ type: 'completed', action: 'Request Rejected', details: 'Workflow terminated. Requires resubmission with revisions.', user: 'System', timestamp: 'Oct 4, 2024 3:16 PM' }
|
|
||||||
],
|
|
||||||
tags: ['showroom', 'renovation', 'rejected', 'south-region']
|
|
||||||
},
|
|
||||||
|
|
||||||
'RE-REQ-2024-006': {
|
|
||||||
id: 'RE-REQ-2024-006',
|
|
||||||
title: 'Spare Parts Inventory Optimization System',
|
|
||||||
description: 'Implementation of AI-powered inventory management system for spare parts across all service centers. Features include demand forecasting, automated reordering, stock level optimization, and real-time tracking. Expected to reduce inventory costs by 20% and improve part availability.',
|
|
||||||
category: 'Technology & Innovation',
|
|
||||||
subcategory: 'Software Implementation',
|
|
||||||
status: 'pending',
|
|
||||||
priority: 'express',
|
|
||||||
amount: '₹42,00,000',
|
|
||||||
slaProgress: 35,
|
|
||||||
slaRemaining: '1 day 16 hours',
|
|
||||||
slaEndDate: 'Oct 12, 2024 5:00 PM',
|
|
||||||
currentStep: 1,
|
|
||||||
totalSteps: 4,
|
|
||||||
template: 'custom',
|
|
||||||
initiator: {
|
|
||||||
name: 'Rahul Deshmukh',
|
|
||||||
role: 'Head - Supply Chain Technology',
|
|
||||||
department: 'Supply Chain',
|
|
||||||
email: 'rahul.deshmukh@royalenfield.com',
|
|
||||||
phone: '+91 98765 43265',
|
|
||||||
avatar: 'RD'
|
|
||||||
},
|
|
||||||
department: 'Supply Chain',
|
|
||||||
createdAt: 'Oct 7, 2024 10:00 AM',
|
|
||||||
updatedAt: 'Oct 8, 2024 9:15 AM',
|
|
||||||
dueDate: '2024-10-12T17:00:00Z',
|
|
||||||
submittedDate: '2024-10-07T10:00:00Z',
|
|
||||||
estimatedCompletion: 'Oct 12, 2024',
|
|
||||||
currentApprover: 'Vikram Singh',
|
|
||||||
approverLevel: '1 of 4',
|
|
||||||
conclusionRemark: '',
|
|
||||||
approvalFlow: [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
approver: 'Vikram Singh',
|
|
||||||
role: 'Head - IT Operations',
|
|
||||||
status: 'pending',
|
|
||||||
tatHours: 48,
|
|
||||||
elapsedHours: 23,
|
|
||||||
assignedAt: '2024-10-07T10:00:00Z',
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
approver: 'Supply Chain Director',
|
|
||||||
role: 'Operations Approval',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 72,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
approver: 'Anil Kapoor',
|
|
||||||
role: 'Finance Manager',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 96,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 4,
|
|
||||||
approver: 'Ramesh Kulkarni',
|
|
||||||
role: 'VP Operations',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 72,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{ name: 'AI_Inventory_System_Proposal.pdf', size: '8.9 MB', type: 'PDF', uploadedBy: 'Rahul Deshmukh', uploadedAt: 'Oct 7, 2024 10:15 AM' },
|
|
||||||
{ name: 'Vendor_Comparison_Analysis.xlsx', size: '3.4 MB', type: 'Excel', uploadedBy: 'IT Procurement', uploadedAt: 'Oct 7, 2024 2:45 PM' },
|
|
||||||
{ name: 'Cost_Benefit_Analysis.pptx', size: '6.2 MB', type: 'PowerPoint', uploadedBy: 'Analytics Team', uploadedAt: 'Oct 7, 2024 4:30 PM' },
|
|
||||||
{ name: 'Implementation_Timeline.pdf', size: '1.5 MB', type: 'PDF', uploadedBy: 'Project Management', uploadedAt: 'Oct 8, 2024 9:15 AM' }
|
|
||||||
],
|
|
||||||
spectators: [
|
|
||||||
{ name: 'Service Center Network', role: 'End Users', avatar: 'SN' },
|
|
||||||
{ name: 'Data Analytics Team', role: 'System Integration', avatar: 'DA' }
|
|
||||||
],
|
|
||||||
auditTrail: [
|
|
||||||
{ type: 'created', action: 'Request Created', details: 'AI inventory system proposal submitted', user: 'Rahul Deshmukh', timestamp: 'Oct 7, 2024 10:00 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Vikram Singh', details: 'Forwarded to IT Operations Head', user: 'System', timestamp: 'Oct 7, 2024 10:01 AM' },
|
|
||||||
{ type: 'updated', action: 'Documents Added', details: 'Implementation timeline document uploaded', user: 'Project Management', timestamp: 'Oct 8, 2024 9:15 AM' }
|
|
||||||
],
|
|
||||||
tags: ['technology', 'ai', 'inventory', 'supply-chain', 'high-priority']
|
|
||||||
},
|
|
||||||
|
|
||||||
'RE-REQ-2024-007': {
|
|
||||||
id: 'RE-REQ-2024-007',
|
|
||||||
title: 'Dealer Network Meeting - Q4 Business Review',
|
|
||||||
description: 'Quarterly business review meeting for all authorized dealers across India. Venue: Bangalore. Topics include Q3 performance review, Q4 targets, new model launches, marketing initiatives, service excellence programs, and dealer support policies. Expected attendance: 250 dealers.',
|
|
||||||
category: 'Events & Conferences',
|
|
||||||
subcategory: 'Dealer Meetings',
|
|
||||||
status: 'in-review',
|
|
||||||
priority: 'standard',
|
|
||||||
amount: '₹28,50,000',
|
|
||||||
slaProgress: 58,
|
|
||||||
slaRemaining: '1 day 12 hours',
|
|
||||||
slaEndDate: 'Oct 11, 2024 5:00 PM',
|
|
||||||
currentStep: 2,
|
|
||||||
totalSteps: 3,
|
|
||||||
template: 'custom',
|
|
||||||
initiator: {
|
|
||||||
name: 'Neha Kapoor',
|
|
||||||
role: 'Dealer Network Manager',
|
|
||||||
department: 'Sales & Distribution',
|
|
||||||
email: 'neha.kapoor@royalenfield.com',
|
|
||||||
phone: '+91 98765 43276',
|
|
||||||
avatar: 'NK'
|
|
||||||
},
|
|
||||||
department: 'Sales & Distribution',
|
|
||||||
createdAt: 'Oct 6, 2024 2:00 PM',
|
|
||||||
updatedAt: 'Oct 8, 2024 11:30 AM',
|
|
||||||
dueDate: '2024-10-11T17:00:00Z',
|
|
||||||
submittedDate: '2024-10-06T14:00:00Z',
|
|
||||||
estimatedCompletion: 'Oct 11, 2024',
|
|
||||||
currentApprover: 'Anil Kapoor',
|
|
||||||
approverLevel: '2 of 3',
|
|
||||||
conclusionRemark: '',
|
|
||||||
approvalFlow: [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
approver: 'Suresh Mehta',
|
|
||||||
role: 'Sales Director',
|
|
||||||
status: 'approved',
|
|
||||||
tatHours: 48,
|
|
||||||
actualHours: 36,
|
|
||||||
assignedAt: '2024-10-06T14:00:00Z',
|
|
||||||
comment: 'Dealer meeting approved. Agenda looks comprehensive.',
|
|
||||||
timestamp: 'Oct 8, 2024 2:00 AM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
approver: 'Anil Kapoor',
|
|
||||||
role: 'Finance Manager',
|
|
||||||
status: 'in-review',
|
|
||||||
tatHours: 72,
|
|
||||||
elapsedHours: 33,
|
|
||||||
assignedAt: '2024-10-08T02:00:00Z',
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
approver: 'Deepika Sharma',
|
|
||||||
role: 'VP Sales & Marketing',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 48,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{ name: 'Q4_Dealer_Meeting_Agenda.pdf', size: '2.8 MB', type: 'PDF', uploadedBy: 'Neha Kapoor', uploadedAt: 'Oct 6, 2024 2:15 PM' },
|
|
||||||
{ name: 'Venue_Booking_Confirmation.pdf', size: '980 KB', type: 'PDF', uploadedBy: 'Events Team', uploadedAt: 'Oct 6, 2024 4:45 PM' },
|
|
||||||
{ name: 'Event_Budget_Breakdown.xlsx', size: '1.2 MB', type: 'Excel', uploadedBy: 'Finance Team', uploadedAt: 'Oct 7, 2024 10:30 AM' },
|
|
||||||
{ name: 'Dealer_Invitations_List.xlsx', size: '580 KB', type: 'Excel', uploadedBy: 'Sales Team', uploadedAt: 'Oct 7, 2024 3:15 PM' }
|
|
||||||
],
|
|
||||||
spectators: [
|
|
||||||
{ name: 'Marketing Team', role: 'Presentation Support', avatar: 'MT' },
|
|
||||||
{ name: 'Events Management', role: 'Logistics Coordination', avatar: 'EM' }
|
|
||||||
],
|
|
||||||
auditTrail: [
|
|
||||||
{ type: 'created', action: 'Request Created', details: 'Dealer meeting proposal submitted', user: 'Neha Kapoor', timestamp: 'Oct 6, 2024 2:00 PM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Suresh Mehta', details: 'Forwarded to Sales Director', user: 'System', timestamp: 'Oct 6, 2024 2:01 PM' },
|
|
||||||
{ type: 'approval', action: 'Approved by Suresh Mehta', details: 'Sales approval completed', user: 'Suresh Mehta', timestamp: 'Oct 8, 2024 2:00 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Anil Kapoor', details: 'Forwarded to Finance Manager', user: 'System', timestamp: 'Oct 8, 2024 2:01 AM' },
|
|
||||||
{ type: 'updated', action: 'Documents Added', details: 'Dealer invitations list uploaded', user: 'Sales Team', timestamp: 'Oct 7, 2024 3:15 PM' }
|
|
||||||
],
|
|
||||||
tags: ['dealer-meeting', 'q4-review', 'event', 'bangalore']
|
|
||||||
},
|
|
||||||
|
|
||||||
'RE-REQ-2024-008': {
|
|
||||||
id: 'RE-REQ-2024-008',
|
|
||||||
title: 'Cybersecurity Infrastructure Upgrade',
|
|
||||||
description: 'Comprehensive upgrade of cybersecurity infrastructure including next-gen firewall, intrusion detection system, endpoint protection for 500+ devices, security information and event management (SIEM) system, and employee security awareness training. Critical for protecting customer data and business operations.',
|
|
||||||
category: 'IT & Infrastructure',
|
|
||||||
subcategory: 'Security & Compliance',
|
|
||||||
status: 'pending',
|
|
||||||
priority: 'urgent',
|
|
||||||
amount: '₹52,00,000',
|
|
||||||
slaProgress: 82,
|
|
||||||
slaRemaining: '4 hours 20 minutes',
|
|
||||||
slaEndDate: 'Oct 8, 2024 6:00 PM',
|
|
||||||
currentStep: 2,
|
|
||||||
totalSteps: 3,
|
|
||||||
template: 'custom',
|
|
||||||
initiator: {
|
|
||||||
name: 'Sameer Joshi',
|
|
||||||
role: 'Chief Information Security Officer',
|
|
||||||
department: 'Information Technology',
|
|
||||||
email: 'sameer.joshi@royalenfield.com',
|
|
||||||
phone: '+91 98765 43287',
|
|
||||||
avatar: 'SJ'
|
|
||||||
},
|
|
||||||
department: 'Information Technology',
|
|
||||||
createdAt: 'Oct 5, 2024 11:30 AM',
|
|
||||||
updatedAt: 'Oct 8, 2024 12:45 PM',
|
|
||||||
dueDate: '2024-10-08T18:00:00Z',
|
|
||||||
submittedDate: '2024-10-05T11:30:00Z',
|
|
||||||
estimatedCompletion: 'Oct 8, 2024',
|
|
||||||
currentApprover: 'Anil Kapoor',
|
|
||||||
approverLevel: '2 of 3',
|
|
||||||
conclusionRemark: '',
|
|
||||||
approvalFlow: [
|
|
||||||
{
|
|
||||||
step: 1,
|
|
||||||
approver: 'Vikram Singh',
|
|
||||||
role: 'Head - IT Operations',
|
|
||||||
status: 'approved',
|
|
||||||
tatHours: 24,
|
|
||||||
actualHours: 18,
|
|
||||||
assignedAt: '2024-10-05T11:30:00Z',
|
|
||||||
comment: 'Critical security upgrade. Approve immediately.',
|
|
||||||
timestamp: 'Oct 6, 2024 5:30 AM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 2,
|
|
||||||
approver: 'Anil Kapoor',
|
|
||||||
role: 'Finance Manager',
|
|
||||||
status: 'pending',
|
|
||||||
tatHours: 48,
|
|
||||||
elapsedHours: 55,
|
|
||||||
assignedAt: '2024-10-06T05:30:00Z',
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
approver: 'Ramesh Kulkarni',
|
|
||||||
role: 'VP Operations',
|
|
||||||
status: 'waiting',
|
|
||||||
tatHours: 72,
|
|
||||||
elapsedHours: 0,
|
|
||||||
assignedAt: null,
|
|
||||||
comment: null,
|
|
||||||
timestamp: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{ name: 'Security_Assessment_Report.pdf', size: '15.3 MB', type: 'PDF', uploadedBy: 'Sameer Joshi', uploadedAt: 'Oct 5, 2024 11:45 AM' },
|
|
||||||
{ name: 'Vendor_Solutions_Comparison.xlsx', size: '4.8 MB', type: 'Excel', uploadedBy: 'IT Security Team', uploadedAt: 'Oct 5, 2024 3:30 PM' },
|
|
||||||
{ name: 'Implementation_Roadmap.pptx', size: '7.6 MB', type: 'PowerPoint', uploadedBy: 'Project Management', uploadedAt: 'Oct 6, 2024 10:15 AM' },
|
|
||||||
{ name: 'Risk_Analysis_Report.pdf', size: '5.9 MB', type: 'PDF', uploadedBy: 'Security Consultant', uploadedAt: 'Oct 6, 2024 4:45 PM' }
|
|
||||||
],
|
|
||||||
spectators: [
|
|
||||||
{ name: 'Legal & Compliance', role: 'Data Protection', avatar: 'LC' },
|
|
||||||
{ name: 'IT Infrastructure', role: 'System Integration', avatar: 'IT' }
|
|
||||||
],
|
|
||||||
auditTrail: [
|
|
||||||
{ type: 'created', action: 'Request Created', details: 'Cybersecurity upgrade proposal submitted', user: 'Sameer Joshi', timestamp: 'Oct 5, 2024 11:30 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Vikram Singh', details: 'Forwarded to IT Operations Head', user: 'System', timestamp: 'Oct 5, 2024 11:31 AM' },
|
|
||||||
{ type: 'approval', action: 'Approved by Vikram Singh', details: 'IT approval - marked as critical', user: 'Vikram Singh', timestamp: 'Oct 6, 2024 5:30 AM' },
|
|
||||||
{ type: 'assignment', action: 'Assigned to Anil Kapoor', details: 'Forwarded to Finance Manager', user: 'System', timestamp: 'Oct 6, 2024 5:31 AM' },
|
|
||||||
{ type: 'reminder', action: 'Urgent Reminder', details: 'TAT breach warning - 4 hours remaining', user: 'System', timestamp: 'Oct 8, 2024 12:45 PM' }
|
|
||||||
],
|
|
||||||
tags: ['cybersecurity', 'urgent', 'critical', 'infrastructure', 'overdue']
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// API Endpoints for Custom Requests (to be implemented with backend)
|
// API Endpoints for Custom Requests (to be implemented with backend)
|
||||||
export const CUSTOM_REQUEST_API_ENDPOINTS = {
|
export const CUSTOM_REQUEST_API_ENDPOINTS = {
|
||||||
|
|||||||
@ -1,188 +0,0 @@
|
|||||||
// Mock Dealer Database - In production, this would be fetched from API
|
|
||||||
export interface DealerInfo {
|
|
||||||
code: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
address: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
region: string;
|
|
||||||
managerName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEALER_DATABASE: Record<string, DealerInfo> = {
|
|
||||||
'RE-MH-001': {
|
|
||||||
code: 'RE-MH-001',
|
|
||||||
name: 'Royal Motors Mumbai',
|
|
||||||
email: 'dealer@royalmotorsmumbai.com',
|
|
||||||
phone: '+91 98765 12345',
|
|
||||||
address: 'Shop No. 12-15, Central Avenue, Andheri West',
|
|
||||||
city: 'Mumbai',
|
|
||||||
state: 'Maharashtra',
|
|
||||||
region: 'West',
|
|
||||||
managerName: 'Rahul Deshmukh'
|
|
||||||
},
|
|
||||||
'RE-DL-002': {
|
|
||||||
code: 'RE-DL-002',
|
|
||||||
name: 'Delhi Enfield Center',
|
|
||||||
email: 'contact@delhienfield.com',
|
|
||||||
phone: '+91 98765 23456',
|
|
||||||
address: '45-48, Rajouri Garden, Main Market',
|
|
||||||
city: 'New Delhi',
|
|
||||||
state: 'Delhi',
|
|
||||||
region: 'North',
|
|
||||||
managerName: 'Vikram Singh'
|
|
||||||
},
|
|
||||||
'RE-BLR-003': {
|
|
||||||
code: 'RE-BLR-003',
|
|
||||||
name: 'Bangalore Royal Bikes',
|
|
||||||
email: 'info@bangaloreroyalbikes.com',
|
|
||||||
phone: '+91 98765 34567',
|
|
||||||
address: '123, MG Road, Near Trinity Metro',
|
|
||||||
city: 'Bangalore',
|
|
||||||
state: 'Karnataka',
|
|
||||||
region: 'South',
|
|
||||||
managerName: 'Suresh Kumar'
|
|
||||||
},
|
|
||||||
'RE-CHN-004': {
|
|
||||||
code: 'RE-CHN-004',
|
|
||||||
name: 'Chennai Enfield Hub',
|
|
||||||
email: 'chennai@enfieldhub.com',
|
|
||||||
phone: '+91 98765 45678',
|
|
||||||
address: '78-80, Anna Salai, T Nagar',
|
|
||||||
city: 'Chennai',
|
|
||||||
state: 'Tamil Nadu',
|
|
||||||
region: 'South',
|
|
||||||
managerName: 'Venkat Ramanan'
|
|
||||||
},
|
|
||||||
'RE-HYD-005': {
|
|
||||||
code: 'RE-HYD-005',
|
|
||||||
name: 'Hyderabad Royal Motorcycles',
|
|
||||||
email: 'hyderabad@royalmotorcycles.com',
|
|
||||||
phone: '+91 98765 56789',
|
|
||||||
address: '234, Banjara Hills, Road No. 12',
|
|
||||||
city: 'Hyderabad',
|
|
||||||
state: 'Telangana',
|
|
||||||
region: 'South',
|
|
||||||
managerName: 'Anil Reddy'
|
|
||||||
},
|
|
||||||
'RE-KOL-006': {
|
|
||||||
code: 'RE-KOL-006',
|
|
||||||
name: 'Kolkata Enfield Motors',
|
|
||||||
email: 'kolkata@enfieldmotors.com',
|
|
||||||
phone: '+91 98765 67890',
|
|
||||||
address: '56-58, Park Street, Near Park Hotel',
|
|
||||||
city: 'Kolkata',
|
|
||||||
state: 'West Bengal',
|
|
||||||
region: 'East',
|
|
||||||
managerName: 'Amit Chatterjee'
|
|
||||||
},
|
|
||||||
'RE-PUN-007': {
|
|
||||||
code: 'RE-PUN-007',
|
|
||||||
name: 'Pune Royal Dealership',
|
|
||||||
email: 'pune@royaldealership.com',
|
|
||||||
phone: '+91 98765 78901',
|
|
||||||
address: '345, FC Road, Deccan Gymkhana',
|
|
||||||
city: 'Pune',
|
|
||||||
state: 'Maharashtra',
|
|
||||||
region: 'West',
|
|
||||||
managerName: 'Sandeep Patil'
|
|
||||||
},
|
|
||||||
'RE-AHM-008': {
|
|
||||||
code: 'RE-AHM-008',
|
|
||||||
name: 'Ahmedabad Enfield Plaza',
|
|
||||||
email: 'ahmedabad@enfieldplaza.com',
|
|
||||||
phone: '+91 98765 89012',
|
|
||||||
address: '123, CG Road, Navrangpura',
|
|
||||||
city: 'Ahmedabad',
|
|
||||||
state: 'Gujarat',
|
|
||||||
region: 'West',
|
|
||||||
managerName: 'Kiran Patel'
|
|
||||||
},
|
|
||||||
'RE-JP-009': {
|
|
||||||
code: 'RE-JP-009',
|
|
||||||
name: 'Jaipur Royal Enfield',
|
|
||||||
email: 'jaipur@royalenfield.com',
|
|
||||||
phone: '+91 98765 90123',
|
|
||||||
address: '67, MI Road, C-Scheme',
|
|
||||||
city: 'Jaipur',
|
|
||||||
state: 'Rajasthan',
|
|
||||||
region: 'North',
|
|
||||||
managerName: 'Rajesh Sharma'
|
|
||||||
},
|
|
||||||
'RE-LKO-010': {
|
|
||||||
code: 'RE-LKO-010',
|
|
||||||
name: 'Lucknow Enfield Showroom',
|
|
||||||
email: 'lucknow@enfieldshowroom.com',
|
|
||||||
phone: '+91 98765 01234',
|
|
||||||
address: '89, Hazratganj, Near Halwasiya Crossing',
|
|
||||||
city: 'Lucknow',
|
|
||||||
state: 'Uttar Pradesh',
|
|
||||||
region: 'North',
|
|
||||||
managerName: 'Ankit Verma'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get dealer information by dealer code
|
|
||||||
* @param dealerCode - The dealer code (e.g., 'RE-MH-001')
|
|
||||||
* @returns DealerInfo object or null if not found
|
|
||||||
*/
|
|
||||||
export function getDealerInfo(dealerCode: string): DealerInfo | null {
|
|
||||||
return DEALER_DATABASE[dealerCode] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all dealers for a specific region
|
|
||||||
* @param region - Region name (North, South, East, West)
|
|
||||||
* @returns Array of DealerInfo objects
|
|
||||||
*/
|
|
||||||
export function getDealersByRegion(region: string): DealerInfo[] {
|
|
||||||
return Object.values(DEALER_DATABASE).filter(
|
|
||||||
dealer => dealer.region.toLowerCase() === region.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all dealers for a specific state
|
|
||||||
* @param state - State name
|
|
||||||
* @returns Array of DealerInfo objects
|
|
||||||
*/
|
|
||||||
export function getDealersByState(state: string): DealerInfo[] {
|
|
||||||
return Object.values(DEALER_DATABASE).filter(
|
|
||||||
dealer => dealer.state.toLowerCase() === state.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all dealers as an array (for dropdowns, etc.)
|
|
||||||
* @returns Array of DealerInfo objects
|
|
||||||
*/
|
|
||||||
export function getAllDealers(): DealerInfo[] {
|
|
||||||
return Object.values(DEALER_DATABASE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search dealers by name or code
|
|
||||||
* @param searchTerm - Search term
|
|
||||||
* @returns Array of matching DealerInfo objects
|
|
||||||
*/
|
|
||||||
export function searchDealers(searchTerm: string): DealerInfo[] {
|
|
||||||
const term = searchTerm.toLowerCase();
|
|
||||||
return Object.values(DEALER_DATABASE).filter(
|
|
||||||
dealer =>
|
|
||||||
dealer.name.toLowerCase().includes(term) ||
|
|
||||||
dealer.code.toLowerCase().includes(term) ||
|
|
||||||
dealer.city.toLowerCase().includes(term)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format dealer address for display
|
|
||||||
* @param dealer - DealerInfo object
|
|
||||||
* @returns Formatted address string
|
|
||||||
*/
|
|
||||||
export function formatDealerAddress(dealer: DealerInfo): string {
|
|
||||||
return `${dealer.address}, ${dealer.city}, ${dealer.state}`;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user