billing adn account

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-05 00:11:06 +00:00
parent 3a7ea1f4f3
commit 6b291671bd
6 changed files with 1373 additions and 18 deletions

View 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>
);
}