feat: Add customer management UI (edit, delete, suspend, activate)

This commit is contained in:
oguz ozturk 2026-01-12 17:59:16 +03:00
parent 0092003a7f
commit 79d6c3c5e6
3 changed files with 498 additions and 18 deletions

View File

@ -68,6 +68,85 @@ def get_customer(current_admin, customer_id):
except Exception as e:
return jsonify({'error': str(e)}), 500
@customers_bp.route('/<int:customer_id>', methods=['PUT'])
@token_required
def update_customer(current_admin, customer_id):
"""Update customer information"""
try:
data = request.get_json()
result = call_customer_api(
f'/api/admin/customers/{customer_id}',
method='PUT',
data=data
)
if result and result.get('status') == 'success':
return jsonify(result), 200
else:
return jsonify(result or {'error': 'Failed to update customer'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@customers_bp.route('/<int:customer_id>', methods=['DELETE'])
@token_required
def delete_customer(current_admin, customer_id):
"""Delete customer"""
try:
result = call_customer_api(
f'/api/admin/customers/{customer_id}',
method='DELETE'
)
if result and result.get('status') == 'success':
return jsonify(result), 200
else:
return jsonify(result or {'error': 'Failed to delete customer'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@customers_bp.route('/<int:customer_id>/suspend', methods=['POST'])
@token_required
def suspend_customer(current_admin, customer_id):
"""Suspend customer"""
try:
result = call_customer_api(
f'/api/admin/customers/{customer_id}/suspend',
method='POST'
)
if result and result.get('status') == 'success':
return jsonify(result), 200
else:
return jsonify(result or {'error': 'Failed to suspend customer'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@customers_bp.route('/<int:customer_id>/activate', methods=['POST'])
@token_required
def activate_customer(current_admin, customer_id):
"""Activate customer"""
try:
result = call_customer_api(
f'/api/admin/customers/{customer_id}/activate',
method='POST'
)
if result and result.get('status') == 'success':
return jsonify(result), 200
else:
return jsonify(result or {'error': 'Failed to activate customer'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@customers_bp.route('/<int:customer_id>/plan', methods=['PUT'])
@token_required
def update_customer_plan(current_admin, customer_id):
@ -81,14 +160,37 @@ def update_customer_plan(current_admin, customer_id):
data=data
)
if result:
if result and result.get('status') == 'success':
return jsonify(result), 200
else:
return jsonify({'error': 'Failed to update plan'}), 500
return jsonify(result or {'error': 'Failed to update plan'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@customers_bp.route('/<int:customer_id>/status', methods=['PUT'])
@token_required
def update_customer_status(current_admin, customer_id):
"""Update customer status"""
try:
data = request.get_json()
result = call_customer_api(
f'/api/admin/customers/{customer_id}/status',
method='PUT',
data=data
)
if result and result.get('status') == 'success':
return jsonify(result), 200
else:
return jsonify(result or {'error': 'Failed to update status'}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500
@customers_bp.route('/stats', methods=['GET'])
@token_required
def get_customer_stats(current_admin):

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import Layout from '../components/Layout';
import api from '../services/api';
import customerService from '../services/customerService';
const Customers = () => {
const [customers, setCustomers] = useState([]);
@ -11,6 +12,8 @@ const Customers = () => {
const [stats, setStats] = useState(null);
const [selectedCustomer, setSelectedCustomer] = useState(null);
const [showPlanModal, setShowPlanModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
useEffect(() => {
fetchCustomers();
@ -42,13 +45,66 @@ const Customers = () => {
const handleUpdatePlan = async (customerId, planData) => {
try {
await api.put(`/api/customers/${customerId}/plan`, planData);
await customerService.updateCustomerPlan(customerId, planData);
fetchCustomers();
setShowPlanModal(false);
setSelectedCustomer(null);
alert('Plan updated successfully!');
} catch (err) {
console.error('Failed to update plan:', err);
alert('Failed to update plan');
alert('Failed to update plan: ' + (err.response?.data?.message || err.message));
}
};
const handleUpdateCustomer = async (customerId, customerData) => {
try {
await customerService.updateCustomer(customerId, customerData);
fetchCustomers();
setShowEditModal(false);
setSelectedCustomer(null);
alert('Customer updated successfully!');
} catch (err) {
console.error('Failed to update customer:', err);
alert('Failed to update customer: ' + (err.response?.data?.message || err.message));
}
};
const handleDeleteCustomer = async (customerId) => {
try {
await customerService.deleteCustomer(customerId);
fetchCustomers();
setShowDeleteConfirm(false);
setSelectedCustomer(null);
alert('Customer deleted successfully!');
} catch (err) {
console.error('Failed to delete customer:', err);
alert('Failed to delete customer: ' + (err.response?.data?.message || err.message));
}
};
const handleSuspendCustomer = async (customerId) => {
if (!confirm('Are you sure you want to suspend this customer?')) return;
try {
await customerService.suspendCustomer(customerId);
fetchCustomers();
alert('Customer suspended successfully!');
} catch (err) {
console.error('Failed to suspend customer:', err);
alert('Failed to suspend customer: ' + (err.response?.data?.message || err.message));
}
};
const handleActivateCustomer = async (customerId) => {
if (!confirm('Are you sure you want to activate this customer?')) return;
try {
await customerService.activateCustomer(customerId);
fetchCustomers();
alert('Customer activated successfully!');
} catch (err) {
console.error('Failed to activate customer:', err);
alert('Failed to activate customer: ' + (err.response?.data?.message || err.message));
}
};
@ -166,15 +222,55 @@ const Customers = () => {
<td>{getStatusBadge(customer.is_active, customer.subscription_status)}</td>
<td>{new Date(customer.created_at).toLocaleDateString()}</td>
<td>
<div className="flex gap-2">
<button
onClick={() => {
setSelectedCustomer(customer);
setShowEditModal(true);
}}
className="text-blue-600 hover:text-blue-800"
title="Edit Customer"
>
</button>
<button
onClick={() => {
setSelectedCustomer(customer);
setShowPlanModal(true);
}}
className="text-primary hover:text-primary-dark mr-3"
className="text-purple-600 hover:text-purple-800"
title="Edit Plan"
>
Edit Plan
📦
</button>
{customer.subscription_status === 'suspended' || !customer.is_active ? (
<button
onClick={() => handleActivateCustomer(customer.id)}
className="text-green-600 hover:text-green-800"
title="Activate Customer"
>
</button>
) : (
<button
onClick={() => handleSuspendCustomer(customer.id)}
className="text-orange-600 hover:text-orange-800"
title="Suspend Customer"
>
</button>
)}
<button
onClick={() => {
setSelectedCustomer(customer);
setShowDeleteConfirm(true);
}}
className="text-red-600 hover:text-red-800"
title="Delete Customer"
>
🗑
</button>
</div>
</td>
</tr>
))
@ -184,6 +280,18 @@ const Customers = () => {
</div>
</div>
{/* Edit Customer Modal */}
{showEditModal && selectedCustomer && (
<EditCustomerModal
customer={selectedCustomer}
onClose={() => {
setShowEditModal(false);
setSelectedCustomer(null);
}}
onUpdate={handleUpdateCustomer}
/>
)}
{/* Plan Update Modal */}
{showPlanModal && selectedCustomer && (
<PlanModal
@ -195,10 +303,160 @@ const Customers = () => {
onUpdate={handleUpdatePlan}
/>
)}
{/* Delete Confirmation Modal */}
{showDeleteConfirm && selectedCustomer && (
<DeleteConfirmModal
customer={selectedCustomer}
onClose={() => {
setShowDeleteConfirm(false);
setSelectedCustomer(null);
}}
onConfirm={handleDeleteCustomer}
/>
)}
</Layout>
);
};
// Edit Customer Modal Component
const EditCustomerModal = ({ customer, onClose, onUpdate }) => {
const [formData, setFormData] = useState({
full_name: customer.full_name || '',
email: customer.email || '',
company_name: customer.company_name || '',
phone: customer.phone || '',
billing_address: customer.billing_address || '',
billing_city: customer.billing_city || '',
billing_country: customer.billing_country || '',
billing_postal_code: customer.billing_postal_code || ''
});
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-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<h2 className="text-2xl font-bold mb-4">Edit Customer - {customer.full_name}</h2>
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-2 gap-4">
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Full Name *
</label>
<input
type="text"
className="input w-full"
value={formData.full_name}
onChange={(e) => setFormData({ ...formData, full_name: e.target.value })}
required
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Email *
</label>
<input
type="email"
className="input w-full"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Company Name
</label>
<input
type="text"
className="input w-full"
value={formData.company_name}
onChange={(e) => setFormData({ ...formData, company_name: e.target.value })}
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Phone
</label>
<input
type="text"
className="input w-full"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
/>
</div>
<div className="mb-4 col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Billing Address
</label>
<textarea
className="input w-full"
rows="2"
value={formData.billing_address}
onChange={(e) => setFormData({ ...formData, billing_address: e.target.value })}
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
City
</label>
<input
type="text"
className="input w-full"
value={formData.billing_city}
onChange={(e) => setFormData({ ...formData, billing_city: e.target.value })}
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Country
</label>
<input
type="text"
className="input w-full"
value={formData.billing_country}
onChange={(e) => setFormData({ ...formData, billing_country: e.target.value })}
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Postal Code
</label>
<input
type="text"
className="input w-full"
value={formData.billing_postal_code}
onChange={(e) => setFormData({ ...formData, billing_postal_code: e.target.value })}
/>
</div>
</div>
<div className="flex gap-3 mt-6">
<button type="submit" className="btn-primary flex-1">
Update Customer
</button>
<button type="button" onClick={onClose} className="btn-outline flex-1">
Cancel
</button>
</div>
</form>
</div>
</div>
);
};
// Plan Update Modal Component
const PlanModal = ({ customer, onClose, onUpdate }) => {
const [formData, setFormData] = useState({
@ -290,5 +548,53 @@ const PlanModal = ({ customer, onClose, onUpdate }) => {
);
};
// Delete Confirmation Modal Component
const DeleteConfirmModal = ({ customer, onClose, onConfirm }) => {
const handleConfirm = () => {
onConfirm(customer.id);
};
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">
<div className="text-center mb-6">
<div className="text-6xl mb-4"></div>
<h2 className="text-2xl font-bold text-red-600 mb-2">Delete Customer</h2>
<p className="text-gray-700">
Are you sure you want to delete <strong>{customer.full_name}</strong>?
</p>
<p className="text-sm text-gray-600 mt-2">
This will permanently delete:
</p>
<ul className="text-sm text-gray-600 mt-2 text-left list-disc list-inside">
<li>Customer account and profile</li>
<li>All domains ({customer.domain_count || 0})</li>
<li>All DNS records</li>
<li>All associated data</li>
</ul>
<p className="text-sm text-red-600 font-semibold mt-3">
This action cannot be undone!
</p>
</div>
<div className="flex gap-3">
<button
onClick={handleConfirm}
className="flex-1 bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700"
>
Yes, Delete
</button>
<button
onClick={onClose}
className="flex-1 btn-outline"
>
Cancel
</button>
</div>
</div>
</div>
);
};
export default Customers;

View File

@ -0,0 +1,72 @@
import api from './api';
/**
* Customer Management Service
*/
// Get all customers
export const getCustomers = async (params = {}) => {
const response = await api.get('/api/customers', { params });
return response.data;
};
// Get single customer
export const getCustomer = async (customerId) => {
const response = await api.get(`/api/customers/${customerId}`);
return response.data;
};
// Update customer information
export const updateCustomer = async (customerId, data) => {
const response = await api.put(`/api/customers/${customerId}`, data);
return response.data;
};
// Delete customer
export const deleteCustomer = async (customerId) => {
const response = await api.delete(`/api/customers/${customerId}`);
return response.data;
};
// Suspend customer
export const suspendCustomer = async (customerId) => {
const response = await api.post(`/api/customers/${customerId}/suspend`);
return response.data;
};
// Activate customer
export const activateCustomer = async (customerId) => {
const response = await api.post(`/api/customers/${customerId}/activate`);
return response.data;
};
// Update customer plan
export const updateCustomerPlan = async (customerId, planData) => {
const response = await api.put(`/api/customers/${customerId}/plan`, planData);
return response.data;
};
// Update customer status
export const updateCustomerStatus = async (customerId, statusData) => {
const response = await api.put(`/api/customers/${customerId}/status`, statusData);
return response.data;
};
// Get customer statistics
export const getCustomerStats = async () => {
const response = await api.get('/api/customers/stats');
return response.data;
};
export default {
getCustomers,
getCustomer,
updateCustomer,
deleteCustomer,
suspendCustomer,
activateCustomer,
updateCustomerPlan,
updateCustomerStatus,
getCustomerStats,
};