billing adn account
This commit is contained in:
300
frontend/src/pages/admin/PaymentApprovalPage.tsx
Normal file
300
frontend/src/pages/admin/PaymentApprovalPage.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Admin Payment Approval Page
|
||||
* For approving/rejecting manual payments (bank transfers, wallet payments)
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Check, X, AlertCircle, Loader2, Building2, Wallet, Clock } from 'lucide-react';
|
||||
import {
|
||||
getPendingPayments,
|
||||
approvePayment,
|
||||
rejectPayment,
|
||||
type PendingPayment,
|
||||
} from '../../services/billing.api';
|
||||
|
||||
export default function AdminPaymentApprovalPage() {
|
||||
const [payments, setPayments] = useState<PendingPayment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [processing, setProcessing] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||
const [selectedPayment, setSelectedPayment] = useState<PendingPayment | null>(null);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [approvalNotes, setApprovalNotes] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadPayments();
|
||||
}, []);
|
||||
|
||||
const loadPayments = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getPendingPayments();
|
||||
setPayments(response.results);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load pending payments');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApprove = async (paymentId: number) => {
|
||||
if (!confirm('Are you sure you want to approve this payment?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setProcessing(paymentId);
|
||||
setError('');
|
||||
|
||||
await approvePayment(paymentId, approvalNotes || undefined);
|
||||
|
||||
// Remove from list
|
||||
setPayments(payments.filter((p) => p.id !== paymentId));
|
||||
setApprovalNotes('');
|
||||
alert('Payment approved successfully!');
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to approve payment');
|
||||
} finally {
|
||||
setProcessing(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
if (!selectedPayment || !rejectReason.trim()) {
|
||||
setError('Please provide a rejection reason');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setProcessing(selectedPayment.id);
|
||||
setError('');
|
||||
|
||||
await rejectPayment(selectedPayment.id, rejectReason);
|
||||
|
||||
// Remove from list
|
||||
setPayments(payments.filter((p) => p.id !== selectedPayment.id));
|
||||
setShowRejectModal(false);
|
||||
setSelectedPayment(null);
|
||||
setRejectReason('');
|
||||
alert('Payment rejected successfully!');
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to reject payment');
|
||||
} finally {
|
||||
setProcessing(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getPaymentMethodIcon = (method: string) => {
|
||||
if (method.includes('bank')) return <Building2 className="w-5 h-5" />;
|
||||
if (method.includes('wallet')) return <Wallet className="w-5 h-5" />;
|
||||
return <Clock className="w-5 h-5" />;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Payment Approvals</h1>
|
||||
<p className="text-gray-600">Review and approve manual payment submissions</p>
|
||||
</div>
|
||||
<div className="bg-yellow-100 text-yellow-800 px-4 py-2 rounded-lg font-semibold">
|
||||
{payments.length} Pending
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6 flex items-start gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{payments.length === 0 ? (
|
||||
<div className="bg-white rounded-lg shadow p-12 text-center">
|
||||
<Clock className="w-16 h-16 mx-auto mb-4 text-gray-400" />
|
||||
<h3 className="text-xl font-semibold text-gray-700 mb-2">No Pending Payments</h3>
|
||||
<p className="text-gray-500">All payments have been reviewed</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{payments.map((payment) => (
|
||||
<div key={payment.id} className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Icon */}
|
||||
<div className="p-3 bg-yellow-100 rounded-lg text-yellow-600">
|
||||
{getPaymentMethodIcon(payment.payment_method)}
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{payment.account_name}</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Payment Method: {payment.payment_method.replace('_', ' ').toUpperCase()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
${payment.amount} {payment.currency}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{new Date(payment.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Details */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 bg-gray-50 rounded p-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Transaction Reference
|
||||
</div>
|
||||
<div className="font-mono text-sm bg-white px-2 py-1 rounded border">
|
||||
{payment.transaction_reference}
|
||||
</div>
|
||||
</div>
|
||||
{payment.invoice_number && (
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
Invoice Number
|
||||
</div>
|
||||
<div className="font-mono text-sm bg-white px-2 py-1 rounded border">
|
||||
{payment.invoice_number}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{payment.admin_notes && (
|
||||
<div className="md:col-span-2">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">
|
||||
User Notes
|
||||
</div>
|
||||
<div className="text-sm bg-white px-3 py-2 rounded border">
|
||||
{payment.admin_notes}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Approval Notes Input */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Approval Notes (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={approvalNotes}
|
||||
onChange={(e) => setApprovalNotes(e.target.value)}
|
||||
placeholder="Add any notes about this approval..."
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => handleApprove(payment.id)}
|
||||
disabled={processing === payment.id}
|
||||
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 font-medium"
|
||||
>
|
||||
{processing === payment.id ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Approving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="w-4 h-4" />
|
||||
Approve Payment
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedPayment(payment);
|
||||
setShowRejectModal(true);
|
||||
}}
|
||||
disabled={processing === payment.id}
|
||||
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 font-medium"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
Reject Payment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reject Modal */}
|
||||
{showRejectModal && selectedPayment && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
||||
<h2 className="text-xl font-bold mb-4">Reject Payment</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Please provide a reason for rejecting this payment. The user will be notified.
|
||||
</p>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Rejection Reason *
|
||||
</label>
|
||||
<textarea
|
||||
value={rejectReason}
|
||||
onChange={(e) => setRejectReason(e.target.value)}
|
||||
placeholder="e.g., Transaction reference not found, incorrect amount, invalid payment proof..."
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowRejectModal(false);
|
||||
setSelectedPayment(null);
|
||||
setRejectReason('');
|
||||
}}
|
||||
disabled={processing !== null}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReject}
|
||||
disabled={processing !== null || !rejectReason.trim()}
|
||||
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{processing !== null ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Rejecting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X className="w-4 h-4" />
|
||||
Reject
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user