commit 6eb940c37c9c0fba79bca7e4c12d70f02e918d1b Author: Markus Date: Tue Mar 31 14:48:36 2026 +0200 init diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..df9c8c4 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,22 @@ +{ + "permissions": { + "allow": [ + "Bash(java --version)", + "Bash(mvn --version)", + "Bash(npm --version)", + "Bash(docker --version)", + "Bash(where.exe mvn:*)", + "Bash(where.exe node:*)", + "Bash(where.exe npm:*)", + "Bash(where.exe gradle:*)", + "Bash(where.exe npx:*)", + "Bash(docker info:*)", + "Bash(docker context:*)", + "Bash(mkdir -p backend/src/main/java/de/strichliste/entity)", + "Bash(mkdir -p backend/src/main/java/de/strichliste/resource)", + "Bash(mkdir -p backend/src/main/java/de/strichliste/filter)", + "Bash(mkdir -p backend/src/main/java/de/strichliste/dto)", + "Bash(mkdir -p backend/src/main/resources/db/migration)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..874f02f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +backups/ +*.log +.env diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..cb2d387 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,6 @@ +target/ +.idea/ +*.iml +.settings/ +.project +.classpath diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..ec3223e --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,17 @@ +FROM maven:3.9-eclipse-temurin-21 AS build +WORKDIR /app +COPY pom.xml . +RUN mvn dependency:go-offline -B +COPY src ./src +RUN mvn package -DskipTests -B + +FROM eclipse-temurin:21-jre +WORKDIR /deployments +COPY --from=build /app/target/quarkus-app/lib/ ./lib/ +COPY --from=build /app/target/quarkus-app/*.jar ./ +COPY --from=build /app/target/quarkus-app/app/ ./app/ +COPY --from=build /app/target/quarkus-app/quarkus/ ./quarkus/ + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "/deployments/quarkus-run.jar"] diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..913a07f --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + de.strichliste + backend + 1.0.0-SNAPSHOT + + + 3.13.0 + 21 + UTF-8 + quarkus-bom + io.quarkus.platform + 3.17.7 + 3.5.2 + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-jdbc-mariadb + + + io.quarkus + quarkus-flyway + + + org.flywaydb + flyway-mysql + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + true + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + + + + + + diff --git a/backend/src/main/docker/Dockerfile.jvm b/backend/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..4790a2c --- /dev/null +++ b/backend/src/main/docker/Dockerfile.jvm @@ -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"] diff --git a/backend/src/main/java/de/strichliste/dto/AccessLinkCreateRequest.java b/backend/src/main/java/de/strichliste/dto/AccessLinkCreateRequest.java new file mode 100644 index 0000000..0511fb9 --- /dev/null +++ b/backend/src/main/java/de/strichliste/dto/AccessLinkCreateRequest.java @@ -0,0 +1,3 @@ +package de.strichliste.dto; + +public record AccessLinkCreateRequest(String role, Long companyId, String description) {} diff --git a/backend/src/main/java/de/strichliste/dto/AccessLinkDto.java b/backend/src/main/java/de/strichliste/dto/AccessLinkDto.java new file mode 100644 index 0000000..cbe13d7 --- /dev/null +++ b/backend/src/main/java/de/strichliste/dto/AccessLinkDto.java @@ -0,0 +1,3 @@ +package de.strichliste.dto; + +public record AccessLinkDto(Long id, String token, String role, Long companyId, String description, boolean active) {} diff --git a/backend/src/main/java/de/strichliste/dto/CompanyCreateRequest.java b/backend/src/main/java/de/strichliste/dto/CompanyCreateRequest.java new file mode 100644 index 0000000..55aee33 --- /dev/null +++ b/backend/src/main/java/de/strichliste/dto/CompanyCreateRequest.java @@ -0,0 +1,3 @@ +package de.strichliste.dto; + +public record CompanyCreateRequest(String name) {} diff --git a/backend/src/main/java/de/strichliste/dto/CompanyDto.java b/backend/src/main/java/de/strichliste/dto/CompanyDto.java new file mode 100644 index 0000000..6190e56 --- /dev/null +++ b/backend/src/main/java/de/strichliste/dto/CompanyDto.java @@ -0,0 +1,3 @@ +package de.strichliste.dto; + +public record CompanyDto(Long id, String name, boolean active) {} diff --git a/backend/src/main/java/de/strichliste/dto/EmployeeCreateRequest.java b/backend/src/main/java/de/strichliste/dto/EmployeeCreateRequest.java new file mode 100644 index 0000000..5268b09 --- /dev/null +++ b/backend/src/main/java/de/strichliste/dto/EmployeeCreateRequest.java @@ -0,0 +1,3 @@ +package de.strichliste.dto; + +public record EmployeeCreateRequest(String firstName, String lastName) {} diff --git a/backend/src/main/java/de/strichliste/dto/EmployeeDto.java b/backend/src/main/java/de/strichliste/dto/EmployeeDto.java new file mode 100644 index 0000000..66a28b7 --- /dev/null +++ b/backend/src/main/java/de/strichliste/dto/EmployeeDto.java @@ -0,0 +1,3 @@ +package de.strichliste.dto; + +public record EmployeeDto(Long id, Long companyId, String firstName, String lastName, boolean active) {} diff --git a/backend/src/main/java/de/strichliste/dto/MonthlyReportDto.java b/backend/src/main/java/de/strichliste/dto/MonthlyReportDto.java new file mode 100644 index 0000000..fd96cb8 --- /dev/null +++ b/backend/src/main/java/de/strichliste/dto/MonthlyReportDto.java @@ -0,0 +1,20 @@ +package de.strichliste.dto; + +import java.util.List; + +public record MonthlyReportDto( + String month, + Long companyId, + String companyName, + List employees, + Long totalCents +) { + public record EmployeeReportLine( + Long employeeId, + String firstName, + String lastName, + Long totalCount, + Long totalCents, + List products + ) {} +} diff --git a/backend/src/main/java/de/strichliste/dto/MonthlyTallyDto.java b/backend/src/main/java/de/strichliste/dto/MonthlyTallyDto.java new file mode 100644 index 0000000..7693c5c --- /dev/null +++ b/backend/src/main/java/de/strichliste/dto/MonthlyTallyDto.java @@ -0,0 +1,8 @@ +package de.strichliste.dto; + +public record MonthlyTallyDto( + String productName, + Integer priceCents, + Long count, + Long totalCents +) {} diff --git a/backend/src/main/java/de/strichliste/dto/ProductCreateRequest.java b/backend/src/main/java/de/strichliste/dto/ProductCreateRequest.java new file mode 100644 index 0000000..2e1ebb5 --- /dev/null +++ b/backend/src/main/java/de/strichliste/dto/ProductCreateRequest.java @@ -0,0 +1,3 @@ +package de.strichliste.dto; + +public record ProductCreateRequest(String name, int priceCents, String iconPlaceholder) {} diff --git a/backend/src/main/java/de/strichliste/dto/ProductDto.java b/backend/src/main/java/de/strichliste/dto/ProductDto.java new file mode 100644 index 0000000..506f519 --- /dev/null +++ b/backend/src/main/java/de/strichliste/dto/ProductDto.java @@ -0,0 +1,3 @@ +package de.strichliste.dto; + +public record ProductDto(Long id, String name, int priceCents, String iconPlaceholder, boolean active) {} diff --git a/backend/src/main/java/de/strichliste/dto/TallyRequest.java b/backend/src/main/java/de/strichliste/dto/TallyRequest.java new file mode 100644 index 0000000..81de2ea --- /dev/null +++ b/backend/src/main/java/de/strichliste/dto/TallyRequest.java @@ -0,0 +1,3 @@ +package de.strichliste.dto; + +public record TallyRequest(Long employeeId, Long productId) {} diff --git a/backend/src/main/java/de/strichliste/entity/AccessLink.java b/backend/src/main/java/de/strichliste/entity/AccessLink.java new file mode 100644 index 0000000..4896180 --- /dev/null +++ b/backend/src/main/java/de/strichliste/entity/AccessLink.java @@ -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 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); + } +} diff --git a/backend/src/main/java/de/strichliste/entity/Company.java b/backend/src/main/java/de/strichliste/entity/Company.java new file mode 100644 index 0000000..059d408 --- /dev/null +++ b/backend/src/main/java/de/strichliste/entity/Company.java @@ -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 employees; + + public static List findAllActive() { + return find("active", true).list(); + } +} diff --git a/backend/src/main/java/de/strichliste/entity/Employee.java b/backend/src/main/java/de/strichliste/entity/Employee.java new file mode 100644 index 0000000..46a4163 --- /dev/null +++ b/backend/src/main/java/de/strichliste/entity/Employee.java @@ -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 findActiveByCompany(Long companyId) { + return find("company.id = ?1 and active = true", companyId).list(); + } + + public static List findAllByCompany(Long companyId) { + return find("company.id", companyId).list(); + } +} diff --git a/backend/src/main/java/de/strichliste/entity/Product.java b/backend/src/main/java/de/strichliste/entity/Product.java new file mode 100644 index 0000000..f116fdb --- /dev/null +++ b/backend/src/main/java/de/strichliste/entity/Product.java @@ -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 findAllActive() { + return find("active", true).list(); + } +} diff --git a/backend/src/main/java/de/strichliste/entity/TallyEntry.java b/backend/src/main/java/de/strichliste/entity/TallyEntry.java new file mode 100644 index 0000000..a853a80 --- /dev/null +++ b/backend/src/main/java/de/strichliste/entity/TallyEntry.java @@ -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); + } + } +} diff --git a/backend/src/main/java/de/strichliste/filter/AuthFilter.java b/backend/src/main/java/de/strichliste/filter/AuthFilter.java new file mode 100644 index 0000000..3479b13 --- /dev/null +++ b/backend/src/main/java/de/strichliste/filter/AuthFilter.java @@ -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); + } + } +} diff --git a/backend/src/main/java/de/strichliste/resource/CompanyAdminResource.java b/backend/src/main/java/de/strichliste/resource/CompanyAdminResource.java new file mode 100644 index 0000000..c61337f --- /dev/null +++ b/backend/src/main/java/de/strichliste/resource/CompanyAdminResource.java @@ -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 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 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 employees = Employee.findAllByCompany(companyId); + List lines = new ArrayList<>(); + long companyTotal = 0; + + for (Employee emp : employees) { + List 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); + } +} diff --git a/backend/src/main/java/de/strichliste/resource/ProviderAdminResource.java b/backend/src/main/java/de/strichliste/resource/ProviderAdminResource.java new file mode 100644 index 0000000..0cf7faf --- /dev/null +++ b/backend/src/main/java/de/strichliste/resource/ProviderAdminResource.java @@ -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 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 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 companies = Company.findAll().list(); + List 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 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 companies, long grandTotalCents) {} +} diff --git a/backend/src/main/java/de/strichliste/resource/PublicResource.java b/backend/src/main/java/de/strichliste/resource/PublicResource.java new file mode 100644 index 0000000..21f49d6 --- /dev/null +++ b/backend/src/main/java/de/strichliste/resource/PublicResource.java @@ -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 getActiveCompanies() { + return Company.findAllActive().stream() + .map(c -> new CompanyDto(c.id, c.name, c.active)) + .toList(); + } + + @GET + @Path("/companies/{id}/employees") + public List 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 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 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(); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 0000000..d4eb7df --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -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 diff --git a/backend/src/main/resources/db/migration/V1__initial_schema.sql b/backend/src/main/resources/db/migration/V1__initial_schema.sql new file mode 100644 index 0000000..b6cdbe4 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1__initial_schema.sql @@ -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; diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql new file mode 100644 index 0000000..6599b3f --- /dev/null +++ b/backend/src/main/resources/db/migration/V2__seed_data.sql @@ -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'); diff --git a/db/backup.sh b/db/backup.sh new file mode 100644 index 0000000..57abdf1 --- /dev/null +++ b/db/backup.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# MariaDB Backup Script - läuft als Endlos-Schleife im Container +BACKUP_DIR="/backups" +RETENTION_DAYS=7 + +while true; do + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + FILENAME="${BACKUP_DIR}/strichliste_${TIMESTAMP}.sql.gz" + + echo "[$(date)] Starting backup..." + mariadb-dump -h db -u strichliste -pstrichliste strichliste | gzip > "${FILENAME}" + + if [ $? -eq 0 ]; then + echo "[$(date)] Backup saved: ${FILENAME}" + else + echo "[$(date)] Backup FAILED" + fi + + # Alte Backups löschen + find ${BACKUP_DIR} -name "strichliste_*.sql.gz" -mtime +${RETENTION_DAYS} -delete + echo "[$(date)] Cleaned up backups older than ${RETENTION_DAYS} days" + + # 24 Stunden warten + sleep 86400 +done diff --git a/db/seed.sql b/db/seed.sql new file mode 100644 index 0000000..67875e3 --- /dev/null +++ b/db/seed.sql @@ -0,0 +1,6 @@ +-- Seed-Daten werden NACH Flyway-Migration eingefügt. +-- Diese Datei wird nur beim ERSTEN Start der DB ausgeführt (docker-entrypoint-initdb.d). +-- Flyway verwaltet das Schema, diese Datei nur Beispieldaten. + +-- Hinweis: Die Tabellen existieren beim DB-Init noch nicht (Flyway läuft im Backend). +-- Daher nutzen wir eine separate Flyway-Migration für Seed-Daten. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0efa89f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +services: + db: + image: mariadb:11.4 + environment: + MARIADB_ROOT_PASSWORD: rootpassword + MARIADB_DATABASE: strichliste + MARIADB_USER: strichliste + MARIADB_PASSWORD: strichliste + ports: + - "3306:3306" + volumes: + - db-data:/var/lib/mysql + - ./db/seed.sql:/docker-entrypoint-initdb.d/seed.sql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: ./backend + environment: + QUARKUS_DATASOURCE_JDBC_URL: jdbc:mariadb://db:3306/strichliste + QUARKUS_DATASOURCE_USERNAME: strichliste + QUARKUS_DATASOURCE_PASSWORD: strichliste + QUARKUS_HTTP_CORS_ORIGINS: http://localhost:5173,http://localhost:4173 + ports: + - "8080:8080" + depends_on: + db: + condition: service_healthy + + frontend: + build: ./frontend + environment: + API_URL: http://backend:8080 + ports: + - "3000:3000" + depends_on: + - backend + + backup: + image: mariadb:11.4.4 + environment: + MARIADB_HOST: db + MARIADB_USER: strichliste + MARIADB_PASSWORD: strichliste + volumes: + - ./backups:/backups + - ./db/backup.sh:/backup.sh + entrypoint: ["/bin/bash", "/backup.sh"] + depends_on: + db: + condition: service_healthy + +volumes: + db-data: diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..0d1a6d3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +build/ +.svelte-kit/ +.env +.env.* diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..8c7e833 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json ./ +RUN npm install +COPY . . +RUN npm run build + +FROM node:22-alpine +WORKDIR /app +COPY --from=build /app/build ./build +COPY --from=build /app/package.json ./ +COPY --from=build /app/node_modules ./node_modules + +ENV PORT=3000 +ENV ORIGIN=http://localhost:3000 +EXPOSE 3000 + +CMD ["node", "build"] diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..cf01d56 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1901 @@ +{ + "name": "strichliste-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "strichliste-frontend", + "version": "1.0.0", + "devDependencies": { + "@sveltejs/adapter-node": "^5.2.0", + "@sveltejs/kit": "^2.8.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.6.0", + "vite": "^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz", + "integrity": "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.4.tgz", + "integrity": "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.59.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz", + "integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.6.4", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@typescript-eslint/types": "^8.2.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.1.tgz", + "integrity": "sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.4", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.6.tgz", + "integrity": "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", + "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9de6421 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "strichliste-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/adapter-node": "^5.2.0", + "@sveltejs/kit": "^2.8.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.6.0", + "vite": "^6.0.0" + }, + "type": "module" +} diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..59203a3 --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,310 @@ +:root { + --color-bg: #1a1a2e; + --color-bg-secondary: #16213e; + --color-bg-card: #0f3460; + --color-primary: #e94560; + --color-primary-hover: #ff6b81; + --color-text: #eaeaea; + --color-text-muted: #a0a0b0; + --color-success: #2ed573; + --color-warning: #ffa502; + --color-border: #2a2a4a; + --radius: 12px; + --radius-sm: 8px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background-color: var(--color-bg); + color: var(--color-text); + min-height: 100vh; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + user-select: none; +} + +a { + color: var(--color-primary); + text-decoration: none; +} + +/* Touch-optimierte Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 16px 32px; + border: none; + border-radius: var(--radius); + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.1s, background-color 0.2s; + touch-action: manipulation; +} + +.btn:active { + transform: scale(0.95); +} + +.btn-primary { + background-color: var(--color-primary); + color: white; +} + +.btn-primary:hover { + background-color: var(--color-primary-hover); +} + +.btn-secondary { + background-color: var(--color-bg-card); + color: var(--color-text); + border: 1px solid var(--color-border); +} + +.btn-success { + background-color: var(--color-success); + color: #1a1a2e; +} + +/* Kacheln für Touch-Auswahl */ +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 16px; + padding: 16px; +} + +.card { + background: var(--color-bg-card); + border-radius: var(--radius); + padding: 24px; + text-align: center; + cursor: pointer; + transition: transform 0.1s, box-shadow 0.2s; + touch-action: manipulation; + min-height: 120px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.card:active { + transform: scale(0.95); +} + +.card:hover { + box-shadow: 0 4px 20px rgba(233, 69, 96, 0.3); +} + +.card h3 { + font-size: 1.3rem; + margin-top: 8px; +} + +.card .subtitle { + color: var(--color-text-muted); + font-size: 0.9rem; + margin-top: 4px; +} + +/* Header */ +.page-header { + padding: 24px; + text-align: center; + background: var(--color-bg-secondary); + border-bottom: 1px solid var(--color-border); +} + +.page-header h1 { + font-size: 1.6rem; +} + +.page-header .back-btn { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + font-size: 1.5rem; + color: var(--color-text); + background: none; + border: none; + cursor: pointer; + padding: 8px; +} + +/* Toast-Benachrichtigung */ +.toast { + position: fixed; + bottom: 40px; + left: 50%; + transform: translateX(-50%); + background: var(--color-success); + color: #1a1a2e; + padding: 16px 32px; + border-radius: var(--radius); + font-size: 1.2rem; + font-weight: 600; + z-index: 1000; + animation: toast-in 0.3s ease-out, toast-out 0.3s ease-in 1.7s forwards; +} + +@keyframes toast-in { + from { opacity: 0; transform: translateX(-50%) translateY(20px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } +} + +@keyframes toast-out { + from { opacity: 1; } + to { opacity: 0; } +} + +/* Admin-Tabellen */ +.admin-table { + width: 100%; + border-collapse: collapse; + margin-top: 16px; +} + +.admin-table th, +.admin-table td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid var(--color-border); +} + +.admin-table th { + background: var(--color-bg-secondary); + font-weight: 600; + color: var(--color-text-muted); + font-size: 0.85rem; + text-transform: uppercase; +} + +.admin-table tr:hover { + background: rgba(255, 255, 255, 0.03); +} + +/* Admin-Layout */ +.admin-container { + max-width: 1200px; + margin: 0 auto; + padding: 24px; +} + +.admin-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; +} + +.admin-header h1 { + font-size: 1.5rem; +} + +/* Formulare */ +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + margin-bottom: 6px; + color: var(--color-text-muted); + font-size: 0.9rem; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 10px 14px; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text); + font-size: 1rem; +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: var(--color-primary); +} + +/* Modal */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.modal { + background: var(--color-bg-secondary); + border-radius: var(--radius); + padding: 32px; + min-width: 400px; + max-width: 90vw; +} + +.modal h2 { + margin-bottom: 20px; +} + +.modal-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 24px; +} + +/* Badge */ +.badge { + display: inline-block; + padding: 4px 10px; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 600; +} + +.badge-active { + background: rgba(46, 213, 115, 0.2); + color: var(--color-success); +} + +.badge-inactive { + background: rgba(255, 165, 2, 0.2); + color: var(--color-warning); +} + +/* Preis-Formatierung */ +.price { + font-variant-numeric: tabular-nums; +} + +/* Monatswähler */ +.month-selector { + display: flex; + align-items: center; + gap: 12px; +} + +.month-selector input[type="month"] { + padding: 8px 12px; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text); + font-size: 1rem; +} diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 0000000..ae4acaf --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,15 @@ + + + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts new file mode 100644 index 0000000..94fa976 --- /dev/null +++ b/frontend/src/hooks.server.ts @@ -0,0 +1,24 @@ +import type { Handle } from '@sveltejs/kit'; + +const API_URL = process.env.API_URL ?? 'http://localhost:8080'; + +export const handle: Handle = async ({ event, resolve }) => { + if (event.url.pathname.startsWith('/api')) { + const targetUrl = `${API_URL}${event.url.pathname}${event.url.search}`; + + const response = await fetch(targetUrl, { + method: event.request.method, + headers: event.request.headers, + body: ['GET', 'HEAD'].includes(event.request.method) ? undefined : event.request.body, + // @ts-ignore + duplex: 'half' + }); + + return new Response(response.body, { + status: response.status, + headers: response.headers + }); + } + + return resolve(event); +}; diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts new file mode 100644 index 0000000..8d5a476 --- /dev/null +++ b/frontend/src/lib/api/client.ts @@ -0,0 +1,153 @@ +const API_BASE = '/api'; + +async function request(path: string, options?: RequestInit): Promise { + const res = await fetch(`${API_BASE}${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...options + }); + if (!res.ok) { + throw new Error(`API error: ${res.status}`); + } + if (res.status === 204 || res.headers.get('content-length') === '0') { + return undefined as T; + } + return res.json(); +} + +export interface Company { + id: number; + name: string; + active: boolean; +} + +export interface Employee { + id: number; + companyId: number; + firstName: string; + lastName: string; + active: boolean; +} + +export interface Product { + id: number; + name: string; + priceCents: number; + iconPlaceholder: string; + active: boolean; +} + +export interface MonthlyTally { + productName: string; + priceCents: number; + count: number; + totalCents: number; +} + +export interface EmployeeReportLine { + employeeId: number; + firstName: string; + lastName: string; + totalCount: number; + totalCents: number; + products: MonthlyTally[]; +} + +export interface MonthlyReport { + month: string; + companyId: number | null; + companyName: string | null; + employees: EmployeeReportLine[]; + totalCents: number; +} + +export interface AccessLink { + id: number; + token: string; + role: string; + companyId: number | null; + description: string; + active: boolean; +} + +export interface ProviderReport { + month: string; + companies: MonthlyReport[]; + grandTotalCents: number; +} + +// --- Public (iPad) --- +export const api = { + getCompanies: () => request('/companies'), + getEmployees: (companyId: number) => request(`/companies/${companyId}/employees`), + getProducts: () => request('/products'), + createTally: (employeeId: number, productId: number) => + request('/tally', { + method: 'POST', + body: JSON.stringify({ employeeId, productId }) + }), + getMonthlyTally: (employeeId: number, month?: string) => + request(`/tally/monthly/${employeeId}${month ? `?month=${month}` : ''}`), + + // --- Company Admin --- + companyAdmin: { + getEmployees: (token: string) => + request(`/admin/company/employees?token=${token}`), + createEmployee: (token: string, firstName: string, lastName: string) => + request(`/admin/company/employees?token=${token}`, { + method: 'POST', + body: JSON.stringify({ firstName, lastName }) + }), + updateEmployee: (token: string, id: number, firstName: string, lastName: string) => + request(`/admin/company/employees/${id}?token=${token}`, { + method: 'PUT', + body: JSON.stringify({ firstName, lastName }) + }), + toggleEmployee: (token: string, id: number) => + request(`/admin/company/employees/${id}/toggle?token=${token}`, { method: 'PUT' }), + getReport: (token: string, month?: string) => + request(`/admin/company/report?token=${token}${month ? `&month=${month}` : ''}`), + getEmployeeReport: (token: string, employeeId: number, month?: string) => + request(`/admin/company/report/employee/${employeeId}?token=${token}${month ? `&month=${month}` : ''}`) + }, + + // --- Provider Admin --- + providerAdmin: { + getCompanies: (token: string) => + request(`/admin/provider/companies?token=${token}`), + createCompany: (token: string, name: string) => + request(`/admin/provider/companies?token=${token}`, { + method: 'POST', + body: JSON.stringify({ name }) + }), + updateCompany: (token: string, id: number, name: string) => + request(`/admin/provider/companies/${id}?token=${token}`, { + method: 'PUT', + body: JSON.stringify({ name }) + }), + toggleCompany: (token: string, id: number) => + request(`/admin/provider/companies/${id}/toggle?token=${token}`, { method: 'PUT' }), + getProducts: (token: string) => + request(`/admin/provider/products?token=${token}`), + createProduct: (token: string, name: string, priceCents: number, iconPlaceholder?: string) => + request(`/admin/provider/products?token=${token}`, { + method: 'POST', + body: JSON.stringify({ name, priceCents, iconPlaceholder }) + }), + updateProduct: (token: string, id: number, name: string, priceCents: number, iconPlaceholder?: string) => + request(`/admin/provider/products/${id}?token=${token}`, { + method: 'PUT', + body: JSON.stringify({ name, priceCents, iconPlaceholder }) + }), + toggleProduct: (token: string, id: number) => + request(`/admin/provider/products/${id}/toggle?token=${token}`, { method: 'PUT' }), + getReport: (token: string, month?: string) => + request(`/admin/provider/report?token=${token}${month ? `&month=${month}` : ''}`), + getAccessLinks: (token: string) => + request(`/admin/provider/access-links?token=${token}`), + createAccessLink: (token: string, role: string, companyId?: number, description?: string) => + request(`/admin/provider/access-links?token=${token}`, { + method: 'POST', + body: JSON.stringify({ role, companyId, description }) + }) + } +}; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..2e511e0 --- /dev/null +++ b/frontend/src/routes/+layout.svelte @@ -0,0 +1,5 @@ + + + diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..796fc88 --- /dev/null +++ b/frontend/src/routes/+page.svelte @@ -0,0 +1,36 @@ + + + + Strichliste + + + + +{#if loading} +
+

Laden...

+
+{:else} +
+ {#each companies as company} + +
🏢
+

{company.name}

+
+ {/each} +
+{/if} diff --git a/frontend/src/routes/admin/company/+page.svelte b/frontend/src/routes/admin/company/+page.svelte new file mode 100644 index 0000000..4ac4181 --- /dev/null +++ b/frontend/src/routes/admin/company/+page.svelte @@ -0,0 +1,228 @@ + + + + Firmen-Admin - Strichliste + + +
+
+

Firmen-Administration

+
+ + {#if !token} +

Kein Zugangstoken angegeben.

+ {:else if loading} +

Laden...

+ {:else} + +
+
+

Mitarbeiter

+ +
+ + + + + + + + + + + {#each employees as emp} + + + + + + {/each} + +
NameStatusAktionen
{emp.firstName} {emp.lastName} + + {emp.active ? 'Aktiv' : 'Inaktiv'} + + + + +
+
+ + +
+
+

Monatsauswertung

+
+ +
+
+ + {#if report} + + + + + + + + + + + {#each report.employees as line} + + + + + + + {/each} + {#if report.employees.length > 0} + + + + + + + {:else} + + {/if} + +
MitarbeiterAnzahlSummeDetail
{line.firstName} {line.lastName}{line.totalCount}×{formatPrice(line.totalCents)} + +
Gesamt{formatPrice(report.totalCents)}
Keine Einträge in diesem Monat
+ {/if} + + {#if selectedEmployee} +
+
+

Detail: {selectedEmployee.firstName} {selectedEmployee.lastName}

+ +
+ + + + + + + + + + + {#each selectedEmployee.products as prod} + + + + + + + {/each} + + + + + + + +
ProduktEinzelpreisAnzahlSumme
{prod.productName}{formatPrice(prod.priceCents)}{prod.count}×{formatPrice(prod.totalCents)}
Gesamt{selectedEmployee.totalCount}×{formatPrice(selectedEmployee.totalCents)}
+
+ {/if} +
+ {/if} +
+ +{#if showModal} + +{/if} diff --git a/frontend/src/routes/admin/provider/+page.svelte b/frontend/src/routes/admin/provider/+page.svelte new file mode 100644 index 0000000..243c3b6 --- /dev/null +++ b/frontend/src/routes/admin/provider/+page.svelte @@ -0,0 +1,402 @@ + + + + Anbieter-Admin - Strichliste + + +
+
+

Anbieter-Administration

+
+ + {#if !token} +

Kein Zugangstoken angegeben.

+ {:else if loading} +

Laden...

+ {:else} + +
+ {#each [ + { key: 'companies', label: 'Firmen' }, + { key: 'products', label: 'Produkte' }, + { key: 'report', label: 'Auswertung' }, + { key: 'links', label: 'Zugangslinks' } + ] as tab} + + {/each} +
+ + + {#if activeTab === 'companies'} +
+

Firmen

+ +
+ + + + + + {#each companies as c} + + + + + + {/each} + +
NameStatusAktionen
{c.name} + + {c.active ? 'Aktiv' : 'Inaktiv'} + + + + +
+ {/if} + + + {#if activeTab === 'products'} +
+

Produkte

+ +
+ + + + + + {#each products as p} + + + + + + + + {/each} + +
NamePreisIconStatusAktionen
{p.name}{formatPrice(p.priceCents)}{p.iconPlaceholder} + + {p.active ? 'Aktiv' : 'Inaktiv'} + + + + +
+ {/if} + + + {#if activeTab === 'report'} +
+

Gesamtauswertung

+
+ +
+
+ {#if report} + {#each report.companies as companyReport} +
+

{companyReport.companyName ?? 'Firma'}

+ + + + + + + + + + {#each companyReport.employees as line} + + + + + + {/each} + + + + + + +
MitarbeiterAnzahlSumme
{line.firstName} {line.lastName}{line.totalCount}×{formatPrice(line.totalCents)}
Firma-Gesamt{formatPrice(companyReport.totalCents)}
+
+ {/each} +
+ Gesamtsumme: {formatPrice(report.grandTotalCents)} +
+ {/if} + {/if} + + + {#if activeTab === 'links'} +
+

Zugangslinks

+ +
+ + + + + + {#each accessLinks as link} + + + + + + {/each} + +
BeschreibungRolleLink
{link.description ?? '-'} + + {link.role === 'COMPANY_ADMIN' ? 'Firmen-Admin' : 'Anbieter-Admin'} + + + {buildLink(link)} +
+ {/if} + {/if} +
+ + +{#if showCompanyModal} + +{/if} + + +{#if showProductModal} + +{/if} + + +{#if showLinkModal} + +{/if} diff --git a/frontend/src/routes/company/[id]/+page.svelte b/frontend/src/routes/company/[id]/+page.svelte new file mode 100644 index 0000000..01290cd --- /dev/null +++ b/frontend/src/routes/company/[id]/+page.svelte @@ -0,0 +1,47 @@ + + + + {companyName} - Strichliste + + + + +{#if loading} +
+

Laden...

+
+{:else} +
+ {#each employees as emp} + +
👤
+

{emp.firstName} {emp.lastName}

+
+ {/each} +
+{/if} diff --git a/frontend/src/routes/company/[id]/tally/+page.svelte b/frontend/src/routes/company/[id]/tally/+page.svelte new file mode 100644 index 0000000..d49543b --- /dev/null +++ b/frontend/src/routes/company/[id]/tally/+page.svelte @@ -0,0 +1,107 @@ + + + + Produkt wählen - Strichliste + + + + +{#if loading} +
+

Laden...

+
+{:else} +
+ {#each products as product} + + {/each} +
+ + {#if tallies.length > 0} +
+

Diesen Monat

+ + + + + + + + + + {#each tallies as tally} + + + + + + {/each} + + + + + + +
ProduktAnzahlSumme
{tally.productName}{tally.count}×{formatPrice(tally.totalCents)}
Gesamt{tallies.reduce((s, t) => s + t.count, 0)}×{formatPrice(tallies.reduce((s, t) => s + t.totalCents, 0))}
+
+ {/if} +{/if} + +{#if showToast} +
✓ {toastMessage}
+{/if} diff --git a/frontend/static/manifest.json b/frontend/static/manifest.json new file mode 100644 index 0000000..2b211c5 --- /dev/null +++ b/frontend/static/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "Strichliste", + "short_name": "Strichliste", + "description": "Digitale Strichliste für die Kaffeeküche", + "start_url": "/", + "display": "standalone", + "background_color": "#1a1a2e", + "theme_color": "#16213e", + "orientation": "portrait", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 0000000..6bc06ad --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,12 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ out: 'build' }) + } +}; + +export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..a8f10c8 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..86e2da4 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,11 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], + server: { + proxy: { + '/api': 'http://localhost:8080' + } + } +});