1import { useEffect, useState } from 'react';
2import { useParams, useNavigate } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4import './Customers.css';
5import DownloadAgentModal from "../components/DownloadAgentModal";
6import { apiFetch } from '../lib/api';
7import { notifySuccess, notifyError } from '../utils/notifications';
9function CustomerDetail() {
10 // Helper to decode JWT and extract tenantId
11 function getTenantIdFromStorageOrJWT() {
12 let tid = localStorage.getItem('selectedTenantId');
13 if (tid) return tid.trim().replace(/['"]/g, '');
14 const token = localStorage.getItem('token');
17 const payload = token.split('.')[1];
18 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
19 tid = decoded.tenantId || decoded.tenant_id || null;
21 const sanitizedTid = String(tid).trim().replace(/['"]/g, '');
22 localStorage.setItem('selectedTenantId', sanitizedTid);
32 // Download Agent modal state for this customer
34 const [showDownload, setShowDownload] = useState(false);
36 const { id } = useParams();
37 const navigate = useNavigate();
38 const [data, setData] = useState(null);
39 const [loading, setLoading] = useState(false);
40 const [error, setError] = useState('');
42 // Contact management state
43 const [showContactForm, setShowContactForm] = useState(false);
44 const [editingContact, setEditingContact] = useState(null);
45 const [contactForm, setContactForm] = useState({
54 // Portal user management state
55 const [portalUsers, setPortalUsers] = useState([]);
56 const [showPortalUserForm, setShowPortalUserForm] = useState(false);
57 const [editingPortalUser, setEditingPortalUser] = useState(null);
58 const [portalUserForm, setPortalUserForm] = useState({
66 const [resetPasswordUserId, setResetPasswordUserId] = useState(null);
67 const [newPassword, setNewPassword] = useState('');
74 async function fetchPortalUsers() {
76 const token = localStorage.getItem('token');
77 const res = await apiFetch(`/customer-users?customer_id=${id}`, {
78 headers: { Authorization: `Bearer ${token}` }
80 if (!res.ok) throw new Error('Failed to load portal users');
81 const json = await res.json();
82 setPortalUsers(json.users || []);
84 console.error('Error fetching portal users:', err);
88 async function handleSavePortalUser(e) {
91 const token = localStorage.getItem('token');
94 if (!portalUserForm.email) {
95 await notifyError('Validation Error', 'Email is required');
99 if (!editingPortalUser && !portalUserForm.password) {
100 await notifyError('Validation Error', 'Password is required for new users');
105 customer_id: parseInt(id),
109 const url = editingPortalUser
110 ? `/customer-users/${editingPortalUser.customer_user_id}`
112 const method = editingPortalUser ? 'PUT' : 'POST';
114 const res = await apiFetch(url, {
117 'Content-Type': 'application/json',
118 Authorization: `Bearer ${token}`
120 body: JSON.stringify(body)
124 const error = await res.json();
125 throw new Error(error.error || 'Failed to save portal user');
128 // Reset form and refresh
129 setShowPortalUserForm(false);
130 setEditingPortalUser(null);
131 setPortalUserForm({ email: '', password: '', first_name: '', last_name: '', phone: '', is_primary: false });
133 editingPortalUser ? 'User Updated' : 'User Created',
134 `Portal user has been ${editingPortalUser ? 'updated' : 'created'} successfully`
139 await notifyError('Save Failed', err.message || 'Failed to save portal user');
143 async function handleResetPassword() {
145 await notifyError('Validation Error', 'Please enter a new password');
150 const token = localStorage.getItem('token');
151 const res = await apiFetch(`/customer-users/${resetPasswordUserId}/reset-password`, {
154 'Content-Type': 'application/json',
155 Authorization: `Bearer ${token}`
157 body: JSON.stringify({ new_password: newPassword })
161 const error = await res.json();
162 throw new Error(error.error || 'Failed to reset password');
165 await notifySuccess('Password Reset', 'Password has been reset successfully');
166 setResetPasswordUserId(null);
170 await notifyError('Reset Failed', err.message || 'Failed to reset password');
174 async function handleTogglePortalUserStatus(userId, currentStatus) {
175 const newStatus = !currentStatus;
176 const action = newStatus ? 'activate' : 'deactivate';
178 if (!confirm(`Are you sure you want to ${action} this portal user?`)) return;
181 const token = localStorage.getItem('token');
182 const res = await apiFetch(`/customer-users/${userId}/status`, {
185 'Content-Type': 'application/json',
186 Authorization: `Bearer ${token}`
188 body: JSON.stringify({ is_active: newStatus })
192 const error = await res.json();
193 throw new Error(error.error || 'Failed to update status');
196 await notifySuccess('Status Updated', `User has been ${action}d successfully`);
200 await notifyError('Update Failed', err.message || 'Failed to update status');
204 async function handleDeletePortalUser(userId) {
205 if (!confirm('Permanently delete this portal user? This cannot be undone.')) return;
208 const token = localStorage.getItem('token');
209 const res = await apiFetch(`/customer-users/${userId}`, {
211 headers: { Authorization: `Bearer ${token}` }
215 const error = await res.json();
216 throw new Error(error.error || 'Failed to delete user');
219 await notifySuccess('User Deleted', 'Portal user has been deleted successfully');
223 await notifyError('Delete Failed', err.message || 'Failed to delete user');
227 function startEditPortalUser(user) {
228 setEditingPortalUser(user);
230 email: user.email || '',
231 password: '', // Don't populate password for edit
232 first_name: user.first_name || '',
233 last_name: user.last_name || '',
234 phone: user.phone || '',
235 is_primary: user.is_primary || false
237 setShowPortalUserForm(true);
240 function cancelPortalUserForm() {
241 setShowPortalUserForm(false);
242 setEditingPortalUser(null);
243 setPortalUserForm({ email: '', password: '', first_name: '', last_name: '', phone: '', is_primary: false });
250 async function fetchDetail() {
254 const token = localStorage.getItem('token');
255 const url = `/customers/${id}`;
256 const res = await apiFetch(url, { headers: { Authorization: `Bearer ${token}` } });
257 if (!res.ok) throw new Error('Failed to load customer');
258 const json = await res.json();
262 setError(err.message || 'Load error');
268 async function handleSaveContact(e) {
271 const token = localStorage.getItem('token');
272 const url = editingContact
273 ? `/customers/${id}/contacts/${editingContact.contact_id}`
274 : `/customers/${id}/contacts`;
275 const method = editingContact ? 'PUT' : 'POST';
276 const res = await apiFetch(url, {
279 'Content-Type': 'application/json',
280 Authorization: `Bearer ${token}`
282 body: JSON.stringify(contactForm)
284 if (!res.ok) throw new Error('Failed to save contact');
285 // Reset form and refresh data
286 setShowContactForm(false);
287 setEditingContact(null);
288 setContactForm({ name: '', role: '', email: '', phone: '', is_primary: false, notes: '' });
289 await notifySuccess(editingContact ? 'Contact Updated' : 'Contact Created', `Contact has been ${editingContact ? 'updated' : 'created'} successfully`);
293 await notifyError('Save Failed', err.message || 'Failed to save contact');
297 async function handleDeleteContact(contactId) {
298 if (!confirm('Delete this contact?')) return;
300 const token = localStorage.getItem('token');
301 const url = `/customers/${id}/contacts/${contactId}`;
302 const res = await apiFetch(url, {
304 headers: { Authorization: `Bearer ${token}` }
306 if (!res.ok) throw new Error('Failed to delete contact');
310 alert(err.message || 'Failed to delete contact');
314 function startEditContact(contact) {
315 setEditingContact(contact);
317 name: contact.name || '',
318 role: contact.role || '',
319 email: contact.email || '',
320 phone: contact.phone || '',
321 is_primary: contact.is_primary || false,
322 notes: contact.notes || ''
324 setShowContactForm(true);
327 function cancelContactForm() {
328 setShowContactForm(false);
329 setEditingContact(null);
330 setContactForm({ name: '', role: '', email: '', phone: '', is_primary: false, notes: '' });
333 if (loading) return (
335 <div className="page-content"><div className="loading">Loading customer...</div></div>
341 <div className="page-content"><div className="error-message">{error}</div></div>
345 if (!data) return null;
347 const { customer, tickets = [], invoices = [], agents = [], contacts = [], contracts = [], domains = [], office365_subscriptions = [], hosting_apps = [] } = data;
351 <div className="page-content">
352 <div className="page-header">
353 <h2>{customer.name}</h2>
355 <button className="btn" onClick={() => navigate(`/customers/${id}/edit`)}>
356 <span className="material-symbols-outlined">edit</span> Edit Customer
361 <section className="dashboard-grid">
362 {/* Customer Information Card */}
363 <div className="dashboard-card">
364 <h3>Customer Information</h3>
365 <div style={{ display: 'grid', gap: '12px' }}>
367 <strong>Status:</strong>{' '}
368 <span className={`status-badge status-${customer.status?.toLowerCase() || 'active'}`}>
369 {customer.status || 'Active'}
373 <div><strong>ABN:</strong> {customer.abn}</div>
375 <div><strong>Primary Contact:</strong> {customer.contact_name || 'N/A'}</div>
376 <div><strong>Email:</strong> {customer.email || 'N/A'}</div>
377 {customer.billing_email && (
378 <div><strong>Billing Email:</strong> {customer.billing_email}</div>
380 <div><strong>Phone:</strong> {customer.phone || 'N/A'}</div>
381 <div style={{ marginTop: '8px' }}>
382 <strong>Address:</strong>
383 <div style={{ marginLeft: '8px', color: 'var(--text-muted)' }}>
384 {customer.address && <div>{customer.address}</div>}
385 {(customer.city || customer.state || customer.postal_code) && (
386 <div>{[customer.city, customer.state, customer.postal_code].filter(Boolean).join(', ')}</div>
388 {customer.country && <div>{customer.country}</div>}
392 <div style={{ marginTop: '8px' }}>
393 <strong>Notes:</strong>
394 <div style={{ marginLeft: '8px', color: 'var(--text-muted)', fontStyle: 'italic' }}>
402 {/* Contacts Card */}
403 <div className="dashboard-card">
404 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
405 <h3 style={{ margin: 0 }}>Contacts</h3>
407 className="btn btn-sm"
408 onClick={() => setShowContactForm(true)}
409 style={{ padding: '4px 12px', fontSize: '0.9em' }}
411 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>add</span> Add Contact
415 {showContactForm && (
416 <form onSubmit={handleSaveContact} style={{
417 marginBottom: '16px',
419 border: '1px solid var(--border)',
421 backgroundColor: 'var(--bg-secondary)'
423 <h4 style={{ marginTop: 0 }}>{editingContact ? 'Edit Contact' : 'New Contact'}</h4>
424 <div style={{ display: 'grid', gap: '8px' }}>
428 value={contactForm.name}
429 onChange={e => setContactForm({ ...contactForm, name: e.target.value })}
431 style={{ padding: '8px' }}
435 placeholder="Role (e.g., IT Manager)"
436 value={contactForm.role}
437 onChange={e => setContactForm({ ...contactForm, role: e.target.value })}
438 style={{ padding: '8px' }}
443 value={contactForm.email}
444 onChange={e => setContactForm({ ...contactForm, email: e.target.value })}
445 style={{ padding: '8px' }}
450 value={contactForm.phone}
451 onChange={e => setContactForm({ ...contactForm, phone: e.target.value })}
452 style={{ padding: '8px' }}
454 <label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
457 checked={contactForm.is_primary}
458 onChange={e => setContactForm({ ...contactForm, is_primary: e.target.checked })}
460 Set as Primary Contact
464 value={contactForm.notes}
465 onChange={e => setContactForm({ ...contactForm, notes: e.target.value })}
467 style={{ padding: '8px' }}
469 <div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
470 <button type="submit" className="btn btn-primary" style={{ flex: 1 }}>
471 {editingContact ? 'Update' : 'Create'}
473 <button type="button" className="btn" onClick={cancelContactForm} style={{ flex: 1 }}>
481 {contacts.length === 0 ? (
482 <div style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>No contacts added</div>
484 <div style={{ display: 'grid', gap: '12px' }}>
485 {contacts.map(contact => (
486 <div key={contact.contact_id} style={{
488 border: '1px solid var(--border)',
490 backgroundColor: contact.is_primary ? 'var(--bg-hover)' : 'transparent'
492 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
493 <div style={{ flex: 1 }}>
494 <div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
496 {contact.is_primary && (
502 backgroundColor: 'var(--primary)',
508 <div style={{ fontSize: '0.9em', color: 'var(--text-muted)', marginBottom: '4px' }}>
513 <div style={{ fontSize: '0.9em' }}>📧 {contact.email}</div>
516 <div style={{ fontSize: '0.9em' }}>📞 {contact.phone}</div>
519 <div style={{ fontSize: '0.85em', color: 'var(--text-muted)', marginTop: '4px', fontStyle: 'italic' }}>
524 <div style={{ display: 'flex', gap: '4px' }}>
526 className="btn btn-sm"
527 onClick={() => startEditContact(contact)}
528 style={{ padding: '4px 8px' }}
530 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>edit</span>
533 className="btn btn-sm"
534 onClick={() => handleDeleteContact(contact.contact_id)}
535 style={{ padding: '4px 8px', color: 'var(--danger)' }}
537 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>delete</span>
547 {/* Portal Users Card */}
548 <div className="dashboard-card">
549 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
550 <h3 style={{ margin: 0 }}>Portal Users ({portalUsers.length})</h3>
552 className="btn btn-sm"
553 onClick={() => setShowPortalUserForm(true)}
554 style={{ padding: '4px 12px', fontSize: '0.9em' }}
556 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>add</span> Add Portal User
560 {showPortalUserForm && (
561 <form onSubmit={handleSavePortalUser} style={{
562 marginBottom: '16px',
564 border: '1px solid var(--border)',
566 backgroundColor: 'var(--bg-secondary)'
568 <h4 style={{ margin: '0 0 12px 0' }}>{editingPortalUser ? 'Edit Portal User' : 'Create Portal User'}</h4>
569 <div style={{ display: 'grid', gap: '8px' }}>
572 placeholder="Email (login username)"
573 value={portalUserForm.email}
574 onChange={e => setPortalUserForm({ ...portalUserForm, email: e.target.value })}
575 style={{ padding: '8px' }}
578 {!editingPortalUser && (
581 placeholder="Password (required for new users)"
582 value={portalUserForm.password}
583 onChange={e => setPortalUserForm({ ...portalUserForm, password: e.target.value })}
584 style={{ padding: '8px' }}
590 placeholder="First Name"
591 value={portalUserForm.first_name}
592 onChange={e => setPortalUserForm({ ...portalUserForm, first_name: e.target.value })}
593 style={{ padding: '8px' }}
597 placeholder="Last Name"
598 value={portalUserForm.last_name}
599 onChange={e => setPortalUserForm({ ...portalUserForm, last_name: e.target.value })}
600 style={{ padding: '8px' }}
605 value={portalUserForm.phone}
606 onChange={e => setPortalUserForm({ ...portalUserForm, phone: e.target.value })}
607 style={{ padding: '8px' }}
609 <label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
612 checked={portalUserForm.is_primary}
613 onChange={e => setPortalUserForm({ ...portalUserForm, is_primary: e.target.checked })}
615 Set as Primary Contact
617 {editingPortalUser && (
618 <div style={{ fontSize: '0.85em', color: 'var(--text-muted)', fontStyle: 'italic' }}>
619 To reset password, save changes and use the Reset Password button on the user card.
622 <div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
623 <button type="submit" className="btn btn-primary" style={{ flex: 1 }}>
624 {editingPortalUser ? 'Update' : 'Create'}
626 <button type="button" className="btn" onClick={cancelPortalUserForm} style={{ flex: 1 }}>
634 {portalUsers.length === 0 ? (
635 <div style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>
636 No portal users created. Portal users can log in to view and manage their services, tickets, and invoices.
639 <div style={{ display: 'grid', gap: '12px' }}>
640 {portalUsers.map(user => (
641 <div key={user.customer_user_id} style={{
643 border: '1px solid var(--border)',
645 backgroundColor: user.is_primary ? 'var(--bg-hover)' : 'transparent',
646 opacity: user.is_active ? 1 : 0.6
648 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
649 <div style={{ flex: 1 }}>
650 <div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
651 {user.first_name || user.last_name
652 ? `${user.first_name || ''} ${user.last_name || ''}`.trim()
654 {user.is_primary && (
660 backgroundColor: 'var(--primary)',
664 {!user.is_active && (
670 backgroundColor: 'var(--danger)',
675 <div style={{ fontSize: '0.9em' }}>📧 {user.email}</div>
677 <div style={{ fontSize: '0.9em' }}>📞 {user.phone}</div>
679 {user.last_login && (
680 <div style={{ fontSize: '0.85em', color: 'var(--text-muted)', marginTop: '4px' }}>
681 Last login: {new Date(user.last_login).toLocaleString()}
684 <div style={{ fontSize: '0.85em', color: 'var(--text-muted)' }}>
685 Created: {new Date(user.created_at).toLocaleDateString()}
688 <div style={{ display: 'flex', gap: '4px', flexDirection: 'column' }}>
689 <div style={{ display: 'flex', gap: '4px' }}>
691 className="btn btn-sm"
692 onClick={() => startEditPortalUser(user)}
693 style={{ padding: '4px 8px' }}
696 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>edit</span>
699 className="btn btn-sm"
701 setResetPasswordUserId(user.customer_user_id);
704 style={{ padding: '4px 8px' }}
705 title="Reset password"
707 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>lock_reset</span>
710 <div style={{ display: 'flex', gap: '4px' }}>
712 className="btn btn-sm"
713 onClick={() => handleTogglePortalUserStatus(user.customer_user_id, user.is_active)}
716 backgroundColor: user.is_active ? 'var(--warning)' : 'var(--success)',
719 title={user.is_active ? 'Disable user' : 'Enable user'}
721 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>
722 {user.is_active ? 'block' : 'check_circle'}
726 className="btn btn-sm"
727 onClick={() => handleDeletePortalUser(user.customer_user_id)}
728 style={{ padding: '4px 8px', color: 'var(--danger)' }}
729 title="Delete user permanently"
731 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>delete</span>
737 {/* Password Reset Form */}
738 {resetPasswordUserId === user.customer_user_id && (
742 borderTop: '1px solid var(--border)'
744 <div style={{ fontWeight: 'bold', marginBottom: '8px' }}>Reset Password</div>
745 <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
748 placeholder="New password"
750 onChange={e => setNewPassword(e.target.value)}
751 style={{ flex: 1, padding: '6px' }}
754 className="btn btn-sm btn-primary"
755 onClick={handleResetPassword}
756 style={{ padding: '6px 12px' }}
761 className="btn btn-sm"
763 setResetPasswordUserId(null);
766 style={{ padding: '6px 12px' }}
779 {/* Contracts Card */}
780 <div className="dashboard-card">
781 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
782 <h3 style={{ margin: 0 }}>Contracts ({contracts.length})</h3>
784 className="btn btn-sm"
785 onClick={() => navigate('/contracts/new')}
786 style={{ padding: '4px 12px', fontSize: '0.9em' }}
788 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>add</span> New
791 {contracts.length === 0 ? (
792 <div style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>No contracts</div>
794 <div style={{ display: 'grid', gap: '8px' }}>
795 {contracts.map(contract => (
797 key={contract.contract_id}
798 onClick={() => navigate(`/contracts/${contract.contract_id}`)}
801 border: '1px solid var(--border)',
804 transition: 'all 0.2s'
806 onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--bg-hover)'}
807 onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
809 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
810 <div style={{ flex: 1 }}>
811 <div style={{ fontWeight: 'bold' }}>#{contract.contract_id} - {contract.contract_name || 'Untitled'}</div>
812 <div style={{ fontSize: '0.9em', color: 'var(--text-muted)' }}>
813 {contract.billing_interval} billing • ${parseFloat(contract.total_amount || 0).toFixed(2)} {contract.currency}
816 <span className={`status-badge status-${contract.status?.toLowerCase() || 'active'}`}>
826 {/* Services Card */}
827 <div className="dashboard-card">
828 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
829 <h3 style={{ margin: 0 }}>Services</h3>
831 className="btn btn-sm"
832 onClick={() => navigate('/services')}
833 style={{ padding: '4px 12px', fontSize: '0.9em' }}
835 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>open_in_new</span> View All
838 <div style={{ display: 'grid', gap: '8px' }}>
840 onClick={() => navigate('/services')}
843 border: '1px solid var(--border)',
846 transition: 'all 0.2s',
848 justifyContent: 'space-between',
851 onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--bg-hover)'}
852 onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
854 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
855 <span className="material-symbols-outlined" style={{ color: 'var(--primary)', fontSize: '20px' }}>language</span>
858 <span className="status-badge">{domains.length}</span>
861 onClick={() => navigate('/services')}
864 border: '1px solid var(--border)',
867 transition: 'all 0.2s',
869 justifyContent: 'space-between',
872 onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--bg-hover)'}
873 onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
875 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
876 <span className="material-symbols-outlined" style={{ color: 'var(--primary)', fontSize: '20px' }}>cloud</span>
877 <span>Office 365 Licenses</span>
879 <span className="status-badge">{office365_subscriptions.length}</span>
882 onClick={() => navigate('/services')}
885 border: '1px solid var(--border)',
888 transition: 'all 0.2s',
890 justifyContent: 'space-between',
893 onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--bg-hover)'}
894 onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
896 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
897 <span className="material-symbols-outlined" style={{ color: 'var(--primary)', fontSize: '20px' }}>dns</span>
898 <span>Web Hosting Apps</span>
900 <span className="status-badge">{hosting_apps.length}</span>
906 <div className="dashboard-card">
907 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
908 <h3 style={{ margin: 0 }}>Tickets ({tickets.length})</h3>
910 className="btn btn-sm"
911 onClick={() => navigate('/tickets/new')}
912 style={{ padding: '4px 12px', fontSize: '0.9em' }}
914 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>add</span> New
917 {tickets.length === 0 ? (
918 <div style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>No tickets</div>
920 <div style={{ display: 'grid', gap: '8px' }}>
921 {tickets.slice(0, 5).map(t => (
924 onClick={() => navigate(`/tickets/${t.ticket_id}`)}
927 border: '1px solid var(--border)',
930 transition: 'all 0.2s'
932 onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--bg-hover)'}
933 onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
935 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
936 <div style={{ flex: 1 }}>
937 <div style={{ fontWeight: 'bold' }}>#{t.ticket_id} - {t.title}</div>
938 <div style={{ fontSize: '0.9em', color: 'var(--text-muted)' }}>
939 Priority: {t.priority} • Assigned: {t.assigned_to || 'Unassigned'}
942 <span className={`status-badge status-${t.status?.toLowerCase().replace(' ', '-') || 'open'}`}>
948 {tickets.length > 5 && (
950 className="btn btn-sm"
951 onClick={() => navigate('/tickets')}
952 style={{ marginTop: '8px' }}
954 View All {tickets.length} Tickets
961 {/* Invoices Card */}
962 <div className="dashboard-card">
963 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
964 <h3 style={{ margin: 0 }}>Invoices ({invoices.length})</h3>
966 className="btn btn-sm"
967 onClick={() => navigate('/invoices/new')}
968 style={{ padding: '4px 12px', fontSize: '0.9em' }}
970 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>add</span> New
973 {invoices.length === 0 ? (
974 <div style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>No invoices</div>
976 <div style={{ display: 'grid', gap: '8px' }}>
977 {invoices.slice(0, 5).map(inv => (
980 onClick={() => navigate(`/invoices/${inv.invoice_id}`)}
983 border: '1px solid var(--border)',
986 transition: 'all 0.2s'
988 onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--bg-hover)'}
989 onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
991 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
992 <div style={{ flex: 1 }}>
993 <div style={{ fontWeight: 'bold' }}>Invoice #{inv.invoice_id}</div>
994 <div style={{ fontSize: '0.9em', color: 'var(--text-muted)' }}>
995 ${parseFloat(inv.total || inv.amount || 0).toFixed(2)} {inv.currency}
997 <div style={{ fontSize: '0.85em', color: 'var(--text-muted)' }}>
998 Due: {inv.due_date ? new Date(inv.due_date).toLocaleDateString() : 'N/A'}
1001 <span className={`status-badge status-${inv.status?.toLowerCase() || 'pending'}`}>
1007 {invoices.length > 5 && (
1009 className="btn btn-sm"
1010 onClick={() => navigate('/invoices')}
1011 style={{ marginTop: '8px' }}
1013 View All {invoices.length} Invoices
1022 <div className="dashboard-card">
1023 <div className="dashboard-card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
1024 <h3 style={{ margin: 0 }}>Agents ({agents.length})</h3>
1026 className="btn btn-sm primary"
1027 onClick={() => setShowDownload(true)}
1028 title="Download Agent Installer"
1029 style={{ padding: '4px 12px', fontSize: '0.9em' }}
1031 <span className="material-symbols-outlined" style={{ fontSize: '18px', marginRight: 4 }}>
1038 {agents.length === 0 ? (
1039 <div style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>No agents registered</div>
1041 <div style={{ display: 'grid', gap: '8px' }}>
1045 onClick={() => navigate(`/agents/${a.agent_id}`)}
1048 border: '1px solid var(--border)',
1049 borderRadius: '4px',
1051 transition: 'all 0.2s'
1053 onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--bg-hover)'}
1054 onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
1056 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
1057 <div style={{ flex: 1 }}>
1058 <div style={{ fontWeight: 'bold' }}>{a.name || a.hostname || 'Unknown'}</div>
1059 <div style={{ fontSize: '0.9em', color: 'var(--text-muted)' }}>
1060 {a.os_info || 'N/A'}
1062 <div style={{ fontSize: '0.85em', color: 'var(--text-muted)' }}>
1063 Last seen: {a.last_seen ? new Date(a.last_seen).toLocaleString() : 'Never'}
1066 <span className={`status-badge status-${a.status?.toLowerCase() || 'offline'}`}>
1075 {/* Download Agent Modal (new unified version) */}
1078 isOpen={showDownload}
1079 onClose={() => setShowDownload(false)}
1080 tenantId={getTenantIdFromStorageOrJWT() || customer?.tenant_id}
1081 customerId={customer?.id}
1092export default CustomerDetail;