Update configuration and UI components

This commit is contained in:
oguz ozturk 2026-01-12 00:18:30 +03:00
parent 1b1d2651f3
commit 5bb53dec22
8 changed files with 3283 additions and 45 deletions

View File

@ -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(',')

2
frontend/.env.production Normal file
View File

@ -0,0 +1,2 @@
VITE_API_URL=https://admin-api.argeict.net

2947
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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 />} />

View File

@ -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 () => {

View File

@ -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>
) : customers.length === 0 ? (
<div className="card text-center py-12">
<div className="text-6xl mb-4">👥</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">No Customers Yet</h3>
<p className="text-gray-600">
Customer data will appear here once the customer platform API is connected.
</p>
</div>
) : (
<div className="card"> <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>
)}
{/* Search */}
<div className="card mb-6">
<input
type="text"
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>
)}
{/* 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>
<td colSpan="8" className="text-center py-8">
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto"></div>
</td>
</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}> <tr key={customer.id}>
<td className="font-medium">{customer.name}</td> <td className="font-medium">{customer.full_name}</td>
<td className="text-gray-600">{customer.email}</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> <td>
<span className="badge badge-info">{customer.plan}</span> <button
</td> onClick={() => {
<td>{customer.domains_count || 0}</td> setSelectedCustomer(customer);
<td>{customer.containers_count || 0}</td> setShowPlanModal(true);
<td> }}
<span className={`badge ${customer.is_active ? 'badge-success' : 'badge-danger'}`}> className="text-primary hover:text-primary-dark mr-3"
{customer.is_active ? 'Active' : 'Inactive'} >
</span> Edit Plan
</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> </button>
</td> </td>
</tr> </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;

View File

@ -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">