This commit is contained in:
suvodip ghosh
2025-04-22 13:46:38 +00:00
parent 10a2c0c2c3
commit 5a32c1a7d2
18 changed files with 2026 additions and 58 deletions

View File

@@ -0,0 +1,422 @@
import React, { useEffect, useState } from "react";
import { useIsLoggedIn } from '../../lib/isLoggedIn';
import Loader from "../../components/ui/loader";
import { PDFDownloadLink, PDFViewer } from '@react-pdf/renderer';
import InvoicePDF from "../../lib/InvoicePDF"; // We'll create this component next
export default function AllSellingList() {
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [billingData, setBillingData] = useState([]);
const [dataLoading, setDataLoading] = useState(true);
const [apiError, setApiError] = useState(null);
const [selectedItem, setSelectedItem] = useState(null);
const [showModal, setShowModal] = useState(false);
const [editMode, setEditMode] = useState(false);
const [formData, setFormData] = useState({
service: '',
tenure: 'monthly',
amount: '',
user: '',
status: 'pending'
});
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
useEffect(() => {
if (isLoggedIn && sessionData?.user_type === 'admin') {
fetchBillingData();
}
}, [isLoggedIn, sessionData]);
const fetchBillingData = async () => {
try {
const res = await fetch(`${INVOICE_API_URL}?query=all-selling-list`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const data = await res.json();
setBillingData(data.data || []);
} catch (err) {
setApiError(err.message);
} finally {
setDataLoading(false);
}
};
const handleViewItem = (item) => {
setSelectedItem(item);
setShowModal(true);
setEditMode(false);
};
const handleEditItem = (item) => {
setSelectedItem(item);
setFormData({
service: item.service,
tenure: item.tenure,
amount: item.amount,
user: item.user,
status: item.status
});
setEditMode(true);
setShowModal(true);
};
const handleDeleteItem = async (id) => {
if (!window.confirm('Are you sure you want to delete this billing record?')) return;
try {
const res = await fetch(`${INVOICE_API_URL}?query=delete-billing&id=${id}`, {
method: 'DELETE',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
fetchBillingData();
} catch (err) {
setApiError(err.message);
}
};
const handleCreateNew = () => {
setSelectedItem(null);
setFormData({
service: '',
tenure: 'monthly',
amount: '',
user: '',
status: 'pending'
});
setEditMode(true);
setShowModal(true);
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const url = selectedItem
? `${INVOICE_API_URL}?query=update-billing&id=${selectedItem.id}`
: `${INVOICE_API_URL}?query=create-billing`;
const method = selectedItem ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
fetchBillingData();
setShowModal(false);
} catch (err) {
setApiError(err.message);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const closeModal = () => {
setShowModal(false);
setSelectedItem(null);
setEditMode(false);
};
const getStatusColor = (status) => {
switch (status) {
case 'completed': return 'bg-green-100 text-green-800';
case 'pending': return 'bg-yellow-100 text-yellow-800';
case 'failed': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
return new Date(dateString).toLocaleDateString(undefined, options);
};
const formatCurrency = (amount) => {
if (!amount) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(parseFloat(amount));
};
if (loading || dataLoading) return <Loader />;
if (error || apiError) return <p>Error: {error?.message || apiError}</p>;
if (!isLoggedIn || sessionData?.user_type !== 'admin') {
return <p className="text-center mt-8">You are not authorized to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
}
return (
<section className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Billing Management</h1>
<button
onClick={handleCreateNew}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Create New
</button>
</div>
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Billing ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Service</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{billingData.map((item) => (
<tr key={item.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{item.billing_id}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{item.service}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{item.user}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">
{formatCurrency(item.amount)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(item.status)}`}>
{item.status.charAt(0).toUpperCase() + item.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(item.created_at)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
onClick={() => handleViewItem(item)}
className="text-indigo-600 hover:text-indigo-900 mr-2"
>
View
</button>
<button
onClick={() => handleEditItem(item)}
className="text-yellow-600 hover:text-yellow-900 mr-2"
>
Edit
</button>
<button
onClick={() => handleDeleteItem(item.id)}
className="text-red-600 hover:text-red-900 mr-2"
>
Delete
</button>
<PDFDownloadLink
document={<InvoicePDF data={item} />}
fileName={`invoice_${item.billing_id}.pdf`}
className="text-green-600 hover:text-green-900"
>
{({ loading }) => (loading ? 'Preparing...' : 'PDF')}
</PDFDownloadLink>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Modal for View/Edit */}
{showModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex justify-between items-start">
<h2 className="text-xl font-bold mb-4">
{editMode ? (selectedItem ? 'Edit Billing' : 'Create New Billing') : 'Billing Details'}
</h2>
<button
onClick={closeModal}
className="text-gray-500 hover:text-gray-700"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{editMode ? (
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Service</label>
<input
type="text"
name="service"
value={formData.service}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Tenure</label>
<select
name="tenure"
value={formData.tenure}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="monthly">Monthly</option>
<option value="yearly">Yearly</option>
<option value="one-time">One-time</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Amount</label>
<input
type="number"
name="amount"
value={formData.amount}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
step="0.01"
min="0"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">User Email</label>
<input
type="email"
name="user"
value={formData.user}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select
name="status"
value={formData.status}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
>
<option value="pending">Pending</option>
<option value="completed">Completed</option>
<option value="failed">Failed</option>
</select>
</div>
</div>
<div className="mt-6 flex justify-end space-x-4">
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
{selectedItem ? 'Update' : 'Create'}
</button>
<button
type="button"
onClick={closeModal}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
>
Cancel
</button>
</div>
</form>
) : (
<>
<div className="grid grid-cols-2 gap-4 mb-6">
<div>
<h3 className="font-semibold text-gray-700">Billing ID</h3>
<p>{selectedItem.billing_id}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">Service</h3>
<p>{selectedItem.service}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">User</h3>
<p>{selectedItem.user}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">Tenure</h3>
<p>{selectedItem.tenure}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">Amount</h3>
<p>{formatCurrency(selectedItem.amount)}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">Status</h3>
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(selectedItem.status)}`}>
{selectedItem.status.charAt(0).toUpperCase() + selectedItem.status.slice(1)}
</span>
</div>
<div>
<h3 className="font-semibold text-gray-700">Created At</h3>
<p>{formatDate(selectedItem.created_at)}</p>
</div>
<div>
<h3 className="font-semibold text-gray-700">Updated At</h3>
<p>{formatDate(selectedItem.updated_at)}</p>
</div>
</div>
<div className="mt-6 flex justify-end space-x-4">
<PDFDownloadLink
document={<InvoicePDF data={selectedItem} />}
fileName={`invoice_${selectedItem.billing_id}.pdf`}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
{({ loading }) => (loading ? 'Preparing PDF...' : 'Download PDF')}
</PDFDownloadLink>
<button
onClick={() => handleEditItem(selectedItem)}
className="px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700"
>
Edit
</button>
<button
onClick={closeModal}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
>
Close
</button>
</div>
</>
)}
</div>
</div>
</div>
)}
</section>
);
}