Proje bazlı Zaman Planı - Gantt Şeması

Hızlı ve pratik olarak Gantt şeması nasıl oluşturabilir ve kullanabilirim. Her defasında Excel de uzun süreler harcamadan kolayca ulaşabileceğim, bir defa kafa sağlam kafa yorayım, sonrasında birkaç dakikada hazırlanan bir yapı elde edeyim, düşüncesiyle ortaya çıkan bir çalışmadır.

Belirli zaman aralığında yapılan çalışmaları görme, Harcanan zamanları bilme, Bir sürecin birden fazla aşamalarını aynı anda takip edebil işlemlerini, Pratiklik sunan bir uygulama olarak "Html olarak hazırlandı." İnternet olmadan, çift tıklamayla çalışan bir uygulamadır.

Html kodu, Not defterine yapıştırınız. dosya ismi verdikten sonra uzantıyı ".html" olarak kaydetmen yeterli olacaktır. İstediğiniz browser ile açabilirsiniz. Sayfanın olduğu yerde "logo.jpg" ismi ve uzantısıyla resim olması ile, başlık alanına logo görülmektedir.

Gantt için, Excel şablonu oluşturulup, koda gömüldü. "Şablon" butonuna basarak indirebilir ve içeriğini değiştirerek Ekleme, Çıkarma yaparak düzenleyebilirsiniz. Sonrasında "Excel yükle" ile seçip, verinin sayfa içerisine aktarılmasını sağlayın.

"Başlık" butonuna tıklarsanız, Başlık belirir. İçeriğe uygun başlık ekleyebilirsiniz.

"Şablon" butonuna basıldığında, şablon indirebilirsiniz. indirdiğinizde tablo görünümü.

"Excel Yükle" butonuna bastığınızda, Excel içeriğine göre, Zaman çizgileriyle dashboard oluşur.

"Bugün" butonuna bastığınızda zaman çizgisini, Aktif/Pasif edebilirsiniz.

"Yazdı/PDF" butonuna bastığınızda, belgeyi Yazdırabilir veya PDF olarak kaydedebilirsiniz.


HTML Kod:
<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Proje bazlı Zaman Planı</title>
    
    <!-- Excel okumak ve yazmak için SheetJS -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
    
    <style>
        /* Siyah Resmi Yazı ve Genel Ayarlar */
        body {
            font-family: 'Segoe UI', Arial, sans-serif;
            background-color: #f0f4f8;
            margin: 0;
            padding: 8px;
            color: #000000;
        }
        
        /* Yatay Sayfaya Sığan Geniş Konteyner */
        .container {
            width: 100%;
            max-width: 1600px;
            margin: 0 auto;
            background: #ffffff;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.05);
            box-sizing: border-box;
        }
        
        /* Üst Kurumsal Başlık ve Alan */
        .exec-header { 
            display: flex; 
            justify-content: space-between; 
            align-items: center; 
            padding: 10px 15px; 
            background: #ffffff; 
            border: 1px solid #cbd5e1; 
            border-left: 6px solid #000000;
            border-radius: 6px; 
            box-shadow: 0 4px 12px rgba(0,0,0,0.04); 
            margin-bottom: 12px; 
            position: relative;
            box-sizing: border-box;
        }
        
        .header-left-group {
            display: flex;
            align-items: center;
            gap: 15px;
        }
        .header-logo {
            height: 40px;
            object-fit: contain;
        }

        .header-title {
            display: flex;
            flex-direction: column;
            justify-content: center;
        }

        /* Ana Başlık (Değiştirilemez) */
        .header-title .title1 { 
            font-size: 14px; 
            font-weight: 800; 
            color: #000000; 
            text-transform: uppercase; 
            display: flex;
            align-items: center;
            justify-content: flex-start;
            min-height: 24px;
        }

        .header-title .title2 { 
            font-size: 10px; 
            color: #333333; 
            font-weight: 600; 
            margin-top: 2px; 
        }

        /* Ortadaki Başlık Alanı: Arka plan yok, çerçeve yok */
        .header-center-custom {
            position: absolute;
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .custom-center-input {
            font-size: 15px;
            font-weight: 800;
            color: #000000;
            background: transparent;
            border: none;
            padding: 2px 6px;
            outline: none;
            text-align: center;
            min-width: 220px;
        }

        /* Sağ Alan ve Kontroller */
        .header-right { 
            display: flex;
            flex-direction: column;
            align-items: flex-end;
            gap: 4px;
        }
        .header-dept { 
            font-size: 10px; 
            font-weight: 700; 
            text-align: right; 
            color: #000000; 
        }

        .controls { 
            display: flex; 
            gap: 6px; 
            justify-content: flex-end; 
            align-items: center; 
        }

        .btn { padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer; font-weight: 700; font-size: 10px; transition: 0.2s; height: 24px; display: inline-flex; align-items: center; box-sizing: border-box; }
        
        /* Başlık Butonu: Arka Planı Beyaz */
        .btn-title-add { background: #ffffff; color: #1e293b; border: 1px solid #cbd5e1; }
        .btn-title-add:hover { background: #f8fafc; }

        .btn-excel { background: #e0f2fe; color: #0369a1; border: 1px solid #bae6fd; }
        .btn-excel:hover { background: #bae6fd; }
        .btn-print { background: #dcfce7; color: #15803d; border: 1px solid #bbf7d0; }
        .btn-print:hover { background: #bbf7d0; }
        
        /* BUGÜN Butonu (Açık Kırmızı) */
        .btn-today { background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; }
        .btn-today:hover { background: #fecaca; }

        /* Şablon Butonu (Açık Gri) */
        .btn-template { background: #e2e8f0; color: #1e293b; border: 1px solid #cbd5e1; }
        .btn-template:hover { background: #cbd5e1; }
        
        input[type="file"] { display: none; }
        #errorMsg { color: #dc2626; text-align: center; font-weight: bold; margin-bottom: 10px; }

        /* İKİNCİ TABLO (Gantt Konteyner) - Eşit Kalınlıkta Tek Parça Dış Çerçeve */
        .gantt-container {
            display: grid;
            grid-template-columns: auto 140px 1fr;
            width: 100%;
            border: 2px solid #000000;
            border-radius: 6px;
            background: #ffffff;
            box-shadow: 0 1px 3px rgba(0,0,0,0.05);
            overflow: hidden; /* Dış çerçeve sınırlarının kusursuz korunması için */
            position: relative;
            box-sizing: border-box;
        }
        
        /* Subgrid ile En Uzun Veriye Göre Eş Zamanlı Sola/Sağa Esneme */
        .gantt-header, .gantt-row, .gantt-axis-row {
            display: grid;
            grid-template-columns: subgrid;
            grid-column: 1 / -1;
            box-sizing: border-box;
            align-items: center;
        }

        .gantt-header {
            background: #f1f5f9;
            font-weight: 800;
            border-bottom: 2px solid #000000;
            padding: 8px 0;
            text-transform: uppercase;
            font-size: 10.5px;
        }
        
        .gantt-row {
            border-bottom: 1px solid #cbd5e1;
            padding: 6px 0;
            font-size: 11px;
            font-weight: 600;
            transition: background 0.2s;
            position: relative;
        }
        .gantt-row:hover { background-color: #f8fafc; }

        /* İLK SATIR (Geçen Süre) ARKA PLAN RENGİ AÇIK GRİ */
        .gantt-row-summary {
            background-color: #e5e7eb !important;
        }

        /* SÜTUN HİZALAMALARI */
        .gantt-col-name { 
            padding: 0 10px 0 12px; 
            text-align: left; 
            word-break: break-word; 
            border-right: 1px solid #cbd5e1;
            box-sizing: border-box;
        }
        
        .gantt-col-dates { 
            padding: 0 10px 0 10px; 
            text-align: left; 
            white-space: nowrap; 
            border-right: 1px solid #cbd5e1;
            box-sizing: border-box;
        }
        
        .gantt-col-timeline { 
            padding: 0 16px 0 10px; 
            position: relative; 
            box-sizing: border-box;
        }

        /* Zaman Çizgisi Rayı */
        .gantt-track {
            position: relative;
            width: 100%;
            height: 22px;
            background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
            border-radius: 11px;
            border: 1px solid #cbd5e1;
            box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
        }

        /* BAR ÖZELLİKLERİ */
        .gantt-bar {
            position: absolute;
            top: 0;
            height: 100%;
            border-radius: 11px;
            box-shadow: 2px 0 4px rgba(0,0,0,0.15);
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.5s ease;
        }

        .gantt-bar-blue {
            background: linear-gradient(180deg, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0.15) 40%, rgba(0,0,0,0.05) 100%),
                        linear-gradient(90deg, #3b82f6 0%, #93c5fd 100%);
        }

        .gantt-bar-green {
            background: linear-gradient(180deg, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0.15) 40%, rgba(0,0,0,0.05) 100%),
                        linear-gradient(90deg, #10b981 0%, #6ee7b7 100%);
        }

        .timeline-value-text {
            font-size: 10.5px;
            font-weight: 800;
            color: #000000;
            text-shadow: 0px 0px 4px #ffffff, 0px 0px 2px #ffffff;
            white-space: nowrap;
            padding: 0 4px;
        }

        /* DİNAMİK TAKVİM EKSENİ */
        .gantt-axis-row {
            background: #f8fafc;
            border-top: none;
            padding: 6px 0 12px 0;
            font-size: 9.5px;
            font-weight: 700;
            color: #334155;
            align-items: flex-start;
        }
        .gantt-axis-timeline {
            padding: 0 35px 0 6px; 
            position: relative;
            height: 22px;
            box-sizing: border-box;
        }
        .axis-tick {
            position: absolute;
            transform: translateX(-50%);
            white-space: nowrap;
            text-align: center;
            padding-top: 2px;
        }
        .axis-tick::before {
            content: "|";
            display: block;
            text-align: center;
            font-size: 10px;
            color: #64748b;
            line-height: 8px;
            margin-bottom: 1px;
            font-weight: 800;
        }

        /* BUGÜN İŞARETÇİSİ */
        .today-line {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 2px;
            background-color: #000000;
            z-index: 20;
            display: none;
        }
        .today-badge-container {
            position: absolute;
            top: -34px;
            transform: translateX(-50%);
            text-align: center;
            z-index: 25;
            display: none;
            white-space: nowrap;
            padding-bottom: 4px;
        }
        .today-badge {
            background-color: #dc2626;
            color: #ffffff;
            font-size: 9px;
            font-weight: 800;
            padding: 2px 6px;
            border-radius: 3px;
            text-transform: uppercase;
            box-shadow: 0 1px 3px rgba(0,0,0,0.2);
        }
        .today-date-sub {
            font-size: 8.5px;
            font-weight: 700;
            color: #000000;
            margin-top: 2px;
            text-shadow: 0 0 2px #ffffff;
        }

        /* YAZDIRMA VE ÖN İZLEME */
        @media print {
            @page { 
                size: A4 landscape; 
                margin: 3mm; 
            }
            .no-print { display: none !important; }
            
            body, html { 
                background: #ffffff !important; 
                padding: 0 !important; 
                margin: 0 !important; 
                width: 100% !important;
                height: 100% !important;
            }
            
            .container { 
                box-shadow: none !important; 
                padding: 0 !important; 
                margin: 0 auto !important; 
                width: 100% !important; 
                max-width: 100% !important; 
                border: none !important;
                box-sizing: border-box !important;
            }
            
            .exec-header { 
                width: 100% !important;
                box-sizing: border-box !important;
                margin-top: 0 !important;
                margin-bottom: 6px !important; 
                border: 1px solid #000 !important; 
                border-left: 6px solid #000 !important; 
                box-shadow: none !important; 
                padding: 8px 12px !important;
            }
            
            .gantt-container { 
                overflow: hidden !important; 
                width: 100% !important;
                box-sizing: border-box !important;
                border: 2px solid #000 !important; 
                page-break-inside: avoid !important; 
                break-inside: avoid !important;
            }
            
            .gantt-row, .gantt-axis-row, .gantt-header { 
                page-break-inside: avoid !important; 
                break-inside: avoid !important; 
                box-sizing: border-box !important;
            }

            .gantt-row-summary {
                background-color: #e5e7eb !important;
                -webkit-print-color-adjust: exact !important;
                print-color-adjust: exact !important;
            }
            
            * { 
                -webkit-print-color-adjust: exact !important; 
                color-adjust: exact !important; 
                print-color-adjust: exact !important; 
            }
        }
    </style>
</head>
<body>

<div class="container">
    
    <!-- Başlık Alanı ve Kontroller -->
    <div class="exec-header">
        <div class="header-left-group">
            <img src="logo.jpg" alt="Logo" class="header-logo" onerror="this.style.display='none'">
            
            <div class="header-title">
                <div class="title1">Proje bazlı Zaman Planı</div>
                <div class="title2">Kullanıcı Destek Koordinatörlüğü</div>
            </div>
        </div>

        <!-- Başlık Butonuna Basılınca Excel Yükle ile Başlık Arasında (Dikey/Yatay Ortada) Beliren Alan -->
        <div id="dynamicCenterArea" class="header-center-custom"></div>

        <div class="header-right">
            <div class="header-dept">TEKNİK İŞLETME DAİRESİ BAŞKANLIĞI</div>
            <div class="controls no-print">
                <input type="file" id="fileInput" accept=".xlsx, .xls">
                <button class="btn btn-excel" onclick="document.getElementById('fileInput').click()">📊 Excel Yükle</button>
                <button class="btn btn-today" id="todayToggleBtn" onclick="toggleTodayMarker()">BUGÜN</button>
                <button class="btn btn-print" onclick="window.print()">🖨️ Yazdır / PDF</button>
                <button class="btn btn-template" onclick="downloadTemplate()">Şablon</button>
                <!-- Başlık Butonu: Arka Planı Beyaz -->
                <button class="btn btn-title-add" onclick="enableCenterTitleInput()">Başlık</button>
            </div>
        </div>
    </div>
    
    <div id="errorMsg"></div>
    <div id="gantt_chart">
        <div style="text-align:center; padding: 25px; color:#64748b; font-size:12px; border: 1px dashed #cbd5e1; border-radius: 6px;">
            Lütfen sağ üstteki "Excel Yükle" butonuna basarak dosya seçin veya "Şablon" butonuna basarak örnek şablonu indirin.
        </div>
    </div>
</div>

<script>
    let isTodayVisible = false;

    function enableCenterTitleInput() {
        let area = document.getElementById('dynamicCenterArea');
        if (!area.querySelector('input')) {
            area.innerHTML = '<input type="text" class="custom-center-input" placeholder="Başlık Yazın..." autofocus>';
            let input = area.querySelector('input');
            input.focus();
        }
    }

    function toggleTodayMarker() {
        isTodayVisible = !isTodayVisible;
        let lines = document.querySelectorAll('.today-line');
        let badges = document.querySelectorAll('.today-badge-container');
        
        lines.forEach(line => {
            line.style.display = isTodayVisible ? 'block' : 'none';
        });
        badges.forEach(badge => {
            badge.style.display = isTodayVisible ? 'block' : 'none';
        });
    }

    function downloadTemplate() {
        var templateData = [
            ["Çalışma", "Başlangıç Tarihi", "Bitiş Tarihi", "Geçen Süre"],
            ["Geçen Süre", "1.06.2025", "10.10.2026", 496],
            ["Çalışma 1", "16.06.2026", "17.09.2026", 93],
            ["Çalışma 2", "17.06.2025", "20.08.2025", 64],
            ["Çalışma 3", "23.06.2025", "27.08.2025", 65],
            ["Çalışma 4", "24.06.2025", "14.09.2025", 82],
            ["Çalışma 5", "25.07.2025", "6.10.2025", 73],
            ["Çalışma 6", "1.07.2025", "21.09.2026", 447],
            ["Çalışma 7", "3.07.2025", "15.09.2025", 74],
            ["Çalışma 8", "28.07.2025", "9.10.2025", 73]
        ];

        var ws = XLSX.utils.aoa_to_sheet(templateData);
        var wb = XLSX.utils.book_new();
        XLSX.utils.book_append_sheet(wb, ws, "Zaman Planı");
        XLSX.writeFile(wb, "Sablon.xlsx");
    }

    document.getElementById('fileInput').addEventListener('change', function(e) {
        var file = e.target.files[0];
        if (!file) return;

        document.getElementById('errorMsg').textContent = "";
        
        var reader = new FileReader();
        reader.onload = function(event) {
            try {
                var data = new Uint8Array(event.target.result);
                var workbook = XLSX.read(data, {type: 'array', cellDates: true});
                var firstSheetName = workbook.SheetNames[0];
                var worksheet = workbook.Sheets[firstSheetName];
                
                var json = XLSX.utils.sheet_to_json(worksheet, {header: 1, raw: false, dateNF: 'yyyy-mm-dd'});
                
                if(json.length < 2) {
                    showError("Dosyada işlenecek yeterli veri bulunamadı.");
                    return;
                }
                
                buildCustomGantt(json);
            } catch (err) {
                showError("Excel okunurken hata oluştu: " + err.message);
            }
        };
        reader.onerror = function() { showError("Dosya okunurken bir hata oluştu."); };
        reader.readAsArrayBuffer(file);
    });

    function showError(msg) {
        document.getElementById('errorMsg').textContent = msg;
        document.getElementById('gantt_chart').innerHTML = "";
    }

    function parseDateCustom(val) {
        if (!val) return null;
        if (val instanceof Date) return val;
        
        let s = String(val).trim();
        if (s.includes('.')) {
            let parts = s.split('.');
            if(parts.length === 3) {
                let y = parseInt(parts[2], 10);
                if (y < 100) y = 2000 + y; 
                if (y === 205) y = 2025;
                return new Date(y, parseInt(parts[1], 10) - 1, parseInt(parts[0], 10));
            }
        } 
        let d = new Date(s);
        return isNaN(d) ? null : d;
    }

    function formatDate(val) {
        if (!val) return "-";
        if (val instanceof Date) {
            let d = String(val.getDate()).padStart(2, '0');
            let m = String(val.getMonth() + 1).padStart(2, '0');
            let y = val.getFullYear();
            return `${d}.${m}.${y}`;
        }
        return String(val);
    }

    function buildCustomGantt(rows) {
        var headers = rows[0];
        var calismaIdx = -1, baslangicIdx = -1, bitisIdx = -1, sureIdx = -1;
        
        for (let i = 0; i < headers.length; i++) {
            if(!headers[i]) continue;
            let h = String(headers[i]).toLowerCase().replace(/\s+/g, '');
            if (h.includes('çalışma') && !h.includes('detay')) calismaIdx = i;
            if (h.includes('başlangıç')) baslangicIdx = i;
            if (h.includes('bitiş')) bitisIdx = i;
            if (h.includes('süre') || h.includes('geçen')) sureIdx = i;
        }

        if (calismaIdx === -1 || baslangicIdx === -1 || bitisIdx === -1) {
            showError("Gerekli sütunlar (Çalışma, Başlangıç, Bitiş) bulunamadı.");
            return;
        }

        let parsedTasks = [];
        let minDate = null;
        let maxDate = null;

        for (var i = 1; i < rows.length; i++) {
            var row = rows[i];
            if (!row || row.length === 0) continue;
            
            var calisma = row[calismaIdx];
            var baslangic = parseDateCustom(row[baslangicIdx]);
            var bitis = parseDateCustom(row[bitisIdx]);
            var sureVal = sureIdx !== -1 && row[sureIdx] ? String(row[sureIdx]).trim() : null;
            
            if (calisma && baslangic && bitis && !isNaN(baslangic) && !isNaN(bitis)) {
                
                if (!minDate || baslangic < minDate) minDate = baslangic;
                if (!maxDate || bitis > maxDate) maxDate = bitis;

                let calculatedDays = Math.ceil(Math.abs(bitis - baslangic) / (1000 * 60 * 60 * 24));
                if (calculatedDays === 0) calculatedDays = 1;
                
                let numericSure = (sureVal && sureVal !== "NaN" && sureVal !== "") ? parseFloat(sureVal) : calculatedDays;
                
                parsedTasks.push({
                    isim: calisma,
                    baslangic: baslangic,
                    bitis: bitis,
                    sureStr: numericSure + " Gün"
                });
            }
        }

        if (parsedTasks.length === 0) {
            showError("Grafiğe dökülecek geçerli tarih verisi bulunamadı.");
            return;
        }

        let totalProjectMs = maxDate.getTime() - minDate.getTime();
        if (totalProjectMs === 0) totalProjectMs = 86400000; 

        let today = new Date(2026, 7, 19); 
        today.setHours(0,0,0,0);
        let todayPercent = ((today.getTime() - minDate.getTime()) / totalProjectMs) * 100;
        if (todayPercent < 0) todayPercent = 0;
        if (todayPercent > 100) todayPercent = 100;
        let formattedTodayStr = formatDate(today);

        let html = `
            <div class="gantt-container">
                <div class="gantt-header">
                    <div class="gantt-col-name">Çalışma İçeriği</div>
                    <div class="gantt-col-dates">Başlangıç / Bitiş</div>
                    <div class="gantt-col-timeline">Zaman Çizelgesi</div>
                </div>
        `;

        parsedTasks.forEach((task, index) => {
            let leftOffset = ((task.baslangic.getTime() - minDate.getTime()) / totalProjectMs) * 100;
            let widthPercent = ((task.bitis.getTime() - task.baslangic.getTime()) / totalProjectMs) * 100;
            
            if (widthPercent < 2) widthPercent = 2; 
            if (leftOffset + widthPercent > 100) widthPercent = 100 - leftOffset;
            
            let barColorClass = (index === 0) ? "gantt-bar-blue" : "gantt-bar-green";
            let rowExtraClass = (index === 0) ? "gantt-row-summary" : "";

            let badgeHTML = '';
            if (index === 0) {
                badgeHTML = `
                    <div class="today-badge-container" style="left: ${todayPercent}%; display: ${isTodayVisible ? 'block' : 'none'};">
                        <div class="today-badge">BUGÜN</div>
                        <div class="today-date-sub">${formattedTodayStr}</div>
                    </div>
                `;
            }

            html += `
                <div class="gantt-row ${rowExtraClass}">
                    ${badgeHTML}
                    <div class="today-line" style="left: ${todayPercent}%; display: ${isTodayVisible ? 'block' : 'none'};"></div>
                    <div class="gantt-col-name">${task.isim}</div>
                    <div class="gantt-col-dates">${formatDate(task.baslangic)} - ${formatDate(task.bitis)}</div>
                    <div class="gantt-col-timeline">
                        <div class="gantt-track">
                            <div class="gantt-bar ${barColorClass}" style="left: ${leftOffset}%; width: ${widthPercent}%;">
                                <span class="timeline-value-text">${task.sureStr}</span>
                            </div>
                        </div>
                    </div>
                </div>
            `;
        });

        let axisTicksHTML = '';
        let tickCount = 5; 
        for (let i = 0; i <= tickCount; i++) {
            let tickTime = minDate.getTime() + (totalProjectMs * (i / tickCount));
            let tickDate = new Date(tickTime);
            let percentPos = (i / tickCount) * 100;
            
            let transformStyle = "translateX(-50%)";
            if (i === 0) transformStyle = "translateX(0%)";
            if (i === tickCount) transformStyle = "translateX(-100%)";

            axisTicksHTML += `<div class="axis-tick" style="left: ${percentPos}%; transform: ${transformStyle};">${formatDate(tickDate)}</div>`;
        }

        html += `
            <div class="gantt-axis-row">
                <div class="gantt-col-name" style="font-weight: 850; border-right: 1px solid #cbd5e1;"></div>
                <div class="gantt-col-dates" style="border-right: 1px solid #cbd5e1;"></div>
                <div class="gantt-axis-timeline">
                    ${axisTicksHTML}
                </div>
            </div>
        `;

        html += `</div>`;
        document.getElementById('gantt_chart').innerHTML = html;
    }
</script>
</body>
</html>