last work on invoice generation

This commit is contained in:
Suvodip
2025-03-31 13:55:35 +05:30
parent c927fd6087
commit 0438c30c97
9 changed files with 545 additions and 72 deletions

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

View File

@@ -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">

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

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

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