Update configuration and UI components
This commit is contained in:
parent
1b1d2651f3
commit
5bb53dec22
|
|
@ -26,6 +26,12 @@ class Config:
|
||||||
CUSTOMER_API_URL = os.getenv('CUSTOMER_API_URL', 'http://localhost:5000')
|
CUSTOMER_API_URL = os.getenv('CUSTOMER_API_URL', 'http://localhost:5000')
|
||||||
CUSTOMER_API_INTERNAL_KEY = os.getenv('CUSTOMER_API_INTERNAL_KEY', 'internal-api-key')
|
CUSTOMER_API_INTERNAL_KEY = os.getenv('CUSTOMER_API_INTERNAL_KEY', 'internal-api-key')
|
||||||
|
|
||||||
|
# Customer Database (read-only access to hosting platform DB)
|
||||||
|
CUSTOMER_DATABASE_URI = os.getenv(
|
||||||
|
'CUSTOMER_DATABASE_URL',
|
||||||
|
'postgresql://hosting:hosting_pass@localhost/hosting'
|
||||||
|
)
|
||||||
|
|
||||||
# CORS
|
# CORS
|
||||||
CORS_ORIGINS = os.getenv('CORS_ORIGINS', 'http://localhost:5173,https://admin.argeict.net').split(',')
|
CORS_ORIGINS = os.getenv('CORS_ORIGINS', 'http://localhost:5173,https://admin.argeict.net').split(',')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
VITE_API_URL=https://admin-api.argeict.net
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.2" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px" y="0px" width="1275.591px" height="417.099px" viewBox="0 0 1275.591 417.099" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<polygon points="160.9,325 160.914,325 160.9,324.944 "/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
|
||||||
|
<rect x="160.913" y="325.007" transform="matrix(-0.9662 -0.2577 0.2577 -0.9662 232.7022 680.5078)" fill="#159052" width="0.053" height="0"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<polygon fill="#159052" points="160.9,324.938 160.9,324.944 160.914,325 160.914,325 "/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<polygon fill="#046D3F" points="282.625,21.69 282.625,21.69 282.625,21.704 "/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<polygon fill="#53BA6F" points="161.095,325.096 160.965,325.014 161.061,325.191 161.106,325 161.106,325 "/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<polygon fill="#53BA6F" points="161.106,325 160.914,325 160.965,325.014 161.095,325.096 "/>
|
||||||
|
</g>
|
||||||
|
<polygon fill="#F69036" points="174.709,246.868 203.22,229.349 237.49,312.503 134.946,386.853 "/>
|
||||||
|
<polygon fill="#0F578B" points="203.201,229.174 237.456,312.301 348.028,235.521 243.98,204.375 "/>
|
||||||
|
<polygon fill="#B42832" points="212.021,114.156 174.709,246.868 243.98,204.375 "/>
|
||||||
|
<polygon fill-rule="evenodd" fill="#159052" points="129.042,0 201.514,0 87.365,386.853 46.251,280.168 "/>
|
||||||
|
<polygon fill-rule="evenodd" fill="#046D3F" points="129.042,0 201.514,0 159.309,143.067 "/>
|
||||||
|
<polygon fill-rule="evenodd" fill="#53BA6F" points="87.365,386.853 125.281,258.348 50.616,291.47 "/>
|
||||||
|
<polygon fill="#B42832" points="222.456,0 261.293,132.308 302.118,0 "/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#B42832" d="M543.577,246.844l-7.372-26.417h-46.73l-7.563,26.417h-25.017l39.653-133.888h35.255l39.645,133.888
|
||||||
|
H543.577z M513.15,137.537l-17.63,62.282h35.042L513.15,137.537z"/>
|
||||||
|
<path fill="#B42832" d="M652.57,246.844l-18.841-43.015c-1.778-4.113-4.26-7.199-7.436-9.287
|
||||||
|
c-3.165-2.079-6.907-3.116-11.212-3.116h-4.81v55.418H585.07V112.956h39.024c7.383,0,14.07,0.615,20.089,1.889
|
||||||
|
c6.004,1.273,11.128,3.364,15.376,6.311c4.225,2.914,7.473,6.761,9.777,11.458c2.278,4.733,3.425,10.501,3.425,17.31
|
||||||
|
c0,4.932-0.713,9.292-2.156,13.112c-1.428,3.831-3.433,7.125-6.028,9.907c-2.607,2.744-5.707,5.003-9.291,6.691
|
||||||
|
c-3.593,1.705-7.484,2.909-11.729,3.587c3.275,0.688,6.277,2.546,9.014,5.589c2.741,3.035,5.46,7.426,8.188,13.158l21.422,44.876
|
||||||
|
H652.57z M646.336,152.394c0-6.567-2.091-11.265-6.248-14.14c-4.178-2.869-10.419-4.307-18.756-4.307h-11.06v37.799h10.245
|
||||||
|
c3.887,0,7.423-0.396,10.606-1.214c3.153-0.826,5.881-2.051,8.148-3.688c2.25-1.646,3.982-3.678,5.217-6.103
|
||||||
|
C645.711,158.326,646.336,155.537,646.336,152.394z"/>
|
||||||
|
<path fill="#B42832" d="M745.493,190.202v-20.801h47.632v71.407c-6.143,2.866-12.562,4.988-19.26,6.351
|
||||||
|
c-6.692,1.366-13.946,2.039-21.803,2.039c-9.293,0-17.636-1.441-25.062-4.301c-7.413-2.854-13.713-7.145-18.895-12.795
|
||||||
|
c-5.2-5.679-9.177-12.72-11.952-21.113c-2.759-8.406-4.161-18.128-4.161-29.204c0-11.115,1.478-21.094,4.381-29.902
|
||||||
|
c2.898-8.815,7.139-16.271,12.689-22.401c5.562-6.103,12.416-10.757,20.495-13.969c8.092-3.218,17.335-4.807,27.722-4.807
|
||||||
|
c6.15,0,12.138,0.521,17.964,1.589c5.844,1.046,11.67,2.448,17.472,4.24v24.887c-5.048-2.379-10.641-4.339-16.76-5.867
|
||||||
|
c-6.107-1.554-12.263-2.321-18.483-2.321c-6.273,0-11.837,1.099-16.623,3.263c-4.837,2.204-8.876,5.32-12.106,9.331
|
||||||
|
c-3.258,4.033-5.7,8.998-7.382,14.865c-1.681,5.871-2.507,12.489-2.507,19.87c0,7.115,0.644,13.522,1.961,19.21
|
||||||
|
c1.285,5.704,3.383,10.538,6.296,14.555c2.891,3.989,6.626,7.041,11.214,9.159c4.572,2.125,10.133,3.165,16.703,3.165
|
||||||
|
c1.565,0,2.927-0.034,4.089-0.15c1.155-0.086,2.252-0.237,3.28-0.457c1.033-0.206,1.982-0.433,2.932-0.714
|
||||||
|
c0.915-0.278,1.881-0.577,2.899-0.931v-34.196H745.493z"/>
|
||||||
|
<path fill="#B42832" d="M821.398,246.844V112.956h80.625v21.312h-55.015v33.275h52.453v20.802h-52.453v37.2h55.015v21.299H821.398
|
||||||
|
z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#0F578B" d="M993.712,112.421v134.423h-17.347V112.421H993.712z"/>
|
||||||
|
<path fill="#0F578B" d="M1117.937,242.453c-6.378,3.192-19.149,6.389-35.506,6.389c-37.898,0-66.413-23.938-66.413-68.015
|
||||||
|
c0-42.079,28.515-70.623,70.198-70.623c16.764,0,27.323,3.609,31.922,6.015l-4.2,14.152c-6.567-3.208-15.953-5.589-27.112-5.589
|
||||||
|
c-31.517,0-52.454,20.151-52.454,55.463c0,32.903,18.953,54.037,51.652,54.037c10.565,0,21.347-2.207,28.335-5.578
|
||||||
|
L1117.937,242.453z"/>
|
||||||
|
<path fill="#0F578B" d="M1170.709,127.163h-40.896v-14.742h99.526v14.742h-41.078v119.681h-17.553V127.163z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
|
|
@ -5,6 +5,7 @@ import Dashboard from './pages/Dashboard'
|
||||||
import Plans from './pages/Plans'
|
import Plans from './pages/Plans'
|
||||||
import CFAccounts from './pages/CFAccounts'
|
import CFAccounts from './pages/CFAccounts'
|
||||||
import Customers from './pages/Customers'
|
import Customers from './pages/Customers'
|
||||||
|
import CloudflareAccounts from './pages/CloudflareAccounts'
|
||||||
import Profile from './pages/Profile'
|
import Profile from './pages/Profile'
|
||||||
import PrivateRoute from './components/PrivateRoute'
|
import PrivateRoute from './components/PrivateRoute'
|
||||||
|
|
||||||
|
|
@ -17,6 +18,7 @@ function App() {
|
||||||
<Route path="/" element={<PrivateRoute><Dashboard /></PrivateRoute>} />
|
<Route path="/" element={<PrivateRoute><Dashboard /></PrivateRoute>} />
|
||||||
<Route path="/plans" element={<PrivateRoute><Plans /></PrivateRoute>} />
|
<Route path="/plans" element={<PrivateRoute><Plans /></PrivateRoute>} />
|
||||||
<Route path="/cf-accounts" element={<PrivateRoute><CFAccounts /></PrivateRoute>} />
|
<Route path="/cf-accounts" element={<PrivateRoute><CFAccounts /></PrivateRoute>} />
|
||||||
|
<Route path="/cloudflare-accounts" element={<PrivateRoute><CloudflareAccounts /></PrivateRoute>} />
|
||||||
<Route path="/customers" element={<PrivateRoute><Customers /></PrivateRoute>} />
|
<Route path="/customers" element={<PrivateRoute><Customers /></PrivateRoute>} />
|
||||||
<Route path="/profile" element={<PrivateRoute><Profile /></PrivateRoute>} />
|
<Route path="/profile" element={<PrivateRoute><Profile /></PrivateRoute>} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@ const Layout = ({ children }) => {
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ path: '/', label: 'Dashboard', icon: '📊' },
|
{ path: '/', label: 'Dashboard', icon: '📊' },
|
||||||
{ path: '/customers', label: 'Customers', icon: '👥' },
|
{ path: '/customers', label: 'Customers', icon: '👥' },
|
||||||
|
{ path: '/cloudflare-accounts', label: 'CF Accounts', icon: '☁️' },
|
||||||
{ path: '/plans', label: 'Plans', icon: '📦' },
|
{ path: '/plans', label: 'Plans', icon: '📦' },
|
||||||
{ path: '/cf-accounts', label: 'CF Accounts', icon: '☁️' },
|
{ path: '/cf-accounts', label: 'Admin CF', icon: '🔧' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,294 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import Layout from '../components/Layout';
|
import Layout from '../components/Layout';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
|
||||||
const Customers = () => {
|
const Customers = () => {
|
||||||
const [customers, setCustomers] = useState([]);
|
const [customers, setCustomers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [stats, setStats] = useState(null);
|
||||||
|
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||||
|
const [showPlanModal, setShowPlanModal] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCustomers();
|
fetchCustomers();
|
||||||
}, []);
|
fetchStats();
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
const fetchCustomers = async () => {
|
const fetchCustomers = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/api/customers');
|
setLoading(true);
|
||||||
|
const response = await api.get(`/api/customers?search=${search}`);
|
||||||
setCustomers(response.data.customers || []);
|
setCustomers(response.data.customers || []);
|
||||||
} catch (error) {
|
setError('');
|
||||||
console.error('Failed to fetch customers:', error);
|
} catch (err) {
|
||||||
|
setError('Failed to load customers');
|
||||||
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get('/api/customers/stats');
|
||||||
|
setStats(response.data.stats);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load stats:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdatePlan = async (customerId, planData) => {
|
||||||
|
try {
|
||||||
|
await api.put(`/api/customers/${customerId}/plan`, planData);
|
||||||
|
fetchCustomers();
|
||||||
|
setShowPlanModal(false);
|
||||||
|
setSelectedCustomer(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update plan:', err);
|
||||||
|
alert('Failed to update plan');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (isActive, subscriptionStatus) => {
|
||||||
|
if (!isActive) {
|
||||||
|
return <span className="badge badge-danger">Inactive</span>;
|
||||||
|
}
|
||||||
|
if (subscriptionStatus === 'active') {
|
||||||
|
return <span className="badge badge-success">Active</span>;
|
||||||
|
}
|
||||||
|
if (subscriptionStatus === 'suspended') {
|
||||||
|
return <span className="badge badge-warning">Suspended</span>;
|
||||||
|
}
|
||||||
|
return <span className="badge badge-secondary">{subscriptionStatus}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlanBadge = (plan) => {
|
||||||
|
const colors = {
|
||||||
|
free: 'secondary',
|
||||||
|
basic: 'primary',
|
||||||
|
pro: 'accent',
|
||||||
|
enterprise: 'success'
|
||||||
|
};
|
||||||
|
return <span className={`badge badge-${colors[plan] || 'secondary'}`}>{plan?.toUpperCase() || 'FREE'}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="mb-8">
|
<div className="mb-6">
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Customers</h1>
|
<h1 className="text-3xl font-bold text-gray-900">Customers</h1>
|
||||||
<p className="text-gray-600 mt-1">View and manage customer accounts</p>
|
<p className="text-gray-600 mt-1">View and manage customer accounts</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{/* Stats Cards */}
|
||||||
<div className="flex items-center justify-center py-12">
|
{stats && (
|
||||||
<div className="w-16 h-16 border-4 border-primary-500 border-t-transparent rounded-full animate-spin"></div>
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-medium text-gray-500 mb-2">Total Customers</h3>
|
||||||
|
<p className="text-3xl font-bold text-primary">{stats.customers?.total || 0}</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-medium text-gray-500 mb-2">Active Customers</h3>
|
||||||
|
<p className="text-3xl font-bold text-success">{stats.customers?.active || 0}</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-medium text-gray-500 mb-2">Total Domains</h3>
|
||||||
|
<p className="text-3xl font-bold text-accent">{stats.domains?.total || 0}</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-medium text-gray-500 mb-2">CF Accounts</h3>
|
||||||
|
<p className="text-3xl font-bold text-secondary">{stats.cf_accounts || 0}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : customers.length === 0 ? (
|
)}
|
||||||
<div className="card text-center py-12">
|
|
||||||
<div className="text-6xl mb-4">👥</div>
|
{/* Search */}
|
||||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">No Customers Yet</h3>
|
<div className="card mb-6">
|
||||||
<p className="text-gray-600">
|
<input
|
||||||
Customer data will appear here once the customer platform API is connected.
|
type="text"
|
||||||
</p>
|
placeholder="Search customers by name, email, or company..."
|
||||||
|
className="input w-full"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-danger mb-6">
|
||||||
|
{error}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)}
|
||||||
<div className="card">
|
|
||||||
|
{/* Customers Table */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
<table className="table">
|
<table className="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Customer</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
|
<th>Company</th>
|
||||||
<th>Plan</th>
|
<th>Plan</th>
|
||||||
<th>Domains</th>
|
<th>Domains</th>
|
||||||
<th>Containers</th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Joined</th>
|
<th>Created</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{customers.map((customer) => (
|
{loading ? (
|
||||||
<tr key={customer.id}>
|
<tr>
|
||||||
<td className="font-medium">{customer.name}</td>
|
<td colSpan="8" className="text-center py-8">
|
||||||
<td className="text-gray-600">{customer.email}</td>
|
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto"></div>
|
||||||
<td>
|
|
||||||
<span className="badge badge-info">{customer.plan}</span>
|
|
||||||
</td>
|
|
||||||
<td>{customer.domains_count || 0}</td>
|
|
||||||
<td>{customer.containers_count || 0}</td>
|
|
||||||
<td>
|
|
||||||
<span className={`badge ${customer.is_active ? 'badge-success' : 'badge-danger'}`}>
|
|
||||||
{customer.is_active ? 'Active' : 'Inactive'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="text-gray-600 text-sm">
|
|
||||||
{new Date(customer.created_at).toLocaleDateString()}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button className="text-primary-600 hover:text-primary-700">
|
|
||||||
View
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
) : customers.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="8" className="text-center py-12">
|
||||||
|
<div className="text-6xl mb-4">👥</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">No Customers Found</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
{search ? 'Try a different search term' : 'Customer data will appear here'}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
customers.map((customer) => (
|
||||||
|
<tr key={customer.id}>
|
||||||
|
<td className="font-medium">{customer.full_name}</td>
|
||||||
|
<td>{customer.email}</td>
|
||||||
|
<td>{customer.company_name || '-'}</td>
|
||||||
|
<td>{getPlanBadge(customer.subscription_plan)}</td>
|
||||||
|
<td>{customer.domain_count || 0} / {customer.max_domains}</td>
|
||||||
|
<td>{getStatusBadge(customer.is_active, customer.subscription_status)}</td>
|
||||||
|
<td>{new Date(customer.created_at).toLocaleDateString()}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCustomer(customer);
|
||||||
|
setShowPlanModal(true);
|
||||||
|
}}
|
||||||
|
className="text-primary hover:text-primary-dark mr-3"
|
||||||
|
>
|
||||||
|
Edit Plan
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Plan Update Modal */}
|
||||||
|
{showPlanModal && selectedCustomer && (
|
||||||
|
<PlanModal
|
||||||
|
customer={selectedCustomer}
|
||||||
|
onClose={() => {
|
||||||
|
setShowPlanModal(false);
|
||||||
|
setSelectedCustomer(null);
|
||||||
|
}}
|
||||||
|
onUpdate={handleUpdatePlan}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Plan Update Modal Component
|
||||||
|
const PlanModal = ({ customer, onClose, onUpdate }) => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
subscription_plan: customer.subscription_plan || 'free',
|
||||||
|
max_domains: customer.max_domains || 5,
|
||||||
|
max_containers: customer.max_containers || 3,
|
||||||
|
subscription_status: customer.subscription_status || 'active'
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onUpdate(customer.id, formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Update Plan - {customer.full_name}</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Subscription Plan
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="input w-full"
|
||||||
|
value={formData.subscription_plan}
|
||||||
|
onChange={(e) => setFormData({ ...formData, subscription_plan: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="free">Free</option>
|
||||||
|
<option value="basic">Basic</option>
|
||||||
|
<option value="pro">Pro</option>
|
||||||
|
<option value="enterprise">Enterprise</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Max Domains
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input w-full"
|
||||||
|
value={formData.max_domains}
|
||||||
|
onChange={(e) => setFormData({ ...formData, max_domains: parseInt(e.target.value) })}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Max Containers
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input w-full"
|
||||||
|
value={formData.max_containers}
|
||||||
|
onChange={(e) => setFormData({ ...formData, max_containers: parseInt(e.target.value) })}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Status
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="input w-full"
|
||||||
|
value={formData.subscription_status}
|
||||||
|
onChange={(e) => setFormData({ ...formData, subscription_status: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="suspended">Suspended</option>
|
||||||
|
<option value="cancelled">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button type="submit" className="btn-primary flex-1">
|
||||||
|
Update Plan
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onClose} className="btn-outline flex-1">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Customers;
|
export default Customers;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
|
||||||
|
|
@ -82,7 +83,17 @@ const Profile = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-6">Account Settings</h1>
|
{/* Header with back button */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Account Settings</h1>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="btn-outline flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span>←</span>
|
||||||
|
<span>Back to Dashboard</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="mb-6 border-b border-gray-200">
|
<div className="mb-6 border-b border-gray-200">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue