1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import { apiFetch } from '../../lib/api';
4import { notifySuccess, notifyError } from '../../utils/notifications';
5import DomainStats from './domains/DomainStats';
6import DomainTable from './domains/DomainTable';
7import DomainFormModal from './domains/DomainFormModal';
8import DomainRegistrationModal from './domains/DomainRegistrationModal';
9import DomainDNSModal from './domains/DomainDNSModal';
10import DomainManagementModal from './domains/DomainManagementModal';
11import './TabDomains.css';
13const initialFormData = {
17 registration_date: '',
24 privacy_protection: false,
28const initialRegistrationData = {
32 domain_type: 'registration',
37 registrant_firstName: '',
38 registrant_lastName: '',
39 registrant_organization: '',
42 registrant_address: '',
45 registrant_postalCode: '',
46 registrant_country: ''
49export default function TabDomains() {
50 const navigate = useNavigate();
51 const [domains, setDomains] = useState([]);
52 const [customers, setCustomers] = useState([]);
53 const [contracts, setContracts] = useState([]);
54 const [loading, setLoading] = useState(true);
55 const [showModal, setShowModal] = useState(false);
56 const [showRegistrationModal, setShowRegistrationModal] = useState(false);
57 const [editingDomain, setEditingDomain] = useState(null);
58 const [formData, setFormData] = useState(initialFormData);
59 const [registrationData, setRegistrationData] = useState(initialRegistrationData);
60 const [searchQuery, setSearchQuery] = useState('');
61 const [searchResult, setSearchResult] = useState(null);
62 const [searching, setSearching] = useState(false);
63 const [showDNSModal, setShowDNSModal] = useState(false);
64 const [dnsEditingDomain, setDnsEditingDomain] = useState(null);
65 const [showManagementModal, setShowManagementModal] = useState(false);
66 const [managingDomain, setManagingDomain] = useState(null);
67 const [syncing, setSyncing] = useState(false);
68 const [activeTab, setActiveTab] = useState('all');
70 // Check if user is root tenant
71 const token = localStorage.getItem('token');
72 const parseJwt = (token) => {
74 const payload = token.split('.')[1];
75 return JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
80 const jwtPayload = token ? parseJwt(token) : null;
81 const isRootTenant = jwtPayload?.is_msp || false;
89 const loadDomains = async () => {
92 console.log('[Domains] Fetching domains from database...');
93 const res = await apiFetch('/domains');
94 if (!res.ok) throw new Error(`HTTP ${res.status}`);
95 const data = await res.json();
96 console.log('[Domains] Received data:', data);
97 const domainsList = Array.isArray(data) ? data : data.domains || [];
98 console.log('[Domains] Loaded', domainsList.length, 'domains from database');
99 setDomains(domainsList);
101 console.error('[Domains] Error loading domains:', err);
102 await notifyError('Load Failed', 'Failed to load domains from database');
109 const loadCustomers = async () => {
111 const res = await apiFetch('/customers?limit=1000&status=active');
113 const data = await res.json();
114 setCustomers(Array.isArray(data) ? data : data.customers || []);
116 console.error('Error loading customers:', err);
120 const loadContracts = async () => {
122 const res = await apiFetch('/contracts?limit=1000&status=active');
124 const data = await res.json();
125 setContracts(Array.isArray(data) ? data : data.contracts || []);
127 console.error('Error loading contracts:', err);
131 const openModal = (domain = null) => {
133 setEditingDomain(domain);
135 customer_id: domain.customer_id || '',
136 domain_name: domain.domain_name || '',
137 registrar: domain.registrar || 'enom',
138 registration_date: domain.registration_date?.split('T')[0] || '',
139 expiry_date: domain.expiry_date?.split('T')[0] || '',
140 auto_renew: domain.auto_renew !== false,
141 status: domain.status || 'active',
142 contract_id: domain.contract_id || '',
143 nameservers: domain.nameservers || '',
144 dns_provider: domain.dns_provider || '',
145 privacy_protection: domain.privacy_protection || false,
146 notes: domain.notes || ''
149 setEditingDomain(null);
150 setFormData(initialFormData);
155 const closeModal = () => {
157 setShowRegistrationModal(false);
158 setEditingDomain(null);
159 setSearchResult(null);
163 const openRegistrationModal = (domainType = 'registration') => {
164 // Pre-fill from search result if available
165 if (searchResult && searchResult.available && !searchResult.alreadyOwned) {
166 setRegistrationData({
167 ...initialRegistrationData,
168 domain_name: searchResult.domain,
169 domain_type: domainType,
170 price: searchResult.price || null,
171 currency: searchResult.currency || 'USD'
174 setRegistrationData({
175 ...initialRegistrationData,
176 domain_type: domainType
179 setShowRegistrationModal(true);
182 const handleDomainSearch = async () => {
183 if (!searchQuery.trim()) return;
187 const result = await apiFetch('/domains/search', {
189 headers: { 'Content-Type': 'application/json' },
190 body: JSON.stringify({ domain: searchQuery.trim() })
193 setSearchResult(result);
195 // If available, pre-fill form with search result
196 if (result.available && !result.alreadyOwned) {
197 const expiryDate = new Date();
198 expiryDate.setFullYear(expiryDate.getFullYear() + 1);
202 domain_name: result.domain,
203 registrar: result.registrar || 'cloudflare',
204 registration_date: new Date().toISOString().split('T')[0],
205 expiry_date: expiryDate.toISOString().split('T')[0],
207 privacy_protection: true
211 await notifyError('Search Failed', err.message || 'Failed to search domain');
212 setSearchResult(null);
218 const handleSubmit = async (data, editing) => {
222 register_now: !editing && data.registrar === 'cloudflare'
226 await apiFetch(`/domains/${editing.domain_id}`, {
228 body: JSON.stringify(payload)
230 await notifySuccess('Domain Updated', 'Domain has been updated successfully');
232 await apiFetch('/domains', {
234 body: JSON.stringify(payload)
237 if (data.registrar === 'cloudflare') {
238 await notifySuccess('Domain Registered', `Domain ${data.domain_name} has been registered with Cloudflare`);
240 await notifySuccess('Domain Created', 'Domain has been created successfully');
246 const errorMsg = err.message || 'Error saving domain';
247 await notifyError('Save Failed', errorMsg);
251 const handleRegistrationRequest = async (data) => {
253 // Convert form data to API format
254 const nameserversArray = data.nameservers
255 ? data.nameservers.split('\n').map(ns => ns.trim()).filter(ns => ns)
259 customer_id: parseInt(data.customer_id),
260 contract_id: data.contract_id ? parseInt(data.contract_id) : null,
261 domain_name: data.domain_name,
264 currency: data.currency,
265 nameservers: nameserversArray,
266 registrant_contact: {
267 firstName: data.registrant_firstName,
268 lastName: data.registrant_lastName,
269 organization: data.registrant_organization || '',
270 email: data.registrant_email,
271 phone: data.registrant_phone,
272 address: data.registrant_address,
273 city: data.registrant_city,
274 state: data.registrant_state,
275 postalCode: data.registrant_postalCode,
276 country: data.registrant_country
280 await apiFetch('/domain-requests', {
282 body: JSON.stringify(payload)
286 'Registration Request Submitted',
287 `Your domain registration request for ${data.domain_name} has been submitted for admin approval`
291 // Optionally reload domains to show pending request
293 const errorMsg = err.message || 'Error submitting registration request';
294 await notifyError('Request Failed', errorMsg);
298 const handleDelete = async (domainId) => {
299 if (!confirm('Delete this domain? This cannot be undone.')) return;
301 await apiFetch(`/domains/${domainId}`, { method: 'DELETE' });
302 await notifySuccess('Domain Deleted', 'Domain has been deleted successfully');
305 const errorMsg = err.message || 'Error deleting domain';
306 await notifyError('Delete Failed', errorMsg);
310 const handleManageDNS = (domain) => {
311 // Navigate to the new DNS management page
312 navigate(`/dns-management/${domain.domain_name}`);
315 const closeDNSModal = () => {
316 setShowDNSModal(false);
317 setDnsEditingDomain(null);
320 const handleManageDomain = (domain) => {
321 setManagingDomain(domain);
322 setShowManagementModal(true);
325 const closeManagementModal = () => {
326 setShowManagementModal(false);
327 setManagingDomain(null);
330 const handleSyncDomains = async () => {
331 if (!isRootTenant) return;
335 await apiFetch('/domains/sync', { method: 'POST' });
336 await notifySuccess('Sync Started', 'Domain sync has been started. This may take a few minutes.');
337 // Reload domains after a short delay
342 await notifyError('Sync Failed', err.message || 'Failed to start domain sync');
348 // Filter domains by metadata/configuration based on active tab
349 const getFilteredDomains = () => {
350 const now = new Date();
351 const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
357 return domains.filter(d => {
358 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
359 return metadata.domain_type === 'registered';
362 return domains.filter(d => {
363 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
364 return metadata.domain_type === 'dns_only';
367 return domains.filter(d => {
368 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
369 return metadata.has_cloudflare_dns || metadata.cloudflare_zone_id;
372 return domains.filter(d => {
373 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
374 return metadata.locked === true;
377 return domains.filter(d => {
378 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
379 if (metadata.domain_type === 'dns_only') return false;
380 if (!d.expiration_date) return false;
381 const expiryDate = new Date(d.expiration_date);
382 return expiryDate <= thirtyDaysFromNow && expiryDate >= now;
385 return domains.filter(d => d.status === 'pending');
391 const filteredDomains = getFilteredDomains();
394 return <div>Loading domains...</div>;
398 <div className="tab-domains">
399 <div className="tab-header">
400 <DomainStats domains={domains} />
401 <div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
402 <div style={{ display: 'flex', gap: '4px' }}>
405 placeholder="Search domain (e.g., example.com)"
407 onChange={(e) => setSearchQuery(e.target.value)}
408 onKeyPress={(e) => e.key === 'Enter' && handleDomainSearch()}
409 style={{ padding: '8px 12px', minWidth: '280px' }}
413 onClick={handleDomainSearch}
414 disabled={searching || !searchQuery.trim()}
416 {searching ? 'Searching...' : 'Search'}
422 onClick={handleSyncDomains}
424 title="Sync all Cloudflare domains and DNS records"
425 style={{ background: '#2196F3', color: 'white' }}
427 <span className="material-symbols-outlined">sync</span>
428 {syncing ? 'Syncing...' : 'Sync Domains'}
433 onClick={() => openRegistrationModal('dns-only')}
434 style={{ background: '#00BCD4', color: 'white' }}
435 title="Add DNS management for a domain registered elsewhere (free)"
437 <span className="material-symbols-outlined">dns</span>
440 <button className="btn btn-primary" onClick={() => openRegistrationModal('registration')}>
441 <span className="material-symbols-outlined">add</span>
447 {/* Domain Filter Tabs */}
449 borderBottom: '2px solid #e0e0e0',
450 marginBottom: '16px',
456 onClick={() => setActiveTab('all')}
458 padding: '12px 20px',
459 background: activeTab === 'all' ? '#2196F3' : 'transparent',
460 color: activeTab === 'all' ? 'white' : '#666',
462 borderBottom: activeTab === 'all' ? '3px solid #2196F3' : '3px solid transparent',
464 fontWeight: activeTab === 'all' ? 'bold' : 'normal',
465 transition: 'all 0.2s'
468 All ({domains.length})
471 onClick={() => setActiveTab('registered')}
473 padding: '12px 20px',
474 background: activeTab === 'registered' ? '#4caf50' : 'transparent',
475 color: activeTab === 'registered' ? 'white' : '#666',
477 borderBottom: activeTab === 'registered' ? '3px solid #4caf50' : '3px solid transparent',
479 fontWeight: activeTab === 'registered' ? 'bold' : 'normal',
480 transition: 'all 0.2s'
482 title="Domains with full registration management"
484 Registered ({domains.filter(d => {
485 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
486 return metadata.domain_type === 'registered';
490 onClick={() => setActiveTab('dns_only')}
492 padding: '12px 20px',
493 background: activeTab === 'dns_only' ? '#00BCD4' : 'transparent',
494 color: activeTab === 'dns_only' ? 'white' : '#666',
496 borderBottom: activeTab === 'dns_only' ? '3px solid #00BCD4' : '3px solid transparent',
498 fontWeight: activeTab === 'dns_only' ? 'bold' : 'normal',
499 transition: 'all 0.2s'
501 title="DNS management only (registered elsewhere)"
503 DNS Only ({domains.filter(d => {
504 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
505 return metadata.domain_type === 'dns_only';
509 onClick={() => setActiveTab('cloudflare')}
511 padding: '12px 20px',
512 background: activeTab === 'cloudflare' ? '#2196F3' : 'transparent',
513 color: activeTab === 'cloudflare' ? 'white' : '#666',
515 borderBottom: activeTab === 'cloudflare' ? '3px solid #2196F3' : '3px solid transparent',
517 fontWeight: activeTab === 'cloudflare' ? 'bold' : 'normal',
518 transition: 'all 0.2s'
520 title="Using Cloudflare DNS"
522 ☁️ Cloudflare ({domains.filter(d => {
523 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
524 return metadata.has_cloudflare_dns || metadata.cloudflare_zone_id;
528 onClick={() => setActiveTab('locked')}
530 padding: '12px 20px',
531 background: activeTab === 'locked' ? '#4caf50' : 'transparent',
532 color: activeTab === 'locked' ? 'white' : '#666',
534 borderBottom: activeTab === 'locked' ? '3px solid #4caf50' : '3px solid transparent',
536 fontWeight: activeTab === 'locked' ? 'bold' : 'normal',
537 transition: 'all 0.2s'
539 title="Transfer protected domains"
541 🔒 Locked ({domains.filter(d => {
542 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
543 return metadata.locked === true;
547 onClick={() => setActiveTab('expiring')}
549 padding: '12px 20px',
550 background: activeTab === 'expiring' ? '#ff9800' : 'transparent',
551 color: activeTab === 'expiring' ? 'white' : '#666',
553 borderBottom: activeTab === 'expiring' ? '3px solid #ff9800' : '3px solid transparent',
555 fontWeight: activeTab === 'expiring' ? 'bold' : 'normal',
556 transition: 'all 0.2s'
558 title="Expiring within 30 days"
560 ⚠️ Expiring ({(() => {
561 const now = new Date();
562 const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
563 return domains.filter(d => {
564 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
565 if (metadata.domain_type === 'dns_only') return false;
566 if (!d.expiration_date) return false;
567 const expiryDate = new Date(d.expiration_date);
568 return expiryDate <= thirtyDaysFromNow && expiryDate >= now;
574 onClick={() => setActiveTab('pending')}
576 padding: '12px 20px',
577 background: activeTab === 'pending' ? '#ff9800' : 'transparent',
578 color: activeTab === 'pending' ? 'white' : '#666',
580 borderBottom: activeTab === 'pending' ? '3px solid #ff9800' : '3px solid transparent',
582 fontWeight: activeTab === 'pending' ? 'bold' : 'normal',
583 transition: 'all 0.2s'
585 title="Pending registration requests"
587 Pending ({domains.filter(d => d.status === 'pending').length})
595 marginBottom: '16px',
596 background: searchResult.available && !searchResult.alreadyOwned ? '#e8f5e9' : '#fff3e0',
597 border: `1px solid ${searchResult.available && !searchResult.alreadyOwned ? '#4caf50' : '#ff9800'}`,
600 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
602 <h3 style={{ margin: '0 0 8px 0' }}>{searchResult.domain}</h3>
603 <p style={{ margin: 0 }}>
604 {searchResult.alreadyOwned ? (
605 <strong>Already registered in your account</strong>
606 ) : searchResult.available ? (
608 <strong style={{ color: '#4caf50' }}>Available!</strong>
609 {searchResult.price && ` - $${searchResult.price} ${searchResult.currency}/year`}
610 {searchResult.note && <span style={{ fontSize: '0.9em', color: '#666', display: 'block', marginTop: '4px' }}>{searchResult.note}</span>}
613 <strong style={{ color: '#ff5722' }}>Not Available</strong>
617 {searchResult.available && !searchResult.alreadyOwned && (
619 className="btn btn-primary"
620 onClick={openRegistrationModal}
630 domains={filteredDomains}
632 onDelete={handleDelete}
633 onManageDNS={handleManageDNS}
634 onManage={handleManageDomain}
638 showModal={showModal}
639 editingDomain={editingDomain}
641 setFormData={setFormData}
642 customers={customers}
643 contracts={contracts}
644 onSubmit={handleSubmit}
648 <DomainRegistrationModal
649 showModal={showRegistrationModal}
650 formData={registrationData}
651 setFormData={setRegistrationData}
652 customers={customers}
653 contracts={contracts}
654 onSubmit={handleRegistrationRequest}
659 showModal={showDNSModal}
660 domain={dnsEditingDomain}
661 onClose={closeDNSModal}
662 onUpdate={loadDomains}
665 {showManagementModal && managingDomain && (
666 <DomainManagementModal
667 domain={managingDomain}
668 onClose={closeManagementModal}
669 onUpdate={loadDomains}