feat: initial commit
This commit is contained in:
214
src/components/FilialleiterDashboard.jsx
Normal file
214
src/components/FilialleiterDashboard.jsx
Normal 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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user