1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4// import removed: TenantSelector
5import '../styles/data-table.css';
7import { apiFetch } from '../lib/api';
8import { isAdmin } from '../utils/auth';
9import { notifySuccess, notifyError } from '../utils/notifications';
13 const token = localStorage.getItem('token');
17 const payload = token.split('.')[1];
18 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
19 tenantId = decoded.tenant_id || decoded.tenantId || '';
22 const navigate = useNavigate();
23 const [tickets, setTickets] = useState([]);
24 const [loading, setLoading] = useState(false);
25 const [error, setError] = useState('');
26 const [isMSP, setIsMSP] = useState(false);
27 const [tenantsMap, setTenantsMap] = useState({});
28 const [page, setPage] = useState(1);
29 const [totalPages, setTotalPages] = useState(1);
30 const [search, setSearch] = useState('');
31 const [showFilters, setShowFilters] = useState(false);
32 const [deleteConfirm, setDeleteConfirm] = useState(null);
35 // Use React state for filters only (no persistence)
36 const [filters, setFilters] = useState({
45 const fetchTickets = async () => {
48 const urlParams = new URLSearchParams();
49 urlParams.set('page', page);
50 urlParams.set('limit', perPage);
52 if (search) urlParams.set('search', search);
53 if (filters.status && filters.status !== 'all') urlParams.set('status', filters.status);
54 if (filters.priority && filters.priority !== 'all') urlParams.set('priority', filters.priority);
55 if (filters.sla_filter && filters.sla_filter !== 'all') urlParams.set('sla_filter', filters.sla_filter);
56 if (filters.date_from) urlParams.set('date_from', filters.date_from);
57 if (filters.date_to) urlParams.set('date_to', filters.date_to);
59 // No need to handle tenant_id — apiFetch injects it automatically
60 // But if you *want* to include it explicitly, you can:
61 // if (selectedTenantId) urlParams.set('tenant_id', selectedTenantId);
63 const res = await apiFetch(`/tickets?${urlParams.toString()}`, {
67 if (!res.ok) throw new Error('Failed to load tickets');
69 const data = await res.json();
70 setTickets(data.tickets);
71 setTotalPages(Math.ceil(data.total / perPage));
74 setError(err.message);
81 // Clear tickets immediately to avoid showing stale data
84 }, [page, search, filters, tenantId]);
86 // Removed selectedTenantId effect; only use tenantId from JWT
88 // Detect MSP and load tenants for mapping
92 const res = await apiFetch('/tenants', { method: 'GET', credentials: 'include' });
95 const data = await res.json();
97 (data.tenants || data || []).forEach(t => { if (t.tenant_id) map[t.tenant_id] = t.name; });
108 const handleCreateClick = () => {
109 navigate('/tickets/new');
112 const handleEditClick = (ticketId) => {
113 navigate(`/tickets/${ticketId}/edit`);
116 const handleFilterChange = (key, value) => {
117 setFilters(prev => ({ ...prev, [key]: value }));
118 setPage(1); // Reset to first page when filters change
121 const handleClearFilters = () => {
122 const defaultFilters = {
129 setFilters(defaultFilters);
134 const handleDeleteTicket = async (ticketId) => {
137 const token = localStorage.getItem('token');
138 const res = await apiFetch(`/tickets/${ticketId}`, {
140 headers: { Authorization: `Bearer ${token}` }
143 const errorData = await res.json();
144 const errorMsg = errorData.error || 'Failed to delete ticket';
145 await notifyError('Delete Failed', errorMsg);
146 throw new Error(errorMsg);
148 setDeleteConfirm(null);
149 await notifySuccess('Ticket Deleted', 'Ticket has been deleted successfully');
152 setError(err.message);
158 const handleDeleteClick = (ticket) => {
159 setDeleteConfirm(ticket);
162 const hasActiveFilters = () => {
163 return filters.status !== 'all' ||
164 filters.priority !== 'all' ||
165 filters.sla_filter !== 'all' ||
174 <div className="page-content">
175 <div className="page-header">
176 <div className="header-content">
178 <button className="btn primary" onClick={handleCreateClick}>
179 <span className="material-symbols-outlined">add</span>
185 {/* Search and Filter Bar */}
186 <div className="toolbar">
187 <div className="toolbar-left">
188 <div className="search-box">
189 <span className="material-symbols-outlined">search</span>
193 placeholder="Search tickets by title or description..."
195 onChange={e => { setSearch(e.target.value); setPage(1); }}
199 <div className="toolbar-right">
201 className={`btn ${showFilters ? 'active' : ''}`}
202 onClick={() => setShowFilters(!showFilters)}
203 title="Toggle filters"
205 <span className="material-symbols-outlined">tune</span>
207 {hasActiveFilters() && (
208 <span className="badge">{Object.values(filters).filter(v => v && v !== 'all').length + (search ? 1 : 0)}</span>
211 {hasActiveFilters() && (
212 <button className="btn" onClick={handleClearFilters}>
213 <span className="material-symbols-outlined">clear_all</span>
222 <div className="filter-panel">
223 <h3>Filter Options</h3>
224 <div className="filter-grid">
225 <div className="filter-group">
226 <label>Status</label>
228 value={filters.status}
229 onChange={e => handleFilterChange('status', e.target.value)}
231 <option value="all">All Statuses</option>
232 <option value="open">Open</option>
233 <option value="in-progress">In Progress</option>
234 <option value="pending">Pending</option>
235 <option value="resolved">Resolved</option>
236 <option value="closed">Closed</option>
240 <div className="filter-group">
241 <label>Priority</label>
243 value={filters.priority}
244 onChange={e => handleFilterChange('priority', e.target.value)}
246 <option value="all">All Priorities</option>
247 <option value="low">Low</option>
248 <option value="medium">Medium</option>
249 <option value="high">High</option>
250 <option value="urgent">Urgent</option>
254 <div className="filter-group">
255 <label>SLA Status</label>
257 value={filters.sla_filter}
258 onChange={e => handleFilterChange('sla_filter', e.target.value)}
260 <option value="all">All Tickets</option>
261 <option value="overdue">Overdue (SLA breach)</option>
262 <option value="due_today">Active Today</option>
263 <option value="due_this_week">This Week</option>
267 <div className="filter-group">
268 <label>Date From</label>
270 id="ticket-date-from"
271 name="ticket-date-from"
273 value={filters.date_from}
274 onChange={e => handleFilterChange('date_from', e.target.value)}
278 <div className="filter-group">
279 <label>Date To</label>
282 name="ticket-date-to"
284 value={filters.date_to}
285 onChange={e => handleFilterChange('date_to', e.target.value)}
290 {hasActiveFilters() && (
291 <div className="active-filters">
292 <div className="active-filters-header">
293 <span className="filter-label">Active Filters:</span>
295 <div className="filter-tags">
296 {filters.status !== 'all' && (
297 <span className="filter-tag">
298 Status: {filters.status}
299 <button onClick={() => handleFilterChange('status', 'all')}>×</button>
302 {filters.priority !== 'all' && (
303 <span className="filter-tag">
304 Priority: {filters.priority}
305 <button onClick={() => handleFilterChange('priority', 'all')}>×</button>
308 {filters.sla_filter !== 'all' && (
309 <span className="filter-tag">
310 SLA: {filters.sla_filter.replace('_', ' ')}
311 <button onClick={() => handleFilterChange('sla_filter', 'all')}>×</button>
314 {filters.date_from && (
315 <span className="filter-tag">
316 From: {filters.date_from}
317 <button onClick={() => handleFilterChange('date_from', '')}>×</button>
320 {filters.date_to && (
321 <span className="filter-tag">
322 To: {filters.date_to}
323 <button onClick={() => handleFilterChange('date_to', '')}>×</button>
327 <span className="filter-tag">
329 <button onClick={() => setSearch('')}>×</button>
338 {error && <div className="error-message">{error}</div>}
341 <div className="loading">Loading tickets...</div>
344 <div className="table-container">
345 <table className="data-table">
360 {tickets.map(ticket => (
361 <tr key={ticket.ticket_id}>
364 href={`/tickets/${ticket.ticket_id}/edit`}
365 style={{ textDecoration: 'underline', color: 'inherit', cursor: 'pointer' }}
366 title={`Edit ticket #${ticket.ticket_id}`}
369 navigate(`/tickets/${ticket.ticket_id}/edit`);
375 <td title={ticket.tenant_id ? `Tenant #${ticket.tenant_id}` : ''}>
376 {ticket.tenant_id === 1 ? 'Root Tenant' : (ticket.tenant_name || tenantsMap[ticket.tenant_id] || (ticket.tenant_id ? `#${ticket.tenant_id}` : '-'))}
378 <td>{ticket.title}</td>
379 <td className="col-customer" title={ticket.customer_name || (ticket.customer_id ? `Customer #${ticket.customer_id}` : '')}>
380 {ticket.customer_name && ticket.customer_name.trim().length > 0
381 ? ticket.customer_name
382 : (ticket.customer_id ? `#${ticket.customer_id}` : '-')}
385 <span className={`status-badge ${ticket.status}`}>
390 <span className={`priority-badge ${ticket.priority}`}>
395 {ticket.sla_status === 'closed' ? (
396 <span className="sla-badge closed">Closed</span>
398 <span className={`sla-badge ${ticket.sla_status}`}>
400 const secs = Number(ticket.sla_remaining_seconds || 0);
401 const sign = secs < 0 ? -1 : 1;
402 const abs = Math.abs(secs);
403 const h = Math.floor(abs / 3600);
404 const m = Math.floor((abs % 3600) / 60);
405 if (ticket.sla_status === 'overdue') {
406 return `Overdue by ${h}h ${m}m`;
408 if (ticket.sla_status === 'due_soon') {
409 return `Due soon: ${h}h ${m}m`;
411 return `Due in ${h}h ${m}m`;
416 <td>{new Date(ticket.created_at).toLocaleDateString()}</td>
420 onClick={() => handleEditClick(ticket.ticket_id)}
421 style={{ marginRight: '8px' }}
423 <span className="material-symbols-outlined">edit</span>
427 className="btn danger"
428 onClick={() => handleDeleteClick(ticket)}
429 title="Delete Ticket"
431 <span className="material-symbols-outlined">delete</span>
441 <div className="pagination">
442 <button className="btn" disabled={page === 1} onClick={() => setPage(p => p - 1)}>
443 <span className="material-symbols-outlined">navigate_before</span>
446 Page {page} of {totalPages}
448 <button className="btn" disabled={page === totalPages} onClick={() => setPage(p => p + 1)}>
449 <span className="material-symbols-outlined">navigate_next</span>
456 <div className="modal-overlay" onClick={() => setDeleteConfirm(null)}>
457 <div className="modal-content" onClick={e => e.stopPropagation()}>
458 <div className="modal-header">
459 <h3>Confirm Delete</h3>
462 onClick={() => setDeleteConfirm(null)}
463 style={{ padding: '4px', minWidth: 'auto' }}
465 <span className="material-symbols-outlined">close</span>
468 <div className="modal-body">
469 <p>Are you sure you want to delete the ticket:</p>
470 <p><strong>"#{deleteConfirm.ticket_id}: {deleteConfirm.title}"</strong></p>
471 <p style={{ color: 'var(--error)', fontSize: '0.9em' }}>
472 This action cannot be undone. Only open or draft tickets can be deleted.
475 <div className="modal-footer">
478 onClick={() => setDeleteConfirm(null)}
479 style={{ marginRight: '8px' }}
484 className="btn danger"
485 onClick={() => handleDeleteTicket(deleteConfirm.ticket_id)}
498export default Tickets;