import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useParams, useNavigate, useLocation, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useIssueDetails, useIssueMutations } from '../hooks/useIssue'; import { FaArrowLeft, FaSave, FaEdit, FaTrash, FaCheckCircle, FaTimesCircle, FaExclamationTriangle, FaUser, FaBuilding, FaEnvelope, FaCalendarAlt, FaTag, FaComment, FaClipboardList } from 'react-icons/fa'; import { toast, ToastContainer, Bounce } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import LinkField from '../components/LinkField'; import type { CreateIssueData } from '../services/issueService'; import CommentSection from '../components/CommentSection'; import WorkflowActions from '../components/WorkflowActions'; import SupportPrecheckWizard from './SupportPrecheckWizard'; import type { NewIssuePrecheckLocationState } from './supportPrecheckContent'; import apiService from '../services/apiService'; const ROLES_CAN_CREATE_WO_FROM_ISSUE = ['Work Control', 'System Manager']; const ISSUE_STATUSES_ALLOW_WO = ['Open', 'Replied', 'On Hold']; // Helper to get today's date in YYYY-MM-DD format const getTodayDate = (): string => { return new Date().toISOString().split('T')[0]; }; // Helper to get current time in HH:MM:SS format const getCurrentTime = (): string => { return new Date().toTimeString().split(' ')[0]; }; // Status badge styles const getStatusStyle = (status: string) => { switch (status?.toLowerCase()) { case 'open': return { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-800 dark:text-blue-300', border: 'border-blue-200 dark:border-blue-800' }; case 'replied': return { bg: 'bg-purple-100 dark:bg-purple-900/30', text: 'text-purple-800 dark:text-purple-300', border: 'border-purple-200 dark:border-purple-800' }; case 'on hold': return { bg: 'bg-yellow-100 dark:bg-yellow-900/30', text: 'text-yellow-800 dark:text-yellow-300', border: 'border-yellow-200 dark:border-yellow-800' }; case 'resolved': return { bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-800 dark:text-green-300', border: 'border-green-200 dark:border-green-800' }; case 'closed': return { bg: 'bg-gray-100 dark:bg-gray-700', text: 'text-gray-800 dark:text-gray-300', border: 'border-gray-200 dark:border-gray-600' }; default: return { bg: 'bg-gray-100 dark:bg-gray-700', text: 'text-gray-800 dark:text-gray-300', border: 'border-gray-200 dark:border-gray-600' }; } }; const IssueDetail: React.FC = () => { const { t } = useTranslation(); const { issueName } = useParams<{ issueName: string }>(); const navigate = useNavigate(); const location = useLocation(); const [searchParams] = useSearchParams(); const isNewIssue = issueName === 'new'; const newIssuePrecheckState = (location.state || undefined) as NewIssuePrecheckLocationState | undefined; const skipNewIssuePrecheck = newIssuePrecheckState?.newIssuePrecheckDone === true || searchParams.get('skip_precheck') === '1'; // Form data state const [formData, setFormData] = useState({ subject: '', raised_by: '', status: 'Open', priority: '', issue_type: '', description: '', contact: '', company: '', customer: '', project: '', resolution_details: '', opening_date: isNewIssue ? getTodayDate() : '', opening_time: isNewIssue ? getCurrentTime() : '', first_responded_on: '', sla_resolution_date: '', sla_resolution_by: '', }); const { issue, loading, error, refetch } = useIssueDetails(isNewIssue ? null : issueName || null); const { createIssue, updateIssue, deleteIssue, loading: saving } = useIssueMutations(); const [isEditing, setIsEditing] = useState(isNewIssue); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const appliedPrecheckReasonRef = useRef(false); const [userRoles, setUserRoles] = useState([]); useEffect(() => { const fetchRoles = async () => { try { const response = await apiService.apiCall( '/api/method/asset_lite.api.user_roles.get_user_roles' ); if (Array.isArray(response)) { setUserRoles(response as string[]); } else if (response && typeof response === 'object' && 'message' in response) { const msg = (response as { message?: string[] }).message; if (Array.isArray(msg)) setUserRoles(msg); } } catch { setUserRoles([]); } }; void fetchRoles(); }, []); const canCreateWorkOrderFromIssue = !isNewIssue && ISSUE_STATUSES_ALLOW_WO.includes((issue?.status || formData.status || '').trim()) && ROLES_CAN_CREATE_WO_FROM_ISSUE.some((r) => userRoles.includes(r)); useEffect(() => { if (!isNewIssue || !skipNewIssuePrecheck) { return; } const reason = newIssuePrecheckState?.precheckCantCompleteReason?.trim(); if (!reason || appliedPrecheckReasonRef.current) { return; } appliedPrecheckReasonRef.current = true; setFormData((prev) => ({ ...prev, description: `${prev.description ? `${prev.description}\n\n` : ''}Could not complete self-service checks: ${reason}`, })); }, [isNewIssue, skipNewIssuePrecheck, newIssuePrecheckState?.precheckCantCompleteReason]); /** Legacy Open issues: ensure workflow_state exists so Work Control sees actions */ useEffect(() => { if (!issueName || isNewIssue || !issue) return; if (issue.workflow_state || issue.status !== 'Open') return; let cancelled = false; void apiService .apiCall('/api/method/asset_lite.api.issue_api.normalize_issue_workflow_state', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ issue_name: issueName }), }) .then(() => { if (!cancelled) void refetch(); }) .catch(() => {}); return () => { cancelled = true; }; }, [issueName, isNewIssue, issue?.name, issue?.workflow_state, issue?.status, refetch]); const issueWorkflowDocData = useMemo(() => { if (!issue || isNewIssue) return undefined; return { status: formData.status || issue.status || '', subject: formData.subject ?? issue.subject ?? '', priority: formData.priority ?? issue.priority ?? '', company: formData.company ?? issue.company ?? '', }; }, [ issue, isNewIssue, formData.status, formData.subject, formData.priority, formData.company, ]); // Load issue data when fetched useEffect(() => { if (issue && !isNewIssue) { setFormData({ subject: issue.subject || '', raised_by: issue.raised_by || '', status: issue.status || 'Open', priority: issue.priority || '', issue_type: issue.issue_type || '', description: issue.description || '', contact: issue.contact || '', company: issue.company || '', customer: issue.customer || '', project: issue.project || '', resolution_details: issue.resolution_details || '', opening_date: issue.opening_date || '', opening_time: issue.opening_time || '', first_responded_on: issue.first_responded_on ? issue.first_responded_on.split(' ')[0] : '', sla_resolution_date: issue.sla_resolution_date ? issue.sla_resolution_date.split(' ')[0] : '', sla_resolution_by: issue.sla_resolution_by || '', }); setIsEditing(false); } }, [issue, isNewIssue]); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; const handleSave = async () => { if (!formData.subject) { toast.error('Please enter a subject', { position: "top-right", autoClose: 4000, icon: }); return; } try { if (isNewIssue) { const newIssue = await createIssue(formData); // Auto-assign the newly created Issue to Work Control for review try { await apiService.apiCall('/api/method/asset_lite.api.issue_api.assign_issue_to_work_control', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ issue_name: newIssue.name }) }); } catch (assignErr) { // Non-blocking: ticket is created even if assignment fails console.warn('Failed to auto-assign issue to Work Control:', assignErr); } toast.success('Issue created successfully!', { position: "top-right", autoClose: 3000, icon: }); navigate(`/support/${newIssue.name}`); } else { await updateIssue(issueName!, formData); toast.success('Issue updated successfully!', { position: "top-right", autoClose: 3000, icon: }); setIsEditing(false); refetch(); } } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; toast.error(`Failed to save: ${errorMessage}`, { position: "top-right", autoClose: 6000, icon: }); } }; const handleDelete = async () => { try { await deleteIssue(issueName!); toast.success('Issue deleted successfully!', { position: "top-right", autoClose: 3000, icon: }); navigate(-1); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; toast.error(`Failed to delete: ${errorMessage}`, { position: "top-right", autoClose: 6000, icon: }); } }; const isFieldDisabled = useCallback((fieldname: string): boolean => { if (!isEditing) return true; // Some fields are always read-only if (['opening_date', 'opening_time'].includes(fieldname) && !isNewIssue) { return true; } return false; }, [isEditing, isNewIssue]); // Format datetime const formatDateTime = (dateStr: string) => { if (!dateStr) return '-'; return new Date(dateStr).toLocaleString(); }; if (isNewIssue && !skipNewIssuePrecheck) { return ; } if (loading) { return (

Loading issue details...

); } if (error && !isNewIssue) { return (

Error Loading Issue

{error}

); } const currentStatus = issue?.status || formData.status || 'Open'; const statusStyle = getStatusStyle(currentStatus); return (
{/* Toast Container */} {/* Header */}

{isNewIssue ? t('issues.newIssue') : issue?.name || t('issues.issueDetails')} {!isNewIssue && ( {currentStatus} )}

{isNewIssue ? t('issues.createNewIssue') : formData.subject}

{!isNewIssue && canCreateWorkOrderFromIssue && issueName && ( )} {!isNewIssue && !isEditing && ( <> )} {isEditing && ( <> )}
{/* Delete Confirmation Modal */} {showDeleteConfirm && (

Delete Issue

Are you sure you want to delete this issue? This action cannot be undone.

)} {/* Form */}
{/* Main Content - Left Column */}
{/* Issue Details */}

{t('issues.issueDetails')}

setFormData({ ...formData, priority: val })} disabled={isFieldDisabled('priority')} placeholder={t('issues.selectPriority')} />
setFormData({ ...formData, issue_type: val })} disabled={isFieldDisabled('issue_type')} placeholder={t('issues.selectIssueType')} />