// App — async session init, API-backed auth
const NAV = {
verwaltung: [
{section:'ARBEITSBEREICH'},
{k:'dashboard',icon:'dashboard',l:'Dashboard'},
{k:'liegenschaften',icon:'building',l:'Liegenschaften'},
{k:'meldungen',icon:'alert',l:'Schadensmeldungen',count:'12',dot:true},
{k:'wartung',icon:'wrench',l:'Wartungsaufträge',count:'4'},
{section:'KOMMUNIKATION'},
{k:'chat',icon:'chat',l:'Chat',count:'3'},
{k:'news',icon:'pin',l:'Schwarzes Brett'},
{k:'kalender',icon:'calendar',l:'Kalender'},
{section:'ADMINISTRATION'},
{k:'docs',icon:'doc',l:'Dokumente'},
{k:'mieter',icon:'users',l:'Mieter & Einladungen'},
{k:'einstellungen',icon:'settings',l:'Einstellungen'},
],
mieter: [
{section:'IHRE WOHNUNG'},
{k:'dashboard',icon:'home',l:'Übersicht'},
{k:'meldungen',icon:'alert',l:'Meine Meldungen',count:'2'},
{k:'docs',icon:'doc',l:'Meine Dokumente'},
{section:'LIEGENSCHAFT'},
{k:'chat',icon:'chat',l:'Liegenschafts-Chat',count:'11'},
{k:'news',icon:'pin',l:'Schwarzes Brett',dot:true},
{k:'kalender',icon:'calendar',l:'Termine'},
{k:'wasch',icon:'washer',l:'Waschküche'},
{section:'KONTO'},
{k:'einstellungen',icon:'settings',l:'Einstellungen'},
],
hauswart: [
{section:'ARBEITSBEREICH'},
{k:'dashboard',icon:'dashboard',l:'Heute'},
{k:'meldungen',icon:'alert',l:'Meldungen',count:'2'},
{k:'wartung',icon:'wrench',l:'Aufträge',count:'6'},
{k:'kalender',icon:'calendar',l:'Kalender'},
{section:'LIEGENSCHAFTEN'},
{k:'liegenschaften',icon:'building',l:'Meine Liegenschaften',count:'4'},
{k:'chat',icon:'chat',l:'Chats',count:'4'},
{section:'KONTO'},
{k:'einstellungen',icon:'settings',l:'Einstellungen'},
],
};
const TONE = {verwaltung:'red', mieter:'gold', hauswart:'green'};
const LoadingScreen = () => (
);
const App = () => {
const [session, setSession] = React.useState(null);
const [org, setOrg] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [authView, setAuthView] = React.useState('login');
const [page, setPage] = React.useState('dashboard');
const [propId, setPropId] = React.useState(null);
const [dataTick, setDataTick] = React.useState(0);
const inviteCode = React.useMemo(() => {
const m = window.location.hash.match(/[#&]invite=([A-Z0-9]+)/i);
return m ? m[1] : null;
}, []);
// Restore session from token on mount
React.useEffect(() => {
if (!Store.hasToken()) { setLoading(false); return; }
Store.getMe().then(data => {
if (data) { setSession(data.session); setOrg(data.org); }
setLoading(false);
});
}, []);
const reload = () => setDataTick(t => t + 1);
const ctx = React.useMemo(() => {
if (!session) return null;
return { session, org, reload, dataTick };
}, [session, org, dataTick]);
const handleLogin = (sess) => {
setSession(sess);
setPage('dashboard');
window.location.hash = '';
Store.getMe().then(d => { if (d) setOrg(d.org); });
};
const handleLogout = async () => {
await Store.logout();
setSession(null); setOrg(null); setPage('dashboard');
};
const navigate = (p, arg) => {
setPage(p);
if (p === 'liegenschaft' && arg) setPropId(arg);
};
if (loading) return ;
// Invite flow
if (inviteCode && !session) return (
);
// Auth flow
if (!session) return (
{authView==='login'
? setAuthView('register')}/>
: setAuthView('login')}/>}
);
const role = session.role;
const renderPage = () => {
if (role==='verwaltung') {
if (page==='dashboard') return ;
if (page==='liegenschaften')return navigate('liegenschaft',id)} ctx={ctx}/>;
if (page==='liegenschaft') return ;
if (page==='meldungen') return {}} ctx={ctx}/>;
if (page==='wartung') return <>
>;
if (page==='chat') return ;
if (page==='news') return ;
if (page==='kalender') return ;
if (page==='docs') return ;
if (page==='mieter') return ;
if (page==='einstellungen') return ;
}
if (role==='mieter') {
if (page==='dashboard') return ;
if (page==='meldung-neu') return navigate(t||'dashboard')} ctx={ctx}/>;
if (page==='meldungen') return ;
if (page==='chat') return ;
if (page==='news') return ;
if (page==='kalender') return ;
if (page==='wasch') return ;
if (page==='docs') return ;
if (page==='einstellungen') return ;
}
if (role==='hauswart') {
if (page==='dashboard') return ;
if (page==='meldungen') return {}} ctx={ctx}/>;
if (page==='wartung') return <>
>;
if (page==='chat') return ;
if (page==='kalender') return ;
if (page==='liegenschaften')return navigate('liegenschaft',id)} ctx={ctx}/>;
if (page==='einstellungen') return ;
}
return null;
};
const nav = NAV[role] || [];
return (
{nav.map((n,i) => {
if (n.section) return {n.section}
;
return navigate(n.k)}/>;
})}
{renderPage()}
);
};
// Mieter list with async invite system
const MieterListView = ({ ctx }) => {
const [users, setUsers] = React.useState([]);
const [pending, setPending] = React.useState([]);
const [props, setProps] = React.useState([]);
const [showModal, setShowModal] = React.useState(false);
const [form, setForm] = React.useState({name:'',email:'',role:'mieter',propId:'',unit:''});
const [genLink, setGenLink] = React.useState('');
const [copied, setCopied] = React.useState(false);
const [loadingInvite, setLoadingInvite] = React.useState(false);
const upd = (k,v) => setForm(f=>({...f,[k]:v}));
const fetchData = () => {
Store.getUsers().then(setUsers);
Store.getPendingInvites().then(setPending);
Store.getProperties().then(setProps);
};
React.useEffect(fetchData, [ctx.dataTick]);
const nonAdmin = users.filter(u => u.role !== 'verwaltung');
const sendInvite = async () => {
if (!form.email||loadingInvite) return;
setLoadingInvite(true);
try {
const inv = await Store.createInvite(form);
const link = window.location.origin + window.location.pathname + '#invite=' + inv.code;
setGenLink(link);
fetchData();
} catch(e) { alert(e.message); }
setLoadingInvite(false);
};
const copyLink = (link) => {
navigator.clipboard.writeText(link).then(()=>{ setCopied(true); setTimeout(()=>setCopied(false),2000); });
};
const closeModal = () => { setShowModal(false); setForm({name:'',email:'',role:'mieter',propId:'',unit:''}); setGenLink(''); };
return (
<>
setShowModal(true)}>Einladen}/>
{nonAdmin.length > 0 && (
Name E-Mail Rolle Einheit Seit
{nonAdmin.map(u=>(
{u.name}
{u.email}
{u.unit||'—'}
{u.created_at ? new Date(u.created_at).toLocaleDateString('de-CH') : '—'}
))}
)}
{nonAdmin.length===0 && pending.length===0 && (
Noch keine Mieter oder Hauswarte eingeladen.
Klicken Sie auf «Einladen» um Einladungslinks zu erstellen.
)}
{pending.length > 0 && (
Name E-Mail Rolle Einheit Erstellt Link
{pending.map(inv=>{
const link = window.location.origin+window.location.pathname+'#invite='+inv.code;
return (
{inv.name||'—'}
{inv.email}
{inv.unit||'—'}
{inv.created_at ? new Date(inv.created_at).toLocaleDateString('de-CH') : '—'}
copyLink(link)}>Link kopieren
);
})}
)}
{showModal && (
e.target===e.currentTarget&&closeModal()}>
Einladung erstellen
{!genLink ? (
Rolle
upd('role',e.target.value)} style={{width:'100%',padding:'8px 10px',border:'1px solid var(--ink-200)',borderRadius:4,fontSize:13,background:'#fff'}}>
Mieter
Hauswart
Verwaltung (Admin)
{props.length>0 && (
)}
Abbrechen
{loadingInvite?'Wird erstellt…':'Einladungslink generieren'}
) : (
Senden Sie diesen Link an {form.email} .
copyLink(genLink)}>{copied?'✓ Kopiert!':'Link kopieren'}
Fertig
)}
)}
>
);
};
const MieterMeldungen = ({onNav}) => (
<>
onNav('meldung-neu')}>Neue Meldung}/>
Ticket Titel Kategorie Status Gemeldet
SM-0377 Kühlschrank-Dichtung Geräte 17.04.2026 Öffnen
SM-0373 Wohnungstür-Scharnier Fenster/Türen 08.04.2026 Öffnen
>
);
const MieterDocs = () => (
<>
>
);
const EinstellungenView = ({ ctx }) => {
const s = ctx?.session;
const o = ctx?.org;
const [team, setTeam] = React.useState([]);
React.useEffect(() => {
if (s?.role === 'verwaltung') Store.getUsers().then(setTeam);
}, [s?.role]);
return (
<>
{[['Name',s?.name],['E-Mail',s?.email],['Rolle',s?.role]].map(([k,v])=>(
{k}
{v||'—'}
))}
{team.length > 0 && (
Name E-Mail Rolle Seit
{team.map(u=>(
{u.name}
{u.email}
{u.created_at ? new Date(u.created_at).toLocaleDateString('de-CH') : '—'}
))}
)}
{o &&
{[['Firmenname',o.name],['Plan',o.plan||'starter'],['Erstellt',o.created_at?new Date(o.created_at).toLocaleDateString('de-CH'):'—']].map(([k,v])=>(
{k}
{v||'—'}
))}
}
>
);
};
ReactDOM.createRoot(document.getElementById('root')).render( );