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