bump
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
package de.strichliste.dto;
|
package de.strichliste.dto;
|
||||||
|
|
||||||
public record CompanyDto(Long id, String name, boolean active) {}
|
public record CompanyDto(Long id, String name, boolean active, boolean hasLogo) {}
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ public class Company extends PanacheEntityBase {
|
|||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
public LocalDateTime createdAt = LocalDateTime.now();
|
public LocalDateTime createdAt = LocalDateTime.now();
|
||||||
|
|
||||||
|
@Column(name = "logo")
|
||||||
|
public byte[] logo;
|
||||||
|
|
||||||
|
@Column(name = "logo_content_type", length = 50)
|
||||||
|
public String logoContentType;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "company", fetch = FetchType.LAZY)
|
@OneToMany(mappedBy = "company", fetch = FetchType.LAZY)
|
||||||
public List<Employee> employees;
|
public List<Employee> employees;
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import de.strichliste.entity.*;
|
|||||||
import de.strichliste.filter.AuthFilter.Secured;
|
import de.strichliste.filter.AuthFilter.Secured;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.*;
|
||||||
import jakarta.ws.rs.core.Context;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.jboss.resteasy.reactive.RestResponse;
|
import org.jboss.resteasy.reactive.RestForm;
|
||||||
|
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -96,6 +98,47 @@ public class CompanyAdminResource {
|
|||||||
return Response.ok(new EmployeeDto(employee.id, employee.company.id, employee.firstName, employee.lastName, employee.active)).build();
|
return Response.ok(new EmployeeDto(employee.id, employee.company.id, employee.firstName, employee.lastName, employee.active)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/logo")
|
||||||
|
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||||
|
@Transactional
|
||||||
|
public Response uploadLogo(@QueryParam("token") String token, @RestForm("file") FileUpload file) throws IOException {
|
||||||
|
AccessLink link = AccessLink.findByToken(token).orElse(null);
|
||||||
|
if (link == null || link.company == null) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Company company = Company.findById(link.company.id);
|
||||||
|
if (company == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
company.logo = Files.readAllBytes(file.uploadedFile());
|
||||||
|
company.logoContentType = file.contentType();
|
||||||
|
|
||||||
|
return Response.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/logo")
|
||||||
|
@Transactional
|
||||||
|
public Response deleteLogo(@QueryParam("token") String token) {
|
||||||
|
AccessLink link = AccessLink.findByToken(token).orElse(null);
|
||||||
|
if (link == null || link.company == null) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Company company = Company.findById(link.company.id);
|
||||||
|
if (company == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
company.logo = null;
|
||||||
|
company.logoContentType = null;
|
||||||
|
|
||||||
|
return Response.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/report")
|
@Path("/report")
|
||||||
public Response getMonthlyReport(@QueryParam("token") String token, @QueryParam("month") String month) {
|
public Response getMonthlyReport(@QueryParam("token") String token, @QueryParam("month") String month) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class ProviderAdminResource {
|
|||||||
return Company.findAll().list().stream()
|
return Company.findAll().list().stream()
|
||||||
.map(obj -> {
|
.map(obj -> {
|
||||||
Company c = (Company) obj;
|
Company c = (Company) obj;
|
||||||
return new CompanyDto(c.id, c.name, c.active);
|
return new CompanyDto(c.id, c.name, c.active, c.logo != null);
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ public class ProviderAdminResource {
|
|||||||
company.name = request.name();
|
company.name = request.name();
|
||||||
company.persist();
|
company.persist();
|
||||||
return Response.status(Response.Status.CREATED)
|
return Response.status(Response.Status.CREATED)
|
||||||
.entity(new CompanyDto(company.id, company.name, company.active))
|
.entity(new CompanyDto(company.id, company.name, company.active, company.logo != null))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ public class ProviderAdminResource {
|
|||||||
return Response.status(Response.Status.NOT_FOUND).build();
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
}
|
}
|
||||||
company.name = request.name();
|
company.name = request.name();
|
||||||
return Response.ok(new CompanyDto(company.id, company.name, company.active)).build();
|
return Response.ok(new CompanyDto(company.id, company.name, company.active, company.logo != null)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@@ -67,7 +67,7 @@ public class ProviderAdminResource {
|
|||||||
return Response.status(Response.Status.NOT_FOUND).build();
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
}
|
}
|
||||||
company.active = !company.active;
|
company.active = !company.active;
|
||||||
return Response.ok(new CompanyDto(company.id, company.name, company.active)).build();
|
return Response.ok(new CompanyDto(company.id, company.name, company.active, company.logo != null)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Products ---
|
// --- Products ---
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class PublicResource {
|
|||||||
@Path("/companies")
|
@Path("/companies")
|
||||||
public List<CompanyDto> getActiveCompanies() {
|
public List<CompanyDto> getActiveCompanies() {
|
||||||
return Company.findAllActive().stream()
|
return Company.findAllActive().stream()
|
||||||
.map(c -> new CompanyDto(c.id, c.name, c.active))
|
.map(c -> new CompanyDto(c.id, c.name, c.active, c.logo != null))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +62,18 @@ public class PublicResource {
|
|||||||
return Response.status(Response.Status.CREATED).build();
|
return Response.status(Response.Status.CREATED).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/companies/{id}/logo")
|
||||||
|
@Produces("*/*")
|
||||||
|
public Response getCompanyLogo(@PathParam("id") Long companyId) {
|
||||||
|
Company company = Company.findById(companyId);
|
||||||
|
if (company == null || company.logo == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
String contentType = company.logoContentType != null ? company.logoContentType : "image/png";
|
||||||
|
return Response.ok(company.logo, contentType).build();
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/tally/monthly/{employeeId}")
|
@Path("/tally/monthly/{employeeId}")
|
||||||
public List<MonthlyTallyDto> getMonthlyTally(
|
public List<MonthlyTallyDto> getMonthlyTally(
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE company ADD COLUMN logo MEDIUMBLOB;
|
||||||
|
ALTER TABLE company ADD COLUMN logo_content_type VARCHAR(50);
|
||||||
@@ -18,6 +18,7 @@ export interface Company {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
hasLogo: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Employee {
|
export interface Employee {
|
||||||
@@ -90,6 +91,17 @@ export const api = {
|
|||||||
|
|
||||||
// --- Company Admin ---
|
// --- Company Admin ---
|
||||||
companyAdmin: {
|
companyAdmin: {
|
||||||
|
uploadLogo: async (token: string, file: File): Promise<void> => {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('file', file);
|
||||||
|
const res = await fetch(`${API_BASE}/admin/company/logo?token=${token}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
||||||
|
},
|
||||||
|
deleteLogo: (token: string) =>
|
||||||
|
request<void>(`/admin/company/logo?token=${token}`, { method: 'DELETE' }),
|
||||||
getEmployees: (token: string) =>
|
getEmployees: (token: string) =>
|
||||||
request<Employee[]>(`/admin/company/employees?token=${token}`),
|
request<Employee[]>(`/admin/company/employees?token=${token}`),
|
||||||
createEmployee: (token: string, firstName: string, lastName: string) =>
|
createEmployee: (token: string, firstName: string, lastName: string) =>
|
||||||
|
|||||||
@@ -28,7 +28,15 @@
|
|||||||
<div class="card-grid" style="padding: 24px;">
|
<div class="card-grid" style="padding: 24px;">
|
||||||
{#each companies as company}
|
{#each companies as company}
|
||||||
<a href="/company/{company.id}" class="card">
|
<a href="/company/{company.id}" class="card">
|
||||||
|
{#if company.hasLogo}
|
||||||
|
<img
|
||||||
|
src="/api/companies/{company.id}/logo"
|
||||||
|
alt={company.name}
|
||||||
|
style="width: 2.5rem; height: 2.5rem; object-fit: contain;"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<div style="font-size: 2.5rem;">🏢</div>
|
<div style="font-size: 2.5rem;">🏢</div>
|
||||||
|
{/if}
|
||||||
<h3>{company.name}</h3>
|
<h3>{company.name}</h3>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { api, type Employee, type MonthlyReport, type EmployeeReportLine } from '$lib/api/client';
|
import { api, type Employee, type MonthlyReport, type EmployeeReportLine, type Company } from '$lib/api/client';
|
||||||
|
|
||||||
let token = '';
|
let token = '';
|
||||||
let employees: Employee[] = [];
|
let employees: Employee[] = [];
|
||||||
@@ -10,6 +10,11 @@
|
|||||||
let loading = true;
|
let loading = true;
|
||||||
let selectedMonth = new Date().toISOString().slice(0, 7);
|
let selectedMonth = new Date().toISOString().slice(0, 7);
|
||||||
|
|
||||||
|
// Logo state
|
||||||
|
let company: Company | null = null;
|
||||||
|
let logoTimestamp = Date.now();
|
||||||
|
let logoUploading = false;
|
||||||
|
|
||||||
// Modal state
|
// Modal state
|
||||||
let showModal = false;
|
let showModal = false;
|
||||||
let editId: number | null = null;
|
let editId: number | null = null;
|
||||||
@@ -25,13 +30,37 @@
|
|||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
loading = true;
|
loading = true;
|
||||||
[employees, report] = await Promise.all([
|
const [emps, rep, companies] = await Promise.all([
|
||||||
api.companyAdmin.getEmployees(token),
|
api.companyAdmin.getEmployees(token),
|
||||||
api.companyAdmin.getReport(token, selectedMonth)
|
api.companyAdmin.getReport(token, selectedMonth),
|
||||||
|
api.getCompanies()
|
||||||
]);
|
]);
|
||||||
|
employees = emps;
|
||||||
|
report = rep;
|
||||||
|
// Identifiziere die eigene Firma über den ersten Mitarbeiter oder via Report
|
||||||
|
if (rep.companyId) {
|
||||||
|
company = companies.find(c => c.id === rep.companyId) ?? null;
|
||||||
|
}
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleLogoUpload(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
logoUploading = true;
|
||||||
|
await api.companyAdmin.uploadLogo(token, file);
|
||||||
|
logoTimestamp = Date.now();
|
||||||
|
if (company) company = { ...company, hasLogo: true };
|
||||||
|
logoUploading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogoDelete() {
|
||||||
|
await api.companyAdmin.deleteLogo(token);
|
||||||
|
logoTimestamp = Date.now();
|
||||||
|
if (company) company = { ...company, hasLogo: false };
|
||||||
|
}
|
||||||
|
|
||||||
async function changeMonth() {
|
async function changeMonth() {
|
||||||
report = await api.companyAdmin.getReport(token, selectedMonth);
|
report = await api.companyAdmin.getReport(token, selectedMonth);
|
||||||
selectedEmployee = null;
|
selectedEmployee = null;
|
||||||
@@ -89,6 +118,29 @@
|
|||||||
{:else if loading}
|
{:else if loading}
|
||||||
<p>Laden...</p>
|
<p>Laden...</p>
|
||||||
{:else}
|
{:else}
|
||||||
|
<!-- Firmenlogo -->
|
||||||
|
<section style="margin-bottom: 32px;">
|
||||||
|
<h2 style="margin-bottom: 12px;">Firmenlogo</h2>
|
||||||
|
<div style="display: flex; align-items: center; gap: 20px;">
|
||||||
|
{#if company?.hasLogo}
|
||||||
|
<img
|
||||||
|
src="/api/companies/{company.id}/logo?t={logoTimestamp}"
|
||||||
|
alt="Firmenlogo"
|
||||||
|
style="width: 2.5rem; height: 2.5rem; object-fit: contain; border-radius: 4px;"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div style="font-size: 2.5rem;">🏢</div>
|
||||||
|
{/if}
|
||||||
|
<label class="btn btn-secondary" style="cursor: pointer;">
|
||||||
|
{logoUploading ? 'Hochladen...' : 'Bild hochladen'}
|
||||||
|
<input type="file" accept="image/*" style="display: none;" on:change={handleLogoUpload} disabled={logoUploading} />
|
||||||
|
</label>
|
||||||
|
{#if company?.hasLogo}
|
||||||
|
<button class="btn btn-secondary" on:click={handleLogoDelete}>Logo entfernen</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Mitarbeiterverwaltung -->
|
<!-- Mitarbeiterverwaltung -->
|
||||||
<section style="margin-bottom: 40px;">
|
<section style="margin-bottom: 40px;">
|
||||||
<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;">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { api, type Product, type MonthlyTally, type Employee } from '$lib/api/client';
|
import { api, type Product, type MonthlyTally, type Employee } from '$lib/api/client';
|
||||||
|
|
||||||
@@ -38,8 +39,10 @@
|
|||||||
await api.createTally(employeeId, productId);
|
await api.createTally(employeeId, productId);
|
||||||
toastMessage = `${productName} hinzugefügt!`;
|
toastMessage = `${productName} hinzugefügt!`;
|
||||||
showToast = true;
|
showToast = true;
|
||||||
setTimeout(() => { showToast = false; }, 2000);
|
setTimeout(() => {
|
||||||
await loadTallies();
|
showToast = false;
|
||||||
|
goto('/');
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPrice(cents: number): string {
|
function formatPrice(cents: number): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user