Takes in a file, process it and output a new file with additional columns.
This commit is contained in:
Stefan Lazic
2026-07-10 15:45:02 +01:00
parent 2f387a4c1c
commit a451060c1b
876 changed files with 169373 additions and 0 deletions
+206
View File
@@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
$latestBatchRowCount = (int) ($latestBatchRowCount ?? 0);
$hasData = $latestBatch !== null && $latestBatchRowCount > 0;
$exportRunUrl = (string) ($exportRunUrl ?? '?action=export-run');
$exportStatusUrl = (string) ($exportStatusUrl ?? '?action=export-status');
$exportDownloadUrl = (string) ($exportDownloadUrl ?? '?action=export-download');
function h(mixed $value): string
{
return htmlspecialchars((string) ($value ?? ''), ENT_QUOTES, 'UTF-8');
}
function flashClasses(?array $flash): string
{
$type = (string) ($flash['type'] ?? 'info');
return match ($type) {
'success' => 'border-emerald-200 bg-emerald-50 text-emerald-900',
'danger' => 'border-rose-200 bg-rose-50 text-rose-900',
'warning' => 'border-amber-200 bg-amber-50 text-amber-900',
default => 'border-sky-200 bg-sky-50 text-sky-900',
};
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h($appName) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script>
tailwind = {
config: {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui'],
},
},
},
},
};
</script>
<script src="https://cdn.tailwindcss.com"></script>
<link href="<?= h(asset_url('styles.css')) ?>" rel="stylesheet">
</head>
<body class="min-h-screen bg-slate-100 text-slate-900 antialiased">
<div class="app-shell">
<?php require __DIR__ . '/partials/navbar.php'; ?>
<div class="mx-auto max-w-7xl px-4 py-4 lg:px-8 lg:py-6">
<section class="mb-4 overflow-hidden rounded-md border border-white/70 bg-white/90 shadow-[0_24px_80px_rgba(15,23,42,0.08)] backdrop-blur">
<div class="p-6 sm:p-8 lg:p-10">
<span class="inline-flex items-center rounded-md bg-slate-900 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-white">Export</span>
<h1 class="mt-4 text-4xl font-semibold tracking-tight sm:text-5xl">Export data</h1>
<p class="mt-4 max-w-2xl text-base leading-7 text-slate-600 sm:text-lg">
Download the imported data in the format you need.
</p>
</div>
</section>
<?php if ($flash !== null): ?>
<div class="mb-4 rounded-md border px-4 py-3 text-sm font-medium shadow-sm <?= h(flashClasses($flash)) ?>">
<?= h($flash['message'] ?? '') ?>
</div>
<?php endif; ?>
<section class="rounded-md border border-white/70 bg-white/90 shadow-[0_24px_80px_rgba(15,23,42,0.08)] backdrop-blur">
<div class="p-6 sm:p-8 lg:p-10">
<h2 class="text-2xl font-semibold tracking-tight">Export options</h2>
<?php if (!$hasData): ?>
<p class="mt-4 text-sm leading-6 text-slate-500">No imported data yet. Upload a workbook first to enable exports.</p>
<?php else: ?>
<div class="mt-6 rounded-md border border-slate-200/80 bg-slate-50/80 p-6">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<div class="text-lg font-semibold text-slate-900">Full table</div>
<p class="mt-1 max-w-xl text-sm leading-6 text-slate-500">
Every imported row (<?= number_format($latestBatchRowCount) ?> total), exported as an Excel workbook.
</p>
</div>
<button
type="button"
id="exportButton"
class="inline-flex shrink-0 items-center justify-center rounded-md bg-sky-600 px-5 py-2.5 text-sm font-semibold text-white shadow-lg shadow-sky-500/20 transition hover:bg-sky-500 disabled:cursor-not-allowed disabled:opacity-60"
>
Export as Excel (.xlsx)
</button>
</div>
<div id="exportProgressWrap" class="mt-5 hidden">
<div class="mb-2 flex items-center justify-between text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">
<span id="exportProgressLabel">Starting export…</span>
<span id="exportProgressPercent">0%</span>
</div>
<div class="h-3 overflow-hidden rounded-md bg-slate-200">
<div id="exportProgressBar" class="h-full w-0 rounded-md bg-sky-500 transition-all duration-300"></div>
</div>
</div>
<div id="exportError" class="mt-4 hidden rounded-md border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-medium text-rose-900"></div>
</div>
<?php endif; ?>
</div>
</section>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const exportButton = document.getElementById('exportButton');
if (!exportButton) {
return;
}
const progressWrap = document.getElementById('exportProgressWrap');
const progressBar = document.getElementById('exportProgressBar');
const progressLabel = document.getElementById('exportProgressLabel');
const progressPercent = document.getElementById('exportProgressPercent');
const errorBox = document.getElementById('exportError');
const runUrl = <?= json_encode($exportRunUrl, JSON_THROW_ON_ERROR) ?>;
const statusUrl = <?= json_encode($exportStatusUrl, JSON_THROW_ON_ERROR) ?>;
const downloadUrl = <?= json_encode($exportDownloadUrl, JSON_THROW_ON_ERROR) ?>;
const makeToken = () => (window.crypto && crypto.randomUUID)
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
const setProgress = (percent, label) => {
progressBar.style.width = `${percent}%`;
progressPercent.textContent = `${percent}%`;
progressLabel.textContent = label;
};
const showError = (message) => {
errorBox.textContent = message;
errorBox.classList.remove('hidden');
};
exportButton.addEventListener('click', () => {
const token = makeToken();
exportButton.disabled = true;
errorBox.classList.add('hidden');
progressWrap.classList.remove('hidden');
setProgress(0, 'Starting export…');
let pollTimer = null;
const stopPolling = () => {
if (pollTimer !== null) {
window.clearInterval(pollTimer);
pollTimer = null;
}
};
const finish = () => {
stopPolling();
exportButton.disabled = false;
window.setTimeout(() => progressWrap.classList.add('hidden'), 1500);
};
pollTimer = window.setInterval(async () => {
try {
const response = await fetch(`${statusUrl}&token=${encodeURIComponent(token)}`, {
headers: { Accept: 'application/json' },
});
const data = await response.json();
if (data.status === 'running') {
setProgress(data.progressPercent ?? 0, data.message || 'Exporting…');
} else if (data.status === 'completed') {
setProgress(100, 'Export complete. Downloading…');
finish();
window.location.href = data.downloadUrl || `${downloadUrl}&token=${encodeURIComponent(token)}`;
} else if (data.status === 'error') {
finish();
progressWrap.classList.add('hidden');
showError(data.message || 'Export failed.');
}
} catch (error) {
finish();
progressWrap.classList.add('hidden');
showError('Lost connection while checking export progress.');
}
}, 700);
fetch(`${runUrl}&token=${encodeURIComponent(token)}`, {
headers: { Accept: 'application/json' },
}).catch(() => {
finish();
progressWrap.classList.add('hidden');
showError('Could not start the export.');
});
});
});
</script>
</body>
</html>