Initial
Takes in a file, process it and output a new file with additional columns.
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user