feat: Add customer management UI (edit, delete, suspend, activate)
This commit is contained in:
parent
0092003a7f
commit
79d6c3c5e6
|
|
@ -68,34 +68,136 @@ 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):
|
||||
"""Update customer's subscription plan"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
|
||||
result = call_customer_api(
|
||||
f'/api/admin/customers/{customer_id}/plan',
|
||||
method='PUT',
|
||||
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):
|
||||
"""Get customer statistics"""
|
||||
try:
|
||||
result = call_customer_api('/api/admin/stats')
|
||||
|
||||
|
||||
if result:
|
||||
return jsonify(result), 200
|
||||
else:
|
||||
|
|
@ -109,7 +211,7 @@ def get_customer_stats(current_admin):
|
|||
'total_containers': 0
|
||||
}
|
||||
}), 200
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedCustomer(customer);
|
||||
setShowPlanModal(true);
|
||||
}}
|
||||
className="text-primary hover:text-primary-dark mr-3"
|
||||
>
|
||||
Edit Plan
|
||||
</button>
|
||||
<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-purple-600 hover:text-purple-800"
|
||||
title="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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue