352 lines
14 KiB
TypeScript
352 lines
14 KiB
TypeScript
"use client";
|
|
import { useState, FormEvent, useEffect, useRef } from 'react';
|
|
import { checkTicketStatus, SupportTicket } from '@/lib/api/supportService';
|
|
|
|
const TicketStatusCheck = () => {
|
|
const [ticketNumber, setTicketNumber] = useState('');
|
|
const [isSearching, setIsSearching] = useState(false);
|
|
const [searchError, setSearchError] = useState<string | null>(null);
|
|
const [ticket, setTicket] = useState<SupportTicket | null>(null);
|
|
|
|
// Refs for scrolling to results
|
|
const errorRef = useRef<HTMLDivElement>(null);
|
|
const ticketRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Scroll to error when it appears
|
|
useEffect(() => {
|
|
if (searchError && errorRef.current) {
|
|
setTimeout(() => {
|
|
errorRef.current?.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
}, 100);
|
|
}
|
|
}, [searchError]);
|
|
|
|
// Scroll to ticket results when they appear
|
|
useEffect(() => {
|
|
if (ticket && ticketRef.current) {
|
|
setTimeout(() => {
|
|
ticketRef.current?.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}, 100);
|
|
}
|
|
}, [ticket]);
|
|
|
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
setIsSearching(true);
|
|
setSearchError(null);
|
|
setTicket(null);
|
|
|
|
try {
|
|
const response = await checkTicketStatus(ticketNumber);
|
|
setTicket(response);
|
|
} catch (error: any) {
|
|
if (error.message === 'Ticket not found') {
|
|
setSearchError('Ticket not found. Please check your ticket number and try again.');
|
|
} else {
|
|
setSearchError('An error occurred while searching. Please try again.');
|
|
}
|
|
} finally {
|
|
setIsSearching(false);
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
const getRelativeTime = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMins / 60);
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
|
|
if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
|
|
if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
|
|
return formatDate(dateString);
|
|
};
|
|
|
|
const getStatusIcon = (statusName: string) => {
|
|
const status = statusName.toLowerCase();
|
|
if (status.includes('open') || status.includes('new')) return 'fa-inbox';
|
|
if (status.includes('progress') || status.includes('working')) return 'fa-spinner';
|
|
if (status.includes('pending') || status.includes('waiting')) return 'fa-clock';
|
|
if (status.includes('resolved') || status.includes('closed')) return 'fa-check-circle';
|
|
return 'fa-ticket';
|
|
};
|
|
|
|
const getPriorityIcon = (priorityName: string) => {
|
|
const priority = priorityName.toLowerCase();
|
|
if (priority.includes('urgent') || priority.includes('critical')) return 'fa-exclamation-triangle';
|
|
if (priority.includes('high')) return 'fa-arrow-up';
|
|
if (priority.includes('medium')) return 'fa-minus';
|
|
if (priority.includes('low')) return 'fa-arrow-down';
|
|
return 'fa-flag';
|
|
};
|
|
|
|
return (
|
|
<div className="ticket-status-check enterprise-status-check">
|
|
<div className="row justify-content-center">
|
|
<div className="col-12">
|
|
<div className="status-header-enterprise">
|
|
<div className="status-header-icon">
|
|
<i className="fa-solid fa-magnifying-glass"></i>
|
|
</div>
|
|
<h2>Check Ticket Status</h2>
|
|
<p>Track your support request in real-time with instant status updates</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="status-search-form-enterprise">
|
|
<div className="search-container">
|
|
<div className="search-input-wrapper">
|
|
<i className="fa-solid fa-ticket search-icon"></i>
|
|
<input
|
|
type="text"
|
|
value={ticketNumber}
|
|
onChange={(e) => setTicketNumber(e.target.value.toUpperCase())}
|
|
placeholder="TKT-YYYYMMDD-XXXXX"
|
|
required
|
|
className="search-input-enterprise"
|
|
pattern="TKT-\d{8}-[A-Z0-9]{5}"
|
|
title="Please enter a valid ticket number (e.g., TKT-20231015-ABCDE)"
|
|
/>
|
|
{ticketNumber && (
|
|
<button
|
|
type="button"
|
|
className="clear-btn"
|
|
onClick={() => {
|
|
setTicketNumber('');
|
|
setTicket(null);
|
|
setSearchError(null);
|
|
}}
|
|
>
|
|
<i className="fa-solid fa-times"></i>
|
|
</button>
|
|
)}
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
className="btn-search-enterprise"
|
|
disabled={isSearching}
|
|
>
|
|
{isSearching ? (
|
|
<>
|
|
<span className="spinner-enterprise"></span>
|
|
<span>Searching...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<i className="fa-solid fa-search"></i>
|
|
<span>Search</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
<p className="search-hint">
|
|
<i className="fa-solid fa-info-circle"></i>
|
|
Enter your ticket number exactly as it appears in your confirmation email
|
|
</p>
|
|
</form>
|
|
|
|
{searchError && (
|
|
<div ref={errorRef} className="alert-enterprise alert-error">
|
|
<i className="fa-solid fa-exclamation-circle"></i>
|
|
<div>
|
|
<strong>Ticket Not Found</strong>
|
|
<p>{searchError}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{ticket && (
|
|
<div ref={ticketRef} className="ticket-details-enterprise">
|
|
{/* Header Section */}
|
|
<div className="ticket-header-enterprise">
|
|
<div className="ticket-number-section">
|
|
<span className="ticket-label">Ticket Number</span>
|
|
<div className="ticket-number-display">
|
|
<i className="fa-solid fa-ticket"></i>
|
|
<span>{ticket.ticket_number}</span>
|
|
<button
|
|
className="btn-copy-inline"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(ticket.ticket_number);
|
|
const btn = document.querySelector('.btn-copy-inline');
|
|
if (btn) {
|
|
const originalHTML = btn.innerHTML;
|
|
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalHTML;
|
|
}, 2000);
|
|
}
|
|
}}
|
|
>
|
|
<i className="fa-solid fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div
|
|
className="status-badge-enterprise"
|
|
style={{
|
|
backgroundColor: ticket.status_color || '#6366f1',
|
|
boxShadow: `0 0 20px ${ticket.status_color}33`
|
|
}}
|
|
>
|
|
<i className={`fa-solid ${getStatusIcon(ticket.status_name)}`}></i>
|
|
<span>{ticket.status_name}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Info Cards Section */}
|
|
<div className="ticket-info-cards">
|
|
<div className="info-card">
|
|
<i className="fa-solid fa-calendar-plus"></i>
|
|
<div>
|
|
<span className="card-label">Created</span>
|
|
<span className="card-value">{getRelativeTime(ticket.created_at)}</span>
|
|
</div>
|
|
</div>
|
|
<div className="info-card">
|
|
<i className="fa-solid fa-clock"></i>
|
|
<div>
|
|
<span className="card-label">Last Updated</span>
|
|
<span className="card-value">{getRelativeTime(ticket.updated_at)}</span>
|
|
</div>
|
|
</div>
|
|
{ticket.priority_name && (
|
|
<div className="info-card">
|
|
<i className={`fa-solid ${getPriorityIcon(ticket.priority_name)}`}></i>
|
|
<div>
|
|
<span className="card-label">Priority</span>
|
|
<span
|
|
className="card-value priority-value"
|
|
style={{ color: ticket.priority_color }}
|
|
>
|
|
{ticket.priority_name}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{ticket.category_name && (
|
|
<div className="info-card">
|
|
<i className="fa-solid fa-folder-open"></i>
|
|
<div>
|
|
<span className="card-label">Category</span>
|
|
<span className="card-value">{ticket.category_name}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="ticket-content-section">
|
|
<div className="content-header">
|
|
<h3>{ticket.title}</h3>
|
|
<div className="content-meta">
|
|
<span><i className="fa-solid fa-user"></i> {ticket.user_name}</span>
|
|
{ticket.company && (
|
|
<span><i className="fa-solid fa-building"></i> {ticket.company}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="description-section">
|
|
<h4><i className="fa-solid fa-align-left"></i> Description</h4>
|
|
<p>{ticket.description}</p>
|
|
</div>
|
|
|
|
{/* Messages Section */}
|
|
{ticket.messages && ticket.messages.filter(msg => !msg.is_internal).length > 0 && (
|
|
<div className="messages-section-enterprise">
|
|
<h4>
|
|
<i className="fa-solid fa-comments"></i>
|
|
Conversation
|
|
<span className="count-badge">{ticket.messages.filter(msg => !msg.is_internal).length}</span>
|
|
</h4>
|
|
<div className="messages-list-enterprise">
|
|
{ticket.messages
|
|
.filter(msg => !msg.is_internal)
|
|
.map((message) => (
|
|
<div key={message.id} className="message-card-enterprise">
|
|
<div className="message-avatar">
|
|
<i className="fa-solid fa-user"></i>
|
|
</div>
|
|
<div className="message-content-wrapper">
|
|
<div className="message-header-enterprise">
|
|
<span className="message-author">{message.author_name || message.author_email}</span>
|
|
<span className="message-time">{getRelativeTime(message.created_at)}</span>
|
|
</div>
|
|
<div className="message-text">
|
|
{message.content}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Activity Timeline */}
|
|
{ticket.activities && ticket.activities.length > 0 && (
|
|
<div className="timeline-section-enterprise">
|
|
<h4>
|
|
<i className="fa-solid fa-list-check"></i>
|
|
Activity Timeline
|
|
</h4>
|
|
<div className="timeline-items">
|
|
{ticket.activities.slice(0, 5).map((activity, index) => (
|
|
<div key={activity.id} className="timeline-item-enterprise">
|
|
<div className="timeline-marker">
|
|
<i className="fa-solid fa-circle"></i>
|
|
</div>
|
|
<div className="timeline-content-wrapper">
|
|
<div className="timeline-text">{activity.description}</div>
|
|
<div className="timeline-time">{getRelativeTime(activity.created_at)}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer Actions */}
|
|
<div className="ticket-footer-enterprise">
|
|
<div className="footer-info">
|
|
<i className="fa-solid fa-shield-check"></i>
|
|
<span>This ticket is securely tracked and monitored by our support team</span>
|
|
</div>
|
|
<button
|
|
className="btn-refresh"
|
|
onClick={() => handleSubmit({ preventDefault: () => {} } as FormEvent<HTMLFormElement>)}
|
|
>
|
|
<i className="fa-solid fa-rotate"></i>
|
|
Refresh Status
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TicketStatusCheck;
|
|
|