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.
Toplu Firma Ekle
Luca'dan indirdiğiniz Müşteri Listesi Excel'ini ya da aşağıdaki
şablonu doldurup buraya bırakın; firmalarınız tek seferde eklenir.
Excel dosyasını buraya sürükleyin
veya tıklayıp seçin — .xlsx / .xls / .csv
Luca kullanmıyorsanız şablonu indirip doldurun: Ünvan,
VKN / T.C. Kimlik No, Vergi Dairesi, İl. Yalnızca ünvan zorunludur.
🤖 Firmaları Lucadan Çek
Kit Robot Luca'ya girer, Yönetici › Müşteri İşlemleri › Müşteri Listesi'ni indirir.
Her firma için yalnızca Luca kısa adı, ünvan, VKN / T.C. no ve vergi dairesi alınır. Önce liste gösterilir;
hangi firmaların aktarılacağını ve hangilerinin pasif olacağını siz seçersiniz.
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
+
Yeni tutanak — hangi firma için?
Ünvan, VKN ya da Luca adıyla arayıp seçin
Enter ilk sonucu seçer · Esc kapatır
📅
Takvim hatırlatmaları
Açık tutanakların cevap son tarihinden 10 gün ve 1 gün önce, sabah 09:00'da kendi takviminize hatırlatma düşer.
Google hesabı bağlamadan kullanmak isteyenler için: takvim abonelik adresi (Outlook / Apple / Google)
🔒
Oturum süresi doldu
Çalışmanız bu sayfada duruyor, kaybolmadı. Devam etmek için şifrenizi girin.
Kaydedilmemiş değişiklikler giriş sonrası otomatik gönderilir
⛔
Yükleme reddedildi
Aşağıdaki dosyalar alınmadı
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 →
Quantum KİT·sürüm 1.5.49·10.11.2026
Firmalarımız
#
Ünvan
VKN / T.C. No
Vergi Dairesi
İl
Tutanak
Pasif yaptığınız firmalar silinmez; yalnızca bu listede ve yeni tutanak için firma seçiminde gizlenir. "Pasif firmaları göster" kutusuyla yeniden görünür, satırdaki düğmeyle tekrar aktif yapılır.
Liste ünvana göre A'dan Z'ye sıralıdır. Yeni firma eklediğinizde aynı ekranlar o firma için de çalışı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önemlerin tasdik makamına GIB, açıklamasına da e-Defter ve berat dosyasının GİB sistemine yüklenme süresinin tutanak tarihi itibarıyla dolmadığını belirten açıklama 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
1 · Yardımcı Programlar
🤖 Kit Robot — Luca robotu…
Luca'ya sizin bilgisayarınızda, gözünüzün önünde açılan bir Chrome penceresinde girer;
programın Luca'dan otomatik çekeceği bilgileri getirir. İndirdiğiniz dosya kullanıma hazırdır: adresiniz ve size özel
anahtar içine yazılmıştır, ayrıca bir şey girmeniz gerekmez.
ℹ️
Nasıl kullanılır? İnen KitRobot.cmd dosyasına çift tıklayın. Windows "bilinmeyen yayımcı" uyarısı verirse
Ek bilgi → Yine de çalıştır deyin. İlk açılışta gerekli bileşenleri kendisi kurar (1-2 dakika); pencerede
HAZIR yazınca robot çalışıyordur. Pencere açık kaldıkça robot bağlıdır, kapatınca durur.
Bilgisayarda Google Chrome kurulu olmalıdır. Dosya size özeldir — başkasıyla paylaşmayın.
Robot dosyası başkasının eline geçtiyse
Robot anahtarını yenilerseniz eski KitRobot.cmd dosyaları çalışmaz; robotu buradan yeniden indirmeniz gerekir.
2 · Luca Bağlantı Ayarları
Her kullanıcı kendi Luca giriş bilgilerini kaydeder. Parola ve 2FA anahtarı sunucuda
şifreli saklanır, ekranda hiçbir zaman açık gösterilmez; yalnızca sizin Kit Robot'unuza, Luca'ya giriş için verilir.
Bilgileri gir / değiştir
2FA anahtarı, Luca'da iki aşamalı doğrulamayı açarken karekodun altında gösterilen harf-rakam dizisidir
(telefondaki 6 haneli kod değil). Luca hesabında iki aşamalı doğrulama açık olmalıdır; kapalıysa Luca captcha sorar ve otomatik giriş yapılamaz.
Luca'da 2FA nasıl açılır?
🔐
Luca'da iki aşamalı doğrulamayı (2FA) açma ve anahtarı buraya kaydetme
Telefonunuza bir doğrulama uygulaması kurun (Google Authenticator, Microsoft Authenticator vb.). (Kurmasanız da olur.)
Luca'da yetkili ve sadece bu iş için kullanacağınız bir kullanıcı adı ve şifre belirleyiniz.
Luca'ya, yukarıya yazdığınız kullanıcı adıyla kendiniz giriş yapın. 2FA her Luca kullanıcısı için ayrı açılır;
başka kullanıcının anahtarı bu kullanıcıda çalışmaz.
Luca'nın giriş / kullanıcı ayarlarındaki iki aşamalı doğrulama bölümünü açıp etkinleştirin. Ekrana bir
karekod ve altında harf-rakamlardan oluşan bir Anahtar gelir.
Bu ekranı kapatmadan önce Anahtar'ı kopyalayın — Luca onu yalnızca kurulum sırasında gösterir, sonradan göremezsiniz.
Karekodu telefondaki uygulamaya okutun; uygulamanın ürettiği 6 haneli kodu Luca'ya yazıp kurulumu onaylayın.
Uygulama kurmadıysanız: önce Anahtar'ı buraya kaydedin, Anlık 2FA kodunu göster'deki 6 haneli kodu Luca'ya yazın.
Kopyaladığınız Anahtar'ı buradaki 2FA anahtarı alanına yapıştırıp Kaydet'e basın.
Anlık 2FA kodunu göster'e basın: çıkan kod telefondaki kodla aynı olmalı. Aynıysa Bağlantıyı Test Et'e geçin.
Anahtarı kaybettiyseniz ya da kodlar tutmuyorsa: Luca'da o kullanıcının 2FA'sını kapatıp yeniden açın;
yeni bir Anahtar verilir. Eski anahtar ve telefondaki eski kayıt geçersiz olur — karekodu yeniden okutun ve yeni Anahtar'ı buraya kaydedin.
Aynı Luca kullanıcısını başka bir programda da (ör. Quantum Asistan) kullanıyorsanız anahtarı orada da güncelleyin.
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
Dijital Vergi Dairesi şablonu (07.10.2026): her belge ayrı satır — sanayi sicil belgesi ve kapasite raporu için iki satır yazılır. Sistemde "Kapasite Raporu" belge türü kaldırıldığından kapasite raporu satırında tür "Diğer", açıklama "Kapasite Raporu" yazılır.
Adı Soyadı/Ünvanı
Vergi/T.C. Kimlik No
Bağlı Bulunduğu Sanayi Odası
Belge Tarihi
Belge Numarası
Belge Türü
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 · aylık ve 3 aylık beyannameler tanınır
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";
imalatciGoc(t); // eski imalatçı satırlarını yeni belge-satırı modeline çevirir (yedeği t.imalatciEski)
});
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" && uc !== "giris") { oturumDustu(); throw new Error("Oturum süresi doldu — tekrar giriş yapın"); }
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;
}
const kc = await api("kaydet", c);
if (kc && kc.google && kc.google.hata) toast("⚠ Google Takvim eşitlemesinde " + kc.google.hata + " hata — " + (kc.google.mesaj || ""));
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 = ""; }
});
/* ================================================================================
TAKVİM HATIRLATMALARI
· Abonelik: kullanıcıya özel gizli .ics adresi (api/takvim.php?k=…). Google / Outlook / Apple
Takvim bu adrese bir kez abone olur; açık tutanakların son tarihinden 10 ve 1 gün önce
hatırlatma otomatik düşer, gönderilenler kalkar. Google aboneliği ~12-24 saatte bir yeniler.
· Tek tık: tutanak ekranındaki "Takvime ekle" — Google Takvim şablon bağlantısı ya da .ics
dosyası (her iki hatırlatma birden; Outlook/Apple için).
================================================================================ */
const TAKVIM_GUN = [[10, "10 gün kaldı", "⏰"], [1, "YARIN son gün", "⚠"]];
function takvimOlaylari(t) {
const son = pDate(t.sonTarih); if (!son) return [];
const f = firmOf(t);
return TAKVIM_GUN.map(([gun, etiket, ikon]) => {
const g = new Date(son); g.setDate(g.getDate() - gun);
const ymd = g.getFullYear() + String(g.getMonth() + 1).padStart(2, "0") + String(g.getDate()).padStart(2, "0");
const aciklama = [`Firma: ${f.ad}`, t.mukellef ? `Karşı firma (mükellef): ${t.mukellef}` : "", t.sayi ? `Tutanak sayısı: ${t.sayi}` : "",
t.tarih ? `Tutanak tarihi: ${t.tarih}` : "", t.teblig ? `Tebliğ tarihi: ${t.teblig}` : "", `Cevap son tarihi: ${t.sonTarih}`, "", "Quantum KİT"].filter((x, i, a) => x !== "" || i === a.length - 2).join("\n");
return { gun, baslik: `${ikon} Karşıt inceleme — ${etiket}: ${f.ad}`, aciklama, bas: ymd + "T060000Z", bit: ymd + "T063000Z", uid: `qk-${t.id}-${gun}@quantumkit` };
});
}
function takvimGoogleUrl(o) {
const q = new URLSearchParams({ action: "TEMPLATE", text: o.baslik, dates: o.bas + "/" + o.bit, details: o.aciklama, ctz: "Europe/Istanbul" });
return "https://calendar.google.com/calendar/render?" + q.toString();
}
function icsKacis(v) { return String(v).replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\r?\n/g, "\\n"); }
function icsSatir(ad, deger) { // 75 oktet katlama
const enc = new TextEncoder(); let s = ad + ":" + deger, out = "", ilk = true;
while (s.length) {
let n = s.length; while (enc.encode(s.slice(0, n)).length > (ilk ? 75 : 74)) n--;
out += (ilk ? "" : " ") + s.slice(0, n) + "\r\n"; s = s.slice(n); ilk = false;
}
return out;
}
function takvimIcs(olaylar) {
const dtstamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
let x = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Quantum KIT//Karsit Inceleme Hatirlatma//TR\r\nCALSCALE:GREGORIAN\r\nMETHOD:PUBLISH\r\n";
olaylar.forEach(o => {
x += "BEGIN:VEVENT\r\n" + icsSatir("UID", o.uid) + `DTSTAMP:${dtstamp}\r\nDTSTART:${o.bas}\r\nDTEND:${o.bit}\r\n`
+ icsSatir("SUMMARY", icsKacis(o.baslik)) + icsSatir("DESCRIPTION", icsKacis(o.aciklama))
+ "BEGIN:VALARM\r\nACTION:DISPLAY\r\nTRIGGER:PT0M\r\n" + icsSatir("DESCRIPTION", icsKacis(o.baslik)) + "END:VALARM\r\nEND:VEVENT\r\n";
});
return x + "END:VCALENDAR\r\n";
}
/* tutanak ekranındaki küçük menü */
function takvimMenu(btn) {
const t = curT(); if (!t) return;
const ol = takvimOlaylari(t);
if (!ol.length) { toast("⚠ Önce cevap son tarihini girin (tebliğ tarihi girilince otomatik hesaplanır)"); return; }
let m = $("takvimMini");
if (!m) { m = document.createElement("div"); m.id = "takvimMini"; m.className = "kmenu"; m.style.cssText = "display:block;left:0;right:auto;top:calc(100% + 4px);min-width:300px;z-index:60"; document.body.appendChild(m); }
const r = btn.getBoundingClientRect();
m.style.position = "fixed"; m.style.left = r.left + "px"; m.style.top = (r.bottom + 4) + "px";
m.innerHTML = ol.map((o, i) => ``).join("")
+ ``
+ (YEREL_MOD ? "" : ``);
m.style.display = "block";
setTimeout(() => document.addEventListener("click", takvimMiniKapat, { once: true }), 0);
}
function takvimMiniKapat() { const m = $("takvimMini"); if (m) m.style.display = "none"; }
function takvimIcsIndir() {
const t = curT(); const f = firmOf(t);
const blob = new Blob([takvimIcs(takvimOlaylari(t))], { type: "text/calendar;charset=utf-8" });
const a = document.createElement("a"); a.href = URL.createObjectURL(blob);
a.download = `karsit-inceleme-${(f.vkn || "firma")}-${(t.sonTarih || "").replace(/\./g, "_")}.ics`; a.click(); URL.revokeObjectURL(a.href);
toast("Hatırlatma dosyası indirildi — takvim uygulamanızda açın");
}
/* abonelik penceresi */
async function takvimAc() {
if (YEREL_MOD) { toast("Takvim bağlantısı yalnızca sunucu kullanımında — tutanak ekranındaki \"Takvime ekle\" ile tek tek ekleyebilirsiniz"); return; }
$("takvimKatman").style.display = "grid";
$("googleGovde").innerHTML = `
Google Takvim bağlantısı henüz açılmadı. Yönetici, Google istemci bilgilerini girdiğinde burada "Google ile bağlan" düğmesi görünecek.${KULLANICI && KULLANICI.rol === "admin" ? ` Ayarları aç →` : ""}
`;
return;
}
if (!d.bagli) {
kutu.innerHTML = `
Google Takvim'inize bağlanın. Düğmeye basınca Google'ın izin ekranı açılır; kendi hesabınızı seçip izin verirsiniz. Sonrasında her açık tutanağın hatırlatması doğrudan takviminize yazılır, gönderilenler kaldırılır.
Hatırlatmalar her kayıtta kendiliğinden güncellenir; ayrıca her sabah topluca tazelenir. Etkinliği takvimden elle silerseniz bir sonraki eşitlemede yeniden oluşturulur.
`;
}
async function googleEsitle() {
try { const r = await api("google_esitle", {}); const s = r.sonuc || {};
toast(s.hata ? `⚠ Eşitleme: ${s.hata} hata — ${s.mesaj}` : `✓ Takvim eşitlendi: +${s.eklendi} ~${s.guncellendi} -${s.silindi}`); googleDurumCiz(); }
catch (e) { toast("Eşitlenemedi: " + e.message); }
}
async function googleKopar() {
if (!confirm("Google Takvim bağlantısı kaldırılsın mı? Takvimdeki Quantum KİT hatırlatmaları silinmez, ama artık güncellenmez.")) return;
try { await api("google_kopar", {}); toast("Bağlantı kaldırıldı"); googleDurumCiz(); } catch (e) { toast(e.message); }
}
/* Google'dan dönüş: ?google=ok|hata */
(function googleDonus() {
const q = new URLSearchParams(location.search); const g = q.get("google"); if (!g) return;
const m = q.get("m") || "";
history.replaceState(null, "", location.pathname);
setTimeout(() => {
if (g === "ok") { toast("✓ Google Takvim bağlandı: " + m); takvimAc(); }
else { toast("⛔ Google bağlantısı kurulamadı: " + m); hataGoster("Google Takvim bağlanamadı", [{ dosya: "Google ile bağlan", neden: esc(m) }], "Tekrar deneyin; sorun sürerse yöneticinize Google ayarlarını kontrol ettirin."); }
}, 1800);
})();
/* yönetici: istemci kimliği / gizli anahtar */
async function googleAyarCiz() {
const k = $("googleAyarKart"); if (!k || !KULLANICI || KULLANICI.rol !== "admin") return;
let a; try { a = await api("ayar_google"); } catch (e) { k.innerHTML = `
${esc(e.message)}
`; return; }
k.innerHTML = `
📅 Google Takvim bağlantısı ${a.istemci ? (a.gizliVar ? "ayarlı" : "gizli anahtar eksik") : "ayarlanmadı"}
Kullanıcıların "Google ile bağlan" diyebilmesi için Google Cloud'da bir OAuth istemcisi gerekir (kurulum kılavuzu paketteki GOOGLE-TAKVIM-KURULUM.md dosyasında). Yönlendirme adresi olarak şunu girin:
${esc(a.donusAdresi)}
Bu adres size özeldir; kimseyle paylaşmayın. Şu an takvime düşecek ${acik} açık tutanak var; yeni tutanaklar eklendikçe takvim kendiliğinden güncellenir, gönderilenler kalkar.
Bildirim için: sol listede takvimin adına gelin → ⋮ → Ayarlar → "Etkinlik bildirimleri" ekleyin (örn. 0 dakika önce).
Google abone takvimleri 12–24 saatte bir yeniler; yeni tutanak takvime ertesi gün düşer. Acil olanı tutanak ekranındaki "Takvime ekle" ile anında ekleyin.
2FA anahtarı kayıtlı değil — Luca captcha sorar ve otomatik giriş yapılamaz. Anahtarı aşağıya yazıp kaydedin.
`)
: `
Henüz Luca bilgisi kaydedilmedi. Aşağıdaki alanları doldurup Kaydet'e basın.
`;
$("lcParNot").textContent = d.var && d.parola ? "(boş = değiştirme)" : "";
$("lcTotpNot").textContent = d.var && d.totp ? "(boş = değiştirme)" : "";
if (formuDoldur) {
$("lcUye").value = ""; $("lcKul").value = ""; $("lcPar").value = ""; $("lcTotp").value = "";
$("lcUye").placeholder = d.var ? d.uye_no + " (boş = değiştirme)" : "";
$("lcKul").placeholder = d.var ? d.kullanici + " (boş = değiştirme)" : "";
$("lcPaket").value = d.paket === "2.0" ? "2.0" : "1";
if (d.is_suruyor && d.is_tur === "giris_testi" && !lucaTestSayac) lucaTestIzle(); // sayfa yenilendiyse süren testi izlemeye devam et
}
}
async function lucaKaydet() {
const g = { uye_no: $("lcUye").value.trim(), kullanici: $("lcKul").value.trim(), parola: $("lcPar").value,
totp: $("lcTotp").value.trim(), paket: $("lcPaket").value };
const var_ = LUCA_DURUM && LUCA_DURUM.var;
if (!var_ && (!g.uye_no || !g.kullanici || !g.parola)) { toast("Üye numarası, kullanıcı adı ve parola gerekli"); return; }
try {
const r = await lucaJson("lc_kaydet", g);
toast(r.uyari ? "Kaydedildi — " + r.uyari : "✓ Luca bilgileri kaydedildi");
await lucaDurumCiz(true);
} catch (e) { toast(e.message); }
}
async function lucaSil() {
if (!confirm("Kayıtlı Luca giriş bilgileriniz (üye no, kullanıcı adı, parola, 2FA anahtarı ve Luca oturumu) sunucudan silinsin mi?\n\nBu işlem geri alınamaz; yeniden kullanmak için bilgileri tekrar girmeniz gerekir.")) return;
try { await lucaJson("lc_sil"); toast("Luca bilgileri silindi"); $("lucaTotpKutu").innerHTML = ""; await lucaDurumCiz(true); }
catch (e) { toast(e.message); }
}
async function lucaOturumSil() {
try { await lucaJson("lc_oturum_sil"); toast("Kayıtlı Luca oturumu silindi — sonraki işlemde taze giriş yapılır"); lucaDurumCiz(false); }
catch (e) { toast(e.message); }
}
async function lucaTotpGoster() {
clearInterval(lucaTotpSayac);
const kutu = $("lucaTotpKutu");
let r;
try { r = await lucaJson("lc_totp_simdi"); } catch (e) { kutu.innerHTML = `
${esc(e.message)}
`; return; }
let kalan = r.kalan_sn;
const ciz = () => { kutu.innerHTML = `
Şu anki 2FA kodu: ${esc(r.kod)}· ${kalan} sn geçerli · telefonunuzdaki doğrulama uygulamasıyla aynı olmalı; farklıysa anahtar yanlış kaydedilmiştir.gizle
`; };
ciz();
lucaTotpSayac = setInterval(() => { kalan--; if (kalan <= 0 || !kisiselAcik()) { clearInterval(lucaTotpSayac); kutu.innerHTML = ""; return; } ciz(); }, 1000);
}
/* Ana sayfadaki "Luca Bağlantı Ayarları için Tıklayınız" → Kişisel › 2 · Luca Bağlantı Ayarları */
function lucaAyarGit() {
go("kisisel");
setTimeout(() => { const k = $("kisiselLuca"); if (k && k.style.display !== "none") k.scrollIntoView({ behavior: "smooth", block: "start" }); }, 150);
}
function lucaTotpYardim() {
const k = $("lcTotpYardim"); k.classList.toggle("hide");
if (!k.classList.contains("hide")) k.scrollIntoView({ block: "nearest", behavior: "smooth" });
}
async function lucaRobotIndir() {
try {
const { r, j } = await lucaApi("lc_robot_indir");
if (!r.ok) throw new Error(j.hata || ("Sunucu hatası (" + r.status + ")"));
const a = document.createElement("a");
a.href = URL.createObjectURL(await r.blob());
a.download = "KitRobot.cmd"; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 5000);
toast("KitRobot.cmd indirildi — çift tıklayıp çalıştırın; pencere HAZIR yazınca robot bağlıdır");
} catch (e) { toast("İndirilemedi: " + e.message); }
}
async function lucaJetonYenile() {
if (!confirm("Robot anahtarı yenilensin mi?\n\nDaha önce indirdiğiniz KitRobot.cmd dosyaları artık çalışmaz; robotu yeniden indirmeniz gerekir.")) return;
try { await lucaJson("lc_robot_jeton"); toast("Robot anahtarı yenilendi — robotu yeniden indirin"); lucaDurumCiz(false); }
catch (e) { toast(e.message); }
}
async function lucaTest() {
try { await lucaJson("lc_test"); }
catch (e) { $("lucaTestKutu").innerHTML = `
${esc(e.message)}
`; return; }
lucaTestIzle();
}
function lucaTestIzle() {
clearInterval(lucaTestSayac);
const kutu = $("lucaTestKutu"); const bas = Date.now(); let mesgul = false;
$("lcTestBtn").disabled = true;
const bitir = html => { clearInterval(lucaTestSayac); lucaTestSayac = null; $("lcTestBtn").disabled = false; kutu.innerHTML = html; lucaDurumCiz(false); };
const tur = async () => {
if (mesgul) return; mesgul = true;
try {
const s = await lucaJson("lc_test_sonuc");
if (s.bitti) {
bitir(s.tamam
? `
✓ Luca bağlantısı başarılı. Robot Luca'ya girdi${s.program_ekrani ? ", program ekranını açtı" : ""} ve güvenli çıkış yaptı.
`
: `
✗ Giriş yapılamadı. ${esc(s.hata || "Robot hata bildirdi.")}
`);
return;
}
const il = s.ilerleme || {}; const sn = Math.round((Date.now() - bas) / 1000);
kutu.innerHTML = `
Robot çalışıyor…${esc(il.metin || (s.is_bekliyor ? "Kit Robot'un işi alması bekleniyor…" : "Luca'ya giriliyor…"))}${sn} sn
${s.ekran ? `` : ""}
Luca yoğunsa robot birkaç dakika arayla yeniden dener; bu pencereyi açık bırakabilirsiniz.
`;
} catch (e) { /* geçici ağ hatası: sonraki turda yeniden denenir */ }
finally { mesgul = false; }
};
tur(); lucaTestSayac = setInterval(tur, 2000);
}
async function lucaDurdur() {
try { await lucaJson("lc_durdur"); } catch (e) { toast(e.message); return; }
clearInterval(lucaTestSayac); lucaTestSayac = null; $("lcTestBtn").disabled = false;
$("lucaTestKutu").innerHTML = `
Durduruldu. Robot açık Luca penceresini birkaç saniye içinde kapatır.
`;
}
/* ---------------- gezinme ---------------- */
function go(v) {
if (TUR_AD) turKapat(false);
["home", "firms", "arsiv", "stat", "edefter", "abonelik", "kisisel", "kullanicilar", "data", "tut"].forEach(x => $("view-" + x).classList.toggle("hide", x !== v));
["home", "firms", "arsiv", "stat", "edefter", "abonelik", "kisisel", "kullanicilar", "data"].forEach(x => $("nav-" + x)?.classList.toggle("act", x === v));
if (v === "kisisel") renderKisisel();
if (v === "home") renderHome();
if (v === "firms") renderFirms();
if (v === "arsiv") renderArsiv();
if (v === "stat") renderStat();
if (v === "edefter") renderEdefter();
if (v === "abonelik") renderAbonelik();
if (v === "kullanicilar") { renderKullanicilar(); renderOdemeler(); renderOnaylar(); renderYedekDurum(); renderEpostalar(); renderTanitim(); }
if (v === "data") { renderBoyut(); klasorGoster(); renderYedekler(); }
}
/* ================================================================
İSTATİSTİK & GRAFİKLER
Form seçimi: büyüklük karşılaştırması → bar (tek hue, sequential);
durum dağılımı → yığılmış çubuk + SABİT status paleti (renk tek başına
anlam taşımaz: her dilim ikon + etiket + sayı ile birlikte verilir),
ayrıca altta tam tablo görünümü mevcut.
================================================================ */
const VIZ = { s1: "#2a78d6", good: "#0ca30c", warn: "#fab219", crit: "#d03b3b", info: "#2a78d6",
ink: "#1e2a3a", ink2: "#64748b", grid: "#e7ecf2" };
let statSortKey = "sonTarih", statSortDir = 1;
function vizTip(e, txt) {
let el = $("vizTip");
if (!el) { el = document.createElement("div"); el.id = "vizTip"; document.body.appendChild(el); }
if (!txt) { el.style.opacity = 0; return; }
el.textContent = txt; el.style.opacity = 1;
el.style.left = Math.min(e.clientX + 14, innerWidth - el.offsetWidth - 12) + "px";
el.style.top = (e.clientY - 34) + "px";
}
/* sağ ucu 4px yuvarlatılmış yatay çubuk */
function barPath(x, y, w, h, r) {
r = Math.min(r, w, h / 2);
return `M${x},${y}h${Math.max(0, w - r)}a${r},${r} 0 0 1 ${r},${r}v${h - 2 * r}a${r},${r} 0 0 1 ${-r},${r}h${-Math.max(0, w - r)}z`;
}
/* üst ucu yuvarlatılmış dikey sütun */
function colPath(x, y, w, h, r) {
r = Math.min(r, w / 2, h);
return `M${x},${y + h}v${-(h - r)}a${r},${r} 0 0 1 ${r},${-r}h${w - 2 * r}a${r},${r} 0 0 1 ${r},${r}v${h - r}z`;
}
const esA = s => esc(s).replace(/'/g, "'");
function yatayBar(hedef, veri, opt) {
const box = $(hedef); if (!box) return;
if (!veri.length || !veri.some(d => d.v > 0)) { box.innerHTML = `
Gösterilecek kayıt yok.
`; return; }
const W = 760, L = opt && opt.label || 200, R = 58, bh = 22, gap = 12, T = 6;
const H = T + veri.length * (bh + gap);
const max = Math.max(...veri.map(d => d.v));
const sk = v => (v / max) * (W - L - R);
let s = ``;
box.querySelectorAll("[data-tip]").forEach(el => {
el.onmousemove = e => vizTip(e, el.dataset.tip);
el.onmouseleave = () => vizTip(null, "");
});
}
function sutunGrafik(hedef, veri) {
const box = $(hedef); if (!box) return;
if (!veri.some(d => d.v > 0)) { box.innerHTML = `
Bu seçim için tutanak yok.
`; return; }
const W = 760, H = 210, L = 34, B = 34, T = 16;
const max = Math.max(1, ...veri.map(d => d.v));
const bw = (W - L - 10) / veri.length, cw = Math.min(24, bw - 10);
let s = ``;
box.querySelectorAll("[data-tip]").forEach(el => {
el.onmousemove = e => vizTip(e, el.dataset.tip);
el.onmouseleave = () => vizTip(null, "");
});
}
function durumGrafik(hedef, veri, toplam) {
const box = $(hedef); if (!box) return;
if (!toplam) { box.innerHTML = `
Gösterilecek tutanak yok.
`; return; }
const W = 760, H = 46, gapPx = 2;
let x = 0, s = `` +
`
${q ? "Aramaya uyan firma yok." + (pasifSayi && !pasifGoster ? " (Pasif firmalar gizli.)" : "")
: tumu.length ? "Tüm firmalar pasif — görmek için Pasif firmaları göster kutusunu işaretleyin."
: "Henüz firma yok — + Firma Ekle, ⇪ Toplu Firma Ekle ya da 🤖 Firmaları Lucadan Çek."}
${hatali.length} satırda vergi numarası 10 (T.C. kimlik 11) hane değil, alınmadı: ${hatali.slice(0, 6).map(esc).join(", ")}${hatali.length > 6 ? " …" : ""}
`;
h += `
Mevcut firmalarınıza dokunulmadı; yalnızca yeni kayıt eklendi. Eksik bilgileri firma kartından düzenleyebilirsiniz.
`;
$("topluSonuc").innerHTML = h;
toast(eklenen.length ? `${eklenen.length} firma eklendi ✓` : "Tüm firmalar zaten kayıtlıydı");
} catch (e) { console.error(e); topluMesaj("bad", "Dosya okunamadı: " + esc(e.message)); }
}
function topluMesaj(tur, m) { $("topluSonuc").innerHTML = `
${m}
`; }
/* ================= FİRMALARI LUCADAN ÇEK =================
Kit Robot Luca'ya girip Müşteri Listesi Excel'ini indirir (iş: musteri_excel); dosya sunucu
üzerinden buraya gelir ve TARAYICIDA okunur. Yalnız dört alan alınır: Luca kısa adı, ünvan,
VKN / T.C. no, vergi dairesi. Önce liste gösterilir; kullanıcı aktarılacakları ve pasifleri seçer.
Kayıtlı firmada yalnız BOŞ alanlar doldurulur, elle girilmiş bilgi ezilmez. */
const LF = { satirlar: [], sayac: null, durumSutunu: false, zaman: "", filtre: "hepsi" };
function lucaFirmaAc() {
if (YEREL_MOD) { toast("Luca'dan çekme yalnızca sunucu sürümünde çalışır (kit.quantumdenetim.com.tr)"); return; }
$("lucaFirmaKatman").style.display = "grid";
if (LF.sayac) return; // süren çekme: ilerleme zaten yazılıyor
if (LF.satirlar.length) { lucaFirmaYenidenEsle(); lucaFirmaListeCiz(); return; }
lucaFirmaHazirlik();
}
function lucaFirmaKapat() { $("lucaFirmaKatman").style.display = "none"; }
async function lucaFirmaHazirlik() {
const kutu = $("lfDurum");
$("lfListe").innerHTML = ""; $("lfOzet").textContent = ""; $("lfAktarBtn").style.display = "none";
kutu.innerHTML = `
Durum denetleniyor…
`;
let d;
try { d = await lucaJson("lc_durum"); } catch (e) { kutu.innerHTML = `
Luca giriş bilgileriniz ${d.var ? "eksik (2FA anahtarı yok)" : "kayıtlı değil"}.
Önce Kişisel → Luca Bağlantı Ayarları ekranından kaydedin.
`;
return;
}
if (d.is_suruyor) {
if (d.is_tur === "musteri_excel") { lucaFirmaIzle(); return; }
kutu.innerHTML = `
Şu anda başka bir Luca işi sürüyor. Bitince tekrar deneyin.
`;
return;
}
if (!d.robot_bagli) {
kutu.innerHTML = `
Kit Robot bu bilgisayarda açık değil.KitRobot.cmd dosyasına çift tıklayın;
pencere "HAZIR" yazınca aşağıdan yeniden denetleyin. Robotu henüz indirmediyseniz buradan indirebilirsiniz.
`;
return;
}
kutu.innerHTML = `
● Kit Robot bağlı. Başlatınca robot bu bilgisayarda Luca'yı açar, müşteri listesini indirir ve güvenli çıkış yapar
(1-2 dakika). Luca penceresine dokunmayın.
`; return; }
lucaFirmaIzle();
}
function lucaFirmaIzle() {
clearInterval(LF.sayac);
const kutu = $("lfDurum"); const bas = Date.now(); let mesgul = false;
$("lfListe").innerHTML = ""; $("lfOzet").textContent = ""; $("lfAktarBtn").style.display = "none";
const bitir = html => { clearInterval(LF.sayac); LF.sayac = null; kutu.innerHTML = html; };
const hataKutusu = m => `
✗ Liste alınamadı. ${esc(m)}
`;
const tur = async () => {
if (mesgul) return; mesgul = true;
try {
const s = await lucaJson("lc_is_sonuc");
if (s.tur && s.tur !== "musteri_excel") { bitir(hataKutusu("Araya başka bir Luca işi girdi.")); return; }
if (s.bitti) {
if (!s.tamam) { bitir(hataKutusu(s.hata || "Robot hata bildirdi.")); return; }
if (!s.kimlik) { bitir(hataKutusu("Robot Luca'ya girdi ama müşteri listesi dosyası gelmedi.")); return; }
bitir(`
Robot çalışıyor…${esc(il.metin || (s.is_bekliyor ? "Kit Robot'un işi alması bekleniyor…" : "Luca'ya giriliyor…"))}${sn} sn
${s.ekran ? `` : ""}
Luca yoğunsa robot birkaç dakika arayla yeniden dener. Bu pencereyi kapatabilirsiniz; işlem sürer.
`;
} catch (e) { /* geçici ağ hatası: sonraki turda yeniden denenir */ }
finally { mesgul = false; }
};
tur(); LF.sayac = setInterval(tur, 2000);
}
async function lucaFirmaDurdur() {
try { await lucaJson("lc_durdur"); } catch (e) { toast(e.message); return; }
clearInterval(LF.sayac); LF.sayac = null;
$("lfDurum").innerHTML = `
Durduruldu. Robot açık Luca penceresini birkaç saniye içinde kapatır.
`;
}
/* Luca'nın Excel'inde boyut (dimension) bilgisi eksik/yanlış olabiliyor: gerçek hücrelerden yeniden hesaplanır */
function sayfaAralikDuzelt(ws) {
let r0 = Infinity, c0 = Infinity, r1 = -1, c1 = -1;
for (const k in ws) {
if (k[0] === "!" || !/^[A-Z]+\d+$/.test(k)) continue;
const a = XLSX.utils.decode_cell(k);
if (a.r < r0) r0 = a.r; if (a.c < c0) c0 = a.c; if (a.r > r1) r1 = a.r; if (a.c > c1) c1 = a.c;
}
if (r1 >= 0) ws["!ref"] = XLSX.utils.encode_range({ s: { r: r0, c: c0 }, e: { r: r1, c: c1 } });
}
/* müşteri listesi → [{kisa, ad, vkn, vknHatali, vd, pasifLuca}] (yalnız dört alan + varsa durum) */
function lucaFirmaExcelCoz(buf) {
const wb = XLSX.read(buf, { type: "array" });
const metin = v => v == null ? "" : (typeof v === "number" ? (Number.isInteger(v) ? String(v) : v.toFixed(0)) : String(v)).replace(/\s+/g, " ").trim();
for (const sn of wb.SheetNames) {
const ws = wb.Sheets[sn]; sayfaAralikDuzelt(ws);
const rows = XLSX.utils.sheet_to_json(ws, { header: 1, raw: true, defval: "" });
for (let i = 0; i < Math.min(rows.length, 20); i++) {
const bas = (rows[i] || []).map(fbasNorm);
const c = fsutunBul(rows[i] || []);
if (!(c.ad >= 0 && (c.kisa >= 0 || c.vkn >= 0 || c.vd >= 0))) continue;
const tc = bas.findIndex(x => /K[İI]ML[İI]K/.test(x));
/* aktif/pasif bilgisi listede varsa okunur (sütun adı: Durum / Aktif / Pasif …) */
const dr = bas.findIndex(x => /^(DURUM|DURUMU|M[ÜU][ŞS]TER[İI] DURUMU|STAT[ÜU]|AKT[İI]F|PAS[İI]F|AKT[İI]F PAS[İI]F|AKT[İI]FL[İI]K)$/.test(x));
const pasifSutun = dr >= 0 && /^PAS[İI]F$/.test(bas[dr]);
const al = (r, k) => (k >= 0 && k < r.length ? metin(r[k]) : "");
const satirlar = [], gorulen = new Set();
rows.slice(i + 1).forEach(r => {
r = r || [];
const ad = al(r, c.ad);
if (!ad || /^TOPLAM$/i.test(ad)) return;
let vkn = al(r, c.vkn).replace(/\D/g, "");
/* şahıs firmasında Luca hem Vergi No hem TC Kimlik No tutar: ikisi de alınır (tckn ayrı alan) */
let tckn = al(r, tc).replace(/\D/g, "");
if (tckn.length !== 11 || tckn === vkn) tckn = "";
if (!vkn && tckn) { vkn = tckn; tckn = ""; } // yalnız T.C. varsa eskisi gibi tek numara
const vknHatali = !!vkn && vkn.length !== 10 && vkn.length !== 11;
const anahtar = (!vknHatali && vkn) || normName(ad);
if (gorulen.has(anahtar)) return; // Luca'da aynı firma iki kısa adla olabilir: ilki kalır
gorulen.add(anahtar);
const vd = al(r, c.vd).replace(/\s*(VERG[İI]\s*DA[İI]RES[İI].*|M[ÜU]D\.?|BA[ŞS]KANLI[ĞG]I)\s*$/i, "").trim();
let pasifLuca = null;
if (dr >= 0) {
const v = trU(al(r, dr));
if (v) pasifLuca = pasifSutun ? /^(E|EVET|1|X|TRUE|PAS[İI]F|VAR)$/.test(v)
: /PAS[İI]F|KAPALI|^(H|HAYIR|0|FALSE|YOK)$/.test(v);
}
satirlar.push({ kisa: c.kisa >= 0 && c.kisa !== c.ad ? al(r, c.kisa) : "", ad, vkn: vknHatali ? "" : vkn, tckn,
vknHatali: vknHatali ? vkn : "", vd, pasifLuca });
});
if (!satirlar.length) throw new Error("Müşteri listesinde firma satırı bulunamadı.");
return { satirlar, durumSutunu: dr >= 0 };
}
}
throw new Error("İnen dosya Luca müşteri listesine benzemiyor (Kısa Ad / Uzun Ad başlıkları bulunamadı).");
}
/* Luca satırı ↔ kayıtlı firma: önce VKN, sonra ünvan, sonra (VKN çelişmiyorsa) Luca kısa adı */
function lucaFirmaEsle(r) {
const rk = trU(r.kisa || "").trim();
return ((r.vkn || r.tckn) && state.firmalar.find(f => { const k = firmaKimlikleri(f); return (r.vkn && k.includes(r.vkn)) || (r.tckn && k.includes(r.tckn)); }))
|| state.firmalar.find(f => normName(f.ad) === normName(r.ad))
|| (rk && state.firmalar.find(f => f.lucaAd && trU(f.lucaAd).trim() === rk && !(f.vkn && r.vkn)))
|| null;
}
/* her satır için: kayıtlı mı, hangi boş alanlar dolacak; ilk okumada varsayılan seçim de kurulur */
function lucaFirmaYenidenEsle(ilk) {
LF.satirlar.forEach(r => {
const f = lucaFirmaEsle(r);
r.mevcut = f ? f.id : null;
r.eksik = !f ? [] : [["lucaAd", "Luca adı", r.kisa], ["vkn", "VKN", r.vkn], ["vd", "vergi dairesi", r.vd]]
.filter(x => !String(f[x[0]] || "").trim() && x[2]).map(x => x[1])
.concat(r.tckn && !firmaKimlikleri(f).includes(r.tckn) && String(f.vkn || "").replace(/\D/g, "").length !== 11 && !f.tckn ? ["T.C. kimlik no"] : [])
.concat(r.tckn && r.vkn && String(f.vkn || "").replace(/\D/g, "") === r.tckn && !f.tckn ? ["VKN (kartta T.C. yazılı)"] : []);
if (ilk) {
r.pasif = f ? !!f.pasif : r.pasifLuca === true;
if (f && r.pasifLuca !== null && r.pasifLuca !== !!f.pasif) r.pasif = r.pasifLuca; // Luca'daki durum değişmiş
r.sec = !f || r.eksik.length > 0 || r.pasif !== !!f.pasif;
} else if (f && !r.eksik.length && r.pasif === !!f.pasif) r.sec = false; // aktarıldı: işi kalmadı
});
}
function lucaFirmaGorunen() {
const q = trU(($("lfAra") || {}).value || "").trim();
return LF.satirlar.map((r, i) => ({ r, i })).filter(({ r }) =>
(LF.filtre === "hepsi" || (LF.filtre === "yeni" && !r.mevcut) || (LF.filtre === "kayitli" && r.mevcut) || (LF.filtre === "pasif" && r.pasif) || (LF.filtre === "aktif" && !r.pasif))
&& (!q || trU(r.ad).includes(q) || trU(r.kisa).includes(q) || r.vkn.includes(q)));
}
function lucaFirmaListeCiz() {
const s = LF.satirlar;
const yeni = s.filter(r => !r.mevcut).length;
const ara = ($("lfAra") || {}).value || "";
const gor = lucaFirmaGorunen();
const sec = k => ``;
$("lfListe").innerHTML = `
Luca'dan ${s.length} firma okundu (${esc(LF.zaman)}) · ${yeni} yeni · ${s.length - yeni} zaten kayıtlı.
${LF.durumSutunu ? "Aktif/pasif bilgisi Luca listesinden alındı." : "Luca listesinde aktif/pasif bilgisi yok — pasif olacakları Pasif sütunundan siz işaretleyin."}
Lucadan yeniden çek
Luca kısa adı
Ünvan
VKN / T.C. No
Vergi Dairesi
Durum
Pasif
`;
lucaFirmaGovdeCiz();
$("lfAktarBtn").style.display = "";
}
function lucaFirmaGovdeCiz() {
const gor = lucaFirmaGorunen();
const tb = $("lfTablo").querySelector("tbody");
tb.innerHTML = gor.length ? gor.map(({ r, i }) => `
Kit Robot bu bilgisayarda Luca'yı açar, firmayı seçer, belgeleri indirir ve güvenli çıkış yapar.
İnen dosyalar aşağıdaki kutulara ve Yasal Defterler bölümüne kendiliğinden işlenir. Luca penceresine dokunmayın.
`;
}
async function lucaBelgeBaslat() {
const t = curT(); if (!t) return;
const yil = yilOf(t); if (!yil) { donemUyar(); return; }
const istenen = [["cari", "lbCari"], ["muavin", "lbMuavin"], ["defter", "lbDefter"]].filter(x => $(x[1]).checked).map(x => x[0]);
if (!istenen.length) { toast("En az bir belge seçin"); return; }
const musteri = $("lbMusteri").value.trim();
if (!musteri && (istenen.includes("cari") || istenen.includes("muavin"))) {
toast("Luca'daki firma adını yazın — cari plan ve muavin seçili firmaya göre alınır"); $("lbMusteri").focus(); return;
}
const f = firmOf(t);
const sinif = LUCA_DEFTER_SINIF;
if (f && musteri && musteri !== (f.lucaAd || "")) { f.lucaAd = musteri; persist(); renderFirms(); } // firma kartına da yazılır
try { await lucaJson("lc_tutanak_cek", { musteri, yil, sinif, istenen }); }
catch (e) { $("lbDurum").innerHTML = `
⚠️ Tutanaktaki nezdinde inceleme yapılan mükellef${t.cUnvan ? " " + esc(t.cUnvan) + "" : ""} (${kimlikTurAd(t.cVkn)} ${esc(t.cVkn)}) seçili firmanızın numarasıyla (${esc(kartNo.join(" / "))}) uyuşmuyor — tutanak başka firmanıza ait olabilir!
`
: kd === "bilinmiyor"
? `
ℹ️ Tutanakta mükellef${t.cUnvan ? " " + esc(t.cUnvan) + "" : ""} için ${kimlikTurAd(t.cVkn)} ${esc(t.cVkn)} yazıyor; firma kartında ise ${kimlikTurAd(kartNo[0])} ${esc(kartNo[0])} kayıtlı.
Şahıs firmalarında ikisi de aynı kişiye aittir (biri T.C. kimlik no, biri vergi no) — bu bir hata değildir.
` : "")) + 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];
/* Tutanak sayısı rakam, harf, eğik çizgi ve tire içerebilir:
"183", "20260824-712-856", "KİT/2026/1164" — boşluk görünce (Tarih sütunu) durur. */
const SAYIBAS = "0-9A-Za-zÇĞİÖŞÜçğıöşü";
m = new RegExp("Sayı\\s*:?\\s*([" + SAYIBAS + "][" + SAYIBAS + "\\-\\/.]*)").exec(all);
if (m && !/^Tarih/i.test(m[1])) 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 --- */
/* ---- tutanak dönemi süzgeci: ay bazında [donemBas .. donemBit] ---- */
function donemAralik(t) {
const pm = x => { const m = /^(\d{2})\.(\d{4})$/.exec(x || ""); return m ? (+m[2]) * 12 + (+m[1]) : null; };
const b = pm(t && t.donemBas), e = pm(t && t.donemBit);
if (b === null || e === null) return null;
return { bas: Math.min(b, e), bit: Math.max(b, e) };
}
/* gg.aa.yyyy tarihi tutanak döneminin içinde mi?
Dönem ya da tarih okunamıyorsa eleme yapılmaz (veri kaybetmemek için). */
function donemdeMi(t, tarih) {
const r = donemAralik(t); if (!r) return true;
const m = /^(\d{1,2})\.(\d{2})\.(20\d{2})$/.exec(String(tarih || "").trim());
if (!m) return true;
const n = (+m[3]) * 12 + (+m[2]);
return n >= r.bas && n <= r.bit;
}
function computeBuys(nCand = 18, t) {
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;
if (t && !donemdeMi(t, e.tarih)) continue; // tutanak dönemi dışı alışlar listeye girmez
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, TT) {
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;
if (TT && !donemdeMi(TT, e.tarih)) continue; // tutanak dönemi dışı satışlar alınmaz
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);
t.satis = s.rows; t.odemeGenel = s.dominant;
t.alis = computeBuys(18, t);
// 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, wrongYil = 0; const yillar = new Set(); const reddedilen = [];
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;
const red = kimlikDenetle(f0, r.vkn, "e-Defter beratı");
if (red) { reddedilen.push({ dosya: file.name, neden: red }); 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 (reddedilen.length) msg += ` · ⛔ ${reddedilen.length} dosya reddedildi`;
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);
if (reddedilen.length) hataGoster("e-Defter beratı reddedildi", reddedilen);
}
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;
}
/* berat süresi henüz dolmamış e-defter dönemlerinin açıklaması (Excel'e tamamı yazılır) */
const EDEF_SURE_ACIKLAMA = "Mükellef e-Defter uygulamasına dahil olup ilgili döneme ilişkin e-Defter ve berat dosyasının oluşturulma ve imzalanması ile aynı sürede Gelir İdaresi Başkanlığı Bilgi İşlem Sistemine yüklenmesi için ilgili mevzuatta belirlenen süre iş bu yazı tarihi itibarıyla dolmadığından, GİB sistemine yüklenmemiştir.";
const edefNevMi = nev => /E-?\s*DEFTER/.test(trU(nev || ""));
/* nev'inde "E-Defter" geçen satırın tasdik makamı boşsa GIB'dir */
const defterMakam = r => (edefNevMi(r.nev) && (!r.makam || !String(r.makam).trim() || String(r.makam).trim() === ".")) ? "GIB" : r.makam;
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);
/* yalnızca tutanak döneminin aylarına ait e-defter dönemleri yazılır */
const ar = donemAralik(t);
const ayNo = x => { const m = /^(\d{2})\.(\d{4})$/.exec(x || ""); return m ? (+m[2]) * 12 + (+m[1]) : null; };
const donemde = dn => {
if (!ar) return true;
const b = ayNo(dn.bas), e = ayNo(dn.bit) ?? ayNo(dn.bas);
if (b === null) return true;
return b <= ar.bit && e >= ar.bas; // çakışan dönemler (çeyreklik tercihte de doğru)
};
let secili;
if (t.defterTumEdef) secili = donemler; // kutucuk: tüm yılı listele
else {
secili = donemler.filter(donemde);
if (!secili.length) { // dönem okunamadıysa eski davranışa düş
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);
}
}
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: "GIB", tarih: "",
no: ((t.beratNo || {})[key]) || "",
aciklama: gelmedi ? EDEF_SURE_ACIKLAMA : "",
turAciklama: "", key, gelmedi: !!gelmedi,
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 ----
Satır modeli (Dijital Vergi Dairesi şablonu, 07.10.2026): her belge ayrı satır.
{ unvan, no, oda, belgeTarih, belgeNo, belgeTur, aciklama }
Eski model (tek satırda sanayi sicil + kapasite raporu) imalatciGoc() ile kayıpsız dönüştürülür;
dönüşümden önceki satırlar t.imalatciEski içinde saklanır, hiçbir alan silinmez. */
const BELGE_SANAYI = "Sanayi Sicil Belgesi", BELGE_KAPASITE = "Kapasite Raporu", BELGE_DIGER = "Diğer";
function imalSatir(k, oda, tarih, no, tur) {
const r = { unvan: k.unvan || k.ad || "", no: k.no || k.vkn || "", oda: oda || "", belgeTarih: tarih || "", belgeNo: no || "", belgeTur: tur || "", aciklama: k.aciklama || "" };
imalKapasiteDuzelt(r);
return r;
}
/* GİB ekranında "Kapasite Raporu" belge türü kaldırıldı (11.2026): tür "Diğer" seçilir,
"Kapasite Raporu" açıklamaya yazılır. Satır yerinde düzeltilir; değiştiyse true döner. */
function imalKapasiteDuzelt(r) {
if (!r || !/^\s*KAPAS[İI]TE\s+RAPORU\s*$/.test(trU(r.belgeTur || ""))) return false;
r.belgeTur = BELGE_DIGER;
const a = (r.aciklama || "").trim();
r.aciklama = !a ? BELGE_KAPASITE : (/KAPAS[İI]TE/.test(trU(a)) ? a : BELGE_KAPASITE + " - " + a);
return true;
}
function imalKapasiteGoc(t) { let d = false; (t.imalatci || []).forEach(r => { if (imalKapasiteDuzelt(r)) d = true; }); return d; }
/* firma kartındaki belgelerden satır üret: belge yoksa boş bir satır */
function imalBelgeSatirlari(k, oda) {
const out = [];
if (k.sanayiNo || k.sanayiTarih) out.push(imalSatir(k, oda, k.sanayiTarih, k.sanayiNo, BELGE_SANAYI));
if (k.kapasiteNo || k.kapasiteTarih) out.push(imalSatir(k, oda, k.kapasiteTarih, k.kapasiteNo, BELGE_KAPASITE));
if (!out.length) out.push(imalSatir(k, oda, "", "", ""));
return out;
}
function imalatciGoc(t) {
const eski = t.imalatci || [];
if (!eski.length || eski.every(r => "belgeTur" in r)) return false;
t.imalatciEski = t.imalatciEski || JSON.parse(JSON.stringify(eski)); // kayıpsız yedek
const f0 = firmOf(t);
/* sanayi odası yalnız firmanın kendi satırına yazılır (VKN eşleşirse); başka imalatçının odası bilinmez */
t.imalatci = eski.flatMap(r => "belgeTur" in r ? [r] : imalBelgeSatirlari(r, (r.no && r.no === f0.vkn) ? f0.sanayiOdasi : ""));
return true;
}
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.
Adı Soyadı/ÜnvanıVergi/T.C. Kimlik NumarasıBağlı Olduğu Vergi DairesiFaturanın TarihiFaturanın SerisiFaturanın NumarasıFaturanın MatrahıFaturanın KDV TutarıGümrük Beyannamesi/Serbest Bölge İşlem Formu TarihiGümrük Beyannamesi/Serbest Bölge İşlem Formu Tescil Numarası
`,
s1son: ``
},
/* imalatci — resmi şablon: resmi.xlsx */
imalatci: {
basliklar: ["Adı Soyadı/Ünvanı", "Vergi/T.C. Kimlik Numarası", "Bağlı Bulunduğu Sanayi Odası", "Belge Tarihi", "Belge Numarası", "Belge Türü", "Açıklama"],
sutun: ["m3", "m3", "m5", "m3", "m3", "m3", "m5"],
sira: ["[Content_Types].xml", "_rels/.rels", "docProps/app.xml", "docProps/core.xml", "xl/sharedStrings.xml", "xl/styles.xml", "xl/workbook.xml", "xl/_rels/workbook.xml.rels", "xl/worksheets/sheet1.xml"],
parcalar: {
"[Content_Types].xml": ``,
"_rels/.rels": ``,
"docProps/app.xml": `
Apache POI`,
"docProps/core.xml": `{TARIH}Apache POI`,
"xl/sharedStrings.xml": `
`,
"xl/styles.xml": `
`,
"xl/workbook.xml": `
`,
"xl/_rels/workbook.xml.rels": ``,
},
s1bas: `
Adı Soyadı/ÜnvanıVergi/T.C. Kimlik NumarasıBağlı Bulunduğu Sanayi OdasıBelge TarihiBelge NumarasıBelge TürüAçıklama
`,
s1son: ``
},
};
const vdKac = s => String(s == null ? "" : s)
.replace(/&/g, "&").replace(//g, ">")
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
const vdSutunAd = i => { let s = ""; i++; while (i > 0) { const k = (i - 1) % 26; s = String.fromCharCode(65 + k) + s; i = (i - k - 1) / 26; } return s; };
/* boş hücre yalnızca biçim taşır () — boş metin DEĞİL (şablonun veri satırı böyle) */
function vdHucre(ref, kod, v) {
const tip = kod[0], st = kod.slice(1);
if (tip === "s") {
const n = typeof v === "number" ? v : (v === "" || v == null ? NaN : Number(String(v).replace(/\./g, "").replace(",", ".")));
if (!isFinite(n)) return ``;
return `${Math.round(n * 100) / 100}`;
}
const s = String(v == null ? "" : v).trim();
return s === "" ? ``
: `${vdKac(s)}`;
}
function vdSayfa1(sb, satirlar) {
const nCol = sb.sutun.length;
let x = sb.s1bas.replace("{DIM}", "A1:" + vdSutunAd(nCol - 1) + (satirlar.length + 1));
satirlar.forEach((r, i) => {
const n = i + 2;
x += `\n`;
for (let c = 0; c < nCol; c++) x += vdHucre(vdSutunAd(c) + n, sb.sutun[c], r[c]);
x += "\n";
});
return x + sb.s1son;
}
/* anahtar: VD_SABLON içindeki ek · satirlar: şablon sütun sırasıyla değer dizileri → xlsx baytları */
function vdKitap(anahtar, satirlar) {
const sb = VD_SABLON[anahtar];
if (!sb) throw new Error("Şablon tanımsız: " + anahtar);
const enc = new TextEncoder();
const zaman = new Date().toISOString().replace(/\.\d+Z$/, "Z");
const girdiler = sb.sira.map(ad => {
let metin = ad === "xl/worksheets/sheet1.xml" ? vdSayfa1(sb, satirlar) : sb.parcalar[ad];
if (ad === "docProps/core.xml") metin = metin.replace("{TARIH}", zaman);
return { ad, veri: enc.encode(metin) };
});
return zipYap(girdiler);
}
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.
Şablonda fatura numarası tek sütunda (C) tutuluyor, "Faturanın Serisi" boş bırakılır. */
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, seri: "", 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, seri: "", 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; }
const satirlar = kayitlar.map(r => [r.tarih, r.seri, r.numara, r.tutar, r.kdv, r.kayit, r.yevmiye, r.odeme, "", ""]);
dlBuf(vdKitap("fatura", satirlar), `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 · Dijital Vergi Dairesi şablonuyla birebir biçimde"
+ (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.
`;
}
async function yedekAlElle() {
const not = prompt("Bu yedek için kısa bir not (isteğe bağlı):", "elle alınan yedek");
if (not === null) return;
try {
if (YEREL_MOD) {
const id = await yerelYedekAl("elle", not || "elle alınan yedek");
if (!id) throw new Error("Yedeklenecek veri yok.");
} else {
await api("yedek-al", { not });
}
toast("Yedek alındı"); renderYedekler();
} catch (e) { toast(e.message); }
}
async function yedekGeri(id) {
const liste = YEREL_MOD ? await yerelYedekListesi() : ((await api("yedekler")).yedekler || []);
const y = liste.find(x => +x.id === +id); if (!y) { toast("Yedek bulunamadı"); return; }
const t = new Date(String(y.olusturma).replace(" ", "T")).toLocaleString("tr-TR");
if (!confirm(`${t} tarihli yedeğe dönülecek.\n\n` +
`Bu yedekte ${y.firma} firma ve ${y.tutanak} tutanak var.\n` +
`Şu anki veriniz bunun yerine geçecek — ama önce mevcut hâlin yedeği alınacak, ` +
`yani bu işlemi de geri alabilirsiniz.\n\nDevam edilsin mi?`)) return;
try {
if (YEREL_MOD) {
await yerelYedekAl("geri", "geri yükleme öncesi (" + t + " tarihli yedeğe dönüldü)");
const v = JSON.parse(y.veri);
state.firmalar = v.firmalar || []; state.tutanaklar = v.tutanaklar || [];
state.arsiv = v.arsiv || {}; state.defterDokum = v.defterDokum || {};
state.edefter = v.edefter || { takvim: {}, firma: {} };
migrate(); await esitle(); await agirHidrasyon();
} else {
await api("yedek-geri", { id: +id });
await sunucudanYukle();
}
toast(`Geri yüklendi: ${y.firma} firma, ${y.tutanak} tutanak`);
renderYedekler(); renderHome(); go("home");
} catch (e) { toast("Geri yüklenemedi: " + e.message); }
}
async function yedekIndir(id) {
try {
if (YEREL_MOD) {
const y = (await yerelYedekListesi()).find(x => +x.id === +id); if (!y) return;
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([y.veri], { type: "application/json" }));
a.download = "quantum_kit_yedek_" + y.olusturma.slice(0, 10) + ".json";
a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 4000);
} else {
window.open("api/api.php?p=yedek-indir&id=" + (+id), "_blank");
}
} catch (e) { toast(e.message); }
}
async function yedekSil(id) {
if (!confirm("Bu yedek kaydı silinsin mi? Verileriniz etkilenmez, yalnızca bu kopya silinir.")) return;
try {
if (YEREL_MOD) await LDB.sil("ayar", "yedek|" + id);
else await api("yedek-sil", { id: +id });
toast("Yedek silindi"); renderYedekler();
} catch (e) { toast(e.message); }
}
/* ================================================================
TARİH MASKESİ — 01042026 → 01.04.2026 · 042026 → 04.2026
================================================================ */
function maskeTipi(el) {
if (!el || el.tagName !== "INPUT") return null;
const p = (el.placeholder || "").toLowerCase();
if (el.classList.contains("mgun") || p === "gg.aa.yyyy") return "gun";
if (el.classList.contains("may") || p === "aa.yyyy") return "ay";
return null;
}
function tarihMaskele(el, tip) {
const sonda = el.selectionStart === el.value.length;
const d = el.value.replace(/\D/g, "").slice(0, tip === "ay" ? 6 : 8);
let out;
if (tip === "ay") out = d.length > 2 ? d.slice(0, 2) + "." + d.slice(2) : d;
else out = d.length > 4 ? d.slice(0, 2) + "." + d.slice(2, 4) + "." + d.slice(4)
: d.length > 2 ? d.slice(0, 2) + "." + d.slice(2) : d;
if (out !== el.value) {
el.value = out;
if (sonda) try { el.setSelectionRange(out.length, out.length); } catch (e) {}
}
}
/* 1.4.26 · 1/4/2026 · 01042026 → 01.04.2026 */
function tarihDuzelt(el, tip) {
let v = el.value.trim(); if (!v) return;
const sad = v.replace(/\D/g, "");
let g, a, y;
if (tip === "ay") {
let m = /^(\d{1,2})[.\/-](\d{2,4})$/.exec(v);
if (m) { a = m[1]; y = m[2]; }
else if (sad.length === 6) { a = sad.slice(0, 2); y = sad.slice(2); }
else if (sad.length === 5) { a = sad.slice(0, 1); y = sad.slice(1); }
else return;
if (y.length === 2) y = "20" + y;
if (+a < 1 || +a > 12 || y.length !== 4) return;
el.value = String(a).padStart(2, "0") + "." + y;
return;
}
let m = /^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})$/.exec(v);
if (m) { g = m[1]; a = m[2]; y = m[3]; }
else if (sad.length === 8) { g = sad.slice(0, 2); a = sad.slice(2, 4); y = sad.slice(4); }
else if (sad.length === 6) { g = sad.slice(0, 2); a = sad.slice(2, 4); y = "20" + sad.slice(4); }
else return;
if (y.length === 2) y = "20" + y;
if (+g < 1 || +g > 31 || +a < 1 || +a > 12 || y.length !== 4) return;
el.value = String(g).padStart(2, "0") + "." + String(a).padStart(2, "0") + "." + y;
}
document.addEventListener("input", e => {
const el = e.target, tip = maskeTipi(el); if (!tip) return;
// kullanıcı ayıracı KENDİ yazdıysa (ya da öyle bir metin yapıştırdıysa) araya girme
if (e.data && /[.\/-]/.test(e.data)) el.dataset.sep = "1";
if (e.inputType === "insertFromPaste" && /[.\/-]/.test(el.value)) el.dataset.sep = "1";
if (!el.value) delete el.dataset.sep;
if (el.dataset.sep) return;
tarihMaskele(el, tip);
});
/* change'i YAKALAMA aşamasında düzelt: alanın kendi onchange'i düzeltilmiş değeri görsün */
document.addEventListener("change", e => { const tip = maskeTipi(e.target); if (tip) { tarihDuzelt(e.target, tip); delete e.target.dataset.sep; } }, true);
document.addEventListener("blur", e => { const tip = maskeTipi(e.target); if (tip) { tarihDuzelt(e.target, tip); delete e.target.dataset.sep; } }, true);
/* ---------------- başlat ---------------- */
bindDrop("dropPdf", "pdfIn", handlePdf);
bindDrop("dropPlan", "planIn", f => handlePlan(f));
bindDrop("dropMuavin", "muavinIn", f => handleMuavin(f));
bindDrop("dropKurum", "kurumIn", f => handleKurumlar(f));
bindDrop("dropToplu", "topluIn", f => handleTopluFirma(f));
/* beyan alanı: çoklu dosya kabul eder */
(() => {
const d = $("dropBeyan"), i = $("beyanIn");
i.onchange = () => i.files.length && handleBeyanFiles([...i.files]);
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.length && handleBeyanFiles([...e.dataTransfer.files]); };
})();
(() => {
const d = $("dropMuh"), i = $("muhIn");
i.onchange = () => i.files.length && handleMuhtasarFiles([...i.files]);
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.length && handleMuhtasarFiles([...e.dataTransfer.files]); };
})();
/* arşiv ekranı yükleme girdileri */
["arsivKdvIn", "arsivMuavinIn", "arsivPlanIn", "arsivKurumIn", "arsivMuhIn"].forEach(id => {
$(id).onchange = e => { if (e.target.files.length) arsivFiles(e.target.files); e.target.value = ""; };
});
$("genelDefterIn").onchange = e => { if (e.target.files[0]) handleDefterDokum(e.target.files[0], genelDefterYil); e.target.value = ""; };
bindDrop("dropDefter", "defterIn", f => handleDefterDokum(f));
/* berat alanı: çoklu zip/xml kabul eder */
(() => {
const d = $("dropBerat"), i = $("beratIn");
i.onchange = () => { if (i.files.length) handleBeratZips([...i.files]); i.value = ""; };
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.length && handleBeratZips([...e.dataTransfer.files]); };
})();
/* tutanak kartlarının sırası: resmi ek listesi (1 ortaklık … 7 tedarik) */
["cardOrtak", "cardDefter", "cardSales", "cardMuh", "cardBeyan", "cardImalatci", "cardBuys", "cardExport"].forEach(id => {
const el = $(id); if (el && el.parentElement) el.parentElement.appendChild(el);
});
/* 1-7 bölümleri: başlığa tıklayınca açılır/kapanır */
const BOLUMLER = ["cardOrtak", "cardDefter", "cardSales", "cardMuh", "cardBeyan", "cardImalatci", "cardBuys", "cardExport"];
BOLUMLER.forEach(id => {
const c = $(id); if (!c) return;
c.classList.add("bolum");
const g = document.createElement("div"); g.className = "bolumgovde";
[...c.children].forEach(ch => { if (ch.tagName !== "H2") g.appendChild(ch); });
c.appendChild(g);
const h = c.querySelector("h2");
const ilk = h.firstChild; // "6) Karşıt …" metni
const m = /^\s*(\d)\)\s*(.*)$/.exec(ilk.textContent);
if (m) {
ilk.textContent = m[2] + " ";
h.insertAdjacentHTML("afterbegin", `▶${m[1]}`);
} else h.insertAdjacentHTML("afterbegin", '▶');
h.insertAdjacentHTML("beforeend", ``);
h.onclick = () => { c.classList.toggle("acik"); };
});
function bolumAc(id) { const c = $(id); if (c) c.classList.add("acik"); }
/* başlıktaki küçük durum özetleri */
/* durum: ok = tamam (yesil), warn = eksik/islemde (amber), bad = hata/engel (kirmizi), bos = nötr */
function bolumDurum(id, metin, durum) {
const el = $("ozet-" + id), c = $(id);
if (el) {
el.textContent = metin;
el.classList.remove("var", "d-ok", "d-warn", "d-bad");
if (durum !== "bos") el.classList.add("d-" + durum);
}
if (c) {
c.classList.remove("d-ok", "d-warn", "d-bad", "d-bos");
c.classList.add("d-" + durum);
}
}
/* ====================================================================================
EKSİK KONTROLÜ
Vergi dairesi, ekleri eksik doldurulmuş dosyayı geri çeviriyor ("envanter defteriniz yok").
Bu liste her ekran çiziminde hesaplanır, Excel Çıktıları kartının üstünde gösterilir ve
toplu indirme / "Hazırlandı" öncesinde onay ister.
onem: "zorunlu" = vergi dairesinin geri çevireceği eksik · "uyari" = kontrol edilmeli
==================================================================================== */
function eksikListesi(t) {
const L = []; if (!t) return L;
const f0 = firmOf(t);
const ekle = (bolum, metin, onem, kart) => L.push({ bolum, metin, onem, kart });
const sirket = /^\d{10}$/.test(f0.vkn || ""); // 10 hane VKN = şirket, 11 hane = şahıs
const LTD = /LTD|L[İI]M[İI]TED/i.test(f0.ad || ""), AS = /A\.?\s?Ş\.?|ANON[İI]M/i.test(f0.ad || "");
/* --- tutanak bilgileri --- */
if (!t.sayi) ekle("Tutanak", "Tutanak sayısı boş", "zorunlu", "cardTutBilgi");
if (!t.tarih) ekle("Tutanak", "Tutanak tarihi boş", "zorunlu", "cardTutBilgi");
if (!t.donemBas || !t.donemBit) ekle("Tutanak", "Tutanak dönemi (başlangıç / bitiş ayı) boş", "zorunlu", "cardTutBilgi");
if (!t.teblig) ekle("Tutanak", "Tebliğ tarihi boş — cevap son tarihi hesaplanamaz", "uyari", "cardTutBilgi");
if (!f0.vkn) ekle("Firma", "Firma kartında VKN / T.C. no yok", "zorunlu", "cardTutBilgi");
if (!f0.vd) ekle("Firma", "Firma kartında vergi dairesi yok", "zorunlu", "cardTutBilgi");
if (firmaKimlikDurum(f0, t.cVkn) === "farkli") ekle("Tutanak", `Tutanaktaki mükellef numarası (${t.cVkn}) seçili firmayla uyuşmuyor`, "zorunlu", "cardTutBilgi");
/* --- 1) ortaklar --- */
const ort = (t.ortaklar || []).filter(o => o.unvan || o.no);
if (sirket && !ort.length) ekle("1 · Ortaklık", "Ortak listesi boş — kurumlar beyannamesini yükleyin ya da elle girin", "zorunlu", "cardOrtak");
const ortEksik = ort.filter(o => !o.no || !o.vd).length;
if (ortEksik) ekle("1 · Ortaklık", `${ortEksik} ortakta VKN/T.C. no ya da vergi dairesi boş`, "uyari", "cardOrtak");
/* --- 2) yasal defterler --- */
const yil = yilOf(t);
if (yil) {
const drows = defterRows(t).concat(t.defterEk || []);
const var_ = re => drows.some(r => re.test(trU(r.nev || "")) && (r.no || r.tarih));
if (!drows.length) ekle("2 · Defterler", "Yasal defter satırı yok — Luca defter dökümünü (ve e-Defter beratlarını) yükleyin", "zorunlu", "cardDefter");
else {
if (!var_(/ENVANTER/)) ekle("2 · Defterler", "ENVANTER DEFTERİ yok — e-Defter mükellefinde de envanter defteri noter tasdiklidir; Luca defter dökümünde yoksa satırı elle ekleyin", "zorunlu", "cardDefter");
if (!var_(/YEVM[İI]YE/)) ekle("2 · Defterler", "YEVMİYE DEFTERİ yok (tasdik ya da e-Defter beratı)", "zorunlu", "cardDefter");
if (!var_(/KEB[İI]R/)) ekle("2 · Defterler", "DEFTERİ KEBİR yok (tasdik ya da e-Defter beratı)", "zorunlu", "cardDefter");
if (sirket && !var_(/KARAR/)) ekle("2 · Defterler", (AS ? "YÖNETİM KURULU" : "MÜDÜRLER KURULU") + " KARAR DEFTERİ yok", "uyari", "cardDefter");
if (sirket && LTD && !var_(/PAY/)) ekle("2 · Defterler", "ORTAKLAR PAY DEFTERİ yok", "uyari", "cardDefter");
if (sirket && AS && !var_(/GENEL KURUL/)) ekle("2 · Defterler", "GENEL KURUL TOPLANTI VE MÜZAKERE DEFTERİ yok", "uyari", "cardDefter");
const beratYok = drows.filter(r => /E-DEFTER/.test(r.nev || "") && !r.no && !r.gelmedi && !/Süresi gelmediğinden|süre .* dolmadığından/.test(r.aciklama || ""));
if (beratYok.length) ekle("2 · Defterler", `${beratYok.length} e-Defter döneminin berat numarası boş (süresi dolmuş) — berat ziplerini yükleyin`, "zorunlu", "cardDefter");
}
}
/* --- 3) faturalar --- */
const fat = t.pdfFaturalar.length ? t.pdfFaturalar : (t.satis || []);
if (!fat.length) ekle("3 · Faturalar", "Fatura listesi boş — tutanak PDF'ini yükleyin ya da muavini işleyin", "zorunlu", "cardSales");
else if (t.pdfFaturalar.length) {
const esle = new Map((t.satis || []).map(r => [String(r.numara), r]));
const kayitsiz = t.pdfFaturalar.filter(p => { const r = esle.get(String(p.no)); return !r || !r.kayit; }).length;
if (kayitsiz) ekle("3 · Faturalar", `${kayitsiz} faturanın defter kayıt tarihi / yevmiye no'su boş — muavin yüklenmemiş ya da fatura muavinde yok`, "uyari", "cardSales");
}
/* --- 4) muhtasar --- */
const muh = A(t.firmaId).muh || {};
const aylar = muhAylar(t);
const muhYok = aylar.filter(d => { const s = muh[d]; const ov = (t.muhOverride || {})[d];
return (ov === undefined || ov === "") && !(s && s.sayi !== null && s.sayi !== undefined); });
if (muhYok.length) ekle("4 · Muhtasar", `Muhtasar beyannamesi eksik aylar: ${muhYok.join(", ")}`, "zorunlu", "cardMuh");
const muhVdYok = aylar.filter(d => !muhYok.includes(d) && !(((t.muhVd || {})[d]) || ((muh[d] || {}).vd))).length;
if (muhVdYok) ekle("4 · Muhtasar", `${muhVdYok} ayda beyannamenin verildiği vergi dairesi boş`, "uyari", "cardMuh");
/* --- 5) KDV --- */
if (t.donemBas && t.donemBit) {
const be = beyanEksik(t);
if (be.beyYok.length) ekle("5 · KDV", `KDV beyannamesi eksik dönemler: ${be.beyYok.join(", ")}`, "zorunlu", "cardBeyan");
if (be.tahYok && be.tahYok.length) ekle("5 · KDV", `KDV tahakkuk fişi eksik dönemler: ${be.tahYok.join(", ")}`, "zorunlu", "cardBeyan");
if (be.oncekiBeyYok) ekle("5 · KDV", `Önceki dönem (${be.oncekiDonem}) KDV beyannamesi yok`, "zorunlu", "cardBeyan");
if (be.oncekiTahYok) ekle("5 · KDV", `Önceki dönem (${be.oncekiDonem}) tahakkuk fişi yok`, "zorunlu", "cardBeyan");
}
/* --- 6) imalatçı --- */
if (f0.imalatci) {
const im = (t.imalatci || []).filter(r => r.unvan || r.no);
if (!im.length) ekle("6 · İmalatçı", "Firma imalatçı işaretli ama belge satırı yok", "zorunlu", "cardImalatci");
const bEksik = im.filter(r => !r.belgeNo || !r.belgeTarih || !r.belgeTur).length;
if (bEksik) ekle("6 · İmalatçı", `${bEksik} satırda belge no / tarihi / türü boş`, "zorunlu", "cardImalatci");
if (im.some(r => !r.oda)) ekle("6 · İmalatçı", "Bağlı bulunduğu sanayi odası boş", "uyari", "cardImalatci");
}
/* --- 7) tedarik --- */
const sec = (t.alis || []).filter(a => a.sec);
if (!sec.length) ekle("7 · Tedarik", "Seçili tedarik firması yok — hizmet işletmesi değilse muavini işleyip tedarikçileri seçin", "uyari", "cardBuys");
const tedEksik = sec.filter(a => !a.vkn || !a.vd).length;
if (tedEksik) ekle("7 · Tedarik", `${tedEksik} tedarik firmasında VKN ya da vergi dairesi boş`, "zorunlu", "cardBuys");
return L;
}
function eksikGit(kart) {
const c = $(kart); if (!c) return;
if (c.classList.contains("bolum")) c.classList.add("acik");
c.scrollIntoView({ behavior: "smooth", block: "start" });
c.classList.remove("vurgu"); void c.offsetWidth; c.classList.add("vurgu"); setTimeout(() => c.classList.remove("vurgu"), 3000);
}
function renderEksikler() {
const t = curT(); const el = $("eksikPanel"); if (!t || !el) return;
const L = eksikListesi(t);
const z = L.filter(x => x.onem === "zorunlu"), u = L.filter(x => x.onem === "uyari");
bolumDurum("cardExport", z.length ? `${z.length} eksik` : (u.length ? `${u.length} uyarı` : "hazır"), z.length ? "bad" : (u.length ? "warn" : "ok"));
/* tutanak başlığının altındaki şerit: iş ilerlemişken (faturalar hesaplanmış ya da durum ilerlemiş)
görünür — yeni açılmış, henüz doldurulmamış tutanakta gürültü yapmaz. Tam liste Excel Çıktıları
kartında her zaman durur; indirme ve "Hazırlandı" kapıları her durumda çalışır. */
const ileri = (t.satis || []).length || (t.alis || []).length || t.durum !== "Bekliyor";
const serit = $("eksikSeritTut");
if (serit) {
if (!ileri) serit.style.display = "none";
else if (z.length) {
serit.style.display = ""; serit.className = "alertbar";
serit.innerHTML = `⛔${z.length} eksik var — vergi dairesi bu haliyle geri çevirir: ${esc(z.slice(0, 3).map(x => x.metin.split(" — ")[0]).join(" · "))}${z.length > 3 ? " · …" : ""}`;
} else if (u.length) {
serit.style.display = ""; serit.className = "alertbar";
serit.innerHTML = `⚠${u.length} kontrol noktası var: ${esc(u.slice(0, 3).map(x => x.metin.split(" — ")[0]).join(" · "))}`;
} else serit.style.display = "none";
}
if (!L.length) { el.innerHTML = `
✓ Eksik kontrolü: tüm bölümler tam görünüyor. Yine de dosyaları göndermeden önce kontrol raporuna göz atın.
`; return; }
const satir = x => `
${esc(x.bolum)}${esc(x.metin)}
`;
el.innerHTML = `
${z.length ? `⛔ ${z.length} eksik var — vergi dairesi bu haliyle dosyayı geri çevirebilir` : `⚠ ${u.length} kontrol noktası`}
${z.length && u.length ? ` · ayrıca ${u.length} uyarı` : ""}
${z.map(satir).join("")}${u.map(satir).join("")}
`;
}
/* toplu indirme / hazırlandı öncesi: zorunlu eksikler varsa açıkça sor */
function eksikOnay(islem) {
const t = curT(); const z = eksikListesi(t).filter(x => x.onem === "zorunlu");
if (!z.length) return true;
return confirm(`⛔ ${z.length} ZORUNLU EKSİK var:\n\n${z.map(x => "• " + x.bolum + ": " + x.metin).join("\n")}\n\nVergi dairesi bu haliyle dosyayı geri çevirebilir. Yine de ${islem}?`);
}
function bolumOzetleri() {
const t = curT(); if (!t) return;
const f0 = firmOf(t);
const yaz = (id, n, birim) => bolumDurum(id, n ? `${n} ${birim}` : "eksik", n ? "ok" : "warn");
yaz("cardOrtak", (t.ortaklar || []).filter(o => o.unvan || o.no).length, "ortak");
if (!yilOf(t)) bolumDurum("cardDefter", "dönem girin", "bad");
else yaz("cardDefter", defterRows(t).concat(t.defterEk || []).length, "satır");
yaz("cardSales", (t.satis || []).length, "fatura");
yaz("cardBeyan", beyanRows(t).filter(r => r.bey).length, "dönem");
const muh = A(t.firmaId).muh || {};
yaz("cardMuh", beyanRows(t).filter(r => muh[r.donem]).length, "ay");
if (!f0.imalatci) bolumDurum("cardImalatci", "imalatçı değil", "bos");
else yaz("cardImalatci", (t.imalatci || []).filter(r => r.unvan || r.no).length, "satır");
yaz("cardBuys", (t.alis || []).filter(a => a.sec).length, "seçili");
renderEksikler();
}
/* ---------------- açılış akışı ---------------- */
(async function baslat() {
try {
const b = await api("ben");
KULLANICI = b.kullanici;
} catch (e) {
if (e && e.yerel) { // sunucu yok → çevrimdışı tek dosya kullanımı
YEREL_MOD = true;
KULLANICI = { id: 0, ad: "Yerel Kullanım", eposta: "", rol: "admin" };
} else { location.href = "giris.html"; return; }
}
kullaniciGoster();
if (!YEREL_MOD && (KULLANICI.sozlesmeEksik || []).length) { await sozPerdeAc(); return; }
await uygulamayiBaslat();
})();
async function uygulamayiBaslat() {
try { await klasorYukle(); } catch (e) {}
try { await sunucudanYukle(); }
catch (e) { toast("Veriler alınamadı: " + e.message); migrate(); }
if (!YEREL_MOD) { try { await acilKopyaKontrol(); } catch (e) { console.warn("acil kopya", e); } }
renderHome();
setTimeout(() => klasoreYedekDene(false), 4000);
if (KULLANICI.sifreDegistir) setTimeout(() => sifreDegistirAc(true), 600);
else setTimeout(() => turBaslat("ana"), 1100); // ilk kullanımda tanıtım turu
try { if (navigator.storage && navigator.storage.persist) await navigator.storage.persist(); } catch (e) {}
}