1 Commits
Author SHA1 Message Date
BartekGit 45354071bd QR: Plik zip z kodami QR 2026-09-24 17:57:45 +02:00
2 changed files with 276 additions and 28 deletions
+210 -28
View File
@@ -2,22 +2,30 @@
require_once __DIR__ . '/auth.php'; require_once __DIR__ . '/auth.php';
requireAdminAuth(true); requireAdminAuth(true);
require_once __DIR__ . '/../../config/database.php'; require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/generator_helpers.php';
$tsql = "SELECT ID, Nazwa FROM dbo.NGastroStolik ORDER BY Nazwa"; $tsql = "SELECT ID, Nazwa FROM dbo.NGastroStolik ORDER BY Nazwa";
$stmt = sqlsrv_query($conn, $tsql); $stmt = sqlsrv_query($conn, $tsql);
$host = $_SERVER['HTTP_HOST'] ?? 'localhost'; [$baseUrl] = buildGuestAppBaseUrl();
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$scriptDir = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? '/public/staff'));
$publicDir = str_replace('\\', '/', dirname($scriptDir));
$basePath = rtrim($publicDir, '/') . '/app.html?h=';
$baseUrl = "{$scheme}://{$host}{$basePath}";
$previewQuery = 'preview=staff'; $previewQuery = 'preview=staff';
function appendPreviewParam(string $link, string $previewQuery): string $tables = [];
{ while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
return str_contains($link, '?') ? $link . '&' . $previewQuery : $link . '?' . $previewQuery; $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> <!doctype html>
<html lang="pl"> <html lang="pl">
@@ -29,6 +37,11 @@ function appendPreviewParam(string $link, string $previewQuery): string
body { font-family: Inter, Arial, sans-serif; padding: 20px; background: #0f172a; color: #e2e8f0; } body { font-family: Inter, Arial, sans-serif; padding: 20px; background: #0f172a; color: #e2e8f0; }
h1 { margin: 0 0 10px; } h1 { margin: 0 0 10px; }
p { color: #94a3b8; } 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; } 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, td { border: 1px solid #334155; padding: 10px; text-align: left; }
th { background: #0b1220; } th { background: #0b1220; }
@@ -40,42 +53,61 @@ function appendPreviewParam(string $link, string $previewQuery): string
.btn-preview:hover { background: #1e293b; border-color: #64748b; color: #e2e8f0; text-decoration: none; } .btn-preview:hover { background: #1e293b; border-color: #64748b; color: #e2e8f0; text-decoration: none; }
.actions-cell { white-space: nowrap; } .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; } .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 { 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%; } .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; } .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; } #qrcode { margin-top: 20px; display: flex; justify-content: center; }
</style> </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/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> </head>
<body> <body>
<a class="btn-nav" href="index.php">Wróć do panelu admina</a> <a class="btn-nav" href="index.php">Wróć do panelu admina</a>
<a class="btn-nav" href="logout.php">Wyloguj</a> <a class="btn-nav" href="logout.php">Wyloguj</a>
<h1>Linki do aplikacji (kody QR)</h1> <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> <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> <table>
<tr> <tr>
<th>Nazwa stolika</th> <th>Nazwa stolika</th>
<th>Plik w ZIP</th>
<th>Hash (ID z bazy)</th> <th>Hash (ID z bazy)</th>
<th>Bezpieczny link</th> <th>Bezpieczny link</th>
<th>Akcje</th> <th>Akcje</th>
</tr> </tr>
<?php while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)): ?> <?php if (!$tables): ?>
<tr>
<td colspan="5">Brak stolików w bazie.</td>
</tr>
<?php endif; ?>
<?php foreach ($tables as $table): ?>
<?php <?php
$id = strtoupper((string)$row['ID']); $id = htmlspecialchars($table['id'], ENT_QUOTES, 'UTF-8');
$nazwa = htmlspecialchars((string)$row['Nazwa'], ENT_QUOTES, 'UTF-8'); $nazwa = htmlspecialchars($table['nazwa'], ENT_QUOTES, 'UTF-8');
$link = $baseUrl . $id; $fileName = htmlspecialchars($table['fileName'], ENT_QUOTES, 'UTF-8');
$previewLink = appendPreviewParam($link, $previewQuery); $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> <tr>
<td><strong><?= $nazwa ?></strong></td> <td><strong><?= $nazwa ?></strong></td>
<td><code><?= $fileName ?></code></td>
<td style="font-size:.85em;color:#94a3b8;"><?= $id ?></td> <td style="font-size:.85em;color:#94a3b8;"><?= $id ?></td>
<td><a href="<?= htmlspecialchars($link, ENT_QUOTES, 'UTF-8') ?>" target="_blank"><?= htmlspecialchars($link, ENT_QUOTES, 'UTF-8') ?></a></td> <td><a href="<?= $link ?>" target="_blank" rel="noopener"><?= $link ?></a></td>
<td class="actions-cell"> <td class="actions-cell">
<button class="btn-qr" onclick="openQR('<?= htmlspecialchars($link, ENT_QUOTES, 'UTF-8') ?>', '<?= $nazwa ?>')">Pokaż QR</button> <button type="button" class="btn-qr" data-link="<?= $link ?>" data-label="<?= $displayName ?>">Pokaż QR</button>
<a class="btn-preview" href="<?= htmlspecialchars($previewLink, ENT_QUOTES, 'UTF-8') ?>" target="_blank" rel="noopener">Podgląd admina</a> <a class="btn-preview" href="<?= $previewLink ?>" target="_blank" rel="noopener">Podgląd admina</a>
</td> </td>
</tr> </tr>
<?php endwhile; ?> <?php endforeach; ?>
</table> </table>
<div id="qrModal" class="modal"> <div id="qrModal" class="modal">
@@ -83,33 +115,183 @@ function appendPreviewParam(string $link, string $previewQuery): string
<span class="close" onclick="closeQR()">&times;</span> <span class="close" onclick="closeQR()">&times;</span>
<h2 id="qrTitle">Kod QR</h2> <h2 id="qrTitle">Kod QR</h2>
<div id="qrcode"></div> <div id="qrcode"></div>
<p style="margin-top:20px;"><a id="qrLink" href="" target="_blank">Otwórz link w nowym oknie</a></p> <p style="margin-top:20px;"><a id="qrLink" href="" target="_blank" rel="noopener">Otwórz link w nowym oknie</a></p>
</div> </div>
</div> </div>
<script> <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) { function openQR(link, nazwa) {
document.getElementById("qrModal").style.display = "flex"; document.getElementById("qrModal").style.display = "flex";
document.getElementById("qrTitle").innerText = "Stolik: " + nazwa; document.getElementById("qrTitle").innerText = nazwa;
document.getElementById("qrLink").href = link; document.getElementById("qrLink").href = link;
const qrContainer = document.getElementById("qrcode"); const qrContainer = document.getElementById("qrcode");
qrContainer.innerHTML = ""; qrContainer.innerHTML = "";
new QRCode(qrContainer, { new QRCode(qrContainer, buildQrOptions(link, 250));
text: link,
width: 250,
height: 250,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
} }
function closeQR() { function closeQR() {
document.getElementById("qrModal").style.display = "none"; 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) { window.onclick = function (event) {
const modal = document.getElementById("qrModal"); const modal = document.getElementById("qrModal");
if (event.target === modal) { if (event.target === modal) {
+66
View File
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
function formatQrDisplayName(string $nazwa): string
{
$nazwa = trim($nazwa);
if ($nazwa === '') {
return 'Stolik';
}
if (preg_match('/^stolik\s*(.*)$/iu', $nazwa, $matches)) {
$suffix = trim((string) $matches[1]);
return $suffix !== '' ? 'Stolik ' . $suffix : 'Stolik';
}
return 'Stolik ' . $nazwa;
}
function sanitizeZipFileName(string $name): string
{
$name = preg_replace('/[\\\\\\/:*?"<>|]+/u', '-', $name) ?? $name;
$name = preg_replace('/\\s+/u', ' ', $name) ?? $name;
$name = trim($name, " .-\t");
return $name !== '' ? $name : 'Stolik';
}
function buildUniqueQrFileNames(array $tables): array
{
$used = [];
foreach ($tables as &$table) {
$base = sanitizeZipFileName(formatQrDisplayName((string) ($table['nazwa'] ?? '')));
$candidate = $base;
$index = 2;
while (isset($used[$candidate])) {
$candidate = $base . ' (' . $index . ')';
$index++;
}
$used[$candidate] = true;
$table['fileName'] = $candidate . '.png';
}
unset($table);
return $tables;
}
function buildGuestAppBaseUrl(): array
{
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$scriptDir = str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? '/public/staff'));
$publicDir = str_replace('\\', '/', dirname($scriptDir));
$basePath = rtrim($publicDir, '/') . '/app.html?h=';
$baseUrl = "{$scheme}://{$host}{$basePath}";
return [$baseUrl, $basePath];
}
function appendPreviewParam(string $link, string $previewQuery): string
{
return str_contains($link, '?') ? $link . '&' . $previewQuery : $link . '?' . $previewQuery;
}