feat: initial commit

This commit is contained in:
2026-03-08 08:34:55 +01:00
parent 3eb7c3ca8e
commit 43c9efd8f5
39 changed files with 13242 additions and 688 deletions

View File

@@ -0,0 +1,214 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { databases, DATABASE_ID } from '../lib/appwrite';
import { Query } from 'appwrite';
import Header from './Header';
import Toast from './Toast';
import { useToast } from '../hooks/useToast';
import { useAuth } from '../context/AuthContext';
function getToday() {
const d = new Date();
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}
function getMonthStart() {
const d = new Date();
return new Date(d.getFullYear(), d.getMonth(), 1);
}
function getLastMonthStart() {
const d = new Date();
return new Date(d.getFullYear(), d.getMonth() - 1, 1);
}
function getLastMonthEnd() {
const d = new Date();
return new Date(d.getFullYear(), d.getMonth(), 0, 23, 59, 59);
}
function getYesterday() {
const d = getToday();
d.setDate(d.getDate() - 1);
return d;
}
function countInRange(assets, start, end) {
return assets.filter((a) => {
const d = new Date(a.$createdAt);
return d >= start && d <= end;
}).length;
}
export default function FilialleiterDashboard() {
const { userMeta } = useAuth();
const { toast, showToast } = useToast();
const locationId = userMeta?.locationId || '';
const [ownAssets, setOwnAssets] = useState([]);
const [allAssetsTotal, setAllAssetsTotal] = useState(0);
const [allLocationsCount, setAllLocationsCount] = useState(1);
const [colleagues, setColleagues] = useState([]);
const loadData = useCallback(async () => {
if (!locationId) return;
try {
const [assetsRes, metaRes, locsRes] = await Promise.all([
databases.listDocuments(DATABASE_ID, 'assets', [Query.limit(500)]),
databases.listDocuments(DATABASE_ID, 'users_meta', [
Query.equal('locationId', [locationId]),
Query.limit(100),
]),
databases.listDocuments(DATABASE_ID, 'locations', [Query.limit(100)]),
]);
setOwnAssets(assetsRes.documents);
setAllAssetsTotal(assetsRes.total);
setAllLocationsCount(Math.max(locsRes.total, 1));
setColleagues(metaRes.documents.filter((d) => d.userName));
} catch (err) {
console.error('Filialleiter-Daten laden fehlgeschlagen:', err);
}
}, [locationId]);
useEffect(() => { loadData(); }, [loadData]);
const now = new Date();
const today = getToday();
const yesterday = getYesterday();
const monthStart = getMonthStart();
const lastMonthStart = getLastMonthStart();
const lastMonthEnd = getLastMonthEnd();
const todayCount = countInRange(ownAssets, today, now);
const yesterdayCount = countInRange(ownAssets, yesterday, today);
const thisMonthCount = countInRange(ownAssets, monthStart, now);
const lastMonthCount = countInRange(ownAssets, lastMonthStart, lastMonthEnd);
const avgAllFilialen = allLocationsCount > 0 ? Math.round(allAssetsTotal / allLocationsCount) : 0;
const ownTotal = ownAssets.length;
const employeeStats = useMemo(() => {
return colleagues.map((c) => {
const assigned = ownAssets.filter((a) => a.zustaendig === c.userName);
const resolved = assigned.filter((a) => a.status === 'entsorgt').length;
const open = assigned.filter((a) => a.status === 'offen').length;
const inProgress = assigned.filter((a) => a.status === 'in_bearbeitung').length;
return {
name: c.userName,
total: assigned.length,
resolved,
open,
inProgress,
rate: assigned.length > 0 ? Math.round((resolved / assigned.length) * 100) : 0,
};
}).sort((a, b) => b.rate - a.rate);
}, [colleagues, ownAssets]);
function trendArrow(current, previous) {
if (current > previous) return { arrow: '▲', cls: 'trend-up' };
if (current < previous) return { arrow: '▼', cls: 'trend-down' };
return { arrow: '', cls: 'trend-flat' };
}
const dayTrend = trendArrow(todayCount, yesterdayCount);
const monthTrend = trendArrow(thisMonthCount, lastMonthCount);
return (
<>
<Header showToast={showToast} />
<div className="panel-page">
<div className="panel-title-bar">
<h1>Filialleiter Dashboard</h1>
<p>Tägliche und monatliche Übersicht deiner Filiale</p>
</div>
<div className="panel-stats">
<div className="panel-stat-card">
<div className="panel-stat-number">{todayCount}</div>
<div className="panel-stat-label">Heute erfasst</div>
<div className={`panel-trend ${dayTrend.cls}`}>
{dayTrend.arrow} Gestern: {yesterdayCount}
</div>
</div>
<div className="panel-stat-card">
<div className="panel-stat-number">{thisMonthCount}</div>
<div className="panel-stat-label">Diesen Monat</div>
<div className={`panel-trend ${monthTrend.cls}`}>
{monthTrend.arrow} Letzter Monat: {lastMonthCount}
</div>
</div>
<div className="panel-stat-card">
<div className="panel-stat-number">{ownTotal}</div>
<div className="panel-stat-label">Meine Filiale</div>
</div>
<div className="panel-stat-card">
<div className="panel-stat-number">{avgAllFilialen}</div>
<div className="panel-stat-label"> Alle Filialen</div>
</div>
</div>
<div className="panel-comparison">
<h2>Filialvergleich</h2>
<div className="comparison-bars">
<div className="comparison-row">
<span className="comparison-label">Meine Filiale</span>
<div className="comparison-bar-bg">
<div
className="comparison-bar own"
style={{ width: `${Math.min(100, avgAllFilialen > 0 ? (ownTotal / avgAllFilialen) * 50 : 50)}%` }}
/>
</div>
<span className="comparison-value">{ownTotal}</span>
</div>
<div className="comparison-row">
<span className="comparison-label"> Durchschnitt</span>
<div className="comparison-bar-bg">
<div className="comparison-bar avg" style={{ width: '50%' }} />
</div>
<span className="comparison-value">{avgAllFilialen}</span>
</div>
</div>
</div>
<div className="panel-card" style={{ marginTop: 24 }}>
<h2>Mitarbeiter-Performance</h2>
{employeeStats.length === 0 ? (
<p className="panel-empty">Keine Mitarbeiter gefunden</p>
) : (
<div className="employee-table-wrap">
<table className="employee-table">
<thead>
<tr>
<th>Mitarbeiter</th>
<th>Zugewiesen</th>
<th>Offen</th>
<th>In Bearbeitung</th>
<th>Erledigt</th>
<th>Erledigungsrate</th>
</tr>
</thead>
<tbody>
{employeeStats.map((e) => (
<tr key={e.name}>
<td><strong>{e.name}</strong></td>
<td>{e.total}</td>
<td>{e.open}</td>
<td>{e.inProgress}</td>
<td>{e.resolved}</td>
<td>
<div className="rate-bar-wrap">
<div className="rate-bar" style={{ width: `${e.rate}%` }} />
<span className="rate-text">{e.rate}%</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
<Toast message={toast.message} color={toast.color} visible={toast.visible} />
</>
);
}