/* global CRQQB_SETTINGS, CRQQB_VARS */ const { useState, useMemo } = React; const fmt2 = (n)=> new Intl.NumberFormat('en-IN',{minimumFractionDigits:2, maximumFractionDigits:2}).format(+n||0); /** daily-sequenced ID: CRQ/YYMMDD/NNNN (start 21, +6) */ function nextAutoId(baseDate) { const d = baseDate ? new Date(baseDate) : new Date(); const y = String(d.getFullYear()).slice(-2); const m = String(d.getMonth()+1).padStart(2,'0'); const day = String(d.getDate()).padStart(2,'0'); const key = `crq-seq-${y}${m}${day}`; let last = parseInt(localStorage.getItem(key)||'15', 10); if (!Number.isFinite(last)) last = 15; let nxt = last + 6; if (nxt < 21) nxt = 21; localStorage.setItem(key, String(nxt)); const serial = String(nxt).padStart(4,'0'); return `CRQ/${y}${m}${day}/${serial}`; } function parseStateFromGSTIN(gstin){ if (!gstin || gstin.length < 2) return {code:"", state:""}; const map = {"27":"Maharashtra","24":"Gujarat","09":"Uttar Pradesh","07":"Delhi"}; // minimal map const code = gstin.slice(0,2); return {code, state: map[code] || ""}; } const INDIA_STATES = [ "Andhra Pradesh","Arunachal Pradesh","Assam","Bihar","Chhattisgarh","Goa","Gujarat","Haryana","Himachal Pradesh", "Jharkhand","Karnataka","Kerala","Madhya Pradesh","Maharashtra","Manipur","Meghalaya","Mizoram","Nagaland", "Odisha","Punjab","Rajasthan","Sikkim","Tamil Nadu","Telangana","Tripura","Uttar Pradesh","Uttarakhand","West Bengal", "Andaman and Nicobar Islands","Chandigarh","Dadra and Nagar Haveli and Daman and Diu","Delhi","Jammu and Kashmir", "Ladakh","Lakshadweep","Puducherry" ]; function inrWords(n){ const a=["","One","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Eleven","Twelve","Thirteen","Fourteen","Fifteen","Sixteen","Seventeen","Eighteen","Nineteen"]; const b=["","","Twenty","Thirty","Forty","Fifty","Sixty","Seventy","Eighty","Ninety"]; n = Math.floor(+n||0); const two = (x)=> x<20?a[x]:b[Math.floor(x/10)]+(x%10?(" "+a[x%10]):""); const three = (x)=> x<100?two(x):(a[Math.floor(x/100)]+" Hundred"+(x%100?(" "+two(x%100)):"")); const crore=Math.floor(n/1e7), lakh=Math.floor((n%1e7)/1e5), th=Math.floor((n%1e5)/1e3), h=n%1e3; return [crore?two(crore)+" Crore":"", lakh?two(lakh)+" Lakh":"", th?two(th)+" Thousand":"", h?three(h):""].filter(Boolean).join(" ")+" Rupees Only"; } function defaultsFromSettings() { const s = CRQQB_SETTINGS || {}; const today = new Date(); const y = String(today.getFullYear()).slice(-2); const m = String(today.getMonth()+1).padStart(2,'0'); return { header: { doc_type: 'Quotation', number_label: '', cr_no: `CRQ/${y}${m}${String(today.getDate()).padStart(2,'0')}/0021`, cr_date: today.toISOString().slice(0,10), chip: s.seal_text, place_of_supply: s.place_of_supply }, job: { job_type: s.default_job_type || 'Re-rubberisation', machine: '', model: '', rubber_grade: s.default_rubber_grade || '', employee_id: '' }, customer: { name: '', gstin: '', phone:'', email:'', contact_person:'', billing_address:'', pin:'', state:'', state_code:'', shipping_same:true, shipping_address:'' }, items: [], default_rate: 0, quote_discount: 0, freight: 0, packaging: 0, freight_taxable: true, packaging_taxable: true, gst_enabled: true, warranty_text: '', settings: { company: { name: s.company_name, website: s.company_website, office: s.office_address, plant: s.plant_address, branch: s.branch_address, phone: s.company_phone, email: s.company_email, gstin: s.gstin, logo_url: s.logo_url }, banking: { bank_name: s.bank_name, account_name: s.account_name, account_no: s.account_no, ifsc: s.ifsc, branch: s.branch, upi_id: s.upi_id }, tax: { mode: s.tax_mode || 'CGST+SGST', percent: +s.tax_percent || 18 }, labels: { warranty: s.warranty_label || 'Warranty' }, // Split terms on real newlines so each line becomes its own list item terms: (s.terms || '').split(/\r?\n/).filter(Boolean), footnote: s.footnote || '', // Pass through signature and designation for the print template signature_url: s.signature_url, designation: s.designation } }; } function computeTotals(payload) { const isRe = payload.job?.job_type === 'Re-rubberisation'; const defaultRate = +payload.default_rate || 0; const subTotal = Math.round(payload.items.reduce((acc, it) => { const base = (Number(it.length_mm)||0) * (Number(it.diameter_mm)||0) / 100; const effRate = (it.rate === '' || it.rate === undefined || it.rate === null) ? defaultRate : (Number(it.rate)||0); const line = base * effRate * (Number(it.qty)||0); const afterDisc = it.disc_type === '%' ? (line - (line * (Number(it.discount)||0) / 100)) : (line - (Number(it.discount)||0)); const spindle = isRe ? 0 : (Number(it.spindle)||0); const withSpindle = afterDisc + spindle; return acc + Math.max(0, withSpindle); }, 0) * 100) / 100; const freight = Number(payload.freight)||0; const packaging = Number(payload.packaging)||0; const quoteDisc = Number(payload.quote_discount)||0; const taxableCharges = (payload.freight_taxable ? freight : 0) + (payload.packaging_taxable ? packaging : 0); const nontaxCharges = (payload.freight_taxable ? 0 : freight) + (payload.packaging_taxable ? 0 : packaging); const afterQuoteDisc = Math.max(0, subTotal - quoteDisc); const taxable = Math.round((afterQuoteDisc + taxableCharges) * 100) / 100; const gstEnabled = !!payload.gst_enabled; const taxPct = Number(payload.settings?.tax?.percent)||0; const isIGST = (payload.settings?.tax?.mode === 'IGST'); let tax=0, cgst=0, sgst=0, igst=0; if (gstEnabled && taxPct>0) { tax = Math.round(taxable * taxPct) / 100; if (isIGST) { igst = tax; } else { cgst = Math.round((tax/2) * 100)/100; sgst = Math.round((tax/2) * 100)/100; } } const preRound = taxable + (gstEnabled ? tax : 0) + nontaxCharges; const grand = Math.round(preRound); const roundOff = Math.round((grand - preRound) * 100) / 100; return { subTotal, taxable, tax: (gstEnabled?tax:0), cgst, sgst, igst, nontaxCharges, roundOff, grand, words: inrWords(grand) }; } function App() { const [payload, setPayload] = useState(defaultsFromSettings()); const totals = useMemo(()=>computeTotals(payload), [payload]); // Add a new item row. New rows default the HSN code to 84439100 as // required. Other fields remain editable. const addRow = ()=> setPayload(p=>({ ...p, items: [...p.items, { unit: 'Inking', type: '', hsn: '84439100', qty: 1, diameter_mm: 0, length_mm: 0, rubber_grade: p.job?.rubber_grade || '', rate: '', discount: 0, disc_type: '%', spindle: 0 }] })); const delRow = (idx)=> setPayload(p=>({...p, items: p.items.filter((_,i)=>i!==idx)})); const updateRow = (idx, key, val)=> setPayload(p=>{ const items = p.items.slice(); items[idx] = {...items[idx], [key]: val}; return {...p, items}; }); const postToPrint = ()=> { const w = window.open(CRQQB_VARS?.print_url, '_blank'); const data = {...payload, totals}; let tries = 0; const send = ()=> { try { w.postMessage({type:'CRQ_PAYLOAD', payload: data}, '*'); } catch(e){} tries++; if (tries<14 && w && !w.closed) setTimeout(send, 250); }; setTimeout(send, 350); }; const gst = parseStateFromGSTIN(payload.customer.gstin||""); return (
| # | Unit | Type | HSN | Qty | Dia (mm) | R.L. (mm) | Rate (₹) | Disc | Disc Type | {payload.job?.job_type==="Re-rubberisation" ? null :Spindle (₹) | }|
|---|---|---|---|---|---|---|---|---|---|---|---|
| {idx+1} | updateRow(idx,'unit', e.target.value)} /> | updateRow(idx,'type', e.target.value)} /> | updateRow(idx,'hsn', e.target.value)} /> | updateRow(idx,'qty', +e.target.value||0)} /> | updateRow(idx,'diameter_mm', +e.target.value||0)} /> | updateRow(idx,'length_mm', +e.target.value||0)} /> | updateRow(idx,'rate', e.target.value===''?'':(+e.target.value||0))} /> | updateRow(idx,'discount', +e.target.value||0)} /> | updateRow(idx,'spindle', +e.target.value||0)} /> | ||
|
|
|||||||||||