150 lines
5.9 KiB
TypeScript
150 lines
5.9 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Sidebar } from './components/Sidebar';
|
|
import { Header } from './components/Header';
|
|
import { Dashboard } from './components/Dashboard';
|
|
import { KanbanBoard } from './components/KanbanBoard';
|
|
import { Tenders } from './components/Tenders';
|
|
import { Customers } from './components/Customers';
|
|
import { Experts } from './components/Experts';
|
|
import { Projects } from './components/Projects';
|
|
import { OrgChart } from './components/OrgChart';
|
|
import { Settings } from './components/Settings';
|
|
import { ViewState, Tender, Expert, Customer, Project } from './types';
|
|
import { supabase, TABLES } from './lib/supabase';
|
|
|
|
const App: React.FC = () => {
|
|
const [currentView, setCurrentView] = useState<ViewState>('Dashboard');
|
|
const [isDarkMode, setIsDarkMode] = useState<boolean>(true);
|
|
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
|
|
|
const [tenders, setTenders] = useState<Tender[]>([]);
|
|
const [customers, setCustomers] = useState<Customer[]>([]);
|
|
const [experts, setExperts] = useState<Expert[]>([]);
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
|
|
const fetchData = async (showRefreshIndicator = false) => {
|
|
if (showRefreshIndicator) setIsRefreshing(true);
|
|
try {
|
|
const { data: customersData } = await supabase
|
|
.from(TABLES.CUSTOMERS)
|
|
.select('*, icon_contacts(*)');
|
|
|
|
const formattedCustomers = (customersData || []).map(c => ({
|
|
...c,
|
|
contacts: c.icon_contacts || []
|
|
}));
|
|
setCustomers(formattedCustomers);
|
|
|
|
const { data: expertsData } = await supabase.from(TABLES.EXPERTS).select('*');
|
|
setExperts(expertsData || []);
|
|
|
|
const { data: projectsData } = await supabase.from(TABLES.PROJECTS).select('*');
|
|
setProjects(projectsData || []);
|
|
|
|
const { data: tendersData } = await supabase
|
|
.from(TABLES.TENDERS)
|
|
.select(`
|
|
*,
|
|
selectedCustomer:icon_customers(*),
|
|
selectedExperts:icon_tender_experts(icon_experts(*))
|
|
`)
|
|
.neq('status', 'Archiviert');
|
|
|
|
if (tendersData) {
|
|
const formattedTenders: Tender[] = tendersData.map(t => ({
|
|
id: t.id,
|
|
title: t.title,
|
|
source: t.source,
|
|
link: t.link,
|
|
details: t.details,
|
|
notes: t.notes,
|
|
status: t.status,
|
|
kanbanStatus: t.kanban_status,
|
|
selectedCustomer: t.selectedCustomer,
|
|
selectedExperts: t.selectedExperts?.map((se: any) => se.icon_experts) || [],
|
|
applicantsCount: t.selectedExperts?.length || 0
|
|
}));
|
|
setTenders(formattedTenders);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching data:', error);
|
|
} finally {
|
|
setIsInitialLoading(false);
|
|
setIsRefreshing(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isDarkMode) {
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
}, [isDarkMode]);
|
|
|
|
const toggleDarkMode = () => setIsDarkMode(!isDarkMode);
|
|
|
|
if (isInitialLoading) {
|
|
return (
|
|
<div className="h-screen w-screen flex items-center justify-center bg-slate-950">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div className="w-12 h-12 border-4 border-brand-500 border-t-transparent rounded-full animate-spin"></div>
|
|
<p className="text-white font-bold tracking-widest animate-pulse">ICON DATABASE CONNECTING...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const renderContent = () => {
|
|
switch (currentView) {
|
|
case 'Dashboard': return <Dashboard />;
|
|
case 'Live!': return <KanbanBoard tenders={tenders} setTenders={setTenders} refreshData={() => fetchData(true)} />;
|
|
case 'Ausschreibungen': return <Tenders tenders={tenders} setTenders={setTenders} customers={customers} experts={experts} refreshData={() => fetchData(true)} />;
|
|
case 'Kunden': return <Customers customers={customers} refreshData={() => fetchData(true)} />;
|
|
case 'Experten': return <Experts experts={experts} setExperts={setExperts} refreshData={() => fetchData(true)} />;
|
|
case 'Laufende Projekte': return <Projects projects={projects} setProjects={setProjects} customers={customers} experts={experts} refreshData={() => fetchData(true)} />;
|
|
case 'Organigramm': return <OrgChart customers={customers} projects={projects} experts={experts} />;
|
|
case 'Einstellungen': return <Settings isDarkMode={isDarkMode} onToggleDarkMode={toggleDarkMode} />;
|
|
default: return <Dashboard />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-screen overflow-hidden bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 font-sans">
|
|
<Sidebar
|
|
currentView={currentView}
|
|
onViewChange={setCurrentView}
|
|
isOpen={isSidebarOpen}
|
|
onToggle={() => setIsSidebarOpen(!isSidebarOpen)}
|
|
/>
|
|
|
|
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
<Header
|
|
title={currentView}
|
|
isDarkMode={isDarkMode}
|
|
onToggleDarkMode={toggleDarkMode}
|
|
onToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
|
|
/>
|
|
<div className="flex-1 overflow-y-auto p-4 md:p-8 relative">
|
|
{isRefreshing && (
|
|
<div className="absolute top-4 right-8 z-50 flex items-center gap-2 bg-white/80 dark:bg-slate-900/80 px-4 py-2 rounded-full shadow-lg border border-slate-100 dark:border-slate-800 backdrop-blur-sm animate-in fade-in slide-in-from-top-2">
|
|
<div className="w-4 h-4 border-2 border-brand-500 border-t-transparent rounded-full animate-spin"></div>
|
|
<span className="text-[10px] font-bold uppercase tracking-widest">Synchronisiere...</span>
|
|
</div>
|
|
)}
|
|
{renderContent()}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|