860 lines
44 KiB
JavaScript
860 lines
44 KiB
JavaScript
import React, { useEffect, useState, useMemo } from "react";
|
|
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
|
import Loader from "../../components/ui/loader";
|
|
import { PDFDownloadLink } from '@react-pdf/renderer';
|
|
import InvoicePDF from "../../lib/InvoicePDF";
|
|
import { Eye, Pencil, Trash2, Download, ChevronUp, ChevronDown, Search, ArrowLeft, FileText, ArrowRight } from "lucide-react";
|
|
import { Button } from "../ui/button";
|
|
import { Input } from "../ui/input";
|
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
|
|
import * as XLSX from 'xlsx';
|
|
|
|
export default function AllSellingList() {
|
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
|
const [billingData, setBillingData] = useState([]);
|
|
const [usersData, setUsersData] = useState([]);
|
|
const [dataLoading, setDataLoading] = useState(true);
|
|
const [apiError, setApiError] = useState(null);
|
|
const [selectedItem, setSelectedItem] = useState(null);
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [editMode, setEditMode] = useState(false);
|
|
const [formData, setFormData] = useState({ service: '', serviceId: 'service-1', cycle: 'monthly', amount: '', user: '', siliconId: '', name: '', status: 'pending', remarks: '' });
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
|
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
|
|
|
|
// Sorting state
|
|
const [sortConfig, setSortConfig] = useState({ key: 'created_at', direction: 'desc' });
|
|
|
|
// Filtering state
|
|
const [filters, setFilters] = useState({ status: '', cycle: '', service: '' });
|
|
|
|
// Search state
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
// Pagination state
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [itemsPerPage] = useState(10);
|
|
|
|
const INVOICE_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
|
|
|
useEffect(() => {
|
|
if (isLoggedIn && sessionData?.user_type === 'admin') {
|
|
fetchBillingData();
|
|
fetchUsersData();
|
|
}
|
|
}, [isLoggedIn, sessionData]);
|
|
|
|
const fetchBillingData = async () => {
|
|
try {
|
|
setDataLoading(true);
|
|
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 fetchUsersData = async () => {
|
|
try {
|
|
const res = await fetch(`${INVOICE_API_URL}?query=get-all-users`, {
|
|
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();
|
|
setUsersData(data.data || []);
|
|
} catch (err) {
|
|
setApiError(err.message);
|
|
}
|
|
};
|
|
|
|
// Filter customers based on search term
|
|
const filteredCustomers = useMemo(() => {
|
|
return usersData.filter(user =>
|
|
user.name.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
|
|
user.email.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
|
|
user.siliconId.toLowerCase().includes(customerSearchTerm.toLowerCase())
|
|
);
|
|
}, [usersData, customerSearchTerm]);
|
|
|
|
const handleViewItem = (item) => {
|
|
setSelectedItem(item);
|
|
setEditMode(false);
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const handleEditItem = (item) => {
|
|
setSelectedItem(item);
|
|
setFormData({
|
|
service: item.service,
|
|
serviceId: item.serviceId,
|
|
cycle: item.cycle,
|
|
amount: item.amount,
|
|
user: item.user,
|
|
siliconId: item.siliconId,
|
|
name: item.name,
|
|
status: item.status,
|
|
remarks: item.remarks,
|
|
billing_date: item.billing_date
|
|
});
|
|
setEditMode(true);
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const handleDeleteItem = async (billingId, serviceType) => {
|
|
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&billingId=${billingId}&serviceType=${serviceType}`, {
|
|
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: '', serviceId: 'service-2', cycle: 'monthly', amount: '', user: '', siliconId: '', name: '', status: 'pending', remarks: '', billing_date: new Date().toISOString().split('T')[0] });
|
|
setEditMode(true);
|
|
setCurrentStep(1);
|
|
setSelectedCustomer(null);
|
|
setCustomerSearchTerm('');
|
|
setDialogOpen(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();
|
|
setDialogOpen(false);
|
|
setCurrentStep(1);
|
|
} catch (err) {
|
|
setApiError(err.message);
|
|
}
|
|
};
|
|
|
|
const handleInputChange = (e) => {
|
|
const { name, value } = e.target;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: value
|
|
}));
|
|
};
|
|
|
|
const handleCustomerSelect = (customer) => {
|
|
setSelectedCustomer(customer);
|
|
setFormData(prev => ({
|
|
...prev,
|
|
user: customer.email,
|
|
siliconId: customer.siliconId,
|
|
name: customer.name
|
|
}));
|
|
setCurrentStep(2);
|
|
};
|
|
|
|
const handleBackToCustomerSelect = () => {
|
|
setCurrentStep(1);
|
|
setSelectedCustomer(null);
|
|
};
|
|
|
|
// Export to Excel function
|
|
const exportToExcel = () => {
|
|
const worksheet = XLSX.utils.json_to_sheet(filteredAndSortedData.map(item => ({
|
|
'Billing ID': item.billing_id,
|
|
'Service': item.service,
|
|
'Customer Name': item.name,
|
|
'Customer Email': item.user,
|
|
'Silicon ID': item.siliconId,
|
|
'Amount': item.amount,
|
|
'cycle': item.cycle,
|
|
'Status': item.status,
|
|
'Service Status': item.service_status,
|
|
'Created At': formatDate(item.created_at),
|
|
'Remarks': item.remarks
|
|
})));
|
|
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Billing Data");
|
|
XLSX.writeFile(workbook, `billing_data_${new Date().toISOString().slice(0, 10)}.xlsx`);
|
|
};
|
|
|
|
// Sorting functionality
|
|
const requestSort = (key) => {
|
|
let direction = 'asc';
|
|
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
|
direction = 'desc';
|
|
}
|
|
setSortConfig({ key, direction });
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
// Filter and sort data
|
|
const filteredAndSortedData = useMemo(() => {
|
|
let filteredData = [...billingData];
|
|
|
|
if (searchTerm) {
|
|
filteredData = filteredData.filter(item =>
|
|
Object.values(item).some(
|
|
val => val && val.toString().toLowerCase().includes(searchTerm.toLowerCase())
|
|
)
|
|
);
|
|
}
|
|
|
|
if (filters.status) {
|
|
filteredData = filteredData.filter(item => item.status === filters.status);
|
|
}
|
|
if (filters.cycle) {
|
|
filteredData = filteredData.filter(item => item.cycle === filters.cycle);
|
|
}
|
|
if (filters.service) {
|
|
filteredData = filteredData.filter(item => item.service === filters.service);
|
|
}
|
|
|
|
if (sortConfig.key) {
|
|
filteredData.sort((a, b) => {
|
|
if (a[sortConfig.key] < b[sortConfig.key]) {
|
|
return sortConfig.direction === 'asc' ? -1 : 1;
|
|
}
|
|
if (a[sortConfig.key] > b[sortConfig.key]) {
|
|
return sortConfig.direction === 'asc' ? 1 : -1;
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
return filteredData;
|
|
}, [billingData, searchTerm, filters, sortConfig]);
|
|
|
|
const currentItems = useMemo(() => {
|
|
const indexOfLastItem = currentPage * itemsPerPage;
|
|
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
|
return filteredAndSortedData.slice(indexOfFirstItem, indexOfLastItem);
|
|
}, [currentPage, itemsPerPage, filteredAndSortedData]);
|
|
|
|
const totalPages = Math.ceil(filteredAndSortedData.length / itemsPerPage);
|
|
|
|
const paginate = (pageNumber) => {
|
|
if (pageNumber > 0 && pageNumber <= totalPages) {
|
|
setCurrentPage(pageNumber);
|
|
}
|
|
};
|
|
|
|
const getPaginationRange = () => {
|
|
const range = [];
|
|
const maxVisiblePages = 5;
|
|
range.push(1);
|
|
if (currentPage > 3) {
|
|
range.push('...');
|
|
}
|
|
let start = Math.max(2, currentPage - 1);
|
|
let end = Math.min(totalPages - 1, currentPage + 1);
|
|
if (currentPage <= 3) {
|
|
end = Math.min(4, totalPages - 1);
|
|
} else if (currentPage >= totalPages - 2) {
|
|
start = Math.max(totalPages - 3, 2);
|
|
}
|
|
for (let i = start; i <= end; i++) {
|
|
if (i > 1 && i < totalPages) {
|
|
range.push(i);
|
|
}
|
|
}
|
|
if (currentPage < totalPages - 2) {
|
|
range.push('...');
|
|
}
|
|
if (totalPages > 1) {
|
|
range.push(totalPages);
|
|
}
|
|
return range;
|
|
};
|
|
|
|
const uniqueServices = [...new Set(billingData.map(item => item.service))];
|
|
const uniqueStatuses = ['completed', 'pending', 'failed'];
|
|
const uniquecycles = ['monthly', 'yearly', 'one-time'];
|
|
|
|
const resetFilters = () => {
|
|
setFilters({
|
|
status: '',
|
|
cycle: '',
|
|
service: ''
|
|
});
|
|
setSearchTerm('');
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
const handleFilterChange = (e) => {
|
|
const { name, value } = e.target;
|
|
setFilters(prev => ({
|
|
...prev,
|
|
[name]: value
|
|
}));
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
const handleSearch = (e) => {
|
|
setSearchTerm(e.target.value);
|
|
setCurrentPage(1);
|
|
};
|
|
|
|
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 || (isLoggedIn && sessionData?.user_type === 'admin' && dataLoading)) {
|
|
return <Loader />;
|
|
}
|
|
if (error || apiError) return <p>Error: {error?.message || apiError.message}</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>
|
|
<div className="flex gap-2">
|
|
<div className="relative inline-block">
|
|
<Button onClick={() => setIsDropdownOpen(!isDropdownOpen)} variant={isDropdownOpen ? 'default' : 'outline'}>Manage Users</Button>
|
|
{isDropdownOpen && (
|
|
<div className="absolute mt-1 w-44 bg-white border rounded-md shadow-lg z-10">
|
|
<a href="/manager/customer-lists" className="block px-4 py-2 hover:bg-gray-100 rounded-md text-sm text-[#6d9e37] font-medium">Manage Customer</a>
|
|
<a href="/manager/selling-list" className="block px-4 py-2 hover:bg-gray-100 rounded-md text-sm text-[#6d9e37] font-medium">Manage Billing</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Button onClick={handleCreateNew} variant="outline">Create New</Button>
|
|
<Button onClick={exportToExcel} variant="outline" className="flex items-center gap-2">
|
|
<FileText className="w-4 h-4" /> Export to Excel
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search and Filter Section */}
|
|
<div className="mb-2 text-sm text-gray-600">
|
|
Showing {currentItems.length} of {filteredAndSortedData.length} results
|
|
</div>
|
|
<div className="bg-white p-4 rounded-t-lg shadow">
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Search className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
placeholder="Search..."
|
|
value={searchTerm}
|
|
onChange={handleSearch}
|
|
className="bg-[#262626] pl-10 w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<select
|
|
name="status"
|
|
value={filters.status}
|
|
onChange={handleFilterChange}
|
|
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
|
>
|
|
<option value="">All Statuses</option>
|
|
{uniqueStatuses.map(status => (
|
|
<option key={status} value={status}>
|
|
{status.charAt(0).toUpperCase() + status.slice(1)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
<select
|
|
name="cycle"
|
|
value={filters.cycle}
|
|
onChange={handleFilterChange}
|
|
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
|
>
|
|
<option value="">All cycles</option>
|
|
{uniquecycles.map(cycle => (
|
|
<option key={cycle} value={cycle}>
|
|
{cycle.charAt(0).toUpperCase() + cycle.slice(1)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
<select
|
|
name="service"
|
|
value={filters.service}
|
|
onChange={handleFilterChange}
|
|
className="bg-[#262626] px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
|
|
>
|
|
<option value="">All Services</option>
|
|
{uniqueServices.map(service => (
|
|
<option key={service} value={service}>
|
|
{service}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{(filters.status || filters.cycle || filters.service || searchTerm) && (
|
|
<div className="mt-3">
|
|
<Button
|
|
onClick={resetFilters}
|
|
size="sm"
|
|
>
|
|
Reset Filters
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="bg-white rounded-b-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 cursor-pointer"
|
|
onClick={() => requestSort('billing_id')}
|
|
>
|
|
<div className="flex items-center">
|
|
Billing ID
|
|
{sortConfig.key === 'billing_id' && (
|
|
sortConfig.direction === 'asc' ?
|
|
<ChevronUp className="ml-1 h-4 w-4" /> :
|
|
<ChevronDown className="ml-1 h-4 w-4" />
|
|
)}
|
|
</div>
|
|
</th>
|
|
<th
|
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
onClick={() => requestSort('service')}
|
|
>
|
|
<div className="flex items-center">
|
|
Service
|
|
{sortConfig.key === 'service' && (
|
|
sortConfig.direction === 'asc' ?
|
|
<ChevronUp className="ml-1 h-4 w-4" /> :
|
|
<ChevronDown className="ml-1 h-4 w-4" />
|
|
)}
|
|
</div>
|
|
</th>
|
|
<th
|
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
onClick={() => requestSort('name')}
|
|
>
|
|
<div className="flex items-center">
|
|
Name
|
|
{sortConfig.key === 'name' && (
|
|
sortConfig.direction === 'asc' ?
|
|
<ChevronUp className="ml-1 h-4 w-4" /> :
|
|
<ChevronDown className="ml-1 h-4 w-4" />
|
|
)}
|
|
</div>
|
|
</th>
|
|
<th
|
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
onClick={() => requestSort('user')}
|
|
>
|
|
<div className="flex items-center">
|
|
User
|
|
{sortConfig.key === 'user' && (
|
|
sortConfig.direction === 'asc' ?
|
|
<ChevronUp className="ml-1 h-4 w-4" /> :
|
|
<ChevronDown className="ml-1 h-4 w-4" />
|
|
)}
|
|
</div>
|
|
</th>
|
|
<th
|
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
onClick={() => requestSort('amount')}
|
|
>
|
|
<div className="flex items-center">
|
|
Amount
|
|
{sortConfig.key === 'amount' && (
|
|
sortConfig.direction === 'asc' ?
|
|
<ChevronUp className="ml-1 h-4 w-4" /> :
|
|
<ChevronDown className="ml-1 h-4 w-4" />
|
|
)}
|
|
</div>
|
|
</th>
|
|
<th
|
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
onClick={() => requestSort('status')}>
|
|
<div className="flex items-center">
|
|
Status
|
|
{sortConfig.key === 'status' && (
|
|
sortConfig.direction === 'asc' ?
|
|
<ChevronUp className="ml-1 h-4 w-4" /> :
|
|
<ChevronDown className="ml-1 h-4 w-4" />
|
|
)}
|
|
</div>
|
|
</th>
|
|
<th
|
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
|
onClick={() => requestSort('service_status')}>
|
|
<div className="flex items-center">
|
|
Ser.. Status
|
|
{sortConfig.key === 'service_status' && (
|
|
sortConfig.direction === 'asc' ?
|
|
<ChevronUp className="ml-1 h-4 w-4" /> :
|
|
<ChevronDown className="ml-1 h-4 w-4" />
|
|
)}
|
|
</div>
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer" onClick={() => requestSort('created_at')}>
|
|
<div className="flex items-center">
|
|
Created At
|
|
{sortConfig.key === 'created_at' && (
|
|
sortConfig.direction === 'asc' ?
|
|
<ChevronUp className="ml-1 h-4 w-4" /> :
|
|
<ChevronDown className="ml-1 h-4 w-4" />
|
|
)}
|
|
</div>
|
|
</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">
|
|
{currentItems.length > 0 ? (
|
|
currentItems.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.name ?? item.name}</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-center">
|
|
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${item.service_status == 1 ? 'bg-green-100 text-green-800' : item.service_status == 0 ? 'bg-red-100 text-red-800' : ''}`}>
|
|
{item.service_status == 1 ? 'Active' : item.service_status == 0 ? 'Deactivate' : ''}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{formatDate(item.created_at)}
|
|
</td>
|
|
<td className="inline-flex px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
<button
|
|
title="View Items"
|
|
onClick={() => handleViewItem(item)}
|
|
className="text-indigo-600 hover:text-indigo-900 mr-2"
|
|
>
|
|
<Eye className="w-5 h-5" />
|
|
</button>
|
|
<button
|
|
title="Edit Items"
|
|
onClick={() => handleEditItem(item)}
|
|
className="text-yellow-600 hover:text-yellow-900 mr-2"
|
|
>
|
|
<Pencil className="w-5 h-5" />
|
|
</button>
|
|
<button
|
|
title="Delete Items"
|
|
onClick={() => handleDeleteItem(item.billing_id, item.service_type)}
|
|
className="text-red-600 hover:text-red-900 mr-2"
|
|
>
|
|
<Trash2 className="w-5 h-5" />
|
|
</button>
|
|
<PDFDownloadLink
|
|
title="Download PDF"
|
|
document={<InvoicePDF data={item} />}
|
|
fileName={`invoice_${item.billing_id}.pdf`}
|
|
className="text-[#6d9e37] hover:text-green-600"
|
|
>
|
|
{({ loading }) => (loading ? '...' : <Download className="w-5 h-5" />)}
|
|
</PDFDownloadLink>
|
|
</td>
|
|
</tr>
|
|
))
|
|
) : (
|
|
<tr>
|
|
<td colSpan="7" className="px-6 py-4 text-center text-sm text-gray-500">
|
|
No records found
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Pagination */}
|
|
{filteredAndSortedData.length > itemsPerPage && (
|
|
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
<div className="text-sm text-gray-600">
|
|
Page {currentPage} of {totalPages}
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{
|
|
currentPage > 1 ? (
|
|
<>
|
|
<Button onClick={() => paginate(1)} disabled={currentPage === 1} variant="outline" size="sm">First</Button>
|
|
<Button onClick={() => paginate(currentPage - 1)} disabled={currentPage === 1} variant="outline" size="sm">Previous</Button >
|
|
</>
|
|
) : ''
|
|
}
|
|
{
|
|
getPaginationRange().map((item, index) => {
|
|
if (item === '...') {
|
|
return (
|
|
<span key={`ellipsis-${index}`} className="px-2 py-1">...</span>
|
|
);
|
|
}
|
|
return (
|
|
<Button key={`page-${item}`} onClick={() => paginate(item)} variant={currentPage === item ? "default" : "outline"} size="sm">{item}</Button>
|
|
);
|
|
})
|
|
}
|
|
|
|
<Button onClick={() => paginate(currentPage + 1)} disabled={currentPage === totalPages} variant="outline" size="sm">Next</Button>
|
|
<Button onClick={() => paginate(totalPages)} disabled={currentPage === totalPages} variant="outline" size="sm">Last</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Dialog for View/Edit/Create */}
|
|
<Dialog open={dialogOpen} onOpenChange={(open) => {
|
|
if (!open) {
|
|
setCurrentStep(1);
|
|
setSelectedCustomer(null);
|
|
}
|
|
setDialogOpen(open);
|
|
}}>
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{editMode ? (selectedItem ? 'Edit Billing' : 'Create New Billing') : 'Billing Details'}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{editMode && !selectedItem && currentStep === 1 ? (
|
|
<div>
|
|
<div className="relative mb-4">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Search className="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
placeholder="Search customers..."
|
|
value={customerSearchTerm}
|
|
onChange={(e) => setCustomerSearchTerm(e.target.value)}
|
|
className="pl-10 w-full px-3 py-2 border border-gray-300 rounded-md outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<h3 className="font-semibold mb-4 text-neutral-400">Select Customer</h3>
|
|
{filteredCustomers.length > 0 ? (
|
|
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
|
{filteredCustomers.map(user => (
|
|
<div
|
|
key={user.id}
|
|
onClick={() => handleCustomerSelect(user)}
|
|
className="group p-3 mx-2 border rounded-md cursor-pointer hover:bg-gray-100 hover:border-[#6d9e37] transition-border duration-700"
|
|
>
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<p className="font-medium text-neutral-950">{user.name}</p>
|
|
<p className="text-sm text-neutral-400">{user.email}</p>
|
|
</div>
|
|
<div className="text-sm inline-flex gap-2">
|
|
<span className="bg-gray-100 px-2 py-1 rounded text-neutral-950">SiliconId: {user.siliconId}</span>
|
|
<ArrowRight className="text-[#6d9e37] opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-center text-gray-500 py-4">No customers found</p>
|
|
)}
|
|
</div>
|
|
) : editMode ? (
|
|
<form onSubmit={handleSubmit}>
|
|
{!selectedItem && selectedCustomer && (
|
|
<div className="mb-6 p-4 bg-gray-50 rounded-md">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h3 className="font-semibold text-neutral-950">Customer Information</h3>
|
|
<p className="text-sm text-neutral-400"><span className="font-medium">Name:</span> {selectedCustomer.name}</p>
|
|
<p className="text-sm text-neutral-400"><span className="font-medium">Email:</span> {selectedCustomer.email}</p>
|
|
<p className="text-sm text-neutral-400"><span className="font-medium">Silicon ID:</span> {selectedCustomer.siliconId}</p>
|
|
</div>
|
|
{!selectedItem && (
|
|
<Button type="button" onClick={handleBackToCustomerSelect} variant="ghost" size="sm" className="text-gray-500">
|
|
<ArrowLeft className="w-4 h-4 mr-1" /> Change Customer
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<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-[#6d9e37] rounded-md outline-none text-neutral-950" required />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">cycle</label>
|
|
<select name="cycle" value={formData.cycle} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950 bg-white" >
|
|
<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-[#6d9e37] rounded-md outline-none text-neutral-950 bg-white" step="0.01" min="0" 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-[#6d9e37] rounded-md outline-none text-neutral-950 bg-white" >
|
|
<option value="pending">Pending</option>
|
|
<option value="completed">Completed</option>
|
|
<option value="failed">Failed</option>
|
|
</select>
|
|
</div>
|
|
<div className="">
|
|
<label htmlFor="billing_date" className="block text-sm font-medium text-gray-700 mb-1">Billing Date</label>
|
|
<input type="date" name="billing_date" value={formData.billing_date} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950" />
|
|
</div>
|
|
<div className="">
|
|
<label htmlFor="remarks" className="block text-sm font-medium text-gray-700 mb-1">Remarks</label>
|
|
<textarea name="remarks" value={formData.remarks} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button type="submit" className="" >{selectedItem ? 'Update' : 'Create'}</Button>
|
|
<Button type="button" onClick={() => { setDialogOpen(false); setCurrentStep(1);}} variant="outline">Cancel</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
) : (
|
|
<>
|
|
<div className="grid grid-cols-1 md: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">Silicon ID</h3>
|
|
<p>{selectedItem?.siliconId}</p>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-gray-700">cycle</h3>
|
|
<p>{selectedItem?.cycle}</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 className="md:col-span-2">
|
|
<h3 className="font-semibold text-gray-700">Remarks</h3>
|
|
<p>{selectedItem?.remarks}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<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={() => setDialogOpen(false)}
|
|
variant="outline"
|
|
>
|
|
Close
|
|
</Button>
|
|
</DialogFooter>
|
|
</>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</section>
|
|
);
|
|
} |