update
This commit is contained in:
470
frontEnd/components/pages/support/CreateTicketForm.tsx
Normal file
470
frontEnd/components/pages/support/CreateTicketForm.tsx
Normal file
@@ -0,0 +1,470 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { createTicket, CreateTicketData } from '@/lib/api/supportService';
|
||||
import { useTicketCategories } from '@/lib/hooks/useSupport';
|
||||
|
||||
interface CreateTicketFormProps {
|
||||
onOpenStatusCheck?: () => void;
|
||||
}
|
||||
|
||||
const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
const { categories, loading: categoriesLoading } = useTicketCategories();
|
||||
|
||||
const [formData, setFormData] = useState<CreateTicketData>({
|
||||
title: '',
|
||||
description: '',
|
||||
ticket_type: 'general',
|
||||
user_name: '',
|
||||
user_email: '',
|
||||
user_phone: '',
|
||||
company: '',
|
||||
category: undefined
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
const [ticketNumber, setTicketNumber] = useState<string>('');
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: name === 'category' ? (value ? parseInt(value) : undefined) : value
|
||||
}));
|
||||
// Clear field error when user starts typing
|
||||
if (fieldErrors[name]) {
|
||||
setFieldErrors(prev => ({ ...prev, [name]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!formData.user_name.trim()) {
|
||||
errors.user_name = 'Full name is required';
|
||||
}
|
||||
|
||||
if (!formData.user_email.trim()) {
|
||||
errors.user_email = 'Email address is required';
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.user_email)) {
|
||||
errors.user_email = 'Please enter a valid email address';
|
||||
}
|
||||
|
||||
if (!formData.title.trim()) {
|
||||
errors.title = 'Subject is required';
|
||||
}
|
||||
|
||||
if (!formData.description.trim()) {
|
||||
errors.description = 'Description is required';
|
||||
} else if (formData.description.trim().length < 10) {
|
||||
errors.description = 'Please provide a more detailed description (minimum 10 characters)';
|
||||
}
|
||||
|
||||
setFieldErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setSubmitError(null);
|
||||
setSubmitSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await createTicket(formData);
|
||||
setTicketNumber(response.ticket_number);
|
||||
setSubmitSuccess(true);
|
||||
|
||||
// Reset form
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
ticket_type: 'general',
|
||||
user_name: '',
|
||||
user_email: '',
|
||||
user_phone: '',
|
||||
company: '',
|
||||
category: undefined
|
||||
});
|
||||
setFieldErrors({});
|
||||
} catch (error: any) {
|
||||
console.error('Ticket creation error:', error);
|
||||
|
||||
// Provide user-friendly error messages based on error type
|
||||
let errorMessage = 'Failed to submit ticket. Please try again.';
|
||||
|
||||
if (error.message) {
|
||||
if (error.message.includes('email')) {
|
||||
errorMessage = 'There was an issue with your email address. Please check and try again.';
|
||||
} else if (error.message.includes('network') || error.message.includes('fetch')) {
|
||||
errorMessage = 'Network error. Please check your connection and try again.';
|
||||
} else if (error.message.includes('validation')) {
|
||||
errorMessage = 'Please check all required fields and try again.';
|
||||
} else if (error.message.includes('server') || error.message.includes('500')) {
|
||||
errorMessage = 'Server error. Our team has been notified. Please try again later.';
|
||||
} else {
|
||||
// Use the actual error message if it's user-friendly
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
setSubmitError(errorMessage);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (submitSuccess) {
|
||||
return (
|
||||
<div className="ticket-success">
|
||||
<div className="success-icon">
|
||||
<i className="fa-solid fa-circle-check"></i>
|
||||
</div>
|
||||
<h3>Ticket Created Successfully!</h3>
|
||||
<div className="ticket-number-container">
|
||||
<div className="ticket-number-label">Your Ticket Number</div>
|
||||
<div className="ticket-number-value">{ticketNumber}</div>
|
||||
<button
|
||||
className="btn-copy-ticket"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(ticketNumber);
|
||||
const btn = document.querySelector('.btn-copy-ticket');
|
||||
if (btn) {
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = 'Copy';
|
||||
}, 2000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p className="ticket-info">
|
||||
We've received your support request and will respond as soon as possible.
|
||||
Please save your ticket number for future reference.
|
||||
</p>
|
||||
<div className="success-actions">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setSubmitSuccess(false)}
|
||||
>
|
||||
<i className="fa-solid fa-plus me-2"></i>
|
||||
Submit Another Ticket
|
||||
</button>
|
||||
{onOpenStatusCheck && (
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
onClick={onOpenStatusCheck}
|
||||
>
|
||||
<i className="fa-solid fa-search me-2"></i>
|
||||
Check Ticket Status
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const issueTypeIcons: Record<string, string> = {
|
||||
general: 'fa-info-circle',
|
||||
technical: 'fa-tools',
|
||||
billing: 'fa-credit-card',
|
||||
feature_request: 'fa-lightbulb',
|
||||
bug_report: 'fa-bug',
|
||||
account: 'fa-user-circle'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="create-ticket-form enterprise-form">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12">
|
||||
<div className="form-header-enterprise">
|
||||
<div className="form-header-icon">
|
||||
<i className="fa-solid fa-ticket"></i>
|
||||
</div>
|
||||
<h2>Submit a Support Ticket</h2>
|
||||
<p>Fill out the form below and our dedicated support team will get back to you within 24 hours.</p>
|
||||
<div className="info-banner">
|
||||
<i className="fa-solid fa-shield-check"></i>
|
||||
<div>
|
||||
<strong>Secure & Confidential</strong>
|
||||
<span>All tickets are encrypted and handled with enterprise-grade security standards.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{submitError && (
|
||||
<div className="alert-enterprise alert-error">
|
||||
<i className="fa-solid fa-triangle-exclamation"></i>
|
||||
<div>
|
||||
<strong>Submission Error</strong>
|
||||
<p>{submitError}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="support-form-enterprise">
|
||||
{/* Personal Information */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
<i className="fa-solid fa-user"></i>
|
||||
<h3>Personal Information</h3>
|
||||
</div>
|
||||
|
||||
<div className="row g-4">
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="user_name" className="form-label-enterprise">
|
||||
<span>Full Name</span>
|
||||
<span className="required-badge">Required</span>
|
||||
</label>
|
||||
<div className="input-with-icon">
|
||||
<i className="fa-solid fa-user"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="user_name"
|
||||
name="user_name"
|
||||
value={formData.user_name}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className={`form-control-enterprise ${fieldErrors.user_name ? 'error' : ''}`}
|
||||
placeholder="Enter your full name"
|
||||
/>
|
||||
</div>
|
||||
{fieldErrors.user_name && (
|
||||
<span className="field-error">{fieldErrors.user_name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="user_email" className="form-label-enterprise">
|
||||
<span>Email Address</span>
|
||||
<span className="required-badge">Required</span>
|
||||
</label>
|
||||
<div className="input-with-icon">
|
||||
<i className="fa-solid fa-envelope"></i>
|
||||
<input
|
||||
type="email"
|
||||
id="user_email"
|
||||
name="user_email"
|
||||
value={formData.user_email}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className={`form-control-enterprise ${fieldErrors.user_email ? 'error' : ''}`}
|
||||
placeholder="your.email@company.com"
|
||||
/>
|
||||
</div>
|
||||
{fieldErrors.user_email && (
|
||||
<span className="field-error">{fieldErrors.user_email}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="user_phone" className="form-label-enterprise">
|
||||
<span>Phone Number</span>
|
||||
<span className="optional-badge">Optional</span>
|
||||
</label>
|
||||
<div className="input-with-icon">
|
||||
<i className="fa-solid fa-phone"></i>
|
||||
<input
|
||||
type="tel"
|
||||
id="user_phone"
|
||||
name="user_phone"
|
||||
value={formData.user_phone}
|
||||
onChange={handleInputChange}
|
||||
className="form-control-enterprise"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="company" className="form-label-enterprise">
|
||||
<span>Company Name</span>
|
||||
<span className="optional-badge">Optional</span>
|
||||
</label>
|
||||
<div className="input-with-icon">
|
||||
<i className="fa-solid fa-building"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="company"
|
||||
name="company"
|
||||
value={formData.company}
|
||||
onChange={handleInputChange}
|
||||
className="form-control-enterprise"
|
||||
placeholder="Your Company Inc."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ticket Details */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
<i className="fa-solid fa-clipboard-list"></i>
|
||||
<h3>Ticket Details</h3>
|
||||
</div>
|
||||
|
||||
<div className="row g-4">
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="ticket_type" className="form-label-enterprise">
|
||||
<span>Issue Type</span>
|
||||
<span className="required-badge">Required</span>
|
||||
</label>
|
||||
<div className="select-with-icon">
|
||||
<i className={`fa-solid ${issueTypeIcons[formData.ticket_type] || 'fa-tag'}`}></i>
|
||||
<select
|
||||
id="ticket_type"
|
||||
name="ticket_type"
|
||||
value={formData.ticket_type}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control-enterprise select-enterprise"
|
||||
>
|
||||
<option value="general">General Inquiry</option>
|
||||
<option value="technical">Technical Issue</option>
|
||||
<option value="billing">Billing Question</option>
|
||||
<option value="feature_request">Feature Request</option>
|
||||
<option value="bug_report">Bug Report</option>
|
||||
<option value="account">Account Issue</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="category" className="form-label-enterprise">
|
||||
<span>Category</span>
|
||||
<span className="optional-badge">Optional</span>
|
||||
</label>
|
||||
<div className="select-with-icon">
|
||||
<i className="fa-solid fa-folder"></i>
|
||||
<select
|
||||
id="category"
|
||||
name="category"
|
||||
value={formData.category || ''}
|
||||
onChange={handleInputChange}
|
||||
className="form-control-enterprise select-enterprise"
|
||||
disabled={categoriesLoading}
|
||||
>
|
||||
<option value="">Select a category</option>
|
||||
{Array.isArray(categories) && categories.map(cat => (
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="title" className="form-label-enterprise">
|
||||
<span>Subject</span>
|
||||
<span className="required-badge">Required</span>
|
||||
</label>
|
||||
<div className="input-with-icon">
|
||||
<i className="fa-solid fa-heading"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className={`form-control-enterprise ${fieldErrors.title ? 'error' : ''}`}
|
||||
placeholder="Brief, descriptive subject line"
|
||||
/>
|
||||
</div>
|
||||
{fieldErrors.title && (
|
||||
<span className="field-error">{fieldErrors.title}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="description" className="form-label-enterprise">
|
||||
<span>Description</span>
|
||||
<span className="required-badge">Required</span>
|
||||
<span className="char-count">{formData.description.length} characters</span>
|
||||
</label>
|
||||
<div className="textarea-wrapper">
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className={`form-control-enterprise textarea-enterprise ${fieldErrors.description ? 'error' : ''}`}
|
||||
rows={6}
|
||||
placeholder="Please provide detailed information about your issue, including any error messages, steps to reproduce, or relevant context..."
|
||||
/>
|
||||
<div className="textarea-footer">
|
||||
<i className="fa-solid fa-lightbulb"></i>
|
||||
<span>Tip: More details help us resolve your issue faster</span>
|
||||
</div>
|
||||
</div>
|
||||
{fieldErrors.description && (
|
||||
<span className="field-error">{fieldErrors.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Section */}
|
||||
<div className="form-submit-section">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-submit-enterprise"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span className="spinner-enterprise"></span>
|
||||
<span>Submitting Ticket...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="fa-solid fa-paper-plane"></i>
|
||||
<span>Submit Ticket</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<p className="submit-note">
|
||||
<i className="fa-solid fa-clock"></i>
|
||||
Average response time: <strong>2-4 hours</strong>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTicketForm;
|
||||
|
||||
217
frontEnd/components/pages/support/KnowledgeBase.tsx
Normal file
217
frontEnd/components/pages/support/KnowledgeBase.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
"use client";
|
||||
import { useState } from 'react';
|
||||
import { useKnowledgeBaseCategories, useFeaturedArticles, useKnowledgeBaseArticles } from '@/lib/hooks/useSupport';
|
||||
import KnowledgeBaseArticleModal from './KnowledgeBaseArticleModal';
|
||||
|
||||
const KnowledgeBase = () => {
|
||||
const { categories, loading: categoriesLoading } = useKnowledgeBaseCategories();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [selectedArticleSlug, setSelectedArticleSlug] = useState<string | null>(null);
|
||||
|
||||
// Fetch all articles (for browsing and category filtering)
|
||||
const { articles: allArticles, loading: allArticlesLoading } = useKnowledgeBaseArticles();
|
||||
|
||||
// Fetch featured articles (for default view)
|
||||
const { articles: featuredArticles, loading: featuredLoading } = useFeaturedArticles();
|
||||
|
||||
// Determine which articles to display
|
||||
let displayArticles = featuredArticles;
|
||||
let isLoading = featuredLoading;
|
||||
let headerText = 'Featured Articles';
|
||||
|
||||
if (searchTerm) {
|
||||
// If searching, filter all articles by search term
|
||||
displayArticles = allArticles.filter(article =>
|
||||
article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
article.summary.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
article.content.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
isLoading = allArticlesLoading;
|
||||
headerText = 'Search Results';
|
||||
} else if (selectedCategory) {
|
||||
// If a category is selected, filter articles by that category
|
||||
displayArticles = allArticles.filter(article => article.category_slug === selectedCategory);
|
||||
isLoading = allArticlesLoading;
|
||||
const categoryName = categories.find(cat => cat.slug === selectedCategory)?.name || 'Category';
|
||||
headerText = `${categoryName} Articles`;
|
||||
}
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// The search is already being performed by the hook
|
||||
};
|
||||
|
||||
const filteredCategories = selectedCategory
|
||||
? categories.filter(cat => cat.slug === selectedCategory)
|
||||
: categories;
|
||||
|
||||
return (
|
||||
<div className="knowledge-base">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10">
|
||||
<div className="form-header text-center">
|
||||
<h2>Knowledge Base</h2>
|
||||
<p>Find answers to frequently asked questions and explore our documentation.</p>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<form onSubmit={handleSearch} className="kb-search-form">
|
||||
<div className="search-input-group">
|
||||
<i className="fa-solid fa-search search-icon"></i>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search articles, topics, or keywords..."
|
||||
className="form-control"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
type="button"
|
||||
className="clear-search"
|
||||
onClick={() => setSearchTerm('')}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Categories */}
|
||||
{!searchTerm && (
|
||||
<div className="kb-categories">
|
||||
<h3>Browse by Category</h3>
|
||||
{categoriesLoading ? (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row g-4">
|
||||
{Array.isArray(categories) && categories.map(category => (
|
||||
<div key={category.id} className="col-md-6 col-lg-4">
|
||||
<div
|
||||
className="category-card"
|
||||
onClick={() => setSelectedCategory(category.slug)}
|
||||
style={{ borderLeftColor: category.color }}
|
||||
>
|
||||
<div
|
||||
className="category-icon"
|
||||
style={{ color: category.color }}
|
||||
>
|
||||
<i className={`fa-solid ${category.icon}`}></i>
|
||||
</div>
|
||||
<div className="category-content">
|
||||
<h4>{category.name}</h4>
|
||||
<p>{category.description}</p>
|
||||
<div className="category-meta">
|
||||
<span className="article-count">
|
||||
{category.article_count} {category.article_count === 1 ? 'article' : 'articles'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Featured/Search Results Articles */}
|
||||
<div className="kb-articles">
|
||||
<div className="articles-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<h3>{headerText}</h3>
|
||||
{selectedCategory && !searchTerm && (
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={() => setSelectedCategory(null)}
|
||||
>
|
||||
<i className="fa-solid fa-arrow-left me-2"></i>
|
||||
Back to All Articles
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{searchTerm && (
|
||||
<p className="search-info">
|
||||
Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : displayArticles.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<i className="fa-solid fa-search empty-icon"></i>
|
||||
<h4>No articles found</h4>
|
||||
<p>
|
||||
{searchTerm
|
||||
? `We couldn't find any articles matching "${searchTerm}". Try different keywords.`
|
||||
: 'No articles available at the moment.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="articles-list">
|
||||
{Array.isArray(displayArticles) && displayArticles.map(article => (
|
||||
<div
|
||||
key={article.id}
|
||||
className="article-item"
|
||||
onClick={() => setSelectedArticleSlug(article.slug)}
|
||||
>
|
||||
<div className="article-header">
|
||||
<h4>{article.title}</h4>
|
||||
{article.is_featured && (
|
||||
<span className="featured-badge">
|
||||
<i className="fa-solid fa-star me-1"></i>
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="article-summary">{article.summary}</p>
|
||||
<div className="article-meta">
|
||||
<span className="article-category">
|
||||
<i className="fa-solid fa-folder me-1"></i>
|
||||
{article.category_name}
|
||||
</span>
|
||||
<span className="article-stats">
|
||||
<i className="fa-solid fa-eye me-1"></i>
|
||||
{article.view_count} views
|
||||
</span>
|
||||
<span className="article-stats">
|
||||
<i className="fa-solid fa-thumbs-up me-1"></i>
|
||||
{article.helpful_count} helpful
|
||||
</span>
|
||||
</div>
|
||||
<button className="article-read-more">
|
||||
Read More <i className="fa-solid fa-arrow-right ms-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Article Modal */}
|
||||
{selectedArticleSlug && (
|
||||
<KnowledgeBaseArticleModal
|
||||
slug={selectedArticleSlug}
|
||||
onClose={() => setSelectedArticleSlug(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBase;
|
||||
|
||||
138
frontEnd/components/pages/support/KnowledgeBaseArticleModal.tsx
Normal file
138
frontEnd/components/pages/support/KnowledgeBaseArticleModal.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useKnowledgeBaseArticle } from '@/lib/hooks/useSupport';
|
||||
import { markArticleHelpful } from '@/lib/api/supportService';
|
||||
|
||||
interface KnowledgeBaseArticleModalProps {
|
||||
slug: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const KnowledgeBaseArticleModal = ({ slug, onClose }: KnowledgeBaseArticleModalProps) => {
|
||||
const { article, loading, error } = useKnowledgeBaseArticle(slug);
|
||||
const [feedbackGiven, setFeedbackGiven] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent body scroll when modal is open
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleFeedback = async (helpful: boolean) => {
|
||||
if (!article || feedbackGiven) return;
|
||||
|
||||
try {
|
||||
await markArticleHelpful(slug, helpful);
|
||||
setFeedbackGiven(true);
|
||||
} catch (error) {
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="kb-modal-overlay" onClick={onClose}>
|
||||
<div className="kb-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="modal-close" onClick={onClose} aria-label="Close modal">
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
|
||||
<div className="modal-content">
|
||||
{loading && (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p>Loading article...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-state">
|
||||
<i className="fa-solid fa-triangle-exclamation"></i>
|
||||
<h3>Error Loading Article</h3>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{article && (
|
||||
<>
|
||||
<div className="article-header">
|
||||
{article.is_featured && (
|
||||
<span className="featured-badge">
|
||||
<i className="fa-solid fa-star me-1"></i>
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
<h2>{article.title}</h2>
|
||||
<div className="article-meta">
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-folder me-1"></i>
|
||||
{article.category_name}
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-calendar me-1"></i>
|
||||
{formatDate(article.published_at || article.created_at)}
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-eye me-1"></i>
|
||||
{article.view_count} views
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="article-body">
|
||||
<div
|
||||
className="article-content"
|
||||
dangerouslySetInnerHTML={{ __html: article.content || article.summary }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="article-footer">
|
||||
<div className="article-feedback">
|
||||
<h4>Was this article helpful?</h4>
|
||||
{feedbackGiven ? (
|
||||
<p className="feedback-thanks">
|
||||
<i className="fa-solid fa-check-circle me-2"></i>
|
||||
Thank you for your feedback!
|
||||
</p>
|
||||
) : (
|
||||
<div className="feedback-buttons">
|
||||
<button
|
||||
className="btn btn-outline-success"
|
||||
onClick={() => handleFeedback(true)}
|
||||
>
|
||||
<i className="fa-solid fa-thumbs-up me-2"></i>
|
||||
Yes ({article.helpful_count})
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
onClick={() => handleFeedback(false)}
|
||||
>
|
||||
<i className="fa-solid fa-thumbs-down me-2"></i>
|
||||
No ({article.not_helpful_count})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseArticleModal;
|
||||
|
||||
174
frontEnd/components/pages/support/SupportCenterContent.tsx
Normal file
174
frontEnd/components/pages/support/SupportCenterContent.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
"use client";
|
||||
import { useEffect } from 'react';
|
||||
import CreateTicketForm from './CreateTicketForm';
|
||||
import KnowledgeBase from './KnowledgeBase';
|
||||
import TicketStatusCheck from './TicketStatusCheck';
|
||||
|
||||
type ModalType = 'create' | 'knowledge' | 'status' | null;
|
||||
|
||||
interface SupportCenterContentProps {
|
||||
activeModal: ModalType;
|
||||
onClose: () => void;
|
||||
onOpenModal?: (type: ModalType) => void;
|
||||
}
|
||||
|
||||
const SupportCenterContent = ({ activeModal, onClose, onOpenModal }: SupportCenterContentProps) => {
|
||||
// Close modal on escape key
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && activeModal) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => document.removeEventListener('keydown', handleEscape);
|
||||
}, [activeModal, onClose]);
|
||||
|
||||
// Prevent body scroll when modal is open
|
||||
useEffect(() => {
|
||||
if (activeModal) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [activeModal]);
|
||||
|
||||
if (!activeModal) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modal Overlay */}
|
||||
<div
|
||||
className="support-modal-overlay"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
zIndex: 9998,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '20px',
|
||||
backdropFilter: 'blur(5px)',
|
||||
}}
|
||||
>
|
||||
{/* Modal Content */}
|
||||
<div
|
||||
className="support-modal-content"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '12px',
|
||||
maxWidth: '1000px',
|
||||
width: '100%',
|
||||
maxHeight: '90vh',
|
||||
overflow: 'auto',
|
||||
position: 'relative',
|
||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||
animation: 'modalSlideIn 0.3s ease-out',
|
||||
}}
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="support-modal-close"
|
||||
aria-label="Close modal"
|
||||
style={{
|
||||
position: 'sticky',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
float: 'right',
|
||||
background: '#f3f4f6',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: '20px',
|
||||
color: '#374151',
|
||||
transition: 'all 0.2s',
|
||||
zIndex: 10,
|
||||
marginBottom: '-40px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#e5e7eb';
|
||||
e.currentTarget.style.transform = 'scale(1.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#f3f4f6';
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="support-modal-body" style={{ padding: '40px' }}>
|
||||
{activeModal === 'create' && (
|
||||
<CreateTicketForm onOpenStatusCheck={() => {
|
||||
if (onOpenModal) {
|
||||
onOpenModal('status');
|
||||
}
|
||||
}} />
|
||||
)}
|
||||
{activeModal === 'knowledge' && <KnowledgeBase />}
|
||||
{activeModal === 'status' && <TicketStatusCheck />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Animation Keyframes */}
|
||||
<style jsx>{`
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.support-modal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.support-modal-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.support-modal-content::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.support-modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.support-modal-body {
|
||||
padding: 20px !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportCenterContent;
|
||||
|
||||
151
frontEnd/components/pages/support/SupportCenterHero.tsx
Normal file
151
frontEnd/components/pages/support/SupportCenterHero.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
type ModalType = 'create' | 'knowledge' | 'status' | null;
|
||||
|
||||
interface SupportCenterHeroProps {
|
||||
onFeatureClick: (type: ModalType) => void;
|
||||
}
|
||||
|
||||
const SupportCenterHero = ({ onFeatureClick }: SupportCenterHeroProps) => {
|
||||
return (
|
||||
<section className="support-hero">
|
||||
{/* Animated Background */}
|
||||
<div className="hero-background">
|
||||
{/* Floating Support Icons */}
|
||||
<div className="floating-tech tech-1">
|
||||
<i className="fa-solid fa-headset"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-2">
|
||||
<i className="fa-solid fa-ticket"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-3">
|
||||
<i className="fa-solid fa-book"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-4">
|
||||
<i className="fa-solid fa-comments"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-5">
|
||||
<i className="fa-solid fa-life-ring"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-6">
|
||||
<i className="fa-solid fa-user-shield"></i>
|
||||
</div>
|
||||
|
||||
{/* Grid Pattern */}
|
||||
<div className="grid-overlay"></div>
|
||||
|
||||
{/* Animated Gradient Orbs */}
|
||||
<div className="gradient-orb orb-1"></div>
|
||||
<div className="gradient-orb orb-2"></div>
|
||||
<div className="gradient-orb orb-3"></div>
|
||||
|
||||
{/* Video Overlay */}
|
||||
<div className="video-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="support-hero__content text-center">
|
||||
<h1 className="support-hero__title">
|
||||
Support Center
|
||||
</h1>
|
||||
<p className="support-hero__subtitle">
|
||||
Get expert assistance whenever you need it. Our dedicated support team is here to help you succeed.
|
||||
</p>
|
||||
|
||||
<div className="support-hero__features">
|
||||
<div className="row g-3 g-md-4 g-lg-5 justify-content-center">
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<div
|
||||
className="feature-item clickable"
|
||||
onClick={() => onFeatureClick('create')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('create')}
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-ticket"></i>
|
||||
</div>
|
||||
<h3>Submit Tickets</h3>
|
||||
<p>Create and track support requests</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<div
|
||||
className="feature-item clickable"
|
||||
onClick={() => onFeatureClick('knowledge')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('knowledge')}
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-book"></i>
|
||||
</div>
|
||||
<h3>Knowledge Base</h3>
|
||||
<p>Find answers to common questions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<div
|
||||
className="feature-item clickable"
|
||||
onClick={() => onFeatureClick('status')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('status')}
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-search"></i>
|
||||
</div>
|
||||
<h3>Track Status</h3>
|
||||
<p>Monitor your ticket progress</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<a
|
||||
href="/policy?type=privacy"
|
||||
className="feature-item clickable link-item"
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-shield-halved"></i>
|
||||
</div>
|
||||
<h3>Privacy Policy</h3>
|
||||
<p>Learn about data protection</p>
|
||||
</a>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<a
|
||||
href="/policy?type=terms"
|
||||
className="feature-item clickable link-item"
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-file-contract"></i>
|
||||
</div>
|
||||
<h3>Terms of Use</h3>
|
||||
<p>Review our service terms</p>
|
||||
</a>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<a
|
||||
href="/policy?type=support"
|
||||
className="feature-item clickable link-item"
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-headset"></i>
|
||||
</div>
|
||||
<h3>Support Policy</h3>
|
||||
<p>Understand our support coverage</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportCenterHero;
|
||||
|
||||
323
frontEnd/components/pages/support/TicketStatusCheck.tsx
Normal file
323
frontEnd/components/pages/support/TicketStatusCheck.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } 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);
|
||||
|
||||
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 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 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;
|
||||
|
||||
Reference in New Issue
Block a user