feat(scraper): implement PDF list generation from registrations export [32788f42]

This commit is contained in:
2026-03-20 18:40:06 +00:00
parent ae61cc44e1
commit 5c69c44ed3
3 changed files with 331 additions and 157 deletions

View File

@@ -14,12 +14,12 @@ type AccountType = 'kiga' | 'schule';
function App() {
const [activeTab, setActiveTab] = useState<AccountType>('kiga');
// Cache to store loaded jobs so we don't reload when switching tabs
const [jobsCache, setJobsCache] = useState<Record<AccountType, Job[] | null>>({
kiga: null,
schule: null,
});
const [isLoading, setIsLoading] = useState(false);
const [processingJobId, setProcessingJobId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://192.168.178.6:8002';
@@ -37,14 +37,37 @@ function App() {
setJobsCache(prev => ({ ...prev, [account]: data }));
} catch (err: any) {
setError(err.message);
console.error("Failed to fetch jobs:", err);
} finally {
setIsLoading(false);
}
};
const handleRefresh = () => {
fetchJobs(activeTab);
const handleRefresh = () => fetchJobs(activeTab);
const handleGeneratePdf = async (job: Job) => {
setProcessingJobId(job.id);
setError(null);
try {
const response = await fetch(`${API_BASE_URL}/api/jobs/${job.id}/generate-pdf?account_type=${activeTab}`);
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.detail || 'PDF Generierung fehlgeschlagen');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `Listen_${job.name.replace(/\s+/g, "_")}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (err: any) {
setError(`PDF Fehler (${job.name}): ${err.message}`);
} finally {
setProcessingJobId(null);
}
};
const currentJobs = jobsCache[activeTab];
@@ -56,76 +79,51 @@ function App() {
{/* Tab Navigation */}
<div className="flex border-b border-gray-200 mb-6">
<button
className={`py-3 px-6 font-medium text-sm rounded-t-lg transition-colors duration-200 ${
activeTab === 'kiga'
? 'bg-indigo-50 border-t-2 border-l-2 border-r-2 border-indigo-500 text-indigo-700'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
onClick={() => setActiveTab('kiga')}
>
📸 Kindergarten Fotografie
</button>
<button
className={`py-3 px-6 font-medium text-sm rounded-t-lg transition-colors duration-200 ${
activeTab === 'schule'
? 'bg-indigo-50 border-t-2 border-l-2 border-r-2 border-indigo-500 text-indigo-700'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
onClick={() => setActiveTab('schule')}
>
🏫 Schul-Fotografie
</button>
{['kiga', 'schule'].map((type) => (
<button
key={type}
className={`py-3 px-6 font-medium text-sm rounded-t-lg transition-colors duration-200 ${
activeTab === type
? 'bg-indigo-50 border-t-2 border-l-2 border-r-2 border-indigo-500 text-indigo-700'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
onClick={() => setActiveTab(type as AccountType)}
>
{type === 'kiga' ? '📸 Kindergarten Fotografie' : '🏫 Schul-Fotografie'}
</button>
))}
</div>
{/* Status and Refresh Area */}
<div className="mb-6 flex items-center justify-between bg-gray-50 p-4 rounded-md border border-gray-100">
<p className="text-sm text-gray-600 font-medium">
{currentJobs === null
? "Aufträge wurden noch nicht geladen."
: `${currentJobs.length} Aufträge geladen.`}
{currentJobs === null ? "Aufträge wurden noch nicht geladen." : `${currentJobs.length} Aufträge geladen.`}
</p>
<button
onClick={handleRefresh}
disabled={isLoading}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 transition-colors"
>
{isLoading ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Selenium läuft (ca. 45s)...
</>
) : (
currentJobs === null ? 'Liste initial abrufen' : 'Liste aktualisieren'
)}
{isLoading ? 'Selenium läuft (ca. 45s)...' : (currentJobs === null ? 'Liste initial abrufen' : 'Liste aktualisieren')}
</button>
</div>
{error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 mb-6">
<p className="text-red-700 font-bold">Fehler beim Scrapen:</p>
<p className="text-red-700 font-bold">Fehler:</p>
<p className="text-red-600">{error}</p>
</div>
)}
{currentJobs !== null && currentJobs.length === 0 && !isLoading && !error && (
<div className="text-center py-10 bg-gray-50 rounded-md border border-dashed border-gray-300">
<p className="text-gray-500 text-lg">Keine Aufträge in diesem Account gefunden.</p>
</div>
)}
{/* Jobs Table */}
{currentJobs !== null && currentJobs.length > 0 && (
{currentJobs !== null && (
<div className="overflow-x-auto rounded-lg border border-gray-200">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-100">
<tr>
<th scope="col" className="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">Name des Auftrags</th>
<th scope="col" className="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">Datum</th>
<th scope="col" className="px-6 py-4 text-center text-xs font-bold text-gray-700 uppercase tracking-wider">Features & Aktionen</th>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">Name des Auftrags</th>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">Datum</th>
<th className="px-6 py-4 text-center text-xs font-bold text-gray-700 uppercase tracking-wider">Features & Aktionen</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
@@ -138,24 +136,26 @@ function App() {
<div className="text-xs text-gray-500 font-normal mt-1">Status: {job.status}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">{job.date}</td>
{/* Actions Column */}
<td className="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
<div className="flex justify-center space-x-2">
<button className="bg-blue-50 text-blue-700 hover:bg-blue-100 border border-blue-200 rounded px-3 py-1.5 text-xs transition-colors shadow-sm" title="Teilnehmerliste als PDF generieren">
📄 1) PDF Liste
<button
onClick={() => handleGeneratePdf(job)}
disabled={processingJobId === job.id}
className="bg-blue-50 text-blue-700 hover:bg-blue-100 border border-blue-200 rounded px-3 py-1.5 text-xs transition-colors shadow-sm disabled:opacity-50"
>
{processingJobId === job.id ? '⌛ Generiere...' : '📄 1) PDF Liste'}
</button>
<button className="bg-emerald-50 text-emerald-700 hover:bg-emerald-100 border border-emerald-200 rounded px-3 py-1.5 text-xs transition-colors shadow-sm" title="QR-Zugangskarten erstellen">
<button className="bg-emerald-50 text-emerald-700 border border-emerald-200 rounded px-3 py-1.5 text-xs opacity-50 cursor-not-allowed">
📇 2) QR-Karten
</button>
<button className="bg-amber-50 text-amber-700 hover:bg-amber-100 border border-amber-200 rounded px-3 py-1.5 text-xs transition-colors shadow-sm" title="Nachfass-E-Mails ermitteln">
<button className="bg-amber-50 text-amber-700 border border-amber-200 rounded px-3 py-1.5 text-xs opacity-50 cursor-not-allowed">
3) Nachfass
</button>
<button className="bg-purple-50 text-purple-700 hover:bg-purple-100 border border-purple-200 rounded px-3 py-1.5 text-xs transition-colors shadow-sm" title="Statistik & Verkaufsquote">
<button className="bg-purple-50 text-purple-700 border border-purple-200 rounded px-3 py-1.5 text-xs opacity-50 cursor-not-allowed">
📊 4) Statistik
</button>
@@ -172,4 +172,4 @@ function App() {
);
}
export default App;
export default App;