feat(frontend): add tabs, caching and feature buttons [32788f42]
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
interface Job {
|
interface Job {
|
||||||
@@ -10,25 +10,31 @@ interface Job {
|
|||||||
shooting_type: string;
|
shooting_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AccountType = 'kiga' | 'schule';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [accountType, setAccountType] = useState('kiga'); // Default to kindergarten
|
const [activeTab, setActiveTab] = useState<AccountType>('kiga');
|
||||||
const [jobs, setJobs] = useState<Job[]>([]);
|
// 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 [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = 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';
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://192.168.178.6:8002';
|
||||||
|
|
||||||
const fetchJobs = async () => {
|
const fetchJobs = async (account: AccountType) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/jobs?account_type=${accountType}`);
|
const response = await fetch(`${API_BASE_URL}/api/jobs?account_type=${account}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errData = await response.json();
|
const errData = await response.json();
|
||||||
throw new Error(errData.detail || 'Fehler beim Abrufen der Aufträge');
|
throw new Error(errData.detail || 'Fehler beim Abrufen der Aufträge');
|
||||||
}
|
}
|
||||||
const data: Job[] = await response.json();
|
const data: Job[] = await response.json();
|
||||||
setJobs(data);
|
setJobsCache(prev => ({ ...prev, [account]: data }));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
console.error("Failed to fetch jobs:", err);
|
console.error("Failed to fetch jobs:", err);
|
||||||
@@ -37,63 +43,123 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleRefresh = () => {
|
||||||
fetchJobs();
|
fetchJobs(activeTab);
|
||||||
}, [accountType]); // Refetch when accountType changes
|
};
|
||||||
|
|
||||||
|
const currentJobs = jobsCache[activeTab];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 p-4">
|
<div className="min-h-screen bg-gray-50 p-4">
|
||||||
<div className="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md">
|
<div className="max-w-7xl mx-auto bg-white p-6 rounded-lg shadow-md border border-gray-200">
|
||||||
<h1 className="text-2xl font-bold mb-4">Fotograf.de Auftragsübersicht</h1>
|
<h1 className="text-3xl font-bold mb-6 text-gray-800 border-b pb-4">Fotograf.de ERP & Scraper</h1>
|
||||||
|
|
||||||
<div className="mb-4">
|
{/* Tab Navigation */}
|
||||||
<label htmlFor="accountType" className="block text-sm font-medium text-gray-700">Account auswählen:</label>
|
<div className="flex border-b border-gray-200 mb-6">
|
||||||
<select
|
<button
|
||||||
id="accountType"
|
className={`py-3 px-6 font-medium text-sm rounded-t-lg transition-colors duration-200 ${
|
||||||
value={accountType}
|
activeTab === 'kiga'
|
||||||
onChange={(e) => setAccountType(e.target.value)}
|
? 'bg-indigo-50 border-t-2 border-l-2 border-r-2 border-indigo-500 text-indigo-700'
|
||||||
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTab('kiga')}
|
||||||
>
|
>
|
||||||
<option value="kiga">Kindergarten</option>
|
📸 Kindergarten Fotografie
|
||||||
<option value="schule">Schule</option>
|
</button>
|
||||||
</select>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
{/* Status and Refresh Area */}
|
||||||
onClick={fetchJobs}
|
<div className="mb-6 flex items-center justify-between bg-gray-50 p-4 rounded-md border border-gray-100">
|
||||||
disabled={isLoading}
|
<p className="text-sm text-gray-600 font-medium">
|
||||||
className="mb-4 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"
|
{currentJobs === null
|
||||||
>
|
? "Aufträge wurden noch nicht geladen."
|
||||||
{isLoading ? 'Lade Aufträge...' : 'Aufträge neu laden'}
|
: `${currentJobs.length} Aufträge geladen.`}
|
||||||
</button>
|
</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'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-red-600 mb-4">Fehler: {error}</p>}
|
{error && (
|
||||||
|
<div className="bg-red-50 border-l-4 border-red-500 p-4 mb-6">
|
||||||
{jobs.length === 0 && !isLoading && !error && (
|
<p className="text-red-700 font-bold">Fehler beim Scrapen:</p>
|
||||||
<p className="text-gray-500">Keine Aufträge gefunden.</p>
|
<p className="text-red-600">{error}</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{jobs.length > 0 && (
|
{currentJobs !== null && currentJobs.length === 0 && !isLoading && !error && (
|
||||||
<div className="overflow-x-auto">
|
<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 && (
|
||||||
|
<div className="overflow-x-auto rounded-lg border border-gray-200">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-100">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
<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-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</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-3 text-left text-xs font-medium text-gray-500 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 scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Typ</th>
|
|
||||||
<th scope="col" className="relative px-6 py-3"><span className="sr-only">Aktionen</span></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{jobs.map((job) => (
|
{currentJobs.map((job) => (
|
||||||
<tr key={job.id}>
|
<tr key={job.id} className="hover:bg-indigo-50 transition-colors">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{job.name}</td>
|
<td className="px-6 py-4 text-sm font-medium text-gray-900">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{job.status}</td>
|
<a href={job.url} target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-900 hover:underline">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{job.date}</td>
|
{job.name}
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{job.shooting_type}</td>
|
</a>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<div className="text-xs text-gray-500 font-normal mt-1">Status: {job.status}</div>
|
||||||
<a href={job.url} target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-900">Details</a>
|
</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>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
📇 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">
|
||||||
|
✉️ 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">
|
||||||
|
📊 4) Statistik
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -106,4 +172,4 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
Reference in New Issue
Block a user