feat: Add customer password change functionality in admin panel
This commit is contained in:
parent
9cba460d4d
commit
50a7717c15
|
|
@ -191,6 +191,28 @@ def update_customer_status(current_admin, customer_id):
|
|||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@customers_bp.route('/<int:customer_id>/password', methods=['PUT'])
|
||||
@token_required
|
||||
def change_customer_password(current_admin, customer_id):
|
||||
"""Change customer password"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
result = call_customer_api(
|
||||
f'/api/admin/customers/{customer_id}/password',
|
||||
method='PUT',
|
||||
data=data
|
||||
)
|
||||
|
||||
if result and result.get('status') == 'success':
|
||||
return jsonify(result), 200
|
||||
else:
|
||||
return jsonify(result or {'error': 'Failed to change password'}), 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):
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const Customers = () => {
|
|||
const [showPlanModal, setShowPlanModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [toastMessage, setToastMessage] = useState('');
|
||||
const [toastType, setToastType] = useState('success'); // success, error
|
||||
|
|
@ -121,6 +122,18 @@ const Customers = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleChangePassword = async (customerId, passwordData) => {
|
||||
try {
|
||||
await customerService.changeCustomerPassword(customerId, passwordData);
|
||||
setShowPasswordModal(false);
|
||||
setSelectedCustomer(null);
|
||||
showSuccessToast('Password changed successfully!');
|
||||
} catch (err) {
|
||||
console.error('Failed to change password:', err);
|
||||
showErrorToast('Failed to change password: ' + (err.response?.data?.message || err.message));
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (isActive, subscriptionStatus) => {
|
||||
if (!isActive) {
|
||||
return <span className="badge badge-danger">Inactive</span>;
|
||||
|
|
@ -256,6 +269,16 @@ const Customers = () => {
|
|||
>
|
||||
📦
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedCustomer(customer);
|
||||
setShowPasswordModal(true);
|
||||
}}
|
||||
className="text-indigo-600 hover:text-indigo-800"
|
||||
title="Change Password"
|
||||
>
|
||||
🔑
|
||||
</button>
|
||||
{customer.subscription_status === 'suspended' || !customer.is_active ? (
|
||||
<button
|
||||
onClick={() => handleActivateCustomer(customer.id)}
|
||||
|
|
@ -329,6 +352,18 @@ const Customers = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Password Change Modal */}
|
||||
{showPasswordModal && selectedCustomer && (
|
||||
<PasswordChangeModal
|
||||
customer={selectedCustomer}
|
||||
onClose={() => {
|
||||
setShowPasswordModal(false);
|
||||
setSelectedCustomer(null);
|
||||
}}
|
||||
onUpdate={handleChangePassword}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Toast Notification */}
|
||||
{showToast && (
|
||||
<div className={`fixed bottom-6 right-6 px-6 py-4 rounded-lg shadow-lg transform transition-all duration-300 z-50 ${
|
||||
|
|
@ -631,5 +666,120 @@ const DeleteConfirmModal = ({ customer, onClose, onConfirm }) => {
|
|||
);
|
||||
};
|
||||
|
||||
// Password Change Modal Component
|
||||
const PasswordChangeModal = ({ customer, onClose, onUpdate }) => {
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
// Validation
|
||||
if (!newPassword) {
|
||||
setError('Password is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
setError('Password must be at least 8 characters');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
setError('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await onUpdate(customer.id, { new_password: newPassword });
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || 'Failed to change password');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Change Password</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 text-2xl"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Customer:</strong> {customer.full_name} ({customer.email})
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="input w-full"
|
||||
placeholder="Enter new password (min 8 characters)"
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="input w-full"
|
||||
placeholder="Confirm new password"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 btn-primary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Changing...' : 'Change Password'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 btn-outline"
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Customers;
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ export const getCustomerStats = async () => {
|
|||
return response.data;
|
||||
};
|
||||
|
||||
// Change customer password
|
||||
export const changeCustomerPassword = async (customerId, passwordData) => {
|
||||
const response = await api.put(`/api/customers/${customerId}/password`, passwordData);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export default {
|
||||
getCustomers,
|
||||
getCustomer,
|
||||
|
|
@ -68,5 +74,6 @@ export default {
|
|||
updateCustomerPlan,
|
||||
updateCustomerStatus,
|
||||
getCustomerStats,
|
||||
changeCustomerPassword,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue