1import { useEffect, useState } from 'react';
2import { useParams, useNavigate } from 'react-router-dom';
3import MainLayout from '../components/Layout/MainLayout';
4import { apiFetch } from '../lib/api';
5import { notifySuccess, notifyError } from '../utils/notifications';
7function HostingAppDetail() {
8 const { id } = useParams();
9 const navigate = useNavigate();
10 const [app, setApp] = useState(null);
11 const [loading, setLoading] = useState(true);
12 const [error, setError] = useState('');
13 const [saving, setSaving] = useState(false);
15 const [customers, setCustomers] = useState([]);
16 const [contracts, setContracts] = useState([]);
17 const [tenants, setTenants] = useState([]);
19 const [formData, setFormData] = useState({
22 assigned_tenant_id: ''
32 async function fetchAppDetail() {
36 const res = await apiFetch(`/hosting/apps/${id}`);
37 if (!res.ok) throw new Error('Failed to load app');
38 const data = await res.json();
41 customer_id: data.app.customer_id || '',
42 contract_id: data.app.contract_id || '',
43 assigned_tenant_id: data.app.assigned_tenant_id || ''
47 setError(err.message || 'Failed to load app');
53 async function fetchCustomers() {
55 const res = await apiFetch('/customers');
57 const data = await res.json();
58 setCustomers(data.customers || []);
61 console.error('Error fetching customers:', err);
65 async function fetchContracts() {
67 const res = await apiFetch('/contracts');
69 const data = await res.json();
70 setContracts(data.contracts || []);
73 console.error('Error fetching contracts:', err);
77 async function fetchTenants() {
79 const res = await apiFetch('/tenants');
81 const data = await res.json();
82 setTenants(data.tenants || []);
85 console.error('Error fetching tenants:', err);
86 // Not root user, ignore
90 async function handleSubmit(e) {
94 const res = await apiFetch(`/hosting/apps/${id}`, {
96 headers: { 'Content-Type': 'application/json' },
97 body: JSON.stringify(formData)
100 if (!res.ok) throw new Error('Failed to update app');
102 await notifySuccess('App Updated', 'Hosting app updated successfully');
106 await notifyError('Update Failed', err.message || 'Failed to update app');
112 // Filter contracts based on selected customer and tenant
113 function getFilteredContracts() {
114 return contracts.filter(contract => {
115 // Filter by customer if one is selected
116 if (formData.customer_id && contract.customer_id !== parseInt(formData.customer_id)) {
119 // Filter by tenant if one is selected, otherwise show only root tenant contracts
120 if (formData.assigned_tenant_id) {
121 return contract.tenant_id === parseInt(formData.assigned_tenant_id);
123 // No tenant selected = show root tenant contracts (tenant_id is null)
124 return !contract.tenant_id;
132 <div className="page-content">
133 <div className="loading">Loading app...</div>
142 <div className="page-content">
143 <div className="error-message">{error}</div>
149 if (!app) return null;
153 <div className="page-content">
154 <div style={{ marginBottom: '24px', display: 'flex', alignItems: 'center', gap: '16px' }}>
156 onClick={() => navigate('/services')}
158 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
160 <span className="material-symbols-outlined">arrow_back</span>
163 <h1 style={{ margin: 0, fontSize: '28px' }}>{app.app_name}</h1>
164 <span className={`badge badge-${getStatusColor(app.status)}`} style={{ marginLeft: 'auto' }}>
165 {getStatusLabel(app.status)}
169 <div style={{ display: 'grid', gap: '24px', gridTemplateColumns: '1fr 1fr' }}>
170 {/* App Information Card */}
171 <div className="card">
172 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
173 <h2 style={{ margin: 0, fontSize: '20px', fontWeight: '600' }}>App Information</h2>
175 <div style={{ padding: '20px' }}>
176 <dl style={{ display: 'grid', gap: '16px', fontSize: '14px' }}>
178 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>App ID</dt>
179 <dd style={{ fontWeight: '500', fontFamily: 'monospace' }}>{app.app_id}</dd>
182 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>DO App ID</dt>
183 <dd style={{ fontWeight: '500', fontFamily: 'monospace' }}>{app.do_app_id}</dd>
186 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Region</dt>
187 <dd style={{ fontWeight: '500', textTransform: 'uppercase' }}>{app.region}</dd>
190 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Type</dt>
191 <dd style={{ fontWeight: '500', textTransform: 'capitalize' }}>{app.app_type}</dd>
193 {app.default_domain && (
195 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Default Domain</dt>
196 <dd style={{ fontWeight: '500' }}>
198 href={`https://${app.default_domain}`}
200 rel="noopener noreferrer"
201 style={{ color: 'var(--primary)', textDecoration: 'none' }}
210 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Live URL</dt>
211 <dd style={{ fontWeight: '500' }}>
215 rel="noopener noreferrer"
216 style={{ color: 'var(--primary)', textDecoration: 'none', wordBreak: 'break-all' }}
224 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Created</dt>
225 <dd style={{ fontWeight: '500' }}>{new Date(app.created_at).toLocaleString()}</dd>
228 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Last Updated</dt>
229 <dd style={{ fontWeight: '500' }}>{new Date(app.updated_at).toLocaleString()}</dd>
235 {/* WordPress Credentials Card - Only show for WordPress apps */}
237 <div className="card">
238 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
239 <h2 style={{ margin: 0, fontSize: '20px', fontWeight: '600', display: 'flex', alignItems: 'center', gap: '8px' }}>
240 <span className="material-symbols-outlined" style={{ color: 'var(--primary)' }}>lock</span>
241 WordPress Credentials
244 <div style={{ padding: '20px' }}>
246 padding: '12px 16px',
247 background: 'var(--warning-bg)',
248 border: '1px solid var(--warning-border)',
250 marginBottom: '20px',
252 color: 'var(--warning-text)',
254 alignItems: 'center',
257 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>info</span>
258 <span>Keep these credentials secure. They provide admin access to your WordPress site.</span>
261 <dl style={{ display: 'grid', gap: '16px', fontSize: '14px' }}>
263 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Admin Username</dt>
266 fontFamily: 'monospace',
267 background: 'var(--input-bg)',
271 alignItems: 'center',
272 justifyContent: 'space-between'
274 <span>{app.wordpress.wp_admin_user}</span>
277 navigator.clipboard.writeText(app.wordpress.wp_admin_user);
278 notifySuccess('Copied', 'Username copied to clipboard');
280 className="btn btn-sm"
281 style={{ padding: '4px 8px', fontSize: '12px' }}
283 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>content_copy</span>
289 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Admin Password</dt>
292 fontFamily: 'monospace',
293 background: 'var(--input-bg)',
297 alignItems: 'center',
298 justifyContent: 'space-between'
300 <span style={{ letterSpacing: '0.1em' }}>{app.wordpress.wp_admin_password}</span>
303 navigator.clipboard.writeText(app.wordpress.wp_admin_password);
304 notifySuccess('Copied', 'Password copied to clipboard');
306 className="btn btn-sm"
307 style={{ padding: '4px 8px', fontSize: '12px' }}
309 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>content_copy</span>
315 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Admin Email</dt>
318 fontFamily: 'monospace',
319 background: 'var(--input-bg)',
323 alignItems: 'center',
324 justifyContent: 'space-between'
326 <span>{app.wordpress.wp_admin_email}</span>
329 navigator.clipboard.writeText(app.wordpress.wp_admin_email);
330 notifySuccess('Copied', 'Email copied to clipboard');
332 className="btn btn-sm"
333 style={{ padding: '4px 8px', fontSize: '12px' }}
335 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>content_copy</span>
340 <div style={{ borderTop: '1px solid var(--border)', paddingTop: '16px' }}>
341 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Database Name</dt>
342 <dd style={{ fontWeight: '500', fontFamily: 'monospace' }}>{app.wordpress.db_name}</dd>
346 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Spaces Path</dt>
347 <dd style={{ fontWeight: '500', fontFamily: 'monospace' }}>
348 everydaytech-wordpress/{app.wordpress.spaces_path}/
356 {/* Assignment Form Card */}
357 <div className="card">
358 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
359 <h2 style={{ margin: 0, fontSize: '20px', fontWeight: '600' }}>Assignments</h2>
361 <form onSubmit={handleSubmit} style={{ padding: '20px' }}>
362 <div style={{ display: 'grid', gap: '20px' }}>
364 <label htmlFor="customer" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
369 value={formData.customer_id}
370 onChange={(e) => setFormData(prev => ({ ...prev, customer_id: e.target.value }))}
371 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px' }}
373 <option value="">Unassigned</option>
374 {customers.map(c => (
375 <option key={c.customer_id} value={c.customer_id}>
383 <label htmlFor="contract" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
388 value={formData.contract_id}
389 onChange={(e) => setFormData(prev => ({ ...prev, contract_id: e.target.value }))}
390 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px', color: 'var(--text)' }}
392 <option value="">Unassigned</option>
393 {getFilteredContracts().map(c => (
394 <option key={c.contract_id} value={c.contract_id}>
395 {c.title} {c.customer_name ? `- ${c.customer_name}` : ''}
401 {tenants.length > 0 && (
403 <label htmlFor="tenant" style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
408 value={formData.assigned_tenant_id}
409 onChange={(e) => setFormData(prev => ({ ...prev, assigned_tenant_id: e.target.value }))}
410 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px', color: 'var(--text)' }}
412 <option value="">Unassigned</option>
414 <option key={t.tenant_id} value={t.tenant_id}>
415 {t.name} ({t.subdomain})
425 className="btn btn-primary"
426 style={{ marginTop: '8px', display: 'flex', alignItems: 'center', gap: '8px', justifyContent: 'center' }}
430 <span className="spinner" style={{ width: '16px', height: '16px' }}></span>
435 <span className="material-symbols-outlined">save</span>
449function getStatusColor(status) {
450 switch (status?.toLowerCase()) {
451 case 'active': return 'success';
452 case 'running': return 'success';
453 case 'building': return 'info';
454 case 'deploying': return 'info';
455 case 'pending': return 'warning';
456 case 'error': return 'danger';
457 case 'deleted': return 'error';
458 case 'terminated': return 'error';
459 case 'canceled': return 'warning';
460 default: return 'secondary';
464function getStatusLabel(status) {
465 if (status?.toLowerCase() === 'deleted') return 'terminated';
469export default HostingAppDetail;