app-peaceful-lobster-dive/components/invoice-generator.tsx

251 lines
12 KiB
TypeScript

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