ALKAM Logo

Dashboard

Canlı veritabanı üzerinden güncel özet

V-
Toplam Cari
0
Aktif Cari
0
Bu Ay Tahsilat
₺0,00
Toplam Bakiye
₺0,00

Son Hareketler

Yükleniyor...

Risk Özeti

3+ Ay Tahsilatsız
0
Riskli Bakiye
₺0,00
Aktif Ödeme Sözü
0
Gecikmiş Söz
0

Üretim Kontrol Merkezi

Yükleniyor...

Üretim Sıkılaştırma Merkezi

Yükleniyor...

İşlem İz Kısa Defteri

Yükleniyor...

Silme Arşivi

Yükleniyor...

Dinamik Cari Listesi

Bir cari seç.

Cari Toplu Tahakkuk Grid

Bu ekran Excel gibi çalışır. Carileri tikle seç, tarihi ve tutarı satır bazında değiştir, sonra seçili satırları toplu kaydet. İstersen sözleşmeye göre otomatik doldur, sonra satır satır düzelt.
Yükleniyor...

2026 Temiz Başlangıç Merkezi

Açılış Tarihi
31.12.2025
Moka Açılışı
0,00 TL
Ücretli Cari
0
Eksik Ay Kaydı
0

Kural

Yükleniyor...

Belge Türü → Yön

Yükleniyor...

Mart 2026 Satış / Tahakkuk Olmayanlar

Yükleniyor...

Nisan 2026 Satış / Tahakkuk Olmayanlar

Yükleniyor...

Ocak 2026'dan İtibaren Eksik Muhasebe Ücreti Ayları

Yükleniyor...

31.12.2025 Açılış Öneri Listesi

Yükleniyor...

2026 Tahakkuk Öneri Listesi

Yükleniyor...

Hesap Özeti

Yükleniyor...

Banka Montaj Paneli

Yükleniyor...

Moka Transfer Paneli

Yükleniyor...

Banka / Moka Montaj Geçmişi

Yükleniyor...

Hesap Hareketi Test Merkezi

Yükleniyor...

Giderler Dinamik Kontrol Paneli

Yükleniyor...

Çalışanlar

Yükleniyor...

Raporlar

Yükleniyor...

Belge / Makbuz Havuzu

Yükleniyor...

Aday Kayıt Köprüsü

Yükleniyor...

Tahsilat Köprüsü Oturumları

Yükleniyor...

Onay Bekleyenler

Yükleniyor...

İşlenenler

Yükleniyor...

Reddedilenler

Yükleniyor...

Operasyon İzleri

Yükleniyor...

Canlıya Alma Kontrolü

Yükleniyor...

Üretim Sıkılaştırma Merkezi

Yükleniyor...

Yedekleme Merkezi

Yükleniyor...

Restore / Geri Yükleme Planı

Yükleniyor...
`; win.document.open(); win.document.write(html); win.document.close(); } function getCariDocuments(id){ return [...state.documents].filter(x=>x.cari_id===id).sort((a,b)=>String(b.document_date || b.created_at || '').localeCompare(String(a.document_date || a.created_at || ''))); } function documentTypeTag(type){ const t=String(type||'').toLowerCase(); if(t==='makbuz') return 'Makbuz'; if(t==='dekont') return 'Dekont'; if(t==='fatura') return 'Fatura'; if(t==='ekstre') return 'Ekstre'; return 'Belge'; } function bridgeCandidatesAsPendingRows(){ return state.bridgeCandidates.map(r=>({ id:r.id, source_type:r.source_type, content_summary:r.content_summary, raw_content:r.raw_content, suggested_cari_name:r.cari_name, suggested_amount:r.suggested_amount, raw_amount:r.suggested_amount, suggested_movement_type:r.suggested_movement_type, confidence_score:r.confidence_score || 70, issue_type:'manual_bridge_candidate', match_reason:r.note || 'Manuel aday kayıt', created_at:r.created_at, is_local_bridge:true })); } function getAllPendingApprovals(){ return [...state.pending, ...bridgeCandidatesAsPendingRows(), ...bankPendingRowsAsApprovalRows()].filter(r=>!isPendingHandled(r)); } function firstDayOfCurrentMonthIso(){ const d = new Date(); return new Date(d.getFullYear(), d.getMonth(), 1).toISOString().slice(0,10); } function feeAmountForCari(c){ const open = getOpenFeePeriod(c?.cari_id); const setup = getFeeSetup(c?.cari_id); return num(open?.current_fee || open?.fee_amount || open?.amount || setup?.monthly_fee_amount || c?.defter_ucreti || 0); } function bulkCariFeeStatusTag(row){ if(!row.line_date) return 'Tarih Yok'; if(num(row.amount)<=0) return 'Tutar Yok'; if(hasSimilarTahakkuk(row.cari_id, row.line_date, row.amount, row.description)) return 'Mükerrer'; return 'Hazır'; } function buildBulkCariFeeBaseRow(c){ const setup = getFeeSetup(c.cari_id); const defaultDate = document.getElementById('bulkCariFeeDefaultDate')?.value || firstDayOfCurrentMonthIso(); const amount = feeAmountForCari(c); return { cari_id:c.cari_id, selected:false, cari_name:c.cari_name || '-', cari_code:c.cari_code || '', taxpayer_type:c.taxpayer_type || '', is_active:c.is_active !== false, opening_date: setup?.opening_date || '', line_date: defaultDate, amount: amount, description: feeDescriptionFromDate(defaultDate), last_tahakkuk_date: c.son_tahakkuk_tarihi || '', last_tahsilat_date: c.son_tahsilat_tarihi || '' }; } function filteredBulkCariCandidates(){ const q=(document.getElementById('bulkCariFeeSearch')?.value || '').trim().toLowerCase(); const type=(document.getElementById('bulkCariFeeTypeFilter')?.value || '').trim(); const status=(document.getElementById('bulkCariFeeStatusFilter')?.value || 'active_fee').trim(); let rows=[...(state.cariler||[])]; if(q){ rows = rows.filter(c=>String(c.cari_name||'').toLowerCase().includes(q) || String(c.cari_code||'').toLowerCase().includes(q)); } if(type){ rows = rows.filter(c=>String(c.taxpayer_type||'')===type); } if(status==='active_fee'){ rows = rows.filter(c=>c.is_active !== false && feeAmountForCari(c) > 0); } else if(status==='active'){ rows = rows.filter(c=>c.is_active !== false); } rows.sort((a,b)=>String(a.cari_name||'').localeCompare(String(b.cari_name||''),'tr')); return rows; } function ensureBulkCariFeeDrafts(reset=false){ const candidates = filteredBulkCariCandidates(); const existing = new Map((state.bulkCariFeeDrafts||[]).map(r=>[String(r.cari_id), r])); const next = candidates.map(c=>{ if(!reset && existing.has(String(c.cari_id))){ const row = existing.get(String(c.cari_id)); return { ...buildBulkCariFeeBaseRow(c), ...row, cari_name:c.cari_name || row.cari_name || '-', cari_code:c.cari_code || row.cari_code || '', taxpayer_type:c.taxpayer_type || row.taxpayer_type || '' }; } return buildBulkCariFeeBaseRow(c); }); state.bulkCariFeeDrafts = next; return next; } function resetCariBulkAccruals(force=false){ state.bulkCariFeeDrafts = []; renderCariBulkAccruals(force); } function updateBulkCariFeeRow(cariId, field, value){ state.bulkCariFeeDrafts = (state.bulkCariFeeDrafts||[]).map(r=>{ if(String(r.cari_id)!==String(cariId)) return r; const next = { ...r, [field]: value }; if(field==='line_date' && (!r.description || r.description===feeDescriptionFromDate(r.line_date || '') || r.description.includes('AYI MUHASEBE ÜCRETİ'))){ next.description = feeDescriptionFromDate(value); } return next; }); renderCariBulkAccruals(); } function setAllBulkCariFeeSelected(flag){ state.bulkCariFeeDrafts = (state.bulkCariFeeDrafts||[]).map(r=>({ ...r, selected: !!flag })); renderCariBulkAccruals(); } function applyContractDefaultsToBulkRows(){ const defaultDate = document.getElementById('bulkCariFeeDefaultDate')?.value || firstDayOfCurrentMonthIso(); state.bulkCariFeeDrafts = (state.bulkCariFeeDrafts||[]).map(r=>{ const c = (state.cariler||[]).find(x=>String(x.cari_id)===String(r.cari_id)); return { ...r, line_date: r.line_date || defaultDate, amount: feeAmountForCari(c || r), description: feeDescriptionFromDate(r.line_date || defaultDate) }; }); renderCariBulkAccruals(); } function applyBulkDefaultsToSelectedRows(){ const defaultDate = document.getElementById('bulkCariFeeDefaultDate')?.value || firstDayOfCurrentMonthIso(); const defaultDesc = (document.getElementById('bulkCariFeeDefaultDesc')?.value || '').trim(); const defaultAmount = num(document.getElementById('bulkCariFeeDefaultAmount')?.value || 0); state.bulkCariFeeDrafts = (state.bulkCariFeeDrafts||[]).map(r=>{ if(!r.selected) return r; const next = { ...r, line_date: defaultDate }; if(defaultAmount > 0) next.amount = defaultAmount; next.description = defaultDesc || feeDescriptionFromDate(defaultDate); return next; }); renderCariBulkAccruals(); } function renderCariBulkAccruals(reset=false){ const target=document.getElementById('bulkCariFeeWrap'); if(!target) return; const rows = ensureBulkCariFeeDrafts(reset); const defaultDateEl = document.getElementById('bulkCariFeeDefaultDate'); if(defaultDateEl && !defaultDateEl.value) defaultDateEl.value = firstDayOfCurrentMonthIso(); const selected = rows.filter(r=>r.selected); const ready = rows.filter(r=>num(r.amount)>0 && r.line_date && !hasSimilarTahakkuk(r.cari_id, r.line_date, r.amount, r.description)); const dup = rows.filter(r=>num(r.amount)>0 && r.line_date && hasSimilarTahakkuk(r.cari_id, r.line_date, r.amount, r.description)); const total = selected.reduce((a,b)=>a+num(b.amount),0); const sumWrap=document.getElementById('bulkCariFeeSummaryWrap'); if(sumWrap){ sumWrap.innerHTML = `
Görünen Cari
${rows.length}
Seçili Satır
${selected.length}
Kayda Hazır
${ready.length}
Mükerrer
${dup.length}
Seçili Toplam
${money(total)}
Milat
${FEE_MILESTONE_START}
`; } if(!rows.length){ target.innerHTML = '
Filtreye uygun cari bulunamadı.
'; return; } target.innerHTML = `
${rows.map(r=>``).join('')}
Seç Cari Kod Tür Açılış Son Tahakkuk Son Tahsilat Tarih Açıklama Tutar Durum
${escapeHtml(r.cari_name || '-')} ${escapeHtml(r.cari_code || '-')} ${escapeHtml(r.taxpayer_type || '-')} ${shortDate(r.opening_date)} ${shortDate(r.last_tahakkuk_date)} ${shortDate(r.last_tahsilat_date)} ${bulkCariFeeStatusTag(r)}
`; } async function saveSelectedCariBulkAccruals(){ const selected = (state.bulkCariFeeDrafts||[]).filter(r=>r.selected); if(!selected.length){ notify('Kaydetmek için satır seç.'); return; } let saved=0, skipped=0, invalid=0; selected.forEach(r=>{ if(!r.line_date || num(r.amount)<=0){ invalid++; return; } if(hasSimilarTahakkuk(r.cari_id, r.line_date, r.amount, r.description)){ skipped++; return; } const cari = (state.cariler||[]).find(x=>String(x.cari_id)===String(r.cari_id)); const row = stampRecordIds({ cari_id:r.cari_id, cari_name:cari?.cari_name || r.cari_name || '-', source_module:'fee_bulk_manual', movement_type:'Tahakkuk', line_date:r.line_date, entry_date:r.line_date, period_key:String(r.line_date).slice(0,7), description:r.description || feeDescriptionFromDate(r.line_date), amount:num(r.amount), debit:num(r.amount), credit:0, target_type:'cari' }, 'LGR'); state.operationalLedger.unshift(row); saved++; }); persistLedger(); await loadSelectedCariHareketler(); renderDashboard(); renderCariList(); renderSelectedCariDetail(); renderReports(); renderCariBulkAccruals(); notify(`Toplu tahakkuk işlendi. Kaydedilen: ${saved} · Mükerrer atlandı: ${skipped} · Eksik satır: ${invalid}`); } function openCariMovementEditModal(rowId, sourceModule){ const row = findCariMovementRow(rowId, sourceModule); if(!row){ notify('Kayıt bulunamadı.'); return; } if(!cariMovementEditable(row)){ notify('Bu satır read-only arşiv kayıt. Düzenleme sadece yerel/aktif kayıtlarda açık.'); return; } document.getElementById('cariMovementEditId').value = String(row.id || ''); document.getElementById('cariMovementEditSource').value = String(row.source_module || ''); document.getElementById('cariMovementEditDate').value = String(row.line_date || row.entry_date || '').slice(0,10); document.getElementById('cariMovementEditAmount').value = num(row.amount || row.debit || row.credit || 0); document.getElementById('cariMovementEditDescription').value = String(row.description || ''); document.getElementById('cariMovementEditDocNo').value = String(row.document_no || ''); document.getElementById('cariMovementEditNote').value = String(row.note || ''); document.getElementById('cariMovementEditInfo').innerHTML = `${escapeHtml(displayTxnNo(row,'HRK'))}
Kaynak: ${escapeHtml(String(row.source_module||''))} · İşlem: ${escapeHtml(String(row.movement_type||''))}`; fillAccountSelect('cariMovementEditAccount'); const accountWrap = document.getElementById('cariMovementEditAccountWrap'); const docWrap = document.getElementById('cariMovementEditDocWrap'); if(String(row.source_module||'')==='local_cari_collection'){ const col = findLocalCollectionById(row.id); accountWrap.style.display = ''; docWrap.style.display = ''; if(col?.account_key && [...document.getElementById('cariMovementEditAccount').options].some(o=>o.value===col.account_key)) document.getElementById('cariMovementEditAccount').value = col.account_key; document.getElementById('cariMovementEditNote').value = String(col?.note || ''); document.getElementById('cariMovementEditDocNo').value = String(col?.document_no || ''); } else { accountWrap.style.display = 'none'; docWrap.style.display = 'none'; } document.getElementById('cariMovementEditModal').style.display = 'flex'; } function closeCariMovementEditModal(){ document.getElementById('cariMovementEditModal').style.display = 'none'; } async function saveCariMovementEdit(){ const rowId = document.getElementById('cariMovementEditId').value; const source = document.getElementById('cariMovementEditSource').value; const row = findCariMovementRow(rowId, source); if(!row){ notify('Kayıt bulunamadı.'); return; } const lineDate = document.getElementById('cariMovementEditDate').value; const amount = num(document.getElementById('cariMovementEditAmount').value); const description = (document.getElementById('cariMovementEditDescription').value || '').trim(); const documentNo = (document.getElementById('cariMovementEditDocNo').value || '').trim(); const note = (document.getElementById('cariMovementEditNote').value || '').trim(); if(!lineDate || amount<=0 || !description){ notify('Tarih, açıklama ve tutar zorunlu.'); return; } if(source==='local_cari_collection'){ if(hasSimilarTahsilatExceptId(state.selectedCariId, lineDate, amount, rowId)){ notify('Benzer tahsilat kaydı var. Önce kontrol et.'); return; } const collections = getLocalCariCollections(); const current = collections.find(x=>String(x.id)===String(rowId)); if(!current){ notify('Tahsilat kaynağı bulunamadı.'); return; } const accountKey = document.getElementById('cariMovementEditAccount').value; const account = getAccountByKey(accountKey); if(!account){ notify('Hesap seç.'); return; } const updatedCollection = { ...current, line_date:lineDate, amount, description, document_no:documentNo || null, note:note || null, account_key:account.account_key, account_name:account.account_name, account_type:account.account_type, updated_at:new Date().toISOString() }; setLocalCariCollections(collections.map(x=>String(x.id)===String(rowId) ? updatedCollection : x)); await upsertLiveRow('cari_collections', updatedCollection); const linkedOps = findCollectionLinkedOps(current); const updatedOps = getLocalAccountOps().map(op=>{ const isLinked = linkedOps.some(l=>String(l.id)===String(op.id)); if(!isLinked) return op; const next = { ...op, collection_id:updatedCollection.id, account_key:account.account_key, account_name:account.account_name, account_type:account.account_type, amount, entry_date:lineDate, note:note || documentNo || op.note || null, updated_at:new Date().toISOString() }; return next; }); setLocalAccountOps(updatedOps); for(const linked of linkedOps){ const next = updatedOps.find(x=>String(x.id)===String(linked.id)); if(next) await upsertLiveRow('account_ops', next); } removeExpectedCashflowsForCollection(current); const expectedRows = buildExpectedCashflowRows({ cari:(state.cariler||[]).find(x=>String(x.cari_id)===String(state.selectedCariId)), amount, lineDate, flowType:updatedCollection.flow_type || 'direct', installmentCount:updatedCollection.installment_count || 1, expectedDate:updatedCollection.expected_date || '', settlementAccount:getAccountByKey(updatedCollection.settlement_account_key || ''), sourceAccount:account, description, note, documentNo, collectionId:updatedCollection.id }); if(expectedRows.length){ setExpectedCashflows([...expectedRows, ...getExpectedCashflows()]); } addAuditEvent('cari_collection_updated',{source_module:'cari_collection_edit', target_name:updatedCollection.cari_name || '-', amount, note:description, summary:`Cari tahsilat güncellendi · ${money(amount)}`}); } else if(source==='fee_auto' || source==='fee_bulk_manual'){ if(hasSimilarTahakkukExceptId(state.selectedCariId, lineDate, amount, description, rowId)){ notify('Benzer tahakkuk kaydı var. Önce kontrol et.'); return; } state.operationalLedger = (state.operationalLedger||[]).map(x=>{ if(String(x.id)!==String(rowId)) return x; return { ...x, line_date:lineDate, entry_date:lineDate, period_key:String(lineDate).slice(0,7), description, amount, debit:amount, credit:0, updated_at:new Date().toISOString() }; }); persistLedger(); addAuditEvent('cari_fee_row_updated',{source_module:'cari_fee_edit', target_name:(state.cariler||[]).find(x=>String(x.cari_id)===String(state.selectedCariId))?.cari_name || '-', amount, note:description, summary:`Cari tahakkuk güncellendi · ${money(amount)}`}); } else { notify('Bu kayıt tipi düzenlenemez.'); return; } closeCariMovementEditModal(); await Promise.all([loadDocuments(), loadAccounts(), loadCariler()]); await loadSelectedCariHareketler(); renderDashboard(); renderCariList(); renderSelectedCariDetail(); renderAccounts(); renderDocuments(); renderReports(); notify('Kayıt güncellendi.'); } function deleteCariMovementEdit(){ const rowId = document.getElementById('cariMovementEditId').value; const source = document.getElementById('cariMovementEditSource').value; const row = findCariMovementRow(rowId, source); if(!row){ notify('Kayıt bulunamadı.'); return; } openDeleteAuthModal(`Cari kayıt silme · ${displayTxnNo(row,'HRK')}`, async (reason)=>{ if(source==='local_cari_collection'){ const collections = getLocalCariCollections(); const current = collections.find(x=>String(x.id)===String(rowId)); if(current){ archiveDeletion('cari_collections', current, reason); setLocalCariCollections(collections.filter(x=>String(x.id)!==String(rowId))); await deleteLiveRow('cari_collections', rowId); const linkedOps = findCollectionLinkedOps(current); if(linkedOps.length){ linkedOps.forEach(op=>archiveDeletion('account_ops', op, reason)); setLocalAccountOps(getLocalAccountOps().filter(op=>!linkedOps.some(l=>String(l.id)===String(op.id)))); for(const op of linkedOps){ await deleteLiveRow('account_ops', op.id); } } removeExpectedCashflowsForCollection(current); } } else if(source==='fee_auto' || source==='fee_bulk_manual'){ const current = (state.operationalLedger||[]).find(x=>String(x.id)===String(rowId)); if(current){ archiveDeletion('operational_ledger', current, reason); state.operationalLedger = (state.operationalLedger||[]).filter(x=>String(x.id)!==String(rowId)); persistLedger(); } } else { notify('Bu kayıt tipi silinemez.'); return; } closeCariMovementEditModal(); await Promise.all([loadDocuments(), loadAccounts(), loadCariler()]); await loadSelectedCariHareketler(); renderDashboard(); renderCariList(); renderSelectedCariDetail(); renderAccounts(); renderDocuments(); renderReports(); notify('Kayıt silindi.'); }); } function renderSelectedCariDetail(){ const c=state.cariler.find(x=>x.cari_id===state.selectedCariId); const card=getCariCard(state.selectedCariId); const target=document.getElementById("selectedCariDetail"); if(!c){target.innerHTML=`
Bir cari seç.
`; return} const meta=getCurrentStatementMeta(); const docs=getCariDocuments(state.selectedCariId).slice(0,5); target.innerHTML=`
${escapeHtml(c.cari_name || 'Cari')}
Kod: ${escapeHtml(c.cari_code || '-')} Tür: ${escapeHtml(c.taxpayer_type || card?.taxpayer_type || '-')} ${c.is_tahsilat_3_ay_gecikmis?'3+ AY TAHSİLAT YOK':''} ${c.aktif_odeme_sozu_var?'ÖDEME SÖZÜ':''} ${meta.isConsistent?'BAKİYE UYUMLU':'BAKİYE KONTROL'}
${state.selectedLedgerView==='allhistory' ? 'Geçmiş kayıtlar görünür ama read-only arşivdir; bakiye motoruna müdahale etmez.' : 'Canlı görünüm açılış bakiyesi ve aktif hareketlerle çalışır.'}
Cari Kodu
${escapeHtml(c.cari_code||"-")}
Mükellef Tipi
${escapeHtml(c.taxpayer_type||card?.taxpayer_type||"-")}
Aktif Ücret
${money(card?.monthly_fee_amount||c.defter_ucreti||0)}
Açılış Tarihi
${shortDate(getFeeSetup(state.selectedCariId)?.opening_date)}
Skor
${Number(c.score_total||0).toFixed(0)} / 100
Son Tahakkuk
${shortDate(c.son_tahakkuk_tarihi)}
${money(c.son_tahakkuk_tutari||0)}
Son Tahsilat
${shortDate(c.son_tahsilat_tarihi)}
${money(c.son_tahsilat_tutari||0)}
Yetkili
${escapeHtml(card?.contact_person||"-")}
${escapeHtml(card?.phone||"-")}
E-posta
${escapeHtml(card?.email||"-")}
Güncel Bakiye
${balanceLabel(c.bakiye||0)}
Açılış Bakiye
${balanceLabel(meta.openingBalance)}
Filtre Kapanış
${balanceLabel(meta.closingBalance)}
Bu Ay
Tahakkuk ${money(c.bu_ay_tahakkuk||0)}
Tahsilat ${money(c.bu_ay_tahsilat||0)}
Tahsilat İstihbaratı
${c.is_tahsilat_3_ay_gecikmis?'3+ AY TAHSİLAT YOK':'Normal'}${c.aktif_odeme_sozu_var?`
Söz: ${shortDate(c.aktif_odeme_sozu_tarihi)} · ${money(c.aktif_odeme_sozu_tutari||0)} · ${escapeHtml(c.aktif_odeme_sozu_durum||"-")}
`:''}
Standart Kontrol
${meta.isConsistent?'UYUMLU':'EXTRE / BAKİYE FARKI'}
Fark: ${money(meta.diff)}
Ücret Yönetimi
${feeAccrualRowsForCari(state.selectedCariId).filter(x=>String(x.line_date||'').slice(0,4)===String(new Date().getFullYear())).length} adet bu yıl tahakkuk
${getFeeControl(state.selectedCariId).length} kontrol kaydı
${buildStandardStatementTable(meta.rowsDesc,{editable:true})}

Cari Belge Geçmişi

${docs.length ? `${docs.map(d=>``).join('')}
TarihBelgeKaynakTutar
${shortDate(d.document_date || d.created_at)}${documentTypeTag(d.document_type)} ${escapeHtml(d.document_title || d.document_no || '-')}
${escapeHtml(d.note || '-')}
${escapeHtml(d.source_type || '-')}${money(d.amount || 0)}
` : `
Bu cariye bağlı belge yok.
`}
`; target.querySelectorAll("#detailDateChips .chip").forEach(btn=>btn.addEventListener("click",()=>{state.selectedRange=btn.dataset.range; renderSelectedCariDetail();})); target.querySelectorAll("#detailLedgerViewChips .chip").forEach(btn=>btn.addEventListener("click",()=>{state.selectedLedgerView=btn.dataset.ledgerView; renderSelectedCariDetail();})); target.querySelectorAll('[data-cari-doc-id]').forEach(el=>el.addEventListener('click',()=>openDocumentDetailModal(el.dataset.cariDocId))); target.querySelectorAll('[data-cari-edit-id]').forEach(el=>el.addEventListener('click',()=>openCariMovementEditModal(el.dataset.cariEditId, el.dataset.cariEditSource))); } function getOpenFeePeriod(id){ const rows = getFeeTimeline(id); return rows.find(r=>!r.valid_to) || null; } function renderFeeModal(){ const c=state.cariler.find(x=>x.cari_id===state.selectedCariId); if(!c){ document.getElementById("feeTimelineWrap").innerHTML = `
Cari seçili değil.
`; document.getElementById("feeControlWrap").innerHTML = `
Cari seçili değil.
`; return; } const periods = [...getFeeTimeline(state.selectedCariId)].sort((a,b)=>String(b.valid_from||"").localeCompare(String(a.valid_from||""))); const controls = [...getFeeControl(state.selectedCariId)].sort((a,b)=>String(b.period_start||"").localeCompare(String(a.period_start||""))); document.getElementById("feeTimelineWrap").innerHTML = periods.length ? `${periods.map(r=>``).join("")}
BaşlangıçBitişÜcretDurumKaynakNot
${shortDate(r.valid_from)}${r.valid_to ? shortDate(r.valid_to) : 'Açık'}${money(r.fee_amount || r.current_fee || r.amount || 0)}${!r.valid_to ? 'Aktif Dönem' : 'Kapalı'}${String(r.source||'')==='local_first' || String(r.sync_status||'')==='local_only' ? 'Yerel Kayıt' : 'Canlı'}${escapeHtml(r.note || r.change_reason || '-')}
` : `
Bu caride ücret geçmişi yok.
`; document.getElementById("feeControlWrap").innerHTML = `
Milat: ${FEE_MILESTONE_START}. Bu tarihten itibaren her ayın 1\'i tahakkuk olarak taranır.
` + (controls.length ? `${controls.map(r=>``).join("")}
DönemDurumBeklenenBulunanAçıklama
${shortDate(r.period_start)}${r.period_end ? ' - ' + shortDate(r.period_end) : ''}${renderTag(r.control_status || r.issue_type || (num(r.diff_amount) !== 0 ? 'farkli_tutar' : 'tamam'))}${money(r.expected_amount || r.expected_fee_amount || r.current_fee || 0)}${money(r.existing_amount || r.actual_amount || r.posted_amount || 0)}${escapeHtml(r.note || r.issue_reason || r.status_note || '-')}
` : `
Bu cari için kontrol sorunu görünmüyor.
`); renderFeeYearSuggestWrap(); } function openFeeModal(){ if(!state.selectedCariId){ notify("Cari seçili değil."); return; } const openPeriod = getOpenFeePeriod(state.selectedCariId); const setup = getFeeSetup(state.selectedCariId); fillFeeYearSelect(); const startValue = openPeriod?.valid_from || setup?.opening_date || isoDate(new Date()) || ""; document.getElementById("feeValidFrom").value = startValue; const y = String(startValue||"").slice(0,4) || String(new Date().getFullYear()); document.getElementById("feeYearSelect").value = y; document.getElementById("feeAmountInput").value = openPeriod ? num(openPeriod.fee_amount || openPeriod.current_fee || openPeriod.amount) : ""; document.getElementById("feeNoteInput").value = ""; document.getElementById("feeAutoClosePrev").value = "true"; const yearEl=document.getElementById("feeYearSelect"); if(yearEl){ yearEl.onchange=syncFeeDateFromYear; } renderFeeModal(); document.getElementById("feeModal").style.display = "flex"; } function closeFeeModal(){ document.getElementById("feeModal").style.display = "none"; } async function saveFeePeriod(){ if(!state.selectedCariId){ notify("Cari seçili değil."); return; } const validFrom = document.getElementById("feeValidFrom").value; const feeAmount = num(document.getElementById("feeAmountInput").value); const note = document.getElementById("feeNoteInput").value.trim() || 'Yıllık ücret dönemi'; const autoClosePrev = document.getElementById("feeAutoClosePrev").value === "true"; if(!validFrom || feeAmount <= 0){ notify("Başlangıç tarihi ve ücret zorunlu."); return; } const openPeriod = getOpenFeePeriod(state.selectedCariId); if(openPeriod && String(openPeriod.valid_from) === validFrom && num(openPeriod.fee_amount || openPeriod.current_fee || openPeriod.amount) === feeAmount){ notify("Aynı başlangıç tarihi ve tutarda açık dönem zaten var."); return; } if(openPeriod && !autoClosePrev && !openPeriod.valid_to){ notify("Açık dönem var. Yeni dönem eklemek için önce eski dönemi kapat."); return; } if(openPeriod && autoClosePrev && new Date(openPeriod.valid_from) >= new Date(validFrom)){ notify("Yeni dönem tarihi mevcut açık dönem başlangıcından sonra olmalı."); return; } if(openPeriod && autoClosePrev){ const closePayload = { valid_to: dayBefore(validFrom), updated_at:new Date().toISOString() }; const closeRes = await supabaseClient.from("cari_fee_history").update(feeHistoryUpdatePayload(closePayload)).eq("id", openPeriod.id); if(closeRes.error){ notify("Önceki dönem kapatılamadı: " + closeRes.error.message); return; } } const insertPayload = { cari_id: state.selectedCariId, fee_amount: feeAmount, valid_from: validFrom, valid_to: null, note, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; const ins = await supabaseClient.from("cari_fee_history").insert([insertPayload]); if(ins.error){ notify("Yeni ücret dönemi eklenemedi: " + ins.error.message); return; } await supabaseClient.from("cari_cards").update({ monthly_fee_amount: feeAmount, monthly_fee_enabled: true, updated_at:new Date().toISOString() }).eq("id", state.selectedCariId); notify("Ücret dönemi kaydedildi."); await Promise.all([loadCariler(), loadFeeData()]); renderSelectedCariDetail(); renderReports(); renderFeeModal(); } async function closeActiveFeePeriodToday(){ if(!state.selectedCariId){ notify("Cari seçili değil."); return; } const openPeriod = getOpenFeePeriod(state.selectedCariId); if(!openPeriod){ notify("Açık ücret dönemi yok."); return; } const today = new Date().toISOString().slice(0,10); if(String(openPeriod.source||'')==='local_first') closeLocalFeePeriod(openPeriod.id, today); let syncMessage = 'Yerel kayıt güncellendi.'; try{ if(String(openPeriod.source||'')!=='local_first'){ const res = await supabaseClient.from("cari_fee_history").update(feeHistoryUpdatePayload({ valid_to:today, updated_at:new Date().toISOString() })).eq("id", openPeriod.id); if(res.error) throw res.error; syncMessage = 'Canlı tablo da güncellendi.'; } }catch(err){ logSystemError('fee_history_sync', 'Açık dönem canlı kapatılamadı', { note:String(err?.message || err || '') }); } await Promise.all([loadCariler(), loadFeeData()]); renderSelectedCariDetail(); renderReports(); renderFeeModal(); notify(`Açık ücret dönemi kapatıldı. ${syncMessage}`); } function approvalConfidence(r){ return num(r.confidence_score ?? r.match_confidence ?? r.guven_puani ?? r.ai_score ?? r.suggested_confidence); } function approvalFlags(r){ const flags = []; const conf = approvalConfidence(r); const text = `${r.issue_type || ''} ${r.content_summary || ''} ${r.raw_content || ''}`.toLowerCase(); if(text.includes("duplicate") || text.includes("çift")) flags.push('Muhtemel Çift'); if(conf && conf < 95) flags.push('Düşük Güven'); if(text.includes("whatsapp") || text.includes("telegram") || text.includes("gorsel") || text.includes("görsel")) flags.push('Belge Adayı'); return flags.join(' ') || 'Standart'; } function bankRowConfidence(r){ return approvalConfidence(r); } function bankSuggestedTarget(r){ return r.suggested_cari_name || r.suggested_expense_name || r.suggested_financial_account_name || r.suggested_financial_account_type || "-"; } function normalizeBankText(v=''){ return String(v||'').toLocaleLowerCase('tr-TR').replace(/\s+/g,' ').trim(); } function bankOriginKey(r,bucket='bank'){ const desc = normalizeBankText(r.description || r.raw_content || ''); const ref = normalizeBankText(r.external_ref || r.reference_no || r.ref_no || ''); const amount = num(r.net_amount || r.amount || r.raw_amount || r.final_amount || 0).toFixed(2); const date = String(r.movement_date || r.line_date || '').slice(0,10); const acct = normalizeBankText(r.bank_account || r.account_no || r.iban || bucket); return `${bucket}|${acct}|${date}|${amount}|${desc}|${ref}`; } function bankDuplicateCandidate(r,bucket='bank'){ const key = bankOriginKey(r,bucket); const processed = (state.approvalOps?.processed || []).some(x=>String(x.bank_origin_key||'')===key); const pending = (state.bankOps?.pending || []).some(x=>String(x.bank_origin_key||'')===key); const history = (state.bankOps?.history || []).filter(x=>String(x.origin_key||x.row_key||'')===key).length; return processed || pending || history > 1; } function bankSuspiciousCandidate(r){ const conf = bankRowConfidence(r); const text = `${r.match_reason || ''} ${r.eslesme_sebebi || ''} ${r.description || ''} ${r.issue_type || ''}`.toLowerCase(); return !r.suggested_financial_account_type || !bankSuggestedTarget(r) || bankSuggestedTarget(r)==="-" || (conf && conf < 95) || text.includes('onay') || text.includes('zayif') || text.includes('zayıf') || text.includes('kontrol'); } function bankNeedsApproval(r){ const text = `${r.match_reason || ''} ${r.eslesme_sebebi || ''} ${r.description || ''} ${r.issue_type || ''}`.toLowerCase(); return bankSuspiciousCandidate(r) || text.includes('duplicate') || text.includes('çift'); } function bankControlBadge(r){ if(bankNeedsApproval(r)) return 'Onaya Düşmeli'; if((bankRowConfidence(r)||0) >= 95) return 'İşlenebilir'; return 'Kontrol Et'; } function mokaRowClass(r){ const text = `${r.eslesme_sebebi || ''} ${r.description || ''} ${r.match_reason || ''}`.toLowerCase(); if(text.includes('banka') || text.includes('aktar') || text.includes('virman') || text.includes('transfer')) return 'banka_aktarim'; if(text.includes('moka') || text.includes('pos') || text.includes('kredi kart')) return 'moka_tahsilat'; return 'belirsiz'; } function mokaRecommendedAction(r){ const cls = mokaRowClass(r); if(cls === 'moka_tahsilat') return 'Önce Moka United tahsilatı'; if(cls === 'banka_aktarim') return 'Sonra banka mahsup'; return 'Onaya düşür / manuel incele'; } function bankRowKey(r,bucket='bank'){ return `${bucket}|${r.id || ''}|${r.movement_date || ''}|${r.description || ''}|${num(r.net_amount || r.amount || 0).toFixed(2)}`; } function bankStatusInfo(r,bucket='bank'){ return state.bankOps?.row_status?.[bankRowKey(r,bucket)] || null; } function bankStatusCode(r,bucket='bank'){ const info = bankStatusInfo(r,bucket); if(info?.status) return info.status; if(bankDuplicateCandidate(r,bucket)) return 'duplicate'; if(bankNeedsApproval(r)) return 'needs_approval'; return 'processable'; } function bankStatusBadge(r,bucket='bank'){ const code = bankStatusCode(r,bucket); if(code==='queued') return 'Onaya Gönderildi'; if(code==='processed') return 'İşlendi'; if(code==='ignored') return 'İşlenmeyen'; if(code==='rejected') return 'Reddedildi'; if(code==='duplicate') return 'Mükerrer Adayı'; if(code==='needs_approval') return 'Şüpheli / Onay'; return 'İşlenebilir'; } function bankStatusNote(r,bucket='bank'){ const info = bankStatusInfo(r,bucket); if(info?.note) return info.note; if(bankDuplicateCandidate(r,bucket)) return 'Aynı banka anahtarıyla daha önce işlenmiş veya kuyruğa alınmış kayıt bulundu'; return r.match_reason || r.eslesme_sebebi || (bankNeedsApproval(r) ? 'Operatör kararı gerekli' : 'Standart eşleşme'); } function bankPendingRowsAsApprovalRows(){ return (state.bankOps.pending || []).map(r=>({...r, is_local_bank_queue:true})); } function rowMatchesBankFilter(r,bucket='bank'){ const filter = bucket==='bank' ? state.bankFilter : state.mokaFilter; const code = bankStatusCode(r,bucket); if(filter==='all') return true; if(filter==='processable') return code==='processable'; if(filter==='needs_approval') return code==='needs_approval'; if(filter==='duplicate') return code==='duplicate'; if(filter==='queued') return code==='queued'; if(filter==='processed') return code==='processed'; if(filter==='ignored') return code==='ignored' || code==='rejected'; if(bucket==='moka' && filter==='moka_tahsilat') return mokaRowClass(r)==='moka_tahsilat'; if(bucket==='moka' && filter==='banka_aktarim') return mokaRowClass(r)==='banka_aktarim'; return true; } function getBankTargetDefaults(r,bucket='bank'){ if(bucket==='moka'){ const cls=mokaRowClass(r); if(cls==='moka_tahsilat') return {targetType:'moka_united', movementType:'tahsilat', targetName:'Moka United', note:'Önce Moka United tahsilatı'}; if(cls==='banka_aktarim') return {targetType:'hesap', movementType:'mahsup', targetName:'Moka United -> Banka', note:'Moka aktarımı banka mahsup'}; } return { targetType: bankNeedsApproval(r) ? 'cari' : (r.suggested_financial_account_type || 'hesap'), movementType: (r.suggested_movement_type || (bankNeedsApproval(r) ? 'tahsilat' : 'mahsup')), targetName: bankSuggestedTarget(r) === '-' ? '' : bankSuggestedTarget(r), note: bankStatusNote(r,bucket) }; } function syncBankTargetNameFromCari(){ const cariId=document.getElementById('bankDecisionCariId')?.value || ''; const input=document.getElementById('bankDecisionTargetName'); if(input && cariId){ const cari=state.cariler.find(x=>String(x.cari_id)===String(cariId)); if(cari) input.value = cari.cari_name || ''; } renderBankActionCariPreview(); } async function renderBankActionCariPreview(){ const target=document.getElementById('bankActionCariPreview'); if(!target) return; const targetType=document.getElementById('bankDecisionTargetType')?.value || 'cari'; const cariId=document.getElementById('bankDecisionCariId')?.value || ''; if(targetType!=='cari'){ target.innerHTML = '
Sağ panel yalnızca cari hedefinde canlı ekstre önizlemesi gösterir.
'; return; } if(!cariId){ target.innerHTML = '
Önizleme için cari seç.
'; return; } target.innerHTML = '
Cari ekstre önizlemesi hazırlanıyor...
'; try{ const c = state.cariler.find(x=>String(x.cari_id)===String(cariId)); const card = getCariCard(cariId); const { data } = await supabaseClient.from("v_cari_hareket_dokumu").select("*").eq("cari_id",cariId).order("line_date",{ascending:false}).limit(500); const localRows = buildLocalCariMovementRows(cariId); const localFeeRows = feeAccrualRowsForCari(cariId); const allRows = [ ...(data||[]), ...localRows, ...localFeeRows ].map(x=>({...x, txn_id:txnId(x,'HRK')})); const rowsAsc = computeRunningRows(allRows, null); const rowsDesc = [...rowsAsc].reverse(); const totalDebit = rowsAsc.reduce((a,r)=>a+num(r.debit),0); const totalCredit = rowsAsc.reduce((a,r)=>a+num(r.credit),0); const closingBalance = rowsAsc.length ? num(rowsAsc[rowsAsc.length-1].running_balance) : 0; const systemBalance = c ? num(c.bakiye) : closingBalance; const diff = Math.abs(closingBalance - systemBalance); const sonTahakkuk = rowsDesc.find(r=>String(r.movement_type||'').toLowerCase().includes('tahakkuk')); const sonTahsilat = rowsDesc.find(r=>String(r.movement_type||'').toLowerCase().includes('tahsilat')); target.innerHTML = `
Canlı Cari Ekstre Önizlemesi
${escapeHtml(c?.cari_name || '-')}
${renderTag(card?.taxpayer_type || c?.taxpayer_type || '-')} ${diff < 0.01 ? 'BAKİYE UYUMLU' : 'BAKİYE FARKI'}
Cari Kodu
${escapeHtml(c?.cari_code || '-')}
Aktif Ücret
${money(card?.monthly_fee_amount || c?.defter_ucreti || 0)}
Açılış Tarihi
${shortDate(getFeeSetup(cariId)?.opening_date)}
Skor
${Number(c?.score_total || 0).toFixed(0)} / 100
Son Tahakkuk
${shortDate(sonTahakkuk?.line_date)}
${money(sonTahakkuk?.debit || 0)}
Son Tahsilat
${shortDate(sonTahsilat?.line_date)}
${money(sonTahsilat?.credit || 0)}
Güncel Bakiye
${balanceLabel(systemBalance)}
Extre Kapanış
${balanceLabel(closingBalance)}
Fark: ${money(diff)}
Toplam Borç
${money(totalDebit)}
Toplam Alacak
${money(totalCredit)}
Satır
${rowsAsc.length}
${buildStandardStatementTable(rowsDesc.slice(0,20))}
`; }catch(err){ target.innerHTML = '
Cari ekstre önizlemesi yüklenemedi.
'; logSystemError('bank_action_cari_preview','Cari ekstre önizleme yüklenemedi',{ note:String(err?.message || err || '') }); } } function getSelectedBankRow(){ const arr = state.selectedBankBucket==='moka' ? state.mokaPanel : state.bankPanel; return arr.find(r=>bankRowKey(r,state.selectedBankBucket)===state.selectedBankKey) || null; } async function openBankActionModal(bucket,key){ state.selectedBankBucket=bucket; state.selectedBankKey=key; const r=getSelectedBankRow(); if(!r){ notify('Banka satırı bulunamadı.'); return; } const defs=getBankTargetDefaults(r,bucket); const info=bankStatusInfo(r,bucket); const amount=num(r.net_amount || r.amount || 0); const recommended=bucket==='moka' ? mokaRecommendedAction(r) : (bankNeedsApproval(r) ? 'Önce onay merkezine gönder' : 'Doğrudan işlenebilir'); const matchedCari = state.cariler.find(c=>String(c.cari_name||'')===String(defs.targetName||'')) || null; const content=`
İşlem ID
${escapeHtml(txnId(r,bucket==='moka' ? 'MOK' : 'BNK'))}
Kaynak
${escapeHtml(bucket==='moka' ? 'Moka' : 'Banka')}
Durum
${bankStatusBadge(r,bucket)}
Güven
${bankRowConfidence(r) ? bankRowConfidence(r).toFixed(0) : '-'}
Tutar
${money(amount)}
Açıklama
${escapeHtml(r.description || '-')}
Operasyon Kuralı
${escapeHtml(recommended)}
${escapeHtml(bankStatusNote(r,bucket))}
Cari ekstre önizlemesi hazırlanıyor...
`; document.getElementById('bankActionContent').innerHTML=content; document.getElementById('bankActionModal').style.display='flex'; await renderBankActionCariPreview(); } function closeBankActionModal(){ document.getElementById('bankActionModal').style.display='none'; } function updateBankRowStatus(r,bucket,status,note,extra={}){ const originKey = bankOriginKey(r,bucket); state.bankOps.row_status[bankRowKey(r,bucket)] = {status, note: note || '', updated_at:new Date().toISOString(), bucket, amount:num(r.net_amount||r.amount||0), description:r.description||'', origin_key:originKey, ...extra}; state.bankOps.history = state.bankOps.history || []; state.bankOps.history.unshift({status, note: note || '', updated_at:new Date().toISOString(), bucket, row_key:bankRowKey(r,bucket), origin_key:originKey, amount:num(r.net_amount||r.amount||0), description:r.description||''}); persistBankOps(); } function queueSelectedBankRowToApproval(){ const r=getSelectedBankRow(); if(!r){ notify('Satır bulunamadı.'); return; } const targetType=document.getElementById('bankDecisionTargetType')?.value || 'cari'; const movementType=document.getElementById('bankDecisionMovementType')?.value || 'tahsilat'; const amount=num(document.getElementById('bankDecisionAmount')?.value || r.net_amount || 0); const targetName=(document.getElementById('bankDecisionTargetName')?.value || '').trim(); const note=(document.getElementById('bankDecisionNote')?.value || '').trim() || 'Banka panelinden onaya gönderildi'; const forceOk=!!document.getElementById('bankDecisionForceCheck')?.checked; if(!targetName){ notify('Hedef adı gir.'); return; } if((bankNeedsApproval(r) || state.selectedBankBucket==='moka') && !forceOk){ notify('Kontrol kutusunu işaretle.'); return; } const originKey = bankOriginKey(r,state.selectedBankBucket); state.bankOps.pending = (state.bankOps.pending || []).filter(x=>String(x.bank_origin_key)!==String(originKey)); const pendingId=uid('bank_pending'); const pendingRow={ id:pendingId, txn_id:pendingId, source_type: state.selectedBankBucket==='moka' ? 'moka_panel' : 'banka_panel', content_summary:r.description || 'Banka satırı', raw_content:`${shortDate(r.movement_date)} | ${r.description || '-'} | ${money(amount)}`, suggested_cari_name: targetType==='cari' ? targetName : null, suggested_expense_name: targetType==='gider' ? targetName : null, suggested_financial_account_name: targetType==='hesap' || targetType==='moka_united' ? targetName : null, suggested_amount: amount, raw_amount: amount, suggested_movement_type: movementType, confidence_score: bankRowConfidence(r) || 80, issue_type:'bank_manual_queue', match_reason: note, created_at:new Date().toISOString(), bank_origin_key: originKey, bank_origin_bucket: state.selectedBankBucket, is_local_bank_queue:true }; state.bankOps.pending = state.bankOps.pending || []; state.bankOps.pending.unshift(pendingRow); updateBankRowStatus(r,state.selectedBankBucket,'queued',note,{pending_id:pendingId,target_type:targetType,target_name:targetName,movement_type:movementType}); persistBankOps(); addAuditEvent('bank_queued_for_approval',{source_module:state.selectedBankBucket==='moka'?'moka_panel':'bank_panel', target_name:targetName, amount, note, summary:`Banka/Moka satırı onaya gönderildi · ${targetName}`}); closeBankActionModal(); renderAccounts(); renderApprovals(); notify('Satır onay merkezine gönderildi.'); } function processSelectedBankRowDirect(){ const r=getSelectedBankRow(); if(!r){ notify('Satır bulunamadı.'); return; } const targetType=document.getElementById('bankDecisionTargetType')?.value || 'cari'; const movementType=document.getElementById('bankDecisionMovementType')?.value || 'tahsilat'; const amount=num(document.getElementById('bankDecisionAmount')?.value || r.net_amount || 0); const targetName=(document.getElementById('bankDecisionTargetName')?.value || '').trim(); const note=(document.getElementById('bankDecisionNote')?.value || '').trim() || 'Banka panelinden doğrudan işlendi'; const forceOk=!!document.getElementById('bankDecisionForceCheck')?.checked; if(!targetName){ notify('Hedef adı gir.'); return; } if((bankNeedsApproval(r) || state.selectedBankBucket==='moka') && !forceOk){ notify('Kontrol kutusunu işaretle.'); return; } const originKey = bankOriginKey(r,state.selectedBankBucket); state.bankOps.pending = (state.bankOps.pending || []).filter(x=>String(x.bank_origin_key)!==String(originKey)); const bankRef = decisionRef('BNK'); const bankProcessedId=uid('bank_processed'); const processed={ id:bankProcessedId, txn_id:bankProcessedId, decision_ref: bankRef, source_type: state.selectedBankBucket==='moka' ? 'moka_panel' : 'banka_panel', content_summary:r.description || 'Banka satırı', raw_content:r.description || '', final_target_type:targetType, final_cari_name: targetType==='cari' ? targetName : null, final_expense_name: targetType==='gider' ? targetName : null, final_financial_account_name: targetType==='hesap' || targetType==='moka_united' ? targetName : null, final_movement_type:movementType, final_amount:amount, decision_note:note, processed_at:new Date().toISOString(), processed_locally:true, bank_origin_key: originKey, bank_origin_bucket: state.selectedBankBucket }; state.approvalOps.processed.unshift(processed); persistApprovalOps(); learnFromBankDecision(r,targetType,targetName,movementType); updateBankRowStatus(r,state.selectedBankBucket,'processed',note,{target_type:targetType,target_name:targetName,movement_type:movementType,decision_ref:bankRef}); addLedgerEntry({ledger_ref:bankRef, source_module:state.selectedBankBucket==='moka'?'moka_panel':'bank_panel', source_summary:r.description || 'Banka satırı', target_type:targetType, target_name:targetName, movement_type:movementType, amount, decision_note:note}); addAuditEvent('bank_processed_direct',{event_ref:bankRef, source_module:state.selectedBankBucket==='moka'?'moka_panel':'bank_panel', source_type:r.source_type || state.selectedBankBucket, target_name:targetName, target_type:targetType, movement_type:movementType, amount, note}); closeBankActionModal(); renderAccounts(); renderApprovals(); notify('Satır işlendi ve eşleşme hafızaya alındı.'); } function ignoreSelectedBankRow(){ const r=getSelectedBankRow(); if(!r){ notify('Satır bulunamadı.'); return; } const note=(document.getElementById('bankDecisionNote')?.value || '').trim() || 'Operatör tarafından işlenmeyen olarak bırakıldı'; const originKey = bankOriginKey(r,state.selectedBankBucket); state.bankOps.pending = (state.bankOps.pending || []).filter(x=>String(x.bank_origin_key)!==String(originKey)); updateBankRowStatus(r,state.selectedBankBucket,'ignored',note,{}); addAuditEvent('bank_ignored',{source_module:state.selectedBankBucket==='moka'?'moka_panel':'bank_panel', target_name:r.description || '-', amount:num(r.net_amount||r.amount||0), note, summary:`Banka/Moka satırı işlenmeyen yapıldı · ${r.description || '-'}`}); closeBankActionModal(); renderAccounts(); renderApprovals(); notify('Satır işlenmeyen olarak işaretlendi.'); } function processBankRowFast(bucket,key){ state.selectedBankBucket=bucket; state.selectedBankKey=key; const r=getSelectedBankRow(); if(!r){ notify('Satır bulunamadı.'); return; } const defs=getBankTargetDefaults(r,bucket); const targetType = defs.targetType || r.suggested_financial_account_type || 'cari'; const targetName = defs.targetName || bankSuggestedTarget(r); const movementType = defs.movementType || r.suggested_movement_type || 'tahsilat'; if(!targetName || targetName==='-'){ notify('Hedef belirsiz, detaydan işlemelisin.'); return; } if(bankDuplicateCandidate(r,bucket)){ notify('Bu satır mükerrer adayı. Tek tık işlenmez.'); return; } if(bankNeedsApproval(r) || bucket==='moka'){ openBankActionModal(bucket,key); notify('Bu satır tek tık için yeterince güvenli değil; detay ekranından kontrol et.'); return; } const originKey = bankOriginKey(r,bucket); state.bankOps.pending = (state.bankOps.pending || []).filter(x=>String(x.bank_origin_key)!==String(originKey)); const bankRef = decisionRef('BNK'); const bankProcessedId=uid('bank_processed'); const amount=num(r.net_amount || r.amount || 0); state.approvalOps.processed.unshift({ id:bankProcessedId, txn_id:bankProcessedId, decision_ref: bankRef, source_type: bucket==='moka' ? 'moka_panel' : 'banka_panel', content_summary:r.description || 'Banka satırı', raw_content:r.description || '', final_target_type:targetType, final_cari_name: targetType==='cari' ? targetName : null, final_expense_name: targetType==='gider' ? targetName : null, final_financial_account_name: targetType==='hesap' || targetType==='moka_united' ? targetName : null, final_movement_type:movementType, final_amount:amount, decision_note:'İşlenebilir satır tek tık işlendi', processed_at:new Date().toISOString(), processed_locally:true, bank_origin_key: originKey, bank_origin_bucket: bucket }); persistApprovalOps(); learnFromBankDecision(r,targetType,targetName,movementType); updateBankRowStatus(r,bucket,'processed','İşlenebilir satır tek tık işlendi',{target_type:targetType,target_name:targetName,movement_type:movementType,decision_ref:bankRef}); addLedgerEntry({ledger_ref:bankRef, source_module:bucket==='moka'?'moka_panel':'bank_panel', source_summary:r.description || 'Banka satırı', target_type:targetType, target_name:targetName, movement_type:movementType, amount, decision_note:'İşlenebilir satır tek tık işlendi'}); addAuditEvent('bank_processed_fast',{event_ref:bankRef, source_module:bucket==='moka'?'moka_panel':'bank_panel', source_type:r.source_type || bucket, target_name:targetName, target_type:targetType, movement_type:movementType, amount, note:'İşlenebilir satır tek tık işlendi'}); renderAccounts(); renderApprovals(); notify('Tek tık işlendi ve eşleşme öğrenildi.'); } function renderAccounts(){ const combinedAccounts=ensureVisibleAccounts(); const manualRows=[...getLocalAccountOps()].sort((a,b)=>new Date(b.entry_date||b.created_at||0)-new Date(a.entry_date||a.created_at||0)); const bankRows = state.bankPanel.filter(r=>rowMatchesBankFilter(r,'bank')); const mokaRows = state.mokaPanel.filter(r=>rowMatchesBankFilter(r,'moka')); const bankStats = { total: state.bankPanel.length, processable: state.bankPanel.filter(r=>bankStatusCode(r,'bank')==='processable').length, needs_approval: state.bankPanel.filter(r=>bankStatusCode(r,'bank')==='needs_approval').length, duplicate: state.bankPanel.filter(r=>bankStatusCode(r,'bank')==='duplicate').length, queued: state.bankPanel.filter(r=>bankStatusCode(r,'bank')==='queued').length, processed: state.bankPanel.filter(r=>bankStatusCode(r,'bank')==='processed').length, ignored: state.bankPanel.filter(r=>['ignored','rejected'].includes(bankStatusCode(r,'bank'))).length, }; const mokaStats = { total: state.mokaPanel.length, tahsilat: state.mokaPanel.filter(r=>mokaRowClass(r)==='moka_tahsilat').length, aktarim: state.mokaPanel.filter(r=>mokaRowClass(r)==='banka_aktarim').length, queued: state.mokaPanel.filter(r=>bankStatusCode(r,'moka')==='queued').length, processed: state.mokaPanel.filter(r=>bankStatusCode(r,'moka')==='processed').length, ignored: state.mokaPanel.filter(r=>['ignored','rejected'].includes(bankStatusCode(r,'moka'))).length, }; const cashForecast=getCashForecastSummary(); document.getElementById("accountSummaryWrap").innerHTML=`
Toplam Hesap
${combinedAccounts.length}
Yerel Hesap İşlemi
${manualRows.length}
Banka Onay Kuyruğu
${bankStats.queued}
Net Toplam
${money(combinedAccounts.reduce((a,b)=>a+num(b.net_bakiye),0))}
Bu Ay Kasaya Gelen
${money(cashForecast.actualKasa)}
Bu Ay Kasaya Beklenen
${money(cashForecast.expectedKasa)}
Moka Bekleyen
${money(cashForecast.pendingMoka)}
Çek / Senet Bekleyen
${money(cashForecast.pendingPaper)}
Standart sıra: banka satırı incele → hedefi doğrula → emin değilsen Onay Merkezimükerrer adayıysa kilitle → Moka kaynaklıysa önce Moka United → bankaya aktarım geldiğinde mahsup.
Banka Mutabakatı
${money(combinedAccounts.filter(x=>String(x.account_type)==='banka').reduce((a,b)=>a+num(b.net_bakiye),0))}
Kasa / Diğer
${money(combinedAccounts.filter(x=>['kasa','diger'].includes(String(x.account_type))).reduce((a,b)=>a+num(b.net_bakiye),0))}
Çek / Senet
${money(combinedAccounts.filter(x=>['cek','senet'].includes(String(x.account_type))).reduce((a,b)=>a+num(b.net_bakiye),0))}
Moka / POS
${money(combinedAccounts.filter(x=>String(x.account_type).includes('moka') || String(x.account_name||'').toLowerCase().includes('pos')).reduce((a,b)=>a+num(b.net_bakiye),0))}

Hesap Kartları

${combinedAccounts.length?`${combinedAccounts.map(a=>``).join("")}
HesapTürNet BakiyeSon Hareket
${escapeHtml(a.account_name)}${renderTag(a.account_type)}${money(a.net_bakiye||0)}${shortDate(a.son_hareket_tarihi)}
`:`
Hesap özeti yok.
`}

Son Manuel İşlemler

${manualRows.length?`${manualRows.slice(0,12).map(op=>{ const label = op.type==='account_transfer' ? 'Transfer' : (op.type==='account_create' ? 'Hesap Tanımı' : (op.type==='cari_collection' ? 'Cari Tahsilat' : 'Hesap İşlemi')); const target = op.type==='account_transfer' ? `${escapeHtml(op.source_account_name || '-') } → ${escapeHtml(op.target_account_name || '-')}` : escapeHtml(op.account_name || '-'); const signed = op.type==='account_transfer' ? money(op.amount||0) : money((String(op.direction||'giris')==='cikis' ? -num(op.amount) : num(op.amount))); return ``; }).join("")}
NoTarihİşlemHedefTutar
${escapeHtml(displayTxnNo(op,'LAC'))}${shortDate(op.entry_date || op.created_at)}${label}${target}
${escapeHtml(op.description || op.note || '-')}
${signed}
`:`
Henüz manuel hesap işlemi yok.
`}
`; document.getElementById("bankPanelWrap").innerHTML=`
Toplam Satır
${bankStats.total}
İşlenebilir
${bankStats.processable}
Şüpheli
${bankStats.needs_approval}
Mükerrer
${bankStats.duplicate}
Kuyruk / İşlenen
${bankStats.queued} / ${bankStats.processed}
Öğrenilmiş eşleşme: ${getBankLearnRows().length}
Satıra tıkla, hedefi doğrula ve karar ver. Kör otomasyon yok. Yanlış cariye yanlış hareket gitmez. Kural tanımlarsan açıklamaya göre otomatik hedef önerir.
` + (bankRows.length?`${bankRows.map(r=>{ const suggestion = bankSuggestedTarget(r); const conf = bankRowConfidence(r); const fastBtn = bankStatusCode(r,'bank')==='processable' ? `` : `Detay`; return ``; }).join("")}
İşlem NoTarihAçıklamaÖnerilen HedefGüvenDurumHızlı İşlemTutar
${escapeHtml(displayTxnNo(r,'BNK'))}${shortDate(r.movement_date)}${escapeHtml(bankDescriptionText(r)||"-")}
${escapeHtml(bankStatusNote(r,'bank'))}
${escapeHtml(suggestion)} ${renderTag(r.suggested_financial_account_type||"-")}${conf ? conf.toFixed(0) : '-'}${bankStatusBadge(r,'bank')}${fastBtn}${money(num(r.net_amount ?? r.amount ?? r.raw_amount ?? r.final_amount ?? 0))}
`:`
Bu filtrede banka satırı yok.
`); document.getElementById("mokaPanelWrap").innerHTML=`
Toplam Moka Satırı
${mokaStats.total}
Moka Tahsilat
${mokaStats.tahsilat}
Banka Aktarım
${mokaStats.aktarim}
Kuyruk / İşlenen
${mokaStats.queued} / ${mokaStats.processed}
Moka kredi kartı tahsilatı doğrudan bankaya gelir yazılmaz. İlk kayıt Moka United içinde tutulur. Vadesi gelince bankaya aktarılan tutar ayrıca işlenir ve Moka United → Banka mahsup mantığıyla kapatılır.
` + (mokaRows.length?`${mokaRows.map(r=>{ const cls = mokaRowClass(r); const tag = cls==='moka_tahsilat' ? 'Moka Tahsilat' : (cls==='banka_aktarim' ? 'Banka Aktarım' : 'Belirsiz'); return ``; }).join("")}
İşlem NoTarihAçıklamaSınıfDurumTutar
${escapeHtml(displayTxnNo(r,'MOK'))}${shortDate(r.movement_date)}${escapeHtml(bankDescriptionText(r)||"-")}
${escapeHtml(mokaRecommendedAction(r))}
${tag}${bankStatusBadge(r,'moka')}${money(num(r.net_amount ?? r.amount ?? r.raw_amount ?? r.final_amount ?? 0))}
`:`
Bu filtrede Moka satırı yok.
`); document.querySelectorAll('#bankFilterChips .chip').forEach(btn=>btn.addEventListener('click',()=>{ state.bankFilter = btn.dataset.bfilter; renderAccounts(); })); document.querySelectorAll('#mokaFilterChips .chip').forEach(btn=>btn.addEventListener('click',()=>{ state.mokaFilter = btn.dataset.mfilter; renderAccounts(); })); document.querySelectorAll('[data-bank-key]').forEach(el=>el.addEventListener('click',()=>openBankActionModal(el.dataset.bankBucket, el.dataset.bankKey))); document.querySelectorAll('[data-account-key]').forEach(el=>el.addEventListener('click',()=>openAccountDetailModal(el.dataset.accountKey))); document.querySelectorAll('[data-manual-op-id]').forEach(el=>el.addEventListener('click',()=>editManualOperation(el.dataset.manualOpId))); renderBankMatchHistory(); renderMovementTestCenter(); } function renderBankMatchHistory(){ const rows = [...(state.operationalLedger || [])].filter(x=>['bank_panel','moka_panel','approval_center'].includes(x.source_module || '')).slice(0,20); const summary = { total: rows.length, cari: rows.filter(x=>x.target_type==='cari').length, gider: rows.filter(x=>x.target_type==='gider').length, hesap: rows.filter(x=>['hesap','moka_united'].includes(x.target_type)).length, }; const target=document.getElementById('bankMatchWrap'); if(!target) return; const html = `
Montaj Kaydı
${summary.total}
Cariye Giden
${summary.cari}
Gidere Giden
${summary.gider}
Hesap/Moka
${summary.hesap}
Bu tablo uygulama içi montaj defteridir. Hangi banka/Moka satırının hangi hedefe, hangi hareket tipiyle işlendiği iz bırakır.
` + (rows.length ? `${rows.map(r=>``).join('')}
RefKaynakHedefHareketTutarTarih
${escapeHtml(r.ledger_ref || '-')}${escapeHtml(r.source_module || '-')}
${escapeHtml(r.source_summary || '-')}
${escapeHtml(r.target_name || '-')}
${escapeHtml(r.target_type || '-')}
${escapeHtml(r.movement_type || '-')}${money(r.amount || 0)}${shortDate(r.created_at)}
` : `
Henüz montaj kaydı yok.
`); } function renderApprovalAudit(){ const rows=[...(state.auditTrail || [])].slice(0,25); const summary={ total: rows.length, approval: rows.filter(x=>String(x.event_type||'').includes('approval')).length, bank: rows.filter(x=>String(x.event_type||'').includes('bank')).length, reject: rows.filter(x=>String(x.event_type||'').includes('reject')).length }; const target=document.getElementById('approvalAuditWrap'); if(!target) return; const html = `
Toplam İz
${summary.total}
Onay İşlemi
${summary.approval}
Banka/Moka İşlemi
${summary.bank}
Red / Ignore
${summary.reject}
Bu ekran karar izi defteridir. Kimlik, kaynak, karar notu ve hedef bilgisi tekrar açıldığında kaybolmaz.
` + (rows.length ? `${rows.map(r=>``).join('')}
NoRefOlayKısa İzTarih Saat
${escapeHtml(r.operation_no || '-')}${escapeHtml(r.event_ref || '-')}${escapeHtml(r.event_type || '-')}${escapeHtml(r.summary || r.note || r.decision_note || '-')}${shortDateTime(r.created_at)}
` : `
Henüz operasyon izi yok.
`); target.innerHTML = html; target.querySelectorAll('[data-audit-id]').forEach(el=>el.addEventListener('click',()=>openAuditDetailModal(el.dataset.auditId))); } function getAuditRows(){ return [...(state.auditTrail || [])].sort((a,b)=>new Date(b.created_at||0)-new Date(a.created_at||0)); } function openAuditDetailModal(auditId){ const row = getAuditRows().find(x=>String(x.id)===String(auditId)); if(!row){ notify('İz kaydı bulunamadı.'); return; } state.selectedAuditId = row.id; document.getElementById('auditDetailContent').innerHTML = `
İşlem No
${escapeHtml(row.operation_no || '-')}
İşlem ID
${escapeHtml(txnId(row,'LOG'))}
Ref
${escapeHtml(row.event_ref || '-')}
Tarih Saat
${escapeHtml(shortDateTime(row.created_at))}
Kısa İz
${escapeHtml(row.summary || '-')}
Olay${escapeHtml(row.event_type || '-')}
Kaynak${escapeHtml(row.source_module || row.source_type || '-')}
Hedef${escapeHtml(row.target_name || '-')}
Not${escapeHtml(row.note || row.decision_note || '-')}
Tutar${money(row.amount || 0)}
Operatör${escapeHtml(row.actor || '-')}
`; document.getElementById('auditDetailModal').style.display='flex'; } function closeAuditDetailModal(){ document.getElementById('auditDetailModal').style.display='none'; } function renderGlobalAuditLog(){ const target=document.getElementById('globalAuditWrap'); if(!target) return; const rows=getAuditRows().slice(0,30); target.innerHTML = `
Toplam İz
${getAuditRows().length}
Bugün
${getAuditRows().filter(x=>String(x.created_at||'').slice(0,10)===new Date().toISOString().slice(0,10)).length}
Hata
${getAuditRows().filter(x=>String(x.event_type||'').includes('error')).length}
Çift Engel
${getAuditRows().filter(x=>String(x.event_type||'').includes('duplicate')).length}
Bütün operasyonlar kısa iz olarak tarih-saat damgasıyla burada tutulur. Satıra tıkla, detayı aç.
` + (rows.length ? `${rows.map(r=>``).join('')}
NoIDKısa İzTarih Saat
${escapeHtml(r.operation_no || '-')}${escapeHtml(txnId(r,'LOG'))}${escapeHtml(r.summary || r.note || '-')}${shortDateTime(r.created_at)}
` : `
Henüz işlem izi yok.
`); target.querySelectorAll('[data-audit-id]').forEach(el=>el.addEventListener('click',()=>openAuditDetailModal(el.dataset.auditId))); } function renderDeleteArchive(){ const target=document.getElementById('deleteArchiveWrap'); if(!target) return; const rows=[...(state.deleteArchive||[])].slice(0,20); target.innerHTML = `
Arşiv Kayıt
${(state.deleteArchive||[]).length}
Son Silme
${rows[0] ? shortDateTime(rows[0].created_at) : '-'}
Modül
${rows[0] ? escapeHtml(rows[0].source_module || '-') : '-'}
Koruma
Parola
Silinen yerel kayıtlar tamamen kaybolmaz; kısa arşiv izi burada tutulur. Silme parolası giriş şifresinin tersidir.
` + (rows.length ? `${rows.map(r=>``).join('')}
NoRefModülSebepTarih Saat
${escapeHtml(r.operation_no || '-')}${escapeHtml(r.event_ref || '-')}${escapeHtml(r.source_module || '-')}${escapeHtml(r.reason || '-')}${shortDateTime(r.created_at)}
` : `
Henüz silme arşivi yok.
`); } function renderMovementTestCenter(){ const target = document.getElementById('movementTestWrap'); if(!target) return; const rows = state.movementTests || []; const summary = { total: rows.length, bank: rows.filter(x=>x.import_bucket!=='moka').length, moka: rows.filter(x=>x.import_bucket==='moka').length, total_amount: rows.reduce((a,b)=>a+num(b.net_amount),0), matched: rows.filter(x=>x.suggested_cari_name).length, processable: rows.filter(x=>!x.approval_required).length, approval: rows.filter(x=>x.approval_required).length, duplicate: rows.filter(x=>x.is_duplicate_candidate).length, balance_error: rows.filter(x=>x.running_balance_check==='failed').length, }; target.innerHTML = `
Test Satırı
${summary.total}
Banka / Moka
${summary.bank} / ${summary.moka}
Eşleşen Cari
${summary.matched}
Toplam
${money(summary.total_amount)}
İşlenebilir
${summary.processable}
Onaya Düşmeli
${summary.approval}
Muhtemel Çift
${summary.duplicate}
Onay Kuyruğuna Alınan
${(state.bankOps?.pending||[]).filter(x=>x.queue_source==='movement_import_review').length}
Bu merkez artık sadece import etmiyor; eşleşme analizi de yapıyor. Güçlü cari eşleşmeleri bankada işlenebilir olarak görünür. Zayıf eşleşme, muhtemel çift ve belirsiz satırlar onay kuyruğuna atılır.
` + (rows.length ? `${rows.slice(0,120).map(r=>``).join('')}
İşlem NoTarihAçıklamaÖneriGüvenSonuçTutar
${escapeHtml(displayTxnNo(r,'IMP'))}${shortDate(r.movement_date)}${escapeHtml(r.description||'-')}
${escapeHtml(r.match_reason||'-')}
${escapeHtml(bankSuggestedTarget(r))} ${renderTag(r.suggested_financial_account_type||'-')}${num(r.match_confidence||r.suggested_confidence) ? Number(r.match_confidence||r.suggested_confidence).toFixed(0) : '-'}${r.approval_required ? 'Onaya Düşmeli' : 'İşlenebilir'}${r.is_duplicate_candidate ? ' Muhtemel Çift' : ''}${money(num(r.net_amount ?? r.amount ?? r.raw_amount ?? r.final_amount ?? 0))}
` : `
Henüz test hareketi yok. Dosya oku ya da metin yapıştır.
`); const fileEl = document.getElementById('movementImportFile'); if(fileEl && !fileEl.dataset.bound){ fileEl.addEventListener('change', handleMovementFileImport); fileEl.dataset.bound='1'; } } function renderReports(){ const riskRows=state.cariler.filter(x=>x.is_tahsilat_3_ay_gecikmis).sort((a,b)=>num(b.bakiye)-num(a.bakiye)).slice(0,15); const topFee=[...state.cariler].sort((a,b)=>num(b.defter_ucreti)-num(a.defter_ucreti)).slice(0,10); const feeIssueCount = state.feeControl.filter(x=>String(x.control_status || x.issue_type || '').toLowerCase() !== 'tamam').length; const s=getGlobalOpsSummary(); document.getElementById("reportWrap").innerHTML=`
İşletme / Bilanço
${s.taxpayer.isletme_defteri} / ${s.taxpayer.bilanco}
Ltd / A.Ş.
${s.taxpayer.limited_sirket} / ${s.taxpayer.anonim_sirket}
Bu Ay T/T
${money(s.thisMonthTahakkuk)} / ${money(s.thisMonthTahsilat)}
Bu Yıl T/T
${money(s.thisYearTahakkuk)} / ${money(s.thisYearTahsilat)}
Bu Ay Gider
${money(s.thisMonthExpenseAcc)} / ${money(s.thisMonthExpensePay)}
Bu Yıl Gider
${money(s.thisYearExpenseAcc)} / ${money(s.thisYearExpensePay)}
Pozitif Bakiye Toplamı
${money(state.cariler.reduce((a,b)=>a+Math.max(num(b.bakiye),0),0))}
Ücret Kontrol Uyarısı
${feeIssueCount}
Toplam Son Tahsilat
${money(state.cariler.reduce((a,b)=>a+num(b.son_tahsilat_tutari),0))}
Pozitif Bakiye Toplamı
${money(state.cariler.reduce((a,b)=>a+Math.max(num(b.bakiye),0),0))}
Ücret Kontrol Uyarısı
${feeIssueCount}

Tahsilat Riski Olanlar

${riskRows.length?`${riskRows.map(r=>``).join("")}
CariSon TahsilatBakiye
${escapeHtml(r.cari_name)}${shortDate(r.son_tahsilat_tarihi)} · ${money(r.son_tahsilat_tutari||0)}${balanceLabel(r.bakiye||0)}
`:`
Riskli cari yok.
`}

En Yüksek Defter Ücretliler

${topFee.map(r=>``).join("")}
CariTürDefter Ücreti
${escapeHtml(r.cari_name)}${escapeHtml(r.taxpayer_type||"-")}${money(r.defter_ucreti||0)}
`; } function approvalSourceText(r){ const s = String(r.source_type || r.channel || r.raw_source || '-').toLowerCase(); if(s.includes('whatsapp')) return 'WhatsApp'; if(s.includes('telegram')) return 'Telegram'; if(s.includes('görsel') || s.includes('gorsel') || s.includes('image')) return 'Görsel / Belge'; if(s.includes('banka')) return 'Banka'; if(s.includes('moka')) return 'Moka'; if(s.includes('gider_tahakkuk') || s.includes('gider tahakkuk')) return 'Gider Tahakkuk'; return r.source_type || r.channel || r.raw_source || '-'; } function approvalTextBlob(r){ return `${r.issue_type || ''} ${r.content_summary || ''} ${r.raw_content || ''} ${r.source_type || ''} ${r.channel || ''}`.toLowerCase(); } function approvalIsDuplicate(r){ const text = approvalTextBlob(r); return text.includes('duplicate') || text.includes('çift'); } function approvalIsDocumentCandidate(r){ const text = approvalTextBlob(r); return text.includes('whatsapp') || text.includes('telegram') || text.includes('görsel') || text.includes('gorsel') || text.includes('belge') || text.includes('makbuz'); } function approvalIsLocked(r){ const conf = approvalConfidence(r); return (conf && conf < 95) || approvalIsDuplicate(r); } function approvalLockReason(r){ const reasons = []; const conf = approvalConfidence(r); if(conf && conf < 95) reasons.push(`Güven puanı düşük (${conf.toFixed(0)})`); if(approvalIsDuplicate(r)) reasons.push('Muhtemel çift kayıt'); return reasons.join(' · ') || 'Kilitsiz'; } function filteredPendingApprovals(){ const rows = getAllPendingApprovals().map((r,i)=>({row:r,__i:i})); if(state.approvalFilter === 'locked') return rows.filter(x=>approvalIsLocked(x.row)); if(state.approvalFilter === 'duplicate') return rows.filter(x=>approvalIsDuplicate(x.row)); if(state.approvalFilter === 'document') return rows.filter(x=>approvalIsDocumentCandidate(x.row)); return rows; } function openApprovalDetailModal(bucket, index){ state.selectedApprovalBucket = bucket; state.selectedApprovalIndex = index; const sourceRows = bucket === 'processed' ? state.processed : (bucket === 'rejected' ? state.rejected : getAllPendingApprovals()); const r = sourceRows[index]; if(!r){ notify('Kayıt bulunamadı.'); return; } const conf = approvalConfidence(r); const content = `
İşlem ID
${escapeHtml(txnId(r,'APR'))}
Kova
${escapeHtml(bucket === 'pending' ? 'Onay Bekleyen' : bucket === 'processed' ? 'İşlenen' : 'Reddedilen')}
Kaynak
${escapeHtml(approvalSourceText(r))}
Güven
${conf ? conf.toFixed(0) : '-'}
Kilit
${approvalIsLocked(r) ? 'Evet' : 'Hayır'}
Karar Ref: ${escapeHtml(r.decision_ref || '-') }${r.processed_at ? `
İşlendi: ${shortDate(r.processed_at)}` : ''}${r.rejected_at ? `
Reddedildi: ${shortDate(r.rejected_at)}` : ''}
Önerilen Hedef
Cari / Gider: ${escapeHtml(r.suggested_cari_name || r.suggested_expense_name || r.final_cari_name || r.final_expense_name || '-')}
Hareket: ${escapeHtml(r.suggested_movement_type || r.final_movement_type || '-')}
Tutar: ${money(r.suggested_amount || r.raw_amount || r.final_amount || 0)}
Kontrol Mantığı
${approvalFlags(r)}
${escapeHtml(approvalLockReason(r))}
${escapeHtml(r.match_reason || r.decision_note || r.issue_type || 'Detay notu yok')}

Özet

${escapeHtml(r.content_summary || r.raw_content || '-')}

Ham İçerik / Belge Metni

${escapeHtml(r.raw_content || r.content_summary || '-')}

Operasyon Kuralı

Bu ekranda kayıt inceleme amaçlı açılıyor. Kural sabit: emin olunmayan kayıt otomatik işlenmez. Düşük güven veya muhtemel çift varsa önce onay merkezinde doğrulanır, sonra muhasebe kaydı açılır.
`; document.getElementById('approvalDetailContent').innerHTML = content; document.getElementById('approvalDetailModal').style.display = 'flex'; } function closeApprovalDetailModal(){ document.getElementById('approvalDetailModal').style.display = 'none'; } function filteredDocuments(){ const q = String(state.documentSearch || '').trim().toLowerCase(); return [...state.documents].filter(d=>{ const typeOk = state.documentFilter === 'all' ? true : (state.documentFilter === 'bagimsiz' ? !d.cari_id : String(d.document_type||'') === state.documentFilter); if(!typeOk) return false; if(!q) return true; const blob = `${d.cari_name||''} ${d.document_title||''} ${d.document_no||''} ${d.source_type||''} ${d.file_ref||''} ${d.note||''}`.toLowerCase(); return blob.includes(q); }).sort((a,b)=>String(b.document_date || b.created_at || '').localeCompare(String(a.document_date || a.created_at || ''))); } function renderDocuments(){ const linkedCount = state.documents.filter(d=>d.cari_id).length; const unlinkedCount = state.documents.filter(d=>!d.cari_id).length; const currentCariCount = state.selectedCariId ? getCariDocuments(state.selectedCariId).length : 0; const docs = filteredDocuments(); document.getElementById("documentWrap").innerHTML=`
Toplam Belge
${state.documents.length}
Cariye Bağlı
${linkedCount}
Bağsız
${unlinkedCount}
Seçili Cari
${currentCariCount}
Belge katmanında kural sabit: belge doğrudan muhasebe fişi üretmez. Önce doğru cariye bağlanır, sonra onay / kontrol akışıyla ilerler.
` + (docs.length ? `${docs.map(d=>``).join('')}
İşlem NoTarihCariBelgeKaynakTutar
${escapeHtml(displayTxnNo(d,'DOC'))}${shortDate(d.document_date || d.created_at)}${escapeHtml(d.cari_name || '-')}${documentTypeTag(d.document_type)} ${escapeHtml(d.document_title || d.document_no || '-')}
${escapeHtml(d.file_ref || d.note || '-')}
${escapeHtml(d.source_type || '-')}${money(d.amount || 0)}
` : `
Bu filtrede belge yok.
`); const pendingBridge = bridgeCandidatesAsPendingRows(); document.getElementById("bridgeWrap").innerHTML=`
Manuel Aday
${state.bridgeCandidates.length}
Onaya Yansıyan
${pendingBridge.length}
Belge Kaynağı
${getAllPendingApprovals().filter(r=>approvalIsDocumentCandidate(r)).length}
Standart
Otomatik işlem yok
WhatsApp, Telegram, OCR veya kopyala-yapıştır gelen tahsilat metinleri burada aday kayıt olur. Otomatik işlenmez, doğrudan onay mantığına düşer.
` + (state.bridgeCandidates.length ? `${[...state.bridgeCandidates].sort((a,b)=>String(b.created_at||'').localeCompare(String(a.created_at||''))).map(r=>``).join('')}
İşlem NoTarihKaynakCariÖneriTutar
${escapeHtml(displayTxnNo(r,'BRG'))}${shortDate(r.created_at)}${escapeHtml(r.source_type || '-')}${escapeHtml(r.cari_name || '-')}${escapeHtml(r.suggested_movement_type || '-')}
${escapeHtml(r.content_summary || '-')}
${money(r.suggested_amount || 0)}
` : `
Henüz aday kayıt yok.
`); const searchEl = document.getElementById('documentSearchInput'); if(searchEl) searchEl.addEventListener('input', e=>{ state.documentSearch = e.target.value; renderDocuments(); }); const typeEl = document.getElementById('documentTypeFilter'); if(typeEl) typeEl.addEventListener('change', e=>{ state.documentFilter = e.target.value; renderDocuments(); }); document.querySelectorAll('[data-doc-id]').forEach(el=>el.addEventListener('click',()=>openDocumentDetailModal(el.dataset.docId))); renderBridgeSessions(); } function renderApprovals(){ const allPending = getAllPendingApprovals(); const pendingRows = filteredPendingApprovals(); const processedRows = getProcessedRows(); const rejectedRows = getRejectedRows(); const lockedCount = allPending.filter(r=>approvalIsLocked(r)).length; const duplicateCount = allPending.filter(r=>approvalIsDuplicate(r)).length; const documentCount = allPending.filter(r=>approvalIsDocumentCandidate(r)).length; document.getElementById("pendingWrap").innerHTML=`
Onay Bekleyen
${allPending.length}
Düşük Güven / Kilit
${lockedCount}
Muhtemel Çift
${duplicateCount}
Belge Adayı
${documentCount}
Kural: güven puanı düşük, muhtemel çift, görselden okunan, kopyala-yapıştır gelen veya import incelemede kilitlenen kayıtlar otomatik işlenmez; önce onaya düşer. İşleme alınan kayıt karar izi ile işlenenlere taşınır.
` + (pendingRows.length?`${pendingRows.map(({row:r,__i})=>``).join("")}
İşlem NoKaynakÖzetÖneriKontrolTutar
${escapeHtml(displayTxnNo(r,'APR'))}${escapeHtml(approvalSourceText(r))}${escapeHtml(r.content_summary||r.raw_content||"-")}${escapeHtml(r.suggested_cari_name||r.suggested_expense_name||"-")} · ${escapeHtml(r.suggested_movement_type||"-")}
Güven: ${approvalConfidence(r) ? approvalConfidence(r).toFixed(0) : '-'}
${approvalFlags(r)}
${escapeHtml(approvalLockReason(r))}
${money(r.suggested_amount||r.raw_amount||0)}
`:`
Bu filtrede kayıt yok.
`); document.getElementById("processedWrap").innerHTML=processedRows.length?`${processedRows.map((r,i)=>``).join("")}
İşlem NoKaynakKararİzTutar
${escapeHtml(displayTxnNo(r,'APR'))}${escapeHtml(approvalSourceText(r))}${escapeHtml(r.final_cari_name||r.final_expense_name||r.suggested_cari_name||"-")} · ${escapeHtml(r.final_movement_type||r.suggested_movement_type||"-")}${escapeHtml(r.decision_note || r.match_reason || 'İşlendi')}${money(r.final_amount||r.suggested_amount||0)}
`:`
İşlenen kayıt yok.
`; document.getElementById("rejectedWrap").innerHTML=rejectedRows.length?`${rejectedRows.map((r,i)=>``).join("")}
İşlem NoKaynakÖzetNot
${escapeHtml(displayTxnNo(r,'APR'))}${escapeHtml(approvalSourceText(r))}${escapeHtml(r.content_summary||r.raw_content||"-")}${escapeHtml(r.decision_note||"-")}
`:`
Reddedilen kayıt yok.
`; document.querySelectorAll('#approvalFilterChips .chip').forEach(btn=>btn.addEventListener('click',()=>{ state.approvalFilter = btn.dataset.afilter; renderApprovals(); })); document.querySelectorAll('[data-approval-bucket]').forEach(el=>el.addEventListener('click',()=>openApprovalDetailModal(el.dataset.approvalBucket, Number(el.dataset.approvalIndex)))); } function getSelectedApprovalRow(){ const bucket = state.selectedApprovalBucket; const sourceRows = bucket === 'processed' ? getProcessedRows() : (bucket === 'rejected' ? getRejectedRows() : getAllPendingApprovals()); return sourceRows[state.selectedApprovalIndex] || null; } function openApprovalDetailModal(bucket, index){ state.selectedApprovalBucket = bucket; state.selectedApprovalIndex = index; const r = getSelectedApprovalRow(); if(!r){ notify('Kayıt bulunamadı.'); return; } const conf = approvalConfidence(r); const sourceTxt = approvalSourceText(r).toLowerCase(); const docTxt = JSON.stringify(String(r.raw_content || r.content_summary || '')); const canOperate = bucket === 'pending'; const content = `
İşlem ID
${escapeHtml(txnId(r,'APR'))}
Kova
${escapeHtml(bucket === 'pending' ? 'Onay Bekleyen' : bucket === 'processed' ? 'İşlenen' : 'Reddedilen')}
Kaynak
${escapeHtml(approvalSourceText(r))}
Güven
${conf ? conf.toFixed(0) : '-'}
Kilit
${approvalIsLocked(r) ? 'Evet' : 'Hayır'}
Önerilen Hedef
Cari / Gider: ${escapeHtml(r.suggested_cari_name || r.suggested_expense_name || r.final_cari_name || r.final_expense_name || '-')}
Hareket: ${escapeHtml(r.suggested_movement_type || r.final_movement_type || '-')}
Tutar: ${money(r.suggested_amount || r.raw_amount || r.final_amount || 0)}
Kontrol Mantığı
${approvalFlags(r)}
${escapeHtml(approvalLockReason(r))}
${escapeHtml(r.match_reason || r.decision_note || r.issue_type || 'Detay notu yok')}

Özet

${escapeHtml(r.content_summary || r.raw_content || '-')}

Ham İçerik / Belge Metni

${escapeHtml(r.raw_content || r.content_summary || '-')}
${approvalIsDocumentCandidate(r) ? `

Belge Erişimi

` : ''} ${canOperate ? `

Operasyon Kararı

` : `

Operasyon Sonucu

Bu kayıt artık karar verilmiş durumda. İz bilgisi korunur, tekrar otomatik pending'e dönmez.
`}`; document.getElementById('approvalDetailContent').innerHTML = content; document.getElementById('approvalDetailModal').style.display = 'flex'; } function closeApprovalDetailModal(){ document.getElementById('approvalDetailModal').style.display = 'none'; } function createDocumentFromSelectedApproval(){ const r = getSelectedApprovalRow(); if(!r){ notify('Onay kaydı bulunamadı.'); return; } const cari = findCariByName(r.suggested_cari_name || r.final_cari_name || ''); openDocumentModal(cari?.cari_id || state.selectedCariId || ''); const sourceTxt = approvalSourceText(r).toLowerCase(); document.getElementById('documentSource').value = sourceTxt.includes('telegram') ? 'telegram' : (sourceTxt.includes('whatsapp') ? 'whatsapp' : 'manuel'); document.getElementById('documentType').value = 'makbuz'; document.getElementById('documentAmount').value = Number(r.suggested_amount || r.raw_amount || 0) || ''; document.getElementById('documentTitle').value = r.content_summary || 'Onay merkezinden oluşturulan belge'; document.getElementById('documentNo').value = r.id || ''; document.getElementById('documentNote').value = r.raw_content || r.content_summary || ''; } function processSelectedApproval(){ const r = getSelectedApprovalRow(); if(!r || state.selectedApprovalBucket !== 'pending'){ notify('İşlenecek pending kayıt yok.'); return; } const targetType = document.getElementById('approvalTargetType')?.value || 'cari'; const movementType = document.getElementById('approvalMovementType')?.value || (r.suggested_movement_type || 'tahsilat'); const amount = num(document.getElementById('approvalAmountInput')?.value || r.suggested_amount || r.raw_amount || 0); const targetName = (document.getElementById('approvalTargetName')?.value || '').trim(); const note = (document.getElementById('approvalDecisionNote')?.value || '').trim() || 'Operatör onayı ile işlendi'; const forceOk = !!document.getElementById('approvalForceCheck')?.checked; if(!targetName){ notify('Hedef adını gir.'); return; } if(approvalIsLocked(r) && !forceOk){ notify('Kilitli kayıtta operatör kontrol kutusunu işaretle.'); return; } const approvalRef = decisionRef('APR'); const approvalProcessedId = uid('approval_processed'); const processed = { ...r, id: approvalProcessedId, txn_id: approvalProcessedId, decision_ref: approvalRef, final_target_type: targetType, final_cari_name: targetType === 'cari' ? targetName : null, final_expense_name: targetType === 'gider' ? targetName : null, final_financial_account_name: targetType === 'hesap' ? targetName : null, final_movement_type: movementType, final_amount: amount, decision_note: note, processed_at: new Date().toISOString(), processed_locally: true }; markPendingHandled(r); state.approvalOps.processed.unshift(processed); if(r.is_local_bridge){ state.bridgeCandidates = state.bridgeCandidates.filter(x=>String(x.id)!==String(r.id)); writeLocalJson(LOCAL_BRIDGE_KEY, state.bridgeCandidates); } if(r.is_local_bank_queue && r.bank_origin_key){ state.bankOps.pending = (state.bankOps.pending || []).filter(x=>String(x.id)!==String(r.id)); state.bankOps.row_status[r.bank_origin_key] = {...(state.bankOps.row_status[r.bank_origin_key] || {}), status:'processed', note, updated_at:new Date().toISOString(), decision_ref:approvalRef}; persistBankOps(); } addLedgerEntry({ledger_ref:approvalRef, source_module:'approval_center', source_summary:r.content_summary || r.raw_content || 'Onay kaydı', target_type:targetType, target_name:targetName, movement_type:movementType, amount, decision_note:note}); addAuditEvent('approval_processed',{event_ref:approvalRef, source_module:'approval_center', source_type:r.source_type || 'approval', target_name:targetName, target_type:targetType, movement_type, amount, note, summary:`Onay işlendi · ${targetName} · ${movementType}`}); persistApprovalOps(); closeApprovalDetailModal(); renderDocuments(); renderExpenses(); renderAccounts(); renderApprovals(); notify('Kayıt işlenenlere taşındı.'); } function rejectSelectedApproval(){ const r = getSelectedApprovalRow(); if(!r || state.selectedApprovalBucket !== 'pending'){ notify('Reddedilecek pending kayıt yok.'); return; } const note = (document.getElementById('approvalDecisionNote')?.value || '').trim() || 'Operatör tarafından reddedildi'; const forceOk = !!document.getElementById('approvalForceCheck')?.checked; if(approvalIsLocked(r) && !forceOk){ notify('Kilitli kayıtta operatör kontrol kutusunu işaretle.'); return; } const rejectRef = decisionRef('REJ'); const approvalRejectedId = uid('approval_rejected'); const rejected = { ...r, id: approvalRejectedId, txn_id: approvalRejectedId, decision_ref: rejectRef, decision_note: note, rejected_at: new Date().toISOString(), rejected_locally: true }; markPendingHandled(r); state.approvalOps.rejected.unshift(rejected); if(r.is_local_bridge){ state.bridgeCandidates = state.bridgeCandidates.filter(x=>String(x.id)!==String(r.id)); writeLocalJson(LOCAL_BRIDGE_KEY, state.bridgeCandidates); } if(r.is_local_bank_queue && r.bank_origin_key){ state.bankOps.pending = (state.bankOps.pending || []).filter(x=>String(x.id)!==String(r.id)); state.bankOps.row_status[r.bank_origin_key] = {...(state.bankOps.row_status[r.bank_origin_key] || {}), status:'rejected', note, updated_at:new Date().toISOString(), decision_ref:rejectRef}; persistBankOps(); } addAuditEvent('approval_rejected',{event_ref:rejectRef, source_module:'approval_center', source_type:r.source_type || 'approval', target_name:r.suggested_cari_name || r.suggested_expense_name || '-', target_type:r.suggested_movement_type || '-', movement_type:'rejected', amount:num(r.suggested_amount || r.raw_amount || 0), note, summary:`Onay reddedildi · ${r.suggested_cari_name || r.suggested_expense_name || '-'}`}); persistApprovalOps(); closeApprovalDetailModal(); renderDocuments(); renderExpenses(); renderAccounts(); renderApprovals(); notify('Kayıt reddedilenlere taşındı.'); } function openDocumentDetailModal(docId){ const doc = state.documents.find(x=>String(x.id)===String(docId)); if(!doc){ notify('Belge bulunamadı.'); return; } state.selectedDocumentId = doc.id; const canOpenUrl = /^https?:\/\//i.test(String(doc.file_ref||'')); const refJson = JSON.stringify(String(doc.file_ref || '')); const cariJson = JSON.stringify(String(doc.cari_id || '')); document.getElementById('documentDetailContent').innerHTML = `
Belge ID
${escapeHtml(txnId(doc,'DOC'))}
Belge Türü
${documentTypeTag(doc.document_type)}
Cari
${escapeHtml(doc.cari_name || '-')}
Tarih
${shortDate(doc.document_date || doc.created_at)}
Tutar
${money(doc.amount || 0)}
Başlık / No
${escapeHtml(doc.document_title || '-')}
${escapeHtml(doc.document_no || '-')}
Kaynak / Ref
${escapeHtml(doc.source_type || '-')}
${escapeHtml(doc.file_ref || 'Ref yok')}

Not

${escapeHtml(doc.note || '-')}
${canOpenUrl ? `` : ``} ${doc.cari_id ? `` : ``}
`; document.getElementById('documentDetailModal').style.display='flex'; } function closeDocumentDetailModal(){ document.getElementById('documentDetailModal').style.display='none'; } function requestDeleteSelectedDocument(){ const doc = state.documents.find(x=>String(x.id)===String(state.selectedDocumentId)); if(!doc){ notify('Belge bulunamadı.'); return; } openDeleteAuthModal(`Belge silme · ${doc.document_title || doc.document_no || doc.id}`, (reason)=>{ archiveDeletion('documents', doc, reason); state.documents = state.documents.filter(x=>String(x.id)!==String(doc.id)); writeLocalJson(LOCAL_DOCS_KEY, state.documents); addAuditEvent('document_deleted',{source_module:'documents', target_name:doc.document_title || doc.document_no || doc.id, amount:doc.amount, note:reason, summary:`Belge silindi · ${doc.document_title || doc.document_no || doc.id}`}); closeDocumentDetailModal(); renderDocuments(); if(state.selectedCariId) renderSelectedCariDetail(); renderDeleteArchive(); renderGlobalAuditLog(); notify('Belge silindi.'); }); } function focusCariFromDocument(cariId){ if(!cariId){ notify('Cari bağlantısı yok.'); return; } state.selectedCariId = cariId; switchTab('cariler'); renderCariList(); renderSelectedCariDetail(); closeDocumentDetailModal(); } function openDocumentModal(cariId=state.selectedCariId){ fillCariOptions('documentCariId', true, 'Cari seçiniz'); document.getElementById('documentCariId').value = cariId || ''; document.getElementById('documentType').value = 'makbuz'; document.getElementById('documentSource').value = 'manuel'; document.getElementById('documentDate').value = new Date().toISOString().slice(0,10); document.getElementById('documentNo').value = ''; document.getElementById('documentAmount').value = ''; document.getElementById('documentFileRef').value = ''; document.getElementById('documentTitle').value = ''; document.getElementById('documentNote').value = ''; document.getElementById('documentModal').style.display='flex'; } function closeDocumentModal(){ document.getElementById('documentModal').style.display='none'; } async function saveDocumentRecord(){ const cariId = document.getElementById('documentCariId').value || null; const cari = state.cariler.find(x=>x.cari_id===cariId); const docId = uid('doc'); const payload = { id: docId, txn_id: docId, cari_id: cariId, cari_name: cari?.cari_name || null, document_type: document.getElementById('documentType').value, source_type: document.getElementById('documentSource').value, document_date: document.getElementById('documentDate').value || new Date().toISOString().slice(0,10), document_no: document.getElementById('documentNo').value.trim() || null, amount: num(document.getElementById('documentAmount').value || 0), file_ref: document.getElementById('documentFileRef').value.trim() || null, document_title: document.getElementById('documentTitle').value.trim() || null, note: document.getElementById('documentNote').value.trim() || null, created_at: new Date().toISOString() }; if(!payload.document_title && !payload.document_no){ notify('Belge başlığı veya belge no gir.'); return; } const isDup = state.documents.some(x=>String(x.cari_id||'')===String(payload.cari_id||'') && String(x.document_no||'')===String(payload.document_no||'') && String(x.document_date||'')===String(payload.document_date||'') && num(x.amount)===num(payload.amount)); if(isDup){ addAuditEvent('duplicate_blocked',{source_module:'documents', target_name:payload.cari_name || '-', amount:payload.amount, note:'Belge çift kayıt engellendi.', summary:`Çift kayıt engeli · belge · ${payload.document_no || payload.document_title || '-'}`}); notify('Muhtemel çift belge kaydı engellendi.'); return; } stampRecordIds(payload,'DOC'); state.documents.unshift(payload); writeLocalJson(LOCAL_DOCS_KEY, state.documents); addAuditEvent('document_saved',{source_module:'documents', target_name:payload.cari_name || '-', amount:payload.amount, note:payload.document_title || payload.document_no || 'Belge kaydedildi', summary:`Belge kaydedildi · ${payload.document_title || payload.document_no || '-'}`}); closeDocumentModal(); renderDocuments(); if(state.selectedCariId) renderSelectedCariDetail(); notify('Belge kaydı eklendi.'); } function normalizeText(v){ return String(v||'').toLocaleLowerCase('tr-TR'); } function parseMoneyFromText(text){ const matches = String(text||'').match(/\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{2})|\d+(?:[.,]\d{2})/g) || []; if(!matches.length) return 0; const cleaned = matches.map(m=>Number(String(m).replace(/\./g,'').replace(',', '.'))).filter(Number.isFinite); return cleaned.sort((a,b)=>b-a)[0] || 0; } function parseDateFromText(text){ const s=String(text||''); let m=s.match(/(\d{2})[./-](\d{2})[./-](\d{4})/); if(m) return `${m[3]}-${m[2]}-${m[1]}`; m=s.match(/(\d{4})-(\d{2})-(\d{2})/); if(m) return `${m[1]}-${m[2]}-${m[3]}`; return ''; } function detectCariFromText(text){ const normalized = normalizeText(text); let best=null; let score=0; for(const c of state.cariler){ const name=normalizeText(c.cari_name||''); if(!name || name.length<3) continue; if(normalized.includes(name) && name.length>score){ best=c; score=name.length; } } return best; } function detectMovementFromText(text){ const n=normalizeText(text); if(/moka|tahsil|ödeme al|havale|eft|fast|kredi kart|virman geldi/.test(n)) return 'tahsilat'; if(/gider|kira|sgk|elektrik|su fatur|internet|telefon|yazılım|yazilim|stopaj|vergi|muhasebe ofis gider/.test(n)) return 'gider'; if(/tahakkuk|ücret|ucret/.test(n)) return 'tahakkuk'; return 'tahsilat'; } function detectSourceFromText(text, fallback='whatsapp'){ const n=normalizeText(text); if(n.includes('telegram')) return 'telegram'; if(n.includes('whatsapp')) return 'whatsapp'; if(n.includes('dekont') || n.includes('makbuz') || n.includes('pdf') || n.includes('görsel') || n.includes('gorsel')) return 'gorsel'; return fallback; } function candidateAnalysisSummary(obj){ return `Kaynak: ${obj.source || '-'} · Hareket: ${obj.movement || '-'} · Tutar: ${money(obj.amount || 0)} · Tarih: ${obj.date || '-'} · Cari: ${obj.cariName || '-'}`; } function analyzeCandidateBridgeText(){ const raw = document.getElementById('candidateRawText')?.value || ''; if(!raw.trim()){ notify('Önce ham metni yapıştır.'); return; } const amount = parseMoneyFromText(raw); const date = parseDateFromText(raw) || new Date().toISOString().slice(0,10); const movement = detectMovementFromText(raw); const source = detectSourceFromText(raw, document.getElementById('candidateSource')?.value || 'whatsapp'); const cari = detectCariFromText(raw + ' ' + (document.getElementById('candidateSummary')?.value || '')); const summary = raw.split(/\n+/).map(x=>x.trim()).filter(Boolean)[0]?.slice(0,140) || 'Mesajdan aday kayıt üretildi'; let conf = 55; if(amount>0) conf += 15; if(date) conf += 10; if(movement) conf += 10; if(cari) conf += 10; document.getElementById('candidateAmount').value = amount || document.getElementById('candidateAmount').value; document.getElementById('candidateDate').value = date; document.getElementById('candidateMovementType').value = movement; document.getElementById('candidateSource').value = source; if(cari){ fillCariOptions('candidateCariId', true, 'Cari seçiniz'); document.getElementById('candidateCariId').value = cari.cari_id; } if(!document.getElementById('candidateSummary').value.trim()) document.getElementById('candidateSummary').value = summary; document.getElementById('candidateConfidence').value = String(conf); const info = {source, movement, amount, date, cariName:cari?.cari_name || ''}; document.getElementById('candidateAnalysisBox').innerHTML = `Analiz sonucu
${escapeHtml(candidateAnalysisSummary(info))}
Güven puanı: ${conf}`; notify('Metin analiz edildi.'); } function quickFillCandidate(kind){ if(kind==='moka'){ document.getElementById('candidateSource').value='whatsapp'; document.getElementById('candidateMovementType').value='tahsilat'; document.getElementById('candidateSummary').value='Moka kredi kartı tahsilat paylaşımı'; document.getElementById('candidateNote').value='Moka paylaşımı otomatik işlenmez, onaydan geçer.'; document.getElementById('candidateAnalysisBox').innerHTML='Moka şablonu
Moka tahsilatı önce Moka United aday kaydı olarak onaya düşer. Bankaya gelen aktarım daha sonra mahsup edilir.'; document.getElementById('candidateConfidence').value='72'; } } function getCurrentPeriodKey(){ const d=new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`; } function getExpenseAccruals(expenseId){ return (state.expenseAccruals||[]).filter(x=>String(x.expense_id)===String(expenseId)); } function getExpensePayments(expenseId){ return (state.expensePayments||[]).filter(x=>String(x.expense_id)===String(expenseId)); } function buildExpenseDueDate(periodKey, dueDay){ const [y,m]=String(periodKey||'').split('-').map(Number); if(!y || !m) return new Date().toISOString().slice(0,10); const lastDay = new Date(y, m, 0).getDate(); const d = Math.max(1, Math.min(lastDay, Number(dueDay||1))); return `${y}-${String(m).padStart(2,'0')}-${String(d).padStart(2,'0')}`; } function monthKeyFromDate(v){ const d=new Date(v||''); return Number.isNaN(d.getTime()) ? '' : `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`; } function getExpensePeriodMonths(exp){ const start = exp?.start_date || new Date().toISOString().slice(0,10); const endBase = exp?.end_date || new Date().toISOString().slice(0,10); const startDate = new Date(start); const endDate = new Date(endBase); if(Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) return []; const cur = new Date(startDate.getFullYear(), startDate.getMonth(), 1); const last = new Date(endDate.getFullYear(), endDate.getMonth(), 1); const out=[]; while(cur <= last){ out.push(`${cur.getFullYear()}-${String(cur.getMonth()+1).padStart(2,'0')}`); cur.setMonth(cur.getMonth()+1); } return out; } function ensureExpenseAccruals(){ const rows=[...(state.expenseAccruals||[])]; const now=new Date(); const currentMonthKey=`${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}`; let changed=false } function ensureExpenseAccruals(){ const rows=[...(state.expenseAccruals||[])]; const now=new Date(); const currentMonthKey=`${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}`; let changed=false; (state.expenses||[]).forEach(exp=>{ if(exp.is_active===false) return; const months=getExpensePeriodMonths({...exp, end_date: exp.end_date || buildExpenseDueDate(currentMonthKey, 1)}); months.forEach(periodKey=>{ if(periodKey>currentMonthKey) return; const exists=rows.find(x=>String(x.expense_id)===String(exp.id) && String(x.period_key)===String(periodKey)); if(exists) return; const accr=stampRecordIds({ expense_id:exp.id, expense_title:exp.title, expense_type:exp.expense_type || 'fixed', period_key:periodKey, due_date:buildExpenseDueDate(periodKey, exp.due_day||1), amount:num(exp.amount||0), status:'accrued', auto_generated:true, created_at:new Date().toISOString() }, 'EAC'); rows.push(accr); changed=true; }); }); rows.sort((a,b)=>String(b.period_key||'').localeCompare(String(a.period_key||'')) || new Date(b.created_at||0)-new Date(a.created_at||0)); state.expenseAccruals=rows; if(changed) writeLocalJson(LOCAL_EXPENSE_ACCRUALS_KEY, state.expenseAccruals); } function getExpenseTotals(exp){ const accruals=getExpenseAccruals(exp.id); const payments=getExpensePayments(exp.id); const now=new Date(); const thisMonthKey=getCurrentPeriodKey(); const thisYear=String(now.getFullYear()); const monthlyAccrued=accruals.filter(x=>x.period_key===thisMonthKey).reduce((a,b)=>a+num(b.amount),0); const yearlyAccrued=accruals.filter(x=>String(x.period_key||'').startsWith(thisYear+'-')).reduce((a,b)=>a+num(b.amount),0); const paidTotal=payments.reduce((a,b)=>a+num(b.amount),0); const thisYearPaid=payments.filter(x=>String(x.payment_date||'').startsWith(thisYear+'-')).reduce((a,b)=>a+num(b.amount),0); const remaining=Math.max(0, accruals.reduce((a,b)=>a+num(b.amount),0)-paidTotal); return {accruals,payments,monthlyAccrued,yearlyAccrued,paidTotal,thisYearPaid,remaining}; } function getAllExpenseMetrics(){ ensureExpenseAccruals(); const monthKey=getCurrentPeriodKey(); const year=monthKey.slice(0,4); const allAcc=state.expenseAccruals||[]; const allPay=state.expensePayments||[]; return { monthAccrued: allAcc.filter(x=>x.period_key===monthKey).reduce((a,b)=>a+num(b.amount),0), yearAccrued: allAcc.filter(x=>String(x.period_key||'').startsWith(year+'-')).reduce((a,b)=>a+num(b.amount),0), yearPaid: allPay.filter(x=>String(x.payment_date||'').startsWith(year+'-')).reduce((a,b)=>a+num(b.amount),0), remaining: Math.max(0, allAcc.reduce((a,b)=>a+num(b.amount),0)-allPay.reduce((a,b)=>a+num(b.amount),0)), }; } function expenseShareRatio(exp){ const total=state.expenseAccruals.reduce((a,b)=>a+num(b.amount),0); if(!total) return 0; return (getExpenseAccruals(exp.id).reduce((a,b)=>a+num(b.amount),0) / total) * 100; } function openExpensePaymentModal(expenseId, accrualId=''){ const exp=state.expenses.find(x=>String(x.id)===String(expenseId || state.selectedExpenseId)); if(!exp){ notify('Gider kartı bulunamadı.'); return; } state.selectedExpenseId=exp.id; state.selectedExpenseAccrualId=accrualId || null; fillAccountSelect('expensePaymentAccount'); document.getElementById('expensePaymentDate').value=new Date().toISOString().slice(0,10); document.getElementById('expensePaymentAmount').value=''; document.getElementById('expensePaymentDocNo').value=''; document.getElementById('expensePaymentDescription').value=`${exp.title} gider ödemesi`; document.getElementById('expensePaymentNote').value=''; const totals=getExpenseTotals(exp); document.getElementById('expensePaymentInfo').innerHTML=`${escapeHtml(exp.title)}
Bu yıl tahakkuk: ${money(totals.yearlyAccrued)} · Bu yıl ödenen: ${money(totals.thisYearPaid)} · Kalan: ${money(totals.remaining)}`; document.getElementById('expensePaymentModal').style.display='flex'; } function closeExpensePaymentModal(){ document.getElementById('expensePaymentModal').style.display='none'; } async function saveExpensePayment(){ const exp=state.expenses.find(x=>String(x.id)===String(state.selectedExpenseId)); if(!exp){ notify('Gider kartı bulunamadı.'); return; } const accountKey=document.getElementById('expensePaymentAccount').value; const account=getAccountByKey(accountKey); const amount=num(document.getElementById('expensePaymentAmount').value||0); const paymentDate=document.getElementById('expensePaymentDate').value || new Date().toISOString().slice(0,10); const docNo=(document.getElementById('expensePaymentDocNo').value||'').trim(); const description=(document.getElementById('expensePaymentDescription').value||'').trim() || `${exp.title} gider ödemesi`; const note=(document.getElementById('expensePaymentNote').value||'').trim(); if(!account){ notify('Ödeme hesabı seç.'); return; } if(!amount){ notify('Ödeme tutarı zorunlu.'); return; } const payment=stampRecordIds({ expense_id:exp.id, expense_title:exp.title, accrual_id:state.selectedExpenseAccrualId || null, account_key:account.account_key, account_name:account.account_name, amount, payment_date:paymentDate, document_no:docNo || null, description, note, created_at:new Date().toISOString() }, 'EPA'); state.expensePayments=[payment, ...(state.expensePayments||[]).filter(x=>String(x.id)!==String(payment.id))]; writeLocalJson(LOCAL_EXPENSE_PAYMENTS_KEY, state.expensePayments); await upsertLiveRow('expense_payments', payment); const op=stampRecordIds({ type:'expense_payment', expense_id:exp.id, account_key:account.account_key, account_name:account.account_name, account_type:account.account_type, direction:'cikis', amount, entry_date:paymentDate, description, note:note || docNo || exp.title, created_at:new Date().toISOString() }, 'LAC'); const ops=getLocalAccountOps().filter(x=>String(x.id)!==String(op.id)); ops.unshift(op); setLocalAccountOps(ops); await upsertLiveRow('account_ops', op); addAuditEvent('expense_payment_saved',{source_module:'expenses', target_name:exp.title, amount, note:note || description, summary:`Gider ödemesi işlendi · ${money(amount)}`}); closeExpensePaymentModal(); await loadAccounts(); renderAccounts(); renderExpenses(); notify('Gider ödemesi kaydedildi ve canlı yazım denendi.'); } function expenseStatusInfo(exp){ const now=new Date(); const periodKey=getCurrentPeriodKey(); const active = exp.is_active !== false && (!exp.start_date || new Date(exp.start_date) <= now) && (!exp.end_date || new Date(exp.end_date) >= new Date(now.getFullYear(), now.getMonth(), 1)); if(!active) return {code:'passive', label:'Pasif', cls:'gray'}; if(String(exp.last_candidate_period||'')===periodKey) return {code:'queued', label:'Bu Ay Aday Oluştu', cls:'amber'}; const dueDay = Math.max(1, Math.min(31, Number(exp.due_day || 1))); const today = now.getDate(); if(today < dueDay) return {code:'upcoming', label:'Yaklaşan', cls:'amber'}; if(String(exp.approval_policy||'approval')==='ready') return {code:'ready', label:'Hazır / Kontrol', cls:'green'}; return {code:'approval', label:'Onaya Düşmeli', cls:'red'}; } function expenseStatusTag(exp){ const s=expenseStatusInfo(exp); return `${escapeHtml(s.label)}`; } function filteredExpenses(){ if(state.expenseFilter==='all') return state.expenses; if(state.expenseFilter==='fixed') return state.expenses.filter(x=>x.expense_type==='fixed'); if(state.expenseFilter==='variable') return state.expenses.filter(x=>x.expense_type==='variable'); if(state.expenseFilter==='approval') return state.expenses.filter(x=>expenseStatusInfo(x).code==='approval'); if(state.expenseFilter==='queued') return state.expenses.filter(x=>expenseStatusInfo(x).code==='queued'); if(state.expenseFilter==='passive') return state.expenses.filter(x=>expenseStatusInfo(x).code==='passive'); return state.expenses; } function openExpenseModal(expenseId=''){ const exp = state.expenses.find(x=>String(x.id)===String(expenseId)); state.selectedExpenseId = exp?.id || null; ensureExpenseAccruals(); const totals = exp ? getExpenseTotals(exp) : null; document.getElementById('expenseTitle').value = exp?.title || ''; document.getElementById('expenseCategory').value = exp?.category || ''; document.getElementById('expenseVendor').value = exp?.vendor_name || ''; document.getElementById('expenseType').value = exp?.expense_type || 'fixed'; document.getElementById('expenseApprovalPolicy').value = exp?.approval_policy || 'approval'; document.getElementById('expenseAmount').value = exp?.amount || ''; document.getElementById('expenseStartDate').value = exp?.start_date || new Date().toISOString().slice(0,10); document.getElementById('expenseEndDate').value = exp?.end_date || ''; document.getElementById('expenseDueDay').value = exp?.due_day || 1; document.getElementById('expenseIsActive').value = String(exp?.is_active !== false); document.getElementById('expenseNote').value = exp?.note || ''; document.getElementById('expenseModalInfo').innerHTML = exp ? `Durum
${expenseStatusTag(exp)}
Sözleşme: ${shortDate(exp.start_date)}${exp.end_date ? ` → ${shortDate(exp.end_date)}` : ' → Açık'}
Otomatik tahakkuk ayı: ${totals.accruals.length}
Ödenen: ${money(totals.paidTotal)} · Kalan: ${money(totals.remaining)}` : 'Yeni gider tanımı oluşturuluyor. Sabit/değişken seçiminden sonra sözleşme başlangıç ve bitiş tarihine göre aylık otomatik tahakkuk planı oluşur.'; document.getElementById('expenseModal').style.display='flex'; } function closeExpenseModal(){ document.getElementById('expenseModal').style.display='none'; } function requestDeleteSelectedExpense(){ const exp = state.expenses.find(x=>String(x.id)===String(state.selectedExpenseId)); if(!exp){ notify('Silinecek gider kartı bulunamadı.'); return; } openDeleteAuthModal(`Gider kartı silme · ${exp.title}`, async (reason)=>{ archiveDeletion('expenses', exp, reason); state.expenses = state.expenses.filter(x=>String(x.id)!==String(exp.id)); state.expenseAccruals = state.expenseAccruals.filter(x=>String(x.expense_id)!==String(exp.id)); state.expensePayments = state.expensePayments.filter(x=>String(x.expense_id)!==String(exp.id)); writeLocalJson(LOCAL_EXPENSES_KEY, state.expenses); writeLocalJson(LOCAL_EXPENSE_ACCRUALS_KEY, state.expenseAccruals); writeLocalJson(LOCAL_EXPENSE_PAYMENTS_KEY, state.expensePayments); await deleteLiveRow('expenses', exp.id); for(const a of (state.expenseAccruals||[]).filter(x=>String(x.expense_id)===String(exp.id))){ await deleteLiveRow('expense_accruals', a.id); } for(const p of (state.expensePayments||[]).filter(x=>String(x.expense_id)===String(exp.id))){ await deleteLiveRow('expense_payments', p.id); } addAuditEvent('expense_deleted',{source_module:'expenses', target_name:exp.title, amount:exp.amount, note:reason, summary:`Gider kartı silindi · ${exp.title}`}); closeExpenseModal(); renderExpenses(); renderDeleteArchive(); renderGlobalAuditLog(); notify('Gider kartı silindi.'); }); } async function saveExpenseAccrual(){ const title=(document.getElementById('expenseTitle').value||'').trim(); if(!title){ notify('Gider adı zorunlu.'); return; } const expenseId = state.selectedExpenseId || uid('expense'); const existing = state.expenses.find(x=>String(x.id)===String(state.selectedExpenseId)); const payload={ id: expenseId, txn_id: existing?.txn_id || expenseId, operation_no: existing?.operation_no || undefined, title, category:(document.getElementById('expenseCategory').value||'').trim() || null, vendor_name:(document.getElementById('expenseVendor').value||'').trim() || null, expense_type:document.getElementById('expenseType').value || 'fixed', approval_policy:document.getElementById('expenseApprovalPolicy').value || 'approval', amount:num(document.getElementById('expenseAmount').value || 0), start_date:document.getElementById('expenseStartDate').value || new Date().toISOString().slice(0,10), end_date:document.getElementById('expenseEndDate').value || null, due_day:num(document.getElementById('expenseDueDay').value || 1), is_active:document.getElementById('expenseIsActive').value === 'true', note:(document.getElementById('expenseNote').value||'').trim() || null, updated_at:new Date().toISOString(), created_at: existing?.created_at || new Date().toISOString(), last_candidate_period: existing?.last_candidate_period || null, last_candidate_at: existing?.last_candidate_at || null, }; stampRecordIds(payload,'EXP'); const idx = state.expenses.findIndex(x=>String(x.id)===String(payload.id)); if(idx>=0) state.expenses[idx]=payload; else state.expenses.unshift(payload); writeLocalJson(LOCAL_EXPENSES_KEY, state.expenses); await upsertLiveRow('expenses', payload); ensureExpenseAccruals(); const relatedAccruals=(state.expenseAccruals||[]).filter(x=>String(x.expense_id)===String(payload.id)); for(const accr of relatedAccruals){ await upsertLiveRow('expense_accruals', accr); } addAuditEvent('expense_saved',{source_module:'expenses', target_name:payload.title, amount:payload.amount, note:`${payload.expense_type==='fixed' ? 'Sabit' : 'Değişken'} gider · ${payload.start_date}${payload.end_date ? ' → ' + payload.end_date : ' → Açık'}`, summary:`Gider kartı kaydedildi · ${payload.title}`}); closeExpenseModal(); renderExpenses(); notify('Gider kartı kaydedildi ve canlı yazım denendi.'); } function queueExpenseCandidate(expenseId){ const exp = state.expenses.find(x=>String(x.id)===String(expenseId)); if(!exp){ notify('Gider kartı bulunamadı.'); return; } ensureExpenseAccruals(); const periodKey=getCurrentPeriodKey(); if(String(exp.last_candidate_period||'')===periodKey){ addAuditEvent('duplicate_blocked',{source_module:'expenses', target_name:exp.title, note:'Bu gider için bu ay ikinci aday engellendi.', summary:`Çift kayıt engeli · gider adayı · ${exp.title}`}); notify('Bu gider için bu ay zaten aday oluşturulmuş.'); return; } const accr = getExpenseAccruals(exp.id).find(x=>String(x.period_key)===String(periodKey)); const bridgeId = uid('bridge'); const payload={ id: bridgeId, txn_id: bridgeId, expense_id: exp.id, expense_title: exp.title, expense_accrual_id: accr?.id || null, cari_id: null, cari_name: exp.vendor_name || exp.title, source_type: 'gider_tahakkuk', suggested_movement_type: 'gider', candidate_date: new Date().toISOString().slice(0,10), suggested_amount: num(accr?.amount || exp.amount || 0), content_summary: `${exp.title} gider tahakkuk adayı`, raw_content: `${periodKey} dönemi için gider tahakkuk adayı oluşturuldu. Kategori: ${exp.category || '-'} · Hedef: ${exp.vendor_name || '-'} · Tutar: ${accr?.amount || exp.amount || 0}`, note: exp.note || 'Gider ekranından üretildi', confidence_score: exp.approval_policy === 'ready' ? 92 : 78, status: 'onay_bekliyor', created_at: new Date().toISOString() }; stampRecordIds(payload,'BRG'); state.bridgeCandidates.unshift(payload); addAuditEvent('expense_candidate_queued',{source_module:'expenses', target_name:exp.title, amount:payload.suggested_amount, note:`${periodKey} dönemi için onaya gönderildi.`, summary:`Gider adayı üretildi · ${exp.title}`}); exp.last_candidate_period = periodKey; exp.last_candidate_at = new Date().toISOString(); writeLocalJson(LOCAL_BRIDGE_KEY, state.bridgeCandidates); writeLocalJson(LOCAL_EXPENSES_KEY, state.expenses); renderExpenses(); renderDocuments(); renderApprovals(); notify('Gider tahakkuk adayı onaya gönderildi.'); } function queueExpenseForCurrentMonth(){ if(!state.selectedExpenseId){ notify('Önce kayıtlı bir gider kartı seç.'); return; } queueExpenseCandidate(state.selectedExpenseId); } function renderExpenses(){ ensureExpenseAccruals(); const rows=filteredExpenses(); const total=state.expenses.length; const fixed=state.expenses.filter(x=>x.expense_type==='fixed').length; const variable=state.expenses.filter(x=>x.expense_type==='variable').length; const approval=state.expenses.filter(x=>expenseStatusInfo(x).code==='approval').length; const queued=state.expenses.filter(x=>expenseStatusInfo(x).code==='queued').length; const totals=getAllExpenseMetrics(); const wrap=document.getElementById('expenseWrap'); if(!wrap) return; wrap.innerHTML = `
Toplam Gider Kartı
${total}
Sabit
${fixed}
Değişken
${variable}
Bu Ay Onaya Düşecek
${approval}
Bu Ay Tahakkuk
${money(totals.monthAccrued)}
Bu Yıl Tahakkuk
${money(totals.yearAccrued)}
Bu Yıl Ödenen
${money(totals.yearPaid)}
Toplam Kalan
${money(totals.remaining)}
Bu Ay Aday
${queued}
Gider kartı kaydedildiğinde sözleşme başlangıç/bitiş tarihine göre aylık tahakkuk planı otomatik oluşur. Ödemeler ayrıca işlenir ve kalan tutar dinamik hesaplanır.
` + (rows.length ? `${rows.map(r=>{ const t=getExpenseTotals(r); return ``; }).join('')}
NoGiderTürSözleşmeBu AyBu YılÖdenenKalanPayİşlem
${escapeHtml(r.operation_no || txnId(r,'EXP'))}${escapeHtml(r.title)}
${escapeHtml(r.category || '-')} · ${escapeHtml(r.vendor_name || '-')}
${r.expense_type==='fixed' ? 'Sabit' : 'Değişken'}
${expenseStatusTag(r)}
${shortDate(r.start_date)}${r.end_date ? ` → ${shortDate(r.end_date)}` : ' → Açık'}
Vade günü: ${escapeHtml(String(r.due_day||1))}
${money(t.monthlyAccrued)}${money(t.yearlyAccrued)}${money(t.thisYearPaid)}${money(t.remaining)}%${expenseShareRatio(r).toFixed(1)}
` : `
Bu filtrede gider kartı yok.
`)+ ((state.expenseAccruals||[]).length ? `

Son Gider Tahakkukları

${[...state.expenseAccruals].sort((a,b)=>String(b.period_key||'').localeCompare(String(a.period_key||''))).slice(0,18).map(a=>{ const paid=(state.expensePayments||[]).filter(p=>String(p.accrual_id||'')===String(a.id)).reduce((x,y)=>x+num(y.amount),0); return ``; }).join('')}
DönemGiderVadeTahakkukÖdenenKalan
${escapeHtml(a.period_key||'-')}${escapeHtml(a.expense_title||'-')}${shortDate(a.due_date)}${money(a.amount||0)}${money(paid)}${money(Math.max(0,num(a.amount)-paid))}
` : ''); wrap.querySelectorAll('[data-expense-id]').forEach(el=>el.addEventListener('click',()=>openExpenseModal(el.dataset.expenseId))); } function openCandidateBridgeModal(cariId=state.selectedCariId){ fillCariOptions('candidateCariId', true, 'Cari seçiniz'); document.getElementById('candidateCariId').value = cariId || ''; document.getElementById('candidateSource').value = 'whatsapp'; document.getElementById('candidateMovementType').value = 'tahsilat'; document.getElementById('candidateDate').value = new Date().toISOString().slice(0,10); document.getElementById('candidateAmount').value = ''; document.getElementById('candidateSummary').value = ''; document.getElementById('candidateRawText').value = ''; document.getElementById('candidateNote').value = ''; document.getElementById('candidateConfidence').value = '70'; document.getElementById('candidateAnalysisBox').innerHTML = 'Köprü kuralı: mesaj doğrudan işlenmez. Önce analiz edilir, sonra aday kayıt olarak onaya düşer.'; document.getElementById('candidateBridgeModal').style.display='flex'; } function closeCandidateBridgeModal(){ document.getElementById('candidateBridgeModal').style.display='none'; } async function saveCandidateBridge(){ const cariId = document.getElementById('candidateCariId').value || null; const cari = state.cariler.find(x=>x.cari_id===cariId); const payload = { id: uid('bridge'), cari_id: cariId, cari_name: cari?.cari_name || null, source_type: document.getElementById('candidateSource').value, suggested_movement_type: document.getElementById('candidateMovementType').value, candidate_date: document.getElementById('candidateDate').value || new Date().toISOString().slice(0,10), suggested_amount: num(document.getElementById('candidateAmount').value || 0), content_summary: document.getElementById('candidateSummary').value.trim(), raw_content: document.getElementById('candidateRawText').value.trim(), note: document.getElementById('candidateNote').value.trim() || null, confidence_score: num(document.getElementById('candidateConfidence')?.value || 70), status: 'onay_bekliyor', created_at: new Date().toISOString() }; payload.is_duplicate = batchDuplicateExists(payload.cari_id, payload.suggested_amount, payload.candidate_date, payload.raw_content); if(!payload.content_summary || !payload.raw_content){ notify('Kısa özet ve ham metin zorunlu.'); return; } stampRecordIds(payload,'BRG'); state.bridgeCandidates.unshift(payload); writeLocalJson(LOCAL_BRIDGE_KEY, state.bridgeCandidates); addAuditEvent(payload.is_duplicate ? 'bridge_duplicate_candidate' : 'bridge_candidate_saved',{source_module:'bridge', target_name:payload.cari_name || '-', amount:payload.suggested_amount, note:payload.is_duplicate ? 'Muhtemel çift olarak onaya alındı.' : 'Aday kayıt oluşturuldu.', summary: payload.is_duplicate ? `Köprü adayı · muhtemel çift · ${payload.content_summary}` : `Köprü adayı oluşturuldu · ${payload.content_summary}`}); closeCandidateBridgeModal(); renderDocuments(); renderExpenses(); renderYearTransitionPanel(); renderApprovals(); renderBackup(); notify('Aday kayıt onay akışına eklendi.'); } function openCariEditModal(){ const card=getCariCard(state.selectedCariId); const c=state.cariler.find(x=>x.cari_id===state.selectedCariId); if(!card && !c){ notify("Cari bulunamadı."); return; } document.getElementById("editCariName").value=card?.cari_name||c?.cari_name||""; document.getElementById("editCariCode").value=card?.cari_code||c?.cari_code||""; document.getElementById("editTaxpayerType").value=card?.taxpayer_type||c?.taxpayer_type||""; document.getElementById("editMonthlyFeeAmount").value=card?.monthly_fee_amount||0; document.getElementById("editMonthlyFeeEnabled").value=String(card?.monthly_fee_enabled ?? false); document.getElementById("editOpeningDate").value=getFeeSetup(state.selectedCariId)?.opening_date||""; document.getElementById("editContactPerson").value=card?.contact_person||""; document.getElementById("editPhone").value=card?.phone||""; document.getElementById("editEmail").value=card?.email||""; document.getElementById("editTaxOffice").value=card?.tax_office||""; document.getElementById("editTaxNo").value=card?.tax_no||""; document.getElementById("editTcNo").value=card?.tc_no||""; document.getElementById("editCity").value=card?.city||""; document.getElementById("editDistrict").value=card?.district||""; document.getElementById("editAddress").value=card?.address||""; document.getElementById("editNotes").value=card?.notes||""; document.getElementById("editIsActive").value=String(card?.is_active ?? true); document.getElementById("editIsSpecial").value=String(card?.is_special ?? false); document.getElementById("cariEditModal").style.display="flex"; } function closeCariEditModal(){ document.getElementById("cariEditModal").style.display="none"; } async function saveCariCard(){ if(!state.selectedCariId){ notify("Cari seçili değil."); return; } const payload={ cari_name: document.getElementById("editCariName").value.trim(), cari_code: document.getElementById("editCariCode").value.trim() || null, taxpayer_type: document.getElementById("editTaxpayerType").value || null, monthly_fee_amount: Number(document.getElementById("editMonthlyFeeAmount").value || 0), monthly_fee_enabled: document.getElementById("editMonthlyFeeEnabled").value === "true", contact_person: document.getElementById("editContactPerson").value.trim() || null, phone: document.getElementById("editPhone").value.trim() || null, email: document.getElementById("editEmail").value.trim() || null, tax_office: document.getElementById("editTaxOffice").value.trim() || null, tax_no: document.getElementById("editTaxNo").value.trim() || null, tc_no: document.getElementById("editTcNo").value.trim() || null, city: document.getElementById("editCity").value.trim() || null, district: document.getElementById("editDistrict").value.trim() || null, address: document.getElementById("editAddress").value.trim() || null, notes: document.getElementById("editNotes").value.trim() || null, is_active: document.getElementById("editIsActive").value === "true", is_special: document.getElementById("editIsSpecial").value === "true", updated_at: new Date().toISOString() }; const {error} = await supabaseClient.from("cari_cards").update(payload).eq("id", state.selectedCariId); if(error){ logSystemError('cari_cards', error.message, {target_name:payload.cari_name || state.selectedCariId}); notify("Cari kaydedilemedi: " + error.message); return; } const opening_date=document.getElementById("editOpeningDate").value || null; const setupRows=(state.cariFeeSetups||[]).filter(x=>String(x.cari_id)!==String(state.selectedCariId)); setupRows.unshift(stampRecordIds({cari_id:state.selectedCariId,cari_name:payload.cari_name, opening_date, monthly_fee_amount:num(payload.monthly_fee_amount), monthly_fee_enabled:payload.monthly_fee_enabled, taxpayer_type:payload.taxpayer_type||null, created_at:new Date().toISOString()}, "CFS")); state.cariFeeSetups=setupRows; writeLocalJson(LOCAL_CARI_FEE_SETUP_KEY, state.cariFeeSetups); if(opening_date && payload.monthly_fee_enabled && num(payload.monthly_fee_amount)>0){ const yearEnd = yearEndFromDate(opening_date); const sameYearFirst = getFeeTimeline(state.selectedCariId).find(x=>String(x.valid_from||'').slice(0,10)===String(opening_date).slice(0,10)); if(!sameYearFirst){ upsertLocalFeePeriod({ cari_id: state.selectedCariId, current_fee: num(payload.monthly_fee_amount), valid_from: String(opening_date).slice(0,10), valid_to: yearEnd, note: 'İlk ücret dönemi', source:'local_first', sync_status:'local_only' }); try{ const insertPayload = feeHistoryInsertPayload({ cari_id: state.selectedCariId, fee_amount: num(payload.monthly_fee_amount), valid_from: String(opening_date).slice(0,10), valid_to: yearEnd, note: 'İlk ücret dönemi', created_at: new Date().toISOString(), updated_at: new Date().toISOString() }); const ins = await supabaseClient.from("cari_fee_history").insert([insertPayload]); if(ins.error) throw ins.error; await loadFeeData(); }catch(err){ logSystemError('cari_fee_history', String(err?.message || err || 'Ücret dönemi canlıya yazılamadı'), {target_name:payload.cari_name || state.selectedCariId, amount:num(payload.monthly_fee_amount)}); } } } ensureFeeAccrualsForAllCariler(); backfillFeeAccrualsForCari(state.selectedCariId, FEE_MILESTONE_START); addAuditEvent('cari_card_updated',{source_module:'cari_cards', target_name:payload.cari_name || state.selectedCariId, note:'Cari kartı güncellendi.', summary:`Cari kartı güncellendi · ${payload.cari_name || state.selectedCariId}`}); notify(`Cari kartı güncellendi ve ${FEE_MILESTONE_START} itibariyle sözleşmeye göre ücret tahakkukları güncellendi.`); closeCariEditModal(); await loadCariler(); await loadSelectedCariHareketler(); renderDashboard(); renderCariList(); renderSelectedCariDetail(); renderReports(); } function openPromiseModal(){ if(!state.selectedCariId){ notify("Cari seçili değil."); return; } document.getElementById("promiseEntryDate").value = new Date().toISOString().slice(0,10); document.getElementById("promisePaymentDate").value = ""; document.getElementById("promiseAmount").value = ""; document.getElementById("promiseChannel").value = ""; document.getElementById("promiseBy").value = ""; document.getElementById("promiseNote").value = ""; document.getElementById("promiseModal").style.display="flex"; } function closePromiseModal(){ document.getElementById("promiseModal").style.display="none"; } async function savePaymentPromise(){ if(!state.selectedCariId){ notify("Cari seçili değil."); return; } const payload={ cari_id: state.selectedCariId, promise_entry_date: document.getElementById("promiseEntryDate").value || new Date().toISOString().slice(0,10), promised_payment_date: document.getElementById("promisePaymentDate").value, promised_amount: Number(document.getElementById("promiseAmount").value || 0), channel: document.getElementById("promiseChannel").value.trim() || null, promised_by: document.getElementById("promiseBy").value.trim() || null, note: document.getElementById("promiseNote").value.trim() || null, status: "aktif" }; if(!payload.promised_payment_date || !payload.promised_amount){ notify("Ödeme tarihi ve tutar zorunlu."); return; } await supabaseClient.from("payment_promises").update({status:"revize"}).eq("cari_id", state.selectedCariId).eq("status","aktif"); const {error} = await supabaseClient.from("payment_promises").insert([payload]); if(error){ logSystemError('payment_promises', error.message, {target_name:state.selectedCariId, amount:payload.promised_amount}); notify("Ödeme sözü kaydedilemedi: " + error.message); return; } addAuditEvent('payment_promise_saved',{source_module:'payment_promises', target_name:state.selectedCariId, amount:payload.promised_amount, note:payload.note || 'Ödeme sözü kaydedildi.', summary:`Ödeme sözü kaydedildi · ${money(payload.promised_amount)}`}); notify("Ödeme sözü kaydedildi."); closePromiseModal(); await loadCariler(); renderDashboard(); renderReports(); } function openManualFinanceModal(mode='entry', opId='', presetAccountKey=''){ window.__editingManualOpId = opId || ''; const existing = opId ? getLocalAccountOps().find(x=>String(x.id)===String(opId)) : null; document.getElementById('manualFinanceMode').value=mode; document.getElementById('manualFinanceDate').value=existing?.entry_date || new Date().toISOString().slice(0,10); document.getElementById('manualFinanceAmount').value=existing?.amount || existing?.opening_balance || ''; document.getElementById('manualFinanceDescription').value=existing?.description || ''; document.getElementById('manualFinanceNote').value=existing?.note || ''; document.getElementById('manualFinanceCreateName').value=existing?.account_name || ''; document.getElementById('manualFinanceCreateType').value=existing?.account_type || 'kasa'; fillAccountSelect('manualFinanceAccount'); fillAccountSelect('manualFinanceTarget'); const createWrap=document.getElementById('manualFinanceCreateFields'); const entryWrap=document.getElementById('manualFinanceEntryFields'); const targetWrap=document.getElementById('manualFinanceTargetWrap'); const directionWrap=document.getElementById('manualFinanceDirectionWrap'); const amountLabel=document.getElementById('manualFinanceAmountLabel'); const info=document.getElementById('manualFinanceInfo'); const title=document.getElementById('manualFinanceTitle'); createWrap.style.display = mode==='create' ? 'grid' : 'none'; entryWrap.style.display = mode==='create' ? 'none' : 'block'; targetWrap.style.display = mode==='transfer' ? 'block' : 'none'; directionWrap.style.display = mode==='entry' ? 'block' : 'none'; if(existing){ if(document.getElementById('manualFinanceAccount').querySelector(`option[value="${existing.account_key || existing.source_account_key || presetAccountKey}"]`)) document.getElementById('manualFinanceAccount').value = existing.account_key || existing.source_account_key || presetAccountKey; if(document.getElementById('manualFinanceTarget').querySelector(`option[value="${existing.target_account_key || ''}"]`)) document.getElementById('manualFinanceTarget').value = existing.target_account_key || ''; if(existing.direction) document.getElementById('manualFinanceDirection').value = existing.direction; } else if(presetAccountKey){ if(document.getElementById('manualFinanceAccount').querySelector(`option[value="${presetAccountKey}"]`)) document.getElementById('manualFinanceAccount').value = presetAccountKey; } if(mode==='create'){ title.textContent=existing ? 'Hesap Tanımını Düzenle' : 'Yeni Hesap Tanımı'; amountLabel.textContent='Açılış Bakiye'; info.innerHTML='Yeni hesap kartı oluştur. İstersen açılış bakiyesini de burada ver.'; } if(mode==='entry'){ title.textContent=existing ? 'Hesap İşlemini Düzenle' : 'Hesaba Manuel İşlem'; amountLabel.textContent='Tutar'; info.innerHTML='Seçtiğin hesaba giriş veya çıkış hareketi işle. Bu kayıt yerel hareket defterine yazılır.'; } if(mode==='transfer'){ title.textContent=existing ? 'Transferi Düzenle' : 'Hesaplar Arası Transfer'; amountLabel.textContent='Transfer Tutarı'; info.innerHTML='Kaynak hesaptan düş, hedef hesaba ekle. Tek kayıtla iki hesap birlikte güncellenir.'; } document.getElementById('manualFinanceModal').style.display='flex'; } function closeManualFinanceModal(){ document.getElementById('manualFinanceModal').style.display='none'; window.__editingManualOpId=''; } async function saveManualFinanceAction(){ const mode=document.getElementById('manualFinanceMode').value || 'entry'; const ops=getLocalAccountOps(); const entryDate=document.getElementById('manualFinanceDate').value || new Date().toISOString().slice(0,10); const amount=num(document.getElementById('manualFinanceAmount').value||0); const description=(document.getElementById('manualFinanceDescription').value||'').trim(); const note=(document.getElementById('manualFinanceNote').value||'').trim(); const editingId = window.__editingManualOpId || ''; const existing = editingId ? ops.find(x=>String(x.id)===String(editingId)) : null; let payload=null; if(mode==='create'){ const accountName=(document.getElementById('manualFinanceCreateName').value||'').trim(); const accountType=document.getElementById('manualFinanceCreateType').value || 'diger'; if(!accountName){ notify('Hesap adı zorunlu.'); return; } payload=stampRecordIds({ id: existing?.id || undefined, txn_id: existing?.txn_id || undefined, operation_no: existing?.operation_no || undefined, type:'account_create', account_key:normalizeAccountKey(accountName), account_name:accountName, account_type:accountType, opening_balance:amount, entry_date:entryDate, description:description || 'Yeni hesap tanımı', note, created_at:existing?.created_at || new Date().toISOString(), updated_at:new Date().toISOString() }, 'LAC'); } else if(mode==='entry'){ const accountKey=document.getElementById('manualFinanceAccount').value; const acc=getAccountByKey(accountKey); const direction=document.getElementById('manualFinanceDirection').value || 'giris'; if(!accountKey || !acc){ notify('Hesap seç.'); return; } if(!amount){ notify('Tutar zorunlu.'); return; } payload=stampRecordIds({ id: existing?.id || undefined, txn_id: existing?.txn_id || undefined, operation_no: existing?.operation_no || undefined, type:'account_entry', account_key:acc.account_key, account_name:acc.account_name, account_type:acc.account_type, direction, amount, entry_date:entryDate, description:description || (direction==='cikis' ? 'Hesaptan çıkış' : 'Hesaba giriş'), note, created_at:existing?.created_at || new Date().toISOString(), updated_at:new Date().toISOString() }, 'LAC'); } else { const sourceKey=document.getElementById('manualFinanceAccount').value; const targetKey=document.getElementById('manualFinanceTarget').value; const source=getAccountByKey(sourceKey); const target=getAccountByKey(targetKey); if(!sourceKey || !targetKey || !source || !target){ notify('Kaynak ve hedef hesap zorunlu.'); return; } if(sourceKey===targetKey){ notify('Aynı hesaba transfer olmaz.'); return; } if(!amount){ notify('Transfer tutarı zorunlu.'); return; } payload=stampRecordIds({ id: existing?.id || undefined, txn_id: existing?.txn_id || undefined, operation_no: existing?.operation_no || undefined, type:'account_transfer', source_account_key:source.account_key, source_account_name:source.account_name, target_account_key:target.account_key, target_account_name:target.account_name, amount, entry_date:entryDate, description:description || 'Hesaplar arası transfer', note, created_at:existing?.created_at || new Date().toISOString(), updated_at:new Date().toISOString() }, 'LAC'); } const nextOps=ops.filter(x=>String(x.id)!==String(payload.id)); nextOps.unshift(payload); setLocalAccountOps(nextOps); await upsertLiveRow('account_ops', payload); addAuditEvent('manual_finance_saved',{source_module:'manual_finance', target_name:payload.account_name || `${payload.source_account_name || '-'} → ${payload.target_account_name || '-'}`, amount:amount, note:payload.note || payload.description, summary:`Manuel hesap işlemi ${existing ? 'güncellendi' : 'kaydedildi'} · ${payload.type}`}); closeManualFinanceModal(); await loadAccounts(); renderAccounts(); notify('Hesap işlemi kaydedildi ve canlı yazım denendi.'); } async function purgeCariTahsilatlarFromDate(dateFrom='2026-01-01'){ if(!state.selectedCariId){ notify('Cari seçili değil.'); return; } const cari=state.cariler.find(x=>x.cari_id===state.selectedCariId); const cariName=String(cari?.cari_name || '').trim(); if(!cariName){ notify('Cari bilgisi bulunamadı.'); return; } if(!confirm(`${cariName} için ${dateFrom} ve sonrası tahsilatlar silinecek. Devam edilsin mi?`)) return; const collections=getLocalCariCollections(); const targetCollections=collections.filter(x=>String(x.cari_id)===String(state.selectedCariId) && String(x.line_date||x.created_at||'').slice(0,10) >= dateFrom); const targetCollectionIds=new Set(targetCollections.map(x=>String(x.id))); setLocalCariCollections(collections.filter(x=>!targetCollectionIds.has(String(x.id)))); const localOps=getLocalAccountOps(); const targetOps=localOps.filter(x=>String(x.type)==='cari_collection' && String(x.entry_date||x.created_at||'').slice(0,10) >= dateFrom && String(x.description||'').includes(cariName)); const targetOpIds=new Set(targetOps.map(x=>String(x.id))); setLocalAccountOps(localOps.filter(x=>!targetOpIds.has(String(x.id)))); state.operationalLedger=(state.operationalLedger||[]).filter(x=>!(String(x.source_module||'')==='local_cari_collection' && String(x.cari_id)===String(state.selectedCariId) && String(x.line_date||x.entry_date||x.created_at||'').slice(0,10) >= dateFrom)); persistLedger(); const docs=readLocalJson(LOCAL_DOCS_KEY); writeLocalJson(LOCAL_DOCS_KEY, docs.filter(x=>!(String(x.cari_id)===String(state.selectedCariId) && String(x.source_type||'')==='local_collection' && String(x.document_date||x.created_at||'').slice(0,10) >= dateFrom))); for(const row of targetCollections){ try{ await deleteLiveRow('cari_collections', row.id); }catch(_){} } for(const row of targetOps){ try{ await deleteLiveRow('account_ops', row.id); }catch(_){} } addAuditEvent('cari_collection_purged',{ source_module:'cari_cleanup', target_name:cariName, amount:targetCollections.reduce((a,b)=>a+num(b.amount||0),0), note:`${dateFrom} ve sonrası tahsilatlar temizlendi`, summary:`Cari tahsilat temizliği · ${cariName}` }); await Promise.all([loadDocuments(), loadAccounts(), loadCariler()]); await loadSelectedCariHareketler(); renderDashboard(); renderCariList(); renderSelectedCariDetail(); renderAccounts(); renderDocuments(); renderReports(); notify(`${targetCollections.length} tahsilat satırı temizlendi. Bankadan yeniden oluşturma için hazır.`); } function getExpectedCashflows(){ return readLocalJson(LOCAL_EXPECTED_CASHFLOWS_KEY); } function setExpectedCashflows(rows){ writeLocalJson(LOCAL_EXPECTED_CASHFLOWS_KEY, rows || []); } function addMonthsIso(iso, months){ const d = new Date(iso); if(Number.isNaN(d.getTime())) return iso; const baseDay = d.getDate(); const n = new Date(d.getFullYear(), d.getMonth() + months, 1); const lastDay = new Date(n.getFullYear(), n.getMonth()+1, 0).getDate(); n.setDate(Math.min(baseDay, lastDay)); return n.toISOString().slice(0,10); } function splitInstallments(total, count){ const c = Math.max(1, Number(count || 1)); const base = Math.floor((num(total) / c) * 100) / 100; const rows = Array(c).fill(base); const sum = rows.reduce((a,b)=>a+b,0); rows[c-1] = Math.round((num(total) - sum + base) * 100) / 100; return rows; } function currentMonthRange(){ const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0,10); const end = new Date(now.getFullYear(), now.getMonth()+1, 0).toISOString().slice(0,10); return { start, end }; } function getCashForecastSummary(){ const flows = getExpectedCashflows(); const { start, end } = currentMonthRange(); const monthFlows = flows.filter(x=>String(x.expected_date||'').slice(0,10) >= start && String(x.expected_date||'').slice(0,10) <= end); const actualKasa = getLocalAccountOps().filter(x=>String(x.account_type||'')==='kasa' && String(x.direction||'')==='giris' && String(x.entry_date||'').slice(0,10) >= start && String(x.entry_date||'').slice(0,10) <= end).reduce((a,b)=>a+num(b.amount),0); const expectedKasa = monthFlows.filter(x=>String(x.settlement_account_type||'')==='kasa').reduce((a,b)=>a+num(b.expected_amount),0); const pendingMoka = flows.filter(x=>String(x.flow_type)==='moka_pos' && !x.settled_at).reduce((a,b)=>a+num(b.expected_amount),0); const pendingPaper = flows.filter(x=>['cek','senet'].includes(String(x.flow_type)) && !x.settled_at).reduce((a,b)=>a+num(b.expected_amount),0); return { actualKasa, expectedKasa, pendingMoka, pendingPaper, thisMonthCount: monthFlows.length }; } function toggleCariCollectionFlowFields(){ const flow = document.getElementById('cariCollectionFlowType')?.value || 'direct'; const wrap = document.getElementById('cariCollectionInstallmentWrap'); if(wrap) wrap.style.display = flow === 'moka_pos' ? 'grid' : (flow === 'cek' || flow === 'senet' ? 'grid' : 'none'); const cnt = document.getElementById('cariCollectionInstallmentCount'); const date = document.getElementById('cariCollectionExpectedDate'); if(flow === 'moka_pos'){ if(cnt) cnt.value = cnt.value || '1'; if(date && !date.value){ const d = new Date(); d.setDate(d.getDate() + 40); date.value = d.toISOString().slice(0,10); } } } function buildExpectedCashflowRows({ cari, amount, lineDate, flowType, installmentCount, expectedDate, settlementAccount, sourceAccount, description, note, documentNo, collectionId=null }){ const rows = []; if(flowType === 'direct') return rows; if(flowType === 'moka_pos'){ const pieces = splitInstallments(amount, installmentCount || 1); pieces.forEach((part, idx)=>{ const dt = idx === 0 ? expectedDate : addMonthsIso(expectedDate, idx); rows.push(stampRecordIds({ collection_id:collectionId, flow_type:'moka_pos', cari_id:cari?.cari_id || null, cari_name:cari?.cari_name || '-', source_account_key:sourceAccount?.account_key || null, source_account_name:sourceAccount?.account_name || null, source_account_type:sourceAccount?.account_type || null, settlement_account_key:settlementAccount?.account_key || null, settlement_account_name:settlementAccount?.account_name || null, settlement_account_type:settlementAccount?.account_type || null, source_date:lineDate, expected_date:dt, expected_amount:part, installment_no:idx+1, installment_count:Number(installmentCount || 1), description: description || `Moka tahsilat beklenen giriş · ${cari?.cari_name || '-'}`, note: note || documentNo || null, settled_at:null, created_at:new Date().toISOString() }, 'ECF')); }); } else if(flowType === 'cek' || flowType === 'senet'){ rows.push(stampRecordIds({ collection_id:collectionId, flow_type:flowType, cari_id:cari?.cari_id || null, cari_name:cari?.cari_name || '-', source_account_key:sourceAccount?.account_key || null, source_account_name:sourceAccount?.account_name || null, source_account_type:sourceAccount?.account_type || null, settlement_account_key:settlementAccount?.account_key || null, settlement_account_name:settlementAccount?.account_name || null, settlement_account_type:settlementAccount?.account_type || null, source_date:lineDate, expected_date:expectedDate, expected_amount:num(amount), installment_no:1, installment_count:1, description: description || `${flowType==='cek' ? 'Çek' : 'Senet'} tahsilat beklenen giriş · ${cari?.cari_name || '-'}`, note: note || documentNo || null, settled_at:null, created_at:new Date().toISOString() }, 'ECF')); } return rows; } function openCariCollectionModal(){ if(!state.selectedCariId){ notify('Cari seçili değil.'); return; } const cari=state.cariler.find(x=>x.cari_id===state.selectedCariId); fillAccountSelect('cariCollectionAccount'); fillAccountSelect('cariCollectionSettlementAccount'); document.getElementById('cariCollectionDate').value=new Date().toISOString().slice(0,10); document.getElementById('cariCollectionAmount').value=''; document.getElementById('cariCollectionDocNo').value=''; document.getElementById('cariCollectionChannel').value=''; document.getElementById('cariCollectionDescription').value=''; document.getElementById('cariCollectionNote').value=''; document.getElementById('cariCollectionFlowType').value='direct'; document.getElementById('cariCollectionInstallmentCount').value='1'; document.getElementById('cariCollectionExpectedDate').value=''; const settlementEl = document.getElementById('cariCollectionSettlementAccount'); const defaultSettlement = ensureVisibleAccounts().find(x=>String(x.account_type)==='banka') || ensureVisibleAccounts()[0]; if(defaultSettlement && settlementEl?.querySelector(`option[value="${defaultSettlement.account_key}"]`)) settlementEl.value = defaultSettlement.account_key; document.getElementById('cariCollectionInfo').innerHTML=`${escapeHtml(cari?.cari_name || 'Cari')} için tahsilat gireceksin. Moka/POS seçersen ilk taksit 40 gün sonra, kalanlar aylık taksitlerle hesaba geçiş bekleyen olarak planlanır. Çek ve senet için vade tarihi bazlı beklenen nakit giriş oluşturulur.`; toggleCariCollectionFlowFields(); document.getElementById('cariCollectionModal').style.display='flex'; } function closeCariCollectionModal(){ document.getElementById('cariCollectionModal').style.display='none'; } async function saveCariCollection(){ if(!state.selectedCariId){ notify('Cari seçili değil.'); return; } const cari=state.cariler.find(x=>x.cari_id===state.selectedCariId); const accountKey=document.getElementById('cariCollectionAccount').value; const account=getAccountByKey(accountKey); const settlementKey=document.getElementById('cariCollectionSettlementAccount').value; const settlementAccount=getAccountByKey(settlementKey); const flowType=document.getElementById('cariCollectionFlowType').value || 'direct'; const installmentCount=Math.max(1, Number(document.getElementById('cariCollectionInstallmentCount').value || 1)); const amount=num(document.getElementById('cariCollectionAmount').value||0); const lineDate=document.getElementById('cariCollectionDate').value || new Date().toISOString().slice(0,10); let expectedDate=document.getElementById('cariCollectionExpectedDate').value || ''; const documentNo=(document.getElementById('cariCollectionDocNo').value||'').trim(); const channel=(document.getElementById('cariCollectionChannel').value||'').trim(); const description=(document.getElementById('cariCollectionDescription').value||'').trim() || `Müşteriden Tahsilat | ${cari?.cari_name || ''}`; const note=(document.getElementById('cariCollectionNote').value||'').trim(); if(!account || !accountKey){ notify('Tahsilat hesabı seç.'); return; } if(!amount){ notify('Tahsilat tutarı zorunlu.'); return; } if((flowType==='moka_pos' || flowType==='cek' || flowType==='senet') && !settlementAccount){ notify('Nihai giriş hesabı seç.'); return; } if(flowType==='moka_pos' && !expectedDate){ const d = new Date(lineDate); d.setDate(d.getDate()+40); expectedDate = d.toISOString().slice(0,10); } if((flowType==='cek' || flowType==='senet') && !expectedDate){ notify('Vade / beklenen giriş tarihi zorunlu.'); return; } if(hasSimilarTahsilat(state.selectedCariId, lineDate, amount)){ notify('Benzer tahsilat kaydı var. Önce kontrol et.'); return; } let collection=stampRecordIds({ cari_id:state.selectedCariId, cari_name:cari?.cari_name || '-', account_key:account.account_key, account_name:account.account_name, account_type:account.account_type, amount, line_date:lineDate, channel, description, note, document_no:documentNo, flow_type:flowType, installment_count:installmentCount, settlement_account_key:settlementAccount?.account_key || null, settlement_account_name:settlementAccount?.account_name || null, expected_date:expectedDate || null, created_at:new Date().toISOString(), updated_at:new Date().toISOString() }, 'LCT'); let op=stampRecordIds({ type:'cari_collection', collection_id:collection.id, expense_id:null, account_key:account.account_key, account_name:account.account_name, account_type:account.account_type, direction:'giris', amount, entry_date:lineDate, description:`Cari tahsilat · ${cari?.cari_name || '-'}`, note:note || documentNo || channel || null, flow_type:flowType, created_at:new Date().toISOString(), updated_at:new Date().toISOString() }, 'LAC'); collection = { ...collection, account_op_id: op.id }; const collections=getLocalCariCollections().filter(x=>String(x.id)!==String(collection.id)); collections.unshift(collection); setLocalCariCollections(collections); await upsertLiveRow('cari_collections', collection); const ops=getLocalAccountOps().filter(x=>String(x.id)!==String(op.id)); ops.unshift(op); setLocalAccountOps(ops); await upsertLiveRow('account_ops', op); const expectedRows = buildExpectedCashflowRows({ cari, amount, lineDate, flowType, installmentCount, expectedDate, settlementAccount, sourceAccount:account, description, note, documentNo, collectionId:collection.id }); if(expectedRows.length){ const currentRows=getExpectedCashflows().filter(x=>!expectedRows.some(n=>String(n.id)===String(x.id))); setExpectedCashflows([...expectedRows, ...currentRows]); } const docs=readLocalJson(LOCAL_DOCS_KEY); docs.unshift(stampRecordIds({ cari_id:state.selectedCariId, document_type: channel && /nakit/i.test(channel) ? 'makbuz' : 'dekont', document_title: documentNo || `Tahsilat ${shortDate(lineDate)}`, document_no: documentNo || null, document_date: lineDate, amount, note: note || description, source_type:'local_collection', created_at:new Date().toISOString() }, 'DOC')); writeLocalJson(LOCAL_DOCS_KEY, docs); addAuditEvent('cari_collection_saved',{source_module:'live_cari_collection', target_name:cari?.cari_name || '-', amount, note:note || description, summary:`Cari tahsilat işlendi · ${money(amount)}`}); closeCariCollectionModal(); await Promise.all([loadDocuments(), loadAccounts(), loadCariler()]); await loadSelectedCariHareketler(); renderDashboard(); renderCariList(); renderSelectedCariDetail(); renderAccounts(); renderDocuments(); renderReports(); notify(expectedRows.length ? 'Cari tahsilatı kaydedildi. Beklenen nakit giriş planı oluşturuldu.' : 'Cari tahsilatı kaydedildi ve canlı yazım denendi.'); } function renderBankRuleList(){ const target=document.getElementById('bankRuleListWrap'); if(!target) return; const rows=getBankRules(); target.innerHTML = rows.length ? `${rows.map(r=>``).join('')}
NoTürAnahtarHedefTipNot
${escapeHtml(displayTxnNo(r,'BRL'))}${escapeHtml(r.rule_type||'-')}${escapeHtml(r.keyword||'-')}${escapeHtml(r.target_name||'-')}${escapeHtml(r.target_type||'-')}${escapeHtml(r.note||'-')}
` : `
Henüz kural yok.
`; } function openBankRuleModal(){ document.getElementById('bankRuleType').value='gider'; document.getElementById('bankRuleKeyword').value=''; document.getElementById('bankRuleTargetName').value=''; document.getElementById('bankRuleTargetType').value='gider'; document.getElementById('bankRuleNote').value=''; renderBankRuleList(); document.getElementById('bankRuleModal').style.display='flex'; } function closeBankRuleModal(){ document.getElementById('bankRuleModal').style.display='none'; } function saveBankRule(){ const rule_type=document.getElementById('bankRuleType').value || 'gider'; const keyword=(document.getElementById('bankRuleKeyword').value||'').trim(); const target_name=(document.getElementById('bankRuleTargetName').value||'').trim(); const target_type=(document.getElementById('bankRuleTargetType').value||'').trim(); const note=(document.getElementById('bankRuleNote').value||'').trim(); if(!keyword || !target_name){ notify('Anahtar kelime ve hedef adı zorunlu.'); return; } const rows=getBankRules(); rows.unshift(stampRecordIds({rule_type, keyword, target_name, target_type, note, created_at:new Date().toISOString()}, 'BRL')); setBankRules(rows); renderBankRuleList(); refreshImportedMovementAnalysis(true); loadAccounts().then(()=>renderAccounts()); notify('Banka kuralı kaydedildi.'); } async function refreshAll(){ const loaders = [ ['Dashboard', ()=>loadDashboard()], ['Cariler', ()=>loadCariler()], ['Hesaplar', ()=>loadAccounts()], ['Onay', ()=>loadApprovals()], ['Ücret', ()=>loadFeeData()], ['Belgeler', ()=>loadDocuments()] ]; const results = await Promise.allSettled(loaders.map(x=>x[1]())); results.forEach((r,i)=>{ if(r.status==='rejected'){ console.error(r.reason); logSystemError('refresh_all', `${loaders[i][0]} yüklenemedi`, { note: String(r.reason?.message || r.reason || '') }); } }); safeRun(()=>sanitizeAllImportedMovementDates(),'Import tarih normalize 1'); safeRun(()=>refreshImportedMovementAnalysis(true),'Import analiz'); safeRun(()=>sanitizeAllImportedMovementDates(),'Import tarih normalize 2'); safeRun(()=>normalizeStateIds(),'ID normalize'); safeRun(()=>{ state.employees = normalizeNumberedList(readLocalJson(LOCAL_EMPLOYEES_KEY),'EMPCRD').rows; },'Çalışan local'); safeRun(()=>{ state.employeeAccruals = normalizeNumberedList(readLocalJson(LOCAL_EMPLOYEE_ACCRUALS_KEY),'EMP').rows; },'Çalışan tahakkuk local'); safeRun(()=>{ state.localFeePeriods = normalizeNumberedList(readLocalJson(LOCAL_FEE_PERIODS_KEY),'LFP').rows; },'Ücret dönem local'); safeRun(()=>ensureFeeAccrualsForAllCariler(),'Ücret tahakkuku'); safeRun(()=>ensureEmployeeSalaryAccruals(),'Maaş tahakkuku'); safeRun(()=>renderDashboard(),'Dashboard'); safeRun(()=>renderAccounts(),'Hesaplar'); safeRun(()=>renderReports(),'Raporlar'); safeRun(()=>renderCariBulkAccruals(),'Cari Toplu Tahakkuk'); safeRun(()=>renderDocuments(),'Belgeler'); safeRun(()=>renderExpenses(),'Giderler'); safeRun(()=>renderEmployees(),'Çalışanlar'); safeRun(()=>renderApprovals(),'Onay'); safeRun(()=>renderBackup(),'Yedekleme'); safeRun(()=>renderOpsHealthPanel(),'Ops'); safeRun(()=>renderProdHardeningPanel(),'Prod'); safeRun(()=>renderGlobalAuditLog(),'Audit'); safeRun(()=>renderDeleteArchive(),'Silme arşivi'); if(state.selectedCariId) safeRun(()=>renderSelectedCariDetail(),'Cari detay'); } document.getElementById("cariSearch").addEventListener("input",renderCariList); document.getElementById("taxpayerFilter").addEventListener("change",renderCariList); document.getElementById("sortBy").addEventListener("change",renderCariList); document.getElementById("versionBadge").textContent=buildVersion(); const __loginInput=document.getElementById("loginPassword"); if(__loginInput){ __loginInput.addEventListener("keydown", function(e){ if(e.key==="Enter"){ e.preventDefault(); submitLogin(e); } }); } if(localStorage.getItem(SESSION_KEY)==="ok"){openApp(); refreshAll()}else{setConnState("Giriş bekleniyor")}