school-timetracker/frontend/src/components/admin/AdminTimeEntriesTab.svelte
Patryk Hegenberg e719f4565f feat: add completed Web-Frontend in Svelte with some new Features
- Added import of Schedules
- Added export for schedule table
- Added import of logo
- Added password change to users
- improved ui/ux
2026-01-15 15:19:53 +01:00

157 lines
8.1 KiB
Svelte

<script>
import { onMount } from 'svelte';
import { getAllTimeEntries, getUsers, createAdminTimeEntry, updateTimeEntry, deleteTimeEntry, getYearlySummary, downloadYearlySummaryPDF } from '../../lib/api';
import { calculateHours } from '../../lib/utils';
import { loading, addToast } from '../../lib/stores';
let timeEntries = [];
let users = [];
let yearlySummary = [];
let manualEntry = { selectedUserId: "", date: "", hours: "", type: "manual" };
let editingEntryId = null;
let editForm = {};
onMount(async () => {
await Promise.all([loadEntries(), loadUsers(), loadSummary()]);
});
async function loadEntries() { timeEntries = await getAllTimeEntries(); }
async function loadUsers() { users = await getUsers(); }
async function loadSummary() { yearlySummary = await getYearlySummary(); }
async function handleManualEntry() {
if(!manualEntry.selectedUserId || !manualEntry.date || !manualEntry.hours) {
addToast("Bitte alle Felder ausfüllen", "warning"); return;
}
await createAdminTimeEntry({ ...manualEntry, hours: parseFloat(manualEntry.hours) });
manualEntry.hours = "";
await loadEntries(); await loadSummary();
addToast("Gebucht", "success");
}
async function handlePdfDownload() {
try {
const blob = await downloadYearlySummaryPDF();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = `Jahresuebersicht-${new Date().getFullYear()}.pdf`;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
} catch (e) { addToast("PDF Fehler", "error"); }
}
function startEdit(entry) { editingEntryId = entry.id; editForm = { ...entry }; }
async function saveEdit() { await updateTimeEntry(editingEntryId, editForm); editingEntryId = null; await loadEntries(); loadSummary(); }
async function deleteEntry(id) { if(confirm('Löschen?')) { await deleteTimeEntry(id); await loadEntries(); loadSummary(); } }
</script>
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4">
<button class="btn btn-primary w-full sm:w-auto" on:click={handlePdfDownload} disabled={$loading}>
<i class="fas fa-file-pdf mr-2"></i> PDF Bericht
</button>
</div>
<div class="card bg-base-100 shadow-xl mb-8 border border-base-200">
<div class="card-body p-4 sm:p-6">
<h3 class="card-title text-sm opacity-60 uppercase mb-4">Jahresübersicht</h3>
<div class="overflow-x-auto">
<table class="table table-zebra w-full whitespace-nowrap">
<thead><tr><th>Mitarbeiter</th><th class="text-right">Soll</th><th class="text-right">Ist</th><th class="text-right">Differenz</th><th>Status</th></tr></thead>
<tbody>
{#each yearlySummary as s}
<tr>
<td class="font-bold">{s.username}</td>
<td class="text-right">{s.yearlyTarget}</td>
<td class="text-right">{s.yearlyActual}</td>
<td class="text-right font-mono {s.remainingYearly > 0 ? 'text-warning' : 'text-success'}">{s.remainingYearly.toFixed(1)}</td>
<td>
{#if s.remainingYearly > 0}
<span class="badge badge-warning badge-sm">Offen</span>
{:else}
<span class="badge badge-success badge-sm">Erfüllt</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-lg mb-8">
<div class="card-body p-4 sm:p-6">
<h3 class="card-title text-sm uppercase opacity-70">Manuelle Korrektur / Eintragung</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 items-end">
<div class="form-control">
<label class="label"><span class="label-text">Mitarbeiter</span></label>
<select class="select select-bordered w-full" bind:value={manualEntry.selectedUserId}>
<option value="">Wählen...</option>
{#each users as u} <option value={u.id}>{u.username}</option> {/each}
</select>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Datum</span></label>
<input type="date" class="input input-bordered w-full" bind:value={manualEntry.date}>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Stunden (+/-)</span></label>
<input type="number" step="0.5" class="input input-bordered w-full" bind:value={manualEntry.hours} placeholder="-1.5 od. 2.0">
</div>
<button class="btn btn-info w-full" on:click={handleManualEntry} disabled={!manualEntry.selectedUserId || $loading}>
Buchen
</button>
</div>
</div>
</div>
<div class="overflow-x-auto bg-base-100 rounded-lg shadow-xl border border-base-200">
<table class="table table-zebra w-full whitespace-nowrap">
<thead><tr><th>User</th><th>Datum</th><th>Zeit</th><th>Typ</th><th>Stunden</th><th>Aktion</th></tr></thead>
<tbody>
{#each timeEntries as entry (entry.id)}
{#if editingEntryId === entry.id}
<tr class="bg-base-200">
<td>{entry.username}</td>
<td><input type="date" class="input input-xs input-bordered w-24" bind:value={editForm.date}></td>
<td>
<div class="flex flex-col gap-1">
<input type="time" class="input input-xs input-bordered" bind:value={editForm.startTime}>
<input type="time" class="input input-xs input-bordered" bind:value={editForm.endTime}>
</div>
</td>
<td>
<select class="select select-bordered select-xs w-20" bind:value={editForm.entryType}>
<option value="lesson">Unt.</option>
<option value="break">Pause</option>
<option value="manual">Man.</option>
</select>
</td>
<td>-</td>
<td>
<div class="join">
<button class="btn btn-xs btn-success join-item" on:click={saveEdit}><i class="fas fa-check"></i></button>
<button class="btn btn-xs btn-ghost join-item" on:click={() => editingEntryId = null}><i class="fas fa-times"></i></button>
</div>
</td>
</tr>
{:else}
<tr>
<td class="font-bold">{entry.username}</td>
<td>{entry.date}</td>
<td>{entry.startTime} - {entry.endTime}</td>
<td>
<span class="badge badge-sm {entry.entryType==='manual'?'badge-info':'badge-ghost'}">
{entry.entryType}
</span>
</td>
<td class="font-mono font-bold">{entry.entryType === 'lesson' ? '1.0' : calculateHours(entry.startTime, entry.endTime)}</td>
<td>
<div class="join">
<button class="btn btn-xs btn-ghost join-item" on:click={() => startEdit(entry)}>Edit</button>
<button class="btn btn-xs btn-ghost text-error join-item" on:click={() => deleteEntry(entry.id)}>Del</button>
</div>
</td>
</tr>
{/if}
{/each}
</tbody>
</table>
</div>