// Shared Bill Components // Extracted from AwaakBill.tsx and JawaakBill.tsx to reduce code duplication import React, { useState, useRef, useEffect, useMemo } from 'react'; import { Search, ChevronDown, Check, X } from 'lucide-react'; import { Party, PartyType } from '../types'; // ============================================ // PARTY DROPDOWN COMPONENT // ============================================ export interface PartyDropdownProps { parties: Party[]; selectedPartyId: string; onSelectParty: (partyId: string) => void; partyType?: PartyType | 'all'; placeholder?: string; disabled?: boolean; className?: string; } export const PartyDropdown: React.FC = ({ parties, selectedPartyId, onSelectParty, partyType = 'all', placeholder = 'Select Party (पार्टी निवडा)', disabled = false, className = '' }) => { const [search, setSearch] = useState(''); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); // Filter parties by type const filteredParties = useMemo(() => { let filtered = parties; if (partyType !== 'all') { filtered = parties.filter(p => p.party_type === partyType || p.party_type === PartyType.BOTH ); } // Apply search filter if (search.trim()) { const term = search.toLowerCase().trim(); filtered = filtered.filter(p => p.name.toLowerCase().includes(term) || (p.city && p.city.toLowerCase().includes(term)) || (p.phone && p.phone.includes(term)) ); } return filtered; }, [parties, partyType, search]); // Get selected party const selectedParty = useMemo(() => { return parties.find(p => p.id === selectedPartyId); }, [parties, selectedPartyId]); // Close dropdown on outside click useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleSelect = (partyId: string) => { onSelectParty(partyId); setSearch(''); setIsOpen(false); }; return (
{/* Trigger Button */} {/* Dropdown */} {isOpen && (
{/* Search Input */}
setSearch(e.target.value)} className="w-full pl-9 pr-3 py-2 text-sm border border-gray-200 rounded-md focus:ring-2 focus:ring-teal-500 focus:border-teal-500 outline-none" autoFocus /> {search && ( )}
{/* Party List */}
{filteredParties.length === 0 ? (
{search ? 'No parties found' : 'No parties available'}
) : ( filteredParties.map(party => ( )) )}
)}
); }; // ============================================ // SUMMARY INPUT COMPONENT // ============================================ export interface SummaryInputProps { label: string; value: number; onChange: (value: number) => void; disabled?: boolean; min?: number; max?: number; step?: number; placeholder?: string; suffix?: string; className?: string; } export const SummaryInput: React.FC = ({ label, value, onChange, disabled = false, min = 0, max, step = 0.01, placeholder = '0', suffix, className = '' }) => { const handleChange = (e: React.ChangeEvent) => { const parsed = parseFloat(e.target.value); let newValue = isNaN(parsed) ? 0 : parsed; // Apply min/max constraints newValue = Math.max(min, newValue); if (max !== undefined) { newValue = Math.min(max, newValue); } onChange(newValue); }; return (
{label}
{suffix && {suffix}}
); }; // ============================================ // PAYMENT SECTION COMPONENT // ============================================ export interface PaymentSectionProps { totalAmount: number; paymentMode: 'cash' | 'online' | 'hybrid' | 'due'; onPaymentModeChange: (mode: 'cash' | 'online' | 'hybrid' | 'due') => void; cashAmount: number; onCashAmountChange: (amount: number) => void; onlineAmount: number; onOnlineAmountChange: (amount: number) => void; disabled?: boolean; className?: string; } export const PaymentSection: React.FC = ({ totalAmount, paymentMode, onPaymentModeChange, cashAmount, onCashAmountChange, onlineAmount, onOnlineAmountChange, disabled = false, className = '' }) => { // Calculate due amount const dueAmount = useMemo(() => { if (paymentMode === 'due') return totalAmount; if (paymentMode === 'hybrid') { return Math.max(0, totalAmount - cashAmount - onlineAmount); } return 0; }, [totalAmount, paymentMode, cashAmount, onlineAmount]); // Handle hybrid payment auto-calculation const handleHybridCashChange = (cash: number) => { onCashAmountChange(cash); const remaining = totalAmount - cash; if (remaining >= 0) { onOnlineAmountChange(remaining); } }; const handleHybridOnlineChange = (online: number) => { onOnlineAmountChange(online); const remaining = totalAmount - online; if (remaining >= 0) { onCashAmountChange(remaining); } }; // Auto-fill amounts when mode changes useEffect(() => { if (paymentMode === 'cash') { onCashAmountChange(totalAmount); onOnlineAmountChange(0); } else if (paymentMode === 'online') { onCashAmountChange(0); onOnlineAmountChange(totalAmount); } else if (paymentMode === 'due') { onCashAmountChange(0); onOnlineAmountChange(0); } else if (paymentMode === 'hybrid') { // Keep existing values or default to 50-50 split if (cashAmount === 0 && onlineAmount === 0) { const half = Math.round(totalAmount / 2); onCashAmountChange(half); onOnlineAmountChange(totalAmount - half); } } }, [paymentMode, totalAmount]); return (
{/* Payment Mode Selection */}
{(['cash', 'online', 'hybrid', 'due'] as const).map(mode => ( ))}
{/* Amount Inputs */} {paymentMode === 'hybrid' && (
)} {/* Summary */}
Total Amount: ₹{totalAmount.toFixed(2)}
{paymentMode !== 'due' && (
Paid Amount: ₹{(paymentMode === 'hybrid' ? cashAmount + onlineAmount : paymentMode === 'cash' ? cashAmount : onlineAmount).toFixed(2)}
)}
Due Amount: 0 ? 'text-red-600' : 'text-green-600'}> ₹{dueAmount.toFixed(2)}
); }; // ============================================ // MOBILE SUMMARY PANEL COMPONENT // ============================================ export interface MobileSummaryPanelProps { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode; } export const MobileSummaryPanel: React.FC = ({ isOpen, onClose, title, children }) => { if (!isOpen) return null; return (
{/* Backdrop */}
{/* Panel */}
{/* Header */}

{title}

{/* Content */}
{children}
{/* Animation */}
); }; // ============================================ // LOADING BUTTON COMPONENT // ============================================ export interface LoadingButtonProps { onClick: () => void | Promise; isLoading: boolean; loadingText?: string; children: React.ReactNode; variant?: 'primary' | 'secondary' | 'danger'; disabled?: boolean; className?: string; icon?: React.ReactNode; } export const LoadingButton: React.FC = ({ onClick, isLoading, loadingText = 'Saving...', children, variant = 'primary', disabled = false, className = '', icon }) => { const variantStyles = { primary: 'bg-teal-600 hover:bg-teal-700 text-white', secondary: 'bg-gray-100 hover:bg-gray-200 text-gray-700', danger: 'bg-red-500 hover:bg-red-600 text-white' }; return ( ); }; export default { PartyDropdown, SummaryInput, PaymentSection, MobileSummaryPanel, LoadingButton };