Kurumların kritik sistemlerindeki yönetici ve ayrıcalıklı kullanıcı hesaplarının erişim yetkilerini düzenli olarak kontrol etmek amacıyla geliştirilmiştir. Gereksiz, aşırı veya kullanılmayan yetkilerin tespit edilmesini sağlayarak yetkisiz erişim ve güvenlik risklerinin azaltılmasına yardımcı olur.
Aracın hedefi; yetki kontrollerini, risk değerlendirmelerini, onay süreçlerini ve düzeltici faaliyetleri tek bir form üzerinden kayıt altına alarak izlenebilirliği ve denetlenebilirliği artırmaktır. Excel ve PDF raporlama özellikleriyle kurumsal bilgi güvenliği yönetimini destekler.
Bir Not defteri açarak, aşağıdaki (Mavi satırlar) html kodu içine kaydedin. Not defteri uzantısını .txt yerine ".html" olarak değiştirip kaydedin. Ör: "Ayrıcalıklı Erişim Gözden Geçirme Formu.html" sayfayı kapatıp tekrar açtığınızda, web browser üzerinden sayfanız açılacaktır.
Sol tarafta görünen logo için, html dosyanız nerede ise, masaüstü veya oluşturulan bir klasör. Logonuzda aynı yerde olursa forma yansır. Logo ismi "logo.jpg" olmalı. Çift tıklayarak açtığınızda, browser üzerinde form açılışı gerçekleşir. Veya alanına 5 kez peş peşe tıklayarak çalışma esnasında logo değişimi de yapılabilir.
Başlık, başlığın altındaki, başlığın sağındaki ve alttaki yazıları, not defteri içinde "Ctrl + F" tuşları ile kelime yazarak bulabilir, ilgili metinleri kendinize göre uyarlayarak kaydederek kullanabilirsiniz. Başlık alanındaki "OLGUN BAŞKANLIĞI" alanına 5 kez peş peşe tıklayarak; Departman, Müdürlük veya Başkanlığınıza ait bilgiyi manuel girebilirsiniz.
Formun boş hali;
Formun dolu hali:
PDF / Yazdır
Dışarı Aktarılan Excel Verisi:
HTML Kod:
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Ayrıcalıklı Erişim Gözden Geçirme Formu</title>
<style>
:root{--bg:#f8fafc;--canvas:#eaf0f6;--white:#fff;--blue:#0284c7;--blue-light:#eff8ff;--line:#cbd5e1;--frame:#a8b9ca;--text:#1e293b;--muted:#60738b;--radius:6px}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:var(--canvas);color:var(--text);font:13px/1.48 'Segoe UI',Arial,sans-serif}
button,input,select,textarea{font:inherit}
button{cursor:pointer}
button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
[hidden]{display:none!important}
/* OLGUN UI2: dış ana çerçeve, tam ekran, sınırlandırılmamış genişlik */
.container{width:calc(100% - 12px);max-width:none;min-height:calc(100vh - 12px);min-height:calc(100dvh - 12px);margin:6px;padding:12px;display:flex;flex-direction:column;background:var(--bg);border:2px solid var(--frame);border-top:5px solid var(--blue);border-radius:8px;box-shadow:0 5px 22px rgba(15,23,42,.12)}
.header,.toolbar,.form-card{width:100%;min-width:0;border-radius:var(--radius);box-shadow:0 2px 7px rgba(15,23,42,.055)}
/* Bağımsız başlık paneli: logo solda, 2 satırlı başlık, kurum adı sağda */
.header{display:flex;align-items:center;gap:15px;min-height:94px;padding:15px 18px;background:#fff;border:1.5px solid #b9cbdc;border-left:5px solid var(--blue)}
.logo-action{appearance:none;display:flex;align-items:center;justify-content:center;flex:0 0 62px;width:62px;height:62px;padding:2px;border:1px solid #b9d8ee;border-radius:6px;background:#fafdff;cursor:pointer;touch-action:manipulation}
.logo-action:hover{background:#f0f9ff;border-color:#7dd3fc}
.logo-action .logo{width:56px;height:56px;max-width:100%;max-height:100%;object-fit:contain}
.logo-action .logo-fallback{width:56px;height:56px;display:flex;align-items:center;justify-content:center;background:#f0f9ff;border:1px dashed #93c5fd;border-radius:4px;color:var(--blue);font:bold 11px 'Segoe UI',Arial,sans-serif;letter-spacing:.65px}
.titles{flex:1 1 auto;min-width:0}
.titles h1{margin:0 0 5px;color:#163453;font-size:20px;font-weight:750;line-height:1.2;letter-spacing:-.2px}
.titles p{margin:0;color:#687b90;font-size:12px}
/* Başlık yazısından yalnızca 1px küçük, kalın; sağ kenar sabit, metin sola yayılır */
.brand{margin-left:auto;flex:0 1 auto;max-width:48%;min-width:0;text-align:right;overflow-wrap:anywhere;padding:7px 2px 7px 10px;color:#101e32;font-size:19px;font-weight:750;line-height:1.25;cursor:pointer;user-select:none;-webkit-user-select:none;touch-action:manipulation}
.brand[contenteditable=true]{min-width:160px;cursor:text;user-select:text;-webkit-user-select:text;outline:2px solid var(--blue);border-radius:4px;background:#f0f9ff;box-shadow:0 0 0 4px #e0f2fe}
/* Bağımsız işlem çubuğu / çerçevesi: HER ZAMAN BAŞLIĞIN ALTINDA, SOLDA */
.toolbar{display:flex;align-items:center;justify-content:flex-start;gap:10px;flex-wrap:wrap;margin:11px 0;padding:11px 14px;min-height:62px;background:#f0f8ff;border:1.5px solid #b7d3e8;border-left:4px solid #69b5e4}
.btn{min-height:37px;white-space:nowrap;padding:8px 14px;border:1px solid #cad7e4;border-radius:6px;font-weight:650;box-shadow:0 1px 3px rgba(15,23,42,.06)}
.btn-import{background:#dbeeff;border-color:#93c5fd;color:#075985}.btn-import:hover{background:#b9e1ff}
.btn-export{background:#dcfce7;border-color:#86efac;color:#166534}.btn-export:hover{background:#bbf7d0}
.btn-print{background:#e9edf2;border-color:#cbd5e1;color:#334155}.btn-print:hover{background:#d9e0e9}
.form-card{flex:1;display:flex;flex-direction:column;padding:12px 18px 15px;background:#fff;border:1.5px solid #c5d2df}
.form-card form{flex:1;display:flex;flex-direction:column;justify-content:space-evenly}
.form-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:6px 0 7px}
.form-head strong{color:#0c4a6e;font-size:13px}.form-head span{color:var(--muted);font-size:11px}
.group{border:0;margin:0;padding:12px 0 14px;min-width:0}.group+.group{border-top:1px solid #dfe8f0}
.group-title{display:flex;align-items:center;gap:9px;margin:0 0 13px;padding:8px 11px;background:#eaf4fc;border-left:4px solid var(--blue);border-radius:0 4px 4px 0;color:#1e4263;font-size:13px;font-weight:750;line-height:1.3}
.fields{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px 14px}
.field{min-width:0}.field.wide{grid-column:span 2}.field.full{grid-column:1/-1}
.field label{display:block;margin:0 0 5px;color:#3f5570;font-size:11.5px;font-weight:650}
.field input,.field select,.field textarea{display:block;width:100%;min-height:35px;padding:7px 9px;background:#fff;border:1px solid #ccd9e7;border-radius:5px;color:#16273d;outline:none}
.field textarea{resize:vertical;min-height:55px;overflow:hidden}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--blue);box-shadow:0 0 0 3px #e0f2fe}
.field input::placeholder,.field textarea::placeholder{color:#a0aec0}
.form-foot{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-top:2px;padding:12px 0 0;border-top:1px solid #e2e8f0;color:var(--muted);font-size:11px}
.form-foot strong{color:#475569}
.screen-page-number{align-self:flex-end;margin-top:7px;color:#64748b;font-size:11px;font-weight:600}
.file-only{position:absolute;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none}
.print-value{display:none}
.toast{position:fixed;right:18px;bottom:18px;z-index:100;background:#0f172a;color:#fff;border-radius:6px;box-shadow:0 8px 25px #0003;padding:11px 15px;font-size:12px;max-width:360px;display:none}.toast.error{background:#991b1b}
@media(max-width:1050px){.fields{grid-template-columns:repeat(2,minmax(0,1fr))}.brand{font-size:18px}.titles h1{font-size:19px}}
@media(max-width:680px){
.container{margin:4px;width:calc(100% - 8px);min-height:calc(100dvh - 8px);padding:8px}
.header{display:grid;grid-template-columns:46px minmax(0,1fr);column-gap:10px;row-gap:8px;padding:11px 12px}
.logo-action{grid-row:1;grid-column:1;width:45px;height:45px;flex-basis:45px}.logo-action .logo,.logo-action .logo-fallback{width:40px;height:40px}
.titles{grid-row:1;grid-column:2}.titles h1{font-size:16px}.titles p{font-size:10.5px}
.brand{grid-row:2;grid-column:1/-1;justify-self:end;max-width:100%;padding:4px 0;font-size:17px}
.toolbar{padding:10px;gap:6px}.btn{padding:8px 10px;font-size:11px}
.form-card{padding:10px 12px}.form-head{align-items:start}.form-head span{font-size:10px}
.fields{gap:10px}
}
@media(max-width:440px){.fields{grid-template-columns:1fr}.field.wide{grid-column:auto}.form-head{flex-direction:column;gap:3px}}
/* Yazdırmada gerçek sayfa numarası, A4 yatay, ekran temasına uygun renkler */
@page{size:A4 landscape;margin:10mm 10mm 15mm 10mm;@bottom-right{content:'Sayfa ' counter(page) ' / ' counter(pages);font:8pt Arial,sans-serif;color:#475569}}
@media print{
html,body{margin:0!important;padding:0!important;background:#fff!important;color:#152536;font:8.5pt/1.26 Arial,sans-serif;-webkit-print-color-adjust:exact;print-color-adjust:exact}
.container{display:block!important;width:100%!important;min-height:0!important;height:auto!important;max-width:none!important;margin:0!important;padding:6px!important;border:1px solid #a8b9ca!important;border-top:3px solid #0284c7!important;border-radius:4px!important;background:#f8fafc!important;box-shadow:none!important;overflow:visible!important}
.toolbar,.toast,.screen-page-number,.screen-only{display:none!important}
.header,.form-card{display:block;box-shadow:none!important;border:1px solid #c5d2df!important;background:#fff!important}
.header{display:flex!important;align-items:center;gap:8px;min-height:0;padding:7px 9px;margin-bottom:6px;border-left:4px solid #0284c7!important}
.logo-action{width:38px!important;height:38px!important;flex-basis:38px!important;padding:0!important;background:#fff!important;border:0!important}
.logo-action .logo,.logo-action .logo-fallback{width:38px!important;height:38px!important}
.titles{flex:1;min-width:0}.titles h1{font-size:12pt!important;margin-bottom:2px}.titles p{font-size:8pt}
.brand{margin-left:auto;font-size:11.5pt!important;max-width:48%;text-align:right;overflow-wrap:anywhere}
.brand[contenteditable=true]{outline:0;box-shadow:none;background:transparent}
.form-card{padding:7px 10px!important;min-height:0!important}
.form-card form{display:block!important}
.form-head{padding:1px 0 5px}.form-head strong{font-size:9pt}.form-head span{font-size:7.5pt}
.group{padding:5px 0 7px;break-inside:auto;page-break-inside:auto}.group+.group{border-top:1px solid #dce6ee}
.group-title{background:#eaf4fc!important;border-left:3px solid #0284c7!important;font-size:9pt;padding:5px 7px;margin-bottom:6px;break-after:avoid}
.fields{display:grid!important;grid-template-columns:repeat(4,minmax(0,1fr))!important;gap:7px 9px;overflow:visible!important}
.field{break-inside:auto;min-width:0}.field.wide{grid-column:span 2}.field.full{grid-column:1/-1}
.field label{font-size:7.5pt;margin-bottom:2px;break-after:avoid}
.field input,.field select,.field textarea{display:none!important}
.print-value{display:block;min-height:21px;padding:3px 5px;background:#fff!important;border:1px solid #ccd9e7;border-radius:3px;white-space:pre-wrap;overflow-wrap:anywhere;font-size:8pt;line-height:1.3;min-width:0;overflow:visible!important}
.field:has(textarea) .print-value{min-height:31px}
.form-foot{display:none!important}
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<button type="button" class="logo-action" id="logoTrigger" title="Logoyu değiştirmek için 5 kez art arda tıklayın" aria-label="Kurum logosu. Değiştirmek için 5 kez art arda tıklayın">
<!-- Logo src adresi, yükleme doğrulaması yapıldıktan sonra JavaScript ile atanır. -->
<img class="logo" id="companyLogo" alt="Kurum logosu" hidden>
<span class="logo-fallback" id="logoFallback" aria-hidden="true">LOGO</span>
</button>
<div class="titles"><h1>Ayrıcalıklı Erişim Gözden Geçirme Formu</h1><p>(Privileged Access Review & Recertification)</p></div>
<div class="brand" id="brand" tabindex="0" title="Kurum adını değiştirmek için 5 kez art arda tıklayın" aria-label="Kurum adı. Değiştirmek için 5 kez art arda tıklayın">OLGUN BAŞKANLIĞI</div>
</header>
<div class="toolbar" role="group" aria-label="Form işlemleri">
<button type="button" class="btn btn-import" id="excelImport">↥ Excel İçeri Aktar</button>
<button type="button" class="btn btn-export" id="excelExport">↧ Excel Dışarı Aktar</button>
<button type="button" class="btn btn-print" id="printButton">▤ PDF / Yazdır</button>
<input type="file" id="excelFile" class="file-only" accept=".xlsx,.xlsm" aria-label="Excel dosyası seç">
<input type="file" id="logoFile" class="file-only" accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp" aria-label="Kurum logosu seç">
</div>
<main class="form-card">
<div class="form-head"><strong>Yetki Gözden Geçirme Formu</strong><span>Bir form = bir ayrıcalıklı hesap / yetki değerlendirmesi</span></div>
<form id="reviewForm" autocomplete="off" novalidate>
<section class="group" aria-labelledby="group1"><h2 class="group-title" id="group1">01 · İnceleme Bilgileri</h2><div class="fields" id="fieldsReview"></div></section>
<section class="group" aria-labelledby="group2"><h2 class="group-title" id="group2">02 · Hesap ve Yetki</h2><div class="fields" id="fieldsAccess"></div></section>
<section class="group" aria-labelledby="group3"><h2 class="group-title" id="group3">03 · Güvenlik ve Uygunluk Kontrolü</h2><div class="fields" id="fieldsControl"></div></section>
<section class="group" aria-labelledby="group4"><h2 class="group-title" id="group4">04 · Karar, Aksiyon ve Onay</h2><div class="fields" id="fieldsAction"></div></section>
</form>
<div class="form-foot"><span><strong>Not:</strong> Erişim kaldırma/daraltma kararı, teknik uygulama ve doğrulama ayrı ayrı kaydedilmelidir.</span><span class="screen-only">Veriler otomatik kaydedilmez; saklamak için Excel'e aktarın.</span></div>
</main>
<div class="screen-page-number" aria-hidden="true">Sayfa 1 / 1</div>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script>
'use strict';
const $=id=>document.getElementById(id);
const F=(key,label,type='text',opts=[],span='',hint='')=>({key,label,type,opts,span,hint});
const checks=['','Evet','Hayır','Kısmen','Bilinmiyor'];
const groups=[
{target:'fieldsReview',fields:[
F('reviewNo','İnceleme No'),F('reviewDate','İnceleme Tarihi','date'),
F('reviewer','İncelemeyi Yapan'),F('unit','Birim / Departman')
]},
{target:'fieldsAccess',fields:[
F('account','Kullanıcı / Hesap Adı'),F('accountType','Hesap Türü','select',['','Yönetici','Servis / Sistem','Acil Durum','Üçüncü Taraf','Diğer']),
F('system','Sistem / Uygulama'),F('systemOwner','Sistem Sorumlusu'),
F('privilege','Ayrıcalıklı Yetki / Rol','text',[],'wide'),F('businessNeed','Yetki Gerekçesi','text',[],'wide'),
F('lastUsed','Son Kullanım Tarihi','date'),F('expiry','Yetki Bitiş Tarihi','date')
]},
{target:'fieldsControl',fields:[
F('isNeeded','Yetki Hâlâ Gerekli mi?','select',checks),F('leastPrivilege','En Az Ayrıcalık Uygun mu?','select',checks),
F('mfa','MFA / Ek Güvenlik Kontrolü','select',['','Aktif','Aktif Değil','Uygulanamaz','Bilinmiyor']),
F('risk','Risk Seviyesi','select',['','Düşük','Orta','Yüksek','Kritik']),
F('finding','Tespit / Değerlendirme','textarea',[],'full','Varsa uygunsuzluk veya gözlem yazınız...')
]},
{target:'fieldsAction',fields:[
F('decision','Gözden Geçirme Kararı','select',['','Yetkiyi Koru','Yetkiyi Daralt','Yetkiyi Kaldır','Geçici Askıya Al','İstisna / Risk Kabulü']),
F('actionStatus','Aksiyon Durumu','select',['','Aksiyon Gerekmiyor','Bekliyor','Devam Ediyor','Tamamlandı']),
F('decisionReason','Karar Gerekçesi','textarea',[],'wide'),F('action','Yapılacak / Yapılan İşlem','textarea',[],'wide'),
F('actionOwner','Aksiyon Sorumlusu'),F('dueDate','Hedef Tarih','date'),
F('verification','İşlem Doğrulaması','select',['','Bekliyor','Doğrulandı','Uygulanmadı','Gerekli Değil']),
F('approver','Sistem Sahibi / Onaylayan'),
F('approvalDate','Onay Tarihi','date'),F('nextReview','Sonraki Gözden Geçirme','date')
]}
];
const fields=groups.flatMap(g=>g.fields);
const fieldMap=new Map(fields.map(f=>[f.key,f]));
function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
function draw(){for(const g of groups){$(g.target).innerHTML=g.fields.map(f=>{
let attrs=`id="${f.key}" name="${f.key}" data-key="${f.key}"`;
let el=f.type==='select'?`<select ${attrs}>${f.opts.map(o=>`<option value="${esc(o)}">${esc(o||'Seçiniz...')}</option>`).join('')}</select>`:
f.type==='textarea'?`<textarea ${attrs} rows="2" placeholder="${esc(f.hint||'Açıklama giriniz...')}"></textarea>`:
`<input ${attrs} type="${f.type}" placeholder="${esc(f.hint)}">`;
return `<div class="field ${f.span}"><label for="${f.key}">${esc(f.label)}</label>${el}<div class="print-value" id="print-${f.key}" aria-hidden="true"></div></div>`}).join('')}}
function formData(){return Object.fromEntries(fields.map(f=>[f.key,$(f.key).value]))}
function fill(data){for(const f of fields){const val=String(data[f.key]??'');const el=$(f.key);if(el.tagName==='SELECT' && ![...el.options].some(o=>o.value===val))el.value='';else el.value=val;if(f.type==='textarea')resize(el)} }
function resize(el){if(!el||el.tagName!=='TEXTAREA')return;el.style.height='auto';el.style.height=Math.max(54,el.scrollHeight+2)+'px'}
function toast(message,error=false){const el=$('toast');el.textContent=message;el.className='toast'+(error?' error':'');el.style.display='block';clearTimeout(window.toastTimeout);window.toastTimeout=setTimeout(()=>el.style.display='none',3800)}
function download(blob,filename){const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=filename;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),2000)}
/* XLSX: gerçek Office Open XML dosyası, ek kütüphane gerekmez. Tek sayfa, dikey alan/değer düzeni. */
const utf8=s=>new TextEncoder().encode(s);
function xesc(s){return String(s??'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g,'')}
function excelCol(n){let str='';for(n++;n;n=Math.floor((n-1)/26))str=String.fromCharCode(65+(n-1)%26)+str;return str}
function excelSheet(rows){const max=Math.max(1,...rows.map(r=>r.length));const sheetData=rows.map((row,ri)=>`<row r="${ri+1}">${row.map((value,ci)=>{
const a=`${excelCol(ci)}${ri+1}`;if(value==null||value==='')return '';return `<c r="${a}" t="inlineStr" s="${ri===0?1:0}"><is><t xml:space="preserve">${xesc(value)}</t></is></c>`}).join('')}</row>`).join('');
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><dimension ref="A1:${excelCol(max-1)}${Math.max(1,rows.length)}"/><sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews><sheetFormatPr defaultRowHeight="16"/><cols>${Array.from({length:max},(_,i)=>`<col min="${i+1}" max="${i+1}" width="${i===0?26:20}" customWidth="1"/>`).join('')}</cols><sheetData>${sheetData}</sheetData>${rows.length>1?`<autoFilter ref="A1:${excelCol(max-1)}${rows.length}"/>`:''}</worksheet>`}
const crcTable=(()=>{const t=[];for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=c&1?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
function crc32(u){let c=0xffffffff;for(const b of u)c=crcTable[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
function makeZip(entries){const chunks=[],central=[];let offset=0;const add=u=>{chunks.push(u);offset+=u.length};
for(const [name,text] of Object.entries(entries)){const nameU=utf8(name),data=utf8(text),start=offset,crc=crc32(data);const local=new Uint8Array(30+nameU.length),v=new DataView(local.buffer);
v.setUint32(0,0x04034b50,true);v.setUint16(4,20,true);v.setUint16(6,0x800,true);v.setUint16(8,0,true);v.setUint32(14,crc,true);v.setUint32(18,data.length,true);v.setUint32(22,data.length,true);v.setUint16(26,nameU.length,true);local.set(nameU,30);add(local);add(data);
const c=new Uint8Array(46+nameU.length),dv=new DataView(c.buffer);dv.setUint32(0,0x02014b50,true);dv.setUint16(4,20,true);dv.setUint16(6,20,true);dv.setUint16(8,0x800,true);dv.setUint16(10,0,true);dv.setUint32(16,crc,true);dv.setUint32(20,data.length,true);dv.setUint32(24,data.length,true);dv.setUint16(28,nameU.length,true);dv.setUint32(42,start,true);c.set(nameU,46);central.push(c)}
const centralOffset=offset;central.forEach(add);const tail=new Uint8Array(22),end=new DataView(tail.buffer);end.setUint32(0,0x06054b50,true);end.setUint16(8,central.length,true);end.setUint16(10,central.length,true);end.setUint32(12,offset-centralOffset,true);end.setUint32(16,centralOffset,true);add(tail);return new Blob(chunks,{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})}
async function unzip(buffer){const view=new DataView(buffer),bytes=new Uint8Array(buffer),decoder=new TextDecoder();let eocd=-1;
for(let i=bytes.length-22;i>=Math.max(0,bytes.length-65557);i--)if(view.getUint32(i,true)===0x06054b50){eocd=i;break}
if(eocd<0)throw Error('Geçerli XLSX ZIP paketi bulunamadı.');
const n=view.getUint16(eocd+10,true),ptr0=view.getUint32(eocd+16,true),found={};let p=ptr0;
for(let i=0;i<n;i++){
if(view.getUint32(p,true)!==0x02014b50)throw Error('Excel ZIP dizini okunamadı.');
const method=view.getUint16(p+10,true),size=view.getUint32(p+20,true),nl=view.getUint16(p+28,true),extra=view.getUint16(p+30,true),comment=view.getUint16(p+32,true),loc=view.getUint32(p+42,true);
const name=decoder.decode(bytes.subarray(p+46,p+46+nl));p+=46+nl+extra+comment;
if(name.endsWith('/')||!(/\.xml$|\.rels$/.test(name)))continue;
if(view.getUint32(loc,true)!==0x04034b50)throw Error('Excel ZIP giriş başlığı hatalı.');
const dn=view.getUint16(loc+26,true),de=view.getUint16(loc+28,true),start=loc+30+dn+de;const raw=bytes.slice(start,start+size);let out;
if(method===0)out=raw;
else if(method===8&&typeof DecompressionStream!=='undefined'){
const stream=new Blob([raw]).stream().pipeThrough(new DecompressionStream('deflate-raw'));
out=new Uint8Array(await new Response(stream).arrayBuffer());
}else throw Error('Bu tarayıcı Excel sıkıştırma biçimini desteklemiyor.');
found[name]=decoder.decode(out);
}return found;
}
const parseXml=s=>{const xml=new DOMParser().parseFromString(s,'application/xml');if(xml.querySelector('parsererror'))throw Error('Excel XML içeriği bozuk.');return xml};
const descendants=(el,local)=>Array.from(el.getElementsByTagNameNS('*',local));
function excelRows(xml,strings){if(!xml)return [];const doc=parseXml(xml);const rows=[];for(const row of descendants(doc,'row')){
const cells=[];for(const c of Array.from(row.children).filter(x=>x.localName==='c')){
const ref=c.getAttribute('r')||'',letters=ref.match(/^[A-Z]+/i)?.[0]||'';let index=0;for(const ch of letters.toUpperCase())index=index*26+ch.charCodeAt(0)-64;index--;
if(index<0||index>1000)continue;const type=c.getAttribute('t');let val='';
if(type==='inlineStr')val=descendants(c,'t').map(x=>x.textContent).join('');
else {val=descendants(c,'v')[0]?.textContent||'';if(type==='s')val=strings[Number(val)]??''}
cells[index]=val;
}rows.push(cells)
}return rows;
}
function excelDate(v){if(!/^\d+(\.\d+)?$/.test(String(v)))return String(v||'');const n=Number(v);if(n<20000||n>100000)return String(v);const d=new Date(Date.UTC(1899,11,30)+n*86400000);return d.toISOString().slice(0,10)}
function createWorkbook(){
const rows=[['Alan','Değer'],...fields.map(f=>[f.label,$(f.key).value])];
const files={
'[Content_Types].xml':`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>`,
'_rels/.rels':`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`,
'xl/workbook.xml':`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="YetkiFormu" sheetId="1" r:id="rId1"/></sheets></workbook>`,
'xl/_rels/workbook.xml.rels':`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>`,
'xl/styles.xml':`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><color rgb="FFFFFFFF"/><sz val="11"/><name val="Calibri"/></font></fonts><fills count="3"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FF0284C7"/><bgColor indexed="64"/></patternFill></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="2"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1"/></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`,
'xl/worksheets/sheet1.xml':excelSheet(rows).replace(/<cols>.*?<\/cols>/,'<cols><col min="1" max="1" width="42" customWidth="1"/><col min="2" max="2" width="80" customWidth="1"/></cols>')
};
return makeZip(files);
}
function exportExcel(){try{download(createWorkbook(),'Ayricalikli_Erisim_Gozden_Gecirme_Formu.xlsx');toast('Excel formu dışarı aktarıldı.')}catch(err){console.error(err);toast('Excel oluşturulamadı: '+err.message,true)}}
async function importExcel(file){
if(!/\.xlsx$|\.xlsm$/i.test(file.name))throw Error('Lütfen .xlsx uzantılı Excel dosyası seçin.');
const files=await unzip(await file.arrayBuffer());
const sharedXml=files['xl/sharedStrings.xml'];const strings=sharedXml?descendants(parseXml(sharedXml),'si').map(si=>descendants(si,'t').map(t=>t.textContent).join('')):[];
const wb=files['xl/workbook.xml'];if(!wb)throw Error('Excel çalışma kitabı okunamadı.');
const rdoc=files['xl/_rels/workbook.xml.rels']?parseXml(files['xl/_rels/workbook.xml.rels']):null;
const rels={};if(rdoc)for(const rel of descendants(rdoc,'Relationship'))rels[rel.getAttribute('Id')]=rel.getAttribute('Target');
const sheets=[];
for(const sh of descendants(parseXml(wb),'sheet')){
const rid=sh.getAttribute('r:id')||sh.getAttributeNS('http://schemas.openxmlformats.org/officeDocument/2006/relationships','id');
let path=rels[rid]||'';path=path.startsWith('/')?path.slice(1):path.startsWith('xl/')?path:'xl/'+path.replace(/^\.\//,'');
if(files[path])sheets.push({name:sh.getAttribute('name'),rows:excelRows(files[path],strings)});
}
const sheet=sheets.find(s=>s.name==='YetkiFormu')||sheets.find(s=>s.rows?.[0]?.[0]==='Alan'&&s.rows?.[0]?.[1]==='Değer');
if(!sheet)throw Error('Bu formun Excel dosyası bulunamadı. "YetkiFormu" sayfası gerekli.');
const labels=new Map(fields.map(f=>[f.label,f.key]));
const values={};for(const row of sheet.rows.slice(1)){const key=labels.get(String(row[0]||'').trim());if(key)values[key]=String(row[1]??'')}
if(!Object.keys(values).length)throw Error('Formla eşleşen alan bulunamadı.');
if(fields.some(f=>$(f.key).value)&&!confirm('Mevcut form bilgileri Excel verileriyle değiştirilsin mi?'))return;
fill(values);toast('Excel bilgileri forma aktarıldı.');
}
/* Beş art arda tıklama (mouse, çift tıklama, dokunma ve klavye) ile
kurum adı düzenlenir veya kurum logosu seçilir. Araya 3 saniyeden
uzun süre girerse sayaç sıfırlanır. Her iki kontrol de birbirinden bağımsızdır. */
const FIVE_CLICKS=5,CLICK_WINDOW_MS=3000;
const tapCounters={brand:{count:0,last:0},logo:{count:0,last:0}};
function fivePress(kind,onComplete){
const state=tapCounters[kind],now=Date.now();
state.count=(now-state.last<=CLICK_WINDOW_MS)?state.count+1:1;
state.last=now;
if(state.count>=FIVE_CLICKS){state.count=0;onComplete();return}
toast(`${kind==='brand'?'Kurum adı':'Logo'} düzenleme: ${state.count}/5 tıklama`);
}
function readStorage(key){try{return localStorage.getItem(key)}catch{return null}}
function writeStorage(key,value){try{localStorage.setItem(key,value);return true}catch{return false}}
let originalBrand='OLGUN BAŞKANLIĞI';
const brand=$('brand');
const savedBrand=readStorage('olgun_brand_text');
if(savedBrand&&savedBrand.trim())brand.textContent=savedBrand.slice(0,160);
function beginBrandEdit(){
if(brand.isContentEditable)return;
originalBrand=brand.textContent.trim();
brand.setAttribute('contenteditable','true');
brand.setAttribute('role','textbox');
brand.setAttribute('aria-label','Kurum adını yazın ve Enter ile kaydedin');
brand.focus();
requestAnimationFrame(()=>{
const selection=window.getSelection(),range=document.createRange();
range.selectNodeContents(brand);selection.removeAllRanges();selection.addRange(range);
});
toast('Kurum adını yazın. Enter: kaydet · Esc: iptal.');
}
function endBrandEdit(save){
if(!brand.isContentEditable)return;
const next=save?(brand.textContent.trim().slice(0,160)||originalBrand):originalBrand;
brand.textContent=next;
brand.removeAttribute('contenteditable');
brand.removeAttribute('role');
brand.setAttribute('aria-label','Kurum adı. Değiştirmek için 5 kez art arda tıklayın');
if(save){
const stored=writeStorage('olgun_brand_text',next);
toast(stored?'Kurum adı kaydedildi.':'Kurum adı değişti. Bu tarayıcı kalıcı saklamayı desteklemiyor.');
}
}
brand.addEventListener('click',()=>{if(!brand.isContentEditable)fivePress('brand',beginBrandEdit)});
brand.addEventListener('keydown',e=>{
if(brand.isContentEditable){
if(e.key==='Enter'){e.preventDefault();endBrandEdit(true);brand.blur()}
else if(e.key==='Escape'){e.preventDefault();endBrandEdit(false);brand.blur()}
}else if(e.key==='Enter'||e.key===' '){e.preventDefault();fivePress('brand',beginBrandEdit)}
});
brand.addEventListener('blur',()=>{if(brand.isContentEditable)endBrandEdit(true)});
/* LOGO DÜZELTMESİ:
1) Her açılışta, HTML ile aynı klasördeki logo.jpg ÖNCELİKLİ olarak sınanır.
2) Dosya varsa o görüntülenir. Daha önce localStorage'a yazılmış bir logo,
artık mevcut logo.jpg dosyasının önüne geçmez.
3) logo.jpg yoksa/kullanılamıyorsa kaydedilmiş logo yedek olarak denenir.
4) Her iki kaynak da yoksa LOGO yazısı gösterilir.
5) Beş tıklamayla seçilen özel logo anında gösterilir ve yedek olarak saklanır.
Resmî varsayılan dosya adı: logo.jpg (harfler tam olarak böyle olmalı).
*/
const logoImg=$('companyLogo'),logoFallback=$('logoFallback'),logoTrigger=$('logoTrigger');
const LOGO_STORAGE_KEY='olgun_ui2_logo_image';
const savedLogo=readStorage(LOGO_STORAGE_KEY);
const fallbackLogo=(savedLogo&&/^data:image\/(?:jpeg|png|webp);base64,/i.test(savedLogo))?savedLogo:null;
let logoLoadSequence=0;
function showLogo(source,alternative=null){
const sequence=++logoLoadSequence;
if(!source){logoImg.removeAttribute('src');logoImg.hidden=true;logoFallback.hidden=false;return}
const testImage=new Image();
testImage.onload=()=>{
if(sequence!==logoLoadSequence)return;
if(!testImage.naturalWidth||!testImage.naturalHeight){
if(alternative&&alternative!==source)showLogo(alternative);
else showLogo(null);
return;
}
logoImg.src=source;
logoImg.hidden=false;
logoFallback.hidden=true;
};
testImage.onerror=()=>{
if(sequence!==logoLoadSequence)return;
if(alternative&&alternative!==source)showLogo(alternative);
else showLogo(null);
};
testImage.src=source;
}
function localLogoUrl(){
try{return new URL('./logo.jpg',document.baseURI).href}
catch{return './logo.jpg'}
}
function loadDefaultLogo(){showLogo(localLogoUrl(),fallbackLogo)}
loadDefaultLogo();
logoTrigger.addEventListener('click',()=>fivePress('logo',()=>$('logoFile').click()));
async function normalizedLogo(file){
if(!/^image\/(jpeg|png|webp)$/.test(file.type))throw Error('Lütfen JPG, PNG veya WEBP logo seçin.');
if(file.size>8*1024*1024)throw Error('Logo dosyası en fazla 8 MB olabilir.');
const data=await new Promise((resolve,reject)=>{
const r=new FileReader();r.onload=()=>resolve(r.result);r.onerror=()=>reject(Error('Logo dosyası okunamadı.'));r.readAsDataURL(file);
});
return await new Promise((resolve,reject)=>{
const img=new Image();img.onload=()=>{
const scale=Math.min(1,400/Math.max(img.naturalWidth,img.naturalHeight));
const c=document.createElement('canvas');
c.width=Math.max(1,Math.round(img.naturalWidth*scale));
c.height=Math.max(1,Math.round(img.naturalHeight*scale));
c.getContext('2d').drawImage(img,0,0,c.width,c.height);
let result=c.toDataURL('image/png');
if(result.length>600000)result=c.toDataURL('image/jpeg',.86);
resolve(result);
};
img.onerror=()=>reject(Error('Logo görseli açılamadı.'));
img.src=data;
});
}
$('logoFile').addEventListener('change',async e=>{
const file=e.target.files?.[0];e.target.value='';if(!file)return;
try{
const result=await normalizedLogo(file);
showLogo(result);
const stored=writeStorage(LOGO_STORAGE_KEY,result);
toast(stored?'Logo güncellendi. logo.jpg bulunamazsa yedek olarak da kullanılacak.':'Logo güncellendi; kalıcı yedek kayıt desteklenmiyor.');
}catch(err){console.error(err);toast(err.message,true)}
});
$('reviewForm').addEventListener('submit',e=>e.preventDefault());
$('reviewForm').addEventListener('input',e=>{if(e.target.matches('textarea'))resize(e.target)});
$('excelImport').addEventListener('click',()=>$('excelFile').click());
$('excelExport').addEventListener('click',exportExcel);
$('excelFile').addEventListener('change',async e=>{const file=e.target.files?.[0];if(file)try{await importExcel(file)}catch(error){console.error(error);toast(error.message,true)}e.target.value=''});
function refreshPrintValues(){
for(const f of fields){
const el=$(f.key),out=$('print-'+f.key);
if(!out)continue;
let value=el.value||'';
if(el.tagName==='SELECT' && el.selectedIndex>=0)value=el.options[el.selectedIndex].textContent;
out.textContent=value==='Seçiniz...'?'':value;
}
}
$('printButton').addEventListener('click',()=>{refreshPrintValues();window.print()});
window.addEventListener('beforeprint',refreshPrintValues);
draw();
</script>
</body>
</html>





