307 lines
13 KiB
PHP
307 lines
13 KiB
PHP
<?php
|
|
require_once __DIR__ . '/auth.php';
|
|
requireAdminAuth(true);
|
|
require_once __DIR__ . '/../../config/database.php';
|
|
require_once __DIR__ . '/generator_helpers.php';
|
|
|
|
$tsql = "SELECT ID, Nazwa FROM dbo.NGastroStolik ORDER BY Nazwa";
|
|
$stmt = sqlsrv_query($conn, $tsql);
|
|
|
|
[$baseUrl] = buildGuestAppBaseUrl();
|
|
$previewQuery = 'preview=staff';
|
|
|
|
$tables = [];
|
|
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
|
|
$id = strtoupper((string) $row['ID']);
|
|
$nazwa = trim((string) $row['Nazwa']);
|
|
$link = $baseUrl . $id;
|
|
|
|
$tables[] = [
|
|
'id' => $id,
|
|
'nazwa' => $nazwa,
|
|
'link' => $link,
|
|
'previewLink' => appendPreviewParam($link, $previewQuery),
|
|
];
|
|
}
|
|
|
|
$tables = buildUniqueQrFileNames($tables);
|
|
$tablesJson = json_encode($tables, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
|
?>
|
|
<!doctype html>
|
|
<html lang="pl">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Generator Linków QR - Stoliki</title>
|
|
<style>
|
|
body { font-family: Inter, Arial, sans-serif; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
|
h1 { margin: 0 0 10px; }
|
|
p { color: #94a3b8; }
|
|
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 16px 0 20px; }
|
|
.btn-zip { padding: 10px 16px; background: #22c55e; color: #0f172a; border: none; border-radius: 8px; cursor: pointer; font-weight: 700; }
|
|
.btn-zip:hover:not(:disabled) { background: #16a34a; color: #fff; }
|
|
.btn-zip:disabled { opacity: 0.65; cursor: wait; }
|
|
.zip-status { font-size: 0.9rem; color: #cbd5e1; min-height: 1.2em; }
|
|
table { border-collapse: collapse; width: 100%; max-width: 1100px; background: #111827; border: 1px solid #334155; }
|
|
th, td { border: 1px solid #334155; padding: 10px; text-align: left; }
|
|
th { background: #0b1220; }
|
|
a { color: #93c5fd; text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
.btn-qr { padding: 8px 10px; background: #3b82f6; color: #fff; border: none; border-radius: 8px; cursor: pointer; }
|
|
.btn-qr:hover { background: #2563eb; }
|
|
.btn-preview { display: inline-block; padding: 8px 10px; background: transparent; color: #cbd5e1; border: 1px solid #475569; border-radius: 8px; cursor: pointer; margin-left: 6px; text-decoration: none; font-size: 14px; }
|
|
.btn-preview:hover { background: #1e293b; border-color: #64748b; color: #e2e8f0; text-decoration: none; }
|
|
.actions-cell { white-space: nowrap; }
|
|
.btn-nav { display:inline-block; margin-right:8px; margin-bottom:14px; color:#cbd5e1; text-decoration:none; border:1px solid #334155; padding:8px 12px; border-radius:10px; }
|
|
.file-name { font-size: 0.82rem; color: #94a3b8; margin-top: 4px; }
|
|
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); align-items: center; justify-content: center; }
|
|
.modal-content { background: #111827; border:1px solid #334155; padding: 20px; border-radius: 8px; text-align: center; max-width: 420px; width: 90%; }
|
|
.close { float: right; font-size: 24px; font-weight: bold; cursor: pointer; line-height: 20px; color:#cbd5e1; }
|
|
#qrcode { margin-top: 20px; display: flex; justify-content: center; }
|
|
</style>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
|
</head>
|
|
<body>
|
|
<a class="btn-nav" href="index.php">Wróć do panelu admina</a>
|
|
<a class="btn-nav" href="logout.php">Wyloguj</a>
|
|
<h1>Linki do aplikacji (kody QR)</h1>
|
|
<p>Skopiuj poniższe linki lub wygeneruj z nich kody QR do umieszczenia na stolikach. Podgląd admina nie trafia do analityki.</p>
|
|
|
|
<div class="toolbar">
|
|
<button type="button" class="btn-zip" id="downloadZipBtn" <?= $tables ? '' : 'disabled' ?>>
|
|
Pobierz ZIP wszystkich kodów QR
|
|
</button>
|
|
<span class="zip-status" id="zipStatus"></span>
|
|
</div>
|
|
|
|
<table>
|
|
<tr>
|
|
<th>Nazwa stolika</th>
|
|
<th>Plik w ZIP</th>
|
|
<th>Hash (ID z bazy)</th>
|
|
<th>Bezpieczny link</th>
|
|
<th>Akcje</th>
|
|
</tr>
|
|
<?php if (!$tables): ?>
|
|
<tr>
|
|
<td colspan="5">Brak stolików w bazie.</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
<?php foreach ($tables as $table): ?>
|
|
<?php
|
|
$id = htmlspecialchars($table['id'], ENT_QUOTES, 'UTF-8');
|
|
$nazwa = htmlspecialchars($table['nazwa'], ENT_QUOTES, 'UTF-8');
|
|
$fileName = htmlspecialchars($table['fileName'], ENT_QUOTES, 'UTF-8');
|
|
$displayName = htmlspecialchars(formatQrDisplayName($table['nazwa']), ENT_QUOTES, 'UTF-8');
|
|
$link = htmlspecialchars($table['link'], ENT_QUOTES, 'UTF-8');
|
|
$previewLink = htmlspecialchars($table['previewLink'], ENT_QUOTES, 'UTF-8');
|
|
?>
|
|
<tr>
|
|
<td><strong><?= $nazwa ?></strong></td>
|
|
<td><code><?= $fileName ?></code></td>
|
|
<td style="font-size:.85em;color:#94a3b8;"><?= $id ?></td>
|
|
<td><a href="<?= $link ?>" target="_blank" rel="noopener"><?= $link ?></a></td>
|
|
<td class="actions-cell">
|
|
<button type="button" class="btn-qr" data-link="<?= $link ?>" data-label="<?= $displayName ?>">Pokaż QR</button>
|
|
<a class="btn-preview" href="<?= $previewLink ?>" target="_blank" rel="noopener">Podgląd admina</a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
|
|
<div id="qrModal" class="modal">
|
|
<div class="modal-content">
|
|
<span class="close" onclick="closeQR()">×</span>
|
|
<h2 id="qrTitle">Kod QR</h2>
|
|
<div id="qrcode"></div>
|
|
<p style="margin-top:20px;"><a id="qrLink" href="" target="_blank" rel="noopener">Otwórz link w nowym oknie</a></p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const TABLES = <?= $tablesJson ?: '[]' ?>;
|
|
const QR_LIGHT = "rgba(255,255,255,0)";
|
|
|
|
function buildQrOptions(text, size) {
|
|
return {
|
|
text,
|
|
width: size,
|
|
height: size,
|
|
colorDark: "#000000",
|
|
colorLight: QR_LIGHT,
|
|
correctLevel: QRCode.CorrectLevel.H
|
|
};
|
|
}
|
|
|
|
function stripWhiteBackground(canvas) {
|
|
const ctx = canvas.getContext("2d");
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
const data = imageData.data;
|
|
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
const r = data[i];
|
|
const g = data[i + 1];
|
|
const b = data[i + 2];
|
|
if (r > 245 && g > 245 && b > 245) {
|
|
data[i + 3] = 0;
|
|
}
|
|
}
|
|
|
|
ctx.putImageData(imageData, 0, 0);
|
|
}
|
|
|
|
function canvasToTransparentPngBlob(canvas) {
|
|
stripWhiteBackground(canvas);
|
|
return new Promise((resolve, reject) => {
|
|
canvas.toBlob((blob) => {
|
|
if (blob) {
|
|
resolve(blob);
|
|
return;
|
|
}
|
|
reject(new Error("Nie udało się wygenerować obrazu QR."));
|
|
}, "image/png");
|
|
});
|
|
}
|
|
|
|
function openQR(link, nazwa) {
|
|
document.getElementById("qrModal").style.display = "flex";
|
|
document.getElementById("qrTitle").innerText = nazwa;
|
|
document.getElementById("qrLink").href = link;
|
|
|
|
const qrContainer = document.getElementById("qrcode");
|
|
qrContainer.innerHTML = "";
|
|
|
|
new QRCode(qrContainer, buildQrOptions(link, 250));
|
|
}
|
|
|
|
function closeQR() {
|
|
document.getElementById("qrModal").style.display = "none";
|
|
}
|
|
|
|
function createQrBlob(link, size = 512) {
|
|
return new Promise((resolve, reject) => {
|
|
const holder = document.createElement("div");
|
|
holder.style.cssText = "position:fixed;left:-9999px;top:0;opacity:0;pointer-events:none;";
|
|
document.body.appendChild(holder);
|
|
|
|
try {
|
|
new QRCode(holder, buildQrOptions(link, size));
|
|
} catch (error) {
|
|
holder.remove();
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
window.setTimeout(() => {
|
|
const canvas = holder.querySelector("canvas");
|
|
const img = holder.querySelector("img");
|
|
|
|
if (canvas) {
|
|
canvasToTransparentPngBlob(canvas)
|
|
.then(resolve)
|
|
.catch(reject)
|
|
.finally(() => holder.remove());
|
|
return;
|
|
}
|
|
|
|
if (img) {
|
|
const tempCanvas = document.createElement("canvas");
|
|
tempCanvas.width = size;
|
|
tempCanvas.height = size;
|
|
const ctx = tempCanvas.getContext("2d");
|
|
const draw = () => {
|
|
ctx.clearRect(0, 0, size, size);
|
|
ctx.drawImage(img, 0, 0, size, size);
|
|
canvasToTransparentPngBlob(tempCanvas)
|
|
.then(resolve)
|
|
.catch(reject)
|
|
.finally(() => holder.remove());
|
|
};
|
|
|
|
if (img.complete) {
|
|
draw();
|
|
} else {
|
|
img.onload = draw;
|
|
img.onerror = () => {
|
|
holder.remove();
|
|
reject(new Error("Nie udało się wczytać obrazu QR."));
|
|
};
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
holder.remove();
|
|
reject(new Error("Brak renderu QR."));
|
|
}, 80);
|
|
});
|
|
}
|
|
|
|
async function downloadAllQrZip() {
|
|
if (!TABLES.length || typeof JSZip === "undefined") {
|
|
return;
|
|
}
|
|
|
|
const button = document.getElementById("downloadZipBtn");
|
|
const status = document.getElementById("zipStatus");
|
|
button.disabled = true;
|
|
|
|
try {
|
|
const zip = new JSZip();
|
|
const total = TABLES.length;
|
|
|
|
for (let i = 0; i < total; i += 1) {
|
|
const table = TABLES[i];
|
|
status.textContent = `Generowanie ${i + 1}/${total}: ${table.fileName}`;
|
|
const blob = await createQrBlob(table.link, 512);
|
|
zip.file(table.fileName, blob);
|
|
}
|
|
|
|
status.textContent = "Pakowanie ZIP…";
|
|
const zipBlob = await zip.generateAsync({
|
|
type: "blob",
|
|
compression: "DEFLATE",
|
|
compressionOptions: { level: 6 }
|
|
});
|
|
|
|
const stamp = new Date().toISOString().slice(0, 10);
|
|
const url = URL.createObjectURL(zipBlob);
|
|
const anchor = document.createElement("a");
|
|
anchor.href = url;
|
|
anchor.download = `kody-qr-stoliki-${stamp}.zip`;
|
|
document.body.appendChild(anchor);
|
|
anchor.click();
|
|
anchor.remove();
|
|
URL.revokeObjectURL(url);
|
|
|
|
status.textContent = `Gotowe — pobrano ${total} plików QR.`;
|
|
} catch (error) {
|
|
console.error(error);
|
|
status.textContent = "Błąd generowania ZIP. Spróbuj ponownie.";
|
|
} finally {
|
|
button.disabled = false;
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll(".btn-qr").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
openQR(button.dataset.link, button.dataset.label);
|
|
});
|
|
});
|
|
|
|
document.getElementById("downloadZipBtn")?.addEventListener("click", downloadAllQrZip);
|
|
|
|
window.onclick = function (event) {
|
|
const modal = document.getElementById("qrModal");
|
|
if (event.target === modal) {
|
|
closeQR();
|
|
}
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
sqlsrv_free_stmt($stmt);
|
|
sqlsrv_close($conn);
|