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
|
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'])
|
@customers_bp.route('/stats', methods=['GET'])
|
||||||
@token_required
|
@token_required
|
||||||
def get_customer_stats(current_admin):
|
def get_customer_stats(current_admin):
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const Customers = () => {
|
||||||
const [showPlanModal, setShowPlanModal] = useState(false);
|
const [showPlanModal, setShowPlanModal] = useState(false);
|
||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||||
const [showToast, setShowToast] = useState(false);
|
const [showToast, setShowToast] = useState(false);
|
||||||
const [toastMessage, setToastMessage] = useState('');
|
const [toastMessage, setToastMessage] = useState('');
|
||||||
const [toastType, setToastType] = useState('success'); // success, error
|
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) => {
|
const getStatusBadge = (isActive, subscriptionStatus) => {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
return <span className="badge badge-danger">Inactive</span>;
|
return <span className="badge badge-danger">Inactive</span>;
|
||||||
|
|
@ -256,6 +269,16 @@ const Customers = () => {
|
||||||
>
|
>
|
||||||
📦
|
📦
|
||||||
</button>
|
</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 ? (
|
{customer.subscription_status === 'suspended' || !customer.is_active ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleActivateCustomer(customer.id)}
|
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 */}
|
{/* Toast Notification */}
|
||||||
{showToast && (
|
{showToast && (
|
||||||
<div className={`fixed bottom-6 right-6 px-6 py-4 rounded-lg shadow-lg transform transition-all duration-300 z-50 ${
|
<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;
|
export default Customers;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,12 @@ export const getCustomerStats = async () => {
|
||||||
return response.data;
|
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 {
|
export default {
|
||||||
getCustomers,
|
getCustomers,
|
||||||
getCustomer,
|
getCustomer,
|
||||||
|
|
@ -68,5 +74,6 @@ export default {
|
||||||
updateCustomerPlan,
|
updateCustomerPlan,
|
||||||
updateCustomerStatus,
|
updateCustomerStatus,
|
||||||
getCustomerStats,
|
getCustomerStats,
|
||||||
|
changeCustomerPassword,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue