Simple Event

CRM Pro

Panele kullanıcı adı ve şifreyle giriş yapın.

all the great things are simple.

İhaleler

İhale süreçlerinizi yönetin, takip edin ve kazanın.

Müşteriler

Müşteri bilgilerinizi düzenleyin ve ilişkileri güçlendirin.

Görevler

Görevlerinizi planlayın, atayın ve takip edin.

Raporlar

Performansı analiz edin, raporlarla yönetin.

Geçici şifreyle giriş yapan kullanıcılar devam etmeden önce yeni şifre belirler.

İşlem başarılı.
'; } async function printCostSheet() { const p = getProject(activeProjectId); if (!p) return showToast("Önce bir işin maliyet cetvelini açın."); const win = window.open("", "_blank"); if (!win) return showToast("Yazdırma penceresi açılamadı. Tarayıcı açılır pencereye izin vermeli."); win.document.open(); win.document.write('HazırlanıyorMaliyet cetveli hazırlanıyor... '); win.document.close(); try { const logoData = await firstImageDataUrl(["offer-assets/logo-top.png", "logo-top.png", "simple-event-logo.png"], EMBEDDED_OFFER_IMAGES.topLogo); win.document.open(); win.document.write(buildCostPrintHtml(p, logoData)); win.document.close(); win.focus(); setTimeout(() => win.print(), 450); addLog("Maliyet cetveli yazdırdı", p, { detail: { kalem: (p.costs || []).length } }); showToast("Maliyet cetveli yazdırma ekranı açıldı."); } catch (error) { console.error(error); win.document.open(); win.document.write(buildCostPrintHtml(p, "")); win.document.close(); win.focus(); setTimeout(() => win.print(), 450); } } function closeLabel(type) { return type === "lost" ? "İhale kaybedildi" : type === "completed" ? "İhale alındı / iş tamamlandı" : "-"; } function isCompletedProject(p) { return p && p.durum === "kapanan" && (p.closeType || "completed") === "completed"; } function isLostProject(p) { return p && p.durum === "kapanan" && p.closeType === "lost"; } function hasPendingCloseRequest(p) { return p?.closeRequest?.status === "pending"; } function hasRejectedCloseRequest(p) { return p?.closeRequest?.status === "rejected"; } function pendingCloseRequests() { return projects.filter(p => hasPendingCloseRequest(p)); } function updateCloseRequestBadge() { const btn = document.getElementById("btn-approvals"); if (!btn) return; const count = pendingCloseRequests().length; btn.textContent = "Kapatma Talepleri" + (count ? " (" + count + ")" : ""); } async function createCloseRequest(id, type, note = "") { const p = getProject(id); if (!p) return; if (hasPendingCloseRequest(p)) return showToast("Bu iş için kapatma talebi zaten gönderilmiş."); const oldState = closeStateSnapshot(p); const request = normalizeCloseRequest({ status: "pending", type, note: note || p.closeNote || "", requestedBy: currentUser ? currentUser.name : "Kullanıcı", requestedAt: new Date().toISOString() }); p.closeRequest = request; p.closeNote = note || p.closeNote || ""; const logDetails = { old: oldState, next: closeStateSnapshot(p) }; save(PROJECTS_KEY, projects); addLog("Kapatma talebi oluşturdu: " + closeLabel(type), p, logDetails); renderLists(); renderCloseRequests(); updateCloseRequestBadge(); if (sharedMode) { try { const result = await apiPostJson("api/close-request.php", { action: "create", projectId: p.id, type, note: p.closeNote, project: p }); applyServerSnapshot(result, result.message || "Kapatma talebi admin onayına gönderildi."); } catch (error) { try { const synced = await syncServerStateNow({ projects }); showToast(synced ? "Talep sunucuya genel veri olarak kaydedildi; admin yenileyince görebilir." : "Talep bu cihazda bekliyor; sunucuya gönderilemedi."); } catch (syncError) { showToast("Talep bu cihazda bekliyor; sunucuya gönderilemedi: " + (error.message || "Bağlantı hatası")); } } return; } showToast("Kapatma talebi admin onayına gönderildi."); } function renderCloseRequests() { updateCloseRequestBadge(); const list = document.getElementById("close-request-list"); if (!list) return; list.innerHTML = ""; const requests = pendingCloseRequests().sort((a, b) => String(a.closeRequest.requestedAt).localeCompare(String(b.closeRequest.requestedAt))); if (!requests.length) return list.appendChild(empty("Bekleyen kapatma talebi yok.")); requests.forEach(p => { const request = p.closeRequest; const div = document.createElement("div"); div.className = "close-request-card"; div.innerHTML = '

'; div.querySelector("h3").textContent = p.kurum + " - " + p.ad; div.querySelectorAll("p")[0].textContent = closeLabel(request.type) + " için admin onayı bekliyor."; div.querySelectorAll("p")[1].textContent = request.note || "Talep notu yok."; const meta = div.querySelector(".close-request-meta"); meta.innerHTML = ''; meta.children[0].textContent = "Talep eden: " + (request.requestedBy || "-"); meta.children[1].textContent = "Talep zamanı: " + new Date(request.requestedAt).toLocaleString("tr-TR"); meta.children[2].textContent = "İş tarihi: " + rangeText(p); const actions = div.querySelector(".actions"); actions.append(button("Detay", "btn-small btn-soft", () => openDetail(p.id))); if (request.type === "completed") { actions.append(button("İş Bitirmeyi Gör", "btn-small btn-primary", () => { if (reportReady(p.completionReport)) openCompletionReport(p.id); else openCompletionReportForm(p.id, false); })); } actions.append( button("Reddet", "btn-small btn-danger", () => rejectCloseRequest(p.id)), button("Onayla ve Kapat", "btn-small btn-success", () => approveCloseRequest(p.id)) ); list.appendChild(div); }); } async function approveCloseRequest(id) { if (!requireAdmin("Kapatma talebini onaylamak için admin yetkisi gerekir.")) return; const p = getProject(id); if (!p || !hasPendingCloseRequest(p)) return showToast("Bekleyen talep bulunamadı."); const request = p.closeRequest; const oldState = closeStateSnapshot(p); if (request.type === "completed" && !reportReady(p.completionReport)) { return showToast("Bu iş tamamlandı olarak kapanmadan önce iş bitirme raporu tamamlanmalı."); } if (sharedMode) { try { const result = await apiPostJson("api/close-request.php", { action: "approve", projectId: p.id }); applyServerSnapshot(result, result.message || "Talep onaylandı ve iş kapatıldı."); switchView("closed"); } catch (error) { showToast(error.message || "Kapatma talebi onaylanamadı."); } return; } p.durum = "kapanan"; p.closeType = request.type; p.closedAt = keyDate(new Date()); p.closeNote = request.note || p.closeNote || ""; p.closeRequest = normalizeCloseRequest({ ...request, status: "approved", reviewedBy: currentUser?.name || "Admin", reviewedAt: new Date().toISOString() }); if (persist()) { addLog("Kapatma talebini onayladı: " + closeLabel(p.closeType), p, { old: oldState, next: closeStateSnapshot(p) }); notifyCloseRequestResult(p, "approved"); showToast("Talep onaylandı ve iş kapatıldı."); renderCloseRequests(); switchView("closed"); } } async function rejectCloseRequest(id) { if (!requireAdmin("Kapatma talebini reddetmek için admin yetkisi gerekir.")) return; const p = getProject(id); if (!p || !hasPendingCloseRequest(p)) return showToast("Bekleyen talep bulunamadı."); if (!confirmDelete("Bu kapatma talebini reddetmek istediğinize emin misiniz?")) return; const request = p.closeRequest; const oldState = closeStateSnapshot(p); if (sharedMode) { try { const result = await apiPostJson("api/close-request.php", { action: "reject", projectId: p.id }); applyServerSnapshot(result, result.message || "Kapatma talebi reddedildi."); } catch (error) { showToast(error.message || "Kapatma talebi reddedilemedi."); } return; } p.closeRequest = normalizeCloseRequest({ ...request, status: "rejected", reviewedBy: currentUser?.name || "Admin", reviewedAt: new Date().toISOString() }); if (persist()) { addLog("Kapatma talebini reddetti: " + closeLabel(request.type), p, { old: oldState, next: closeStateSnapshot(p) }); notifyCloseRequestResult(p, "rejected"); showToast("Kapatma talebi reddedildi."); renderCloseRequests(); } } function openCloseModal(id) { const p = getProject(id); if (!p) return; if (!isAdmin() && hasPendingCloseRequest(p)) return showToast("Bu iş için zaten bekleyen kapatma talebi var."); closeProjectId = id; document.querySelectorAll("input[name='close-type']").forEach(r => { r.checked = r.value === (p.closeType || "completed"); }); const note = document.getElementById("close-note"); if (note) note.value = p.closeNote || ""; document.getElementById("close-project-modal").classList.add("show"); } function closeCloseModal() { document.getElementById("close-project-modal").classList.remove("show"); closeProjectId = null; } function confirmCloseProject() { const selected = document.querySelector("input[name='close-type']:checked")?.value || "completed"; const note = document.getElementById("close-note")?.value.trim() || ""; if (selected === "completed") { const id = closeProjectId; closeCloseModal(); openCompletionReportForm(id, true, note); return; } if (!isAdmin()) { createCloseRequest(closeProjectId, selected, note); closeCloseModal(); return; } updateStatus(closeProjectId, "kapanan", selected, note); closeCloseModal(); } function updateStatus(id, status, closeType = "", closeNote = "") { const p = getProject(id); if (!p) return; if (status === "takip" && p.durum === "kapanan" && !isAdmin()) return showToast("Kapanan işleri sadece admin tekrar takibe alabilir."); if (status === "kapanan" && !closeType) return openCloseModal(id); if (status === "kapanan" && !isAdmin()) { if (closeType === "completed") return openCompletionReportForm(id, true, closeNote); return createCloseRequest(id, closeType || "lost", closeNote); } if (status === "kapanan" && (closeType || "completed") === "completed" && !reportReady(p.completionReport)) { return openCompletionReportForm(id, true, closeNote); } const oldState = closeStateSnapshot(p); p.durum = status; if (status === "kapanan") { p.closeType = closeType || p.closeType || "completed"; p.closedAt = p.closedAt || keyDate(new Date()); p.closeNote = closeNote || p.closeNote || ""; p.closeRequest = p.closeRequest?.status === "pending" ? normalizeCloseRequest({ ...p.closeRequest, status: "approved", reviewedBy: currentUser?.name || "Admin", reviewedAt: new Date().toISOString() }) : p.closeRequest; } else { p.closeType = ""; p.closedAt = ""; p.closeNote = ""; p.closeRequest = null; } persist(); addLog(status === "kapanan" ? "İşi kapattı: " + closeLabel(p.closeType) : "İşi takibe aldı", p, { old: oldState, next: closeStateSnapshot(p) }); switchView(status === "kapanan" ? "closed" : "active"); } function monthKeyFromDate(value) { const d = parseDateKey(value); return d ? d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") : ""; } function monthLabel(key) { const [year, month] = String(key).split("-").map(Number); return month ? MONTHS[month - 1] + " " + year : "-"; } function lastMonthKeys(count = 6) { const now = new Date(); return Array.from({ length: count }, (_, i) => { const d = new Date(now.getFullYear(), now.getMonth() - (count - 1 - i), 1); return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0"); }); } function profitMonthKeyForProject(p) { if (!p) return ""; // Kâr/zarar ayı kapatma tarihinden değil, işin gerçekleştiği/iş bitirme raporuna konu olan tarihten alınır. // Aksi halde eski aylara ait işler CRM'e bugün kapatıldığında hepsi mevcut aya yığılır. const report = p.completionReport && typeof p.completionReport === "object" ? p.completionReport : {}; return monthKeyFromDate(p.isBitis || p.isBaslangic || p.isTarihi || report.completedAt || report.updatedAt || p.closedAt); } function lostMonthKeyForProject(p) { if (!p) return ""; return monthKeyFromDate(p.ihaleTarihi || p.isBitis || p.isBaslangic || p.isTarihi || p.closedAt); } function profitMonthKeys() { const keys = new Set(lastMonthKeys(6)); projects.forEach(p => { const key = isCompletedProject(p) ? profitMonthKeyForProject(p) : (isLostProject(p) ? lostMonthKeyForProject(p) : ""); if (key) keys.add(key); }); return Array.from(keys).sort((a, b) => a.localeCompare(b)); } function monthlyProfitRows() { const keys = profitMonthKeys(); return keys.map(key => { const completed = projects.filter(p => isCompletedProject(p) && profitMonthKeyForProject(p) === key); const lost = projects.filter(p => isLostProject(p) && lostMonthKeyForProject(p) === key); const sales = completed.reduce((sum, p) => sum + completionReportTotals(p).income, 0); const cost = completed.reduce((sum, p) => sum + completionReportTotals(p).expense, 0); const profit = sales - cost; const margin = sales ? profit / sales * 100 : 0; return { key, label: monthLabel(key), completed: completed.length, lost: lost.length, sales, cost, profit, margin }; }).filter(row => row.completed || row.lost || row.sales || row.cost || row.profit || lastMonthKeys(6).includes(row.key)); } function renderAdminProfitDashboard() { const card = document.getElementById("admin-profit-card"); if (!card || !isAdmin()) return; const rows = monthlyProfitRows(); const totalsRow = rows.reduce((acc, row) => ({ completed: acc.completed + row.completed, lost: acc.lost + row.lost, sales: acc.sales + row.sales, cost: acc.cost + row.cost, profit: acc.profit + row.profit, }), { completed: 0, lost: 0, sales: 0, cost: 0, profit: 0 }); const margin = totalsRow.sales ? totalsRow.profit / totalsRow.sales * 100 : 0; const kpi = document.getElementById("profit-kpi-grid"); if (kpi) { kpi.innerHTML = ""; [["Tamamlanan İş", totalsRow.completed], ["Kaybedilen İhale", totalsRow.lost], ["Toplam Satış", money(totalsRow.sales)], ["Toplam Maliyet", money(totalsRow.cost)], ["Net Kâr/Zarar", money(totalsRow.profit) + " • " + percent(margin)]].forEach(item => { const div = document.createElement("div"); div.className = "profit-kpi"; div.innerHTML = ""; div.querySelector("span").textContent = item[0]; div.querySelector("strong").textContent = item[1]; if (String(item[0]).includes("Kâr") && totalsRow.profit < 0) div.querySelector("strong").className = "profit-bad"; kpi.appendChild(div); }); } const maxAbs = Math.max(1, ...rows.map(r => Math.abs(r.profit))); const bars = document.getElementById("profit-bars"); if (bars) { bars.innerHTML = rows.map(row => '
' + escapeHtml(row.label.replace(" ", "\n")) + '
' + money(row.profit) + '
').join(""); } const body = document.getElementById("profit-month-body"); if (body) { body.innerHTML = rows.map(row => '' + escapeHtml(row.label) + '' + row.completed + '' + row.lost + '' + money(row.sales) + '' + money(row.cost) + '' + money(row.profit) + '' + percent(row.margin) + '').join("") || 'Kâr/zarar verisi yok.'; } } async function downloadMonthlyProfitExcel() { if (!requireAdmin("Aylık kâr/zarar raporunu sadece admin indirebilir.")) return; const rows = [["Ay", "Tamamlanan İş", "Kaybedilen İhale", "Satış", "Maliyet", "Net Kar/Zarar", "Kar Oranı"]]; monthlyProfitRows().forEach(r => rows.push([r.label, r.completed, r.lost, r.sales, r.cost, r.profit, r.margin.toFixed(2)])); const blob = await buildXlsxWorkbook([{ name: "Aylık Kar Zarar", rows }]); download(blob, "simple-event-aylik-kar-zarar.xlsx"); addLog("Aylık kâr/zarar Excel indirdi"); } function downloadMonthlyProfitCsv() { downloadMonthlyProfitExcel(); } const completionCoreColumns = { expense: [ { key: "rowNo", title: "No", width: "46px" }, { key: "firma", title: "Firma" }, { key: "aciklama", title: "Açıklama" }, { key: "faturaNo", title: "Fatura No" }, { key: "tutar", title: "Tutar", width: "130px", type: "number" }, { key: "odemeSekli", title: "Ödeme Şekli" }, { key: "odemeVadesi", title: "Ödeme Vadesi", type: "date" }, { key: "notlar", title: "Notlar" } ], income: [ { key: "rowNo", title: "No", width: "46px" }, { key: "firma", title: "Firma veya Kurum" }, { key: "aciklama", title: "Açıklama" }, { key: "tutar", title: "Tutar", width: "130px", type: "number" }, { key: "nakit", title: "Nakit Tahsilat", width: "130px", type: "number" }, { key: "kart", title: "K. Kartı Tahsilat", width: "130px", type: "number" }, { key: "kalan", title: "Kalan Bakiye", width: "130px", type: "number", readonly: true }, { key: "notlar", title: "Notlar" } ], advance: [ { key: "rowNo", title: "No", width: "46px" }, { key: "firma", title: "Firma" }, { key: "tarih", title: "Tarih", width: "135px", type: "date" }, { key: "aciklama", title: "Açıklama" }, { key: "tutar", title: "Tutar", width: "130px", type: "number" } ] }; const completionColumnState = { expense: [], income: [], advance: [] }; function completionColumnKey(type) { return type === "advance" ? "advanceColumns" : type + "Columns"; } function completionBodyId(type) { return type === "advance" ? "cr-advance-spend-body" : "cr-" + type + "-body"; } function renderCompletionHeaders(type, extras = completionColumnState[type] || []) { const head = document.getElementById("cr-" + type + "-head"); if (!head) return; completionColumnState[type] = [...extras]; const core = completionCoreColumns[type] || []; head.innerHTML = core.map(col => '
' + escapeHtml(col.title) + '
').join("") + completionColumnState[type].map((title, index) => '
' + escapeHtml(title || "Yeni Sütun") + '
').join("") + 'Sil'; } function readCompletionExtraColumns(type) { const head = document.getElementById("cr-" + type + "-head"); if (!head) return completionColumnState[type] || []; return [...head.querySelectorAll("th[data-extra-index] .completion-th-title")].map(x => x.textContent.trim() || "Yeni Sütun"); } function addCompletionColumn(type) { const r = readCompletionReportForm(); const key = completionColumnKey(type); r[key].push("Yeni Sütun"); if (type === "expense") renderCompletionExpenseRows(r.expenses, r.expenseColumns); if (type === "income") renderCompletionIncomeRows(r.incomes, r.incomeColumns); if (type === "advance") renderCompletionAdvanceSpendRows(r.advanceSpends, r.advanceColumns); recalcCompletionReportForm(); } function removeCompletionColumn(type, index) { if (!confirmDelete("Bu sütunu silmek istediğinize emin misiniz?")) return; const r = readCompletionReportForm(); const key = completionColumnKey(type); r[key].splice(index, 1); const rowsKey = type === "advance" ? "advanceSpends" : type + "s"; (r[rowsKey] || []).forEach(row => { if (Array.isArray(row.extra)) row.extra.splice(index, 1); }); if (type === "expense") renderCompletionExpenseRows(r.expenses, r.expenseColumns); if (type === "income") renderCompletionIncomeRows(r.incomes, r.incomeColumns); if (type === "advance") renderCompletionAdvanceSpendRows(r.advanceSpends, r.advanceColumns); recalcCompletionReportForm(); } function compactDateLabel(date) { if (!date) return "-"; const d = parseDateKey(date); if (!d) return "-"; return String(d.getDate()).padStart(2,"0") + " " + MONTHS[d.getMonth()].slice(0,3); } function upcomingOpenTasks(limit = Infinity) { const items = []; projects.forEach(p => { (p.tasks || []).forEach(t => { if (!t.done) items.push({ project: p, task: t }); }); }); const sorted = items .sort((a,b) => String(a.task.date || "9999-12-31").localeCompare(String(b.task.date || "9999-12-31")) || String(a.project.kurum).localeCompare(String(b.project.kurum))); return Number.isFinite(limit) ? sorted.slice(0, limit) : sorted; } function taskDateClass(date) { if (!date) return ""; const today = keyDate(new Date()); if (date < today) return "overdue"; if (date === today) return "today"; return ""; } function showUpcomingTasksPopup() { if (taskPopupShown || !currentUser) return; taskPopupShown = true; const modal = document.getElementById("upcoming-tasks-modal"); const list = document.getElementById("login-task-popup-list"); if (!modal || !list) return; const items = upcomingOpenTasks(10); list.innerHTML = ""; if (!items.length) { list.appendChild(empty("Açık yaklaşan görev bulunmuyor.")); } else { items.forEach(item => { const div = document.createElement("div"); div.className = "task-popup-item " + taskDateClass(item.task.date); div.innerHTML = '
'; div.querySelector(".agenda-date").textContent = compactDateLabel(item.task.date); div.querySelector("strong").textContent = item.task.text; div.querySelector("span").textContent = item.project.kurum + " • " + item.project.ad; div.querySelector("button").onclick = () => { closeUpcomingTasksPopup(); openDetail(item.project.id); }; list.appendChild(div); }); } modal.classList.add("show"); } function closeUpcomingTasksPopup() { document.getElementById("upcoming-tasks-modal")?.classList.remove("show"); } function renderHomePlanner() { const jobs = document.getElementById("upcoming-jobs-list"); const tasks = document.getElementById("upcoming-tasks-list"); if (jobs) { jobs.innerHTML = ""; const today = keyDate(new Date()); const upcoming = projects .filter(p => p.durum === "takip" && String(p.isBitis || p.isBaslangic || p.ihaleTarihi || "9999-12-31") >= today) .sort((a,b) => String(a.isBaslangic || a.ihaleTarihi).localeCompare(String(b.isBaslangic || b.ihaleTarihi))) .slice(0, 8); if (!upcoming.length) jobs.appendChild(empty("Yaklaşan aktif iş yok.")); upcoming.forEach(p => { const div = document.createElement("div"); div.className = "agenda-item"; div.innerHTML = '
'; div.querySelector(".agenda-date").textContent = compactDateLabel(p.isBaslangic || p.ihaleTarihi); div.querySelector("strong").textContent = p.kurum; div.querySelector("span").textContent = p.ad + " • " + rangeText(p) + " • " + (p.isYeri || "-"); div.querySelector("button").onclick = () => openDetail(p.id); jobs.appendChild(div); }); } if (tasks) { tasks.innerHTML = ""; const allTasks = upcomingOpenTasks(); if (!allTasks.length) tasks.appendChild(empty("Açık görev yok.")); allTasks.forEach(item => { const div = document.createElement("div"); div.className = "agenda-item"; div.innerHTML = '
'; div.querySelector(".agenda-date").textContent = compactDateLabel(item.task.date); div.querySelector("strong").textContent = item.task.text; div.querySelector("span").textContent = item.project.kurum + " • " + item.project.ad; div.querySelector("button").onclick = () => openDetail(item.project.id); tasks.appendChild(div); }); } renderTodoList(); } function priorityLabel(value) { return value === "high" ? "Acil" : value === "low" ? "Düşük" : "Normal"; } function renderTodoList() { const list = document.getElementById("todo-list"); if (!list) return; list.innerHTML = ""; const priorityOrder = { high: 0, normal: 1, low: 2 }; const sorted = [...todos].sort((a,b) => Number(a.done) - Number(b.done) || (priorityOrder[a.priority] ?? 1) - (priorityOrder[b.priority] ?? 1)); if (!sorted.length) return list.appendChild(empty("To-do listesi boş.")); sorted.forEach(todo => { const div = document.createElement("div"); div.className = "todo-item" + (todo.done ? " done" : ""); div.innerHTML = '
' + (isAdmin() ? '' : '') + '
'; div.querySelector("input").checked = !!todo.done; div.querySelector("input").onchange = () => toggleTodo(todo.id); div.querySelector("strong").textContent = todo.text; div.querySelector("span").innerHTML = '' + priorityLabel(todo.priority) + ''; const buttons = div.querySelectorAll("button"); buttons[0].onclick = () => editTodo(todo.id); if (buttons[1]) buttons[1].onclick = () => removeTodo(todo.id); list.appendChild(div); }); } function addTodo() { const text = read("todo-text"); if (!text) return showToast("To-do metni boş olamaz."); todos.push({ id: Date.now(), text, date: "", priority: read("todo-priority", "normal"), done: false }); document.getElementById("todo-text").value = ""; document.getElementById("todo-priority").value = "normal"; saveTodos(); addLog("To-do ekledi", text); showToast("To-do eklendi."); } function toggleTodo(id) { const t = todos.find(x => String(x.id) === String(id)); if (!t) return; t.done = !t.done; saveTodos(); } function editTodo(id) { const t = todos.find(x => String(x.id) === String(id)); if (!t) return; const next = prompt("To-do metni", t.text); if (next === null) return; t.text = next.trim() || t.text; saveTodos(); } function removeTodo(id) { if (!requireAdmin("To-do silmek için admin yetkisi gerekir.")) return; if (!confirmDelete("Bu to-do kaydını silmek istediğinize emin misiniz?")) return; todos = todos.filter(x => String(x.id) !== String(id)); saveTodos(); } function renderStats() { const active = projects.filter(p => p.durum === "takip").length; const closed = projects.filter(p => p.durum === "kapanan").length; document.getElementById("stat-active").textContent = active; document.getElementById("stat-closed").textContent = closed; renderAdminProfitDashboard(); renderHomePlanner(); } function renderCalendar() { const grid = document.getElementById("calendar-grid"); if (!grid) return; grid.innerHTML = ""; document.getElementById("calendar-title").textContent = MONTHS[calendarDate.getMonth()] + " " + calendarDate.getFullYear(); DAYS.forEach((d, index) => { const cell = document.createElement("div"); cell.className = "day-name"; cell.textContent = d; cell.style.gridColumn = (index + 1) + " / " + (index + 2); cell.style.gridRow = "1 / 2"; grid.appendChild(cell); }); const y = calendarDate.getFullYear(), m = calendarDate.getMonth(); const first = new Date(y, m, 1); const offset = (first.getDay() + 6) % 7; const start = new Date(y, m, 1 - offset); const today = keyDate(new Date()); for (let i = 0; i < 42; i++) { const d = new Date(start); d.setDate(start.getDate() + i); const key = keyDate(d); const box = document.createElement("div"); box.className = "cal-day"; box.style.gridColumn = (i % 7 + 1) + " / " + (i % 7 + 2); box.style.gridRow = (Math.floor(i / 7) + 2) + " / " + (Math.floor(i / 7) + 3); if (d.getMonth() !== m) box.classList.add("out"); if (key === today) box.classList.add("today"); grid.appendChild(box); } renderCalendarBars(grid, start); renderCalendarDayNumbers(grid, start, m, today); } function renderCalendarDayNumbers(grid, start, month, today) { for (let i = 0; i < 42; i++) { const d = new Date(start); d.setDate(start.getDate() + i); const key = keyDate(d); const num = document.createElement("div"); num.className = "day-num-overlay"; if (d.getMonth() !== month) num.classList.add("out"); if (key === today) num.classList.add("today"); num.textContent = d.getDate(); num.style.gridColumn = (i % 7 + 1) + " / " + (i % 7 + 2); num.style.gridRow = (Math.floor(i / 7) + 2) + " / " + (Math.floor(i / 7) + 3); grid.appendChild(num); } } function renderCalendarBars(grid, start) { const visibleStart = keyDate(start); const visibleEnd = addDays(visibleStart, 41); const lanes = Array.from({ length: 6 }, () => []); const bars = []; projects.forEach(p => { if (p.durum === "kapanan" && !isCompletedProject(p)) return; if (p.ihaleTarihi >= visibleStart && p.ihaleTarihi <= visibleEnd) { bars.push({ id: p.id, start: p.ihaleTarihi, end: p.ihaleTarihi, label: "İhale: " + p.kurum, title: p.ad, color: "#d97706", ihale: true }); } const workStart = normalizeDateKey(p.isBaslangic || p.isTarihi); const workEnd = normalizeDateKey(p.isBitis || p.isBaslangic || p.isTarihi); if (workStart && workEnd && workEnd >= visibleStart && workStart <= visibleEnd) { bars.push({ id: p.id, start: workStart < visibleStart ? visibleStart : workStart, end: workEnd > visibleEnd ? visibleEnd : workEnd, label: p.ad, title: p.kurum + " - " + rangeText(p), color: COLORS[Math.abs(Number(p.id)) % COLORS.length], ihale: false }); } }); bars.sort((a, b) => a.start.localeCompare(b.start) || a.end.localeCompare(b.end)); bars.forEach(bar => { const startIndex = daysBetween(visibleStart, bar.start); const endIndex = daysBetween(visibleStart, bar.end); if (startIndex < 0 || endIndex < 0) return; for (let cursor = startIndex; cursor <= endIndex; ) { const week = Math.floor(cursor / 7); const weekEnd = Math.min(week * 7 + 6, endIndex); const colStart = cursor % 7 + 1; const colEnd = weekEnd % 7 + 2; const occupied = lanes[week]; let lane = 0; while (occupied[lane] && occupied[lane] >= colStart) lane++; occupied[lane] = colEnd; const el = document.createElement("button"); el.type = "button"; el.className = "event-bar" + (bar.ihale ? " ihale" : ""); el.textContent = cursor === startIndex ? bar.label : ""; el.title = bar.title; el.style.backgroundColor = bar.color; el.style.gridColumn = colStart + " / " + colEnd; el.style.gridRow = (week + 2) + " / " + (week + 3); el.style.marginTop = (18 + Math.min(lane, 2) * 15) + "px"; el.onclick = () => openDetail(bar.id); grid.appendChild(el); cursor = weekEnd + 1; } }); } function daysBetween(startKey, endKey) { const start = parseDateKey(startKey); const end = parseDateKey(endKey); if (!start || !end) return -1; return Math.round((end - start) / 86400000); } function moveCalendar(diff) { calendarDate = new Date(calendarDate.getFullYear(), calendarDate.getMonth() + diff, 1); renderCalendar(); } function goToday() { calendarDate = new Date(); renderCalendar(); } function filters(status) { const s = status === "kapanan" ? "-closed" : ""; return { q: (document.getElementById("filter-search" + s)?.value || "").toLowerCase(), person: (document.getElementById("filter-person" + s)?.value || "").toLowerCase(), start: document.getElementById("filter-start" + s)?.value || "", end: document.getElementById("filter-end" + s)?.value || "" }; } function match(p, f) { const text = [p.kurum, p.ad, p.personel, p.sartname, p.ihaleTip, p.isYeri].join(" ").toLowerCase(); if (f.q && !text.includes(f.q)) return false; if (f.person && !String(p.personel).toLowerCase().includes(f.person)) return false; if (f.start && p.isBitis < f.start) return false; if (f.end && p.isBaslangic > f.end) return false; return true; } let closedTab = "completed"; function projectWorkDateKey(p) { return normalizeDateKey(p?.isBaslangic || p?.isTarihi || p?.isBitis || p?.ihaleTarihi || ""); } function activeProjectSort(a, b) { const today = keyDate(new Date()); const ad = projectWorkDateKey(a); const bd = projectWorkDateKey(b); const aMissing = ad ? 0 : 1; const bMissing = bd ? 0 : 1; if (aMissing !== bMissing) return aMissing - bMissing; if (!ad && !bd) return Number(b.id || 0) - Number(a.id || 0); const aPast = ad < today; const bPast = bd < today; if (aPast !== bPast) return aPast ? 1 : -1; if (!aPast) return ad.localeCompare(bd) || (Number(b.id || 0) - Number(a.id || 0)); return bd.localeCompare(ad) || (Number(b.id || 0) - Number(a.id || 0)); } function closedProjectSort(a, b) { const ad = normalizeDateKey(a?.closedAt || a?.isBitis || a?.isBaslangic || a?.ihaleTarihi || ""); const bd = normalizeDateKey(b?.closedAt || b?.isBitis || b?.isBaslangic || b?.ihaleTarihi || ""); return (bd || "").localeCompare(ad || "") || (Number(b.id || 0) - Number(a.id || 0)); } function isClosedCompletedTabProject(p) { if (!p || p.durum !== "kapanan") return false; return (p.closeType || "completed") === "completed"; } function isClosedLostTabProject(p) { return p && p.durum === "kapanan" && !isClosedCompletedTabProject(p); } function setClosedTab(tab) { closedTab = tab === "lost" ? "lost" : "completed"; renderLists(); } function renderClosedTabs() { const completed = projects.filter(isClosedCompletedTabProject).length; const lost = projects.filter(isClosedLostTabProject).length; const c = document.getElementById("closed-count-completed"); const l = document.getElementById("closed-count-lost"); if (c) c.textContent = completed; if (l) l.textContent = lost; const completedBtn = document.getElementById("closed-tab-completed"); const lostBtn = document.getElementById("closed-tab-lost"); if (completedBtn) completedBtn.classList.toggle("active", closedTab !== "lost"); if (lostBtn) lostBtn.classList.toggle("active", closedTab === "lost"); } function renderLists() { renderClosedTabs(); renderList("takip", document.getElementById("active-list")); renderList("kapanan", document.getElementById("closed-list")); } function renderList(status, container) { if (!container) return; const f = filters(status); let items = projects.filter(p => p.durum === status && match(p, f)); if (status === "takip") items = items.sort(activeProjectSort); if (status === "kapanan") { items = items.filter(p => closedTab === "lost" ? isClosedLostTabProject(p) : isClosedCompletedTabProject(p)).sort(closedProjectSort); } container.innerHTML = ""; items.forEach(p => container.appendChild(card(p))); if (!items.length) { const message = status === "takip" ? "Takip listesinde uygun iş bulunmuyor." : (closedTab === "lost" ? "Kaybedilen / iptal olan iş bulunmuyor." : "Tamamlanan iş bulunmuyor."); container.appendChild(empty(message)); } } function card(p) { const article = document.createElement("article"); article.className = "project card " + p.durum; const title = document.createElement("h4"); title.textContent = p.kurum; article.appendChild(title); field(article, "İş:", p.ad); field(article, "İhale:", dateText(p.ihaleTarihi) + (p.ihaleSaati ? " " + p.ihaleSaati : "")); field(article, "İş Tarihi:", rangeText(p) + " - " + p.isYeri); field(article, "Sorumlu:", p.personel); field(article, "Maliyet:", money(isCompletedProject(p) ? completionReportTotals(p).expense : totals(p).finalCost), "var(--green-dark)"); if (p.durum === "kapanan") { const badge = document.createElement("div"); badge.className = "result-badge " + (isLostProject(p) ? "lost" : "completed"); badge.textContent = closeLabel(p.closeType || "completed"); article.appendChild(badge); if ((p.closeType || "completed") === "completed" && !p.completionReport?.reportCompleted) { const miss = document.createElement("div"); miss.className = "result-badge report-missing-badge"; miss.textContent = "İş bitirme raporu eksik"; article.appendChild(miss); } } else if (hasPendingCloseRequest(p)) { const badge = document.createElement("div"); badge.className = "result-badge pending"; badge.textContent = "Kapatma talebi bekliyor"; article.appendChild(badge); } else if (hasRejectedCloseRequest(p)) { const badge = document.createElement("div"); badge.className = "result-badge rejected"; badge.textContent = "Onay verilmedi"; article.appendChild(badge); } const actions = document.createElement("div"); actions.className = "actions"; actions.append(button("Detay", "btn-small btn-soft", () => openDetail(p.id)), button("Bütçe", "btn-small btn-primary", () => openCost(p.id))); if (p.durum === "takip") actions.append(button("Teklif Hazırla", "btn-small btn-soft", () => openOfferForProject(p.id))); actions.append(closeStatusButton(p)); if (p.durum === "takip") actions.append(button("İş Bitirme Formu", "btn-small btn-soft", () => openCompletionReportForm(p.id, false))); if (isCompletedProject(p)) actions.append(button("İş Bitirme Raporu", "btn-small btn-primary", () => openCompletionReport(p.id)), button("Raporu Düzenle", "btn-small btn-soft", () => openCompletionReportForm(p.id, false))); if (hasSartnameDownload(p)) actions.append(button("Şartname İndir", "btn-small btn-soft", () => downloadSartname(p.id))); if (isAdmin()) actions.append(button("Sil", "btn-small btn-danger", () => deleteProject(p.id))); article.appendChild(actions); return article; } function field(parent, label, value, color) { const p = document.createElement("p"); const s = document.createElement("strong"); const v = document.createElement("span"); s.textContent = label; v.textContent = value || "-"; if (color) { v.style.color = color; v.style.fontWeight = "900"; } p.append(s, v); parent.appendChild(p); } function empty(text) { const div = document.createElement("div"); div.className = "empty"; div.textContent = text; return div; } function clearFilters() { ["filter-search", "filter-person", "filter-start", "filter-end", "filter-search-closed", "filter-person-closed", "filter-start-closed", "filter-end-closed"].forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; }); renderLists(); } function openDetail(id) { detailProjectId = id; const p = getProject(id); if (!p) return; renderDetailTitleField("detail-title", p.kurum, "kurum"); renderDetailTitleField("detail-subtitle", p.ad, "ad"); const grid = document.getElementById("detail-grid"); grid.innerHTML = ""; const netResult = isCompletedProject(p) ? completionReportTotals(p).result : (isLostProject(p) ? 0 : totals(p).profit); const detailRows = [ { label: "İhale", value: dateText(p.ihaleTarihi) + (p.ihaleSaati ? " " + p.ihaleSaati : ""), field: "ihale" }, { label: "İş Tarihi", value: rangeText(p), field: "isTarihi" }, { label: "İş Yeri", value: p.isYeri, field: "isYeri" }, { label: "Kişi", value: p.kisi, field: "kisi" }, { label: "Sorumlu", value: p.personel, field: "personel" }, { label: "İhale Tipi / İKN", value: p.ihaleTip, field: "ihaleTip" }, { label: "İrtibat", value: p.irtibat, field: "irtibat" }, { label: "İş Açıklaması", value: p.detay, field: "detay" }, { label: "İş Evrakları", value: allProjectDocuments(p).length ? (allProjectDocuments(p).length + " dosya") : "Dosya yok" }, { label: "Teklif", value: isLostProject(p) ? "-" : money(p.sales) }, { label: "Net Kâr", value: money(netResult) } ]; if (p.durum === "kapanan") detailRows.push({ label: "Kapanış Durumu", value: closeLabel(p.closeType || "completed") }, { label: "Kapanış Tarihi", value: dateText(p.closedAt || p.isBitis) }, { label: "Kapanış Notu", value: p.closeNote || "-" }); if (hasPendingCloseRequest(p)) detailRows.push({ label: "Kapatma Talebi", value: closeLabel(p.closeRequest.type) + " / Admin onayı bekliyor" }, { label: "Talep Notu", value: p.closeRequest.note || "-" }); if (hasRejectedCloseRequest(p)) detailRows.push({ label: "Kapatma Talebi", value: closeLabel(p.closeRequest.type) + " / Onay verilmedi" }, { label: "Ret Notu", value: p.closeRequest.reviewNote || p.closeRequest.note || "-" }); detailRows.forEach(row => grid.appendChild(detailCard(row.label, row.value, row.field))); renderDetailOffersPanel(); renderProjectDocumentsPanel(); renderDetailReportPanel(); document.getElementById("detail-notes").value = p.notes || ""; renderTasks(); document.getElementById("detail-modal").classList.add("show"); } function closeDetail() { document.getElementById("detail-modal").classList.remove("show"); detailProjectId = null; } function canEditDetailField(field, p = getProject(detailProjectId)) { if (!p || !(p.durum === "takip" || isAdmin())) return false; if ((field === "ihale" || field === "isTarihi") && p.durum !== "takip") return false; return true; } function detailEditButton(field) { const edit = document.createElement("button"); edit.type = "button"; edit.className = "detail-edit-btn"; edit.textContent = "✎"; edit.title = "Düzenle"; edit.setAttribute("aria-label", "Düzenle"); edit.onclick = event => { event.stopPropagation(); startDetailInlineEdit(field); }; return edit; } function renderDetailTitleField(id, text, field) { const el = document.getElementById(id); if (!el) return; const p = getProject(detailProjectId); el.innerHTML = ""; const span = document.createElement("span"); span.textContent = text || "-"; el.appendChild(span); if (canEditDetailField(field, p)) el.appendChild(detailEditButton(field)); } function detailCard(label, value, field) { const p = getProject(detailProjectId); const d = document.createElement("div"); d.className = "detail"; if (field) d.dataset.field = field; const labelRow = document.createElement("div"); labelRow.className = "detail-label-row"; const s = document.createElement("span"); s.textContent = label; labelRow.appendChild(s); if (field && canEditDetailField(field, p)) labelRow.appendChild(detailEditButton(field)); const strong = document.createElement("strong"); strong.textContent = value || "-"; d.append(labelRow, strong); return d; } function startDetailInlineEdit(field) { const p = getProject(detailProjectId); if (!p || !canEditDetailField(field, p)) return showToast("Bu alan şu anda düzenlenemez."); if (field === "kurum" || field === "ad") { const label = field === "kurum" ? "Kurum" : "İş adı"; const next = prompt(label, p[field] || ""); if (next === null) return; return saveDetailField(field, next.trim()); } document.querySelectorAll(".detail-inline-editor").forEach(el => el.remove()); const card = document.querySelector('.detail[data-field="' + field + '"]'); if (!card) return; const editor = document.createElement("div"); editor.className = "detail-inline-editor"; editor.dataset.field = field; const addButton = (text, cls, fn) => { const b = document.createElement("button"); b.type = "button"; b.className = cls; b.textContent = text; b.onclick = fn; return b; }; if (field === "ihale") { editor.classList.add("two-fields"); const date = document.createElement("input"); date.type = "date"; date.value = p.ihaleTarihi || ""; const time = document.createElement("input"); time.type = "time"; time.value = p.ihaleSaati || ""; editor.append(date, time, addButton("Kaydet", "btn-small btn-success", () => saveDetailField(field, { ihaleTarihi: date.value, ihaleSaati: time.value })), addButton("Vazgeç", "btn-small btn-soft", () => editor.remove())); } else if (field === "isTarihi") { editor.classList.add("two-fields"); const start = document.createElement("input"); start.type = "date"; start.value = p.isBaslangic || ""; const end = document.createElement("input"); end.type = "date"; end.value = p.isBitis || ""; editor.append(start, end, addButton("Kaydet", "btn-small btn-success", () => saveDetailField(field, { isBaslangic: start.value, isBitis: end.value })), addButton("Vazgeç", "btn-small btn-soft", () => editor.remove())); } else { let inputEl; if (field === "personel") { inputEl = document.createElement("select"); users().forEach(user => { const option = document.createElement("option"); option.value = user.name; option.textContent = user.name + " (" + roleLabel(user.role) + ")"; if (user.name === p.personel) option.selected = true; inputEl.appendChild(option); }); } else if (field === "detay") { inputEl = document.createElement("textarea"); inputEl.value = p.detay || ""; } else { inputEl = document.createElement("input"); inputEl.type = "text"; inputEl.value = p[field] || ""; } editor.append(inputEl, addButton("Kaydet", "btn-small btn-success", () => saveDetailField(field, inputEl.value.trim())), addButton("Vazgeç", "btn-small btn-soft", () => editor.remove())); } card.appendChild(editor); const focusable = editor.querySelector("input, select, textarea"); if (focusable) focusable.focus(); } function saveDetailField(field, value) { const p = getProject(detailProjectId); if (!p) return showToast("Önce bir iş detayı açmalısınız."); if (!canEditDetailField(field, p)) return showToast("Bu alan şu anda düzenlenemez."); if ((field === "kurum" || field === "ad") && !String(value || "").trim()) return showToast("Kurum ve iş adı boş olamaz."); const oldState = projectSnapshot(p); const oldInfo = {}; const nextInfo = {}; if (field === "ihale") { const ihaleTarihi = value.ihaleTarihi || ""; const ihaleSaati = value.ihaleSaati || ""; if (!ihaleTarihi) return showToast("İhale tarihi zorunludur."); oldInfo.ihaleTarihi = p.ihaleTarihi || ""; oldInfo.ihaleSaati = p.ihaleSaati || ""; p.ihaleTarihi = ihaleTarihi; p.ihaleSaati = ihaleSaati; nextInfo.ihaleTarihi = ihaleTarihi; nextInfo.ihaleSaati = ihaleSaati; } else if (field === "isTarihi") { const isBaslangic = value.isBaslangic || ""; const isBitis = value.isBitis || ""; if (!isBaslangic || !isBitis) return showToast("İş başlangıç ve bitiş tarihleri zorunludur."); if (parseDateKey(isBitis) < parseDateKey(isBaslangic)) return showToast("İş bitiş tarihi başlangıçtan önce olamaz."); oldInfo.isBaslangic = p.isBaslangic || ""; oldInfo.isBitis = p.isBitis || ""; p.isBaslangic = isBaslangic; p.isBitis = isBitis; nextInfo.isBaslangic = isBaslangic; nextInfo.isBitis = isBitis; } else { const normalized = String(value || "").trim() || "-"; oldInfo[field] = p[field] || ""; nextInfo[field] = normalized; p[field] = normalized; if (p.completionReport) { if (field === "kurum" && (!p.completionReport.kurum || p.completionReport.kurum === oldInfo[field])) p.completionReport.kurum = normalized; if (field === "ad" && (!p.completionReport.ad || p.completionReport.ad === oldInfo[field])) p.completionReport.ad = normalized; } } persist(); addLog("İş detay alanını güncelledi", p, { old: oldInfo, next: nextInfo, previous: oldState }); showToast("Bilgi güncellendi."); openDetail(p.id); } function updateNotes(value) { const p = getProject(detailProjectId); if (!p) return; p.notes = value; persist(); } function renderDetailInfoEditor(p) { const panel = document.getElementById("detail-info-panel"); if (!panel) return; const editable = p && (p.durum === "takip" || isAdmin()); panel.classList.toggle("hidden", !editable); if (!editable) return; const values = { "detail-edit-kurum": p.kurum || "", "detail-edit-ad": p.ad || "", "detail-edit-is-yeri": p.isYeri || "", "detail-edit-kisi": p.kisi || "", "detail-edit-ihale-tip": p.ihaleTip || "", "detail-edit-irtibat": p.irtibat || "", "detail-edit-detay": p.detay || "" }; Object.entries(values).forEach(([id, value]) => { const el = document.getElementById(id); if (el) el.value = value; }); const personel = document.getElementById("detail-edit-personel"); if (personel) { const current = p.personel || ""; personel.innerHTML = ""; const localUsers = users(); if (current && !localUsers.some(user => user.name === current)) { const option = document.createElement("option"); option.value = current; option.textContent = current; personel.appendChild(option); } localUsers.forEach(user => { const option = document.createElement("option"); option.value = user.name; option.textContent = user.name + " (" + roleLabel(user.role) + ")"; if (user.name === current) option.selected = true; personel.appendChild(option); }); } } function updateProjectInfo() { const p = getProject(detailProjectId); if (!p) return showToast("Önce bir iş detayı açmalısınız."); if (p.durum === "kapanan" && !isAdmin()) return showToast("Kapanan iş bilgilerini sadece admin düzenleyebilir."); const nextInfo = { kurum: read("detail-edit-kurum"), ad: read("detail-edit-ad"), isYeri: read("detail-edit-is-yeri", "-"), kisi: read("detail-edit-kisi", "-"), ihaleTip: read("detail-edit-ihale-tip", "-"), irtibat: read("detail-edit-irtibat", "-"), personel: read("detail-edit-personel", "-"), detay: read("detail-edit-detay", "-") }; if (!nextInfo.kurum || !nextInfo.ad) return showToast("Kurum ve iş adı boş olamaz."); const oldInfo = { kurum: p.kurum || "", ad: p.ad || "", isYeri: p.isYeri || "", kisi: p.kisi || "", ihaleTip: p.ihaleTip || "", irtibat: p.irtibat || "", personel: p.personel || "", detay: p.detay || "" }; Object.assign(p, nextInfo); if (p.completionReport) { if (!p.completionReport.kurum || p.completionReport.kurum === oldInfo.kurum) p.completionReport.kurum = nextInfo.kurum; if (!p.completionReport.ad || p.completionReport.ad === oldInfo.ad) p.completionReport.ad = nextInfo.ad; } persist(); addLog("İş bilgilerini güncelledi", p, { old: oldInfo, next: nextInfo }); showToast("İş bilgileri güncellendi."); openDetail(p.id); } function renderDetailDateEditor(p) { const panel = document.getElementById("detail-date-panel"); if (!panel) return; const editable = p && p.durum === "takip"; panel.classList.toggle("hidden", !editable); if (!editable) return; const values = { "detail-ihale-tarihi": p.ihaleTarihi || "", "detail-ihale-saati": p.ihaleSaati || "", "detail-is-baslangic": p.isBaslangic || "", "detail-is-bitis": p.isBitis || "" }; Object.entries(values).forEach(([id, value]) => { const el = document.getElementById(id); if (el) el.value = value; }); } function updateProjectDates() { const p = getProject(detailProjectId); if (!p) return showToast("Önce bir iş detayı açmalısınız."); if (p.durum !== "takip") return showToast("Kapanan işlerde tarih güncellemesi yapılamaz."); const ihaleTarihi = document.getElementById("detail-ihale-tarihi")?.value || ""; const ihaleSaati = document.getElementById("detail-ihale-saati")?.value || ""; const isBaslangic = document.getElementById("detail-is-baslangic")?.value || ""; const isBitis = document.getElementById("detail-is-bitis")?.value || ""; if (!ihaleTarihi || !isBaslangic || !isBitis) return showToast("İhale, başlangıç ve bitiş tarihleri zorunludur."); if (parseDateKey(isBitis) < parseDateKey(isBaslangic)) return showToast("İş bitiş tarihi başlangıçtan önce olamaz."); const oldDates = { ihaleTarihi: p.ihaleTarihi || "", ihaleSaati: p.ihaleSaati || "", isBaslangic: p.isBaslangic || "", isBitis: p.isBitis || "" }; const newDates = { ihaleTarihi, ihaleSaati, isBaslangic, isBitis }; p.ihaleTarihi = ihaleTarihi; p.ihaleSaati = ihaleSaati; p.isBaslangic = isBaslangic; p.isBitis = isBitis; persist(); addLog("İş tarihlerini güncelledi", p, { old: oldDates, next: newDates }); showToast("Tarihler güncellendi."); openDetail(p.id); } function renderDetailOffersPanel() { const p = getProject(detailProjectId); const panel = document.getElementById("detail-offers-panel"); const status = document.getElementById("detail-offers-status"); const list = document.getElementById("detail-offers-list"); if (!panel || !status || !list) return; const offers = normalizeProjectOffers(p?.offers || []); list.innerHTML = ""; status.textContent = offers.length ? offers.length + " teklif kayıtlı" : "Teklif yok"; if (!offers.length) { list.appendChild(empty("Bu işe bağlı kayıtlı teklif yok. Teklif Hazırla ekranından bağlı işi seçip teklif indirdiğinizde burada görünecek.")); return; } offers.slice(0, 5).forEach((offer, index) => { const card = document.createElement("div"); card.className = "offer-history-card"; const safeRows = (offer.rows || []).slice(0, 6); const rowsHtml = safeRows.length ? safeRows.map(row => { const desc = escapeHtml(row.desc || row.values?.[1] || "-"); const unit = escapeHtml(row.unit || row.values?.[2] || "-"); const qty = escapeHtml(row.qty || row.values?.[3] || "-"); const days = escapeHtml(row.days || row.values?.[4] || "-"); return `${desc}${unit}${qty}${days}${money(row.total)}`; }).join("") : 'Tablo satırı yok.'; const more = offer.rows.length > safeRows.length ? `
+${offer.rows.length - safeRows.length} satır daha var.
` : ""; const versionNo = offers.length - index; const lastBadge = index === 0 ? 'Son teklif' : ''; card.innerHTML = `
V${versionNo}${escapeHtml(offer.title || "Teklif")}${lastBadge}${new Date(offer.createdAt || Date.now()).toLocaleString("tr-TR")}
${money(offer.total)}
${rowsHtml}
AçıklamaBirimAdetGünToplam
${more}
`; list.appendChild(card); }); } function openSavedOfferFromDetail(index) { const p = getProject(detailProjectId); const offer = normalizeProjectOffers(p?.offers || [])[index]; if (!p || !offer) return showToast("Teklif bulunamadı."); closeDetail(); switchView("offer"); renderOfferProjects(); document.getElementById("offer-project").value = p.id; setRich("offer-title", offer.titleHtml || escapeHtml(offer.title || "")); setRich("offer-letter", offer.letterHtml || ""); setRich("offer-note", offer.noteHtml || ""); const body = document.getElementById("offer-table-body"); if (body) body.innerHTML = ""; (offer.rows || []).forEach(row => addOfferRow(row)); if (!document.querySelector("#offer-table-body tr")) addOfferRow(); recalcOfferTable(); showToast("Kayıtlı teklif açıldı. Düzenleyip tekrar indirebilirsiniz."); } function renderSartnamePanel() { const p = getProject(detailProjectId); const status = document.getElementById("detail-sartname-status"); const fileInput = document.getElementById("detail-sartname-file"); const downloadBtn = document.getElementById("detail-sartname-download"); const removeBtn = document.getElementById("detail-sartname-remove"); if (!status || !downloadBtn || !removeBtn) return; if (fileInput) fileInput.value = ""; const file = normalizeSartnameFile(p?.sartnameFile); const hasDownloadableFile = !!file?.dataUrl; const hasOnlyName = !!p?.sartname && p.sartname !== "Şartname yüklenmedi" && !hasDownloadableFile; if (!p) { status.textContent = "Proje seçili değil"; } else if (hasDownloadableFile) { const sizeText = file.size ? " • " + Math.round(file.size / 1024) + " KB" : ""; status.textContent = "Yüklü ve indirilebilir: " + (file.name || p.sartname || "Şartname") + sizeText; } else if (hasOnlyName) { status.textContent = "Kayıtta dosya adı var fakat indirilebilir dosya yok: " + p.sartname; } else { status.textContent = "Henüz şartname/proje dosyası yüklenmedi."; } downloadBtn.disabled = !hasDownloadableFile; removeBtn.disabled = !(hasDownloadableFile || hasOnlyName); } function renderDetailReportPanel() { const panel = document.getElementById("detail-completion-report-panel"); const p = getProject(detailProjectId); if (panel) panel.classList.toggle("hidden", !(p && (p.durum === "kapanan" || p.completionReport))); } function projectOwnLogs(project) { if (!project) return []; const label = [project.kurum, project.ad].filter(Boolean).join(" - "); return load(LOGS_KEY, []).filter(log => { if (String(log.projectId || "") === String(project.id)) return true; const text = String(log.project || ""); return text === label || (text.includes(project.kurum || "") && text.includes(project.ad || "")); }); } function renderDetailActivityPanel() { const p = getProject(detailProjectId); const list = document.getElementById("detail-activity-list"); const status = document.getElementById("detail-activity-status"); if (!list || !status) return; const ownLogs = projectOwnLogs(p).slice(0, 12); list.innerHTML = ""; status.textContent = ownLogs.length ? ownLogs.length + " son hareket" : "Hareket yok"; if (!ownLogs.length) { list.appendChild(empty("Bu işe ait hareket kaydı henüz yok.")); return; } ownLogs.forEach(log => { const item = document.createElement("div"); item.className = "log-item"; item.innerHTML = ""; item.querySelector("strong").textContent = log.action || "-"; item.querySelector("span").textContent = (log.actor || "Sistem") + " • " + new Date(log.time || Date.now()).toLocaleString("tr-TR"); appendLogDetails(item, log.details); list.appendChild(item); }); } function openProjectLogModal(id) { const p = getProject(id); if (!p) return showToast("İş bulunamadı."); projectLogProjectId = id; document.getElementById("project-log-title").textContent = "İş Log Kaydı"; document.getElementById("project-log-subtitle").textContent = p.kurum + " - " + p.ad; renderProjectLogPanel(p); document.getElementById("project-log-modal").classList.add("show"); } function closeProjectLogModal() { document.getElementById("project-log-modal").classList.remove("show"); projectLogProjectId = null; } function renderProjectLogPanel(project) { const list = document.getElementById("project-log-list"); if (!list) return; const logs = projectOwnLogs(project).slice(0, 30); list.innerHTML = ""; if (!logs.length) { list.appendChild(empty("Bu işe ait işlem kaydı yok.")); return; } logs.forEach(log => { const item = document.createElement("div"); item.className = "log-item"; item.innerHTML = ""; item.querySelector("strong").textContent = log.action || "-"; item.querySelector("span").textContent = (log.actor || "Sistem") + " • " + new Date(log.time || Date.now()).toLocaleString("tr-TR"); appendLogDetails(item, log.details); list.appendChild(item); }); } async function uploadDetailSartname() { const p = getProject(detailProjectId); const fileInput = document.getElementById("detail-sartname-file"); const file = fileInput?.files?.[0]; if (!p) return showToast("Önce bir iş detayı açmalısınız."); if (!file) return showToast("Yüklemek için bir şartname/proje dosyası seçin."); const sartnameFile = await storeProjectFile(file, p.id, "sartname"); if (!isDownloadableFile(sartnameFile)) return showToast("Dosya yüklenemedi. Sunucu bağlantısını ve dosya boyutunu kontrol edin."); const oldFile = { name: p.sartname || "", file: normalizeLogDetails(p.sartnameFile || null) }; p.sartname = file.name; p.sartnameFile = sartnameFile; const saved = save(PROJECTS_KEY, projects); if (!saved) return; renderStats(); renderCalendar(); renderLists(); addLog("Şartname/proje dosyası yükledi", p, { old: oldFile, next: { name: file.name, size: file.size, type: file.type } }); showToast("Şartname/proje dosyası yüklendi."); openDetail(p.id); } function removeDetailSartname() { const p = getProject(detailProjectId); if (!p) return showToast("Önce bir iş detayı açmalısınız."); if (!hasSartnameDownload(p) && (!p.sartname || p.sartname === "Şartname yüklenmedi")) return showToast("Silinecek şartname/proje dosyası bulunmuyor."); if (!confirmDelete("Bu işteki şartname/proje dosyası bilgisini silmek istediğinize emin misiniz?")) return; const oldFile = { name: p.sartname || "Şartname", file: normalizeLogDetails(p.sartnameFile || null) }; p.sartname = "Şartname yüklenmedi"; p.sartnameFile = null; persist(); addLog("Şartname/proje dosyasını sildi", p, { deleted: oldFile }); showToast("Şartname/proje dosyası silindi."); openDetail(p.id); } function allProjectDocuments(project) { if (!project) return []; const docs = []; const sartnameFile = normalizeSartnameFile(project.sartnameFile); if (isDownloadableFile(sartnameFile)) { docs.push({ id: "__sartname__", source: "sartname", label: "Şartname / Proje Dosyası", ...sartnameFile, name: sartnameFile.name || project.sartname || "Şartname / Proje Dosyası" }); } else if (project.sartname && project.sartname !== "Şartname yüklenmedi") { docs.push({ id: "__sartname_name_only__", source: "sartname-name-only", label: "Şartname / Proje Dosyası", name: project.sartname, type: "Dosya adı kayıtlı", size: 0, dataUrl: "" }); } normalizeProjectDocuments(project.documents || []).forEach(doc => { docs.push({ ...doc, source: "document", label: "Ek Evrak" }); }); return docs; } function renderProjectDocumentsPanel() { const p = getProject(detailProjectId); const list = document.getElementById("detail-document-list"); const status = document.getElementById("detail-documents-status"); const fileInput = document.getElementById("detail-document-file"); if (!list || !status) return; if (fileInput) fileInput.value = ""; if (p) p.documents = normalizeProjectDocuments(p.documents || []); const docs = allProjectDocuments(p); const downloadableCount = docs.filter(doc => isDownloadableFile(doc)).length; list.innerHTML = ""; status.textContent = docs.length ? (downloadableCount + " indirilebilir / " + docs.length + " kayıt") : "Dosya yok"; if (!docs.length) { list.appendChild(empty("Henüz iş evrakı yüklenmedi.")); return; } docs.forEach(doc => { const item = document.createElement("div"); item.className = "document-item" + (!isDownloadableFile(doc) ? " is-warning" : ""); item.innerHTML = "
"; item.querySelector("strong").textContent = (doc.label ? doc.label + ": " : "") + (doc.name || "Evrak"); const sizeText = doc.size ? " • " + Math.round(doc.size / 1024) + " KB" : ""; item.querySelector("span").textContent = isDownloadableFile(doc) ? ((doc.type || "dosya") + sizeText + " • indirilebilir") : "Dosya adı var ama indirilebilir veri yok; tekrar yükleyin."; const actions = item.querySelector(".actions"); const downloadButton = button("İndir", "btn-small btn-primary", () => downloadProjectDocument(doc.id)); downloadButton.disabled = !isDownloadableFile(doc); actions.append(downloadButton, button("Sil", "btn-small btn-danger", () => removeProjectDocument(doc.id))); list.appendChild(item); }); } async function uploadProjectDocuments() { const p = getProject(detailProjectId); const fileInput = document.getElementById("detail-document-file"); const files = Array.from(fileInput?.files || []); if (!p) return showToast("Önce bir iş detayı açmalısınız."); if (!files.length) return showToast("Yüklemek için en az bir evrak seçin."); const uploaded = []; for (const file of files) { const stored = await storeProjectFile(file, p.id, "document"); if (isDownloadableFile(stored)) uploaded.push({ id: Date.now() + "-" + Math.random().toString(16).slice(2), ...stored }); } if (!uploaded.length) return showToast("Evraklar yüklenemedi. Sunucu bağlantısını, oturumu ve dosya boyutunu kontrol edin."); p.documents = normalizeProjectDocuments([...(p.documents || []), ...uploaded]); persist(); addLog("İş evrakı yükledi", p, { next: uploaded.map(file => ({ name: file.name, size: file.size, type: file.type })) }); showToast(uploaded.length + " evrak yüklendi ve indirilebilir olarak kaydedildi."); renderProjectDocumentsPanel(); } function downloadProjectDocument(documentId) { const p = getProject(detailProjectId); if (!p) return showToast("Önce bir iş detayı açmalısınız."); if (String(documentId) === "__sartname__") return downloadSartname(p.id); if (String(documentId) === "__sartname_name_only__") return showToast("Bu dosya sadece isim olarak kayıtlı. İndirilebilir olması için İş Evrakları bölümünden yeniden yükleyin."); const file = normalizeProjectDocuments(p.documents || []).find(doc => String(doc.id) === String(documentId)); if (!isDownloadableFile(file)) return showToast("Bu evrak indirilebilir değil. Dosyayı yeniden yükleyin."); downloadStoredFile(file, file.name || "evrak"); addLog("İş evrakı indirdi", p, { detail: { name: file.name || "evrak" } }); } function removeProjectDocument(documentId) { const p = getProject(detailProjectId); if (!p) return; if (String(documentId) === "__sartname__" || String(documentId) === "__sartname_name_only__") { if (!confirmDelete("Bu işteki şartname/proje dosyası bilgisini silmek istediğinize emin misiniz?")) return; const oldFile = { name: p.sartname || "Şartname", file: normalizeLogDetails(p.sartnameFile || null) }; p.sartname = "Şartname yüklenmedi"; p.sartnameFile = null; persist(); addLog("İş evrakı sildi", p, { deleted: oldFile }); showToast("Evrak silindi."); renderProjectDocumentsPanel(); return; } const file = normalizeProjectDocuments(p.documents || []).find(doc => String(doc.id) === String(documentId)); if (!file) return showToast("Silinecek evrak bulunamadı."); if (!confirmDelete("Bu evrakı silmek istediğinize emin misiniz?")) return; p.documents = normalizeProjectDocuments(p.documents || []).filter(doc => String(doc.id) !== String(documentId)); persist(); addLog("İş evrakı sildi", p, { deleted: normalizeLogDetails(file) }); showToast("Evrak silindi."); renderProjectDocumentsPanel(); } function renderTasks() { const p = getProject(detailProjectId), list = document.getElementById("task-list"); list.innerHTML = ""; if (!p || !p.tasks.length) return list.appendChild(empty("Bu proje için görev bulunmuyor.")); p.tasks.forEach(t => { const row = document.createElement("div"); row.className = "task-row" + (t.done ? " done" : ""); const check = document.createElement("input"); check.type = "checkbox"; check.checked = !!t.done; check.onchange = () => { const oldTask = clone(t); t.done = check.checked; persist(); addLog("Görev durumunu değiştirdi", p, { old: oldTask, next: t }); renderTasks(); }; const text = document.createElement("span"); text.textContent = t.text + (t.date ? " | " + dateText(t.date) : ""); row.append(check, text, button("Sil", "btn-small btn-danger", () => removeTask(t.id))); list.appendChild(row); }); } function addTask() { const p = getProject(detailProjectId); const text = read("task-text"); if (!p || !text) return showToast("Görev metni boş olamaz."); const task = { id: Date.now(), text, date: read("task-date"), done: false }; p.tasks.push(task); document.getElementById("task-text").value = ""; document.getElementById("task-date").value = ""; persist(); addLog("Görev ekledi", p, { next: task }); renderTasks(); } function removeTask(id) { const p = getProject(detailProjectId); if (!p) return; const removed = (p.tasks || []).find(t => String(t.id) === String(id)); if (!removed) return; if (!confirmDelete("Bu görevi silmek istediğinize emin misiniz?")) return; p.tasks = p.tasks.filter(t => String(t.id) !== String(id)); persist(); addLog("Görev sildi", p, { deleted: removed }); renderTasks(); } function renderLogs() { const box = document.getElementById("log-list"); if (!box) return; if (!isAdmin()) { box.innerHTML = ""; box.appendChild(empty("İşlem loglarını sadece admin görüntüleyebilir.")); return; } const logs = load(LOGS_KEY, []); box.innerHTML = ""; if (!logs.length) return box.appendChild(empty("Henüz işlem logu yok.")); logs.forEach(log => { const item = document.createElement("div"); item.className = "log-item"; const time = new Date(log.time).toLocaleString("tr-TR"); item.innerHTML = ""; item.querySelector("strong").textContent = (log.actor || "Sistem") + " - " + (log.action || "-"); item.querySelectorAll("span")[0].textContent = time; item.querySelectorAll("span")[1].textContent = log.project || ""; appendLogDetails(item, log.details); box.appendChild(item); }); } function clearLogs() { if (!requireAdmin("Logları sadece admin temizleyebilir.")) return; save(LOGS_KEY, []); syncServerState({ logs: [] }); renderLogs(); showToast("Loglar temizlendi."); } function renderUsers() { const box = document.getElementById("user-list"); if (!box) return; if (!isAdmin()) { box.innerHTML = ""; box.appendChild(empty("Bu ekran için admin yetkisi gerekir.")); return; } const list = users(); box.innerHTML = ""; if (!list.length) return box.appendChild(empty("Kayıtlı kullanıcı yok.")); list.forEach(user => { const item = document.createElement("div"); item.className = "log-item"; const active = user.active !== false; const temp = !!user.mustChangePassword; const statusText = active ? (temp ? "Geçici şifre" : "Aktif") : "Pasif"; item.innerHTML = "
"; item.querySelector("strong").textContent = user.name + " - " + user.username; const spans = item.querySelectorAll("span"); spans[0].textContent = "Yetki: " + roleLabel(user.role) + " | Durum: " + statusText; spans[1].textContent = "Son giriş: " + formatUserDate(user.lastLoginAt); const actions = item.querySelector(".actions"); if (user.username !== currentUser.username) { actions.append(button("Şifre Sıfırla", "btn-small btn-soft", () => resetUserPassword(user.username))); } if (user.username !== "admin" && user.username !== currentUser.username) { actions.append(button(active ? "Pasif Yap" : "Aktif Yap", active ? "btn-small btn-warning" : "btn-small btn-success", () => toggleUserActive(user.username, !active))); actions.append(button("Sil", "btn-small btn-danger", () => deleteUser(user.username))); } box.appendChild(item); }); } async function deleteUser(username) { if (!requireAdmin("Kullanıcı silmek için admin yetkisi gerekir.")) return; if (!confirmDelete(username + " kullanıcısını silmek istediğinize emin misiniz?")) return; const deletedUser = users().find(user => user.username === username) || { username }; let serverUsers = null; try { const response = await fetch("api/delete-user.php", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ adminUsername: currentUser.username, adminPassword: currentPassword, username }) }); const result = await response.json(); if (response.ok && result.success && Array.isArray(result.users)) serverUsers = result.users; } catch (error) { if (sharedMode) return showToast(error.message || "Sunucuda kullanıcı silinemedi."); // PHP/API yoksa yerel tarayıcı kaydı silinir. } const list = serverUsers || users().filter(user => user.username !== username); save(USERS_KEY, list); addLog("Kullanıcı sildi: " + username, "", { deleted: deletedUser }); renderUsers(); showToast("Kullanıcı silindi."); } function deleteProject(id) { if (!requireAdmin("İş silme yetkisi sadece admindedir.")) return; const p = getProject(id); if (!p) return; if (!confirmDelete(p.kurum + " - " + p.ad + " işini silmek istediğinize emin misiniz?")) return; const deleted = projectSnapshot(p); projects = projects.filter(item => String(item.id) !== String(id)); rememberDeletedProjectIds([id]); if (!persist()) return; addLog("İşi sildi", p, { deleted }); switchView(p.durum === "kapanan" ? "closed" : "active"); } function populateUserSelects() { const select = document.getElementById("f-personel"); const userList = users(); if (select) { const current = select.value || (currentUser ? currentUser.name : ""); select.innerHTML = ""; userList.forEach(user => { const option = document.createElement("option"); option.value = user.name; option.textContent = user.name + " (" + roleLabel(user.role) + ")"; if (user.name === current) option.selected = true; select.appendChild(option); }); } ["filter-person", "filter-person-closed"].forEach(id => { const filter = document.getElementById(id); if (!filter) return; const current = filter.value; filter.innerHTML = ''; userList.forEach(user => { const option = document.createElement("option"); option.value = user.name; option.textContent = user.name; if (user.name === current) option.selected = true; filter.appendChild(option); }); }); } function renderOfferProjects() { const select = document.getElementById("offer-project"); if (!select) return; const current = select.value; select.innerHTML = ''; projects.filter(project => project.durum === "takip").forEach(project => { const option = document.createElement("option"); option.value = project.id; option.textContent = project.kurum + " - " + project.ad; if (String(project.id) === String(current)) option.selected = true; select.appendChild(option); }); if (current && !Array.from(select.options).some(option => String(option.value) === String(current))) select.value = ""; refreshOfferColumnRemoveButtons(); if (!document.querySelector("#offer-table-body tr")) addOfferRow(); } function fillOfferFromProject() { const project = getProject(document.getElementById("offer-project").value); if (!project) return; setRich("offer-title", '
' + escapeHtml(project.kurum + "’na") + '
'); setRich("offer-letter", '
' + escapeHtml(project.ad + " kapsamında talep edilen hizmetlere ilişkin fiyat teklifimiz aşağıdaki gibidir.") + '
'); const body = document.getElementById("offer-table-body"); body.innerHTML = ""; project.costs.forEach(cost => addOfferRow({ desc: cost.desc || cost.category || "", unit: cost.unit || "Adet", qty: cost.qty || 1, days: cost.days || 1, price: cost.price || 0 })); if (!project.costs.length) addOfferRow(); } function openOfferForProject(projectId) { const project = getProject(projectId); if (!project) return showToast("İş bulunamadı."); if (project.durum !== "takip") return showToast("Kapanan işler için yeni teklif hazırlanamaz."); switchView("offer"); renderOfferProjects(); document.getElementById("offer-project").value = projectId; fillOfferFromProject(); } function escapeAttr(value) { return String(value).replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">"); } function offerColumns() { return Array.from(document.querySelectorAll("#offer-table-head th")) .filter(th => !th.classList.contains("offer-action-head")) .map((th, index) => { const label = th.querySelector(".offer-col-label, .offer-header-title"); return { el: th, index, key: th.dataset.key || "custom", title: (label ? label.textContent.trim() : th.textContent.replace("×", "").trim()) || " " }; }); } function cellForOfferColumn(col, data = {}, rowNo = 1) { const customIndex = Number(col.el.dataset.customIndex || 0); if (col.key === "no") return ``; if (col.key === "desc") return ``; if (col.key === "unit") return ``; if (col.key === "qty") return ``; if (col.key === "days") return ``; if (col.key === "price") return ``; if (col.key === "total") return ``; return ``; } function addOfferRow(data = {}) { const body = document.getElementById("offer-table-body"); if (!body) return; refreshOfferColumnRemoveButtons(); const tr = document.createElement("tr"); const rowNo = body.children.length + 1; tr.innerHTML = offerColumns().map(col => cellForOfferColumn(col, data, rowNo)).join("") + ``; body.appendChild(tr); bindOfferRowDrag(tr); recalcOfferTable(); updateEditableTableWidths(); } function bindOfferRowDrag(row) { if (!row) return; row.setAttribute("ondragover", "allowOfferRowDrop(event,this)"); row.setAttribute("ondrop", "dropOfferRow(event,this)"); row.setAttribute("ondragleave", "clearDragDropTargets()"); const handle = row.querySelector(".row-drag-handle"); if (handle && !handle.dataset.dragBound) { handle.dataset.dragBound = "1"; handle.addEventListener("dragstart", event => startOfferRowDrag(event, row)); handle.addEventListener("dragend", clearDragDropTargets); } } function offerCustomHeaders() { return offerColumns().filter(col => col.key === "custom").map(col => col.title || "Yeni Sütun"); } function refreshOfferColumnRemoveButtons() { offerColumns().forEach((col, index) => { let inner = col.el.querySelector(".offer-th-inner"); const label = col.el.querySelector(".offer-col-label, .offer-header-title"); const remove = col.el.querySelector(".offer-col-remove"); if (!inner) { inner = document.createElement("div"); inner.className = "offer-th-inner"; const currentNodes = Array.from(col.el.childNodes); currentNodes.forEach(node => inner.appendChild(node)); col.el.appendChild(inner); } let handle = col.el.querySelector(".column-drag-handle"); if (!handle) { handle = document.createElement("button"); handle.type = "button"; handle.className = "column-drag-handle"; handle.textContent = "⛶"; handle.title = "Sütunu tutup taşı"; handle.draggable = true; inner.insertBefore(handle, inner.firstChild); } handle.setAttribute("draggable", "true"); handle.ondragstart = event => startOfferColumnDrag(event, index); handle.ondragend = clearDragDropTargets; col.el.ondragover = event => allowOfferColumnDrop(event, index); col.el.ondrop = event => dropOfferColumn(event, index); col.el.ondragleave = clearDragDropTargets; if (remove) remove.setAttribute("onclick", "removeOfferColumnAt(" + index + ")"); }); Array.from(document.querySelectorAll("#offer-table-head th[data-key='custom']")).forEach((th, index) => th.dataset.customIndex = index); } function getCostTableScrollBox() { return document.getElementById("cost-table-scroll") || document.querySelector("#cost-body")?.closest(".table-wrap"); } function scrollCostTable(direction = 1) { const box = getCostTableScrollBox(); if (!box) return; const amount = Math.max(280, Math.floor(box.clientWidth * 0.72)); box.scrollBy({ left: amount * (direction >= 0 ? 1 : -1), behavior: "smooth" }); box.classList.add("is-nudged"); setTimeout(() => box.classList.remove("is-nudged"), 650); } function focusCostColumnInScroll(th) { const box = getCostTableScrollBox(); if (!box || !th) return; const left = th.offsetLeft - Math.max(20, (box.clientWidth - th.offsetWidth) / 2); box.scrollTo({ left: Math.max(0, left), behavior: "smooth" }); box.classList.add("is-nudged"); setTimeout(() => box.classList.remove("is-nudged"), 650); } function getOfferTableScrollBox() { return document.getElementById("offer-table-scroll") || document.querySelector("#offer-edit-table")?.closest(".table-wrap"); } function scrollOfferTable(direction = 1) { const box = getOfferTableScrollBox(); if (!box) return; const amount = Math.max(260, Math.floor(box.clientWidth * 0.72)); box.scrollBy({ left: amount * (direction >= 0 ? 1 : -1), behavior: "smooth" }); box.classList.add("is-nudged"); setTimeout(() => box.classList.remove("is-nudged"), 650); } function focusOfferColumnInScroll(th) { const box = getOfferTableScrollBox(); if (!box || !th) return; const left = th.offsetLeft - Math.max(20, (box.clientWidth - th.offsetWidth) / 2); box.scrollTo({ left: Math.max(0, left), behavior: "smooth" }); box.classList.add("is-nudged"); setTimeout(() => box.classList.remove("is-nudged"), 650); } function addOfferColumn(title = "Yeni Sütun") { const head = document.getElementById("offer-table-head"); if (!head) return; refreshOfferColumnRemoveButtons(); const cols = offerColumns(); const descIndex = cols.findIndex(col => col.key === "desc"); const insertIndex = descIndex >= 0 ? descIndex + 1 : Math.min(cols.length, 2); const beforeColumn = cols[insertIndex]?.el || head.querySelector(".offer-action-head"); const th = document.createElement("th"); th.dataset.key = "custom"; th.className = "offer-custom-header"; th.innerHTML = '
'; const label = th.querySelector(".offer-col-label"); label.textContent = title || "Yeni Sütun"; head.insertBefore(th, beforeColumn); document.querySelectorAll("#offer-table-body tr").forEach(row => { const td = document.createElement("td"); td.innerHTML = ''; const beforeCell = row.children[insertIndex] || row.lastElementChild; row.insertBefore(td, beforeCell); }); refreshOfferColumnRemoveButtons(); renumberOfferRows(); recalcOfferTable(); updateEditableTableWidths(); setTimeout(() => { focusOfferColumnInScroll(th); label?.focus(); const selection = window.getSelection?.(); if (selection && label) { const range = document.createRange(); range.selectNodeContents(label); selection.removeAllRanges(); selection.addRange(range); } }, 60); showToast("Teklif sütunu açıklama alanının yanına eklendi."); } function removeOfferColumnAt(index) { const cols = offerColumns(); if (!cols.length || index < 0 || index >= cols.length) return showToast("Silinecek sütun yok."); if (cols.length === 1) return showToast("Tabloda en az bir sütun kalmalı."); if (!confirmDelete("Bu teklif sütununu silmek istediğinize emin misiniz?")) return; const th = cols[index].el; th.remove(); document.querySelectorAll("#offer-table-body tr").forEach(row => { if (row.children[index]) row.children[index].remove(); }); refreshOfferColumnRemoveButtons(); renumberOfferRows(); recalcOfferTable(); updateEditableTableWidths(); showToast("Sütun silindi."); } function removeOfferColumn() { const cols = offerColumns(); if (!cols.length) return showToast("Silinecek sütun yok."); removeOfferColumnAt(cols.length - 1); } function removeOfferRow(buttonEl) { const row = buttonEl.closest("tr"); if (!row) return; if (!confirmDelete("Bu teklif satırını silmek istediğinize emin misiniz?")) return; row.remove(); renumberOfferRows(); recalcOfferTable(); showToast("Satır silindi."); } function moveOfferRow(buttonEl, dir) { const row = buttonEl.closest("tr"); const body = row?.parentElement; if (!row || !body) return; const sibling = dir < 0 ? row.previousElementSibling : row.nextElementSibling; if (!sibling) return; if (dir < 0) body.insertBefore(row, sibling); else body.insertBefore(sibling, row); renumberOfferRows(); recalcOfferTable(); } function startOfferRowDrag(event, row) { event.stopPropagation(); const rows = Array.from(document.querySelectorAll("#offer-table-body tr")); window.crmDragState = { type: "offer-row", from: rows.indexOf(row) }; event.dataTransfer?.setData("text/plain", "offer-row:" + window.crmDragState.from); event.dataTransfer && (event.dataTransfer.effectAllowed = "move"); row.classList.add("dragging-row"); } function allowOfferRowDrop(event, row) { if (window.crmDragState?.type !== "offer-row") return; event.preventDefault(); clearDragDropTargets(); row.classList.add("drag-drop-target"); } function dropOfferRow(event, targetRow) { if (window.crmDragState?.type !== "offer-row") return; event.preventDefault(); const body = document.getElementById("offer-table-body"); const rows = Array.from(body?.children || []); const from = window.crmDragState.from; const to = rows.indexOf(targetRow); clearDragDropTargets(); window.crmDragState = null; if (!body || from < 0 || to < 0 || from === to) return; const row = rows[from]; const ref = rows[to]; if (!row || !ref) return; body.insertBefore(row, from < to ? ref.nextSibling : ref); renumberOfferRows(); recalcOfferTable(); } function startOfferColumnDrag(event, index) { event.stopPropagation(); window.crmDragState = { type: "offer-col", from: index }; event.dataTransfer?.setData("text/plain", "offer-col:" + index); event.dataTransfer && (event.dataTransfer.effectAllowed = "move"); event.currentTarget.closest("th")?.classList.add("dragging-col"); } function allowOfferColumnDrop(event, targetIndex) { if (window.crmDragState?.type !== "offer-col") return; event.preventDefault(); clearDragDropTargets(); event.currentTarget.classList.add("drag-drop-target"); } function dropOfferColumn(event, targetIndex) { if (window.crmDragState?.type !== "offer-col") return; event.preventDefault(); const from = window.crmDragState.from; clearDragDropTargets(); window.crmDragState = null; moveOfferColumn(from, targetIndex); } function moveOfferColumn(from, to) { const cols = offerColumns(); if (from < 0 || to < 0 || from >= cols.length || to >= cols.length || from === to) return; const head = document.getElementById("offer-table-head"); const movedTh = cols[from].el; const targetTh = cols[to].el; if (!head || !movedTh || !targetTh) return; head.insertBefore(movedTh, from < to ? targetTh.nextSibling : targetTh); document.querySelectorAll("#offer-table-body tr").forEach(row => { const cell = row.children[from]; const target = row.children[to]; if (cell && target) row.insertBefore(cell, from < to ? target.nextSibling : target); }); refreshOfferColumnRemoveButtons(); renumberOfferRows(); recalcOfferTable(); } function clearDragDropTargets() { document.querySelectorAll(".dragging-row, .dragging-col, .drag-drop-target").forEach(el => el.classList.remove("dragging-row", "dragging-col", "drag-drop-target")); } function renumberOfferRows() { document.querySelectorAll("#offer-table-body tr").forEach((row, index) => { const noInput = row.querySelector(".offer-no-input"); if (noInput && !noInput.dataset.manual) noInput.value = index + 1; const no = row.querySelector(".offer-no"); if (no) no.textContent = index + 1; }); } function recalcOfferTable() { let grandTotal = 0; document.querySelectorAll("#offer-table-body tr").forEach(row => { const qtyEl = row.querySelector(".offer-qty"); const daysEl = row.querySelector(".offer-days"); const priceEl = row.querySelector(".offer-price"); const total = n(qtyEl ? qtyEl.value : 1) * n(daysEl ? daysEl.value : 1) * n(priceEl ? priceEl.value : 0); const totalText = row.querySelector(".offer-total"); if (totalText) totalText.textContent = money(total); const totalInput = row.querySelector(".offer-total-input"); if (totalInput && !totalInput.dataset.manual) totalInput.value = total.toFixed(2); grandTotal += totalInput ? n(totalInput.value) : total; }); renderOfferTotalRow(grandTotal); } function renderOfferTotalRow(total) { const foot = document.getElementById("offer-table-foot"); if (!foot) return; const cols = offerColumns(); if (!cols.length) return foot.innerHTML = ""; const totalIndex = cols.findIndex(col => col.key === "total"); const valueIndex = totalIndex >= 0 ? totalIndex : cols.length - 1; const labelIndex = Math.max(0, valueIndex - 1); const cells = cols.map((col, index) => { if (index === valueIndex) return '' + money(total) + ''; if (index === labelIndex) return 'Genel Toplam'; return ''; }).join(""); foot.innerHTML = '' + cells + ''; } function clearOfferRows() { if (!confirmDelete("Teklif tablosunu temizlemek istediğinize emin misiniz?")) return; document.getElementById("offer-table-body").innerHTML = ""; addOfferRow(); } function getOfferCellValue(row, col) { const cell = row.children[col.index]; if (!cell) return ""; const input = cell.querySelector("input, textarea, select"); if (input) { if (col.key === "price" || col.key === "total") return money(n(input.value)); return input.value.trim(); } return cell.textContent.trim(); } function offerRows() { const cols = offerColumns(); return Array.from(document.querySelectorAll("#offer-table-body tr")).map(row => { const qty = n(row.querySelector(".offer-qty")?.value ?? 1); const days = n(row.querySelector(".offer-days")?.value ?? 1); const price = n(row.querySelector(".offer-price")?.value ?? 0); const calculatedTotal = qty * days * price; const manualTotal = row.querySelector(".offer-total-input") ? n(row.querySelector(".offer-total-input").value) : calculatedTotal; const values = cols.map(col => { if (col.key === "no") return row.querySelector(".offer-no-input")?.value.trim() || String(Array.from(row.parentElement.children).indexOf(row) + 1); if (col.key === "total") return money(manualTotal); return getOfferCellValue(row, col); }); const desc = row.querySelector(".offer-desc")?.value.trim() || values.find(Boolean) || ""; const unit = row.querySelector(".offer-unit")?.value.trim() || ""; const extras = Array.from(row.querySelectorAll(".offer-extra")).map(input => input.value.trim()); return { desc, unit, qty, days, price, total: manualTotal, extras, values }; }).filter(row => row.values.some(Boolean)); } async function loadOfferTableFile(event) { const file = event.target.files[0]; if (!file) return; try { if (!file.name.toLowerCase().endsWith(".xlsx")) throw new Error("Lütfen güncel Excel (.xlsx) dosyası yükleyin."); const rows = await readXlsxRows(file); applyOfferImportedRows(rows); showToast("Excel tablosu yüklendi."); } catch (error) { showToast(error.message || "Tablo okunamadı."); } finally { event.target.value = ""; } } function readTextTableRows(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || "").split(/\r?\n/).map(line => line.split(/[;,]/).map(cell => cell.trim())).filter(row => row.some(Boolean))); reader.onerror = () => reject(new Error("Dosya okunamadı.")); reader.readAsText(file, "utf-8"); }); } async function readXlsxRows(file) { if (file.name.toLowerCase().endsWith(".xls")) throw new Error("Lütfen eski .xls yerine Excel'den .xlsx olarak kaydedip yükleyin."); if (!window.JSZip) throw new Error("Excel okumak için jszip.min.js dosyası aynı klasörde olmalı."); const zip = await JSZip.loadAsync(await file.arrayBuffer()); const sharedXml = await zip.file("xl/sharedStrings.xml")?.async("text").catch(() => "") || ""; const sharedDoc = new DOMParser().parseFromString(sharedXml, "application/xml"); const shared = Array.from(sharedDoc.querySelectorAll("si")).map(si => si.textContent || ""); const sheetXml = await zip.file("xl/worksheets/sheet1.xml")?.async("text"); if (!sheetXml) throw new Error("Excel içindeki ilk sayfa okunamadı."); const doc = new DOMParser().parseFromString(sheetXml, "application/xml"); return Array.from(doc.querySelectorAll("sheetData row")).map(row => { const cells = []; row.querySelectorAll("c").forEach(cell => { const ref = cell.getAttribute("r") || ""; const index = columnIndex(ref); const valueNode = cell.querySelector("v"); const inlineNode = cell.querySelector("is t"); const raw = inlineNode ? inlineNode.textContent : (valueNode ? valueNode.textContent : ""); const value = cell.getAttribute("t") === "s" ? (shared[Number(raw)] || "") : raw; cells[index] = value || ""; }); return cells.map(cell => String(cell ?? "").trim()); }).filter(row => row.some(Boolean)); } function columnIndex(cellRef) { const letters = String(cellRef || "").replace(/[0-9]/g, "").toUpperCase(); let total = 0; for (const char of letters) total = total * 26 + char.charCodeAt(0) - 64; return Math.max(0, total - 1); } function applyOfferImportedRows(rows) { if (!rows.length) return showToast("Tabloda okunacak satır bulunamadı."); const header = rows[0].map(x => String(x || "").toLocaleLowerCase("tr-TR")); const hasHeader = header.some(h => ["no", "açıklama", "aciklama", "birim", "adet", "gün", "gun", "birim fiyat", "toplam"].includes(h)); const dataRows = hasHeader ? rows.slice(1) : rows; const pick = (row, names, fallbackIndex) => { const found = hasHeader ? header.findIndex(h => names.some(name => h.includes(name))) : -1; return row[found >= 0 ? found : fallbackIndex] || ""; }; const body = document.getElementById("offer-table-body"); body.innerHTML = ""; dataRows.forEach((row, index) => addOfferRow({ no: pick(row, ["no"], hasHeader ? 0 : 0) || index + 1, desc: pick(row, ["açıklama", "aciklama", "hizmet", "iş", "is"], hasHeader ? 1 : 0), unit: pick(row, ["birim"], hasHeader ? 2 : 1) || "Adet", qty: pick(row, ["adet", "miktar"], hasHeader ? 3 : 2) || 1, days: pick(row, ["gün", "gun"], hasHeader ? 4 : 3) || 1, price: pick(row, ["birim fiyat", "fiyat"], hasHeader ? 5 : 4) || 0, total: pick(row, ["toplam"], hasHeader ? 6 : 5) || undefined })); if (!body.children.length) addOfferRow(); recalcOfferTable(); } async function submitOffer(event) { event.preventDefault(); await downloadOffer("word"); } function toggleOfferDownload() { const menu = document.getElementById("offer-download-options"); if (menu) menu.classList.toggle("hidden"); } function offerPayload() { const title = richText("offer-title"); const titleHtml = richHtml("offer-title"); const letterHtml = richHtml("offer-letter"); const noteHtml = richHtml("offer-note"); const rows = offerRows(); if (!title || !richText("offer-letter") || !rows.length) return showToast("Başlık, üst yazı ve en az bir tablo satırı girin."); return { title, titleHtml, letterHtml, noteHtml, rows, headers: offerTableHeaders(), projectId: read("offer-project"), total: rows.reduce((sum, row) => sum + n(row.total), 0) }; } function syncOfferTotalToProject(payload, format = "word") { const p = getProject(payload?.projectId); if (!p) return; p.sales = n(payload.total); const snapshot = normalizeProjectOffers([{ ...payload, id: Date.now() + "-" + Math.random().toString(16).slice(2), format, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }])[0]; if (snapshot) { const currentOffers = normalizeProjectOffers(p.offers || []); p.offers = [snapshot, ...currentOffers].slice(0, 10); } persist(); const salesInput = document.getElementById("calc-sales"); if (salesInput && String(activeProjectId) === String(p.id)) { salesInput.value = p.sales || ""; calculate(); } if (String(detailProjectId || "") === String(p.id)) renderDetailOffersPanel(); addLog("Teklif tutarını satış alanına aktardı", p.kurum + " - " + money(p.sales)); } async function downloadOffer(format) { const menu = document.getElementById("offer-download-options"); if (menu) menu.classList.add("hidden"); const payload = offerPayload(); if (!payload) return; let preOpenedPdfWindow = null; if (format === "pdf") { preOpenedPdfWindow = window.open("", "_blank"); if (preOpenedPdfWindow) { preOpenedPdfWindow.document.open(); preOpenedPdfWindow.document.write('Teklif hazırlanıyor
Teklif hazırlanıyor...
'); preOpenedPdfWindow.document.close(); } } try { if (format === "pdf") { const topLogoData = await firstImageDataUrl(["offer-assets/logo-top.png", "logo-top.png", "simple-event-logo.png"], EMBEDDED_OFFER_IMAGES.topLogo); const bgLogoData = await firstImageDataUrl(["offer-assets/logo-bg.png"], EMBEDDED_OFFER_IMAGES.bgLogo); const footerData = await firstImageDataUrl(["offer-assets/antetli-alt.png"], EMBEDDED_OFFER_IMAGES.footer); syncOfferTotalToProject(payload, "pdf"); openOfferPdf(payload, topLogoData, bgLogoData, footerData, preOpenedPdfWindow); addLog("Teklif PDF ekranı açtı", payload.title); return showToast("PDF için açılan pencerede Yazdır / PDF olarak kaydet seçin."); } const blob = await buildTemplateDocx(payload); syncOfferTotalToProject(payload, "word"); download(blob, safeName(payload.title) + "-fiyat-teklifi.docx"); addLog("Teklif oluşturdu", payload.title); showToast("Antetli Word teklifi indirildi."); } catch (error) { console.error(error); if (format === "word") { const topLogoData = await firstImageDataUrl(["offer-assets/logo-top.png", "logo-top.png", "simple-event-logo.png"], EMBEDDED_OFFER_IMAGES.topLogo); const bgLogoData = await firstImageDataUrl(["offer-assets/logo-bg.png"], EMBEDDED_OFFER_IMAGES.bgLogo); const footerData = await firstImageDataUrl(["offer-assets/antetli-alt.png"], EMBEDDED_OFFER_IMAGES.footer); const blob = buildFallbackWord(payload, topLogoData, bgLogoData, footerData); syncOfferTotalToProject(payload, "word"); download(blob, safeName(payload.title) + "-fiyat-teklifi.doc"); return showToast("DOCX oluşturulamadı; logolu Word uyumlu DOC indirildi."); } showToast("Teklif indirilemedi: " + (error.message || "Bilinmeyen hata")); } } function offerTableHeaders() { return offerColumns().map(col => col.title || " "); } async function imageDataUrl(src) { const response = await fetch(src); if (!response.ok) return ""; const blob = await response.blob(); return await new Promise(resolve => { const reader = new FileReader(); reader.onload = () => resolve(reader.result || ""); reader.onerror = () => resolve(""); reader.readAsDataURL(blob); }); } async function firstImageDataUrl(paths, fallback = "") { if (fallback) return fallback; for (const path of paths) { const data = await imageDataUrl(path).catch(() => ""); if (data) return data; } return ""; } async function buildTemplateDocx(payload) { if (!window.JSZip) throw new Error("Word oluşturucu yüklenemedi. jszip.min.js aynı klasörde olmalı."); const zip = new JSZip(); const logoBuffer = dataUrlToArrayBuffer(EMBEDDED_OFFER_IMAGES.topLogo) || await firstArrayBuffer(["offer-assets/logo-top.png", "logo-top.png", "simple-event-logo.png"]); const bgBuffer = dataUrlToArrayBuffer(EMBEDDED_OFFER_IMAGES.bgLogo) || await fetchArrayBufferMaybe("offer-assets/logo-bg.png"); const footerBuffer = dataUrlToArrayBuffer(EMBEDDED_OFFER_IMAGES.footer) || await fetchArrayBufferMaybe("offer-assets/antetli-alt.png"); zip.file("[Content_Types].xml", contentTypesXml()); zip.folder("_rels").file(".rels", packageRelsXml()); zip.folder("docProps").file("core.xml", docCoreXml(payload.title)); zip.folder("docProps").file("app.xml", docAppXml()); const word = zip.folder("word"); word.file("document.xml", documentXml(buildDocxBody(payload, !!bgBuffer))); word.file("styles.xml", stylesXml()); word.folder("_rels").file("document.xml.rels", documentRelsXml()); word.file("header1.xml", headerXml(!!logoBuffer)); word.file("footer1.xml", footerXml(!!footerBuffer)); word.folder("_rels").file("header1.xml.rels", imageRelsXml("rIdLogo", "media/logo-top.png")); word.folder("_rels").file("footer1.xml.rels", imageRelsXml("rIdFooter", "media/antetli-alt.png")); if (logoBuffer) word.folder("media").file("logo-top.png", logoBuffer); if (bgBuffer) word.folder("media").file("logo-bg.png", bgBuffer); if (footerBuffer) word.folder("media").file("antetli-alt.png", footerBuffer); return await zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }); } async function fetchArrayBufferMaybe(src) { try { const response = await fetch(src); if (!response.ok) return null; return await response.arrayBuffer(); } catch { return null; } } async function firstArrayBuffer(paths) { for (const path of paths) { const buffer = await fetchArrayBufferMaybe(path); if (buffer) return buffer; } return null; } function dataUrlToArrayBuffer(dataUrl) { if (!dataUrl || !String(dataUrl).startsWith("data:")) return null; const base64 = String(dataUrl).split(",")[1] || ""; if (!base64) return null; const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return bytes.buffer; } function contentTypesXml() { return '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; } function packageRelsXml() { return '' + '' + '' + '' + '' + ''; } function documentRelsXml() { return '' + '' + '' + '' + '' + ''; } function imageRelsXml(id, target) { return '' + '' + '' + ''; } function docCoreXml(title) { const now = new Date().toISOString(); return '' + '' + '' + escapeXml(title || 'Simple Event Fiyat Teklifi') + 'Simple Event CRM' + 'Simple Event CRM' + now + '' + now + '' + ''; } function docAppXml() { return 'Simple Event CRM'; } function stylesXml() { return ''; } function documentXml(bodyXml) { return '' + '' + '' + bodyXml + ''; } function headerXml(hasLogo) { const content = hasLogo ? docxImageDrawing('rIdLogo', 2200000, 761538, 'Simple Event Logo') : 'Simple Event'; return docPartXml('hdr', '' + content + ''); } function footerXml(hasFooter) { const content = hasFooter ? docxImageDrawing('rIdFooter', 6200000, 680000, 'Simple Event Antet Alt') : 'event@simpleevent.com.tr | www.simpleevent.com.tr'; return docPartXml('ftr', '' + content + ''); } function docPartXml(type, innerXml) { return '' + '' + innerXml + ''; } function docxImageDrawing(relId, cx, cy, name) { return ''; } function docxWatermarkDrawing(relId, cx, cy, name) { return 'centercenter'; } function buildDocxBody(payload, hasBgLogo = false) { let xml = ""; if (hasBgLogo) xml += '' + docxWatermarkDrawing('rIdBgLogo', 5200000, 5200000, 'Simple Event Arka Plan Logo') + ''; htmlToLines(payload.titleHtml).forEach(line => { xml += docxParagraph(line, { center: true, bold: true, size: 28, after: 180 }); }); htmlToLines(payload.letterHtml).forEach(line => { xml += docxParagraph(line, { size: 22, after: 120 }); }); xml += docxParagraph("", { after: 80 }); xml += docxOfferTable(payload); htmlToLines(payload.noteHtml).forEach(line => { xml += docxParagraph(line, { bold: true, size: 20, after: 90 }); }); return xml; } function htmlToLines(html) { const div = document.createElement("div"); div.innerHTML = sanitizeRichHtml(html || ""); const text = (div.innerText || div.textContent || "").replace(/\u00a0/g, " ").trim(); return text ? text.split(/\n+/).map(line => line.trim()).filter(Boolean) : []; } function docxOfferTable(payload) { const headers = payload.headers || ["No", "Açıklama", "Birim", "Adet", "Gün", "Birim Fiyat", "Toplam"]; const width = Math.max(700, Math.floor(9000 / headers.length)); let xml = ''; headers.forEach(() => { xml += ''; }); xml += ''; headers.forEach((header, index) => { xml += docxCell(header, width, true, index === 1 ? "left" : "center"); }); xml += ''; let grand = 0; payload.rows.forEach((row, index) => { grand += n(row.total); const values = row.values || [String(index + 1), row.desc, row.unit, String(row.qty), String(row.days), money(row.price), money(row.total), ...(row.extras || [])]; xml += ''; headers.forEach((_, cellIndex) => { const value = values[cellIndex] ?? ""; xml += docxCell(value, width, false, cellIndex === 0 || cellIndex >= 3 ? "right" : "left"); }); xml += ''; }); const totalSpan = Math.max(1, headers.length - 1); xml += '' + docxCellSpan("GENEL TOPLAM", width * totalSpan, totalSpan, "right") + docxCell(money(grand), width, false, "right"); xml += ''; xml += '' + docxParagraph("", { after: 100 }); return xml; } function docxParagraph(text, opts = {}) { const jc = opts.center ? '' : ''; const bold = opts.bold ? '' : ''; const size = opts.size || 22; const after = opts.after ?? 120; const content = text ? '' + escapeXml(text) + '' : ' '; return '' + jc + '' + bold + '' + content + ''; } function docxCell(text, width, header = false, align = "left") { const fill = header ? '' : ''; const color = header ? '' : ''; const bold = header ? '' : ''; const jc = align === "center" ? '' : (align === "right" ? '' : ''); return '' + fill + '' + jc + '' + bold + color + '' + escapeXml(text) + ''; } function docxCellSpan(text, width, span, align = "left") { const jc = align === "center" ? '' : (align === "right" ? '' : ''); return '' + jc + '' + escapeXml(text) + ''; } function escapeXml(value) { return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); } function openOfferPdf(payload, topLogoData, bgLogoData, footerData, existingWindow = null) { const win = existingWindow || window.open("", "_blank"); if (!win) return showToast("PDF penceresi açılamadı. Tarayıcı açılır pencereye izin vermeli."); win.document.open(); win.document.write(buildOfferHtml(payload, topLogoData, bgLogoData, footerData, true)); win.document.close(); try { win.focus(); } catch (error) {} setTimeout(() => { try { win.print(); } catch (error) { showToast("Yazdırma ekranı otomatik açılmazsa yeni sekmedeki tarayıcı menüsünden Yazdır seçin."); } }, 650); } function buildOfferHtml(payload, topLogoData, bgLogoData, footerData, printable = false) { const total = payload.rows.reduce((sum, row) => sum + n(row.total), 0); const cleanTitle = sanitizeRichHtml(payload.titleHtml); const cleanLetter = sanitizeRichHtml(payload.letterHtml); const cleanNote = sanitizeRichHtml(payload.noteHtml); const headers = payload.headers || ["No", "Açıklama", "Birim", "Adet", "Gün", "Birim Fiyat", "Toplam"]; const totalSpan = Math.max(1, headers.length - 1); const rowsHtml = payload.rows.map((row, index) => { const values = row.values || [String(index + 1), row.desc, row.unit, String(row.qty), String(row.days), money(row.price), money(row.total), ...(row.extras || [])]; return "" + headers.map((_, i) => '' + escapeHtml(values[i] || "") + "").join("") + ""; }).join(""); const logoHtml = topLogoData ? `Simple Event` : "SIMPLE EVENT"; const bgHtml = bgLogoData ? `` : ""; const footerHtml = footerData ? `Simple Event antet alt` : ""; return `${escapeHtml(payload.title)} Fiyat Teklifi
${bgHtml}
${cleanTitle}
${cleanLetter}
${headers.map(h => "").join("")}${rowsHtml}
" + escapeHtml(h) + "
GENEL TOPLAM${money(total)}
${cleanNote}
`; } function buildOfferWord(titleHtml, letterHtml, rows, logoData) { const total = rows.reduce((sum, row) => sum + n(row.total), 0); const cleanTitle = sanitizeRichHtml(titleHtml); const cleanLetter = sanitizeRichHtml(letterHtml); const logoHtml = logoData ? `Simple Event` : "SIMPLE EVENT"; const rowsHtml = rows.map((row, index) => ` ${index + 1} ${escapeHtml(row.desc)} ${escapeHtml(row.unit)} ${row.qty} ${row.days} ${money(row.price)} ${money(row.total)} `).join(""); const html = `
Organizasyon & Etkinlik Yönetimi
simpleevent.com.tr
${cleanTitle}
${cleanLetter}
${rowsHtml}
NoAçıklamaBirimAdetGünBirim FiyatToplam
GENEL TOPLAM${money(total)}

Fiyatlarımıza KDV dahil değildir. Teklifimiz belirtilen hizmet kalemleri için hazırlanmıştır.

`; return new Blob(["\uFEFF" + html], { type: "application/msword;charset=utf-8" }); } function buildFallbackWord(payload, topLogoData = "", bgLogoData = "", footerData = "") { const total = payload.rows.reduce((sum, row) => sum + n(row.total), 0); const headers = payload.headers || []; const rowsHtml = payload.rows.map(row => { const values = row.values || []; return "" + headers.map((_, i) => "" + escapeHtml(values[i] || "") + "").join("") + ""; }).join(""); const logoHtml = topLogoData ? 'Simple Event' : 'SIMPLE EVENT'; const bgHtml = bgLogoData ? '' : ""; const footerHtml = footerData ? '' : '
event@simpleevent.com.tr / www.simpleevent.com.tr
'; const html = `
${logoHtml}
${bgHtml}
${sanitizeRichHtml(payload.titleHtml)}
${sanitizeRichHtml(payload.letterHtml)}
${headers.map(h => "").join("")}${rowsHtml}
" + escapeHtml(h) + "
GENEL TOPLAM${money(total)}
${sanitizeRichHtml(payload.noteHtml)}
`; return new Blob(["\uFEFF" + html], { type: "application/msword;charset=utf-8" }); } function sanitizeRichHtml(html) { const template = document.createElement("template"); template.innerHTML = html || ""; template.content.querySelectorAll("script, iframe, object, embed, link, meta").forEach(node => node.remove()); template.content.querySelectorAll("*").forEach(node => { [...node.attributes].forEach(attr => { if (/^on/i.test(attr.name) || String(attr.value).toLowerCase().includes("javascript:")) node.removeAttribute(attr.name); }); }); return template.innerHTML; } function fillCompletionReportForm(report) { document.getElementById("cr-kurum").value = report.kurum || ""; document.getElementById("cr-ad").value = report.ad || ""; document.getElementById("cr-tarih-yer").value = report.tarihYer || ""; document.getElementById("cr-otel").value = report.otel || ""; document.getElementById("cr-advance-person").value = report.advancePerson || ""; document.getElementById("cr-advance-date").value = report.advanceDate || ""; document.getElementById("cr-advance-amount").value = report.advanceAmount || ""; document.getElementById("cr-report-note").value = report.note || ""; renderCompletionExpenseRows(report.expenses || [blankCompletionExpense()], report.expenseColumns || []); renderCompletionIncomeRows(report.incomes || [blankCompletionIncome()], report.incomeColumns || []); renderCompletionAdvanceSpendRows(report.advanceSpends || [], report.advanceColumns || []); recalcCompletionReportForm(); } function readCompletionReportForm() { const expenses = [...document.querySelectorAll("#cr-expense-body tr")].map(row => ({ firma: row.querySelector('[data-field="firma"]')?.value.trim() || "", aciklama: row.querySelector('[data-field="aciklama"]')?.value.trim() || "", faturaNo: row.querySelector('[data-field="faturaNo"]')?.value.trim() || "", tutar: n(row.querySelector('[data-field="tutar"]')?.value), odemeSekli: row.querySelector('[data-field="odemeSekli"]')?.value.trim() || "", odemeVadesi: row.querySelector('[data-field="odemeVadesi"]')?.value || "", notlar: row.querySelector('[data-field="notlar"]')?.value.trim() || "", extra: [...row.querySelectorAll('[data-extra]')].map(input => input.value.trim()) })); const incomes = [...document.querySelectorAll("#cr-income-body tr")].map(row => ({ firma: row.querySelector('[data-field="firma"]')?.value.trim() || "", aciklama: row.querySelector('[data-field="aciklama"]')?.value.trim() || "", tutar: n(row.querySelector('[data-field="tutar"]')?.value), nakit: n(row.querySelector('[data-field="nakit"]')?.value), kart: n(row.querySelector('[data-field="kart"]')?.value), notlar: row.querySelector('[data-field="notlar"]')?.value.trim() || "", extra: [...row.querySelectorAll('[data-extra]')].map(input => input.value.trim()) })).map(row => ({ ...row, kalan: Math.max(0, row.tutar - row.nakit - row.kart) })); const advanceSpends = [...document.querySelectorAll("#cr-advance-spend-body tr")].map(row => ({ firma: row.querySelector('[data-field="firma"]')?.value.trim() || "", tarih: row.querySelector('[data-field="tarih"]')?.value || "", aciklama: row.querySelector('[data-field="aciklama"]')?.value.trim() || "", tutar: n(row.querySelector('[data-field="tutar"]')?.value), extra: [...row.querySelectorAll('[data-extra]')].map(input => input.value.trim()) })); return normalizeCompletionReport({ kurum: read("cr-kurum"), ad: read("cr-ad"), tarihYer: read("cr-tarih-yer"), otel: read("cr-otel"), expenseColumns: readCompletionExtraColumns("expense"), incomeColumns: readCompletionExtraColumns("income"), advanceColumns: readCompletionExtraColumns("advance"), expenses, incomes, advancePerson: read("cr-advance-person"), advanceDate: read("cr-advance-date"), advanceAmount: n(read("cr-advance-amount")), advanceSpends, note: read("cr-report-note") }, getProject(reportFormProjectId) || {}); } function completionInput(type, field, value, inputType = "text") { const safe = escapeAttr(value ?? ""); const step = inputType === "number" ? ' min="0" step="0.01"' : ""; return ''; } function completionExtraInputs(row, extras) { return (extras || []).map((_, index) => '').join(""); } function renderCompletionExpenseRows(rows, extras = completionColumnState.expense || []) { renderCompletionHeaders("expense", extras); const body = document.getElementById("cr-expense-body"); if (!body) return; body.innerHTML = ""; (rows.length ? rows : [blankCompletionExpense()]).forEach((r, i) => { const tr = document.createElement("tr"); tr.innerHTML = '' + (i + 1) + '' + completionInput("expense", "firma", r.firma) + '' + completionInput("expense", "aciklama", r.aciklama) + '' + completionInput("expense", "faturaNo", r.faturaNo) + '' + completionInput("expense", "tutar", r.tutar, "number") + '' + completionInput("expense", "odemeSekli", r.odemeSekli) + '' + completionInput("expense", "odemeVadesi", r.odemeVadesi, "date") + '' + completionInput("expense", "notlar", r.notlar) + '' + completionExtraInputs(r, extras) + ''; body.appendChild(tr); }); } function renderCompletionIncomeRows(rows, extras = completionColumnState.income || []) { renderCompletionHeaders("income", extras); const body = document.getElementById("cr-income-body"); if (!body) return; body.innerHTML = ""; (rows.length ? rows : [blankCompletionIncome()]).forEach((r, i) => { const tr = document.createElement("tr"); const kalan = Math.max(0, n(r.tutar) - n(r.nakit) - n(r.kart)); tr.innerHTML = '' + (i + 1) + '' + completionInput("income", "firma", r.firma) + '' + completionInput("income", "aciklama", r.aciklama) + '' + completionInput("income", "tutar", r.tutar, "number") + '' + completionInput("income", "nakit", r.nakit, "number") + '' + completionInput("income", "kart", r.kart, "number") + '' + completionInput("income", "notlar", r.notlar) + '' + completionExtraInputs(r, extras) + ''; body.appendChild(tr); }); } function renderCompletionAdvanceSpendRows(rows, extras = completionColumnState.advance || []) { renderCompletionHeaders("advance", extras); const body = document.getElementById("cr-advance-spend-body"); if (!body) return; body.innerHTML = ""; (rows || []).forEach((r, i) => { const tr = document.createElement("tr"); tr.innerHTML = '' + (i + 1) + '' + completionInput("advance", "firma", r.firma) + '' + completionInput("advance", "tarih", r.tarih, "date") + '' + completionInput("advance", "aciklama", r.aciklama) + '' + completionInput("advance", "tutar", r.tutar, "number") + '' + completionExtraInputs(r, extras) + ''; body.appendChild(tr); }); } function addCompletionExpenseRow() { const r = readCompletionReportForm(); r.expenses.push(blankCompletionExpense()); renderCompletionExpenseRows(r.expenses, r.expenseColumns); recalcCompletionReportForm(); } function addCompletionIncomeRow() { const r = readCompletionReportForm(); r.incomes.push(blankCompletionIncome()); renderCompletionIncomeRows(r.incomes, r.incomeColumns); recalcCompletionReportForm(); } function addCompletionAdvanceSpendRow() { const r = readCompletionReportForm(); r.advanceSpends.push(blankAdvanceSpend()); renderCompletionAdvanceSpendRows(r.advanceSpends, r.advanceColumns); recalcCompletionReportForm(); } function moveCompletionRow(btn, tbodyId, dir) { const row = btn.closest("tr"); const body = document.getElementById(tbodyId); if (!row || !body) return; const sibling = dir < 0 ? row.previousElementSibling : row.nextElementSibling; if (!sibling) return; if (dir < 0) body.insertBefore(row, sibling); else body.insertBefore(sibling, row); renumberCompletionRows(tbodyId); recalcCompletionReportForm(); } function removeCompletionExpenseRow(btn) { if (!confirmDelete("Bu gider satırını silmek istediğinize emin misiniz?")) return; btn.closest("tr")?.remove(); if (!document.querySelectorAll("#cr-expense-body tr").length) addCompletionExpenseRow(); renumberCompletionRows("cr-expense-body"); recalcCompletionReportForm(); } function removeCompletionIncomeRow(btn) { if (!confirmDelete("Bu gelir satırını silmek istediğinize emin misiniz?")) return; btn.closest("tr")?.remove(); if (!document.querySelectorAll("#cr-income-body tr").length) addCompletionIncomeRow(); renumberCompletionRows("cr-income-body"); recalcCompletionReportForm(); } function removeCompletionAdvanceSpendRow(btn) { if (!confirmDelete("Bu avans harcama satırını silmek istediğinize emin misiniz?")) return; btn.closest("tr")?.remove(); renumberCompletionRows("cr-advance-spend-body"); recalcCompletionReportForm(); } function renumberCompletionRows(tbodyId) { document.querySelectorAll("#" + tbodyId + " tr").forEach((tr, i) => { const cell = tr.querySelector("td"); if (cell) cell.textContent = i + 1; }); } function syncCompletionIncomeBalances() { document.querySelectorAll("#cr-income-body tr").forEach(row => { const tutar = n(row.querySelector('[data-field="tutar"]')?.value); const nakit = n(row.querySelector('[data-field="nakit"]')?.value); const kart = n(row.querySelector('[data-field="kart"]')?.value); const kalan = row.querySelector('[data-field="kalan"]'); if (kalan) kalan.value = Math.max(0, tutar - nakit - kart).toFixed(2); }); } function recalcCompletionReportForm() { syncCompletionIncomeBalances(); const r = readCompletionReportForm(); const t = completionReportTotals({ completionReport: r }); const sectionExpense = document.getElementById("cr-total-expense"); if (sectionExpense) sectionExpense.textContent = money(t.baseExpense); const summaryExpense = document.getElementById("cr-summary-expense"); if (summaryExpense) summaryExpense.textContent = money(t.expense); ["cr-total-income", "cr-summary-income"].forEach(id => { const el = document.getElementById(id); if (el) el.textContent = money(t.income); }); const spend = document.getElementById("cr-total-advance-spend"); if (spend) spend.textContent = money(t.advanceSpend); const ret = document.getElementById("cr-advance-return"); if (ret) ret.textContent = money(Math.max(0, t.advanceReturn)); const result = document.getElementById("cr-summary-result"); if (result) { result.textContent = money(t.result); result.className = t.result >= 0 ? "profit-good" : "profit-bad"; } } function openCompletionReportForm(projectId, closeAfterSave = false, closeNote = "") { const p = getProject(projectId); if (!p) return showToast("Proje bulunamadı."); reportFormProjectId = projectId; reportFormCloseAfterSave = !!closeAfterSave; document.getElementById("completion-form-subtitle").textContent = p.kurum + " - " + p.ad + (closeAfterSave ? " • Kapanıştan önce rapor tamamlanmalı" : ""); if (closeNote) p.closeNote = closeNote; p.completionReport = normalizeCompletionReport(p.completionReport, p); fillCompletionReportForm(p.completionReport); const completeBtn = document.getElementById("completion-report-complete-btn"); if (completeBtn) { if (p.durum === "kapanan" || p.completionReport?.reportCompleted) completeBtn.textContent = "Raporu Güncelle"; else if (closeAfterSave && isAdmin()) completeBtn.textContent = "Raporu Tamamla ve İşi Kapat"; else if (closeAfterSave) completeBtn.textContent = "Raporu Tamamla ve Talep Gönder"; else completeBtn.textContent = "Raporu Tamamla"; } document.getElementById("completion-report-form-modal").classList.add("show"); } function closeCompletionReportForm() { document.getElementById("completion-report-form-modal").classList.remove("show"); reportFormProjectId = null; reportFormCloseAfterSave = false; } function saveCompletionReport(markComplete = false) { const p = getProject(reportFormProjectId); if (!p) return showToast("Proje bulunamadı."); const oldReport = completionReportSnapshot(p.completionReport, p); const oldCloseState = closeStateSnapshot(p); const report = readCompletionReportForm(); if (markComplete && !reportReady(report)) return showToast("İş bitirme raporu tamamlanmadan iş kapatılamaz. Kurum, iş adı, tarih/yer ve en az bir gider veya gelir satırı doldurun."); report.reportCompleted = markComplete || !!p.completionReport?.reportCompleted; report.updatedAt = new Date().toISOString(); if (markComplete && !report.completedAt) report.completedAt = new Date().toISOString(); p.completionReport = report; if (markComplete) p.reportCompleted = true; if (markComplete && p.durum === "takip" && !isAdmin()) { addLog("İş bitirme raporunu tamamladı", p, { old: oldReport, next: completionReportSnapshot(report, p) }); closeCompletionReportForm(); createCloseRequest(p.id, "completed", p.closeNote || "İş bitirme raporu tamamlandı, kapatma onayı bekleniyor."); return; } if (markComplete && reportFormCloseAfterSave) { p.durum = "kapanan"; p.closeType = "completed"; p.closedAt = keyDate(new Date()); p.closeNote = p.closeNote || "İş bitirme raporu tamamlanarak kapatıldı."; p.closeRequest = p.closeRequest?.status === "pending" ? normalizeCloseRequest({ ...p.closeRequest, status: "approved", reviewedBy: currentUser?.name || "Admin", reviewedAt: new Date().toISOString() }) : p.closeRequest; addLog("İşi iş bitirme raporu ile kapattı", p, { old: { rapor: oldReport, kapanış: oldCloseState }, next: { rapor: completionReportSnapshot(report, p), kapanış: closeStateSnapshot(p) } }); showToast("İş bitirme raporu tamamlandı ve iş kapatıldı."); closeCompletionReportForm(); } else { addLog(markComplete ? "İş bitirme raporunu tamamladı" : "İş bitirme raporu taslağını kaydetti", p, { old: oldReport, next: completionReportSnapshot(report, p) }); showToast(markComplete ? "İş bitirme raporu tamamlandı." : "İş bitirme raporu taslak kaydedildi."); } persist(); } function previewCompletionReportFromForm() { const p = getProject(reportFormProjectId); if (!p) return; p.completionReport = readCompletionReportForm(); openCompletionReport(p.id, true); } function reportHeaderCells(base, extras = []) { return [...base, ...(extras || [])].map(x => '' + escapeHtml(x) + '').join(""); } function reportExtraCells(row) { return (row.extra || []).map(x => '' + escapeHtml(x || "") + '').join(""); } function reportTotalRow(label, value, columnCount) { return '' + escapeHtml(label) + '' + money(value) + ''; } function buildCompletionReportHtml(p, printable = false, logoData = "") { const report = normalizeCompletionReport(p.completionReport, p); const t = completionReportTotals({ completionReport: report }); const expenseHeads = ["No", "FİRMA", "AÇIKLAMA", "FATURA NO", "TUTAR", "ÖDEME ŞEKLİ", "ÖDEME VADESİ", "NOTLAR", ...(report.expenseColumns || [])]; const incomeHeads = ["No", "FİRMA veya KURUM", "AÇIKLAMA", "TUTAR", "NAKİT TAHSİLAT", "K. KARTI TAHSİLAT", "KALAN BAKİYE", "NOTLAR", ...(report.incomeColumns || [])]; const advanceHeads = ["No", "FİRMA", "TARİH", "AÇIKLAMA", "TUTAR", ...(report.advanceColumns || [])]; const expenseRows = report.expenses.map((r, i) => '' + (i + 1) + '' + escapeHtml(r.firma || "") + '' + escapeHtml(r.aciklama || "") + '' + escapeHtml(r.faturaNo || "") + '' + money(r.tutar) + '' + escapeHtml(r.odemeSekli || "") + '' + escapeHtml(r.odemeVadesi ? dateText(r.odemeVadesi) : "") + '' + escapeHtml(r.notlar || "") + '' + reportExtraCells(r) + '').join(""); const incomeRows = report.incomes.map((r, i) => '' + (i + 1) + '' + escapeHtml(r.firma || "") + '' + escapeHtml(r.aciklama || "") + '' + money(r.tutar) + '' + money(r.nakit) + '' + money(r.kart) + '' + money(r.kalan) + '' + escapeHtml(r.notlar || "") + '' + reportExtraCells(r) + '').join(""); const advanceRows = (report.advanceSpends || []).map((r, i) => '' + (i + 1) + '' + escapeHtml(r.firma || "") + '' + escapeHtml(r.tarih ? dateText(r.tarih) : "") + '' + escapeHtml(r.aciklama || "") + '' + money(r.tutar) + '' + reportExtraCells(r) + '').join(""); const logoHtml = logoData ? 'Simple Event' : 'SIMPLE EVENT'; return '
' + '
' + logoHtml + '
Rapor Tarihi: ' + new Date().toLocaleDateString("tr-TR") + '
CRM İş No: ' + escapeHtml(p.id) + '
Durum: ' + escapeHtml(report.reportCompleted ? "Tamamlandı" : "Taslak") + '
' + '

İş Bitirme Raporu

' + '
İLGİLİ KURUM' + escapeHtml(report.kurum) + '
İŞİN ADI' + escapeHtml(report.ad) + '
İŞİN YERİ / TARİHİ' + escapeHtml(report.tarihYer) + '
OTEL' + escapeHtml(report.otel || "") + '
' + '
GİDERLER
' + reportHeaderCells(expenseHeads) + '' + expenseRows + reportTotalRow("TOPLAM GİDER", t.baseExpense, expenseHeads.length) + '
' + '
GELİRLER
' + reportHeaderCells(incomeHeads) + '' + incomeRows + reportTotalRow("TOPLAM GELİR", t.income, incomeHeads.length) + '
' + '
ORGANİZASYON DİĞER GİDERLERİ
AVANSI ALAN KİŞİ' + escapeHtml(report.advancePerson || "") + 'TARİH' + escapeHtml(report.advanceDate ? dateText(report.advanceDate) : "") + 'TUTAR' + money(report.advanceAmount) + '
' + '' + reportHeaderCells(advanceHeads) + '' + (advanceRows || '') + reportTotalRow("TOPLAM HARCAMA", t.advanceSpend, advanceHeads.length) + reportTotalRow("MUHASEBEYE İADE EDİLECEK TUTAR", Math.max(0, t.advanceReturn), advanceHeads.length) + '
Avans harcaması yok.
' + '
SONUÇ
Toplam Gelir' + money(t.income) + '
Toplam Gider' + money(t.expense) + '
Sonuç' + money(t.result) + '
' + '
' + escapeHtml(report.note || "Rapor, fatura ve tahsilat bilgileri iş bitirme kapanış sürecinde sisteme girilen değerlere göre oluşturulmuştur.") + '
' + '
Teslim Eden
Onay
' + '
'; } async function openCompletionReport(projectId, fromForm = false) { const p = getProject(projectId); if (!p) return showToast("Proje bulunamadı."); if (!p.completionReport) p.completionReport = normalizeCompletionReport(null, p); reportProjectId = projectId; const logoData = await imageDataUrl("logo-top.png").catch(() => ""); document.getElementById("completion-report-subtitle").textContent = p.kurum + " - " + p.ad; document.getElementById("completion-report-preview").innerHTML = buildCompletionReportHtml(p, false, logoData); document.getElementById("completion-report-modal").classList.add("show"); if (!fromForm) addLog("İş bitirme raporu açtı", p.kurum + " - " + p.ad); } function closeCompletionReport() { document.getElementById("completion-report-modal").classList.remove("show"); reportProjectId = null; } function editOpenCompletionReport() { const projectId = reportProjectId; if (!projectId) return showToast("Düzenlenecek rapor bulunamadı."); closeCompletionReport(); openCompletionReportForm(projectId, false); } async function printCompletionReport() { const p = getProject(reportProjectId); if (!p) return; const logoData = await imageDataUrl("logo-top.png").catch(() => ""); const win = window.open("", "_blank"); if (!win) return showToast("Yazdırma penceresi açılamadı. Tarayıcı açılır pencereye izin vermeli."); const style = '@page{size:A4;margin:12mm;}body{font-family:Arial,sans-serif;color:#111827}.completion-report .report-letterhead{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:3px solid #193154;padding-bottom:12px;margin-bottom:16px}.completion-report .report-letterhead img{width:160px;height:auto}.completion-report h1{color:#193154;font-size:22px}.completion-report .report-section-title{margin:18px 0 8px;color:#193154;font-size:15px;border-left:5px solid #4abe32;padding-left:8px}.completion-report table{width:100%;border-collapse:collapse;font-size:10.8px;margin-bottom:8px}.completion-report th,.completion-report td{border:1px solid #cbd5e1;padding:5px;vertical-align:top}.completion-report th{background:#193154;color:#fff}.completion-report .plain-table th{width:170px;text-align:left}.right{text-align:right}.report-note{margin-top:10px;padding:10px;background:#f8fafc;line-height:1.45}.signature-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:26px}.signature-box{border-top:1px solid #94a3b8;padding-top:8px;text-align:center;color:#475569}'; win.document.open(); win.document.write('' + escapeHtml(p.kurum + ' İş Bitirme Raporu') + '' + buildCompletionReportHtml(p, true, logoData) + ' '); win.document.close(); win.focus(); setTimeout(() => win.print(), 450); addLog("İş bitirme raporu PDF/Yazdır açtı", p.kurum + " - " + p.ad); } function buildCompletionWorkbookHtml(p) { const report = normalizeCompletionReport(p.completionReport, p); const t = completionReportTotals({ completionReport: report }); const expenseHeads = ["GİDERLER", "FİRMA", "AÇIKLAMA", "FATURA NO", "TUTAR", "ÖDEME ŞEKLİ", "ÖDEME VADESİ", "NOTLAR", ...(report.expenseColumns || [])]; const incomeHeads = ["GELİRLER", "FİRMA veya KURUM", "AÇIKLAMA", "TUTAR", "NAKİT TAHSİLAT", "K. KARTI TAHSİLAT", "KALAN BAKİYE", "NOTLAR", ...(report.incomeColumns || [])]; const advanceHeads = ["FİRMA", "TARİH", "AÇIKLAMA", "TUTAR", ...(report.advanceColumns || [])]; const xlsExtraCells = row => (row.extra || []).map(x => '' + escapeHtml(x || "") + '').join(""); const expenseRows = report.expenses.map((r, i) => '' + escapeHtml(r.firma || "") + '' + escapeHtml(r.aciklama || "") + '' + escapeHtml(r.faturaNo || "") + '' + n(r.tutar).toFixed(2) + '' + escapeHtml(r.odemeSekli || "") + '' + escapeHtml(r.odemeVadesi || "") + '' + escapeHtml(r.notlar || "") + '' + xlsExtraCells(r) + '').join(""); const incomeRows = report.incomes.map(r => '' + escapeHtml(r.firma || "") + '' + escapeHtml(r.aciklama || "") + '' + n(r.tutar).toFixed(2) + '' + n(r.nakit).toFixed(2) + '' + n(r.kart).toFixed(2) + '' + n(r.kalan).toFixed(2) + '' + escapeHtml(r.notlar || "") + '' + xlsExtraCells(r) + '').join(""); const advanceRows = (report.advanceSpends || []).map(r => '' + escapeHtml(r.firma || "") + '' + escapeHtml(r.tarih || "") + '' + escapeHtml(r.aciklama || "") + '' + n(r.tutar).toFixed(2) + '' + xlsExtraCells(r) + '').join(""); const css = ''; return '' + css + 'İş BitirmeOrganizasyon Giderleri' + '' + expenseHeads.map(h => '').join("") + '' + expenseRows + '' + incomeHeads.map(h => '').join("") + '' + incomeRows + '
İLGİLİ KURUM' + escapeHtml(report.kurum) + '
İŞİN ADI' + escapeHtml(report.ad) + '
İŞİN YERİ / TARİHİ' + escapeHtml(report.tarihYer) + '
OTEL' + escapeHtml(report.otel || "") + '
' + escapeHtml(h) + '
TOPLAM GİDER' + t.baseExpense.toFixed(2) + '
' + escapeHtml(h) + '
TOPLAM GELİR' + t.income.toFixed(2) + '
' + '
' + advanceHeads.map(h => '').join("") + '' + advanceRows + '
ORGANİZASYON DİĞER GİDERLERİ
İŞİN ADI' + escapeHtml(report.kurum) + '
DÜZENLEYEN KURUM' + escapeHtml(report.ad) + '
İŞİN TARİHİ - YERİ' + escapeHtml(report.tarihYer) + '
OTEL' + escapeHtml(report.otel || "") + '
AVANSI ALAN KİŞİTARİHTUTAR
' + escapeHtml(report.advancePerson || "") + '' + escapeHtml(report.advanceDate || "") + '' + n(report.advanceAmount).toFixed(2) + '
TOPLAM TUTAR' + n(report.advanceAmount).toFixed(2) + '
' + escapeHtml(h) + '
TOPLAM HARCAMA' + t.advanceSpend.toFixed(2) + '
MUHASEBEYE İADE EDİLECEK TUTAR' + Math.max(0, t.advanceReturn).toFixed(2) + '
TOPLAM GELİR' + t.income.toFixed(2) + '
TOPLAM GİDER' + t.expense.toFixed(2) + '
SONUÇ' + t.result.toFixed(2) + '
'; } function downloadCompletionReportExcel(projectId) { const p = getProject(projectId); if (!p) return showToast("Proje bulunamadı."); if (!p.completionReport?.reportCompleted) return showToast("Excel raporu indirmek için önce iş bitirme raporunu tamamlayın."); download(new Blob(["\uFEFF" + buildCompletionWorkbookHtml(p)], { type: "application/vnd.ms-excel;charset=utf-8" }), safeName(p.kurum + "-" + p.ad) + "-is-bitirme-raporu.xls"); addLog("İş bitirme raporu Excel indirdi", p.kurum + " - " + p.ad); } function downloadSartname(projectId) { const p = getProject(projectId); const file = normalizeSartnameFile(p?.sartnameFile); if (!isDownloadableFile(file)) return showToast("Bu işte dosya adı var ama indirilebilir dosya verisi yok. Detaydan dosyayı tekrar Yükle / Güncelle yapın."); try { downloadStoredFile(file, file.name || p.sartname || "sartname"); addLog("Şartname indirdi", p.kurum + " - " + p.ad); } catch (error) { showToast("Şartname indirilemedi. Dosyayı yeniden yükleyip tekrar deneyin."); } } function xlsxColumnName(index) { let name = ""; let n = index + 1; while (n > 0) { const rem = (n - 1) % 26; name = String.fromCharCode(65 + rem) + name; n = Math.floor((n - 1) / 26); } return name; } function xlsxSheetName(name, index) { const clean = String(name || "Sayfa " + (index + 1)).replace(/[\\/?*\[\]:]/g, " ").trim().slice(0, 31); return clean || "Sayfa " + (index + 1); } function xlsxCellXml(value, rowIndex, colIndex) { const ref = xlsxColumnName(colIndex) + rowIndex; if (typeof value === "number" && Number.isFinite(value)) return '' + value + ''; const text = escapeXml(value ?? ""); return '' + text + ''; } function xlsxSheetXml(rows) { const safeRows = Array.isArray(rows) ? rows : []; const body = safeRows.map((row, rIndex) => { const rowNo = rIndex + 1; const cells = (Array.isArray(row) ? row : []).map((value, cIndex) => xlsxCellXml(value, rowNo, cIndex)).join(""); return '' + cells + ''; }).join(""); return '' + '' + body + ''; } async function buildXlsxWorkbook(sheets) { if (!window.JSZip) throw new Error("Excel oluşturucu yüklenemedi. jszip.min.js aynı klasörde olmalı."); const safeSheets = (Array.isArray(sheets) && sheets.length ? sheets : [{ name: "Sayfa 1", rows: [] }]).map((sheet, index) => ({ name: xlsxSheetName(sheet.name, index), rows: sheet.rows || [] })); const zip = new JSZip(); zip.file("[Content_Types].xml", '' + safeSheets.map((_, i) => '').join("") + ''); zip.folder("_rels").file(".rels", ''); const workbookSheets = safeSheets.map((sheet, i) => '').join(""); zip.folder("xl").file("workbook.xml", '' + workbookSheets + ''); zip.folder("xl").folder("_rels").file("workbook.xml.rels", '' + safeSheets.map((_, i) => '').join("") + ''); safeSheets.forEach((sheet, i) => zip.folder("xl").folder("worksheets").file("sheet" + (i + 1) + ".xml", xlsxSheetXml(sheet.rows))); return await zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }); } async function forceSyncVisibleProjectsToServer() { if (!requireAdmin("Sunucu verisini eşitlemek için admin yetkisi gerekir.")) return; if (!confirmDelete("Bu işlem sunucudaki iş listesini şu anda ekranda gördüğün listeyle değiştirecek. Devam edilsin mi?")) return; saveAutoBackup("sunucu-zorla-esitleme-oncesi"); allowBulkProjectReplace = true; const ok = sharedMode && currentUser ? await syncServerStateNow({ projects, forceReplaceProjects: true }) : false; allowBulkProjectReplace = false; if (ok) { updateProjectSyncBaseline(projects); addLog("Sunucu iş listesini bu ekranla eşitledi", "", { count: projects.length }); showToast("Sunucu iş listesi bu ekrandaki listeyle eşitlendi."); } else { showToast("Sunucu eşitlemesi yapılamadı. Önce giriş ve bağlantıyı kontrol et."); } } function downloadBackup() { download(new Blob([JSON.stringify(projects, null, 2)], { type: "application/json;charset=utf-8" }), "simple-event-crm-yedek.json"); addLog("JSON yedek aldı"); } function importBackup(e) { const file = e.target.files[0]; if (!file) return; const r = new FileReader(); r.onload = async () => { try { const parsed = JSON.parse(r.result); if (!Array.isArray(parsed)) throw new Error(); saveAutoBackup("json-ice-aktarma-oncesi"); projects = normalizeProjects(parsed); allowBulkProjectReplace = true; save(PROJECTS_KEY, projects); if (sharedMode && currentUser) await syncServerStateNow({ projects, forceReplaceProjects: true }); else persist(); addLog("JSON yedek içe aktardı", "", { count: projects.length }); renderAll(); showToast("JSON yedek içe aktarıldı ve sunucu bu listeyle eşitlendi."); } catch { showToast("JSON dosyası okunamadı."); } finally { allowBulkProjectReplace = false; e.target.value = ""; } }; r.readAsText(file); } async function downloadCostExcel() { const p = getProject(activeProjectId); if (!p) return; const cols = costColumnDefinitions(p); const headers = cols.map(col => col.title); const rows = [headers]; p.costs.forEach((c, index) => { const ex = n(c.qty) * n(c.days) * n(c.price); const inc = ex * (1 + n(c.vat) / 100); rows.push(cols.map(col => { if (col.key === "no") return index + 1; if (col.key === "category") return c.category; if (col.key === "supplier") return c.supplier; if (col.key === "desc") return c.desc; if (col.key === "unit") return c.unit; if (col.key === "qty") return c.qty; if (col.key === "days") return c.days; if (col.key === "price") return c.price; if (col.key === "net") return ex; if (col.key === "vat") return c.vat; if (col.key === "gross") return inc; if (col.custom) return (c.extra || [])[col.extraIndex] || ""; return ""; })); }); const blob = await buildXlsxWorkbook([{ name: "Maliyet", rows }]); download(blob, safeName(p.kurum + "-" + p.ad) + "-maliyet.xlsx"); addLog("Maliyet Excel indirdi", p.kurum + " - " + p.ad); } function downloadCsv() { downloadCostExcel(); } function download(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } function downloadDataUrl(dataUrl, filename) { const a = document.createElement("a"); a.href = dataUrl; a.download = filename || "dosya"; document.body.appendChild(a); a.click(); a.remove(); } function safeName(text) { return String(text).toLowerCase().replace(/[^a-z0-9ğüşöçıİĞÜŞÖÇ]+/gi, "-").replace(/^-+|-+$/g, "") || "simple-event"; } function renderAll() { renderStats(); renderCalendar(); renderLists(); renderLogs(); renderUsers(); renderOfferProjects(); populateUserSelects(); applyPermissions(); renderAdminProfitDashboard(); renderHomePlanner(); renderCloseRequests(); updateCloseRequestBadge(); } document.getElementById("login-form").addEventListener("submit", handleLogin); document.getElementById("admin-user-form").addEventListener("submit", createUserFromAdmin); document.getElementById("password-change-form")?.addEventListener("submit", handlePasswordChange); document.getElementById("project-form").addEventListener("submit", saveProject); document.getElementById("offer-form").addEventListener("submit", submitOffer); window.addEventListener("pagehide", logoutOnPageClose); window.addEventListener("beforeunload", logoutOnPageClose); document.addEventListener("click", event => { if (!event.target.closest?.(".profile-photo-picker")) closeProfilePhotoMenu(); if (!event.target.closest?.("#user-menu-wrap")) closeUserMenu(); }); const profileCropCanvas = document.getElementById("profile-crop-canvas"); if (profileCropCanvas) { profileCropCanvas.addEventListener("pointerdown", event => { if (!profileCropState) return; profileCropState.dragging = true; profileCropState.lastX = event.clientX; profileCropState.lastY = event.clientY; profileCropCanvas.setPointerCapture?.(event.pointerId); }); profileCropCanvas.addEventListener("pointermove", event => { if (!profileCropState?.dragging) return; const rect = profileCropCanvas.getBoundingClientRect(); const ratio = 512 / Math.max(1, rect.width); profileCropState.offsetX += (event.clientX - profileCropState.lastX) * ratio; profileCropState.offsetY += (event.clientY - profileCropState.lastY) * ratio; profileCropState.lastX = event.clientX; profileCropState.lastY = event.clientY; drawProfileCrop(); }); ["pointerup", "pointercancel", "pointerleave"].forEach(type => { profileCropCanvas.addEventListener(type, () => { if (profileCropState) profileCropState.dragging = false; }); }); } initFontPickers(); bindRichEditors(); if (!localStorage.getItem(PROJECTS_KEY)) save(PROJECTS_KEY, projects); applySavedTheme(); checkSession(); if ("serviceWorker" in navigator && location.protocol !== "file:") { navigator.serviceWorker.register("./sw.js").catch(() => {}); }