school-timetracker/frontend/src/components/admin/AdminTimeEntriesTab.svelte
2026-01-16 11:25:34 +01:00

312 lines
11 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>