ADD neue seite für firmen admins auf der sie die produkteinträge einzeln löschen können
This commit is contained in:
13
backend/src/main/java/de/strichliste/dto/TallyEntryDto.java
Normal file
13
backend/src/main/java/de/strichliste/dto/TallyEntryDto.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package de.strichliste.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public record TallyEntryDto(
|
||||||
|
Long id,
|
||||||
|
Long employeeId,
|
||||||
|
String employeeFirstName,
|
||||||
|
String employeeLastName,
|
||||||
|
String productName,
|
||||||
|
int priceCents,
|
||||||
|
LocalDateTime createdAt
|
||||||
|
) {}
|
||||||
@@ -187,6 +187,53 @@ public class CompanyAdminResource {
|
|||||||
return Response.ok(line).build();
|
return Response.ok(line).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/entries")
|
||||||
|
public Response getTallyEntries(@QueryParam("token") String token, @QueryParam("month") String month) {
|
||||||
|
AccessLink link = AccessLink.findByToken(token).orElse(null);
|
||||||
|
if (link == null || link.company == null) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String monthKey = month != null ? month : LocalDateTime.now().format(MONTH_FORMAT);
|
||||||
|
|
||||||
|
List<TallyEntry> entries = TallyEntry.find(
|
||||||
|
"SELECT t FROM TallyEntry t JOIN FETCH t.employee JOIN FETCH t.product " +
|
||||||
|
"WHERE t.employee.company.id = ?1 AND t.monthKey = ?2 ORDER BY t.createdAt DESC",
|
||||||
|
link.company.id, monthKey).list();
|
||||||
|
|
||||||
|
List<TallyEntryDto> dtos = entries.stream()
|
||||||
|
.map(t -> new TallyEntryDto(
|
||||||
|
t.id,
|
||||||
|
t.employee.id,
|
||||||
|
t.employee.firstName,
|
||||||
|
t.employee.lastName,
|
||||||
|
t.product.name,
|
||||||
|
t.priceCentsAtBooking,
|
||||||
|
t.createdAt))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Response.ok(dtos).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/entries/{id}")
|
||||||
|
@Transactional
|
||||||
|
public Response deleteTallyEntry(@QueryParam("token") String token, @PathParam("id") Long id) {
|
||||||
|
AccessLink link = AccessLink.findByToken(token).orElse(null);
|
||||||
|
if (link == null || link.company == null) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
TallyEntry entry = TallyEntry.findById(id);
|
||||||
|
if (entry == null || !entry.employee.company.id.equals(link.company.id)) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.delete();
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
static MonthlyReportDto buildCompanyReport(Long companyId, String monthKey) {
|
static MonthlyReportDto buildCompanyReport(Long companyId, String monthKey) {
|
||||||
Company company = Company.findById(companyId);
|
Company company = Company.findById(companyId);
|
||||||
List<Employee> employees = Employee.findAllByCompany(companyId);
|
List<Employee> employees = Employee.findAllByCompany(companyId);
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ export interface MonthlyTally {
|
|||||||
totalCents: number;
|
totalCents: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TallyEntry {
|
||||||
|
id: number;
|
||||||
|
employeeId: number;
|
||||||
|
employeeFirstName: string;
|
||||||
|
employeeLastName: string;
|
||||||
|
productName: string;
|
||||||
|
priceCents: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EmployeeReportLine {
|
export interface EmployeeReportLine {
|
||||||
employeeId: number;
|
employeeId: number;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
@@ -119,7 +129,11 @@ export const api = {
|
|||||||
getReport: (token: string, month?: string) =>
|
getReport: (token: string, month?: string) =>
|
||||||
request<MonthlyReport>(`/admin/company/report?token=${token}${month ? `&month=${month}` : ''}`),
|
request<MonthlyReport>(`/admin/company/report?token=${token}${month ? `&month=${month}` : ''}`),
|
||||||
getEmployeeReport: (token: string, employeeId: number, month?: string) =>
|
getEmployeeReport: (token: string, employeeId: number, month?: string) =>
|
||||||
request<EmployeeReportLine>(`/admin/company/report/employee/${employeeId}?token=${token}${month ? `&month=${month}` : ''}`)
|
request<EmployeeReportLine>(`/admin/company/report/employee/${employeeId}?token=${token}${month ? `&month=${month}` : ''}`),
|
||||||
|
getTallyEntries: (token: string, month?: string) =>
|
||||||
|
request<TallyEntry[]>(`/admin/company/entries?token=${token}${month ? `&month=${month}` : ''}`),
|
||||||
|
deleteTallyEntry: (token: string, id: number) =>
|
||||||
|
request<void>(`/admin/company/entries/${id}?token=${token}`, { method: 'DELETE' })
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- Provider Admin ---
|
// --- Provider Admin ---
|
||||||
|
|||||||
@@ -179,6 +179,17 @@
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Einträge verwalten -->
|
||||||
|
<section style="margin-bottom: 40px;">
|
||||||
|
<h2 style="margin-bottom: 12px;">Einträge verwalten</h2>
|
||||||
|
<p style="color: var(--color-text-muted); margin-bottom: 12px; font-size: 0.9rem;">
|
||||||
|
Einzelne Produktbuchungen einsehen und bei Bedarf löschen.
|
||||||
|
</p>
|
||||||
|
<a class="btn btn-secondary" href="/admin/company/entries?token={token}">
|
||||||
|
Einträge anzeigen & löschen
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Monatsauswertung -->
|
<!-- Monatsauswertung -->
|
||||||
<section>
|
<section>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||||
|
|||||||
145
frontend/src/routes/admin/company/entries/+page.svelte
Normal file
145
frontend/src/routes/admin/company/entries/+page.svelte
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { api, type TallyEntry } from '$lib/api/client';
|
||||||
|
import Header from '$lib/components/Header.svelte';
|
||||||
|
import MonthPicker from '$lib/components/MonthPicker.svelte';
|
||||||
|
|
||||||
|
let token = '';
|
||||||
|
let entries: TallyEntry[] = [];
|
||||||
|
let loading = true;
|
||||||
|
let selectedMonth = new Date().toISOString().slice(0, 7);
|
||||||
|
let deletingId: number | null = null;
|
||||||
|
let confirmId: number | null = null;
|
||||||
|
|
||||||
|
$: token = $page.url.searchParams.get('token') ?? '';
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!token) return;
|
||||||
|
await loadEntries();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadEntries() {
|
||||||
|
loading = true;
|
||||||
|
entries = await api.companyAdmin.getTallyEntries(token, selectedMonth);
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeMonth(e: CustomEvent<string>) {
|
||||||
|
selectedMonth = e.detail;
|
||||||
|
await loadEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
function askConfirm(id: number) {
|
||||||
|
confirmId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDelete() {
|
||||||
|
if (confirmId === null) return;
|
||||||
|
deletingId = confirmId;
|
||||||
|
confirmId = null;
|
||||||
|
await api.companyAdmin.deleteTallyEntry(token, deletingId);
|
||||||
|
deletingId = null;
|
||||||
|
await loadEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPrice(cents: number): string {
|
||||||
|
return (cents / 100).toFixed(2).replace('.', ',') + ' €';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(iso: string): string {
|
||||||
|
const d = new Date(iso);
|
||||||
|
return d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Einträge löschen – Qaffee</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<Header title="Einträge verwalten" />
|
||||||
|
|
||||||
|
<div class="admin-container">
|
||||||
|
{#if !token}
|
||||||
|
<p>Kein Zugangstoken angegeben.</p>
|
||||||
|
{:else}
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
|
||||||
|
<h2>Produkteinträge</h2>
|
||||||
|
<MonthPicker bind:value={selectedMonth} on:change={changeMonth} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<p>Laden...</p>
|
||||||
|
{:else if entries.length === 0}
|
||||||
|
<p style="color: var(--color-text-muted); text-align: center; margin-top: 40px;">
|
||||||
|
Keine Einträge in diesem Monat.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p style="color: var(--color-text-muted); margin-bottom: 12px; font-size: 0.9rem;">
|
||||||
|
{entries.length} Einträge · {formatPrice(entries.reduce((s, e) => s + e.priceCents, 0))} gesamt
|
||||||
|
</p>
|
||||||
|
<table class="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Zeitpunkt</th>
|
||||||
|
<th>Mitarbeiter</th>
|
||||||
|
<th>Produkt</th>
|
||||||
|
<th style="text-align: right;">Preis</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each entries as entry}
|
||||||
|
<tr>
|
||||||
|
<td style="color: var(--color-text-muted); font-size: 0.85rem; white-space: nowrap;">
|
||||||
|
{formatDateTime(entry.createdAt)}
|
||||||
|
</td>
|
||||||
|
<td>{entry.employeeFirstName} {entry.employeeLastName}</td>
|
||||||
|
<td>{entry.productName}</td>
|
||||||
|
<td style="text-align: right;" class="price">{formatPrice(entry.priceCents)}</td>
|
||||||
|
<td>
|
||||||
|
{#if deletingId === entry.id}
|
||||||
|
<span style="font-size: 0.85rem; color: var(--color-text-muted);">Löschen...</span>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
style="padding: 5px 10px; font-size: 0.8rem; color: #e05555; border-color: #e05555;"
|
||||||
|
on:click={() => askConfirm(entry.id)}
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if confirmId !== null}
|
||||||
|
<div class="modal-overlay" on:click|self={() => confirmId = null}>
|
||||||
|
<div class="modal">
|
||||||
|
<h2>Eintrag löschen?</h2>
|
||||||
|
{#if entries.find(e => e.id === confirmId) as entry}
|
||||||
|
<p style="margin: 16px 0; color: var(--color-text-muted);">
|
||||||
|
<strong style="color: var(--color-text-muted);">{entry.employeeFirstName} {entry.employeeLastName}</strong>
|
||||||
|
– {entry.productName} ({formatPrice(entry.priceCents)})
|
||||||
|
am {formatDateTime(entry.createdAt)}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<p style="margin-bottom: 20px; font-size: 0.9rem;">Dieser Eintrag wird unwiderruflich gelöscht.</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary" on:click={() => confirmId = null}>Abbrechen</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
style="background: #e05555; border-color: #e05555;"
|
||||||
|
on:click={confirmDelete}
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
Reference in New Issue
Block a user