`); w.document.close(); } function deleteStoredDoc(arr,id,field){ const rec=byId(db[arr]||[],id); if(!rec || !rec[field]) return; if(requireDeletePassword('Bu ekli dosya silinsin mi?')){delete rec[field]; save(); render();} } function option(arr,sel){return arr.map(x=>``).join('')} function shortDashboardText(v,len=105){v=String(v||'').replace(/\s+/g,' ').trim(); return v.length>len?v.slice(0,len-3)+'...':v;} function dashboardMaterialLocation(i){ // Ana Sayfa > Malzeme Durumu > Konum alanı, Stok / Malzeme Giriş ekranındaki // Konum Bilgileri bölümünde tutulan Son Kiradaki Konumu bilgisini esas alır. let loc=cleanDepot((i&&i.lastDepotEntryLocation)||'') || cleanDepot((i&&i.location)||'') || cleanDepot(itemEntryLocation(i)||''); if(!loc) return '-'; if(isDepotStatusSiteInfoLocation(loc)){ let cust=isDepotStatusSiteInfoLocation(loc)?(nameOf(db.customers,(i&&i.lastDepotEntryCustomerId)||'')||lastCustomerNameForItem((i&&i.id)||'')):''; let note=String((i&&i.lastDepotEntrySiteNote)||'').trim(); let extra=[]; if(cust && cust!=='-') extra.push('Müşteri: '+esc(cust)); if(note) extra.push('Açıklama: '+esc(note)); return esc(loc)+(extra.length?'
'+extra.join(' • ')+'':''); } return esc(loc); } function dashboardImportantNotesHtml(){ let all=(db.importantNotes||[]).filter(n=>String(n.status||'Açık')!=='Tamamlandı'); const rank={Kritik:0,'Yüksek':1,Normal:2}; all.sort((a,b)=>(rank[a.priority||'Normal']??3)-(rank[b.priority||'Normal']??3) || String(a.date||'9999-12-31').localeCompare(String(b.date||'9999-12-31')) || String(b.created||'').localeCompare(String(a.created||''))); let notes=all.slice(0,6); if(!notes.length){ return `
📌 Önemli Notlar
Açık önemli not bulunmuyor.
`; } return `
📌 Önemli Notlar ${all.length} açık
${notes.map(n=>`
${badge(n.priority||'Normal')}${esc(n.title||'Başlıksız Not')}
${esc(shortDashboardText(n.note||''))}
${n.date?`📅 ${esc(n.date)}`:'📅 Tarih yok'} ${crmRelatedName(n)&&crmRelatedName(n)!=='-'?' • '+esc(crmRelatedName(n)):''}
`).join('')}
${all.length>notes.length?`
+ ${all.length-notes.length} not daha var. Tümünü Gör butonundan açabilirsin.
`:''}
`; } const views={ dashboard(){let kirada=db.items.reduce((s,i)=>s+rentedQty(i.id),0), depoda=db.items.reduce((s,i)=>s+availableQty(i),0), kesilmedi=db.rentMonths.filter(m=>m.invoiceStatus!=='Kesildi').length, faturali=db.rentMonths.filter(m=>m.invoiceStatus==='Kesildi').length;document.getElementById('view').innerHTML=` ${dashboardLogoHtml()} ${dashboardImportantNotesHtml()}
Toplam Malzeme Adedi
${sumQty(db.items)}
Kiradaki Adet
${kirada}
Depodaki Adet
${depoda}
Yatırımcı Fatura Bekleyen
${money(ownerSupplierWaitingTotal())}

Malzeme Sahibi + Malzeme Grubuna Göre Adet

${table(groupQtyByOwnerMaterial(),['Malzeme Sahibi','Malzeme','Toplam Adet','Kirada','Depoda'])}

Malzeme Adına Göre Toplam Adet

${table(groupQtyBy('name'),['Malzeme Grubu','Toplam Adet','Kirada','Depoda','Bakımda','İade'])}

Malzeme Tipine Göre Toplam Adet

${table(groupQtyBy('type'),['Malzeme Tipi','Toplam Adet','Kirada','Depoda','Bakımda','İade'])}
${dashboardThisMonthUninvoicedHtml()}${dashboardPastUninvoicedHtml()}
${dashboardThisMonthInvestorMissingHtml()}${dashboardPastInvestorMissingHtml()}

Malzeme Durumu

${table(db.items.map(i=>[i.code,i.name,qtyOf(i),nameOf(db.owners,i.ownerId),badge(stockStatus(i)),dashboardMaterialLocation(i)]),['Kod','Malzeme','Adet','Sahibi','Durum','Son Kiradaki Konumu'])}
`}, owners(){crudOwners()}, customers(){crudCustomers()}, items(){crudItems()}, intake(){intakeView()}, rentables(){rentablesView()}, rent(){rentView()}, rentals(){rentalsView()}, months(){monthsView()}, ownerInvoices(){ownerInvoicesView()}, contracts(){contractsView()}, returns(){returnsView()}, transfer(){transferView()}, depots(){depotsView()}, materialMaintenance(){materialMaintenanceView()}, vehicles(){vehiclesView()}, personnel(){personnelView()}, stock(){stockView()}, shipments(){shipmentsView()}, cargo(){cargoView()}, whatsapp(){whatsappView()}, crm(){crmView()}, offers(){offersView()}, sms(){smsView()}, realEstates(){realEstatesView()}, reports(){reportsView()}, settings(){settingsView()}, backup(){backupView()} }; function table(rows,heads){if(!rows.length)return '
Kayıt yok.
'; return `
${heads.map(h=>``).join('')}${rows.map(r=>`${r.map(c=>``).join('')}`).join('')}
${h}
${c}
`} const INTAKE_COL_STORE='kuleVincIntakeVisibleColumns_v1'; const INTAKE_COL_ORDER_STORE='kuleVincIntakeColumnOrder_v1'; function intakeColumnDefs(){return [ {key:'created',head:'Giriş Tarihi',cell:i=>i.created||'-'}, {key:'code',head:'Kod',cell:i=>i.code||'-'}, {key:'name',head:'Malzeme',cell:i=>i.name||'-'}, {key:'qty',head:'Toplam Adet',cell:i=>qtyOf(i)}, {key:'rented',head:'Kirada',cell:i=>rentedQty(i.id)}, {key:'available',head:'Depoda',cell:i=>availableQty(i)}, {key:'returned',head:'Sahibine İade',cell:i=>returnedQty(i.id)}, {key:'owner',head:'Sahibi',cell:i=>nameOf(db.owners,i.ownerId)}, {key:'lessor',head:'Kiraya Veren',cell:i=>itemLessor(i)}, {key:'type',head:'Tip',cell:i=>i.type||'-'}, {key:'serviceType',head:'Kullanım Tipi',cell:i=>itemServiceType(i)}, {key:'lastMaint',head:'Son Bakım',cell:i=>formatDateTR(i.lastMaintenanceDate)||'-'}, {key:'nextMaint',head:'Sonraki Bakım',cell:i=>formatDateTR(itemNextMaintenanceDate(i))||'-'}, {key:'maintStatus',head:'Bakım Durumu',cell:i=>badge(maintenanceStatus(i))}, {key:'monthlyRent',head:'Aylık Kira',cell:i=>money(i.monthlyRent)}, {key:'status',head:'Durum',cell:i=>badge(stockStatus(i))}, {key:'lastDepotLoc',head:'Son Kiradaki Konumu',cell:i=>i.lastDepotEntryLocation||'-'}, {key:'lastDepotCity',head:'Son Kiradaki İl',cell:i=>i.lastDepotEntryCity||'-'}, {key:'lastDepotDistrict',head:'Son Kiradaki İlçe',cell:i=>i.lastDepotEntryDistrict||'-'}, {key:'emanetCustomer',head:'Son Kiradaki Konumu Müşteri',cell:i=>isDepotStatusSiteInfoLocation(i.lastDepotEntryLocation)?(nameOf(db.customers,i.lastDepotEntryCustomerId||'')||lastCustomerNameForItem(i.id)||'-'):'-'}, {key:'emanetNote',head:'Son Kiradaki Konumu İlave Açıklama',cell:i=>isDepotStatusSiteInfoLocation(i.lastDepotEntryLocation)?(i.lastDepotEntrySiteNote||'-'):'-'}, {key:'lastDepotNote',head:'Son Giriş Açıklaması',cell:i=>i.lastDepotEntryNote||'-'}, {key:'note',head:'Açıklama',cell:i=>i.serial||i.note||'-'}, {key:'actions',head:'İşlem',always:true,cell:i=>` `} ]} function intakeColumnOrderKeys(){ let defs=intakeColumnDefs(), all=defs.map(x=>x.key), raw=null, oldVisible=null; try{raw=JSON.parse(localStorage.getItem(INTAKE_COL_ORDER_STORE)||'null')}catch(e){} try{oldVisible=JSON.parse(localStorage.getItem(INTAKE_COL_STORE)||'null')}catch(e){} let base=Array.isArray(raw)&&raw.length?raw:(Array.isArray(oldVisible)&&oldVisible.length?oldVisible:all); let out=[]; base.forEach(k=>{if(all.includes(k)&&!out.includes(k))out.push(k)}); all.forEach(k=>{if(!out.includes(k))out.push(k)}); return out; } function intakeOrderedColumnDefs(){let defs=intakeColumnDefs(), map={}; defs.forEach(d=>map[d.key]=d); return intakeColumnOrderKeys().map(k=>map[k]).filter(Boolean);} function intakeVisibleColumnKeys(){ let defs=intakeColumnDefs(), all=defs.map(x=>x.key), order=intakeColumnOrderKeys(), raw=null; try{raw=JSON.parse(localStorage.getItem(INTAKE_COL_STORE)||'null')}catch(e){} if(!Array.isArray(raw)||!raw.length)return order; let set=new Set(raw.filter(k=>all.includes(k))); defs.forEach(d=>{if(d.always)set.add(d.key)}); return order.filter(k=>set.has(k)); } function saveIntakeVisibleSet(visible){let order=intakeColumnOrderKeys(); localStorage.setItem(INTAKE_COL_STORE,JSON.stringify(order.filter(k=>visible.has(k))));} function setIntakeColumnVisible(key,checked){ let defs=intakeColumnDefs(); let visible=new Set(intakeVisibleColumnKeys()); let def=defs.find(x=>x.key===key); if(def&&def.always)return; if(checked)visible.add(key); else visible.delete(key); defs.forEach(d=>{if(d.always)visible.add(d.key)}); if(!visible.size)visible.add('actions'); saveIntakeVisibleSet(visible); render(); } function moveIntakeColumn(key,dir){ let order=intakeColumnOrderKeys(), idx=order.indexOf(key); if(idx<0)return; let ni=idx+dir; if(ni<0||ni>=order.length)return; [order[idx],order[ni]]=[order[ni],order[idx]]; localStorage.setItem(INTAKE_COL_ORDER_STORE,JSON.stringify(order)); let visible=new Set(intakeVisibleColumnKeys()); saveIntakeVisibleSet(visible); render(); } function resetIntakeColumns(){localStorage.removeItem(INTAKE_COL_STORE); localStorage.removeItem(INTAKE_COL_ORDER_STORE); render();} function selectAllIntakeColumns(){let all=intakeColumnOrderKeys(); localStorage.setItem(INTAKE_COL_STORE,JSON.stringify(all)); localStorage.setItem(INTAKE_COL_ORDER_STORE,JSON.stringify(all)); render();} function hideMostIntakeColumns(){let keep=['name','qty','rented','available','owner','type','status','lastDepotLoc','lastDepotCity','lastDepotDistrict','actions']; let order=intakeColumnOrderKeys(); localStorage.setItem(INTAKE_COL_STORE,JSON.stringify(order.filter(k=>keep.includes(k)))); localStorage.setItem(INTAKE_COL_ORDER_STORE,JSON.stringify(order)); render();} function intakeColumnChooserHtml(){ let visible=new Set(intakeVisibleColumnKeys()), defs=intakeOrderedColumnDefs(); return `
⚙️ Kolonları Göster / Gizle ve SıralaAç / Kapat
İşaretli kolonlar görünür. Ok tuşlarıyla kolonların tablodaki sırasını değiştirebilirsin. İşlem kolonu gizlenmez.
${defs.map((d,idx)=>`
`).join('')}
` } function intakeRecordsTableHtml(){let defsMap={}; intakeColumnDefs().forEach(d=>defsMap[d.key]=d);let visible=new Set(intakeVisibleColumnKeys());let active=intakeColumnOrderKeys().map(k=>defsMap[k]).filter(d=>d&&(visible.has(d.key)||d.always));let rows=(db.items||[]).slice().reverse().map(i=>active.map(d=>d.cell(i)));return intakeColumnChooserHtml()+table(rows,active.map(d=>d.head));} function crudOwners(){document.getElementById('topActions').innerHTML='';document.getElementById('view').innerHTML=table(db.owners.map(o=>{let its=db.items.filter(i=>i.ownerId===o.id);return [o.name,o.person||'-',o.phone||'-',o.tax||'-',sumQty(its),its.reduce((s,i)=>s+rentedQty(i.id),0),its.reduce((s,i)=>s+availableQty(i),0),` `]}),['Firma','Yetkili','Telefon','Vergi No','Toplam Adet','Kirada','Depoda','İşlem'])} function ownerForm(id=''){let o=byId(db.owners,id); openModal(id?'Firma Düzenle':'Firma Ekle',`
`)} function saveOwner(id){let o=id?byId(db.owners,id):{id:uid('OWN')}; Object.assign(o,{name:formVal('oname'),person:formVal('operson'),phone:formVal('ophone'),taxOffice:formVal('otaxoffice'),tax:formVal('otax'),iban:formVal('oiban'),note:formVal('onote')}); if(!id)db.owners.push(o); save(); closeModal(); render()} function ownerDetail(id){ let o=byId(db.owners,id); if(!o.id)return; let its=(db.items||[]).filter(i=>i.ownerId===id); let total=sumQty(its), rented=its.reduce((s,i)=>s+rentedQty(i.id),0), available=its.reduce((s,i)=>s+availableQty(i),0), returned=its.reduce((s,i)=>s+returnedQty(i.id),0); let grouped={}; its.forEach(i=>{let key=(i.name||'Belirtilmemiş')+'|'+(i.type||'-'); if(!grouped[key])grouped[key]={name:i.name||'Belirtilmemiş',type:i.type||'-',total:0,rented:0,available:0,returned:0,rent:Number(i.monthlyRent||0),locations:{}}; grouped[key].total+=qtyOf(i); grouped[key].rented+=rentedQty(i.id); grouped[key].available+=availableQty(i); grouped[key].returned+=returnedQty(i.id); grouped[key].rent=Number(i.monthlyRent||grouped[key].rent||0); let loc=i.location||'-'; grouped[key].locations[loc]=(grouped[key].locations[loc]||0)+availableQty(i);}); let groupRows=Object.values(grouped).sort((a,b)=>a.name.localeCompare(b.name,'tr')).map(g=>[g.name,g.type,g.total,g.rented,g.available,g.returned,Object.entries(g.locations).filter(x=>x[1]>0).map(x=>x[0]+': '+x[1]).join('
')||'-',money(g.rent)]); let itemRows=its.map(i=>{let actions=[]; if(rentedQty(i.id)>0)actions.push(``); if(returnedQty(i.id)>0)actions.push(``); return [i.code||'-',i.name||'-',i.type||'-',towerSummary(i),itemServiceType(i),qtyOf(i),rentedQty(i.id),availableQty(i),returnedQty(i.id),badge(stockStatus(i)),itemEntryLocation(i)||'-',i.location||'-',(i.city||'-')+' / '+(i.district||'-'),money(i.monthlyRent),i.serial||'-',i.note||'-',actions.join(' ')||'-'];}); let activeRentalRows=(db.rentalItems||[]).filter(ri=>its.some(i=>i.id===ri.itemId)&&rentQtyOf(ri)>0).map(ri=>{let r=byId(db.rentals,ri.rentalId), i=byId(db.items,ri.itemId); return [nameOf(db.customers,r.customerId),i.name||'-',i.type||'-',rentQtyOf(ri),r.start||'-',r.end||'-',r.location||'-',money(ri.amount||0),badge(r.status||'Aktif')];}); let invRows=(db.ownerInvoices||[]).filter(x=>x.ownerId===id).sort((a,b)=>(b.month||'').localeCompare(a.month||'')).map(x=>[monthTitle(x.month),x.invoiceNo||'-',x.invoiceDate||'-',ownerInvoiceHasInvoiceInfo(x)?money(x.amountOverride||x.amount||0):'',ownerInvoiceStatusBadge(x),x.note||'-']); let retRows=(db.ownerReturns||[]).filter(x=>x.ownerId===id).sort((a,b)=>(b.date||'').localeCompare(a.date||'')).map(x=>[x.date||'-',x.itemCode||'-',x.itemName||'-',x.qty||0,x.returnedLocation||'İade Deposu',x.returnedCity||'-',x.returnedDistrict||'-',x.note||'-']); let takeRows=(db.ownerTakeBacks||[]).filter(x=>x.ownerId===id).sort((a,b)=>(b.date||'').localeCompare(a.date||'')).map(x=>[x.date||'-',x.itemCode||'-',x.itemName||'-',x.qty||0,x.location||'-',x.note||'-']); let kirayaAlRows=(db.kirayaAlLogs||[]).filter(x=>x.ownerId===id).sort((a,b)=>(b.date||'').localeCompare(a.date||'')).map(x=>[x.date||'-',x.itemCode||'-',x.itemName||'-',x.qty||0,x.oldUsage||'-',x.newUsage||'-',x.lessor||'-',x.location||'-',x.note||'-']); openModal('Malzeme Sahibi Detayı',`

${esc(o.name||'-')}

Yetkili: ${esc(o.person||'-')}   |   Telefon: ${esc(o.phone||'-')}   |   Vergi No: ${esc(o.tax||'-')}
Toplam Adet
${total}
Kirada
${rented}
Depoda
${available}
Sahibine İade
${returned}

Bu Firmaya Girilmiş Malzeme Özeti

${table(groupRows,['Malzeme Adı','Tip','Toplam Adet','Kirada','Depoda','Sahibine İade','Depo Dağılımı','Birim Aylık Kira'])}

Detaylı Malzeme Kayıtları

${table(itemRows,['Kod','Malzeme','Tip','Kule Vinç Detay','Kullanım Tipi','Toplam','Kirada','Depoda','Sahibine İade','Durum','Birim Aylık Kira','Seri No / Açıklama','Açıklama','İşlem'])}

Bu Firmaya Ait Aktif Kiralamalar

${table(activeRentalRows,['Müşteri','Malzeme','Tip','Adet','Başlangıç','Bitiş','Şantiye/Konum','Aylık Tutar','Durum'])}

Bu Firmadan Gelen Faturalar

${table(invRows,['Ay','Fatura No','Fatura Tarihi','Tutar','Durum','Açıklama'])}

Sahibine İade Geçmişi

${table(retRows,['İade Tarihi','Kod','Malzeme','Adet','İade Deposu','İl','İlçe','Açıklama'])}

Sahibinden Tekrar Depoya Alma Geçmişi

${table(takeRows,['Alış Tarihi','Kod','Malzeme','Adet','Depo/Konum','Açıklama'])}

Kiraya Al Geçmişi

${table(kirayaAlRows,['Kiraya Al Tarihi','Kod','Malzeme','Adet','Önceki Kullanım','Yeni Kullanım','Kiraya Veren','Depo/Konum','Açıklama'])}
`); } function crudCustomers(){document.getElementById('topActions').innerHTML='';document.getElementById('view').innerHTML=`
Müşteriye ait tüm işlemleri görmek için satırdaki Detay butonunu kullanın. TEKLİF VER butonu seçilen müşteriyi teklif ekranına otomatik getirir.
`+table(db.customers.map(o=>[esc(o.name),esc(o.person||'-'),esc(o.phone||'-'),esc(o.tax||'-'),db.rentals.filter(r=>r.customerId===o.id).length,` `]),['Müşteri','Yetkili','Telefon','Vergi No','Kiralama','İşlem'])} function customerForm(id=''){let o=byId(db.customers,id); openModal(id?'Müşteri Düzenle':'Müşteri Ekle',`
`)} function customerDetail(id){ let c=byId(db.customers,id); if(!c.id){alert('Müşteri bulunamadı');return} let rentals=(db.rentals||[]).filter(r=>r.customerId===id); let rentalIds=new Set(rentals.map(r=>r.id)); let rentalItems=(db.rentalItems||[]).filter(ri=>rentalIds.has(ri.rentalId)); let months=(db.rentMonths||[]).filter(m=>rentalIds.has(m.rentalId)).slice().sort((a,b)=>String(a.periodStart||a.month||'').localeCompare(String(b.periodStart||b.month||''))); let offers=(db.offers||[]).filter(o=>o.customerId===id).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))); let crm=(db.crmRecords||[]).filter(x=>x.customerId===id).slice().sort((a,b)=>String(b.created||'').localeCompare(String(a.created||''))); let notes=(db.importantNotes||[]).filter(x=>x.customerId===id).slice().sort((a,b)=>String(a.status||'').localeCompare(String(b.status||''))||String(a.date||'').localeCompare(String(b.date||''))); let moves=(db.moves||[]).filter(m=>m.customerId===id).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))); let totalInvoice=months.reduce((s,m)=>s+Number(m.amount||0),0), invoiced=months.filter(m=>m.invoiceStatus==='Kesildi'), pending=months.filter(m=>m.invoiceStatus!=='Kesildi'); let activeRentals=rentals.filter(r=>r.status==='Aktif'), closedRentals=rentals.filter(r=>r.status!=='Aktif'); function rentalMaterialText(rid){let arr=(db.rentalItems||[]).filter(ri=>ri.rentalId===rid).map(ri=>{let it=byId(db.items,ri.itemId); return `${esc(it.name||'-')} / ${esc(it.type||'-')} • ${rentQtyOf(ri)} adet • ${money(ri.amount||Number(ri.unitMonthly||ri.unitAmount||0)*rentQtyOf(ri))}`}); return arr.length?arr.join('
'):'-'} let rentalRows=rentals.slice().sort((a,b)=>String(b.start||'').localeCompare(String(a.start||''))).map(r=>[badge(r.status||'Aktif'),esc(r.start||'-'),esc(r.end||'-'),esc(r.projectName||r.location||'-'),esc(r.city||'-'),esc(r.district||'-'),rentalMaterialText(r.id),money((db.rentalItems||[]).filter(ri=>ri.rentalId===r.id).reduce((s,ri)=>s+Number(ri.amount||0),0)),``]); let monthRows=months.map(m=>{let r=byId(db.rentals,m.rentalId), it=byId(db.items,m.itemId); return [monthTitle(m.month),periodText(m),esc(it.name||'-'),esc(nameOf(db.owners,it.ownerId)),m.qty||1,money(m.amount),badge(m.invoiceStatus||'Bekliyor'),esc(m.invoiceNo||'-'),esc(m.invoiceDate||'-')];}); let itemRows=rentalItems.map(ri=>{let r=byId(db.rentals,ri.rentalId), it=byId(db.items,ri.itemId); return [esc(r.start||'-'),esc(r.end||'-'),badge(r.status||'-'),esc(it.code||'-'),esc(it.name||'-'),esc(it.type||'-'),esc(nameOf(db.owners,it.ownerId)),rentQtyOf(ri),money(ri.unitMonthly||ri.unitAmount||0),money(ri.amount||0)];}); let offerRows=offers.map(o=>[esc(o.no||'-'),esc(o.date||'-'),esc(o.validUntil||'-'),esc(o.subject||'-'),badge(o.status||'Hazırlandı'),money(offerTotal(o)),` `]); let crmRows=crm.map(x=>[esc(x.created||'-'),esc(x.title||'-'),esc(x.contact||'-'),esc(x.phone||'-'),badge(x.stage||'Yeni'),esc(x.nextDate||'-'),esc(x.note||'-'),``]); let noteRows=notes.map(n=>[badge(n.priority||'Normal'),esc(n.date||'-'),esc(n.title||'-'),badge(n.status||'Açık'),esc(n.note||'-'),``]); let moveRows=moves.map(m=>[esc(m.date||'-'),esc(m.type||'-'),esc(m.itemCode||'-'),esc(m.itemName||'-'),m.qty||0,esc(m.location||'-'),esc(m.note||'-')]); let html=`

${esc(c.name||'-')}

Yetkili: ${esc(c.person||'-')}   |   Telefon: ${esc(c.phone||'-')}   |   Vergi: ${esc(c.taxOffice||'-')} / ${esc(c.tax||'-')}   |   E-posta: ${esc(c.mail||'-')}   |   Banka: ${esc(c.bank||'-')}   |   Web: ${esc(c.web||'-')}
Aktif Kiralama
${activeRentals.length}
Geçmiş/Kapalı Kiralama
${closedRentals.length}
Bekleyen Fatura
${pending.length}
Toplam Fatura
${money(totalInvoice)}
`+ `

Kiralama Hareketleri

${table(rentalRows,['Durum','Başlangıç','Bitiş','Şantiye Proje Adı','İl','İlçe','Malzemeler','Aylık Toplam','İşlem'])}
`+ `

Kiralanan Malzeme Detayları

${table(itemRows,['Başlangıç','Bitiş','Kira Durumu','Kod','Malzeme','Tip','Malzeme Sahibi','Adet','Birim Kira','Toplam'])}
`+ `

Aylık Kira / Fatura Geçmişi

${table(monthRows,['Ay','Dönem','Malzeme','Malzeme Sahibi','Adet','Tutar','Durum','Fatura No','Fatura Tarihi'])}
`+ `

Teklifler

${table(offerRows,['Teklif No','Tarih','Geçerlilik','Konu','Durum','Toplam','İşlem'])}
`+ `

CRM Görüşme / Takip Kayıtları

${table(crmRows,['Tarih','Konu','Yetkili','Telefon','Durum','Takip Tarihi','Not','İşlem'])}
`+ `

Önemli Notlar

${table(noteRows,['Öncelik','Tarih','Başlık','Durum','Not','İşlem'])}
`+ `

Stoktan Kullanılan Malzemeler

${table((db.stockMovements||[]).filter(m=>m.type==='Müşteriye Kullanıldı'&&m.customerId===id).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))).map(m=>[m.date||'-',stockProductName(m.productId),m.qty||0,vehicleName(m.placeId),m.crane||'-',m.note||'-']),['Tarih','Ürün','Adet','Araç','Kule Vinç / İş','Açıklama'])}
`+ `

Sevkiyat Geçmişi

${table((db.shipments||[]).filter(s=>s.customerId===id).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))).map(s=>[s.date||'-',s.shipmentNo||'-',shipmentStatusBadge(s.status),s.type||'-',stockWarehouseName(s.fromWarehouseId),shipmentLineSummary(s),s.crane||'-',s.receiver||'-',``]),['Tarih','Sevk No','Durum','Tür','Çıkış Deposu','Malzemeler','Kule Vinç / İş','Teslim Alan','İşlem'])}
`+ `

Kargo Geçmişi

${table((db.cargos||[]).filter(k=>k.customerId===id).slice().sort((a,b)=>String(b.sendDate||'').localeCompare(String(a.sendDate||''))).map(k=>[k.sendDate||'-',k.company||'-',k.trackingNo||'-',cargoStatusBadge(k.status),k.receiverName||'-',k.content||'-',money(k.amount),``]),['Gönderi Tarihi','Firma','Takip No','Durum','Alıcı','İçerik','Ücret','İşlem'])}
`+ `

Depo / Kiradan Dönüş Hareketleri

${table(moveRows,['Tarih','İşlem','Kod','Malzeme','Adet','Depo / Konum','Açıklama'])}
`; openModal('Müşteri Detayı',html); } function saveCustomer(id){let o=id?byId(db.customers,id):{id:uid('CUS')}; Object.assign(o,{name:formVal('cname'),person:formVal('cperson'),phone:formVal('cphone'),taxOffice:formVal('ctaxoffice'),tax:formVal('ctax'),mail:formVal('cmail'),bank:formVal('cbank'),web:formVal('cweb'),note:formVal('cnote')}); if(!id)db.customers.push(o); save(); closeModal(); render()} function cleanLessorName(n){return String(n||'').trim()} function lessorList(){if(!db.settings)db.settings={}; if(!db.settings.lessors)db.settings.lessors=[]; let company=(db.settings.program&&db.settings.program.companyName)||'SİSTEM KULE VİNÇ'; if(company&&!db.settings.lessors.some(x=>String(x).toLocaleLowerCase('tr-TR')===String(company).toLocaleLowerCase('tr-TR')))db.settings.lessors.push(company); return db.settings.lessors.filter(Boolean).slice().sort((a,b)=>a.localeCompare(b,'tr'));} function invoiceLessorName(){if(!db.settings)db.settings={}; if(!db.settings.invoiceLessor)db.settings.invoiceLessor=(lessorList()[0]||((db.settings.program&&db.settings.program.companyName)||'SİSTEM KULE VİNÇ')); return db.settings.invoiceLessor;} function lessorOptions(sel=''){let list=lessorList(); return ``+list.map(x=>``).join('')} function addLessorToSettings(n,doSave=true){n=cleanLessorName(n); if(!n)return; if(!db.settings)db.settings={}; if(!db.settings.lessors)db.settings.lessors=[]; if(!db.settings.lessors.some(x=>String(x).toLocaleLowerCase('tr-TR')===n.toLocaleLowerCase('tr-TR'))){db.settings.lessors.push(n); db.settings.lessors.sort((a,b)=>a.localeCompare(b,'tr')); if(doSave)save();}} function itemLessor(i){if(itemServiceType(i)==='Kiralanamaz')return '-';return i&&i.lessor?i.lessor:invoiceLessorName()} function itemCreatesCustomerInvoice(i){if(itemServiceType(i)==='Vinç Sahibi Tarafından Kiralandı')return false; return String(itemLessor(i)||'').toLocaleLowerCase('tr-TR')===String(invoiceLessorName()||'').toLocaleLowerCase('tr-TR')} function isSystemKuleVincLessor(i){return String(itemLessor(i)||'').trim().toLocaleLowerCase('tr-TR')==='sistem kule vinç'} function showInvestorRentFieldsForItem(i){return !!(i&&i.id&&isSystemKuleVincLessor(i))} function rentalItemHasInvestorRentInfo(ri){return !!(ri&&(ri.investorRentStart||ri.investorRentEnd||(ri.investorMonthlyAmount!==undefined&&ri.investorMonthlyAmount!==null&&ri.investorMonthlyAmount!=='')))} function trNorm(v){return String(v||'').toLocaleLowerCase('tr-TR').replace(/ı/g,'i').replace(/İ/g,'i').replace(/ğ/g,'g').replace(/ü/g,'u').replace(/ş/g,'s').replace(/ö/g,'o').replace(/ç/g,'c');} function isOwnerSiteUseItem(i){let l=trNorm(itemLessor(i)), st=trNorm(i&&i.status); return l.includes('vinc sahibi kullaniminda') || st.includes('vinc sahibi santiyesinde') || st.includes('vinc sahibi kendi kullaniminda');} function isKirayaAlBlockedItem(i){let st=stockStatus(i); return !kirayaAlApproved(i) || itemServiceType(i)==='Kiralanamaz' || isOwnerSiteUseItem(i) || i.status==='İade Edildi' || st==='İade Edildi' || i.ownerReturned || !isRentableItem(i);} function isKirayaAlActiveItem(i){return isRentableItem(i) && !isOwnerSiteUseItem(i) && i.status!=='İade Edildi';} function openIntakeEditFromReturns(id){current='intake'; render(); setTimeout(()=>editIntake(id),80);} function openIntakeEditFromRentables(id){current='intake'; render(); setTimeout(()=>editIntake(id),80);} function returnAreaAction(i){let st=stockStatus(i); if(!kirayaAlApproved(i)||itemServiceType(i)==='Kiralanamaz'||isOwnerSiteUseItem(i)||i.status==='İade Edildi'||st==='İade Edildi')return ``; return ``;} function rentalCreatesCustomerInvoice(rid){let ris=(db.rentalItems||[]).filter(x=>x.rentalId===rid); return ris.some(ri=>itemCreatesCustomerInvoice(byId(db.items,ri.itemId)));} function crudItems(){document.getElementById('topActions').innerHTML='';document.getElementById('view').innerHTML=table(db.items.map(i=>[i.code,i.name,qtyOf(i),rentedQty(i.id),availableQty(i),returnedQty(i.id),i.type||'-',itemServiceType(i),itemLessor(i),nameOf(db.owners,i.ownerId),i.serial||'-',money(i.monthlyRent),badge(stockStatus(i)),i.location||'-',i.city||'-',i.district||'-',` `]),['Kod','Malzeme','Toplam Adet','Kirada','Depoda','Sahibine İade','Tip','Kullanım Tipi','Kiraya Veren','Sahibi','Seri No','Birim Aylık Kira','Durum','Konum','İl','İlçe','İşlem'])} function itemForm(id=''){let i=byId(db.items,id); openModal(id?'Malzeme Düzenle':'Malzeme Ekle',`
Manuel yazı kapalıdır. Malzeme adını önce Ayarlar > Malzeme Adı Listesi bölümünden oluştur.
Tip elle yazılamaz; Ayarlar > Tip Listesi içinden seçilir.
Kiralanabilir, Yükseltme Ekipman ve Vinç Sahibi Tarafından Kiralandı kiraya verilebilir. Vinç Sahibi Tarafından Kiralandı seçilirse Aylık Kira/Fatura oluşturulmaz. Teknik servis bakım takibinde görünür. Kiralanamaz kiraya verilmez, iade ve bakım takibinde görünmez.
Aylık Kira/Fatura sadece Program Ayarları'nda seçilen kiraya veren için oluşur.
Sadece Ayarlar > Depo Listesi bölümünde oluşturduğun depolar görünür.
Boş bırakırsan Ayarlar’daki periyoda göre otomatik hesaplanır.
`)} function saveItem(id){let i=id?byId(db.items,id):{id:uid('ITM')}; let type=cleanType(formVal('itype')), loc=formVal('iloc'); if(!isValidSettingItemNameOrCurrent(formVal('iname'),i.name)){alert('Lütfen Malzeme Adı Listesi içinden seçim yapınız. Manuel malzeme adı girilemez.');return} if(!isValidSettingType(type)){alert('Lütfen Ayarlar > Tip Listesi içinden seçim yapınız. Manuel tip girilemez.');return} if(formVal('istatus')==='Depoda'&&!loc){alert('Depo / konum seçiniz. Depo yoksa önce Ayarlar > Depo Listesi bölümünden ekleyin.');return} let lastM=formVal('ilastMaint'), nextM=formVal('inextMaint')||addDaysISO(lastM,materialMaintenancePeriodDays()); let svc=formVal('iserviceType')||'Kiralanabilir'; Object.assign(i,{code:formVal('icode'),name:formVal('iname'),qty:Number(formVal('iqty')||1),type,lessor:svc==='Kiralanamaz'?'':(formVal('ilessor')||invoiceLessorName()),serviceType:svc,maintenanceEnabled:svc!=='Yükseltme Ekipman'&&svc!=='Kiralanamaz',lastMaintenanceDate:lastM,nextMaintenanceDate:nextM,maintenanceNote:formVal('imaintNote'),ownerId:formVal('iowner'),brand:formVal('ibrand'),model:formVal('imodel'),serial:formVal('iserial'),monthlyRent:svc==='Kiralanamaz'?0:Number(formVal('irent')),status:formVal('istatus'),kirayaAlApproved:id?kirayaAlApproved(i):false,entryLocation:i.entryLocation||loc,entryCity:i.entryCity||formVal('icity'),entryDistrict:i.entryDistrict||formVal('idistrict'),location:loc,city:formVal('icity'),district:formVal('idistrict'),note:formVal('inote')}); if(!id)db.items.push(i); save(); closeModal(); render()} function normTR(s){return String(s||'').trim().toLocaleLowerCase('tr-TR').replace(/ç/g,'c').replace(/ı/g,'i').replace(/ğ/g,'g').replace(/ü/g,'u').replace(/ş/g,'s').replace(/ö/g,'o')} function isTowerCraneTypeValue(t){let v=normTR(t); return v==='kule vinc'||v==='kule vinci'} function towerCraneSettings(){if(!db.settings)db.settings={}; if(!db.settings.towerCrane)db.settings.towerCrane={brands:[],models:[],years:[],serialNos:[],scanAreas:[],heights:[],chassisTypes:[]}; ['brands','models','years','serialNos','scanAreas','heights','chassisTypes'].forEach(k=>{if(!db.settings.towerCrane[k])db.settings.towerCrane[k]=[]}); return db.settings.towerCrane} const towerCraneDefs={brands:{field:'towerBrand',label:'Marka',ph:'Örn: Potain, Liebherr'},models:{field:'towerModel',label:'Model',ph:'Örn: MC 85, 154 EC-H'},years:{field:'towerYear',label:'Üretim Yılı',ph:'Örn: 2018'},serialNos:{field:'towerSerialNo',label:'Seri No',ph:'Örn: TC-2026-001'},scanAreas:{field:'towerScanArea',label:'Tarama Alanı',ph:'Örn: 50 m'},heights:{field:'towerHeight',label:'Yükseklik',ph:'Örn: 40 m'},chassisTypes:{field:'towerChassisType',label:'Şase Tipi',ph:'Örn: Sabit Şase, Yürüyüşlü'}}; function towerSettingList(key){let s=towerCraneSettings(); return (s[key]||[]).filter(Boolean).slice().sort((a,b)=>String(a).localeCompare(String(b),'tr'))} function towerDatalist(key,id){return `${towerSettingList(key).map(v=>``).join('')}`} function towerFieldValue(i,key){let d=towerCraneDefs[key]; return i&&d?String(i[d.field]||''):''} function towerFieldInputs(prefix,i={}){let visible=prefix==='new'?false:isTowerCraneTypeValue(i.type||''); return `
🏗️

Kule Vinç Teknik Bilgileri

Tip Kule Vinç seçildiğinde marka, model, üretim yılı, seri no, tarama alanı, yükseklik ve şase tipi bilgileri bu bölümden girilir. Seçenekleri Ayarlar > Malzeme / Depo bölümünden yönetebilirsin.

${towerDatalist('brands',prefix+'TowerBrands')}
${towerDatalist('models',prefix+'TowerModels')}
${towerDatalist('years',prefix+'TowerYears')}
${towerDatalist('serialNos',prefix+'TowerSerialNos')}
${towerDatalist('scanAreas',prefix+'TowerScanAreas')}
${towerDatalist('heights',prefix+'TowerHeights')}
${towerDatalist('chassisTypes',prefix+'TowerChassisTypes')}
`} function toggleTowerCraneFields(prefix){let typeEl=document.getElementById(prefix==='new'?'newType':'editType'); let box=document.getElementById(prefix+'TowerFields'); if(box)box.style.display=isTowerCraneTypeValue(typeEl?typeEl.value:'')?'block':'none'} function towerFieldsFromForm(prefix){return {towerBrand:formVal(prefix+'TowerBrand'),towerModel:formVal(prefix+'TowerModel'),towerYear:formVal(prefix+'TowerYear'),towerSerialNo:formVal(prefix+'TowerSerialNo'),towerScanArea:formVal(prefix+'TowerScanArea'),towerHeight:formVal(prefix+'TowerHeight'),towerChassisType:formVal(prefix+'TowerChassisType')}} function towerRowsForItem(i){if(!i||!isTowerCraneTypeValue(i.type))return []; return [['Kule Vinç Marka',esc(i.towerBrand||'-')],['Kule Vinç Model',esc(i.towerModel||'-')],['Üretim Yılı',esc(i.towerYear||'-')],['Kule Vinç Seri No',esc(i.towerSerialNo||'-')],['Tarama Alanı',esc(i.towerScanArea||'-')],['Yükseklik',esc(i.towerHeight||'-')],['Şase Tipi',esc(i.towerChassisType||'-')]]} function towerSummary(i){if(!i||!isTowerCraneTypeValue(i.type))return '-'; let parts=[]; [['Marka',i.towerBrand],['Model',i.towerModel],['Üretim Yılı',i.towerYear],['Seri No',i.towerSerialNo],['Tarama Alanı',i.towerScanArea],['Yükseklik',i.towerHeight],['Şase Tipi',i.towerChassisType]].forEach(x=>{if(x[1])parts.push(''+esc(x[0])+': '+esc(x[1]))}); return parts.join('
')||'-'} function addTowerSetting(key,inputId){let def=towerCraneDefs[key], val=String(formVal(inputId)||'').trim(); if(!def||!val){alert('Değer yazın');return} let list=towerSettingList(key); if(!list.some(x=>String(x).toLocaleLowerCase('tr-TR')===val.toLocaleLowerCase('tr-TR'))){towerCraneSettings()[key].push(val)} save(); render()} function towerSettingUsed(key,val){let def=towerCraneDefs[key]; if(!def)return false; return (db.items||[]).some(i=>String(i[def.field]||'').toLocaleLowerCase('tr-TR')===String(val||'').toLocaleLowerCase('tr-TR'))} function editTowerSetting(key,v){let def=towerCraneDefs[key], old=dec(v); if(!def)return; openModal(def.label+' Düzenle',`
Bu değer kullanılıyorsa bağlı Kule Vinç kayıtları da yeni değerle güncellenir.
`)} function saveTowerSettingEdit(key,v){let def=towerCraneDefs[key], old=dec(v), neu=String(formVal('editTowerSettingValue')||'').trim(); if(!def||!neu){alert('Değer yazın');return} let s=towerCraneSettings(); s[key]=(s[key]||[]).map(x=>String(x).toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR')?neu:x); s[key]=[...new Set(s[key])]; (db.items||[]).forEach(i=>{if(String(i[def.field]||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR'))i[def.field]=neu}); save(); closeModal(); render()} function deleteTowerSetting(key,v){let def=towerCraneDefs[key], val=dec(v); if(!def)return; if(towerSettingUsed(key,val)){alert('Bu değer Kule Vinç kayıtlarında kullanılıyor. Silinemez; isterseniz Düzenle ile adını değiştirin.');return} if(!requireDeletePassword('Bu Kule Vinç alan değerini silmek istiyor musunuz?'))return; let s=towerCraneSettings(); s[key]=(s[key]||[]).filter(x=>String(x).toLocaleLowerCase('tr-TR')!==String(val).toLocaleLowerCase('tr-TR')); save(); render()} function towerSettingsPanel(){let keys=['brands','models','years','serialNos','scanAreas','heights','chassisTypes']; let addFields=keys.map(k=>{let d=towerCraneDefs[k], id='settingTower_'+k; return `
`}).join(''); let tables=keys.map(k=>{let d=towerCraneDefs[k]; return `

${d.label} Listesi

${table(towerSettingList(k).map(v=>[v,towerSettingUsed(k,v)?badge('Kullanılıyor'):badge('Boşta'),` `]),[d.label,'Durum','İşlem'])}
`}).join(''); return `

Kule Vinç Detay Alanları

Tip Listesi’nde Kule Vinç seçildiğinde Stok / Malzeme Giriş formunda bu özel alanlar açılır. Buradan marka, model, üretim yılı, seri no, tarama alanı, yükseklik ve şase tipi seçeneklerini ekleyebilirsin.
${addFields}
${tables}
`} function itemHistoryFields(){return [ ['code','Kod'],['name','Malzeme'],['qty','Toplam Adet'],['ownerName','Malzeme Sahibi'],['type','Tip'],['status','Durum'],['serviceType','Kullanım Tipi'],['lessor','Kiraya Veren'],['monthlyRent','Birim Aylık Kira'], ['lastDepotEntryLocation','Son Kiradaki Konumu'],['lastDepotEntryCity','Son Kiradaki İl'],['lastDepotEntryDistrict','Son Kiradaki İlçe'],['lastDepotEntryCustomerName','Sahada Müşteri'],['lastDepotEntrySiteNote','İlave Açıklama'],['lastDepotEntryNote','Son Giriş Açıklaması'], ['serial','Seri No / Açıklama'],['created','Giriş Tarihi'],['lastMaintenanceDate','Son Bakım Tarihi'],['nextMaintenanceDate','Sonraki Bakım Tarihi'],['maintenanceNote','Bakım Açıklaması'], ['towerBrand','Kule Vinç Marka'],['towerModel','Kule Vinç Model'],['towerYear','Üretim Yılı'],['towerSerialNo','Kule Vinç Seri No'],['towerScanArea','Tarama Alanı'],['towerHeight','Yükseklik'],['towerChassisType','Şase Tipi'] ]} function itemHistorySnapshot(i){return {code:i.code||'',name:i.name||'',qty:qtyOf(i),ownerName:nameOf(db.owners,i.ownerId),type:i.type||'',status:i.status||'',serviceType:itemServiceType(i),lessor:itemLessor(i),monthlyRent:Number(i.monthlyRent||0),entryLocation:itemEntryLocation(i)||'',entryCity:itemEntryCity(i)||'',entryDistrict:itemEntryDistrict(i)||'',location:i.location||'',city:i.city||'',district:i.district||'',lastDepotEntryLocation:i.lastDepotEntryLocation||'',lastDepotEntryCity:i.lastDepotEntryCity||'',lastDepotEntryDistrict:i.lastDepotEntryDistrict||'',lastDepotEntryCustomerName:nameOf(db.customers,i.lastDepotEntryCustomerId)||'',lastDepotEntrySiteNote:i.lastDepotEntrySiteNote||'',lastDepotEntryNote:i.lastDepotEntryNote||'',serial:i.serial||i.note||'',created:i.created||'',lastMaintenanceDate:i.lastMaintenanceDate||'',nextMaintenanceDate:itemNextMaintenanceDate(i)||'',maintenanceNote:i.maintenanceNote||'',towerBrand:i.towerBrand||'',towerModel:i.towerModel||'',towerYear:i.towerYear||'',towerSerialNo:i.towerSerialNo||'',towerScanArea:i.towerScanArea||'',towerHeight:i.towerHeight||'',towerChassisType:i.towerChassisType||''}} function sameHistoryValue(a,b){return String(a??'')===String(b??'')} function itemHistoryChanges(oldSnap,newSnap){let rows=[]; itemHistoryFields().forEach(([key,label])=>{let oldV=oldSnap?oldSnap[key]:''; let newV=newSnap?newSnap[key]:''; if(!sameHistoryValue(oldV,newV))rows.push({field:label,oldValue:oldV||'-',newValue:newV||'-'});}); return rows;} function recordItemHistory(item,oldSnap,action='Güncelleme',note=''){if(!db.itemHistories)db.itemHistories=[]; let newSnap=itemHistorySnapshot(item); let changes=itemHistoryChanges(oldSnap,newSnap); if(action==='Güncelleme'&&!changes.length)return; db.itemHistories.push({id:uid('HIS'),itemId:item.id,itemCode:item.code||'',itemName:item.name||'',ownerName:nameOf(db.owners,item.ownerId),date:today(),time:new Date().toLocaleString('tr-TR'),action,changes,note});} function itemHistoryCount(itemId){return (db.itemHistories||[]).filter(h=>h.itemId===itemId).length} function viewItemHistory(id){let item=byId(db.items,id); let logs=(db.itemHistories||[]).filter(h=>h.itemId===id).slice().reverse(); if(!logs.length){openModal('Stok / Malzeme Geçmişi',`
${esc(item.code||'')} - ${esc(item.name||'')} için henüz güncelleme geçmişi yok.
`);return} let html=logs.map(h=>{let rows=(h.changes||[]).map(c=>[c.field,esc(c.oldValue),esc(c.newValue)]); let changesHtml=rows.length?table(rows,['Alan','Eski Değer','Yeni Değer']):'
İlk kayıt oluşturuldu.
'; return `

${esc(h.action||'Güncelleme')} ${esc(h.time||h.date||'-')}

${esc(h.itemCode||item.code||'')} • ${esc(h.itemName||item.name||'')} • ${esc(h.ownerName||'-')}
${(h.changes||[]).length} değişiklik
${changesHtml}${h.note?'
'+esc(h.note)+'
':''}
`}).join(''); openModal('Stok / Malzeme Geçmişi',`
${esc(item.code||'')} - ${esc(item.name||'')} kayıt geçmişi. Her güncellemede eski ve yeni değerler burada tutulur.
${html}`)} function intakeView(){ let totalItems=(db.items||[]).reduce((sum,i)=>sum+qtyOf(i),0); let depotItems=(db.items||[]).reduce((sum,i)=>sum+availableQty(i),0); let rentedItems=(db.items||[]).reduce((sum,i)=>sum+rentedQty(i.id),0); let ownerCount=new Set((db.items||[]).map(i=>i.ownerId).filter(Boolean)).size; document.getElementById('view').innerHTML=`
📦 Stok / Malzeme Yönetimi

Stok / Malzeme Giriş

Yeni malzeme girişini adım adım doldur; firma, kiralama, konum, bakım ve Kule Vinç teknik bilgileri ayrı bölümlerde takip edilir.

Toplam Stok
${totalItems}
Kayıtlı toplam adet
Depoda
${depotItems}
Müsait / seçilebilir adet
Kirada
${rentedItems}
Aktif kiralamadaki adet
Malzeme Sahibi
${ownerCount}
Stokta görünen firma

📋 Stok / Malzeme Giriş Kayıtları

Kolon seçimiyle görmek istediğin alanları açıp kapatabilirsin. Seçimler tarayıcıda saklanır.
${(db.items||[]).length} kayıt
${intakeRecordsTableHtml()}
`} function showNewIntakeForm(){ let p=document.getElementById('newIntakeFormPanel'); if(!p)return; p.style.display='block'; setTimeout(()=>{ try{toggleTowerCraneFields('new'); toggleRentalFieldsForUsage('new'); toggleIntakeStatusRules('new'); toggleLastReturnSiteFields('new');}catch(e){} p.scrollIntoView({behavior:'smooth',block:'start'}); let first=document.getElementById('newOwner'); if(first)first.focus(); },0); } function hideNewIntakeForm(){ let p=document.getElementById('newIntakeFormPanel'); if(p){p.style.display='none'; window.scrollTo({top:0,behavior:'smooth'});} } function addIntake(){let owner=formVal('newOwner'); let lastLoc=formVal('newLastReturnLoc'); if(!owner||!formVal('newItem')){alert('Firma ve malzeme adı gerekli');return} if(!isValidSettingItemName(formVal('newItem'))){alert('Lütfen Malzeme Adı Listesi içinden seçim yapınız. Manuel malzeme adı girilemez.');return} let type=cleanType(formVal('newType')); if(!isValidSettingType(type)){alert('Lütfen Ayarlar > Tip Listesi içinden seçim yapınız. Manuel tip girilemez.');return} if(!lastLoc){alert('Son Depo / Konum seçiniz. Depo yoksa önce Ayarlar > Depo Listesi bölümünden ekleyin.');return} if(isEmanetDeposuSahada(lastLoc)&&!formVal('newLastReturnCustomer')){alert('Son Kiradaki Konumu Emanet Deposu Sahada ise müşteri seçiniz.');return} let entryLoc=lastLoc, loc=lastLoc; let entryCity=formVal('newLastReturnCity'), entryDistrict=formVal('newLastReturnDistrict'); let city=entryCity, district=entryDistrict; let svc=intakeServiceTypeForSave('new'); let lastM=formVal('newLastMaint')||''; let nextM=formVal('newNextMaint')||addDaysISO(lastM,materialMaintenancePeriodDays()); let item={id:uid('ITM'),code:'MLZ-'+String(db.items.length+1).padStart(4,'0'),name:formVal('newItem'),qty:Number(formVal('newQty')||1),type,ownerId:owner,lessor:intakeLessorForSave('new',svc),monthlyRent:intakeRentForSave('new',svc),status:formVal('newStatus')||'Depoda',entryLocation:entryLoc,entryCity,entryDistrict,location:loc,serial:formVal('newSerial'),created:formVal('newDate')||today(),serviceType:svc,kirayaAlApproved:false,maintenanceEnabled:svc!=='Yükseltme Ekipman'&&svc!=='Kiralanamaz',lastMaintenanceDate:lastM,nextMaintenanceDate:nextM,city,district,lastDepotEntryLocation:lastLoc,lastDepotEntryCity:entryCity,lastDepotEntryDistrict:entryDistrict,lastDepotEntryNote:formVal('newLastReturnNote'),lastDepotEntryCustomerId:isEmanetDeposuSahada(lastLoc)?formVal('newLastReturnCustomer'):'',lastDepotEntrySiteNote:isEmanetDeposuSahada(lastLoc)?formVal('newLastReturnSiteNote'):'',lastDepotEntryDate:formVal('newDate')||today(),...towerFieldsFromForm('new'),maintenanceNote:formVal('newMaintNote')}; db.items.push(item); recordItemHistory(item,{},'İlk Kayıt','Stok / Malzeme Giriş ekranından oluşturuldu'); save(); render()} function createsCustomerInvoiceByLessor(serviceType,lessor){ if(String(serviceType||'')==='Kiralanamaz')return false; if(String(serviceType||'')==='Vinç Sahibi Tarafından Kiralandı')return false; return String(lessor||'').toLocaleLowerCase('tr-TR')===String(invoiceLessorName()||'').toLocaleLowerCase('tr-TR'); } function syncCustomerInvoicesAfterItemLessorChange(itemId,beforeServiceType,beforeLessor,afterServiceType,afterLessor){ let beforeCreates=createsCustomerInvoiceByLessor(beforeServiceType,beforeLessor); let afterCreates=createsCustomerInvoiceByLessor(afterServiceType,afterLessor); let result={cancelled:0,added:0}; if(beforeCreates && !afterCreates){ let rows=(db.rentMonths||[]).filter(m=>m.itemId===itemId); result.cancelled=rows.length; db.rentMonths=(db.rentMonths||[]).filter(m=>m.itemId!==itemId); }else if(!beforeCreates && afterCreates){ let rentalIds=[...new Set((db.rentalItems||[]).filter(ri=>ri.itemId===itemId&&rentQtyOf(ri)>0).map(ri=>ri.rentalId))]; rentalIds.forEach(rid=>{let r=byId(db.rentals,rid); if(r.id&&r.status!=='Kapandı')result.added+=ensureRentMonthsForRental(rid,r.months,r.end);}); } return result; } function editIntake(id){let i=byId(db.items,id); if(!i.id)return; let rented=rentedQty(i.id); openModal('Stok / Malzeme Giriş Düzenle',`

${esc(i.code||'')} kodlu kayıt düzenleniyor

Kirada olan adet: ${rented}. Toplam adet kiradaki adetten düşük olamaz.

${badge(stockStatus(i))}
🏢

Firma ve Malzeme Bilgileri

Kod, firma, malzeme adı, tip ve adet bilgilerini güncelle.

Manuel yazı kapalıdır. Malzeme adını Ayarlar > Malzeme Adı Listesi’nden seç.
Tip Kule Vinç olursa özel teknik alanlar açılır. Tipler Ayarlar > Tip Listesi’nden yönetilir.
${towerFieldInputs('edit',i)}
💼

Kiralama Bilgileri

Kullanım tipi, kiraya veren ve aylık kira bilgilerini düzenle. Durum Vinç Sahibi Kendi Kullanımında seçilse bile bu bölüm görünür kalır; kiraya veren ve kira bedeli bilgileri girilebilir.

Kiralanamaz seçilirse kira alanları gizlenir; kiralanabilir, iade ve bakım listelerinde görünmez.
Yeni kiralama faturalarının oluşup oluşmayacağını belirler.
📍

Konum Bilgileri

Malzemenin aktif takip konumu için Son Kiradaki Konumu alanını düzenle.

Son Kiradaki Konumu (Kiraya Ver ve kapatma işleminden otomatik gelir, gerekirse düzenlenebilir)

Sadece Emanet Deposu Sahada seçilirse müşteri ve ilave açıklama alanları açılır.
🛠️

Tarih ve Açıklama

Giriş tarihi ve açıklama bilgilerini güncelle.

Güncelleme sonrası malzeme kullanım tipine göre ilgili listelerde otomatik güncellenir.
`); setTimeout(()=>{try{toggleTowerCraneFields('edit'); toggleRentalFieldsForUsage('edit'); toggleIntakeStatusRules('edit'); toggleLastReturnSiteFields('edit');}catch(e){}},0)} function saveIntakeEdit(id){let i=byId(db.items,id); if(!i.id)return; let oldSnap=itemHistorySnapshot(i); let rented=rentedQty(i.id), qty=Number(formVal('editQty')||1); if(!formVal('editOwner')||!formVal('editName')){alert('Firma ve malzeme adı gerekli');return} if(!isValidSettingItemNameOrCurrent(formVal('editName'),i.name)){alert('Lütfen Malzeme Adı Listesi içinden seçim yapınız. Manuel malzeme adı girilemez.');return} if(qty Depo Listesi bölümünden ekleyin.');return} if(isEmanetDeposuSahada(lastLoc)&&!formVal('editLastReturnCustomer')){alert('Son Kiradaki Konumu Emanet Deposu Sahada ise müşteri seçiniz.');return} if(wasBlocked && (svc==='Kiralanabilir'||svc==='Yükseltme Ekipman'||svc==='Vinç Sahibi Tarafından Kiralandı') && (editStatus==='İade Edildi'||before.status==='İade Edildi'||before.ownerReturned)) editStatus='Depoda'; let editTypeValue=cleanType(formVal('editType')); if(!isValidSettingType(editTypeValue)){alert('Lütfen Ayarlar > Tip Listesi içinden seçim yapınız. Manuel tip girilemez.');return} Object.assign(i,{code:formVal('editCode')||i.code,name:formVal('editName'),qty,ownerId:formVal('editOwner'),type:editTypeValue,lessor:intakeLessorForSave('edit',svc),serviceType:svc,maintenanceEnabled:svc!=='Yükseltme Ekipman'&&svc!=='Kiralanamaz',monthlyRent:intakeRentForSave('edit',svc),status:editStatus,entryLocation:editEntryLoc,entryCity:editEntryCity,entryDistrict:editEntryDistrict,location:editLoc,city:lastCity,district:lastDistrict,lastDepotEntryLocation:lastLoc,lastDepotEntryCity:lastCity,lastDepotEntryDistrict:lastDistrict,lastDepotEntryNote:formVal('editLastReturnNote'),lastDepotEntryCustomerId:isEmanetDeposuSahada(lastLoc)?formVal('editLastReturnCustomer'):'',lastDepotEntrySiteNote:isEmanetDeposuSahada(lastLoc)?formVal('editLastReturnSiteNote'):'',...towerFieldsFromForm('edit'),serial:formVal('editSerial'),created:formVal('editDate')||i.created||today()}); if(wasBlocked && isKirayaAlActiveItem(i)){ if(!db.kirayaAlLogs)db.kirayaAlLogs=[]; if(before.returnedQty>0){ if(!db.ownerTakeBacks)db.ownerTakeBacks=[]; db.ownerTakeBacks.push({id:uid('TAKE'),itemId:i.id,ownerId:i.ownerId,ownerName:nameOf(db.owners,i.ownerId),itemCode:i.code,itemName:i.name,qty:before.returnedQty,date:today(),location:i.location||'-',note:'Kiraya Al işlemiyle sahibinden tekrar aktif edildi',stockIncreased:false,fromKirayaAl:true}); } i.ownerReturned=false; i.kirayaAlApproved=true; if(i.status==='İade Edildi')i.status='Depoda'; db.kirayaAlLogs.push({id:uid('KAL'),itemId:i.id,ownerId:i.ownerId,ownerName:nameOf(db.owners,i.ownerId),itemCode:i.code,itemName:i.name,qty:qtyOf(i),date:today(),oldUsage:before.serviceType||'-',newUsage:itemServiceType(i),oldLessor:before.lessor||'-',lessor:itemLessor(i)||'-',oldStatus:before.status||'-',newStatus:i.status||'-',location:i.location||'-',note:'Stok / Malzeme Giriş Düzenle üzerinden Kiraya Al yapıldı'}); } let invSync=syncCustomerInvoicesAfterItemLessorChange(i.id,beforeServiceType,beforeLessor,itemServiceType(i),itemLessor(i)); recordItemHistory(i,oldSnap,'Güncelleme','Stok / Malzeme Giriş düzenleme ekranından güncellendi'); save(); closeModal(); render(); if(invSync.cancelled>0)alert('Kiraya Veren değiştiği için bu malzemeye ait '+invSync.cancelled+' adet Aylık Kira/Fatura kaydı iptal edildi.'); else if(invSync.added>0)alert('Kiraya Veren fatura oluşturan firma olarak değiştiği için '+invSync.added+' adet eksik Aylık Kira/Fatura kaydı oluşturuldu.')} function isSantiyeSahasindaBeklemekte(loc){let v=trNorm(loc||'');return v==='santiye sahasinda beklemekte' || v.includes('santiye sahasinda beklemekte');} function isEmanetDeposuSahada(loc){let v=trNorm(loc||'');return v==='emanet deposu sahada' || v==='emanet deposu saha' || v.includes('emanet deposu sahada') || v.includes('emanet deposu saha');} function isSantiyeSahasindaBeklemekte(loc){let v=trNorm(loc||'');return v==='santiye sahasinda beklemekte' || v.includes('santiye sahasinda beklemekte');} function isDepotStatusSiteInfoLocation(loc){return isEmanetDeposuSahada(loc)||isSantiyeSahasindaBeklemekte(loc);} function lastCustomerInfoForItem(itemId){ let item=byId(db.items||[],itemId); if(item&&item.id&&item.lastDepotEntryCustomerId){ return {customer:nameOf(db.customers,item.lastDepotEntryCustomerId),city:item.lastDepotEntryCity||item.city||'',district:item.lastDepotEntryDistrict||item.district||''}; } let moves=(db.moves||[]).filter(m=>m.itemId===itemId&&m.customerId).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))); if(moves.length)return {customer:nameOf(db.customers,moves[0].customerId),city:moves[0].city||'',district:moves[0].district||''}; let links=(db.rentalItems||[]).filter(ri=>ri.itemId===itemId).map(ri=>({ri,r:byId(db.rentals,ri.rentalId)})).filter(x=>x.r&&x.r.id&&x.r.customerId).sort((a,b)=>String(b.r.closedDate||b.r.end||b.r.start||'').localeCompare(String(a.r.closedDate||a.r.end||a.r.start||''))); if(links.length)return {customer:nameOf(db.customers,links[0].r.customerId),city:links[0].r.city||'',district:links[0].r.district||''}; return {customer:'',city:'',district:''}; } function toggleLastReturnSiteFields(prefix){let locEl=document.getElementById(prefix+'LastReturnLoc');let show=isEmanetDeposuSahada(locEl?locEl.value:'');['LastReturnCustomerWrap','LastReturnSiteNoteWrap'].forEach(s=>{let el=document.getElementById(prefix+s); if(el)el.style.display=show?'block':'none';}); if(!show){let c=document.getElementById(prefix+'LastReturnCustomer');let n=document.getElementById(prefix+'LastReturnSiteNote'); if(c)c.value=''; if(n)n.value='';}} function toggleReturnRemainCustomerFields(){let locEl=document.getElementById('retRemainLoc');let show=isDepotStatusSiteInfoLocation(locEl?locEl.value:'');let el=document.getElementById('retRemainCustomerWrap'); if(el)el.style.display=show?'block':'none';} function lastCustomerNameForItem(itemId){return lastCustomerInfoForItem(itemId).customer||'';} function rentableLocationCell(r){let loc=esc(r.location||'-'); if(r.showLastCustomer&&r.lastCustomer){return loc+'
Son Müşteri: '+esc(r.lastCustomer)+'';} return loc;} function rentableLastLocationCell(r){let loc=esc(r.lastLocation||'-'), extra=''; if(r.lastShowLastCustomer&&r.lastCustomer){extra+='
Son Müşteri: '+esc(r.lastCustomer)+'';} if(r.lastEmanetSite){let sameCustomer=((r.emanetCustomer||'').trim().toLocaleLowerCase('tr-TR')===(r.lastCustomer||'').trim().toLocaleLowerCase('tr-TR')); if(r.emanetCustomer&&!sameCustomer){extra+='
Müşteri: '+esc(r.emanetCustomer)+'';} if(r.emanetNote){extra+='
Açıklama: '+esc(r.emanetNote)+'';}} return loc+extra;} function rentableStockLocation(i){ // Kiralanabilir Malzemeler ekranında depo/konum olarak malzemenin güncel son konumu gösterilir. // Kirayı Kapat / Depoya Al işleminde seçilen depo lastDepotEntryLocation ve location alanlarına yazılır; // bu nedenle eski ilk giriş deposu bu ekranda öncelikli kullanılmamalıdır. return cleanDepot((i&&i.lastDepotEntryLocation)||'') || cleanDepot((i&&i.location)||'') || cleanDepot(itemEntryLocation(i)||'') || '-'; } function rentableStockCity(i){return cleanDepot((i&&i.lastDepotEntryCity)||'') || cleanDepot((i&&i.city)||'') || cleanDepot(itemEntryCity(i)||'') || '-';} function rentableStockDistrict(i){return cleanDepot((i&&i.lastDepotEntryDistrict)||'') || cleanDepot((i&&i.district)||'') || cleanDepot(itemEntryDistrict(i)||'') || '-';} function rentablesView(){ const keep=(id)=>{let el=document.getElementById(id); return el?el.value:'';}; let selectedType=keep('rentablesTypeFilter'); let selectedOwner=keep('rentablesOwnerFilter'); let selectedName=keep('rentablesNameFilter'); let selectedLoc=keep('rentablesLocFilter'); let selectedStatus=keep('rentablesStatusFilter'); let searchText=keep('rentablesSearchFilter'); let norm=(v)=>trNorm(String(v||'').trim()); let selectedTypeNorm=norm(selectedType); let matchesSearch=(r,q)=>!q || [r.owner,r.lessor,r.name,r.type,r.code,r.lastLocation,r.lastDepotNote,r.lastCity,r.lastDistrict,r.status,r.emanetCustomer,r.emanetNote].some(v=>norm(v).includes(q)); let rawRows=(db.items||[]).filter(i=>canItemBeRented(i)) .map(i=>({ id:i.id, owner:nameOf(db.owners,i.ownerId), lessor:itemLessor(i)||'-', name:i.name||'-', type:i.type||'-', code:i.code||'-', location:i.location||'-', city:i.city||'-', district:i.district||'-', lastLocation:rentableStockLocation(i), lastCity:rentableStockCity(i), lastDistrict:rentableStockDistrict(i), lastDepotNote:i.lastDepotEntryNote||'', showLastCustomer:isSantiyeSahasindaBeklemekte(i.location), lastShowLastCustomer:isSantiyeSahasindaBeklemekte(i.lastDepotEntryLocation), lastCustomer:(isSantiyeSahasindaBeklemekte(i.location)||isSantiyeSahasindaBeklemekte(i.lastDepotEntryLocation))?lastCustomerNameForItem(i.id):'', lastEmanetSite:isDepotStatusSiteInfoLocation(i.lastDepotEntryLocation), emanetCustomer:isDepotStatusSiteInfoLocation(i.lastDepotEntryLocation)?(nameOf(db.customers,i.lastDepotEntryCustomerId||'')||lastCustomerNameForItem(i.id)):'', emanetNote:isDepotStatusSiteInfoLocation(i.lastDepotEntryLocation)?(i.lastDepotEntrySiteNote||''):'', available:availableQty(i), rented:rentedQty(i.id), total:qtyOf(i), rent:Number(i.monthlyRent||0), status:stockStatus(i) })).sort((a,b)=>a.owner.localeCompare(b.owner,'tr')||a.name.localeCompare(b.name,'tr')||a.lastLocation.localeCompare(b.lastLocation,'tr')); let uniq=(arr)=>[...new Set(arr.filter(v=>v&&v!=='-'))].sort((a,b)=>String(a).localeCompare(String(b),'tr')); let typeOptions=materialTypeList(); let ownerOptions=uniq(rawRows.map(r=>r.owner)); let nameOptions=uniq(rawRows.map(r=>r.name)); let locOptions=uniq(rawRows.map(r=>r.lastLocation)); let statusOptions=uniq(rawRows.map(r=>r.status)); let opt=(arr,val,allText)=>``+arr.map(x=>``).join(''); let q=norm(searchText); let rows=rawRows .filter(r=>!selectedTypeNorm || norm(r.type)===selectedTypeNorm) .filter(r=>!selectedOwner || r.owner===selectedOwner) .filter(r=>!selectedName || r.name===selectedName) .filter(r=>!selectedLoc || r.lastLocation===selectedLoc) .filter(r=>!selectedStatus || r.status===selectedStatus) .filter(r=>matchesSearch(r,q)); let total=rows.reduce((s,r)=>s+r.available,0); let owners=new Set(rows.map(r=>r.owner)).size; let materials=new Set(rows.map(r=>r.name)).size; let depots=new Set(rows.map(r=>r.lastLocation)).size; let activeFilterParts=[]; if(selectedType)activeFilterParts.push('Tip: '+esc(selectedType)); if(selectedOwner)activeFilterParts.push('Sahip: '+esc(selectedOwner)); if(selectedName)activeFilterParts.push('Malzeme: '+esc(selectedName)); if(selectedLoc)activeFilterParts.push('Konum: '+esc(selectedLoc)); if(selectedStatus)activeFilterParts.push('Durum: '+esc(selectedStatus)); if(searchText)activeFilterParts.push('Arama: '+esc(searchText)); let activeFilterText=activeFilterParts.length?' • '+activeFilterParts.join(' • ')+'':''; let byOwner={}; rows.forEach(r=>{if(!byOwner[r.owner])byOwner[r.owner]={available:0,materials:new Set(),rows:[]}; byOwner[r.owner].available+=r.available; byOwner[r.owner].materials.add(r.name); byOwner[r.owner].rows.push(r);}); let ownerPanels=Object.entries(byOwner).map(([owner,g])=>{ let detail=g.rows.map(r=>[r.name,r.type,rentableLastLocationCell(r),r.lastCity,r.lastDistrict,r.available,r.rented,r.total,money(r.rent),r.code,badge(r.status),` `]); return `

${esc(owner)} • Kiralanabilir: ${g.available} adet • Malzeme çeşidi: ${g.materials.size}

${table(detail,['Malzeme','Tip','Son Kiradaki Konumu','Son İl','Son İlçe','Kiralanabilir Adet','Kirada','Toplam','Birim Aylık Kira','Kod','Durum','İşlem'])}
`; }).join(''); let typeFilterHtml=`

Filtreler

Filtreler birlikte çalışır. Konum filtresi, Stok / Malzeme Giriş ekranındaki Son Kiradaki Konumu bilgisini baz alır.
`; document.getElementById('topActions').innerHTML=''; document.getElementById('view').innerHTML=`
Bu bölüm sadece depoda bulunan, kiraya verilebilir adedi olan, sahibine iade edilmemiş ve Kiraya Al işlemi yapılmış malzemeleri gösterir. Sahibine iade edilen kayıtlar Genel Liste ve firma gruplarında listelenmez. Filtreler Tip, Malzeme Sahibi, Malzeme, Son Kiradaki Konumu, Durum ve Arama alanlarını birlikte uygular.
${typeFilterHtml}
Toplam Kiralanabilir Adet
${total}
Malzeme Sahibi Sayısı
${owners}
Malzeme Çeşidi
${materials}
Konum Sayısı
${depots}

Kiralanabilir Malzemeler Genel Liste ${activeFilterText}

${table(rows.map(r=>[r.owner,r.lessor,r.name,r.type,rentableLastLocationCell(r),r.lastDepotNote||'-',r.lastCity,r.lastDistrict,r.available,r.rented,r.total,money(r.rent),badge(r.status),` `]),['Malzeme Sahibi','Kiraya Veren','Malzeme','Tip','İlk Girilen Depo / Konum','Son Depoya Giriş Açıklaması','İlk Girilen İl','İlk Girilen İlçe','Kiralanabilir Adet','Kirada','Toplam','Birim Aylık Kira','Durum','İşlem'])}
${ownerPanels || '

Kiralanabilir Malzeme

Seçilen filtrelere uygun kiralanabilir malzeme yok.
'} `; } function rentableItemDetail(id){ let i=byId(db.items,id); if(!i||!i.id){alert('Malzeme kaydı bulunamadı.');return;} let owner=byId(db.owners,i.ownerId)||{}; let activeRows=(db.rentalItems||[]) .filter(ri=>ri.itemId===id) .map(ri=>({ri,r:byId(db.rentals,ri.rentalId)})) .filter(x=>x.r&&x.r.id&&x.r.status!=='Kapandı') .sort((a,b)=>(b.r.start||'').localeCompare(a.r.start||'')) .map(x=>[nameOf(db.customers,x.r.customerId),rentQtyOf(x.ri),x.r.start||'-',x.r.end||'-',x.r.depotLocation||i.location||'-',x.r.city||'-',x.r.district||'-',x.r.projectName||x.r.location||'-',badge(x.r.status||'Aktif')]); let returnRows=(db.ownerReturns||[]) .filter(r=>r.itemId===id) .slice().sort((a,b)=>(b.date||'').localeCompare(a.date||'')) .map(r=>[r.date||'-',r.ownerName||nameOf(db.owners,r.ownerId)||owner.name||'-',r.qty||0,r.returnedLocation||'İade Deposu',r.returnedCity||'-',r.returnedDistrict||'-',r.note||'-']); let moveRows=(db.moves||[]) .filter(m=>m.itemId===id) .slice().sort((a,b)=>(b.date||'').localeCompare(a.date||'')) .slice(0,12) .map(m=>[m.date||'-',m.type||'-',nameOf(db.customers,m.customerId),m.qty||0,m.location||'-',m.note||'-']); let invoiceRows=(db.rentMonths||[]) .filter(m=>m.itemId===id) .slice().sort((a,b)=>(b.periodStart||b.month||'').localeCompare(a.periodStart||a.month||'')) .slice(0,12) .map(m=>[monthTitle(m.month),periodText(m),m.qty||1,money(m.unitMonthly||m.unitAmount||0),money(m.amount||0),badge(m.invoiceStatus||'Kesilmedi'),m.invoiceNo||'-',m.invoiceDate||'-']); let generalRows=[ ['Kod',esc(i.code||'-')], ['Malzeme Sahibi',esc(owner.name||'-')], ['Yetkili / Telefon',esc((owner.person||'-')+' / '+(owner.phone||'-'))], ['Malzeme Adı',esc(i.name||'-')], ['Tip',esc(i.type||'-')], ...towerRowsForItem(i), ['Kullanım Tipi',esc(itemServiceType(i))], ['Kiraya Veren',esc(itemLessor(i))], ['Durum',badge(stockStatus(i))], ['Son Kiradaki Konumu',esc((i.lastDepotEntryLocation||'-')+' / '+(i.lastDepotEntryCity||'-')+' / '+(i.lastDepotEntryDistrict||'-'))], ['Son Depoya Giriş Açıklaması',esc(i.lastDepotEntryNote||'-')], ['Seri No / Açıklama',esc(i.serial||i.note||'-')], ['Giriş Tarihi',esc(i.created||'-')], ['Son Bakım',esc(formatDateTR(i.lastMaintenanceDate)||'-')], ['Sonraki Bakım',esc(formatDateTR(itemNextMaintenanceDate(i))||'-')], ['Bakım Durumu',badge(maintenanceStatus(i))] ]; openModal('Kiralanabilir Malzeme Detayı',`
${esc(i.name||'-')} / ${esc(i.type||'-')} — ${esc(owner.name||'-')} malzemesi.
Toplam Adet
${qtyOf(i)}
Kiralanabilir
${availableQty(i)}
Kirada
${rentedQty(i.id)}
Aylık Kira
${money(i.monthlyRent||0)}

Malzeme Bilgileri

${table(generalRows,['Alan','Bilgi'])}

Aktif Kiralama Bilgileri

${table(activeRows,['Müşteri','Adet','Başlangıç','Bitiş','Çıkış Deposu','İl','İlçe','Şantiye Proje Adı','Durum'])}

Son Fatura Dönemleri

${table(invoiceRows,['Ay','Dönem','Adet','Birim Aylık Kira','Tutar','Durum','Fatura No','Fatura Tarihi'])}

Sahibine İade Geçmişi

${table(returnRows,['İade Tarihi','Sahibinin Adı','Adet','İade Deposu','İl','İlçe','Açıklama'])}

Son Hareketler

${table(moveRows,['Tarih','İşlem','Müşteri','Adet','Depo / Konum','Açıklama'])}
`); } function isReturnedToOwnerItem(i){if(!i)return true; let st=trNorm(i.status||''), loc=trNorm(i.location||''), av=availableQty(i), rq=rentedQty(i.id); let markedReturned=!!i.ownerReturned || st==='iade edildi' || st.includes('sahibine iade') || loc.includes('sahibine iade') || loc.includes('iade edildi') || stockStatus(i)==='İade Edildi'; return markedReturned && av<=0 && rq<=0;} function rentThisItem(id){ pendingRentItemId=id||''; try{sessionStorage.setItem('pendingRentItemId',pendingRentItemId)}catch(e){} go('rent'); } function applyPendingRentItem(){ let id=pendingRentItemId||''; if(!id){try{id=sessionStorage.getItem('pendingRentItemId')||''}catch(e){}} if(!id)return; pendingRentItemId=''; try{sessionStorage.removeItem('pendingRentItemId')}catch(e){} setTimeout(()=>{ let sel=document.getElementById('ritem'); if(!sel)return; let exists=[...sel.options].some(op=>op.value===id); if(!exists){alert('Seçilen malzeme artık kiralanabilir stokta görünmüyor.'); return;} selectRentStockOption(id); let qty=document.getElementById('rqty'); if(qty){qty.focus(); qty.select&&qty.select();} },0); } function offerThisItem(id){ pendingOfferItemId=id||''; try{sessionStorage.setItem('pendingOfferItemId',pendingOfferItemId)}catch(e){} go('offers'); setTimeout(()=>offerForm(),60); } function takePendingOfferItem(){ let id=pendingOfferItemId||''; if(!id){try{id=sessionStorage.getItem('pendingOfferItemId')||''}catch(e){}} pendingOfferItemId=''; try{sessionStorage.removeItem('pendingOfferItemId')}catch(e){} return id; } function pendingOfferItemData(){ let id=takePendingOfferItem(); let item=byId(db.items,id); if(!item||!item.id)return null; return {item,desc:(item.name||'Malzeme')+(item.type?' - '+item.type:'')+' kira bedeli',qty:1,unitPrice:Number(item.monthlyRent||0)}; } function toggleInvestorRentFields(){ let item=byId(db.items,formVal('ritem')); let wrap=document.getElementById('investorRentWrap'); if(!wrap)return; let show=showInvestorRentFieldsForItem(item); wrap.style.display=show?'':'none'; if(show)syncInvestorRentDefaults(true); } function syncInvestorRentDefaults(force=false){ let wrap=document.getElementById('investorRentWrap'); if(wrap&&wrap.style.display==='none')return; let sEl=document.getElementById('rinvestorStart'), mEl=document.getElementById('rinvestorMonths'), eEl=document.getElementById('rinvestorEnd'), aEl=document.getElementById('rinvestorAmount'); if(sEl&&(force||!sEl.value))sEl.value=formVal('rstart')||today(); if(mEl&&(force||!mEl.value))mEl.value=Number(formVal('rmonths')||1)||1; if(eEl&&(force||!eEl.value))eEl.value=formVal('rend')||rentalEndFromMonths(formVal('rstart')||today(),Number(formVal('rmonths')||1)); if(aEl&&(force||!aEl.value))aEl.value=formVal('ramount')||0; } function syncInvestorEndFromMonths(){ let start=formVal('rinvestorStart')||formVal('rstart')||today(); let months=Number(formVal('rinvestorMonths')||1); if(months<1)months=1; let mEl=document.getElementById('rinvestorMonths'), eEl=document.getElementById('rinvestorEnd'); if(mEl)mEl.value=months; if(eEl)eEl.value=rentalEndFromMonths(start,months); } function syncInvestorMonthsFromEnd(){ let start=formVal('rinvestorStart')||formVal('rstart')||today(); let end=formVal('rinvestorEnd'); let mEl=document.getElementById('rinvestorMonths'); if(mEl&&end)mEl.value=Math.max(1,rentalMonthsFromEnd(start,end)); } function rentView(){let available=db.items.filter(i=>canItemBeRented(i));document.getElementById('view').innerHTML=`

Yeni Kiralama

Arama kutusuna yazınca sonuçlar altta görünür; satıra tıklayarak seçebilirsin.
Seçilen çıkış deposu/il/ilçe Aktif Kiralamalar’da gösterilir. Kısmi kiralamada Kiralanabilir Malzemeler’de kalan stokun ilk depo bilgisi değişmez.
Örn: 08.07.2026 + 7 ay = 08.02.2027. Dönemler 08-08 şeklinde oluşur.
Not: Ay hesabı başlangıç gününü korur. Örneğin 08.07.2026 başlangıç ve 7 ay seçilirse dönemler 08.07-08.08, 08.08-08.09 şeklinde oluşur. Tam ay dönemleri 1 aylık kira olarak hesaplanır. Aylık Kira/Fatura kaydı sadece kiraya veren "${invoiceLessorName()}" olan ve kullanım tipi "Vinç Sahibi Tarafından Kiralandı" olmayan malzemelerde oluşturulur.
`; applyPendingRentItem(); setTimeout(syncRentEndFromMonths,0);} function fillRentDefaults(){let item=byId(db.items,formVal('ritem')); if(item.id){document.getElementById('ramount').value=item.monthlyRent||0; document.getElementById('rqty').max=availableQty(item); document.getElementById('rqty').value=Math.min(Number(document.getElementById('rqty').value||1),availableQty(item))||1; let depot=document.getElementById('rdepot'); if(depot)depot.value=item.location||''; let city=document.getElementById('rcity'); if(city){city.value=item.city||''; updateDistrictSelect('rcity','rdistrict'); let dist=document.getElementById('rdistrict'); if(dist)dist.value=item.district||'';} toggleInvestorRentFields();}else{let wrap=document.getElementById('investorRentWrap'); if(wrap)wrap.style.display='none';}} function selectRentStockOption(id){let sel=document.getElementById('ritem'), inp=document.getElementById('ritemSearch'), box=document.getElementById('ritemResults'); if(!sel)return; sel.value=id; let op=sel.selectedOptions[0]; if(inp&&op)inp.value=op.textContent; if(box)box.innerHTML=''; fillRentDefaults();} function filterRentStockOptions(){let q=(document.getElementById('ritemSearch')?.value||'').toLowerCase().trim();let sel=document.getElementById('ritem'); if(!sel)return; let opts=[...sel.options].slice(1); opts.forEach(op=>{let hay=(op.dataset.search||op.textContent||'').toLowerCase(); op.hidden=!!(q && !hay.includes(q));}); if(sel.selectedOptions[0]?.hidden){sel.value=''; fillRentDefaults();} let box=document.getElementById('ritemResults'); if(!box)return; let list=opts.filter(op=>!op.hidden).slice(0,30); if(!list.length){box.innerHTML='
Sonuç bulunamadı.
';return} box.innerHTML=list.map(op=>``).join('');} function saveRental(){ let item=byId(db.items,formVal('ritem')); if(!formVal('rcustomer')||!item.id){alert('Müşteri ve malzeme sahibi/stok seçiniz');return} if(!canItemBeRented(item)){alert('Bu malzeme için önce Kiradan Dönüş / İade ekranından Kiraya Al işlemi yapılmalıdır. Kiraya Al yapılmadan kiraya verilemez.');return} let qty=Number(formVal('rqty')||1), av=availableQty(item); if(qty<1){alert('Adet en az 1 olmalı');return} if(qty>av){alert('Bu malzeme sahibinde depoda sadece '+av+' adet uygun malzeme var.');return} let depot=formVal('rdepot'); if(!depot){alert('Çıkış Deposu / Konum seçiniz.');return} let rid=uid('REN'); let unitMonthly=Number(formVal('ramount')||item.monthlyRent||0); let monthlyTotal=unitMonthly*qty; let start=formVal('rstart')||today(), end=formVal('rend')||''; let months=Number(formVal('rmonths')||1); if(months<1)months=1; if(!end)end=rentalEndFromMonths(start,months); else if(end!==rentalEndFromMonths(start,months)) months=rentalMonthsFromEnd(start,end); let city=formVal('rcity'), district=formVal('rdistrict'); let rentalItemRecord={id:uid('RIT'),rentalId:rid,itemId:item.id,qty,unitAmount:unitMonthly,unitMonthly,amount:monthlyTotal}; if(showInvestorRentFieldsForItem(item)){ let invStart=formVal('rinvestorStart'), invMonths=Number(formVal('rinvestorMonths')||1), invEnd=formVal('rinvestorEnd'), invAmount=formVal('rinvestorAmount'); if(invMonths<1)invMonths=1; if(!invEnd&&invStart)invEnd=rentalEndFromMonths(invStart,invMonths); if(!invStart||!invEnd||invAmount===''){alert('Yatırımcı kira başlangıç tarihi, kira ay, bitiş tarihi ve tutarı giriniz.');return} if(invEnd<=invStart){alert('Yatırımcı kira bitiş tarihi başlangıç tarihinden büyük olmalı.');return} rentalItemRecord.investorRentStart=invStart; rentalItemRecord.investorRentMonths=invMonths; rentalItemRecord.investorRentEnd=invEnd; rentalItemRecord.investorMonthlyAmount=Number(invAmount||0); } db.rentals.push({id:rid,customerId:formVal('rcustomer'),projectName:formVal('rloc'),location:formVal('rloc'),depotLocation:depot,city,district,start,end,invoiceDay:Number(formVal('rday')),months,status:'Aktif',note:formVal('rnote')}); db.rentalItems.push(rentalItemRecord); // Yeni Kiralama kaydedildiğinde bakım başlangıcı kiradaki Başlangıç Tarihi kabul edilir. // Bakım Takibi ekranındaki Son Bakım/Bakım Başlangıcı ve sonraki bakım tarihi buna göre güncellenir. if(itemServiceType(item)!=='Kiralanamaz' && itemServiceType(item)!=='Yükseltme Ekipman'){ item.maintenanceEnabled=true; item.maintenanceDate=start; item.lastMaintenanceDate=start; item.nextMaintenanceDate=addDaysISO(start,materialMaintenancePeriodDays()); } // Kısmi kiralamada seçilen depo/il/ilçe sadece aktif kiralama kaydında tutulur. // Böylece Kiralanabilir Malzemeler ekranındaki kalan stok, ilk girilen depo bilgisini göstermeye devam eder. if(qty>=av){ let oldSnap=itemHistorySnapshot(item); item.lastDepotEntryLocation=depot; item.lastDepotEntryCity=city; item.lastDepotEntryDistrict=district; item.lastDepotEntryDate=start; item.lastDepotEntryCustomerId=formVal('rcustomer'); item.lastDepotEntryNote='Kiraya Ver işleminde çıkış deposu olarak seçildi'; item.lastDepotEntrySiteNote=''; recordItemHistory(item,oldSnap,'Kiralama','Tüm kalan adet kiraya verildiği için Çıkış Deposu / Konum bilgisi Son Kiradaki Konumu alanına işlendi'); }else{ if(!db.moves)db.moves=[]; db.moves.push({id:uid('MOV'),type:'Kısmi Kiralama',itemId:item.id,itemCode:item.code,itemName:item.name,ownerId:item.ownerId,customerId:formVal('rcustomer'),rentalId:rid,qty,date:start,location:depot,city,district,note:'Kısmi kiralama çıkış depo/il/ilçe bilgisi aktif kiralama kaydında tutuldu'}); } if(itemCreatesCustomerInvoice(item)){ensureRentMonthsForRental(rid,months,end);} save(); go('rentals') } function rentalRemainingDays(r){if(!r||!r.end)return null;return invoiceDays(today(),r.end)} function rentalRemainingTimeText(r,history=false){ if(!r||!r.end)return '-'; if(history||r.status==='Kapandı')return 'Kapandı'; let todayStr=today(); let endStr=String(r.end||''); if(endStrSüre geçti: ${past} gün`;} let days=invoiceDays(todayStr,endStr); if(days===0)return `Bugün bitiyor`; let months=0; let cursor=todayStr; for(let i=0;i<240;i++){ let next=fmtDate(addMonthsSameDay(cursor,1)); if(next<=endStr){months++;cursor=next;}else break; } let remDays=invoiceDays(cursor,endStr); let detail=[]; if(months>0)detail.push(months+' ay'); if(remDays>0)detail.push(remDays+' gün'); let txt=detail.length?detail.join(' '):days+' gün'; return `${txt}
${days} gün kaldı`; } function rentalInvoiceMonths(r){return (db.rentMonths||[]).filter(m=>m.rentalId===(r&&r.id));} function rentalAllInvoicesCut(r){let months=rentalInvoiceMonths(r);return !!(r&&r.status!=='Kapandı'&&months.length&&months.every(m=>m.invoiceStatus==='Kesildi'));} function rentalExtensionWarningCell(r){ let months=rentalInvoiceMonths(r), invoiced=months.filter(m=>m.invoiceStatus==='Kesildi').length; if(rentalAllInvoicesCut(r))return `
✅ Tüm faturalar kesildi
Kira uzatması yapıldı mı / yapılacak mı?
`; let d=rentalRemainingDays(r);if(d===null)return '-';if(d<0)return `
⚠️ Süre geçti
${Math.abs(d)} gün gecikti
`;if(d<=31)return `
⚠️ Kira süresi 1 ay kaldı
${d} gün kaldı. Uzatma olacak mı?
`;return '-'} function rentalAllInvoiceWarningsPanel(active){let warn=(active||[]).filter(r=>rentalAllInvoicesCut(r));if(!warn.length)return '';let rows=warn.map(r=>{let ri=(db.rentalItems||[]).find(x=>x.rentalId===r.id)||{};let item=byId(db.items,ri.itemId);let months=rentalInvoiceMonths(r);return [nameOf(db.customers,r.customerId),nameOf(db.items,ri.itemId),nameOf(db.owners,item.ownerId),r.start||'-',r.end||'-',months.filter(m=>m.invoiceStatus==='Kesildi').length+'/'+months.length,` `]});return `

✅ Tüm Faturalar Kesildi — Kira Uzatma Kontrolü

Firmaya ait tüm aylık kira/fatura dönemleri kesilmiş aktif kiralamalar var. Bu kayıtlar için kira uzatması yapıldı mı / yapılacak mı kontrol et. Uzatma olacaksa Uzatma Yap, uzatma yoksa Uzatma Yok / Kirayı Kapat butonunu kullan.
${table(rows,['Müşteri','Malzeme','Malzeme Sahibi','Başlangıç','Bitiş','Kesilen Fatura','İşlem'])}
`} function rentalInvestorInvoiceMissingGroups(rid){ let r=byId(db.rentals,rid); if(!r.id)return []; let missing=[], seen=new Set(); (db.rentalItems||[]).filter(ri=>ri.rentalId===rid&&rentQtyOf(ri)>0).forEach(ri=>{ let item=byId(db.items,ri.itemId); if(!showInvestorRentFieldsForItem(item) || !rentalItemHasInvestorRentInfo(ri))return; let ownerId=item.ownerId||'unknown'; if(!ownerId || ownerId==='unknown')return; let start=ri.investorRentStart||r.start||today(); let end=ri.investorRentEnd||r.end||''; let months=end?rentalMonthsFromEnd(start,end):Number(ri.investorRentMonths||r.months||1); generateInvoicePeriods(start,months,end).forEach(pr=>{ let key=ownerInvoiceKey(ownerId,pr.month); if(seen.has(key))return; seen.add(key); let inv=ownerInvoiceMeta(ownerId,pr.month); if(ownerInvoiceDisplayStatus(inv)!=='Fatura Geldi'){ missing.push({ownerId,month:pr.month,itemName:item.name||'-',period:pr.start+' / '+pr.end}); } }); }); return missing.sort((a,b)=>nameOf(db.owners,a.ownerId).localeCompare(nameOf(db.owners,b.ownerId),'tr')||(a.month||'').localeCompare(b.month||'')); } function rentalInvestorInvoiceBlockMessage(missing){ if(!missing||!missing.length)return ''; let list=missing.slice(0,10).map(x=>nameOf(db.owners,x.ownerId)+' - '+monthTitle(x.month)+' - '+x.itemName+' ('+x.period+')').join('\n'); return 'Kiralama kapatılamaz. Bu kiralamaya ait yatırımcı kira faturası girilmemiş '+missing.length+' dönem var.\n\nÖnce Yatırımcı Faturaları bölümünden bu dönemlerin Fatura Tutarı, Fatura No ve Fatura Tarihi bilgilerini giriniz.\n\nEksik yatırımcı faturaları:\n'+list+(missing.length>10?'\n...':''); } function rentalExtensionWarningsPanel(active){let warn=(active||[]).filter(r=>{if(rentalAllInvoicesCut(r))return false;let d=rentalRemainingDays(r);return d!==null&&d<=31});if(!warn.length)return '';let rows=warn.map(r=>{let ri=(db.rentalItems||[]).find(x=>x.rentalId===r.id)||{};let item=byId(db.items,ri.itemId);let d=rentalRemainingDays(r);return [nameOf(db.customers,r.customerId),nameOf(db.items,ri.itemId),nameOf(db.owners,item.ownerId),r.start||'-',r.end||'-',d<0?('Süre '+Math.abs(d)+' gün geçti'):(d+' gün kaldı'),``]});return `

⚠️ Kira Uzatma Uyarısı

Kira süresinin bitmesine 1 ay veya daha az kalan kayıtlar var. Son kira dönemine girilmeden önce müşteriye uzatma olacak mı? diye kontrol edebilirsin.
${table(rows,['Müşteri','Malzeme','Malzeme Sahibi','Başlangıç','Bitiş','Kalan Süre','İşlem'])}
`} function rentalSummaryRow(r,history=false){let ri=db.rentalItems.find(x=>x.rentalId===r.id)||{};let item=byId(db.items,ri.itemId);let months=(db.rentMonths||[]).filter(m=>m.rentalId===r.id);let monthCount=months.length;let invoiced=months.filter(m=>m.invoiceStatus==='Kesildi').length;let total=months.reduce((s,m)=>s+Number(m.amount||0),0);let actions=history?` `:` `;return [nameOf(db.owners,item.ownerId),itemLessor(item),nameOf(db.customers,r.customerId),nameOf(db.items,ri.itemId),rentQtyOf(ri),r.depotLocation||item.location||'-',r.city||'-',r.district||'-',r.projectName||r.location||'-',r.start||'-',r.end||'-',rentalRemainingTimeText(r,history),monthCount,invoiced+'/'+monthCount,money(ri.unitAmount||ri.amount),money(total||ri.amount),badge(r.status),r.closedDate||'-',history?'-':rentalExtensionWarningCell(r),actions]} function rentalFilterGet(k){return localStorage.getItem('klm_rental_filter_'+k)||''} function rentalFilterSet(k,v){localStorage.setItem('klm_rental_filter_'+k,v||'');rentalsView()} function rentalFilterReset(){['q','customer','owner','lessor','item','city','district','invoice','due','startFrom','startTo','endFrom','endTo'].forEach(k=>localStorage.removeItem('klm_rental_filter_'+k));rentalsView()} function rentalFilterOptions(arr,sel){return ''+(arr||[]).map(x=>``).join('')} function rentalFilterTextOptions(values,sel){let list=[...new Set((values||[]).filter(Boolean))].sort((a,b)=>String(a).localeCompare(String(b),'tr'));return ''+list.map(v=>``).join('')} function rentalInvoiceFilterStatus(r){let months=rentalInvoiceMonths(r);if(!months.length)return 'Fatura Yok';let cut=months.filter(m=>m.invoiceStatus==='Kesildi').length;if(cut===0)return 'Kesilmedi';if(cut===months.length)return 'Tamamı Kesildi';return 'Kısmi Kesildi'} function rentalDueFilterOptions(sel){let opts=[['','Tümü'],['overdue','Süresi Geçenler'],['today','Bugün Bitecek'],['7','7 Gün İçinde Bitecek'],['15','15 Gün İçinde Bitecek'],['30','30 Gün İçinde Bitecek'],['60','60 Gün İçinde Bitecek'],['noEnd','Bitiş Tarihi Yok']];return opts.map(o=>``).join('')} function rentalDueFilterMatches(r){let f=rentalFilterGet('due');if(!f)return true;if(f==='noEnd')return !r.end;let d=rentalRemainingDays(r);if(d===null)return false;if(f==='overdue')return d<0;if(f==='today')return d===0;let n=Number(f);if(!isNaN(n))return d>=0&&d<=n;return true} function rentalMatchesActiveFilter(r){let ri=(db.rentalItems||[]).find(x=>x.rentalId===r.id)||{}, item=byId(db.items,ri.itemId), q=rentalFilterGet('q').toLocaleLowerCase('tr-TR').trim();let hay=[nameOf(db.owners,item.ownerId),itemLessor(item),nameOf(db.customers,r.customerId),nameOf(db.items,ri.itemId),item.type,r.depotLocation||item.location,r.city,r.district,r.projectName||r.location,r.start,r.end,r.status,r.note].join(' ').toLocaleLowerCase('tr-TR');if(q&& !hay.includes(q))return false;if(rentalFilterGet('customer')&&r.customerId!==rentalFilterGet('customer'))return false;if(rentalFilterGet('owner')&&item.ownerId!==rentalFilterGet('owner'))return false;if(rentalFilterGet('lessor')&&String(itemLessor(item))!==String(rentalFilterGet('lessor')))return false;if(rentalFilterGet('item')&&ri.itemId!==rentalFilterGet('item'))return false;if(rentalFilterGet('city')&&String(r.city||'')!==String(rentalFilterGet('city')))return false;if(rentalFilterGet('district')&&String(r.district||'')!==String(rentalFilterGet('district')))return false;if(rentalFilterGet('invoice')&&rentalInvoiceFilterStatus(r)!==rentalFilterGet('invoice'))return false;if(!rentalDueFilterMatches(r))return false;let sf=rentalFilterGet('startFrom'), st=rentalFilterGet('startTo'), ef=rentalFilterGet('endFrom'), et=rentalFilterGet('endTo');if(sf&&String(r.start||'')st)return false;if(ef&&String(r.end||'')et)return false;return true} function activeRentalFilterPanel(active,filtered){let relatedItems=(db.rentalItems||[]).filter(ri=>active.some(r=>r.id===ri.rentalId)).map(ri=>byId(db.items,ri.itemId)).filter(i=>i&&i.id);let lessors=[...new Set(relatedItems.map(i=>itemLessor(i)).filter(v=>v&&v!=='-'))].sort((a,b)=>String(a).localeCompare(String(b),'tr'));let cities=[...new Set(active.map(r=>r.city).filter(Boolean))];let districts=[...new Set(active.map(r=>r.district).filter(Boolean))];let q=rentalFilterGet('q');return `

Aktif Kiralamalar Filtreleme

Yaklaşan veya süresi geçen kiraları listeler.
Gösterilen: ${filtered.length} / Toplam aktif: ${active.length}
`} function rentalsView(){let active=(db.rentals||[]).filter(r=>r.status!=='Kapandı');let filteredActive=active.filter(rentalMatchesActiveFilter);let closed=(db.rentals||[]).filter(r=>r.status==='Kapandı');let heads=['Malzeme Sahibi','Kiraya Veren','Müşteri','Malzeme','Verilen Adet','Çıkış Deposu / Konum','İl','İlçe','Şantiye Proje Adı','Başlangıç','Bitiş','Kalan Kira Süresi','Fatura Dönemi','Kesilen Fatura','Birim Aylık Kira','Toplam Fatura','Durum','Kapanış','Uzatma Uyarısı','İşlem'];document.getElementById('view').innerHTML=`${rentalAllInvoiceWarningsPanel(active)}${rentalExtensionWarningsPanel(active)}
Kirayı Kapat işlemi şifre ister. Bir kiralama sadece bütün aylık faturaları Kesildi olduğunda kapatılır; Kiraya Veren SİSTEM KULE VİNÇ ise ilgili Yatırımcı Faturaları da girilmiş olmalıdır. Eksik müşteri faturası veya yatırımcı faturası varsa kapatma engellenir. Kapanan kiralamalar Aylık Kira/Fatura ekranından çıkar ve aşağıdaki Geçmiş Kiralamalar bölümünde görünür.
${activeRentalFilterPanel(active,filteredActive)}

Aktif Kiralamalar

${table(filteredActive.map(r=>rentalSummaryRow(r,false)),heads)}

Geçmiş Kiralamalar

${table(closed.map(r=>rentalSummaryRow(r,true)),heads)}
`} function closeRental(id){ let r=byId(db.rentals,id); if(!r.id)return; let months=(db.rentMonths||[]).filter(m=>m.rentalId===id); if(!months.length && rentalCreatesCustomerInvoice(id)){alert('Bu kiralamaya ait aylık kira/fatura kaydı bulunamadı. Kapatmadan önce fatura dönemlerini oluşturun.');return} let missing=months.filter(m=>m.invoiceStatus!=='Kesildi'); if(missing.length){let list=missing.slice(0,8).map(m=>monthTitle(m.month)+' ('+periodText(m)+')').join('\n'); alert('Kiralama kapatılamaz. Bu kiralamada fatura kesilmemiş '+missing.length+' dönem var.\n\nEksik dönemler:\n'+list+(missing.length>8?'\n...':'')); return;} let investorMissing=rentalInvestorInvoiceMissingGroups(id); if(investorMissing.length){alert(rentalInvestorInvoiceBlockMessage(investorMissing)); save(); render(); return;} let items=(db.rentalItems||[]).filter(ri=>ri.rentalId===id&&rentQtyOf(ri)>0); if(!items.length){alert('Bu kiralamada depoya alınacak aktif malzeme/adet bulunamadı.');return} let depoOpts=depotOptions(''); if(!depoOpts){alert('Depo listesi boş. Önce Ayarlar > Depo Listesi bölümünden depo ekleyin.');return} let itemRows=items.map(ri=>{let it=byId(db.items,ri.itemId);return [it.code||'-',it.name||'-',nameOf(db.owners,it.ownerId),rentQtyOf(ri)]}); openModal('Kirayı Kapat / Depoya Al',`
Bu kiralama kapatılacak.
Tüm faturalar kesilmiş görünüyor. Depoya alınacak konumu seçin. Seçtiğin depo / il / ilçe malzemenin Son Kiradaki Konumu alanına işlenir ve Depo Durumu sayımları bu alandan yapılır.

Depoya Alınacak Malzemeler

${table(itemRows,['Kod','Malzeme','Malzeme Sahibi','Adet'])}
Ayarlar > Depo Listesi bölümünde oluşturduğun depolar görünür.
`); } function saveCloseRental(id){ let r=byId(db.rentals,id); if(!r.id)return; let loc=formVal('closeDepot'), closeCity=formVal('closeCity'), closeDistrict=formVal('closeDistrict'), pass=formVal('closePass'), closeDate=formVal('closeDate')||today(), closeNote=formVal('closeNote'); if(!loc){alert('Depo / konum seçiniz.');return} if(!closeCity){alert('Depoya alınacak il seçiniz.');return} if(!closeDistrict){alert('Depoya alınacak ilçe seçiniz.');return} if(String(pass).trim()!=='735466'){alert('Şifre hatalı. Kiralama kapatılmadı.');return} let months=(db.rentMonths||[]).filter(m=>m.rentalId===id); let missing=months.filter(m=>m.invoiceStatus!=='Kesildi'); if(missing.length){alert('Kiralama kapatılamaz. Fatura kesilmemiş dönem var.');return} let investorMissing=rentalInvestorInvoiceMissingGroups(id); if(investorMissing.length){alert(rentalInvestorInvoiceBlockMessage(investorMissing)); save(); closeModal(); render(); return} let items=(db.rentalItems||[]).filter(ri=>ri.rentalId===id&&rentQtyOf(ri)>0); items.forEach(ri=>{let it=byId(db.items,ri.itemId); if(!it.id)return; it.status='Depoda'; it.location=loc; it.city=closeCity; it.district=closeDistrict; it.lastDepotEntryLocation=loc; it.lastDepotEntryCity=closeCity; it.lastDepotEntryDistrict=closeDistrict; it.lastDepotEntryDate=closeDate; it.lastDepotEntryNote=closeNote||'Kira kapatılırken depoya alındı'; it.lastDepotEntryCustomerId=isDepotStatusSiteInfoLocation(loc)?r.customerId:''; it.lastDepotEntrySiteNote=isDepotStatusSiteInfoLocation(loc)?(closeNote||''):''; if(!db.moves)db.moves=[]; db.moves.push({id:uid('MOV'),type:'Kira Kapatma Depoya Al',itemId:it.id,itemCode:it.code,itemName:it.name,ownerId:it.ownerId,customerId:r.customerId,rentalId:id,qty:rentQtyOf(ri),date:closeDate,location:loc,city:closeCity,district:closeDistrict,note:closeNote||'Kira kapatılırken depoya alındı'});}); r.status='Kapandı'; r.closedDate=closeDate; r.closedDepot=loc; r.closedDepotCity=closeCity; r.closedDepotDistrict=closeDistrict; r.closeNote=closeNote; save(); closeModal(); render(); } function closedRentalDetails(id){let r=byId(db.rentals,id); if(!r.id)return; let ri=db.rentalItems.find(x=>x.rentalId===id)||{}; let item=byId(db.items,ri.itemId); let rows=(db.rentMonths||[]).filter(m=>m.rentalId===id).sort((a,b)=>(a.periodStart||'').localeCompare(b.periodStart||'')).map(m=>[monthTitle(m.month),periodText(m),m.days||30,nameOf(db.owners,item.ownerId),nameOf(db.items,m.itemId),m.qty||1,money(m.unitMonthly||m.unitAmount||0),money(m.amount),badge(m.invoiceStatus),m.invoiceNo||'-',m.invoiceDate||'-']); openModal('Geçmiş Kiralama Detayı',`
Müşteri: ${nameOf(db.customers,r.customerId)}   |   Çıkış Deposu: ${esc(r.depotLocation||item.location||'-')}   |   İl/İlçe: ${esc(r.city||'-')} / ${esc(r.district||'-')}   |   Şantiye Proje Adı: ${esc(r.projectName||r.location||'-')}   |   Kapanış: ${r.closedDate||'-'}   |   Depoya Giriş: ${esc(r.closedDepot||'-')} / ${esc(r.closedDepotCity||'-')} / ${esc(r.closedDepotDistrict||'-')}

Kesilmiş Fatura Dönemleri

${table(rows,['Ay','Dönem','Gün','Malzeme Sahibi','Malzeme','Adet','Birim Aylık Kira','Tutar','Durum','Fatura No','Fatura Tarihi'])}
`)} function deleteRental(id){let r=byId(db.rentals,id); let customer=nameOf(db.customers,r.customerId); if(requireDeletePassword(customer+' müşterisine ait bu kiralama silinsin mi? Bu kiralamaya bağlı aylık kira/fatura kayıtları da silinecek.')){db.rentals=db.rentals.filter(x=>x.id!==id); db.rentalItems=db.rentalItems.filter(x=>x.rentalId!==id); db.rentMonths=db.rentMonths.filter(x=>x.rentalId!==id); save(); render();}} function rentalMonthCount(id){return (db.rentMonths||[]).filter(m=>m.rentalId===id).length} function ensureRentMonthsForRental(rid,count,endOverride=''){ let r=byId(db.rentals,rid); if(!r.id)return 0; let months=Number(count||r.months||rentalMonthCount(rid)||12); if(months<1)months=1; let end=endOverride||r.end||''; let periods=generateInvoicePeriods(r.start||today(),months,end); let added=0; (db.rentalItems||[]).filter(ri=>ri.rentalId===rid&&rentQtyOf(ri)>0&&itemCreatesCustomerInvoice(byId(db.items,ri.itemId))).forEach(ri=>{ periods.forEach(pr=>{ let exists=(db.rentMonths||[]).some(m=>m.rentalId===rid&&m.itemId===ri.itemId&&m.periodStart===pr.start&&m.periodEnd===pr.end); if(exists)return; let unitMonthly=Number(ri.unitMonthly||ri.unitAmount||0), qty=rentQtyOf(ri); let amount=Math.round(((unitMonthly/30)*pr.days*qty)*100)/100; db.rentMonths.push({id:uid('MON'),rentalId:rid,itemId:ri.itemId,qty,unitAmount:unitMonthly,unitMonthly,month:pr.month,periodStart:pr.start,periodEnd:pr.end,days:pr.days,amount,invoiceStatus:'Kesilmedi',invoiceNo:'',invoiceDate:''}); added++; }); }); return added; } function editRental(id){ let r=byId(db.rentals,id); if(!r.id)return; let ris=(db.rentalItems||[]).filter(x=>x.rentalId===id); let ri=ris[0]||{}; let item=byId(db.items,ri.itemId); let existing=rentalMonthCount(id); let months=Number(r.months||existing||12); let showInvestor=showInvestorRentFieldsForItem(item); let invStart=ri.investorRentStart||r.start||today(); let invMonths=Number(ri.investorRentMonths||r.months||months||1); if(invMonths<1)invMonths=1; let invEnd=ri.investorRentEnd||r.end||rentalEndFromMonths(invStart,invMonths); let invAmount=(ri.investorMonthlyAmount!==undefined&&ri.investorMonthlyAmount!==null&&ri.investorMonthlyAmount!=='')?ri.investorMonthlyAmount:(ri.unitMonthly||ri.unitAmount||0); let investorHtml=showInvestor?`

Yatırımcı Kira Süreleri / Tutarı

Bu bölüm yalnızca Kiraya Veren = SİSTEM KULE VİNÇ olan kiralamalarda görünür. Burada kira ayı uzatılırsa Yatırımcı Faturaları’na yeni aylar eklenir; kira ayı kısaltılırsa fazla yatırımcı fatura dönemleri listeden çıkar.
`:''; openModal('Aktif Kiralama Düzenle',`
Kira süresi uzadıysa Bitiş Tarihi veya Oluşturulacak Ay Sayısı değerini artırıp kaydedin. Program eksik yeni fatura dönemlerini ekler; süre kısaltılırsa fazla aylar uyarı vermeden silinir. Yatırımcı kira süresi de aşağıdaki ayrı bölümden aynı şekilde uzatılıp/kısaltılabilir.
Bu bilgi Aktif Kiralamalar tablosunda gösterilir; stokta kalan malzemenin ilk depo bilgisi değişmez.
Mevcut fatura dönemi: ${existing}. Ay sayısı değişince bitiş tarihi otomatik hesaplanır.
${investorHtml}
Malzeme: ${nameOf(db.items,ri.itemId)}   |   Malzeme Sahibi: ${nameOf(db.owners,item.ownerId)}   |   Kiraya Veren: ${esc(itemLessor(item))}
`) } function saveRentalEdit(id){ let r=byId(db.rentals,id); if(!r.id)return; let oldStart=r.start||today(); let oldMonths=Number(r.months||rentalMonthCount(id)||1); let newStart=formVal('editStart')||oldStart; let newEnd=formVal('editEnd')||''; let newMonths=Number(formVal('editMonths')||oldMonths||1); if(newMonths<1)newMonths=1; if(!newEnd)newEnd=rentalEndFromMonths(newStart,newMonths); else if(newEnd!==rentalEndFromMonths(newStart,newMonths)) newMonths=rentalMonthsFromEnd(newStart,newEnd); let unit=Number(formVal('editUnit')||0), qty=Number(formVal('editQty')||1); if(qty<1){alert('Adet en az 1 olmalı.');return} let ris=(db.rentalItems||[]).filter(x=>x.rentalId===id); let primaryItem=byId(db.items,(ris[0]||{}).itemId); let editInvestorData=null; if(showInvestorRentFieldsForItem(primaryItem)){ let invStart=formVal('editInvestorStart')||newStart; let invMonths=Number(formVal('editInvestorMonths')||newMonths||1); if(invMonths<1)invMonths=1; let invEnd=formVal('editInvestorEnd')||rentalEndFromMonths(invStart,invMonths); let invAmount=formVal('editInvestorAmount'); if(!invStart||!invEnd||invAmount===''){alert('Yatırımcı kira başlangıç tarihi, kira ay, bitiş tarihi ve tutarı giriniz.');return} if(invEnd<=invStart){alert('Yatırımcı kira bitiş tarihi başlangıç tarihinden büyük olmalı.');return} editInvestorData={start:invStart,months:invMonths,end:invEnd,amount:Number(invAmount||0)}; } // Yeni seçilen ay sayısına göre kalması gereken fatura dönemlerini hesapla. let allowedPeriods=generateInvoicePeriods(newStart,newMonths,newEnd); let allowedKeys=new Set(allowedPeriods.map(p=>p.start+'|'+p.end)); // Ay sayısı düşürülürse seçilen sürenin dışında kalan dönemler uyarı vermeden silinir. // Fatura edilmiş olsa bile kullanıcının yeni süre tercihi esas alınır. r.customerId=formVal('editCustomer')||r.customerId; r.start=newStart; r.end=newEnd; r.invoiceDay=Number(formVal('editDay')||1); r.projectName=formVal('editLoc'); r.location=formVal('editLoc'); r.depotLocation=formVal('editDepot'); r.city=formVal('editCity'); r.district=formVal('editDistrict'); // Aktif kiralama düzenlemesindeki çıkış depo/il/ilçe bilgisi kiralama kaydında kalır; // stokta kalan malzemenin Kiralanabilir Malzemeler ekranındaki ilk depo bilgisi değiştirilmez. r.note=formVal('editNote'); r.months=newMonths; ris.forEach(ri=>{ ri.qty=qty; ri.unitAmount=unit; ri.unitMonthly=unit; ri.amount=unit*qty; let riItem=byId(db.items,ri.itemId); if(showInvestorRentFieldsForItem(riItem) && editInvestorData){ ri.investorRentStart=editInvestorData.start; ri.investorRentMonths=editInvestorData.months; ri.investorRentEnd=editInvestorData.end; ri.investorMonthlyAmount=editInvestorData.amount; }else if(!showInvestorRentFieldsForItem(riItem)){ delete ri.investorRentStart; delete ri.investorRentMonths; delete ri.investorRentEnd; delete ri.investorMonthlyAmount; } }); // Başlangıç/bitiş/ay sayısı değiştiğinde seçilen dönemlerin dışındaki tüm fazla ayları sil. let before=(db.rentMonths||[]).filter(m=>m.rentalId===id).length; db.rentMonths=(db.rentMonths||[]).filter(m=>{ if(m.rentalId!==id)return true; let key=(m.periodStart||'')+'|'+(m.periodEnd||''); return allowedKeys.has(key); }); let removed=before-(db.rentMonths||[]).filter(m=>m.rentalId===id).length; // Başlangıç tarihi değiştiyse seçili dönemlerin içinde kalsa bile fatura edilmemiş eski dönemleri yeniden oluşturmak için temizle. if(r.start!==oldStart){ db.rentMonths=(db.rentMonths||[]).filter(m=>!(m.rentalId===id&&m.invoiceStatus!=='Kesildi')); } (db.rentMonths||[]).filter(m=>m.rentalId===id&&m.invoiceStatus!=='Kesildi').forEach(m=>{m.qty=qty; m.unitAmount=unit; m.unitMonthly=unit; recalcMonthAmount(m);}); let added=ensureRentMonthsForRental(id,r.months,r.end); save(); closeModal(); render(); alert('Kiralama güncellendi. Aylık Kira/Fatura eklenen dönem: '+added+' | Silinen fazla dönem: '+removed+(editInvestorData?' | Yatırımcı faturaları yeni yatırımcı kira süresine göre güncellendi.':'')); } function pad2(n){return String(n).padStart(2,'0')} function parseLocalDate(s){let [y,m,d]=String(s||today()).split('-').map(Number); return new Date(y,(m||1)-1,d||1)} function fmtDate(d){return d.getFullYear()+'-'+pad2(d.getMonth()+1)+'-'+pad2(d.getDate())} function lastDayOfMonth(y,mIndex){return new Date(y,mIndex+1,0).getDate()} function addMonthsSameDay(startStr,count){ let start=parseLocalDate(startStr||today()); let target=new Date(start.getFullYear(),start.getMonth()+Number(count||0),1); let day=Math.min(start.getDate(),lastDayOfMonth(target.getFullYear(),target.getMonth())); return new Date(target.getFullYear(),target.getMonth(),day); } function periodEndFromStart(startStr){return fmtDate(addMonthsSameDay(startStr||today(),1))} function invoiceDays(startStr,endStr){let a=parseLocalDate(startStr), b=parseLocalDate(endStr); return Math.max(0,Math.round((b-a)/86400000))} function rentalPeriodDays(periodStart,periodEnd,expectedEnd){ // Tam ay dönemlerinde tutar her zaman 1 aylık kira olarak hesaplansın. // Örnek: 08.07.2026 - 08.08.2026 = 1. AY = 30 gün kabul edilir. if(fmtDate(periodEnd)===fmtDate(expectedEnd))return 30; return Math.max(0,invoiceDays(fmtDate(periodStart),fmtDate(periodEnd))); } function generateInvoicePeriods(startStr,count,endStr=''){ let start=parseLocalDate(startStr||today()), periods=[], finalEnd=endStr?parseLocalDate(endStr):null; for(let k=0;k=finalEnd) break; if(finalEnd && finalEnd0) periods.push({month:fmtDate(monthStart).slice(0,7),start:fmtDate(monthStart),end:fmtDate(monthEnd),days}); } return periods; } function rentalEndFromMonths(startStr,count){return fmtDate(addMonthsSameDay(startStr||today(),Number(count||1)));} function rentalMonthsFromEnd(startStr,endStr){if(!endStr)return 1; let periods=generateInvoicePeriods(startStr||today(),120,endStr); return Math.max(1,periods.length||1);} function syncRentEndFromMonths(){let start=formVal('rstart')||today(); let months=Number(formVal('rmonths')||1); if(months<1)months=1; let end=rentalEndFromMonths(start,months); let el=document.getElementById('rend'); if(el)el.value=end; syncInvestorRentDefaults();} function syncRentMonthsFromEnd(){let start=formVal('rstart')||today(); let end=formVal('rend'); if(!end)return; let el=document.getElementById('rmonths'); if(el)el.value=rentalMonthsFromEnd(start,end); syncInvestorRentDefaults();} function syncEditEndFromMonths(){let start=formVal('editStart')||today(); let months=Number(formVal('editMonths')||1); if(months<1)months=1; let end=rentalEndFromMonths(start,months); let el=document.getElementById('editEnd'); if(el)el.value=end;} function syncEditMonthsFromEnd(){let start=formVal('editStart')||today(); let end=formVal('editEnd'); if(!end)return; let el=document.getElementById('editMonths'); if(el)el.value=rentalMonthsFromEnd(start,end);} function syncEditInvestorEndFromMonths(){let start=formVal('editInvestorStart')||formVal('editStart')||today(); let months=Number(formVal('editInvestorMonths')||1); if(months<1)months=1; let mEl=document.getElementById('editInvestorMonths'), eEl=document.getElementById('editInvestorEnd'); if(mEl)mEl.value=months; if(eEl)eEl.value=rentalEndFromMonths(start,months);} function syncEditInvestorMonthsFromEnd(){let start=formVal('editInvestorStart')||formVal('editStart')||today(); let end=formVal('editInvestorEnd'); let mEl=document.getElementById('editInvestorMonths'); if(mEl&&end)mEl.value=Math.max(1,rentalMonthsFromEnd(start,end));} function periodText(m){return (m.periodStart||((m.month||'')+'-01'))+' / '+(m.periodEnd||periodEndFromStart((m.month||ym(today()))+'-01'))} function monthTitle(m){let parts=String(m||'').split('-'); if(parts.length<2)return m||'-'; const names=['Ocak','Şubat','Mart','Nisan','Mayıs','Haziran','Temmuz','Ağustos','Eylül','Ekim','Kasım','Aralık']; return names[Number(parts[1])-1]+' '+parts[0];} function monthsView(){ document.getElementById('topActions').innerHTML=``; let prevSearch=document.getElementById('monthSearch')?document.getElementById('monthSearch').value:''; let prevCustomer=document.getElementById('monthCustomerFilter')?document.getElementById('monthCustomerFilter').value:''; let prevStatus=document.getElementById('monthStatusFilter')?document.getElementById('monthStatusFilter').value:''; let prevMonth=document.getElementById('monthPeriodFilter')?document.getElementById('monthPeriodFilter').value:''; let quick=window.__monthQuickFilter||null; let prevPastOnly=false; if(quick){ prevSearch=quick.search||''; prevCustomer=quick.customer||''; prevStatus=quick.status||''; prevMonth=quick.month||''; prevPastOnly=!!quick.pastOnly; } let activeMonthRows=(db.rentMonths||[]).filter(m=>{let r=byId(db.rentals,m.rentalId); return r.status!=='Kapandı';}); if(!activeMonthRows.length){document.getElementById('view').innerHTML='
Aktif kiralamaya ait aylık kira/fatura kaydı yok. Kapatılan kiralamalar Aktif Kiralamalar > Geçmiş Kiralamalar bölümünde görünür.
';return} let customers=[...new Set(activeMonthRows.map(m=>{let r=byId(db.rentals,m.rentalId); return r.customerId||'unknown'}))].sort((a,b)=>nameOf(db.customers,a).localeCompare(nameOf(db.customers,b),'tr')); let months=[...new Set(activeMonthRows.map(m=>m.month||''))].filter(Boolean).sort(); let filtered=activeMonthRows.filter(m=>{ let rental=byId(db.rentals,m.rentalId), item=byId(db.items,m.itemId), owner=byId(db.owners,item.ownerId); let hay=[monthTitle(m.month),periodText(m),nameOf(db.customers,rental.customerId),nameOf(db.items,m.itemId),nameOf(db.owners,item.ownerId),rental.location||'',m.invoiceNo||'',m.invoiceDate||'',item.type||''].join(' ').toLocaleLowerCase('tr-TR'); if(prevSearch && !hay.includes(prevSearch.toLocaleLowerCase('tr-TR'))) return false; if(prevCustomer && String(rental.customerId||'unknown')!==String(prevCustomer)) return false; if(prevStatus && String(m.invoiceStatus||'Kesilmedi')!==String(prevStatus)) return false; if(prevMonth && String(m.month||'')!==String(prevMonth)) return false; if(prevPastOnly && !(String(m.month||'') && String(m.month||'')s+Number(m.amount||0),0); let invoiced=filtered.filter(m=>m.invoiceStatus==='Kesildi').reduce((s,m)=>s+Number(m.amount||0),0); let waiting=total-invoiced; let count=filtered.length; let invCount=filtered.filter(m=>m.invoiceStatus==='Kesildi').length; let waitCount=count-invCount; let pct=count?Math.round(invCount/count*100):0; let customerOptions=''+customers.map(cid=>``).join(''); let monthOptions=''+months.map(mm=>``).join(''); let filterHtml=`

Aylık Kira / Fatura Yönetimi

Müşteri ve malzeme bazlı fatura dönemlerini, kesilen/bekleyen tutarları ve fatura bilgilerini tek ekranda takip et.

${pct}% tamamlandı
${invCount} kesildi • ${waitCount} bekliyor
Toplam Fatura Tutarı
${money(total)}
Fatura Edilen
${money(invoiced)}
Bekleyen
${money(waiting)}
Kayıt / Bekleyen
${count} / ${waitCount}

Filtreler

`; setTimeout(()=>['monthSearch','monthCustomerFilter','monthStatusFilter','monthPeriodFilter'].forEach(id=>{let el=document.getElementById(id); if(el){el.oninput=()=>monthsView(); el.onchange=()=>monthsView();}}),0); if(!filtered.length){document.getElementById('view').innerHTML=filterHtml+'
Seçilen filtrelere uygun aylık kira/fatura kaydı bulunamadı.
';return} let customerIds=[...new Set(filtered.map(m=>{let r=byId(db.rentals,m.rentalId); return r.customerId||'unknown'}))].sort((a,b)=>nameOf(db.customers,a).localeCompare(nameOf(db.customers,b),'tr')); let html=customerIds.map(cid=>{ let customerRows=filtered.filter(m=>{let r=byId(db.rentals,m.rentalId); return (r.customerId||'unknown')===cid;}).sort((a,b)=>(a.month||'').localeCompare(b.month||'')); let cTotal=customerRows.reduce((s,m)=>s+Number(m.amount||0),0); let cInvoiced=customerRows.filter(m=>m.invoiceStatus==='Kesildi').reduce((s,m)=>s+Number(m.amount||0),0); let cWaiting=cTotal-cInvoiced; let cInvCount=customerRows.filter(m=>m.invoiceStatus==='Kesildi').length; let cPct=customerRows.length?Math.round(cInvCount/customerRows.length*100):0; let monthCount=[...new Set(customerRows.map(m=>m.month))].length; let itemIds=[...new Set(customerRows.map(m=>m.itemId||'unknown'))].sort((a,b)=>nameOf(db.items,a).localeCompare(nameOf(db.items,b),'tr')); let initial=(nameOf(db.customers,cid)||'?').trim().slice(0,1).toLocaleUpperCase('tr-TR'); let itemHtml=itemIds.map(itemId=>{ let rows=customerRows.filter(m=>(m.itemId||'unknown')===itemId).sort((a,b)=>(a.month||'').localeCompare(b.month||'')); let firstItem=byId(db.items,itemId); let itemTotal=rows.reduce((s,m)=>s+Number(m.amount||0),0); let itemInvoiced=rows.filter(m=>m.invoiceStatus==='Kesildi').reduce((s,m)=>s+Number(m.amount||0),0); let itemWaiting=itemTotal-itemInvoiced; let itemQty=rows.reduce((s,m)=>s+Number(m.qty||0),0); let tableHtml=table(rows.map(m=>{ let rental=byId(db.rentals,m.rentalId), item=byId(db.items,m.itemId); let actions=m.invoiceStatus==='Kesildi' ? `
` : `
`; return [monthTitle(m.month), periodText(m), m.days||30, nameOf(db.owners,item.ownerId), m.qty||1, money(m.unitMonthly||m.unitAmount||m.amount), money(m.amount), badge(m.invoiceStatus), m.invoiceNo||'-', formatDateTR(m.invoiceDate)||'-', rental.location||'-', actions] }),['Ay','Dönem','Gün','Sahibi','Adet','Birim Aylık Kira','Fatura Tutarı','Durum','Fatura No','Fatura Tarihi','Konum','İşlem']); return `

${esc(nameOf(db.items,itemId))} ${firstItem && firstItem.type ? ' / '+esc(firstItem.type) : ''}${firstItem && firstItem.code ? ' / '+esc(firstItem.code) : ''}

${rows.length} dönem • ${itemQty} toplam adet
Toplam ${money(itemTotal)}Kesilen ${money(itemInvoiced)}Bekleyen ${money(itemWaiting)}
${tableHtml}
`; }).join(''); return `
${esc(initial)}

${esc(nameOf(db.customers,cid))}

${monthCount} ay • ${itemIds.length} malzeme grubu • ${customerRows.length} kayıt
${cWaiting>0?'Bekleyen Var':'Tamamlandı'}
Toplam
${money(cTotal)}
Kesilen
${money(cInvoiced)}
Bekleyen
${money(cWaiting)}
Tamamlanma
${cPct}%
Dönem
${customerRows.length}
${itemHtml}
`; }).join(''); document.getElementById('view').innerHTML=filterHtml+html; if(quick&&quick.scroll){setTimeout(()=>{let el=document.querySelector('.invoice-card'); if(el)el.scrollIntoView({behavior:'smooth',block:'start'});},80)} window.__monthQuickFilter=null; } function invoiceForm(id){let m=byId(db.rentMonths,id); openModal('Fatura Bilgisi Gir',`
Fatura dönemi: ${periodText(m)}   |   Gün: ${m.days||30}   |   Hesap: Birim aylık kira / 30 × gün × adet
`)} function saveInvoice(id){let m=byId(db.rentMonths,id); m.invoiceStatus='Kesildi'; m.invoiceNo=formVal('invNo'); m.invoiceDate=formVal('invDate')||today(); save(); closeModal(); render()} function clearInvoice(id){let m=byId(db.rentMonths,id); if(confirm('Bu ayın fatura bilgisi temizlensin mi?')){m.invoiceStatus='Kesilmedi'; m.invoiceNo=''; m.invoiceDate=''; save(); render()}} function ownerInvoiceHasInvoiceInfo(inv){return !!(inv && inv.manualSaved===true && ((inv.invoiceNo||'').trim()) && inv.invoiceDate)} function ownerInvoiceDisplayStatus(inv){return ownerInvoiceHasInvoiceInfo(inv)?'Fatura Geldi':'Fatura Girilmedi'} function ownerInvoiceStatusBadge(inv){return badge(ownerInvoiceDisplayStatus(inv))} function ownerInvoiceShownNo(inv){return ownerInvoiceHasInvoiceInfo(inv)?(inv.invoiceNo||''):''} function ownerInvoiceShownDate(inv){return ownerInvoiceHasInvoiceInfo(inv)?(inv.invoiceDate||''):''} function ownerInvoiceShownAmount(inv,autoAmount){return ownerInvoiceHasInvoiceInfo(inv) && inv.amountOverride!==null && inv.amountOverride!==undefined && inv.amountOverride!=='' ? Number(inv.amountOverride||0) : Number(autoAmount||0)} function ownerInvoiceKey(ownerId,month){return String(ownerId||'unknown')+'|'+String(month||'')} function ownerInvoiceMeta(ownerId,month){ if(!db.ownerInvoices)db.ownerInvoices=[]; let key=ownerInvoiceKey(ownerId,month); let inv=db.ownerInvoices.find(x=>x.autoKey===key || (x.ownerId===ownerId && x.month===month)); if(inv && !inv.autoKey) inv.autoKey=key; if(!inv){inv={id:uid('OIF'),autoKey:key,ownerId,month,invoiceNo:'',invoiceDate:'',dueDate:'',amountOverride:null,status:'Fatura Girilmedi',note:''}; db.ownerInvoices.push(inv);} return inv; } function ownerInvoiceHasManualInfo(inv){return !!(inv && ((inv.invoiceNo||'').trim() || inv.invoiceDate || inv.note || (inv.amountOverride!==null && inv.amountOverride!==undefined && inv.amountOverride!=='')))} function rentalIsActiveForOwnerInvoices(rental){return !!(rental && rental.id && rental.status!=='Kapandı')} function addOwnerInvoiceMapRow(map,ownerId,month,item,rental,detail){ let key=ownerInvoiceKey(ownerId,month); if(!map[key]) map[key]={ownerId,month,amount:0,qty:0,days:0,details:[]}; map[key].amount+=Number(detail.amount||0); map[key].qty+=Number(detail.qty||0); map[key].days+=Number(detail.days||0); map[key].details.push({monthId:detail.monthId||'',itemId:item.id,itemName:item.name||'-',itemType:item.type||'-',customer:nameOf(db.customers,rental.customerId),period:detail.period,days:detail.days,qty:detail.qty,unit:Number(detail.unit||0),amount:Number(detail.amount||0),cityDistrict:[rental.city||'',rental.district||''].filter(Boolean).join(' / ')||'-',location:rental.location||item.location||'-'}); } function ownerMonthlyInvoiceGroups(){ let map={}, investorHandled=new Set(); (db.rentalItems||[]).forEach(ri=>{ if(!rentalItemHasInvestorRentInfo(ri))return; let item=byId(db.items,ri.itemId), ownerId=item.ownerId||'unknown', rental=byId(db.rentals,ri.rentalId); if(!ownerId || ownerId==='unknown' || !rentalIsActiveForOwnerInvoices(rental) || !showInvestorRentFieldsForItem(item))return; let start=ri.investorRentStart||rental.start||today(), end=ri.investorRentEnd||rental.end||'', months=end?rentalMonthsFromEnd(start,end):Number(ri.investorRentMonths||rental.months||1); let monthly=Number(ri.investorMonthlyAmount||0), qty=rentQtyOf(ri); generateInvoicePeriods(start,months,end).forEach(pr=>{ let amount=Math.round(((monthly/30)*pr.days*qty)*100)/100; addOwnerInvoiceMapRow(map,ownerId,pr.month,item,rental,{monthId:'INV-'+ri.id+'-'+pr.start,period:pr.start+' / '+pr.end,days:pr.days,qty,unit:monthly,amount}); }); investorHandled.add(String(ri.rentalId)+'|'+String(ri.itemId)); }); (db.rentMonths||[]).forEach(m=>{ if(investorHandled.has(String(m.rentalId)+'|'+String(m.itemId)))return; let item=byId(db.items,m.itemId), ownerId=item.ownerId||'unknown', rental=byId(db.rentals,m.rentalId); if(!ownerId || ownerId==='unknown' || !rentalIsActiveForOwnerInvoices(rental)) return; let month=m.month||ym(m.periodStart||today()); // Yatırımcı faturaları artık sadece içinde bulunulan ayla sınırlanmaz. // Kiraya Ver ekranında oluşan tüm kira dönemleri kadar yatırımcı fatura kaydı gösterilir. let amount=Number(m.amount||0), qty=Number(m.qty||0), days=Number(m.days||30); addOwnerInvoiceMapRow(map,ownerId,month,item,rental,{monthId:m.id,period:periodText(m),days,qty,unit:Number(m.unitMonthly||m.unitAmount||0),amount}); }); return Object.values(map).map(g=>{let inv=ownerInvoiceMeta(g.ownerId,g.month); g.meta=inv; g.displayAmount=ownerInvoiceShownAmount(inv,g.amount); return g;}) .sort((a,b)=>nameOf(db.owners,a.ownerId).localeCompare(nameOf(db.owners,b.ownerId),'tr')||(a.month||'').localeCompare(b.month||'')); } function ownerSupplierWaitingTotal(){ return ownerMonthlyInvoiceGroups().filter(g=>ownerInvoiceDisplayStatus(g.meta)==='Fatura Girilmedi').reduce((s,g)=>s+Number(g.displayAmount||0),0); } function syncOwnerInvoices(){ownerMonthlyInvoiceGroups(); save(); alert('Yatırımcı faturaları güncellendi. Sadece Aktif Kiralamalar içindeki kayıtların ayları yatırımcı faturalarına dahil edilir.'); render();} function ownerInvoiceForm(ownerId,month){ let group=ownerMonthlyInvoiceGroups().find(g=>g.ownerId===ownerId && g.month===month); if(!group){alert('Bu ay için otomatik fatura kaydı bulunamadı.');return} let inv=group.meta; let autoAmount=Number(group.amount||0); let saved=ownerInvoiceHasInvoiceInfo(inv); let shownAmount=saved && inv.amountOverride!==null && inv.amountOverride!==undefined && inv.amountOverride!=='' ? Number(inv.amountOverride||0) : autoAmount; let shownNo=saved ? (inv.invoiceNo||'') : ''; let shownDate=saved ? (inv.invoiceDate||'') : ''; let shownStatus=saved ? 'Fatura Geldi' : 'Fatura Girilmedi'; openModal('Yatırımcı Fatura Gir',`
Yatırımcı: ${nameOf(db.owners,ownerId)}   |   Ay: ${monthTitle(month)}   |   Otomatik hesaplanan tutar: ${money(autoAmount)}
Formül: Birim Aylık Kira / 30 × Gün × Adet
Bu pencere artık fatura girilmeden eski/otomatik bilgileri doldurmaz. Fatura No ve Fatura Tarihi siz kaydetmeden durum Fatura Girilmedi kalır.
Hesaplanan tutar otomatik gelir; gerekirse elle değiştirilebilir.
Fatura No ve Fatura Tarihi girilmeden kayıt Fatura Geldi olmaz.
`) } function saveOwnerInvoiceAuto(ownerId,month){ let group=ownerMonthlyInvoiceGroups().find(g=>g.ownerId===ownerId && g.month===month); if(!group){alert('Kayıt bulunamadı.');return} let inv=group.meta; let autoAmount=Number(group.amount||0); let enteredAmount=formVal('oiAmount'); let invoiceNo=formVal('oiNo').trim(); let invoiceDate=formVal('oiDate'); if(!invoiceNo || !invoiceDate){alert('Yatırımcı faturasını kaydetmek için Fatura No ve Fatura Tarihi zorunludur. Bu bilgiler girilmeden kayıt Fatura Geldi olarak işlenmez.');return} Object.assign(inv,{autoKey:ownerInvoiceKey(ownerId,month),ownerId,month,invoiceNo,invoiceDate,dueDate:'',amountOverride:enteredAmount===''?autoAmount:Number(enteredAmount||0),note:'',manualSaved:true}); inv.status=ownerInvoiceDisplayStatus(inv); save(); closeModal(); render(); } function clearOwnerInvoiceStatus(ownerId,month){let inv=ownerInvoiceMeta(ownerId,month); Object.assign(inv,{invoiceNo:'',invoiceDate:'',dueDate:'',amountOverride:null,status:'Fatura Girilmedi',note:'',manualSaved:false}); save(); render();} function deleteOwnerInvoice(id){if(requireDeletePassword('Bu yatırımcı fatura bilgisi temizlensin mi?')){let inv=byId(db.ownerInvoices,id); if(inv.id){Object.assign(inv,{invoiceNo:'',invoiceDate:'',dueDate:'',amountOverride:null,status:'Fatura Girilmedi',note:'',manualSaved:false}); save(); render();}}} function ownerInvoicesView(){ document.getElementById('topActions').innerHTML=''; let list=ownerMonthlyInvoiceGroups(); if(!list.length){document.getElementById('view').innerHTML='
Henüz kiralama/fatura dönemi olmadığı için yatırımcı faturası oluşmadı. Kiraya verme yaptığınızda bu bölümde ay ay otomatik görünecek.
'; return} let quick=window.__ownerInvoiceQuickFilter||null; let quickOwner=quick?String(quick.ownerId||''):''; let quickMonth=quick?String(quick.month||''):''; let prevSearch=quick?'':(document.getElementById('ownerInvSearch')?document.getElementById('ownerInvSearch').value:''); let prevOwner=quickOwner||(document.getElementById('ownerInvOwnerFilter')?document.getElementById('ownerInvOwnerFilter').value:''); let prevCustomer=quick?'':(document.getElementById('ownerInvCustomerFilter')?document.getElementById('ownerInvCustomerFilter').value:''); let prevStatus=quick?String(quick.status||''):(document.getElementById('ownerInvStatusFilter')?document.getElementById('ownerInvStatusFilter').value:''); let prevMonth=quickMonth||(document.getElementById('ownerInvMonthFilter')?document.getElementById('ownerInvMonthFilter').value:''); let prevLocation=quick?'':(document.getElementById('ownerInvLocationFilter')?document.getElementById('ownerInvLocationFilter').value:''); if(quick) window.__ownerInvoiceQuickFilter=null; let owners=[...new Set(list.map(g=>g.ownerId||'unknown'))].sort((a,b)=>nameOf(db.owners,a).localeCompare(nameOf(db.owners,b),'tr')); let customers=[...new Set(list.flatMap(g=>(g.details||[]).map(d=>d.customer||'-')))].filter(Boolean).sort((a,b)=>String(a).localeCompare(String(b),'tr')); let months=[...new Set(list.map(g=>g.month||''))].filter(Boolean).sort(); let locations=[...new Set(list.flatMap(g=>(g.details||[]).map(d=>d.location||'-')))].filter(Boolean).sort((a,b)=>String(a).localeCompare(String(b),'tr')); let filtered=list.filter(g=>{ let status=ownerInvoiceDisplayStatus(g.meta); let hay=[nameOf(db.owners,g.ownerId),monthTitle(g.month),status,g.meta.invoiceNo||'',g.meta.invoiceDate||'',g.meta.note||'',...(g.details||[]).flatMap(d=>[d.customer,d.itemName,d.itemType,d.period,d.days,d.qty,d.unit,d.amount,d.cityDistrict,d.location])].join(' ').toLocaleLowerCase('tr-TR'); if(prevSearch && !hay.includes(prevSearch.toLocaleLowerCase('tr-TR'))) return false; if(prevOwner && String(g.ownerId||'unknown')!==String(prevOwner)) return false; if(prevStatus && status!==prevStatus) return false; if(prevMonth && String(g.month||'')!==String(prevMonth)) return false; if(prevCustomer && !(g.details||[]).some(d=>String(d.customer||'-')===String(prevCustomer))) return false; if(prevLocation && !(g.details||[]).some(d=>String(d.location||'-')===String(prevLocation))) return false; return true; }); let total=filtered.reduce((s,g)=>s+Number(g.displayAmount||0),0), waiting=filtered.filter(g=>ownerInvoiceDisplayStatus(g.meta)==='Fatura Girilmedi').reduce((s,g)=>s+Number(g.displayAmount||0),0), paid=filtered.filter(g=>ownerInvoiceDisplayStatus(g.meta)==='Fatura Geldi').reduce((s,g)=>s+Number(g.displayAmount||0),0); let totalAll=list.reduce((s,g)=>s+Number(g.displayAmount||0),0), waitingAll=list.filter(g=>ownerInvoiceDisplayStatus(g.meta)==='Fatura Girilmedi').reduce((s,g)=>s+Number(g.displayAmount||0),0), paidAll=list.filter(g=>ownerInvoiceDisplayStatus(g.meta)==='Fatura Geldi').reduce((s,g)=>s+Number(g.displayAmount||0),0); let ownerOptions=''+owners.map(oid=>``).join(''); let customerOptions=''+customers.map(c=>``).join(''); let monthOptions=''+months.map(mm=>``).join(''); let locationOptions=''+locations.map(loc=>``).join(''); let filterHtml=`
Filtrelenen Toplam
${money(total)}
Genel toplam: ${money(totalAll)}
Fatura Girilmedi
${money(waiting)}
Genel: ${money(waitingAll)}
Fatura Geldi
${money(paid)}
Genel: ${money(paidAll)}
Ay/Yatırımcı Kaydı
${filtered.length} / ${list.length}

Filtreler

Yatırımcı Faturaları: Birim aylık kira ve fatura tutarı yatırımcı kira bilgilerine göre otomatik hesaplanır: Birim Aylık Kira / 30 × Gün × Adet. Fatura No ve Fatura Tarihi siz Fatura Gir ekranından kaydetmeden dolmaz; o zamana kadar durum Fatura Girilmedi kalır.
`; setTimeout(()=>['ownerInvSearch','ownerInvOwnerFilter','ownerInvCustomerFilter','ownerInvStatusFilter','ownerInvMonthFilter','ownerInvLocationFilter'].forEach(id=>{let el=document.getElementById(id); if(el){el.oninput=()=>ownerInvoicesView(); el.onchange=()=>ownerInvoicesView();}}),0); if(!filtered.length){document.getElementById('view').innerHTML=filterHtml+'
Seçilen filtrelere uygun yatırımcı faturası kaydı bulunamadı.
'; return} let ownerIds=[...new Set(filtered.map(x=>x.ownerId||'unknown'))].sort((a,b)=>nameOf(db.owners,a).localeCompare(nameOf(db.owners,b),'tr')); let html=ownerIds.map(oid=>{ let rows=filtered.filter(x=>(x.ownerId||'unknown')===oid); let t=rows.reduce((s,x)=>s+Number(x.displayAmount||0),0), w=rows.filter(x=>ownerInvoiceDisplayStatus(x.meta)==='Fatura Girilmedi').reduce((s,x)=>s+Number(x.displayAmount||0),0); let flatRows=[]; rows.forEach(g=>{ (g.details||[]).forEach(d=>{ if(prevCustomer && String(d.customer||'-')!==String(prevCustomer)) return; if(prevLocation && String(d.location||'-')!==String(prevLocation)) return; let status=ownerInvoiceDisplayStatus(g.meta); flatRows.push([monthTitle(g.month),d.period,d.days,d.customer,d.qty,money(d.unit||0),money(d.amount||0),ownerInvoiceStatusBadge(g.meta),ownerInvoiceShownNo(g.meta)||'',ownerInvoiceShownDate(g.meta)||'',d.location||'-',``]); }); }); let pct=t>0?Math.round(((t-w)/t)*100):100; let statusText=w>0?'Bekleyen Var':'Tamamlandı'; let shouldOpen=(w>0)||(quickOwner && String(oid)===String(quickOwner)); return `
${esc((nameOf(db.owners,oid)||'?').slice(0,1))}

${esc(nameOf(db.owners,oid))}

${rows.length} ay • ${flatRows.length} kalem
${statusText}
Toplam
${money(t)}
Fatura Girilmedi
${money(w)}
Fatura Geldi
${money(t-w)}
Tamamlanma
${pct}%
Kalem
${flatRows.length}
Toplam: ${money(t)}   |   Fatura Girilmedi: ${money(w)}   |   Ay Sayısı: ${rows.length}
${table(flatRows,['Ay','Dönem','Gün','Müşteri','Adet','Birim Aylık Kira','Fatura Tutarı','Durum','Fatura No','Fatura Tarihi','Konum','İşlem'])}
`; }).join(''); document.getElementById('view').innerHTML=filterHtml+html; if(quickOwner || quickMonth){setTimeout(()=>{let el=document.querySelector('.invoice-card[open]'); if(el) el.scrollIntoView({behavior:'smooth',block:'start'});},100);} } function returnsView(){let returnRows=(db.ownerReturns||[]).slice().sort((a,b)=>(b.date||'').localeCompare(a.date||'')).map(r=>[r.date||'-',r.ownerName||nameOf(db.owners,r.ownerId),r.itemCode||'-',r.itemName||'-',r.qty||0,r.returnedLocation||'İade Deposu',r.returnedCity||'-',r.returnedDistrict||'-',r.note||'-']);let depotRows=(db.moves||[]).filter(m=>m.type==='Kiradan Dönüş').slice().sort((a,b)=>(b.date||'').localeCompare(a.date||'')).map(m=>[m.date||'-',nameOf(db.customers,m.customerId),m.itemCode||'-',m.itemName||'-',m.qty||0,m.location||'-',m.note||'-']);let returnItems=(db.items||[]).filter(i=>itemServiceType(i)!=='Kiralanamaz');document.getElementById('view').innerHTML=`

Kiradan Dönüş / Sahibine İade

Bu ekranda sadece kiralama/iade takibine uygun kayıtlar görünür. Kiraya Al yapılmamış kayıtlar Kiraya Ver ekranında seçilemez. Kullanım Tipi Kiralanamaz olan malzemeler bu ekranda listelenmez ve bakım takibinde de görünmez. Kiraya Veren VİNÇ SAHİBİ KULLANIMINDA ise, durum Vinç Sahibi Şantiyesinde veya İade Edildi ise Kiraya Al butonu görünür ve stok/malzeme düzenleme ekranına yönlendirir.
${table(returnItems.map(i=>[i.code,i.name,qtyOf(i),rentedQty(i.id),availableQty(i),returnedQty(i.id),nameOf(db.owners,i.ownerId),badge(stockStatus(i)),i.location||'-',returnAreaAction(i)]),['Kod','Malzeme','Toplam','Kirada','Depoda','Sahibine İade','Sahibi','Durum','Konum','İşlem'])}

Kiradan Depoya Dönüş Geçmişi

${table(depotRows,['Dönüş Tarihi','Müşteri','Kod','Malzeme','Adet','Depo/Konum','Açıklama'])}

Yatırımcı Faturaları

${table(ownerMonthlyInvoiceGroups().map(g=>[nameOf(db.owners,g.ownerId),monthTitle(g.month),ownerInvoiceShownNo(g.meta)||'',ownerInvoiceShownDate(g.meta)||'',money(g.displayAmount),ownerInvoiceStatusBadge(g.meta),g.meta.note||'-']),['Malzeme Sahibi','Ay','Fatura No','Fatura Tarihi','Tutar','Durum','Açıklama'])}

Sahibine İade Geçmişi

${table(returnRows,['İade Tarihi','Malzeme Sahibi','Kod','Malzeme','İade Adedi','İade Deposu','İl','İlçe','Açıklama'])}
`} function activeRentalItemsForItem(id){let activeIds=new Set(activeRentals().map(r=>r.id));return (db.rentalItems||[]).filter(ri=>ri.itemId===id&&activeIds.has(ri.rentalId)&&rentQtyOf(ri)>0)} function returnDepot(id){let i=byId(db.items,id), rq=rentedQty(id); if(!i.id)return; if(rq<=0){setItemStatus(id,'Depoda',i.location||''); save(); render(); alert('Bu malzeme zaten depoda uygun görünüyor.'); return;} let active=activeRentalItemsForItem(id); let rentalOptions=active.map(ri=>{let r=byId(db.rentals,ri.rentalId);return ``}).join(''); openModal('Kiradan Depoya Al',`
${i.name} için kiradaki toplam adet: ${rq}. Depoya alınacak kiralama kaydını ve adedi seçin.
Sadece Ayarlar > Depo Listesi bölümünde oluşturduğun depolar görünür.
`)} function fillReturnDepotMax(itemId){let rid=formVal('depRental'), ri=(db.rentalItems||[]).find(x=>x.itemId===itemId&&x.rentalId===rid); let inp=document.getElementById('depQty'); if(inp&&ri){inp.max=rentQtyOf(ri); inp.value=Math.min(Number(inp.value||1),rentQtyOf(ri))||1;}} function recalcMonthAmount(m){let unit=Number(m.unitMonthly||m.unitAmount||0), days=Number(m.days||30), qty=Number(m.qty||0); m.amount=Math.round(((unit/30)*days*qty)*100)/100;} function saveDepotReturn(id){let i=byId(db.items,id); if(!i.id)return; let rid=formVal('depRental'), qty=Number(formVal('depQty')||0), ri=(db.rentalItems||[]).find(x=>x.itemId===id&&x.rentalId===rid); if(!rid||!ri){alert('Kiralama / müşteri seçiniz.');return} let current=rentQtyOf(ri); if(qty<1){alert('Depoya alınacak adet en az 1 olmalı.');return} if(qty>current){alert('Bu kiralama kaydında sadece '+current+' adet kirada görünüyor.');return} let r=byId(db.rentals,rid), retDate=formVal('depDate')||today(), loc=formVal('depLoc'); if(!loc){alert('Depo / konum seçiniz. Depo yoksa önce Ayarlar > Depo Listesi bölümünden ekleyin.');return} ri.qty=current-qty; ri.amount=Number(ri.unitMonthly||ri.unitAmount||0)*ri.qty; if(ri.qty<=0){db.rentalItems=db.rentalItems.filter(x=>x.id!==ri.id)} (db.rentMonths||[]).filter(m=>m.rentalId===rid&&m.itemId===id&&m.invoiceStatus!=='Kesildi'&&(!m.periodEnd||m.periodEnd>=retDate)).forEach(m=>{m.qty=Math.max(0,Number(m.qty||0)-qty); recalcMonthAmount(m);}); db.rentMonths=(db.rentMonths||[]).filter(m=>!(m.rentalId===rid&&m.itemId===id&&m.invoiceStatus!=='Kesildi'&&Number(m.qty||0)<=0)); let hasItems=(db.rentalItems||[]).some(x=>x.rentalId===rid&&rentQtyOf(x)>0); if(!hasItems&&r.id){r.status='Kapandı'; r.end=retDate;} i.status='Depoda'; i.location=loc; i.lastDepotEntryLocation=loc; i.lastDepotEntryCity=r.city||i.city||''; i.lastDepotEntryDistrict=r.district||i.district||''; i.lastDepotEntryDate=retDate; i.lastDepotEntryNote=formVal('depNote')||'Kiradan depoya alındı'; i.lastDepotEntryCustomerId=isDepotStatusSiteInfoLocation(loc)?r.customerId:''; i.lastDepotEntrySiteNote=isDepotStatusSiteInfoLocation(loc)?(formVal('depNote')||''):''; if(!db.moves)db.moves=[]; db.moves.push({id:uid('MOV'),type:'Kiradan Dönüş',itemId:id,itemCode:i.code,itemName:i.name,ownerId:i.ownerId,customerId:r.customerId,rentalId:rid,qty,date:retDate,location:loc,city:i.lastDepotEntryCity,district:i.lastDepotEntryDistrict,note:formVal('depNote')}); save(); closeModal(); render();} function ownerTakeBack(id){let i=byId(db.items,id); if(!i.id)return; let ret=returnedQty(id); if(ret<=0){alert('Bu malzemede sahibinde iade olarak bekleyen adet yok.');return} openModal('Sahibinden Tekrar Depoya Al',`
${nameOf(db.owners,i.ownerId)} sahibinden tekrar alınabilecek ${i.name} adedi: ${ret}
Sadece Ayarlar > Depo Listesi bölümünde oluşturduğun depolar görünür.
`)} function saveOwnerTakeBack(id){let i=byId(db.items,id); if(!i.id)return; let ret=returnedQty(id), qty=Number(formVal('takeQty')||0), loc=formVal('takeLoc'); if(qty<1){alert('Adet en az 1 olmalı.');return} if(qty>ret){alert('Sahibinde iade olarak bekleyen adet '+ret+' olduğu için daha fazla depoya alınamaz.');return} if(!loc){alert('Depo / konum seçiniz. Depo yoksa önce Ayarlar > Depo Listesi bölümünden ekleyin.');return} let hasReducedReturn=(db.ownerReturns||[]).some(r=>r.itemId===id&&r.stockReduced); db.ownerTakeBacks.push({id:uid('TAKE'),itemId:i.id,ownerId:i.ownerId,ownerName:nameOf(db.owners,i.ownerId),itemCode:i.code,itemName:i.name,qty,date:formVal('takeDate')||today(),location:loc,note:formVal('takeNote'),stockIncreased:hasReducedReturn}); db.moves.push({id:uid('MOV'),type:'Sahibinden Tekrar Al',itemId:i.id,customerId:'',itemCode:i.code,itemName:i.name,qty,date:formVal('takeDate')||today(),location:loc,note:formVal('takeNote')}); if(hasReducedReturn)i.qty=qtyOf(i)+qty; i.location=loc; i.ownerReturned=false; if(i.status==='İade Edildi')i.status='Depoda'; save(); closeModal(); render()} function returnRemainDepotOptions(sel=''){ let all=depotList().filter(d=>!isHiddenInDepotStatus(d)); if(sel && !isHiddenInDepotStatus(sel) && !all.some(x=>x.toLocaleLowerCase('tr-TR')===String(sel).toLocaleLowerCase('tr-TR'))) all.unshift(sel); return all.map(d=>``).join('') } function ownerReturnRemainDefault(i){ let candidates=[i&&i.lastDepotEntryLocation,i&&i.location,itemEntryLocation(i),i&&i.entryLocation]; let loc=(candidates.find(x=>x && !isHiddenInDepotStatus(x) && String(x).toLocaleLowerCase('tr-TR')!=='sahibine iade')||''); return cleanDepot(loc); } function returnOwner(id){ let i=byId(db.items,id), av=availableQty(i); if(!i.id)return; if(av<=0){alert('Bu malzemede depoda iade edilebilir adet yok. Önce kiradan dönüş/kapatma yapılmalı.');return} let remainDefault=ownerReturnRemainDefault(i); let remainCity=i.lastDepotEntryCity||i.city||itemEntryCity(i)||''; let remainDistrict=i.lastDepotEntryDistrict||i.district||itemEntryDistrict(i)||''; openModal('Sahibine İade Et',`
${nameOf(db.owners,i.ownerId)} sahibine ait ${i.name} için depoda iade edilebilir adet: ${av}
Kısmi iade kuralı: Örneğin 5 adet malzemeden 2 adet sahibine iade edilirse sadece 2 adet iade kaydı oluşur; kalan 3 adet İade Deposu’na düşmez, bizdeki gerçek depo/konumunda kalır.
Bu alan sahibine iade edilen adet içindir.
Sahibine İade Geçmişinde görünür.
Sahibine İade Geçmişinde görünür.

Kalan Malzemenin Bizdeki Konumu (kısmi iade varsa İade Deposu seçilemez)

Bu alan sadece bizde kalan adet için geçerlidir. Sahibine iade edilen adet ayrı olarak iade geçmişine yazılır.
Bu alan kalan malzemenin il bilgisini günceller.
Bu alan kalan malzemenin ilçe bilgisini günceller.
`) } function saveOwnerReturn(id){ let i=byId(db.items,id); if(!i.id)return; let av=availableQty(i), qty=Number(formVal('retQty')||0); if(qty<1){alert('İade adedi en az 1 olmalı.');return} if(qty>av){alert('Depoda iade edilebilir adet '+av+' olduğu için daha fazla iade kaydedilemez.');return} let remainAvailable=av-qty; let oldLoc=ownerReturnRemainDefault(i); let oldCity=i.lastDepotEntryCity||i.city||itemEntryCity(i)||''; let oldDistrict=i.lastDepotEntryDistrict||i.district||itemEntryDistrict(i)||''; let remainLoc=formVal('retRemainLoc')||oldLoc; let remainCity=formVal('retRemainCity')||oldCity; let remainDistrict=formVal('retRemainDistrict')||oldDistrict; let remainCustomerId=formVal('retRemainCustomer')||''; if(remainAvailable>0 && isHiddenInDepotStatus(remainLoc)){ alert('Kalan malzemenin bizdeki konumu İade Deposu olamaz. İade Deposu sadece sahibine iade edilen adet için kullanılır.');return } if(remainAvailable>0&&!remainLoc){alert('Kalan '+remainAvailable+' adet malzeme için Depo / Konum seçiniz. Depo yoksa önce Ayarlar > Depo Listesi bölümünden ekleyin.');return} if(remainAvailable>0&&isDepotStatusSiteInfoLocation(remainLoc)&&!remainCustomerId){alert('Kalan malzeme sahada ise müşteri seçiniz.');return} if(!isDepotStatusSiteInfoLocation(remainLoc))remainCustomerId=''; let oldSnap=itemHistorySnapshot(i), ownerName=nameOf(db.owners,i.ownerId), retDate=formVal('retDate')||today(), returnLoc=formVal('retReturnLoc')||'İade Deposu', returnCity=formVal('retReturnCity'), returnDistrict=formVal('retReturnDistrict'); db.ownerReturns.push({id:uid('RET'),itemId:i.id,ownerId:i.ownerId,ownerName,itemCode:i.code,itemName:i.name,qty,date:retDate,note:formVal('retNote'),stockReduced:true,returnedLocation:returnLoc,returnedCity:returnCity,returnedDistrict:returnDistrict,remainingQty:remainAvailable,remainingLocation:remainAvailable>0?remainLoc:'',remainingCity:remainAvailable>0?remainCity:'',remainingDistrict:remainAvailable>0?remainDistrict:'',remainingCustomerId:remainAvailable>0?remainCustomerId:''}); i.qty=Math.max(0,qtyOf(i)-qty); let total=qtyOf(i), rented=rentedQty(i.id); i.ownerReturned=total<=0&&rented<=0; if(i.ownerReturned){ i.status='İade Edildi'; i.location='Sahibine İade'; i.city=''; i.district=''; i.entryLocation=''; i.entryCity=''; i.entryDistrict=''; i.lastDepotEntryLocation=''; i.lastDepotEntryCity=''; i.lastDepotEntryDistrict=''; i.lastDepotEntryCustomerId=''; i.lastDepotEntrySiteNote=''; i.lastDepotEntryNote='Sahibine tamamen iade edildi'; } else if(remainAvailable>0){ if(i.status==='İade Edildi')i.status='Depoda'; i.status=rented>0?'Kirada':'Depoda'; i.location=remainLoc; i.city=remainCity; i.district=remainDistrict; i.entryLocation=remainLoc; i.entryCity=remainCity; i.entryDistrict=remainDistrict; i.lastDepotEntryLocation=remainLoc; i.lastDepotEntryCity=remainCity; i.lastDepotEntryDistrict=remainDistrict; i.lastDepotEntryCustomerId=remainCustomerId; i.lastDepotEntrySiteNote=isDepotStatusSiteInfoLocation(remainLoc)?(i.lastDepotEntrySiteNote||''):''; i.lastDepotEntryDate=retDate; i.lastDepotEntryNote='Sahibine kısmi iade sonrası kalan adetlerin bizdeki konumu'; } recordItemHistory(i,oldSnap,'Sahibine İade','Sahibine iade işleminde sadece iade edilen adet stoktan düşüldü; kısmi iadede kalan adetler İade Deposu’na alınmadan mevcut/belirlenen bizdeki konumda bırakıldı.'); save(); closeModal(); render() } function depotAvailableLocation(i){ // Depodaki/kiraya verilmemiş kalan adetler için Stok / Malzeme Giriş ekranındaki // Son Kiradaki Konumu esas alınır. Böylece Teknik Servis Hizmet Deposu, // Şantiye Sahasında Beklemekte ve Emanet Deposu Sahada seçimleri Depo Durumu'nda doğru sayılır. return cleanDepot((i&&i.lastDepotEntryLocation)||'') || cleanDepot((i&&i.location)||'') || cleanDepot(itemEntryLocation(i)||''); } function depotPhysicalAvailableQty(i,loc){ let base=Math.max(0,qtyOf(i)-rentedQty(i.id)-legacyReturnedQty(i.id)); if(base<=0)return 0; if(i.status==='Vinç Sahibi Kendi Kullanımında'||i.status==='Vinç Sahibi Şantiyesinde')return 0; if(i.status==='Bakımda'||isTeknikServisHizmetDeposu(loc))return base; return availableQty(i); } function depotRentedLocationForRental(i,r){ // Kiraya verilen adetler, Kiraya Ver / Aktif Kiralama ekranındaki Çıkış Deposu bilgisinden; // yoksa stok kaydındaki Son Kiradaki Konumu bilgisinden sayılır. return cleanDepot((r&&r.depotLocation)||'') || cleanDepot((i&&i.lastDepotEntryLocation)||'') || cleanDepot((i&&i.location)||'') || cleanDepot(itemEntryLocation(i)||''); } function depotInventoryRowsForItem(i){ let rows=[]; let loc=depotAvailableLocation(i); let depoda=depotPhysicalAvailableQty(i,loc); if(depoda>0){ if(loc){ let siteInfo=isDepotStatusSiteInfoLocation(loc); let siteLast=isSantiyeSahasindaBeklemekte(loc)?lastCustomerInfoForItem(i.id):{}; rows.push({ depot:loc, code:i.code||'-', name:i.name||'-', type:i.type||'-', owner:nameOf(db.owners,i.ownerId), customer:siteInfo?((isSantiyeSahasindaBeklemekte(loc)?siteLast.customer:'')||nameOf(db.customers,i.lastDepotEntryCustomerId||i.customerId||'')||'-'):'-', city:siteInfo?((isSantiyeSahasindaBeklemekte(loc)?siteLast.city:'')||i.lastDepotEntryCity||i.city||itemEntryCity(i)||'-'):'-', district:siteInfo?((isSantiyeSahasindaBeklemekte(loc)?siteLast.district:'')||i.lastDepotEntryDistrict||i.district||itemEntryDistrict(i)||'-'):'-', depoda, kirada:0, total:depoda, rent:money(i.monthlyRent||0), status:stockStatus(i) }); } } let rentedParts={}; activeRentalItemsForItem(i.id).forEach(ri=>{ let r=byId(db.rentals,ri.rentalId); let loc=depotRentedLocationForRental(i,r); if(!loc)return; let siteInfo=isDepotStatusSiteInfoLocation(loc); let customer=siteInfo?nameOf(db.customers,(r&&r.customerId)||i.lastDepotEntryCustomerId||i.customerId||''):'-'; let city=siteInfo?((r&&r.city)||i.lastDepotEntryCity||i.city||'-'):'-'; let district=siteInfo?((r&&r.district)||i.lastDepotEntryDistrict||i.district||'-'):'-'; let key=siteInfo?[loc,customer||'-',city||'-',district||'-'].join('|'):loc; if(!rentedParts[key])rentedParts[key]={loc,customer,city,district,qty:0}; rentedParts[key].qty+=rentQtyOf(ri); }); Object.values(rentedParts).forEach(part=>{ let loc=part.loc; let kirada=Number(part.qty||0); if(kirada<=0)return; rows.push({ depot:loc, code:i.code||'-', name:i.name||'-', type:i.type||'-', owner:nameOf(db.owners,i.ownerId), customer:part.customer||'-', city:part.city||'-', district:part.district||'-', depoda:0, kirada, total:kirada, rent:money(i.monthlyRent||0), status:'Kirada' }); }); return rows; } function depotInventoryData(){ let depotSet=new Set(depotList().filter(d=>!isHiddenInDepotStatus(d))); let allRows=[]; (db.items||[]).filter(i=>!isReturnedToOwnerItem(i)).forEach(i=>{ depotInventoryRowsForItem(i).forEach(row=>{ if(isHiddenInDepotStatus(row.depot))return; allRows.push(row); if(row.depot)depotSet.add(row.depot); }); }); let depots=[...depotSet].filter(d=>!isHiddenInDepotStatus(d)).sort((a,b)=>a.localeCompare(b,'tr')); return depots.map(depot=>{ let rows=allRows.filter(r=>r.depot===depot); rows.sort((a,b)=>a.name.localeCompare(b.name,'tr')||a.owner.localeCompare(b.owner,'tr')); return {depot,total:rows.reduce((s,r)=>s+r.depoda,0),kiradaTotal:rows.reduce((s,r)=>s+r.kirada,0),rows}; }).sort((a,b)=>(b.total+b.kiradaTotal)-(a.total+a.kiradaTotal) || a.depot.localeCompare(b.depot,'tr')); } function depotGroupedData(rows,includeSiteInfo=false){ let map={}; rows.forEach(r=>{ let key=includeSiteInfo ? [r.name,r.type,r.owner,r.customer||'-',r.city||'-',r.district||'-'].join('|') : [r.name,r.type,r.owner].join('|'); if(!map[key]) map[key]={name:r.name,type:r.type,owner:r.owner,customer:r.customer||'-',city:r.city||'-',district:r.district||'-',depoda:0,kirada:0,total:0,codes:[]}; map[key].depoda+=Number(r.depoda||0); map[key].kirada+=Number(r.kirada||0); map[key].total+=Number(r.total||0); if(r.code&&r.code!=='-') map[key].codes.push(r.code); }); return Object.values(map).sort((a,b)=>a.name.localeCompare(b.name,'tr')||a.owner.localeCompare(b.owner,'tr')||String(a.customer||'').localeCompare(String(b.customer||''),'tr')); } function transferItemOptions(sel=''){ let items=(db.items||[]).filter(i=>isRentableItem(i)&&!isReturnedToOwnerItem(i)&&availableQty(i)>0).sort((a,b)=>nameOf(db.owners,a.ownerId).localeCompare(nameOf(db.owners,b.ownerId),'tr')||String(a.name||'').localeCompare(String(b.name||''),'tr')); return ''+items.map(i=>{ let txt=(i.code||'-')+' • '+nameOf(db.owners,i.ownerId)+' • '+(i.name||'-')+' / '+(i.type||'-')+' • Depoda: '+availableQty(i)+' adet • '+(rentableStockLocation(i)||i.location||'-'); return ``; }).join(''); } function transferNewItemCode(base=''){ let n=1, code; do{code=(base||'MLZ')+'-AKT-'+String(n).padStart(3,'0'); n++;}while((db.items||[]).some(i=>String(i.code||'')===code)); return code; } function fillTransferDefaults(){ let i=byId(db.items,formVal('transferItem')); let info=document.getElementById('transferInfo'); if(!i.id){if(info)info.innerHTML='
Aktarılacak malzeme seçiniz.
'; return;} let av=availableQty(i); let q=document.getElementById('transferQty'); if(q){q.max=av; q.value=Math.min(Number(q.value||av||1),av)||1;} if(info)info.innerHTML=`
Mevcut: ${esc(i.code||'-')} • ${esc(nameOf(db.owners,i.ownerId))} • ${esc(i.name||'-')} / ${esc(i.type||'-')}
Mevcut Depo: ${esc(rentableStockLocation(i)||i.location||'-')} / ${esc(rentableStockCity(i)||i.city||'-')} / ${esc(rentableStockDistrict(i)||i.district||'-')}   |   Aktarılabilir Adet: ${av}
`; } function transferView(){ let history=(db.moves||[]).filter(m=>m.type==='Depodan Depoya Aktarım').slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))||String(b.id||'').localeCompare(String(a.id||''))); let rows=history.map(m=>[m.date||'-',m.itemCode||'-',m.itemName||'-',nameOf(db.owners,m.ownerId),m.qty||0,m.fromLocation||'-',m.fromCity||'-',m.fromDistrict||'-',m.location||'-',isDepotStatusSiteInfoLocation(m.location)?nameOf(db.customers,m.customerId):'-',m.city||'-',m.district||'-',m.newItemCode||'-',m.note||'-']); document.getElementById('topActions').innerHTML=''; document.getElementById('view').innerHTML=`

Depodan Depoya Malzeme Aktar

Sadece depoda kiralanabilir adedi bulunan malzemeler aktarılır. Aktarım adedi mevcut kiralanabilir adedin tamamıysa aynı stok kaydı yeni depoya taşınır. Kısmi aktarımda program stok kaydını böler: kalan adet eski depoda kalır, aktarılan adet yeni depo kaydı olarak açılır.
Emanet Deposu Sahada veya Şantiye Sahasında Beklemekte seçilirse müşteri seçimi zorunludur.
Aktarılacak malzeme seçiniz.

Depodan Depoya Aktarım Geçmişi

${table(rows,['Tarih','Kod','Malzeme','Malzeme Sahibi','Adet','Eski Depo','Eski İl','Eski İlçe','Yeni Depo','Müşteri','Yeni İl','Yeni İlçe','Yeni Stok Kodu','Açıklama'])}
`; } function toggleTransferCustomerFields(){let loc=formVal('transferDepot');let show=isDepotStatusSiteInfoLocation(loc);let wrap=document.getElementById('transferCustomerWrap');if(wrap)wrap.style.display=show?'block':'none';if(!show){let c=document.getElementById('transferCustomer');if(c)c.value='';}} function saveDepotTransfer(){ let item=byId(db.items,formVal('transferItem')); if(!item.id){alert('Aktarılacak malzemeyi seçiniz.');return} let av=availableQty(item), qty=Number(formVal('transferQty')||0); if(qty<1){alert('Aktarım adedi en az 1 olmalı.');return} if(qty>av){alert('Bu kayıtta aktarılabilir adet '+av+' olduğu için daha fazla aktarılamaz.');return} let toLoc=formVal('transferDepot'), toCity=formVal('transferCity'), toDistrict=formVal('transferDistrict'), toCustomerId=formVal('transferCustomer')||''; if(!toLoc){alert('Yeni Depo / Konum seçiniz.');return} if(isDepotStatusSiteInfoLocation(toLoc)&&!toCustomerId){alert('Yeni Depo / Konum sahada ise müşteri seçiniz.');return} if(!isDepotStatusSiteInfoLocation(toLoc))toCustomerId=''; let fromLoc=rentableStockLocation(item)||item.location||''; let fromCity=rentableStockCity(item)||item.city||''; let fromDistrict=rentableStockDistrict(item)||item.district||''; if(String(fromLoc||'').toLocaleLowerCase('tr-TR')===String(toLoc||'').toLocaleLowerCase('tr-TR') && String(fromCity||'')===String(toCity||'') && String(fromDistrict||'')===String(toDistrict||'')){ alert('Yeni depo/il/ilçe mevcut konumla aynı. Farklı depo veya konum seçiniz.');return; } let transferDate=formVal('transferDate')||today(), note=formVal('transferNote')||''; if(!db.moves)db.moves=[]; let oldSnap=itemHistorySnapshot(item); let newItemCode=''; if(qtys+d.total,0); let active=data.filter(d=>d.rows.length>0).length; let panels=data.map(d=>{ let isSite=isDepotStatusSiteInfoLocation(d.depot); let grouped=depotGroupedData(d.rows,isSite); let detailRows=isSite ? grouped.map(r=>[r.name,r.type,r.owner,r.customer||'-',r.city||'-',r.district||'-',r.depoda,r.kirada,r.total]) : grouped.map(r=>[r.name,r.type,r.owner,r.depoda,r.kirada,r.total]); let heads=isSite ? ['Malzeme','Tip','Malzeme Sahibi','Müşteri','İl','İlçe','Depodaki Adet','Kiradaki Adet','Toplam Kayıt Adedi'] : ['Malzeme','Tip','Malzeme Sahibi','Depodaki Adet','Kiradaki Adet','Toplam Kayıt Adedi']; return `

${esc(d.depot)} Depoda ${d.total} adet Kirada ${d.kiradaTotal||0} adet

${isSite?'
Bu özel konumda müşteri adı, il ve ilçe bilgileri stok/aktarım/kiralama kaydından alınır.
':''}${table(detailRows,heads)}
`; }).join(''); document.getElementById('view').innerHTML=`
Bu bölüm adet bazlı depo sayımı yapar. Kiraya verilen adetler Kiraya Ver ekranındaki Çıkış Deposu / Konum yani Son Kiradaki Konumu altında sayılır. Kiraya verilmeyen kalan adetler stok kaydındaki Son Kiradaki Konumu bilgisinden sayılır. Örnek: 100 malzemenin 33 adedi kiradaysa 33 adet Son Kiradaki Konumu'nda, kalan 67 adet aktif depo konumunda görünür. Sahibine tamamen iade edilen malzemeler depo sayımında ve depo listelerinde gösterilmez; ayrıca İade Deposu Depo Durumu ekranında gösterilmez. Kısmi iadede kalan adet, girilen Kalan Malzemenin Bizdeki Konumu altında görünür. Teknik Servis Hizmet Deposu içindeki bakım malzemeleri de fiziksel depo sayımına dahil edilir.
Depodaki Toplam Adet
${grand}
Dolu Depo Sayısı
${active}
Tanımlı Depo Sayısı
${data.length}
Kiradaki Toplam Adet
${(db.items||[]).reduce((s,i)=>s+rentedQty(i.id),0)}
${panels || '

Depo İçeriği

Kayıt yok.
'} `; } function vehicleOptions(sel=''){return (db.vehicles||[]).map(v=>``).join('')} function vehicleName(id){let v=byId(db.vehicles,id); return v.id ? `${v.plate||'-'} - ${v.brand||''} ${v.model||''}`.trim() : '-'} function vehicleCost(arr){return (arr||[]).reduce((s,x)=>s+Number(x.amount||0),0)} function parseDateOnly(dateStr){ if(!dateStr)return null; let s=String(dateStr).trim(); let y,m,d; let iso=s.match(/^(\d{4})-(\d{2})-(\d{2})$/); let tr=s.match(/^(\d{1,2})[.\/](\d{1,2})[.\/](\d{4})$/); if(iso){y=Number(iso[1]);m=Number(iso[2]);d=Number(iso[3]);} else if(tr){d=Number(tr[1]);m=Number(tr[2]);y=Number(tr[3]);} else {return null;} if(!y||!m||!d||m<1||m>12||d<1||d>31)return null; return {y,m,d,utc:Date.UTC(y,m-1,d)}; } function formatDateTR(dateStr){let p=parseDateOnly(dateStr); if(!p)return dateStr||'-'; return String(p.d).padStart(2,'0')+'.'+String(p.m).padStart(2,'0')+'.'+p.y;} function daysLeft(dateStr){let p=parseDateOnly(dateStr); if(!p)return null; let t=parseDateOnly(today()); if(!t)return null; return Math.round((p.utc-t.utc)/(1000*60*60*24));} function dateBadge(dateStr){let dl=daysLeft(dateStr); if(dl===null)return '-'; let cls=dl<0?'b-danger':(dl<=30?'b-warn':'b-ok'); let txt=dl<0?`${Math.abs(dl)} gün geçmiş`:(dl===0?'Bugün bitiyor':`${dl} gün kaldı`); return `${txt}`} function latestVehicleInsurance(vehicleId,type){let list=(db.vehicleInsurances||[]).filter(x=>x.vehicleId===vehicleId&&x.type===type).slice().sort((a,b)=>(b.endDate||'').localeCompare(a.endDate||'')); return list[0]||{};} function latestVehicleInspection(vehicleId){let list=(db.vehicleInspections||[]).filter(x=>x.vehicleId===vehicleId).slice().sort((a,b)=>(b.endDate||'').localeCompare(a.endDate||'')); return list[0]||{};} function vehicleRecordRows(vehicleId){ let maintenance=(db.vehicleMaintenances||[]).filter(x=>x.vehicleId===vehicleId).slice().sort((a,b)=>(b.date||'').localeCompare(a.date||'')); let tolls=(db.vehicleTolls||[]).filter(x=>x.vehicleId===vehicleId).slice().sort((a,b)=>(b.date||'').localeCompare(a.date||'')); let fines=(db.vehicleFines||[]).filter(x=>x.vehicleId===vehicleId).slice().sort((a,b)=>(b.date||'').localeCompare(a.date||'')); let accidents=(db.vehicleAccidents||[]).filter(x=>x.vehicleId===vehicleId).slice().sort((a,b)=>(b.date||'').localeCompare(a.date||'')); let insurances=(db.vehicleInsurances||[]).filter(x=>x.vehicleId===vehicleId).slice().sort((a,b)=>(a.endDate||'9999-99-99').localeCompare(b.endDate||'9999-99-99')); let inspections=(db.vehicleInspections||[]).filter(x=>x.vehicleId===vehicleId).slice().sort((a,b)=>(a.endDate||'9999-99-99').localeCompare(b.endDate||'9999-99-99')); let fuels=(db.vehicleFuels||[]).filter(x=>x.vehicleId===vehicleId).slice().sort((a,b)=>(b.date||'').localeCompare(a.date||'')); return {maintenance,tolls,fines,accidents,insurances,inspections,fuels}; } function vehiclesView(){ document.getElementById('topActions').innerHTML=''; let maintenanceTotal=vehicleCost(db.vehicleMaintenances), tollTotal=vehicleCost(db.vehicleTolls), fineTotal=vehicleCost(db.vehicleFines), accidentTotal=vehicleCost(db.vehicleAccidents), insuranceTotal=vehicleCost(db.vehicleInsurances), inspectionTotal=vehicleCost(db.vehicleInspections), fuelTotal=vehicleCost(db.vehicleFuels); let vehicleRows=(db.vehicles||[]).map(v=>{ let rec=vehicleRecordRows(v.id); let total=vehicleCost(rec.maintenance)+vehicleCost(rec.tolls)+vehicleCost(rec.fines)+vehicleCost(rec.accidents)+vehicleCost(rec.insurances)+vehicleCost(rec.inspections)+vehicleCost(rec.fuels); let muayene=latestVehicleInspection(v.id); let kasko=latestVehicleInsurance(v.id,'Kasko'); let trafik=latestVehicleInsurance(v.id,'Trafik Sigortası'); return [v.plate||'-',v.brand||'-',v.model||'-',v.year||'-',v.owner||'-',v.licenseSerial||'-',docCell('vehicles',v.id,'licenseFile'),v.driver||'-',v.tireSize||'-',v.rimSize||'-',formatDateTR(muayene.endDate),dateBadge(muayene.endDate),formatDateTR(kasko.endDate),dateBadge(kasko.endDate),formatDateTR(trafik.endDate),dateBadge(trafik.endDate),badge(v.status||'Aktif'),money(total),` `]; }); let detailHtml=(db.vehicles||[]).map(v=>{ let rec=vehicleRecordRows(v.id); return `

${esc(vehicleName(v.id))}

Ruhsat Seri No: ${esc(v.licenseSerial||'-')}   |   Ruhsat PDF/Görsel: ${docCell('vehicles',v.id,'licenseFile')}
Lastik: ${esc(v.tireSize||'-')}   |   Jant: ${esc(v.rimSize||'-')}
Bakım: ${money(vehicleCost(rec.maintenance))}   |   HGS/OGS: ${money(vehicleCost(rec.tolls))}   |   Ceza: ${money(vehicleCost(rec.fines))}   |   Kaza: ${money(vehicleCost(rec.accidents))}   |   Kasko/Sigorta: ${money(vehicleCost(rec.insurances))}   |   Muayene: ${money(vehicleCost(rec.inspections))}   |   Yakıt: ${money(vehicleCost(rec.fuels))}

Bakım Kayıtları

${table(rec.maintenance.map(x=>[x.date||'-',x.km||'-',x.type||'-',x.company||'-',money(x.amount),x.nextKm||x.nextDate||'-',x.note||'-',` `]),['Tarih','KM','Bakım Türü','Servis/Firma','Tutar','Sonraki Bakım KM','Açıklama','İşlem'])}

Yakıt Kayıtları

${table(rec.fuels.map(x=>[x.date||'-',x.km||'-',x.fuelType||'-',x.liter||0,money(x.unitPrice),money(x.amount),x.station||'-',x.receiptNo||'-',x.paymentType||'-',x.note||'-',` `]),['Tarih','KM','Yakıt Türü','Litre','Litre Fiyatı','Toplam Tutar','İstasyon','Fiş/Fatura No','Ödeme Tipi','Açıklama','İşlem'])}

HGS / OGS Geçişleri

${table(rec.tolls.map(x=>[x.date||'-',x.route||'-',x.system||'-',money(x.amount),x.invoiceNo||'-',x.note||'-',` `]),['Tarih','Geçiş / Güzergah','Sistem','Tutar','Fiş/Fatura No','Açıklama','İşlem'])}

Trafik Cezaları

${table(rec.fines.map(x=>[x.date||'-',x.fineNo||'-',x.reason||'-',money(x.amount),x.status||'-',x.paymentDate||'-',x.note||'-',` `]),['Ceza Tarihi','Ceza No','Sebep','Tutar','Durum','Ödeme Tarihi','Açıklama','İşlem'])}

Kasko / Trafik Sigortası

${table(rec.insurances.map(x=>[x.type||'-',x.policyNo||'-',x.company||'-',formatDateTR(x.startDate),formatDateTR(x.endDate),dateBadge(x.endDate),money(x.amount),x.note||'-',docCell('vehicleInsurances',x.id,'pdfFile'),` `]),['Tür','Poliçe No','Sigorta Şirketi','Başlangıç','Bitiş','Durum','Tutar','Açıklama','PDF/Görsel','İşlem'])}

Muayene

${table(rec.inspections.map(x=>[formatDateTR(x.startDate),formatDateTR(x.endDate),dateBadge(x.endDate),money(x.amount),x.status||'-',x.note||'-',` `]),['Başlangıç','Bitiş','Durum','Tutar','Kayıt Durumu','Açıklama','İşlem'])}

Kaza İşlemleri

${table(rec.accidents.map(x=>[x.date||'-',x.location||'-',x.driver||'-',x.damage||'-',x.insurance||'-',money(x.amount),x.status||'-',x.note||'-',docCell('vehicleAccidents',x.id,'accidentFile'),` `]),['Kaza Tarihi','Yer','Sürücü','Hasar','Sigorta/Dosya','Masraf','Durum','Açıklama','Kaza Görseli/PDF','İşlem'])}
`; }).join(''); document.getElementById('view').innerHTML=`
Bu bölümde araç kartlarını, araçlara yaptırılan bakımları, HGS/OGS geçişlerini, trafik cezalarını, kaza işlemlerini, kasko, trafik sigortası, muayene ve yakıt kayıtlarını araç bazında takip edebilirsiniz.
Toplam Araç
${(db.vehicles||[]).length}
Bakım Toplamı
${money(maintenanceTotal)}
HGS/OGS Toplamı
${money(tollTotal)}
Ceza + Kaza
${money(fineTotal+accidentTotal)}
Kasko/Sigorta Toplamı
${money(insuranceTotal)}
Muayene Toplamı
${money(inspectionTotal)}
Yakıt Toplamı
${money(fuelTotal)}

Araç Listesi

${table(vehicleRows,['Plaka','Marka','Model','Yıl','Araç Sahibi','Ruhsat Seri No','Ruhsat PDF/Görsel','Sürücü','Lastik Ebatı','Jant Ebatı','Muayene Bitiş','Muayene Durum','Kasko Bitiş','Kasko Durum','Trafik Sigorta Bitiş','Trafik Durum','Durum','Toplam Masraf','İşlem'])}
${detailHtml || '

Araç Kayıtları

Henüz araç eklenmemiş.
'}`; } function vehicleForm(id=''){let v=byId(db.vehicles,id); openModal(id?'Araç Düzenle':'Araç Ekle',`
Mevcut: ${v.licenseFile?esc(v.licenseFile.name||'Ekli dosya'):'Dosya yok'}
`)} async function saveVehicle(id=''){ try{ if(!db.vehicles)db.vehicles=[]; let v=id?byId(db.vehicles,id):{id:uid('arac')}; if(!formVal('vplate')){alert('Plaka girin');return} let lf=null; try{lf=await fileToDataUrl('vlicensefile');}catch(e){console.error(e); alert('Ruhsat dosyası okunamadı. Dosya olmadan araç kaydedilecek.'); lf=null;} if(lf==='__too_big__')return; Object.assign(v,{plate:formVal('vplate'),brand:formVal('vbrand'),model:formVal('vmodel'),year:formVal('vyear'),owner:formVal('vowner'),licenseSerial:formVal('vlicense'),driver:formVal('vdriver'),tireSize:formVal('vtire'),rimSize:formVal('vrim'),status:formVal('vstatus')||'Aktif',note:formVal('vnote')}); if(lf)v.licenseFile=lf; if(!id)db.vehicles.push(v); if(!save()){ if(lf){delete v.licenseFile; if(save()){alert('Araç kaydedildi fakat ruhsat dosyası tarayıcı hafızası dolduğu için eklenemedi.'); closeModal(); render();}} return; } closeModal(); render(); }catch(e){console.error('Araç kayıt hatası:',e); alert('Araç kaydı sırasında hata oluştu: '+(e.message||e));} } function fuelForm(id='',vehicleId=''){ let x=byId(db.vehicleFuels,id); openModal(id?'Yakıt Kaydı Düzenle':'Yakıt Kaydı Ekle',`
`) } function calcFuelTotal(){let l=Number(formVal('yliter')||0), u=Number(formVal('yunit')||0), el=document.getElementById('yamount'); if(el)el.value=(l*u).toFixed(2)} function saveFuel(id=''){ if(!db.vehicleFuels)db.vehicleFuels=[]; let x=id?byId(db.vehicleFuels,id):{id:uid('yakit')}; if(!formVal('yvehicle')){alert('Araç seçin');return} let liter=Number(formVal('yliter')||0), unitPrice=Number(formVal('yunit')||0), amount=Number(formVal('yamount')||0); if(!amount && liter && unitPrice)amount=liter*unitPrice; Object.assign(x,{vehicleId:formVal('yvehicle'),date:formVal('ydate'),km:formVal('ykm'),fuelType:formVal('ytype')||'Dizel',liter:liter,unitPrice:unitPrice,amount:amount,station:formVal('ystation'),receiptNo:formVal('yreceipt'),paymentType:formVal('ypayment'),note:formVal('ynote')}); if(!id)db.vehicleFuels.push(x); save(); closeModal(); render(); } function maintenanceForm(id='',vehicleId=''){let x=byId(db.vehicleMaintenances,id); openModal(id?'Bakım Düzenle':'Bakım Kaydı Ekle',`
`)} function saveMaintenance(id=''){let x=id?byId(db.vehicleMaintenances,id):{id:uid('bakim')}; if(!formVal('mvehicle')){alert('Araç seçin');return} Object.assign(x,{vehicleId:formVal('mvehicle'),date:formVal('mdate'),km:formVal('mkm'),type:formVal('mtype'),company:formVal('mcompany'),amount:Number(formVal('mamount')||0),nextKm:formVal('mnext'),note:formVal('mnote')}); if(!id)db.vehicleMaintenances.push(x); save(); closeModal(); render();} function tollForm(id='',vehicleId=''){let x=byId(db.vehicleTolls,id); openModal(id?'HGS/OGS Düzenle':'HGS/OGS Geçişi Ekle',`
`)} function saveToll(id=''){let x=id?byId(db.vehicleTolls,id):{id:uid('hgs')}; if(!formVal('tvehicle')){alert('Araç seçin');return} Object.assign(x,{vehicleId:formVal('tvehicle'),date:formVal('tdate'),system:formVal('tsystem'),route:formVal('troute'),amount:Number(formVal('tamount')||0),invoiceNo:formVal('tinvoice'),note:formVal('tnote')}); if(!id)db.vehicleTolls.push(x); save(); closeModal(); render();} function fineForm(id='',vehicleId=''){let x=byId(db.vehicleFines,id); openModal(id?'Trafik Cezası Düzenle':'Trafik Cezası Ekle',`
`)} function saveFine(id=''){let x=id?byId(db.vehicleFines,id):{id:uid('ceza')}; if(!formVal('fvehicle')){alert('Araç seçin');return} Object.assign(x,{vehicleId:formVal('fvehicle'),date:formVal('fdate'),fineNo:formVal('ffineno'),reason:formVal('freason'),amount:Number(formVal('famount')||0),status:formVal('fstatus'),paymentDate:formVal('fpayment'),note:formVal('fnote')}); if(!id)db.vehicleFines.push(x); save(); closeModal(); render();} function accidentForm(id='',vehicleId=''){let x=byId(db.vehicleAccidents,id); openModal(id?'Kaza İşlemi Düzenle':'Kaza İşlemi Ekle',`
Mevcut: ${x.accidentFile?esc(x.accidentFile.name||'Ekli dosya'):'Dosya yok'}
`)} async function saveAccident(id=''){let x=id?byId(db.vehicleAccidents,id):{id:uid('kaza')}; if(!formVal('avehicle')){alert('Araç seçin');return} const af=await fileToDataUrl('accfile'); if(af==='__too_big__')return; Object.assign(x,{vehicleId:formVal('avehicle'),date:formVal('adate'),location:formVal('alocation'),driver:formVal('adriver'),damage:formVal('adamage'),insurance:formVal('ainsurance'),amount:Number(formVal('aamount')||0),status:formVal('astatus'),note:formVal('anote')}); if(af)x.accidentFile=af; if(!id)db.vehicleAccidents.push(x); save(); closeModal(); render();} function insuranceForm(id='',vehicleId=''){ let x=byId(db.vehicleInsurances,id); openModal(id?'Kasko / Trafik Sigortası Düzenle':'Kasko / Trafik Sigortası Ekle',`
Mevcut: ${x.pdfFile?esc(x.pdfFile.name||'Ekli dosya'):'Dosya yok'}
`) } async function saveInsurance(id=''){ let x=id?byId(db.vehicleInsurances,id):{id:uid('sigorta')}; if(!formVal('insvehicle')){alert('Araç seçin');return} if(!formVal('insend')){alert('Bitiş tarihi girin');return} const pf=await fileToDataUrl('inspdf'); if(pf==='__too_big__')return; Object.assign(x,{vehicleId:formVal('insvehicle'),type:formVal('instype'),policyNo:formVal('inspolicy'),company:formVal('inscompany'),startDate:formVal('insstart'),endDate:formVal('insend'),amount:Number(formVal('insamount')||0),status:formVal('insstatus')||'Aktif',note:formVal('insnote')}); if(pf)x.pdfFile=pf; if(!id)db.vehicleInsurances.push(x); save(); closeModal(); render(); } function inspectionForm(id='',vehicleId=''){ let x=byId(db.vehicleInspections,id); openModal(id?'Muayene Düzenle':'Muayene Ekle',`
`) } function saveInspection(id=''){ let x=id?byId(db.vehicleInspections,id):{id:uid('muayene')}; if(!formVal('ivvehicle')){alert('Araç seçin');return} if(!formVal('ivend')){alert('Bitiş tarihi girin');return} Object.assign(x,{vehicleId:formVal('ivvehicle'),startDate:formVal('ivstart'),endDate:formVal('ivend'),amount:Number(formVal('ivamount')||0),status:formVal('ivstatus')||'Aktif',note:formVal('ivnote')}); if(!id)db.vehicleInspections.push(x); save(); closeModal(); render(); } function personnelName(id){let p=byId(db.personnel,id);return p.id?(p.name||'-'):'-'} function personnelOptions(sel=''){return (db.personnel||[]).map(p=>``).join('')} function personnelView(){ document.getElementById('topActions').innerHTML=''; let active=(db.personnel||[]).filter(p=>(p.status||'Aktif')==='Aktif').length; let advanceTotal=(db.personnelPayments||[]).filter(x=>x.type==='Avans').reduce((s,x)=>s+Number(x.amount||0),0); let salaryTotal=(db.personnel||[]).reduce((s,x)=>s+Number(x.salary||0),0); let openNotes=(db.personnelNotes||[]).filter(n=>(n.status||'Açık')!=='Tamamlandı').length; document.getElementById('view').innerHTML=`
Toplam Personel
${(db.personnel||[]).length}
Aktif Personel
${active}
Puantaj Kaydı
${(db.personnelTimesheets||[]).length}
Aylık Maaş Toplamı
${money(salaryTotal)}
Açık Personel Notu
${openNotes}

Personel Kartları

${table((db.personnel||[]).map(p=>[p.name||'-',p.tc||'-',p.phone||'-',p.role||'-',p.department||'-',p.shoeSize||'-',p.bloodGroup||'-',p.startDate||'-',money(p.salary||0),badge(p.status||'Aktif'),p.note||'-',` `]),['Ad Soyad','T.C.','Telefon','Görev','Departman','Ayakkabı No','Kan Grubu','İşe Giriş','Maaş','Durum','Not','İşlem'])}

Aylık Personel Puantajı

${monthlyPuantajPanel()}

İzin Takibi

${table((db.personnelLeaves||[]).slice().reverse().map(x=>[personnelName(x.personId),x.type||'-',x.startDate||'-',x.endDate||'-',x.days||'-',badge(x.status||'Planlandı'),x.note||'-',` `]),['Personel','İzin Türü','Başlangıç','Bitiş','Gün','Durum','Açıklama','İşlem'])}

Maaş / Avans / Ödeme

${table((db.personnelPayments||[]).slice().reverse().map(x=>[x.date||'-',personnelName(x.personId),x.type||'-',money(x.amount||0),x.note||'-',` `]),['Tarih','Personel','Tür','Tutar','Açıklama','İşlem'])}

Zimmetler

${table((db.personnelAssets||[]).slice().reverse().map(x=>[personnelName(x.personId),x.item||'-',x.serial||'-',x.givenDate||'-',x.returnDate||'-',badge(x.status||'Teslim Edildi'),x.note||'-',` `]),['Personel','Zimmet','Seri No','Veriliş','İade','Durum','Açıklama','İşlem'])}

Personel Önemli Notları

${table((db.personnelNotes||[]).slice().reverse().map(x=>[x.date||'-',personnelName(x.personId),x.title||'-',badge(x.priority||'Normal'),badge(x.status||'Açık'),x.note||'-',` `]),['Tarih','Personel','Başlık','Öncelik','Durum','Not','İşlem'])}
`; } function personnelForm(id=''){ let p=id?byId(db.personnel,id):{}; openModal(id?'Personel Düzenle':'Personel Ekle',`
`) } function savePersonnel(id=''){ let p=id?byId(db.personnel,id):{id:uid('PRS')}; if(!formVal('pname')){alert('Ad soyad girin');return} Object.assign(p,{name:formVal('pname'),tc:formVal('ptc'),phone:formVal('pphone'),role:formVal('prole'),department:formVal('pdepartment'),shoeSize:formVal('pshoe'),bloodGroup:formVal('pblood'),startDate:formVal('pstart'),salary:Number(formVal('psalary')||0),status:formVal('pstatus'),note:formVal('pnote')}); if(!id)db.personnel.push(p); save(); closeModal(); render(); } function printPersonnelPdf(id){ let p=byId(db.personnel,id); if(!p.id){alert('Personel kaydı bulunamadı');return} let leaves=(db.personnelLeaves||[]).filter(x=>x.personId===id).slice().reverse().slice(0,10); let payments=(db.personnelPayments||[]).filter(x=>x.personId===id).slice().reverse().slice(0,10); let assets=(db.personnelAssets||[]).filter(x=>x.personId===id).slice().reverse().slice(0,10); let notes=(db.personnelNotes||[]).filter(x=>x.personId===id).slice().reverse().slice(0,10); const tr=(a,b)=>`${esc(a)}${b||'-'}`; const simpleRows=(arr,cols,empty='Kayıt yok')=>arr.length?`${cols.map(c=>``).join('')}${arr.map(x=>`${cols.map(c=>``).join('')}`).join('')}
${esc(c[0])}
${c[1](x)||'-'}
`:`
${esc(empty)}
`; let html=`Personel Kartı - ${esc(p.name||'')}
SİSTEM KULE VİNÇ
Personel Kartı
Çıktı Tarihi
${today()}

${esc(p.name||'-')}

${tr('T.C. Kimlik No',esc(p.tc||'-'))}${tr('Telefon',esc(p.phone||'-'))}${tr('Görev / Pozisyon',esc(p.role||'-'))}${tr('Departman',esc(p.department||'-'))}${tr('Ayakkabı Numarası',esc(p.shoeSize||'-'))}${tr('Kan Grubu',esc(p.bloodGroup||'-'))}${tr('İşe Giriş Tarihi',esc(p.startDate||'-'))}${tr('Aylık Maaş',money(p.salary||0))}${tr('Durum',esc(p.status||'Aktif'))}${tr('Adres / Açıklama',esc(p.note||'-'))}

Son Maaş / Avans / Ödeme Kayıtları

${simpleRows(payments,[['Tarih',x=>esc(x.date||'-')],['Tür',x=>esc(x.type||'-')],['Tutar',x=>money(x.amount||0)],['Açıklama',x=>esc(x.note||'-')]])}

İzin Kayıtları

${simpleRows(leaves,[['Tür',x=>esc(x.type||'-')],['Başlangıç',x=>esc(x.startDate||'-')],['Bitiş',x=>esc(x.endDate||'-')],['Gün',x=>esc(x.days||'-')],['Durum',x=>esc(x.status||'-')],['Açıklama',x=>esc(x.note||'-')]])}

Zimmet Kayıtları

${simpleRows(assets,[['Zimmet',x=>esc(x.item||'-')],['Seri No',x=>esc(x.serial||'-')],['Veriliş',x=>esc(x.givenDate||'-')],['İade',x=>esc(x.returnDate||'-')],['Durum',x=>esc(x.status||'-')],['Açıklama',x=>esc(x.note||'-')]])}

Personel Notları

${simpleRows(notes,[['Tarih',x=>esc(x.date||'-')],['Başlık',x=>esc(x.title||'-')],['Öncelik',x=>esc(x.priority||'-')],['Durum',x=>esc(x.status||'-')],['Not',x=>esc(x.note||'-')]])}
Personel İmza
Yetkili İmza
`; let w=window.open('','_blank'); if(!w){alert('PDF yazdırma penceresi açılamadı. Tarayıcı açılır pencere iznini kontrol edin.');return} w.document.open(); w.document.write(html); w.document.close(); w.focus(); setTimeout(()=>w.print(),300); } function showPersonnelSalaries(){ let personnel=(db.personnel||[]).slice().sort((a,b)=>(a.name||'').localeCompare(b.name||'','tr')); let total=personnel.reduce((s,p)=>s+Number(p.salary||0),0); let activeTotal=personnel.filter(p=>(p.status||'Aktif')==='Aktif').reduce((s,p)=>s+Number(p.salary||0),0); let rows=personnel.map(p=>[p.name||'-',p.role||'-',p.department||'-',p.startDate||'-',money(p.salary||0),badge(p.status||'Aktif'),``]); let html=`
Personel Sayısı
${personnel.length}
Aktif Personel Maaşı
${money(activeTotal)}
Toplam Maaş
${money(total)}
Personel Maaşları Göster: Bu liste Personel Kartındaki Aylık Maaş alanından gelir. Maaş değişikliği için ilgili personelde Düzenle butonunu kullanın.
${table(rows,['Personel','Görev','Departman','İşe Giriş','Aylık Maaş','Durum','İşlem'])}`; openModal('Personel Maaşları Göster',html); } function scrollToPuantaj(){let el=document.getElementById('puantajPanel'); if(el)el.scrollIntoView({behavior:'smooth',block:'start'});} function daysInMonth(y,m){return new Date(Number(y),Number(m),0).getDate()} function puantajMonthDefaults(){let d=today();return {y:d.slice(0,4),m:d.slice(5,7)}} function puantajCodeOptions(sel=''){ const ops=[['','-'],['Ç','Ç'],['H','H'],['İ','İ'],['R','R'],['G','G'],['Y','Y'],['M','M'],['B','B']]; return ops.map(o=>``).join(''); } function puantajLabelToCode(v){v=String(v||''); if(['Ç','H','İ','R','G','Y','M','B'].includes(v))return v; return {'Çalıştı':'Ç','Gelmedi':'G','İzinli':'İ','Raporlu':'R','Resmi Tatil':'B','Yarım Gün':'Y','Mesai':'M','Hafta Tatili':'H'}[v]||'';} function puantajCodeToLabel(c){return {'Ç':'Çalıştı','H':'Hafta Tatili','İ':'İzinli','R':'Raporlu','G':'Gelmedi','Y':'Yarım Gün','M':'Mesai','B':'Bayram / Resmi Tatil'}[c]||'';} function puantajFind(personId,date){return (db.personnelTimesheets||[]).find(x=>x.personId===personId && x.date===date) || {};} function puantajNote(personId,month){return ((db.personnelPuantajNotes||[]).find(x=>x.personId===personId && x.month===month)||{}).note||'';} function monthlyPuantajPanel(monthArg=''){ const def=puantajMonthDefaults(); const month=monthArg||ym(today()); const y=month.slice(0,4)||def.y; const m=month.slice(5,7)||def.m; const dim=daysInMonth(y,m); const activePeople=(db.personnel||[]).filter(p=>(p.status||'Aktif')==='Aktif'); const monthOptions=Array.from({length:12},(_,i)=>String(i+1).padStart(2,'0')).map(mm=>``).join(''); const yearNow=Number(def.y); const years=Array.from({length:7},(_,i)=>String(yearNow-3+i)); const yearOptions=years.map(yy=>``).join(''); const headers=Array.from({length:dim},(_,i)=>`${i+1}`).join(''); const rows=activePeople.map(p=>{ let cells=''; for(let d=1;d<=dim;d++){ let date=`${y}-${m}-${String(d).padStart(2,'0')}`; let rec=puantajFind(p.id,date); let code=puantajLabelToCode(rec.code||rec.status); let ot=Number(rec.overtimeHours||0)||''; cells+=``; } return `${esc(p.name||'-')}
${esc(p.role||'')}
${cells}00000`; }).join('') || `Aktif personel kaydı yok.`; setTimeout(()=>{activePeople.forEach(p=>recalcPuantajRow(p.id,dim));},0); return `
Aylık tablo mantığı: Personeller satırda, ayın günleri sütunda görünür. Her güne kod seçilir; mesai saati için küçük MS kutusuna saat yazılır.
Ç: ÇalıştıH: Hafta Tatiliİ: İzinliR: RaporluG: GelmediY: Yarım GünM: MesaiB: Bayram/Resmi TatilMS: Mesai Saati
${headers}${rows}
PersonelÇalışmaİzin/RaporGelmediTatilMesai SaatAçıklama
`; } function recalcPuantajRow(pid,dim){ let work=0,leave=0,abs=0,hol=0,ot=0; for(let d=1;d<=dim;d++){ let code=(document.getElementById(`pn_${pid}_${d}`)||{}).value||''; let ms=Number((document.getElementById(`po_${pid}_${d}`)||{}).value||0); if(code==='Ç'||code==='M')work+=1; else if(code==='Y')work+=0.5; else if(code==='İ'||code==='R')leave+=1; else if(code==='G')abs+=1; else if(code==='H'||code==='B')hol+=1; ot+=ms; } ['work','leave','abs','hol','ot'].forEach(k=>{let el=document.getElementById(`pt_${k}_${pid}`); if(el)el.textContent=({work,leave,abs,hol,ot}[k]||0);}); } function refreshPuantajMonth(){let y=formVal('puantajYear'),m=formVal('puantajMonth'); let p=document.getElementById('puantajPanel'); if(p)p.innerHTML=`

Aylık Personel Puantajı

${monthlyPuantajPanel(`${y}-${m}`)}`;} function saveMonthlyPuantaj(){ let y=formVal('puantajYear'),m=formVal('puantajMonth'); if(!y||!m){alert('Ay/yıl seçin');return} const month=`${y}-${m}`, dim=daysInMonth(y,m); const activePeople=(db.personnel||[]).filter(p=>(p.status||'Aktif')==='Aktif'); activePeople.forEach(p=>{ for(let d=1;d<=dim;d++){ let date=`${month}-${String(d).padStart(2,'0')}`; let code=(document.getElementById(`pn_${p.id}_${d}`)||{}).value||''; let overtime=Number((document.getElementById(`po_${p.id}_${d}`)||{}).value||0); let idx=(db.personnelTimesheets||[]).findIndex(x=>x.personId===p.id && x.date===date); if(!code && !overtime){ if(idx>-1)db.personnelTimesheets.splice(idx,1); continue; } let rec=idx>-1?db.personnelTimesheets[idx]:{id:uid('PNT')}; Object.assign(rec,{personId:p.id,date,code,status:puantajCodeToLabel(code),normalHours:(code==='Ç'||code==='M')?8:(code==='Y'?4:0),overtimeHours:overtime,nightHours:0,site:'',note:''}); if(idx<0)db.personnelTimesheets.push(rec); } let note=(document.getElementById(`pnote_${p.id}`)||{}).value||''; if(!db.personnelPuantajNotes)db.personnelPuantajNotes=[]; let ni=db.personnelPuantajNotes.findIndex(x=>x.personId===p.id&&x.month===month); if(note){let n=ni>-1?db.personnelPuantajNotes[ni]:{id:uid('PNOT')}; Object.assign(n,{personId:p.id,month,note}); if(ni<0)db.personnelPuantajNotes.push(n);} else if(ni>-1)db.personnelPuantajNotes.splice(ni,1); }); save(); alert('Aylık puantaj kaydedildi.'); refreshPuantajMonth(); } function printMonthlyPuantaj(){window.print();} function exportMonthlyPuantajCsv(){ let y=formVal('puantajYear'),m=formVal('puantajMonth'); const month=`${y}-${m}`, dim=daysInMonth(y,m); let lines=[]; let head=['Personel']; for(let d=1;d<=dim;d++)head.push(String(d)); head.push('Çalışma','İzin/Rapor','Gelmedi','Tatil','Mesai Saat','Açıklama'); lines.push(head.join(';')); (db.personnel||[]).filter(p=>(p.status||'Aktif')==='Aktif').forEach(p=>{let row=[`"${String(p.name||'').replaceAll('"','""')}"`]; let work=0,leave=0,abs=0,hol=0,ot=0; for(let d=1;d<=dim;d++){let code=(document.getElementById(`pn_${p.id}_${d}`)||{}).value||''; let ms=Number((document.getElementById(`po_${p.id}_${d}`)||{}).value||0); row.push(code+(ms?` MS:${ms}`:'')); if(code==='Ç'||code==='M')work++; else if(code==='Y')work+=0.5; else if(code==='İ'||code==='R')leave++; else if(code==='G')abs++; else if(code==='H'||code==='B')hol++; ot+=ms;} row.push(work,leave,abs,hol,ot,`"${String((document.getElementById(`pnote_${p.id}`)||{}).value||'').replaceAll('"','""')}"`); lines.push(row.join(';'));}); let blob=new Blob(['\ufeff'+lines.join('\n')],{type:'text/csv;charset=utf-8;'}); let a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`puantaj_${month}.csv`; a.click(); URL.revokeObjectURL(a.href); } function personnelTimesheetSummary(){ let rows=[]; let activePeople=(db.personnel||[]).filter(p=>(p.status||'Aktif')==='Aktif'); let month=ym(today()); activePeople.forEach(p=>{ let list=(db.personnelTimesheets||[]).filter(x=>x.personId===p.id && String(x.date||'').slice(0,7)===month); let worked=list.filter(x=>x.status==='Çalıştı').length; let absent=list.filter(x=>x.status==='Gelmedi').length; let leave=list.filter(x=>x.status==='İzinli' || x.status==='Raporlu').length; let normal=list.reduce((s,x)=>s+Number(x.normalHours||0),0); let overtime=list.reduce((s,x)=>s+Number(x.overtimeHours||0),0); rows.push([p.name||'-',month,worked,absent,leave,normal,overtime]); }); return `
Bu Ay Puantaj Özeti: Personel bazlı çalıştı/gelmedi/izinli gün ve saat toplamları.
${table(rows,['Personel','Ay','Çalıştı Gün','Gelmedi Gün','İzin/Rapor Gün','Normal Saat','Mesai Saat'])}`; } function timesheetForm(id=''){ let x=id?byId(db.personnelTimesheets,id):{}; openModal(id?'Puantaj Düzenle':'Puantaj Ekle',`
`) } function saveTimesheet(id=''){ let x=id?byId(db.personnelTimesheets,id):{id:uid('PNT')}; if(!formVal('tsperson')){alert('Personel seçin');return} if(!formVal('tsdate')){alert('Tarih girin');return} Object.assign(x,{personId:formVal('tsperson'),date:formVal('tsdate'),status:formVal('tsstatus'),normalHours:Number(formVal('tsnormal')||0),overtimeHours:Number(formVal('tsovertime')||0),nightHours:Number(formVal('tsnight')||0),site:formVal('tssite'),note:formVal('tsnote')}); if(!id)db.personnelTimesheets.push(x); save(); closeModal(); render(); } function leaveForm(id=''){ let x=id?byId(db.personnelLeaves,id):{}; openModal(id?'İzin Düzenle':'İzin Ekle',`
`) } function saveLeave(id=''){let x=id?byId(db.personnelLeaves,id):{id:uid('IZN')}; if(!formVal('lperson')){alert('Personel seçin');return} Object.assign(x,{personId:formVal('lperson'),type:formVal('ltype'),startDate:formVal('lstart'),endDate:formVal('lend'),days:Number(formVal('ldays')||0),status:formVal('lstatus'),note:formVal('lnote')}); if(!id)db.personnelLeaves.push(x); save(); closeModal(); render();} function paymentForm(id=''){ let x=id?byId(db.personnelPayments,id):{}; openModal(id?'Maaş / Avans Düzenle':'Maaş / Avans Ekle',`
`) } function savePayment(id=''){let x=id?byId(db.personnelPayments,id):{id:uid('PAY')}; if(!formVal('payperson')){alert('Personel seçin');return} Object.assign(x,{personId:formVal('payperson'),type:formVal('paytype'),date:formVal('paydate'),amount:Number(formVal('payamount')||0),note:formVal('paynote')}); if(!id)db.personnelPayments.push(x); save(); closeModal(); render();} function assetForm(id=''){ let x=id?byId(db.personnelAssets,id):{}; openModal(id?'Zimmet Düzenle':'Zimmet Ekle',`
`) } function saveAsset(id=''){let x=id?byId(db.personnelAssets,id):{id:uid('ZIM')}; if(!formVal('asperson')){alert('Personel seçin');return} Object.assign(x,{personId:formVal('asperson'),item:formVal('asitem'),serial:formVal('asserial'),givenDate:formVal('asgiven'),returnDate:formVal('asreturn'),status:formVal('asstatus'),note:formVal('asnote')}); if(!id)db.personnelAssets.push(x); save(); closeModal(); render();} function personnelNoteForm(id=''){ let x=id?byId(db.personnelNotes,id):{}; openModal(id?'Personel Notu Düzenle':'Personel Notu Ekle',`
`) } function savePersonnelNote(id=''){let x=id?byId(db.personnelNotes,id):{id:uid('PNOT')}; Object.assign(x,{personId:formVal('pnperson'),title:formVal('pntitle'),priority:formVal('pnpriority'),date:formVal('pndate'),status:formVal('pnstatus'),note:formVal('pnnote')}); if(!id)db.personnelNotes.push(x); save(); closeModal(); render();} function crmView(){ let openNotes=(db.importantNotes||[]).filter(n=>n.status!=='Tamamlandı'); document.getElementById('topActions').innerHTML=''; document.getElementById('view').innerHTML=`
CRM Kaydı
${(db.crmRecords||[]).length}
Açık Önemli Not
${openNotes.length}
Yüksek Öncelik
${openNotes.filter(n=>n.priority==='Yüksek').length}
Bugün / Geciken Takip
${openNotes.filter(n=>n.date&&n.date<=today()).length}

Önemli Notlar

${table((db.importantNotes||[]).slice().sort((a,b)=>(a.status==='Tamamlandı')-(b.status==='Tamamlandı') || String(a.date||'').localeCompare(String(b.date||''))).map(n=>[badge(n.priority||'Normal'),n.date||'-',n.title||'-',crmRelatedName(n),badge(n.status||'Açık'),n.note||'-',` `]),['Öncelik','Tarih','Başlık','Bağlı Kayıt','Durum','Not','İşlem'])}

CRM Müşteri / Firma Takipleri

${table((db.crmRecords||[]).slice().sort((a,b)=>String(b.created||'').localeCompare(String(a.created||''))).map(c=>[c.created||'-',c.title||'-',crmRelatedName(c),c.contact||'-',c.phone||'-',badge(c.stage||'Yeni'),c.nextDate||'-',c.note||'-',` `]),['Tarih','Konu','Firma/Müşteri','Yetkili','Telefon','Durum','Takip Tarihi','Not','İşlem'])}
`; } function crmRelatedName(x){if(x.customerId)return 'Müşteri: '+nameOf(db.customers,x.customerId); return x.relatedName||'-'} function crmForm(id=''){let c=id?byId(db.crmRecords,id):{}; openModal(id?'CRM Kaydı Düzenle':'CRM Kaydı Ekle',`
`)} function saveCrm(id=''){let c=id?byId(db.crmRecords,id):{id:uid('crm'),created:today()}; Object.assign(c,{customerId:formVal('crmCustomer'),ownerId:formVal('crmOwner'),title:formVal('crmTitle'),contact:formVal('crmContact'),phone:formVal('crmPhone'),stage:formVal('crmStage'),nextDate:formVal('crmNext'),note:formVal('crmNote')}); if(!id)db.crmRecords.push(c); save(); closeModal(); render();} function importantNoteForm(id=''){let n=id?byId(db.importantNotes,id):{}; openModal(id?'Önemli Not Düzenle':'Önemli Not Ekle',`
`)} function saveImportantNote(id=''){let n=id?byId(db.importantNotes,id):{id:uid('note'),created:today()}; Object.assign(n,{customerId:formVal('noteCustomer'),ownerId:'',title:formVal('noteTitle'),priority:formVal('notePriority'),date:formVal('noteDate'),status:formVal('noteStatus'),note:formVal('noteText')}); if(!id)db.importantNotes.push(n); save(); closeModal(); render();} function smsSettings(){ if(!db.settings)db.settings={}; if(!db.settings.sms)db.settings.sms={username:'',password:'',originator:'',apiUrl:'https://smsgw.mutlucell.com/smsgw-ws/sndblkex',testMode:true}; db.settings.sms.apiUrl=db.settings.sms.apiUrl||'https://smsgw.mutlucell.com/smsgw-ws/sndblkex'; return db.settings.sms; } function normalizePhone(v){ let d=String(v||'').replace(/\D/g,''); if(!d)return ''; if(d.startsWith('00'))d=d.slice(2); if(d.startsWith('0')&&d.length===11)d='9'+d; if(d.length===10&&d.startsWith('5'))d='90'+d; if(d.length===12&&d.startsWith('90'))return d; if(d.length===11&&d.startsWith('90'))return d; return d; } function smsXmlEscape(v){return String(v||'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"').replaceAll("'",''')} function buildSmsXml(text,nums){ const st=smsSettings(); return `\n\n \n ${smsXmlEscape(text)}\n ${smsXmlEscape(nums.join(','))}\n \n`; } function phoneListFromSource(src){ let arr=[]; if(src==='customers')arr=(db.customers||[]).map(x=>({name:x.name,phone:x.phone})); else if(src==='owners')arr=(db.owners||[]).map(x=>({name:x.name,phone:x.phone})); else if(src==='personnel')arr=(db.personnel||[]).map(x=>({name:x.name,phone:x.phone})); return arr.map(x=>normalizePhone(x.phone)).filter(Boolean); } function fillSmsPhones(){ let src=formVal('smsSource'); let nums=phoneListFromSource(src); document.getElementById('smsPhones').value=[...new Set(nums)].join('\n'); } function smsTemplateOptions(sel=''){ return (db.smsTemplates||[]).map(t=>``).join('') } function useSmsTemplate(){ let id=formVal('smsTpl'); let t=byId(db.smsTemplates||[],id); if(t.id)document.getElementById('smsText').value=t.text||''; smsCountUpdate(); } function smsCountUpdate(){ let txt=(document.getElementById('smsText')||{}).value||''; let len=txt.length; let parts=len?Math.ceil(len/160):0; let el=document.getElementById('smsCount'); if(el)el.textContent=`${len} karakter / yaklaşık ${parts} SMS`; } function smsSettingsForm(){ const st=smsSettings(); openModal('Mutlucell SMS Ayarları',`
Deneme modu kapalıysa program Mutlucell XML API adresine gönderim denemesi yapar. Tarayıcı engeli olursa online/web panel aşamasında sunucu üzerinden gönderim yapılır.
`) } function saveSmsSettings(){ let st=smsSettings(); Object.assign(st,{username:formVal('smsUser'),password:formVal('smsPass'),originator:formVal('smsOrg'),apiUrl:formVal('smsUrl')||'https://smsgw.mutlucell.com/smsgw-ws/sndblkex',testMode:document.getElementById('smsTest').checked}); save(); closeModal(); render(); } function smsTemplateForm(id=''){ let t=id?byId(db.smsTemplates||[],id):{}; openModal(id?'SMS Şablonu Düzenle':'SMS Şablonu Ekle',`
`) } function saveSmsTemplate(id){ let t=id?byId(db.smsTemplates,id):{id:uid('SMSTPL')}; Object.assign(t,{title:formVal('tplTitle'),text:formVal('tplText')}); if(!id)db.smsTemplates.push(t); save(); closeModal(); render(); } function deleteSmsTemplate(id){ if(requireDeletePassword('SMS şablonu silinsin mi?')){db.smsTemplates=(db.smsTemplates||[]).filter(x=>x.id!==id); save(); render();}} function smsPanelHtml(){ const st=smsSettings(); const logs=db.smsMessages||[]; return `
Gönderim Kaydı
${logs.length}
Başarılı
${logs.filter(x=>x.status==='Başarılı').length}
Bekleyen/Hata
${logs.filter(x=>x.status!=='Başarılı').length}
Deneme Modu
${st.testMode?'Açık':'Kapalı'}

SMS Paneli

SMS paneli artık Ayarlar bölümünün içindedir. Mutlucell ayarları, şablonlar, SMS gönderme ve gönderim geçmişi bu ekrandan yönetilir.

SMS Gönder

Mutlucell XML API formatına göre hazırlanır. Yerel HTML sürümünde tarayıcı güvenliği gönderimi engelleyebilir; bu durumda kayıt/şablon sistemi çalışır, gerçek gönderim web panel aşamasında sunucu üzerinden bağlanır.
0 karakter / yaklaşık 0 SMS

SMS Şablonları

${table((db.smsTemplates||[]).map(t=>[t.title||'-',esc(t.text||''),` `]),['Başlık','Metin','İşlem'])}

Gönderim Geçmişi

${table(logs.slice().reverse().map(x=>[x.date||'-',x.toCount||0,x.preview||'-',badge(x.status||'Bekliyor'),x.result||'-',` `]),['Tarih','Alıcı','Mesaj','Durum','Sonuç','İşlem'])}
` } function smsView(){ document.getElementById('topActions').innerHTML=''; document.getElementById('view').innerHTML=smsPanelHtml(); } function settingsSmsPanel(){return smsPanelHtml()} function getSmsFormData(){ let nums=[...new Set(String(formVal('smsPhones')).split(/[\n,;\s]+/).map(normalizePhone).filter(Boolean))]; let text=formVal('smsText'); return {nums,text,xml:buildSmsXml(text,nums)}; } function previewSmsXml(){ let d=getSmsFormData(); if(!d.text||!d.nums.length){alert('Mesaj ve telefon numarası giriniz.');return} openModal('Mutlucell XML Önizleme',`
Bu XML, Mutlucell API adresine text/xml; charset=UTF-8 içerik tipiyle gönderilir.
`) } function showSmsXml(id){let x=byId(db.smsMessages||[],id); openModal('SMS XML Kaydı',``) } async function sendSms(){ let d=getSmsFormData(); const st=smsSettings(); if(!d.text){alert('SMS metni yazınız.');return} if(!d.nums.length){alert('Telefon numarası giriniz.');return} if(!st.username||!st.password||!st.originator){alert('Önce Mutlucell ayarlarından kullanıcı adı, şifre/API Key ve SMS başlığı giriniz.');return} let rec={id:uid('SMS'),date:new Date().toLocaleString('tr-TR'),toCount:d.nums.length,phones:d.nums.join(','),preview:d.text.slice(0,80),text:d.text,xml:d.xml,status:st.testMode?'Deneme':'Bekliyor',result:st.testMode?'Deneme modunda kaydedildi, gerçek SMS gönderilmedi.':''}; if(st.testMode){db.smsMessages.push(rec); save(); render(); alert('Deneme modunda SMS kaydı oluşturuldu. Gerçek SMS gönderilmedi.');return} if(!confirm(`${d.nums.length} alıcıya SMS gönderilecek. Devam edilsin mi?`))return; try{ let res=await fetch(st.apiUrl,{method:'POST',headers:{'Content-Type':'text/xml; charset=UTF-8'},body:d.xml}); let txt=await res.text(); rec.status=txt.trim().startsWith('$')?'Başarılı':'Hata'; rec.result=txt.trim()||('HTTP '+res.status); }catch(e){ rec.status='Hata'; rec.result='Tarayıcı/API erişim hatası: '+(e&&e.message?e.message:e); } db.smsMessages.push(rec); save(); render(); alert(rec.status==='Başarılı'?'SMS gönderim sonucu başarılı görünüyor.':'SMS gönderiminde hata/engel oluştu. Geçmişten sonucu kontrol edin.'); } function materialMaintenanceView(){ let period=materialMaintenancePeriodDays(); let items=(db.items||[]).filter(i=>isMaintenanceTrackableItem(i)); let serviceTypes=[...new Set(items.map(i=>itemServiceType(i)).filter(Boolean))].sort(); let ownerIds=[...new Set(items.map(i=>i.ownerId).filter(Boolean))]; let customerNames=[...new Set(items.map(i=>itemRentalCustomers(i.id)).filter(x=>x&&x!=='-'))].sort(); document.getElementById('topActions').innerHTML=''; document.getElementById('view').innerHTML=`

Malzeme Bakım Takibi

Sadece aktif kiralamada olan malzemeler burada takip edilir. Kiralama kapatıldığında malzeme bu bakım tablosunda görünmez. Yükseltme Ekipman seçilen malzemeler bakım takibinde kesinlikle görünmez. Varsayılan bakım periyodu: ${period} gün.
`; renderMaterialMaintenanceTable(); } function clearMaterialMaintenanceFilters(){['mmFilterSearch','mmFilterService','mmFilterStatus','mmFilterOwner','mmFilterCustomer','mmFilterCity','mmFilterDistrict','mmFilterDays'].forEach(id=>{let el=document.getElementById(id); if(el)el.value='';}); updateDistrictSelect('mmFilterCity','mmFilterDistrict'); renderMaterialMaintenanceTable();} function materialMaintenanceFilterMatch(i){ let q=(formVal('mmFilterSearch')||'').toLowerCase().trim(); let service=formVal('mmFilterService'), status=formVal('mmFilterStatus'), owner=formVal('mmFilterOwner'), customer=formVal('mmFilterCustomer'), city=formVal('mmFilterCity'), district=formVal('mmFilterDistrict'), dayFilter=formVal('mmFilterDays'); let cust=itemRentalCustomers(i.id); let stat=maintenanceStatus(i); let d=maintenanceDaysLeft(i); if(service && itemServiceType(i)!==service)return false; if(status && stat!==status)return false; if(owner && i.ownerId!==owner)return false; if(customer && !cust.split(', ').includes(customer))return false; if(city && i.city!==city)return false; if(district && i.district!==district)return false; if(dayFilter==='overdue' && !(d!=='' && d<0))return false; if(dayFilter==='today' && !(d===0))return false; if(dayFilter==='7' && !(d!=='' && d>=0 && d<=7))return false; if(dayFilter==='30' && !(d!=='' && d>=0 && d<=30))return false; if(dayFilter==='ok' && !(d!=='' && d>30))return false; if(dayFilter==='none' && d!=='')return false; if(q){ let hay=[i.name,i.type,itemServiceType(i),nameOf(db.owners,i.ownerId),cust,i.city,i.district,maintenanceDaysText(i),stat,i.maintenanceNote].join(' ').toLowerCase(); if(!hay.includes(q))return false; } return true; } function renderMaterialMaintenanceTable(){ let rows=(db.items||[]).filter(i=>isMaintenanceTrackableItem(i)).filter(materialMaintenanceFilterMatch).slice().sort((a,b)=>String(itemNextMaintenanceDate(a)||'').localeCompare(String(itemNextMaintenanceDate(b)||''))).map(i=>[i.name||'-',i.type||'-',nameOf(db.owners,i.ownerId),itemLessor(i),itemRentalCustomers(i.id),i.city||'-',i.district||'-',qtyOf(i),formatDateTR(i.lastMaintenanceDate)||'-',formatDateTR(itemNextMaintenanceDate(i))||'-',maintenanceDaysText(i),badge(maintenanceStatus(i)),i.maintenanceNote||'-',``]); let el=document.getElementById('materialMaintenanceTable'); if(el)el.innerHTML=`
Filtreye uyan kayıt: ${rows.length}
`+table(rows,['Malzeme','Tip','Sahibi','Kiraya Veren','Kiralandığı Müşteri','İl','İlçe','Adet','Son Bakım','Sonraki Bakım','Kalan Gün','Bakım Durumu','Açıklama','İşlem']); } function materialMaintenanceForm(id){let i=byId(db.items,id); if(!i.id)return; openModal('Malzeme Bakım Güncelle',`
${esc(i.name||'-')} / ${esc(nameOf(db.owners,i.ownerId))}
Boş bırakırsan ayarlardaki periyoda göre hesaplanır.
`)} function saveMaterialMaintenance(id){let i=byId(db.items,id); if(!i.id)return; let last=formVal('mmLast')||today(); i.lastMaintenanceDate=last; i.nextMaintenanceDate=formVal('mmNext')||addDaysISO(last,materialMaintenancePeriodDays()); i.maintenanceNote=formVal('mmNote'); i.maintenanceEnabled=true; save(); closeModal(); render();} function stockProductName(id){let p=byId(db.stockProducts||[],id); return p.id ? [p.name,p.brand,p.model].filter(Boolean).join(' / ') : '-'} function stockWarehouseName(id){let w=byId(db.stockWarehouses||[],id); return w.id ? w.name : '-'} function stockMovementSign(t){return ['Giriş','Transfer Giriş','Araca Giriş','Emanete Ver','Emanetten İade','Depoya İade'].includes(t)?1:-1} function stockQty(productId,placeType='warehouse',placeId=''){ return (db.stockMovements||[]).filter(m=>m.productId===productId && m.placeType===placeType && (!placeId || m.placeId===placeId)).reduce((s,m)=>s+(stockMovementSign(m.type)*Number(m.qty||0)),0) } function stockWarehouseOptions(sel=''){return ``+(db.stockWarehouses||[]).map(w=>``).join('')} function stockProductOptions(sel=''){return ``+(db.stockProducts||[]).map(p=>``).join('')} function vehicleOptions(sel=''){return ``+(db.vehicles||[]).map(v=>``).join('')} function customerOptions(sel=''){return ``+(db.customers||[]).map(c=>``).join('')} function stockSummary(){ let wh=(db.stockWarehouses||[]), pr=(db.stockProducts||[]), mv=(db.stockMovements||[]); let warehouseQty=pr.reduce((s,p)=>s+stockQty(p.id,'warehouse'),0), vehicleQty=pr.reduce((s,p)=>s+stockQty(p.id,'vehicle'),0), emanetQty=pr.reduce((s,p)=>s+stockQty(p.id,'emanet'),0); return {wh:wh.length, pr:pr.length, warehouseQty, vehicleQty, emanetQty, moves:mv.length}; } function stockEmanetGroups(){ let map={}; (db.stockMovements||[]).filter(m=>m.placeType==='emanet').forEach(m=>{ let key=(m.productId||'')+'|'+(m.placeId||''); if(!map[key])map[key]={productId:m.productId, emanetId:m.placeId, person:m.placeName||m.toName||m.fromName||'-', firstDate:m.date||'', lastDate:m.date||'', given:0, returned:0, note:m.note||''}; let g=map[key]; if(m.type==='Emanete Ver')g.given+=Number(m.qty||0); if(m.type==='Emanet İade Çıkış')g.returned+=Number(m.qty||0); if(String(m.date||'')>String(g.lastDate||''))g.lastDate=m.date||g.lastDate; if(!g.firstDate || String(m.date||''){g.balance=stockQty(g.productId,'emanet',g.emanetId); return g;}).sort((a,b)=>String(b.lastDate||'').localeCompare(String(a.lastDate||''))); } function stockView(){ document.getElementById('topActions').innerHTML=''; let sum=stockSummary(); let whRows=[]; (db.stockWarehouses||[]).forEach(w=>{(db.stockProducts||[]).forEach(p=>{let q=stockQty(p.id,'warehouse',w.id); if(q>0)whRows.push([w.name,stockProductName(p.id),p.unit||'Adet',q,` `]);})}); let vehicleRows=[]; (db.vehicles||[]).forEach(v=>{(db.stockProducts||[]).forEach(p=>{let q=stockQty(p.id,'vehicle',v.id); if(q>0)vehicleRows.push([vehicleName(v.id),stockProductName(p.id),p.unit||'Adet',q,``]);})}); let emanetGroups=stockEmanetGroups(); let emanetRows=emanetGroups.filter(g=>Number(g.balance||0)>0).map(g=>[g.firstDate||'-',g.person||'-',stockProductName(g.productId),g.given||0,g.returned||0,g.balance||0,g.note||'-',``]); let emanetHistoryRows=emanetGroups.filter(g=>Number(g.balance||0)<=0 && Number(g.given||0)>0).map(g=>[g.firstDate||'-',g.lastDate||'-',g.person||'-',stockProductName(g.productId),g.given||0,g.returned||0,badge('Tamamı Geldi'),g.note||'-']); let useRows=(db.stockMovements||[]).filter(m=>m.type==='Müşteriye Kullanıldı').slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))).map(m=>[m.date||'-',nameOf(db.customers,m.customerId),vehicleName(m.placeId),stockProductName(m.productId),m.qty||0,m.crane||'-',m.note||'-']); document.getElementById('view').innerHTML=`
Depo
${sum.wh}
Ürün Çeşidi
${sum.pr}
Depolardaki Adet
${sum.warehouseQty}
Araçlardaki Adet
${sum.vehicleQty}
Emanetteki Adet
${sum.emanetQty}

Stok Ürünleri

${table((db.stockProducts||[]).map(p=>[p.code||'-',p.name||'-',p.brand||'-',p.model||'-',p.unit||'Adet',stockQty(p.id,'warehouse'),stockQty(p.id,'vehicle'),stockQty(p.id,'emanet'),p.minQty||0,p.note||'-',` `]),['Kod','Ürün','Marka','Model','Birim','Depoda','Araçta','Emanette','Min. Stok','Not','İşlem'])}

Depolar

${table((db.stockWarehouses||[]).map(w=>[w.name||'-',w.city||'-',w.district||'-',w.responsible||'-',w.phone||'-',w.note||'-',` `]),['Depo','İl','İlçe','Sorumlu','Telefon','Not','İşlem'])}

Depolardaki Ürünler

${table(whRows,['Depo','Ürün','Birim','Adet','İşlem'])}

Araçlardaki Malzemeler

${table(vehicleRows,['Araç','Ürün','Birim','Adet','İşlem'])}

Müşteriye Kullanılan Malzeme Geçmişi

${table(useRows,['Tarih','Müşteri','Araç','Ürün','Adet','Kule Vinç / İş','Açıklama'])}

Emanet Verilen Malzemeler

Sadece henüz tamamen iade edilmemiş emanetler görünür.
${table(emanetRows,['Veriliş Tarihi','Emanet Verilen Kişi/Firma','Ürün','Verilen Adet','Gelen Adet','Kalan Emanet','Açıklama','İşlem'])}

Emanet Malzeme Geçmişi

Emanetten tamamı gelen malzemeler burada geçmiş olarak saklanır.
${table(emanetHistoryRows,['Veriliş Tarihi','Son İade Tarihi','Emanet Verilen Kişi/Firma','Ürün','Verilen Adet','Gelen Adet','Durum','Açıklama'])}

Stok Hareket Geçmişi

${table((db.stockMovements||[]).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))).map(m=>[m.date||'-',badge(m.type||'-'),stockProductName(m.productId),m.qty||0,m.fromName||'-',m.toName||m.placeName||'-',nameOf(db.customers,m.customerId)||'-',m.note||'-']),['Tarih','İşlem','Ürün','Adet','Çıkış','Giriş / Yer','Müşteri','Açıklama'])}
`; } function stockWarehouseForm(id=''){let w=byId(db.stockWarehouses||[],id); openModal(id?'Depo Düzenle':'Depo Ekle',`
`)} function saveStockWarehouse(id=''){let w=id?byId(db.stockWarehouses,id):{id:uid('SDP')}; if(!formVal('swName')){alert('Depo adı yazın.');return} Object.assign(w,{name:formVal('swName'),city:formVal('swCity'),district:formVal('swDistrict'),responsible:formVal('swResponsible'),phone:formVal('swPhone'),note:formVal('swNote')}); if(!id)db.stockWarehouses.push(w); save(); closeModal(); render();} function deleteStockWarehouse(id){if((db.stockMovements||[]).some(m=>m.placeId===id||m.fromId===id||m.toId===id)){alert('Bu depoda hareket var. Silmek yerine pasif/not olarak takip edin.');return} del('stockWarehouses',id)} function stockProductForm(id=''){let p=byId(db.stockProducts||[],id); openModal(id?'Ürün Düzenle':'Ürün Ekle',`
`)} function saveStockProduct(id=''){let p=id?byId(db.stockProducts,id):{id:uid('STK')}; if(!formVal('spName')){alert('Ürün adı yazın.');return} Object.assign(p,{code:formVal('spCode'),name:formVal('spName'),brand:formVal('spBrand'),model:formVal('spModel'),unit:formVal('spUnit'),minQty:Number(formVal('spMin')||0),note:formVal('spNote')}); if(!id)db.stockProducts.push(p); save(); closeModal(); render();} function deleteStockProduct(id){if((db.stockMovements||[]).some(m=>m.productId===id)){alert('Bu üründe hareket var. Silmek yerine açıklama ile pasif takip edin.');return} del('stockProducts',id)} function addStockMovement(m){db.stockMovements.push(Object.assign({id:uid('SHR'),date:today(),qty:0,note:''},m)); save(); closeModal(); render();} function stockInForm(productId='',warehouseId=''){openModal('Depoya Ürün Girişi',`
`)} function saveStockIn(){let pid=formVal('siProduct'), wid=formVal('siWarehouse'), q=Number(formVal('siQty')||0); if(!pid||!wid||q<=0){alert('Ürün, depo ve adet girin.');return} let w=byId(db.stockWarehouses,wid); addStockMovement({type:'Giriş',date:formVal('siDate'),productId:pid,qty:q,placeType:'warehouse',placeId:wid,placeName:w.name,toId:wid,toName:w.name,note:formVal('siNote')});} function stockTransferForm(productId='',fromId=''){openModal('Depolar Arası Transfer',`
`)} function saveStockTransfer(){let pid=formVal('stProduct'), from=formVal('stFrom'), to=formVal('stTo'), q=Number(formVal('stQty')||0); if(!pid||!from||!to||q<=0){alert('Ürün, depolar ve adet girin.');return} if(from===to){alert('Çıkış ve giriş deposu aynı olamaz.');return} if(stockQty(pid,'warehouse',from)
`)} function saveStockVehicleIssue(){let pid=formVal('svProduct'), wid=formVal('svWarehouse'), vid=formVal('svVehicle'), q=Number(formVal('svQty')||0); if(!pid||!wid||!vid||q<=0){alert('Ürün, depo, araç ve adet girin.');return} if(stockQty(pid,'warehouse',wid)
`)} function saveStockVehicleUse(){let pid=formVal('suProduct'), vid=formVal('suVehicle'), cid=formVal('suCustomer'), q=Number(formVal('suQty')||0); if(!pid||!vid||!cid||q<=0){alert('Ürün, araç, müşteri ve adet girin.');return} if(stockQty(pid,'vehicle',vid)
`)} function saveStockEmanet(){let pid=formVal('seProduct'), wid=formVal('seWarehouse'), person=formVal('sePerson'), q=Number(formVal('seQty')||0); if(!pid||!wid||!person||q<=0){alert('Ürün, depo, emanet kişi/firma ve adet girin.');return} if(stockQty(pid,'warehouse',wid)m.placeType==='emanet'&&m.placeId===emanetId); let name=rows[0]?rows[0].placeName:''; openModal('Emanetten Depoya İade Al',`
`)} function saveStockEmanetReturn(emanetId){let pid=formVal('srProduct'), wid=formVal('srWarehouse'), q=Number(formVal('srQty')||0); if(!pid||!wid||q<=0){alert('Ürün, depo ve adet girin.');return} if(stockQty(pid,'emanet',emanetId)m.placeId===emanetId)?.placeName||emanetId; db.stockMovements.push({id:uid('SHR'),type:'Emanet İade Çıkış',date:formVal('srDate'),productId:pid,qty:q,placeType:'emanet',placeId:emanetId,placeName:person,fromId:emanetId,fromName:person,toId:wid,toName:w.name,note:formVal('srNote')}); db.stockMovements.push({id:uid('SHR'),type:'Emanetten İade',date:formVal('srDate'),productId:pid,qty:q,placeType:'warehouse',placeId:wid,placeName:w.name,fromId:emanetId,fromName:person,toId:wid,toName:w.name,note:formVal('srNote')}); save(); closeModal(); render();} function shipmentStatusBadge(s){return badge(s||'Planlandı')} function shipmentRows(){return (db.shipments||[]).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))||String(b.created||'').localeCompare(String(a.created||'')))} function shipmentLineSummary(sh){let lines=sh.lines||[]; if(!lines.length)return '-'; return lines.map(l=>`${stockProductName(l.productId)} x ${Number(l.qty||0)}`).join('
')} function shipmentTargetName(sh){let t=sh.targetType||''; if(t==='Müşteri / Şantiye')return nameOf(db.customers||[],sh.customerId); if(t==='Araç')return vehicleName(sh.vehicleId); if(t==='Depo')return stockWarehouseName(sh.toWarehouseId); if(t==='Emanet')return sh.emanetPerson||'-'; return sh.address||'-'} function shipmentSummary(){let all=db.shipments||[]; return {total:all.length, planned:all.filter(x=>String(x.status||'Planlandı')==='Planlandı').length, road:all.filter(x=>['Yüklendi','Yolda'].includes(String(x.status||''))).length, delivered:all.filter(x=>String(x.status||'')==='Teslim Edildi').length, today:all.filter(x=>String(x.date||'')===today()).length};} function shipmentLineInputs(lines=[]){let rows=[]; for(let i=0;i
`)} return rows.join('')+``;} function addShipmentLine(){let wrap=document.getElementById('shipmentLinesWrap'); let n=Number(document.getElementById('shLineCount')?.value||1); if(!wrap)return; wrap.insertAdjacentHTML('beforeend',`
`); document.getElementById('shLineCount').value=n+1;} function toggleShipmentTargetFields(){let t=formVal('shTargetType'); ['customer','vehicle','toWarehouse','emanet','address'].forEach(k=>{let el=document.getElementById('sh_'+k); if(el)el.style.display='none'}); if(t==='Müşteri / Şantiye'){document.getElementById('sh_customer').style.display='block'; document.getElementById('sh_address').style.display='block'} if(t==='Araç'){document.getElementById('sh_vehicle').style.display='block'} if(t==='Depo'){document.getElementById('sh_toWarehouse').style.display='block'} if(t==='Emanet'){document.getElementById('sh_emanet').style.display='block'; document.getElementById('sh_address').style.display='block'}} function shipmentsView(){let sum=shipmentSummary(); let rows=shipmentRows(); document.getElementById('view').innerHTML=`

Sevkiyat

Kiralama, stok, depo, araç, müşteri/şantiye ve emanet malzeme sevkiyatlarını tek yerden takip edin.
Toplam Sevkiyat
${sum.total}
Planlanan
${sum.planned}
Yükte / Yolda
${sum.road}
Teslim Edilen
${sum.delivered}
Bugünkü Sevkiyat
${sum.today}

Sevkiyat Listesi

${table(rows.map(x=>[x.date||'-',x.shipmentNo||'-',shipmentStatusBadge(x.status),x.type||'-',stockWarehouseName(x.fromWarehouseId),shipmentTargetName(x),nameOf(db.customers||[],x.customerId)||'-',vehicleName(x.vehicleId)||'-',x.driver||'-',x.city||'-',x.district||'-',shipmentLineSummary(x),` `]),['Tarih','Sevk No','Durum','Tür','Çıkış Deposu','Hedef','Müşteri','Araç','Şoför','İl','İlçe','Malzemeler','İşlem'])}
`;} function shipmentForm(id=''){let x=id?byId(db.shipments||[],id):{id:'',date:today(),status:'Planlandı',type:'Stok Sevkiyatı',targetType:'Müşteri / Şantiye',lines:[{}],stockAffects:true}; openModal(id?'Sevkiyat Düzenle':'Sevkiyat Ekle',`

Sevk Edilen Malzemeler

${shipmentLineInputs(x.lines||[{}])}
Müşteri/şantiye hedefinde müşteri geçmişine “Müşteriye Kullanıldı” olarak işlenir. Araç hedefinde araç stokuna, depo hedefinde giriş deposuna, emanet hedefinde emanet takibine aktarılır.
`; } function openCargoPdf(id){ let x=byId(db.cargos||[],id); if(!x.id){alert('Kargo kaydı bulunamadı');return} let w=window.open('','_blank'); if(!w){alert('PDF ekranı açılamadı. Tarayıcı açılır pencere iznini kontrol edin.');return} w.document.open(); w.document.write(cargoPdfHtml(x)); w.document.close(); } function cargoPdfPlainText(v){ let t=String(v==null?'':v); const map={'ç':'c','Ç':'C','ğ':'g','Ğ':'G','ı':'i','İ':'I','ö':'o','Ö':'O','ş':'s','Ş':'S','ü':'u','Ü':'U','₺':'TL'}; return t.replace(/[çÇğĞıİöÖşŞüÜ₺]/g,ch=>map[ch]||ch).replace(/[^\x20-\x7E]/g,' '); } function cargoPdfEscape(v){return cargoPdfPlainText(v).replace(/\\/g,'\\\\').replace(/\(/g,'\\(').replace(/\)/g,'\\)');} function cargoPdfBlob(x){ let lines=[ 'SISTEM KULE VINC - KARGO ICERIGI PDF', 'Cikti Tarihi: '+formatDateTR(today()), '----------------------------------------', 'Kargo Firmasi: '+(x.company||'-'), 'Takip No: '+(x.trackingNo||'-'), 'Durum: '+(x.status||'-'), 'Gonderi Tarihi: '+(formatDateTR(x.sendDate)||x.sendDate||'-'), 'Tahmini Teslim: '+(formatDateTR(x.estimatedDate)||x.estimatedDate||'-'), 'Teslim Tarihi: '+(formatDateTR(x.deliveryDate)||x.deliveryDate||'-'), 'Gonderici: '+(x.sender||'-'), 'Alici Firma: '+(x.receiverCompany||'-'), 'Alici: '+(x.receiverName||'-'), 'Il / Ilce: '+([x.city,x.district].filter(Boolean).join(' / ')||'-'), 'Adres: '+(x.address||'-'), 'Kargo Icerigi: '+(x.content||'-'), 'Adet / Paket: '+(x.qty||1), 'Kargo Ucreti: '+money(x.amount), 'Odeme Tipi: '+(x.paymentType||'-'), 'Aciklama / Not: '+(x.note||'-'), '----------------------------------------', 'Bu belge kargo icerigi ve teslimat takibi icin hazirlanmistir.', '', 'Hazirlayan / Gonderen Imza: ____________________', 'Teslim Alan Imza: ____________________' ].map(cargoPdfPlainText); let content='BT\n/F1 11 Tf\n50 800 Td\n14 TL\n'+lines.slice(0,48).map(l=>'('+cargoPdfEscape(l).slice(0,110)+') Tj').join(' T*\n')+'\nET'; let objs=[]; objs.push('1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj'); objs.push('2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj'); objs.push('3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> endobj'); objs.push('4 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj'); objs.push('5 0 obj << /Length '+content.length+' >> stream\n'+content+'\nendstream endobj'); let pdf='%PDF-1.3\n'; let offsets=[0]; for(let o of objs){offsets.push(pdf.length); pdf+=o+'\n';} let xref=pdf.length; pdf+='xref\n0 '+(objs.length+1)+'\n0000000000 65535 f \n'; for(let i=1;i>\nstartxref\n'+xref+'\n%%EOF'; return new Blob([pdf],{type:'application/pdf'}); } async function sendCargoPdfWhatsApp(id){ let x=byId(db.cargos||[],id); if(!x.id){alert('Kargo kaydı bulunamadı');return} let filename='kargo-icerigi-'+String(x.trackingNo||x.id||'kayit').replace(/[^a-zA-Z0-9ğüşöçıİĞÜŞÖÇ_-]+/g,'_')+'.pdf'; let blob=cargoPdfBlob(x); let msg='Kargo içeriği PDF: '+(x.receiverCompany||x.receiverName||'Alıcı')+' - '+(x.company||'Kargo')+' - Takip No: '+(x.trackingNo||'-'); let phone=x.receiverPhone||x.phone||''; if(!phone){alert('Bu kargo kaydında Alıcı WhatsApp Telefonu yok. Kargo Düzenle içinden telefon ekleyin.');return} if(!whatsappApiReady()&&!whatsappApiSettings().testMode){alert('WhatsApp API ayarları eksik. Ayarlar > WhatsApp bölümünden API Ayarları kısmına Phone Number ID ve Access Token giriniz.'); whatsappApiSettingsForm(); return} try{ let res=await whatsappApiSendDocument(phone,blob,filename,msg); db.whatsappMessages=db.whatsappMessages||[]; db.whatsappMessages.push({id:uid('WA'),date:today(),created:new Date().toISOString(),name:x.receiverCompany||x.receiverName||'',phone:normalizeWhatsappPhone(phone),message:msg,source:'cargo:'+x.id,sourceType:'Kargo PDF',api:true,status:'Gönderildi',apiResult:res&&res.messages?JSON.stringify(res.messages):JSON.stringify(res||{})}); save(); render(); alert('Kargo PDF WhatsApp API ile gönderildi.'); }catch(e){alert('WhatsApp API gönderim hatası: '+e.message)} } function deleteCargo(id){if(requireDeletePassword('Kargo kaydı silinsin mi?')){db.cargos=(db.cargos||[]).filter(x=>x.id!==id); save(); render();}} function normalizeWhatsappPhone(v){ let n=String(v||'').replace(/[^0-9]/g,''); if(!n)return ''; if(n.startsWith('00'))n=n.slice(2); if(n.startsWith('0'))n='90'+n.slice(1); if(n.length===10)n='90'+n; return n; } function whatsappUrl(phone,msg){ let n=normalizeWhatsappPhone(phone); let base=n?'https://wa.me/'+n:'https://wa.me/'; let q=String(msg||'').trim(); return q?base+'?text='+encodeURIComponent(q):base; } function openWhatsappWeb(){window.open('https://web.whatsapp.com/','_blank')} function whatsappDefaultMessage(){return 'Merhaba, Sistem Kule Vinç tarafından bilgilendirme mesajıdır.'} function whatsappApiSettings(){ if(!db.settings)db.settings={}; if(!db.settings.whatsappApi)db.settings.whatsappApi={enabled:false,apiVersion:'v23.0',phoneNumberId:'',accessToken:'',businessNumber:'',testMode:false}; db.settings.whatsappApi.apiVersion=db.settings.whatsappApi.apiVersion||'v23.0'; return db.settings.whatsappApi; } function whatsappApiReady(){let c=whatsappApiSettings(); return !!(c.enabled&&c.phoneNumberId&&c.accessToken);} function whatsappApiEndpoint(path){let c=whatsappApiSettings(); return 'https://graph.facebook.com/'+encodeURIComponent(c.apiVersion||'v23.0')+'/'+String(path||'').replace(/^\/+/, '');} function whatsappApiHelpText(){return 'WhatsApp API ile otomatik gönderim için Meta WhatsApp Business Cloud API bilgileri gerekir: Phone Number ID ve kalıcı erişim tokeni. Kişisel WhatsApp hesabı API ile otomatik gönderim yapamaz.'} function whatsappApiSettingsForm(){let c=whatsappApiSettings(); openModal('WhatsApp API Ayarları',`
Direkt otomatik gönderim modu. WhatsApp Web kullanılmaz. Meta WhatsApp Business Cloud API üzerinden gönderim yapılır. Phone Number ID ve Access Token girilmelidir.
`)} function saveWhatsappApiSettings(){let c=whatsappApiSettings(); c.enabled=formVal('waApiEnabled')==='true'; c.apiVersion=formVal('waApiVersion')||'v23.0'; c.phoneNumberId=formVal('waPhoneNumberId'); c.accessToken=formVal('waAccessToken'); c.businessNumber=formVal('waBusinessNumber'); c.testMode=formVal('waApiTestMode')==='true'; save(); closeModal(); render(); alert('WhatsApp API ayarları kaydedildi.');} async function testWhatsappApiConnection(){let c={apiVersion:formVal('waApiVersion')||'v23.0',phoneNumberId:formVal('waPhoneNumberId'),accessToken:formVal('waAccessToken')}; if(!c.phoneNumberId||!c.accessToken){alert('Phone Number ID ve Access Token giriniz.');return} try{let r=await fetch('https://graph.facebook.com/'+encodeURIComponent(c.apiVersion)+'/'+encodeURIComponent(c.phoneNumberId),{headers:{Authorization:'Bearer '+c.accessToken}}); let t=await r.text(); if(r.ok)alert('WhatsApp API bağlantısı başarılı.'); else alert('API bağlantı hatası: '+t);}catch(e){alert('API test edilemedi: '+e.message)} } async function whatsappApiSendText(phone,msg,meta={}){let c=whatsappApiSettings(); let to=normalizeWhatsappPhone(phone); if(!to)throw new Error('Alıcı WhatsApp telefonu yok.'); if(!msg)throw new Error('Mesaj boş.'); if(c.testMode)return {test:true,id:'TEST-'+Date.now()}; let payload={messaging_product:'whatsapp',recipient_type:'individual',to,type:'text',text:{preview_url:false,body:msg}}; let r=await fetch(whatsappApiEndpoint(c.phoneNumberId+'/messages'),{method:'POST',headers:{Authorization:'Bearer '+c.accessToken,'Content-Type':'application/json'},body:JSON.stringify(payload)}); let data=await r.json().catch(()=>({})); if(!r.ok)throw new Error(data.error?data.error.message:('HTTP '+r.status)); return data;} async function whatsappApiUploadMedia(blob,filename){let c=whatsappApiSettings(); if(c.testMode)return {id:'TEST-MEDIA-'+Date.now()}; let fd=new FormData(); fd.append('messaging_product','whatsapp'); fd.append('type','application/pdf'); fd.append('file',blob,filename); let r=await fetch(whatsappApiEndpoint(c.phoneNumberId+'/media'),{method:'POST',headers:{Authorization:'Bearer '+c.accessToken},body:fd}); let data=await r.json().catch(()=>({})); if(!r.ok)throw new Error(data.error?data.error.message:('Medya yükleme hatası HTTP '+r.status)); return data;} async function whatsappApiSendDocument(phone,blob,filename,caption){let c=whatsappApiSettings(); let to=normalizeWhatsappPhone(phone); if(!to)throw new Error('Alıcı WhatsApp telefonu yok.'); if(!whatsappApiReady()&&!c.testMode)throw new Error('WhatsApp API ayarları eksik.'); let media=await whatsappApiUploadMedia(blob,filename); if(c.testMode)return {test:true,id:'TEST-DOC-'+Date.now(),media}; let payload={messaging_product:'whatsapp',recipient_type:'individual',to,type:'document',document:{id:media.id,filename:filename,caption:caption||''}}; let r=await fetch(whatsappApiEndpoint(c.phoneNumberId+'/messages'),{method:'POST',headers:{Authorization:'Bearer '+c.accessToken,'Content-Type':'application/json'},body:JSON.stringify(payload)}); let data=await r.json().catch(()=>({})); if(!r.ok)throw new Error(data.error?data.error.message:('Belge gönderim hatası HTTP '+r.status)); return data;} function whatsappSourceOptions(sel=''){let groups=[]; (db.customers||[]).forEach(c=>groups.push({v:'customer:'+c.id,t:'Müşteri - '+(c.name||c.company||c.id)})); (db.cargos||[]).forEach(k=>groups.push({v:'cargo:'+k.id,t:'Kargo - '+(k.receiverCompany||k.receiverName||k.trackingNo||k.id)})); (db.shipments||[]).forEach(sh=>groups.push({v:'shipment:'+sh.id,t:'Sevkiyat - '+(sh.shipmentNo||shipmentTargetName(sh).replace(/<[^>]*>/g,'')||sh.id)})); return ''+groups.map(g=>``).join(''); } function whatsappApplySource(){ let src=formVal('waSource'); if(!src){document.getElementById('waMessage').value=whatsappDefaultMessage(); return;} let [type,id]=src.split(':'); let phone='',name='',msg=''; if(type==='customer'){ let c=byId(db.customers||[],id)||{}; name=c.name||c.company||''; phone=c.phone||c.mobile||c.gsm||''; msg=`Merhaba ${name||''}, Sistem Kule Vinç tarafından bilgilendirme mesajıdır.`; } else if(type==='cargo'){ let k=byId(db.cargos||[],id)||{}; name=k.receiverName||k.receiverCompany||''; phone=k.receiverPhone||k.phone||''; msg=`Merhaba ${name||''}, kargo bilginiz:\nKargo Firması: ${k.company||'-'}\nTakip No: ${k.trackingNo||'-'}\nDurum: ${k.status||'-'}\nİçerik: ${k.content||'-'}`; } else if(type==='shipment'){ let sh=byId(db.shipments||[],id)||{}; name=sh.receiver||shipmentTargetName(sh).replace(/<[^>]*>/g,'')||''; phone=sh.receiverPhone||''; msg=`Merhaba ${name||''}, sevkiyat bilginiz:\nSevk No: ${sh.shipmentNo||'-'}\nTarih: ${formatDateTR(sh.date)||sh.date||'-'}\nDurum: ${sh.status||'-'}\nMalzemeler: ${shipmentLineSummary(sh).replace(/<[^>]*>/g,'')||'-'}`; } document.getElementById('waName').value=name||''; if(phone)document.getElementById('waPhone').value=phone; document.getElementById('waMessage').value=msg; } function whatsappView(){ const rows=(db.whatsappMessages||[]).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))||String(b.created||'').localeCompare(String(a.created||''))); document.getElementById('topActions').innerHTML=' '; document.getElementById('view').innerHTML=`
Toplam WhatsApp Kaydı
${rows.length}
Bugün Gönderilen
${rows.filter(x=>x.date===today()).length}
Kargo Mesajı
${rows.filter(x=>x.sourceType==='Kargo').length}
Sevkiyat Mesajı
${rows.filter(x=>x.sourceType==='Sevkiyat').length}

WhatsApp API Modülü

Direkt gönderim modu: WhatsApp Web kullanılmaz. Mesaj ve PDF gönderimleri Meta WhatsApp Business Cloud API üzerinden otomatik gönderilir. Kişisel WhatsApp API ile kullanılamaz; WhatsApp Business API bilgileri gerekir.

WhatsApp Gönderim Geçmişi

${table(rows.map(x=>[x.date||'-',x.name||'-',x.phone||'-',x.sourceType||'Serbest',shortDashboardText(x.message||''),` `]),['Tarih','Alıcı','Telefon','Kaynak','Mesaj','İşlem'])}
`; } function whatsappSendForm(){ openModal('WhatsApp API ile Mesaj Gönder',`
Telefon numarasını 05xx formatında yazabilirsiniz. Program Türkiye kodunu otomatik ekler. Gönderim WhatsApp Business API üzerinden direkt yapılır.
`); } async function saveAndOpenWhatsapp(){ let phone=formVal('waPhone'), msg=formVal('waMessage'), name=formVal('waName'); if(!phone){alert('WhatsApp telefon numarası yazınız.');return} if(!msg){alert('Mesaj yazınız.');return} if(!whatsappApiReady()&&!whatsappApiSettings().testMode){alert('WhatsApp API ayarları eksik. Phone Number ID ve Access Token giriniz.'); whatsappApiSettingsForm(); return} let src=formVal('waSource'), sourceType='Serbest'; if(src.startsWith('cargo:'))sourceType='Kargo'; else if(src.startsWith('shipment:'))sourceType='Sevkiyat'; else if(src.startsWith('customer:'))sourceType='Müşteri'; try{ let res=await whatsappApiSendText(phone,msg,{source:src}); db.whatsappMessages=db.whatsappMessages||[]; db.whatsappMessages.push({id:uid('WA'),date:today(),created:new Date().toISOString(),name,phone:normalizeWhatsappPhone(phone),message:msg,source:src,sourceType,api:true,status:'Gönderildi',apiResult:res&&res.messages?JSON.stringify(res.messages):JSON.stringify(res||{})}); save(); closeModal(); render(); alert('WhatsApp mesajı API ile direkt gönderildi.'); }catch(e){alert('WhatsApp API gönderim hatası: '+e.message)} } async function resendWhatsapp(id){let x=byId(db.whatsappMessages||[],id); if(!x.id)return; try{let res=await whatsappApiSendText(x.phone,x.message); x.date=today(); x.created=new Date().toISOString(); x.status='Tekrar Gönderildi'; x.apiResult=res&&res.messages?JSON.stringify(res.messages):JSON.stringify(res||{}); save(); render(); alert('WhatsApp mesajı API ile tekrar gönderildi.')}catch(e){alert('WhatsApp API gönderim hatası: '+e.message)}} function deleteWhatsappLog(id){if(requireDeletePassword('WhatsApp geçmiş kaydı silinsin mi?')){db.whatsappMessages=(db.whatsappMessages||[]).filter(x=>x.id!==id); save(); render();}} function settingsWhatsAppPanel(){ const rows=(db.whatsappMessages||[]).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))||String(b.created||'').localeCompare(String(a.created||''))); const st=whatsappApiSettings(); return `
Toplam WhatsApp Kaydı
${rows.length}
Bugün Gönderilen
${rows.filter(x=>x.date===today()).length}
API Durumu
${st.enabled?'Aktif':'Pasif'}
Test Modu
${st.testMode?'Açık':'Kapalı'}

WhatsApp API Paneli

WhatsApp modülü artık Ayarlar içindedir. WhatsApp Web kullanılmaz. Mesaj ve PDF gönderimleri Meta WhatsApp Business Cloud API üzerinden yapılır.

WhatsApp Gönderim Geçmişi

${table(rows.map(x=>[x.date||'-',x.name||'-',x.phone||'-',x.sourceType||'Serbest',shortDashboardText(x.message||''),` `]),['Tarih','Alıcı','Telefon','Kaynak','Mesaj','İşlem'])}
`; } function realEstateStatusBadge(s){return badge(s||'Boş')} function realEstateFilteredRows(){ let q=String(localStorage.getItem('klm_realestate_q')||'').toLocaleLowerCase('tr-TR'); let type=localStorage.getItem('klm_realestate_type')||''; let status=localStorage.getItem('klm_realestate_status')||''; return (db.realEstates||[]).filter(x=>{ let hay=[x.name,x.type,x.city,x.district,x.address,x.tenant,x.owner,x.block,x.parcel,x.deedNo,x.note].join(' ').toLocaleLowerCase('tr-TR'); return (!q||hay.includes(q)) && (!type||x.type===type) && (!status||x.status===status); }).sort((a,b)=>String(a.city||'').localeCompare(String(b.city||''),'tr')||String(a.name||'').localeCompare(String(b.name||''),'tr')); } function realEstateSummary(){ let all=db.realEstates||[]; let totalValue=all.reduce((s,x)=>s+Number(x.value||0),0); let totalRent=all.reduce((s,x)=>s+Number(x.monthlyRent||0),0); let rented=all.filter(x=>String(x.status||'')==='Kirada').length; let empty=all.filter(x=>String(x.status||'Boş')==='Boş').length; return {count:all.length,totalValue,totalRent,rented,empty}; } function realEstatesView(){ const sum=realEstateSummary(); const rows=realEstateFilteredRows(); const types=[...new Set((db.realEstates||[]).map(x=>x.type).filter(Boolean))].sort((a,b)=>a.localeCompare(b,'tr')); const statuses=['Boş','Kirada','Kendi Kullanımımızda','Satılık','Bakımda / Tadilatta','Devredildi']; document.getElementById('view').innerHTML=`

Taşınmazlarım

Arsa, daire, dükkan, ofis, depo ve diğer taşınmazlarınızı tek bölümde takip edin.
Toplam Taşınmaz
${sum.count}
Kiradaki
${sum.rented}
Boş
${sum.empty}
Aylık Kira Toplamı
${money(sum.totalRent)}
Tahmini Değer
${money(sum.totalValue)}

Filtrele

Taşınmaz Listesi

${table(rows.map(x=>[x.type||'-',x.name||'-',realEstateStatusBadge(x.status),[x.city,x.district].filter(Boolean).join(' / ')||'-',x.address||'-',x.block||'-',x.parcel||'-',x.deedNo||'-',x.tenant||'-',money(x.monthlyRent),money(x.value),x.rentStart||'-',x.rentEnd||'-',` `]),['Tür','Adı','Durum','İl / İlçe','Adres','Ada','Parsel','Tapu No','Kiracı','Aylık Kira','Tahmini Değer','Kira Başlangıç','Kira Bitiş','İşlem'])}
`; } function realEstateForm(id=''){ let x=id?byId(db.realEstates||[],id):{id:'',type:'Daire',status:'Boş'}; const statuses=['Boş','Kirada','Kendi Kullanımımızda','Satılık','Bakımda / Tadilatta','Devredildi']; const types=['Daire','Arsa','Dükkan','Ofis','Depo','Tarla','Villa','Fabrika','Diğer']; openModal(id?'Taşınmaz Düzenle':'Taşınmaz Ekle',`
`); } function saveRealEstate(id=''){ if(!formVal('reName')){alert('Taşınmaz adı yazınız.');return} let x=id?byId(db.realEstates||[],id):{id:uid('TAS')}; Object.assign(x,{type:formVal('reType'),name:formVal('reName'),status:formVal('reStatus')||'Boş',city:formVal('reCity'),district:formVal('reDistrict'),address:formVal('reAddress'),block:formVal('reBlock'),parcel:formVal('reParcel'),deedNo:formVal('reDeedNo'),owner:formVal('reOwner'),purchaseDate:formVal('rePurchaseDate'),purchasePrice:Number(formVal('rePurchasePrice')||0),value:Number(formVal('reValue')||0),tenant:formVal('reTenant'),monthlyRent:Number(formVal('reMonthlyRent')||0),rentStart:formVal('reRentStart'),rentEnd:formVal('reRentEnd'),expense:Number(formVal('reExpense')||0),insuranceEnd:formVal('reInsuranceEnd'),note:formVal('reNote'),updated:today()}); if(!id)db.realEstates.push(x); save(); closeModal(); render(); } function realEstateDetail(id){ let x=byId(db.realEstates||[],id); if(!x.id)return; openModal('Taşınmaz Detayı',`

${esc(x.name||'-')}

${table([ ['Tür',x.type||'-'],['Durum',realEstateStatusBadge(x.status)],['İl / İlçe',[x.city,x.district].filter(Boolean).join(' / ')||'-'],['Adres',x.address||'-'],['Ada / Parsel',[x.block,x.parcel].filter(Boolean).join(' / ')||'-'],['Tapu No',x.deedNo||'-'],['Sahibi',x.owner||'-'],['Alış Tarihi',x.purchaseDate||'-'],['Alış Bedeli',money(x.purchasePrice)],['Tahmini Değer',money(x.value)],['Kiracı',x.tenant||'-'],['Aylık Kira',money(x.monthlyRent)],['Kira Tarihleri',[x.rentStart,x.rentEnd].filter(Boolean).join(' / ')||'-'],['Vergi / Aidat',money(x.expense)],['Sigorta Bitiş',x.insuranceEnd||'-'],['Not',x.note||'-'] ],['Alan','Bilgi'])}
`); } function deleteRealEstate(id){ if(!requireDeletePassword('Bu taşınmaz kaydı silinsin mi?'))return; db.realEstates=(db.realEstates||[]).filter(x=>x.id!==id); save(); render(); } function reportsView(){let ownerRows=db.owners.map(o=>{let its=db.items.filter(i=>i.ownerId===o.id);return [o.name,sumQty(its),its.reduce((s,i)=>s+rentedQty(i.id),0),its.reduce((s,i)=>s+availableQty(i),0),sumQty(its.filter(i=>i.status==='İade Edildi'))]});let unpaid=db.rentMonths.filter(m=>m.invoiceStatus!=='Kesildi');let invoiced=db.rentMonths.filter(m=>m.invoiceStatus==='Kesildi');let income=db.rentMonths.reduce((s,m)=>s+Number(m.amount||0),0);document.getElementById('view').innerHTML=`
Toplam Oluşan Kira
${money(income)}
Fatura Edilmeyen Ay
${unpaid.length}
Fatura Edilen Ay
${invoiced.length}
Toplam Malzeme Adedi
${sumQty(db.items)}

Malzeme Sahibi + Malzeme Grubuna Göre Adet Raporu

${table(groupQtyByOwnerMaterial(),['Malzeme Sahibi','Malzeme','Toplam Adet','Kirada','Depoda'])}

Malzeme Adına Göre Gruplu Adet Raporu

${table(groupQtyBy('name'),['Malzeme Grubu','Toplam Adet','Kirada','Depoda','Bakımda','İade'])}

Malzeme Tipine Göre Gruplu Adet Raporu

${table(groupQtyBy('type'),['Malzeme Tipi','Toplam Adet','Kirada','Depoda','Bakımda','İade'])}

Firma Bazlı Malzeme Raporu

${table(ownerRows,['Firma','Toplam','Kirada','Depoda','İade'])}

Fatura Edilmeyen Aylar

${table(unpaid.map(m=>[m.month,periodText(m),m.days||30,nameOf(db.customers,byId(db.rentals,m.rentalId).customerId),nameOf(db.items,m.itemId),money(m.amount)]),['Ay','Dönem','Gün','Müşteri','Malzeme','Tutar'])}

Fatura Edilen Aylar

${table(invoiced.map(m=>[m.month,periodText(m),m.days||30,nameOf(db.customers,byId(db.rentals,m.rentalId).customerId),nameOf(db.items,m.itemId),money(m.amount),m.invoiceNo||'-',m.invoiceDate||'-']),['Ay','Dönem','Gün','Müşteri','Malzeme','Tutar','Fatura No','Fatura Tarihi'])}

Yatırımcı Faturaları

${table(ownerMonthlyInvoiceGroups().map(g=>[nameOf(db.owners,g.ownerId),monthTitle(g.month),ownerInvoiceShownNo(g.meta)||'',ownerInvoiceShownDate(g.meta)||'',money(g.displayAmount),ownerInvoiceStatusBadge(g.meta),g.meta.note||'-']),['Malzeme Sahibi','Ay','Fatura No','Fatura Tarihi','Tutar','Durum','Açıklama'])}

Sahibine İade Geçmişi

${table((db.ownerReturns||[]).slice().sort((a,b)=>(b.date||'').localeCompare(a.date||'')).map(r=>[r.date||'-',r.ownerName||nameOf(db.owners,r.ownerId),r.itemCode||'-',r.itemName||'-',r.qty||0,r.returnedLocation||'İade Deposu',r.returnedCity||'-',r.returnedDistrict||'-',r.note||'-']),['İade Tarihi','Malzeme Sahibi','Kod','Malzeme','İade Adedi','İade Deposu','İl','İlçe','Açıklama'])}

Sahibinden Tekrar Depoya Alma Geçmişi

${table((db.ownerTakeBacks||[]).slice().sort((a,b)=>(b.date||'').localeCompare(a.date||'')).map(r=>[r.date||'-',r.ownerName||nameOf(db.owners,r.ownerId),r.itemCode||'-',r.itemName||'-',r.qty||0,r.location||'-',r.note||'-']),['Alış Tarihi','Malzeme Sahibi','Kod','Malzeme','Adet','Depo/Konum','Açıklama'])}
`} function enc(v){return encodeURIComponent(String(v||''))} function dec(v){return decodeURIComponent(String(v||''))} function settingUsed(kind,val){let v=String(val||'').toLocaleLowerCase('tr-TR'); if(kind==='type')return (db.items||[]).some(i=>String(i.type||'').toLocaleLowerCase('tr-TR')===v); if(kind==='itemName')return (db.items||[]).some(i=>String(i.name||'').toLocaleLowerCase('tr-TR')===v); if(kind==='depot')return (db.items||[]).some(i=>String(i.location||'').toLocaleLowerCase('tr-TR')===v||String(i.entryLocation||'').toLocaleLowerCase('tr-TR')===v||String(i.lastDepotEntryLocation||'').toLocaleLowerCase('tr-TR')===v)||(db.moves||[]).some(i=>String(i.location||'').toLocaleLowerCase('tr-TR')===v)||(db.ownerReturns||[]).some(i=>String(i.remainingLocation||'').toLocaleLowerCase('tr-TR')===v); return false;} function programSettingsPanel(){ const p=programSettings(); return `

Program Ayarları

Program başlığı, firma adı ve logo buradan değişir. Kaydedilen bilgiler ana sayfa, sol menü ve teklif PDF çıktısında kullanılır.
Bu seçilen kiraya veren, programın kendi fatura keseceği firma olarak kabul edilir.
PNG, JPG veya WEBP yükleyebilirsin. Çok büyük dosya eklememek için 3.5 MB altı önerilir.
${p.logo&&p.logo.data?`Logo`:'
Logo yok. Varsayılan vinç ikonu kullanılıyor.
'}
${p.logo&&p.logo.data?'':''}
`; } async function saveProgramSettings(){ const p=programSettings(); const file=await fileToDataUrl('psLogo'); if(file==='__too_big__')return; db.settings.program={ companyName:formVal('psCompany')||'SİSTEM KULE VİNÇ', appTitle:formVal('psTitle')||'Emanet Kira Takip', subtitle:formVal('psSubtitle')||'', dashboardSubtitle:formVal('psDashSubtitle')||'', logo:file||p.logo||null }; db.settings.invoiceLessor=formVal('psInvoiceLessor')||db.settings.program.companyName; addLessorToSettings(db.settings.program.companyName,false); addLessorToSettings(db.settings.invoiceLessor,false); save(); render(); alert('Program ayarları kaydedildi.'); } function removeProgramLogo(){ if(requireDeletePassword('Program logosu silinsin mi?')){let p=programSettings(); db.settings.program={...p,logo:null}; save(); render();} } function materialMaintenanceSettingsPanel(){return `

Malzeme Bakım Ayarları

Stok / Malzeme Giriş’te bakım tarihi girilen takip edilebilir malzemelerin sonraki bakım tarihi bu periyoda göre hesaplanır. Kiralanabilir, teknik servis ve Vinç Sahibi Tarafından Kiralandı bakım takibinde görünür; Kiralanamaz ve Yükseltme Ekipman görünmez.
`} function saveMaterialMaintenanceSettings(){if(!db.settings)db.settings={}; db.settings.materialMaintenancePeriodDays=Math.max(1,Number(formVal('materialMaintenancePeriodDays')||30)); (db.items||[]).forEach(i=>{if((!i.nextMaintenanceDate)&&i.lastMaintenanceDate)i.nextMaintenanceDate=addDaysISO(i.lastMaintenanceDate,db.settings.materialMaintenancePeriodDays)}); if(save()){alert('Bakım ayarı kaydedildi.'); render();}} function settingTab(){return window._settingsTab||'program'} function setSettingTab(t){window._settingsTab=t; settingsView()} function settingsTabButton(key,label,icon){return ``} function settingsIntro(types,itemNames,depots,lessors){return `

Program Ayarları

Ayarlar artık bölümlere ayrıldı. İhtiyacın olan ayarı üstteki sekmeden seçerek daha kolay yönetebilirsin.

${types.length}Tip
${itemNames.length}Malzeme Adı
${depots.length}Depo
${lessors.length}Kiraya Veren
${settingsTabButton('program','Program','🏢')}${settingsTabButton('rental','Kiralama','📄')}${settingsTabButton('material','Malzeme / Depo','📦')}${settingsTabButton('maintenance','Bakım','🛠️')}${settingsTabButton('sms','SMS Paneli','📲')}${settingsTabButton('whatsapp','WhatsApp','💬')}${settingsTabButton('backup','Yedekleme','💾')}${settingsTabButton('security','Güvenlik','🔐')}
`} function settingsRentalPanel(lessors){return `

Kiraya Veren Listesi

Stok / Malzeme Giriş’te seçilir
Stok / Malzeme Giriş ekranındaki Kiraya Veren seçeneklerini buradan ekleyebilirsin. Aylık Kira/Fatura oluşturacak firma Program Ayarları bölümünden seçilir.

Kayıtlı Kiraya Verenler

${table(lessors.map(n=>[n,String(n)===String(invoiceLessorName())?badge('Aylık Fatura Oluşturur'):badge('Fatura Oluşturmaz'),` `]),['Kiraya Veren','Fatura Kuralı','İşlem'])}
`} function settingsMaterialPanel(types,itemNames,depots){return `
🏷️

Tip Listesi

Stok / Malzeme Giriş’te seçilecek tipleri yönet.

📦

Malzeme Adı Listesi

Stok / Malzeme Giriş’te seçilecek malzeme adlarını yönet.

🏬

Depo Listesi

Depo ve konum kayıtlarını düzenle.

Ayarlar güvenliği: Tip Listesi artık kilitli değildir; eklenebilir, düzenlenebilir ve uygunsa silinebilir. Malzeme Adı ve Depo kayıtlarında kullanılan kayıtlar silinmez. Silme işlemlerinde artık şifre sorulmaz; yalnızca onay alınır. Düzenle ile isim değiştirildiğinde bağlı kayıtlar da yeni isimle güncellenir.

Tip Listesi

Stok / Malzeme Giriş ekranındaki Tip alanında sadece buraya eklediğin tipler seçilir; elle yazılamaz.

Malzeme Adı Listesi

Stok / Malzeme Giriş ekranındaki Malzeme Adı seçeneklerinde sadece buraya eklediğin adlar görünür.

Depo Listesi

Stok / Malzeme Giriş ve depoya alma işlemlerindeki Depo/Konum seçeneklerinde sadece buraya eklediğin depolar görünür.

Kayıtlı Depolar

Kiralama Deposu, İade Deposu, Emanet Deposu Sahada ve Şantiye Sahasında Beklemekte sabittir; silinemez. Düzenle ile adları değiştirilebilir ve bağlı kayıtlar güncellenir.
${table(depots.map(d=>[d,isFixedDepot(d)?badge('Sabit'):settingUsed('depot',d)?badge('Kullanılıyor'):badge('Boşta'),isFixedDepot(d)?` ${badge('Silinemez')}`:` `]),['Depo / Konum','Durum','İşlem'])}

Kayıtlı Tipler

${table(types.map(t=>[t,settingUsed('type',t)?badge('Kullanılıyor'):badge('Boşta'),` `]),['Tip','Durum','İşlem'])}

Kayıtlı Malzeme Adları

${table(itemNames.map(n=>[n,settingUsed('itemName',n)?badge('Kullanılıyor'):badge('Boşta'),` `]),['Malzeme Adı','Durum','İşlem'])}
${towerSettingsPanel()}`} function settingsSecurityPanel(){return `

Güvenlik ve Kullanıcı Girişi

Programa giriş artık kullanıcı adı ve şifre ile yapılır. Silme işlemlerinde şifre sorulmaz; işlem öncesi onay alınır.
🔐

Kullanıcı Girişi Aktif

Giriş yapılmadan program ekranları açılmaz.

👥

Yetki Yönetimi

Her kullanıcı için bölüm ve işlem bazlı yetki verebilirsin.

💾

Yedekleme

Yedekleme işlemleri Ayarlar > Yedekleme sekmesinden yapılır.

${userManagementHtml()}`} function settingsView(){ensureDefaultMaterialTypes(); ensureFixedDepots(); let types=materialTypeList();let itemNames=(db.settings&&db.settings.itemNames?db.settings.itemNames:[]).slice().sort((a,b)=>a.localeCompare(b,'tr'));let depots=(db.settings&&db.settings.depots?db.settings.depots:[]).slice().sort((a,b)=>a.localeCompare(b,'tr'));let lessors=lessorList();let body=settingsIntro(types,itemNames,depots,lessors);let tab=settingTab(); if(tab==='program')body+=programSettingsPanel(); else if(tab==='rental')body+=settingsRentalPanel(lessors); else if(tab==='material')body+=settingsMaterialPanel(types,itemNames,depots); else if(tab==='maintenance')body+=materialMaintenanceSettingsPanel(); else if(tab==='sms')body+=settingsSmsPanel(); else if(tab==='whatsapp')body+=settingsWhatsAppPanel(); else if(tab==='backup')body+=canPerm('settings','backup')?settingsBackupPanel():`

Yedekleme

Yedekleme işlemleri için yetkiniz yok.
`; else if(tab==='security')body+=canPerm('settings','manageUsers')?settingsSecurityPanel():`

Güvenlik

Kullanıcı ve yetki yönetimi için yetkiniz yok.
`; document.getElementById('view').innerHTML=body} function addSettingLessor(){let n=cleanLessorName(formVal('settingLessorName')); if(!n){alert('Kiraya veren adı yazın');return} addLessorToSettings(n,true); render();} function editSettingLessor(v){let old=dec(v); openModal('Kiraya Veren Düzenle',`
Bu ad kullanılıyorsa stok / malzeme kayıtlarındaki kiraya veren adı da güncellenir.
`)} function saveSettingLessorEdit(v){let old=dec(v), neu=cleanLessorName(formVal('editSettingLessorName')); if(!neu){alert('Kiraya veren adı yazın');return} db.settings.lessors=(db.settings.lessors||[]).map(x=>String(x).toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR')?neu:x); db.settings.lessors=[...new Set(db.settings.lessors)]; (db.items||[]).forEach(i=>{if(String(itemLessor(i)||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR'))i.lessor=neu}); if(String(db.settings.invoiceLessor||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR'))db.settings.invoiceLessor=neu; save(); closeModal(); render()} function deleteSettingLessor(v){let n=dec(v); if(String(n).toLocaleLowerCase('tr-TR')===String(invoiceLessorName()).toLocaleLowerCase('tr-TR')){alert('Aylık Kira/Fatura oluşturacak kiraya veren silinemez. Önce Program Ayarları bölümünden başka bir kiraya veren seçin.');return} if((db.items||[]).some(i=>String(itemLessor(i)||'').toLocaleLowerCase('tr-TR')===String(n).toLocaleLowerCase('tr-TR'))){alert('Bu kiraya veren işlem görmüş/kullanılmış malzemelerde var. Silinemez, isterseniz Düzenle ile adını değiştirin.');return} if(!requireDeletePassword('Bu kiraya vereni listeden silmek istiyor musunuz?'))return; db.settings.lessors=(db.settings.lessors||[]).filter(x=>String(x).toLocaleLowerCase('tr-TR')!==String(n).toLocaleLowerCase('tr-TR')); save(); render()} function addSettingType(){let n=cleanType(formVal('settingTypeName')); if(!n){alert('Tip adı yazın');return} addTypeToSettings(n,true); render()} function editSettingType(v){let old=dec(v); openModal('Tip Düzenle',`
Bu tip kullanılıyorsa bağlı stok/malzeme kayıtlarındaki tip adı da güncellenir.
`)} function saveSettingTypeEdit(v){let old=dec(v), neu=cleanType(formVal('editSettingTypeName')); if(!neu){alert('Tip adı yazın');return} ensureDefaultMaterialTypes(); if((db.settings.types||[]).some(x=>String(x||'').toLocaleLowerCase('tr-TR')===neu.toLocaleLowerCase('tr-TR')&&String(x||'').toLocaleLowerCase('tr-TR')!==old.toLocaleLowerCase('tr-TR'))){alert('Bu tip zaten listede var.');return} db.settings.types=(db.settings.types||[]).map(x=>String(x||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR')?neu:x); db.settings.types=[...new Set(db.settings.types)].sort((a,b)=>a.localeCompare(b,'tr')); (db.items||[]).forEach(i=>{if(String(i.type||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR'))i.type=neu}); save(); closeModal(); render()} function deleteSettingType(v){let n=dec(v); if(settingUsed('type',n)){alert('Bu tip stok/malzeme kayıtlarında kullanılıyor. Silinemez, isterseniz Düzenle ile adını değiştirin.');return} if(!requireDeletePassword('Bu tipi listeden silmek istiyor musunuz?'))return; ensureDefaultMaterialTypes(); db.settings.types=(db.settings.types||[]).filter(x=>String(x||'').toLocaleLowerCase('tr-TR')!==String(n).toLocaleLowerCase('tr-TR')); save(); render()} function cleanItemName(n){return String(n||'').trim()} function addItemNameToSettings(n,doSave=true){n=cleanItemName(n); if(!n)return; if(!db.settings)db.settings={types:[],itemNames:[]}; if(!db.settings.itemNames)db.settings.itemNames=[]; let exists=db.settings.itemNames.some(x=>x.toLocaleLowerCase('tr-TR')===n.toLocaleLowerCase('tr-TR')); if(!exists){db.settings.itemNames.push(n); db.settings.itemNames.sort((a,b)=>a.localeCompare(b,'tr')); if(doSave)save();}} function addSettingItemName(){let n=cleanItemName(formVal('settingItemName')); if(!n){alert('Malzeme adı yazın');return} addItemNameToSettings(n,true); render()} function editSettingItemName(v){let old=dec(v); openModal('Malzeme Adı Düzenle',`
Bu ad kullanılıyorsa bağlı malzeme kayıtları silinmez, malzeme adı yeni adla güncellenir.
`)} function saveSettingItemNameEdit(v){let old=dec(v), neu=cleanItemName(formVal('editSettingItemName')); if(!neu){alert('Malzeme adı yazın');return} db.settings.itemNames=(db.settings.itemNames||[]).map(x=>x.toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR')?neu:x); db.settings.itemNames=[...new Set(db.settings.itemNames)]; (db.items||[]).forEach(i=>{if(String(i.name||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR'))i.name=neu}); save(); closeModal(); render()} function deleteSettingItemName(v){let n=dec(v); if(settingUsed('itemName',n)){alert('Bu malzeme adı işlem görmüş/kullanılmış malzemelerde var. Silinemez, isterseniz Düzenle ile adını değiştirin.');return} if(!requireDeletePassword('Bu malzeme adını listeden silmek istiyor musunuz?'))return; if(!db.settings)db.settings={types:[],itemNames:[]}; db.settings.itemNames=(db.settings.itemNames||[]).filter(x=>x.toLocaleLowerCase('tr-TR')!==String(n).toLocaleLowerCase('tr-TR')); save(); render()} function addSettingDepot(){let n=cleanDepot(formVal('settingDepotName')); if(!n){alert('Depo / konum adı yazın');return} addDepotToSettings(n,true); render()} function editSettingDepot(v){let old=dec(v); openModal('Depo Düzenle',`
Bu depo kullanılıyorsa bağlı malzeme/dönüş kayıtları silinmez, depo adı yeni adla güncellenir. Sabit depolar düzenlenebilir ancak silinemez.
`)} function saveSettingDepotEdit(v){let old=dec(v), neu=cleanDepot(formVal('editSettingDepotName')); if(!neu){alert('Depo adı yazın');return} if(db.settings&&db.settings.depots&&(db.settings.depots||[]).some(x=>String(x||'').toLocaleLowerCase('tr-TR')===neu.toLocaleLowerCase('tr-TR')&&String(x||'').toLocaleLowerCase('tr-TR')!==old.toLocaleLowerCase('tr-TR'))){alert('Bu depo adı zaten listede var.');return} let wasFixed=isFixedDepot(old); if(wasFixed){db.settings.fixedDepots=fixedDepotNames().map(x=>String(x||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR')?neu:x)} db.settings.depots=(db.settings.depots||[]).map(x=>String(x||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR')?neu:x); db.settings.depots=[...new Set(db.settings.depots)]; (db.items||[]).forEach(i=>{if(String(i.location||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR'))i.location=neu; if(String(i.entryLocation||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR'))i.entryLocation=neu; if(String(i.lastDepotEntryLocation||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR'))i.lastDepotEntryLocation=neu;}); (db.moves||[]).forEach(i=>{if(String(i.location||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR'))i.location=neu}); (db.ownerReturns||[]).forEach(i=>{if(String(i.remainingLocation||'').toLocaleLowerCase('tr-TR')===old.toLocaleLowerCase('tr-TR'))i.remainingLocation=neu}); ensureFixedDepots(); save(); closeModal(); render()} function deleteSettingDepot(v){let n=dec(v); if(isFixedDepot(n)){alert('Sabit / kilitli depo ve konum kayıtları silinemez. Düzenle ile adını değiştirebilirsiniz.'); ensureFixedDepots(); save(); render(); return} if(settingUsed('depot',n)){alert('Bu depo işlem görmüş/kullanılmış kayıtların içinde var. Silinemez, isterseniz Düzenle ile adını değiştirin.');return} if(!requireDeletePassword('Bu depoyu listeden silmek istiyor musunuz?'))return; if(!db.settings)db.settings={types:[],itemNames:[],depots:[]}; db.settings.depots=(db.settings.depots||[]).filter(x=>x.toLocaleLowerCase('tr-TR')!==String(n).toLocaleLowerCase('tr-TR')); ensureFixedDepots(); save(); render()} function offerTotal(o){return (o.items||[]).reduce((sum,it)=>sum+(Number(it.qty||0)*Number(it.unitPrice||0)),0)} function offerItemsText(o){return (o.items||[]).map(it=>`${it.desc||''};${it.qty||1};${it.unitPrice||0}`).join('\n')} function parseOfferItems(txt){return String(txt||'').split(/\n+/).map(line=>line.trim()).filter(Boolean).map(line=>{let parts=line.split(';').map(x=>x.trim()); return {desc:parts[0]||'',qty:Number(parts[1]||1),unitPrice:Number(parts[2]||0)}}).filter(x=>x.desc)} function offerItemOptions(){ let arr=(db.settings&&db.settings.offerItems?db.settings.offerItems:[]).map(x=>({id:x.id,desc:x.desc,qty:Number(x.qty||1),unitPrice:Number(x.unitPrice||0),source:'Ayar Kalemi'})); return arr.sort((a,b)=>String(a.desc||'').localeCompare(String(b.desc||''),'tr')); } function saveOfferItemsToSettings(items){return} function offerPresetOptions(){let arr=offerItemOptions(); return arr.map((it,i)=>``).join('')} function fillOfferPreset(){ let sel=document.getElementById('ofPresetItem'); if(!sel)return; let arr=window.__offerPresetItems||offerItemOptions(); let it=arr[Number(sel.value)]; if(!it)return; let q=document.getElementById('ofLineQty'), p=document.getElementById('ofLinePrice'); if(q)q.value=Number(it.qty||1); if(p)p.value=Number(it.unitPrice||0); } function addPresetOfferItem(){ let sel=document.getElementById('ofPresetItem'); if(!sel)return; let arr=window.__offerPresetItems||offerItemOptions(); let it=arr[Number(sel.value)]; if(!it){alert('Kalem seçin');return} let q=Number((document.getElementById('ofLineQty')||{}).value||it.qty||1); let price=Number((document.getElementById('ofLinePrice')||{}).value||it.unitPrice||0); addOfferTableRow(it.desc||'',q,price); } function offerRowHtml(desc='',qty=1,unitPrice=0){ let total=Number(qty||0)*Number(unitPrice||0); return `${money(total)}`; } function addOfferTableRow(desc='',qty=1,unitPrice=0){ let body=document.getElementById('ofItemsBody'); if(!body)return; body.insertAdjacentHTML('beforeend',offerRowHtml(desc,qty,unitPrice)); recalcOfferRows(); } function recalcOfferRows(){ let sum=0; document.querySelectorAll('#ofItemsBody tr').forEach(tr=>{ let q=Number((tr.querySelector('.ofItemQty')||{}).value||0); let p=Number((tr.querySelector('.ofItemPrice')||{}).value||0); let t=q*p; sum+=t; let cell=tr.querySelector('.ofItemTotal'); if(cell)cell.textContent=money(t); }); let el=document.getElementById('ofItemsSum'); if(el)el.textContent=money(sum); } function collectOfferItems(){ return Array.from(document.querySelectorAll('#ofItemsBody tr')).map(tr=>({ desc:(tr.querySelector('.ofItemDesc')||{}).value||'', qty:Number((tr.querySelector('.ofItemQty')||{}).value||0), unitPrice:Number((tr.querySelector('.ofItemPrice')||{}).value||0) })).filter(x=>String(x.desc||'').trim()); } function offerItemsRowsHtml(items){ let rows=(items||[]).map(it=>offerRowHtml(it.desc,it.qty,it.unitPrice)).join(''); return rows || `Henüz kalem eklenmedi. Üstteki listeden kalem seçip adet ve birim fiyat girerek ekleyin.`; } function clearEmptyOfferRow(){ let body=document.getElementById('ofItemsBody'); if(!body)return; if(body.children.length===1 && body.textContent.includes('Henüz kalem eklenmedi')) body.innerHTML=''; } function addOfferTableRow(desc='',qty=1,unitPrice=0){ let body=document.getElementById('ofItemsBody'); if(!body)return; clearEmptyOfferRow(); body.insertAdjacentHTML('beforeend',offerRowHtml(desc,qty,unitPrice)); recalcOfferRows(); } function offerItemSettingForm(id=''){ let x=id?(db.settings.offerItems||[]).find(i=>i.id===id):{}; openModal(id?'Teklif Kalemi Düzenle':'Teklif Kalemi Ekle',`
`) } function saveOfferItemSetting(id=''){ let desc=formVal('oisDesc').trim(); if(!desc){alert('Kalem açıklaması yazın');return} if(!db.settings)db.settings={types:[],itemNames:[],depots:[],currencies:['TL'],offerItems:[]}; if(!db.settings.offerItems)db.settings.offerItems=[]; if(!db.settings.sms)db.settings.sms={username:'',password:'',originator:'',apiUrl:'https://smsgw.mutlucell.com/smsgw-ws/sndblkex',testMode:true}; if(!db.settings.whatsappApi)db.settings.whatsappApi={enabled:false,apiVersion:'v23.0',phoneNumberId:'',accessToken:'',businessNumber:'',testMode:false}; db.settings.whatsappApi.apiVersion=db.settings.whatsappApi.apiVersion||'v23.0'; db.settings.whatsappApi.enabled=!!db.settings.whatsappApi.enabled; db.settings.whatsappApi.phoneNumberId=db.settings.whatsappApi.phoneNumberId||''; db.settings.whatsappApi.accessToken=db.settings.whatsappApi.accessToken||''; db.settings.whatsappApi.businessNumber=db.settings.whatsappApi.businessNumber||''; db.settings.sms.apiUrl=db.settings.sms.apiUrl||'https://smsgw.mutlucell.com/smsgw-ws/sndblkex'; if(typeof db.settings.sms.testMode==='undefined')db.settings.sms.testMode=true; if(!db.settings.program)db.settings.program={companyName:'SİSTEM KULE VİNÇ',appTitle:'Emanet Kira Takip',subtitle:'Firma malzemesi • Kiralama • Fatura no/tarih',dashboardSubtitle:'Emanet Kira Takip • Malzeme • Kiralama • Fatura Takibi',logo:null}; db.settings.program.companyName=db.settings.program.companyName||'SİSTEM KULE VİNÇ'; if(!db.settings.lessors)db.settings.lessors=[]; if(!db.settings.lessors.includes(db.settings.program.companyName))db.settings.lessors.push(db.settings.program.companyName); if(!db.settings.lessors.some(x=>String(x).toLocaleLowerCase('tr-TR')==='vinç sahibi kullanımında'))db.settings.lessors.push('VİNÇ SAHİBİ KULLANIMINDA'); if(!db.settings.invoiceLessor)db.settings.invoiceLessor=db.settings.program.companyName; db.settings.program.appTitle=db.settings.program.appTitle||'Emanet Kira Takip'; db.settings.program.subtitle=db.settings.program.subtitle||'Firma malzemesi • Kiralama • Fatura no/tarih'; db.settings.program.dashboardSubtitle=db.settings.program.dashboardSubtitle||'Emanet Kira Takip • Malzeme • Kiralama • Fatura Takibi'; let exists=db.settings.offerItems.find(x=>x.id!==id && String(x.desc||'').toLocaleLowerCase('tr-TR')===desc.toLocaleLowerCase('tr-TR')); if(exists){exists.qty=Number(formVal('oisQty')||1); exists.unitPrice=Number(formVal('oisPrice')||0);} else if(id){let x=db.settings.offerItems.find(i=>i.id===id); if(x)Object.assign(x,{desc,qty:Number(formVal('oisQty')||1),unitPrice:Number(formVal('oisPrice')||0)});} else db.settings.offerItems.push({id:uid('OK'),desc,qty:Number(formVal('oisQty')||1),unitPrice:Number(formVal('oisPrice')||0)}); save(); closeModal(); render(); } function deleteOfferItemSetting(id){ if(!requireDeletePassword('Bu teklif kalemini ayarlardan silmek istiyor musunuz?'))return; db.settings.offerItems=(db.settings.offerItems||[]).filter(x=>x.id!==id); save(); render(); } function offersView(){ document.getElementById('topActions').innerHTML=' '; let list=(db.offers||[]).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))); let totalOpen=list.filter(o=>['Hazırlandı','Gönderildi','Beklemede'].includes(o.status||'')); let accepted=list.filter(o=>(o.status||'')==='Kabul Edildi'); document.getElementById('view').innerHTML=`
Toplam Teklif
${list.length}
Açık / Bekleyen
${totalOpen.length}
Kabul Edilen
${accepted.length}
Teklif Toplamı
${money(list.reduce((s,o)=>s+offerTotal(o),0))}

Teklif Yönetimi

Teklifler artık Açıklama / Adet / Birim Fiyat / Toplam tablo mantığıyla hazırlanır. Açıklama kalemleri sadece aşağıdaki Teklif Kalemleri Ayarları listesinden seçilir.
${table(list.map(o=>[o.no||'-',o.date||'-',o.validUntil||'-',nameOf(db.customers,o.customerId),o.subject||'-',badge(o.status||'Hazırlandı'),money(offerTotal(o)),o.note||'-',` `]),['Teklif No','Tarih','Geçerlilik','Müşteri','Konu','Durum','Toplam','Not','İşlem'])}

Teklif Kalemleri Ayarları

Buraya eklediğin kalemler, teklif oluştururken Açıklama seçeneği olarak gelir. Örnek: Kule vinç kira bedeli.
${table(offerItemOptions().map(it=>[it.desc||'-',Number(it.qty||1),money(it.unitPrice||0),` `]),['Kalem Açıklaması','Varsayılan Adet','Varsayılan Birim Fiyat','İşlem'])}
` } function fmtMoneyCurrency(n,currency){ let cur=currency==='USD'?'USD':currency==='EUR'?'EUR':'TRY'; try{return Number(n||0).toLocaleString('tr-TR',{style:'currency',currency:cur})}catch(e){return Number(n||0).toLocaleString('tr-TR')+' '+(currency||'TL')} } function offerPdf(id){ let o=byId(db.offers,id); if(!o){alert('Teklif bulunamadı');return} let cust=byId(db.customers,o.customerId)||{}; let araToplam=offerTotal(o), kdv=araToplam*Number(o.vatRate||0)/100, genel=araToplam+kdv, cur=o.currency||'TL'; let rows=(o.items||[]).map((it,i)=>`${i+1}${esc(it.desc||'')}${Number(it.qty||0)}${fmtMoneyCurrency(it.unitPrice||0,cur)}${fmtMoneyCurrency(Number(it.qty||0)*Number(it.unitPrice||0),cur)}`).join('') || 'Teklif kalemi yok'; let html=`${esc(o.no||'Teklif')}
${programSettings().logo&&programSettings().logo.data?`
`:""}
${esc(programSettings().appTitle)} • Teklif Formu
TEKLİF
Teklif No: ${esc(o.no||'-')}
Müşteri
${esc(cust.name||'-')}
Yetkili: ${esc(cust.contact||'-')}
Telefon: ${esc(cust.phone||'-')}
Adres: ${esc(cust.address||'-')}
Teklif Bilgileri
Tarih: ${formatDateTR(o.date)||'-'}
Geçerlilik: ${formatDateTR(o.validUntil)||'-'}
Durum: ${esc(o.status||'-')}
Konu: ${esc(o.subject||'-')}
Teklif Kalemleri
${rows}
NoAçıklamaAdetBirim FiyatTutar
Ara Toplam${fmtMoneyCurrency(araToplam,cur)}
KDV (%${Number(o.vatRate||0)})${fmtMoneyCurrency(kdv,cur)}
Genel Toplam${fmtMoneyCurrency(genel,cur)}
Ödeme / Teslim Şartı
${esc(o.terms||'-')}
Not
${esc(o.note||'-')}
Firma Yetkilisi



Müşteri Onayı



`; let w=window.open('','_blank'); if(!w){alert('PDF ekranı açılamadı. Tarayıcı pop-up engelini kapatıp tekrar deneyin.');return} w.document.open(); w.document.write(html); w.document.close(); } function offerFormForCustomer(customerId){ offerForm(); let sel=document.getElementById('ofCustomer'); if(sel)sel.value=customerId||''; let cust=byId(db.customers,customerId)||{}; let subject=document.getElementById('ofSubject'); if(subject && !subject.value)subject.value='Kule vinç ekipman kiralama teklifi'; } function offerForm(id=''){ let pendingOffer=!id?pendingOfferItemData():null; let o=id?byId(db.offers,id):{no:'TKL-'+String((db.offers||[]).length+1).padStart(4,'0'),date:today(),validUntil:today(),status:'Hazırlandı',items:pendingOffer?[{desc:pendingOffer.desc,qty:pendingOffer.qty,unitPrice:pendingOffer.unitPrice}]:[]}; if(pendingOffer){o.subject='Kule vinç ekipman kiralama teklifi - '+(pendingOffer.item.name||'Malzeme');} window.__offerPresetItems=offerItemOptions(); openModal(id?'Teklif Düzenle':'Teklif Ekle',`

Teklif Kalemi Ekle

Açıklama alanı, Teklif Kalemleri Ayarları bölümünde oluşturduğun listeden gelir.
${offerItemsRowsHtml(o.items||[])}
AçıklamaAdetBirim FiyatToplamİşlem
Ara Toplam${money(offerTotal(o))}
`); recalcOfferRows(); } function saveOffer(id=''){ let o=id?byId(db.offers,id):{id:uid('TKL')}; if(!formVal('ofCustomer')){alert('Müşteri seçin');return} let parsedItems=collectOfferItems(); if(!parsedItems.length){alert('Teklife en az bir kalem ekleyin');return} Object.assign(o,{no:formVal('ofNo'),date:formVal('ofDate'),validUntil:formVal('ofValid'),status:formVal('ofStatus'),customerId:formVal('ofCustomer'),subject:formVal('ofSubject'),items:parsedItems,vatRate:Number(formVal('ofVat')||0),currency:formVal('ofCurrency'),terms:formVal('ofTerms'),note:formVal('ofNote')}); if(!id)db.offers.push(o); save(); closeModal(); render(); } function defaultContractTemplateHtml(){return actContractTemplateHtml()} function actContractTemplateHtml(){return `
{{SANTIYE_PROJE}}
KULE VİNÇ KİRALAMA
TEKNİK SERVİS HİZMET VE YEDEK PARÇA
SÖZLEŞMESİ
{{SOZLESME_NO}}
- {{KIRAYA_VEREN_ADI}}
- {{MUSTERI_ADI}}{{HIZMET_VEREN_KAPAK}}
{{SOZLESME_TARIHI}}
☑ Makina Kiralama ve Teknik Servis Hizmeti
☐ Yedek Parça Satışı
☐ Ekipman Kiralama
☐ Diğer

{{SOZLESME_TURU}}

1- SÖZLEŞMENİN TARAFLARI

KİRACI
Firma{{MUSTERI_ADI}}
Adres{{MUSTERI_ADRESI}}
Tel / E-posta{{MUSTERI_TELEFON}} / {{MUSTERI_EPOSTA}}
Yetkili{{MUSTERI_YETKILI}}
Vergi Dairesi / No{{VERGI_DAIRESI}} / {{VERGI_NO}}
KİRALAYAN
Firma{{KIRAYA_VEREN_ADI}}
Adres{{KIRAYA_VEREN_ADRESI}}
Telefon / E-posta{{KIRAYA_VEREN_TELEFON}} / {{KIRAYA_VEREN_EPOSTA}}
Yetkili{{KIRAYA_VEREN_YETKILI}}
Vergi Dairesi / No{{KIRAYA_VEREN_VERGI_DAIRESI}} / {{KIRAYA_VEREN_VERGI_NO}}
Banka{{KIRAYA_VEREN_BANKA}}
Web{{KIRAYA_VEREN_WEB}}
{{HIZMET_VEREN_BLOK}}

2- SÖZLEŞMENİN KONUSU

2.1. İşbu sözleşmenin konusu, KİRACI'nın {{SANTIYE_PROJE}} kapsamında kullanmak üzere aşağıda özellikleri belirtilen {{ADET}} adet kule vinci KİRALAYAN'dan kiralaması, teknik servis hizmetlerinin ve tarafların sorumluluklarının belirlenmesidir.

2.2. Sözleşmenin tüm ekleri ayrılmaz parçasıdır. Hükümler ile ekler arasında çelişki olması halinde sözleşme hükümleri uygulanır.

2.3. Kira dönemi {{BASLANGIC_TARIHI}} - {{BITIS_TARIHI}} tarihleri arasındadır. Bir ay 30 gün kabul edilir.

3- KULE VİNCİN TEKNİK ÖZELLİKLERİ

MalzemeMarkaModelSeri NoÜretim Yılı
{{MALZEME_ADI}}{{VINC_MARKA}}{{VINC_MODEL}}{{VINC_SERI_NO}}{{VINC_URETIM_YILI}}
Tarama Alanı{{VINC_TARAMA_ALANI}}
Yükseklik{{VINC_YUKSEKLIK}}
Şase Tipi{{VINC_SASE_TIPI}}
Malzeme Sahibi{{MALZEME_SAHIBI}}

4- SÖZLEŞMENİN BAŞLANGICI VE SONA ERMESİ

4.1. Sözleşme, kule vincin sevk için yüklenmesiyle başlar; demontajı, KİRALAYAN'ın deposuna teslimi ve indirilmesiyle sona erer.

4.2. KİRACI, en az bir ay önceden yazılı talepte bulunmak ve KİRALAYAN'ın yazılı onayını almak şartıyla kira süresini yeni koşullarla uzatabilir.

4.3. Süre sonunda vinç teslim edilmezse sözleşme teslim tarihine kadar devam eder ve kira bedeli güncel TEFE/TÜFE oranlarına göre artırılabilir.

4.4. Erken teslim veya sözleşmeden vazgeçme halinde bir ay önceden yazılı bildirim gerekir; aksi halde iki aylık kira bedeli cezai şart olarak uygulanır.

5- KİRA BEDELİ, FATURALANDIRMA VE ÖDEME

AdetAylık Birim KiraToplam Sözleşme TutarıKDVDepozito / Teminat
{{ADET}}{{AYLIK_KIRA}} {{PARA_BIRIMI}}{{TOPLAM_TUTAR}} {{PARA_BIRIMI}}{{KDV_ORANI}}{{DEPOZITO}}

5.1. Kira ve servis faturaları aylık olarak e-fatura/e-arşiv şeklinde düzenlenir. {{ODEME_SARTI}}

5.2. Teknik servis, arıza ve yedek parça faturaları fatura tarihinden itibaren yedi gün içinde ödenir. Süresinde ödenmeyen tutarlara sözleşmede kararlaştırılan gecikme hükümleri uygulanır.

5.3. Arıza nedeniyle vincin çalışmadığı süreler için, KİRALAYAN'ın kusuru ayrıca kanıtlanmadıkça, kira indirimi veya zarar talep edilemez.

6- NAKLİYE, MONTAJ VE DEMONTAJ

6.1. Nakliye, yol izinleri, taşıma sigortası, mobil vinç ve şantiye içi yükleme/indirme giderleri aksi yazılı kararlaştırılmadıkça KİRACI'ya aittir.

6.2. {{TESLIM_SARTI}}

6.3. Teslim ve iade, tarafların yetkili personeli tarafından imzalanacak tutanakla gerçekleştirilir.

7- SİGORTA, KONTROLLER VE İSG

7.1. KİRACI, kule vinç için uygun bedelli makine kırılması ve üçüncü şahıs mali sorumluluk sigortalarını yaptırır; poliçeleri sevkten önce KİRALAYAN'a sunar.

7.2. Periyodik kontroller, operatör belgeleri, saha güvenliği, elektrik ve topraklama altyapısı ile günlük kontroller KİRACI'nın sorumluluğundadır.

8- TEKNİK SERVİS HİZMETLERİ

8.1. Montaj, demontaj, yükseltme, deplasman, bakım ve arıza müdahaleleri yalnızca KİRALAYAN/HİZMET VEREN teknik ekibi tarafından yapılır.

8.2. Zemin etüdü, temel betonu, elektrik hattı ve topraklama hazırlıkları KİRACI tarafından projeye uygun şekilde tamamlanır.

8.3. Periyodik bakım en geç 60 günde bir yapılır. Arıza ihbarları yazılı olarak iletilir; müdahale planı teknik ekip tarafından bildirilir.

9- KİRALAYANIN SORUMLULUKLARI

KİRALAYAN, kule vinci bakımları yapılmış, çalışır ve yasal belgeleri hazır şekilde teslim eder; sözleşme süresince teknik servis organizasyonunu sağlar.

10- KİRACININ SORUMLULUKLARI

KİRACI; ehil operatör temini, ücret ve sigortaları, güvenli kullanım, muhafaza, elektrik giderleri, günlük kontroller ve talimatlara uyulmasından sorumludur. Yazılı onay olmadan vinci devredemez, başka sahaya götüremez veya üçüncü kişilere müdahale ettiremez.

11- SÖZLEŞMENİN FESHİ

Ödemelerin yedi günden fazla gecikmesi, teknik talimatlara uyulmaması, izinsiz devir veya müdahale, konkordato/iflas ya da sözleşme yükümlülüklerinin esaslı ihlali halinde KİRALAYAN yazılı bildirimle sözleşmeyi feshedebilir ve vinci sökerek geri alabilir. İlgili masraflar kusurlu tarafa aittir.

12- MÜCBİR SEBEPLER

Deprem, sel, yangın, yasal grev/lokavt, seferberlik ve savaş gibi tarafların kontrolü dışındaki olaylar mücbir sebep sayılır. Etkilenen taraf beş gün içinde yazılı bildirim yapar. Durumun on beş günden fazla sürmesi halinde taraflar sözleşmeyi feshedebilir.

13- TEBLİGAT

Tarafların bu sözleşmede belirtilen adresleri kanuni tebligat adresidir. Değişiklikler yazılı bildirilmedikçe eski adrese yapılan tebligatlar geçerli kabul edilir.

14- SÖZLEŞME MASRAFLARI

Damga vergisi ve diğer yasal masrafların paylaşımı tarafların yazılı mutabakatına göre uygulanır.

15- SÖZLEŞMENİN EKLERİ

EK-1 Fiyatlandırma ve Ödeme Tablosu, EK-2 Kule Vinç Genel Kullanım Talimatı, EK-3 Teknik Servis Hizmetleri, EK-4 İSG Sorumlulukları ve teslim tutanakları sözleşmenin ayrılmaz parçasıdır.

16- YETKİLİ MAHKEME

Uyuşmazlıkların çözümünde Ankara Mahkemeleri ve İcra Daireleri yetkilidir.

ÖZEL ŞARTLAR

{{OZEL_SARTLAR}}
KİRACI
{{MUSTERI_ADI}}

Kaşe / İmza
KİRALAYAN
{{KIRAYA_VEREN_ADI}}

Kaşe / İmza
{{HIZMET_VEREN_IMZA}}
`} function ensureContractTemplates(){ if(!db.settings)db.settings={}; if(!Array.isArray(db.settings.contractTemplates))db.settings.contractTemplates=[]; if(!db.settings.contractTemplates.length){let id='CTPL-ACT-2026';db.settings.contractTemplates.push({id,name:'Profesyonel Kule Vinç Sözleşmesi (ACT Formatı)',html:actContractTemplateHtml(),created:today(),updated:today(),version:3});db.settings.defaultContractTemplateId=id;} let act=db.settings.contractTemplates.find(x=>x.id==='CTPL-ACT-2026'); if(!act){db.settings.contractTemplates.push({id:'CTPL-ACT-2026',name:'Profesyonel Kule Vinç Sözleşmesi (ACT Formatı)',html:actContractTemplateHtml(),created:today(),updated:today(),version:3});db.settings.defaultContractTemplateId='CTPL-ACT-2026';} if(act&&Number(act.version||0)<3){act.html=actContractTemplateHtml();act.version=3;act.updated=today();} if(!db.settings.defaultContractTemplateId||!db.settings.contractTemplates.some(x=>x.id===db.settings.defaultContractTemplateId))db.settings.defaultContractTemplateId=db.settings.contractTemplates[0].id; } function contractTemplateList(){ensureContractTemplates();return db.settings.contractTemplates} function contractTemplateById(id=''){ensureContractTemplates();return contractTemplateList().find(x=>x.id===id)||contractTemplateList().find(x=>x.id===db.settings.defaultContractTemplateId)||contractTemplateList()[0]} function contractTemplateOptions(sel=''){ensureContractTemplates();return contractTemplateList().map(t=>``).join('')} const CONTRACT_TEMPLATE_TAGS=['MUSTERI_ADRESI','MUSTERI_EPOSTA','VINC_MARKA','VINC_MODEL','VINC_SERI_NO','VINC_URETIM_YILI','VINC_TARAMA_ALANI','VINC_YUKSEKLIK','VINC_SASE_TIPI','SOZLESME_TURU','SOZLESME_NO','SOZLESME_TARIHI','DURUM','KIRAYA_VEREN','MUSTERI_ADI','MUSTERI_YETKILI','MUSTERI_TELEFON','VERGI_DAIRESI','VERGI_NO','MALZEME_ADI','MALZEME_TIPI','MALZEME_SAHIBI','ADET','AYLIK_KIRA','TOPLAM_TUTAR','PARA_BIRIMI','BASLANGIC_TARIHI','BITIS_TARIHI','SANTIYE_PROJE','IL','ILCE','ADRES','KDV_ORANI','DEPOZITO','ODEME_SARTI','TESLIM_SARTI','OZEL_SARTLAR']; function contractTemplateManager(selectedId=''){ ensureContractTemplates();let selected=contractTemplateById(selectedId); let list=contractTemplateList().map(t=>`
${esc(t.name)}
${t.id===db.settings.defaultContractTemplateId?'Varsayılan şablon':'Son güncelleme: '+esc(t.updated||t.created||'-')}
`).join(''); let groups={ 'Sözleşme':['SOZLESME_TURU','SOZLESME_NO','SOZLESME_TARIHI','DURUM'], 'Taraflar':['KIRAYA_VEREN','MUSTERI_ADI','MUSTERI_YETKILI','MUSTERI_TELEFON','VERGI_DAIRESI','VERGI_NO'], 'Ekipman':['MALZEME_ADI','MALZEME_TIPI','MALZEME_SAHIBI','ADET'], 'Kira ve Tarih':['AYLIK_KIRA','TOPLAM_TUTAR','PARA_BIRIMI','BASLANGIC_TARIHI','BITIS_TARIHI'], 'Şantiye ve Şartlar':['SANTIYE_PROJE','IL','ILCE','ADRES','KDV_ORANI','DEPOZITO','ODEME_SARTI','TESLIM_SARTI','OZEL_SARTLAR'] }; let labels={SOZLESME_TURU:'Sözleşme Türü',SOZLESME_NO:'Sözleşme No',SOZLESME_TARIHI:'Sözleşme Tarihi',DURUM:'Durum',KIRAYA_VEREN:'Kiraya Veren',MUSTERI_ADI:'Müşteri Adı',MUSTERI_YETKILI:'Müşteri Yetkilisi',MUSTERI_TELEFON:'Müşteri Telefonu',VERGI_DAIRESI:'Vergi Dairesi',VERGI_NO:'Vergi No',MALZEME_ADI:'Malzeme / Vinç',MALZEME_TIPI:'Malzeme Tipi',MALZEME_SAHIBI:'Malzeme Sahibi',ADET:'Adet',AYLIK_KIRA:'Aylık Kira',TOPLAM_TUTAR:'Toplam Tutar',PARA_BIRIMI:'Para Birimi',BASLANGIC_TARIHI:'Başlangıç Tarihi',BITIS_TARIHI:'Bitiş Tarihi',SANTIYE_PROJE:'Şantiye / Proje',IL:'İl',ILCE:'İlçe',ADRES:'Adres',KDV_ORANI:'KDV Oranı',DEPOZITO:'Depozito',ODEME_SARTI:'Ödeme Şartı',TESLIM_SARTI:'Teslim Şartı',OZEL_SARTLAR:'Özel Şartlar'}; let fields=Object.entries(groups).map(([g,a])=>`
${g}
${a.map(x=>``).join('')}`).join(''); openModal('Profesyonel Sözleşme Tasarımcısı',`

Şablonlar

${list}
${selected.html||''}
${selected.id!==db.settings.defaultContractTemplateId?``:''}

Otomatik Alanlar

Alana tıklayın; imlecin olduğu yere eklenir.
${fields}
`); document.querySelector('#modal .modalbox').style.maxWidth='1420px'; } function contractEditorCmd(cmd){document.getElementById('tplEditor')?.focus();document.execCommand(cmd,false,null)} function contractEditorBlock(tag){if(!tag)return;document.getElementById('tplEditor')?.focus();document.execCommand('formatBlock',false,tag)} function insertContractTemplateTag(tag){let ed=document.getElementById('tplEditor');if(!ed)return;ed.focus();document.execCommand('insertText',false,tag)} function syncContractEditor(){let ed=document.getElementById('tplEditor'),ta=document.getElementById('tplHtml');if(ed&&ta)ta.value=ed.innerHTML;return ed?ed.innerHTML:''} function newContractTemplate(){let t={id:uid('CTPL'),name:'Yeni Sözleşme Taslağı',html:defaultContractTemplateHtml(),created:today(),updated:today()};db.settings.contractTemplates.push(t);save();closeModal();contractTemplateManager(t.id)} function saveContractTemplate(){let id=formVal('tplId'),t=contractTemplateById(id),name=formVal('tplName').trim(),html=syncContractEditor();if(!name){alert('Taslak adı girin.');return}if(!html.trim()){alert('Taslak içeriği boş olamaz.');return}t.name=name;t.html=html;t.updated=today();save();alert('Sözleşme taslağı kaydedildi.');closeModal();contractTemplateManager(t.id)} function deleteContractTemplate(id){if(contractTemplateList().length<=1){alert('En az bir sözleşme taslağı bulunmalıdır.');return}if(!confirm('Bu sözleşme taslağı silinsin mi?'))return;db.settings.contractTemplates=db.settings.contractTemplates.filter(x=>x.id!==id);if(db.settings.defaultContractTemplateId===id)db.settings.defaultContractTemplateId=db.settings.contractTemplates[0].id;(db.contracts||[]).forEach(c=>{if(c.templateId===id)c.templateId=db.settings.defaultContractTemplateId});save();closeModal();contractTemplateManager()} function setDefaultContractTemplate(id){db.settings.defaultContractTemplateId=id;save();alert('Varsayılan sözleşme taslağı değiştirildi.');closeModal();contractTemplateManager(id)} function resetContractTemplate(id){if(!confirm('Bu taslak varsayılan başlangıç içeriğine döndürülsün mü?'))return;let t=contractTemplateById(id);t.html=defaultContractTemplateHtml();t.updated=today();save();closeModal();contractTemplateManager(id)} function contractPartyData(customerId){let x=byId(db.customers||[],customerId)||{};return {name:x.name||'-',person:x.person||'-',phone:x.phone||'-',address:x.note||'-',mail:x.mail||'-',taxOffice:x.taxOffice||'-',tax:x.tax||'-',bank:x.bank||'-',web:x.web||'-'}} function contractTemplateValues(c){let cust=byId(db.customers||[],c.customerId),item=byId(db.items||[],c.itemId),owner=byId(db.owners||[],item.ownerId),lessor=contractPartyData(c.lessorCustomerId),service=c.serviceProviderCustomerId?contractPartyData(c.serviceProviderCustomerId):null;let serviceBlock=service?`
HİZMET VEREN
Firma${esc(service.name)}
Adres${esc(service.address)}
Telefon / E-posta${esc(service.phone)} / ${esc(service.mail)}
Yetkili${esc(service.person)}
Vergi Dairesi / No${esc(service.taxOffice)} / ${esc(service.tax)}
Banka${esc(service.bank)}
Web${esc(service.web)}
`:'';let serviceCover=service?`
- ${esc(service.name)}`:'';let serviceSign=service?`
HİZMET VEREN
${esc(service.name)}

Kaşe / İmza
`:'';return {SOZLESME_TURU:c.contractType||'Sözleşme',SOZLESME_NO:c.no||'-',SOZLESME_TARIHI:c.date||'-',DURUM:c.status||'-',KIRAYA_VEREN:lessor.name,KIRAYA_VEREN_ADI:lessor.name,KIRAYA_VEREN_YETKILI:lessor.person,KIRAYA_VEREN_TELEFON:lessor.phone,KIRAYA_VEREN_ADRESI:lessor.address,KIRAYA_VEREN_EPOSTA:lessor.mail,KIRAYA_VEREN_VERGI_DAIRESI:lessor.taxOffice,KIRAYA_VEREN_VERGI_NO:lessor.tax,KIRAYA_VEREN_BANKA:lessor.bank,KIRAYA_VEREN_WEB:lessor.web,HIZMET_VEREN_ADI:service?service.name:'',HIZMET_VEREN_BLOK:serviceBlock,HIZMET_VEREN_KAPAK:serviceCover,HIZMET_VEREN_IMZA:serviceSign,MUSTERI_ADI:cust.name||'-',MUSTERI_YETKILI:cust.person||'-',MUSTERI_TELEFON:cust.phone||'-',MUSTERI_ADRESI:cust.note||'-',MUSTERI_EPOSTA:cust.mail||'-',VINC_MARKA:item.towerBrand||item.brand||'-',VINC_MODEL:item.towerModel||item.model||'-',VINC_SERI_NO:item.towerSerialNo||item.serial||'-',VINC_URETIM_YILI:item.towerYear||'-',VINC_TARAMA_ALANI:item.towerScanArea||'-',VINC_YUKSEKLIK:item.towerHeight||'-',VINC_SASE_TIPI:item.towerChassisType||'-',VERGI_DAIRESI:cust.taxOffice||'-',VERGI_NO:cust.tax||'-',MALZEME_ADI:item.name||'-',MALZEME_TIPI:item.type||'-',MALZEME_SAHIBI:owner.name||'-',ADET:String(Number(c.qty||1)),AYLIK_KIRA:money(c.monthly||0),TOPLAM_TUTAR:money(c.total||0),PARA_BIRIMI:c.currency||'TL',BASLANGIC_TARIHI:c.start||'-',BITIS_TARIHI:c.end||'-',SANTIYE_PROJE:c.project||'-',IL:c.city||'-',ILCE:c.district||'-',ADRES:c.address||'-',KDV_ORANI:'%'+Number(c.vatRate||0),DEPOZITO:money(c.deposit||0),ODEME_SARTI:c.paymentTerms||'-',TESLIM_SARTI:c.deliveryTerms||'-',OZEL_SARTLAR:String(c.note||'-').replace(/\n/g,'
')}} function renderContractTemplate(html,c){let vals=contractTemplateValues(c);return String(html||'').replace(/\{\{([A-Z0-9_]+)\}\}/g,(m,k)=>Object.prototype.hasOwnProperty.call(vals,k)?vals[k]:m)} function previewContractTemplate(){let htmlSource=syncContractEditor();let fake={no:'ÖNİZLEME-001',date:today(),status:'Taslak',contractType:'Kule Vinç Kiralama Sözleşmesi',qty:1,monthly:0,total:0,currency:'TL',vatRate:20,deposit:0,project:'Örnek Şantiye',city:'Ankara',district:'',address:'Örnek adres',paymentTerms:'Aylık ödeme',deliveryTerms:'Tarafların mutabakatı ile',note:'Örnek özel şart'};let html=renderContractTemplate(htmlSource,fake);openContractPrintWindow('Sözleşme Taslak Ön İzleme',html,false)} function openContractPrintWindow(title,body,autoPrint=true){let html=`${esc(title)}${body}`;let w=window.open('','_blank');w.document.write(html);w.document.close();if(autoPrint)setTimeout(()=>w.print(),400)} function contractStatusBadge(s){let v=s||'Taslak'; if(v==='İmzalandı'||v==='Aktif')return badge(v); if(v==='Taslak'||v==='Hazırlanıyor')return `${esc(v)}`; if(v==='Süresi Bitti')return `${esc(v)}`; return `${esc(v)}`} function contractRentalOptions(sel=''){ let rows=(db.rentals||[]).slice().sort((a,b)=>String(b.start||'').localeCompare(String(a.start||''))); return ``+rows.map(r=>{let ri=(db.rentalItems||[]).find(x=>x.rentalId===r.id)||{}, it=byId(db.items||[],ri.itemId), txt=[nameOf(db.customers,r.customerId), it.name||'-', r.start||'', r.end||'', r.status||''].filter(Boolean).join(' / '); return ``}).join('') } function contractItemOptions(sel=''){ return ``+(db.items||[]).map(i=>``).join('') } function contractAutoNo(){return 'SOZ-'+new Date().getFullYear()+'-'+String((db.contracts||[]).length+1).padStart(4,'0')} function rentalFirstItem(rid){return (db.rentalItems||[]).find(x=>x.rentalId===rid)||{};} function contractFillFromRental(){let rid=formVal('ctRentalId'); if(!rid)return; let r=byId(db.rentals||[],rid); let ri=rentalFirstItem(rid); let item=byId(db.items||[],ri.itemId); let set=(id,val)=>{let el=document.getElementById(id); if(el&&!el.value)el.value=val||''}; let force=(id,val)=>{let el=document.getElementById(id); if(el)el.value=val||''}; force('ctCustomerId',r.customerId||''); force('ctItemId',ri.itemId||''); set('ctStart',r.start||''); set('ctEnd',r.end||''); set('ctProject',r.projectName||r.location||''); set('ctCity',r.city||''); updateDistrictSelect('ctCity','ctDistrict',r.district||''); set('ctDistrict',r.district||''); set('ctMonthly',Number(ri.unitAmount||ri.unitMonthly||item.monthlyRent||0)); set('ctQty',rentQtyOf(ri)); contractCalcTotal();} function contractCalcTotal(){let start=formVal('ctStart'), end=formVal('ctEnd'), monthly=Number(formVal('ctMonthly')||0), qty=Number(formVal('ctQty')||1); let total=0; if(start&&end&&typeof invoiceDays==='function'){let days=invoiceDays(start,end); total=(monthly/30)*days*qty;} else total=monthly*qty; let el=document.getElementById('ctTotal'); if(el)el.value=Math.round(total*100)/100;} function contractsView(){ let q=(localStorage.getItem('contract_q')||'').toLocaleLowerCase('tr-TR'), status=localStorage.getItem('contract_status')||''; let list=(db.contracts||[]).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))||String(b.created||'').localeCompare(String(a.created||''))); let filtered=list.filter(c=>{let hay=[c.no,c.date,c.status,nameOf(db.customers,c.customerId),nameOf(db.items,c.itemId),c.project,c.city,c.district,c.note].join(' ').toLocaleLowerCase('tr-TR'); if(q&&!hay.includes(q))return false; if(status&&c.status!==status)return false; return true;}); let active=list.filter(c=>['İmzalandı','Aktif'].includes(c.status)).length, signed=list.filter(c=>c.status==='İmzalandı').length, ending=list.filter(c=>c.end&&c.end>=today()&&c.end<=fmtDate(addDays(new Date(),30))).length; document.getElementById('view').innerHTML=`
Toplam Sözleşme
${list.length}
Aktif / İmzalı
${active}
İmzalanan
${signed}
30 Gün İçinde Biten
${ending}

Sözleşmeler

Kule vinç kiralamaları için sözleşme kaydı oluşturabilir, bağlı kiralama/müşteri/malzeme bilgilerini otomatik çekebilir ve PDF/Yazdır çıktısı alabilirsiniz.
${table(filtered.map(c=>[c.no||'-',c.date||'-',contractStatusBadge(c.status),nameOf(db.customers,c.customerId),nameOf(db.items,c.itemId),c.project||'-',c.start||'-',c.end||'-',Number(c.qty||1),money(c.monthly||0),money(c.total||0),c.contractType||'-',` `]),['Sözleşme No','Tarih','Durum','KİRACI','Malzeme / Kule Vinç','Şantiye / Proje','Başlangıç','Bitiş','Adet','Aylık Birim','Toplam','Tür','İşlem'])}
` } function contractForm(id='',rentalId=''){ let c=id?byId(db.contracts||[],id):{id:'',no:contractAutoNo(),date:today(),status:'Taslak',contractType:'Kule Vinç Kiralama Sözleşmesi',currency:'TL',vatRate:20,qty:1}; if(rentalId&&!id)c.rentalId=rentalId; let districts=districtOptions(c.city||'',c.district||''); openModal(id?'Sözleşme Düzenle':'Sözleşme Ekle',`
PDF çıktısı seçilen taslağa göre hazırlanır.
Aktif kiradan sözleşme oluşturursanız müşteri, malzeme, tarih ve tutar bilgileri otomatik gelir.
Müşteriler listesinden seçilir ve sözleşmede KİRALAYAN olarak gösterilir.
Boş bırakılırsa sözleşme ve imza alanlarında görünmez.
`); if(rentalId&&!id)setTimeout(contractFillFromRental,50); } function saveContract(id=''){ let customerId=formVal('ctCustomerId'), lessorCustomerId=formVal('ctLessorCustomerId'), serviceProviderCustomerId=formVal('ctServiceProviderCustomerId'), itemId=formVal('ctItemId'); if(!formVal('ctNo')){alert('Sözleşme no girin.');return} if(!customerId){alert('KİRACI seçin.');return} if(!lessorCustomerId){alert('KİRAYA VEREN seçin.');return} let c=id?byId(db.contracts,id):{id:uid('SOZ'),created:today()}; Object.assign(c,{no:formVal('ctNo'),date:formVal('ctDate')||today(),status:formVal('ctStatus')||'Taslak',contractType:formVal('ctType'),templateId:formVal('ctTemplateId')||db.settings.defaultContractTemplateId,rentalId:formVal('ctRentalId'),customerId,lessorCustomerId,serviceProviderCustomerId,itemId,start:formVal('ctStart'),end:formVal('ctEnd'),qty:Number(formVal('ctQty')||1),monthly:Number(formVal('ctMonthly')||0),total:Number(formVal('ctTotal')||0),currency:formVal('ctCurrency')||'TL',vatRate:Number(formVal('ctVat')||0),deposit:Number(formVal('ctDeposit')||0),project:formVal('ctProject'),city:formVal('ctCity'),district:formVal('ctDistrict'),address:formVal('ctAddress'),paymentTerms:formVal('ctPayment'),deliveryTerms:formVal('ctDelivery'),note:formVal('ctNote'),updated:today()}); if(!id)db.contracts.push(c); save(); closeModal(); render(); } function contractDetail(id){let c=byId(db.contracts||[],id); if(!c.id)return; openModal('Sözleşme Detayı',`

${esc(c.no||'-')} - ${esc(c.contractType||'Sözleşme')}

KİRACI: ${esc(nameOf(db.customers,c.customerId))}   |   KİRAYA VEREN: ${esc(nameOf(db.customers,c.lessorCustomerId)||'-')}   |   HİZMET VEREN: ${esc(nameOf(db.customers,c.serviceProviderCustomerId)||'-')}   |   Durum: ${contractStatusBadge(c.status)}   |   Tarih: ${esc(c.date||'-')}
${table([['Malzeme / Kule Vinç',nameOf(db.items,c.itemId)],['Şantiye / Proje',c.project||'-'],['İl / İlçe',[c.city,c.district].filter(Boolean).join(' / ')||'-'],['Başlangıç / Bitiş',[c.start,c.end].filter(Boolean).join(' / ')||'-']],['Bilgi','Değer'])}
${table([['Adet',c.qty||1],['Birim Aylık Kira',money(c.monthly||0)],['Toplam Tutar',money(c.total||0)],['Depozito / Teminat',money(c.deposit||0)]],['Bilgi','Değer'])}

Şartlar ve Notlar

Ödeme Şartı: ${esc(c.paymentTerms||'-')}

Teslim / Kurulum: ${esc(c.deliveryTerms||'-')}

Adres: ${esc(c.address||'-')}

Açıklama: ${esc(c.note||'-')}

`)} function contractPdf(id){let c=byId(db.contracts||[],id);if(!c.id)return;let tpl=contractTemplateById(c.templateId||db.settings.defaultContractTemplateId);let body=renderContractTemplate(tpl.html,c);openContractPrintWindow(c.no||'Sözleşme',body,true)} function settingsBackupPanel(){return `

Yedekleme

Verilerinizi düzenli yedekleyin. Yedek dosyası JSON formatındadır.

Not: Tüm Verileri Sıfırla işlemi yapılsa bile Ayarlar bölümündeki kayıtlar korunur.
`} function backupView(){window._settingsTab='backup'; current='settings'; settingsView()} function backup(){let blob=new Blob([JSON.stringify(db,null,2)],{type:'application/json'}),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='emanet-kira-yedek-'+today()+'.json';a.click()} function restore(e){let f=e.target.files[0]; if(!f)return; let r=new FileReader(); r.onload=()=>{try{db=JSON.parse(r.result);save();render();alert('Yedek yüklendi')}catch(err){alert('Yedek dosyası okunamadı')}}; r.readAsText(f)} function resetAll(){ if(!confirm('Tüm veriler silinecek. Ayarlar bölümündeki kayıtlar sabit kalacak. Emin misiniz?')) return; let savedSettings=JSON.parse(JSON.stringify(db.settings||seed().settings)); db=seed(); db.settings=savedSettings; migrate(); save(); alert('Tüm işlem verileri silindi. Ayarlar bölümü kayıtlı/sabit olarak korundu.'); render(); } function exportCSV(type){let rows=[['Ay','Dönem Başlangıç','Dönem Bitiş','Gün','Müşteri','Malzeme Sahibi','Malzeme','Verilen Adet','Birim Aylık Kira','Fatura Tutarı','Fatura Durumu','Fatura No','Fatura Tarihi']].concat(db.rentMonths.map(m=>[m.month,m.periodStart||'',m.periodEnd||'',m.days||30,nameOf(db.customers,byId(db.rentals,m.rentalId).customerId),nameOf(db.owners,byId(db.items,m.itemId).ownerId),nameOf(db.items,m.itemId),m.qty||1,m.unitMonthly||m.unitAmount||m.amount,m.amount,m.invoiceStatus,m.invoiceNo||'',m.invoiceDate||''])); let csv=rows.map(r=>r.map(x=>'"'+String(x).replaceAll('"','""')+'"').join(';')).join('\n'); let blob=new Blob(['\ufeff'+csv],{type:'text/csv;charset=utf-8'}),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='aylik-kira-fatura-takibi.csv';a.click()} if(SERVER_MODE){if(checkServerSession()){db=load();migrate();ensureUsers();showApp();}else{db=seed();migrate();showLogin();}}else{ensureUsers();if(isLoggedIn())showApp();else showLogin();}