Spaces:
Sleeping
Sleeping
| import { useState, useEffect } from "react"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Label } from "@/components/ui/label"; | |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; | |
| import { Separator } from "@/components/ui/separator"; | |
| import { Alert, AlertDescription } from "@/components/ui/alert"; | |
| import { ArrowLeft, Save, Printer, AlertCircle } from "lucide-react"; | |
| import { Link, useNavigate } from "react-router-dom"; | |
| import { useToast } from "@/hooks/use-toast"; | |
| import { parties, billBooks, lots, getNextBillNumber, getAvailableLotsByVariety } from "@/lib/mockData"; | |
| import type { Lot } from "@/types"; | |
| import { useLanguage } from "@/contexts/LanguageContext"; | |
| import { LanguageToggle } from "@/components/LanguageToggle"; | |
| export default function InvoiceEnhanced() { | |
| const { toast } = useToast(); | |
| const navigate = useNavigate(); | |
| const { t } = useLanguage(); | |
| const [formData, setFormData] = useState({ | |
| partyId: "", | |
| partyName: "", | |
| date: new Date().toISOString().split('T')[0], | |
| serialNumber: "", | |
| billBookId: "", | |
| particular: "", | |
| lotNumber: "", | |
| bags: 0, | |
| grossWeight: 40, | |
| rate: 0, | |
| }); | |
| const [charges, setCharges] = useState({ | |
| bardhana: true, | |
| hamali: false, | |
| adhath: 0, | |
| cess: 2, | |
| gaadiBharni: 0, | |
| }); | |
| const [availableLots, setAvailableLots] = useState<Lot[]>([]); | |
| const [selectedLot, setSelectedLot] = useState<Lot | null>(null); | |
| const [showBillWarning, setShowBillWarning] = useState(false); | |
| // Auto-generate bill number from active bill book | |
| useEffect(() => { | |
| try { | |
| const { billBookId, serialNumber } = getNextBillNumber(); | |
| setFormData(prev => ({ | |
| ...prev, | |
| billBookId, | |
| serialNumber, | |
| })); | |
| } catch (error: any) { | |
| toast({ | |
| title: "त्रुटी", | |
| description: error.message, | |
| variant: "destructive", | |
| }); | |
| } | |
| }, []); | |
| // Load available lots when variety is selected | |
| useEffect(() => { | |
| if (formData.particular) { | |
| const lots = getAvailableLotsByVariety(formData.particular); | |
| setAvailableLots(lots); | |
| if (lots.length === 0) { | |
| toast({ | |
| title: "माहिती", | |
| description: `${formData.particular} साठी कोणताही लॉट उपलब्ध नाही`, | |
| }); | |
| } | |
| } else { | |
| setAvailableLots([]); | |
| setSelectedLot(null); | |
| } | |
| }, [formData.particular]); | |
| // Load lot details when lot is selected | |
| useEffect(() => { | |
| if (formData.lotNumber) { | |
| const lot = availableLots.find(l => l.lotNumber === formData.lotNumber); | |
| setSelectedLot(lot || null); | |
| if (lot) { | |
| setFormData(prev => ({ | |
| ...prev, | |
| grossWeight: lot.grossWeightPerBag, | |
| rate: lot.purchaseRate, | |
| })); | |
| } | |
| } | |
| }, [formData.lotNumber, availableLots]); | |
| const calculateNetWeight = () => { | |
| const bags = parseFloat(formData.bags.toString()) || 0; | |
| const grossPerBag = parseFloat(formData.grossWeight.toString()) || 40; | |
| return bags * (grossPerBag - 1); | |
| }; | |
| const calculateBasicAmount = () => { | |
| const netWeight = calculateNetWeight(); | |
| const rate = parseFloat(formData.rate.toString()) || 0; | |
| return netWeight * rate; | |
| }; | |
| const calculateCharges = () => { | |
| const bags = parseFloat(formData.bags.toString()) || 0; | |
| const basicAmount = calculateBasicAmount(); | |
| let total = basicAmount; | |
| const breakdown = { | |
| bardhana: charges.bardhana ? bags * 18 : 0, | |
| hamali: charges.hamali ? bags * 6 : 0, | |
| adhath: (charges.adhath / 100) * basicAmount, | |
| cess: (charges.cess / 100) * basicAmount, | |
| gaadiBharni: parseFloat(charges.gaadiBharni.toString()) || 0, | |
| }; | |
| total += breakdown.bardhana + breakdown.hamali + breakdown.adhath + breakdown.cess + breakdown.gaadiBharni; | |
| return { breakdown, total }; | |
| }; | |
| const handleSave = () => { | |
| // Validation | |
| if (!formData.partyId) { | |
| toast({ | |
| title: "त्रुटी", | |
| description: "कृपया पार्टी निवडा", | |
| variant: "destructive", | |
| }); | |
| return; | |
| } | |
| if (!formData.particular || !formData.lotNumber) { | |
| toast({ | |
| title: "त्रुटी", | |
| description: "कृपया जात आणि लॉट नंबर निवडा", | |
| variant: "destructive", | |
| }); | |
| return; | |
| } | |
| if (formData.bags <= 0) { | |
| toast({ | |
| title: "त्रुटी", | |
| description: "कृपया पोत्यांची संख्या भरा", | |
| variant: "destructive", | |
| }); | |
| return; | |
| } | |
| // Check if enough bags available in lot | |
| if (selectedLot && formData.bags > selectedLot.availableBags) { | |
| toast({ | |
| title: "त्रुटी", | |
| description: `फक्त ${selectedLot.availableBags} पोत्या उपलब्ध आहेत`, | |
| variant: "destructive", | |
| }); | |
| return; | |
| } | |
| const { breakdown, total } = calculateCharges(); | |
| const invoiceData = { | |
| ...formData, | |
| items: [{ | |
| particular: formData.particular, | |
| lotNumber: formData.lotNumber, | |
| bags: formData.bags, | |
| grossWeightPerBag: formData.grossWeight, | |
| netWeightPerBag: formData.grossWeight - 1, | |
| rate: formData.rate, | |
| basicAmount: calculateBasicAmount(), | |
| }], | |
| charges: { | |
| bardhana: breakdown.bardhana, | |
| hamali: breakdown.hamali, | |
| adhath: breakdown.adhath, | |
| cess: breakdown.cess, | |
| gaadiBharni: breakdown.gaadiBharni, | |
| }, | |
| subtotal: calculateBasicAmount(), | |
| totalCharges: breakdown.bardhana + breakdown.hamali + breakdown.adhath + breakdown.cess + breakdown.gaadiBharni, | |
| totalAmount: total, | |
| }; | |
| console.log("Invoice Data:", invoiceData); | |
| toast({ | |
| title: "जावक बिल जतन झाले", | |
| description: `बिल नं. ${formData.serialNumber} यशस्वीरित्या जतन केले`, | |
| }); | |
| // In production, save to database and update lot stock | |
| // updateLotAfterSale(selectedLot.id, formData.bags); | |
| // Navigate to stock or dashboard | |
| // navigate('/stock'); | |
| }; | |
| const handlePrint = () => { | |
| window.print(); | |
| }; | |
| const { breakdown, total } = calculateCharges(); | |
| return ( | |
| <> | |
| {/* Mobile-optimized header */} | |
| <header className="sticky top-0 z-20 h-14 border-b bg-card/95 backdrop-blur print:hidden"> | |
| <div className="px-4 h-full flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <Link to="/"> | |
| <Button variant="ghost" size="icon"> | |
| <ArrowLeft className="h-5 w-5" /> | |
| </Button> | |
| </Link> | |
| <div> | |
| <h1 className="text-lg font-bold">{t('invoice.title')}</h1> | |
| </div> | |
| </div> | |
| <LanguageToggle /> | |
| </div> | |
| </header> | |
| {/* Main Content */} | |
| <main className="pb-32"> | |
| <div className="container mx-auto px-4 py-6"> | |
| <div className="grid gap-6 lg:grid-cols-3"> | |
| {/* Invoice Form */} | |
| <div className="lg:col-span-2"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>{t('invoice.billDetails')}</CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-6"> | |
| {/* Bill Number and Date */} | |
| <div className="grid gap-4 md:grid-cols-3"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="billBook" className="text-base">{t('invoice.billBook')}</Label> | |
| <Select | |
| value={formData.billBookId} | |
| onValueChange={(value) => setFormData({ ...formData, billBookId: value })} | |
| > | |
| <SelectTrigger id="billBook" className="h-12 text-base"> | |
| <SelectValue placeholder="बिल बुक निवडा" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {billBooks.map((bb) => ( | |
| <SelectItem key={bb.id} value={bb.id} disabled={!bb.isActive}> | |
| {bb.name} ({bb.serialFrom}-{bb.serialTo}) | |
| {bb.isActive && " ✓"} | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="serialNumber" className="text-base">{t('invoice.billNumber')}</Label> | |
| <Input | |
| id="serialNumber" | |
| value={formData.serialNumber} | |
| onChange={(e) => { | |
| setFormData({ ...formData, serialNumber: e.target.value }); | |
| setShowBillWarning(true); | |
| }} | |
| className="h-12 text-base font-semibold" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="date" className="text-base">{t('invoice.date')}</Label> | |
| <Input | |
| id="date" | |
| type="date" | |
| value={formData.date} | |
| onChange={(e) => setFormData({ ...formData, date: e.target.value })} | |
| className="h-12 text-base" | |
| /> | |
| </div> | |
| </div> | |
| {showBillWarning && ( | |
| <Alert> | |
| <AlertCircle className="h-4 w-4" /> | |
| <AlertDescription> | |
| {t('invoice.billNumberWarning')} | |
| </AlertDescription> | |
| </Alert> | |
| )} | |
| <div className="space-y-2"> | |
| <Label htmlFor="party" className="text-base">{t('invoice.partyName')}</Label> | |
| <Select | |
| value={formData.partyId} | |
| onValueChange={(value) => { | |
| const party = parties.find(p => p.id === value); | |
| setFormData({ | |
| ...formData, | |
| partyId: value, | |
| partyName: party?.name || '', | |
| }); | |
| }} | |
| > | |
| <SelectTrigger id="party"> | |
| <SelectValue placeholder={t('invoice.selectParty')} /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {parties.map((party) => ( | |
| <SelectItem key={party.id} value={party.id}> | |
| {party.name} | |
| {party.balance !== 0 && ( | |
| <span className={`ml-2 text-xs ${party.balance > 0 ? 'text-green-600' : 'text-red-600'}`}> | |
| (बाकी: ₹{Math.abs(party.balance)}) | |
| </span> | |
| )} | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <Separator /> | |
| {/* Product Details */} | |
| <div className="space-y-4"> | |
| <h3 className="font-semibold">माल तपशील</h3> | |
| <div className="grid gap-4 md:grid-cols-2"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="particular">जात (Variety)</Label> | |
| <Select | |
| value={formData.particular} | |
| onValueChange={(value) => setFormData({ ...formData, particular: value, lotNumber: "" })} | |
| > | |
| <SelectTrigger id="particular"> | |
| <SelectValue placeholder="जात निवडा" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="Teja">तेजा (Teja)</SelectItem> | |
| <SelectItem value="Garuda">गरुड (Garuda)</SelectItem> | |
| <SelectItem value="341">३४१ (341)</SelectItem> | |
| <SelectItem value="Sannam">सन्नम (Sannam)</SelectItem> | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="lotNumber">लॉट नंबर</Label> | |
| <Select | |
| value={formData.lotNumber} | |
| onValueChange={(value) => setFormData({ ...formData, lotNumber: value })} | |
| disabled={!formData.particular || availableLots.length === 0} | |
| > | |
| <SelectTrigger id="lotNumber"> | |
| <SelectValue placeholder={ | |
| !formData.particular ? "प्रथम जात निवडा" : | |
| availableLots.length === 0 ? "लॉट उपलब्ध नाही" : | |
| "लॉट निवडा" | |
| } /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {availableLots.map((lot) => ( | |
| <SelectItem key={lot.id} value={lot.lotNumber}> | |
| {lot.lotNumber} ({lot.availableBags} पोत्या उपलब्ध) | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| </div> | |
| {selectedLot && ( | |
| <Alert className="bg-blue-50 border-blue-200"> | |
| <AlertDescription className="text-sm"> | |
| <div className="grid grid-cols-2 gap-2"> | |
| <div><strong>खरेदी रेट:</strong> ₹{selectedLot.purchaseRate}/kg</div> | |
| <div><strong>उपलब्ध पोत्या:</strong> {selectedLot.availableBags}</div> | |
| <div><strong>दर्जा:</strong> {selectedLot.quality || 'N/A'}</div> | |
| <div><strong>पार्टी:</strong> {selectedLot.partyName}</div> | |
| </div> | |
| </AlertDescription> | |
| </Alert> | |
| )} | |
| <div className="grid gap-4 md:grid-cols-2"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="bags">पोत्यांची संख्या</Label> | |
| <Input | |
| id="bags" | |
| type="number" | |
| value={formData.bags || ''} | |
| onChange={(e) => setFormData({ ...formData, bags: parseFloat(e.target.value) || 0 })} | |
| placeholder="10" | |
| max={selectedLot?.availableBags} | |
| /> | |
| {selectedLot && formData.bags > selectedLot.availableBags && ( | |
| <p className="text-xs text-destructive"> | |
| फक्त {selectedLot.availableBags} पोत्या उपलब्ध आहेत | |
| </p> | |
| )} | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="rate">रेट (₹/kg)</Label> | |
| <Input | |
| id="rate" | |
| type="number" | |
| value={formData.rate || ''} | |
| onChange={(e) => setFormData({ ...formData, rate: parseFloat(e.target.value) || 0 })} | |
| placeholder="200" | |
| /> | |
| </div> | |
| </div> | |
| <div className="grid gap-4 md:grid-cols-3"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="grossWeight">ग्रॉस वजन (kg/पोती)</Label> | |
| <Input | |
| id="grossWeight" | |
| type="number" | |
| value={formData.grossWeight || ''} | |
| onChange={(e) => setFormData({ ...formData, grossWeight: parseFloat(e.target.value) || 0 })} | |
| placeholder="40" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label>नेट वजन (kg/पोती)</Label> | |
| <Input | |
| value={(parseFloat(formData.grossWeight.toString()) || 40) - 1} | |
| disabled | |
| className="bg-muted" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label>एकूण नेट वजन</Label> | |
| <Input | |
| value={`${calculateNetWeight()} kg`} | |
| disabled | |
| className="bg-muted font-semibold" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <Separator /> | |
| {/* Additional Charges */} | |
| <div className="space-y-4"> | |
| <h3 className="font-semibold">अतिरिक्त खर्च</h3> | |
| <div className="space-y-3"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <input | |
| type="checkbox" | |
| id="bardhana" | |
| checked={charges.bardhana} | |
| onChange={(e) => setCharges({ ...charges, bardhana: e.target.checked })} | |
| className="h-4 w-4" | |
| /> | |
| <Label htmlFor="bardhana">बर्डाणा (₹18/पोती)</Label> | |
| </div> | |
| <span className="text-sm text-muted-foreground">₹{breakdown.bardhana.toFixed(2)}</span> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <input | |
| type="checkbox" | |
| id="hamali" | |
| checked={charges.hamali} | |
| onChange={(e) => setCharges({ ...charges, hamali: e.target.checked })} | |
| className="h-4 w-4" | |
| /> | |
| <Label htmlFor="hamali">हमाली (₹6/पोती)</Label> | |
| </div> | |
| <span className="text-sm text-muted-foreground">₹{breakdown.hamali.toFixed(2)}</span> | |
| </div> | |
| <div className="flex items-center justify-between gap-4"> | |
| <Label htmlFor="adhath">अडत (Commission %)</Label> | |
| <div className="flex items-center gap-2"> | |
| <Input | |
| id="adhath" | |
| type="number" | |
| value={charges.adhath} | |
| onChange={(e) => setCharges({ ...charges, adhath: parseFloat(e.target.value) || 0 })} | |
| className="w-20" | |
| min="0" | |
| max="6" | |
| step="0.5" | |
| /> | |
| <span className="text-sm text-muted-foreground">₹{breakdown.adhath.toFixed(2)}</span> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between gap-4"> | |
| <Label htmlFor="cess">सेस (Cess %)</Label> | |
| <div className="flex items-center gap-2"> | |
| <Input | |
| id="cess" | |
| type="number" | |
| value={charges.cess} | |
| onChange={(e) => setCharges({ ...charges, cess: parseFloat(e.target.value) || 0 })} | |
| className="w-20" | |
| min="0" | |
| max="6" | |
| step="0.5" | |
| /> | |
| <span className="text-sm text-muted-foreground">₹{breakdown.cess.toFixed(2)}</span> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between gap-4"> | |
| <Label htmlFor="gaadiBharni">गाडी भरणी (Optional)</Label> | |
| <div className="flex items-center gap-2"> | |
| <Input | |
| id="gaadiBharni" | |
| type="number" | |
| value={charges.gaadiBharni || ''} | |
| onChange={(e) => setCharges({ ...charges, gaadiBharni: parseFloat(e.target.value) || 0 })} | |
| className="w-20" | |
| placeholder="0" | |
| /> | |
| <span className="text-sm text-muted-foreground">₹{breakdown.gaadiBharni.toFixed(2)}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* Invoice Summary */} | |
| <div className="lg:col-span-1"> | |
| <Card className="sticky top-4"> | |
| <CardHeader> | |
| <CardTitle>बिल सारांश</CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div className="space-y-2 text-sm"> | |
| <div className="flex justify-between"> | |
| <span className="text-muted-foreground">पोत्या:</span> | |
| <span className="font-medium">{formData.bags || 0}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-muted-foreground">ग्रॉस वजन:</span> | |
| <span className="font-medium"> | |
| {(parseFloat(formData.bags.toString()) || 0) * (parseFloat(formData.grossWeight.toString()) || 40)} kg | |
| </span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-muted-foreground">नेट वजन:</span> | |
| <span className="font-medium">{calculateNetWeight()} kg</span> | |
| </div> | |
| <Separator /> | |
| <div className="flex justify-between text-base"> | |
| <span className="font-medium">मूळ रक्कम:</span> | |
| <span className="font-bold">₹{calculateBasicAmount().toFixed(2)}</span> | |
| </div> | |
| <div className="flex justify-between text-sm"> | |
| <span className="text-muted-foreground">अतिरिक्त खर्च:</span> | |
| <span>₹{(breakdown.bardhana + breakdown.hamali + breakdown.adhath + breakdown.cess + breakdown.gaadiBharni).toFixed(2)}</span> | |
| </div> | |
| <Separator /> | |
| <div className="flex justify-between text-lg font-bold text-primary"> | |
| <span>एकूण रक्कम:</span> | |
| <span>₹{total.toFixed(2)}</span> | |
| </div> | |
| </div> | |
| {selectedLot && ( | |
| <div className="mt-4 p-3 bg-muted rounded-lg text-xs space-y-1"> | |
| <p className="font-semibold">लॉट माहिती:</p> | |
| <p>नंबर: {selectedLot.lotNumber}</p> | |
| <p>उपलब्ध: {selectedLot.availableBags - formData.bags} पोत्या</p> | |
| <p>खरेदी रेट: ₹{selectedLot.purchaseRate}/kg</p> | |
| {formData.rate > selectedLot.purchaseRate && ( | |
| <p className="text-green-600 font-semibold"> | |
| नफा: ₹{((formData.rate - selectedLot.purchaseRate) * calculateNetWeight()).toFixed(2)} | |
| </p> | |
| )} | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| {/* Bottom Action Bar - Thumb zone */} | |
| <div className="fixed bottom-0 left-0 right-0 z-30 bg-card/95 backdrop-blur border-t p-4 safe-area-inset-bottom shadow-2xl print:hidden"> | |
| <div className="container mx-auto flex items-center justify-between gap-4"> | |
| <div className="flex-1 min-w-0"> | |
| <div className="text-xs text-muted-foreground">एकूण रक्कम</div> | |
| <div className="text-2xl font-bold truncate"> | |
| ₹{total.toLocaleString('en-IN')} | |
| </div> | |
| </div> | |
| <div className="flex gap-2"> | |
| <Button | |
| variant="outline" | |
| onClick={handlePrint} | |
| className="h-12 px-4" | |
| > | |
| <Printer className="h-5 w-5" /> | |
| </Button> | |
| <Button | |
| onClick={handleSave} | |
| className="h-12 px-8 font-semibold" | |
| > | |
| <Save className="h-5 w-5 mr-2" /> | |
| जतन करा | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| </> | |
| ); | |
| } | |