Update components/invoice-generator.tsx
This commit is contained in:
parent
6ad33c030e
commit
6618838972
|
|
@ -0,0 +1,250 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Container } from "@/components/container";
|
||||
import { Heading } from "@/components/heading";
|
||||
import { Subheading } from "@/components/subheading";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Plus, Trash2, Download, Printer } from "lucide-react";
|
||||
|
||||
interface Item {
|
||||
id: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export function InvoiceGenerator() {
|
||||
const [clientName, setClientName] = useState("");
|
||||
const [clientEmail, setClientEmail] = useState("");
|
||||
const [clientAddress, setClientAddress] = useState("");
|
||||
const [invoiceNumber, setInvoiceNumber] = useState("INV-001");
|
||||
const [invoiceDate, setInvoiceDate] = useState(new Date().toISOString().split('T')[0]);
|
||||
const [items, setItems] = useState<Item[]>([{ id: "1", description: "", quantity: 1, price: 0 }]);
|
||||
const [taxRate, setTaxRate] = useState(0);
|
||||
const [paymentTerms, setPaymentTerms] = useState("Net 30");
|
||||
|
||||
const addItem = () => {
|
||||
setItems([...items, { id: Math.random().toString(36).substr(2, 9), description: "", quantity: 1, price: 0 }]);
|
||||
};
|
||||
|
||||
const removeItem = (id: string) => {
|
||||
if (items.length > 1) {
|
||||
setItems(items.filter(item => item.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const updateItem = (id: string, field: keyof Item, value: string | number) => {
|
||||
setItems(items.map(item => item.id === id ? { ...item, [field]: value } : item));
|
||||
};
|
||||
|
||||
const subtotal = items.reduce((acc, item) => acc + (item.quantity * item.price), 0);
|
||||
const taxAmount = subtotal * (taxRate / 100);
|
||||
const total = subtotal + taxAmount;
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="py-12 bg-neutral-50 min-h-screen">
|
||||
<Container>
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
|
||||
<div>
|
||||
<Heading>Invoice Generator</Heading>
|
||||
<Subheading>Create professional invoices for your clients in seconds.</Subheading>
|
||||
</div>
|
||||
<div className="flex gap-2 no-print">
|
||||
<Button onClick={handlePrint} variant="outline" className="flex items-center gap-2">
|
||||
<Printer className="w-4 h-4" /> Print / PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Editor Side */}
|
||||
<div className="lg:col-span-2 space-y-6 no-print">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Client Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="clientName">Client Name</Label>
|
||||
<Input id="clientName" value={clientName} onChange={(e) => setClientName(e.target.value)} placeholder="Acme Corp" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="clientEmail">Client Email</Label>
|
||||
<Input id="clientEmail" type="email" value={clientEmail} onChange={(e) => setClientEmail(e.target.value)} placeholder="billing@acme.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="clientAddress">Client Address</Label>
|
||||
<Textarea id="clientAddress" value={clientAddress} onChange={(e) => setClientAddress(e.target.value)} placeholder="123 Business St, City, Country" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Line Items</CardTitle>
|
||||
<Button onClick={addItem} size="sm" variant="secondary" className="flex items-center gap-1">
|
||||
<Plus className="w-4 h-4" /> Add Item
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="grid grid-cols-12 gap-2 items-end border-b pb-4 last:border-0">
|
||||
<div className="col-span-6 space-y-1">
|
||||
<Label className="text-xs">Description</Label>
|
||||
<Input value={item.description} onChange={(e) => updateItem(item.id, 'description', e.target.value)} placeholder="Service or product name" />
|
||||
</div>
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-xs">Qty</Label>
|
||||
<Input type="number" value={item.quantity} onChange={(e) => updateItem(item.id, 'quantity', parseFloat(e.target.value) || 0)} />
|
||||
</div>
|
||||
<div className="col-span-3 space-y-1">
|
||||
<Label className="text-xs">Price</Label>
|
||||
<Input type="number" value={item.price} onChange={(e) => updateItem(item.id, 'price', parseFloat(e.target.value) || 0)} />
|
||||
</div>
|
||||
<div className="col-span-1 pb-1">
|
||||
<Button variant="ghost" size="icon" onClick={() => removeItem(item.id)} className="text-red-500 hover:text-red-700 hover:bg-red-50">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Settings & Terms</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="taxRate">Tax Rate (%)</Label>
|
||||
<Input id="taxRate" type="number" value={taxRate} onChange={(e) => setTaxRate(parseFloat(e.target.value) || 0)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="invoiceNum">Invoice #</Label>
|
||||
<Input id="invoiceNum" value={invoiceNumber} onChange={(e) => setInvoiceNumber(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="paymentTerms">Payment Terms</Label>
|
||||
<Input id="paymentTerms" value={paymentTerms} onChange={(e) => setPaymentTerms(e.target.value)} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Preview Side */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="sticky top-8">
|
||||
<Card className="print:shadow-none print:border-none overflow-hidden">
|
||||
<div className="bg-neutral-900 text-white p-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">INVOICE</h2>
|
||||
<p className="text-neutral-400 text-sm">{invoiceNumber}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-semibold">InvoiceGen</p>
|
||||
<p className="text-xs text-neutral-400">Your Business Name</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent className="p-6 space-y-6">
|
||||
<div className="flex justify-between text-sm">
|
||||
<div>
|
||||
<p className="text-neutral-500 uppercase tracking-wider font-semibold text-[10px] mb-1">Bill To</p>
|
||||
<p className="font-bold">{clientName || "Client Name"}</p>
|
||||
<p className="text-neutral-600 whitespace-pre-line">{clientAddress || "Client Address"}</p>
|
||||
<p className="text-neutral-600">{clientEmail}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-neutral-500 uppercase tracking-wider font-semibold text-[10px] mb-1">Date</p>
|
||||
<p className="font-medium">{invoiceDate}</p>
|
||||
<p className="text-neutral-500 uppercase tracking-wider font-semibold text-[10px] mt-3 mb-1">Terms</p>
|
||||
<p className="font-medium">{paymentTerms}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-b py-4">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-neutral-500 text-left">
|
||||
<th className="pb-2 font-medium">Description</th>
|
||||
<th className="pb-2 font-medium text-right">Qty</th>
|
||||
<th className="pb-2 font-medium text-right">Price</th>
|
||||
<th className="pb-2 font-medium text-right">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{items.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td className="py-2">{item.description || "New Item"}</td>
|
||||
<td className="py-2 text-right">{item.quantity}</td>
|
||||
<td className="py-2 text-right">${item.price.toFixed(2)}</td>
|
||||
<td className="py-2 text-right font-medium">${(item.quantity * item.price).toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-neutral-500">Subtotal</span>
|
||||
<span>${subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
{taxRate > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-neutral-500">Tax ({taxRate}%)</span>
|
||||
<span>${taxAmount.toFixed(2)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-lg font-bold pt-2 border-t">
|
||||
<span>Total</span>
|
||||
<span>${total.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8 text-[10px] text-neutral-400 text-center">
|
||||
<p>Thank you for your business!</p>
|
||||
<p className="mt-1">Generated via InvoiceGen</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<style jsx global>{`
|
||||
@media print {
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
.print\\:shadow-none, .print\\:shadow-none * {
|
||||
visibility: visible;
|
||||
}
|
||||
.print\\:shadow-none {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue