Quantum KİT'i kullanabilmek için aşağıdaki belgeleri okuyup elektronik olarak onaylamanız
gerekiyor. Onay tarihiniz, IP adresiniz ve belge sürümü kayıt altına alınır.
Belgeler yükleniyor…
🛡️
Son kontrol size aittir. Yazılımın ürettiği belgeleri idareye sunmadan önce tutar, tarih, dönem ve
unvan bilgilerini kendi kayıtlarınızla karşılaştırın. Yanlış ya da hatalı doldurmalardan doğan sorumluluk
kullanıcıya aittir.
Firma Bilgileri
Buradaki bilgiler tutanak eklerine (ortaklık, defterler, fatura excelleri) otomatik yazılır.
❔
Bu dosya nereden alınır?
Luca'da izlenecek yol
Esc ile de kapatabilirsiniz
Şifre Değiştir
İlk girişinizde şifrenizi değiştirmeniz gerekiyor.
Hoş geldiniz
Karşıt inceleme tutanaklarınızı buradan takip edin.
Karşıt İnceleme Tutanakları
Firmamız
Karşı Firma (Mükellef)
Sayı
Tutanak
Tebliğ
Son Tarih
Kalan
Durum
Cevap süresi: tebliğ tarihinden itibaren 1 ay (son tarih otomatik hesaplanır, tutanak ekranından değiştirilebilir). Tebliğ tarihi el ile girilir.
⏱ Yaklaşan Son Tarihler
📈 Son 6 Ay
🛡️
Son kontrol size aittir. Quantum KİT bir yardımcı araçtır; ürettiği belgeleri teslim etmeden
önce tutar, tarih, dönem ve unvan bilgilerini kendi kayıtlarınızla karşılaştırmanızı rica ederiz.
Sözleşmeler ve yasal bilgiler →
Firmalarımız
Yeni firma eklediğinizde aynı ekranlar o firma için de çalışır; kurgu Atasan ile birebir aynıdır. "Kopyala" mevcut firmanın bilgilerini şablon olarak alır.
Belge Kontrol Panosu
firma × yıl özeti — boş hücreye tıklayıp yükleyin
Firma
Yıllar
Cari Hesap Planı
Muavin Döküm
KDV Beyan.
Muhtasar Bey.
Kurumlar Bey.
Defter Dökümü
Durum
Belge Arşivi — Detay
firma × yıl — bir kez yükleyin, tüm tutanaklarda kullanılsın
Her firma için yıl yıl: Cari Hesap Planı, Muavin Döküm, KDV beyannameleri (12 ay + önceki dönem Aralık), Muhtasar beyannameleri ve Kurumlar Vergisi Beyannamesi. Yıl başlığına tıklayınca o yılın belgeleri açılır. Yüklenen belge yeşil tik ile işaretlenir; aynı yıla ait yeni bir karşıt inceleme geldiğinde tekrar yüklemeniz gerekmez.
İstatistikler
Grafikler
Firma bazında tutanak sayısıTutanağı düzenleyen YMM'ye göreAylara göre gelen tutanaklar (tutanak tarihi)Durum dağılımı
Tutanak Listesi
Firmamız ⇅
Karşı Firma ⇅
Sayı ⇅
YMM ⇅
Tutanak Tarihi ⇅
Tebliğ ⇅
Son Tarih ⇅
Dönem
Durum ⇅
e-Defter Mükellefiyeti ve Berat Takvimi
1) Firma bazında e-defter durumu
Firma e-defter mükellefi ise, tutanağın yasal defterler bölümünde kâğıt defter yerine e-defter dönemleri listelenir ve berat süresi henüz dolmamış dönemlere "Süresi gelmediğinden verilmemiştir." yazılır.
Toplu işlem:
Firma
e-Defter mükellefi mi?
Berat yükleme tercihi
Dönem sayısı
2) Berat yükleme son tarihleri
Tarihler mevzuattaki genel kurala göre önceden doldurulur (ilgili ayı takip eden 4. ayın 14'ü; Aralık için beyanname ayını takip eden ayın 14'ü). Uzatma (sirküler) varsa aşağıdaki hücreleri elle düzeltin — girdiğiniz tarihler kaydedilir ve tüm firmalarda kullanılır.
Ay
Berat son tarihi
Ay
Berat son tarihi
Geçici vergi dönemi
Berat son tarihi
Aboneliğim
▶🧾 Fatura Bilgilerim
Üyelik bedeli için adınıza e-arşiv fatura düzenlenir.
Bilgileriniz değiştiyse buradan güncelleyin.
Ödeme Yap
Havale/EFT ile ödeyin, ödemeyi bildirin; onaylandığında aboneliğiniz
otomatik uzar. Kredi kartı ile ödeme yakında eklenecektir.
Ödeme Geçmişim
Tarih
Referans
Tutar
Süre
Durum
Not
Kullanıcılar
Her kullanıcı yalnızca kendi eklediği firmaları ve onlara ait
tutanakları görür. Yönetici tüm kullanıcıları yönetebilir ve firmaları devredebilir. Yeni kullanıcı ilk girişinde
şifresini değiştirmek zorundadır.
Ad Soyad
E-posta
Rol
Durum
Abonelik
Firma
Tutanak
Son giriş
Yeni Kullanıcı
Ödeme Bildirimleri
Kullanıcı
Referans
Tutar
Süre
Bildirim
Durum
Sözleşme Onayları
Bu bilgiler sözleşme metinlerinde satıcı/veri sorumlusu olarak
görünür ve giriş sayfasının altbilgisinde (ünvan, adres, telefon, e-posta, WhatsApp) yayınlanır.
Boş bırakılan alanlar metinde sarı uyarı olarak çıkar; yayına almadan önce doldurun.
Tanıtım Sayfası — Sosyal Kanıt
Giriş sayfasındaki "Kullananlar" bölümünü buradan yönetirsiniz.
Yalnızca gerçekten aldığınız, sahibinin izin verdiği yorumları yazın. Uydurma referans yayınlamak
hem etik değildir hem de reklam mevzuatına aykırıdır. Boş bıraktığınız yorumlar sayfada görünmez;
hiç yorum yoksa ve sayaç kapalıysa bölümün tamamı gizlenir.
aktif abone olmadan sayaç görünmesin
Sözleşmeleri onaylamayan kullanıcılar programı kullanamaz; giriş
yaptıklarında onay ekranı çıkar. Onay anındaki tarih, IP adresi, tarayıcı bilgisi ve metnin SHA-256 özeti
saklanır. Belge bağlantısına tıklayarak onay kaşesi basılmış nüshayı indirebilirsiniz.
Ad Soyad / Ünvan
Telefon
E-posta
Durum
Onayladığı sözleşmeler
Onay tarihi
IP
📧 E-posta Bildirimleri
Hesap onayında kullanıcıya "aboneliğiniz şu tarihe kadar açılmıştır"
e-postası gider. Süre bitimine 7 gün, 3 gün kala ve son gün, yenilemeyen kullanıcıya
hatırlatma gönderilir (her sabah otomatik kontrol edilir; aynı hatırlatma iki kez gitmez). Ödeme onayı ve
süre uzatmalarında da yeni bitiş tarihi bildirilir.
Zaman
Kime
Tür
Konu
Durum
🛟 Yedek Durumu
Her kullanıcının verisi, silme işlemlerinden hemen önce ve düzenli
aralıklarla kendiliğinden yedeklenir; kullanıcı kendi Veri / Yedek ekranından geri dönebilir.
Sunucunun tamamı ayrıca her gece yedeklenir (/var/backups/quantumkit).
Kullanıcı
Firma
Yedek
Son yedek
Alan
Firma Devri
Bir firmayı tutanakları ve arşiviyle birlikte başka kullanıcıya aktarır.
→
Son İşlemler
Zaman
Kullanıcı
Eylem
Ayrıntı
IP
Veri Kaydetme / Yükleme
Tarayıcı uygulaması verileri bellekte tutar. Çalışmanızı JSON dosyası olarak kaydedin; programı tekrar açtığınızda bu dosyayı yükleyerek kaldığınız yerden devam edin.
Belge Klasörü (bilgisayarınızda)
Beyanname PDF'leri, muavin ve cari plan dosyaları sunucuya gönderilmez;
bilgisayarınızda kalır. Bir klasör bağlarsanız yüklediğiniz belgeler oraya da yazılır ve tarayıcı temizliğinden etkilenmez.
Klasör seçimi kaydedilir, her açılışta yeniden seçmeniz gerekmez.
"Klasör bağla": Chrome/Edge'de bilgisayarınızdan bir klasör seçersiniz. Yüklediğiniz belgeler oraya yazılır; ayrıca QuantumKIT-Yedek alt klasörüne günde bir kez yedek dosyası bırakılır. Seçim kaydedilir, her açılışta yeniden seçmeniz gerekmez.
🛟 Otomatik Yedekler
🛟
Program kendiliğinden yedek alır. Bir firmayı veya tutanağı silmeden hemen önce, ayrıca
düzenli aralıklarla o anki hâlin kopyası saklanır. Yanlışlıkla bir şey silerseniz aşağıdan tek tıkla
geri dönebilirsiniz. Geri yüklemeden önce mevcut hâlin yedeği de alınır — yani geri yükleme de geri alınabilir.
Sistemden gelen KARŞIT İNCELEME formunu sürükleyinveya tıklayıp seçin — sayı, tarih, karşı firma, dönem ve fatura listesi otomatik okunur
Tutanak Bilgileri ve Süre Takibi
Luca'dan Alınan Exceller
Cari Hesap Plan ListesiLuca → cari hesap planı (.xlsx)
Muavin DökümLütfen ilgili yıla ait muavin döküm yükleyiniz (.xlsx)
3) Mükellefin Karşıt İncelemeye Konu Olan Faturalarına İlişkin Bilgiler satışlarımız → karşı firmaya
Fatura Tarihi
Seri
Numara
Tutar (TL)
KDV (TL)
Defter Kayıt
Yevmiye
Ödeme Şekli
Kontrol (program notu)
7) İlgili Dönemde Satılan Malların Tedarik Edildiği Firmalar Hakkında Bilgiler en yüksek 10 alış faturası — belge bazında, aynı firma tekrar edebilir
191 İndirilecek KDV'den en yüksek KDV'li alışlar bulunur, matrah hesaplanır ve 15'li grup / diğer hesaplar ile çapraz kontrol edilir. Duran varlık (taşıt, demirbaş) çıkan alımların işareti otomatik kaldırılır — "satılan mal" kapsamına girmediği için. İlk seçili 10 satır Excel'e yazılır; kutucuklarla değiştirebilirsiniz.
Ünvan
VKN
Vergi Dairesi
Fatura Tarihi
Seri
Numara
KDV Dahil
Kontrol (program notu)
5) Önceki Dönem ve İlgili Dönem KDV Beyan Bilgileri KDV1 beyanname + tahakkuk PDF'lerinden
Beyannameler firma arşivinde saklanır — bir dahaki tutanakta yeniden yüklemeniz gerekmez. Aynı aya ait birden fazla beyanname varsa (düzeltme) onay zamanı en yeni olan kullanılır. Tahakkuk fiş numarası tahakkuk PDF'inden alınır.
1) PDF YükleBeyanname + tahakkuk PDF'lerini (toplu) buraya bırakın
2) Klasörden OkuBilgisayarınızdaki beyanname klasörünü seçin — içindeki tüm PDF'ler taranır (Chrome/Edge)
3) E-Beyanname BağlantısıBekleniyor — dijital vergi dairesinden otomatik indirme ileride eklenecek
Dönem
Teslim ve Hizmet Bedeli
Özel Matrah Dahil Olm.
KDV Matrahı
Hesaplanan KDV
İlave KDV
Toplam KDV
İndirimler Toplamı
Ödenmesi Gereken
Devreden KDV
Tahakkuk Fiş No
Durum
6) Karşıt İncelemeye Konu Faturada Yer Alan Malın İmalatçısı Olan Mükellef Hakkında Bilgiler yalnız imalatçı firmalar
Adı Soyadı/Ünvanı
Vergi/T.C. Kimlik No
Bağlı Olduğu Vergi Dairesi
Sanayi Sicil Belgesi No
Sanayi Sicil Belgesi Tarihi
Kapasite Raporu No
Kapasite Raporu Tarihi
Açıklama
2) Mükellefin Yasal Defterlere İlişkin Bilgileri tutanak 2. bölüm
Luca Defter Döküm Listesi (.xls / .zip)bu dosya tüm firmaları kapsar — yılda bir kez yüklemeniz yeterli · sıkıştırılmış (.zip) da olur
Zip'ler sisteme kaydedilmez; yalnızca berat numarası, dönem ve tarih okunur ve ilgili yıl için saklanır — aynı yıl için bir daha yüklemeniz gerekmez.
Defterin Nev'i
Başlangıç
Bitiş
Tasdik Makamı
Tasdik Tarihi
Tasdik / Berat No
Açıklama
Defter Türü Açıklama
4) Karşıt İncelemeye Konu Dönemde/Dönemlerde Mükellefin Muhtasar Beyannamesinde Yer Alan Çalışan Kişi Sayıları MUHSGK beyanname PDF'lerinden
Her ayın Muhtasar ve Prim Hizmet Beyannamesindeki "VERGİ BİLDİRİMİ" tablosunun "Toplam Çalışan Sayısı" sütunu okunur. Beyannameler firma arşivinde saklanır; düzeltme varsa onay zamanı en yeni olan geçerlidir.
1) PDF YükleMuhtasar beyanname PDF'lerini (toplu) buraya bırakın
2) Klasörden OkuMuhtasar beyanname klasörünü seçin — içindeki tüm PDF'ler taranır (Chrome/Edge)
3) E-Beyanname BağlantısıBekleniyor — dijital vergi dairesinden otomatik indirme ileride eklenecek
Dönem
Çalışan İşçi Sayısı
Beyannamenin Verildiği Vergi Dairesi
Durum
1) Mükellefin Ortaklık Bilgileri kurumlar vergisi beyannamesinden
İlgili yılın Kurumlar Vergisi Beyannamesindeki "KURUM ORTAKLARINA VE YÖNETİM KURULU ÜYELERİNE İLİŞKİN LİSTE" okunur. Beyanname firma arşivinde yıl bazında saklanır — bir kez yüklemeniz yeterli. İl, ikametgâh adresinden çıkarılır; vergi dairesi beyannamede yer almadığından elle doldurulur.
Kurumlar Vergisi Beyannamesi (PDF)ilgili yıla veya bir önceki yıla ait beyannameyi bırakın
Adı Soyadı/Ünvanı
Vergi/T.C. Kimlik No
Bağlı Olduğu İl
Bağlı Olduğu Vergi Dairesi
Pay Oranı
Açıklama
Beyannamedeki görev / adres
Excel Çıktıları
Resmi dosyalar sistem şablonuyla birebir aynı formatta, resmi ek listesindeki numara ve adlarla üretilir (6 no'lu bölüm yalnız imalatçı işaretli firmalarda üretilir; kontrol notları resmi dosyaya yazılmaz). Dosya adındaki tarih = cevap son tarihi.
Fatura Bilgileri
{
t.ortaklar = t.ortaklar || []; t.muhOverride = t.muhOverride || {}; t.muhVd = t.muhVd || {};
t.pdfFaturalar = t.pdfFaturalar || []; t.satis = t.satis || []; t.alis = t.alis || [];
t.defterEk = t.defterEk || []; t.beratNo = t.beratNo || {};
t.durum = t.durum || "Bekliyor";
});
for (const fid in state.arsiv) { state.arsiv[fid].muh = state.arsiv[fid].muh || {}; }
state.defterDokum = state.defterDokum || {};
edef();
}
/* firma arşivi */
function A(firmaId) {
state.arsiv = state.arsiv || {};
const a = state.arsiv[firmaId] = state.arsiv[firmaId] || { kdv: {}, yillar: {} };
a.kdv = a.kdv || {}; a.yillar = a.yillar || {};
return a;
}
function AY(firmaId, yil) {
const a = A(firmaId);
return a.yillar[yil] = a.yillar[yil] || {};
}
const yilOf = t => (t && t.donemBas ? t.donemBas.split(".")[1] : "") || (t && t.donemBit ? t.donemBit.split(".")[1] : "");
let cur = null; // açık tutanak id
let filter = "all"; // ana sayfa filtresi
let fsHandle = null; // File System Access kayıt dosyası
const $ = id => document.getElementById(id);
/* ================================================================
SUNUCU + YEREL DEPO KATMANI
· Sunucuda yalnız çıkarılmış değerler durur (firma-yıl başına ~10 KB)
· PDF'ler, muavin ve cari plan kullanıcının kendi bilgisayarında:
tarayıcı veritabanı (IndexedDB) + isteğe bağlı kalıcı klasör bağlantısı
================================================================ */
let KULLANICI = null; // {id, ad, eposta, rol}
let YEREL_MOD = false; // sunucu yoksa (tek dosya / çevrimdışı kullanım)
let SUNUCU_HATA = 0;
let esitlemeZaman = null;
async function api(uc, gövde) {
const ayar = { headers: { "X-QK": "1" }, credentials: "same-origin" };
if (gövde !== undefined) {
ayar.method = "POST";
ayar.headers["Content-Type"] = "application/json";
ayar.body = JSON.stringify(gövde);
}
let r;
try { r = await fetch("api/api.php?p=" + uc, ayar); }
catch (err) { const e = new Error("Sunucuya ulaşılamadı"); e.yerel = true; throw e; }
let j = {};
try { j = await r.json(); } catch (e) {}
if (r.status === 401 && uc !== "ben") { location.href = "giris.html"; throw new Error("Oturum bitti"); }
if (r.status === 403 && Array.isArray(j.sozlesmeEksik) && j.sozlesmeEksik.length && uc !== "onayla") {
if ($("sozKatman").style.display !== "grid") sozPerdeAc();
throw new Error(j.hata || "Sözleşme onayı gerekiyor.");
}
if (!r.ok) throw new Error(j.hata || ("Sunucu hatası (" + r.status + ")"));
return j;
}
/* ---------------- IndexedDB: ağır dosyalar ---------------- */
const LDB = {
_db: null,
async ac() {
if (this._db) return this._db;
this._db = await new Promise((cz, rd) => {
const q = indexedDB.open("quantumkit", 1);
q.onupgradeneeded = () => {
const d = q.result;
if (!d.objectStoreNames.contains("dosya")) d.createObjectStore("dosya");
if (!d.objectStoreNames.contains("ayar")) d.createObjectStore("ayar");
};
q.onsuccess = () => cz(q.result);
q.onerror = () => rd(q.error);
});
return this._db;
},
async yaz(depo, anahtar, deger) {
const d = await this.ac();
return new Promise((cz, rd) => {
const t = d.transaction(depo, "readwrite");
t.objectStore(depo).put(deger, anahtar);
t.oncomplete = cz; t.onerror = () => rd(t.error);
});
},
async oku(depo, anahtar) {
const d = await this.ac();
return new Promise((cz, rd) => {
const q = d.transaction(depo, "readonly").objectStore(depo).get(anahtar);
q.onsuccess = () => cz(q.result); q.onerror = () => rd(q.error);
});
},
async sil(depo, anahtar) {
const d = await this.ac();
return new Promise((cz) => {
const t = d.transaction(depo, "readwrite");
t.objectStore(depo).delete(anahtar); t.oncomplete = cz;
});
},
async anahtarlar(depo) {
const d = await this.ac();
return new Promise((cz, rd) => {
const q = d.transaction(depo, "readonly").objectStore(depo).getAllKeys();
q.onsuccess = () => cz(q.result || []); q.onerror = () => rd(q.error);
});
},
async boyut() {
if (!navigator.storage || !navigator.storage.estimate) return null;
const e = await navigator.storage.estimate();
return { kullanilan: e.usage || 0, kota: e.quota || 0 };
}
};
/* ---------------- kalıcı klasör bağlantısı ---------------- */
let KLASOR = null;
async function klasorYukle() {
try {
KLASOR = await LDB.oku("ayar", "klasor");
if (KLASOR) {
const izin = await KLASOR.queryPermission({ mode: "readwrite" });
if (izin !== "granted") KLASOR.izinGerekli = true;
}
} catch (e) { KLASOR = null; }
try { klasorYedekZaman = (await LDB.oku("ayar", "sonKlasorYedek")) || 0; } catch (e) {}
klasorGoster();
}
async function klasorSec() {
if (!window.showDirectoryPicker) { toast("Klasör bağlama Chrome/Edge gerektirir"); return; }
try {
const h = await window.showDirectoryPicker({ mode: "readwrite" });
KLASOR = h;
await LDB.yaz("ayar", "klasor", h);
klasorGoster(); toast("Klasör bağlandı — bir daha seçmeniz gerekmeyecek");
} catch (e) {}
}
async function klasorIzin() {
if (!KLASOR) return klasorSec();
const i = await KLASOR.requestPermission({ mode: "readwrite" });
if (i === "granted") { delete KLASOR.izinGerekli; klasorGoster(); toast("Klasöre erişim açıldı"); }
else toast("İzin verilmedi");
}
async function klasorKaldir() {
KLASOR = null; await LDB.sil("ayar", "klasor"); klasorGoster(); toast("Klasör bağlantısı kaldırıldı");
}
function klasorGoster() {
const el = $("klasorDurum"); if (!el) return;
if (!KLASOR) {
el.innerHTML = `
Klasör bağlı değil. Beyanname ve muavin dosyalarınız yalnızca tarayıcı
veritabanında saklanıyor. Bir klasör bağlarsanız dosyalar bilgisayarınızda kalıcı olur ve tarayıcı temizliğinden etkilenmez.
`;
} else if (KLASOR.izinGerekli) {
el.innerHTML = `
📁 ${esc(KLASOR.name)} klasörü kayıtlı ancak bu oturumda erişim izni gerekiyor.
`;
} else {
const sy = klasorYedekZaman
? `Bilgisayara son yedek: ${new Date(klasorYedekZaman).toLocaleString("tr-TR")}`
: `Bilgisayara henüz yedek yazılmadı`;
el.innerHTML = `
📁 Bağlı klasör: ${esc(KLASOR.name)} — yüklenen belgeler buraya yazılır.
🛟 Ayrıca ${esc(KLASOR.name)}/QuantumKIT-Yedek klasörüne
günde bir kez yedek dosyası yazılır (son 30 gün saklanır). ${sy}
`;
}
}
/* dosyayı bağlı klasöre yaz (varsa) */
async function klasoreYaz(altKlasor, dosyaAdi, base64) {
if (!KLASOR || KLASOR.izinGerekli) return false;
try {
let d = KLASOR;
for (const p of altKlasor.split("/").filter(Boolean)) d = await d.getDirectoryHandle(p, { create: true });
const f = await d.getFileHandle(dosyaAdi, { create: true });
const w = await f.createWritable();
const bin = atob(base64); const u = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i);
await w.write(u); await w.close();
return true;
} catch (e) { console.warn("klasöre yazılamadı", e); return false; }
}
/* metin dosyasını bağlı klasöre yaz */
async function klasoreMetinYaz(altKlasor, dosyaAdi, metin) {
if (!KLASOR || KLASOR.izinGerekli) return false;
try {
let d = KLASOR;
for (const p of altKlasor.split("/").filter(Boolean)) d = await d.getDirectoryHandle(p, { create: true });
const f = await d.getFileHandle(dosyaAdi, { create: true });
const w = await f.createWritable();
await w.write(new Blob([metin], { type: "application/json" }));
await w.close();
return true;
} catch (e) { console.warn("yedek klasöre yazılamadı", e); return false; }
}
/* ================================================================
BAĞLI KLASÖRE GÜNLÜK YEDEK — sunucu sürümünde de çalışır
================================================================ */
const KLASOR_YEDEK_ARALIK = 20 * 3600 * 1000; // günde bir (20 saat)
const KLASOR_YEDEK_SAKLA = 30; // son 30 günlük dosya
let klasorYedekZaman = 0;
function yedekIcerigi() {
const c = sunucuKopyasi();
return {
program: "Quantum KİT", olusturma: new Date().toISOString(),
kullanici: (KULLANICI && KULLANICI.eposta) || "yerel",
firmalar: state.firmalar, tutanaklar: state.tutanaklar, arsiv: c.arsiv,
defterDokum: state.defterDokum, edefter: state.edefter
};
}
async function klasoreYedekDene(zorla) {
if (!KLASOR || KLASOR.izinGerekli) return false;
if (!zorla && Date.now() - klasorYedekZaman < KLASOR_YEDEK_ARALIK) return false;
if (!state.firmalar.length && !state.tutanaklar.length) return false;
try {
const izin = await KLASOR.queryPermission({ mode: "readwrite" });
if (izin !== "granted") { KLASOR.izinGerekli = true; klasorGoster(); return false; }
} catch (e) { return false; }
const g = new Date();
const ad = "yedek_" + g.toISOString().slice(0, 10) + ".json";
const ok = await klasoreMetinYaz("QuantumKIT-Yedek", ad, JSON.stringify(yedekIcerigi()));
if (ok) {
klasorYedekZaman = Date.now();
try { await LDB.yaz("ayar", "sonKlasorYedek", klasorYedekZaman); } catch (e) {}
await klasorYedekBuda();
klasorGoster();
}
return ok;
}
async function klasorYedekBuda() {
try {
const d = await KLASOR.getDirectoryHandle("QuantumKIT-Yedek", { create: true });
const adlar = [];
for await (const [ad] of d.entries())
if (/^yedek_\d{4}-\d{2}-\d{2}\.json$/.test(ad)) adlar.push(ad);
adlar.sort();
for (const ad of adlar.slice(0, Math.max(0, adlar.length - KLASOR_YEDEK_SAKLA)))
await d.removeEntry(ad);
} catch (e) {}
}
async function klasoreYedekSimdi() {
if (!KLASOR) { klasorSec(); return; }
if (KLASOR.izinGerekli) { await klasorIzin(); }
const ok = await klasoreYedekDene(true);
toast(ok ? "Bilgisayarınızdaki klasöre yedek yazıldı" : "Klasöre yazılamadı — izni kontrol edin");
renderYedekler();
}
/* ================================================================
OTOMATİK YEDEKLEME (yerel mod) — tarayıcı veritabanında dönen kopyalar
================================================================ */
const YEREL_YEDEK_ARALIK = 6 * 3600 * 1000; // 6 saat
const YEREL_YEDEK_ADET = 12;
let sonYerelYedek = 0, sonSayim = null;
function yedekOzetiCikar(s) {
return { firma: (s.firmalar || []).length, tutanak: (s.tutanaklar || []).length };
}
/* yazmadan önce: veri küçülüyorsa ya da süre dolduysa yedek al */
function yerelYedekGerekli(yeniSayim) {
if (!sonSayim) return "ilk yedek";
if (yeniSayim.firma < sonSayim.firma || yeniSayim.tutanak < sonSayim.tutanak) {
const n = [];
if (yeniSayim.firma < sonSayim.firma) n.push((sonSayim.firma - yeniSayim.firma) + " firma");
if (yeniSayim.tutanak < sonSayim.tutanak) n.push((sonSayim.tutanak - yeniSayim.tutanak) + " tutanak");
return "silme öncesi (" + n.join(", ") + " azalıyor)";
}
if (Date.now() - sonYerelYedek > YEREL_YEDEK_ARALIK) return "düzenli otomatik yedek";
return null;
}
async function yerelYedekAl(tur, not, icerik) {
try {
/* otomatik yedek, kaydedilmiş son hâlin kopyasıdır — silinen veri böyle korunur */
let veri = icerik;
if (!veri && tur === "oto") veri = await LDB.oku("ayar", "state");
if (!veri) { const c = sunucuKopyasi();
veri = { firmalar: state.firmalar, tutanaklar: state.tutanaklar, arsiv: c.arsiv,
defterDokum: state.defterDokum, edefter: state.edefter }; }
if (!(veri.firmalar || []).length && !(veri.tutanaklar || []).length) return null;
const json = JSON.stringify(veri);
const kayit = { id: Date.now(), tur, not: not || "", olusturma: new Date().toISOString(),
firma: (veri.firmalar || []).length, tutanak: (veri.tutanaklar || []).length,
boyut: json.length, veri: json };
await LDB.yaz("ayar", "yedek|" + kayit.id, kayit);
sonYerelYedek = Date.now();
await yerelYedekBuda();
/* bağlı klasör varsa oraya da yaz — tarayıcı temizliğinden etkilenmesin */
await klasoreMetinYaz("QuantumKIT-Yedek",
"yedek_" + kayit.olusturma.slice(0, 10) + "_" + kayit.olusturma.slice(11, 16).replace(":", "") + ".json", json);
return kayit.id;
} catch (e) { console.warn("yerel yedek alınamadı", e); return null; }
}
async function yerelYedekListesi() {
try {
const anahtarlar = (await LDB.anahtarlar("ayar")).filter(a => String(a).startsWith("yedek|"));
const l = [];
for (const a of anahtarlar) { const v = await LDB.oku("ayar", a); if (v) l.push(v); }
return l.sort((a, b) => b.id - a.id);
} catch (e) { return []; }
}
async function yerelYedekBuda() {
const l = await yerelYedekListesi();
const oto = l.filter(x => x.tur === "oto").slice(YEREL_YEDEK_ADET);
const elle = l.filter(x => x.tur !== "oto").slice(YEREL_YEDEK_ADET);
for (const x of oto.concat(elle)) await LDB.sil("ayar", "yedek|" + x.id);
}
/* ---------------- ağır veri: kaydet / getir ---------------- */
async function agirYaz(anahtar, deger) { try { await LDB.yaz("dosya", anahtar, deger); } catch (e) { console.warn(e); } }
async function agirOku(anahtar) { try { return await LDB.oku("dosya", anahtar); } catch (e) { return null; } }
/* ---------------- sunucuya gönderilecek hafif kopya ---------------- */
function sunucuKopyasi() {
const s = JSON.parse(JSON.stringify({
firmalar: state.firmalar, tutanaklar: state.tutanaklar,
arsiv: state.arsiv || {}, defterDokum: state.defterDokum || {}, edefter: state.edefter || {}
}));
for (const fid in s.arsiv) {
const a = s.arsiv[fid];
for (const d in (a.kdv || {})) {
if (a.kdv[d].bey) delete a.kdv[d].bey.pdf64;
if (a.kdv[d].tah) delete a.kdv[d].tah.pdf64;
}
for (const d in (a.muh || {})) delete a.muh[d].pdf64;
for (const y in (a.yillar || {})) {
const yy = a.yillar[y];
if (yy.kurumlar) delete yy.kurumlar.pdf64;
if (yy.muavin) { yy.muavin = { fileName: yy.muavin.fileName, yuklenme: yy.muavin.yuklenme,
firm: yy.muavin.firm, donem: yy.muavin.donem, hesapSayisi: (yy.muavin.sections || []).length }; }
if (yy.plan) { yy.plan = { fileName: yy.plan.fileName, yuklenme: yy.plan.yuklenme, n: yy.plan.n }; }
}
}
const kveri = {};
for (const y in s.defterDokum) kveri["defterDokum/" + y] = s.defterDokum[y];
kveri["edefterFirma"] = s.edefter.firma || {};
const genel = {};
for (const y in (s.edefter.takvim || {})) genel["edefterTakvim/" + y] = s.edefter.takvim[y];
return { firmalar: s.firmalar, tutanaklar: s.tutanaklar, arsiv: s.arsiv, kveri, genel };
}
/* ---------------- senkron ---------------- */
let esitleZamanlayici = null, esitleBekliyor = false;
function esitleDurum(m, tur) {
const el = $("senkDurum"); if (!el) return;
el.className = "senk " + (tur || "");
el.textContent = m;
}
async function esitle() {
if (!KULLANICI) return;
esitleDurum("kaydediliyor…", "bekle");
try {
const c = sunucuKopyasi();
if (YEREL_MOD) {
const sayim = yedekOzetiCikar(state);
const neden = yerelYedekGerekli(sayim);
if (neden) await yerelYedekAl("oto", neden); // yazmadan önce güvenlik kopyası
sonSayim = sayim;
await LDB.yaz("ayar", "state", { firmalar: state.firmalar, tutanaklar: state.tutanaklar,
arsiv: c.arsiv, defterDokum: state.defterDokum, edefter: state.edefter });
esitlemeZaman = new Date();
esitleDurum("yerel kayıt " + esitlemeZaman.toLocaleTimeString("tr-TR"), "ok");
klasoreYedekDene(false);
esitleBekliyor = false; return;
}
await api("kaydet", c);
esitlemeZaman = new Date();
esitleDurum("kaydedildi " + esitlemeZaman.toLocaleTimeString("tr-TR"), "ok");
SUNUCU_HATA = 0;
klasoreYedekDene(false); // günde bir: bilgisayardaki klasöre kopya
} catch (e) {
SUNUCU_HATA++;
esitleDurum("kaydedilemedi: " + e.message, "hata");
if (SUNUCU_HATA === 1) toast("Sunucuya kaydedilemedi: " + e.message);
} finally { esitleBekliyor = false; }
}
/* eski persist() çağrıları buraya düşer: geciktirmeli senkron */
async function persist() {
if (!KULLANICI) return;
esitleBekliyor = true;
esitleDurum("değişiklik var…", "bekle");
clearTimeout(esitleZamanlayici);
esitleZamanlayici = setTimeout(esitle, 1200);
}
window.addEventListener("beforeunload", e => {
if (esitleBekliyor) { e.preventDefault(); e.returnValue = ""; }
});
/* ---------------- açılış ---------------- */
const nesne = v => (v && typeof v === "object" && !Array.isArray(v)) ? v : {};
async function sunucudanYukle() {
if (YEREL_MOD) {
const y = await LDB.oku("ayar", "state");
if (y) { state = Object.assign(state, y); }
sonSayim = yedekOzetiCikar(state);
const ly = await yerelYedekListesi();
sonYerelYedek = ly.length ? ly[0].id : 0;
migrate(); await agirHidrasyon(); return;
}
const j = await api("veri");
state.firmalar = (j.firmalar || []).map(f => ({ ...f }));
state.tutanaklar = (j.tutanaklar || []).map(t => ({ ...t }));
state.arsiv = nesne(j.arsiv);
for (const fid in state.arsiv) {
const a = state.arsiv[fid] = nesne(state.arsiv[fid]);
a.kdv = nesne(a.kdv); a.muh = nesne(a.muh); a.yillar = nesne(a.yillar);
for (const y in a.yillar) a.yillar[y] = nesne(a.yillar[y]);
}
state.defterDokum = {};
state.edefter = { takvim: {}, firma: {} };
for (const k in (j.kveri || {})) {
if (k.startsWith("defterDokum/")) state.defterDokum[k.split("/")[1]] = j.kveri[k];
else if (k === "edefterFirma") state.edefter.firma = nesne(j.kveri[k]);
}
for (const k in (j.genel || {})) {
if (k.startsWith("edefterTakvim/")) state.edefter.takvim[k.split("/")[1]] = nesne(j.genel[k]);
}
migrate();
await agirHidrasyon();
}
/* sunucudan gelen hafif kayıtlara yerel ağır verileri iliştir */
async function agirHidrasyon() {
for (const fid in state.arsiv) {
const a = state.arsiv[fid];
for (const d in (a.kdv || {})) {
const s = a.kdv[d];
if (s.bey) s.bey.pdf64 = await agirOku(`pdf|${fid}|kdvbey|${d}`) || undefined;
if (s.tah) s.tah.pdf64 = await agirOku(`pdf|${fid}|kdvtah|${d}`) || undefined;
}
for (const d in (a.muh || {})) a.muh[d].pdf64 = await agirOku(`pdf|${fid}|muh|${d}`) || undefined;
for (const y in (a.yillar || {})) {
const yy = a.yillar[y];
if (yy.kurumlar) yy.kurumlar.pdf64 = await agirOku(`pdf|${fid}|kurumlar|${y}`) || undefined;
if (yy.muavin) { const sec = await agirOku(`mu|${fid}|${y}`); if (sec) yy.muavin.sections = sec; }
if (yy.plan) { const dt = await agirOku(`plan|${fid}|${y}`); if (dt) yy.plan.data = dt; }
}
}
}
const esc = s => String(s ?? "").replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
const trU = s => (s || "").replace(/i/g, "İ").replace(/ı/g, "I").replace(/ç/g, "Ç").replace(/ğ/g, "Ğ").replace(/ö/g, "Ö").replace(/ş/g, "Ş").replace(/ü/g, "Ü").toUpperCase();
const money = n => (n ?? 0).toLocaleString("tr-TR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
function toastTipi(m) {
if (/⛔|hata|okunamadı|okunamıyor|yüklenemez|alınmadı|başarısız|uyuşmuyor|bulunamadı/i.test(m)) return "bad";
if (/⚠|uyarı|eksik|önce |gerekli|yok$/i.test(m)) return "warn";
if (/✓|hazır|tamam|yüklendi|indirildi|dolduruldu|kaydedildi|oluşturuldu|güncellendi|eklendi|alındı|okundu/i.test(m)) return "ok";
return "";
}
function toast(m, tip) {
const t = $("toast"); if (!t) return;
t.className = "toast " + (tip || toastTipi(m));
t.textContent = m;
void t.offsetWidth;
t.classList.add("show");
clearTimeout(toast._z);
toast._z = setTimeout(() => t.classList.remove("show"), 3400);
}
function pick(id) { $(id).click(); }
/* tarih yardımcıları: gg.aa.yyyy */
function pDate(s) { const m = /^(\d{1,2})[./](\d{1,2})[./](\d{4})$/.exec((s || "").trim()); return m ? new Date(+m[3], +m[2] - 1, +m[1]) : null; }
function fDate(d) { return d ? String(d.getDate()).padStart(2, "0") + "." + String(d.getMonth() + 1).padStart(2, "0") + "." + d.getFullYear() : ""; }
function addMonth(d) { const x = new Date(d); const day = x.getDate(); x.setMonth(x.getMonth() + 1); if (x.getDate() < day) x.setDate(0); return x; }
function daysLeft(s) { const d = pDate(s); if (!d) return null; const t = new Date(); t.setHours(0,0,0,0); return Math.round((d - t) / 864e5); }
function curT() { return state.tutanaklar.find(t => t.id === cur); }
function firmOf(t) { return state.firmalar.find(f => f.id === t.firmaId) || { ad: "?", vkn: "" }; }
/* ---------------- kimlik üretimi ---------------- */
function yeniId(on) {
const r = (crypto && crypto.getRandomValues)
? Array.from(crypto.getRandomValues(new Uint8Array(6))).map(x => x.toString(16).padStart(2, "0")).join("")
: Math.random().toString(16).slice(2, 14);
return on + "-" + r;
}
/* ---------------- oturum arayüzü ---------------- */
function kullaniciGoster() {
const el = $("kullaniciKutu"); if (!el || !KULLANICI) return;
if (YEREL_MOD) {
el.innerHTML = `
` : "")) + mhKutu;
$("dropPlan").classList.remove("okay"); $("dropMuavin").classList.remove("okay");
$("dropPlan").innerHTML = `Cari Hesap Plan ListesiLuca → cari hesap planı (.xlsx)`;
$("dropMuavin").innerHTML = `Muavin DökümLütfen ilgili yıla ait muavin döküm yükleyiniz (.xlsx)`;
$("calcInfo").textContent = ""; $("donemWarn").innerHTML = "";
loadFromArsiv(); // arşivdeki muavin/cari planı otomatik getir
const kk = kurumlarBul(t.firmaId, yilOf(t)).k;
if (kk && !(t.ortaklar || []).length) applyOrtaklar(t, kk);
renderSales(); renderBuys(); renderBeyan(); renderMuhtasar(); renderOrtak(); renderDefter(); renderImalatci(); renderTutHeaderBits();
}
/* ================================================================
DOSYA OKUMA
================================================================ */
let MU = null, PLAN = null; // bellekteki muavin/plan (tutanak başına yeniden yüklenir)
function bindDrop(dropId, inputId, handler) {
const d = $(dropId), i = $(inputId);
/* aynı dosyayı tekrar seçebilmek için değer sıfırlanır */
i.onchange = () => { const f = i.files[0]; i.value = ""; if (f) handler(f); };
d.ondragover = e => { e.preventDefault(); d.classList.add("over"); };
d.ondragleave = () => d.classList.remove("over");
d.ondrop = e => { e.preventDefault(); d.classList.remove("over"); e.dataTransfer.files[0] && handler(e.dataTransfer.files[0]); };
}
/* ---- PDF satır çıkarma (ortak yardımcı) ---- */
async function extractPdfLines(buf) {
const pdf = await pdfjsLib.getDocument({ data: buf }).promise;
let lines = [];
for (let p = 1; p <= pdf.numPages; p++) {
const pg = await pdf.getPage(p);
const tc = await pg.getTextContent();
const items = tc.items.map(it => ({ x: it.transform[4], y: it.transform[5], s: it.str })).filter(it => it.s.trim());
const rows = {};
items.forEach(it => { const key = Math.round(it.y / 4) * 4; (rows[key] = rows[key] || []).push(it); });
Object.keys(rows).sort((a, b) => b - a).forEach(k => {
lines.push(rows[k].sort((a, b) => a.x - b.x).map(i => i.s).join(" ").replace(/\s+/g, " ").trim());
});
lines.push("§SAYFA§");
}
return lines;
}
async function handlePdf(file) {
try {
const lines = await extractPdfLines(await file.arrayBuffer());
const t = curT();
parseTutanakLines(lines, t);
donemOtoDoldur(t, false, true); // dönem: önce fatura tarihleri, yoksa tutanak tarihi
persist(); renderTut();
toast("PDF okundu: " + (t.mukellef || "bilgiler alınamadı, elle girin")
+ (t.donemBas ? ` · dönem ${t.donemBas} – ${t.donemBit}` : ""));
} catch (e) { console.error(e); toast("PDF okunamadı: " + e.message); }
}
function parseTutanakLines(lines, t) {
const all = lines.join("\n");
let m = /Tarih\s*:?\s*(\d{2}\.\d{2}\.\d{4})/.exec(all); if (m) t.tarih = m[1];
m = /Sayı\s*:?\s*([0-9][0-9\-\/]*)/.exec(all); if (m) t.sayi = m[1].replace(/[-\/]+$/, "");
if (!t.sayi) { m = /:\s*(\d{1,6})\s*$/m.exec(lines.slice(0, 8).join("\n")); if (m) t.sayi = m[1]; }
// Bölüm bazlı ayrıştırma.
// A) Karşıt incelemeyi yapan YMM
// B) Tasdik hizmeti verilen mükellef → karşı firma (bizim sattığımız)
// C) "…MAL/HİZMET TESLİMİNDE BULUNDUĞU MÜKELLEF" → tedarikçi (üçüncü firma) — bazı tutanaklarda var
// Ç) "…NEZDİNDE KARŞIT İNCELEME YAPILAN MÜKELLEFİN BİLGİLERİ" → BİZİM MÜŞTERİMİZ
// Not: eski tutanaklarda müşteri C) harfindeydi. Bu yüzden bölüm harfe göre değil,
// başlığın metnine göre bulunur: "MAL/HİZMET" geçen başlık tedarikçi, geçmeyen müşteridir.
const idxA = lines.findIndex(l => /A\)\s*KARŞIT İNCELEMEYİ YAPAN/i.test(l));
const idxB = lines.findIndex(l => /B\)\s*TASDİK HİZMETİ VERİLEN/i.test(l));
const NEZ = /NEZD[İI]NDE\s+KARŞIT\s+[İI]NCELEME\s+YAPILAN\s+M[ÜU]KELLEF[İI]N/i;
const MALHIZ = /MAL\s*\/?\s*H[İI]ZMET\s+TESL[İI]M[İI]NDE\s+BULUNDU[ĞG]U/i;
const idxMH = lines.findIndex(l => NEZ.test(l) && MALHIZ.test(l)); // tedarikçi bölümü
const idxC = lines.findIndex(l => NEZ.test(l) && !MALHIZ.test(l)); // müşterimiz (C ya da Ç)
const grab = (seg, re) => { for (const l of seg) { const m = re.exec(l); if (m) return m[1].trim(); } return ""; };
/* Ünvan bazen iki satıra bölünür ve etiket ortada kalır:
"ÖZHANTEKS GİYİM MODA …" / "ADI VE SOYADI / ÜNVANI" / "TİCARET LİMİTED ŞİRKETİ" */
const ETIKET = /^\s*(VERG[İI]|ADRES|TELEFON|ADI VE|BA[ĞG]LI|M[ÜU]H[ÜU]R|S[İI]C[İI]L|Belge ID|Versiyon)/i;
function unvanBul(seg) {
const tek = grab(seg, /ADI VE SOYADI\s*\/?\s*ÜNVANI\s+(.+)/);
if (tek) return tek;
const i = seg.findIndex(l => /^\s*ADI VE SOYADI\s*\/?\s*ÜNVANI\s*:?\s*$/i.test(l));
if (i < 0) return "";
const p = [];
if (i > 0 && !ETIKET.test(seg[i - 1]) && !NEZ.test(seg[i - 1])) p.push(seg[i - 1].trim());
for (let j = i + 1; j < seg.length && !ETIKET.test(seg[j]); j++) p.push(seg[j].trim());
return p.join(" ").replace(/\s+/g, " ").trim();
}
const bolumSonu = (bas) => {
const adaylar = [idxMH, idxC].filter(x => x > bas);
return adaylar.length ? Math.min(...adaylar) : bas + 14;
};
if (idxA >= 0) t.ymm = grab(lines.slice(idxA, idxB > 0 ? idxB : idxA + 12), /ADI VE SOYADI\s+([A-ZÇĞİÖŞÜ][A-ZÇĞİÖŞÜa-zçğıöşü .]+)/) || t.ymm;
// B) tasdik hizmeti verilen mükellef — KDV iadesi alan firma (zincirin sonu), bilgi amaçlı
if (idxB >= 0) {
const seg = lines.slice(idxB, bolumSonu(idxB));
t.tasdikUnvan = unvanBul(seg) || t.tasdikUnvan || "";
t.tasdikVkn = grab(seg, /K[İI]ML[İI]K NUMARASI\s+(\d{10,11})/) || t.tasdikVkn || "";
t.tasdikVd = grab(seg, /VERG[İI] DA[İI]RES[İI]\s+([A-ZÇĞİÖŞÜ][^\n]*)/) || t.tasdikVd || "";
}
/* KARŞI FİRMA = müşterimizin fatura kestiği firma.
Yeni biçimde bu, "…MAL/HİZMET TESLİMİNDE BULUNDUĞU MÜKELLEF" bölümüdür
(zincir: müşterimiz → bu firma → tasdik hizmeti verilen mükellef).
Eski biçimde böyle bir bölüm yoktur; orada karşı firma B) bölümüdür. */
if (idxMH >= 0) {
/* ---- TİP 2: ZİNCİRLİ TUTANAK (3 taraflı) ----
müşterimiz → mal/hizmet teslim edilen firma → tasdik hizmeti verilen mükellef
Faturalar ortadaki firmaya kesilmiştir; cari hesap onun üzerinden aranır. */
t.tutanakTipi = "zincir";
const segM = lines.slice(idxMH, idxC > idxMH ? idxC : idxMH + 12);
t.mukellef = unvanBul(segM) || t.mukellef;
t.mukellefVkn = grab(segM, /K[İI]ML[İI]K NUMARASI\s+(\d{10,11})/) || t.mukellefVkn;
t.mukellefVd = (grab(segM, /VERG[İI] DA[İI]RES[İI]\s+([A-ZÇĞİÖŞÜ][^\n]*)/) || "").replace(/\s*(VERG[İI] DA[İI]RES[İI].*|M[ÜU]D\.?)$/i, "").trim() || t.mukellefVd;
} else {
/* ---- TİP 1: STANDART TUTANAK (2 taraflı) — eski davranış, dokunulmadı ----
müşterimiz → tasdik hizmeti verilen mükellef. Faturalar doğrudan B) firmasına kesilmiştir. */
t.tutanakTipi = "standart";
t.mukellef = t.tasdikUnvan || t.mukellef;
t.mukellefVkn = t.tasdikVkn || t.mukellefVkn;
t.mukellefVd = t.tasdikVd || t.mukellefVd;
}
if (!t.mukellefVkn) { // yedek: ünvan satırının hemen altındaki 10 haneli numara
for (let i = 0; i < lines.length - 1; i++) {
const m1 = /(ŞİRKETİ|ŞTİ\.?|A\.Ş\.?|LTD\.?)\s*$/.exec(lines[i]); const m2 = /^(\d{10})$/.exec(lines[i + 1]);
if (m1 && m2) { t.mukellef = t.mukellef || lines[i]; t.mukellefVkn = m2[1]; break; }
}
}
// Dönem: "İadeye Esas ..." tablosundaki satır (örn: "01.2025 12.2025 01.2026 25.03.2026 ...")
for (const l of lines) {
const m2 = /^(\d{2}\.20\d{2})\s+(\d{2}\.20\d{2})\s+(\d{2}\.20\d{2})/.exec(l);
if (m2) { t.donemBas = m2[1]; t.donemBit = m2[2]; break; }
}
// Nezdinde karşıt inceleme yapılan mükellef = BİZİM MÜŞTERİMİZ (yanlış firma seçimini yakalamak için)
if (idxC >= 0) {
const segC = lines.slice(idxC, idxC + 12);
t.cUnvan = unvanBul(segC);
t.cVkn = grab(segC, /K[İI]ML[İI]K NUMARASI\s+(\d{10,11})/) || "";
t.cVd = (grab(segC, /VERG[İI] DA[İI]RES[İI]\s+([A-ZÇĞİÖŞÜ][^\n]*)/) || "").replace(/\s*(VERG[İI] DA[İI]RES[İI].*|M[ÜU]D\.?)$/i, "").trim();
const adr = segC.find(l => /\/[A-ZÇĞİÖŞÜ]+$/.test(l.trim()));
if (adr) { const p = adr.trim().split("/"); t.cIl = trU(p[p.length - 1].trim()); }
// firma kaydında il/vd boşsa doldur
const f1 = firmOf(t);
if (f1 && f1.id) { if (!f1.vd && t.cVd) f1.vd = t.cVd; if (!f1.il && t.cIl) f1.il = t.cIl; }
}
// 3. bölüm fatura listesi — satır grupları:
// a) "10.01.2025 ATA20250000" / "3.800,00 760,00" / "00018" (numara ikiye bölünmüş)
// b) "10.01.2025 ATA2025000000018 3.800,00 760,00" (tek satır)
const invs = [];
for (let i = 0; i < lines.length; i++) {
let m1 = /^(\d{2}\.\d{2}\.\d{4})\s+(\S+)\s+([\d.]+,\d{2})\s+([\d.]+,\d{2})$/.exec(lines[i]);
if (m1) { invs.push({ no: m1[2], tarih: m1[1], tutar: trNum(m1[3]), kdv: trNum(m1[4]) }); continue; }
m1 = /^(\d{2}\.\d{2}\.\d{4})\s+([A-Z0-9]{2,15})$/.exec(lines[i]);
if (m1 && i + 2 < lines.length) {
const m2 = /^([\d.]+,\d{2})\s+([\d.]+,\d{2})$/.exec(lines[i + 1]);
const m3 = /^(\d{1,8})$/.exec(lines[i + 2]);
if (m2 && m3) { invs.push({ no: m1[2] + m3[1], tarih: m1[1], tutar: trNum(m2[1]), kdv: trNum(m2[2]) }); i += 2; }
}
}
t.pdfFaturalar = invs;
}
const trNum = s => parseFloat(String(s).replace(/\./g, "").replace(",", "."));
/* ---- Cari plan ---- */
async function handlePlan(file, yilOverride, firmaIdOverride) {
try {
const wb = XLSX.read(await file.arrayBuffer(), { type: "array" });
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1, raw: true, defval: "" });
const hdr = rows[0].map(x => trU(String(x)));
const col = n => hdr.findIndex(h => h.includes(n));
const ck = col("HESAP KODU"), cn = col("HESAP ADI"), cv = col("VERGİ NO"), cd = col("VERGİ DAİRESİ"), ct = col("T.C");
if (ck < 0) throw new Error("'HESAP KODU' sütunu bulunamadı — doğru dosya mı?");
const data = {};
rows.slice(1).forEach(r => {
const code = String(r[ck] || "").trim(); if (!code) return;
data[code] = { ad: String(r[cn] || "").trim(), vkn: String(r[cv] || "").trim(), vd: String(r[cd] || "").trim(), tckn: String(r[ct] || "").trim() };
});
// arşive yaz (firma + yıl)
const t = curT();
const fid = firmaIdOverride || (t && t.firmaId);
const yil = yilOverride || (t && yilOf(t));
if (fid && yil) { AY(fid, yil).plan = { fileName: file.name, yuklenme: fDate(new Date()), n: Object.keys(data).length, data };
agirYaz(`plan|${fid}|${yil}`, data); }
if (!yilOverride) { PLAN = data; planShow(); renderTutHeaderBits(); otoHesapla(); }
persist(); renderArsiv();
toast(`Cari hesap planı okundu (${Object.keys(data).length} hesap)` + (yil ? ` · ${yil} arşivine kaydedildi` : ""));
} catch (e) { toast("Cari plan okunamadı: " + e.message); }
}
function planShow() {
const d = $("dropPlan"); if (!d || !PLAN) return;
const t = curT(); const a = t ? (AY(t.firmaId, yilOf(t)).plan || {}) : {};
d.classList.add("okay");
d.innerHTML = `Cari plan hazır${Object.keys(PLAN).length} hesap${a.yuklenme ? " · arşiv: " + esc(a.yuklenme) : ""} · değiştirmek için tıklayın`;
}
/* ---- Muavin ---- */
/* Excel seri numarasını saat diliminden bağımsız gg.aa.yyyy'ye çevir */
function serialDMY(n) {
const d = new Date(Math.round((n - 25569) * 86400000));
return String(d.getUTCDate()).padStart(2, "0") + "." + String(d.getUTCMonth() + 1).padStart(2, "0") + "." + d.getUTCFullYear();
}
function cellDMY(v, acik) {
// 1) öncelik: açıklamadaki "gg/aa/yyyy-" öneki (muavinde her kayıtta var, saat dilimi sorunu yok)
const m = /^\s*(\d{2})\/(\d{2})\/(\d{4})/.exec(acik || "");
if (m) return m[1] + "." + m[2] + "." + m[3];
// 2) Excel seri numarası → UTC aritmetiği
if (typeof v === "number") return serialDMY(v);
// 3) Date nesnesi → UTC alanları (öğleye kaydırarak gün kaymasını önle)
if (v instanceof Date && !isNaN(v)) { const d = new Date(v.getTime() + 43200000); return String(d.getUTCDate()).padStart(2, "0") + "." + String(d.getUTCMonth() + 1).padStart(2, "0") + "." + d.getUTCFullYear(); }
// 4) metin
if (typeof v === "string") { const m2 = /^(\d{1,2})[./](\d{1,2})[./](\d{4})/.exec(v.trim()); if (m2) return m2[1].padStart(2, "0") + "." + m2[2].padStart(2, "0") + "." + m2[3]; }
return "";
}
async function handleMuavin(file, yilOverride, firmaIdOverride) {
try {
const wb = XLSX.read(await file.arrayBuffer(), { type: "array" });
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1, raw: true, defval: null });
const heads = [];
rows.forEach((r, i) => { if (typeof r[0] === "string" && /^\d{3}\./.test(r[0]) && r[4] === "TL") heads.push([i, r[0].trim()]); });
heads.push([rows.length, "END"]);
const sections = [];
for (let h = 0; h < heads.length - 1; h++) {
const [i, name] = heads[h]; const j = heads[h + 1][0];
const code = name.split(/[\s\t]/)[0];
const ents = [];
for (let r = i + 2; r < j; r++) {
const row = rows[r]; if (!row || (row[0] == null && row[3] == null)) continue;
const acik = row[3] != null ? String(row[3]) : "";
ents.push({
tarih: cellDMY(row[0], acik),
fis: row[2] != null ? String(row[2]) : "",
acik,
borc: typeof row[4] === "number" ? row[4] : 0,
alacak: typeof row[5] === "number" ? row[5] : 0
});
}
sections.push({ code, name: name.slice(code.length).trim(), entries: ents });
}
const mu = { fileName: file.name, yuklenme: fDate(new Date()),
firm: rows[1] ? String(rows[1][0] || "") : "", donem: rows[2] ? String(rows[2][1] || "") : "", sections };
const t = curT();
const fid = firmaIdOverride || (t && t.firmaId);
// yıl: dosyadaki dönemden çıkar, yoksa tutanak yılı
const my = (mu.donem.match(/(20\d{2})/) || [])[1];
const yil = yilOverride || my || (t && yilOf(t));
if (fid && yil) { AY(fid, yil).muavin = mu; agirYaz(`mu|${fid}|${yil}`, sections); }
if (!yilOverride) { MU = mu; muavinShow(); muavinDonemWarn(); renderTutHeaderBits(); otoHesapla(); }
persist(); renderArsiv();
toast(`Muavin okundu (${sections.length} hesap)` + (yil ? ` · ${yil} arşivine kaydedildi` : ""));
} catch (e) { console.error(e); toast("Muavin okunamadı: " + e.message); }
}
function muavinHesapSayisi(m) { return m ? ((m.sections && m.sections.length) || m.hesapSayisi || 0) : 0; }
function muavinShow() {
const d = $("dropMuavin"); if (!d || !MU) return;
d.classList.add("okay");
d.innerHTML = `Muavin hazır${esc((MU.firm || "").slice(0, 34))} · ${esc(MU.donem)} · ${muavinHesapSayisi(MU)} hesap${MU.yuklenme ? " · arşiv: " + esc(MU.yuklenme) : ""} · değiştirmek için tıklayın`;
}
function muavinDonemWarn() {
const t = curT(); if (!t || !MU) return;
if (t.donemBas && MU.donem && !MU.donem.includes(t.donemBas.split(".")[1]))
$("donemWarn").innerHTML = `
⚠️ Dikkat: Tutanak dönemi ${esc(t.donemBas)}–${esc(t.donemBit)}, yüklenen muavin dönemi ise ${esc(MU.donem)}. Lütfen ilgili yıla ait muavin döküm yükleyiniz.
`;
else $("donemWarn").innerHTML = "";
}
/* tutanak açılınca arşivden otomatik yükle */
async function loadFromArsiv() {
const t = curT(); if (!t) return;
const y = yilOf(t); if (!y) return;
const a = AY(t.firmaId, y);
if (a.muavin && !a.muavin.sections) {
const sec = await agirOku(`mu|${t.firmaId}|${y}`);
if (sec) a.muavin.sections = sec;
}
if (a.plan && !a.plan.data) {
const dt = await agirOku(`plan|${t.firmaId}|${y}`);
if (dt) a.plan.data = dt;
}
if (a.muavin && a.muavin.sections) { MU = a.muavin; muavinShow(); muavinDonemWarn(); }
else if (a.muavin) {
$("dropMuavin").innerHTML = `⚠ Muavin bu bilgisayarda yok${esc(a.muavin.fileName || "")} ·
kayıt sunucuda var ama dosya bu cihazda değil — yeniden yükleyin`;
MU = null;
}
if (a.plan && a.plan.data) { PLAN = a.plan.data; planShow(); }
else if (a.plan) {
$("dropPlan").innerHTML = `⚠ Cari plan bu bilgisayarda yokyeniden yükleyin`;
PLAN = null;
}
otoHesapla();
renderTutHeaderBits();
}
/* ================================================================
HESAPLAMA MOTORU
================================================================ */
const JUNK = /AÇILIŞ|VİRMAN|VIRMAN|DEVİR|KAPAMA|KUR FARKI|ENFLASYON|YANSITMA|Nakli Yekün/i;
function parseAcik(acik) {
const m = /^\s*(\d{2})\/(\d{2})\/(\d{4})-([^-]+)-(.*)$/.exec(acik || "");
if (!m) return { ref: "", firma: (acik || "").trim() };
let rest = m[5].trim();
if (rest.includes("--")) rest = rest.split("--").pop().trim();
return { ref: m[4].trim(), firma: rest.replace(/[,. ]+$/, "") };
}
function splitRef(ref) {
let m = /^([A-ZÇĞİÖŞÜa-z]+)(\d+)$/.exec(ref);
if (m) return [m[1], m[2]];
if (/^\d{16}$/.test(ref)) return [ref.slice(0, 3), ref.slice(3)];
return ["", ref];
}
function normName(s) {
s = trU(s).replace(/[^A-ZÇĞİÖŞÜ0-9 ]/g, " ");
[["SANAYİ","SAN"],["TİCARET","TİC"],["LİMİTED","LTD"],["ŞİRKETİ","ŞTİ"],["ANONİM","A"]].forEach(([a,b]) => s = s.split(a).join(b));
return s.replace(/\s+/g, " ").trim();
}
function planLookup(code, name) {
let p = code && PLAN[code];
if (p && (p.vkn || p.tckn)) return p;
const nn = normName(name);
for (const c in PLAN) { const q = PLAN[c]; if ((q.vkn || q.tckn) && normName(q.ad) === nn) return q; }
return p || { ad: name, vkn: "", vd: "", tckn: "" };
}
function findInFis(fis, ref, codePred) {
const hits = [];
for (const sec of MU.sections) {
if (/^19[013]/.test(sec.code)) continue;
if (!codePred(sec.code)) continue;
for (const e of sec.entries) if (e.fis === fis && ref && e.acik.includes(ref)) hits.push([sec, e]);
}
return hits;
}
function rateOf(name) { const m = /%\s*(\d+)/.exec(name) || /(\d+)\s*%/.exec(name); return m ? +m[1] / 100 : 0.20; }
const fmtD = d => d instanceof Date && !isNaN(d) ? fDate(d) : "";
/* --- alışlar: 191 tabanlı en yüksek N fatura + çapraz kontrol --- */
function computeBuys(nCand = 18) {
const cands = [];
for (const sec of MU.sections) {
if (!sec.code.startsWith("191")) continue;
if (/İADE|IADE|SATIŞ|SATIS/.test(trU(sec.name))) continue;
const rate = rateOf(sec.name);
for (const e of sec.entries) {
if (e.borc <= 0 || JUNK.test(e.acik)) continue;
const { ref, firma } = parseAcik(e.acik);
if (!ref) continue;
cands.push({ kdv: e.borc, rate, tarih: e.tarih, fis: e.fis, ref, firma, kdvHesap: sec.code });
}
}
cands.sort((a, b) => b.kdv - a.kdv);
return cands.slice(0, nCand).map(c => {
const matrah = Math.round(c.kdv / c.rate * 100) / 100;
// cari karşı kaydı (KDV dahil tutar buradan)
const hC = findInFis(c.fis, c.ref, cd => ["320", "329", "336", "321"].includes(cd.slice(0, 3)));
let gross = null, cariSec = null;
hC.forEach(([s, e]) => { if (e.alacak > 0) { gross = e.alacak; cariSec = s; } });
if (gross === null) gross = Math.round((matrah + c.kdv) * 100) / 100;
// matrah kontrolü: fişteki TÜM borç kayıtları (191 ve cari hariç) toplanır — 15'li grup öncelikli
const hD = findInFis(c.fis, c.ref, cd => !/^19/.test(cd) && !/^39/.test(cd) && !["320","329","336","321","120","100","101","102","103","108"].includes(cd.slice(0, 3)))
.filter(([s, e]) => e.borc > 0);
const sum = hD.reduce((a, [s, e]) => a + e.borc, 0);
const sum15 = hD.filter(([s]) => s.code.startsWith("15")).reduce((a, [s, e]) => a + e.borc, 0);
const sum2x = hD.filter(([s]) => /^2/.test(s.code)).reduce((a, [s, e]) => a + e.borc, 0);
const codes = [...new Set(hD.map(([s]) => s.code))];
const duran = sum2x > matrah * 0.5;
let note, level, sel;
if (!hD.length) { note = "15'li grupta ve diğer hesaplarda karşı kayıt bulunamadı – manuel kontrol edin"; level = "bad"; sel = false; }
else {
const ok = Math.abs(sum - matrah) < 1;
const pre = sum15 > 0 ? "" : "15'li grupta bulunamadı; ";
note = pre + codes.join(" + ") + " ile check edildi – " + (ok ? "tutarlı" : "TUTARSIZ (bulunan toplam " + money(sum) + ", matrah " + money(matrah) + ")");
if (duran) note += " · DURAN VARLIK (taşıt/demirbaş — satılan mal kapsamı dışında)";
level = !ok ? "bad" : (duran ? "warn" : (sum15 > 0 ? "ok" : "warn"));
sel = ok && !duran;
}
const p = planLookup(cariSec ? cariSec.code : "", c.firma);
if (!(p.vkn || p.tckn) || !p.vd) { note += " · VKN/vergi dairesi cari planda eksik — elle tamamlayın"; if (level === "ok") level = "warn"; }
const [seri, numara] = splitRef(c.ref);
return { sec: sel, firma: c.firma, vkn: p.vkn || p.tckn, vd: p.vd,
tarih: c.tarih, seri, numara, kdvDahil: gross, matrah, kdv: c.kdv, kontrol: note, level, duran };
});
}
/* --- satışlar: karşı firmanın 120 hesabı --- */
const PAYP = [[/FAST|HAVALE|EFT|IBAN|TR\d{10,}|BANKA|POS|DEKONT/i, "BANKA"], [/ÇEK|CEK/i, "ÇEK"], [/SENET/i, "SENET"], [/NAKİT|NAKIT|KASA|TAHSİLAT/i, "NAKİT"]];
/* Karşı firmanın cari hesabını bul.
Önce 120 (Alıcılar). Bulunamazsa 320 (Satıcılar) — aynı firma hem alıcı hem
satıcı olabilir ve bazı bürolar tek cari kullanır; o zaman satış faturası
320 hesabına BORÇ yazılır. 320'den gelen satırlarda alışların karışmaması
için satış kanıtı (aynı fişte 391 hesaplanan KDV veya 600 satış alacağı)
ZORUNLUDUR — kanıtı olmayan satır alınmaz. */
function cariBul(kok, vkn, name) {
const codes = Object.keys(PLAN).filter(c => c.startsWith(kok) && vkn && (PLAN[c].vkn === vkn || PLAN[c].tckn === vkn));
let secs = MU.sections.filter(s => s.code.startsWith(kok) && (codes.includes(s.code) || normName(s.name) === normName(name)));
if (!secs.length) {
const nn = normName(name).slice(0, 20);
secs = MU.sections.filter(s => s.code.startsWith(kok) && nn && normName(s.name).includes(nn));
}
return secs;
}
function computeSales(vkn, name) {
let secs = cariBul("120", vkn, name);
let kaynak = "120";
if (!secs.length) { const s320 = cariBul("320", vkn, name); if (s320.length) { secs = s320; kaynak = "320"; } }
const out = [], pays = [];
for (const sec of secs) {
for (const e of sec.entries) {
if (e.alacak > 0 && !JUNK.test(e.acik)) {
const hit = PAYP.find(([re]) => re.test(e.acik));
pays.push(hit ? hit[1] : "BANKA");
}
if (e.borc <= 0 || JUNK.test(e.acik)) continue;
const { ref } = parseAcik(e.acik);
if (!ref || !/\d{6,}/.test(ref)) continue;
const gross = e.borc;
let kdv = null, kdvSec = null, net600 = null, sec600 = null;
findInFis(e.fis, ref, cd => cd.startsWith("391")).forEach(([s2, e2]) => { if (e2.alacak > 0) { kdv = (kdv || 0) + e2.alacak; kdvSec = s2; } });
findInFis(e.fis, ref, cd => cd.startsWith("60")).forEach(([s2, e2]) => { if (e2.alacak > 0) { net600 = (net600 || 0) + e2.alacak; sec600 = s2; } });
/* 320'den okunuyorsa: satış kanıtı yoksa satır alınmaz (alış/ödeme kaydı olabilir) */
if (kaynak === "320" && kdv === null && net600 === null) continue;
let note, level = "ok";
if (kdv === null && net600 !== null) { kdv = Math.round((gross - net600) * 100) / 100; note = `600 (${sec600.code}) üzerinden hesaplandı`; level = "warn"; }
else if (kdv === null) { kdv = Math.round((gross - gross / 1.2) * 100) / 100; note = "391/600 kaydı bulunamadı; KDV %20 varsayıldı – kontrol edin"; level = "bad"; }
else {
const net = Math.round((gross - kdv) * 100) / 100;
if (net600 !== null && Math.abs(net600 - net) < 1) note = `${kdvSec.code} ve ${sec600.code} ile check edildi – tutarlı`;
else if (net600 !== null) { note = `${kdvSec.code} ile check edildi; 600 toplamı farklı (${money(net600)}) – kontrol edin`; level = "warn"; }
else note = `${kdvSec.code} ile check edildi – tutarlı`;
}
out.push({ tarih: e.tarih, seri: "", numara: ref, tutar: Math.round((gross - kdv) * 100) / 100, kdv: Math.round(kdv * 100) / 100,
kayit: e.tarih, yevmiye: e.fis, odeme: "", kontrol: note, level });
}
}
let dominant = "BANKA";
if (pays.length) { const cnt = {}; pays.forEach(p => cnt[p] = (cnt[p] || 0) + 1); dominant = Object.keys(cnt).sort((a, b) => cnt[b] - cnt[a])[0]; }
out.forEach(r => r.odeme = dominant);
out.sort((a, b) => (pDate(a.tarih) || 0) - (pDate(b.tarih) || 0));
if (kaynak === "320") out.forEach(r => { r.kontrol = "320 satıcı carisinden alındı · " + r.kontrol; if (r.level === "ok") r.level = "warn"; });
return { rows: out, dominant, hesap: secs.length ? secs[0].code : "", kaynak: secs.length ? kaynak : "" };
}
/* iki Luca dosyası da hazırsa ve tablolar boşsa hesaplamayı kendiliğinden yap */
function otoHesapla() {
const t = curT(); if (!t) return;
if (MU && PLAN && (t.mukellefVkn || t.mukellef) && !(t.satis || []).length && !(t.alis || []).length) {
calc(true);
}
}
/* iskelet (skeleton) satirlari: "sistem calisiyor" hissi */
function iskeletGoster(tableId, satir, kolon) {
const tb = $(tableId); if (!tb) return;
const body = tb.querySelector("tbody"); if (!body) return;
body.innerHTML = Array.from({ length: satir }, () =>
`
${Array.from({ length: kolon }, (_, j) =>
`
`).join("")}
`).join("");
}
function calcTikla() {
const b = $("btnCalc"); if (!b || b.disabled) return;
bolumAc("cardSales"); bolumAc("cardBuys");
iskeletGoster("salesTable", 6, 9);
iskeletGoster("buyTable", 6, 9);
b.disabled = true; b.textContent = "⏳ Tablolar hazırlanıyor…";
setTimeout(() => {
b.textContent = "⚙️ İşle: Tabloları Doldur";
try { calc(); } finally { renderTutHeaderBits(); }
}, 420);
}
function calc(oto) {
const t = curT();
if (!MU || !PLAN) { toast("Önce cari plan ve muavin yükleyin"); return; }
if (!t.mukellefVkn && !t.mukellef) { toast("Karşı firma bilgisi yok — PDF yükleyin veya 2. adımda elle girin"); return; }
const s = computeSales(t.mukellefVkn, t.mukellef);
t.satis = s.rows; t.odemeGenel = s.dominant;
t.alis = computeBuys(18);
// seçimleri ilk 10'a ayarla
let n = 0; t.alis.forEach(a => { if (a.sec && n < 10) n++; else a.sec = false; });
$("calcInfo").textContent = s.hesap
? `Karşı firma hesabı: ${s.hesap}${s.kaynak === "320" ? " (320 satıcı carisi)" : ""} · ${t.satis.length} satış faturası bulundu`
: "⚠️ Karşı firmanın 120 / 320 cari hesabı bulunamadı!";
t.satisKaynak = s.kaynak || "";
persist(); renderSales(); renderBuys(); renderTutHeaderBits();
bolumAc("cardSales"); bolumAc("cardBuys");
toast(oto ? "Luca dosyaları hazır — tablolar otomatik dolduruldu ✓" : "Tablolar dolduruldu");
}
/* ================================================================
TABLO GÖRÜNÜMLERİ
================================================================ */
function renderSales() {
const t = curT(); if (!t) return;
const tb = $("salesTable").querySelector("tbody");
if (!t.satis.length) { tb.innerHTML = `
Henüz hesaplanmadı — 3. adımda dosyaları yükleyip "İşle"ye basın.
${money(t.satis.reduce((a, r) => a + r.tutar, 0))}
${money(t.satis.reduce((a, r) => a + r.kdv, 0))}
`;
const k320 = t.satisKaynak === "320"
? `
Karşı firmanın 120 alıcılar hesabı bulunamadı; satışlar 320 satıcılar carisinden alındı
(aynı firma hem alıcı hem satıcı olabilir). Yalnızca aynı fişte 391 hesaplanan KDV ya da 600 satış
kaydı bulunan satırlar alındı — alış ve ödeme kayıtları dışarıda bırakıldı. Yine de listeyi bir kez gözden geçirin.
`
: "";
// PDF çapraz kontrol
if (t.pdfFaturalar.length) {
const ours = new Map(t.satis.map(r => [r.numara, r]));
const missing = t.pdfFaturalar.filter(f => !ours.has(f.no));
const extra = t.satis.filter(r => !t.pdfFaturalar.some(f => f.no === r.numara));
const diff = t.pdfFaturalar.filter(f => { const r = ours.get(f.no); return r && (Math.abs(r.tutar - f.tutar) > 0.01 || Math.abs(r.kdv - f.kdv) > 0.01); });
if (!missing.length && !extra.length && !diff.length)
$("salesCross").innerHTML = k320 + `
Aylık KDV: ${k.aylar.map(m => `${m.d.slice(0, 2)}`).join("")}
${k.onceki.d}
Aylık muhtasar: ${mh.aylar.map(m => `${m.d.slice(0, 2)}`).join("")}
Dosyalar Luca'da nerede?
`;
}).join("");
}
/* yıl kartlarını aç/kapa — varsayılan: yalnız en güncel yıl açık */
const ARSIV_ACIK = new Set(), ARSIV_ILK = new Set();
function yilAc(fid, y) {
const k = fid + "|" + y;
ARSIV_ACIK.has(k) ? ARSIV_ACIK.delete(k) : ARSIV_ACIK.add(k);
renderArsiv();
}
function yilHepsi(ac) {
const fid = arsivFid();
yillarOf(fid).forEach(y => ac ? ARSIV_ACIK.add(fid + "|" + y) : ARSIV_ACIK.delete(fid + "|" + y));
renderArsiv();
}
async function arsivFiles(files) {
if (!arsivUp) return;
const { fid, yil, tip } = arsivUp;
if (tip === "kdv") await handleBeyanFiles([...files], fid);
else if (tip === "muavin") await handleMuavin(files[0], yil, fid);
else if (tip === "plan") await handlePlan(files[0], yil, fid);
else if (tip === "kurumlar") await handleKurumlar(files[0], yil, fid);
else if (tip === "muh") await handleMuhtasarFiles([...files], fid);
arsivUp = null; renderArsiv();
}
async function beyanFolder() {
if (!window.showDirectoryPicker) { toast("Klasör okuma bu tarayıcıda desteklenmiyor — Chrome/Edge kullanın veya PDF'leri 1. alandan yükleyin"); return; }
try {
const dir = await window.showDirectoryPicker();
const files = [];
async function walk(d, depth) {
for await (const e of d.values()) {
if (e.kind === "file" && /\.pdf$/i.test(e.name)) files.push(await e.getFile());
else if (e.kind === "directory" && depth < 3) await walk(e, depth + 1);
}
}
await walk(dir, 0);
if (!files.length) { toast("Klasörde PDF bulunamadı"); return; }
toast(files.length + " PDF tarandı, işleniyor…");
await handleBeyanFiles(files);
} catch (e) { /* kullanıcı vazgeçti */ }
}
/* tutanak dönemine göre satır listesi: ilgili aylar + en altta önceki dönem */
function beyanRows(t) {
const A = arsiv(t.firmaId);
const pm = s => { const m = /^(\d{2})\.(\d{4})$/.exec(s || ""); return m ? { m: +m[1], y: +m[2] } : null; };
const key = d => String(d.m).padStart(2, "0") + "." + d.y;
let bas = pm(t.donemBas), bit = pm(t.donemBit);
let months = [], onceki = null;
if (bas && bit) {
let d = { ...bas };
while (d.y < bit.y || (d.y === bit.y && d.m <= bit.m)) { months.push(key(d)); d.m++; if (d.m > 12) { d.m = 1; d.y++; } }
const p = { ...bas }; p.m--; if (p.m < 1) { p.m = 12; p.y--; }
onceki = key(p);
months.push(onceki); // önceki dönem en altta
} else {
months = Object.keys(A).sort((a, b) => a.split(".").reverse().join("") < b.split(".").reverse().join("") ? -1 : 1);
// dönem girilmemişse: arşivdeki her yıl için bir önceki yılın Aralık'ı da beklenir
const yillar = [...new Set(months.map(x => x.split(".")[1]))];
for (const y of yillar) {
const dec = "12." + (parseInt(y) - 1);
if (!months.includes(dec) && months.some(x => x.endsWith("." + y) && x !== dec)) { months.push(dec); onceki = onceki || dec; }
}
}
return months.map(dn => {
const s = A[dn] || {};
return { donem: dn, bey: s.bey || null, tah: s.tah || null, onceki: dn === onceki };
});
}
/* eksik kontrolü: önceki dönem ayrı ve önemli — hem beyanname hem tahakkuk aranır */
function beyanEksik(t) {
const rows = beyanRows(t);
const o = rows.find(r => r.onceki);
return {
rows,
oncekiDonem: o ? o.donem : null,
oncekiBeyYok: o ? !o.bey : false,
oncekiTahYok: o ? !o.tah : false,
beyYok: rows.filter(r => !r.onceki && !r.bey).map(r => r.donem),
tahYok: rows.filter(r => !r.onceki && r.bey && !r.tah).map(r => r.donem)
};
}
/* 1.5.2 öncesi arşivlenen beyannamelerde istisna alanı yok — bu cihazda PDF'i
duran dönemler bir kez yeniden okunup "Özel Matrah Dahil Olm." değeri doldurulur */
const ISTISNA_TARAMA = new Set();
async function beyanIstisnaTamamla(fid) {
if (!fid || ISTISNA_TARAMA.has(fid)) return;
ISTISNA_TARAMA.add(fid);
const A = arsiv(fid);
let n = 0;
for (const d of Object.keys(A)) {
const b = A[d] && A[d].bey;
if (!b || "istisna" in b) continue;
const pdf64 = await agirOku(`pdf|${fid}|kdvbey|${d}`);
if (!pdf64) continue; // PDF bu cihazda yok — yeniden yüklenince dolar
try {
const bin = atob(pdf64); const u = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i);
const r = parseBeyanPdf(await extractPdfLines(u.buffer), b.fileName || "");
if (r.tip !== "beyanname") continue;
b.ozelBedel = r.ozelBedel; b.istisna = r.istisna; b.ozel = r.ozel; n++;
} catch (e) { console.warn("istisna tarama", d, e); }
}
if (n) { persist(); renderBeyan(); toast(`${n} dönemin "Özel Matrah Dahil Olm." değeri beyannameden dolduruldu`); }
}
function renderBeyan() {
const t = curT(); if (!t) return;
const A = arsiv(t.firmaId);
beyanIstisnaTamamla(t.firmaId);
const nDon = Object.keys(A).length;
$("beyanArsivInfo").innerHTML = nDon
? `📁 Firma arşivinde ${nDon} dönem kayıtlı (${Object.keys(A).sort().join(", ")}). Beyanname PDF'leri yedek JSON'a dahildir.`
: "Arşiv boş — beyanname ve tahakkuk PDF'lerini yükleyin.";
const ek = beyanEksik(t);
const rows = ek.rows;
const tb = $("beyanTable").querySelector("tbody");
if (!rows.length) { tb.innerHTML = `
Tutanak dönemi (2. adım) girildiğinde aylar burada listelenir.
`; $("beyanWarn").innerHTML = ""; return; }
// uyarılar — önceki dönem özel olarak vurgulanır
let w = "";
if (ek.oncekiDonem && (ek.oncekiBeyYok || ek.oncekiTahYok)) {
const neler = [ek.oncekiBeyYok ? "beyannamesi" : "", ek.oncekiTahYok ? "tahakkuku" : ""].filter(Boolean).join(" ve ");
w += `
⚠️ ÖNCEKİ DÖNEM EKSİK: ${ek.oncekiDonem} dönemine ait KDV ${neler} yüklenmemiş. Tutanağın 5. bölümü önceki dönemi de ister — lütfen ${ek.oncekiDonem} beyanname ve tahakkuk PDF'lerini de yükleyin.
`;
} else if (ek.oncekiDonem) {
w += `
✓ Önceki dönem (${ek.oncekiDonem}) beyanname ve tahakkuku arşivde mevcut.
`;
}
if (ek.beyYok.length) w += `
İlgili dönemde beyannamesi eksik aylar: ${ek.beyYok.join(", ")}
`;
if (ek.tahYok.length) w += `
Beyannamesi var ama tahakkuku eksik aylar (fiş no yazılamaz): ${ek.tahYok.join(", ")}
`;
$("beyanWarn").innerHTML = w;
tb.innerHTML = rows.map(r => {
const b = r.bey, k = r.tah;
const durum = b && k ? `tam` : b ? `tahakkuk yok` : k ? `beyanname yok` : `eksik`;
const v = x => x === null || x === undefined ? "—" : money(x);
return `
${r.donem}${r.onceki ? ' önceki dönem' : ''}
${b ? v(b.teslim) : "—"}
${b ? v(b.ozel) : "—"}
${b ? v(b.matrah) : "—"}
${b ? v(b.hesaplanan) : "—"}
${b ? v(b.ilave) : "—"}
${b ? v(b.toplam) : "—"}
${b ? v(b.indirimler) : "—"}
${b ? v(b.odenmesi) : "—"}
${b ? v(b.devreden) : "—"}
${k ? esc(k.fisNo || "—") : "—"}
${durum}
`;
}).join("");
}
async function exportBeyan() {
const t = curT(); const f = firmOf(t);
const ek = beyanEksik(t);
const rows = ek.rows.filter(r => r.bey || r.tah);
if (!rows.length) { toast("Arşivde bu döneme ait beyanname yok"); return; }
if (ek.oncekiDonem && (ek.oncekiBeyYok || ek.oncekiTahYok)) {
if (!confirm(`DİKKAT: Önceki dönem (${ek.oncekiDonem}) ${ek.oncekiBeyYok ? "beyannamesi" : "tahakkuku"} eksik!\nTutanağın 5. bölümü önceki dönemi de ister.\n\nYine de eksik haliyle indirilsin mi?`)) return;
}
const eksik = ek.rows.filter(r => !r.bey).map(r => r.donem);
const wb = await makeWb(
["Dönem", "Teslim ve Hizmet Karşılığını Teşkil Eden Bedel", "Özel Matrah Şekline Tabi İşl. Matr. Dahil Olm. Bedel", "KDV Matrahı", "Hesaplanan KDV", "İlave Edilecek KDV", "Toplam KDV", "İndirimler Toplamı", "Ödenmesi Gereken KDV", "Sonraki Döneme Devreden KDV", "Tahakkuk Fişinin Numarası"],
[16, 25, 25, 19, 19, 19, 19, 22, 19, 19, 25],
rows.map(r => [r.donem,
r.bey ? r.bey.teslim : "", r.bey ? r.bey.ozel : "", r.bey ? r.bey.matrah : "", r.bey ? r.bey.hesaplanan : "",
r.bey ? r.bey.ilave : "", r.bey ? r.bey.toplam : "", r.bey ? r.bey.indirimler : "", r.bey ? r.bey.odenmesi : "",
r.bey ? r.bey.devreden : "", r.tah ? (r.tah.fisNo || "") : ""]),
[2, 3, 4, 5, 6, 7, 8, 9, 10], null);
await dl(wb, `5_ONCEKI_DONEM_VE_ILGILI_DONEM_KDV_BEYAN_BILGILERI_${f.vkn}_${fnDate(t)}.xlsx`);
toast("KDV beyan bilgileri Excel'i indirildi" + (eksik.length ? " · EKSİK aylar: " + eksik.join(", ") : ""));
}
/* ---- e-defter berat zipleri: zip saklanmaz, yalnız bilgiler okunur ---- */
/* küçük zip okuyucu (merkezi dizin + deflate-raw) — dış kütüphane gerekmez */
async function zipDosyalari(buf) {
const dv = new DataView(buf), u8 = new Uint8Array(buf);
let eocd = -1;
for (let i = buf.byteLength - 22; i >= Math.max(0, buf.byteLength - 65558); i--)
if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; }
if (eocd < 0) throw new Error("zip biçimi tanınamadı");
const adet = dv.getUint16(eocd + 10, true);
let p = dv.getUint32(eocd + 16, true);
const out = [];
for (let k = 0; k < adet; k++) {
if (dv.getUint32(p, true) !== 0x02014b50) break;
const yontem = dv.getUint16(p + 10, true);
const boy = dv.getUint32(p + 20, true);
const adLen = dv.getUint16(p + 28, true), ekLen = dv.getUint16(p + 30, true), yorumLen = dv.getUint16(p + 32, true);
const ofs = dv.getUint32(p + 42, true);
const ad = new TextDecoder().decode(u8.subarray(p + 46, p + 46 + adLen));
p += 46 + adLen + ekLen + yorumLen;
const lAd = dv.getUint16(ofs + 26, true), lEk = dv.getUint16(ofs + 28, true);
const ham = u8.subarray(ofs + 30 + lAd + lEk, ofs + 30 + lAd + lEk + boy);
let veri;
if (yontem === 0) veri = ham;
else if (yontem === 8)
veri = new Uint8Array(await new Response(new Blob([ham]).stream()
.pipeThrough(new DecompressionStream("deflate-raw"))).arrayBuffer());
else continue;
out.push({ ad, veri });
}
return out;
}
/* GİB berat XML'inden: tür (KEB/YEV), berat no, dönem, oluşturma tarihi, VKN */
function parseBeratXml(x, dosyaAdi) {
const val = re => { const m = re.exec(x); return m ? m[1] : ""; };
const no = val(/<[\w-]*:?uniqueID[^>]*>\s*((?:KEB|YEV)[A-Z0-9]+)\s*]*gib\.gov\.tr[^>]*>\s*(\d{10,11})\s*]*>\s*(\d{10,11})\s*]*>\s*(\d{4})-(\d{2})-(\d{2})/i.exec(x);
const tarih = m ? `${m[3]}.${m[2]}.${m[1]}` : "";
m = /<[\w-]*:?periodCoveredStart[^>]*>\s*(\d{4})-(\d{2})/i.exec(x);
const bas = m ? `${m[2]}.${m[1]}` : "";
m = /<[\w-]*:?periodCoveredEnd[^>]*>\s*(\d{4})-(\d{2})/i.exec(x);
const bit = m ? `${m[2]}.${m[1]}` : bas;
return { tur, no, vkn, tarih, bas, bit };
}
async function handleBeratZips(files) {
const t = curT(); if (!t) { toast("Önce bir tutanak açın"); return; }
const tYil = yilOf(t);
if (!tYil) {
toast("⛔ Önce tutanak dönemini girin — e-Defter beratları yalnızca tutanağın dönemine yüklenebilir");
donemUyar();
return;
}
const fid = t.firmaId, f0 = firmOf(t);
let n = 0, nSkip = 0, wrongVkn = 0, wrongYil = 0; const yillar = new Set();
for (const file of files) {
try {
const xmller = [];
if (/\.xml$/i.test(file.name)) xmller.push({ ad: file.name, veri: new Uint8Array(await file.arrayBuffer()) });
else if (/\.zip$/i.test(file.name))
for (const g of await zipDosyalari(await file.arrayBuffer()))
if (/\.xml$/i.test(g.ad)) xmller.push(g);
if (!xmller.length) { nSkip++; continue; }
let alindi = false;
for (const g of xmller) {
const r = parseBeratXml(new TextDecoder().decode(g.veri), file.name);
if (!r.tur || !r.no || !r.bas) continue;
if (f0.vkn && r.vkn && r.vkn !== f0.vkn) { wrongVkn++; alindi = true; break; }
const yil = r.bas.split(".")[1];
if (yil !== tYil) { wrongYil++; alindi = true; break; } // tutanak dönemi dışı berat alınmaz
const slot = AY(fid, yil);
slot.beratlar = slot.beratlar || {};
slot.beratlar[`${r.tur}|${r.bas}`] = { bit: r.bit, no: r.no, tarih: r.tarih };
const c = edefFirma(fid, yil);
if (!c.mukellef) { c.mukellef = true; c.elle = true; } // berat varsa e-defter mükellefidir
yillar.add(yil); n++; alindi = true;
}
if (!alindi) nSkip++;
} catch (e) { console.warn("berat", file.name, e); nSkip++; }
}
if (n) persist();
renderDefter(); renderArsiv();
let msg = n ? `${n} berat okundu (${[...yillar].sort().join(", ")}) — zip'ler kaydedilmedi, yalnız bilgiler alındı`
: "Berat bilgisi okunamadı";
if (wrongVkn) msg += ` · ${wrongVkn} dosya BAŞKA FİRMAYA ait (alınmadı)`;
if (wrongYil) msg += ` · ${wrongYil} dosya BAŞKA DÖNEME ait (alınmadı — tutanak dönemi ${tYil})`;
if (nSkip) msg += ` · ${nSkip} dosya tanınamadı`;
toast(msg);
}
const yilBeratlar = (fid, yil) => (fid && yil && (A(fid).yillar[yil] || {}).beratlar) || {};
/* ---- yasal defterler: Luca dökümü + e-defter dönemleri ---- */
/* defter bilgileri YALNIZCA tutanağın dönemine göre işlenir.
Dönem girilmemişse hiçbir defter/berat verisi alınmaz, kullanıcı uyarılır. */
function donemZorunlu() {
const t = curT();
if (t && yilOf(t)) return true;
toast("⛔ Önce tutanak dönemini girin — defter bilgileri yalnızca tutanağın dönemine yüklenir");
donemUyar();
return false;
}
function defterPick() { if (donemZorunlu()) pick("defterIn"); }
function kurumPick() {
const t = curT();
if (t && yilOf(t)) { pick("kurumIn"); return; }
toast("⛔ Önce tutanak dönemini girin — ortaklık bilgileri tutanağın dönemine göre alınır");
donemUyar();
}
function beratPick() { if (donemZorunlu()) pick("beratIn"); }
/* dönem girilmeden defter/berat yüklenmek istenirse: kullanıcıyı dönem alanına götür */
function donemUyar() {
const el = $("f_dbas"); if (!el) return;
el.scrollIntoView({ behavior: "smooth", block: "center" });
el.classList.remove("vurgu"); void el.offsetWidth; el.classList.add("vurgu");
setTimeout(() => { try { el.focus(); } catch (e) {} }, 250);
setTimeout(() => el.classList.remove("vurgu"), 3000);
}
function defterFirmaEsle(yil, f0) {
const dd = (state.defterDokum || {})[yil];
if (!dd || !Array.isArray(dd.rows)) return null;
const nn = normName(f0.ad);
if (f0.lucaAd) { const h0 = dd.rows.find(r => r.firma === f0.lucaAd); if (h0) return h0; }
let hit = dd.rows.find(r => normName(r.firma) === nn);
if (!hit) hit = dd.rows.find(r => { const n2 = normName(r.firma); return n2.length >= 4 && nn.startsWith(n2); });
if (!hit) hit = dd.rows.find(r => { const n2 = normName(r.firma); return n2.length >= 4 && nn.includes(n2); });
if (!hit) hit = dd.rows.find(r => { const n2 = normName(r.firma); return nn.length >= 4 && n2.startsWith(nn); });
if (!hit) hit = dd.rows.find(r => { const n2 = normName(r.firma); return nn.length >= 4 && n2.includes(nn); });
return hit || null;
}
function defterRows(t) {
const f0 = firmOf(t), yil = yilOf(t);
const out = [];
if (!yil) return out;
const hit = defterFirmaEsle(yil, f0);
if (hit) (hit.defterler || []).forEach(d => out.push({
nev: d.ad, bas: "01." + yil, bit: "12." + yil, makam: hit.noter, tarih: d.tarih, no: d.no,
aciklama: "", turAciklama: "", kaynak: "Luca defter dökümü"
}));
const cfg = edefFirma(t.firmaId, yil);
const beratlar = yilBeratlar(t.firmaId, yil);
if (cfg.mukellef) {
const ref = pDate(t.tarih) || pDate(t.teblig) || new Date();
const donemler = edefDonemler(t.firmaId, yil);
let secili;
if (t.defterTumEdef) secili = donemler;
else {
secili = donemler.filter(dn => { const sd = pDate(dn.son);
return (sd && sd >= ref) || beratlar["KEB|" + dn.bas] || beratlar["YEV|" + dn.bas]; });
if (!secili.length) secili = donemler.slice(-1); // tüm süreler geçtiyse son dönem yine de yazılır
}
secili.forEach(dn => {
const sd = pDate(dn.son);
const gelmedi = sd && sd >= ref;
["E-DEFTER DEFTERİ KEBİR", "E-DEFTER YEVMİYE DEFTERİ"].forEach(ad => {
const key = ad + "|" + dn.key;
const b = beratlar[(ad.includes("KEBİR") ? "KEB" : "YEV") + "|" + dn.bas];
if (b) { // berat zipinden okunan bilgiler
out.push({ nev: ad, bas: dn.bas, bit: dn.bit, makam: "GIB", tarih: b.tarih || "",
no: b.no, aciklama: "", turAciklama: "", kaynak: "berat zipinden okundu" });
return;
}
out.push({
nev: ad, bas: dn.bas, bit: dn.bit, makam: ".", tarih: "",
no: ((t.beratNo || {})[key]) || "",
aciklama: gelmedi ? "Süresi gelmediğinden verilmemiştir." : ((t.beratNo || {})[key] ? "" : ""),
turAciklama: "", key,
kaynak: `e-defter · berat son tarihi ${dn.son}` + (gelmedi ? " · süresi gelmedi" : " · süresi doldu, berat zipini yükleyin veya no girin")
});
});
});
}
return out;
}
/* Luca dökümünde kâğıt yevmiye/kebir tasdiki yoksa firma e-defter mükellefi kabul edilir */
function edefOtoTespit(t) {
const yil = yilOf(t); if (!yil) return;
const cfg = edefFirma(t.firmaId, yil);
if (cfg.elle) return; // kullanıcı elle ayarladıysa dokunma
const hit = defterFirmaEsle(yil, firmOf(t));
if (!hit) return;
const kagit = (hit.defterler || []).some(d => /YEVMİYE|KEBİR/.test(d.ad) && d.no);
cfg.mukellef = !kagit; cfg.oto = true;
}
function renderDefter() {
const t = curT(); if (!t) return;
edefOtoTespit(t);
const yil = yilOf(t);
const f0 = firmOf(t);
const dd = (state.defterDokum || {})[yil];
const d = $("dropDefter");
d.classList.toggle("okay", !!dd);
d.classList.toggle("kilitli", !yil);
d.innerHTML = !yil
? `Önce tutanak dönemini girindefter dökümü yalnızca tutanağın dönemine yüklenebilir`
: dd
? `${esc(yil)} defter dökümü sistemde${dd.rows.length} firma · ${esc(dd.yuklenme)} · değiştirmek için tıklayın`
: `Luca Defter Döküm Listesi (.xls / .zip)${yil} yılı dökümü yüklenmemiş — tüm firmaları kapsar`;
const brt = yilBeratlar(t.firmaId, yil);
const nBrt = Object.keys(brt).length;
const db = $("dropBerat");
db.classList.toggle("okay", !!nBrt);
db.classList.toggle("kilitli", !yil);
db.innerHTML = !yil
? `Önce tutanak dönemini girinberatlar yalnızca tutanağın dönemine yüklenebilir`
: nBrt
? `${esc(yil)} için ${nBrt} berat kayıtlıbilgiler alındı, zip'ler saklanmadı · eklemek için tıklayın`
: `${esc(yil)} e-Defter Berat Zipleri (.zip)GİB'den indirilen GIB…KB / GIB…YB ziplerini (toplu) bırakın — açılmış .xml de olur`;
const hit = yil ? defterFirmaEsle(yil, f0) : null;
const cfg = yil ? edefFirma(t.firmaId, yil) : { mukellef: false };
if (!yil) {
$("defterInfo").innerHTML = `
⛔ Tutanak dönemi girilmemiş. Yasal defter bilgileri her zaman tutanağın dönemine göre işlenir; dönem girilmeden defter dökümü veya e-Defter beratı yüklenemez.
`;
return;
}
$("kurumInfo").innerHTML = k
? (k.hata ? `
⚠️ ${esc(k.hata)} — ortakları elle girin.
`
: `
✓ ${esc(ky)} beyannamesinden ${k.ortaklar.length} kayıt okundu${onceki ? ` (bir önceki yıl — ${esc(String(y))} beyannamesi henüz verilmemiş olabilir)` : ""}. Vergi dairesi beyannamede yer almaz — kontrol edip düzeltin.
`)
: `
${y ? `${parseInt(y, 10) - 1} veya ${y}` : "İlgili yıl"} kurumlar vergisi beyannamesini yükleyin ya da ortakları aşağıdan elle girin — beyanname zorunlu değildir.
Pay oranı toplamı: ${rows.reduce((a, o) => a + (parseFloat(String(o.pay).replace(",", ".")) || 0), 0).toLocaleString("tr-TR")}
`;
}
function oEdit(i, k, v) { const t = curT(); t.ortaklar[i][k] = v; if (k === "vd") t.ortaklar[i].vdVarsayilan = false; persist(); }
function addOrtak() { const t = curT(); t.ortaklar = t.ortaklar || []; t.ortaklar.push({ unvan: "", no: "", il: "", vd: "", pay: "", aciklama: "" }); persist(); renderOrtak(); }
function delOrtak(i) { const t = curT(); t.ortaklar.splice(i, 1); persist(); renderOrtak(); }
/* ---- 6) malın imalatçısı olan mükellef ---- */
function renderImalatci() {
const t = curT(); if (!t || !$("imalatciTable")) return;
const f0 = firmOf(t);
const govde = $("imalatciGovde");
if (!f0.imalatci) {
$("imalatciInfo").innerHTML = `
Bu bölüm yalnız imalatçı mükellefler için doldurulur.
${esc(f0.ad || "Firma")} imalatçı olarak işaretli değil — imalatçıysa
"Bu firma imalatçıdır" kutusunu işaretleyin.
`;
}
function imalEdit(i, k, v) { const t = curT(); t.imalatci[i][k] = v; persist(); }
function addImalatci() { const t = curT(); t.imalatci = t.imalatci || []; t.imalatci.push({ unvan: "", no: "", vd: "", sanayiNo: "", sanayiTarih: "", kapasiteNo: "", kapasiteTarih: "", aciklama: "" }); persist(); renderImalatci(); }
function delImalatci(i) { const t = curT(); t.imalatci.splice(i, 1); persist(); renderImalatci(); }
async function exportImalatci() {
const t = curT(); const f = firmOf(t);
if (!f.imalatci) { if (!TOPLU) toast("Bu firma imalatçı olarak işaretli değil — 6 no'lu bölüm imalatçılar içindir"); return; }
const rows = (t.imalatci || []).filter(r => r.unvan || r.no);
if (!rows.length) { if (!TOPLU) toast("İmalatçı bilgisi boş — tabloyu doldurun"); return; }
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet("Veriler");
ws.columns = [{ width: 30 }, { width: 16 }, { width: 24 }, { width: 20 }, { width: 20 }, { width: 20 }, { width: 20 }, { width: 24 }];
ws.addRow(["Adı Soyadı/Ünvanı", "Vergi/T.C. Kimlik Numarası", "Bağlı Olduğu Vergi Dairesi", "Sanayi Sicil Belgesi Numarası", "Sanayi Sicil Belgesi Tarihi", "Kapasite Raporu Numarası", "Kapasite Raporu Tarihi", "Açıklama"]);
for (let c = 1; c <= 8; c++) { const h = ws.getRow(1).getCell(c);
h.font = { bold: true }; h.border = EX_KENAR; h.numFmt = "@"; h.fill = EX_GRI;
h.alignment = { horizontal: "center", vertical: "middle", wrapText: true }; }
rows.forEach(r => {
const row = ws.addRow([r.unvan, r.no, r.vd, r.sanayiNo, r.sanayiTarih, r.kapasiteNo, r.kapasiteTarih, r.aciklama]);
for (let c = 1; c <= 8; c++) { const cell = row.getCell(c);
cell.border = EX_KENAR; cell.numFmt = "@";
cell.alignment = { horizontal: "center", vertical: "middle" }; }
});
await dl(wb, `6_KARSIT_INCELEMEYE_KONU_FATURADA_YER_ALAN_MALIN_IMALATCISI_OLAN_MUKELLEF_HAKKINDA_BILGILER_${f.vkn}_${fnDate(t)}.xlsx`);
toast("İmalatçı bilgileri Excel'i indirildi");
}
async function exportOrtak() {
const t = curT(); const f = firmOf(t);
const rows = (t.ortaklar || []).filter(o => o.unvan || o.no);
if (!rows.length) { toast("Ortak listesi boş"); return; }
const eksikVd = rows.filter(o => !o.vd).length;
if (eksikVd && !confirm(`${eksikVd} satırda vergi dairesi boş. Yine de indirilsin mi?`)) return;
const wb = await makeWb(
["Adı Soyadı/Ünvanı", "Vergi/T.C. Kimlik Numarası", "Bağlı Olduğu İl", "Bağlı Olduğu Vergi Dairesi", "Pay Oranı", "Açıklama"],
[30, 16, 16, 30, 16, 20],
rows.map(o => [o.unvan, o.no, o.il, o.vd, o.pay, o.aciklama || null]),
[], null);
await dl(wb, `1_MUKELLEFIN_ORTAKLIK_BILGILERI_${f.vkn}_${fnDate(t)}.xlsx`);
toast("Ortaklık bilgileri Excel'i indirildi");
}
/* ================================================================
EXCEL ÇIKTILARI (ExcelJS — şablonla birebir format)
================================================================ */
const THIN = { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" } };
function styleRow(ws, rn, moneyCols) {
ws.getRow(rn).eachCell({ includeEmpty: false }, (c, col) => {
c.border = THIN;
if (moneyCols.includes(col) && typeof c.value === "number") { c.numFmt = "#,##0.00"; c.alignment = { horizontal: "right", wrapText: true }; }
else { c.numFmt = "@"; c.alignment = { horizontal: "center" }; }
});
}
async function makeWb(headers, widths, rows, moneyCols, totalRow) {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet("Veriler");
ws.columns = widths.map(w => ({ width: w }));
ws.addRow(headers);
ws.getRow(1).eachCell(c => { c.font = { bold: true }; c.border = THIN; c.numFmt = "@";
c.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFD9E1F2" } };
c.alignment = { horizontal: "center", vertical: "middle", wrapText: true }; });
rows.forEach(r => ws.addRow(r));
if (totalRow) ws.addRow(totalRow);
for (let i = 2; i <= ws.rowCount; i++) styleRow(ws, i, moneyCols);
return wb;
}
let TOPLU = null; // "Tüm Excelleri İndir" sırasında dosyalar burada biriktirilir
async function dl(wb, name) {
const buf = await wb.xlsx.writeBuffer();
if (TOPLU) { TOPLU.push({ name, buf }); return; }
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([buf], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }));
a.download = name; a.click(); URL.revokeObjectURL(a.href);
}
/* ---- sıkıştırmasız zip yazıcı (xlsx zaten sıkışık) ---- */
const CRC_TABLO = (() => { const t = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1; t[n] = c >>> 0; } return t; })();
function crc32(u8) { let c = 0xFFFFFFFF; for (let i = 0; i < u8.length; i++) c = CRC_TABLO[(c ^ u8[i]) & 0xFF] ^ (c >>> 8); return (c ^ 0xFFFFFFFF) >>> 0; }
function zipYap(girdiler) { // [{ad, veri:Uint8Array}] → Uint8Array
const enc = new TextEncoder();
const parcalar = [], merkez = [];
let ofs = 0;
const d = new Date();
const zaman = ((d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1)) & 0xFFFF;
const tarih = (((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate()) & 0xFFFF;
for (const g of girdiler) {
const ad = enc.encode(g.ad), veri = g.veri, crc = crc32(veri);
const y = new DataView(new ArrayBuffer(30));
y.setUint32(0, 0x04034b50, true); y.setUint16(4, 20, true); y.setUint16(6, 0x0800, true);
y.setUint16(10, zaman, true); y.setUint16(12, tarih, true);
y.setUint32(14, crc, true); y.setUint32(18, veri.length, true); y.setUint32(22, veri.length, true);
y.setUint16(26, ad.length, true);
parcalar.push(new Uint8Array(y.buffer), ad, veri);
merkez.push({ ad, crc, boy: veri.length, ofs });
ofs += 30 + ad.length + veri.length;
}
const mBas = ofs;
for (const m of merkez) {
const c = new DataView(new ArrayBuffer(46));
c.setUint32(0, 0x02014b50, true); c.setUint16(4, 20, true); c.setUint16(6, 20, true); c.setUint16(8, 0x0800, true);
c.setUint16(12, zaman, true); c.setUint16(14, tarih, true);
c.setUint32(16, m.crc, true); c.setUint32(20, m.boy, true); c.setUint32(24, m.boy, true);
c.setUint16(28, m.ad.length, true);
c.setUint32(42, m.ofs, true);
parcalar.push(new Uint8Array(c.buffer), m.ad);
ofs += 46 + m.ad.length;
}
const e = new DataView(new ArrayBuffer(22));
e.setUint32(0, 0x06054b50, true); e.setUint16(8, merkez.length, true); e.setUint16(10, merkez.length, true);
e.setUint32(12, ofs - mBas, true); e.setUint32(16, mBas, true);
parcalar.push(new Uint8Array(e.buffer));
const cikti = new Uint8Array(parcalar.reduce((a, p) => a + p.length, 0));
let k = 0; for (const p of parcalar) { cikti.set(p, k); k += p.length; }
return cikti;
}
/* resmi ek listesindeki sıra ve numaralarla tüm excelleri tek zip'te indir */
async function exportHepsiZip() {
const t = curT(); const f = firmOf(t);
TOPLU = [];
let dosyalar;
try {
for (const fn of [exportOrtak, exportDefter, exportSales, exportMuhtasar, exportBeyan, exportImalatci, exportBuys]) {
try { await fn(); } catch (e) { console.warn("toplu indirme", e); }
}
} finally { dosyalar = TOPLU; TOPLU = null; }
if (!dosyalar.length) { toast("İndirilecek Excel yok — önce tabloları doldurun"); return; }
const klasor = `${f.vkn}_TUTANAK_EKLERI_${fnDate(t)}`;
const zip = zipYap(dosyalar.map(x => ({ ad: `${klasor}/${x.name}`, veri: new Uint8Array(x.buf) })));
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([zip], { type: "application/zip" }));
a.download = klasor + ".zip"; a.click(); URL.revokeObjectURL(a.href);
toast(`${dosyalar.length} Excel "${klasor}" klasörü olarak tek zip'te indirildi`);
}
function fnDate(t) { return (t.sonTarih || fDate(new Date())).replace(/\./g, "_"); }
/* resmi ek şablonlarının ortak stili */
const EX_GRI = { type: "pattern", pattern: "solid", fgColor: { argb: "FFC0C0C0" } };
const EX_KENAR = { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" } };
async function exportBuys() {
const t = curT(); const f = firmOf(t);
const rows = t.alis.filter(a => a.sec);
if (!rows.length) { toast("Seçili satır yok"); return; }
/* yeni şablon (29.07.2026): Matrah ve KDV ayrı sütun + gümrük beyannamesi sütunları */
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet("Veriler");
ws.columns = [{ width: 30 }, { width: 16 }, { width: 24 }, { width: 16 }, { width: 16 },
{ width: 20 }, { width: 30 }, { width: 30 }, { width: 21 }, { width: 24 }];
ws.addRow(["Adı Soyadı/Ünvanı", "Vergi/T.C. Kimlik Numarası", "Bağlı Olduğu Vergi Dairesi", "Faturanın Tarihi", "Faturanın Serisi", "Faturanın Numarası", "Faturanın Matrahı", "Faturanın KDV Tutarı", "Gümrük Beyannamesi/Serbest Bölge İşlem Formu Tarihi", "Gümrük Beyannamesi/Serbest Bölge İşlem Formu Tescil Numarası"]);
for (let c = 1; c <= 10; c++) { const h = ws.getRow(1).getCell(c);
h.font = { bold: true }; h.border = EX_KENAR; h.numFmt = "@"; h.fill = EX_GRI;
h.alignment = { horizontal: "center", vertical: "middle", wrapText: true }; }
rows.forEach(r => {
const row = ws.addRow([r.firma, r.vkn, r.vd, r.tarih, r.seri || "", r.numara, r.matrah, r.kdv, "", ""]);
for (let c = 1; c <= 10; c++) {
const cell = row.getCell(c);
cell.border = EX_KENAR;
if (c === 7 || c === 8) { cell.numFmt = "#,##0.00"; cell.alignment = { horizontal: "right", vertical: "middle", wrapText: true }; }
else { cell.numFmt = "@"; cell.alignment = { horizontal: "center", vertical: "middle" }; }
}
});
await dl(wb, `7_ILGILI_DONEMDE_SATILAN_MALLARIN_TEDARIK_EDILDIGI_FIRMALAR_HAKKINDA_BILGILER_${f.vkn}_${fnDate(t)}.xlsx`);
toast("Tedarik firmaları Excel'i indirildi");
}
/* ödeme şekli kodları (tutanak eki şablonu): 1 Nakit · 2 Banka · 3 Çek · 4 Senet · 5 C/H · 6 Diğer */
const ODEME_KOD = { "NAKİT": "1", "NAKIT": "1", "BANKA": "2", "ÇEK": "3", "CEK": "3", "SENET": "4", "C/H": "5", "DİĞER": "6", "DIGER": "6" };
const odemeKodu = v => { const s = String(v || "").trim(); return /^[1-6]$/.test(s) ? s : (ODEME_KOD[trU(s)] || "2"); };
const yevmiye5 = v => { const s = String(v == null ? "" : v).trim(); return /^\d+$/.test(s) ? s.padStart(5, "0") : s; };
async function exportSales() {
const t = curT(); const f = firmOf(t);
/* satırlar: gelen tutanağın 3. bölümündeki fatura listesi, AYNI SIRAYLA.
Defter kayıt tarihi / yevmiye / ödeme şekli muavinden eşleştirilerek doldurulur. */
const esle = new Map((t.satis || []).map(r => [String(r.numara), r]));
let kayitlar;
if (t.pdfFaturalar.length) {
kayitlar = t.pdfFaturalar.map(p => {
const r = esle.get(String(p.no)) || {};
return { tarih: p.tarih, numara: String(p.no), tutar: p.tutar, kdv: p.kdv,
kayit: r.kayit || "", yevmiye: yevmiye5(r.yevmiye), odeme: odemeKodu(r.odeme || t.odemeGenel) };
});
} else {
kayitlar = (t.satis || []).map(r => ({ tarih: r.tarih, numara: String((r.seri || "") + (r.numara || "")), tutar: r.tutar, kdv: r.kdv,
kayit: r.kayit || "", yevmiye: yevmiye5(r.yevmiye), odeme: odemeKodu(r.odeme) }));
}
if (!kayitlar.length) { toast("Fatura listesi boş — tutanak PDF'ini yükleyin veya \"İşle\"ye basın"); return; }
/* biçim: onaylı örnek dosyayla birebir (gri başlık + gri zebra + ince kenarlık) */
const GRI = { type: "pattern", pattern: "solid", fgColor: { argb: "FFC0C0C0" } };
const K = { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" } };
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet("Veriler");
ws.columns = [{ width: 16 }, { width: 13 }, { width: 20 }, { width: 30 }, { width: 16 },
{ width: 13 }, { width: 20 }, { width: 22 }, { width: 20 }, { width: 13 }];
ws.addRow(["Faturanın Tarihi", "Faturanın Serisi", "Faturanın Numarası", "Faturanın Tutarı (TL)", "K.D.V(TL)", "Defter Kayıt Tarihi", "Yevmiye Numarası", "Ödeme Şekli ve Ödemeye İlişkin Bilgiler", "Açıklama", "Hatalı Satır Açıklama"]);
ws.getRow(1).height = 45;
for (let c = 1; c <= 10; c++) { const h = ws.getRow(1).getCell(c);
h.font = { bold: true }; h.border = K; h.numFmt = "@"; h.fill = GRI;
h.alignment = { horizontal: "center", vertical: "middle", wrapText: true }; }
kayitlar.forEach((r, i) => {
const row = ws.addRow([r.tarih, "", r.numara, r.tutar, r.kdv, r.kayit, r.yevmiye, r.odeme, "", ""]);
for (let c = 1; c <= 10; c++) {
const cell = row.getCell(c);
cell.border = K;
if (i % 2 === 1) cell.fill = GRI;
if (c === 4 || c === 5) { cell.numFmt = "#,##0.00"; cell.alignment = { horizontal: "right", vertical: "middle", wrapText: true }; }
else { cell.numFmt = "@"; cell.alignment = { horizontal: "center", vertical: "middle" }; }
}
});
/* ikinci sayfa: ödeme şekli kod referansı */
const ws2 = wb.addWorksheet("Ödeme Şekli Referans");
ws2.columns = [{ width: 10 }, { width: 20 }];
ws2.addRow(["Kod", "Ödeme Şekli"]); ws2.getRow(1).height = 20.1;
[["1", "Nakit"], ["2", "Banka"], ["3", "Çek"], ["4", "Senet"], ["5", "C/H"], ["6", "Diğer"]].forEach(x => ws2.addRow(x));
for (let rI = 1; rI <= 7; rI++) for (let c = 1; c <= 2; c++) {
const cell = ws2.getRow(rI).getCell(c);
cell.border = K; cell.numFmt = "@";
cell.alignment = { horizontal: "center", vertical: "middle", wrapText: rI === 1 };
if (rI === 1) { cell.font = { bold: true }; cell.fill = GRI; }
}
await dl(wb, `3_MUKELLEFIN_KARSIT_INCELEMEYE_KONU_OLAN_FATURALARINA_ILISKIN_BILGILERI_${f.vkn}_${fnDate(t)}.xlsx`);
const eksik = t.pdfFaturalar.length ? kayitlar.filter(r => !r.kayit).length : 0;
toast("Fatura bilgileri Excel'i indirildi" + (t.pdfFaturalar.length ? ` · tutanaktaki ${kayitlar.length} fatura, tutanak sırasıyla` : "") + (eksik ? ` · ${eksik} satırın defter kaydı muavinde bulunamadı` : ""));
}
/* durum renkleri: dolgu renk TEK BAŞINA anlam taşımaz — her satırda "Durum" sütunu da yazılır */
const DURUM_ST = {
bad: { fill: "FFF8D7D7", font: "FF9B1C1C", txt: "⛔ HATALI — kontrol edin" },
warn: { fill: "FFFDF0CE", font: "FF8A5A08", txt: "⚠ KONTROL EDİN" },
ok: { fill: null, font: "FF1E2A3A", txt: "✔ Tutarlı" }
};
function boyaSatir(ws, rn, lvl, nCol) {
const s = DURUM_ST[lvl] || DURUM_ST.ok;
const row = ws.getRow(rn);
for (let c = 1; c <= nCol; c++) {
const cell = row.getCell(c);
cell.border = { top: { style: "hair", color: { argb: "FFD8DEE6" } }, bottom: { style: "hair", color: { argb: "FFD8DEE6" } } };
if (s.fill) cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: s.fill } };
cell.font = { color: { argb: s.font }, bold: lvl === "bad" };
cell.alignment = { vertical: "middle", wrapText: c === nCol };
}
}
function baslikSatiri(ws, nCol) {
const r = ws.getRow(1);
r.height = 22;
for (let c = 1; c <= nCol; c++) {
const cell = r.getCell(c);
cell.font = { bold: true, color: { argb: "FFFFFFFF" } };
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF0F2557" } };
cell.alignment = { horizontal: "center", vertical: "middle", wrapText: true };
cell.border = { bottom: { style: "thin", color: { argb: "FF0F2557" } } };
}
ws.views = [{ state: "frozen", ySplit: 1 }];
ws.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: nCol } };
}
async function exportKontrol() {
const t = curT(); const f = firmOf(t);
const wb = new ExcelJS.Workbook();
const w0 = wb.addWorksheet("Özet");
w0.columns = [{ width: 34 }, { width: 60 }];
const bad1 = t.alis.filter(r => r.level === "bad").length, warn1 = t.alis.filter(r => r.level === "warn").length;
const bad2 = t.satis.filter(r => r.level === "bad").length, warn2 = t.satis.filter(r => r.level === "warn").length;
[["KONTROL RAPORU", ""], ["Firmamız", f.ad], ["Karşı firma", t.mukellef || ""], ["Tutanak sayı / tarih", (t.sayi || "") + " · " + (t.tarih || "")],
["Dönem", (t.donemBas || "") + " – " + (t.donemBit || "")], ["Cevap son tarihi", t.sonTarih || ""], ["", ""],
["Alışlarda hatalı satır", bad1], ["Alışlarda kontrol gereken", warn1],
["Satışlarda hatalı satır", bad2], ["Satışlarda kontrol gereken", warn2], ["", ""],
["Renk açıklaması", "Kırmızı = hatalı/tutarsız · Sarı = kontrol edilmeli · Renksiz = tutarlı"]
].forEach(r => w0.addRow(r));
w0.getRow(1).font = { bold: true, size: 14, color: { argb: "FF0F2557" } };
w0.getColumn(1).font = { bold: true };
[8, 9, 10, 11].forEach((rn, i) => {
const lvl = (i % 2 === 0) ? "bad" : "warn";
const v = w0.getRow(rn).getCell(2).value;
if (v) { w0.getRow(rn).getCell(2).fill = { type: "pattern", pattern: "solid", fgColor: { argb: DURUM_ST[lvl].fill } };
w0.getRow(rn).getCell(2).font = { bold: true, color: { argb: DURUM_ST[lvl].font } }; }
});
const w1 = wb.addWorksheet("Alışlar Kontrol");
w1.columns = [{width:6},{width:34},{width:14},{width:22},{width:12},{width:8},{width:18},{width:16},{width:16},{width:16},{width:22},{width:60}];
w1.addRow(["Seç", "Ünvan", "VKN", "Vergi Dairesi", "Tarih", "Seri", "Numara", "KDV Dahil", "Matrah", "KDV", "Durum", "Kontrol Notu"]);
t.alis.forEach(r => {
const lvl = r.level || "ok";
w1.addRow([r.sec ? "✓" : "", r.firma, r.vkn, r.vd, r.tarih, r.seri, r.numara, r.kdvDahil, r.matrah, r.kdv, DURUM_ST[lvl].txt, r.kontrol]);
boyaSatir(w1, w1.rowCount, lvl, 12);
[8, 9, 10].forEach(c => w1.getRow(w1.rowCount).getCell(c).numFmt = "#,##0.00");
});
baslikSatiri(w1, 12);
const w2 = wb.addWorksheet("Satışlar Kontrol");
w2.columns = [{width:12},{width:8},{width:20},{width:14},{width:14},{width:12},{width:10},{width:12},{width:22},{width:55}];
w2.addRow(["Tarih", "Seri", "Numara", "Tutar", "KDV", "Kayıt Tarihi", "Yevmiye", "Ödeme", "Durum", "Kontrol Notu"]);
t.satis.forEach(r => {
const lvl = r.level || "ok";
w2.addRow([r.tarih, r.seri, r.numara, r.tutar, r.kdv, r.kayit, r.yevmiye, r.odeme, DURUM_ST[lvl].txt, r.kontrol]);
boyaSatir(w2, w2.rowCount, lvl, 10);
[4, 5].forEach(c => w2.getRow(w2.rowCount).getCell(c).numFmt = "#,##0.00");
});
baslikSatiri(w2, 10);
await dl(wb, `KONTROL_RAPORU_${(t.mukellefVkn || "tutanak")}_${fnDate(t)}.xlsx`);
toast(`Kontrol raporu indirildi · ${bad1 + bad2} hatalı, ${warn1 + warn2} kontrol gereken satır`);
}
/* ================================================================
KAYIT / YÜKLEME
================================================================ */
function snapshot() { return JSON.stringify(state, null, 1); }
function saveJson() {
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([snapshot()], { type: "application/json" }));
a.download = "quantum_kit_yedek_" + fDate(new Date()).replace(/\./g, "_") + ".json";
a.click(); URL.revokeObjectURL(a.href); toast("Yedek indirildi");
}
function loadJson(file) {
if (!file) return;
const r = new FileReader();
r.onload = async () => { try { state = JSON.parse(r.result); migrate();
await esitle(); await agirHidrasyon(); toast("Yedek yüklendi ve sunucuya kaydedildi"); go("home"); }
catch (e) { toast("JSON okunamadı: " + e.message); } };
r.readAsText(file);
}
async function fsConnect() { klasorSec(); }
/* ================================================================
YEDEK EKRANI
================================================================ */
const YEDEK_TUR = { oto: ["b-prep", "Otomatik"], elle: ["b-done", "Elle alınan"],
geri: ["b-wait", "Geri yükleme öncesi"] };
function kb(n) {
n = +n || 0;
return n > 1048576 ? (n / 1048576).toFixed(1).replace(".", ",") + " MB"
: n > 1024 ? Math.round(n / 1024) + " KB" : n + " B";
}
function yZaman(v) {
const d = new Date(String(v).includes("T") ? v : String(v).replace(" ", "T"));
if (isNaN(d)) return esc(v);
const fark = Math.floor((Date.now() - d.getTime()) / 60000);
const bagil = fark < 2 ? "az önce" : fark < 60 ? fark + " dk önce"
: fark < 1440 ? Math.floor(fark / 60) + " saat önce" : Math.floor(fark / 1440) + " gün önce";
return `${d.toLocaleDateString("tr-TR")} ${d.toLocaleTimeString("tr-TR", { hour: "2-digit", minute: "2-digit" })}
${bagil}`;
}
async function renderYedekler() {
const tb = $("yedekTablo")?.querySelector("tbody"); if (!tb) return;
let liste = [];
try {
liste = YEREL_MOD ? await yerelYedekListesi() : ((await api("yedekler")).yedekler || []);
} catch (e) { toast(e.message); return; }
const toplam = liste.reduce((a, x) => a + (+x.boyut || 0), 0);
$("yedekOzet").textContent = liste.length
? `${liste.length} yedek · ${kb(toplam)}` : "henüz yedek yok";
/* uyarı: son yedek çok eskiyse ya da hiç yoksa */
const son = liste[0];
const gun = son ? (Date.now() - new Date(String(son.olusturma).replace(" ", "T")).getTime()) / 86400000 : 999;
$("yedekUyari").innerHTML = !liste.length
? `
Henüz yedek alınmadı. Firma veya tutanak ekleyip
kaydettiğinizde ilk yedek kendiliğinden oluşur; dilerseniz "Şimdi Yedek Al" ile hemen bir kopya oluşturun.
`
: gun > 14
? `
En son yedek ${Math.floor(gun)} gün önce alınmış.
Bu süre içinde veri değişmemiş olabilir; yine de bir kopya almanız iyi olur.
`
: (YEREL_MOD && !KLASOR)
? `
Yedekler yalnızca bu tarayıcının içinde duruyor.
Tarayıcı verilerini silerseniz yedekler de gider. Yukarıdan bir klasör bağlarsanız her yedek
bilgisayarınıza da yazılır.