MotorSignal-App/website/components/ops/support-report-modal.tsx

241 lines
9.0 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { SupportTicketSummary } from './types';
interface SupportReportModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: (ticket: SupportTicketSummary) => void;
defaultReporterEmail?: string;
}
export function SupportReportModal({
isOpen,
onClose,
onSuccess,
defaultReporterEmail = '',
}: SupportReportModalProps) {
const [ticketType, setTicketType] = useState('general');
const [reporterEmail, setReporterEmail] = useState(defaultReporterEmail || '');
const [subject, setSubject] = useState('');
const [description, setDescription] = useState('');
const [severity, setSeverity] = useState('medium');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const modalRef = useRef<HTMLDivElement>(null);
const firstInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen) {
setTicketType('general');
setReporterEmail(defaultReporterEmail || '');
setSubject('');
setDescription('');
setSeverity('medium');
setError(null);
// Focus first input after render
setTimeout(() => firstInputRef.current?.focus(), 100);
}
}, [isOpen, defaultReporterEmail]);
// Keyboard accessibility
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!isOpen) return;
if (e.key === 'Escape') {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting) return;
const trimmedEmail = reporterEmail.trim();
const trimmedSubject = subject.trim();
const trimmedDescription = description.trim();
if (!trimmedSubject) {
setError('Subject cannot be empty or contain only whitespace.');
return;
}
if (!trimmedDescription) {
setError('Description cannot be empty or contain only whitespace.');
return;
}
setIsSubmitting(true);
setError(null);
try {
const resp = await fetch('/api/ops/support/tickets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ticket_type: ticketType,
reporter_email: trimmedEmail,
subject: trimmedSubject,
description: trimmedDescription,
severity,
})
});
const data = await resp.json().catch(() => null);
if (resp.ok) {
onSuccess(data);
onClose();
} else {
setError(data?.detail || 'Failed to submit report');
}
} catch {
setError('Network error during submission');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div
ref={modalRef}
className="bg-surface-container-high border border-outline-variant/30 w-full max-w-lg rounded-2xl shadow-xl overflow-hidden flex flex-col"
>
<div className="px-6 py-4 border-b border-outline-variant/20 flex items-center justify-between">
<h2 id="modal-title" className="text-lg font-bold text-on-surface">New Support Report</h2>
<button
type="button"
onClick={onClose}
className="p-1 rounded-full text-outline hover:bg-surface-container-highest hover:text-on-surface transition-colors"
aria-label="Close"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-6 space-y-4">
{error && (
<div className="p-3 text-sm bg-error/10 text-error border border-error/20 rounded-lg">
{error}
</div>
)}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<label htmlFor="reporter_email" className="block text-sm font-medium text-on-surface">Reporter Email</label>
{reporterEmail && (
<button
type="button"
onClick={() => setReporterEmail('')}
className="text-xs font-semibold text-primary hover:text-primary/80 transition-colors"
aria-label="Clear email"
>
Clear
</button>
)}
</div>
<input
id="reporter_email"
ref={firstInputRef}
type="email"
required
minLength={3}
maxLength={255}
value={reporterEmail}
onChange={e => setReporterEmail(e.target.value)}
className="w-full px-3 py-2 bg-surface-container-lowest border border-outline-variant/50 rounded-lg text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/50"
placeholder="user@example.com"
/>
<p className="text-[11px] text-outline">
Auto-populated with your verified operator account. You may edit or clear this if reporting for a rider.
</p>
</div>
<div className="space-y-1.5">
<label htmlFor="ticket_type" className="block text-sm font-medium text-on-surface">Ticket Type</label>
<select
id="ticket_type"
value={ticketType}
onChange={e => setTicketType(e.target.value)}
className="w-full px-3 py-2 bg-surface-container-lowest border border-outline-variant/50 rounded-lg text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/50"
>
<option value="general">General</option>
<option value="bug_report">Bug Report</option>
<option value="telemetry_issue">Telemetry Issue</option>
<option value="contact_request">Contact Request</option>
</select>
</div>
<div className="space-y-1.5">
<label htmlFor="severity" className="block text-sm font-medium text-on-surface">Severity</label>
<select
id="severity"
value={severity}
onChange={e => setSeverity(e.target.value)}
className="w-full px-3 py-2 bg-surface-container-lowest border border-outline-variant/50 rounded-lg text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/50"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
<div className="space-y-1.5">
<label htmlFor="subject" className="block text-sm font-medium text-on-surface">Subject</label>
<input
id="subject"
type="text"
required
minLength={1}
maxLength={255}
value={subject}
onChange={e => setSubject(e.target.value)}
className="w-full px-3 py-2 bg-surface-container-lowest border border-outline-variant/50 rounded-lg text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/50"
placeholder="Brief summary"
/>
</div>
<div className="space-y-1.5">
<label htmlFor="description" className="block text-sm font-medium text-on-surface">Description</label>
<textarea
id="description"
required
minLength={1}
maxLength={10000}
value={description}
onChange={e => setDescription(e.target.value)}
className="w-full px-3 py-2 bg-surface-container-lowest border border-outline-variant/50 rounded-lg text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/50 min-h-[120px]"
placeholder="Detailed description..."
/>
</div>
<div className="pt-4 flex items-center justify-end gap-3 border-t border-outline-variant/20">
<button
type="button"
onClick={onClose}
disabled={isSubmitting}
className="px-4 py-2 text-sm font-semibold text-on-surface-variant hover:text-on-surface transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-5 py-2 text-sm font-semibold bg-primary text-on-primary rounded-xl hover:bg-primary/90 transition-colors shadow-sm disabled:opacity-50"
>
{isSubmitting ? 'Submitting...' : 'Create Ticket'}
</button>
</div>
</form>
</div>
</div>
);
}