471 lines
18 KiB
TypeScript
471 lines
18 KiB
TypeScript
"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;
|
|
|