s22
This commit is contained in:
422
src/components/Manager/AllSellingList.jsx
Normal file
422
src/components/Manager/AllSellingList.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user