ஸ்ரீ வெங்கடா ஜலபதி பவுல்ட்ரி
SVP Poultry Billing
Login Type தேர்வு செய்யவும்
ஸ்ரீ வெங்கடா ஜலபதி பவுல்ட்ரி
SRI VENKATA JALAPATHY POULTRY
GSTIN: 33DWJPP8310N1Z2
OLD NO.329/1, NEW NO.28/3, TVN MAIN ROAD, KALAVANUR, ULUNDURPETTAI TALUK, KALLAKURICHI - 606102
Mobile: 8610722004 | 9159633300 | Landline: 04149-293301
Owner Admin Panel / உரிமையாளர் நிர்வாகம்
முதலில் இந்த Admin Menu மட்டும் தெரியும். எந்த option-ஐ தொட்டாலும் அந்த option-ன் content மட்டும் திறக்கும்; கீழே வேறு content தானாக காட்டாது.
Admin Module இந்த module திறந்தே இருக்கும். Back அல்லது வேறு module தேர்வு செய்தால் மட்டுமே மாறும்.
📦 Order Booking / தினசரி ஆர்டர் புக்கிங்
Customer order-ஐ Boxes / KG / Birds என்ற மூன்று முறைகளில் பதிவு செய்யலாம். Save செய்தவுடன் 4-digit OTP உருவாகும். SMS option ON இருந்தால் Customer-க்கு order details + OTP message அனுப்பப்படும். இந்த order Customer Billing screen-லும் இன்று booking ஆக காட்டப்படும்.
Active Orders0
Boxes0
KG0
Birds0
Active totals = Booked / Loaded orders மட்டும். Delivered / Cancelled orders history-ல் மட்டும் இருக்கும்.
OTP customer-க்கு order confirmation ஆக அனுப்பப்படும். Delivery நேரத்தில் OTP verify செய்து Delivered status செய்யலாம்.
Admin Order History: பழைய தேதி, இன்றைய தேதி, future delivery date order அனைத்தும் இங்கே save ஆகும். Supervisor/Loadman login-ல் மட்டும் இன்றைய தேதி order மட்டும் காட்டப்படும்.
🕒 Login Activity / Login Status
குறிப்பு: இந்த standalone HTML version-ல் login history இந்த device/browser storage-ல் சேமிக்கப்படுகிறது. மற்ற mobile-களின் login-ஐ ஒரே Owner phone-ல் live-ஆக பார்க்க Cloud Sync / Server இணைப்பு தேவை.
🔐 Login Accounts / Username & PIN நிர்வாகம்
👑 Owner / Admin, 🧾 Supervisor / Staff, 👤 Customer — மூவருடைய Username / Mobile + PIN இங்கே தெரியும்.
Created Login Accounts
Owner/Admin panel-ல் மட்டும் Username + PIN நேரடியாக பார்க்கலாம் மற்றும் Edit செய்து மாற்றலாம்.
🔗 Customer ↔ Supervisor + Loadman / டெலிவரி டீம் செட்
Owner / Admin ஒவ்வொரு Customer-க்கும் Supervisor + Loadman இருவரையும் செட் செய்யலாம். Daily Assignment SMS = ON என்றால் assignment save செய்த உடனே Customer-க்கு delivery team message அனுப்ப முயலும். Fast2SMS/Webhook இருந்தால் Owner login செய்யும் நாளில் ஒருமுறை daily message தானாக அனுப்பும்.
இன்று SMS இன்னும் அனுப்பாத ON customers-க்கு Supervisor + Loadman details அனுப்பலாம்.
Customer Mobile / Place Assigned Team Supervisor Loadman Daily SMS Last SMS Save + Notify
Customer Master / வாடிக்கையாளர் பதிவு 0 / 30
⏱️ Staff Time / Attendance / வேலை நேர பதிவு
Supervisor, Loadman மற்றும் login access உள்ள Staff login செய்த நேரம் Check-In ஆகும். Logout செய்த நேரம் Check-Out ஆகும். நடுவிலுள்ள வேலை நேரம் HH:MM:SS ஆக கணக்கிடப்படும்.
Today Sessions0
On Duty Now0
Supervisors0
Loadmen0
இந்த standalone HTML build-ல் attendance இந்த mobile/browser storage-ல் பதிவு ஆகும். எல்லா ஊழியர்களின் வேறு mobile attendance-ஐ Owner ஒரே இடத்தில் live-ஆக பார்க்க Cloud Sync / Server தேவை.
👷 Staff Admin / Employee Master 0 Staff
Staff ID Automatic: New Staff அழுத்தியதும் Staff Type-க்கு ஏற்ப ID தானாக உருவாகும். Staff ID-ஐ manual-ஆக type செய்ய வேண்டியதில்லை.
இந்த பகுதி Owner / Admin Control மட்டும். Company-ல் வேலை செய்யும் அனைத்து Staff-களையும் பெயர், வேலை வகை, mobile, வேலை இடம், status ஆகிய விவரங்களுடன் பதிவு செய்யலாம். App login தேவையான Staff-க்கு மட்டும் Mobile + PIN enable செய்யலாம்.
Supervisor0
Loadman0
Driver0
Shop Worker0
Cutting Master0
Login Access = Yes மற்றும் PIN set செய்திருந்தால், அந்த PIN கீழே Staff display card-ல் Owner/Admin-க்கு மட்டும் காட்டப்படும்.

📷 Staff Photo / பணியாளர் போட்டோ

Staff Photo Preview
Photo இன்னும் சேர்க்கப்படவில்லை

🪪 ID Card Proof / அடையாள அட்டை ப்ரூஃப்

Front Side / முன்பக்கம்
ID Proof Front Preview
Front side இன்னும் சேர்க்கப்படவில்லை
Back Side / பின்ன்பக்கம்
ID Proof Back Preview
Back side இன்னும் சேர்க்கப்படவில்லை
ID proof capture செய்யும்போது card முழுவதும் frame-க்குள் தெளிவாக வருமாறு front side மற்றும் back side இரண்டையும் தனித்தனியாக capture செய்து save செய்யலாம்.
Mobile-ல் Camera / Gallery button அழுத்தி Staff photo மற்றும் ID proof front side / back side இரண்டையும் எடுத்து Save Staff செய்யலாம். படம் browser/device storage-ல் staff record-உடன் சேமிக்கப்படும்.
Staff ID Name Staff Type Mobile Work Place App Login Login PIN Documents Status Edit
Daily Purchase • Stock • Profit/Loss / தினசரி கொள்முதல் & லாப நட்டம்
Purchase Party Master / பர்ச்சேஸ் சப்ளையர் பதிவு 0
Purchase IDNameMobilePlaceSinceSelect
Select Purchase ID for Today's Purchase
Opening Stock / ஆரம்ப ஸ்டாக்
Today Purchase / இன்றைய கொள்முதல்
No file selected
Purchase → Sales → Stock Ledger / கொள்முதல் → விற்பனை → மீதி ஸ்டாக்
Ledger Birds Weight (kg) Rate / kg Amount / Value
Opening Stock 0 0.000 - ₹0.00
Purchase Ledger 0 0.000 ₹0.00 ₹0.00
Sales Ledger (-) 0 0.000 ₹0.00 ₹0.00
Stock Ledger (=) 0 0.000 ₹0.00 ₹0.00
Stock Ledger = Opening Stock + Purchase − Sales − Mortality/Wastage.
Closing Stock & Weight Loss / மீதி ஸ்டாக் & எடை குறைவு
Daily Expenses / தினசரி செலவுகள்
Available Birds0
Available Weight0.000 kg
Today's Sold Birds0
Today's Sold Weight0.000 kg
Sales Revenue₹0.00
Average Cost / kg₹0.00
Cost of Sales₹0.00
Weight Loss Cost₹0.00
Closing Birds0
Closing Weight0.000 kg
Closing Stock Cost Value₹0.00
Closing Stock Market Value₹0.00
Total Expenses₹0.00
Gross Margin₹0.00
Net Profit / Loss₹0.00
Weight Loss0.000 kg
Daily Purchase • Sales • Stock Files / தினசரி Ledger History
Open / View = அந்த தேதியின் Daily Purchase, Sales, Stock, Expense, Profit/Loss பதிவை மேலே உள்ள Daily Ledger form-ல் திறந்து பார்க்கும் வசதி. Edit = அதே பதிவை மாற்றி மீண்டும் Save செய்யும் வசதி.
DatePurchase kgSold kgClosing kgWeight Loss SalesExpensesProfit/LossBill FileView LedgerAction
Vehicle Management / வாகன மேலாண்மை 0 Vehicles
Vehicle Master / வாகன பதிவு
Vehicle IDNumberTypeDriverOdometerNext ServiceStatusSelect
Vehicle Maintenance / செலவு & மெயின்டெனன்ஸ் பதிவு
Total Vehicle Expense₹0.00
Total Fuel Expense₹0.00
Total Fuel Litres0.00 L
Last Odometer0 KM
DateVehicleActivityOdometerFuel LAmountVendorRemarks
💬 SMS Message Center / கடன் & பில் மெசேஜ் சென்டர்
Fast2SMS DLT பயன்படுத்தினால் உங்கள் DLT-approved Sender ID, Message ID மற்றும் template variable order சரியாக இருக்க வேண்டும். API Key code-ல் hard-code செய்யப்படாது; Owner Settings-ல் மட்டும் save செய்யப்படும்.
இப்போது Manual SMS Mode பயன்படுத்தலாம்.
Bill / Order / Due message தயார் ஆனதும் உங்கள் mobile SMS app திறக்கும். Message பார்த்து Send அழுத்தினால் போதும். DLT Sender ID approve ஆன பிறகு Fast2SMS Direct Mode-க்கு மாறலாம்.
Total Customers0
கடன் உள்ள Customers0
மொத்த பாக்கி₹0.00
Today SMS Log0
⚙️ Fast2SMS / Auto Billing SMS Settings
Hosting-safe build: API key HTML-ல் embed செய்யப்படவில்லை. Manual SMS உடனே பயன்படுத்தலாம்; Direct SMS-க்கு பின்னர் secure server-side setup செய்யவும்.
Fast2SMS DLT Sender ID என்பது உங்கள் DLT-approved Header. API Key அல்ல. Approved Header-ஐ மட்டும் இங்கே பயன்படுத்த வேண்டும். பொதுவாக 3–6 English letters.
📝 Message Content / மெசேஜ் கன்டென்ட்
Placeholders: {customer}, {customer_id}, {phone}, {balance}, {bill_no}, {date}, {net_weight}, {rate}, {bill_amount}, {previous_balance}, {old_payment_adjustment}, {adjusted_old_balance}, {total_due}, {current_payment}, {final_balance}, {payment_mode}. Order placeholders: {order_id}, {product}, {qty_text}, {quantity}, {unit}, {delivery_date}, {slot}, {otp}. Team placeholders: {supervisor}, {supervisor_phone}, {loadman}, {loadman_phone}.
Customer தேர்வு செய்யுங்கள்.
💰 Due Customers / கடன் பட்டியல்
CustomerMobilePlaceBalanceSend
📋 SMS Send History
Date/TimeTypeCustomerMobileStatusProviderReference
Bill Sending Settings / பில் அனுப்பும் அமைப்பு
SMS: Mobile SMS App opens the phone SMS screen with number + bill message ready; tap Send. Fast2SMS/Webhook can be used when API settings are configured. WhatsApp also opens the customer chat with the bill message ready.
💳 Payment QR Settings / பணம் செலுத்தும் QR அமைப்பு
Dynamic amount works with Auto UPI QR. Each bill QR contains that bill amount.
Uploaded image is compressed and saved on this device.
Payment QR Preview

📊 Daily Data Chart / தினசரி டேட்டா சார்ட்

வாங்கிய கோழி / Purchased Birds0
வாங்கிய எடை / Purchase Weight0.000 kg
விற்ற கோழி / Sold Birds0
விற்ற எடை / Sold Weight0.000 kg
இன்றைய Sales / விற்பனை₹0.00
வசூல் / Collection Received₹0.00
இன்றைய கடன் / Credit Added₹0.00
கடன் வசூல் / Old Due Collected₹0.00
மீதிக் கோழி / Closing Birds0
மீதி எடை / Closing Weight0.000 kg
Weight Loss / எடை குறைவு0.000 kg
Profit / Loss / லாப நட்டம்₹0.00
💰 வரவு ரசீது / Payment Receipt
Owner Data Control / ஓனர் டேட்டா கட்டுப்பாடு

💾 Full Data Backup / Restore

Customer, Staff, Bills, Orders, Ledger, Login Accounts/PIN, Staff Photo + ID Proof, Purchase, Stock, Vehicle, Branch, Settings உள்ளிட்ட SVP data அனைத்தையும் ஒரு backup file-ஆக சேமிக்கலாம்.
⬇️ Full Backup File புதிய software version update செய்வதற்கு முன் இதை அழுத்துங்கள். முழு SVP data JSON file-ஆக Downloads-ல் சேமிக்கப்படும்.
⬆️ Restore Backup File புதிய version-ல் பழைய backup JSON file-ஐ தேர்வு செய்தால் Customer/Staff/Bills/Orders உள்ளிட்ட data திரும்ப வரும்.
🛡️ Safety Snapshot Browser storage-க்கு வெளியே IndexedDB-ல் ஒரு safety copy வைக்கும். Clear/Delete செய்யும் முன் இந்த snapshot தானாக உருவாகும்.
↩️ Restore Last Safety Snapshot தவறாக Clear/Delete செய்திருந்தால் கடைசியாக உருவாக்கிய safety snapshot-க்கு திரும்பலாம்.
Last downloaded backup: Not created
Last safety snapshot: Checking...
முக்கியம்: புதிய HTML/APK update செய்வதற்கு முன் Download Full Backup எடுத்து வைத்துக்கொள்ளுங்கள். Restore செய்யும்போது தற்போதைய data-க்கு safety snapshot முதலில் தானாக உருவாக்கப்படும்.

Hosting Migration: content:// / local file-ல் உள்ள data hosting URL-க்கு தானாக வராது. பழைய version-ல் Full Backup எடுத்து, hosting version-ல் Restore Backup செய்ய வேண்டும்.
Billing / Saved Bills / Customer / Purchase / Stock / Vehicle / Branch data-ஐ Owner/Admin மட்டும் clear அல்லது delete செய்யலாம்.

Danger Zone

இந்த browser-ல் SVP software save செய்த operational data அனைத்தையும் reset செய்யும். இந்த செயலை Undo செய்ய முடியாது.

Branch Master / கிளை நிர்வாகம்
Owner/Admin மட்டும் Add, Edit, Update, Delete செய்ய முடியும்.
Branch Stock Transfer / கிளைகளுக்கிடையிலான ஸ்டாக் மாற்றம்
🏬 Own Shop Daily Stock / சொந்த கறிக்கடை தினசரி ஸ்டாக்
Branch Master-ல் உங்கள் சொந்த கடைகளை முதலில் பதிவு செய்யுங்கள். இந்த module Opening Stock + Transfer In + Customer Return − Transfer Out − Billing Sales − Manual Counter Sales − Wastage − Return to Main என்ற முறையில் Closing Stock-ஐ தானாக கணக்கிடும். Billing-ல் Selling Shop தேர்வு செய்தால் அந்த கடையின் விற்பனை இங்கே Auto Sales-ஆக வரும்.
Opening Birds / Qty0
Opening Weight0.000 kg
Transfer In0 / 0.000 kg
Auto Billing Sales0 / 0.000 kg
Customer Return Auto0 / 0.000 kg
Transfer Out0 / 0.000 kg
Expected Closing0 / 0.000 kg
Physical Variance0 / 0.000 kg
Auto Stock Inputs / தானாக வரும் கணக்குகள்
Manual Counter Update / கடையில் கைமுறை பதிவு
Closing Stock / நாள் முடிவு ஸ்டாக்
Current Shop Stock Dashboard / தற்போதைய கடை ஸ்டாக்
Daily Shop Stock History / தினசரி வரலாறு
DateShopProductOpening kgIn kgAuto Sales kgManual Sales kgReturn In kgOut kgWastage kgReturn Main kgExpected kgActual kgVariance kgSales ₹Action
All Branch Report / அனைத்து கிளை அறிக்கை

Sales Billing / விற்பனை பில்

Staff Billing
⏱️ Live Duty Time / வேலை நேரம்
Staff
ON DUTY
Current Time 00:00:00
Check-In --:--:--
Working Duration 00:00:00
Logout செய்யும் வரை timer தொடர்ந்து ஓடும். Logout செய்தவுடன் Check-Out + Total Work Time save ஆகும்.

📦 Today Delivery Orders / இன்றைய டெலிவரி ஆர்டர்கள்

இன்றைய தேதி delivery orders மட்டும்
0 Orders
Orders0
Boxes0
KG0.000
Birds0
Net Weight / நிகர எடை0.000 kg
Average / Bird0.000 kg
Today Bill / இன்றைய பில்₹0.00

ஸ்ரீ வெங்கடா ஜலபதி பவுல்ட்ரி

SRI VENKATA JALAPATHY POULTRY
GSTIN: 33DWJPP8310N1Z2
OLD NO.329/1, NEW NO.28/3, TVN MAIN ROAD, KALAVANUR, ULUNDURPETTAI, KALLAKURICHI - 606102
உரிமையாளர் / Proprietor: P. சுகந்தி பிரபாகரன்   |   கடை இடம் / Store Location: Kalavanur, Ulundurpettai
Mobile: 8610722004   |   9159633300   |   Landline: 04149-293301   |   GPay: 6379532554
Customer Billing / கஸ்டமர் பில்லிங்
Role
Direct Customer Billing • Single Store Billing
Bill #1
தேதி / Date: --
நேரம் / Time: --
Generated: --
மொத்தம்: 1 கூண்டு • 10 கோழி • நிகர எடை 22.000 kg
📷 Weight Photo Proof / எடை புகைப்பட சான்று
Photo not captured
Photo not captured
Net Weight = Gross Weight − Empty Cage Weight. Photo proof is linked to the saved Bill No.
நிகர எடை / Net Weight22.000 kg
Average Weight / Bird2.200 kg
இன்றைய பில் தொகை: ₹2420.00
Customer Account / வாடிக்கையாளர் கணக்கு
ஃபைனல் மீதி பாக்கி / Final Remaining Balance₹0.00
Bill Staff Assignment / பணியாளர் தேர்வு
வாங்குபவர் கையெழுத்து
Buyer / Customer Signature
Supervisor கையெழுத்து
Supervisor Signature
Driver கையெழுத்து
Driver Signature
📅 இன்றைய பில்கள் / Today's Bills
இந்த login-ல் அன்றைய தேதி records மட்டும் காட்டப்படும்.
Saved Bills / Sales
Sales Bill Correction / Return / விற்பனை பில் திருத்தம் & ரிட்டன்
Correction Details
`; const blob=new Blob([html],{type:'text/html;charset=utf-8'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=`SVP_Bill_${b.no}_${String(b.date||'').replace(/[^\d-]/g,'')}.html`; document.body.appendChild(a);a.click();a.remove(); setTimeout(()=>URL.revokeObjectURL(url),1200); } function customerPurchaseHistoryHtml(){ const cid=customerCurrentId(); const c=getCustomers().find(x=>String(x.id)===String(cid)); const bills=customerOwnBills(); const totalBirds=bills.reduce((a,b)=>a+Number(b.birds||0),0); const totalKg=bills.reduce((a,b)=>a+Number(b.net||0),0); const totalAmount=bills.reduce((a,b)=>a+Number(b.amount||0),0); const rows=bills.map((b,i)=>` ${i+1} #${escapeHtml(b.no||'')} ${escapeHtml(b.generatedAt||b.date||'')} ${escapeHtml(b.product||'')} ${Number(b.birds||0)} ${Number(b.totalCages||0)} ${Number(b.net||0).toFixed(3)} ₹${money(b.rate||0)} ₹${money(b.amount||0)} `).join(''); return `

Customer Purchase History / கோழி வாங்கிய வரலாறு

Customer ID: ${escapeHtml(c?.id||'')}
Name: ${escapeHtml(c?.name||'')}
Mobile: ${escapeHtml(c?.phone||'')}
Place: ${escapeHtml(c?.place||'-')}
Shop: ${escapeHtml(c?.shop||'-')}
Current Balance: ₹${money(c?.currentBalance||0)}
${rows||''}
#BillDateProductBirdsBoxes/CagesNet KGRateAmount
Purchase history இல்லை
Total Bills${bills.length}
Total Birds${totalBirds}
Total Net KG${totalKg.toFixed(3)}
Total Purchase₹${money(totalAmount)}
`; } function customerPrintPurchaseHistory(){ if(!customerCurrentId()){alert('Customer login இல்லை');return} customerPrintDocument(customerPurchaseHistoryHtml()); } function customerDownloadPurchaseHistoryCsv(){ const cid=customerCurrentId(); if(!cid){alert('Customer login இல்லை');return} const c=getCustomers().find(x=>String(x.id)===String(cid)); const bills=customerOwnBills(); if(!bills.length){alert('Purchase history இல்லை');return} const cell=v=>'"'+String(v??'').replace(/"/g,'""')+'"'; const rows=[ ['Customer ID',c?.id||''], ['Customer Name',c?.name||''], ['Mobile',c?.phone||''], [], ['Bill No','Date','Product','Birds','Boxes/Cages','Net KG','Rate','Amount','Paid','Balance'] ]; bills.forEach(b=>rows.push([ b.no,b.generatedAt||b.date||'',b.product||'',Number(b.birds||0),Number(b.totalCages||0), Number(b.net||0).toFixed(3),Number(b.rate||0).toFixed(2),Number(b.amount||0).toFixed(2), Number(b.receivedAmount||0).toFixed(2),Number(b.remainingBalance||0).toFixed(2) ])); const csv='\ufeff'+rows.map(r=>r.map(cell).join(',')).join('\r\n'); const blob=new Blob([csv],{type:'text/csv;charset=utf-8'}); const url=URL.createObjectURL(blob),a=document.createElement('a'); a.href=url;a.download=`SVP_Customer_${cid}_Purchase_History.csv`; document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1200); } // V42: Owner/Admin module navigation const ADMIN_MODULE_ORDER=[ 'customerBilling','orders','customer','staff','loginAccounts','loginActivity','attendance','customerSupervisor', 'purchase','sales','daily','vehicle','receipt','smsCenter','settings', 'dataControl','branch','transfer','shopStock','consolidated' ]; const ADMIN_MODULE_LABELS={ customerBilling:'🧾 Customer Billing / கஸ்டமர் பில்லிங்', orders:'📦 Order Booking / ஆர்டர் புக்கிங்', customer:'👥 Customer Admin', staff:'👷 Staff Admin / Employee Master', loginAccounts:'🔐 Login Accounts / Username & PIN', loginActivity:'🕒 Login Activity / Status', attendance:'⏱️ Staff Time / Attendance', customerSupervisor:'🔗 Customer ↔ Supervisor Set', purchase:'🐔 Purchase Admin', sales:'📊 Sales / Stock', daily:'📅 Daily Data Chart', vehicle:'🚚 Vehicle Admin', receipt:'💰 Receipt / வரவு ரசீது', smsCenter:'💬 SMS Center', settings:'⚙️ Settings', dataControl:'🛡️ Data Control', branch:'🏪 Branch Master', transfer:'🔄 Stock Transfer', shopStock:'🏬 Own Shop Stock', consolidated:'📈 All Branch Report' }; function updateAdminModuleNav(name){ const nav=document.getElementById('adminModuleNav'); const title=document.getElementById('adminModuleNavTitle'); const sel=document.getElementById('adminModuleQuickSelect'); if(nav) nav.style.display=''; if(title) title.textContent=ADMIN_MODULE_LABELS[name]||'Admin Module'; if(sel) sel.value=name||''; } function switchAdminModule(name){ if(!name) return; if(name==='customerBilling'){ openCustomerBilling(); return; } showAdminModule(name); } function nextAdminModule(){ let i=ADMIN_MODULE_ORDER.indexOf(activeAdminModule); if(i<0) i=-1; const next=ADMIN_MODULE_ORDER[(i+1)%ADMIN_MODULE_ORDER.length]; switchAdminModule(next); } function backToAdminMenu(scrollTop=true){ if(!isOwner()) return; // V43: reset old hidden-state classes so the next module opens normally. document.querySelectorAll('.admin-module,#customerBillingSection').forEach(el=>{ el.classList.remove('v11-hidden-module','v12-workspace-hidden'); }); // V44: Admin home shows ONLY the menu. Content appears only after a menu button is touched. document.body.classList.add('owner-admin-home'); document.body.classList.remove('admin-single-module'); document.body.classList.remove('customer-billing-mode'); document.querySelectorAll('.admin-module').forEach(el=>el.classList.remove('active')); const nav=document.getElementById('adminModuleNav'); if(nav) nav.style.display='none'; const billingNav=document.getElementById('customerBillingNav'); if(billingNav) billingNav.classList.add('hidden'); activeAdminModule=''; const menu=document.getElementById('adminMenuSection'); if(menu){ menu.style.display=''; if(scrollTop) setTimeout(()=>menu.scrollIntoView({behavior:'smooth',block:'start'}),60); } } // V19: Dedicated Customer Billing direct mode. function openCustomerBilling(){ if(!isOwner()) return; activeAdminModule='customerBilling'; // V44: Billing was selected; leave menu-only home. document.body.classList.remove('owner-admin-home'); // V43: old workspace engines must not keep Billing hidden. const billingSection=document.getElementById('customerBillingSection'); if(billingSection) billingSection.classList.remove('v11-hidden-module','v12-workspace-hidden'); document.body.classList.remove('admin-single-module'); document.body.classList.add('customer-billing-mode'); const moduleNav=document.getElementById('adminModuleNav'); if(moduleNav) moduleNav.style.display='none'; document.querySelectorAll('.admin-module').forEach(el=>el.classList.remove('active')); const nav=document.getElementById('customerBillingNav'); if(nav) nav.classList.remove('hidden'); loadCustomerOptions(); loadBillingStaffOptions(); updateBillNo(); updateClock(); calc(); const section=document.getElementById('customerBillingSection'); if(section) setTimeout(()=>section.scrollIntoView({behavior:'smooth',block:'start'}),60); const picker=document.getElementById('billingCustomerSelect'); if(picker) setTimeout(()=>picker.focus(),250); } function closeCustomerBilling(){ backToAdminMenu(true); } let activeAdminModule=''; function showAdminModule(name){ if(!isOwner()) return; // V44: a module was selected; leave menu-only home and show only this module. document.body.classList.remove('owner-admin-home'); // V43: clear legacy V11/V12 hide classes before the current module is shown. document.querySelectorAll('.admin-module').forEach(el=>{ el.classList.remove('v11-hidden-module','v12-workspace-hidden'); }); document.body.classList.add('admin-single-module'); document.body.classList.remove('customer-billing-mode'); const billingNav=document.getElementById('customerBillingNav'); if(billingNav) billingNav.classList.add('hidden'); activeAdminModule=name; document.querySelectorAll('.admin-module').forEach(el=>{ el.classList.toggle('active',el.dataset.module===name); }); updateAdminModuleNav(name); if(name==='orders'){ orderInit(); }else if(name==='purchase'){ renderPurchaseParties(); loadPurchasePartyOptions(); initDailyLedger(); }else if(name==='staff'){ renderStaffTable(); staffToggleLoginFields(); }else if(name==='loginAccounts'){ initLoginAccountManager(); }else if(name==='loginActivity'){ if(typeof svpInitLoginActivity==='function')svpInitLoginActivity(); }else if(name==='attendance'){ if(typeof svpInitAttendance==='function')svpInitAttendance(); }else if(name==='customerSupervisor'){ csaInit(); }else if(name==='customer'){ loadCustomerOptions(); }else if(name==='sales'){ renderBills(); renderDailyHistory(); loadSalesCorrectionOptions(); }else if(name==='daily'){ if(document.getElementById('dailyChartDate') && !document.getElementById('dailyChartDate').value) document.getElementById('dailyChartDate').value=todayIso(); if(typeof renderDailyDataChart==='function') renderDailyDataChart(); setTimeout(()=>{ if(typeof v14Render==='function') v14Render(); },80); }else if(name==='vehicle'){ initVehicleModule(); }else if(name==='settings'){ loadSendSettings(); }else if(name==='smsCenter'){ initSmsCenter(); }else if(name==='receipt'){ rcptInit(); }else if(name==='dataControl'){ // Owner-only data management panel. if(typeof svpUpdateBackupStatus==='function')svpUpdateBackupStatus(); }else if(name==='branch'){ mbRenderBranches(); }else if(name==='transfer'){ mbRenderTransfers(); if(typeof mbPopulateBranchHelpers==='function') mbPopulateBranchHelpers(); }else if(name==='shopStock'){ if(typeof svpInitShopStock==='function') svpInitShopStock(); }else if(name==='consolidated'){ mbRenderConsolidated(); } // Stay here until Owner taps Back, Next, or selects another module. const sec=document.querySelector('.admin-module[data-module="'+name+'"]'); if(sec) setTimeout(()=>sec.scrollIntoView({behavior:'smooth',block:'start'}),70); if(name==='attendance'){ setTimeout(()=>{if(typeof svpRenderAttendance==='function')svpRenderAttendance();},100); } } // V47: Daily Order Booking + OTP function getOrders(){try{return JSON.parse(localStorage.getItem('svp_orders')||'[]')}catch(e){return[]}} function setOrders(v){localStorage.setItem('svp_orders',JSON.stringify(v))} function orderIsoDate(d=new Date()){ const p=n=>String(n).padStart(2,'0'); return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`; } function orderDateTime(){ const d=new Date(),p=n=>String(n).padStart(2,'0'); return `${orderIsoDate(d)} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`; } function orderUnitLabel(mode){ return mode==='kg'?'KG':mode==='birds'?'Birds':'Boxes'; } function orderQtyText(o={}){ return `${Number(o.qty||0)} ${orderUnitLabel(o.qtyMode)}`; } function orderNextId(){ const date=orderIsoDate().replace(/-/g,''); let max=0; getOrders().forEach(o=>{ const m=String(o.id||'').match(/^ORD-(\d{8})-(\d+)$/); if(m&&m[1]===date)max=Math.max(max,Number(m[2]||0)); }); return `ORD-${date}-${String(max+1).padStart(3,'0')}`; } function orderGenerateOtp(){ let n=1000+Math.floor(Math.random()*9000); try{const a=new Uint32Array(1);crypto.getRandomValues(a);n=1000+(a[0]%9000)}catch(e){} const el=document.getElementById('orderOtp');if(el)el.value=String(n); return String(n); } function orderQtyModeChanged(){ const mode=document.getElementById('orderQtyMode')?.value||'boxes'; const l=document.getElementById('orderQtyLabel'); const q=document.getElementById('orderQty'); if(l)l.textContent=mode==='kg'?'KG / கிலோ':mode==='birds'?'Bird Count / கோழி எண்ணிக்கை':'Boxes / பாக்ஸ்'; if(q)q.step=mode==='kg'?'0.001':'1'; } function orderPopulateDeliveryStaff(){ const staff=getStaff().filter(x=>(x.status||'active')==='active'); const sup=document.getElementById('orderSupervisorSelect'); const load=document.getElementById('orderLoadmanSelect'); const curSup=sup?.value||'',curLoad=load?.value||''; if(sup){ sup.innerHTML=''+ staff.filter(x=>x.role==='supervisor').map(x=>``).join(''); if(staff.some(x=>x.id===curSup))sup.value=curSup; } if(load){ load.innerHTML=''+ staff.filter(x=>x.role==='loadman').map(x=>``).join(''); if(staff.some(x=>x.id===curLoad))load.value=curLoad; } orderUpdateTeamInfo(); } function orderUpdateTeamInfo(){ const staff=getStaff(); const sid=document.getElementById('orderSupervisorSelect')?.value||''; const lid=document.getElementById('orderLoadmanSelect')?.value||''; const sn=staff.find(x=>x.id===sid)?.name||''; const ln=staff.find(x=>x.id===lid)?.name||''; const e=document.getElementById('orderTeamInfo'); if(e)e.value=[sn?('Supervisor: '+sn):'',ln?('Loadman: '+ln):''].filter(Boolean).join(' • '); } function orderPopulateCustomers(){ const sel=document.getElementById('orderCustomerSelect'); if(!sel)return; const cur=sel.value; const list=getCustomers(); sel.innerHTML=''+list.map(c=>``).join(''); if(list.some(c=>c.id===cur))sel.value=cur; } function orderCustomerChanged(){ const id=document.getElementById('orderCustomerSelect')?.value||''; const c=getCustomers().find(x=>x.id===id); const p=document.getElementById('orderPhone');if(p)p.value=c?.phone||''; const sup=document.getElementById('orderSupervisorSelect'); const load=document.getElementById('orderLoadmanSelect'); if(sup && c?.assignedSupervisorId) sup.value=c.assignedSupervisorId; if(load && c?.assignedLoadmanId) load.value=c.assignedLoadmanId; orderUpdateTeamInfo(); } function orderNew(){ const set=(id,v)=>{const e=document.getElementById(id);if(e)e.value=v??''}; set('orderEditingId',''); set('orderCustomerSelect',''); set('orderPhone',''); set('orderProduct','Broiler Chicken'); set('orderQtyMode','boxes'); set('orderQty',''); set('orderSlot','Morning'); set('orderDeliveryDate',orderIsoDate()); set('orderAutoSms','on'); set('orderSupervisorSelect',''); set('orderLoadmanSelect',''); set('orderTeamInfo',''); set('orderNote',''); orderGenerateOtp(); orderQtyModeChanged(); const m=document.getElementById('orderMsg');if(m)m.textContent=''; } function orderInit(){ orderPopulateCustomers(); orderPopulateDeliveryStaff(); const hd=document.getElementById('orderHistoryDate'); if(hd&&!hd.value)hd.value=orderIsoDate(); const dd=document.getElementById('orderDeliveryDate'); if(dd&&!dd.value)dd.value=orderIsoDate(); if(!document.getElementById('orderOtp')?.value)orderGenerateOtp(); orderQtyModeChanged(); orderRenderStats(); orderRenderHistory(); } function orderBuildFromForm(){ const cid=document.getElementById('orderCustomerSelect')?.value||''; const c=getCustomers().find(x=>x.id===cid); if(!c)throw new Error('Customer தேர்வு செய்யுங்கள்'); const qtyMode=document.getElementById('orderQtyMode')?.value||'boxes'; const qty=Number(document.getElementById('orderQty')?.value||0); if(!(qty>0))throw new Error('Order quantity உள்ளிடுங்கள்'); const otp=(document.getElementById('orderOtp')?.value||'').trim()||orderGenerateOtp(); const editing=document.getElementById('orderEditingId')?.value||''; const old=getOrders().find(x=>x.id===editing)||{}; return { ...old, id:editing||orderNextId(), customerId:c.id, customerName:c.name, phone:c.phone||'', product:document.getElementById('orderProduct')?.value||'Broiler Chicken', qtyMode,qty, deliveryDate:document.getElementById('orderDeliveryDate')?.value||orderIsoDate(), slot:document.getElementById('orderSlot')?.value||'Morning', deliverySupervisorId:document.getElementById('orderSupervisorSelect')?.value||'', deliverySupervisorName:getStaff().find(x=>x.id===(document.getElementById('orderSupervisorSelect')?.value||''))?.name||'', loadmanId:document.getElementById('orderLoadmanSelect')?.value||'', loadmanName:getStaff().find(x=>x.id===(document.getElementById('orderLoadmanSelect')?.value||''))?.name||'', otp, note:document.getElementById('orderNote')?.value.trim()||'', status:old.status||'Booked', bookedAt:old.bookedAt||orderDateTime(), updatedAt:orderDateTime(), createdById:old.createdById||localStorage.getItem('svp_staff_id')||'', createdByName:old.createdByName||currentUserName||'', smsStatus:old.smsStatus||'Not sent' }; } function orderUpsert(o){ const list=getOrders(),i=list.findIndex(x=>x.id===o.id); if(i>=0)list[i]=o;else list.unshift(o); setOrders(list); } function orderStaffVisibleRows(){ const today=orderIsoDate(); const myId=localStorage.getItem('svp_staff_id')||''; const role=currentRole||''; const customers=getCustomers(); // V54: Staff-side delivery screen is strictly TODAY ONLY. // Yesterday/future orders never appear here. They remain saved in Admin Order History. return getOrders().filter(o=>{ const deliveryDate=String(o.deliveryDate||'').slice(0,10); if(deliveryDate!==today) return false; if(o.status==='Cancelled' || o.status==='Delivered') return false; if(role==='supervisor'){ if(o.deliverySupervisorId) return String(o.deliverySupervisorId)===String(myId); const c=customers.find(x=>x.id===o.customerId); return String(c?.assignedSupervisorId||'')===String(myId); } if(role==='loadman'){ return !o.loadmanId || String(o.loadmanId)===String(myId); } if(role==='driver'){ return true; } return false; }); } function renderStaffDeliveryOrders(){ const panel=document.getElementById('staffDeliveryOrdersPanel'); const host=document.getElementById('staffDeliveryOrdersList'); if(!panel||!host)return; const role=currentRole||''; const allowed=['supervisor','loadman','driver'].includes(role); panel.style.display=allowed?'':'none'; if(!allowed)return; const rows=orderStaffVisibleRows(); const sum=m=>rows.filter(o=>o.qtyMode===m).reduce((a,o)=>a+Number(o.qty||0),0); const t=(id,v)=>{const e=document.getElementById(id);if(e)e.textContent=v}; t('staffOrderCount',rows.length+' Orders'); t('staffOrderTotal',rows.length); t('staffOrderBoxes',sum('boxes')); t('staffOrderKg',sum('kg').toFixed(3)); t('staffOrderBirds',sum('birds')); const rt=document.getElementById('staffOrderRoleText'); if(rt)rt.textContent=role==='supervisor' ?'இன்றைய தேதி உங்களுக்கு assign செய்யப்பட்ட Customer / Delivery orders மட்டும்' :role==='loadman' ?'இன்றைய தேதி vehicle load orders மட்டும்' :'இன்றைய தேதி delivery orders மட்டும்'; if(!rows.length){host.innerHTML='
இன்று உங்களுக்கு delivery order இல்லை.
';return} const customers=getCustomers(); host.innerHTML=rows.map(o=>{ const c=customers.find(x=>x.id===o.customerId)||{}; return `
${escapeHtml(o.customerName||c.name||'Customer')}
${escapeHtml(c.shop||c.place||'-')} • ${escapeHtml(o.phone||c.phone||'')}
${escapeHtml(o.id||'')}
${escapeHtml(o.status||'Booked')}
Order${escapeHtml(orderQtyText(o))}
Product${escapeHtml(o.product||'')}
Time${escapeHtml(o.slot||'')}
Supervisor${escapeHtml(o.deliverySupervisorName||'-')}
Loadman${escapeHtml(o.loadmanName||'-')}
OTPCustomer-ிடம் பெறவும்
${o.note?`
Note: ${escapeHtml(o.note)}
`:''}
${role==='supervisor'?``:''}
`; }).join(''); } function staffOrderCanAccess(o){ return orderStaffVisibleRows().some(x=>x.id===o.id); } function staffOrderSetStatus(id,status){ const list=getOrders(),i=list.findIndex(x=>x.id===id);if(i<0)return; if(!staffOrderCanAccess(list[i])){alert('இந்த order உங்களுக்கு assign செய்யப்படவில்லை');return} list[i]={...list[i],status,updatedAt:orderDateTime()}; setOrders(list); renderStaffDeliveryOrders(); } function staffOrderVerifyOtp(id){ const list=getOrders(),i=list.findIndex(x=>x.id===id);if(i<0)return; const o=list[i]; if(!staffOrderCanAccess(o)){alert('இந்த order உங்களுக்கு assign செய்யப்படவில்லை');return} const entered=prompt('Customer OTP உள்ளிடுங்கள் / Enter delivery OTP:'); if(entered===null)return; if(String(entered).trim()!==String(o.otp||'')){alert('OTP தவறு');return} list[i]={...o,status:'Delivered',updatedAt:orderDateTime(),deliveredById:localStorage.getItem('svp_staff_id')||'',deliveredByName:currentUserName||''}; setOrders(list); renderStaffDeliveryOrders(); alert('OTP verified. Delivery completed.'); } function staffUseOrderForBilling(id){ const o=getOrders().find(x=>x.id===id);if(!o)return; if(!staffOrderCanAccess(o)){alert('இந்த order உங்களுக்கு assign செய்யப்படவில்லை');return} const compact=document.getElementById('staffCompactCustomer'); const orig=document.getElementById('billingCustomerSelect'); if(orig){orig.value=o.customerId;if(typeof selectBillingCustomer==='function')selectBillingCustomer()} if(compact)compact.value=o.customerId; if(o.product){ const prod=document.getElementById('product'); const compactProd=document.getElementById('staffCompactProduct'); if(prod){ [...prod.options].forEach(op=>{if(String(op.textContent||'').toLowerCase().includes(String(o.product||'').toLowerCase().split(' ')[0]))prod.value=op.value}); } if(compactProd&&prod)compactProd.value=prod.value; } if(o.qtyMode==='birds'){ const birds=document.getElementById('birds');if(birds)birds.value=o.qty; const cb=document.getElementById('staffCompactBirds');if(cb)cb.value=o.qty; } if(typeof calc==='function')calc(); document.getElementById('staffCompactCustomer')?.scrollIntoView({behavior:'smooth',block:'center'}); } document.addEventListener('change',e=>{ if(['orderSupervisorSelect','orderLoadmanSelect'].includes(e.target?.id||''))orderUpdateTeamInfo(); }); function orderMessageContext(o={}){ return { customer:o.customerName||'',customer_id:o.customerId||'',phone:o.phone||'', order_id:o.id||'',product:o.product||'',qty_text:orderQtyText(o), quantity:String(o.qty||''),unit:orderUnitLabel(o.qtyMode), delivery_date:o.deliveryDate||'',slot:o.slot||'',otp:o.otp||'', date:o.bookedAt||orderDateTime() }; } function buildOrderMessage(o){ const tpl=smsSettings().orderTemplate||SVP_SMS_DEFAULTS.orderTemplate; return renderSmsTokens(tpl,orderMessageContext(o)); } async function orderSendOtpSms(o){ try{ const r=await sendSmsTo(o.phone,buildOrderMessage(o),{type:'order',order:o,ref:o.id}); o.smsStatus=String(r?.mode||'sent'); o.smsSentAt=orderDateTime(); orderUpsert(o); smsLogAdd('order',o.customerId,o.customerName,o.phone,'sent',r?.mode||'sent',smsResponseRef(r)); return r; }catch(e){ o.smsStatus='Failed: '+e.message; orderUpsert(o); smsLogAdd('order',o.customerId,o.customerName,o.phone,'failed','order',e.message); throw e; } } async function orderSave(sendSms=false){ if(!ownerOnly())return; let o; try{o=orderBuildFromForm()}catch(e){alert(e.message);return} orderUpsert(o); document.getElementById('orderEditingId').value=o.id; let msg='✓ Order saved: '+o.id+' • '+orderQtyText(o); if(sendSms || document.getElementById('orderAutoSms')?.value==='on'){ try{ const r=await orderSendOtpSms(o); msg += String(r?.mode||'').startsWith('device') ? ' • SMS app opened' : ' • OTP SMS sent'; }catch(e){ msg += ' • SMS error: '+e.message; } } const m=document.getElementById('orderMsg');if(m)m.textContent=msg; orderRenderStats();orderRenderHistory(); renderBillingOrderSummary(document.getElementById('billingCustomerSelect')?.value||''); if(typeof renderStaffDeliveryOrders==='function')renderStaffDeliveryOrders(); } function orderSaveFromForm(){orderSave(false)} function orderSaveAndSend(){orderSave(true)} function orderEdit(id){ const o=getOrders().find(x=>x.id===id);if(!o)return; const set=(id,v)=>{const e=document.getElementById(id);if(e)e.value=v??''}; set('orderEditingId',o.id);set('orderCustomerSelect',o.customerId);set('orderPhone',o.phone); set('orderProduct',o.product);set('orderQtyMode',o.qtyMode);set('orderQty',o.qty); set('orderSlot',o.slot);set('orderDeliveryDate',o.deliveryDate);set('orderOtp',o.otp); set('orderSupervisorSelect',o.deliverySupervisorId||'');set('orderLoadmanSelect',o.loadmanId||'');set('orderNote',o.note||''); orderUpdateTeamInfo(); orderQtyModeChanged(); document.getElementById('adminOrdersSection')?.scrollIntoView({behavior:'smooth',block:'start'}); } function orderSetStatus(id,status){ if(!ownerOnly())return; const list=getOrders(),i=list.findIndex(x=>x.id===id);if(i<0)return; list[i]={...list[i],status,updatedAt:orderDateTime()}; setOrders(list);orderRenderStats();orderRenderHistory(); renderBillingOrderSummary(document.getElementById('billingCustomerSelect')?.value||''); if(typeof renderStaffDeliveryOrders==='function')renderStaffDeliveryOrders(); } function orderVerifyDelivery(id){ if(!ownerOnly())return; const o=getOrders().find(x=>x.id===id);if(!o)return; const entered=prompt('Customer OTP உள்ளிடுங்கள் / Enter delivery OTP:'); if(entered===null)return; if(String(entered).trim()!==String(o.otp||'')){alert('OTP தவறு');return} orderSetStatus(id,'Delivered');alert('OTP verified. Order Delivered.'); } function orderDelete(id){ if(!ownerOnly())return; // V54: Only Owner/Admin can permanently delete an order from history. if(!confirm(id+' order-ஐ Admin History-லிருந்து நிரந்தரமாக delete செய்யவா?'))return; setOrders(getOrders().filter(x=>x.id!==id)); orderRenderStats();orderRenderHistory(); } function orderRenderStats(){ const today=orderIsoDate(); // V53: Owner/Admin summary must match Supervisor pending-delivery summary. // Delivered and Cancelled orders are history only and are not counted in active totals. const rows=getOrders().filter(o=> o.deliveryDate===today && o.status!=='Cancelled' && o.status!=='Delivered' ); const sum=m=>rows.filter(o=>o.qtyMode===m).reduce((a,o)=>a+Number(o.qty||0),0); const t=(id,v)=>{const e=document.getElementById(id);if(e)e.textContent=v}; t('orderStatCount',rows.length);t('orderStatBoxes',sum('boxes'));t('orderStatKg',sum('kg').toFixed(3));t('orderStatBirds',sum('birds')); } function orderRenderHistory(){ const host=document.getElementById('orderHistoryList');if(!host)return; const d=document.getElementById('orderHistoryDate')?.value||orderIsoDate(); const q=(document.getElementById('orderSearch')?.value||'').trim().toLowerCase(); let rows=getOrders().filter(o=>!d||o.deliveryDate===d); if(q)rows=rows.filter(o=>[o.id,o.customerId,o.customerName,o.phone,o.product,o.note].some(v=>String(v||'').toLowerCase().includes(q))); if(!rows.length){host.innerHTML='
இந்த தேதிக்கு order இல்லை.
';return} host.innerHTML=rows.map(o=>`
${escapeHtml(o.customerName||'')}
${escapeHtml(o.id)} • ${escapeHtml(o.phone||'')}
${escapeHtml(o.status||'Booked')}
Order${escapeHtml(orderQtyText(o))}
Product${escapeHtml(o.product||'')}
Delivery${escapeHtml(o.deliveryDate||'')} • ${escapeHtml(o.slot||'')}
OTP${escapeHtml(o.otp||'')}
SMS${escapeHtml(o.smsStatus||'Not sent')}
Booked By${escapeHtml(o.createdByName||'-')}
Supervisor${escapeHtml(o.deliverySupervisorName||'-')}
Loadman${escapeHtml(o.loadmanName||'-')}
${o.note?`
Note: ${escapeHtml(o.note)}
`:''}
`).join(''); } function renderBillingOrderSummary(customerId){ const box=document.getElementById('billingOrderSummary');if(!box)return; if(!customerId){box.classList.add('hidden');box.innerHTML='';return} const today=orderIsoDate(); const rows=getOrders().filter(o=> o.customerId===customerId && o.deliveryDate===today && o.status!=='Cancelled' && o.status!=='Delivered' ); box.classList.remove('hidden'); if(!rows.length){ box.innerHTML='

📦 இன்றைய Order / Today Booking

இந்த customer-க்கு இன்று booking இல்லை.
'; return; } const sum=m=>rows.filter(o=>o.qtyMode===m).reduce((a,o)=>a+Number(o.qty||0),0); box.innerHTML=`

📦 இன்றைய Order / Today Booking

`+ rows.map(o=>`
${escapeHtml(orderQtyText(o))} • ${escapeHtml(o.product||'')} • ${escapeHtml(o.slot||'')} • ${escapeHtml(o.status||'Booked')} (${escapeHtml(o.id)})
`).join('')+ `
${sum('boxes')?`Boxes: ${sum('boxes')}`:''} ${sum('kg')?`KG: ${sum('kg').toFixed(3)}`:''} ${sum('birds')?`Birds: ${sum('birds')}`:''}
`; } // V50: Full Backup / Restore const SVP_BACKUP_META_KEY='svp_backup_meta_v50'; const SVP_BACKUP_DB='SVP_Poultry_Safety_Backup'; const SVP_BACKUP_STORE='snapshots'; const SVP_SESSION_KEYS=new Set([ 'svp_logged','svp_role','svp_user_name','svp_login_phone','svp_staff_id', 'svp_customer_logged','svp_customer_id', 'svp_active_login_session' ]); function svpNowStamp(){ const d=new Date(),p=n=>String(n).padStart(2,'0'); return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`; } function svpSafeFileStamp(){ const d=new Date(),p=n=>String(n).padStart(2,'0'); return `${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`; } function svpCollectAllData(){ const data={}; for(let i=0;i{ if(!window.indexedDB){reject(new Error('IndexedDB not supported'));return} const req=indexedDB.open(SVP_BACKUP_DB,1); req.onupgradeneeded=()=>{ const db=req.result; if(!db.objectStoreNames.contains(SVP_BACKUP_STORE))db.createObjectStore(SVP_BACKUP_STORE); }; req.onsuccess=()=>resolve(req.result); req.onerror=()=>reject(req.error||new Error('Backup DB open failed')); }); } async function svpIdbPut(key,value){ const db=await svpOpenBackupDb(); return new Promise((resolve,reject)=>{ const tx=db.transaction(SVP_BACKUP_STORE,'readwrite'); tx.objectStore(SVP_BACKUP_STORE).put(value,key); tx.oncomplete=()=>{db.close();resolve(true)}; tx.onerror=()=>{const e=tx.error;db.close();reject(e||new Error('Snapshot save failed'))}; }); } async function svpIdbGet(key){ const db=await svpOpenBackupDb(); return new Promise((resolve,reject)=>{ const tx=db.transaction(SVP_BACKUP_STORE,'readonly'); const req=tx.objectStore(SVP_BACKUP_STORE).get(key); req.onsuccess=()=>{const v=req.result;db.close();resolve(v)}; req.onerror=()=>{const e=req.error;db.close();reject(e||new Error('Snapshot read failed'))}; }); } async function svpCreateSafetySnapshot(showMsg=false){ try{ const payload=svpCollectAllData(); await svpIdbPut('latest',payload); const meta=svpGetBackupMeta(); meta.lastSnapshot=payload.createdAt; svpSetBackupMeta(meta); await svpUpdateBackupStatus(); if(showMsg)svpBackupMsg('✓ Safety snapshot created: '+payload.createdAt); return true; }catch(e){ if(showMsg)svpBackupMsg('Snapshot failed: '+e.message,true); return false; } } async function svpUpdateBackupStatus(){ const meta=svpGetBackupMeta(); const b=document.getElementById('svpLastBackupText'); const sn=document.getElementById('svpLastSnapshotText'); if(b)b.textContent=meta.lastDownloadedBackup||'Not created'; let snapText=meta.lastSnapshot||'Not created'; try{ const snap=await svpIdbGet('latest'); if(snap?.createdAt)snapText=snap.createdAt; }catch(e){} if(sn)sn.textContent=snapText; } async function svpDownloadFullBackup(){ if(!ownerOnly())return; try{ const payload=svpCollectAllData(); await svpCreateSafetySnapshot(false); const json=JSON.stringify(payload,null,2); const filename='SVP_Poultry_Full_Backup_'+svpSafeFileStamp()+'.json'; // V64: Android APK WebView cannot reliably download blob: URLs. // Use native Android bridge when available; normal browser keeps Blob download. if(window.AndroidFiles && typeof window.AndroidFiles.saveText==='function'){ const result=String(window.AndroidFiles.saveText(filename,json)||''); if(!result.startsWith('OK:'))throw new Error(result||'Android backup save failed'); }else{ const blob=new Blob([json],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(url),1500); } const meta=svpGetBackupMeta(); meta.lastDownloadedBackup=payload.createdAt; svpSetBackupMeta(meta); await svpUpdateBackupStatus(); svpBackupMsg('✓ Full backup saved. Android app: Downloads/SVP Poultry folder-ல் பார்க்கவும்.'); }catch(e){ svpBackupMsg('Backup failed: '+e.message,true); } } function svpValidateBackupPayload(payload){ if(!payload || typeof payload!=='object')throw new Error('Invalid backup file'); if(payload.format!=='SVP_FULL_BACKUP')throw new Error('இது SVP Full Backup file இல்லை'); if(!payload.localStorage || typeof payload.localStorage!=='object')throw new Error('Backup data missing'); return true; } function svpClearPersistentSvpData(){ const remove=[]; for(let i=0;ilocalStorage.removeItem(k)); } function svpClearLoginSession(){ SVP_SESSION_KEYS.forEach(k=>localStorage.removeItem(k)); } async function svpApplyBackupPayload(payload){ svpValidateBackupPayload(payload); await svpCreateSafetySnapshot(false); svpClearPersistentSvpData(); Object.entries(payload.localStorage).forEach(([k,v])=>{ if(String(k).startsWith('svp_') && !SVP_SESSION_KEYS.has(k) && k!==SVP_BACKUP_META_KEY){ localStorage.setItem(k,String(v ?? '')); } }); svpClearLoginSession(); const meta=svpGetBackupMeta(); meta.lastRestored=svpNowStamp(); svpSetBackupMeta(meta); } function svpRestoreBackupFile(input){ if(!ownerOnly()){if(input)input.value='';return} const file=input?.files?.[0]; if(!file)return; if(!confirm('Backup Restore செய்தால் தற்போதைய SVP data மாற்றப்படும். தற்போதைய data-க்கு safety snapshot தானாக எடுக்கப்படும். தொடரவா?')){ input.value='';return; } const reader=new FileReader(); reader.onerror=()=>{svpBackupMsg('Backup file read failed',true);input.value=''}; reader.onload=async()=>{ try{ const payload=JSON.parse(reader.result); await svpApplyBackupPayload(payload); svpBackupMsg('✓ Backup restored. Login screen reload ஆகிறது...'); setTimeout(()=>location.reload(),700); }catch(e){ svpBackupMsg('Restore failed: '+e.message,true); } input.value=''; }; reader.readAsText(file); } async function svpRestoreSafetySnapshot(){ if(!ownerOnly())return; try{ const snap=await svpIdbGet('latest'); if(!snap){svpBackupMsg('Safety snapshot இல்லை.',true);return} if(!confirm('Last safety snapshot ('+(snap.createdAt||'')+') restore செய்யவா?'))return; // Save current state as a temporary fallback before restoring the older snapshot. const current=svpCollectAllData(); await svpIdbPut('before_last_restore',current); svpValidateBackupPayload(snap); svpClearPersistentSvpData(); Object.entries(snap.localStorage).forEach(([k,v])=>{ if(String(k).startsWith('svp_') && !SVP_SESSION_KEYS.has(k) && k!==SVP_BACKUP_META_KEY){ localStorage.setItem(k,String(v ?? '')); } }); svpClearLoginSession(); const meta=svpGetBackupMeta();meta.lastRestored=svpNowStamp();svpSetBackupMeta(meta); svpBackupMsg('✓ Safety snapshot restored. Login screen reload ஆகிறது...'); setTimeout(()=>location.reload(),700); }catch(e){ svpBackupMsg('Snapshot restore failed: '+e.message,true); } } async function svpSafeDestructive(fnName){ if(!ownerOnly())return; try{ await svpCreateSafetySnapshot(false); }catch(e){} const fn=window[fnName]; if(typeof fn==='function')fn(); else alert('Action not available: '+fnName); } // V33: Central Login Account Manager - Owner controlled function initLoginAccountManager(){ if(!isOwner()) return; loginMgrRefreshRecordOptions(); loginMgrLoadSelected(); renderLoginAccounts(); } function loginMgrTypeChanged(){ loginMgrRefreshRecordOptions(); loginMgrLoadSelected(); } function loginMgrRefreshRecordOptions(){ const type=document.getElementById('loginMgrType')?.value||'owner'; const sel=document.getElementById('loginMgrRecord'); if(!sel) return; if(type==='owner'){ sel.innerHTML=''; }else if(type==='staff'){ sel.innerHTML=''+ getStaff().map(x=>``).join(''); }else{ sel.innerHTML=''+ getCustomers().map(x=>``).join(''); } const roleWrap=document.getElementById('loginMgrRoleWrap'); if(roleWrap) roleWrap.style.display=type==='staff'?'':'none'; const status=document.getElementById('loginMgrStatus'); if(status) status.disabled=type==='owner'; } function loginMgrLoadSelected(){ const type=document.getElementById('loginMgrType')?.value||'owner'; const id=document.getElementById('loginMgrRecord')?.value||''; const set=(k,v)=>{const e=document.getElementById(k);if(e)e.value=v??''}; const msg=document.getElementById('loginMgrMsg'); if(msg) msg.textContent=''; if(type==='owner'){ OWNER_USER=loadOwnerUser(); set('loginMgrId',OWNER_USER.staffId); set('loginMgrName',OWNER_USER.name); set('loginMgrPhone',OWNER_USER.phone); set('loginMgrPin',OWNER_USER.pin); set('loginMgrStatus','active'); return; } if(type==='staff'){ if(!id){ const role=document.getElementById('loginMgrRole')?.value||'supervisor'; set('loginMgrId',nextStaffId(role)); set('loginMgrName',''); set('loginMgrPhone',''); set('loginMgrPin',''); set('loginMgrStatus','active'); return; } const x=getStaff().find(v=>v.id===id); if(!x) return; set('loginMgrId',x.id); set('loginMgrName',x.name); set('loginMgrRole',x.role||'supervisor'); set('loginMgrPhone',x.phone); set('loginMgrPin',x.pin); set('loginMgrStatus',x.status||'active'); return; } if(!id){ set('loginMgrId',nextCustomerId()); set('loginMgrName',''); set('loginMgrPhone',''); set('loginMgrPin',''); set('loginMgrStatus','active'); return; } const c=getCustomers().find(v=>v.id===id); if(!c) return; set('loginMgrId',c.id); set('loginMgrName',c.name); set('loginMgrPhone',c.phone); set('loginMgrPin',c.loginPin||String(c.phone||'').slice(-4)); set('loginMgrStatus',c.loginStatus||'active'); } function loginMgrRoleChanged(){ if((document.getElementById('loginMgrType')?.value||'')!=='staff') return; if((document.getElementById('loginMgrRecord')?.value||'')!=='') return; const role=document.getElementById('loginMgrRole')?.value||'supervisor'; const id=document.getElementById('loginMgrId'); if(id) id.value=nextStaffId(role); } function loginMgrGeneratePin(){ let n; try{ const a=new Uint32Array(1); crypto.getRandomValues(a); n=1000+(a[0]%9000); }catch(e){ n=1000+Math.floor(Math.random()*9000); } const el=document.getElementById('loginMgrPin'); if(el) el.value=String(n); } function loginMgrSave(){ if(!ownerOnly()) return; const type=document.getElementById('loginMgrType').value; const selectedRecord=document.getElementById('loginMgrRecord')?.value||''; const name=document.getElementById('loginMgrName').value.trim(); const phone=document.getElementById('loginMgrPhone').value.trim(); const pin=document.getElementById('loginMgrPin').value.trim(); const status=document.getElementById('loginMgrStatus').value||'active'; if(!name){alert('Name தேவை');return} if(!/^\d{10}$/.test(phone)){alert('10 digit mobile number தேவை');return} if(!/^\d{4,6}$/.test(pin)){alert('PIN 4 முதல் 6 digits இருக்க வேண்டும்');return} let savedId=''; if(type==='owner'){ saveOwnerUser({name,phone,pin,status:'active'}); currentUserName=OWNER_USER.name; localStorage.setItem('svp_user_name',OWNER_USER.name); localStorage.setItem('svp_login_phone',OWNER_USER.phone); localStorage.setItem('svp_staff_id',OWNER_USER.staffId); savedId=OWNER_USER.staffId; }else if(type==='staff'){ const role=document.getElementById('loginMgrRole').value||'supervisor'; let list=getStaff(); const realId=selectedRecord||nextStaffId(role); const idx=list.findIndex(x=>x.id===realId); const dup=list.find(x=>x.phone===phone && x.id!==realId); if(dup){alert('இந்த mobile number வேறு staff account-ல் உள்ளது');return} const oldRec=idx>=0?list[idx]:{}; const rec={...oldRec,id:realId,name,role,phone,pin,status,loginAccess:true}; if(idx>=0) list[idx]=rec; else list.push(rec); setStaff(list); savedId=realId; }else{ let list=getCustomers(); const realId=selectedRecord||nextCustomerId(); const idx=list.findIndex(x=>x.id===realId); const old=idx>=0?list[idx]:null; const dup=list.find(x=>x.phone===phone && x.id!==realId); if(dup){alert('இந்த mobile number வேறு customer account-ல் உள்ளது');return} const rec={ id:realId, name, phone, loginPin:pin, loginStatus:status, assignedSupervisorId:old?.assignedSupervisorId||'', assignedSupervisorName:old?.assignedSupervisorName||'', email:old?.email||'', whatsapp:old?.whatsapp||phone, place:old?.place||'', shop:old?.shop||'Main Store', since:old?.since||new Date().toISOString().slice(0,10), notes:old?.notes||'', openingBalance:Number(old?.openingBalance||0), currentBalance:Number(old?.currentBalance||0), createdAt:old?.createdAt||getDateTimeParts().full }; if(idx>=0) list[idx]=rec; else list.push(rec); setCustomers(list); selectedCustomerId=realId; savedId=realId; } loadCustomerOptions(); renderStaffTable(); loadBillingStaffOptions(); loginMgrRefreshRecordOptions(); if(type!=='owner'){ document.getElementById('loginMgrRecord').value=savedId; } loginMgrLoadSelected(); renderLoginAccounts(); document.getElementById('loginMgrMsg').textContent= (type==='owner'?'Owner / Admin':type==='staff'?'Staff':'Customer')+ ' login saved: '+savedId; } function loginMgrEdit(type,id){ const t=document.getElementById('loginMgrType'); if(!t) return; t.value=type; loginMgrRefreshRecordOptions(); const r=document.getElementById('loginMgrRecord'); if(r) r.value=id||''; loginMgrLoadSelected(); document.getElementById('adminLoginAccountsSection')?.scrollIntoView({behavior:'smooth',block:'start'}); } function renderLoginAccounts(){ const body=document.getElementById('loginMgrTableBody'); if(!body) return; OWNER_USER=loadOwnerUser(); const rows=[ {type:'owner',typeLabel:'Owner',id:OWNER_USER.staffId,name:OWNER_USER.name,phone:OWNER_USER.phone,pin:OWNER_USER.pin,status:'active'} ] .concat(getStaff().filter(x=>x.loginAccess!==false).map(x=>({ type:'staff', typeLabel:(x.role==='supervisor'?'Supervisor':'Staff'), id:x.id, name:x.name, phone:x.phone, pin:x.pin||'', status:x.status||'active' }))) .concat(getCustomers().map(x=>({ type:'customer', typeLabel:'Customer', id:x.id, name:x.name, phone:x.phone, pin:x.loginPin||String(x.phone||'').slice(-4), status:x.loginStatus||'active' }))); body.innerHTML=rows.map(x=>` ${escapeHtml(x.typeLabel)} ${escapeHtml(x.id)} ${escapeHtml(x.name)} ${escapeHtml(x.phone)} ${escapeHtml(x.pin||'')} `).join(''); } function loginMgrCopyCredentials(){ const type=document.getElementById('loginMgrType').value; const name=document.getElementById('loginMgrName').value.trim(); const phone=document.getElementById('loginMgrPhone').value.trim(); const pin=document.getElementById('loginMgrPin').value.trim(); if(!phone||!pin){alert('Mobile + PIN முதலில் உள்ளிடுங்கள்');return} const label=type==='owner'?'Owner / Admin':type==='staff'?'Supervisor / Staff':'Customer'; const text=`SVP Poultry Billing Login ${label}: ${name} Username / Mobile: ${phone} PIN: ${pin}`; if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(text) .then(()=>{document.getElementById('loginMgrMsg').textContent='Login details copied.'}) .catch(()=>prompt('Copy Login Details',text)); }else{ prompt('Copy Login Details',text); } } // V52: Customer ↔ Supervisor + Loadman + Daily Assignment SMS function csaSupervisors(){ return getStaff().filter(s=>s.role==='supervisor' && (s.status||'active')==='active'); } function csaLoadmen(){ return getStaff().filter(s=>s.role==='loadman' && (s.status||'active')==='active'); } function csaStaffOptions(list,selectedId='',label='Staff',includeUnassigned=true){ let html=includeUnassigned ? `` : ``; html += list.map(x=>``).join(''); return html; } function csaSupervisorOptions(selectedId='',includeUnassigned=true){ return csaStaffOptions(csaSupervisors(),selectedId,'Supervisor',includeUnassigned); } function csaLoadmanOptions(selectedId='',includeUnassigned=true){ return csaStaffOptions(csaLoadmen(),selectedId,'Loadman',includeUnassigned); } function csaInit(){ if(!isOwner())return; const customers=getCustomers(); const csel=document.getElementById('csaCustomer'); const ssel=document.getElementById('csaSupervisor'); const lsel=document.getElementById('csaLoadman'); const dsel=document.getElementById('csaDailySms'); if(csel){ csel.innerHTML=''+ customers.map(c=>``).join(''); } if(ssel)ssel.innerHTML=csaSupervisorOptions('',true); if(lsel)lsel.innerHTML=csaLoadmanOptions('',true); if(csel){ csel.onchange=()=>{ const c=getCustomers().find(x=>x.id===csel.value); if(ssel)ssel.value=c?.assignedSupervisorId||''; if(lsel)lsel.value=c?.assignedLoadmanId||''; if(dsel)dsel.value=c?.dailyAssignmentSms===false?'off':'on'; }; } csaRenderTable(); setTimeout(()=>csaDailyAutoCheck(),300); } function csaAssign(customerId,supervisorId,loadmanId,dailySms='on'){ let customers=getCustomers(); const i=customers.findIndex(c=>c.id===customerId); if(i<0)return false; const sup=csaSupervisors().find(x=>x.id===supervisorId)||null; const load=csaLoadmen().find(x=>x.id===loadmanId)||null; customers[i]={ ...customers[i], assignedSupervisorId:sup?.id||'', assignedSupervisorName:sup?.name||'', assignedSupervisorPhone:sup?.phone||'', assignedLoadmanId:load?.id||'', assignedLoadmanName:load?.name||'', assignedLoadmanPhone:load?.phone||'', dailyAssignmentSms:dailySms!=='off' }; setCustomers(customers); loadCustomerOptions(); return true; } function csaAssignmentContext(c={}){ return { customer:c.name||'', customer_id:c.id||'', phone:c.phone||'', date:todayIso(), supervisor:c.assignedSupervisorName||'Not Assigned', supervisor_phone:c.assignedSupervisorPhone||'-', loadman:c.assignedLoadmanName||'Not Assigned', loadman_phone:c.assignedLoadmanPhone||'-' }; } function buildAssignmentMessage(c){ const set=smsSettings(); return renderSmsTokens(set.assignmentTemplate||SVP_SMS_DEFAULTS.assignmentTemplate,csaAssignmentContext(c)); } async function csaSendAssignmentSms(customerId,reason='assignment'){ const c=getCustomers().find(x=>x.id===customerId); if(!c)throw new Error('Customer இல்லை'); if(!c.phone)throw new Error('Customer mobile number இல்லை'); if(!c.assignedSupervisorId && !c.assignedLoadmanId)throw new Error('Supervisor / Loadman assign செய்யப்படவில்லை'); const r=await sendSmsTo(c.phone,buildAssignmentMessage(c),{type:'assignment',customer:c,ref:'TEAM-'+c.id}); let list=getCustomers(),i=list.findIndex(x=>x.id===customerId); if(i>=0){ list[i]={ ...list[i], lastAssignmentSmsDate:todayIso(), lastAssignmentSmsAt:new Date().toLocaleString(), lastAssignmentSmsStatus:String(r?.mode||'sent') }; setCustomers(list); } smsLogAdd('assignment',c.id,c.name,c.phone,'sent',r?.mode||'sent',smsResponseRef(r)); return r; } async function csaSaveSingle(){ if(!ownerOnly())return; const customerId=document.getElementById('csaCustomer')?.value||''; const supervisorId=document.getElementById('csaSupervisor')?.value||''; const loadmanId=document.getElementById('csaLoadman')?.value||''; const dailySms=document.getElementById('csaDailySms')?.value||'on'; if(!customerId){alert('Customer தேர்வு செய்யுங்கள்');return} csaAssign(customerId,supervisorId,loadmanId,dailySms); let msg='✓ Delivery team saved.'; if(dailySms==='on'){ try{ const r=await csaSendAssignmentSms(customerId,'save'); msg += String(r?.mode||'').startsWith('device') ? ' Mobile SMS app திறக்கப்பட்டது; Send அழுத்துங்கள்.' : ' Customer notification sent.'; }catch(e){ msg += ' SMS: '+e.message; } } const e=document.getElementById('csaMsg');if(e)e.textContent=msg; csaInit(); } async function csaSaveRow(customerId){ if(!ownerOnly())return; const key=String(customerId).replace(/[^a-zA-Z0-9_-]/g,'_'); const supervisorId=document.getElementById('csaSup_'+key)?.value||''; const loadmanId=document.getElementById('csaLoad_'+key)?.value||''; const dailySms=document.getElementById('csaSms_'+key)?.value||'on'; csaAssign(customerId,supervisorId,loadmanId,dailySms); let msg=`${customerId} delivery team saved.`; if(dailySms==='on'){ try{ const r=await csaSendAssignmentSms(customerId,'row-save'); msg += String(r?.mode||'').startsWith('device')?' SMS app opened.':' Customer SMS sent.'; }catch(e){msg+=' SMS: '+e.message} } const m=document.getElementById('csaMsg');if(m)m.textContent=msg; csaRenderTable(); } function csaCustomersPendingToday(){ const today=todayIso(); return getCustomers().filter(c=> c.dailyAssignmentSms!==false && (c.assignedSupervisorId||c.assignedLoadmanId) && c.phone && c.lastAssignmentSmsDate!==today ); } async function csaSendTodayDailyMessages(manual=false){ if(!ownerOnly())return; const rows=csaCustomersPendingToday(); const msg=document.getElementById('csaMsg'); if(!rows.length){ if(msg)msg.textContent='✓ இன்று அனுப்ப வேண்டிய pending Daily Assignment SMS இல்லை.'; return; } const provider=smsSettings().smsProvider||'device'; if(provider==='device' && rows.length>1){ if(msg)msg.textContent='Mobile SMS App mode-ல் bulk auto SMS முடியாது. Fast2SMS/Webhook தேர்வு செய்தால் daily messages தானாக அனுப்பலாம்.'; if(manual)alert('Mobile SMS App mode-ல் பல customer SMS தானாக அனுப்ப முடியாது. Fast2SMS/Webhook பயன்படுத்துங்கள்.'); return; } let ok=0,fail=0; for(const c of rows){ try{await csaSendAssignmentSms(c.id,'daily');ok++} catch(e){ fail++; smsLogAdd('assignment',c.id,c.name,c.phone,'failed',provider,e.message); } } if(msg)msg.textContent=`Daily Assignment SMS: ${ok} sent/accepted${fail?` • ${fail} failed`:''}.`; csaRenderTable(); } function csaDailyAutoCheck(){ if(!isOwner())return; const provider=smsSettings().smsProvider||'device'; if(provider==='fast2sms'||provider==='webhook'){ csaSendTodayDailyMessages(false); }else{ const n=csaCustomersPendingToday().length; const msg=document.getElementById('csaMsg'); if(msg&&n)msg.textContent=`${n} Daily Assignment SMS pending. Device SMS mode-ல் “Send Today's Daily Messages” பயன்படுத்தவும் அல்லது Fast2SMS/Webhook அமைக்கவும்.`; } } function csaRenderTable(){ const body=document.getElementById('csaTableBody'); if(!body)return; const q=(document.getElementById('csaSearch')?.value||'').trim().toLowerCase(); const customers=getCustomers().filter(c=>{ if(!q)return true; return [ c.id,c.name,c.phone,c.place,c.shop, c.assignedSupervisorName,c.assignedSupervisorId, c.assignedLoadmanName,c.assignedLoadmanId ].some(v=>String(v||'').toLowerCase().includes(q)); }); if(!customers.length){ body.innerHTML='Customer இல்லை / Search result இல்லை'; return; } body.innerHTML=customers.map(c=>{ const key=String(c.id).replace(/[^a-zA-Z0-9_-]/g,'_'); const team=`
${c.assignedSupervisorId ? `SUP: ${escapeHtml(c.assignedSupervisorName||c.assignedSupervisorId)}` : 'Supervisor இல்லை'} ${c.assignedLoadmanId ? `LOAD: ${escapeHtml(c.assignedLoadmanName||c.assignedLoadmanId)}` : 'Loadman இல்லை'}
`; const on=c.dailyAssignmentSms!==false; return ` ${escapeHtml(c.id)}
${escapeHtml(c.name||'')} ${escapeHtml(c.phone||'-')}
${escapeHtml(c.place||'-')} ${team} ${escapeHtml(c.lastAssignmentSmsDate||'-')}
${escapeHtml(c.lastAssignmentSmsStatus||'')} `; }).join(''); } function csaAssignedCustomerCount(supervisorId){ return getCustomers().filter(c=>String(c.assignedSupervisorId||'')===String(supervisorId||'')).length; } function isOwner(){ return currentRole==='owner'; } function applyRoleAccess(){ const owner=isOwner(); document.querySelectorAll('.admin-only').forEach(el=>{ el.style.display=owner ? '' : 'none'; }); if(owner){ setTimeout(()=>backToAdminMenu(false),0); // V52: Fast2SMS/Webhook இருந்தால் இன்று அனுப்பாத delivery-team daily SMS-ஐ ஒருமுறை check செய். setTimeout(()=>{if(typeof csaDailyAutoCheck==='function')csaDailyAutoCheck();},1200); } document.querySelectorAll('.owner-only-action').forEach(el=>{ el.style.display=owner ? '' : 'none'; }); const badge=document.getElementById('roleBadge'); if(badge) badge.textContent=(currentUserName||currentRole||'Role'); const note=document.getElementById('billingRoleNote'); if(note){ note.textContent=owner ? 'OWNER / ADMIN - Full Access' : currentRole==='supervisor' ? 'SUPERVISOR - Assigned Customers • Billing + Preview + Print' : currentRole==='loadman' ? 'LOADMAN - Today Delivery Orders' : currentRole==='driver' ? 'DRIVER - Delivery Orders + Billing' : currentRole==='salesperson' ? 'SALES PERSON - Billing + Preview + Print' : 'WORKER - Billing + Preview + Print'; } // Non-owner cannot edit customer identity or previous balance. const lockIds=['billingCustomerId','customer','customerPhone','billingEmail','billingWhatsApp','previousBalance']; lockIds.forEach(id=>{ const el=document.getElementById(id); if(!el) return; el.readOnly=!owner; el.classList.toggle('role-locked',!owner); }); // Owner can still type/select freely. const picker=document.getElementById('billingCustomerSelect'); if(picker) picker.disabled=false; } function ownerOnly(){ if(!isOwner()){ alert('இந்த வசதி Owner / Admin மட்டும் பயன்படுத்த முடியும்.'); return false; } return true; } function selectBillingCustomer(){ const id=document.getElementById('billingCustomerSelect').value; const master=document.getElementById('customerSelect'); if(master) master.value=id; selectedCustomerId=id; renderBillingOrderSummary(id); if(id){ selectCustomer(); // Assigned supervisor is auto-selected in the bill. const c=getCustomers().find(x=>x.id===id); if(c && c.assignedSupervisorId){ const useS=document.getElementById('useSupervisor'); const sup=document.getElementById('supervisorSelect'); if(useS) useS.checked=true; if(sup) sup.value=c.assignedSupervisorId; if(typeof toggleStaffAssignment==='function') toggleStaffAssignment(); if(typeof updateStaffNamesInBill==='function') updateStaffNamesInBill(); } }else{ renderBillingOrderSummary(''); document.getElementById('billingCustomerId').value=''; document.getElementById('customer').value=''; document.getElementById('customerPhone').value=''; document.getElementById('billingEmail').value=''; document.getElementById('billingWhatsApp').value=''; document.getElementById('previousBalance').value='0'; const oldAdj=document.getElementById('oldPaymentAdjustment');if(oldAdj)oldAdj.value='0'; calc(); } } function showApp(){ if(typeof svpTouchActiveLogin==='function')svpTouchActiveLogin(); document.getElementById('login').classList.add('hidden'); document.getElementById('app').classList.remove('hidden'); document.getElementById('topbar').classList.remove('hidden'); loadCustomerOptions();loadSendSettings();renderStaffTable();loadBillingStaffOptions();applyRoleAccess();initDailyLedger();initVehicleModule();loadSalesCorrectionOptions();calc();updateBillNo();updateClock();renderBills(); if(typeof svpPopulateBillingBranchOptions==='function') svpPopulateBillingBranchOptions(); if(typeof mbPopulateBranchHelpers==='function') mbPopulateBranchHelpers(); } function getVehicles(){ try{return JSON.parse(localStorage.getItem('svp_vehicles')||'[]')}catch(e){return[]} } function setVehicles(v){ localStorage.setItem('svp_vehicles',JSON.stringify(v)); } function getVehicleMaintenance(){ try{return JSON.parse(localStorage.getItem('svp_vehicle_maintenance')||'[]')}catch(e){return[]} } function setVehicleMaintenance(v){ localStorage.setItem('svp_vehicle_maintenance',JSON.stringify(v)); } function nextVehicleId(){ let max=0; getVehicles().forEach(v=>{ const m=String(v.id||'').match(/(\d+)$/); if(m)max=Math.max(max,parseInt(m[1])); }); return 'VEH-'+String(max+1).padStart(3,'0'); } function populateVehicleDrivers(){ const sel=document.getElementById('vehicleDriver'); if(!sel)return; const current=sel.value; const drivers=getStaff().filter(s=>s.role==='driver' && (s.status||'active')==='active'); sel.innerHTML=''+ drivers.map(d=>``).join(''); if(current&&drivers.some(d=>d.id===current))sel.value=current; } function newVehicle(){ if(!ownerOnly())return; document.getElementById('vehicleId').value=nextVehicleId(); document.getElementById('vehicleNumber').value=''; document.getElementById('vehicleType').value='Poultry Van'; document.getElementById('vehicleModel').value=''; document.getElementById('vehicleDriver').value=''; document.getElementById('vehicleStatus').value='active'; document.getElementById('vehicleOdometer').value='0'; document.getElementById('vehicleNextServiceKm').value='0'; document.getElementById('vehicleNextServiceDate').value=''; document.getElementById('vehicleInsuranceExpiry').value=''; document.getElementById('vehicleFcExpiry').value=''; document.getElementById('vehicleNotes').value=''; document.getElementById('vehicleMsg').textContent=''; } function saveVehicle(){ if(!ownerOnly())return; let id=document.getElementById('vehicleId').value.trim()||nextVehicleId(); const number=document.getElementById('vehicleNumber').value.trim(); if(!number){alert('Vehicle Number தேவை');return} let list=getVehicles(); const idx=list.findIndex(v=>v.id===id); const rec={ id, number, type:document.getElementById('vehicleType').value, model:document.getElementById('vehicleModel').value.trim(), driverId:document.getElementById('vehicleDriver').value, status:document.getElementById('vehicleStatus').value, odometer:Number(document.getElementById('vehicleOdometer').value||0), nextServiceKm:Number(document.getElementById('vehicleNextServiceKm').value||0), nextServiceDate:document.getElementById('vehicleNextServiceDate').value, insuranceExpiry:document.getElementById('vehicleInsuranceExpiry').value, fcExpiry:document.getElementById('vehicleFcExpiry').value, notes:document.getElementById('vehicleNotes').value.trim(), createdAt:idx>=0?list[idx].createdAt:getDateTimeParts().full }; if(idx>=0)list[idx]=rec;else list.push(rec); setVehicles(list); document.getElementById('vehicleId').value=id; document.getElementById('vehicleMsg').textContent='Vehicle saved: '+id+' - '+number; renderVehicles(); loadVehicleOptions(); } function selectVehicle(id){ const v=getVehicles().find(x=>x.id===id);if(!v)return; document.getElementById('vehicleId').value=v.id; document.getElementById('vehicleNumber').value=v.number||''; document.getElementById('vehicleType').value=v.type||'Poultry Van'; document.getElementById('vehicleModel').value=v.model||''; populateVehicleDrivers(); document.getElementById('vehicleDriver').value=v.driverId||''; document.getElementById('vehicleStatus').value=v.status||'active'; document.getElementById('vehicleOdometer').value=v.odometer||0; document.getElementById('vehicleNextServiceKm').value=v.nextServiceKm||0; document.getElementById('vehicleNextServiceDate').value=v.nextServiceDate||''; document.getElementById('vehicleInsuranceExpiry').value=v.insuranceExpiry||''; document.getElementById('vehicleFcExpiry').value=v.fcExpiry||''; document.getElementById('vehicleNotes').value=v.notes||''; const ms=document.getElementById('maintVehicleId'); if(ms){ms.value=v.id;renderVehicleMaintenance();} } function editVehicle(){ if(!ownerOnly())return; const id=document.getElementById('vehicleId').value.trim(); if(!id){alert('Vehicle Select செய்யுங்கள்');return} selectVehicle(id); } function deleteVehicle(){ if(!ownerOnly())return; const id=document.getElementById('vehicleId').value.trim(); if(!id){alert('Vehicle Select செய்யுங்கள்');return} if(!confirm(id+' delete செய்யவா?'))return; setVehicles(getVehicles().filter(v=>v.id!==id)); setVehicleMaintenance(getVehicleMaintenance().filter(m=>m.vehicleId!==id)); newVehicle();renderVehicles();loadVehicleOptions();renderVehicleMaintenance(); } function renderVehicles(){ const body=document.getElementById('vehicleTableBody');if(!body)return; const list=getVehicles(); const count=document.getElementById('vehicleCount');if(count)count.textContent=list.length+' Vehicles'; if(!list.length){body.innerHTML='Vehicle records இல்லை';return} body.innerHTML=list.map(v=>{ const driver=staffById(v.driverId); return ` ${escapeHtml(v.id)} ${escapeHtml(v.number)} ${escapeHtml(v.type||'')} ${escapeHtml(driver?driver.name:'')} ${Number(v.odometer||0).toFixed(0)} KM ${escapeHtml(v.nextServiceDate||'')} / ${Number(v.nextServiceKm||0).toFixed(0)} KM ${escapeHtml(v.status||'active')} `; }).join(''); } function loadVehicleOptions(){ const sel=document.getElementById('maintVehicleId');if(!sel)return; const current=sel.value; const list=getVehicles(); sel.innerHTML=''+ list.map(v=>``).join(''); if(current&&list.some(v=>v.id===current))sel.value=current; } function clearVehicleMaintenance(){ if(!ownerOnly())return; document.getElementById('maintDate').value=todayIso(); document.getElementById('maintType').value='Fuel'; document.getElementById('maintOdometer').value='0'; document.getElementById('maintFuelLitres').value='0'; document.getElementById('maintAmount').value='0'; document.getElementById('maintVendor').value=''; document.getElementById('maintRemarks').value=''; } function saveVehicleMaintenance(){ if(!ownerOnly())return; const vehicleId=document.getElementById('maintVehicleId').value; if(!vehicleId){alert('Vehicle தேர்வு செய்யுங்கள்');return} const rec={ id:'VM-'+Date.now(), vehicleId, date:document.getElementById('maintDate').value||todayIso(), type:document.getElementById('maintType').value, odometer:Number(document.getElementById('maintOdometer').value||0), fuelLitres:Number(document.getElementById('maintFuelLitres').value||0), amount:Number(document.getElementById('maintAmount').value||0), vendor:document.getElementById('maintVendor').value.trim(), remarks:document.getElementById('maintRemarks').value.trim(), createdAt:getDateTimeParts().full }; const list=getVehicleMaintenance();list.push(rec);setVehicleMaintenance(list); // update vehicle odometer if this entry is higher const vehicles=getVehicles(); const idx=vehicles.findIndex(v=>v.id===vehicleId); if(idx>=0 && rec.odometer>Number(vehicles[idx].odometer||0)){ vehicles[idx].odometer=rec.odometer; setVehicles(vehicles); } renderVehicles(); renderVehicleMaintenance(); clearVehicleMaintenance(); } function renderVehicleMaintenance(){ const body=document.getElementById('vehicleMaintenanceBody');if(!body)return; const vehicleId=document.getElementById('maintVehicleId')?.value||''; let list=getVehicleMaintenance(); if(vehicleId)list=list.filter(m=>m.vehicleId===vehicleId); list.sort((a,b)=>String(b.date).localeCompare(String(a.date))); let total=0,fuelExpense=0,fuelLitres=0,lastOdo=0; list.forEach(m=>{ total+=Number(m.amount||0); if(m.type==='Fuel'){fuelExpense+=Number(m.amount||0);fuelLitres+=Number(m.fuelLitres||0)} lastOdo=Math.max(lastOdo,Number(m.odometer||0)); }); document.getElementById('vehicleTotalExpense').textContent='₹'+money(total); document.getElementById('vehicleFuelExpense').textContent='₹'+money(fuelExpense); document.getElementById('vehicleFuelLitres').textContent=fuelLitres.toFixed(2)+' L'; document.getElementById('vehicleLastOdometer').textContent=lastOdo.toFixed(0)+' KM'; if(!list.length){body.innerHTML='Maintenance records இல்லை';return} body.innerHTML=list.map(m=>{ const v=getVehicles().find(x=>x.id===m.vehicleId); return ` ${escapeHtml(m.date||'')} ${escapeHtml(v?(v.id+' - '+v.number):m.vehicleId)} ${escapeHtml(m.type||'')} ${Number(m.odometer||0).toFixed(0)} ${Number(m.fuelLitres||0).toFixed(2)} ₹${money(m.amount||0)} ${escapeHtml(m.vendor||'')} ${escapeHtml(m.remarks||'')} `; }).join(''); } function initVehicleModule(){ populateVehicleDrivers(); renderVehicles(); loadVehicleOptions(); if(!document.getElementById('maintDate').value)document.getElementById('maintDate').value=todayIso(); renderVehicleMaintenance(); } function getPurchaseParties(){ try{return JSON.parse(localStorage.getItem('svp_purchase_parties')||'[]')}catch(e){return[]} } function setPurchaseParties(v){ localStorage.setItem('svp_purchase_parties',JSON.stringify(v)); } function nextPurchasePartyId(){ let max=0; getPurchaseParties().forEach(p=>{ const m=String(p.id||'').match(/(\d+)$/); if(m) max=Math.max(max,parseInt(m[1])); }); return 'PUR-'+String(max+1).padStart(3,'0'); } function newPurchaseParty(){ if(!ownerOnly()) return; document.getElementById('purchasePartyId').value=nextPurchasePartyId(); document.getElementById('purchasePartyName').value=''; document.getElementById('purchasePartyPhone').value=''; document.getElementById('purchasePartyPlace').value=''; document.getElementById('purchasePartySince').value=todayIso(); document.getElementById('purchasePartyNotes').value=''; document.getElementById('purchasePartyMsg').textContent=''; } function savePurchaseParty(){ if(!ownerOnly()) return; let id=document.getElementById('purchasePartyId').value.trim()||nextPurchasePartyId(); const name=document.getElementById('purchasePartyName').value.trim(); const phone=document.getElementById('purchasePartyPhone').value.trim(); if(!name){alert('Supplier / Party name தேவை');return} let list=getPurchaseParties(); const idx=list.findIndex(p=>p.id===id); const rec={ id, name, phone, place:document.getElementById('purchasePartyPlace').value.trim(), since:document.getElementById('purchasePartySince').value, notes:document.getElementById('purchasePartyNotes').value.trim(), createdAt: idx>=0 ? list[idx].createdAt : getDateTimeParts().full }; if(idx>=0) list[idx]=rec; else list.push(rec); setPurchaseParties(list); document.getElementById('purchasePartyId').value=id; document.getElementById('purchasePartyMsg').textContent='Purchase Party saved: '+id; renderPurchaseParties(); loadPurchasePartyOptions(); document.getElementById('dailyPurchasePartyId').value=id; loadPurchasePartyIntoDaily(); } function selectPurchaseParty(id){ const p=getPurchaseParties().find(x=>x.id===id);if(!p)return; document.getElementById('purchasePartyId').value=p.id; document.getElementById('purchasePartyName').value=p.name||''; document.getElementById('purchasePartyPhone').value=p.phone||''; document.getElementById('purchasePartyPlace').value=p.place||''; document.getElementById('purchasePartySince').value=p.since||''; document.getElementById('purchasePartyNotes').value=p.notes||''; } function editPurchaseParty(){ if(!ownerOnly()) return; const id=document.getElementById('purchasePartyId').value.trim(); if(!id){alert('Purchase Party தேர்வு செய்யுங்கள்');return} selectPurchaseParty(id); } function deletePurchaseParty(){ if(!ownerOnly()) return; const id=document.getElementById('purchasePartyId').value.trim(); if(!id){alert('Purchase Party தேர்வு செய்யுங்கள்');return} if(!confirm(id+' delete செய்யவா?'))return; setPurchaseParties(getPurchaseParties().filter(p=>p.id!==id)); newPurchaseParty(); renderPurchaseParties(); loadPurchasePartyOptions(); } function renderPurchaseParties(){ const body=document.getElementById('purchasePartyBody'); if(!body)return; const list=getPurchaseParties(); const count=document.getElementById('purchasePartyCount'); if(count)count.textContent=list.length+' Purchase IDs'; if(!list.length){ body.innerHTML='Purchase Party records இல்லை'; return; } body.innerHTML=list.map(p=>` ${escapeHtml(p.id)} ${escapeHtml(p.name)} ${escapeHtml(p.phone||'')} ${escapeHtml(p.place||'')} ${escapeHtml(p.since||'')} `).join(''); } function loadPurchasePartyOptions(){ const sel=document.getElementById('dailyPurchasePartyId');if(!sel)return; const current=sel.value; const list=getPurchaseParties(); sel.innerHTML=''+ list.map(p=>``).join(''); if(current&&list.some(p=>p.id===current))sel.value=current; } function loadPurchasePartyIntoDaily(){ const id=document.getElementById('dailyPurchasePartyId').value; const p=getPurchaseParties().find(x=>x.id===id); const sum=document.getElementById('dailyPurchasePartySummary'); if(!p){ document.getElementById('dailySupplier').value=''; if(sum)sum.textContent=''; return; } document.getElementById('dailySupplier').value=p.name||''; if(sum)sum.innerHTML=`${escapeHtml(p.id)} - ${escapeHtml(p.name)} | 📱 ${escapeHtml(p.phone||'-')} | 📍 ${escapeHtml(p.place||'-')} | Since: ${escapeHtml(p.since||'-')}`; } function getPurchasePartyLedger(id){ return getDailyLedger().filter(r=>r.purchasePartyId===id).sort((a,b)=>String(b.date).localeCompare(String(a.date))); } function getDailyLedger(){ try{return JSON.parse(localStorage.getItem('svp_daily_ledger')||'[]')}catch(e){return[]} } function setDailyLedger(v){ localStorage.setItem('svp_daily_ledger',JSON.stringify(v)); } function nval(id){ const el=document.getElementById(id); if(!el || el.value==='') return 0; return Number(el.value)||0; } function nullableNumber(id){ const el=document.getElementById(id); if(!el || String(el.value).trim()==='') return null; const v=Number(el.value); return Number.isFinite(v)?v:null; } function todayIso(){ const d=new Date(),p=x=>String(x).padStart(2,'0'); return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`; } function billDateKey(b){ const raw=String(b.generatedAt||b.date||'').trim(); let m=raw.match(/^(\d{2})\/(\d{2})\/(\d{4})/); if(m) return `${m[3]}-${m[2]}-${m[1]}`; m=raw.match(/^(\d{4})-(\d{2})-(\d{2})/); if(m) return `${m[1]}-${m[2]}-${m[3]}`; return ''; } function dailySalesForDate(date){ const bills=getBills().filter(b=>{ if(billDateKey(b)!==date) return false; const st=b.status||'active'; return st!=='corrected'; }); return { bills, soldBirds:bills.reduce((s,b)=>s+Math.max(0,Number(b.birds||0)-Number(b.returnedBirds||0)),0), soldKg:bills.reduce((s,b)=>s+Math.max(0,Number(b.net||0)-Number(b.returnedKg||0)),0), salesAmount:bills.reduce((s,b)=>s+Math.max(0,Number(b.amount||0)-Number(b.returnedAmount||0)),0) }; } function getPreviousDailyRecord(date){ return getDailyLedger() .filter(r=>r.dateString(b.date).localeCompare(String(a.date)))[0]||null; } function loadPreviousClosing(){ if(!ownerOnly()) return; const date=document.getElementById('dailyDate').value||todayIso(); const prev=getPreviousDailyRecord(date); if(!prev){ alert('Previous saved daily ledger இல்லை. First day என்றால் opening stock manually enter செய்யுங்கள்.'); return; } document.getElementById('dailyOpeningBirds').value=Number(prev.closingBirds||0); document.getElementById('dailyOpeningKg').value=Number(prev.closingKg||0).toFixed(3); document.getElementById('dailyOpeningValue').value=Number(prev.closingValue||0).toFixed(2); calculateDailyLedger(); } function calculateDailyLedger(){ const date=document.getElementById('dailyDate')?.value||todayIso(); const sale=dailySalesForDate(date); const openingBirds=nval('dailyOpeningBirds'); const openingKg=nval('dailyOpeningKg'); const openingValue=nval('dailyOpeningValue'); const purchaseBirds=nval('dailyPurchaseBirds'); const purchaseKg=nval('dailyPurchaseKg'); const purchaseRate=nval('dailyPurchaseRate'); const purchaseAvg=purchaseBirds>0 ? purchaseKg/purchaseBirds : 0; const purchaseCost=purchaseKg*purchaseRate; const availableBirds=openingBirds+purchaseBirds; const availableKg=openingKg+purchaseKg; const availableValue=openingValue+purchaseCost; const avgCost=availableKg>0 ? availableValue/availableKg : 0; const salesAvgRate=sale.soldKg>0 ? sale.salesAmount/sale.soldKg : 0; const mortality=nval('dailyMortalityBirds'); const expectedClosingBirds=Math.max(0,availableBirds-sale.soldBirds-mortality); const expectedClosingKg=Math.max(0,availableKg-sale.soldKg); let closingBirds=nullableNumber('dailyClosingBirds'); let closingKg=nullableNumber('dailyClosingKg'); if(closingBirds===null) closingBirds=expectedClosingBirds; if(closingKg===null) closingKg=expectedClosingKg; const weightLossKg=Math.max(0,expectedClosingKg-closingKg); const costOfSales=sale.soldKg*avgCost; const weightLossCost=weightLossKg*avgCost; const closingValue=closingKg*avgCost; const marketRate=nval('dailyMarketRate'); const marketValue=closingKg*marketRate; const driverSalary=nval('dailyDriverSalary'); const supervisorSalary=nval('dailySupervisorSalary'); const labour=nval('dailyLabour'); const fuel=nval('dailyFuel'); const other=nval('dailyOtherExpense'); const expenses=driverSalary+supervisorSalary+labour+fuel+other; const grossMargin=sale.salesAmount-costOfSales; const netProfit=sale.salesAmount-costOfSales-weightLossCost-expenses; const pa=document.getElementById('dailyPurchaseAvg'); if(pa) pa.value=purchaseAvg.toFixed(3); const pc=document.getElementById('dailyPurchaseCost'); if(pc) pc.value=money(purchaseCost); const wl=document.getElementById('dailyWeightLossKg'); if(wl) wl.value=weightLossKg.toFixed(3); const stockLedgerBirds=Math.max(0,availableBirds-sale.soldBirds-mortality); const stockLedgerKg=Math.max(0,availableKg-sale.soldKg); const stockLedgerValue=stockLedgerKg*avgCost; const setLedger=(id,v)=>{const el=document.getElementById(id);if(el)el.textContent=v}; setLedger('plOpeningBirds',Math.round(openingBirds)); setLedger('plOpeningKg',openingKg.toFixed(3)); setLedger('plOpeningValue','₹'+money(openingValue)); setLedger('plPurchaseBirds',Math.round(purchaseBirds)); setLedger('plPurchaseKg',purchaseKg.toFixed(3)); setLedger('plPurchaseRate','₹'+money(purchaseRate)); setLedger('plPurchaseCost','₹'+money(purchaseCost)); setLedger('plSalesBirds',Math.round(sale.soldBirds)); setLedger('plSalesKg',sale.soldKg.toFixed(3)); setLedger('plSalesAvgRate','₹'+money(salesAvgRate)); setLedger('plSalesAmount','₹'+money(sale.salesAmount)); setLedger('plStockBirds',Math.round(stockLedgerBirds)); setLedger('plStockKg',stockLedgerKg.toFixed(3)); setLedger('plStockCostRate','₹'+money(avgCost)); setLedger('plStockValue','₹'+money(stockLedgerValue)); const set=(id,v)=>{const el=document.getElementById(id);if(el)el.textContent=v}; set('mAvailableBirds',Math.round(availableBirds)); set('mAvailableKg',availableKg.toFixed(3)+' kg'); set('mSoldBirds',Math.round(sale.soldBirds)); set('mSoldKg',sale.soldKg.toFixed(3)+' kg'); set('mSalesAmount','₹'+money(sale.salesAmount)); set('mAvgCost','₹'+money(avgCost)); set('mCostOfSales','₹'+money(costOfSales)); set('mWeightLossCost','₹'+money(weightLossCost)); set('mClosingBirds',Math.round(closingBirds)); set('mClosingKg',closingKg.toFixed(3)+' kg'); set('mClosingValue','₹'+money(closingValue)); set('mMarketValue','₹'+money(marketValue)); set('mExpenses','₹'+money(expenses)); set('mGrossMargin','₹'+money(grossMargin)); set('mNetProfit',(netProfit<0?'-₹':'₹')+money(Math.abs(netProfit))); set('mWeightLoss',weightLossKg.toFixed(3)+' kg'); const metric=document.getElementById('profitMetric'); if(metric){ metric.classList.toggle('daily-loss',netProfit<0); metric.classList.toggle('daily-profit',netProfit>=0); } return { date, openingBirds,openingKg,openingValue, purchaseBirds,purchaseKg,purchaseAvg,purchaseRate,purchaseCost, availableBirds,availableKg,availableValue,avgCost,salesAvgRate, soldBirds:sale.soldBirds,soldKg:sale.soldKg,salesAmount:sale.salesAmount, expectedClosingBirds,expectedClosingKg,closingBirds,closingKg, stockLedgerBirds,stockLedgerKg,stockLedgerValue, mortalityBirds:mortality,weightLossKg,weightLossCost, costOfSales,closingValue,marketRate,marketValue, driverSalary,supervisorSalary,labour,fuel,otherExpense:other,expenses, grossMargin,netProfit }; } function refreshDailySales(){ if(!ownerOnly()) return; calculateDailyLedger(); document.getElementById('dailyMsg').textContent='Sales loaded from saved bills for '+(document.getElementById('dailyDate').value||todayIso()); } function populateDailyStaffOptions(){ const staff=getStaff().filter(s=>(s.status||'active')==='active'); const drv=document.getElementById('dailyDriverStaff'); const sup=document.getElementById('dailySupervisorStaff'); if(drv) drv.innerHTML=''+staff.filter(s=>s.role==='driver').map(s=>``).join(''); if(sup) sup.innerHTML=''+staff.filter(s=>s.role==='supervisor').map(s=>``).join(''); } function initDailyLedger(){ const d=document.getElementById('dailyDate'); if(d && !d.value) d.value=todayIso(); populateDailyStaffOptions(); populateVehicleDrivers(); renderPurchaseParties(); loadPurchasePartyOptions(); loadDailyRecord(); renderDailyHistory(); } function clearDailyForm(){ if(!ownerOnly()) return; const ids=['dailySupplier','dailyPurchaseBillNo','dailyExpenseNote']; ids.forEach(id=>{const el=document.getElementById(id);if(el)el.value=''}); const nums=['dailyOpeningBirds','dailyOpeningKg','dailyOpeningValue','dailyPurchaseBirds','dailyPurchaseKg','dailyPurchaseRate','dailyMarketRate','dailyMortalityBirds','dailyDriverSalary','dailySupervisorSalary','dailyLabour','dailyFuel','dailyOtherExpense']; nums.forEach(id=>{const el=document.getElementById(id);if(el)el.value='0'}); const blank=['dailyClosingBirds','dailyClosingKg']; blank.forEach(id=>{const el=document.getElementById(id);if(el)el.value=''}); const file=document.getElementById('dailyPurchaseFile'); if(file) file.value=''; const fs=document.getElementById('dailyFileStatus'); if(fs) fs.textContent='No file selected'; calculateDailyLedger(); } function loadDailyRecord(){ if(!isOwner()) return; const date=document.getElementById('dailyDate').value||todayIso(); const r=getDailyLedger().find(x=>x.date===date); if(!r){ clearDailyForm(); document.getElementById('dailyDate').value=date; document.getElementById('dailyPurchasePartyId').value=''; loadPurchasePartyIntoDaily(); const prev=getPreviousDailyRecord(date); if(prev){ document.getElementById('dailyOpeningBirds').value=Number(prev.closingBirds||0); document.getElementById('dailyOpeningKg').value=Number(prev.closingKg||0).toFixed(3); document.getElementById('dailyOpeningValue').value=Number(prev.closingValue||0).toFixed(2); } calculateDailyLedger(); return; } const setv=(id,v)=>{const el=document.getElementById(id);if(el)el.value=(v??'')}; setv('dailyPurchasePartyId',r.purchasePartyId||''); loadPurchasePartyIntoDaily(); setv('dailySupplier',r.supplier||''); setv('dailyPurchaseBillNo',r.purchaseBillNo||''); setv('dailyMarketRate',r.marketRate||0); setv('dailyOpeningBirds',r.openingBirds||0); setv('dailyOpeningKg',r.openingKg||0); setv('dailyOpeningValue',r.openingValue||0); setv('dailyPurchaseBirds',r.purchaseBirds||0); setv('dailyPurchaseKg',r.purchaseKg||0); setv('dailyPurchaseRate',r.purchaseRate||0); setv('dailyClosingBirds',r.closingBirds??''); setv('dailyClosingKg',r.closingKg??''); setv('dailyMortalityBirds',r.mortalityBirds||0); setv('dailyDriverStaff',r.driverStaffId||''); setv('dailySupervisorStaff',r.supervisorStaffId||''); setv('dailyDriverSalary',r.driverSalary||0); setv('dailySupervisorSalary',r.supervisorSalary||0); setv('dailyLabour',r.labour||0); setv('dailyFuel',r.fuel||0); setv('dailyOtherExpense',r.otherExpense||0); setv('dailyExpenseNote',r.expenseNote||''); const fs=document.getElementById('dailyFileStatus'); if(fs) fs.textContent=r.attachmentName ? 'Saved: '+r.attachmentName : 'No saved purchase bill file'; calculateDailyLedger(); } function openPurchaseFileDb(){ return new Promise((resolve,reject)=>{ if(!window.indexedDB){reject(new Error('IndexedDB not supported'));return} const req=indexedDB.open('svp_purchase_files',1); req.onupgradeneeded=()=>{const db=req.result;if(!db.objectStoreNames.contains('files'))db.createObjectStore('files',{keyPath:'key'})}; req.onsuccess=()=>resolve(req.result); req.onerror=()=>reject(req.error); }); } async function savePurchaseAttachment(file,key){ if(!file)return null; const db=await openPurchaseFileDb(); await new Promise((resolve,reject)=>{ const tx=db.transaction('files','readwrite'); tx.objectStore('files').put({key,name:file.name,type:file.type||'application/octet-stream',blob:file}); tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error); }); db.close(); return {key,name:file.name}; } async function getPurchaseAttachment(key){ if(!key)return null; const db=await openPurchaseFileDb(); const result=await new Promise((resolve,reject)=>{ const tx=db.transaction('files','readonly'); const req=tx.objectStore('files').get(key); req.onsuccess=()=>resolve(req.result||null);req.onerror=()=>reject(req.error); }); db.close(); return result; } async function viewPurchaseBill(key){ if(!ownerOnly()) return; try{ const rec=await getPurchaseAttachment(key); if(!rec){alert('Purchase bill file கிடைக்கவில்லை');return} const url=URL.createObjectURL(rec.blob); window.open(url,'_blank'); setTimeout(()=>URL.revokeObjectURL(url),60000); }catch(e){alert('File open error: '+e.message)} } async function saveDailyLedger(){ if(!ownerOnly()) return; const calc=calculateDailyLedger(); const date=calc.date; let list=getDailyLedger(); const existing=list.find(x=>x.date===date); let attachmentKey=existing?.attachmentKey||''; let attachmentName=existing?.attachmentName||''; const file=document.getElementById('dailyPurchaseFile')?.files?.[0]; if(file){ try{ const key='PUR-'+date+'-'+Date.now(); const saved=await savePurchaseAttachment(file,key); attachmentKey=saved.key; attachmentName=saved.name; }catch(e){ alert('Purchase bill file save ஆகவில்லை: '+e.message); } } const rec={ ...calc, purchasePartyId:document.getElementById('dailyPurchasePartyId').value, supplier:document.getElementById('dailySupplier').value.trim(), purchaseBillNo:document.getElementById('dailyPurchaseBillNo').value.trim(), driverStaffId:document.getElementById('dailyDriverStaff').value, supervisorStaffId:document.getElementById('dailySupervisorStaff').value, expenseNote:document.getElementById('dailyExpenseNote').value.trim(), attachmentKey,attachmentName, savedAt:getDateTimeParts().full, savedBy:currentUserName||'Owner' }; const idx=list.findIndex(x=>x.date===date); if(idx>=0) list[idx]=rec; else list.push(rec); list.sort((a,b)=>String(b.date).localeCompare(String(a.date))); setDailyLedger(list); document.getElementById('dailyMsg').textContent='Daily file saved: '+date; const fs=document.getElementById('dailyFileStatus'); if(fs) fs.textContent=attachmentName ? 'Saved: '+attachmentName : 'No purchase bill file'; renderDailyHistory(); } function renderDailyHistory(){ const body=document.getElementById('dailyHistoryBody'); if(!body)return; const list=getDailyLedger().sort((a,b)=>String(b.date).localeCompare(String(a.date))); if(!list.length){ body.innerHTML='Daily ledger records இல்லை'; return; } body.innerHTML=list.map(r=>` ${escapeHtml(r.date||'')}
${escapeHtml(r.purchasePartyId||'')} ${Number(r.purchaseKg||0).toFixed(3)} ${Number(r.soldKg||0).toFixed(3)} ${Number(r.closingKg||0).toFixed(3)} ${Number(r.weightLossKg||0).toFixed(3)} ₹${money(r.salesAmount||0)} ₹${money(r.expenses||0)} ${Number(r.netProfit||0)<0?'-₹':'₹'}${money(Math.abs(Number(r.netProfit||0)))} ${r.attachmentName?escapeHtml(r.attachmentName):'-'} ${r.attachmentKey?``:''} `).join(''); } function editDailyLedgerRecord(date){ if(!ownerOnly())return; document.getElementById('dailyDate').value=date; loadDailyRecord(); const sec=document.getElementById('adminPurchaseSection'); if(sec)sec.scrollIntoView({behavior:'smooth',block:'start'}); const msg=document.getElementById('dailyMsg'); if(msg)msg.textContent='Edit mode: '+date+' — மாற்றங்கள் செய்து Save Daily File அழுத்தவும்.'; } async function deletePurchaseAttachmentByKey(key){ if(!key || !window.indexedDB)return; try{ const db=await openPurchaseFileDb(); await new Promise((resolve,reject)=>{ const tx=db.transaction('files','readwrite'); tx.objectStore('files').delete(key); tx.oncomplete=resolve; tx.onerror=()=>reject(tx.error); }); db.close(); }catch(e){} } async function deleteDailyLedgerRecord(date){ if(!ownerOnly())return; let list=getDailyLedger(); const rec=list.find(x=>x.date===date); if(!rec){alert('Ledger record கிடைக்கவில்லை.');return;} const details = 'Date: '+date+ '\nPurchase: '+Number(rec.purchaseKg||0).toFixed(3)+' kg'+ '\nSales: ₹'+money(rec.salesAmount||0)+ '\nClosing: '+Number(rec.closingKg||0).toFixed(3)+' kg'; if(!confirm('இந்த Daily Purchase / Sales / Stock Ledger record-ஐ Delete செய்யவா?\n\n'+details))return; list=list.filter(x=>x.date!==date); setDailyLedger(list); if(rec.attachmentKey) await deletePurchaseAttachmentByKey(rec.attachmentKey); const current=document.getElementById('dailyDate')?.value||''; if(current===date){ clearDailyForm(); const d=document.getElementById('dailyDate'); if(d)d.value=todayIso(); } renderDailyHistory(); calculateDailyLedger(); const msg=document.getElementById('dailyMsg'); if(msg)msg.textContent='Deleted ledger record: '+date; alert('Ledger record Delete செய்யப்பட்டது.'); } function openDailyFile(date){ if(!ownerOnly())return; // Ensure the Purchase/Daily Ledger module is visible before loading the record. showAdminModule('purchase'); const dateEl=document.getElementById('dailyDate'); if(dateEl) dateEl.value=date; loadDailyRecord(); calculateDailyLedger(); const rec=getDailyLedger().find(x=>String(x.date||'')===String(date||'')); const msg=document.getElementById('dailyMsg'); if(msg){ msg.textContent=rec ? 'Opened Daily Ledger: '+date+' — இப்போது முழு Purchase / Sales / Stock details மேலே பார்க்கலாம்.' : 'Daily Ledger record கிடைக்கவில்லை: '+date; } const sec=document.getElementById('adminPurchaseSection'); if(sec) setTimeout(()=>sec.scrollIntoView({behavior:'smooth',block:'start'}),80); } function printDailyLedger(){ if(!ownerOnly()) return; calculateDailyLedger(); document.body.classList.add('print-daily'); setTimeout(()=>{ window.print(); setTimeout(()=>document.body.classList.remove('print-daily'),500); },100); } function getStaff(){ try{ const list=JSON.parse(localStorage.getItem('svp_staff')||'[]'); if(Array.isArray(list) && list.length) return list; }catch(e){} // First-time default supervisor and driver const defaults=[ {id:'SUP-001',name:'Supervisor',role:'supervisor',phone:'9159633300',pin:'1111',status:'active'}, {id:'DRV-001',name:'Driver',role:'driver',phone:'6379532554',pin:'2222',status:'active'} ]; localStorage.setItem('svp_staff',JSON.stringify(defaults)); return defaults; } function setStaff(list){ localStorage.setItem('svp_staff',JSON.stringify(list)); } function staffRoleLabel(role){ return role==='supervisor'?'Supervisor / சூப்பர்வைசர்' :role==='loadman'?'Loadman / லோட்மேன்' :role==='driver'?'Driver / டிரைவர்' :role==='shopworker'?'Shop Worker / கடை பணியாளர்' :role==='cuttingmaster'?'Chicken Cutting Master / சிக்கன் கட்டிங் மாஸ்டர்' :role==='salesperson'?'Sales Person / விற்பனையாளர்' :role==='worker'?'Other Worker / மற்ற பணியாளர்' :role||'Staff'; } function nextStaffId(role){ const prefix=role==='supervisor'?'SUP' :role==='loadman'?'LDM' :role==='driver'?'DRV' :role==='shopworker'?'SHP' :role==='cuttingmaster'?'CUT' :role==='salesperson'?'SAL' :'STF'; let max=0; getStaff().filter(s=>String(s.id||'').startsWith(prefix+'-')).forEach(s=>{ const m=String(s.id).match(/(\d+)$/); if(m) max=Math.max(max,parseInt(m[1])); }); return prefix+'-'+String(max+1).padStart(3,'0'); } let staffEditingExisting=false; let staffPhotoDraft=''; let staffIdProofFrontDraft=''; let staffIdProofBackDraft=''; function staffPickImage(kind){ const id=kind==='photo' ?'staffPhotoInput' :kind==='proofFront' ?'staffIdProofFrontInput' :'staffIdProofBackInput'; const input=document.getElementById(id); if(input) input.click(); } function staffCompressImage(file,maxDim=1000,quality=.78){ return new Promise((resolve,reject)=>{ const reader=new FileReader(); reader.onerror=()=>reject(new Error('File read failed')); reader.onload=()=>{ const img=new Image(); img.onerror=()=>reject(new Error('Image open failed')); img.onload=()=>{ let w=img.naturalWidth||img.width; let h=img.naturalHeight||img.height; const scale=Math.min(1,maxDim/Math.max(w,h)); w=Math.max(1,Math.round(w*scale)); h=Math.max(1,Math.round(h*scale)); const canvas=document.createElement('canvas'); canvas.width=w; canvas.height=h; const ctx=canvas.getContext('2d'); ctx.drawImage(img,0,0,w,h); try{ resolve(canvas.toDataURL('image/jpeg',quality)); }catch(e){ resolve(reader.result); } }; img.src=reader.result; }; reader.readAsDataURL(file); }); } async function staffImageSelected(kind,input){ const file=input?.files?.[0]; if(!file) return; if(!String(file.type||'').startsWith('image/')){ alert('Image file மட்டும் தேர்வு செய்யுங்கள்'); input.value=''; return; } try{ const data=await staffCompressImage(file,1000,.78); if(kind==='photo') staffPhotoDraft=data; else if(kind==='proofFront') staffIdProofFrontDraft=data; else if(kind==='proofBack') staffIdProofBackDraft=data; staffRefreshMediaPreview(); }catch(e){ alert('Image load செய்ய முடியவில்லை'); } input.value=''; } function staffRefreshMediaPreview(){ const set=(imgId,emptyId,data)=>{ const img=document.getElementById(imgId); const empty=document.getElementById(emptyId); if(img){ if(data){img.src=data;img.style.display='block'} else{img.removeAttribute('src');img.style.display='none'} } if(empty) empty.style.display=data?'none':'flex'; }; set('staffPhotoPreview','staffPhotoEmpty',staffPhotoDraft); set('staffIdProofFrontPreview','staffIdProofFrontEmpty',staffIdProofFrontDraft); set('staffIdProofBackPreview','staffIdProofBackEmpty',staffIdProofBackDraft); } function staffRemoveImage(kind){ if(kind==='photo') staffPhotoDraft=''; else if(kind==='proofFront') staffIdProofFrontDraft=''; else if(kind==='proofBack') staffIdProofBackDraft=''; staffRefreshMediaPreview(); } function staffViewImage(data,title){ if(!data){alert('Image இல்லை');return} const modal=document.getElementById('staffImageModal'); const img=document.getElementById('staffImageModalImg'); const ttl=document.getElementById('staffImageModalTitle'); if(img) img.src=data; if(ttl) ttl.textContent=title||'Staff Document'; if(modal) modal.classList.add('open'); } function staffViewDraftImage(kind){ if(kind==='photo') staffViewImage(staffPhotoDraft,'Staff Photo'); else if(kind==='proofFront') staffViewImage(staffIdProofFrontDraft,'ID Card Proof - Front Side'); else if(kind==='proofBack') staffViewImage(staffIdProofBackDraft,'ID Card Proof - Back Side'); } function staffViewSavedImage(id,kind){ const x=getStaff().find(v=>v.id===id); if(!x)return; if(kind==='photo') staffViewImage(x.photoData||'','Staff Photo - '+(x.name||id)); else if(kind==='proofFront') staffViewImage(x.idProofFrontData||'','ID Proof Front - '+(x.name||id)); else if(kind==='proofBack') staffViewImage(x.idProofBackData||'','ID Proof Back - '+(x.name||id)); } function staffCloseImageModal(){ const modal=document.getElementById('staffImageModal'); const img=document.getElementById('staffImageModalImg'); if(modal) modal.classList.remove('open'); if(img) img.removeAttribute('src'); } function staffRoleChanged(){ const role=document.getElementById('staffRole')?.value||'supervisor'; const id=document.getElementById('staffId'); if(!id) return; // New staff: Staff ID is always auto-generated from selected role. // Existing staff being edited: keep the existing Staff ID unchanged. if(!staffEditingExisting){ id.value=nextStaffId(role); } } function staffToggleLoginFields(){ const yes=(document.getElementById('staffLoginAccess')?.value||'yes')==='yes'; const phone=document.getElementById('staffPhone'); const user=document.getElementById('staffLoginUser'); const pin=document.getElementById('staffPin'); if(user) user.value=yes?(phone?.value||''):''; if(user) user.disabled=!yes; if(pin) pin.disabled=!yes; const help=document.getElementById('staffLoginHelp'); if(help){ help.textContent=yes ? 'Login Access = Yes: Mobile number Username ஆகவும் PIN Password ஆகவும் பயன்படுத்தப்படும்.' : 'Login Access = No: இந்த Staff Employee Register-ல் மட்டும் இருப்பார்; App login செய்ய முடியாது.'; } } function staffGeneratePin(){ const access=document.getElementById('staffLoginAccess'); if(access) access.value='yes'; let n=1000+Math.floor(Math.random()*9000); try{ const a=new Uint32Array(1); crypto.getRandomValues(a); n=1000+(a[0]%9000); }catch(e){} const pin=document.getElementById('staffPin'); if(pin) pin.value=String(n); staffToggleLoginFields(); } function newStaffForm(){ if(!ownerOnly()) return; staffEditingExisting=false; const role=document.getElementById('staffRole').value||'supervisor'; document.getElementById('staffId').value=nextStaffId(role); document.getElementById('staffName').value=''; document.getElementById('staffPhone').value=''; document.getElementById('staffWorkPlace').value=''; document.getElementById('staffLoginAccess').value='no'; document.getElementById('staffLoginUser').value=''; document.getElementById('staffPin').value=''; document.getElementById('staffStatus').value='active'; document.getElementById('staffIdProofType').value=''; document.getElementById('staffIdProofNumber').value=''; staffPhotoDraft=''; staffIdProofFrontDraft=''; staffIdProofBackDraft=''; staffRefreshMediaPreview(); document.getElementById('staffMsg').textContent=''; staffToggleLoginFields(); } function saveStaff(){ if(!ownerOnly()) return; const role=document.getElementById('staffRole').value; let id=document.getElementById('staffId').value.trim(); // Staff ID is system-generated. User never needs to type it. if(!id) id=nextStaffId(role); const existingIds=new Set(getStaff().map(x=>String(x.id||''))); if(!staffEditingExisting && existingIds.has(id)){ id=nextStaffId(role); document.getElementById('staffId').value=id; } const name=document.getElementById('staffName').value.trim(); const phone=document.getElementById('staffPhone').value.trim(); const workPlace=document.getElementById('staffWorkPlace').value.trim(); const pin=document.getElementById('staffPin').value.trim(); const status=document.getElementById('staffStatus').value; const loginAccess=document.getElementById('staffLoginAccess').value==='yes'; const idProofType=document.getElementById('staffIdProofType').value; const idProofNumber=document.getElementById('staffIdProofNumber').value.trim(); if(!name){alert('Staff பெயர் தேவை');return} if(phone && !/^\d{10}$/.test(phone)){alert('Mobile number 10 digits இருக்க வேண்டும்');return} if(loginAccess){ if(!/^\d{10}$/.test(phone)){alert('App Login-க்கு 10 digit mobile number தேவை');return} if(!/^\d{4,6}$/.test(pin)){alert('App Login PIN 4 முதல் 6 digits இருக்க வேண்டும்');return} } let list=getStaff(); if(phone){ const samePhone=list.find(s=>s.phone===phone && s.id!==id); if(samePhone){alert('இந்த mobile number ஏற்கனவே வேறு staff-க்கு உள்ளது');return} } const idx=list.findIndex(s=>s.id===id); const old=idx>=0?list[idx]:{}; const rec={ ...old,id,name,role,phone,workPlace, pin:loginAccess?pin:'', loginAccess,status, idProofType,idProofNumber, photoData:staffPhotoDraft||'', idProofFrontData:staffIdProofFrontDraft||'', idProofBackData:staffIdProofBackDraft||'' }; if(idx>=0) list[idx]=rec; else list.push(rec); setStaff(list); staffEditingExisting=true; document.getElementById('staffId').value=id; document.getElementById('staffMsg').textContent='Staff saved: '+id+' - '+name+' - '+staffRoleLabel(role); renderStaffTable(); loadBillingStaffOptions(); populateDailyStaffOptions(); populateVehicleDrivers(); } function selectStaffForEdit(id){ const x=getStaff().find(v=>v.id===id); if(!x)return; staffEditingExisting=true; document.getElementById('staffId').value=x.id; document.getElementById('staffName').value=x.name||''; document.getElementById('staffRole').value=x.role||'worker'; document.getElementById('staffPhone').value=x.phone||''; document.getElementById('staffWorkPlace').value=x.workPlace||''; document.getElementById('staffLoginAccess').value=x.loginAccess===false?'no':'yes'; document.getElementById('staffLoginUser').value=x.loginAccess===false?'':(x.phone||''); document.getElementById('staffPin').value=x.pin||''; document.getElementById('staffStatus').value=x.status||'active'; document.getElementById('staffIdProofType').value=x.idProofType||''; document.getElementById('staffIdProofNumber').value=x.idProofNumber||''; staffPhotoDraft=x.photoData||''; staffIdProofFrontDraft=x.idProofFrontData||''; staffIdProofBackDraft=x.idProofBackData||''; staffRefreshMediaPreview(); staffToggleLoginFields(); } function editSelectedStaff(){ if(!ownerOnly()) return; const id=document.getElementById('staffId').value.trim(); if(!id){alert('Table-ல் staff Select செய்யுங்கள்');return} selectStaffForEdit(id); } function deleteSelectedStaff(){ if(!ownerOnly()) return; const id=document.getElementById('staffId').value.trim(); if(!id){alert('Staff தேர்வு செய்யுங்கள்');return} if(!confirm(id+' staff-ஐ delete செய்யவா?'))return; setStaff(getStaff().filter(s=>s.id!==id)); newStaffForm(); renderStaffTable(); loadBillingStaffOptions(); populateDailyStaffOptions(); populateVehicleDrivers(); } function renderStaffTable(){ const body=document.getElementById('staffTableBody'); if(!body)return; const all=getStaff(); const count=document.getElementById('staffCount'); if(count) count.textContent=all.length+' Staff'; const counts={ supervisor:all.filter(x=>x.role==='supervisor').length, loadman:all.filter(x=>x.role==='loadman').length, driver:all.filter(x=>x.role==='driver').length, shopworker:all.filter(x=>x.role==='shopworker').length, cuttingmaster:all.filter(x=>x.role==='cuttingmaster').length }; const setText=(id,v)=>{const e=document.getElementById(id);if(e)e.textContent=v}; setText('staffCountSupervisor',counts.supervisor); setText('staffCountLoadman',counts.loadman); setText('staffCountDriver',counts.driver); setText('staffCountShopWorker',counts.shopworker); setText('staffCountCuttingMaster',counts.cuttingmaster); const q=(document.getElementById('staffSearch')?.value||'').trim().toLowerCase(); const rf=document.getElementById('staffRoleFilter')?.value||''; const list=all.filter(x=>{ if(rf && x.role!==rf) return false; if(!q) return true; return [x.id,x.name,x.phone,x.workPlace,staffRoleLabel(x.role)] .some(v=>String(v||'').toLowerCase().includes(q)); }); if(!list.length){ body.innerHTML='Staff இல்லை / Search result இல்லை'; const mobile=document.getElementById('staffMobileList'); if(mobile) mobile.innerHTML='
Staff இல்லை / Search result இல்லை
'; return; } body.innerHTML=list.map(x=>` ${escapeHtml(x.id)} ${escapeHtml(x.name||'')} ${escapeHtml(staffRoleLabel(x.role))} ${escapeHtml(x.phone||'-')} ${escapeHtml(x.workPlace||'-')} ${x.loginAccess===false?'No':'Yes'} ${x.loginAccess===false?'-':escapeHtml(x.pin||'-')} ${x.photoData?``:''} ${x.idProofFrontData?``:''} ${x.idProofBackData?``:''} ${!x.photoData&&!x.idProofFrontData&&!x.idProofBackData?'-':''} ${escapeHtml(x.status||'active')} `).join(''); const mobile=document.getElementById('staffMobileList'); if(mobile){ mobile.innerHTML=list.map(x=>`
${x.photoData?`Staff Photo`:''}
${escapeHtml(x.name||'Unnamed Staff')}
${escapeHtml(x.id)}
${escapeHtml(x.status||'active')}
${escapeHtml(staffRoleLabel(x.role))}
Mobile ${escapeHtml(x.phone||'-')}
Work Place ${escapeHtml(x.workPlace||'-')}
App Login ${x.loginAccess===false?'No':'Yes'}
Login PIN ${x.loginAccess===false?'-':escapeHtml(x.pin||'-')}
Staff Type ${escapeHtml(staffRoleLabel(x.role))}
ID Proof ${escapeHtml(x.idProofType||'-')} ${x.idProofNumber?'- '+escapeHtml(x.idProofNumber):''}
${x.photoData?``:''} ${x.idProofFrontData?``:''} ${x.idProofBackData?``:''}
`).join(''); } } function loadBillingStaffOptions(){ const list=getStaff().filter(s=>(s.status||'active')==='active'); const sups=list.filter(s=>s.role==='supervisor'); const drivers=list.filter(s=>s.role==='driver'); const sup=document.getElementById('supervisorSelect'); const drv=document.getElementById('driverSelect'); if(sup){ sup.innerHTML=''+ sups.map(s=>``).join(''); } if(drv){ drv.innerHTML=''+ drivers.map(s=>``).join(''); } // Logged-in supervisor/driver auto-tick and auto-select if(currentRole==='supervisor'){ const me=list.find(s=>s.phone===localStorage.getItem('svp_login_phone')); if(me && sup){ document.getElementById('useSupervisor').checked=true; sup.value=me.id; } } if(currentRole==='driver'){ const me=list.find(s=>s.phone===localStorage.getItem('svp_login_phone')); if(me && drv){ document.getElementById('useDriver').checked=true; drv.value=me.id; } } toggleStaffAssignment(); updateStaffNamesInBill(); } function staffById(id){ return getStaff().find(s=>s.id===id)||null; } function toggleStaffAssignment(){ const useS=document.getElementById('useSupervisor')?.checked; const useD=document.getElementById('useDriver')?.checked; const sup=document.getElementById('supervisorSelect'); const drv=document.getElementById('driverSelect'); if(sup) sup.disabled=!useS; if(drv) drv.disabled=!useD; updateStaffNamesInBill(); } function updateStaffNamesInBill(){ const supId=document.getElementById('useSupervisor')?.checked ? document.getElementById('supervisorSelect')?.value : ''; const drvId=document.getElementById('useDriver')?.checked ? document.getElementById('driverSelect')?.value : ''; const sup=staffById(supId); const drv=staffById(drvId); const supName=document.getElementById('supervisorName'); const drvName=document.getElementById('driverName'); if(supName) supName.value=sup ? `${sup.id} - ${sup.name}` : ''; if(drvName) drvName.value=drv ? `${drv.id} - ${drv.name}` : ''; const logged=document.getElementById('loggedStaffInfo'); const created=document.getElementById('billCreatedBy'); if(logged) logged.value=currentRole==='owner' ? `${OWNER_USER.staffId} - ${OWNER_USER.name}` : `${localStorage.getItem('svp_staff_id')||''} - ${currentUserName||''}`; if(created) created.value=logged ? logged.value : ''; } function getCustomers(){try{return JSON.parse(localStorage.getItem('svp_customers')||'[]')}catch(e){return[]}} function setCustomers(v){localStorage.setItem('svp_customers',JSON.stringify(v))} function getLedger(){try{return JSON.parse(localStorage.getItem('svp_customer_ledger')||'[]')}catch(e){return[]}} function setLedger(v){localStorage.setItem('svp_customer_ledger',JSON.stringify(v))} function getBills(){try{return JSON.parse(localStorage.getItem('svp_bills')||'[]')}catch(e){return[]}} function nextCustomerId(){ let max=0; getCustomers().forEach(c=>{const m=String(c.id||'').match(/(\d+)/);if(m)max=Math.max(max,parseInt(m[1]))}); return 'CUST-'+String(max+1).padStart(3,'0'); } function loadCustomerOptions(){ const customers=getCustomers(); const options=''+ customers.map(c=>``).join(''); const sel=document.getElementById('customerSelect'); if(sel) sel.innerHTML=options; // Owner sees all customers. Supervisor sees only customers assigned to that supervisor. let billingCustomers=customers; if(currentRole==='supervisor'){ const myStaffId=localStorage.getItem('svp_staff_id')||''; billingCustomers=customers.filter(c=>String(c.assignedSupervisorId||'')===String(myStaffId)); } const billingOptions=''+ billingCustomers.map(c=>``).join(''); const billSel=document.getElementById('billingCustomerSelect'); if(billSel) billSel.innerHTML=billingOptions; const count=document.getElementById('custCount'); if(count) count.textContent=`${customers.length} Customers`; if(selectedCustomerId&&customers.some(c=>c.id===selectedCustomerId)){ if(sel) sel.value=selectedCustomerId; if(billSel && billingCustomers.some(c=>c.id===selectedCustomerId)) billSel.value=selectedCustomerId; } } function newCustomerForm(){ if(!ownerOnly()) return; selectedCustomerId=""; document.getElementById('customerForm').classList.remove('hidden'); document.getElementById('custId').value=nextCustomerId(); document.getElementById('custMasterName').value=''; document.getElementById('custMasterPhone').value=''; document.getElementById('custLoginPin').value=''; document.getElementById('custEmail').value=''; document.getElementById('custWhatsApp').value=''; document.getElementById('custPlace').value=''; document.getElementById('custShop').value='Main Store'; document.getElementById('custSince').value=new Date().toISOString().slice(0,10); document.getElementById('custOpening').value='0'; document.getElementById('custNotes').value=''; document.getElementById('custMsg').textContent=''; } function saveCustomer(){ if(!ownerOnly()) return; const id=document.getElementById('custId').value.trim()||nextCustomerId(); const name=document.getElementById('custMasterName').value.trim(); const phone=document.getElementById('custMasterPhone').value.trim(); if(!name){alert('Customer பெயர் தேவை');return} if(!phone){alert('Mobile number தேவை');return} let customers=getCustomers(),idx=customers.findIndex(c=>c.id===id),old=idx>=0?customers[idx]:null; const opening=num('custOpening'); const loginPin=(document.getElementById('custLoginPin').value.trim()||(old&&old.loginPin)||phone.slice(-4)); const c={id,name,phone,loginPin,loginStatus:(old&&old.loginStatus)||'active', assignedSupervisorId:(old&&old.assignedSupervisorId)||'', assignedSupervisorName:(old&&old.assignedSupervisorName)||'', email:document.getElementById('custEmail').value.trim(), whatsapp:document.getElementById('custWhatsApp').value.trim()||phone, place:document.getElementById('custPlace').value.trim(), shop:document.getElementById('custShop').value.trim(), since:document.getElementById('custSince').value, notes:document.getElementById('custNotes').value.trim(), openingBalance:opening, currentBalance:old?Number(old.currentBalance||0):opening, createdAt:old?old.createdAt:getDateTimeParts().full }; if(idx>=0)customers[idx]=c; else{ customers.push(c); if(opening>0){ let led=getLedger();led.unshift({customerId:id,date:getDateTimeParts().full,type:'opening',billNo:'',debit:opening,credit:0,balance:opening,note:'Opening Balance'});setLedger(led); } } setCustomers(customers);selectedCustomerId=id;loadCustomerOptions(); document.getElementById('customerSelect').value=id;selectCustomer(); document.getElementById('custMsg').textContent='Customer saved: '+id; } function selectCustomer(){ const master=document.getElementById('customerSelect'); const id=(master&&master.value) ? master.value : selectedCustomerId; selectedCustomerId=id; const billSel=document.getElementById('billingCustomerSelect'); if(billSel) billSel.value=id; if(!id){document.getElementById('customerSummary').classList.add('hidden');return} const c=getCustomers().find(x=>x.id===id);if(!c)return; document.getElementById('billingCustomerId').value=c.id; document.getElementById('customer').value=c.name||''; document.getElementById('customerPhone').value=c.phone||''; document.getElementById('billingEmail').value=c.email||''; document.getElementById('billingWhatsApp').value=c.whatsapp||c.phone||''; document.getElementById('buyerName').value=c.name||''; document.getElementById('previousBalance').value=money(c.currentBalance||0); const oldAdj=document.getElementById('oldPaymentAdjustment');if(oldAdj)oldAdj.value='0'; document.getElementById('receivedAmount').value='0'; const s=document.getElementById('customerSummary');s.classList.remove('hidden'); s.innerHTML=`${escapeHtml(c.id)} - ${escapeHtml(c.name)}
📍 ${escapeHtml(c.place||'-')}   |   📱 ${escapeHtml(c.phone||'-')}
🟢 WhatsApp: ${escapeHtml(c.whatsapp||c.phone||'-')}   |   ✉️ ${escapeHtml(c.email||'-')}
🏪 ${escapeHtml(c.shop||'-')}   |   வாங்கத் தொடங்கியது: ${escapeHtml(c.since||'-')}
தற்போதைய பாக்கி: ₹${money(c.currentBalance||0)}`; document.getElementById('customerForm').classList.add('hidden');document.getElementById('customerProfileSection').classList.add('hidden');calc();renderLedger(); } function editSelectedCustomer(){ if(!ownerOnly()) return; if(!selectedCustomerId){alert('முதலில் Customer தேர்வு செய்யுங்கள்');return} const c=getCustomers().find(x=>x.id===selectedCustomerId);if(!c)return; document.getElementById('customerForm').classList.remove('hidden'); document.getElementById('custId').value=c.id; document.getElementById('custMasterName').value=c.name||''; document.getElementById('custMasterPhone').value=c.phone||''; document.getElementById('custLoginPin').value=c.loginPin||String(c.phone||'').slice(-4); document.getElementById('custEmail').value=c.email||''; document.getElementById('custWhatsApp').value=c.whatsapp||c.phone||''; document.getElementById('custPlace').value=c.place||''; document.getElementById('custShop').value=c.shop||''; document.getElementById('custSince').value=c.since||''; document.getElementById('custOpening').value=c.openingBalance||0; document.getElementById('custNotes').value=c.notes||''; } function deleteSelectedCustomer(){ if(!ownerOnly()) return; const id=document.getElementById('custId').value.trim()||selectedCustomerId;if(!id)return; if(!confirm(id+' customer-ஐ delete செய்யவா?'))return; setCustomers(getCustomers().filter(c=>c.id!==id));setLedger(getLedger().filter(x=>x.customerId!==id)); selectedCustomerId="";loadCustomerOptions(); document.getElementById('customerForm').classList.add('hidden'); document.getElementById('customerSummary').classList.add('hidden'); document.getElementById('diarySection').classList.add('hidden');document.getElementById('customerProfileSection').classList.add('hidden'); } function showCustomerDiary(){ if(!ownerOnly()) return; if(!selectedCustomerId){alert('முதலில் Customer தேர்வு செய்யுங்கள்');return} document.getElementById('diarySection').classList.remove('hidden');renderLedger(); } function renderLedger(){ const body=document.getElementById('ledgerBody'); if(!selectedCustomerId){body.innerHTML='Customer தேர்வு செய்யுங்கள்';return} const rows=getLedger().filter(x=>x.customerId===selectedCustomerId); if(!rows.length){body.innerHTML='Diary entries இல்லை';return} body.innerHTML=rows.map(x=>{ const cl=x.type==='receipt'?'receipt':(x.type==='opening'?'opening':'bill'); const lb=x.type==='receipt'?'வரவு':(x.type==='opening'?'Opening':'Bill'); return `${escapeHtml(x.date)}${lb}${escapeHtml(x.billNo||'')}₹${money(x.debit)}₹${money(x.credit)}₹${money(x.balance)}${escapeHtml(x.note||'')}` }).join(''); } function addManualReceipt(){ if(!ownerOnly()) return; if(!selectedCustomerId){alert('Customer தேர்வு செய்யுங்கள்');return} const amt=num('manualReceipt');if(amt<=0){alert('வரவு தொகை உள்ளிடுங்கள்');return} let customers=getCustomers(),i=customers.findIndex(c=>c.id===selectedCustomerId);if(i<0)return; const newBal=Math.max(0,Number(customers[i].currentBalance||0)-amt); customers[i].currentBalance=newBal;setCustomers(customers); let led=getLedger();led.unshift({customerId:selectedCustomerId,date:getDateTimeParts().full,type:'receipt',billNo:'',debit:0,credit:amt,balance:newBal,note:document.getElementById('manualReceiptNote').value.trim()||'Manual Receipt'});setLedger(led); document.getElementById('manualReceipt').value='0';document.getElementById('manualReceiptNote').value=''; document.getElementById('previousBalance').value=money(newBal);selectCustomer(); document.getElementById('diarySection').classList.remove('hidden');renderLedger(); } function showCustomerProfile(){ if(!ownerOnly()) return; if(!selectedCustomerId){ alert('முதலில் Customer தேர்வு செய்யுங்கள்'); return; } const c=getCustomers().find(x=>x.id===selectedCustomerId); if(!c) return; const bills=getBills().filter(b=>b.customerId===selectedCustomerId); const ledger=getLedger().filter(x=>x.customerId===selectedCustomerId); const totalPurchase=bills.reduce((s,b)=>s+Number(b.amount||0),0); const totalReceived=ledger.reduce((s,x)=>s+Number(x.credit||0),0); document.getElementById('profileId').textContent=c.id||''; document.getElementById('profileName').textContent=c.name||''; document.getElementById('profilePhone').textContent=c.phone||''; document.getElementById('profilePlace').textContent=c.place||'-'; document.getElementById('profileShop').textContent=c.shop||'-'; document.getElementById('profileSince').textContent=c.since||'-'; document.getElementById('profileTotalPurchase').textContent='₹'+money(totalPurchase); document.getElementById('profileTotalReceived').textContent='₹'+money(totalReceived); document.getElementById('profileOpening').textContent='₹'+money(c.openingBalance||0); document.getElementById('profileBalance').textContent='₹'+money(c.currentBalance||0); const billsBody=document.getElementById('profileBillsBody'); if(!bills.length){ billsBody.innerHTML='Bills இல்லை'; }else{ billsBody.innerHTML=bills.map(b=>` #${escapeHtml(b.no||'')} ${escapeHtml(b.generatedAt||b.date||'')} ${escapeHtml(b.product||'')} ₹${money(b.amount||0)} ₹${money(b.receivedAmount||0)} ₹${money(b.remainingBalance||0)} `).join(''); } const ledgerBody=document.getElementById('profileLedgerBody'); if(!ledger.length){ ledgerBody.innerHTML='Ledger entries இல்லை'; }else{ ledgerBody.innerHTML=ledger.map(x=>` ${escapeHtml(x.date||'')} ${escapeHtml(x.type||'')} ${escapeHtml(x.billNo||'')} ₹${money(x.debit||0)} ₹${money(x.credit||0)} ₹${money(x.balance||0)} ${escapeHtml(x.note||'')} `).join(''); } document.getElementById('customerProfileSection').classList.remove('hidden'); } function printCustomerProfile(){ if(!ownerOnly()) return; if(!selectedCustomerId){ alert('Customer தேர்வு செய்யுங்கள்'); return; } showCustomerProfile(); document.body.classList.add('print-profile'); setTimeout(()=>{ window.print(); setTimeout(()=>document.body.classList.remove('print-profile'),500); },100); } function downloadCustomerCsv(){ if(!ownerOnly()) return; if(!selectedCustomerId){ alert('Customer தேர்வு செய்யுங்கள்'); return; } const c=getCustomers().find(x=>x.id===selectedCustomerId); const ledger=getLedger().filter(x=>x.customerId===selectedCustomerId); const rows=[ ['Customer ID',c?.id||''], ['Name',c?.name||''], ['Phone',c?.phone||''], ['Place',c?.place||''], ['Shop',c?.shop||''], ['Since',c?.since||''], ['Opening Balance',c?.openingBalance||0], ['Current Balance',c?.currentBalance||0], [], ['Date','Type','Bill No','Debit','Credit','Balance','Note'], ...ledger.map(x=>[x.date||'',x.type||'',x.billNo||'',x.debit||0,x.credit||0,x.balance||0,x.note||'']) ]; const csv=rows.map(r=>r.map(v=>`"${String(v).replaceAll('"','""')}"`).join(',')).join('\n'); const blob=new Blob([csv],{type:'text/csv;charset=utf-8;'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=(c?.id||'customer')+'_ledger.csv'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } function getSendSettings(){try{return JSON.parse(localStorage.getItem('svp_send_settings')||'{}')}catch(e){return{}}} function loadSendSettings(){ const s=getSendSettings(); document.getElementById('autoSms').value=s.autoSms||'off'; document.getElementById('smsProvider').value=s.smsProvider||'device'; document.getElementById('autoWhatsApp').value=s.autoWhatsApp||'off'; document.getElementById('smsApiKey').value=s.smsApiKey||''; document.getElementById('smsWebhookUrl').value=s.smsWebhookUrl||''; if(typeof loadQrSettings==='function') loadQrSettings(); } function saveSendSettings(){ if(!ownerOnly()) return; const old=getSendSettings(); const s={...old,autoSms:document.getElementById('autoSms').value,smsProvider:document.getElementById('smsProvider').value,autoWhatsApp:document.getElementById('autoWhatsApp').value,smsApiKey:document.getElementById('smsApiKey').value.trim(),smsWebhookUrl:document.getElementById('smsWebhookUrl').value.trim()}; localStorage.setItem('svp_send_settings',JSON.stringify(s)); document.getElementById('sendSettingsMsg').textContent='Sending settings saved'; } /* ===== V17 Admin QR Management ===== */ const SVP_QR_DEFAULTS={ showQr:'on', mode:'auto', upiId:'sonikatextileskalavanur-2@okhdfcbank', payeeName:'Sri Venkata Jalapathy Poultry', upiMobile:'6379532554', dynamicAmount:'on', position:'top-right', uploadedImage:'' }; let svpPendingQrImageData=''; function getQrSettings(){ let saved={}; try{saved=JSON.parse(localStorage.getItem('svp_qr_settings')||'{}')||{}}catch(e){saved={}} return {...SVP_QR_DEFAULTS,...saved}; } function loadQrSettings(){ const s=getQrSettings(); const set=(id,v)=>{const el=document.getElementById(id);if(el)el.value=v}; set('qrShow',s.showQr||'on'); set('qrMode',s.mode||'auto'); set('qrPosition',s.position||'top-right'); set('qrUpiId',s.upiId||''); set('qrPayeeName',s.payeeName||''); set('qrUpiMobile',s.upiMobile||''); set('qrDynamicAmount',s.dynamicAmount||'on'); svpPendingQrImageData=s.uploadedImage||''; toggleQrModeUI(); renderQrSettingsPreview(); } function toggleQrModeUI(){ const mode=document.getElementById('qrMode')?.value||'auto'; const box=document.getElementById('qrUploadBox'); if(box)box.style.opacity=mode==='upload'?'1':'.65'; const dyn=document.getElementById('qrDynamicAmount'); if(dyn)dyn.disabled=(mode==='upload'); } function qrSettingsFromForm(){ const old=getQrSettings(); return { showQr:document.getElementById('qrShow')?.value||old.showQr, mode:document.getElementById('qrMode')?.value||old.mode, upiId:(document.getElementById('qrUpiId')?.value||old.upiId||'').trim(), payeeName:(document.getElementById('qrPayeeName')?.value||old.payeeName||'').trim(), upiMobile:(document.getElementById('qrUpiMobile')?.value||old.upiMobile||'').trim(), dynamicAmount:document.getElementById('qrDynamicAmount')?.value||old.dynamicAmount, position:document.getElementById('qrPosition')?.value||old.position, uploadedImage:svpPendingQrImageData||'' }; } function saveQrSettings(){ if(!ownerOnly())return; const s=qrSettingsFromForm(); if(s.mode==='auto' && !s.upiId){ document.getElementById('qrSettingsMsg').textContent='UPI ID தேவை.'; return; } if(s.mode==='upload' && !s.uploadedImage){ document.getElementById('qrSettingsMsg').textContent='QR image upload செய்யவும் அல்லது Auto UPI QR தேர்வு செய்யவும்.'; return; } try{ localStorage.setItem('svp_qr_settings',JSON.stringify(s)); document.getElementById('qrSettingsMsg').textContent='✓ QR settings saved. புதிய bill / print-ல் இந்த QR பயன்படுத்தப்படும்.'; renderQrSettingsPreview(); }catch(e){ document.getElementById('qrSettingsMsg').textContent='QR image save ஆகவில்லை. சிறிய image பயன்படுத்தவும்.'; } } function buildUpiPaymentUri(bill={},settings=getQrSettings()){ const p=new URLSearchParams(); p.set('pa',settings.upiId||''); if(settings.payeeName)p.set('pn',settings.payeeName); p.set('cu','INR'); if(settings.dynamicAmount==='on'){const due=(bill.remainingBalance!=null?Number(bill.remainingBalance):(bill.totalDue!=null?Number(bill.totalDue):Number(bill.amount||0)));if(due>0)p.set('am',money(due));} const no=String(bill.no||'').trim(); p.set('tn',no?`SVP Poultry Bill ${no}`:'SVP Poultry Payment'); return 'upi://pay?'+p.toString(); } function svpQrDataUrl(text,size=300){ if(!window.SVPQRCode)throw new Error('QR encoder unavailable'); const level=(window.SVPQRErrorCorrectLevel&&window.SVPQRErrorCorrectLevel.M!=null)?window.SVPQRErrorCorrectLevel.M:0; const qr=new window.SVPQRCode(-1,level); qr.addData(String(text||'')); qr.make(); const count=qr.getModuleCount(), quiet=4, cells=count+quiet*2, modulePx=Math.max(2,Math.floor(size/cells)), px=cells*modulePx; const c=document.createElement('canvas'); c.width=px; c.height=px; const ctx=c.getContext('2d'); ctx.imageSmoothingEnabled=false; ctx.fillStyle='#fff'; ctx.fillRect(0,0,px,px); ctx.fillStyle='#000'; for(let r=0;r0 ? ` • ₹${money(qrDue)}` : ''; if(printMode){ return `
Scan & Pay${amountNote}
UPI QR Code
${isAuto?'UPI: '+escapeHtml(s.upiId):'Payment QR'}
Any UPI App
`; } return `
Scan & Pay / ஸ்கேன் செய்து செலுத்தவும்${amountNote}
UPI QR Code
${isAuto?'UPI ID: '+escapeHtml(s.upiId):'Uploaded Payment QR'}
Pay with any UPI app
`; } function renderQrSettingsPreview(showMsg=false){ const img=document.getElementById('qrSettingsPreview'), meta=document.getElementById('qrSettingsPreviewMeta'); if(!img||!meta)return; const s=qrSettingsFromForm(); let src=''; if(s.showQr==='on'){ if(s.mode==='upload'&&s.uploadedImage)src=s.uploadedImage; else if(s.upiId){try{src=svpQrDataUrl(buildUpiPaymentUri({no:'PREVIEW',amount:5420},s),300)}catch(e){src=''}} } if(src){img.src=src;img.style.visibility='visible'}else{img.removeAttribute('src');img.style.visibility='hidden'} const amount=(s.mode==='auto'&&s.dynamicAmount==='on')?'Preview amount: ₹5,420.00':'Amount: customer enters manually / static image'; meta.innerHTML=`${s.showQr==='on'?'QR ON':'QR OFF'}
Type: ${escapeHtml(s.mode==='auto'?'Auto UPI QR':'Uploaded QR Image')}
UPI: ${escapeHtml(s.upiId||'-')}
Payee: ${escapeHtml(s.payeeName||'-')}
${amount}
Print: ${escapeHtml(s.position||'top-right')}`; if(showMsg){const m=document.getElementById('qrSettingsMsg');if(m)m.textContent='Preview updated. Save QR Settings அழுத்தினால் நிரந்தரமாக save ஆகும்.'} } function handleQrImageUpload(event){ const file=event.target.files&&event.target.files[0]; if(!file)return; const reader=new FileReader(); reader.onload=()=>{ const im=new Image(); im.onload=()=>{ const max=500, scale=Math.min(1,max/Math.max(im.width,im.height)), w=Math.max(1,Math.round(im.width*scale)), h=Math.max(1,Math.round(im.height*scale)); const c=document.createElement('canvas');c.width=w;c.height=h;const ctx=c.getContext('2d');ctx.imageSmoothingEnabled=false;ctx.fillStyle='#fff';ctx.fillRect(0,0,w,h);ctx.drawImage(im,0,0,w,h); svpPendingQrImageData=c.toDataURL('image/png'); const mode=document.getElementById('qrMode');if(mode)mode.value='upload';toggleQrModeUI();renderQrSettingsPreview(); const m=document.getElementById('qrSettingsMsg');if(m)m.textContent='QR image ready. Save QR Settings அழுத்தவும்.'; }; im.onerror=()=>{const m=document.getElementById('qrSettingsMsg');if(m)m.textContent='இந்த image படிக்க முடியவில்லை.'}; im.src=reader.result; }; reader.readAsDataURL(file); } function clearUploadedQr(){ svpPendingQrImageData=''; const f=document.getElementById('qrImageFile');if(f)f.value=''; if(document.getElementById('qrMode')?.value==='upload')document.getElementById('qrMode').value='auto'; toggleQrModeUI();renderQrSettingsPreview(); const m=document.getElementById('qrSettingsMsg');if(m)m.textContent='Uploaded QR removed from form. Save QR Settings to confirm.'; } function normalizeIndianMobile(v){ let x=String(v||'').replace(/\D/g,'');if(x.startsWith('91')&&x.length>10)x=x.slice(-10);return x.slice(-10); } const SVP_SMS_DEFAULTS={ fastRoute:'dlt', fast2smsSenderId:'', billMessageId:'', billVarOrder:'customer|bill_no|bill_amount|current_payment|final_balance', dueMessageId:'', dueVarOrder:'customer|balance', orderMessageId:'', orderVarOrder:'customer|order_id|qty_text|delivery_date|slot|otp', assignmentMessageId:'', assignmentVarOrder:'customer|supervisor|supervisor_phone|loadman|loadman_phone|date', minDue:1, billTemplate:'SVP Poultry Billing\nBill #{bill_no}\nDate: {date}\nCustomer: {customer}\nNet Weight: {net_weight} kg\nRate: Rs.{rate}\nToday Bill: Rs.{bill_amount}\nPrevious Balance: Rs.{previous_balance}\nTotal Due: Rs.{total_due}\nCurrent Payment: Rs.{current_payment}\nFinal Balance: Rs.{final_balance}\nPayment Mode: {payment_mode}\nThank you.', dueTemplate:'Dear {customer}, your pending balance with SVP Poultry is Rs.{balance}. Please arrange payment. Thank you.', orderTemplate:'SVP Poultry Order Confirmed\nOrder: {order_id}\nCustomer: {customer}\nOrder: {qty_text}\nProduct: {product}\nDelivery: {delivery_date} {slot}\nOTP: {otp}\nPlease keep this OTP for delivery confirmation.', assignmentTemplate:'Dear {customer}, SVP Poultry delivery team for {date}: Supervisor {supervisor} ({supervisor_phone}), Loadman {loadman} ({loadman_phone}). Thank you.' }; // V60: Owner-provided Fast2SMS API key. // First run only: save the key into existing SMS settings when no API key exists. const SVP_FAST2SMS_OWNER_KEY=''; function svpEnsureFast2SmsKey(){ // V63 hosting-safe build: no API key is embedded in public HTML. // Manual SMS works immediately. Configure direct SMS later through a secure server-side endpoint. try{ const raw=localStorage.getItem('svp_send_settings'); const cur=raw?JSON.parse(raw):{}; if(!cur.smsProvider)cur.smsProvider='device'; if(!cur.fastRoute)cur.fastRoute='dlt'; localStorage.setItem('svp_send_settings',JSON.stringify(cur)); }catch(e){} } svpEnsureFast2SmsKey(); function smsSettings(){return {...SVP_SMS_DEFAULTS,...getSendSettings()}} function smsMoney(v){return money(Number(v||0))} function smsContextFromBill(bill={}){ return { customer:bill.customer||bill.customerName||'',customer_id:bill.customerId||'',phone:bill.phone||'', balance:smsMoney(bill.remainingBalance),bill_no:bill.no||'',date:bill.generatedAt||bill.date||'', net_weight:Number(bill.net||0).toFixed(3),rate:smsMoney(bill.rate),bill_amount:smsMoney(bill.amount), previous_balance:smsMoney(bill.previousBalance),old_payment_adjustment:smsMoney(bill.oldPaymentAdjustment), adjusted_old_balance:smsMoney(bill.adjustedOldBalance!=null?bill.adjustedOldBalance:Math.max(0,Number(bill.previousBalance||0)-Number(bill.oldPaymentAdjustment||0))), total_due:smsMoney(bill.totalDue),current_payment:smsMoney(bill.receivedAmount),final_balance:smsMoney(bill.remainingBalance), payment_mode:bill.pay||'-',created_by:[bill.createdById||'',bill.createdByName||''].join(' ').trim(),supervisor:bill.supervisorName||'-',driver:bill.driverName||'-',place:bill.place||'',shop:bill.shop||'' }; } function smsContextFromCustomer(c={}){ return {customer:c.name||'',customer_id:c.id||'',phone:c.phone||'',balance:smsMoney(c.currentBalance),place:c.place||'',shop:c.shop||'',date:getDateTimeParts().full}; } function renderSmsTokens(template,ctx={}){ return String(template||'').replace(/\{([a-z0-9_]+)\}/gi,(m,k)=>Object.prototype.hasOwnProperty.call(ctx,k)?String(ctx[k]??''):m); } function buildBillMessage(bill){if(!bill)return'';return renderSmsTokens(smsSettings().billTemplate,smsContextFromBill(bill))} function buildDueMessage(customer){return renderSmsTokens(smsSettings().dueTemplate,smsContextFromCustomer(customer))} function smsContextFromOrder(o={}){return orderMessageContext(o)} function getLatestBill(){const bills=getBills();return bills.length?bills[0]:null} function openDeviceSms(phone,message){ const mobile=normalizeIndianMobile(phone); if(!mobile)throw new Error('Customer mobile number missing'); if(window.AndroidBridge && typeof window.AndroidBridge.composeSms==='function'){window.AndroidBridge.composeSms(mobile,message||'');return true} window.location.href=`sms:+91${mobile}?body=${encodeURIComponent(message||'')}`; return true; } function smsVariableValues(order,ctx){return String(order||'').split('|').map(x=>x.trim()).filter(Boolean).map(k=>String(ctx[k]??'').replace(/\|/g,'/').trim()).join('|')} function makeFast2SmsPayload(phone,message,meta={}){ const s=smsSettings(), mobile=normalizeIndianMobile(phone); if(!mobile)throw new Error('Customer mobile number missing'); if((s.fastRoute||'dlt')==='q'){ return {route:'q',message:String(message||''),language:/[^\x00-\x7F]/.test(String(message||''))?'unicode':'english',numbers:mobile}; } if(meta.type==='custom')throw new Error('DLT mode-ல் Custom free-text அனுப்ப முடியாது. Due/Bill/Order/Team approved template தேர்வு செய்யுங்கள் அல்லது Quick SMS route பயன்படுத்துங்கள்.'); const type=meta.type==='assignment'?'assignment':(meta.type==='order'?'order':(meta.type==='due'?'due':'bill')); const msgId=type==='assignment'?s.assignmentMessageId:(type==='order'?s.orderMessageId:(type==='due'?s.dueMessageId:s.billMessageId)); const varOrder=type==='assignment'?s.assignmentVarOrder:(type==='order'?s.orderVarOrder:(type==='due'?s.dueVarOrder:s.billVarOrder)); if(!s.fast2smsSenderId)throw new Error('DLT Sender ID missing'); if(!msgId)throw new Error((type==='assignment'?'Team Assignment':type==='order'?'Order OTP':type==='due'?'Due Reminder':'Bill')+' DLT Message ID missing'); const ctx=type==='assignment'?csaAssignmentContext(meta.customer||{}):(type==='order'?smsContextFromOrder(meta.order||{}):(type==='due'?smsContextFromCustomer(meta.customer||{}):smsContextFromBill(meta.bill||{}))); const vars=smsVariableValues(varOrder,ctx); return {route:'dlt',sender_id:s.fast2smsSenderId,message:msgId,variables_values:vars,numbers:mobile,sms_details:'1',udf1:String(meta.ref||ctx.customer_id||ctx.bill_no||'SVP').slice(0,80)}; } let svpNativeSmsSeq=0; const svpNativeSmsCallbacks={}; window.svpFast2SmsNativeResult=function(id,ok,response){ const cb=svpNativeSmsCallbacks[id]; if(!cb)return; delete svpNativeSmsCallbacks[id]; let data=response; try{data=JSON.parse(response)}catch(e){} if(ok && !(data&&typeof data==='object'&&data.return===false))cb.resolve({mode:'fast2sms-native',data}); else cb.reject(new Error(typeof data==='string'?data:(data&&data.message?String(data.message):'Fast2SMS native request failed'))); }; function sendFast2SmsNative(apiKey,payload){ return new Promise((resolve,reject)=>{ if(!(window.AndroidBridge&&typeof window.AndroidBridge.sendFast2Sms==='function'))return reject(new Error('Native Fast2SMS bridge unavailable')); const id='sms_'+Date.now()+'_'+(++svpNativeSmsSeq); svpNativeSmsCallbacks[id]={resolve,reject}; try{window.AndroidBridge.sendFast2Sms(apiKey,JSON.stringify(payload),id)}catch(e){delete svpNativeSmsCallbacks[id];reject(e)} setTimeout(()=>{if(svpNativeSmsCallbacks[id]){delete svpNativeSmsCallbacks[id];reject(new Error('Fast2SMS request timeout'))}},30000); }); } async function sendSmsTo(phone,message,meta={}){ const s=smsSettings(), mobile=normalizeIndianMobile(phone); if(!mobile)throw new Error('Customer mobile number missing'); const provider=s.smsProvider||'device'; if(provider==='device'){openDeviceSms(mobile,message);return {mode:'device'}} if(provider==='webhook'){ if(!s.smsWebhookUrl){openDeviceSms(mobile,message);return {mode:'device-fallback'}} const r=await fetch(s.smsWebhookUrl,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({phone:mobile,message,meta})}); const text=await r.text(); if(!r.ok)throw new Error('Webhook SMS failed: '+r.status+' '+text.slice(0,160)); return {mode:'webhook',data:text}; } if(!s.smsApiKey)throw new Error('Fast2SMS API Key missing. SMS Center Settings-ல் API Key save செய்யுங்கள்.'); const payload=makeFast2SmsPayload(mobile,message,meta); if(window.AndroidBridge&&typeof window.AndroidBridge.sendFast2Sms==='function') return sendFast2SmsNative(s.smsApiKey,payload); const r=await fetch('https://www.fast2sms.com/dev/bulkV2',{method:'POST',headers:{authorization:s.smsApiKey,'Content-Type':'application/json','Accept':'application/json'},body:JSON.stringify(payload)}); const text=await r.text(); let data=text; try{data=JSON.parse(text)}catch(e){} if(!r.ok || (data&&typeof data==='object'&&data.return===false)) throw new Error((data&&data.message?String(data.message):('Fast2SMS failed: '+r.status))); return {mode:'fast2sms-web',data}; } async function sendCurrentBillSms(){ if(!ownerOnly())return; const bill=(window.svpLastSavedBill||getLatestBill()||buildCurrentBillPreviewData()); if(!bill){alert('Bill data இல்லை');return} try{const r=await sendSmsTo(bill.phone,buildBillMessage(bill),{type:'bill',bill,ref:'BILL-'+bill.no}); smsLogAdd('bill',bill.customerId,bill.customer,bill.phone,'sent',r.mode,smsResponseRef(r)); alert(r&&String(r.mode).startsWith('device')?'Mobile SMS app திறக்கப்படுகிறது. Message-ஐ சரிபார்த்து Send அழுத்துங்கள்.':'SMS sent / accepted.')}catch(e){smsLogAdd('bill',bill.customerId,bill.customer,bill.phone,'failed','fast2sms',e.message);alert('SMS error: '+e.message)} } function sendBillWhatsApp(bill){if(!bill){alert('முதலில் ஒரு bill save செய்யுங்கள்');return}const mobile=normalizeIndianMobile(bill.whatsapp||bill.phone);if(!mobile){alert('WhatsApp number இல்லை');return}window.open(`https://wa.me/91${mobile}?text=${encodeURIComponent(buildBillMessage(bill))}`,'_blank')} function sendCurrentBillWhatsApp(){if(!ownerOnly())return;sendBillWhatsApp(getLatestBill())} function sendCurrentBillEmail(){if(!ownerOnly())return;const bill=getLatestBill();if(!bill){alert('முதலில் ஒரு bill save செய்யுங்கள்');return}if(!bill.email){alert('Customer Email இல்லை');return}window.location.href=`mailto:${encodeURIComponent(bill.email)}?subject=${encodeURIComponent(`SVP Poultry Bill #${bill.no}`)}&body=${encodeURIComponent(buildBillMessage(bill))}`} async function testSms(){ if(!ownerOnly())return; const phone=prompt('Test SMS mobile number:');if(!phone)return; try{const s=smsSettings(); if((s.smsProvider||'device')==='fast2sms'&&(s.fastRoute||'dlt')==='dlt'){alert('DLT test-க்கு SMS Center-ல் Customer select செய்து “Test Selected Customer” பயன்படுத்துங்கள்.');return} const r=await sendSmsTo(phone,'SVP Poultry Billing - Test SMS',{type:'custom',ref:'TEST'}); alert(r&&String(r.mode).startsWith('device')?'Mobile SMS app திறக்கப்படுகிறது. Send அழுத்துங்கள்.':'Test SMS request accepted')}catch(e){alert('Test SMS error: '+e.message)} } /* ===== V22 SMS Center ===== */ function getSmsHistory(){try{return JSON.parse(localStorage.getItem('svp_sms_history')||'[]')}catch(e){return[]}} function setSmsHistory(v){localStorage.setItem('svp_sms_history',JSON.stringify(v.slice(0,500)))} function smsLogAdd(type,customerId,customer,phone,status,provider,reference){const a=getSmsHistory();a.unshift({date:new Date().toISOString(),type,customerId:customerId||'',customer:customer||'',phone:normalizeIndianMobile(phone),status,provider:provider||'',reference:String(reference||'').slice(0,220)});setSmsHistory(a);renderSmsHistory();renderSmsStats()} function smsResponseRef(r){if(!r)return'';const d=r.data;if(d&&typeof d==='object')return d.request_id||d.message||JSON.stringify(d).slice(0,180);return String(d||r.mode||'').slice(0,180)} function smsToggleRouteHelp(){const el=document.getElementById('smsRouteHelp');if(!el)return;const route=document.getElementById('smsFastRoute')?.value||'dlt';el.textContent=route==='dlt'?'DLT mode: approved Sender ID + Message ID required. Message Content box is your local preview; Fast2SMS sends the approved DLT template using the variable order below.':'Quick SMS: Message Content box text is sent directly. Use mainly for testing; production business messaging should use your approved DLT setup.'} // V61: DLT Sender ID helper function svpNormalizeSenderId(input){ if(!input)return ''; let v=String(input.value||'').replace(/[^A-Za-z]/g,'').toUpperCase().slice(0,6); input.value=v; const st=document.getElementById('smsSenderIdStatus'); if(st){ if(!v)st.textContent=''; else if(v.length<3)st.textContent='Sender ID குறைந்தது 3 letters இருக்க வேண்டும்.'; else st.textContent='✓ Sender ID format OK. Save/Fast2SMS approval இன்னும் verify செய்ய வேண்டும்.'; } return v; } function svpValidSenderId(v){ return /^[A-Za-z]{3,6}$/.test(String(v||'').trim()); } function svpAutoSaveSenderId(){ if(!ownerOnly())return; const e=document.getElementById('smsSenderId'); if(!e)return; const v=svpNormalizeSenderId(e); const st=document.getElementById('smsSenderIdStatus'); if(!v)return; if(!svpValidSenderId(v)){ if(st)st.textContent='❌ 3–6 English letters உள்ள approved DLT Header மட்டும்.'; return; } const old=getSendSettings(); old.fast2smsSenderId=v; localStorage.setItem('svp_send_settings',JSON.stringify(old)); if(st)st.textContent='✓ Sender ID saved: '+v; } function svpSenderFromAny(item){ if(!item)return ''; if(typeof item==='string')return item; return item.sender_id||item.senderId||item.sender||item.header||item.header_name||item.name||''; } function svpExtractSenderIds(data){ const out=[]; const push=v=>{ v=String(v||'').trim(); if(svpValidSenderId(v) && !out.includes(v))out.push(v); }; if(Array.isArray(data))data.forEach(x=>push(svpSenderFromAny(x))); if(data && typeof data==='object'){ ['data','sender','senders','sender_ids','senderIds','result','results'].forEach(k=>{ const v=data[k]; if(Array.isArray(v))v.forEach(x=>push(svpSenderFromAny(x))); else if(v && typeof v==='object')Object.values(v).forEach(x=>push(svpSenderFromAny(x))); else push(v); }); } return out; } function svpUseSenderId(v){ const e=document.getElementById('smsSenderId'); if(!e)return; e.value=String(v||'').toUpperCase(); svpAutoSaveSenderId(); } async function svpFetchApprovedSenderIds(){ if(!ownerOnly())return; const st=document.getElementById('smsSenderIdStatus'); const list=document.getElementById('smsSenderIdList'); if(st)st.textContent='Fast2SMS DLT Manager-லிருந்து Approved Sender ID பார்க்கிறது...'; if(list)list.innerHTML=''; const set=smsSettings(); const key=set.smsApiKey||document.getElementById('smsCenterApiKey')?.value.trim()||''; if(!key){ if(st)st.textContent='❌ Fast2SMS API Key இல்லை.'; return; } try{ const r=await fetch('https://www.fast2sms.com/dev/dlt_manager?type=sender',{ method:'GET', headers:{Authorization:key,Accept:'application/json'} }); const text=await r.text(); let data=text; try{data=JSON.parse(text)}catch(e){} if(!r.ok)throw new Error('HTTP '+r.status+' '+String(text).slice(0,120)); const ids=svpExtractSenderIds(data); if(!ids.length){ if(st)st.textContent='Approved Sender ID கிடைக்கவில்லை. Fast2SMS DLT Manager-ல் Header add/approve செய்யப்பட்டுள்ளதா பார்க்கவும்.'; return; } if(list){ list.innerHTML=ids.map(v=>``).join(''); } if(ids.length===1){ svpUseSenderId(ids[0]); if(st)st.textContent='✓ Approved Sender ID fetched & saved: '+ids[0]; }else{ if(st)st.textContent='✓ '+ids.length+' approved Sender IDs கிடைத்தது. கீழே ஒன்றை தேர்வு செய்யுங்கள்.'; } }catch(e){ if(st)st.textContent='Fetch முடியவில்லை: '+e.message+' • Approved Header-ஐ manual-ஆக 3–6 letters type செய்து Save SMS Settings அழுத்தலாம்.'; } } // V62: Manual SMS first; Fast2SMS can be enabled later. function svpUseManualSmsMode(){ if(!ownerOnly())return; const p=document.getElementById('smsCenterProvider'); if(p)p.value='device'; const old=getSendSettings(); old.smsProvider='device'; old.autoSms='off'; localStorage.setItem('svp_send_settings',JSON.stringify(old)); const a=document.getElementById('smsCenterAutoBill');if(a)a.value='off'; svpRenderSmsModeInfo(); const msg=document.getElementById('smsCenterSettingsMsg'); if(msg)msg.textContent='✓ Manual SMS Mode ON. Send button அழுத்தினால் mobile SMS app திறக்கும்.'; } function svpUseFast2SmsMode(){ if(!ownerOnly())return; const p=document.getElementById('smsCenterProvider'); if(p)p.value='fast2sms'; const old=getSendSettings(); old.smsProvider='fast2sms'; localStorage.setItem('svp_send_settings',JSON.stringify(old)); svpRenderSmsModeInfo(); const msg=document.getElementById('smsCenterSettingsMsg'); if(msg)msg.textContent='Fast2SMS selected. Approved DLT Sender ID + Message IDs set செய்த பிறகு direct SMS பயன்படுத்தலாம்.'; } function svpRenderSmsModeInfo(){ const e=document.getElementById('smsModeInfo'); if(!e)return; const p=document.getElementById('smsCenterProvider')?.value||'device'; e.textContent=p==='device' ?'Manual Mode: Customer number + message SMS app-ல் auto-fill ஆகும். நீங்கள் Send மட்டும் அழுத்த வேண்டும்.' :p==='fast2sms' ?'Direct Mode: Fast2SMS API + approved DLT Sender ID + approved Message ID தேவை.' :'Webhook Mode: உங்கள் server SMS endpoint தேவை.'; } function svpTestManualSms(){ if(!ownerOnly())return; const phone=prompt('Test SMS mobile number:'); if(!phone)return; const old=getSendSettings(); old.smsProvider='device'; localStorage.setItem('svp_send_settings',JSON.stringify(old)); const p=document.getElementById('smsCenterProvider');if(p)p.value='device'; svpRenderSmsModeInfo(); sendSmsTo(phone,'SVP Poultry - Manual SMS Test',{type:'custom',ref:'MANUAL-TEST'}) .then(()=>alert('Mobile SMS app திறக்கிறது. Message பார்த்து Send அழுத்துங்கள்.')) .catch(e=>alert('Manual SMS error: '+e.message)); } function initSmsCenter(){ svpEnsureFast2SmsKey(); const s=smsSettings(); const val=(id,v)=>{const e=document.getElementById(id);if(e)e.value=v??''}; val('smsCenterProvider',s.smsProvider||'device');val('smsFastRoute',s.fastRoute||'dlt');val('smsCenterAutoBill',s.autoSms||'off');val('smsCenterApiKey',s.smsApiKey||'');val('smsSenderId',s.fast2smsSenderId||''); const senderInput=document.getElementById('smsSenderId');if(senderInput&&senderInput.value)svpNormalizeSenderId(senderInput);val('smsBillMessageId',s.billMessageId||'');val('smsBillVarOrder',s.billVarOrder||SVP_SMS_DEFAULTS.billVarOrder);val('smsDueMessageId',s.dueMessageId||'');val('smsDueVarOrder',s.dueVarOrder||SVP_SMS_DEFAULTS.dueVarOrder);val('smsOrderMessageId',s.orderMessageId||'');val('smsOrderVarOrder',s.orderVarOrder||SVP_SMS_DEFAULTS.orderVarOrder);val('smsAssignmentMessageId',s.assignmentMessageId||'');val('smsAssignmentVarOrder',s.assignmentVarOrder||SVP_SMS_DEFAULTS.assignmentVarOrder);val('smsMinDue',Number(s.minDue||1));val('smsCenterWebhook',s.smsWebhookUrl||''); smsToggleRouteHelp(); smsLoadTemplateToEditor(); renderSmsStats(); renderSmsDueCustomers(); renderSmsHistory(); svpRenderSmsModeInfo(); } function saveSmsCenterSettings(){ if(!ownerOnly())return; const old=getSendSettings(); const senderEl=document.getElementById('smsSenderId'); const sender=svpNormalizeSenderId(senderEl); const provider=document.getElementById('smsCenterProvider').value; const route=document.getElementById('smsFastRoute').value; if(provider==='fast2sms' && route==='dlt' && sender && !svpValidSenderId(sender)){ alert('DLT Sender ID 3–6 English letters உள்ள approved Header ஆக இருக்க வேண்டும்.'); senderEl?.focus(); return; } const autoBill=provider==='device'?'off':document.getElementById('smsCenterAutoBill').value; const s={...old,smsProvider:provider,fastRoute:route,autoSms:autoBill,smsApiKey:document.getElementById('smsCenterApiKey').value.trim(),fast2smsSenderId:sender,billMessageId:document.getElementById('smsBillMessageId').value.trim(),billVarOrder:document.getElementById('smsBillVarOrder').value.trim()||SVP_SMS_DEFAULTS.billVarOrder,dueMessageId:document.getElementById('smsDueMessageId').value.trim(),dueVarOrder:document.getElementById('smsDueVarOrder').value.trim()||SVP_SMS_DEFAULTS.dueVarOrder,orderMessageId:document.getElementById('smsOrderMessageId').value.trim(),orderVarOrder:document.getElementById('smsOrderVarOrder').value.trim()||SVP_SMS_DEFAULTS.orderVarOrder,assignmentMessageId:document.getElementById('smsAssignmentMessageId').value.trim(),assignmentVarOrder:document.getElementById('smsAssignmentVarOrder').value.trim()||SVP_SMS_DEFAULTS.assignmentVarOrder,minDue:Math.max(0,Number(document.getElementById('smsMinDue').value||1)),smsWebhookUrl:document.getElementById('smsCenterWebhook').value.trim()}; localStorage.setItem('svp_send_settings',JSON.stringify(s)); const msg=document.getElementById('smsCenterSettingsMsg');if(msg)msg.textContent= s.smsProvider==='device' ?'✓ Manual SMS Mode saved. DLT Sender ID இப்போது தேவையில்லை.' :'✓ SMS settings saved.'+(s.fast2smsSenderId?' Sender ID: '+s.fast2smsSenderId:' Sender ID இன்னும் இல்லை.'); const a=document.getElementById('autoSms'),p=document.getElementById('smsProvider'),k=document.getElementById('smsApiKey'),w=document.getElementById('smsWebhookUrl');if(a)a.value=s.autoSms;if(p)p.value=s.smsProvider;if(k)k.value=s.smsApiKey;if(w)w.value=s.smsWebhookUrl;renderSmsDueCustomers();renderSmsStats(); } function smsLoadTemplateToEditor(){const type=document.getElementById('smsMessageType')?.value||'due',s=smsSettings(),e=document.getElementById('smsMessageContent');if(!e)return;if(type==='due')e.value=s.dueTemplate||SVP_SMS_DEFAULTS.dueTemplate;else if(type==='bill')e.value=s.billTemplate||SVP_SMS_DEFAULTS.billTemplate;else e.value='';smsRenderPreview()} function smsSaveCurrentTemplate(){if(!ownerOnly())return;const type=document.getElementById('smsMessageType')?.value||'due';if(type==='custom'){alert('Custom message template save செய்யப்படாது. Due அல்லது Bill type தேர்வு செய்யுங்கள்.');return}const old=getSendSettings(),text=document.getElementById('smsMessageContent')?.value||'';old[type==='due'?'dueTemplate':'billTemplate']=text;localStorage.setItem('svp_send_settings',JSON.stringify(old));document.getElementById('smsSendMsg').textContent='✓ '+(type==='due'?'Due Reminder':'Bill')+' template saved.'} function smsDueCustomers(){const min=Number(smsSettings().minDue||1);return getCustomers().filter(c=>Number(c.currentBalance||0)>=min).sort((a,b)=>Number(b.currentBalance||0)-Number(a.currentBalance||0))} function renderSmsStats(){const all=getCustomers(),due=smsDueCustomers(),total=due.reduce((s,c)=>s+Number(c.currentBalance||0),0),today=new Date().toISOString().slice(0,10),logs=getSmsHistory().filter(x=>String(x.date||'').slice(0,10)===today);const t=(id,v)=>{const e=document.getElementById(id);if(e)e.textContent=v};t('smsStatCustomers',all.length);t('smsStatDueCustomers',due.length);t('smsStatTotalDue','₹'+money(total));t('smsStatSentToday',logs.length)} function renderSmsDueCustomers(){ const due=smsDueCustomers(),sel=document.getElementById('smsCustomerSelect');if(sel){const cur=sel.value;sel.innerHTML=''+getCustomers().map(c=>``).join('');if(getCustomers().some(c=>c.id===cur))sel.value=cur} const b=document.getElementById('smsDueCustomersBody');if(b)b.innerHTML=due.length?due.map(c=>`${escapeHtml(c.id)} - ${escapeHtml(c.name)}${escapeHtml(c.phone||'')}${escapeHtml(c.place||'-')}₹${money(c.currentBalance||0)}`).join(''):'No due customers above the selected minimum balance.'; smsCustomerChanged(); } function smsCustomerChanged(){const id=document.getElementById('smsCustomerSelect')?.value||'',c=getCustomers().find(x=>x.id===id);const e=document.getElementById('smsCustomerBalance');if(e)e.value=c?'₹'+money(c.currentBalance||0):'₹0.00';smsRenderPreview()} function smsSelectedCustomer(){const id=document.getElementById('smsCustomerSelect')?.value||'';return getCustomers().find(c=>c.id===id)||null} function smsRenderPreview(){ const type=document.getElementById('smsMessageType')?.value||'due',c=smsSelectedCustomer(),box=document.getElementById('smsMessagePreview');if(!box)return; let text=document.getElementById('smsMessageContent')?.value||''; if(type==='bill'){ let b=getBills().find(x=>x.customerId===(c&&c.id))||getLatestBill();box.textContent=b?renderSmsTokens(text,smsContextFromBill(b)):'இந்த Customer-க்கு bill history இல்லை.'; }else if(type==='due')box.textContent=c?renderSmsTokens(text,smsContextFromCustomer(c)):'Customer தேர்வு செய்யுங்கள்.'; else box.textContent=c?renderSmsTokens(text,smsContextFromCustomer(c)):text; } function smsPickAndSendDue(id){const sel=document.getElementById('smsCustomerSelect');if(sel)sel.value=id;const t=document.getElementById('smsMessageType');if(t)t.value='due';smsLoadTemplateToEditor();smsCustomerChanged();smsSendSelected()} async function smsSendSelected(){ if(!ownerOnly())return; const type=document.getElementById('smsMessageType')?.value||'due',c=smsSelectedCustomer();if(!c){alert('Customer தேர்வு செய்யுங்கள்');return} let message='',meta={type,customer:c,ref:c.id}; if(type==='bill'){const b=getBills().find(x=>x.customerId===c.id)||getLatestBill();if(!b){alert('இந்த Customer-க்கு bill இல்லை');return}message=renderSmsTokens(document.getElementById('smsMessageContent').value,smsContextFromBill(b));meta={type:'bill',bill:b,ref:'BILL-'+b.no};} else {message=renderSmsTokens(document.getElementById('smsMessageContent').value,smsContextFromCustomer(c));meta.type=type==='custom'?'custom':'due'} const m=document.getElementById('smsSendMsg');if(m)m.textContent='Sending...'; try{const r=await sendSmsTo(c.phone,message,meta);smsLogAdd(type,c.id,c.name,c.phone,'sent',r.mode,smsResponseRef(r));if(m)m.textContent='✓ SMS sent / accepted for '+c.name;} catch(e){smsLogAdd(type,c.id,c.name,c.phone,'failed',smsSettings().smsProvider,e.message);if(m)m.textContent='✗ '+e.message;} } async function smsTestSelectedCustomer(){ const c=smsSelectedCustomer()||smsDueCustomers()[0]||getCustomers()[0];if(!c){alert('Customer இல்லை');return} const type=(document.getElementById('smsFastRoute')?.value||'dlt')==='dlt'?'due':'custom'; try{const message=type==='due'?buildDueMessage(c):'SVP Poultry SMS test for '+c.name;const r=await sendSmsTo(c.phone,message,{type,customer:c,ref:'TEST-'+c.id});smsLogAdd('test',c.id,c.name,c.phone,'sent',r.mode,smsResponseRef(r));alert('Test SMS sent / accepted.');}catch(e){smsLogAdd('test',c.id,c.name,c.phone,'failed',smsSettings().smsProvider,e.message);alert('Test SMS error: '+e.message)} } async function smsSendAllDue(){ if(!ownerOnly())return; const s=smsSettings();if((s.smsProvider||'device')==='device'){alert('Bulk Due Reminder-க்கு Fast2SMS API அல்லது Custom Webhook தேர்வு செய்யுங்கள். Mobile SMS App bulk mode support செய்யாது.');return} const due=smsDueCustomers();if(!due.length){alert('கடன் உள்ள customer இல்லை');return}if(!confirm(`${due.length} customers-க்கு due reminder அனுப்பவா?`))return; const editor=document.getElementById('smsMessageContent');const template=(document.getElementById('smsMessageType')?.value==='due'&&editor?.value)?editor.value:(s.dueTemplate||SVP_SMS_DEFAULTS.dueTemplate); const m=document.getElementById('smsSendMsg');let ok=0,fail=0;if(m)m.textContent='Bulk SMS starting...'; for(let i=0;isetTimeout(r,350)); } if(m)m.textContent=`Bulk complete • Success ${ok} • Failed ${fail}`;renderSmsStats(); } function renderSmsHistory(){const b=document.getElementById('smsHistoryBody');if(!b)return;const a=getSmsHistory();b.innerHTML=a.length?a.map(x=>`${escapeHtml(new Date(x.date).toLocaleString())}${escapeHtml(x.type)}${escapeHtml((x.customerId||'')+' '+(x.customer||''))}${escapeHtml(x.phone||'')}${escapeHtml(x.status)}${escapeHtml(x.provider||'')}${escapeHtml(x.reference||'')}`).join(''):'No SMS history.'} function clearSmsHistory(){if(!ownerOnly())return;if(!confirm('SMS history clear செய்யவா?'))return;setSmsHistory([]);renderSmsHistory();renderSmsStats()} /* V29 Minimal multi-weighing: preserve original billing layout */ let svpExtraWeighSeq=0; function svpWN(v){const n=Number(v);return Number.isFinite(n)?n:0} function svpGetWeighingRows(){ const rows=[{ cages:Math.max(0,Math.round(svpWN(document.getElementById('cages')?.value))), birds:Math.max(0,Math.round(svpWN(document.getElementById('birds')?.value))), gross:Math.max(0,svpWN(document.getElementById('gross')?.value)), cage:Math.max(0,svpWN(document.getElementById('cage')?.value)) }]; document.querySelectorAll('#svpAdditionalWeighings .svp-extra-weigh-card').forEach(card=>{ const get=k=>card.querySelector(`[data-svp-weigh="${k}"]`); rows.push({ cages:Math.max(0,Math.round(svpWN(get('cages')?.value))), birds:Math.max(0,Math.round(svpWN(get('birds')?.value))), gross:Math.max(0,svpWN(get('gross')?.value)), cage:Math.max(0,svpWN(get('cage')?.value)) }); }); return rows.map(r=>({...r,net:Math.max(0,r.gross-r.cage),avg:r.birds>0?Math.max(0,r.gross-r.cage)/r.birds:0})); } function svpWeighingTotals(){ return svpGetWeighingRows().reduce((a,r)=>{ a.cages+=r.cages;a.birds+=r.birds;a.gross+=r.gross;a.cage+=r.cage;a.net+=r.net;return a; },{cages:0,birds:0,gross:0,cage:0,net:0}); } function svpRefreshExtraWeighCards(){ document.querySelectorAll('#svpAdditionalWeighings .svp-extra-weigh-card').forEach((card,i)=>{ const get=k=>card.querySelector(`[data-svp-weigh="${k}"]`); const birds=Math.max(0,svpWN(get('birds')?.value)),gross=Math.max(0,svpWN(get('gross')?.value)),cage=Math.max(0,svpWN(get('cage')?.value)); const net=Math.max(0,gross-cage),avg=birds>0?net/birds:0; const title=card.querySelector('[data-svp-title]');if(title)title.textContent='⚖️ கூடுதல் எடை '+(i+2); const ne=card.querySelector('[data-svp-net]');if(ne)ne.textContent=net.toFixed(3)+' kg'; const av=card.querySelector('[data-svp-avg]');if(av)av.textContent=avg.toFixed(3)+' kg'; }); const t=svpWeighingTotals(),e=document.getElementById('svpWeighingTotalsMini'); if(e)e.textContent=`மொத்தம்: ${t.cages} கூண்டு • ${t.birds} கோழி • நிகர எடை ${t.net.toFixed(3)} kg`; } function svpAddAnotherWeighing(){ const wrap=document.getElementById('svpAdditionalWeighings');if(!wrap)return; const id=++svpExtraWeighSeq,card=document.createElement('div');card.className='svp-extra-weigh-card';card.dataset.svpId=id; card.innerHTML=`
⚖️ கூடுதல் எடை
நிகர எடை / Net Weight — Auto0.000 kg
Average Weight / Bird — Auto0.000 kg
`; wrap.appendChild(card);svpRefreshExtraWeighCards(); setTimeout(()=>card.querySelector('[data-svp-weigh="cages"]')?.focus(),30); window.svpLastSavedBill=null; } function svpRemoveExtraWeighing(id){ document.querySelector(`#svpAdditionalWeighings .svp-extra-weigh-card[data-svp-id="${id}"]`)?.remove(); calc();window.svpLastSavedBill=null; } function calc(){ // V58: normal billing uses simple balance only; old-payment adjustment is disabled. const legacyAdj=document.getElementById('oldPaymentAdjustment'); if(legacyAdj && legacyAdj.value!=='0')legacyAdj.value='0'; const t=svpWeighingTotals(),birds=t.birds,gross=t.gross,cage=t.cage,net=t.net,rate=num('rate'); const previous=Math.max(0,num('previousBalance')); let oldAdjustment=Math.max(0,num('oldPaymentAdjustment')); if(oldAdjustment>previous){oldAdjustment=previous;const el=document.getElementById('oldPaymentAdjustment');if(el)el.value=money(oldAdjustment);} const received=Math.max(0,num('receivedAmount')); const adjustedOld=Math.max(0,previous-oldAdjustment); const avg=birds>0?net/birds:0,amount=net*rate,totalDue=adjustedOld+amount,remaining=Math.max(0,totalDue-received); document.getElementById('net').textContent=net.toFixed(3)+' kg'; document.getElementById('avg').textContent=avg.toFixed(3)+' kg'; document.getElementById('amount').textContent=money(amount); const today=document.getElementById('todayBillAmount');if(today)today.value=money(amount); const adj=document.getElementById('adjustedOldBalance');if(adj)adj.value=money(adjustedOld); document.getElementById('totalDue').value=money(totalDue); document.getElementById('remainingBalance').textContent='₹'+money(remaining); svpRefreshExtraWeighCards(); } function updateBillNo(){document.getElementById('billNo').textContent='Bill #'+(getBills().length+1)} function saveBill(){ try{ if(!isOwner() && !selectedCustomerId){ alert('Billing செய்ய existing Customer-ஐ தேர்வு செய்யுங்கள்.'); return; } const bills=getBills(); const weighingRows=svpGetWeighingRows(),wt=svpWeighingTotals(); const birds=wt.birds,gross=wt.gross,cage=wt.cage,net=wt.net,rate=Math.max(0,num('rate')); const avg=birds>0?net/birds:0,amount=net*rate,totalCages=wt.cages; if(birds<=0){alert('கோழி எண்ணிக்கை 0-க்கு மேல் இருக்க வேண்டும்.');return;} if(net<=0){alert('Net Weight 0-க்கு மேல் இருக்க வேண்டும். Gross / Cage weight சரிபார்க்கவும்.');return;} if(rate<=0){alert('Rate / kg பதிவு செய்யுங்கள்.');return;} const previousBalance=Math.max(0,num('previousBalance')); let oldPaymentAdjustment=Math.max(0,num('oldPaymentAdjustment')); if(oldPaymentAdjustment>previousBalance)oldPaymentAdjustment=previousBalance; const adjustedOldBalance=Math.max(0,previousBalance-oldPaymentAdjustment); const receivedAmount=Math.max(0,num('receivedAmount')); const totalDue=adjustedOldBalance+amount; const remainingBalance=Math.max(0,totalDue-receivedAmount); const dt=getDateTimeParts(); const bill={no:bills.length+1,date:dt.date,time:dt.time,generatedAt:dt.full, customerId:selectedCustomerId||document.getElementById('billingCustomerId').value.trim(), customer:document.getElementById('customer').value.trim()||'Walk-in', phone:document.getElementById('customerPhone').value.trim(), email:document.getElementById('billingEmail').value.trim(), whatsapp:document.getElementById('billingWhatsApp').value.trim()||document.getElementById('customerPhone').value.trim(), branchId:(document.getElementById('billingBranchSelect')?.value||'MAIN'), branchName:(document.getElementById('billingBranchSelect')?.selectedOptions?.[0]?.textContent||'MAIN - Main Store'), product:document.getElementById('product').value, birds,gross,cage,net,avg,rate,amount,totalCages,weighingRows,previousBalance,oldPaymentAdjustment,adjustedOldBalance,receivedAmount,totalDue,remainingBalance, pay:document.getElementById('pay').value, buyerName:document.getElementById('buyerName').value.trim(), supervisorName:document.getElementById('supervisorName').value.trim(), driverName:document.getElementById('driverName').value.trim(), supervisorId:(document.getElementById('useSupervisor')?.checked ? document.getElementById('supervisorSelect')?.value : '') || '', driverId:(document.getElementById('useDriver')?.checked ? document.getElementById('driverSelect')?.value : '') || '', createdById:localStorage.getItem('svp_staff_id') || OWNER_USER.staffId, createdByName:currentUserName || OWNER_USER.name, createdByRole:currentRole || 'owner', weightProofKey:'bill_'+(bills.length+1)+'_'+Date.now() }; bills.unshift(bill); try{localStorage.setItem('svp_bills',JSON.stringify(bills));} catch(e){alert('Bill save ஆகவில்லை. Phone storage/local data space நிரம்பியிருக்கலாம். Data backup செய்து பழைய unnecessary data/images clear செய்யவும்.');return;} window.svpLastSavedBill=bill; if(typeof svpSavePendingWeightProof==='function'){ try{Promise.resolve(svpSavePendingWeightProof(bill.weightProofKey,bill)).catch(e=>console.warn('Weight proof save failed',e));}catch(e){console.warn(e)} } if(bill.customerId){ let customers=getCustomers(),i=customers.findIndex(c=>c.id===bill.customerId); if(i>=0){ customers[i].currentBalance=remainingBalance;setCustomers(customers); let led=getLedger(); led.unshift({customerId:bill.customerId,date:bill.generatedAt,type:'bill',billNo:String(bill.no),debit:amount,credit:oldPaymentAdjustment+receivedAmount,balance:remainingBalance,note:bill.product+` | Old Adj ₹${money(oldPaymentAdjustment)} | Current Pay ₹${money(receivedAmount)}`}); setLedger(led); } } const msg=document.getElementById('msg'); if(msg)msg.textContent='✓ Bill #'+bill.no+' saved successfully • '+bill.generatedAt+' • Final Balance ₹'+money(remainingBalance); const safe=fn=>{try{if(typeof fn==='function')fn()}catch(e){console.warn(e)}}; safe(updateBillNo);safe(renderBills);safe(loadCustomerOptions); if(isOwner())safe(calculateDailyLedger); if(selectedCustomerId){ try{document.getElementById('customerSelect').value=selectedCustomerId;selectCustomer();}catch(e){console.warn(e)} } const sendSettings=getSendSettings(); if(isOwner() && sendSettings.autoSms==='on'&&bill.phone){ setTimeout(()=>sendSmsTo(bill.phone,buildBillMessage(bill),{type:'bill',bill,ref:'BILL-'+bill.no}).then(r=>{smsLogAdd('bill',bill.customerId,bill.customer,bill.phone,'sent',r.mode,smsResponseRef(r));if(msg)msg.textContent+=' • '+(r&&String(r.mode).startsWith('device')?'SMS app opened':'SMS accepted')}).catch(e=>{smsLogAdd('bill',bill.customerId,bill.customer,bill.phone,'failed',smsSettings().smsProvider,e.message);if(msg)msg.textContent+=' • SMS: '+e.message}),100); } if(isOwner() && sendSettings.autoWhatsApp==='on'&&(bill.whatsapp||bill.phone))setTimeout(()=>sendBillWhatsApp(bill),300); if(typeof shopRenderAll==='function') setTimeout(()=>shopRenderAll(),50); return bill; }catch(e){ console.error('Save Bill error',e); const msg=document.getElementById('msg');if(msg)msg.textContent='Bill save error: '+e.message; alert('Bill save error: '+e.message); } } function buildCurrentBillPreviewData(){ const dt=getDateTimeParts(); const weighingRows=svpGetWeighingRows(),wt=svpWeighingTotals(); const birds=wt.birds; const gross=wt.gross; const cage=wt.cage; const net=wt.net; const avg=birds>0?net/birds:0; const totalCages=wt.cages; const rate=num('rate'); const amount=net*rate; const previousBalance=Math.max(0,num('previousBalance')); const oldPaymentAdjustment=Math.min(previousBalance,Math.max(0,num('oldPaymentAdjustment'))); const adjustedOldBalance=Math.max(0,previousBalance-oldPaymentAdjustment); const receivedAmount=Math.max(0,num('receivedAmount')); const totalDue=adjustedOldBalance+amount; const remainingBalance=Math.max(0,totalDue-receivedAmount); const supId=document.getElementById('useSupervisor')?.checked ? document.getElementById('supervisorSelect')?.value : ''; const drvId=document.getElementById('useDriver')?.checked ? document.getElementById('driverSelect')?.value : ''; const sup=staffById(supId); const drv=staffById(drvId); return { no:getBills().length+1, date:dt.date, time:dt.time, generatedAt:dt.full, customerId:selectedCustomerId||document.getElementById('billingCustomerId').value.trim(), customer:document.getElementById('customer').value.trim()||'Walk-in', phone:document.getElementById('customerPhone').value.trim(), branchId:(document.getElementById('billingBranchSelect')?.value||'MAIN'), branchName:(document.getElementById('billingBranchSelect')?.selectedOptions?.[0]?.textContent||'MAIN - Main Store'), product:document.getElementById('product').value, birds,gross,cage,net,avg,rate,amount,totalCages,weighingRows, previousBalance,oldPaymentAdjustment,adjustedOldBalance,receivedAmount,totalDue,remainingBalance, pay:document.getElementById('pay').value, buyerName:document.getElementById('buyerName').value.trim(), supervisorName:sup ? `${sup.id} - ${sup.name}` : document.getElementById('supervisorName').value.trim(), driverName:drv ? `${drv.id} - ${drv.name}` : document.getElementById('driverName').value.trim(), createdById:localStorage.getItem('svp_staff_id')||OWNER_USER.staffId, createdByName:currentUserName||OWNER_USER.name, createdByRole:currentRole||'owner' }; } function getPrintableBillData(){ const justSaved=window.svpLastSavedBill; if(justSaved)return justSaved; return buildCurrentBillPreviewData(); } function billHtml(b,printMode=false){ const metaClass=printMode?'staff-print-meta':'staff-preview-grid'; const tableClass=printMode?'staff-print-table':'staff-preview-table'; const summary = `
இன்றைய பில் / Bill Amount₹${money(b.amount)}
முன்பாக்கி / Previous Balance₹${money(b.previousBalance)}
மொத்தம் / Total Due₹${money(b.totalDue)}
இன்றைய வரவு / Current Payment- ₹${money(b.receivedAmount)}
ஃபைனல் மீதி பாக்கி / Final Balance₹${money(b.remainingBalance)}
`; const qrHtml=qrBlockHtml(b,printMode); return `
${printMode?'':`
Bill #${b.no}${escapeHtml(b.generatedAt||'')}
`}
Bill No: #${escapeHtml(b.no)}
Date: ${escapeHtml(b.date||'')}
Time: ${escapeHtml(b.time||'')}
Customer ID: ${escapeHtml(b.customerId||'-')}
Customer: ${escapeHtml(b.customer||'')}
Phone: ${escapeHtml(b.phone||'')}
Selling Shop: ${escapeHtml(b.branchName||b.branchId||'MAIN - Main Store')}
Created By: ${escapeHtml((b.createdById||'')+' - '+(b.createdByName||''))}
Supervisor: ${escapeHtml(b.supervisorName||'-')}
Driver: ${escapeHtml(b.driverName||'-')}
${(()=>{ const rows=Array.isArray(b.weighingRows)&&b.weighingRows.length?b.weighingRows:[{cages:b.totalCages||0,birds:b.birds||0,gross:b.gross||0,cage:b.cage||0,net:b.net||0,avg:b.avg||0}]; return rows.map((r,i)=>{ const rn=Number(r.net!=null?r.net:Math.max(0,Number(r.gross||0)-Number(r.cage||0))); const ra=Number(r.avg!=null?r.avg:(Number(r.birds||0)>0?rn/Number(r.birds||0):0)); return ``; }).join(''); })()}
#ProductCagesBirdsGross kgCage kgNet kgAvg/BirdRateAmount
${i+1}${escapeHtml(b.product||'')}${Number(r.cages||0)}${Number(r.birds||0)}${Number(r.gross||0).toFixed(3)}${Number(r.cage||0).toFixed(3)}${rn.toFixed(3)}${ra.toFixed(3)}₹${money(b.rate)}₹${money(rn*Number(b.rate||0))}
${summary} ${qrHtml} ${printMode?`
வாங்குபவர் / Customer Signature
Supervisor Signature
Driver / Sales Staff Signature
`:''}
`; } function previewCurrentBill(){ try{ const b=buildCurrentBillPreviewData(); const body=document.getElementById('staffBillPreviewBody'); if(body) body.innerHTML=billHtml(b,false); const card=document.getElementById('staffBillPreviewCard'); if(card){card.classList.remove('hidden');card.scrollIntoView({behavior:'smooth',block:'start'});} }catch(e){console.error('Bill Preview error',e);alert('Bill Preview error: '+e.message);} } function printCurrentBill(){ const b=getPrintableBillData(); const holder=document.getElementById('staffPrintableBillBody'); const printable=document.getElementById('staffPrintableBill'); if(!holder || !printable){ alert('Official bill print area not found'); return; } holder.innerHTML=billHtml(b,true); const qrs=getQrSettings(); const topQr=printable.querySelector('.staff-print-top-qr'); const sourceQr=holder.querySelector('.staff-print-qr-box'); if(topQr){ topQr.innerHTML=''; if(qrs.showQr==='on' && sourceQr){ topQr.appendChild(sourceQr.cloneNode(true)); } } const qrClass= (qrs.showQr==='on' && qrs.position==='bottom-right') ? 'qr-bottom-right' : 'qr-top-right'; const allCss=[...document.querySelectorAll('style')] .map(x=>x.innerHTML) .join('\n'); const html= ''+ ''+ ''+ ''+ ''+ ''+ ''+ '
'+ printable.innerHTML+ '
'+ ''; if(window.AndroidPrint && typeof AndroidPrint.printHtml==='function'){ AndroidPrint.printHtml(html); return; } const old=document.getElementById('svpPrintOnlyRoot'); if(old) old.remove(); const root=document.createElement('div'); root.id='svpPrintOnlyRoot'; root.className=qrClass; root.innerHTML=printable.innerHTML; document.body.appendChild(root); document.body.classList.add('svp-print-only'); const cleanup=function(){ document.body.classList.remove('svp-print-only'); const r=document.getElementById('svpPrintOnlyRoot'); if(r) r.remove(); }; window.addEventListener('afterprint',cleanup,{once:true}); setTimeout(function(){ window.print(); setTimeout(cleanup,4000); },200); } function getSalesReturns(){ try{return JSON.parse(localStorage.getItem('svp_sales_returns')||'[]')}catch(e){return[]} } function setSalesReturns(v){ localStorage.setItem('svp_sales_returns',JSON.stringify(v)); } function getBillByNo(no){ return getBills().find(b=>String(b.no)===String(no))||null; } function getCustomerForBill(bill){ return getCustomers().find(c=>c.id===bill?.customerId)||null; } function nextReturnNo(){ const list=getSalesReturns(); let max=0; list.forEach(r=>{ const m=String(r.returnNo||'').match(/(\d+)$/); if(m)max=Math.max(max,parseInt(m[1])); }); return 'RET-'+String(max+1).padStart(4,'0'); } function loadSalesCorrectionOptions(){ const sel=document.getElementById('salesCorrectionBill'); if(!sel)return; const bills=getBills(); sel.innerHTML=''+ bills.map(b=>``).join(''); } function toggleSalesCorrectionAction(){ const action=document.getElementById('salesCorrectionAction').value; document.getElementById('modifyBillBox').classList.toggle('hidden',action!=='modify'); document.getElementById('returnBillBox').classList.toggle('hidden',action!=='return'); } function loadSalesCorrectionBill(){ if(!ownerOnly())return; const no=document.getElementById('salesCorrectionBill').value; const b=getBillByNo(no); if(!b)return; document.getElementById('corrBirds').value=Number(b.birds||0); document.getElementById('corrNetKg').value=Number(b.net||0).toFixed(3); document.getElementById('corrRate').value=Number(b.rate||0).toFixed(2); document.getElementById('corrReceived').value=Number(b.receivedAmount||0).toFixed(2); document.getElementById('returnBirds').value=Number(b.birds||0); document.getElementById('returnKg').value=Number(b.net||0).toFixed(3); document.getElementById('returnRate').value=Number(b.rate||0).toFixed(2); document.getElementById('returnRefund').value='0'; calculateCorrectionPreview(); calculateReturnPreview(); } function calculateCorrectionPreview(){ const no=document.getElementById('salesCorrectionBill')?.value; const b=getBillByNo(no); if(!b)return; const birds=Number(document.getElementById('corrBirds').value||0); const netKg=Number(document.getElementById('corrNetKg').value||0); const rate=Number(document.getElementById('corrRate').value||0); const received=Number(document.getElementById('corrReceived').value||0); const amount=netKg*rate; // Rebuild from the original bill's adjusted old balance. const baseOld=(b.adjustedOldBalance!=null)?Number(b.adjustedOldBalance||0):Math.max(0,Number(b.previousBalance||0)-Number(b.oldPaymentAdjustment||0)); const balance=Math.max(0,baseOld+amount-received); document.getElementById('corrAmount').value=money(amount); document.getElementById('corrBalance').value=money(balance); } function calculateReturnPreview(){ const no=document.getElementById('salesCorrectionBill')?.value; const b=getBillByNo(no); if(!b)return; const kg=Math.min(Number(document.getElementById('returnKg').value||0),Number(b.net||0)); const rate=Number(document.getElementById('returnRate').value||0); const refund=Number(document.getElementById('returnRefund').value||0); const amount=kg*rate; // Return reduces customer's outstanding balance. Any explicit cash refund does not // reduce outstanding further because that money is paid back to the customer. const customer=getCustomerForBill(b); const currentBalance=Number(customer?.currentBalance||0); const adjusted=Math.max(0,currentBalance-amount+refund); document.getElementById('returnAmount').value=money(amount); document.getElementById('returnBalance').value=money(adjusted); } function saveBillCorrection(){ if(!ownerOnly())return; const no=document.getElementById('salesCorrectionBill').value; const original=getBillByNo(no); if(!original){alert('Bill தேர்வு செய்யுங்கள்');return} if((original.status||'active')==='returned'){alert('Returned bill-ஐ correction செய்ய முடியாது');return} const reason=document.getElementById('salesCorrectionReason').value.trim()||'Bill correction'; const birds=Number(document.getElementById('corrBirds').value||0); const net=Number(document.getElementById('corrNetKg').value||0); const rate=Number(document.getElementById('corrRate').value||0); const amount=net*rate; const received=Number(document.getElementById('corrReceived').value||0); const baseOld=(original.adjustedOldBalance!=null)?Number(original.adjustedOldBalance||0):Math.max(0,Number(original.previousBalance||0)-Number(original.oldPaymentAdjustment||0)); const balance=Math.max(0,baseOld+amount-received); let bills=getBills(); const originalIndex=bills.findIndex(b=>String(b.no)===String(no)); if(originalIndex<0)return; // Mark original record; keep it for audit. bills[originalIndex].status='corrected'; bills[originalIndex].correctedAt=getDateTimeParts().full; bills[originalIndex].correctionReason=reason; const newNo=Math.max(0,...bills.map(b=>Number(b.no)||0))+1; const corrected={ ...original, no:newNo, status:'active', correctionOf:Number(original.no), correctionReason:reason, birds, net, avg:birds>0?net/birds:0, rate, amount, receivedAmount:received, adjustedOldBalance:baseOld, totalDue:baseOld+amount, remainingBalance:balance, generatedAt:getDateTimeParts().full, date:getDateTimeParts().date, time:getDateTimeParts().time, createdById:localStorage.getItem('svp_staff_id')||OWNER_USER.staffId, createdByName:currentUserName||OWNER_USER.name, createdByRole:'owner' }; bills.unshift(corrected); localStorage.setItem('svp_bills',JSON.stringify(bills)); // Reconcile customer balance by removing original bill effect and applying corrected bill effect. let customers=getCustomers(); const ci=customers.findIndex(c=>c.id===original.customerId); if(ci>=0){ const cur=Number(customers[ci].currentBalance||0); const originalNetEffect=Number(original.amount||0)-Number(original.oldPaymentAdjustment||0)-Number(original.receivedAmount||0); const correctedNetEffect=amount-Number(original.oldPaymentAdjustment||0)-received; customers[ci].currentBalance=Math.max(0,cur-originalNetEffect+correctedNetEffect); setCustomers(customers); let led=getLedger(); led.unshift({ customerId:original.customerId, date:getDateTimeParts().full, type:'correction', billNo:String(newNo), debit:Math.max(0,correctedNetEffect-originalNetEffect), credit:Math.max(0,originalNetEffect-correctedNetEffect), balance:customers[ci].currentBalance, note:`Correction of Bill #${original.no}: ${reason}` }); setLedger(led); } document.getElementById('salesCorrectionMsg').textContent=`Bill #${original.no} corrected. New Bill #${newNo} created.`; loadSalesCorrectionOptions(); renderBills(); loadCustomerOptions(); if(isOwner()) calculateDailyLedger(); } function saveSalesReturn(){ if(!ownerOnly())return; const no=document.getElementById('salesCorrectionBill').value; const b=getBillByNo(no); if(!b){alert('Bill தேர்வு செய்யுங்கள்');return} const reason=document.getElementById('salesCorrectionReason').value.trim()||'Sales return'; const returnBirds=Math.min(Number(document.getElementById('returnBirds').value||0),Number(b.birds||0)); const returnKg=Math.min(Number(document.getElementById('returnKg').value||0),Number(b.net||0)); const returnRate=Number(document.getElementById('returnRate').value||0); const returnAmount=returnKg*returnRate; const refund=Number(document.getElementById('returnRefund').value||0); if(returnKg<=0 && returnBirds<=0){alert('Return qty / kg enter செய்யுங்கள்');return} const returnNo=nextReturnNo(); const now=getDateTimeParts(); const rec={ returnNo, originalBillNo:Number(b.no), customerId:b.customerId||'', customer:b.customer||'', phone:b.phone||'', date:now.date, time:now.time, generatedAt:now.full, birds:returnBirds, kg:returnKg, rate:returnRate, amount:returnAmount, refund, reason, createdById:localStorage.getItem('svp_staff_id')||OWNER_USER.staffId, createdByName:currentUserName||OWNER_USER.name }; const returns=getSalesReturns(); returns.unshift(rec); setSalesReturns(returns); // Update original bill return totals/status let bills=getBills(); const bi=bills.findIndex(x=>String(x.no)===String(b.no)); if(bi>=0){ bills[bi].returnedBirds=Number(bills[bi].returnedBirds||0)+returnBirds; bills[bi].returnedKg=Number(bills[bi].returnedKg||0)+returnKg; bills[bi].returnedAmount=Number(bills[bi].returnedAmount||0)+returnAmount; const fullyReturned = bills[bi].returnedKg >= Number(bills[bi].net||0)-0.0005 || bills[bi].returnedBirds >= Number(bills[bi].birds||0); bills[bi].status=fullyReturned?'returned':'partially-returned'; localStorage.setItem('svp_bills',JSON.stringify(bills)); } // Reduce customer balance by returned goods value, then add any cash refund. let customers=getCustomers(); const ci=customers.findIndex(c=>c.id===b.customerId); if(ci>=0){ const cur=Number(customers[ci].currentBalance||0); customers[ci].currentBalance=Math.max(0,cur-returnAmount+refund); setCustomers(customers); let led=getLedger(); led.unshift({ customerId:b.customerId, date:now.full, type:'return', billNo:String(b.no), debit:refund, credit:returnAmount, balance:customers[ci].currentBalance, note:`${returnNo} - Return from Bill #${b.no}: ${reason}` }); setLedger(led); } document.getElementById('salesCorrectionMsg').textContent=`${returnNo} saved for Bill #${b.no}. Return amount ₹${money(returnAmount)}.`; buildReturnPrint(rec); renderBills(); loadCustomerOptions(); loadSalesCorrectionOptions(); if(isOwner()) calculateDailyLedger(); } function buildReturnPrint(r){ const el=document.getElementById('salesReturnPrintBody'); if(!el)return; el.innerHTML=`
Return No: ${escapeHtml(r.returnNo)}
Original Bill: #${escapeHtml(r.originalBillNo)}
Date: ${escapeHtml(r.generatedAt)}
Customer: ${escapeHtml(r.customer)}
Phone: ${escapeHtml(r.phone)}
Return BirdsReturn WeightRateReturn AmountRefund
${Number(r.birds||0)} ${Number(r.kg||0).toFixed(3)} kg ₹${money(r.rate)} ₹${money(r.amount)} ₹${money(r.refund)}
Reason: ${escapeHtml(r.reason||'')}
Customer Signature
Owner / Admin Signature
Stock Received By
`; } function printLatestReturn(){ if(!ownerOnly())return; const list=getSalesReturns(); if(!list.length){alert('Return note இல்லை');return} buildReturnPrint(list[0]); document.body.classList.add('print-return'); document.getElementById('salesReturnPrint').classList.remove('hidden'); setTimeout(()=>{ window.print(); setTimeout(()=>{ document.body.classList.remove('print-return'); document.getElementById('salesReturnPrint').classList.add('hidden'); },500); },100); } function renderBills(){ const bills=getBills(),el=document.getElementById('billList'); if(!bills.length){el.innerHTML='

No saved bills

';return} el.innerHTML=''+ bills.slice(0,50).map(b=>{ const st=b.status||'active'; const cls=st==='active'?'status-active':(st==='corrected'?'status-corrected':'status-returned'); return ``; }).join('')+'
BillStatusDateCustomerCreated BySupervisorDriverBillOld AdjCurrent PayBalanceAction
#${b.no} ${escapeHtml(st)} ${escapeHtml(b.generatedAt||b.date||'')} ${escapeHtml(b.customerId||'')} ${escapeHtml(b.customer||'')} ${escapeHtml((b.createdById||'')+' '+(b.createdByName||''))} ${escapeHtml(b.supervisorName||'')} ${escapeHtml(b.driverName||'')} ₹${money(b.amount)} ₹${money(b.oldPaymentAdjustment||0)} ₹${money(b.receivedAmount)} ₹${money(b.remainingBalance)}
'; } if(localStorage.getItem('svp_logged')==='1'){ currentRole=localStorage.getItem('svp_role')||''; currentUserName=localStorage.getItem('svp_user_name')||''; showApp(); }else if(localStorage.getItem('svp_customer_logged')==='1'){ const cid=localStorage.getItem('svp_customer_id')||''; const cc=getCustomers().find(x=>x.id===cid); if(cc) showCustomerPortal(cc); else { localStorage.removeItem('svp_customer_logged'); localStorage.removeItem('svp_customer_id'); calc();updateClock(); } }else{calc();updateClock()} setInterval(updateClock,1000);

📊 Daily Data Chart / தினசரி டேட்டா சார்ட்

Stock Formula: Opening Stock + Purchase − Sales = Closing Stock
Weight Loss: Expected Closing Weight − Actual Closing Weight

Details

📷 Weight Photo Proof

🐔 Gross Weight
🧺 Empty Cage
Staff Document
Staff document