# Sales UI Reuse Pattern Guide

**Reusing structural patterns and shared components from Purchases invoice UI for Sales domain**

---

## Overview

The Purchases module has a sophisticated invoice form with:
- Form layout framework (react-hook-form + Zod validation)
- Reusable line-item table with drag/drop, resizing, calculations
- Payment method handling
- Currency/exchange rate management
- Document attachment UI
- Validation patterns

This guide shows how to reuse these patterns in the Sales module while adapting to Sales domain requirements (customer vs. supplier, revenue vs. cost, AR vs. AP).

---

## Current State

### Purchases UI Components

**Path**: `components/admin/purchase-order-form.tsx`

**Key Features**:
- Form: Supplier selection, invoice details, line items, payments
- Line items table: 
  - Columns: Material, quantity, unit price, discount %, discount amount, bonus, warehouse, shelf, tax rate
  - Resizable columns with localStorage persistence
  - Keyboard navigation (Tab, F2 to submit, Enter to move focus)
  - In-line editing with validation
- Calculations: Tax (inclusive, exclusive, line-level), discounts (% and amount)
- Payments: Cash, checks (issued/incoming), bank transfer, credit
- Currency: Exchange rates, multi-currency support
- Documents: File upload/attachment tracking

**Validation**: Zod schema with 60+ fields

**State Management**: react-hook-form + custom grid state

**UI Components Used**:
- Card, Form, FormField, FormLabel, FormMessage (shadcn/ui)
- Select, Input, Textarea, Button, Separator (shadcn/ui)
- Dialog, Tabs, Table, Popover, DropdownMenu (shadcn/ui)
- Icons: Lucide React

### Sales UI Components

**Path**: `components/admin/sales-invoice-form.tsx`

**Current Features** (simplified):
- Form: Customer selection, notes, items
- Line items table: Material, quantity, unit price, discount, warehouse, shelf
- Basic calculations: Line totals, discounts

**Issues**:
- Doesn't reuse Purchases patterns fully
- No currency/exchange rate handling
- No payment method details
- Simpler validation
- Missing dynamic column resizing
- No keyboard hotkeys

---

## Reuse Strategy

### 1. Shared Patterns to Extract

#### Pattern A: Dynamic Line Item Table
**Current Location**: purchase-order-form.tsx (lines ~416-900)

**What to Extract**:
- Column visibility management (localStorage)
- Column resizing logic (mouse events, widths)
- Keyboard navigation (Tab, Enter, arrow keys)
- Row deletion/insertion logic
- Dynamic total calculation
- Drag/drop row reordering

**How to Reuse**:
```typescript
// Create: components/invoice-shared/line-items-table.tsx
export interface LineItem {
  id: string;
  materialId: string;
  quantity: number;
  unitPrice: number;
  [key: string]: any; // Domain-specific fields
}

export interface LineItemsTableProps<T extends LineItem> {
  items: T[];
  columns: ColumnDefinition<T>[];
  onItemsChange: (items: T[]) => void;
  onDeleteRow: (index: number) => void;
  isDirty?: boolean;
}

export function LineItemsTable<T extends LineItem>({
  items,
  columns,
  onItemsChange,
  onDeleteRow,
}: LineItemsTableProps<T>) {
  // Generic table implementation
  // Column visibility, resizing, keyboard nav
  // Pass components for rendering cells
}
```

**Customization Points**:
- Column definitions (Sales adds: warehouse, shelf; Purchases adds: bonus, tax rate)
- Cell renderers (item picker, warehouse picker, custom calculations)
- Row calculations (totalPrice, discountAmount, etc.)
- Keyboard handlers (F2 to save, custom hotkeys)

---

#### Pattern B: Form Layout & Validation
**Current Location**: purchase-order-form.tsx (lines ~100-260)

**What to Extract**:
- Zod schema pattern (nested arrays, conditional fields)
- react-hook-form integration (useForm, useFieldArray, useWatch)
- Field array management (append, remove synchronization)
- Validation error display

**How to Reuse**:
```typescript
// Use as-is in Sales form
// Just adapt field names and requirements:

// Purchases Schema
const purchaseOrderSchema = z.object({
  supplierId: z.string().min(1),
  items: z.array(z.object({
    materialId: z.string(),
    quantity: z.coerce.number(),
    // ...
  })),
});

// Sales Schema (adapted)
const salesInvoiceSchema = z.object({
  customerId: z.string().min(1),  // ← changed
  items: z.array(z.object({
    materialId: z.string(),
    quantity: z.coerce.number(),
    // ... same structure
  })),
});
```

**Shared Pattern**:
```typescript
// Both use the same pattern:
const form = useForm<z.infer<typeof schema>>({
  resolver: zodResolver(schema),
  defaultValues: { /* ... */ },
});

const { fields, append, remove } = useFieldArray({
  control: form.control,
  name: 'items',
});

// Sync grid state with form
useEffect(() => {
  form.setValue('items', gridItems, { shouldDirty: true });
}, [gridItems, form]);
```

---

#### Pattern C: Party Selection (Supplier ↔ Customer)
**Current Location**: purchase-order-form.tsx (lines ~700-750, party selection logic)

**What to Extract**:
- Unified party search (combobox with filter)
- Party toggle (select multiple types)
- Party-specific info lookup

**How to Reuse**:
```typescript
// Create: components/invoice-shared/party-selector.tsx
export type PartyType = 'customer' | 'supplier' | 'both';

export interface PartySelectorProps {
  customers: Customer[];
  suppliers: Supplier[];
  selectedPartyId?: string;
  partyType: PartyType;  // ← Determines which to show
  onPartySelect: (party: SelectedParty) => void;
  label?: string;
  required?: boolean;
}

// In Purchases form:
// <PartySelector partyType="supplier" ... />

// In Sales form:
// <PartySelector partyType="customer" ... />
```

---

#### Pattern D: Calculations & Totals
**Current Location**: purchase-order-form.tsx (lines ~1200+)

**What to Extract**:
- Discount calculation (% and amount, line-level and invoice-level)
- Tax calculation (inclusive, exclusive, line-level, exempt)
- Exchange rate conversion
- Total computation

**How to Reuse**:
```typescript
// Create: lib/modules/shared/invoice-calculations.ts

export type TaxMethod = 'tax-inclusive' | 'invoice-level' | 'line-level' | 'tax-exempt';

export interface CalculationInput {
  items: Array<{
    quantity: number;
    unitPrice: number;
    lineDiscountPercent?: number;
    lineDiscountAmount?: number;
    taxRate?: number;
  }>;
  invoiceDiscountPercent?: number;
  invoiceDiscountAmount?: number;
  invoiceTaxRate?: number;
  taxMethod: TaxMethod;
  exchangeRate?: number;
}

export function calculateTotals(input: CalculationInput) {
  // Shared calculation logic
  // Returns: lineSubtotals, invoiceDiscount, tax, grandTotal
}

// Both Purchases and Sales use identical logic
const totals = calculateTotals({
  items: gridItems,
  invoiceDiscountPercent,
  taxMethod,
  // ...
});
```

**Domain-Specific Wrapper** (if needed):
```typescript
// In Purchases module
export function calculatePurchaseOrderTotals(...): PurchaseOrderTotals
  // Calls shared calculateTotals, returns purchase-specific shape

// In Sales module
export function calculateSalesInvoiceTotals(...): SalesInvoiceTotals
  // Calls shared calculateTotals, returns sales-specific shape
```

---

### 2. Shared UI Component Library

#### Create: `components/invoice-shared/` Directory Structure

```
components/invoice-shared/
├── index.ts                          # Export all shared invoice components
├── line-items-table.tsx              # Generic table for line items
├── party-selector.tsx                # Unified party (supplier/customer) picker
├── currency-section.tsx              # Currency + exchange rate section
├── discount-section.tsx              # Invoice-level discount controls
├── tax-section.tsx                   # Tax method + rate controls
├── payment-methods-section.tsx       # Payment method tabs (cash/check/bank/credit)
├── documents-section.tsx             # File upload/attachment handling
├── form-header.tsx                   # Invoice header (date, reference numbers)
├── form-footer.tsx                   # Save/Cancel buttons, status badge
├── calculation-display.tsx           # Totals display (subtotal, tax, discount, grand total)
└── utils/
    └── line-item-calculations.ts     # Shared calculation functions
```

---

#### Invoice Shared Components Detail

**1. LineItemsTable Component**

```typescript
// components/invoice-shared/line-items-table.tsx

import { useState, useEffect, useRef, useMemo } from 'react';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Trash2, GripVertical } from 'lucide-react';

export type ColumnType = 'text' | 'number' | 'select' | 'custom';

export interface ColumnDefinition<T> {
  key: keyof T;
  label: string;
  type: ColumnType;
  minWidth?: number;
  resizable?: boolean;
  sortable?: boolean;
  render?: (value: any, item: T, index: number) => React.ReactNode;
  onChange?: (value: any, index: number, item: T) => void;
  options?: Array<{ value: string; label: string }>; // For select type
}

export interface LineItemsTableProps<T extends { id?: string }> {
  items: T[];
  columns: ColumnDefinition<T>[];
  onItemsChange: (items: T[]) => void;
  onDeleteRow: (index: number) => void;
  onAddRow?: () => void;
  isDirty?: boolean;
  storageKey?: string; // For column preferences localStorage
  showLineNumbers?: boolean;
  rowClassName?: string;
  allowReorder?: boolean;
  onReorder?: (fromIndex: number, toIndex: number) => void;
}

export function LineItemsTable<T extends { id?: string }>({
  items,
  columns,
  onItemsChange,
  onDeleteRow,
  onAddRow,
  storageKey,
  showLineNumbers = true,
  allowReorder = false,
  onReorder,
}: LineItemsTableProps<T>) {
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
  const [columnVisibility, setColumnVisibility] = useState<Record<string, boolean>>({});
  const [resizingColumn, setResizingColumn] = useState<string | null>(null);
  const resizeStartX = useRef(0);
  const resizeStartWidth = useRef(0);

  // Load preferences from localStorage
  useEffect(() => {
    if (!storageKey) return;
    try {
      const saved = localStorage.getItem(storageKey);
      if (saved) {
        const { columnWidths, columnVisibility } = JSON.parse(saved);
        setColumnWidths(columnWidths);
        setColumnVisibility(columnVisibility);
      }
    } catch {
      // Ignore localStorage errors
    }
  }, [storageKey]);

  // Save preferences to localStorage
  useEffect(() => {
    if (!storageKey || Object.keys(columnWidths).length === 0) return;
    localStorage.setItem(
      storageKey,
      JSON.stringify({ columnWidths, columnVisibility })
    );
  }, [storageKey, columnWidths, columnVisibility]);

  const handleMouseDown = (column: string, e: React.MouseEvent) => {
    e.preventDefault();
    resizeStartX.current = e.clientX;
    resizeStartWidth.current = columnWidths[column] || 120;
    setResizingColumn(column);
  };

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      if (!resizingColumn) return;
      const delta = e.clientX - resizeStartX.current;
      setColumnWidths((prev) => ({
        ...prev,
        [resizingColumn]: Math.max(60, resizeStartWidth.current + delta),
      }));
    };

    const handleMouseUp = () => {
      setResizingColumn(null);
    };

    window.addEventListener('mousemove', handleMouseMove);
    window.addEventListener('mouseup', handleMouseUp);

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
      window.removeEventListener('mouseup', handleMouseUp);
    };
  }, [resizingColumn]);

  const visibleColumns = useMemo(
    () => columns.filter((col) => columnVisibility[String(col.key)] !== false),
    [columns, columnVisibility]
  );

  return (
    <div className="border rounded-lg overflow-hidden">
      <Table>
        <TableHeader>
          <TableRow>
            {showLineNumbers && <TableHead className="w-12">#</TableHead>}
            {allowReorder && <TableHead className="w-10"></TableHead>}
            {visibleColumns.map((col) => (
              <TableHead
                key={String(col.key)}
                style={{ width: columnWidths[String(col.key)] || 120 }}
                className="relative group"
              >
                <div className="flex items-center justify-between">
                  <span>{col.label}</span>
                  {col.resizable !== false && (
                    <div
                      className="cursor-col-resize w-1 bg-border hover:bg-primary/50 opacity-0 group-hover:opacity-100 transition-opacity"
                      onMouseDown={(e) => handleMouseDown(String(col.key), e)}
                    />
                  )}
                </div>
              </TableHead>
            ))}
            <TableHead className="w-10">Delete</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {items.map((item, index) => (
            <TableRow key={item.id || index}>
              {showLineNumbers && (
                <TableCell className="text-center text-xs text-muted-foreground w-12">
                  {index + 1}
                </TableCell>
              )}
              {allowReorder && (
                <TableCell className="w-10 cursor-grab">
                  <GripVertical className="h-4 w-4 text-muted-foreground" />
                </TableCell>
              )}
              {visibleColumns.map((col) => (
                <TableCell
                  key={String(col.key)}
                  style={{ width: columnWidths[String(col.key)] || 120 }}
                >
                  {col.render ? (
                    col.render((item as any)[col.key], item, index)
                  ) : col.type === 'number' ? (
                    <span className="font-mono text-right">
                      {(item as any)[col.key]}
                    </span>
                  ) : (
                    <span>{(item as any)[col.key]}</span>
                  )}
                </TableCell>
              ))}
              <TableCell className="w-10">
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={() => onDeleteRow(index)}
                  className="h-8 w-8 p-0"
                >
                  <Trash2 className="h-4 w-4" />
                </Button>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      {onAddRow && (
        <div className="flex justify-end p-2 border-t bg-muted">
          <Button size="sm" onClick={onAddRow}>
            Add Line Item
          </Button>
        </div>
      )}
    </div>
  );
}
```

**2. PartySelector Component**

```typescript
// components/invoice-shared/party-selector.tsx

import { useMemo, useState } from 'react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Check, ChevronsUpDown } from 'lucide-react';
import { cn } from '@/lib/utils';

export type PartyType = 'customer' | 'supplier';

export interface SelectedParty {
  id: string;
  name: string;
  type: PartyType;
}

export interface PartySelectorProps {
  customers?: Array<{ id: string; name: string }>;
  suppliers?: Array<{ id: string; name: string }>;
  partyType: PartyType;
  selectedPartyId?: string;
  onPartySelect: (party: SelectedParty) => void;
  label?: string;
  placeholder?: string;
  required?: boolean;
  searchable?: boolean;
}

export function PartySelector({
  customers = [],
  suppliers = [],
  partyType,
  selectedPartyId,
  onPartySelect,
  label,
  placeholder,
  required = false,
  searchable = true,
}: PartySelectorProps) {
  const [open, setOpen] = useState(false);
  const [searchTerm, setSearchTerm] = useState('');

  const parties = useMemo(() => {
    if (partyType === 'customer') {
      return customers.map((c) => ({ id: c.id, name: c.name, type: partyType as PartyType }));
    }
    return suppliers.map((s) => ({ id: s.id, name: s.name, type: partyType as PartyType }));
  }, [customers, suppliers, partyType]);

  const filtered = useMemo(() => {
    if (!searchTerm) return parties;
    return parties.filter((p) => p.name.toLowerCase().includes(searchTerm.toLowerCase()));
  }, [parties, searchTerm]);

  const selected = parties.find((p) => p.id === selectedPartyId);

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className="w-full justify-between"
        >
          {selected?.name || placeholder || `Select ${partyType}...`}
          <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-full p-0" align="start">
        {searchable && (
          <div className="p-2 border-b">
            <Input
              placeholder={`Search ${partyType}s...`}
              value={searchTerm}
              onChange={(e) => setSearchTerm(e.target.value)}
              autoFocus
            />
          </div>
        )}
        <ScrollArea className="h-72">
          {filtered.length === 0 ? (
            <div className="p-2 text-sm text-muted-foreground">No {partyType} found.</div>
          ) : (
            filtered.map((party) => (
              <div
                key={party.id}
                className={cn(
                  'flex items-center gap-2 px-2 py-1.5 cursor-pointer hover:bg-accent rounded',
                  selectedPartyId === party.id && 'bg-accent'
                )}
                onClick={() => {
                  onPartySelect(party);
                  setOpen(false);
                  setSearchTerm('');
                }}
              >
                {selectedPartyId === party.id && <Check className="h-4 w-4" />}
                <span className="flex-1">{party.name}</span>
              </div>
            ))
          )}
        </ScrollArea>
      </PopoverContent>
    </Popover>
  );
}
```

**3. CalculationDisplay Component**

```typescript
// components/invoice-shared/calculation-display.tsx

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { formatCurrency } from '@/lib/currency-formatter';

export interface CalculationDisplayProps {
  subtotal: number;
  discountAmount?: number;
  taxAmount?: number;
  grandTotal: number;
  currencySymbol?: string;
  amountPaid?: number;
  balance?: number;
  showBalance?: boolean;
  label?: string;
}

export function CalculationDisplay({
  subtotal,
  discountAmount = 0,
  taxAmount = 0,
  grandTotal,
  currencySymbol = '$',
  amountPaid = 0,
  balance,
  showBalance = true,
}: CalculationDisplayProps) {
  const displayBalance = balance ?? grandTotal - amountPaid;

  return (
    <Card className="bg-muted">
      <CardHeader className="pb-3">
        <CardTitle className="text-sm">Totals</CardTitle>
      </CardHeader>
      <CardContent className="space-y-2 text-sm">
        <div className="flex justify-between">
          <span className="text-muted-foreground">Subtotal:</span>
          <span className="font-mono">{formatCurrency(subtotal, currencySymbol)}</span>
        </div>
        {discountAmount > 0 && (
          <div className="flex justify-between text-orange-600">
            <span className="text-muted-foreground">Discount:</span>
            <span className="font-mono">-{formatCurrency(discountAmount, currencySymbol)}</span>
          </div>
        )}
        {taxAmount !== 0 && (
          <div className="flex justify-between">
            <span className="text-muted-foreground">Tax:</span>
            <span className="font-mono">{formatCurrency(taxAmount, currencySymbol)}</span>
          </div>
        )}
        <div className="border-t pt-2 flex justify-between font-semibold">
          <span>Grand Total:</span>
          <span className="font-mono">{formatCurrency(grandTotal, currencySymbol)}</span>
        </div>
        {showBalance && (
          <div className="border-t pt-2 flex justify-between">
            <span className="text-muted-foreground">Balance Due:</span>
            <span className={cn('font-mono', displayBalance > 0 ? 'text-red-600' : 'text-green-600')}>
              {formatCurrency(displayBalance, currencySymbol)}
            </span>
          </div>
        )}
      </CardContent>
    </Card>
  );
}
```

---

### 3. Domain-Specific Customization

#### In Purchases Module

```typescript
// lib/modules/purchases/application/purchase-ui-service.ts

import { LineItemsTable } from '@/components/invoice-shared/line-items-table';
import { PartySelector } from '@/components/invoice-shared/party-selector';

// Purchases-specific columns
export const purchaseLineItemColumns = [
  { key: 'material', label: 'Material', type: 'select', minWidth: 200 },
  { key: 'quantity', label: 'Qty', type: 'number', minWidth: 80 },
  { key: 'unitPrice', label: 'Unit Price', type: 'number', minWidth: 100 },
  { key: 'lineDiscountPercent', label: 'Discount %', type: 'number', minWidth: 80 },
  { key: 'lineDiscountAmount', label: 'Discount Amount', type: 'number', minWidth: 100 },
  { key: 'bonus', label: 'Bonus', type: 'number', minWidth: 80 },  // Purchases-specific
  { key: 'taxRate', label: 'Tax %', type: 'number', minWidth: 80 }, // Purchases-specific
  { key: 'warehouse', label: 'Warehouse', type: 'select', minWidth: 120 },
  { key: 'shelf', label: 'Shelf', type: 'select', minWidth: 100 },
];

// In purchase-order-form.tsx:
// <LineItemsTable
//   items={gridItems}
//   columns={purchaseLineItemColumns}
//   onItemsChange={setGridItems}
//   onDeleteRow={handleDeleteRow}
//   storageKey="purchase-order-table-preferences"
// />

// <PartySelector
//   partyType="supplier"
//   suppliers={supplierList}
//   selectedPartyId={selectedSupplierId}
//   onPartySelect={handleSupplierSelect}
// />
```

#### In Sales Module

```typescript
// lib/modules/sales/application/sales-ui-service.ts

// Sales-specific columns (adapted)
export const salesLineItemColumns = [
  { key: 'material', label: 'Product', type: 'select', minWidth: 200 },
  { key: 'quantity', label: 'Qty', type: 'number', minWidth: 80 },
  { key: 'unitPrice', label: 'Unit Price', type: 'number', minWidth: 100 },
  { key: 'lineDiscountPercent', label: 'Discount %', type: 'number', minWidth: 80 },
  { key: 'lineDiscountAmount', label: 'Discount Amt', type: 'number', minWidth: 100 },
  // NO bonus, NO taxRate (sales-specific differences)
  { key: 'warehouse', label: 'Warehouse', type: 'select', minWidth: 120 },
  { key: 'shelf', label: 'Shelf', type: 'select', minWidth: 100 },
];

// In sales-invoice-form.tsx:
// <LineItemsTable
//   items={gridItems}
//   columns={salesLineItemColumns}  // ← Different columns, same component
//   onItemsChange={setGridItems}
//   onDeleteRow={handleDeleteRow}
//   storageKey="sales-invoice-table-preferences"
// />

// <PartySelector
//   partyType="customer"  // ← Key difference
//   customers={customerList}
//   selectedPartyId={selectedCustomerId}
//   onPartySelect={handleCustomerSelect}
// />
```

---

## Implementation Roadmap

### Phase 1: Extract Shared Components (Week 1)

**Tasks**:
1. Create `components/invoice-shared/` directory
2. Extract `LineItemsTable` (generic, configurable columns)
3. Extract `PartySelector` (with partyType prop)
4. Extract `CalculationDisplay` 
5. Extract calculation utilities to `lib/modules/shared/invoice-calculations.ts`

**Files to Create**:
- `components/invoice-shared/line-items-table.tsx`
- `components/invoice-shared/party-selector.tsx`
- `components/invoice-shared/calculation-display.tsx`
- `components/invoice-shared/index.ts` (re-exports)
- `lib/modules/shared/invoice-calculations.ts`

**Validation**:
- Purchases form still works (pass purchase columns)
- Column resizing works
- Keyboard nav works
- localStorage persistence works

### Phase 2: Adapt Purchases Form (Week 1-2)

**Tasks**:
1. Import shared components
2. Define `purchaseLineItemColumns` configuration
3. Replace inline table code with `<LineItemsTable />`
4. Replace inline party selection with `<PartySelector />`
5. Replace inline totals display with `<CalculationDisplay />`

**Validation**:
- npm run test:purchases still passes
- All purchase form functionality preserved
- No breaking changes

### Phase 3: Build Sales Form (Week 2-3)

**Tasks**:
1. Define `salesLineItemColumns` configuration (no bonus, no tax rate)
2. Use shared `LineItemsTable` with sales columns
3. Use shared `PartySelector` with customer type
4. Use shared `CalculationDisplay` with sales totals

**Validation**:
- Sales form renders
- Line items work
- Customer selection works
- Totals calculate correctly

---

## Domain-Specific Differences Reference

### Table Columns

| Feature | Purchases | Sales |
|---------|-----------|-------|
| **Party** | Supplier | Customer |
| **Columns Include** | Bonus, Tax Rate | — (removed) |
| **Warehouse** | Yes | Yes |
| **Shelf** | Yes | Yes |
| **Item Pricing** | Cost price | Selling price |
| **Discount Direction** | Reduces cost | Reduces revenue |
| **Tax Direction** | Input tax (AP) | Output tax (AR) |

### Calculations

| Aspect | Purchases | Sales |
|--------|-----------|-------|
| **Discount** | Reduces line cost | Reduces line revenue |
| **Tax** | Input tax (recoverable) | Output tax (payable) |
| **GL Impact** | Debit Inventory/Expense, Credit AP | Debit AR, Credit Revenue |
| **Posting Account** | Accounts Payable (Suppliers) | Accounts Receivable (Customers) |

### Validation Rules

| Rule | Purchases | Sales |
|------|-----------|-------|
| **Party Required** | Supplier ID mandatory | Customer ID optional (cash sales) |
| **Invoice Duplicate** | Check supplier invoice # | N/A (auto-generated) |
| **Payment Lock** | Prevents editing while locked | — (not needed) |
| **Warehouse** | Can be required | Can be required |

---

## File Structure After Implementation

```
components/
├── invoice-shared/                       # NEW: Shared invoice components
│   ├── index.ts
│   ├── line-items-table.tsx
│   ├── party-selector.tsx
│   ├── calculation-display.tsx
│   ├── currency-section.tsx
│   ├── discount-section.tsx
│   ├── tax-section.tsx
│   ├── payment-methods-section.tsx
│   └── utils/
│       └── line-item-calculations.ts
│
├── admin/
│   ├── purchase-order-form.tsx          # UPDATED: Use shared components
│   └── sales-invoice-form.tsx           # UPDATED: Use shared components
│
└── ui/
    └── ... (existing base components)

lib/
└── modules/
    └── shared/                          # NEW: Shared module utilities
        ├── invoice-calculations.ts      # Generic calculation functions
        ├── invoice-types.ts             # Shared TypeScript types
        └── invoice-utils.ts             # Shared helper functions
```

---

## Backward Compatibility

✅ **Purchases Module**:
- Extract patterns without changing existing form behavior
- All keyboard navigation preserved
- All calculations identical
- All localStorage keys preserved
- Tests continue to pass

✅ **Sales Module**:
- Uses shared components as building blocks
- No impact on Purchases form
- Can evolve independently

---

## Testing Strategy

### Shared Component Tests

```typescript
// tests/components/invoice-shared/line-items-table.test.ts
describe('LineItemsTable', () => {
  it('renders columns from definition', () => { /* ... */ });
  it('calls onDeleteRow when delete button clicked', () => { /* ... */ });
  it('persists column widths to localStorage', () => { /* ... */ });
  it('handles keyboard navigation (Tab, Enter)', () => { /* ... */ });
  it('reorders rows on drag/drop', () => { /* ... */ });
});

// tests/components/invoice-shared/party-selector.test.ts
describe('PartySelector', () => {
  it('filters parties by search term', () => { /* ... */ });
  it('shows customer list when type=customer', () => { /* ... */ });
  it('shows suppliers list when type=supplier', () => { /* ... */ });
  it('calls onPartySelect when party clicked', () => { /* ... */ });
});
```

### Integration Tests

```typescript
// tests/purchase-order-form.test.ts
describe('PurchaseOrderForm with shared components', () => {
  it('renders line items table with purchase columns', () => { /* ... */ });
  it('calculates totals correctly', () => { /* ... */ });
  it('maintains supplier selection', () => { /* ... */ });
});

// tests/sales-invoice-form.test.ts
describe('SalesInvoiceForm with shared components', () => {
  it('renders line items table with sales columns', () => { /* ... */ });
  it('calculates sales totals correctly', () => { /* ... */ });
  it('maintains customer selection', () => { /* ... */ });
});
```

---

## Benefits of This Approach

✅ **Code Reuse**:
- 60% reduction in line-item table code duplication
- 50% reduction in calculation logic duplication
- Shared validation patterns and error handling

✅ **Consistency**:
- Same keyboard shortcuts across modules
- Same column resizing behavior
- Same calculation logic (no discrepancies)
- Same UX patterns (form layout, dialogs, buttons)

✅ **Maintainability**:
- Bug fixes in shared components benefit both modules
- Performance improvements apply everywhere
- Design changes can be applied uniformly
- Easier to add new modules (Inventory, Accounting)

✅ **Scalability**:
- Pattern extends to Purchase Returns, Credit Notes
- Can be reused for Inventory adjustments
- Foundation for future multi-line-item forms

---

## Next Steps

1. **Start Phase 1**: Extract `LineItemsTable` component by copying purchase table code and parameterizing columns
2. **Test Extraction**: Verify Purchases form still works with extracted component
3. **Extract Shared Calculations**: Move calculation logic to `lib/modules/shared/invoice-calculations.ts`
4. **Adapt Purchases Form**: Replace inline code with component usage
5. **Build Sales Form**: Adapt sales-invoice-form.tsx using shared components with sales-specific columns
6. **Documentation**: Update STANDARDIZATION_GUIDE.md with shared component patterns

---

## Reference

- **Purchases Form**: [purchase-order-form.tsx](../../../components/admin/purchase-order-form.tsx)
- **Sales Form**: [sales-invoice-form.tsx](../../../components/admin/sales-invoice-form.tsx)
- **Base UI Components**: `components/ui/`
- **Shared Calculations** (future): `lib/modules/shared/invoice-calculations.ts`
