[32788f42] Add Termin-Übersicht feature, dynamic Event-Type selection, and refactor QR cards UI into Job Details

This commit is contained in:
2026-03-21 13:46:26 +00:00
parent c62db8a2ef
commit f72719b9a4
4 changed files with 468 additions and 138 deletions

View File

@@ -33,13 +33,12 @@ function App() {
const [isStatsRunning, setIsStatsRunning] = useState(false);
// States for QR Generator
const [isQrModalOpen, setIsQrModalOpen] = useState(false);
const [qrStartTime, setQrStartTime] = useState(new Date().toISOString().split('T')[0]);
const [qrEndTime, setQrEndTime] = useState(new Date().toISOString().split('T')[0]);
const [qrEventType, setQrEventType] = useState('Familie');
const [qrPdfFile, setQrPdfFile] = useState<File | null>(null);
const [isQrGenerating, setIsQrGenerating] = useState(false);
const [eventTypes, setEventTypes] = useState<any[]>([]);
const [selectedEventType, setSelectedEventType] = useState<string>("");
const [isListGenerating, setIsListGenerating] = useState(false);
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://192.168.178.6:8002';
const fetchJobs = async (account: AccountType, forceRefresh = false) => {
@@ -68,6 +67,24 @@ function App() {
}, [activeTab]);
const handleRefresh = () => fetchJobs(activeTab, true);
useEffect(() => {
const fetchEventTypes = async () => {
try {
const res = await fetch(`${API_BASE_URL}/api/calendly/event-types`);
if (res.ok) {
const data = await res.json();
setEventTypes(data.event_types || []);
if (data.event_types && data.event_types.length > 0) {
setSelectedEventType(data.event_types[0].name);
}
}
} catch (err) {
console.error("Failed to fetch event types:", err);
}
};
fetchEventTypes();
}, []);
// Statistics Task Functions
const handleStartStatistics = async (job: Job) => {
@@ -147,8 +164,9 @@ function App() {
}
};
const handleGenerateQrCards = async () => {
if (!qrPdfFile) {
const handleGenerateQrCards = async (job: Job, file: File) => {
if (!file) {
setError("Bitte wähle eine PDF-Vorlage aus.");
return;
}
@@ -157,10 +175,8 @@ function App() {
setError(null);
const formData = new FormData();
formData.append('pdf_file', qrPdfFile);
formData.append('start_time', `${qrStartTime}T00:00:00Z`);
formData.append('end_time', `${qrEndTime}T23:59:59Z`);
if (qrEventType) formData.append('event_type_name', qrEventType);
formData.append('pdf_file', file);
if (selectedEventType) formData.append('event_type_name', selectedEventType);
try {
const response = await fetch(`${API_BASE_URL}/api/qr-cards/generate`, {
@@ -170,7 +186,7 @@ function App() {
if (!response.ok) {
if (response.status === 404) {
throw new Error("Keine passenden Calendly-Termine in diesem Zeitraum gefunden.");
throw new Error("Keine passenden Calendly-Termine gefunden.");
}
const errData = await response.json();
throw new Error(errData.detail || 'Generierung fehlgeschlagen');
@@ -180,13 +196,11 @@ function App() {
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `QR_Karten_Andruck_${qrStartTime}.pdf`;
a.download = `QR_Karten_Andruck_${job.id}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
setIsQrModalOpen(false);
} catch (err: any) {
setError(err.message);
} finally {
@@ -194,6 +208,35 @@ function App() {
}
};
const handleGenerateAppointmentList = async (job: Job) => {
setIsListGenerating(true);
setError(null);
try {
const response = await fetch(`${API_BASE_URL}/api/jobs/${job.id}/appointment-list?event_type_name=${encodeURIComponent(selectedEventType)}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error("Keine passenden Calendly-Termine gefunden.");
}
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 = `Terminuebersicht_${job.name.replace(/\s+/g, "_")}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (err: any) {
setError(`Listen-Fehler (${job.name}): ${err.message}`);
} finally {
setIsListGenerating(false);
}
};
const currentJobs = jobsCache[activeTab];
return (
@@ -227,14 +270,7 @@ function App() {
</button>
</nav>
<div className="flex items-center gap-3">
<button
onClick={() => setIsQrModalOpen(true)}
className="flex items-center gap-2 px-3 py-2 bg-emerald-50 text-emerald-700 text-sm font-semibold rounded-lg hover:bg-emerald-100 transition-colors"
>
📇 QR-Karten Tool
</button>
</div>
</div>
</header>
@@ -411,20 +447,81 @@ function App() {
</button>
</div>
{/* Tool 2: QR Cards */}
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-emerald-300 transition-colors shadow-sm">
<div className="flex items-start justify-between mb-2">
<div className="p-2 bg-emerald-50 rounded-lg text-emerald-600 text-xl">📇</div>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600">Demnächst</span>
{/* Tool 2: Shooting-Planung (QR & List) */}
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-emerald-300 transition-colors shadow-sm md:col-span-2">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-50 rounded-lg text-emerald-600 text-xl">📆</div>
<h5 className="font-bold text-gray-900 text-lg">Shooting-Planung</h5>
</div>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-emerald-100 text-emerald-800">Neu</span>
</div>
<h5 className="font-bold text-gray-900 mb-1">QR-Zugangskarten</h5>
<p className="text-sm text-gray-500 mb-4 line-clamp-2">Druckt Namen und Buchungszeiten auf vorbereitete PDF-Bögen für Familienfotos.</p>
<button className="w-full px-4 py-2 bg-gray-100 text-gray-500 text-sm font-medium rounded-lg cursor-not-allowed">
Karten-Generator öffnen
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Event Selection */}
<div className="md:col-span-1 border-r border-gray-100 pr-4">
<label className="block text-sm font-bold text-gray-700 mb-2">Calendly Event auswählen</label>
<select
value={selectedEventType}
onChange={(e) => setSelectedEventType(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-sm"
>
{eventTypes.length === 0 && <option value="">Lade Events...</option>}
{eventTypes.map(et => (
<option key={et.uri} value={et.name}>{et.name}</option>
))}
</select>
<p className="text-xs text-gray-500 mt-2">Die Termine für diesen Event-Typ werden aus Calendly importiert.</p>
</div>
{/* Tool 3: Follow-up Emails */}
{/* Actions */}
<div className="md:col-span-2 grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Action 1: QR Cards */}
<div className="bg-gray-50 p-4 rounded-lg flex flex-col justify-between">
<div>
<h6 className="font-bold text-sm text-gray-800 mb-1">📇 QR-Zugangskarten</h6>
<p className="text-xs text-gray-600 mb-3">Druckt Namen und Uhrzeit auf vorbereitete PDF-Bögen.</p>
<input
type="file"
accept=".pdf"
id={`qr-upload-${selectedJob.id}`}
className="hidden"
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
handleGenerateQrCards(selectedJob, e.target.files[0]);
e.target.value = ''; // reset
}
}}
/>
</div>
<button
onClick={() => document.getElementById(`qr-upload-${selectedJob.id}`)?.click()}
disabled={!selectedEventType || isQrGenerating}
className="w-full px-3 py-2 bg-emerald-600 text-white text-xs font-bold rounded-lg hover:bg-emerald-700 disabled:opacity-50 transition-all flex justify-center items-center gap-2 mt-auto"
>
{isQrGenerating ? 'Generiere...' : 'Blanko PDF hochladen & starten'}
</button>
</div>
{/* Action 2: Appointment List */}
<div className="bg-gray-50 p-4 rounded-lg flex flex-col justify-between">
<div>
<h6 className="font-bold text-sm text-gray-800 mb-1">📄 Termin-Übersicht</h6>
<p className="text-xs text-gray-600 mb-3">PDF mit 6-Minuten Taktung und Lücken für den Shooting-Tag.</p>
</div>
<button
onClick={() => handleGenerateAppointmentList(selectedJob)}
disabled={!selectedEventType || isListGenerating}
className="w-full px-3 py-2 bg-indigo-600 text-white text-xs font-bold rounded-lg hover:bg-indigo-700 disabled:opacity-50 transition-all flex justify-center items-center gap-2 mt-auto"
>
{isListGenerating ? 'Generiere...' : 'PDF Liste generieren'}
</button>
</div>
</div>
</div>
</div>
{/* Tool 3: Follow-up Emails */}
<div className="bg-white border border-gray-200 rounded-xl p-5 hover:border-amber-300 transition-colors shadow-sm">
<div className="flex items-start justify-between mb-2">
<div className="p-2 bg-amber-50 rounded-lg text-amber-600 text-xl"></div>
@@ -535,104 +632,7 @@ function App() {
</div>
)}
{/* --- QR Generator Modal --- */}
{isQrModalOpen && (
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 transition-opacity" onClick={() => setIsQrModalOpen(false)}></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen">&#8203;</span>
<div className="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full">
<div className="bg-white px-6 py-6 border-b border-gray-100 flex justify-between items-center">
<h3 className="text-xl font-bold text-gray-900">QR-Karten Generator</h3>
<button onClick={() => setIsQrModalOpen(false)} className="text-gray-400 hover:text-gray-600">
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div className="p-6 space-y-6">
{error && (
<div className="p-3 bg-red-50 border border-red-100 text-red-700 text-sm rounded-lg">
{error}
</div>
)}
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-gray-500 uppercase mb-1">Von Datum</label>
<input
type="date"
value={qrStartTime}
onChange={(e) => setQrStartTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 uppercase mb-1">Bis Datum</label>
<input
type="date"
value={qrEndTime}
onChange={(e) => setQrEndTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
/>
</div>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 uppercase mb-1">Event-Typ Filter (Calendly Name)</label>
<input
type="text"
placeholder="z.B. Familie"
value={qrEventType}
onChange={(e) => setQrEventType(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 uppercase mb-1">Blanko PDF-Vorlage (mit QR-Codes)</label>
<input
type="file"
accept=".pdf"
onChange={(e) => setQrPdfFile(e.target.files?.[0] || null)}
className="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-emerald-50 file:text-emerald-700 hover:file:bg-emerald-100 cursor-pointer"
/>
</div>
</div>
<div className="bg-emerald-50 p-4 rounded-xl">
<h4 className="text-sm font-bold text-emerald-800 mb-1">Info</h4>
<p className="text-xs text-emerald-700 leading-relaxed">
Das Tool lädt alle passenden Termine aus deinem Calendly-Account und druckt Name, Personenanzahl und Uhrzeit exakt auf die hochgeladene Vorlage (2 Karten pro Seite).
</p>
</div>
</div>
<div className="bg-gray-50 px-6 py-4 flex flex-row-reverse gap-3">
<button
onClick={handleGenerateQrCards}
disabled={isQrGenerating || !qrPdfFile}
className="px-6 py-2 bg-emerald-600 text-white text-sm font-bold rounded-lg hover:bg-emerald-700 focus:ring-4 focus:ring-emerald-100 disabled:opacity-50 transition-all flex items-center gap-2"
>
{isQrGenerating ? (
<>
<svg className="animate-spin h-4 w-4 text-white" viewBox="0 0 24 24" fill="none"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><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" /></svg>
Generiere PDF...
</>
) : 'PDF jetzt generieren'}
</button>
<button
onClick={() => setIsQrModalOpen(false)}
className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800"
>
Abbrechen
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}