316 lines
9.8 KiB
TypeScript
316 lines
9.8 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { getWorkflowDetails } from '@/services/workflowApi';
|
|
import { getAllConfigurations, AdminConfiguration } from '@/services/adminApi';
|
|
|
|
export interface RequestTemplate {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
category: string;
|
|
icon: React.ComponentType<any>;
|
|
estimatedTime: string;
|
|
commonApprovers: string[];
|
|
suggestedSLA: number;
|
|
priority: 'high' | 'medium' | 'low';
|
|
fields: {
|
|
amount?: boolean;
|
|
vendor?: boolean;
|
|
timeline?: boolean;
|
|
impact?: boolean;
|
|
};
|
|
}
|
|
|
|
export interface FormData {
|
|
template: string;
|
|
title: string;
|
|
description: string;
|
|
category: string;
|
|
priority: string;
|
|
urgency: string;
|
|
businessImpact: string;
|
|
amount: string;
|
|
currency: string;
|
|
vendor: string;
|
|
timeline: string;
|
|
slaTemplate: string;
|
|
slaHours: number;
|
|
customSlaHours: number;
|
|
slaEndDate: Date | undefined;
|
|
expectedCompletionDate: Date | undefined;
|
|
breachEscalation: boolean;
|
|
reminderSchedule: '25' | '50' | '75';
|
|
workflowType: 'sequential' | 'parallel';
|
|
requiresAllApprovals: boolean;
|
|
escalationEnabled: boolean;
|
|
reminderEnabled: boolean;
|
|
minimumLevel: number;
|
|
maxLevel: number;
|
|
approvers: any[];
|
|
approverCount: number;
|
|
spectators: any[];
|
|
ccList: any[];
|
|
invitedUsers: any[];
|
|
allowComments: boolean;
|
|
allowDocumentUpload: boolean;
|
|
documents: File[];
|
|
tags: string[];
|
|
relatedRequests: string[];
|
|
costCenter: string;
|
|
project: string;
|
|
}
|
|
|
|
const initialFormData: FormData = {
|
|
template: '',
|
|
title: '',
|
|
description: '',
|
|
category: '',
|
|
priority: '',
|
|
urgency: '',
|
|
businessImpact: '',
|
|
amount: '',
|
|
currency: 'USD',
|
|
vendor: '',
|
|
timeline: '',
|
|
slaTemplate: '',
|
|
slaHours: 0,
|
|
customSlaHours: 0,
|
|
slaEndDate: undefined,
|
|
expectedCompletionDate: undefined,
|
|
breachEscalation: true,
|
|
reminderSchedule: '50',
|
|
workflowType: 'sequential',
|
|
requiresAllApprovals: true,
|
|
escalationEnabled: true,
|
|
reminderEnabled: true,
|
|
minimumLevel: 1,
|
|
maxLevel: 1,
|
|
approvers: [],
|
|
approverCount: 1,
|
|
spectators: [],
|
|
ccList: [],
|
|
invitedUsers: [],
|
|
allowComments: true,
|
|
allowDocumentUpload: true,
|
|
documents: [],
|
|
tags: [],
|
|
relatedRequests: [],
|
|
costCenter: '',
|
|
project: ''
|
|
};
|
|
|
|
export interface SystemPolicy {
|
|
maxApprovalLevels: number;
|
|
maxParticipants: number;
|
|
allowSpectators: boolean;
|
|
maxSpectators: number;
|
|
}
|
|
|
|
export interface DocumentPolicy {
|
|
maxFileSizeMB: number;
|
|
allowedFileTypes: string[];
|
|
}
|
|
|
|
/**
|
|
* Custom Hook: useCreateRequestForm
|
|
*
|
|
* Purpose: Manages form state, policies, and draft loading for CreateRequest
|
|
*
|
|
* Responsibilities:
|
|
* - Manages form data state
|
|
* - Loads system and document policies
|
|
* - Handles draft loading in edit mode
|
|
* - Provides form data update functions
|
|
*/
|
|
export function useCreateRequestForm(
|
|
isEditing: boolean,
|
|
editRequestId: string,
|
|
templates: RequestTemplate[]
|
|
) {
|
|
const { user } = useAuth();
|
|
const [formData, setFormData] = useState<FormData>(initialFormData);
|
|
const [selectedTemplate, setSelectedTemplate] = useState<RequestTemplate | null>(null);
|
|
const [loadingDraft, setLoadingDraft] = useState(isEditing);
|
|
const [systemPolicy, setSystemPolicy] = useState<SystemPolicy>({
|
|
maxApprovalLevels: 10,
|
|
maxParticipants: 50,
|
|
allowSpectators: true,
|
|
maxSpectators: 20
|
|
});
|
|
const [documentPolicy, setDocumentPolicy] = useState<DocumentPolicy>({
|
|
maxFileSizeMB: 10,
|
|
allowedFileTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'jpg', 'jpeg', 'png', 'gif']
|
|
});
|
|
const [existingDocuments, setExistingDocuments] = useState<any[]>([]);
|
|
|
|
// Load policies on mount
|
|
useEffect(() => {
|
|
const loadPolicies = async () => {
|
|
try {
|
|
// Load document policy
|
|
const docConfigs = await getAllConfigurations('DOCUMENT_POLICY');
|
|
const docConfigMap: Record<string, string> = {};
|
|
docConfigs.forEach((c: AdminConfiguration) => {
|
|
docConfigMap[c.configKey] = c.configValue;
|
|
});
|
|
|
|
const maxFileSizeMB = parseInt(docConfigMap['MAX_FILE_SIZE_MB'] || '10');
|
|
const allowedFileTypesStr = docConfigMap['ALLOWED_FILE_TYPES'] || 'pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png,gif';
|
|
const allowedFileTypes = allowedFileTypesStr.split(',').map(ext => ext.trim().toLowerCase());
|
|
|
|
setDocumentPolicy({
|
|
maxFileSizeMB,
|
|
allowedFileTypes
|
|
});
|
|
|
|
// Load system policy
|
|
const workflowConfigs = await getAllConfigurations('WORKFLOW_SHARING');
|
|
const tatConfigs = await getAllConfigurations('TAT_SETTINGS');
|
|
const allConfigs = [...workflowConfigs, ...tatConfigs];
|
|
const configMap: Record<string, string> = {};
|
|
allConfigs.forEach((c: AdminConfiguration) => {
|
|
configMap[c.configKey] = c.configValue;
|
|
});
|
|
|
|
setSystemPolicy({
|
|
maxApprovalLevels: parseInt(configMap['MAX_APPROVAL_LEVELS'] || '10'),
|
|
maxParticipants: parseInt(configMap['MAX_PARTICIPANTS_PER_REQUEST'] || '50'),
|
|
allowSpectators: configMap['ALLOW_ADD_SPECTATOR']?.toLowerCase() === 'true',
|
|
maxSpectators: parseInt(configMap['MAX_SPECTATORS_PER_REQUEST'] || '20')
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load policies:', error);
|
|
}
|
|
};
|
|
|
|
loadPolicies();
|
|
}, []);
|
|
|
|
// Load draft data when in edit mode
|
|
useEffect(() => {
|
|
if (!isEditing || !editRequestId) return;
|
|
|
|
let mounted = true;
|
|
(async () => {
|
|
try {
|
|
setLoadingDraft(true);
|
|
const details = await getWorkflowDetails(editRequestId);
|
|
if (!mounted || !details) return;
|
|
|
|
const wf = details.workflow || {};
|
|
const approvals = Array.isArray(details.approvals) ? details.approvals : [];
|
|
const participants = Array.isArray(details.participants) ? details.participants : [];
|
|
const documents = Array.isArray(details.documents) ? details.documents.filter((d: any) => !d.isDeleted) : [];
|
|
|
|
// Store existing documents for tracking
|
|
setExistingDocuments(documents);
|
|
|
|
// Map priority
|
|
const priority = (wf.priority || '').toString().toLowerCase();
|
|
const priorityMap: Record<string, string> = {
|
|
'standard': 'standard',
|
|
'express': 'express'
|
|
};
|
|
|
|
// Map template type
|
|
const templateType = wf.templateType === 'TEMPLATE' ? 'existing-template' : 'custom';
|
|
const template = templates.find(t => t.id === templateType) || templates[0] || null;
|
|
setSelectedTemplate(template);
|
|
|
|
// Map approvers
|
|
const mappedApprovers = approvals
|
|
.sort((a: any, b: any) => (a.levelNumber || 0) - (b.levelNumber || 0))
|
|
.map((approval: any) => {
|
|
const tatHours = Number(approval.tatHours || 24);
|
|
const tatDays = Math.floor(tatHours / 24);
|
|
const tatHoursRemainder = tatHours % 24;
|
|
return {
|
|
id: approval.approverId || `temp-${approval.levelNumber}`,
|
|
name: approval.approverName || approval.approverEmail || '',
|
|
email: approval.approverEmail || '',
|
|
role: approval.levelName || `Level ${approval.levelNumber}`,
|
|
department: '',
|
|
avatar: (approval.approverName || approval.approverEmail || 'XX').substring(0, 2).toUpperCase(),
|
|
level: approval.levelNumber || 1,
|
|
canClose: false,
|
|
tat: tatDays > 0 ? tatDays : tatHoursRemainder,
|
|
tatType: tatDays > 0 ? 'days' as const : 'hours' as const,
|
|
userId: approval.approverId
|
|
};
|
|
});
|
|
|
|
// Map spectators
|
|
const mappedSpectators = participants
|
|
.filter((p: any) => {
|
|
const pt = (p.participantType || p.participant_type || '').toString().toUpperCase().trim();
|
|
const isSpectator = pt === 'SPECTATOR';
|
|
if (!isSpectator) return false;
|
|
const hasEmail = !!(p.userEmail || p.user_email || p.email);
|
|
return hasEmail;
|
|
})
|
|
.map((p: any, index: number) => {
|
|
const userId = p.userId || p.user_id || p.id;
|
|
const userName = p.userName || p.user_name || p.name || '';
|
|
const userEmail = p.userEmail || p.user_email || p.email || '';
|
|
const avatarText = userName || userEmail || 'XX';
|
|
const avatar = avatarText
|
|
.split(' ')
|
|
.map((s: string) => s[0])
|
|
.filter(Boolean)
|
|
.join('')
|
|
.slice(0, 2)
|
|
.toUpperCase();
|
|
|
|
return {
|
|
id: userId || `spectator-${editRequestId}-${index}-${Date.now()}`,
|
|
userId: userId,
|
|
name: userName || userEmail || 'Spectator',
|
|
email: userEmail,
|
|
role: 'Spectator',
|
|
department: p.department || '',
|
|
avatar: avatar,
|
|
level: 1,
|
|
canClose: false
|
|
};
|
|
});
|
|
|
|
setFormData(prev => ({
|
|
...prev,
|
|
template: templateType,
|
|
title: wf.title || '',
|
|
description: wf.description || '',
|
|
priority: priorityMap[priority] || 'standard',
|
|
approvers: mappedApprovers,
|
|
approverCount: mappedApprovers.length || 1,
|
|
spectators: mappedSpectators,
|
|
maxLevel: Math.max(...mappedApprovers.map((a: any) => a.level || 1), 1)
|
|
}));
|
|
} catch (error) {
|
|
console.error('Failed to load draft:', error);
|
|
} finally {
|
|
if (mounted) setLoadingDraft(false);
|
|
}
|
|
})();
|
|
|
|
return () => { mounted = false; };
|
|
}, [isEditing, editRequestId, templates]);
|
|
|
|
const updateFormData = (field: keyof FormData, value: any) => {
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
return {
|
|
formData,
|
|
setFormData,
|
|
updateFormData,
|
|
selectedTemplate,
|
|
setSelectedTemplate,
|
|
loadingDraft,
|
|
systemPolicy,
|
|
documentPolicy,
|
|
existingDocuments,
|
|
setExistingDocuments
|
|
};
|
|
}
|
|
|