This commit is contained in:
2026-03-31 14:48:36 +02:00
commit 6eb940c37c
50 changed files with 4433 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
FROM eclipse-temurin:21-jre
ENV LANGUAGE='de_DE:de'
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
COPY target/quarkus-app/lib/ /deployments/lib/
COPY target/quarkus-app/*.jar /deployments/
COPY target/quarkus-app/app/ /deployments/app/
COPY target/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 185
ENTRYPOINT ["java", "-jar", "/deployments/quarkus-run.jar"]

View File

@@ -0,0 +1,3 @@
package de.strichliste.dto;
public record AccessLinkCreateRequest(String role, Long companyId, String description) {}

View File

@@ -0,0 +1,3 @@
package de.strichliste.dto;
public record AccessLinkDto(Long id, String token, String role, Long companyId, String description, boolean active) {}

View File

@@ -0,0 +1,3 @@
package de.strichliste.dto;
public record CompanyCreateRequest(String name) {}

View File

@@ -0,0 +1,3 @@
package de.strichliste.dto;
public record CompanyDto(Long id, String name, boolean active) {}

View File

@@ -0,0 +1,3 @@
package de.strichliste.dto;
public record EmployeeCreateRequest(String firstName, String lastName) {}

View File

@@ -0,0 +1,3 @@
package de.strichliste.dto;
public record EmployeeDto(Long id, Long companyId, String firstName, String lastName, boolean active) {}

View File

@@ -0,0 +1,20 @@
package de.strichliste.dto;
import java.util.List;
public record MonthlyReportDto(
String month,
Long companyId,
String companyName,
List<EmployeeReportLine> employees,
Long totalCents
) {
public record EmployeeReportLine(
Long employeeId,
String firstName,
String lastName,
Long totalCount,
Long totalCents,
List<MonthlyTallyDto> products
) {}
}

View File

@@ -0,0 +1,8 @@
package de.strichliste.dto;
public record MonthlyTallyDto(
String productName,
Integer priceCents,
Long count,
Long totalCents
) {}

View File

@@ -0,0 +1,3 @@
package de.strichliste.dto;
public record ProductCreateRequest(String name, int priceCents, String iconPlaceholder) {}

View File

@@ -0,0 +1,3 @@
package de.strichliste.dto;
public record ProductDto(Long id, String name, int priceCents, String iconPlaceholder, boolean active) {}

View File

@@ -0,0 +1,3 @@
package de.strichliste.dto;
public record TallyRequest(Long employeeId, Long productId) {}

View File

@@ -0,0 +1,45 @@
package de.strichliste.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.Optional;
@Entity
@Table(name = "access_link")
public class AccessLink extends PanacheEntityBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@Column(nullable = false, unique = true, length = 64)
public String token;
@Column(nullable = false, length = 20)
public String role;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "company_id")
public Company company;
public String description;
@Column(nullable = false)
public boolean active = true;
@Column(name = "created_at", nullable = false, updatable = false)
public LocalDateTime createdAt = LocalDateTime.now();
public static Optional<AccessLink> findByToken(String token) {
return find("token = ?1 and active = true", token).firstResultOptional();
}
public boolean isProviderAdmin() {
return "PROVIDER_ADMIN".equals(role);
}
public boolean isCompanyAdmin() {
return "COMPANY_ADMIN".equals(role);
}
}

View File

@@ -0,0 +1,31 @@
package de.strichliste.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "company")
public class Company extends PanacheEntityBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@Column(nullable = false)
public String name;
@Column(nullable = false)
public boolean active = true;
@Column(name = "created_at", nullable = false, updatable = false)
public LocalDateTime createdAt = LocalDateTime.now();
@OneToMany(mappedBy = "company", fetch = FetchType.LAZY)
public List<Employee> employees;
public static List<Company> findAllActive() {
return find("active", true).list();
}
}

View File

@@ -0,0 +1,39 @@
package de.strichliste.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "employee")
public class Employee extends PanacheEntityBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "company_id", nullable = false)
public Company company;
@Column(name = "first_name", nullable = false)
public String firstName;
@Column(name = "last_name", nullable = false)
public String lastName;
@Column(nullable = false)
public boolean active = true;
@Column(name = "created_at", nullable = false, updatable = false)
public LocalDateTime createdAt = LocalDateTime.now();
public static List<Employee> findActiveByCompany(Long companyId) {
return find("company.id = ?1 and active = true", companyId).list();
}
public static List<Employee> findAllByCompany(Long companyId) {
return find("company.id", companyId).list();
}
}

View File

@@ -0,0 +1,34 @@
package de.strichliste.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "product")
public class Product extends PanacheEntityBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@Column(nullable = false)
public String name;
@Column(name = "price_cents", nullable = false)
public int priceCents;
@Column(name = "icon_placeholder")
public String iconPlaceholder = "coffee";
@Column(nullable = false)
public boolean active = true;
@Column(name = "created_at", nullable = false, updatable = false)
public LocalDateTime createdAt = LocalDateTime.now();
public static List<Product> findAllActive() {
return find("active", true).list();
}
}

View File

@@ -0,0 +1,38 @@
package de.strichliste.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Entity
@Table(name = "tally_entry")
public class TallyEntry extends PanacheEntityBase {
private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM");
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "employee_id", nullable = false)
public Employee employee;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
public Product product;
@Column(name = "month_key", nullable = false, length = 7)
public String monthKey;
@Column(name = "created_at", nullable = false, updatable = false)
public LocalDateTime createdAt = LocalDateTime.now();
@PrePersist
public void setMonthKey() {
if (monthKey == null) {
monthKey = LocalDateTime.now().format(MONTH_FORMAT);
}
}
}

View File

@@ -0,0 +1,74 @@
package de.strichliste.filter;
import de.strichliste.entity.AccessLink;
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.NameBinding;
import java.lang.annotation.*;
@Provider
@AuthFilter.Secured
@Priority(Priorities.AUTHENTICATION)
public class AuthFilter implements ContainerRequestFilter {
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Secured {
String[] roles() default {};
}
@Inject
ResourceInfo resourceInfo;
@Override
public void filter(ContainerRequestContext ctx) {
String token = ctx.getUriInfo().getQueryParameters().getFirst("token");
if (token == null) {
token = ctx.getHeaderString("X-Auth-Token");
}
if (token == null || token.isBlank()) {
ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
return;
}
var linkOpt = AccessLink.findByToken(token);
if (linkOpt.isEmpty()) {
ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
return;
}
AccessLink link = linkOpt.get();
Secured secured = resourceInfo.getResourceMethod().getAnnotation(Secured.class);
if (secured == null) {
secured = resourceInfo.getResourceClass().getAnnotation(Secured.class);
}
if (secured != null && secured.roles().length > 0) {
boolean hasRole = false;
for (String role : secured.roles()) {
if (role.equals(link.role)) {
hasRole = true;
break;
}
}
if (!hasRole) {
ctx.abortWith(Response.status(Response.Status.FORBIDDEN).build());
return;
}
}
ctx.setProperty("accessLink", link);
ctx.setProperty("role", link.role);
if (link.company != null) {
ctx.setProperty("companyId", link.company.id);
}
}
}

View File

@@ -0,0 +1,174 @@
package de.strichliste.resource;
import de.strichliste.dto.*;
import de.strichliste.entity.*;
import de.strichliste.filter.AuthFilter.Secured;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.jboss.resteasy.reactive.RestResponse;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@Path("/api/admin/company")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Secured(roles = {"COMPANY_ADMIN"})
public class CompanyAdminResource {
private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM");
@GET
@Path("/employees")
public Response getEmployees(@QueryParam("token") String token) {
AccessLink link = AccessLink.findByToken(token).orElse(null);
if (link == null || link.company == null) {
return Response.status(Response.Status.FORBIDDEN).build();
}
List<EmployeeDto> employees = Employee.findAllByCompany(link.company.id).stream()
.map(e -> new EmployeeDto(e.id, e.company.id, e.firstName, e.lastName, e.active))
.toList();
return Response.ok(employees).build();
}
@POST
@Path("/employees")
@Transactional
public Response createEmployee(@QueryParam("token") String token, EmployeeCreateRequest request) {
AccessLink link = AccessLink.findByToken(token).orElse(null);
if (link == null || link.company == null) {
return Response.status(Response.Status.FORBIDDEN).build();
}
Employee employee = new Employee();
employee.company = link.company;
employee.firstName = request.firstName();
employee.lastName = request.lastName();
employee.persist();
return Response.status(Response.Status.CREATED)
.entity(new EmployeeDto(employee.id, employee.company.id, employee.firstName, employee.lastName, employee.active))
.build();
}
@PUT
@Path("/employees/{id}")
@Transactional
public Response updateEmployee(@QueryParam("token") String token, @PathParam("id") Long id, EmployeeCreateRequest request) {
AccessLink link = AccessLink.findByToken(token).orElse(null);
if (link == null || link.company == null) {
return Response.status(Response.Status.FORBIDDEN).build();
}
Employee employee = Employee.findById(id);
if (employee == null || !employee.company.id.equals(link.company.id)) {
return Response.status(Response.Status.NOT_FOUND).build();
}
employee.firstName = request.firstName();
employee.lastName = request.lastName();
return Response.ok(new EmployeeDto(employee.id, employee.company.id, employee.firstName, employee.lastName, employee.active)).build();
}
@PUT
@Path("/employees/{id}/toggle")
@Transactional
public Response toggleEmployee(@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();
}
Employee employee = Employee.findById(id);
if (employee == null || !employee.company.id.equals(link.company.id)) {
return Response.status(Response.Status.NOT_FOUND).build();
}
employee.active = !employee.active;
return Response.ok(new EmployeeDto(employee.id, employee.company.id, employee.firstName, employee.lastName, employee.active)).build();
}
@GET
@Path("/report")
public Response getMonthlyReport(@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);
return Response.ok(buildCompanyReport(link.company.id, monthKey)).build();
}
@GET
@Path("/report/employee/{employeeId}")
public Response getEmployeeReport(
@QueryParam("token") String token,
@PathParam("employeeId") Long employeeId,
@QueryParam("month") String month) {
AccessLink link = AccessLink.findByToken(token).orElse(null);
if (link == null || link.company == null) {
return Response.status(Response.Status.FORBIDDEN).build();
}
Employee employee = Employee.findById(employeeId);
if (employee == null || !employee.company.id.equals(link.company.id)) {
return Response.status(Response.Status.NOT_FOUND).build();
}
String monthKey = month != null ? month : LocalDateTime.now().format(MONTH_FORMAT);
List<MonthlyTallyDto> tallies = TallyEntry.find(
"SELECT t.product.name, t.product.priceCents, COUNT(t), COUNT(t) * t.product.priceCents " +
"FROM TallyEntry t WHERE t.employee.id = ?1 AND t.monthKey = ?2 " +
"GROUP BY t.product.id, t.product.name, t.product.priceCents",
employeeId, monthKey)
.project(MonthlyTallyDto.class)
.list();
long totalCents = tallies.stream().mapToLong(MonthlyTallyDto::totalCents).sum();
long totalCount = tallies.stream().mapToLong(MonthlyTallyDto::count).sum();
var line = new MonthlyReportDto.EmployeeReportLine(
employee.id, employee.firstName, employee.lastName, totalCount, totalCents, tallies);
return Response.ok(line).build();
}
static MonthlyReportDto buildCompanyReport(Long companyId, String monthKey) {
Company company = Company.findById(companyId);
List<Employee> employees = Employee.findAllByCompany(companyId);
List<MonthlyReportDto.EmployeeReportLine> lines = new ArrayList<>();
long companyTotal = 0;
for (Employee emp : employees) {
List<MonthlyTallyDto> tallies = TallyEntry.find(
"SELECT t.product.name, t.product.priceCents, COUNT(t), COUNT(t) * t.product.priceCents " +
"FROM TallyEntry t WHERE t.employee.id = ?1 AND t.monthKey = ?2 " +
"GROUP BY t.product.id, t.product.name, t.product.priceCents",
emp.id, monthKey)
.project(MonthlyTallyDto.class)
.list();
long totalCents = tallies.stream().mapToLong(MonthlyTallyDto::totalCents).sum();
long totalCount = tallies.stream().mapToLong(MonthlyTallyDto::count).sum();
companyTotal += totalCents;
if (totalCount > 0) {
lines.add(new MonthlyReportDto.EmployeeReportLine(
emp.id, emp.firstName, emp.lastName, totalCount, totalCents, tallies));
}
}
return new MonthlyReportDto(monthKey, companyId, company != null ? company.name : "", lines, companyTotal);
}
}

View File

@@ -0,0 +1,188 @@
package de.strichliste.resource;
import de.strichliste.dto.*;
import de.strichliste.entity.*;
import de.strichliste.filter.AuthFilter.Secured;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Path("/api/admin/provider")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Secured(roles = {"PROVIDER_ADMIN"})
public class ProviderAdminResource {
private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM");
// --- Companies ---
@GET
@Path("/companies")
public List<CompanyDto> getAllCompanies() {
return Company.findAll().list().stream()
.map(obj -> {
Company c = (Company) obj;
return new CompanyDto(c.id, c.name, c.active);
})
.toList();
}
@POST
@Path("/companies")
@Transactional
public Response createCompany(CompanyCreateRequest request) {
Company company = new Company();
company.name = request.name();
company.persist();
return Response.status(Response.Status.CREATED)
.entity(new CompanyDto(company.id, company.name, company.active))
.build();
}
@PUT
@Path("/companies/{id}")
@Transactional
public Response updateCompany(@PathParam("id") Long id, CompanyCreateRequest request) {
Company company = Company.findById(id);
if (company == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
company.name = request.name();
return Response.ok(new CompanyDto(company.id, company.name, company.active)).build();
}
@PUT
@Path("/companies/{id}/toggle")
@Transactional
public Response toggleCompany(@PathParam("id") Long id) {
Company company = Company.findById(id);
if (company == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
company.active = !company.active;
return Response.ok(new CompanyDto(company.id, company.name, company.active)).build();
}
// --- Products ---
@GET
@Path("/products")
public List<ProductDto> getAllProducts() {
return Product.findAll().list().stream()
.map(obj -> {
Product p = (Product) obj;
return new ProductDto(p.id, p.name, p.priceCents, p.iconPlaceholder, p.active);
})
.toList();
}
@POST
@Path("/products")
@Transactional
public Response createProduct(ProductCreateRequest request) {
Product product = new Product();
product.name = request.name();
product.priceCents = request.priceCents();
product.iconPlaceholder = request.iconPlaceholder() != null ? request.iconPlaceholder() : "coffee";
product.persist();
return Response.status(Response.Status.CREATED)
.entity(new ProductDto(product.id, product.name, product.priceCents, product.iconPlaceholder, product.active))
.build();
}
@PUT
@Path("/products/{id}")
@Transactional
public Response updateProduct(@PathParam("id") Long id, ProductCreateRequest request) {
Product product = Product.findById(id);
if (product == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
product.name = request.name();
product.priceCents = request.priceCents();
if (request.iconPlaceholder() != null) {
product.iconPlaceholder = request.iconPlaceholder();
}
return Response.ok(new ProductDto(product.id, product.name, product.priceCents, product.iconPlaceholder, product.active)).build();
}
@PUT
@Path("/products/{id}/toggle")
@Transactional
public Response toggleProduct(@PathParam("id") Long id) {
Product product = Product.findById(id);
if (product == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
product.active = !product.active;
return Response.ok(new ProductDto(product.id, product.name, product.priceCents, product.iconPlaceholder, product.active)).build();
}
// --- Reports ---
@GET
@Path("/report")
public Response getOverallReport(@QueryParam("month") String month) {
String monthKey = month != null ? month : LocalDateTime.now().format(MONTH_FORMAT);
List<Company> companies = Company.findAll().list();
List<MonthlyReportDto> reports = new ArrayList<>();
for (Company company : companies) {
MonthlyReportDto report = CompanyAdminResource.buildCompanyReport(company.id, monthKey);
if (!report.employees().isEmpty()) {
reports.add(report);
}
}
long grandTotal = reports.stream().mapToLong(MonthlyReportDto::totalCents).sum();
var result = new ProviderReportDto(monthKey, reports, grandTotal);
return Response.ok(result).build();
}
// --- Access Links ---
@GET
@Path("/access-links")
public List<AccessLinkDto> getAccessLinks() {
return AccessLink.findAll().list().stream()
.map(obj -> {
AccessLink a = (AccessLink) obj;
return new AccessLinkDto(a.id, a.token, a.role, a.company != null ? a.company.id : null, a.description, a.active);
})
.toList();
}
@POST
@Path("/access-links")
@Transactional
public Response createAccessLink(AccessLinkCreateRequest request) {
AccessLink link = new AccessLink();
link.token = UUID.randomUUID().toString().replace("-", "");
link.role = request.role();
link.description = request.description();
if ("COMPANY_ADMIN".equals(request.role()) && request.companyId() != null) {
Company company = Company.findById(request.companyId());
if (company == null) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
link.company = company;
}
link.persist();
return Response.status(Response.Status.CREATED)
.entity(new AccessLinkDto(link.id, link.token, link.role, link.company != null ? link.company.id : null, link.description, link.active))
.build();
}
public record ProviderReportDto(String month, List<MonthlyReportDto> companies, long grandTotalCents) {}
}

View File

@@ -0,0 +1,81 @@
package de.strichliste.resource;
import de.strichliste.dto.*;
import de.strichliste.entity.*;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Path("/api")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PublicResource {
private static final DateTimeFormatter MONTH_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM");
@GET
@Path("/companies")
public List<CompanyDto> getActiveCompanies() {
return Company.findAllActive().stream()
.map(c -> new CompanyDto(c.id, c.name, c.active))
.toList();
}
@GET
@Path("/companies/{id}/employees")
public List<EmployeeDto> getEmployeesByCompany(@PathParam("id") Long companyId) {
return Employee.findActiveByCompany(companyId).stream()
.map(e -> new EmployeeDto(e.id, e.company.id, e.firstName, e.lastName, e.active))
.toList();
}
@GET
@Path("/products")
public List<ProductDto> getActiveProducts() {
return Product.findAllActive().stream()
.map(p -> new ProductDto(p.id, p.name, p.priceCents, p.iconPlaceholder, p.active))
.toList();
}
@POST
@Path("/tally")
@Transactional
public Response createTally(TallyRequest request) {
Employee employee = Employee.findById(request.employeeId());
if (employee == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
Product product = Product.findById(request.productId());
if (product == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
TallyEntry entry = new TallyEntry();
entry.employee = employee;
entry.product = product;
entry.persist();
return Response.status(Response.Status.CREATED).build();
}
@GET
@Path("/tally/monthly/{employeeId}")
public List<MonthlyTallyDto> getMonthlyTally(
@PathParam("employeeId") Long employeeId,
@QueryParam("month") String month) {
String monthKey = month != null ? month : LocalDateTime.now().format(MONTH_FORMAT);
return TallyEntry.find(
"SELECT t.product.name, t.product.priceCents, COUNT(t), COUNT(t) * t.product.priceCents " +
"FROM TallyEntry t WHERE t.employee.id = ?1 AND t.monthKey = ?2 " +
"GROUP BY t.product.id, t.product.name, t.product.priceCents",
employeeId, monthKey)
.project(MonthlyTallyDto.class)
.list();
}
}

View File

@@ -0,0 +1,14 @@
# Datasource
quarkus.datasource.db-kind=mariadb
quarkus.datasource.username=strichliste
quarkus.datasource.password=strichliste
quarkus.datasource.jdbc.url=jdbc:mariadb://localhost:3306/strichliste
# Hibernate
quarkus.hibernate-orm.database.generation=none
# Flyway
quarkus.flyway.migrate-at-start=true
# CORS ist deaktiviert, da alle Anfragen über den SvelteKit-Proxy laufen
quarkus.http.cors=false

View File

@@ -0,0 +1,49 @@
CREATE TABLE company (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE employee (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
company_id BIGINT NOT NULL,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_employee_company FOREIGN KEY (company_id) REFERENCES company(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE product (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price_cents INT NOT NULL DEFAULT 0,
icon_placeholder VARCHAR(50) DEFAULT 'coffee',
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE tally_entry (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
employee_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
month_key VARCHAR(7) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_tally_employee FOREIGN KEY (employee_id) REFERENCES employee(id),
CONSTRAINT fk_tally_product FOREIGN KEY (product_id) REFERENCES product(id),
INDEX idx_tally_month (month_key),
INDEX idx_tally_employee_month (employee_id, month_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE access_link (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
token VARCHAR(64) NOT NULL UNIQUE,
role VARCHAR(20) NOT NULL,
company_id BIGINT,
description VARCHAR(255),
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_access_link_company FOREIGN KEY (company_id) REFERENCES company(id),
CONSTRAINT chk_role CHECK (role IN ('COMPANY_ADMIN', 'PROVIDER_ADMIN'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -0,0 +1,25 @@
-- Beispiel-Firmen
INSERT INTO company (name) VALUES ('Musterfirma GmbH');
INSERT INTO company (name) VALUES ('Beispiel AG');
-- Beispiel-Mitarbeiter
INSERT INTO employee (company_id, first_name, last_name) VALUES (1, 'Max', 'Mustermann');
INSERT INTO employee (company_id, first_name, last_name) VALUES (1, 'Erika', 'Musterfrau');
INSERT INTO employee (company_id, first_name, last_name) VALUES (2, 'Hans', 'Beispiel');
-- Beispiel-Produkte
INSERT INTO product (name, price_cents, icon_placeholder) VALUES ('Kaffee', 50, 'coffee');
INSERT INTO product (name, price_cents, icon_placeholder) VALUES ('Cappuccino', 80, 'coffee');
INSERT INTO product (name, price_cents, icon_placeholder) VALUES ('Espresso', 40, 'coffee');
INSERT INTO product (name, price_cents, icon_placeholder) VALUES ('Kakao', 60, 'chocolate');
INSERT INTO product (name, price_cents, icon_placeholder) VALUES ('Tee', 30, 'tea');
-- Zugangslinks
INSERT INTO access_link (token, role, company_id, description)
VALUES ('company1-admin-token', 'COMPANY_ADMIN', 1, 'Admin-Zugang Musterfirma GmbH');
INSERT INTO access_link (token, role, company_id, description)
VALUES ('company2-admin-token', 'COMPANY_ADMIN', 2, 'Admin-Zugang Beispiel AG');
INSERT INTO access_link (token, role, company_id, description)
VALUES ('provider-admin-token', 'PROVIDER_ADMIN', NULL, 'Anbieter-Admin Zugang');