Raporlama ve Analiz. Bu iki kavram etkin rapor sunmayla varlığını sürdürmektedir. Bu aşamada ise en önemli kısım hızdır. Farklı listeler üzerinde analizler söz konusu olduğunda, elimde bir tool olsa oradan hızlıca filtreleyip hızlıca sonuca varabilsem düşüncesinin bir sonucu olarak ortaya çıkan bir ürün.
HTML kod olarak hazırlanan çalışma, sayfa sonunda yer almaktadır. Bir not defteri açıp kodu not alanına yapıştırın. Dosya ismi verip, uzantıyı ".html" olarak kaydetmeniz yeterli. sonrasında, çift tıklayarak çalıştırın.
Aşağıda uygulamanın çalışmış hali görünmektedir. Helpdesk biriminin Envanterini görüntüleyip analiz etmek istiyoruz. Sayfanın sonunda bir logo yer almakta. Kurumunuzun logosunu "logo.jpg" dosya ismiyle html dosyasının bulunduğu alanda yer alması ile logo belirir. "Envanter, Helpdesk Koordinatörlüğü, MYO MÜDÜRLÜĞÜ" ifadelerini, Kod üzerinde Not defteri ile açıp "Ctrl+F" ile bulup değiştirip kaydetmek suretiyle, kendinize göre uyarlayabilirsiniz.
Başlık alanında orta kısımda, tablodaki toplam satır sayısı, filtre durumunda bulunan sonuca göre oranı ve bulunan filtre adeti bu alana yansımaktadır. dosya seç ile Excel dosyanızı içeri aktarabilirsiniz. (Excel dosyasının tablosu köşelere yanaşık ve boşluk olmadan yer almalı) Filtreleme sonrası, Filtrelenen Excel dosyasını indirebilir ve PDF olarak kaydedebilir yazdırabilirsiniz.
Başlıklara göre, Veri dilimleyici ekleyebilir (Filtre içinde filtre seçimi ile) hızla sonuca ulaşabilir. "Tüm Filtreleri Temizle" butonuna basarak filtreleri kaldırabilirsiniz. Başlıklara tıklayarak (A-Z veya Z-A) sıralama yapabilir, her başlık üzerindeki arama penceresini kullanarak arama yapabilirsiniz.
Helpdesk için için hazırlanan helpdesk datası.
"Dosya seç" butonuna basarak, Excel datanızı içeri aktarın.
"Veri Dilimleyici Ekle" butonuna barak, filtre başlıkları ekleyebilirsiniz. (İhtiyacınız olacak her başlık için, seçim yapıp "Ekle" butonuna basınız.
Eklenen dilimleyici başlıklarında ayrıca, bağımsız değişkenlerin adet bilgileri (toplam değeri) yer alır.
Seçilen filtreye göre, anlık olarak tablo filtrelenir. seçilen filtredeki satır sayısı ve Toplama göre oranı, grafiğe yansır.
Birden fazla filtre seçebilirsiniz. (filtre içinde filtre) seçilen filtrenin alt kümesi olmayan (Filtrelerde yer almayan diğer seçimler) açık gri olarak filtrede görünür.
Adetli seçimler, ihtiyacınız hızlıca ulaşmanızı sağlar.
Başlıklar üzerine basarak liste üzerinde (A-Z veya Z-A) sıralama yapabilirsiniz.
iste üzerinde (A-Z veya Z-A) sıralama yapabilirsiniz.
Yazdırma ön izlemesi yapabilir, PDF olarak kaydedebilir, yazdırabilirsiniz.
Filtrelediğiniz Veriyi, Excel olarak indirebilirsiniz.
İndirdiğiz Excel görünümü.
İndirdiğiniz Exceli, dosya seç ile yeniden içeri aktararak, toplam adet bilgisine göre değerlendirebileceğiniz filtreleyip raporlayabileceğiniz özeldeki dataya erişerek paylaşabilirsiniz.
Yukarıda anlatıldığı gibi aynı şekilde dilimleyici ekleyerek filtreleyebilirsiniz.
.png)
Başlıklarda ayrıca filtreleme yapabilirsiniz.
.png)
PDF olarak kaydedebilir, Yazdırabilirsiniz.
HTML Kod:
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Envanter Sistemi</title>
<!-- SheetJS kütüphanesi - Excel okuma ve yazma için -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
/* --- TEMEL DEĞİŞKENLER VE SIFIRLAMALAR --- */
:root {
--bg: #f0f2f5;
--shadow: 0 8px 24px rgba(0,0,0,.05);
--radius: 10px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Segoe UI", system-ui, Arial, sans-serif;
}
body {
background: var(--bg);
padding: 20px;
}
/* --- BAŞLIK ALANI STİLLERİ (SABİT) --- */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: #fff;
border: 1px solid #e1e4e8;
border-radius: var(--radius);
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
margin-bottom: 25px;
gap: 15px;
/* Sayfa kaydırıldığında ana başlığın sabit kalması için */
position: sticky;
top: 20px;
z-index: 1000;
}
.header .left {
display: flex;
align-items: center;
gap: 15px;
width: 30%;
}
.header .logo img {
height: 50px;
}
.header .title1 {
font-size: 22px;
font-weight: 800;
color: #1a1e23;
letter-spacing: -0.5px;
}
.header .title2 {
font-size: 12px;
color: #6a737d;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* --- ESTETİK VE TEKNOLOJİK GRAFİK BÖLÜMÜ (HUD STİLİ) --- */
.header .center-title {
flex-grow: 1;
display: flex;
justify-content: center;
}
.gauges-wrapper {
display: flex;
gap: 30px;
background: #fafbfc;
border: 1px solid #eef0f3;
padding: 12px 30px;
border-radius: 50px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.02), 0 4px 10px rgba(0,0,0,0.03);
}
.tech-gauge {
display: flex;
flex-direction: column;
align-items: center;
width: 100px;
position: relative;
}
.tg-label {
font-size: 11px;
font-weight: 800;
color: #586069;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: -2px;
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap;
}
.tg-label.total::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #00c6ff; box-shadow: 0 0 6px #00c6ff; }
.tg-label.percent::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #ff416c; box-shadow: 0 0 6px #ff416c; }
.tg-label.filtered::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #11998e; box-shadow: 0 0 6px #11998e; }
.tech-gauge svg {
width: 100%;
overflow: visible;
filter: drop-shadow(0px 4px 4px rgba(0,0,0,0.1));
}
.tg-value {
font-size: 22px;
font-weight: 900;
fill: #24292e;
font-family: "Courier New", Courier, monospace;
}
.header .right {
width: 30%;
font-size: 13px;
font-weight: 800;
text-align: right;
color: #24292e;
letter-spacing: 0.5px;
}
/* --- KONTROL & DOSYA YÜKLEME PANELİ --- */
.controls {
display: flex;
gap: 15px;
align-items: center;
background: #fff;
padding: 15px;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 15px;
border-left: 5px solid #00c6ff;
}
input[type="file"], button, select {
padding: 8px 15px;
border: 1px solid #d1d5da;
border-radius: 6px;
font-size: 13px;
outline: none;
}
input[type="file"]:focus, select:focus { border-color: #00c6ff; }
.btn-primary {
background: linear-gradient(135deg, #007bff, #0056b3);
color: white; border: none; cursor: pointer; font-weight: bold; transition: 0.3s;
box-shadow: 0 4px 10px rgba(0, 123, 255, 0.3);
}
.btn-primary:hover { box-shadow: 0 6px 15px rgba(0, 123, 255, 0.5); transform: translateY(-1px); }
.btn-danger {
background: linear-gradient(135deg, #dc3545, #a71d2a);
color: white; border: none; cursor: pointer; font-weight: bold; transition: 0.3s;
box-shadow: 0 4px 10px rgba(220, 53, 69, 0.3);
}
.btn-danger:hover { box-shadow: 0 6px 15px rgba(220, 53, 69, 0.5); transform: translateY(-1px); }
.btn-success {
background: linear-gradient(135deg, #28a745, #218838);
color: white; border: none; cursor: pointer; font-weight: bold; transition: 0.3s;
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.btn-success:hover { box-shadow: 0 6px 15px rgba(40, 167, 69, 0.5); transform: translateY(-1px); }
progress { width: 150px; height: 18px; display: none; border-radius: 10px; }
progress::-webkit-progress-bar { background-color: #f0f2f5; border-radius: 10px; }
progress::-webkit-progress-value { background: linear-gradient(90deg, #00c6ff, #0072ff); border-radius: 10px; }
/* --- EXCEL DİLİMLEYİCİ (SLICER) ALANI --- */
.slicer-container {
background: #f8f9fc;
padding: 20px;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 20px;
border: 1px solid #e1e4e8;
}
.slicer-header-bar {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.slicer-header-bar span { font-weight: 800; color: #0056b3; font-size: 14px; }
select#slicerColSelect { min-width: 200px; }
.slicer-area { display: flex; flex-wrap: wrap; gap: 15px; }
.slicer-box {
border: 1px solid #d1d5da;
border-radius: 8px;
background: #fff;
width: 220px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 10px rgba(0,0,0,0.04);
}
.slicer-box-header {
background: #24292e;
color: white;
padding: 10px 12px;
font-size: 13px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
.slicer-box-close {
cursor: pointer; color: #ff6b6b; font-weight: bold; border: none; background: transparent; font-size: 15px;
}
.slicer-item-list {
max-height: 180px; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 5px;
}
.slicer-item {
display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 6px; cursor: pointer; border-radius: 4px; transition: 0.2s;
}
.slicer-item:hover { background: #f0f2f5; }
/* FİLTRE DIŞI KALAN DİLİMLEYİCİ ELEMANLARI İÇİN CSS */
.slicer-item.disabled-slicer {
color: #adb5bd;
}
.slicer-item.disabled-slicer input {
opacity: 0.4;
}
/* --- TABLO VE BAŞLIK ÜSTÜ ARAMA STİLLERİ --- */
.table-container {
width: 100%;
max-height: 65vh;
overflow: auto;
background: #fff;
border-radius: var(--radius);
box-shadow: var(--shadow);
position: relative;
}
table { width: max-content; min-width: 100%; border-collapse: separate; border-spacing: 0; }
th, td { border-bottom: 1px solid #e1e4e8; border-right: 1px solid #e1e4e8; padding: 12px; text-align: left; font-size: 13px; white-space: nowrap; }
th:first-child, td:first-child { border-left: 1px solid #e1e4e8; }
thead {
position: sticky;
top: 0;
z-index: 20;
background: #fff;
box-shadow: 0 4px 6px rgba(0,0,0,0.06);
}
thead th.main-header {
background: #1a1e23 !important;
color: white !important;
font-weight: bold;
border-top: 1px solid #1a1e23;
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
thead th.main-header:hover {
background: #2c3238 !important;
}
.filter-row { background-color: #f8f9fa; }
.filter-row th { padding: 6px; border-bottom: 1px solid #e1e4e8; background: #f8f9fa; }
.col-search-input {
width: 100%; min-width: 100px; padding: 8px; font-size: 12px; border: 1px solid #ced4da; border-radius: 4px; box-sizing: border-box; outline: none; transition: 0.2s;
}
.col-search-input:focus { border-color: #00c6ff; box-shadow: 0 0 0 3px rgba(0, 198, 255, 0.15); }
.sort-icon {
font-size: 11px;
margin-left: 5px;
color: #00c6ff;
display: inline-block;
width: 12px;
}
tbody tr:nth-child(even) { background-color: #f8f9fc; }
tbody tr:hover { background-color: #e6f2ff; }
/* --- SAYFALAMA (PAGINATION) STİLLERİ --- */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #fff;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-top: 15px;
}
.pagination-controls button { margin: 0 5px; }
/* --- YAZDIRMA (PRINT) VE PDF STİLLERİ --- */
@media print {
.no-print { display: none !important; }
body { background: white; padding: 0; margin: 0; }
.header { box-shadow: none; border-color: #000; margin-bottom: 20px; position: static; }
.table-container { box-shadow: none; border-radius: 0; overflow: visible; max-height: none; }
table { width: 100%; max-width: 100%; font-size: 10px; }
th, td { white-space: nowrap !important; padding: 6px; }
thead { position: static; box-shadow: none; }
thead th.main-header { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
@page { size: landscape; margin: 10mm; @bottom-right { content: "Sayfa " counter(page) " / " counter(pages); font-size: 10px; } }
}
</style>
</head>
<body>
<!-- SVG GRADYAN TANIMLAMALARI -->
<svg style="width:0; height:0; position:absolute;" aria-hidden="true" focusable="false">
<defs>
<linearGradient id="gradTotal" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#00c6ff" />
<stop offset="100%" stop-color="#0072ff" />
</linearGradient>
<linearGradient id="gradPercent" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#ff416c" />
<stop offset="100%" stop-color="#ff4b2b" />
</linearGradient>
<linearGradient id="gradFiltered" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#11998e" />
<stop offset="100%" stop-color="#38ef7d" />
</linearGradient>
</defs>
</svg>
<!-- BAŞLIK ALANI BAŞLANGICI -->
<div class="header">
<div class="left">
<div class="logo"><img src="logo.jpg" alt="Logo" onerror="this.style.display='none'"></div>
<div>
<div class="title1">Envanter</div>
<div class="title2">Helpdesk Koordinatörlüğü</div>
</div>
</div>
<div class="center-title">
<div class="gauges-wrapper">
<!-- 1. TOPLAM VERİ -->
<div class="tech-gauge">
<div class="tg-label total">Adet</div>
<svg viewBox="0 0 100 55">
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#e1e4e8" stroke-width="14" stroke-linecap="round" />
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#ffffff" stroke-width="14" stroke-dasharray="2 4" />
<path id="pathTotal" d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="url(#gradTotal)" stroke-width="10" stroke-linecap="round"
style="stroke-dasharray: 125.6; stroke-dashoffset: 0; transition: stroke-dashoffset 1s ease-out;" />
<text x="50" y="47" text-anchor="middle" class="tg-value" id="gTotal">0</text>
</svg>
</div>
<!-- 2. YÜZDE ORANI -->
<div class="tech-gauge">
<div class="tg-label percent">Oran</div>
<svg viewBox="0 0 100 55">
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#e1e4e8" stroke-width="14" stroke-linecap="round" />
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#ffffff" stroke-width="14" stroke-dasharray="2 4" />
<path id="pathPercent" d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="url(#gradPercent)" stroke-width="10" stroke-linecap="round"
style="stroke-dasharray: 125.6; stroke-dashoffset: 125.6; transition: stroke-dashoffset 1s ease-out;" />
<text x="50" y="47" text-anchor="middle" class="tg-value" id="gPercent">%0</text>
</svg>
</div>
<!-- 3. FİLTRELENEN -->
<div class="tech-gauge">
<div class="tg-label filtered">Filtre Adet</div>
<svg viewBox="0 0 100 55">
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#e1e4e8" stroke-width="14" stroke-linecap="round" />
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#ffffff" stroke-width="14" stroke-dasharray="2 4" />
<path id="pathFiltered" d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="url(#gradFiltered)" stroke-width="10" stroke-linecap="round"
style="stroke-dasharray: 125.6; stroke-dashoffset: 125.6; transition: stroke-dashoffset 1s ease-out;" />
<text x="50" y="47" text-anchor="middle" class="tg-value" id="gFiltered">0</text>
</svg>
</div>
</div>
</div>
<div class="right">MYO MÜDÜRLÜĞÜ</div>
</div>
<!-- DOSYA YÜKLEME VE YAZDIRMA -->
<div class="controls no-print">
<input type="file" id="excelFile" accept=".xls,.xlsx">
<progress id="progressBar" value="0" max="100"></progress>
<div style="margin-left: auto; display: flex; gap: 10px;">
<button class="btn-success" type="button" onclick="exportToExcel()">📥 Excel İndir</button>
<button class="btn-primary" type="button" onclick="window.print()">🖨️ PDF / Yazdır</button>
</div>
</div>
<!-- EXCEL DİLİMLEYİCİ (SLICER) ALANI -->
<div class="slicer-container no-print">
<div class="slicer-header-bar">
<span>🎛️ Veri Dilimleyici Ekle:</span>
<select id="slicerColSelect">
<option value="">-- Önce Excel Yükleyin --</option>
</select>
<button class="btn-primary" id="btnAddSlicer">+ Ekle</button>
<div style="margin-left: auto; display: flex; flex-direction: column; align-items: flex-end;">
<button class="btn-danger" id="btnClearSlicers">✖ Tüm Filtreleri Temizle</button>
<span style="font-size: 10px; color: #ced4da; margin-top: 5px; font-weight: 600; letter-spacing: 0.5px;">AI Web Designer: Muhammed Yusuf OLGUN</span>
</div>
</div>
<div id="slicerArea" class="slicer-area"></div>
</div>
<!-- TABLO ALANI -->
<div class="table-container" id="tableContainer">
<table id="dataTable">
<thead id="tableHead"></thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<!-- SAYFALAMA KONTROLLERİ -->
<div class="pagination-container no-print" id="paginationContainer" style="display:none;">
<div id="pageInfo" style="font-size: 13px; font-weight: bold; color: #586069;"></div>
<div class="pagination-controls">
<button class="btn-primary" onclick="changePage(-1)">Önceki</button>
<span id="pageIndicator" style="margin: 0 10px; font-weight: bold; color: #24292e;"></span>
<button class="btn-primary" onclick="changePage(1)">Sonraki</button>
</div>
<div>
<select id="rowsPerPage" onchange="changeRowsPerPage()">
<option value="50">Sayfada 50 Kayıt</option>
<option value="100" selected>Sayfada 100 Kayıt</option>
<option value="500">Sayfada 500 Kayıt</option>
<option value="1000">Sayfada 1000 Kayıt</option>
</select>
</div>
</div>
<script>
let excelData = [];
let actualHeaders = {};
let headerKeys = [];
// --- SAYFALAMA VE FİLTRELENMİŞ VERİ DEĞİŞKENLERİ ---
let currentFilteredData = [];
let currentPage = 1;
let rowsPerPage = 100;
// --- SIRALAMA (SORTING) DEĞİŞKENLERİ ---
let currentSortCol = null;
let sortAsc = true;
const PATH_LENGTH = 125.6;
document.getElementById('excelFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const pb = document.getElementById('progressBar');
pb.style.display = 'block';
pb.value = 10;
const reader = new FileReader();
reader.onload = function(event) {
pb.value = 40;
try {
const data = new Uint8Array(event.target.result);
const workbook = XLSX.read(data, {type: 'array'});
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
pb.value = 60;
const rawData = XLSX.utils.sheet_to_json(worksheet, {header: "A", defval: ""});
pb.value = 80;
if (rawData.length > 0) {
actualHeaders = rawData[0];
headerKeys = Object.keys(actualHeaders).filter(key => String(actualHeaders[key]).trim() !== "");
excelData = rawData.slice(1).filter(row => {
return headerKeys.some(key => String(row[key]).trim() !== "");
});
currentSortCol = null;
sortAsc = true;
currentPage = 1;
populateSlicerDropdown();
renderHeaders();
currentFilteredData = excelData;
renderPagination();
updateGauges(excelData.length, excelData.length);
document.getElementById('slicerArea').innerHTML = '';
} else {
alert("Dosyada veri bulunamadı!");
}
pb.value = 100;
setTimeout(() => { pb.style.display = 'none'; }, 500);
} catch (error) {
alert("Dosya okunurken bir hata oluştu: " + error.message);
pb.style.display = 'none';
}
};
reader.readAsArrayBuffer(file);
});
function renderHeaders() {
const thead = document.getElementById('tableHead');
thead.innerHTML = '';
let headerRow = document.createElement('tr');
headerKeys.forEach(k => {
let th = document.createElement('th');
th.className = 'main-header';
th.title = 'Sıralamak için tıklayın';
th.innerHTML = `<span>${actualHeaders[k]}</span><span class="sort-icon" id="sort_icon_${k}"></span>`;
th.addEventListener('click', () => {
if (currentSortCol === k) {
sortAsc = !sortAsc;
} else {
currentSortCol = k;
sortAsc = true;
}
updateSortIcons();
applyFiltersAndSlicers();
});
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
let filterRow = document.createElement('tr');
filterRow.className = 'filter-row no-print';
headerKeys.forEach(k => {
let th = document.createElement('th');
let input = document.createElement('input');
input.type = 'text';
input.placeholder = '🔍 Ara...';
input.dataset.col = k;
input.className = 'col-search-input';
input.addEventListener('input', applyFiltersAndSlicers);
th.appendChild(input);
filterRow.appendChild(th);
});
thead.appendChild(filterRow);
}
function updateSortIcons() {
headerKeys.forEach(k => {
const iconSpan = document.getElementById('sort_icon_' + k);
if (iconSpan) {
if (currentSortCol === k) {
iconSpan.innerHTML = sortAsc ? '▲' : '▼';
} else {
iconSpan.innerHTML = '';
}
}
});
}
function renderPagination() {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
const totalRows = currentFilteredData.length;
if (totalRows === 0) {
document.getElementById('paginationContainer').style.display = 'none';
return;
}
document.getElementById('paginationContainer').style.display = 'flex';
const totalPages = Math.ceil(totalRows / rowsPerPage);
if (currentPage > totalPages) currentPage = totalPages;
if (currentPage < 1) currentPage = 1;
const startIndex = (currentPage - 1) * rowsPerPage;
const endIndex = Math.min(startIndex + rowsPerPage, totalRows);
const pageData = currentFilteredData.slice(startIndex, endIndex);
pageData.forEach(row => {
let tr = document.createElement('tr');
headerKeys.forEach(k => {
let td = document.createElement('td');
td.textContent = row[k] !== undefined ? row[k] : '';
tr.appendChild(td);
});
tbody.appendChild(tr);
});
document.getElementById('pageInfo').innerText = `Toplam ${totalRows} kayıttan ${startIndex + 1} - ${endIndex} arası gösteriliyor.`;
document.getElementById('pageIndicator').innerText = `Sayfa ${currentPage} / ${totalPages}`;
}
window.changePage = function(dir) {
currentPage += dir;
renderPagination();
document.getElementById('tableContainer').scrollTo({ top: 0, behavior: 'smooth' });
}
window.changeRowsPerPage = function() {
rowsPerPage = parseInt(document.getElementById('rowsPerPage').value);
currentPage = 1;
renderPagination();
document.getElementById('tableContainer').scrollTo({ top: 0, behavior: 'smooth' });
}
window.exportToExcel = function() {
if (!currentFilteredData || currentFilteredData.length === 0) {
alert("Dışa aktarılacak veri bulunamadı.");
return;
}
const exportData = currentFilteredData.map(row => {
let cleanRow = {};
headerKeys.forEach(k => {
cleanRow[actualHeaders[k]] = row[k];
});
return cleanRow;
});
const ws = XLSX.utils.json_to_sheet(exportData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Filtrelenmis_Veri");
XLSX.writeFile(wb, "Envanter_Raporu.xlsx");
}
function populateSlicerDropdown() {
const select = document.getElementById('slicerColSelect');
select.innerHTML = '<option value="">-- Filtrelenecek Sütunu Seçin --</option>';
headerKeys.forEach(k => {
let opt = document.createElement('option');
opt.value = k;
opt.textContent = actualHeaders[k];
select.appendChild(opt);
});
}
document.getElementById('btnAddSlicer').addEventListener('click', function() {
const select = document.getElementById('slicerColSelect');
const colKey = select.value;
if (!colKey) return;
if (document.getElementById('slicer_' + colKey)) {
alert('Bu sütun için dilimleyici zaten açık!');
return;
}
const headerName = actualHeaders[colKey];
let uniqueVals = new Set();
excelData.forEach(row => {
let val = row[colKey] !== undefined ? String(row[colKey]).trim() : "";
uniqueVals.add(val);
});
let sortedVals = Array.from(uniqueVals).sort((a, b) => a.localeCompare(b, 'tr-TR', {numeric: true}));
const slicerArea = document.getElementById('slicerArea');
const box = document.createElement('div');
box.className = 'slicer-box';
box.id = 'slicer_' + colKey;
let listHtml = sortedVals.map(v => `
<label class="slicer-item" data-active="1">
<input type="checkbox" value="${v.replace(/"/g, '"')}" onchange="applyFiltersAndSlicers()">
${v === "" ? "<i>(Boş Kayıtlar)</i>" : v}
</label>
`).join('');
// Başlık içine slicer-title sınıfı ve data-name özelliği eklendi (Sayıyı parantez içinde göstermek için)
box.innerHTML = `
<div class="slicer-box-header">
<span class="slicer-title" data-name="${headerName}">${headerName} (${sortedVals.length})</span>
<button class="slicer-box-close" onclick="removeSlicer('${colKey}')">✖</button>
</div>
<div class="slicer-item-list">
${listHtml}
</div>
`;
slicerArea.appendChild(box);
select.value = "";
// Yeni dilimleyici eklendiğinde mevcut filtrelere göre kendini güncellemesi için:
applyFiltersAndSlicers();
});
window.removeSlicer = function(colKey) {
const box = document.getElementById('slicer_' + colKey);
if (box) {
box.remove();
applyFiltersAndSlicers();
}
};
document.getElementById('btnClearSlicers').addEventListener('click', function() {
document.getElementById('slicerArea').innerHTML = '';
document.querySelectorAll('.col-search-input').forEach(inp => inp.value = '');
applyFiltersAndSlicers();
});
// --- YENİ ÇAPRAZ FİLTRELEME (CROSS FILTER) UI GÜNCELLEME FONKSİYONU ---
function updateSlicerUIs(slicerConds, searchConds) {
const boxes = document.querySelectorAll('.slicer-box');
boxes.forEach(box => {
const colKey = box.id.replace('slicer_', '');
// Bu dilimleyici HARİÇ diğer tüm kuralları al
let otherSlicerConds = slicerConds.filter(cond => cond.col !== colKey);
let subset = excelData;
// Diğer kuralları veriye uygula
if (otherSlicerConds.length > 0 || searchConds.length > 0) {
subset = subset.filter(row => {
let passesSlicer = otherSlicerConds.every(cond => {
let cellVal = row[cond.col] !== undefined ? String(row[cond.col]).trim() : "";
return cond.allowed.includes(cellVal);
});
if (!passesSlicer) return false;
let passesSearch = searchConds.every(cond => {
let cellVal = String(row[cond.col] || '').toLocaleLowerCase('tr-TR');
return cellVal.includes(cond.val);
});
return passesSearch;
});
}
// Geriye kalan alt kümedeki (subset) geçerli değerleri bul
let validVals = new Set();
subset.forEach(row => {
let val = row[colKey] !== undefined ? String(row[colKey]).trim() : "";
validVals.add(val);
});
// Dilimleyici içindeki DOM elemanlarını güncelle ve taşı
const itemList = box.querySelector('.slicer-item-list');
const labels = Array.from(itemList.querySelectorAll('.slicer-item'));
let activeCount = 0; // O anki aktif filtreleri saymak için değişken
labels.forEach(label => {
const input = label.querySelector('input');
const val = input.value;
if (validVals.has(val)) {
label.classList.remove('disabled-slicer');
label.dataset.active = "1"; // Aktif
activeCount++; // Aktifleri say
} else {
label.classList.add('disabled-slicer');
label.dataset.active = "0"; // Pasif
}
});
// Başlık metnini (Sayıyı) Güncelle
const titleSpan = box.querySelector('.slicer-title');
if (titleSpan) {
titleSpan.innerText = `${titleSpan.dataset.name} (${activeCount})`;
}
// Sıralama Yap: Önce aktifler(1), sonra pasifler(0). Kendi içlerinde alfabetik.
labels.sort((a, b) => {
const activeA = a.dataset.active;
const activeB = b.dataset.active;
// 1 olanlar 0 olanların üstüne gelsin
if (activeA !== activeB) {
return activeB.localeCompare(activeA);
}
// Aynı durumdalarsa alfabetik sırala
const valA = a.querySelector('input').value;
const valB = b.querySelector('input').value;
return valA.localeCompare(valB, 'tr-TR', {numeric: true});
});
// DOM'a yeniden ekleyerek sıralamayı ekrana yansıt (Aşağıya iter)
labels.forEach(label => itemList.appendChild(label));
});
}
window.applyFiltersAndSlicers = function() {
const boxes = document.querySelectorAll('.slicer-box');
let slicerConds = [];
boxes.forEach(box => {
const colKey = box.id.replace('slicer_', '');
const checkedInputs = box.querySelectorAll('input[type="checkbox"]:checked');
if (checkedInputs.length > 0) {
let allowedVals = Array.from(checkedInputs).map(inp => inp.value);
slicerConds.push({ col: colKey, allowed: allowedVals });
}
});
const searchInputs = document.querySelectorAll('.col-search-input');
let searchConds = [];
searchInputs.forEach(inp => {
const val = inp.value.trim().toLocaleLowerCase('tr-TR');
if (val) { searchConds.push({ col: inp.dataset.col, val: val }); }
});
// 1) DİLİMLEYİCİ ARAYÜZLERİNİ GÜNCELLE (HANGİ SEÇENEKLER GRİ OLACAK, HANGİLERİ ÜSTTE VE SAYILAR)
updateSlicerUIs(slicerConds, searchConds);
let dataToProcess = excelData;
if (slicerConds.length > 0 || searchConds.length > 0) {
dataToProcess = excelData.filter(row => {
let passesSlicer = slicerConds.every(cond => {
let cellVal = row[cond.col] !== undefined ? String(row[cond.col]).trim() : "";
return cond.allowed.includes(cellVal);
});
if (!passesSlicer) return false;
let passesSearch = searchConds.every(cond => {
let cellVal = String(row[cond.col] || '').toLocaleLowerCase('tr-TR');
return cellVal.includes(cond.val);
});
return passesSearch;
});
}
if (currentSortCol) {
dataToProcess = [...dataToProcess];
dataToProcess.sort((a, b) => {
let valA = a[currentSortCol] !== undefined ? String(a[currentSortCol]).trim() : "";
let valB = b[currentSortCol] !== undefined ? String(b[currentSortCol]).trim() : "";
let numA = parseFloat(valA.replace(',', '.'));
let numB = parseFloat(valB.replace(',', '.'));
let isNum = !isNaN(numA) && !isNaN(numB) && valA !== "" && valB !== "";
if (isNum) {
return sortAsc ? numA - numB : numB - numA;
} else {
return sortAsc
? valA.localeCompare(valB, 'tr-TR', { numeric: true })
: valB.localeCompare(valA, 'tr-TR', { numeric: true });
}
});
}
currentPage = 1;
currentFilteredData = dataToProcess;
renderPagination();
updateGauges(excelData.length, dataToProcess.length);
};
function updateGauges(total, filtered) {
document.getElementById('gTotal').textContent = total;
let percent = total === 0 ? 0 : Math.round((filtered / total) * 100);
document.getElementById('gPercent').textContent = '%' + percent;
document.getElementById('gFiltered').textContent = filtered;
document.getElementById('pathTotal').style.strokeDashoffset = total > 0 ? 0 : PATH_LENGTH;
let pOffset = PATH_LENGTH - (PATH_LENGTH * percent / 100);
document.getElementById('pathPercent').style.strokeDashoffset = isNaN(pOffset) ? PATH_LENGTH : pOffset;
let fOffset = PATH_LENGTH - (PATH_LENGTH * percent / 100);
document.getElementById('pathFiltered').style.strokeDashoffset = isNaN(fOffset) ? PATH_LENGTH : fOffset;
}
</script>
</body>
</html>
Resimli:
İsim alanında aynı isimde jpg dosyası ile başlıkta görsel yansır. (Html kodun bulunduğu yerde "Resim" isminde klasör olmalı.) A1 sütunundaki hücredeki isim, ürün vs. aynı isimde uzantı ".jpg" olduğunda başlık alanına resim yansır. (ilk satırdaki değer ile eşleşir.)
HTML Kod:
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Envanter Sistemi</title>
<!-- SheetJS kütüphanesi - Excel okuma ve yazma için -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
/* --- TEMEL DEĞİŞKENLER VE SIFIRLAMALAR --- */
:root {
--bg: #f0f2f5;
--shadow: 0 8px 24px rgba(0,0,0,.05);
--radius: 10px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Segoe UI", system-ui, Arial, sans-serif;
}
body {
background: var(--bg);
padding: 20px;
}
/* --- BAŞLIK ALANI STİLLERİ (SABİT) --- */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: #fff;
border: 1px solid #e1e4e8;
border-radius: var(--radius);
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
margin-bottom: 25px;
gap: 15px;
/* Sayfa kaydırıldığında ana başlığın sabit kalması için */
position: sticky;
top: 20px;
z-index: 1000;
}
.header .left {
display: flex;
align-items: center;
gap: 15px;
width: 30%;
}
.header .logo img {
height: 50px;
}
.header .title1 {
font-size: 22px;
font-weight: 800;
color: #1a1e23;
letter-spacing: -0.5px;
}
.header .title2 {
font-size: 12px;
color: #6a737d;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* --- ESTETİK VE TEKNOLOJİK GRAFİK BÖLÜMÜ (HUD STİLİ) --- */
.header .center-title {
flex-grow: 1;
display: flex;
justify-content: center;
}
.gauges-wrapper {
display: flex;
gap: 30px;
background: #fafbfc;
border: 1px solid #eef0f3;
padding: 12px 30px;
border-radius: 50px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.02), 0 4px 10px rgba(0,0,0,0.03);
align-items: center;
}
.tech-gauge {
display: flex;
flex-direction: column;
align-items: center;
width: 100px;
position: relative;
}
.tg-label {
font-size: 11px;
font-weight: 800;
color: #586069;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: -2px;
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap;
}
.tg-label.total::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #00c6ff; box-shadow: 0 0 6px #00c6ff; }
.tg-label.percent::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #ff416c; box-shadow: 0 0 6px #ff416c; }
.tg-label.filtered::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #11998e; box-shadow: 0 0 6px #11998e; }
.tech-gauge svg {
width: 100%;
overflow: visible;
filter: drop-shadow(0px 4px 4px rgba(0,0,0,0.1));
}
.tg-value {
font-size: 22px;
font-weight: 900;
fill: #24292e;
font-family: "Courier New", Courier, monospace;
}
/* YENİ: RESİM KUTUCUĞU STİLİ */
.profile-photo-box {
width: 65px;
height: 65px;
border-radius: 8px;
background: #fff;
border: 1px solid #e1e4e8;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
margin-left: 10px;
}
.profile-photo-box img {
width: 100%;
height: 100%;
object-fit: contain; /* Resmi kutuya sığacak şekilde ortalar */
display: none; /* onerror ve onload ile gösterilecek */
}
.header .right {
width: 30%;
font-size: 13px;
font-weight: 800;
text-align: right;
color: #24292e;
letter-spacing: 0.5px;
}
/* --- KONTROL & DOSYA YÜKLEME PANELİ --- */
.controls {
display: flex;
gap: 15px;
align-items: center;
background: #fff;
padding: 15px;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 15px;
border-left: 5px solid #00c6ff;
}
input[type="file"], button, select {
padding: 8px 15px;
border: 1px solid #d1d5da;
border-radius: 6px;
font-size: 13px;
outline: none;
}
input[type="file"]:focus, select:focus { border-color: #00c6ff; }
.btn-primary {
background: linear-gradient(135deg, #007bff, #0056b3);
color: white; border: none; cursor: pointer; font-weight: bold; transition: 0.3s;
box-shadow: 0 4px 10px rgba(0, 123, 255, 0.3);
}
.btn-primary:hover { box-shadow: 0 6px 15px rgba(0, 123, 255, 0.5); transform: translateY(-1px); }
.btn-danger {
background: linear-gradient(135deg, #dc3545, #a71d2a);
color: white; border: none; cursor: pointer; font-weight: bold; transition: 0.3s;
box-shadow: 0 4px 10px rgba(220, 53, 69, 0.3);
}
.btn-danger:hover { box-shadow: 0 6px 15px rgba(220, 53, 69, 0.5); transform: translateY(-1px); }
.btn-success {
background: linear-gradient(135deg, #28a745, #218838);
color: white; border: none; cursor: pointer; font-weight: bold; transition: 0.3s;
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.btn-success:hover { box-shadow: 0 6px 15px rgba(40, 167, 69, 0.5); transform: translateY(-1px); }
progress { width: 150px; height: 18px; display: none; border-radius: 10px; }
progress::-webkit-progress-bar { background-color: #f0f2f5; border-radius: 10px; }
progress::-webkit-progress-value { background: linear-gradient(90deg, #00c6ff, #0072ff); border-radius: 10px; }
/* --- EXCEL DİLİMLEYİCİ (SLICER) ALANI --- */
.slicer-container {
background: #f8f9fc;
padding: 20px;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 20px;
border: 1px solid #e1e4e8;
}
.slicer-header-bar {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.slicer-header-bar span { font-weight: 800; color: #0056b3; font-size: 14px; }
select#slicerColSelect { min-width: 200px; }
.slicer-area { display: flex; flex-wrap: wrap; gap: 15px; }
.slicer-box {
border: 1px solid #d1d5da;
border-radius: 8px;
background: #fff;
width: 220px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 10px rgba(0,0,0,0.04);
}
.slicer-box-header {
background: #24292e;
color: white;
padding: 10px 12px;
font-size: 13px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
.slicer-box-close {
cursor: pointer; color: #ff6b6b; font-weight: bold; border: none; background: transparent; font-size: 15px;
}
.slicer-item-list {
max-height: 180px; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 5px;
}
.slicer-item {
display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 6px; cursor: pointer; border-radius: 4px; transition: 0.2s;
}
.slicer-item:hover { background: #f0f2f5; }
/* FİLTRE DIŞI KALAN DİLİMLEYİCİ ELEMANLARI İÇİN CSS */
.slicer-item.disabled-slicer {
color: #adb5bd;
}
.slicer-item.disabled-slicer input {
opacity: 0.4;
}
/* --- TABLO VE BAŞLIK ÜSTÜ ARAMA STİLLERİ --- */
.table-container {
width: 100%;
max-height: 65vh;
overflow: auto;
background: #fff;
border-radius: var(--radius);
box-shadow: var(--shadow);
position: relative;
}
table { width: max-content; min-width: 100%; border-collapse: separate; border-spacing: 0; }
th, td { border-bottom: 1px solid #e1e4e8; border-right: 1px solid #e1e4e8; padding: 12px; text-align: left; font-size: 13px; white-space: nowrap; }
th:first-child, td:first-child { border-left: 1px solid #e1e4e8; }
thead {
position: sticky;
top: 0;
z-index: 20;
background: #fff;
box-shadow: 0 4px 6px rgba(0,0,0,0.06);
}
thead th.main-header {
background: #1a1e23 !important;
color: white !important;
font-weight: bold;
border-top: 1px solid #1a1e23;
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
thead th.main-header:hover {
background: #2c3238 !important;
}
.filter-row { background-color: #f8f9fa; }
.filter-row th { padding: 6px; border-bottom: 1px solid #e1e4e8; background: #f8f9fa; }
.col-search-input {
width: 100%; min-width: 100px; padding: 8px; font-size: 12px; border: 1px solid #ced4da; border-radius: 4px; box-sizing: border-box; outline: none; transition: 0.2s;
}
.col-search-input:focus { border-color: #00c6ff; box-shadow: 0 0 0 3px rgba(0, 198, 255, 0.15); }
.sort-icon {
font-size: 11px;
margin-left: 5px;
color: #00c6ff;
display: inline-block;
width: 12px;
}
tbody tr:nth-child(even) { background-color: #f8f9fc; }
tbody tr:hover { background-color: #e6f2ff; }
/* --- SAYFALAMA (PAGINATION) STİLLERİ --- */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #fff;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-top: 15px;
}
.pagination-controls button { margin: 0 5px; }
/* --- YAZDIRMA (PRINT) VE PDF STİLLERİ --- */
@media print {
.no-print { display: none !important; }
body { background: white; padding: 0; margin: 0; }
.header { box-shadow: none; border-color: #000; margin-bottom: 20px; position: static; }
.table-container { box-shadow: none; border-radius: 0; overflow: visible; max-height: none; }
table { width: 100%; max-width: 100%; font-size: 10px; }
th, td { white-space: nowrap !important; padding: 6px; }
thead { position: static; box-shadow: none; }
thead th.main-header { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
@page { size: landscape; margin: 10mm; @bottom-right { content: "Sayfa " counter(page) " / " counter(pages); font-size: 10px; } }
}
</style>
</head>
<body>
<!-- SVG GRADYAN TANIMLAMALARI -->
<svg style="width:0; height:0; position:absolute;" aria-hidden="true" focusable="false">
<defs>
<linearGradient id="gradTotal" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#00c6ff" />
<stop offset="100%" stop-color="#0072ff" />
</linearGradient>
<linearGradient id="gradPercent" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#ff416c" />
<stop offset="100%" stop-color="#ff4b2b" />
</linearGradient>
<linearGradient id="gradFiltered" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#11998e" />
<stop offset="100%" stop-color="#38ef7d" />
</linearGradient>
</defs>
</svg>
<!-- BAŞLIK ALANI BAŞLANGICI -->
<div class="header">
<div class="left">
<div class="logo"><img src="logo.jpg" alt="Logo" onerror="this.style.display='none'"></div>
<div>
<div class="title1">Envanter</div>
<div class="title2">Kullanıcı Destek Koordinatörlüğü</div>
</div>
</div>
<div class="center-title">
<div class="gauges-wrapper">
<!-- 1. TOPLAM VERİ -->
<div class="tech-gauge">
<div class="tg-label total">Adet</div>
<svg viewBox="0 0 100 55">
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#e1e4e8" stroke-width="14" stroke-linecap="round" />
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#ffffff" stroke-width="14" stroke-dasharray="2 4" />
<path id="pathTotal" d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="url(#gradTotal)" stroke-width="10" stroke-linecap="round"
style="stroke-dasharray: 125.6; stroke-dashoffset: 0; transition: stroke-dashoffset 1s ease-out;" />
<text x="50" y="47" text-anchor="middle" class="tg-value" id="gTotal">0</text>
</svg>
</div>
<!-- 2. YÜZDE ORANI -->
<div class="tech-gauge">
<div class="tg-label percent">Oran</div>
<svg viewBox="0 0 100 55">
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#e1e4e8" stroke-width="14" stroke-linecap="round" />
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#ffffff" stroke-width="14" stroke-dasharray="2 4" />
<path id="pathPercent" d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="url(#gradPercent)" stroke-width="10" stroke-linecap="round"
style="stroke-dasharray: 125.6; stroke-dashoffset: 125.6; transition: stroke-dashoffset 1s ease-out;" />
<text x="50" y="47" text-anchor="middle" class="tg-value" id="gPercent">%0</text>
</svg>
</div>
<!-- 3. FİLTRELENEN -->
<div class="tech-gauge">
<div class="tg-label filtered">Filtre Adet</div>
<svg viewBox="0 0 100 55">
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#e1e4e8" stroke-width="14" stroke-linecap="round" />
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#ffffff" stroke-width="14" stroke-dasharray="2 4" />
<path id="pathFiltered" d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="url(#gradFiltered)" stroke-width="10" stroke-linecap="round"
style="stroke-dasharray: 125.6; stroke-dashoffset: 125.6; transition: stroke-dashoffset 1s ease-out;" />
<text x="50" y="47" text-anchor="middle" class="tg-value" id="gFiltered">0</text>
</svg>
</div>
<!-- 4. YENİ: KAYIT PROFİL FOTOĞRAFI -->
<div class="profile-photo-box no-print">
<!-- Resim varsa gösterilecek, yoksa onerror fonksiyonu ile görünmez olacak -->
<img id="profileImage" src="" alt="Kayıt Fotoğrafı" onerror="this.style.display='none'" onload="this.style.display='block'">
</div>
</div>
</div>
<div class="right">TEKNİK İŞLETME DAİRESİ BAŞKANLIĞI</div>
</div>
<!-- DOSYA YÜKLEME VE YAZDIRMA -->
<div class="controls no-print">
<input type="file" id="excelFile" accept=".xls,.xlsx">
<progress id="progressBar" value="0" max="100"></progress>
<div style="margin-left: auto; display: flex; gap: 10px;">
<button class="btn-success" type="button" onclick="exportToExcel()">📥 Excel İndir</button>
<button class="btn-primary" type="button" onclick="window.print()">🖨️ PDF / Yazdır</button>
</div>
</div>
<!-- EXCEL DİLİMLEYİCİ (SLICER) ALANI -->
<div class="slicer-container no-print">
<div class="slicer-header-bar">
<span>🎛️ Veri Dilimleyici Ekle:</span>
<select id="slicerColSelect">
<option value="">-- Önce Excel Yükleyin --</option>
</select>
<button class="btn-primary" id="btnAddSlicer">+ Ekle</button>
<div style="margin-left: auto; display: flex; flex-direction: column; align-items: flex-end;">
<button class="btn-danger" id="btnClearSlicers">✖ Tüm Filtreleri Temizle</button>
<span style="font-size: 10px; color: #ced4da; margin-top: 5px; font-weight: 600; letter-spacing: 0.5px;">AI Web Designer: Muhammed Yusuf OLGUN</span>
</div>
</div>
<div id="slicerArea" class="slicer-area"></div>
</div>
<!-- TABLO ALANI -->
<div class="table-container" id="tableContainer">
<table id="dataTable">
<thead id="tableHead"></thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<!-- SAYFALAMA KONTROLLERİ -->
<div class="pagination-container no-print" id="paginationContainer" style="display:none;">
<div id="pageInfo" style="font-size: 13px; font-weight: bold; color: #586069;"></div>
<div class="pagination-controls">
<button class="btn-primary" onclick="changePage(-1)">Önceki</button>
<span id="pageIndicator" style="margin: 0 10px; font-weight: bold; color: #24292e;"></span>
<button class="btn-primary" onclick="changePage(1)">Sonraki</button>
</div>
<div>
<select id="rowsPerPage" onchange="changeRowsPerPage()">
<option value="50">Sayfada 50 Kayıt</option>
<option value="100" selected>Sayfada 100 Kayıt</option>
<option value="500">Sayfada 500 Kayıt</option>
<option value="1000">Sayfada 1000 Kayıt</option>
</select>
</div>
</div>
<script>
let excelData = [];
let actualHeaders = {};
let headerKeys = [];
// --- SAYFALAMA VE FİLTRELENMİŞ VERİ DEĞİŞKENLERİ ---
let currentFilteredData = [];
let currentPage = 1;
let rowsPerPage = 100;
// --- SIRALAMA (SORTING) DEĞİŞKENLERİ ---
let currentSortCol = null;
let sortAsc = true;
const PATH_LENGTH = 125.6;
document.getElementById('excelFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const pb = document.getElementById('progressBar');
pb.style.display = 'block';
pb.value = 10;
const reader = new FileReader();
reader.onload = function(event) {
pb.value = 40;
try {
const data = new Uint8Array(event.target.result);
const workbook = XLSX.read(data, {type: 'array'});
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
pb.value = 60;
const rawData = XLSX.utils.sheet_to_json(worksheet, {header: "A", defval: ""});
pb.value = 80;
if (rawData.length > 0) {
actualHeaders = rawData[0];
headerKeys = Object.keys(actualHeaders).filter(key => String(actualHeaders[key]).trim() !== "");
excelData = rawData.slice(1).filter(row => {
return headerKeys.some(key => String(row[key]).trim() !== "");
});
currentSortCol = null;
sortAsc = true;
currentPage = 1;
populateSlicerDropdown();
renderHeaders();
currentFilteredData = excelData;
renderPagination();
updateGauges(excelData.length, excelData.length);
updateProfileImage(currentFilteredData); // Yüklemede ilk kaydın resmini getir
document.getElementById('slicerArea').innerHTML = '';
} else {
alert("Dosyada veri bulunamadı!");
}
pb.value = 100;
setTimeout(() => { pb.style.display = 'none'; }, 500);
} catch (error) {
alert("Dosya okunurken bir hata oluştu: " + error.message);
pb.style.display = 'none';
}
};
reader.readAsArrayBuffer(file);
});
// YENİ FONKSİYON: FİLTRELENMİŞ VERİNİN İLK SATIRINDAKİ (A SÜTUNU) RESMİ GETİR
function updateProfileImage(filteredData) {
const imgEl = document.getElementById('profileImage');
if (filteredData && filteredData.length > 0) {
// A sütunu varsayılan olarak SheetJS tarafından "A" nesne anahtarı ile tutulur.
let firstVal = filteredData[0]["A"];
if (firstVal !== undefined && String(firstVal).trim() !== "") {
let cleanVal = String(firstVal).trim();
imgEl.style.display = 'none'; // Yenisi yüklenene kadar gizle
imgEl.src = `Resim/${cleanVal}.jpg`;
} else {
imgEl.src = "";
imgEl.style.display = 'none';
}
} else {
imgEl.src = "";
imgEl.style.display = 'none';
}
}
function renderHeaders() {
const thead = document.getElementById('tableHead');
thead.innerHTML = '';
let headerRow = document.createElement('tr');
headerKeys.forEach(k => {
let th = document.createElement('th');
th.className = 'main-header';
th.title = 'Sıralamak için tıklayın';
th.innerHTML = `<span>${actualHeaders[k]}</span><span class="sort-icon" id="sort_icon_${k}"></span>`;
th.addEventListener('click', () => {
if (currentSortCol === k) {
sortAsc = !sortAsc;
} else {
currentSortCol = k;
sortAsc = true;
}
updateSortIcons();
applyFiltersAndSlicers();
});
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
let filterRow = document.createElement('tr');
filterRow.className = 'filter-row no-print';
headerKeys.forEach(k => {
let th = document.createElement('th');
let input = document.createElement('input');
input.type = 'text';
input.placeholder = '🔍 Ara...';
input.dataset.col = k;
input.className = 'col-search-input';
input.addEventListener('input', applyFiltersAndSlicers);
th.appendChild(input);
filterRow.appendChild(th);
});
thead.appendChild(filterRow);
}
function updateSortIcons() {
headerKeys.forEach(k => {
const iconSpan = document.getElementById('sort_icon_' + k);
if (iconSpan) {
if (currentSortCol === k) {
iconSpan.innerHTML = sortAsc ? '▲' : '▼';
} else {
iconSpan.innerHTML = '';
}
}
});
}
function renderPagination() {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
const totalRows = currentFilteredData.length;
if (totalRows === 0) {
document.getElementById('paginationContainer').style.display = 'none';
return;
}
document.getElementById('paginationContainer').style.display = 'flex';
const totalPages = Math.ceil(totalRows / rowsPerPage);
if (currentPage > totalPages) currentPage = totalPages;
if (currentPage < 1) currentPage = 1;
const startIndex = (currentPage - 1) * rowsPerPage;
const endIndex = Math.min(startIndex + rowsPerPage, totalRows);
const pageData = currentFilteredData.slice(startIndex, endIndex);
pageData.forEach(row => {
let tr = document.createElement('tr');
headerKeys.forEach(k => {
let td = document.createElement('td');
td.textContent = row[k] !== undefined ? row[k] : '';
tr.appendChild(td);
});
tbody.appendChild(tr);
});
document.getElementById('pageInfo').innerText = `Toplam ${totalRows} kayıttan ${startIndex + 1} - ${endIndex} arası gösteriliyor.`;
document.getElementById('pageIndicator').innerText = `Sayfa ${currentPage} / ${totalPages}`;
}
window.changePage = function(dir) {
currentPage += dir;
renderPagination();
document.getElementById('tableContainer').scrollTo({ top: 0, behavior: 'smooth' });
}
window.changeRowsPerPage = function() {
rowsPerPage = parseInt(document.getElementById('rowsPerPage').value);
currentPage = 1;
renderPagination();
document.getElementById('tableContainer').scrollTo({ top: 0, behavior: 'smooth' });
}
window.exportToExcel = function() {
if (!currentFilteredData || currentFilteredData.length === 0) {
alert("Dışa aktarılacak veri bulunamadı.");
return;
}
const exportData = currentFilteredData.map(row => {
let cleanRow = {};
headerKeys.forEach(k => {
cleanRow[actualHeaders[k]] = row[k];
});
return cleanRow;
});
const ws = XLSX.utils.json_to_sheet(exportData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Filtrelenmis_Veri");
XLSX.writeFile(wb, "Envanter_Raporu.xlsx");
}
function populateSlicerDropdown() {
const select = document.getElementById('slicerColSelect');
select.innerHTML = '<option value="">-- Filtrelenecek Sütunu Seçin --</option>';
headerKeys.forEach(k => {
let opt = document.createElement('option');
opt.value = k;
opt.textContent = actualHeaders[k];
select.appendChild(opt);
});
}
document.getElementById('btnAddSlicer').addEventListener('click', function() {
const select = document.getElementById('slicerColSelect');
const colKey = select.value;
if (!colKey) return;
if (document.getElementById('slicer_' + colKey)) {
alert('Bu sütun için dilimleyici zaten açık!');
return;
}
const headerName = actualHeaders[colKey];
let uniqueVals = new Set();
excelData.forEach(row => {
let val = row[colKey] !== undefined ? String(row[colKey]).trim() : "";
uniqueVals.add(val);
});
let sortedVals = Array.from(uniqueVals).sort((a, b) => a.localeCompare(b, 'tr-TR', {numeric: true}));
const slicerArea = document.getElementById('slicerArea');
const box = document.createElement('div');
box.className = 'slicer-box';
box.id = 'slicer_' + colKey;
let listHtml = sortedVals.map(v => `
<label class="slicer-item" data-active="1">
<input type="checkbox" value="${v.replace(/"/g, '"')}" onchange="applyFiltersAndSlicers()">
${v === "" ? "<i>(Boş Kayıtlar)</i>" : v}
</label>
`).join('');
box.innerHTML = `
<div class="slicer-box-header">
<span class="slicer-title" data-name="${headerName}">${headerName} (${sortedVals.length})</span>
<button class="slicer-box-close" onclick="removeSlicer('${colKey}')">✖</button>
</div>
<div class="slicer-item-list">
${listHtml}
</div>
`;
slicerArea.appendChild(box);
select.value = "";
applyFiltersAndSlicers();
});
window.removeSlicer = function(colKey) {
const box = document.getElementById('slicer_' + colKey);
if (box) {
box.remove();
applyFiltersAndSlicers();
}
};
document.getElementById('btnClearSlicers').addEventListener('click', function() {
document.getElementById('slicerArea').innerHTML = '';
document.querySelectorAll('.col-search-input').forEach(inp => inp.value = '');
applyFiltersAndSlicers();
});
function updateSlicerUIs(slicerConds, searchConds) {
const boxes = document.querySelectorAll('.slicer-box');
boxes.forEach(box => {
const colKey = box.id.replace('slicer_', '');
let otherSlicerConds = slicerConds.filter(cond => cond.col !== colKey);
let subset = excelData;
if (otherSlicerConds.length > 0 || searchConds.length > 0) {
subset = subset.filter(row => {
let passesSlicer = otherSlicerConds.every(cond => {
let cellVal = row[cond.col] !== undefined ? String(row[cond.col]).trim() : "";
return cond.allowed.includes(cellVal);
});
if (!passesSlicer) return false;
let passesSearch = searchConds.every(cond => {
let cellVal = String(row[cond.col] || '').toLocaleLowerCase('tr-TR');
return cellVal.includes(cond.val);
});
return passesSearch;
});
}
let validVals = new Set();
subset.forEach(row => {
let val = row[colKey] !== undefined ? String(row[colKey]).trim() : "";
validVals.add(val);
});
const itemList = box.querySelector('.slicer-item-list');
const labels = Array.from(itemList.querySelectorAll('.slicer-item'));
let activeCount = 0;
labels.forEach(label => {
const input = label.querySelector('input');
const val = input.value;
if (validVals.has(val)) {
label.classList.remove('disabled-slicer');
label.dataset.active = "1";
activeCount++;
} else {
label.classList.add('disabled-slicer');
label.dataset.active = "0";
}
});
const titleSpan = box.querySelector('.slicer-title');
if (titleSpan) {
titleSpan.innerText = `${titleSpan.dataset.name} (${activeCount})`;
}
labels.sort((a, b) => {
const activeA = a.dataset.active;
const activeB = b.dataset.active;
if (activeA !== activeB) {
return activeB.localeCompare(activeA);
}
const valA = a.querySelector('input').value;
const valB = b.querySelector('input').value;
return valA.localeCompare(valB, 'tr-TR', {numeric: true});
});
labels.forEach(label => itemList.appendChild(label));
});
}
window.applyFiltersAndSlicers = function() {
const boxes = document.querySelectorAll('.slicer-box');
let slicerConds = [];
boxes.forEach(box => {
const colKey = box.id.replace('slicer_', '');
const checkedInputs = box.querySelectorAll('input[type="checkbox"]:checked');
if (checkedInputs.length > 0) {
let allowedVals = Array.from(checkedInputs).map(inp => inp.value);
slicerConds.push({ col: colKey, allowed: allowedVals });
}
});
const searchInputs = document.querySelectorAll('.col-search-input');
let searchConds = [];
searchInputs.forEach(inp => {
const val = inp.value.trim().toLocaleLowerCase('tr-TR');
if (val) { searchConds.push({ col: inp.dataset.col, val: val }); }
});
updateSlicerUIs(slicerConds, searchConds);
let dataToProcess = excelData;
if (slicerConds.length > 0 || searchConds.length > 0) {
dataToProcess = excelData.filter(row => {
let passesSlicer = slicerConds.every(cond => {
let cellVal = row[cond.col] !== undefined ? String(row[cond.col]).trim() : "";
return cond.allowed.includes(cellVal);
});
if (!passesSlicer) return false;
let passesSearch = searchConds.every(cond => {
let cellVal = String(row[cond.col] || '').toLocaleLowerCase('tr-TR');
return cellVal.includes(cond.val);
});
return passesSearch;
});
}
if (currentSortCol) {
dataToProcess = [...dataToProcess];
dataToProcess.sort((a, b) => {
let valA = a[currentSortCol] !== undefined ? String(a[currentSortCol]).trim() : "";
let valB = b[currentSortCol] !== undefined ? String(b[currentSortCol]).trim() : "";
let numA = parseFloat(valA.replace(',', '.'));
let numB = parseFloat(valB.replace(',', '.'));
let isNum = !isNaN(numA) && !isNaN(numB) && valA !== "" && valB !== "";
if (isNum) {
return sortAsc ? numA - numB : numB - numA;
} else {
return sortAsc
? valA.localeCompare(valB, 'tr-TR', { numeric: true })
: valB.localeCompare(valA, 'tr-TR', { numeric: true });
}
});
}
currentPage = 1;
currentFilteredData = dataToProcess;
renderPagination();
updateGauges(excelData.length, dataToProcess.length);
updateProfileImage(currentFilteredData); // Filtre sonrası dinamik olarak resmi güncelle
};
function updateGauges(total, filtered) {
document.getElementById('gTotal').textContent = total;
let percent = total === 0 ? 0 : Math.round((filtered / total) * 100);
document.getElementById('gPercent').textContent = '%' + percent;
document.getElementById('gFiltered').textContent = filtered;
document.getElementById('pathTotal').style.strokeDashoffset = total > 0 ? 0 : PATH_LENGTH;
let pOffset = PATH_LENGTH - (PATH_LENGTH * percent / 100);
document.getElementById('pathPercent').style.strokeDashoffset = isNaN(pOffset) ? PATH_LENGTH : pOffset;
let fOffset = PATH_LENGTH - (PATH_LENGTH * percent / 100);
document.getElementById('pathFiltered').style.strokeDashoffset = isNaN(fOffset) ? PATH_LENGTH : fOffset;
}
</script>
</body>
</html>
Geniş Resimli:
İsim alanında aynı isimde jpg dosyası ile başlıkta görsel yansır. (Html kodun bulunduğu yerde "Resim" isminde klasör olmalı.) A1 sütunundaki hücredeki isim, ürün vs. aynı isimde uzantı ".jpg" olduğunda başlık alanına resim yansır. (ilk satırdaki değer ile eşleşir.)
.png)
HTML Kod:
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Envanter Sistemi</title>
<!-- SheetJS kütüphanesi - Excel okuma ve yazma için -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
/* --- TEMEL DEĞİŞKENLER VE SIFIRLAMALAR --- */
:root {
--bg: #f0f2f5;
--shadow: 0 8px 24px rgba(0,0,0,.05);
--radius: 10px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Segoe UI", system-ui, Arial, sans-serif;
}
body {
background: var(--bg);
padding: 20px;
}
/* --- BAŞLIK ALANI STİLLERİ (SABİT) --- */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: #fff;
border: 1px solid #e1e4e8;
border-radius: var(--radius);
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
margin-bottom: 25px;
gap: 15px;
/* Sayfa kaydırıldığında ana başlığın sabit kalması için */
position: sticky;
top: 20px;
z-index: 1000;
}
.header .left {
display: flex;
align-items: center;
gap: 15px;
width: 30%;
}
.header .logo img {
height: 50px;
}
.header .title1 {
font-size: 22px;
font-weight: 800;
color: #1a1e23;
letter-spacing: -0.5px;
}
.header .title2 {
font-size: 12px;
color: #6a737d;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* --- ESTETİK VE TEKNOLOJİK GRAFİK BÖLÜMÜ (HUD STİLİ) --- */
.header .center-title {
flex-grow: 1;
display: flex;
justify-content: center;
}
.gauges-wrapper {
display: flex;
gap: 30px;
background: #fafbfc;
border: 1px solid #eef0f3;
padding: 12px 30px;
border-radius: 50px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.02), 0 4px 10px rgba(0,0,0,0.03);
align-items: center;
}
.tech-gauge {
display: flex;
flex-direction: column;
align-items: center;
width: 100px;
position: relative;
}
.tg-label {
font-size: 11px;
font-weight: 800;
color: #586069;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: -2px;
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap;
}
.tg-label.total::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #00c6ff; box-shadow: 0 0 6px #00c6ff; }
.tg-label.percent::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #ff416c; box-shadow: 0 0 6px #ff416c; }
.tg-label.filtered::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #11998e; box-shadow: 0 0 6px #11998e; }
.tech-gauge svg {
width: 100%;
overflow: visible;
filter: drop-shadow(0px 4px 4px rgba(0,0,0,0.1));
}
.tg-value {
font-size: 22px;
font-weight: 900;
fill: #24292e;
font-family: "Courier New", Courier, monospace;
}
/* YENİ: RESİM KUTUCUĞU STİLİ */
.profile-photo-box {
width: 130px; /* 65px'ten 130px'e çıkarılarak yatayda 2 katına büyütüldü */
height: 65px;
border-radius: 8px;
background: #fff;
border: 1px solid #e1e4e8;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
margin-left: 10px;
}
.profile-photo-box img {
width: 100%;
height: 100%;
object-fit: contain; /* Resmi kutuya sığacak şekilde ortalar */
display: none; /* onerror ve onload ile gösterilecek */
}
.header .right {
width: 30%;
font-size: 13px;
font-weight: 800;
text-align: right;
color: #24292e;
letter-spacing: 0.5px;
}
/* --- KONTROL & DOSYA YÜKLEME PANELİ --- */
.controls {
display: flex;
gap: 15px;
align-items: center;
background: #fff;
padding: 15px;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 15px;
border-left: 5px solid #00c6ff;
}
input[type="file"], button, select {
padding: 8px 15px;
border: 1px solid #d1d5da;
border-radius: 6px;
font-size: 13px;
outline: none;
}
input[type="file"]:focus, select:focus { border-color: #00c6ff; }
.btn-primary {
background: linear-gradient(135deg, #007bff, #0056b3);
color: white; border: none; cursor: pointer; font-weight: bold; transition: 0.3s;
box-shadow: 0 4px 10px rgba(0, 123, 255, 0.3);
}
.btn-primary:hover { box-shadow: 0 6px 15px rgba(0, 123, 255, 0.5); transform: translateY(-1px); }
.btn-danger {
background: linear-gradient(135deg, #dc3545, #a71d2a);
color: white; border: none; cursor: pointer; font-weight: bold; transition: 0.3s;
box-shadow: 0 4px 10px rgba(220, 53, 69, 0.3);
}
.btn-danger:hover { box-shadow: 0 6px 15px rgba(220, 53, 69, 0.5); transform: translateY(-1px); }
.btn-success {
background: linear-gradient(135deg, #28a745, #218838);
color: white; border: none; cursor: pointer; font-weight: bold; transition: 0.3s;
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.btn-success:hover { box-shadow: 0 6px 15px rgba(40, 167, 69, 0.5); transform: translateY(-1px); }
progress { width: 150px; height: 18px; display: none; border-radius: 10px; }
progress::-webkit-progress-bar { background-color: #f0f2f5; border-radius: 10px; }
progress::-webkit-progress-value { background: linear-gradient(90deg, #00c6ff, #0072ff); border-radius: 10px; }
/* --- EXCEL DİLİMLEYİCİ (SLICER) ALANI --- */
.slicer-container {
background: #f8f9fc;
padding: 20px;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 20px;
border: 1px solid #e1e4e8;
}
.slicer-header-bar {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.slicer-header-bar span { font-weight: 800; color: #0056b3; font-size: 14px; }
select#slicerColSelect { min-width: 200px; }
.slicer-area { display: flex; flex-wrap: wrap; gap: 15px; }
.slicer-box {
border: 1px solid #d1d5da;
border-radius: 8px;
background: #fff;
width: 220px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 10px rgba(0,0,0,0.04);
}
.slicer-box-header {
background: #24292e;
color: white;
padding: 10px 12px;
font-size: 13px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
.slicer-box-close {
cursor: pointer; color: #ff6b6b; font-weight: bold; border: none; background: transparent; font-size: 15px;
}
.slicer-item-list {
max-height: 180px; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 5px;
}
.slicer-item {
display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 6px; cursor: pointer; border-radius: 4px; transition: 0.2s;
}
.slicer-item:hover { background: #f0f2f5; }
/* FİLTRE DIŞI KALAN DİLİMLEYİCİ ELEMANLARI İÇİN CSS */
.slicer-item.disabled-slicer {
color: #adb5bd;
}
.slicer-item.disabled-slicer input {
opacity: 0.4;
}
/* --- TABLO VE BAŞLIK ÜSTÜ ARAMA STİLLERİ --- */
.table-container {
width: 100%;
max-height: 65vh;
overflow: auto;
background: #fff;
border-radius: var(--radius);
box-shadow: var(--shadow);
position: relative;
}
table { width: max-content; min-width: 100%; border-collapse: separate; border-spacing: 0; }
th, td { border-bottom: 1px solid #e1e4e8; border-right: 1px solid #e1e4e8; padding: 12px; text-align: left; font-size: 13px; white-space: nowrap; }
th:first-child, td:first-child { border-left: 1px solid #e1e4e8; }
thead {
position: sticky;
top: 0;
z-index: 20;
background: #fff;
box-shadow: 0 4px 6px rgba(0,0,0,0.06);
}
thead th.main-header {
background: #1a1e23 !important;
color: white !important;
font-weight: bold;
border-top: 1px solid #1a1e23;
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
thead th.main-header:hover {
background: #2c3238 !important;
}
.filter-row { background-color: #f8f9fa; }
.filter-row th { padding: 6px; border-bottom: 1px solid #e1e4e8; background: #f8f9fa; }
.col-search-input {
width: 100%; min-width: 100px; padding: 8px; font-size: 12px; border: 1px solid #ced4da; border-radius: 4px; box-sizing: border-box; outline: none; transition: 0.2s;
}
.col-search-input:focus { border-color: #00c6ff; box-shadow: 0 0 0 3px rgba(0, 198, 255, 0.15); }
.sort-icon {
font-size: 11px;
margin-left: 5px;
color: #00c6ff;
display: inline-block;
width: 12px;
}
tbody tr:nth-child(even) { background-color: #f8f9fc; }
tbody tr:hover { background-color: #e6f2ff; }
/* --- SAYFALAMA (PAGINATION) STİLLERİ --- */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #fff;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-top: 15px;
}
.pagination-controls button { margin: 0 5px; }
/* --- YAZDIRMA (PRINT) VE PDF STİLLERİ --- */
@media print {
.no-print { display: none !important; }
body { background: white; padding: 0; margin: 0; }
.header { box-shadow: none; border-color: #000; margin-bottom: 20px; position: static; }
.table-container { box-shadow: none; border-radius: 0; overflow: visible; max-height: none; }
table { width: 100%; max-width: 100%; font-size: 10px; }
th, td { white-space: nowrap !important; padding: 6px; }
thead { position: static; box-shadow: none; }
thead th.main-header { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
@page { size: landscape; margin: 10mm; @bottom-right { content: "Sayfa " counter(page) " / " counter(pages); font-size: 10px; } }
}
</style>
</head>
<body>
<!-- SVG GRADYAN TANIMLAMALARI -->
<svg style="width:0; height:0; position:absolute;" aria-hidden="true" focusable="false">
<defs>
<linearGradient id="gradTotal" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#00c6ff" />
<stop offset="100%" stop-color="#0072ff" />
</linearGradient>
<linearGradient id="gradPercent" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#ff416c" />
<stop offset="100%" stop-color="#ff4b2b" />
</linearGradient>
<linearGradient id="gradFiltered" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#11998e" />
<stop offset="100%" stop-color="#38ef7d" />
</linearGradient>
</defs>
</svg>
<!-- BAŞLIK ALANI BAŞLANGICI -->
<div class="header">
<div class="left">
<div class="logo"><img src="logo.jpg" alt="Logo" onerror="this.style.display='none'"></div>
<div>
<div class="title1">Envanter</div>
<div class="title2">Kullanıcı Destek Koordinatörlüğü</div>
</div>
</div>
<div class="center-title">
<div class="gauges-wrapper">
<!-- 1. TOPLAM VERİ -->
<div class="tech-gauge">
<div class="tg-label total">Adet</div>
<svg viewBox="0 0 100 55">
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#e1e4e8" stroke-width="14" stroke-linecap="round" />
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#ffffff" stroke-width="14" stroke-dasharray="2 4" />
<path id="pathTotal" d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="url(#gradTotal)" stroke-width="10" stroke-linecap="round"
style="stroke-dasharray: 125.6; stroke-dashoffset: 0; transition: stroke-dashoffset 1s ease-out;" />
<text x="50" y="47" text-anchor="middle" class="tg-value" id="gTotal">0</text>
</svg>
</div>
<!-- 2. YÜZDE ORANI -->
<div class="tech-gauge">
<div class="tg-label percent">Oran</div>
<svg viewBox="0 0 100 55">
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#e1e4e8" stroke-width="14" stroke-linecap="round" />
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#ffffff" stroke-width="14" stroke-dasharray="2 4" />
<path id="pathPercent" d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="url(#gradPercent)" stroke-width="10" stroke-linecap="round"
style="stroke-dasharray: 125.6; stroke-dashoffset: 125.6; transition: stroke-dashoffset 1s ease-out;" />
<text x="50" y="47" text-anchor="middle" class="tg-value" id="gPercent">%0</text>
</svg>
</div>
<!-- 3. FİLTRELENEN -->
<div class="tech-gauge">
<div class="tg-label filtered">Filtre Adet</div>
<svg viewBox="0 0 100 55">
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#e1e4e8" stroke-width="14" stroke-linecap="round" />
<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#ffffff" stroke-width="14" stroke-dasharray="2 4" />
<path id="pathFiltered" d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="url(#gradFiltered)" stroke-width="10" stroke-linecap="round"
style="stroke-dasharray: 125.6; stroke-dashoffset: 125.6; transition: stroke-dashoffset 1s ease-out;" />
<text x="50" y="47" text-anchor="middle" class="tg-value" id="gFiltered">0</text>
</svg>
</div>
<!-- 4. YENİ: KAYIT PROFİL FOTOĞRAFI -->
<div class="profile-photo-box no-print">
<!-- Resim varsa gösterilecek, yoksa onerror fonksiyonu ile görünmez olacak -->
<img id="profileImage" src="" alt="Kayıt Fotoğrafı" onerror="this.style.display='none'" onload="this.style.display='block'">
</div>
</div>
</div>
<div class="right">TEKNİK İŞLETME DAİRESİ BAŞKANLIĞI</div>
</div>
<!-- DOSYA YÜKLEME VE YAZDIRMA -->
<div class="controls no-print">
<input type="file" id="excelFile" accept=".xls,.xlsx">
<progress id="progressBar" value="0" max="100"></progress>
<div style="margin-left: auto; display: flex; gap: 10px;">
<button class="btn-success" type="button" onclick="exportToExcel()">📥 Excel İndir</button>
<button class="btn-primary" type="button" onclick="window.print()">🖨️ PDF / Yazdır</button>
</div>
</div>
<!-- EXCEL DİLİMLEYİCİ (SLICER) ALANI -->
<div class="slicer-container no-print">
<div class="slicer-header-bar">
<span>🎛️ Veri Dilimleyici Ekle:</span>
<select id="slicerColSelect">
<option value="">-- Önce Excel Yükleyin --</option>
</select>
<button class="btn-primary" id="btnAddSlicer">+ Ekle</button>
<div style="margin-left: auto; display: flex; flex-direction: column; align-items: flex-end;">
<button class="btn-danger" id="btnClearSlicers">✖ Tüm Filtreleri Temizle</button>
<span style="font-size: 10px; color: #ced4da; margin-top: 5px; font-weight: 600; letter-spacing: 0.5px;">AI Web Designer: Muhammed Yusuf OLGUN</span>
</div>
</div>
<div id="slicerArea" class="slicer-area"></div>
</div>
<!-- TABLO ALANI -->
<div class="table-container" id="tableContainer">
<table id="dataTable">
<thead id="tableHead"></thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<!-- SAYFALAMA KONTROLLERİ -->
<div class="pagination-container no-print" id="paginationContainer" style="display:none;">
<div id="pageInfo" style="font-size: 13px; font-weight: bold; color: #586069;"></div>
<div class="pagination-controls">
<button class="btn-primary" onclick="changePage(-1)">Önceki</button>
<span id="pageIndicator" style="margin: 0 10px; font-weight: bold; color: #24292e;"></span>
<button class="btn-primary" onclick="changePage(1)">Sonraki</button>
</div>
<div>
<select id="rowsPerPage" onchange="changeRowsPerPage()">
<option value="50">Sayfada 50 Kayıt</option>
<option value="100" selected>Sayfada 100 Kayıt</option>
<option value="500">Sayfada 500 Kayıt</option>
<option value="1000">Sayfada 1000 Kayıt</option>
</select>
</div>
</div>
<script>
let excelData = [];
let actualHeaders = {};
let headerKeys = [];
// --- SAYFALAMA VE FİLTRELENMİŞ VERİ DEĞİŞKENLERİ ---
let currentFilteredData = [];
let currentPage = 1;
let rowsPerPage = 100;
// --- SIRALAMA (SORTING) DEĞİŞKENLERİ ---
let currentSortCol = null;
let sortAsc = true;
const PATH_LENGTH = 125.6;
document.getElementById('excelFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const pb = document.getElementById('progressBar');
pb.style.display = 'block';
pb.value = 10;
const reader = new FileReader();
reader.onload = function(event) {
pb.value = 40;
try {
const data = new Uint8Array(event.target.result);
const workbook = XLSX.read(data, {type: 'array'});
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
pb.value = 60;
const rawData = XLSX.utils.sheet_to_json(worksheet, {header: "A", defval: ""});
pb.value = 80;
if (rawData.length > 0) {
actualHeaders = rawData[0];
headerKeys = Object.keys(actualHeaders).filter(key => String(actualHeaders[key]).trim() !== "");
excelData = rawData.slice(1).filter(row => {
return headerKeys.some(key => String(row[key]).trim() !== "");
});
currentSortCol = null;
sortAsc = true;
currentPage = 1;
populateSlicerDropdown();
renderHeaders();
currentFilteredData = excelData;
renderPagination();
updateGauges(excelData.length, excelData.length);
updateProfileImage(currentFilteredData); // Yüklemede ilk kaydın resmini getir
document.getElementById('slicerArea').innerHTML = '';
} else {
alert("Dosyada veri bulunamadı!");
}
pb.value = 100;
setTimeout(() => { pb.style.display = 'none'; }, 500);
} catch (error) {
alert("Dosya okunurken bir hata oluştu: " + error.message);
pb.style.display = 'none';
}
};
reader.readAsArrayBuffer(file);
});
// YENİ FONKSİYON: FİLTRELENMİŞ VERİNİN İLK SATIRINDAKİ (A SÜTUNU) RESMİ GETİR
function updateProfileImage(filteredData) {
const imgEl = document.getElementById('profileImage');
if (filteredData && filteredData.length > 0) {
// A sütunu varsayılan olarak SheetJS tarafından "A" nesne anahtarı ile tutulur.
let firstVal = filteredData[0]["A"];
if (firstVal !== undefined && String(firstVal).trim() !== "") {
let cleanVal = String(firstVal).trim();
imgEl.style.display = 'none'; // Yenisi yüklenene kadar gizle
imgEl.src = `Resim/${cleanVal}.jpg`;
} else {
imgEl.src = "";
imgEl.style.display = 'none';
}
} else {
imgEl.src = "";
imgEl.style.display = 'none';
}
}
function renderHeaders() {
const thead = document.getElementById('tableHead');
thead.innerHTML = '';
let headerRow = document.createElement('tr');
headerKeys.forEach(k => {
let th = document.createElement('th');
th.className = 'main-header';
th.title = 'Sıralamak için tıklayın';
th.innerHTML = `<span>${actualHeaders[k]}</span><span class="sort-icon" id="sort_icon_${k}"></span>`;
th.addEventListener('click', () => {
if (currentSortCol === k) {
sortAsc = !sortAsc;
} else {
currentSortCol = k;
sortAsc = true;
}
updateSortIcons();
applyFiltersAndSlicers();
});
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
let filterRow = document.createElement('tr');
filterRow.className = 'filter-row no-print';
headerKeys.forEach(k => {
let th = document.createElement('th');
let input = document.createElement('input');
input.type = 'text';
input.placeholder = '🔍 Ara...';
input.dataset.col = k;
input.className = 'col-search-input';
input.addEventListener('input', applyFiltersAndSlicers);
th.appendChild(input);
filterRow.appendChild(th);
});
thead.appendChild(filterRow);
}
function updateSortIcons() {
headerKeys.forEach(k => {
const iconSpan = document.getElementById('sort_icon_' + k);
if (iconSpan) {
if (currentSortCol === k) {
iconSpan.innerHTML = sortAsc ? '▲' : '▼';
} else {
iconSpan.innerHTML = '';
}
}
});
}
function renderPagination() {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
const totalRows = currentFilteredData.length;
if (totalRows === 0) {
document.getElementById('paginationContainer').style.display = 'none';
return;
}
document.getElementById('paginationContainer').style.display = 'flex';
const totalPages = Math.ceil(totalRows / rowsPerPage);
if (currentPage > totalPages) currentPage = totalPages;
if (currentPage < 1) currentPage = 1;
const startIndex = (currentPage - 1) * rowsPerPage;
const endIndex = Math.min(startIndex + rowsPerPage, totalRows);
const pageData = currentFilteredData.slice(startIndex, endIndex);
pageData.forEach(row => {
let tr = document.createElement('tr');
headerKeys.forEach(k => {
let td = document.createElement('td');
td.textContent = row[k] !== undefined ? row[k] : '';
tr.appendChild(td);
});
tbody.appendChild(tr);
});
document.getElementById('pageInfo').innerText = `Toplam ${totalRows} kayıttan ${startIndex + 1} - ${endIndex} arası gösteriliyor.`;
document.getElementById('pageIndicator').innerText = `Sayfa ${currentPage} / ${totalPages}`;
}
window.changePage = function(dir) {
currentPage += dir;
renderPagination();
document.getElementById('tableContainer').scrollTo({ top: 0, behavior: 'smooth' });
}
window.changeRowsPerPage = function() {
rowsPerPage = parseInt(document.getElementById('rowsPerPage').value);
currentPage = 1;
renderPagination();
document.getElementById('tableContainer').scrollTo({ top: 0, behavior: 'smooth' });
}
window.exportToExcel = function() {
if (!currentFilteredData || currentFilteredData.length === 0) {
alert("Dışa aktarılacak veri bulunamadı.");
return;
}
const exportData = currentFilteredData.map(row => {
let cleanRow = {};
headerKeys.forEach(k => {
cleanRow[actualHeaders[k]] = row[k];
});
return cleanRow;
});
const ws = XLSX.utils.json_to_sheet(exportData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Filtrelenmis_Veri");
XLSX.writeFile(wb, "Envanter_Raporu.xlsx");
}
function populateSlicerDropdown() {
const select = document.getElementById('slicerColSelect');
select.innerHTML = '<option value="">-- Filtrelenecek Sütunu Seçin --</option>';
headerKeys.forEach(k => {
let opt = document.createElement('option');
opt.value = k;
opt.textContent = actualHeaders[k];
select.appendChild(opt);
});
}
document.getElementById('btnAddSlicer').addEventListener('click', function() {
const select = document.getElementById('slicerColSelect');
const colKey = select.value;
if (!colKey) return;
if (document.getElementById('slicer_' + colKey)) {
alert('Bu sütun için dilimleyici zaten açık!');
return;
}
const headerName = actualHeaders[colKey];
let uniqueVals = new Set();
excelData.forEach(row => {
let val = row[colKey] !== undefined ? String(row[colKey]).trim() : "";
uniqueVals.add(val);
});
let sortedVals = Array.from(uniqueVals).sort((a, b) => a.localeCompare(b, 'tr-TR', {numeric: true}));
const slicerArea = document.getElementById('slicerArea');
const box = document.createElement('div');
box.className = 'slicer-box';
box.id = 'slicer_' + colKey;
let listHtml = sortedVals.map(v => `
<label class="slicer-item" data-active="1">
<input type="checkbox" value="${v.replace(/"/g, '"')}" onchange="applyFiltersAndSlicers()">
${v === "" ? "<i>(Boş Kayıtlar)</i>" : v}
</label>
`).join('');
box.innerHTML = `
<div class="slicer-box-header">
<span class="slicer-title" data-name="${headerName}">${headerName} (${sortedVals.length})</span>
<button class="slicer-box-close" onclick="removeSlicer('${colKey}')">✖</button>
</div>
<div class="slicer-item-list">
${listHtml}
</div>
`;
slicerArea.appendChild(box);
select.value = "";
applyFiltersAndSlicers();
});
window.removeSlicer = function(colKey) {
const box = document.getElementById('slicer_' + colKey);
if (box) {
box.remove();
applyFiltersAndSlicers();
}
};
document.getElementById('btnClearSlicers').addEventListener('click', function() {
document.getElementById('slicerArea').innerHTML = '';
document.querySelectorAll('.col-search-input').forEach(inp => inp.value = '');
applyFiltersAndSlicers();
});
function updateSlicerUIs(slicerConds, searchConds) {
const boxes = document.querySelectorAll('.slicer-box');
boxes.forEach(box => {
const colKey = box.id.replace('slicer_', '');
let otherSlicerConds = slicerConds.filter(cond => cond.col !== colKey);
let subset = excelData;
if (otherSlicerConds.length > 0 || searchConds.length > 0) {
subset = subset.filter(row => {
let passesSlicer = otherSlicerConds.every(cond => {
let cellVal = row[cond.col] !== undefined ? String(row[cond.col]).trim() : "";
return cond.allowed.includes(cellVal);
});
if (!passesSlicer) return false;
let passesSearch = searchConds.every(cond => {
let cellVal = String(row[cond.col] || '').toLocaleLowerCase('tr-TR');
return cellVal.includes(cond.val);
});
return passesSearch;
});
}
let validVals = new Set();
subset.forEach(row => {
let val = row[colKey] !== undefined ? String(row[colKey]).trim() : "";
validVals.add(val);
});
const itemList = box.querySelector('.slicer-item-list');
const labels = Array.from(itemList.querySelectorAll('.slicer-item'));
let activeCount = 0;
labels.forEach(label => {
const input = label.querySelector('input');
const val = input.value;
if (validVals.has(val)) {
label.classList.remove('disabled-slicer');
label.dataset.active = "1";
activeCount++;
} else {
label.classList.add('disabled-slicer');
label.dataset.active = "0";
}
});
const titleSpan = box.querySelector('.slicer-title');
if (titleSpan) {
titleSpan.innerText = `${titleSpan.dataset.name} (${activeCount})`;
}
labels.sort((a, b) => {
const activeA = a.dataset.active;
const activeB = b.dataset.active;
if (activeA !== activeB) {
return activeB.localeCompare(activeA);
}
const valA = a.querySelector('input').value;
const valB = b.querySelector('input').value;
return valA.localeCompare(valB, 'tr-TR', {numeric: true});
});
labels.forEach(label => itemList.appendChild(label));
});
}
window.applyFiltersAndSlicers = function() {
const boxes = document.querySelectorAll('.slicer-box');
let slicerConds = [];
boxes.forEach(box => {
const colKey = box.id.replace('slicer_', '');
const checkedInputs = box.querySelectorAll('input[type="checkbox"]:checked');
if (checkedInputs.length > 0) {
let allowedVals = Array.from(checkedInputs).map(inp => inp.value);
slicerConds.push({ col: colKey, allowed: allowedVals });
}
});
const searchInputs = document.querySelectorAll('.col-search-input');
let searchConds = [];
searchInputs.forEach(inp => {
const val = inp.value.trim().toLocaleLowerCase('tr-TR');
if (val) { searchConds.push({ col: inp.dataset.col, val: val }); }
});
updateSlicerUIs(slicerConds, searchConds);
let dataToProcess = excelData;
if (slicerConds.length > 0 || searchConds.length > 0) {
dataToProcess = excelData.filter(row => {
let passesSlicer = slicerConds.every(cond => {
let cellVal = row[cond.col] !== undefined ? String(row[cond.col]).trim() : "";
return cond.allowed.includes(cellVal);
});
if (!passesSlicer) return false;
let passesSearch = searchConds.every(cond => {
let cellVal = String(row[cond.col] || '').toLocaleLowerCase('tr-TR');
return cellVal.includes(cond.val);
});
return passesSearch;
});
}
if (currentSortCol) {
dataToProcess = [...dataToProcess];
dataToProcess.sort((a, b) => {
let valA = a[currentSortCol] !== undefined ? String(a[currentSortCol]).trim() : "";
let valB = b[currentSortCol] !== undefined ? String(b[currentSortCol]).trim() : "";
let numA = parseFloat(valA.replace(',', '.'));
let numB = parseFloat(valB.replace(',', '.'));
let isNum = !isNaN(numA) && !isNaN(numB) && valA !== "" && valB !== "";
if (isNum) {
return sortAsc ? numA - numB : numB - numA;
} else {
return sortAsc
? valA.localeCompare(valB, 'tr-TR', { numeric: true })
: valB.localeCompare(valA, 'tr-TR', { numeric: true });
}
});
}
currentPage = 1;
currentFilteredData = dataToProcess;
renderPagination();
updateGauges(excelData.length, dataToProcess.length);
updateProfileImage(currentFilteredData); // Filtre sonrası dinamik olarak resmi güncelle
};
function updateGauges(total, filtered) {
document.getElementById('gTotal').textContent = total;
let percent = total === 0 ? 0 : Math.round((filtered / total) * 100);
document.getElementById('gPercent').textContent = '%' + percent;
document.getElementById('gFiltered').textContent = filtered;
document.getElementById('pathTotal').style.strokeDashoffset = total > 0 ? 0 : PATH_LENGTH;
let pOffset = PATH_LENGTH - (PATH_LENGTH * percent / 100);
document.getElementById('pathPercent').style.strokeDashoffset = isNaN(pOffset) ? PATH_LENGTH : pOffset;
let fOffset = PATH_LENGTH - (PATH_LENGTH * percent / 100);
document.getElementById('pathFiltered').style.strokeDashoffset = isNaN(fOffset) ? PATH_LENGTH : fOffset;
}
</script>
</body>
</html>
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)