Library/Components/Tables/InvoiceTable

InvoiceTable

react
CompareTableTablesMatrixTable
InvoiceTable.tsx — preview
reactlive
InvoiceTable.tsx
use client

import { useState, useCallback } from 'react'

interface InvoiceLineItem {
  id: string
  description: string
  quantity: number
  unitPrice: number
}

interface InvoiceState {
  lineItems: InvoiceLineItem[]
  discountPercent: number
  taxPercent: number
}

export default function InvoiceTable() {
  const [invoice, setInvoice] = useState<InvoiceState>({
    lineItems: [
      { id: '1', description: 'Professional Consulting Services', quantity: 10, unitPrice: 150 },
      { id: '2', description: 'Software Development - Frontend', quantity: 40, unitPrice: 120 },
      { id: '3', description: 'UI/UX Design Package', quantity: 20, unitPrice: 100 },
      { id: '4', description: 'QA Testing & Deployment', quantity: 15, unitPrice: 85 },
    ],
    discountPercent: 5,
    taxPercent: 10,
  })

  const calculateSubtotal = useCallback((quantity: number, unitPrice: number) => {
    return quantity * unitPrice
  }, [])

  const subtotal = invoice.lineItems.reduce((sum, item) => {
    return sum + calculateSubtotal(item.quantity, item.unitPrice)
  }, 0)

  const discountAmount = (subtotal * invoice.discountPercent) / 100
  const taxableAmount = subtotal - discountAmount
  const taxAmount = (taxableAmount * invoice.taxPercent) / 100
  const total = taxableAmount + taxAmount

  const addLineItem = useCallback(() => {
    const newItem: InvoiceLineItem = {
      id: Date.now().toString(),
      description: 'New Line Item',
      quantity: 1,
      unitPrice: 0,
    }
    setInvoice((prev) => ({
      ...prev,
      lineItems: [...prev.lineItems, newItem],
    }))
  }, [])

  const removeLineItem = useCallback((id: string) => {
    setInvoice((prev) => ({
      ...prev,
      lineItems: prev.lineItems.filter((item) => item.id !== id),
    }))
  }, [])

  const updateLineItem = useCallback(
    (id: string, field: keyof InvoiceLineItem, value: string | number) => {
      setInvoice((prev) => ({
        ...prev,
        lineItems: prev.lineItems.map((item) =>
          item.id === id
            ? {
                ...item,
                [field]: field === 'description' ? value : Number(value),
              }
            : item
        ),
      }))
    },
    []
  )

  const updateDiscount = useCallback((value: string) => {
    setInvoice((prev) => ({
      ...prev,
      discountPercent: Number(value),
    }))
  }, [])

  const updateTax = useCallback((value: string) => {
    setInvoice((prev) => ({
      ...prev,
      taxPercent: Number(value),
    }))
  }, [])

  return (
    <div className="min-h-screen bg-[#0A0A0A] p-8">
      <div className="max-w-6xl mx-auto">
        <h1 className="text-3xl font-bold text-[#F5F5F0] mb-2">Invoice</h1>
        <p className="text-[#999999] mb-8">INV-2024-001</p>

        <div className="bg-[#0F0F0F] rounded-lg border border-[#1A1A1A] overflow-hidden">
          <table className="w-full">
            <thead>
              <tr className="border-b border-[#1A1A1A] bg-[#141414]">
                <th className="px-6 py-4 text-left text-[#F5F5F0] font-semibold text-sm">
                  Description
                </th>
                <th className="px-6 py-4 text-right text-[#F5F5F0] font-semibold text-sm w-24">
                  Qty
                </th>
                <th className="px-6 py-4 text-right text-[#F5F5F0] font-semibold text-sm w-32">
                  Unit Price
                </th>
                <th className="px-6 py-4 text-right text-[#F5F5F0] font-semibold text-sm w-32">
                  Subtotal
                </th>
                <th className="px-6 py-4 text-center text-[#F5F5F0] font-semibold text-sm w-20">
                  Action
                </th>
              </tr>
            </thead>
            <tbody>
              {invoice.lineItems.map((item) => (
                <tr key={item.id} className="border-b border-[#1A1A1A] hover:bg-[#141414] transition">
                  <td className="px-6 py-4">
                    <input
                      type="text"
                      value={item.description}
                      onChange={(e) => updateLineItem(item.id, 'description', e.target.value)}
                      className="w-full bg-[#0A0A0A] text-[#F5F5F0] border border-[#2A2A2A] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#C9A84C]"
                    />
                  </td>
                  <td className="px-6 py-4">
                    <input
                      type="number"
                      value={item.quantity}
                      onChange={(e) => updateLineItem(item.id, 'quantity', e.target.value)}
                      className="w-full bg-[#0A0A0A] text-[#F5F5F0] border border-[#2A2A2A] rounded px-3 py-2 text-sm text-right focus:outline-none focus:border-[#C9A84C]"
                    />
                  </td>
                  <td className="px-6 py-4">
                    <input
                      type="number"
                      step="0.01"
                      value={item.unitPrice}
                      onChange={(e) => updateLineItem(item.id, 'unitPrice', e.target.value)}
                      className="w-full bg-[#0A0A0A] text-[#F5F5F0] border border-[#2A2A2A] rounded px-3 py-2 text-sm text-right focus:outline-none focus:border-[#C9A84C]"
                    />
                  </td>
                  <td className="px-6 py-4 text-right text-[#F5F5F0] font-medium">
                    ${calculateSubtotal(item.quantity, item.unitPrice).toFixed(2)}
                  </td>
                  <td className="px-6 py-4 text-center">
                    <button
                      onClick={() => removeLineItem(item.id)}
                      className="text-[#C9A84C] hover:text-[#E8C547] transition font-medium text-sm"
                    >
                      Remove
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>

          <div className="px-6 py-4 border-t border-[#1A1A1A] bg-[#0F0F0F]">
            <button
              onClick={addLineItem}
              className="px-4 py-2 bg-[#1A1A1A] text-[#C9A84C] rounded border border-[#C9A84C] hover:bg-[#C9A84C] hover:text-[#0A0A0A] transition font-medium text-sm"
            >
              + Add Line Item
            </button>
          </div>

          <div className="px-6 py-6 bg-[#0A0A0A] border-t border-[#1A1A1A]">
            <div className="flex justify-end max-w-md ml-auto space-y-4">
              <div className="w-full">
                <div className="flex justify-between items-center py-3 border-b border-[#1A1A1A]">
                  <span className="text-[#999999] text-sm">Subtotal</span>
                  <span className="text-[#F5F5F0] font-medium">${subtotal.toFixed(2)}</span>
                </div>

                <div className="flex justify-between items-center py-3 border-b border-[#1A1A1A]">
                  <label className="text-[#999999] text-sm flex items-center gap-2">
                    Discount
                    <input
                      type="number"
                      value={invoice.discountPercent}
                      onChange={(e) => updateDiscount(e.target.value)}
                      className="w-16 bg-[#0A0A0A] text-[#F5F5F0] border border-[#2A2A2A] rounded px-2 py-1 text-xs focus:outline-none focus:border-[#C9A84C]"
                    />
                    %
                  </label>
                  <span className="text-[#F5F5F0] font-medium">-${discountAmount.toFixed(2)}</span>
                </div>

                <div className="flex justify-between items-center py-3 border-b border-[#1A1A1A]">
                  <label className="text-[#999999] text-sm flex items-center gap-2">
                    Tax
                    <input
                      type="number"
                      value={invoice.taxPercent}
                      onChange={(e) => updateTax(e.target.value)}
                      className="w-16 bg-[#0A0A0A] text-[#F5F5F0] border border-[#2A2A2A] rounded px-2 py-1 text-xs focus:outline-none focus:border-[#C9A84C]"
                    />
                    %
                  </label>
                  <span className="text-[#F5F5F0] font-medium">+${taxAmount.toFixed(2)}</span>
                </div>

                <div className="flex justify-between items-center py-4 mt-2">
                  <span className="text-[#C9A84C] text-lg font-bold">Total</span>
                  <span className="text-[#C9A84C] text-2xl font-bold">${total.toFixed(2)}</span>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}
Same category

Similar components

MatrixTable
MatrixTable
DataTable
DataTable
AnalyticsTable
AnalyticsTable
DataTable
DataTable