last work on invoice generation
This commit is contained in:
273
src/components/PrintInvoice.tsx
Normal file
273
src/components/PrintInvoice.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Invoice {
|
||||
invoice_id: number;
|
||||
invoice_number: string;
|
||||
customer_id: string;
|
||||
invoice_date: string;
|
||||
due_date: string;
|
||||
subtotal: number;
|
||||
tax_amount: number;
|
||||
total_amount: number;
|
||||
status: string;
|
||||
payment_terms: string | null;
|
||||
notes: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export default function PrintInvoice() {
|
||||
const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const getInvoiceListData = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/invoice/invoice-info/', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || 'Session fetch failed');
|
||||
}
|
||||
|
||||
setInvoiceList(data.data);
|
||||
} catch (error: any) {
|
||||
console.error('Fetch error:', error);
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
getInvoiceListData();
|
||||
}, []);
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
const viewInvoiceDetails = (invoice: Invoice) => {
|
||||
setSelectedInvoice(invoice);
|
||||
}
|
||||
|
||||
const closeInvoiceDetails = () => {
|
||||
setSelectedInvoice(null);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="p-4 text-red-600">Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-4">Loading invoice data...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-6xl mx-auto">
|
||||
{/* Invoice List */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Invoices</h1>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full bg-white border border-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">Invoice #</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Due Date</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">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{invoiceList.map((invoice) => (
|
||||
<tr key={invoice.invoice_id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">{invoice.invoice_number}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatDate(invoice.invoice_date)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatDate(invoice.due_date)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">{formatCurrency(invoice.total_amount)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||
${invoice.status === 'paid' ? 'bg-green-100 text-green-800' :
|
||||
invoice.status === 'draft' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-red-100 text-red-800'}`}>
|
||||
{invoice.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<button
|
||||
onClick={() => viewInvoiceDetails(invoice)}
|
||||
className="text-blue-600 hover:text-blue-900 mr-2"
|
||||
>
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Print
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice Detail Modal */}
|
||||
{selectedInvoice && (
|
||||
<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-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||
{/* Printable Invoice */}
|
||||
<div id="printable-invoice" className="p-8">
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-800">INVOICE</h1>
|
||||
<p className="text-gray-600">{selectedInvoice.invoice_number}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-gray-600">Status: <span className={`font-semibold
|
||||
${selectedInvoice.status === 'paid' ? 'text-green-600' :
|
||||
selectedInvoice.status === 'draft' ? 'text-yellow-600' :
|
||||
'text-red-600'}`}>
|
||||
{selectedInvoice.status.toUpperCase()}
|
||||
</span></p>
|
||||
<p className="text-gray-600">Date: {formatDate(selectedInvoice.invoice_date)}</p>
|
||||
<p className="text-gray-600">Due: {formatDate(selectedInvoice.due_date)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">From</h2>
|
||||
<p className="text-gray-800">Your Company Name</p>
|
||||
<p className="text-gray-600">123 Business Street</p>
|
||||
<p className="text-gray-600">City, State 12345</p>
|
||||
<p className="text-gray-600">contact@yourcompany.com</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">Bill To</h2>
|
||||
<p className="text-gray-800">Customer ID: {selectedInvoice.customer_id}</p>
|
||||
<p className="text-gray-600">[Customer Name]</p>
|
||||
<p className="text-gray-600">[Customer Address]</p>
|
||||
<p className="text-gray-600">[Customer Email]</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-b border-gray-200 py-4 mb-6">
|
||||
<div className="grid grid-cols-12 gap-4 font-semibold text-gray-700">
|
||||
<div className="col-span-6">Description</div>
|
||||
<div className="col-span-2 text-right">Subtotal</div>
|
||||
<div className="col-span-2 text-right">Tax</div>
|
||||
<div className="col-span-2 text-right">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="grid grid-cols-12 gap-4 mb-2">
|
||||
<div className="col-span-6 text-gray-800">[Service/Product Description]</div>
|
||||
<div className="col-span-2 text-right">{formatCurrency(selectedInvoice.subtotal)}</div>
|
||||
<div className="col-span-2 text-right">{formatCurrency(selectedInvoice.tax_amount)}</div>
|
||||
<div className="col-span-2 text-right font-semibold">{formatCurrency(selectedInvoice.total_amount)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mb-8">
|
||||
<div className="w-64">
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span className="font-semibold">Subtotal:</span>
|
||||
<span>{formatCurrency(selectedInvoice.subtotal)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span className="font-semibold">Tax:</span>
|
||||
<span>{formatCurrency(selectedInvoice.tax_amount)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 font-bold text-lg">
|
||||
<span>Total:</span>
|
||||
<span>{formatCurrency(selectedInvoice.total_amount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedInvoice.notes && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-2">Notes</h2>
|
||||
<p className="text-gray-600">{selectedInvoice.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-8 border-t border-gray-200">
|
||||
<p className="text-gray-600 text-center">Thank you for your business!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-200 flex justify-end">
|
||||
<button
|
||||
onClick={closeInvoiceDetails}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 mr-2"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Print Invoice
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Print Styles */}
|
||||
<style>
|
||||
{`
|
||||
@media print {
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
#printable-invoice, #printable-invoice * {
|
||||
visibility: visible;
|
||||
}
|
||||
#printable-invoice {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding: 20mm;
|
||||
}
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -88,10 +88,6 @@ export default function ProfilePage() {
|
||||
}
|
||||
return (
|
||||
<div className="space-y-6 container mx-auto">
|
||||
{/* <div>
|
||||
<h3 className="text-lg font-medium">Profile</h3>
|
||||
<p className="text-sm text-muted-foreground">Update your profile settings.</p>
|
||||
</div> */}
|
||||
<Separator />
|
||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
|
||||
<div className="flex-1 lg:max-w-2xl">
|
||||
@@ -128,15 +124,6 @@ export default function ProfilePage() {
|
||||
<Input id="email" type="email" defaultValue={userData.session_data?.user_email || ''} />
|
||||
</div>
|
||||
|
||||
{/* <div className="space-y-2">
|
||||
<Label htmlFor="bio">Bio</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
defaultValue="I'm a software developer based in New York."
|
||||
className="resize-none"
|
||||
rows={5}
|
||||
/>
|
||||
</div> */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="mt-6">
|
||||
@@ -145,7 +132,6 @@ export default function ProfilePage() {
|
||||
<CardDescription>
|
||||
View your billing history.
|
||||
</CardDescription>
|
||||
{/* <CardContent> */}
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -157,7 +143,7 @@ export default function ProfilePage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* {
|
||||
{
|
||||
invoiceList.map((invoice) => (
|
||||
<tr key={invoice.id}>
|
||||
<td>{invoice.invoice_id}</td>
|
||||
@@ -167,65 +153,11 @@ export default function ProfilePage() {
|
||||
<td className="text-center"><a href="">Print</a></td>
|
||||
</tr>
|
||||
))
|
||||
} */}
|
||||
<tr>
|
||||
<td>dcdicdicib</td>
|
||||
<td>2023-10-01</td>
|
||||
<td>Subscription Fee</td>
|
||||
<td className="text-right">$10.00</td>
|
||||
<td className="text-center"><a href="">Print</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>dcibcdici</td>
|
||||
<td>2023-09-01</td>
|
||||
<td>Subscription Fee</td>
|
||||
<td className="text-right">$10.00</td>
|
||||
<td className="text-center"><a href="" >Print</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* </CardContent> */}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
{/* <Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Preferences</CardTitle>
|
||||
<CardDescription>
|
||||
Configure your application preferences.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language">Language</Label>
|
||||
<Select defaultValue="english">
|
||||
<SelectTrigger id="language">
|
||||
<SelectValue placeholder="Select language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="english">English</SelectItem>
|
||||
<SelectItem value="french">French</SelectItem>
|
||||
<SelectItem value="german">German</SelectItem>
|
||||
<SelectItem value="spanish">Spanish</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="timezone">Timezone</Label>
|
||||
<Select defaultValue="est">
|
||||
<SelectTrigger id="timezone">
|
||||
<SelectValue placeholder="Select timezone" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="est">Eastern Standard Time (EST)</SelectItem>
|
||||
<SelectItem value="cst">Central Standard Time (CST)</SelectItem>
|
||||
<SelectItem value="mst">Mountain Standard Time (MST)</SelectItem>
|
||||
<SelectItem value="pst">Pacific Standard Time (PST)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card> */}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 lg:max-w-md">
|
||||
|
||||
16
src/components/ui/container.tsx
Normal file
16
src/components/ui/container.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ContainerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Container: React.FC<ContainerProps> = ({ children, className = '' }) => {
|
||||
return (
|
||||
<div className={`max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Container;
|
||||
38
src/components/ui/table.tsx
Normal file
38
src/components/ui/table.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
interface TableProps {
|
||||
headers: string[];
|
||||
data: any[];
|
||||
className?: string;
|
||||
children?: React.ReactNode; // Add children here
|
||||
}
|
||||
|
||||
const Table: React.FC<TableProps> = ({ headers, data, className = '', children }) => {
|
||||
return (
|
||||
<div className={`overflow-x-auto ${className}`}>
|
||||
<table className="min-w-full bg-white border border-gray-300">
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, index) => (
|
||||
<th key={index} className="px-6 py-3 text-left text-sm font-medium text-gray-600">
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, index) => (
|
||||
<tr key={index} className="border-t">
|
||||
{Object.values(row).map((value, idx) => (
|
||||
<td key={idx} className="px-6 py-4 text-sm text-gray-800">
|
||||
{value as React.ReactNode}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
{children} {/* This will allow you to pass additional content */}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
42
src/components/ui/typography.tsx
Normal file
42
src/components/ui/typography.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { ElementType } from 'react';
|
||||
|
||||
interface TypographyProps {
|
||||
variant: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'body1' | 'body2' | 'caption';
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
color?: string; // Add color prop
|
||||
}
|
||||
|
||||
const Typography: React.FC<TypographyProps> = ({ variant, children, className = '', color }) => {
|
||||
const variantStyles: Record<string, React.CSSProperties> = {
|
||||
h1: { fontSize: '2.5rem', fontWeight: 'bold' },
|
||||
h2: { fontSize: '2rem', fontWeight: 'bold' },
|
||||
h3: { fontSize: '1.75rem', fontWeight: 'bold' },
|
||||
h4: { fontSize: '1.5rem', fontWeight: 'bold' },
|
||||
h5: { fontSize: '1.25rem', fontWeight: 'bold' },
|
||||
h6: { fontSize: '1rem', fontWeight: 'bold' },
|
||||
body1: { fontSize: '1rem', fontWeight: 'normal' },
|
||||
body2: { fontSize: '0.875rem', fontWeight: 'normal' },
|
||||
caption: { fontSize: '0.75rem', fontWeight: 'normal' },
|
||||
};
|
||||
|
||||
const combinedStyle = { ...variantStyles[variant], ...(color && { color }), ...(className ? { className } : {}) };
|
||||
|
||||
const variantToTag: Record<string, ElementType> = {
|
||||
h1: 'h1',
|
||||
h2: 'h2',
|
||||
h3: 'h3',
|
||||
h4: 'h4',
|
||||
h5: 'h5',
|
||||
h6: 'h6',
|
||||
body1: 'p',
|
||||
body2: 'p',
|
||||
caption: 'span',
|
||||
};
|
||||
|
||||
const Tag: ElementType = variantToTag[variant];
|
||||
|
||||
return <Tag style={combinedStyle}>{children}</Tag>;
|
||||
};
|
||||
|
||||
export default Typography;
|
||||
Reference in New Issue
Block a user