Future Link Broadband (Private) Limited, Uch Sharif

Uch Sharif  |  رابطہ نمبر: 0301 3399665

Offline Management System — Professional Management Dashboard 7.0

`); w.document.close(); } function todayLocalDate(){ const d=new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; } function formatDateDMY(value){ if(!/^\\d{4}-\\d{2}-\\d{2}$/.test(String(value||"")))return value||"-"; const [y,m,d]=value.split("-"); return `${d}-${m}-${y}`; } function renewPackage(id){ const c=db.customers.find(x=>x.id===id);if(!c)return; const activationDate=todayLocalDate(); const expiryDate=addDaysToDate(activationDate,30); if(!expiryDate)return alert("Could not calculate package expiry date."); c.packageActive=true; c.lastActivatedAt=new Date().toISOString(); c.packageActivationDate=activationDate; c.expiryDate=expiryDate; if(!save())return; const msg=`Assalam-o-Alaikum ${c.name}, your ${c.packageName} package has been activated/renewed on ${formatDateDMY(activationDate)} and will expire on ${formatDateDMY(expiryDate)} (30 days). Please clear your package payment if it is still pending. — Future Link Broadband (Private) Limited, Uch Sharif.`; if(confirm(`Package activated on ${formatDateDMY(activationDate)}. Expiry date: ${formatDateDMY(expiryDate)}. Open WhatsApp message to notify the customer?`)) { const url=messageUrl(c.phone,msg);if(url)window.open(url,"_blank");else alert("Customer mobile number is missing."); } customers(); } function reminders(){ const today=new Date();today.setHours(0,0,0,0); const target=new Date(today);target.setDate(target.getDate()+2); const targetKey=target.toISOString().slice(0,10); const due=db.customers.filter(c=>(c.accountStatus||"Active")==="Active"&&c.expiryDate===targetKey); show(`

Package Expiry Reminders

Customers whose package expires in exactly two days are listed here. This page must be opened to prepare reminders; automatic background sending is not enabled in this offline file.

Reminder date: ${targetKey}

${table(["Customer","Mobile","Package","Expiry Date","Action"],due.map(c=>`${esc(c.name)}${esc(c.phone||"-")}${esc(c.packageName||"-")}${esc(formatDateDMY(c.expiryDate))} `))}
`); } function sendExpiryMessage(id,channel){ const c=db.customers.find(x=>x.id===id);if(!c)return; const msg=`Assalam-o-Alaikum ${c.name}, your ${c.packageName} internet package is due to expire on ${c.expiryDate}. Please renew your package in time to avoid interruption. — Future Link Broadband (Private) Limited, Uch Sharif.`; if(channel==="whatsapp"){ const url=messageUrl(c.phone,msg);if(!url)return alert("Customer mobile number is missing.");window.open(url,"_blank"); }else{ const digits=String(c.phone||"").replace(/[^\d+]/g,"");if(!digits)return alert("Customer mobile number is missing."); window.location.href="sms:"+digits+"?body="+encodeURIComponent(msg); } } function expenses(){ const month=window.expenseMonth||monthNow(); const category=window.expenseCategory||"All"; const categories=["Rent","Salaries","Electricity","Internet / Bandwidth","Fuel / Transport","Maintenance","Office Supplies","Marketing","Other"]; const rows=db.expenses.filter(x=>(!month||String(x.date||"").slice(0,7)===month)&&(category==="All"||x.category===category)).slice().sort((a,b)=>String(b.date).localeCompare(String(a.date))); const total=rows.reduce((a,x)=>a+Number(x.amount||0),0); show(`

Expenses Management

Record business operating expenses separately from vendor inventory purchases. Each entry is saved with date, category and payment details.

Expense Report

Total Expenses: Rs. ${money(total)}

${table(["Date","Category","Description","Paid To","Amount","Mode","Notes","Action"],rows.map(x=>`${esc(x.date)}${esc(x.category)}${esc(x.description)}${esc(x.payee||"-")}Rs. ${money(x.amount)}${esc(x.mode||"")}${esc(x.note||"-")}`))}
`) } function addExpense(e){ e.preventDefault(); const date=$("expenseDate").value,category=$("expenseCat").value,description=$("expenseDesc").value.trim(),amount=Number($("expenseAmount").value); if(!/^\d{4}-\d{2}-\d{2}$/.test(date)||!description||!Number.isFinite(amount)||amount<=0)return alert("Enter a valid date, description and amount."); db.expenses.push({id:uid(),date,category,description,payee:$("expensePayee").value.trim(),amount,mode:$("expenseMode").value,note:$("expenseNote").value.trim(),createdAt:new Date().toISOString()}); if(save())expenses(); } function deleteExpense(id){ const x=db.expenses.find(x=>x.id===id);if(!x)return; if(!confirm("Delete this expense record? This cannot be undone unless you restore a backup."))return; db.expenses=db.expenses.filter(x=>x.id!==id);if(save())expenses(); } function backup(){ show(`

Backup & Restore

Data is saved in this browser on this computer. Make regular backups, especially before replacing files or clearing browser data.

Restore will replace current data. Download a backup first.

`) } function downloadBackup(){ const blob=new Blob([JSON.stringify(db,null,2)],{type:"application/json"}); const url=URL.createObjectURL(blob); const a=document.createElement("a"); a.href=url; a.download="FutureLink_Backup_"+new Date().toISOString().slice(0,10)+".json"; a.click(); setTimeout(()=>URL.revokeObjectURL(url),1000) } async function restoreBackup(){ const f=$("restoreFile").files[0]; if(!f)return alert("Select backup JSON first."); let data; try{data=JSON.parse(await f.text())} catch(e){return alert("Invalid JSON backup.")} if(!data||!["packages","customers","stock","bills"] .every(k=>Array.isArray(data[k]))) return alert("Backup is not a valid Future Link Broadband (Private) Limited, Uch Sharif database."); if(!confirm("Replace ALL current records with this backup?"))return; db=data; for(const k of ["areas","vendors","vendorPayments","stockPurchases","inventoryItems"])if(!Array.isArray(db[k]))db[k]=[]; save(); menu(); packages(); alert("Backup restored.") } function reportStatus(b){ if(!b)return "No Invoice"; const amount=Number(b.amount||0); const paid=Number(b.paidAmount??(b.status==="Paid"?amount:0))+Number(b.collectionDiscount||0); if(amount>0 && paid>=amount-0.001)return "Paid"; if(paid>0 && paid{ const match=!query||String(row.dataset.customerSearch||"").includes(query); row.style.display=match?"":"none"; if(match)shown++; }); const note=tableSection.querySelector(".customer-search-count"); if(note)note.textContent=`Showing ${shown} matching customer records.`; } function reports(){ const month=window.reportMonth||monthNow(); const filter=window.reportFilter||"All"; const collectionMonth=window.collectionMonth||monthNow(); const collectorFilter=window.collectorFilter||"All"; const search=(window.reportSearch||"").toLowerCase(); // Collector totals are based on the actual payment date, not the invoice month. const collectorNames=[...new Set(db.bills.flatMap(b=>(b.payments||[]).map(p=>String(p.collector||"").trim()).filter(Boolean)))].sort((a,b)=>a.localeCompare(b)); const collectorStats=collectorNames.map(name=>{ const payments=db.bills.flatMap(b=>(b.payments||[]).filter(p=>String(p.collector||"").trim()===name && String(p.date||"").slice(0,7)===collectionMonth)); const received=payments.reduce((sum,p)=>sum+Number(p.amount||0),0); const discount=payments.reduce((sum,p)=>sum+Number(p.discount||0),0); return {name,received,discount,total:received+discount,count:payments.length}; }); const activeCollectorStats=collectorStats.filter(x=>x.count>0); const selectedCollectorStats=collectorFilter==="All"?activeCollectorStats:collectorStats.filter(x=>x.name===collectorFilter); const rows=db.customers.map(c=>{ const b=db.bills.find(x=>x.customerId===c.id&&x.month===month); const status=reportStatus(b); const paid=b?Number(b.paidAmount??(b.status==="Paid"?b.amount:0))+Number(b.collectionDiscount||0):0; const customerPayments=db.bills.filter(x=>x.customerId===c.id).flatMap(x=>x.payments||[]) .filter(p=>String(p.date||"").slice(0,7)===collectionMonth && (collectorFilter==="All" || String(p.collector||"").trim()===collectorFilter)); const collectionReceivedNow=customerPayments.reduce((sum,p)=>sum+Number(p.amount||0),0); const collectionDiscountNow=customerPayments.reduce((sum,p)=>sum+Number(p.discount||0),0); const hasCollectorPayment=collectorFilter==="All" || customerPayments.length>0; return {c,b,status,paid,hasCollectorPayment,collectionReceivedNow,collectionDiscountNow}; }).filter(x=>(filter==="All"||x.status===filter) && ((window.accountStatusFilter||"All")==="All" || (x.c.accountStatus||"Active")===(window.accountStatusFilter||"All")) && x.hasCollectorPayment && (!search || [x.c.name,x.c.phone,x.c.customerCode,x.c.area,x.c.packageName] .some(v=>String(v||"").toLowerCase().includes(search)))); const counts={"All":db.customers.length,"Paid":0,"Unpaid":0,"Partial":0,"No Invoice":0}; let billed=0,recovered=0,outstanding=0; db.customers.forEach(c=>{ const b=db.bills.find(x=>x.customerId===c.id&&x.month===month); const s=reportStatus(b);counts[s]++; if(b){const a=Number(b.amount||0),p=Number(b.paidAmount??(b.status==="Paid"?a:0))+Number(b.collectionDiscount||0);billed+=a;recovered+=p;outstanding+=Math.max(0,a-p);} }); const collectionReceived=activeCollectorStats.reduce((a,x)=>a+x.received,0); const collectionDiscount=activeCollectorStats.reduce((a,x)=>a+x.discount,0); show(`

Customer Billing & Payment Status

Customer Account Overview

All Customers

${db.customers.length}

Active Customers

${db.customers.filter(c=>(c.accountStatus||"Active")==="Active").length}

Non-Active Customers

${db.customers.filter(c=>(c.accountStatus||"Active")==="Non-Active").length}

Terminated Customers

${db.customers.filter(c=>c.accountStatus==="Terminated").length}

${["All","Paid","Unpaid","Partial","No Invoice"].map(s=>``).join("")}
Monthly Billing

Rs. ${money(billed)}

Total Recovered

Rs. ${money(recovered)}

Outstanding

Rs. ${money(outstanding)}

Collection Person Report

Total Cash Received

Rs. ${money(collectionReceived)}

Total Collection Discounts

Rs. ${money(collectionDiscount)}

Persons With Collections

${activeCollectorStats.length}

Cash received is calculated from payment entries recorded during the selected collection month. Discounts are shown separately and are not counted as cash received.

${table(["Collection Person","Payments Received","Discount Given","Total Settled","Transactions"], selectedCollectorStats.map(x=>`${esc(x.name)}Rs. ${money(x.received)}Rs. ${money(x.discount)}Rs. ${money(x.total)}${x.count}`))}

Customer Records

Showing ${rows.length} matching customer records. Invoice status is based on billing month ${esc(month)}. Collector filter is based on payments received during ${esc(collectionMonth)}.

${table(["Customer Code & Name","Mobile","Installation Date","Area","Package","Monthly Fee","Invoice Status","Invoice No.","Billed","Received This Collection Month","Invoice Balance"], rows.map(x=>` ${esc(x.c.customerCode||"-")}
${esc(x.c.name)}
ID: ${esc(x.c.id||"-")} ${esc(x.c.phone||"-")}${esc(x.c.installDate||"-")} ${esc(x.c.area||"-")}${esc(x.c.packageName||"-")} Rs. ${money(x.c.monthlyFee)}${esc(x.status)} ${esc(x.b?.invoiceNo||"-")}Rs. ${money(x.b?.amount||0)} Rs. ${money(x.collectionReceivedNow)}Rs. ${money(Math.max(0,Number(x.b?.amount||0)-x.paid))} `))}
`); } // Local-only single-admin login. Credentials are PBKDF2-hashed in this browser's localStorage. const ADMIN_AUTH_KEY="futurelink_local_admin_auth_v1"; const ADMIN_SESSION_KEY="futurelink_local_admin_session_v1"; const ADMIN_PBKDF2_ITERATIONS=210000; const adminEncoder=new TextEncoder(); function getAdminAuth(){ try{return JSON.parse(localStorage.getItem(ADMIN_AUTH_KEY)||"null")}catch(e){return null} } function bytesToHex(bytes){return Array.from(new Uint8Array(bytes)).map(b=>b.toString(16).padStart(2,"0")).join("")} function hexToBytes(hex){const out=new Uint8Array(hex.length/2);for(let i=0;i
FL

Future Link Broadband

ایڈمن مینجمنٹ سسٹم · اُچ شریف

${inner}`; } function renderAdminSetup(){ authShell(`

ایڈمن اکاؤنٹ بنائیں

یہ پہلی مرتبہ کی ترتیب ہے۔ اپنا ایڈمن صارف نام اور پاس ورڈ بنائیں۔

اہم: یہ مقامی لاگ اِن ہے۔ پاس ورڈ اسی براؤزر/ڈیوائس میں محفوظ ہوگا۔ براؤزر کا ڈیٹا صاف کرنے سے لاگ اِن کی ترتیب ختم ہو سکتی ہے۔ یہ سرور پر مبنی سکیورٹی یا ڈیٹا انکرپشن نہیں ہے۔

`); document.getElementById("adminSetupForm").addEventListener("submit",async e=>{ e.preventDefault();const username=document.getElementById("setupUsername").value.trim();const password=document.getElementById("setupPassword").value;const confirm=document.getElementById("setupConfirm").value; if(username.length<3)return authError("صارف نام کم از کم 3 حروف کا ہونا چاہیے۔"); if(password.length<8)return authError("پاس ورڈ کم از کم 8 حروف یا اعداد پر مشتمل ہونا چاہیے۔"); if(password!==confirm)return authError("دونوں پاس ورڈ ایک جیسے نہیں ہیں۔"); try{const salt=bytesToHex(crypto.getRandomValues(new Uint8Array(16)));const hash=await deriveAdminHash(password,salt);localStorage.setItem(ADMIN_AUTH_KEY,JSON.stringify({username,salt,hash,iterations:ADMIN_PBKDF2_ITERATIONS,createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()}));sessionStorage.setItem(ADMIN_SESSION_KEY,"1");unlockAdminApp();} catch(err){authError(err.message||"اکاؤنٹ محفوظ نہیں ہو سکا۔")} }); } function renderAdminLogin(){ const record=getAdminAuth(); authShell(`

ایڈمن لاگ اِن

Future Link Management System جاری رکھنے کے لیے لاگ اِن کریں۔

یہ لاگ اِن صرف اسی براؤزر/ڈیوائس کے لیے ہے جہاں ایڈمن اکاؤنٹ بنایا گیا تھا۔ اگر آپ براؤزر کا ڈیٹا صاف کریں تو مقامی لاگ اِن دوبارہ ترتیب دینا پڑ سکتا ہے۔

`); document.getElementById("adminLoginForm").addEventListener("submit",async e=>{ e.preventDefault();const username=document.getElementById("loginUsername").value.trim();const password=document.getElementById("loginPassword").value;const latest=getAdminAuth(); try{if(!latest||username.toLowerCase()!==String(latest.username).toLowerCase()||!(await verifyAdminPassword(password,latest)))return authError("صارف نام یا پاس ورڈ درست نہیں ہے۔");sessionStorage.setItem(ADMIN_SESSION_KEY,"1");unlockAdminApp();} catch(err){authError(err.message||"لاگ اِن نہیں ہو سکا۔")} }); } function unlockAdminApp(){document.body.classList.remove("auth-locked");document.getElementById("authRoot").innerHTML=""} function initAdminAuth(){ if(sessionStorage.getItem(ADMIN_SESSION_KEY)==="1"){unlockAdminApp();return} if(getAdminAuth())renderAdminLogin();else renderAdminSetup(); } function adminLogout(){ if(!confirm("کیا آپ واقعی لاگ آؤٹ کرنا چاہتے ہیں؟"))return; sessionStorage.removeItem(ADMIN_SESSION_KEY);renderAdminLogin(); } function adminSecurity(){ const record=getAdminAuth();if(!record)return renderAdminSetup(); show(`

ایڈمن اکاؤنٹ اور پاس ورڈ

یہ تبدیلی صرف اسی براؤزر/ڈیوائس پر لاگو ہوگی۔

پاس ورڈ PBKDF2 کے ذریعے ہیش کرکے محفوظ کیا جاتا ہے۔ تاہم یہ مقامی لاگ اِن ہے، مکمل سرور سکیورٹی نہیں۔ ایپ کا ڈیٹا الگ سے مقامی اسٹوریج میں ہے اور اس فیچر سے انکرپٹ نہیں ہوتا۔

`); document.getElementById("adminChangeForm").addEventListener("submit",async e=>{ e.preventDefault();const msg=document.getElementById("changeAdminMessage");const current=document.getElementById("currentAdminPassword").value;const username=document.getElementById("changeUsername").value.trim();const next=document.getElementById("newAdminPassword").value;const confirmNext=document.getElementById("confirmAdminPassword").value; try{const latest=getAdminAuth();if(!(await verifyAdminPassword(current,latest))){msg.textContent="موجودہ پاس ورڈ درست نہیں ہے۔";msg.style.color="#b42318";return} if(username.length<3){msg.textContent="صارف نام کم از کم 3 حروف کا ہونا چاہیے۔";msg.style.color="#b42318";return} if(next.length<8){msg.textContent="نیا پاس ورڈ کم از کم 8 حروف یا اعداد کا ہونا چاہیے۔";msg.style.color="#b42318";return} if(next!==confirmNext){msg.textContent="نئے پاس ورڈ ایک جیسے نہیں ہیں۔";msg.style.color="#b42318";return} const salt=bytesToHex(crypto.getRandomValues(new Uint8Array(16)));const hash=await deriveAdminHash(next,salt);localStorage.setItem(ADMIN_AUTH_KEY,JSON.stringify({...latest,username,salt,hash,iterations:ADMIN_PBKDF2_ITERATIONS,updatedAt:new Date().toISOString()}));msg.textContent="ایڈمن صارف نام اور پاس ورڈ کامیابی سے تبدیل ہو گئے ہیں۔";msg.style.color="#15803d";document.getElementById("currentAdminPassword").value="";document.getElementById("newAdminPassword").value="";document.getElementById("confirmAdminPassword").value=""; }catch(err){msg.textContent=err.message||"تبدیلی محفوظ نہیں ہو سکی۔";msg.style.color="#b42318"} }); } menu(); dashboard(); initAdminAuth();