- Added import of Schedules - Added export for schedule table - Added import of logo - Added password change to users - improved ui/ux
157 lines
8.1 KiB
Svelte
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>
|