Add report templates and unify template storage
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
package de.svencarstensen.muh.domain;
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Document("reportTemplates")
|
||||||
|
public record ReportTemplate(
|
||||||
|
@Id String userId,
|
||||||
|
List<InvoiceTemplateElement> elements,
|
||||||
|
LocalDateTime createdAt,
|
||||||
|
LocalDateTime updatedAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.svencarstensen.muh.domain;
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.mongodb.core.index.CompoundIndex;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Document("templates")
|
||||||
|
@CompoundIndex(name = "user_template_type_unique", def = "{'userId': 1, 'type': 1}", unique = true)
|
||||||
|
public record Template(
|
||||||
|
@Id String id,
|
||||||
|
String userId,
|
||||||
|
TemplateType type,
|
||||||
|
List<InvoiceTemplateElement> elements,
|
||||||
|
LocalDateTime createdAt,
|
||||||
|
LocalDateTime updatedAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package de.svencarstensen.muh.domain;
|
||||||
|
|
||||||
|
public enum TemplateType {
|
||||||
|
INVOICE,
|
||||||
|
REPORT
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.svencarstensen.muh.repository;
|
||||||
|
|
||||||
|
import de.svencarstensen.muh.domain.ReportTemplate;
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
|
||||||
|
public interface ReportTemplateRepository extends MongoRepository<ReportTemplate, String> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.svencarstensen.muh.repository;
|
||||||
|
|
||||||
|
import de.svencarstensen.muh.domain.Template;
|
||||||
|
import de.svencarstensen.muh.domain.TemplateType;
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface TemplateRepository extends MongoRepository<Template, String> {
|
||||||
|
|
||||||
|
Optional<Template> findByUserIdAndType(String userId, TemplateType type);
|
||||||
|
}
|
||||||
@@ -3,8 +3,11 @@ package de.svencarstensen.muh.service;
|
|||||||
import de.svencarstensen.muh.domain.AppUser;
|
import de.svencarstensen.muh.domain.AppUser;
|
||||||
import de.svencarstensen.muh.domain.InvoiceTemplate;
|
import de.svencarstensen.muh.domain.InvoiceTemplate;
|
||||||
import de.svencarstensen.muh.domain.InvoiceTemplateElement;
|
import de.svencarstensen.muh.domain.InvoiceTemplateElement;
|
||||||
|
import de.svencarstensen.muh.domain.Template;
|
||||||
|
import de.svencarstensen.muh.domain.TemplateType;
|
||||||
import de.svencarstensen.muh.repository.AppUserRepository;
|
import de.svencarstensen.muh.repository.AppUserRepository;
|
||||||
import de.svencarstensen.muh.repository.InvoiceTemplateRepository;
|
import de.svencarstensen.muh.repository.InvoiceTemplateRepository;
|
||||||
|
import de.svencarstensen.muh.repository.TemplateRepository;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -12,40 +15,52 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class InvoiceTemplateService {
|
public class InvoiceTemplateService {
|
||||||
|
|
||||||
|
private static final TemplateType TEMPLATE_TYPE = TemplateType.INVOICE;
|
||||||
|
|
||||||
private final AppUserRepository appUserRepository;
|
private final AppUserRepository appUserRepository;
|
||||||
|
private final TemplateRepository templateRepository;
|
||||||
private final InvoiceTemplateRepository invoiceTemplateRepository;
|
private final InvoiceTemplateRepository invoiceTemplateRepository;
|
||||||
|
|
||||||
public InvoiceTemplateService(
|
public InvoiceTemplateService(
|
||||||
AppUserRepository appUserRepository,
|
AppUserRepository appUserRepository,
|
||||||
|
TemplateRepository templateRepository,
|
||||||
InvoiceTemplateRepository invoiceTemplateRepository
|
InvoiceTemplateRepository invoiceTemplateRepository
|
||||||
) {
|
) {
|
||||||
this.appUserRepository = appUserRepository;
|
this.appUserRepository = appUserRepository;
|
||||||
|
this.templateRepository = templateRepository;
|
||||||
this.invoiceTemplateRepository = invoiceTemplateRepository;
|
this.invoiceTemplateRepository = invoiceTemplateRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InvoiceTemplateResponse currentTemplate(String actorId) {
|
public InvoiceTemplateResponse currentTemplate(String actorId) {
|
||||||
String userId = requireActorId(actorId);
|
String userId = requireActorId(actorId);
|
||||||
requireActiveUser(userId);
|
requireActiveUser(userId);
|
||||||
return invoiceTemplateRepository.findById(userId)
|
return templateRepository.findByUserIdAndType(userId, TEMPLATE_TYPE)
|
||||||
.map(this::toResponse)
|
.map(this::toResponse)
|
||||||
|
.or(() -> invoiceTemplateRepository.findById(userId).map(this::toLegacyResponse))
|
||||||
.orElseGet(() -> new InvoiceTemplateResponse(false, List.of(), null));
|
.orElseGet(() -> new InvoiceTemplateResponse(false, List.of(), null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public InvoiceTemplateResponse saveTemplate(String actorId, List<InvoiceTemplateElementPayload> payloadElements) {
|
public InvoiceTemplateResponse saveTemplate(String actorId, List<InvoiceTemplateElementPayload> payloadElements) {
|
||||||
String userId = requireActorId(actorId);
|
String userId = requireActorId(actorId);
|
||||||
requireActiveUser(userId);
|
requireActiveUser(userId);
|
||||||
InvoiceTemplate existing = invoiceTemplateRepository.findById(userId).orElse(null);
|
Template existing = templateRepository.findByUserIdAndType(userId, TEMPLATE_TYPE).orElse(null);
|
||||||
|
InvoiceTemplate legacyTemplate = existing == null
|
||||||
|
? invoiceTemplateRepository.findById(userId).orElse(null)
|
||||||
|
: null;
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
InvoiceTemplate saved = invoiceTemplateRepository.save(new InvoiceTemplate(
|
Template saved = templateRepository.save(new Template(
|
||||||
|
existing != null ? existing.id() : templateId(userId),
|
||||||
userId,
|
userId,
|
||||||
|
TEMPLATE_TYPE,
|
||||||
sanitizeElements(payloadElements),
|
sanitizeElements(payloadElements),
|
||||||
existing != null ? existing.createdAt() : now,
|
existing != null ? existing.createdAt() : legacyTemplate != null ? legacyTemplate.createdAt() : now,
|
||||||
now
|
now
|
||||||
));
|
));
|
||||||
return toResponse(saved);
|
return toResponse(saved);
|
||||||
@@ -108,11 +123,12 @@ public class InvoiceTemplateService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private InvoiceTemplateResponse toResponse(InvoiceTemplate template) {
|
private InvoiceTemplateResponse toResponse(Template template) {
|
||||||
List<InvoiceTemplateElementPayload> elements = template.elements() == null
|
return toResponse(template.elements(), template.updatedAt());
|
||||||
? List.of()
|
}
|
||||||
: template.elements().stream().map(this::toPayload).toList();
|
|
||||||
return new InvoiceTemplateResponse(true, elements, template.updatedAt());
|
private InvoiceTemplateResponse toLegacyResponse(InvoiceTemplate template) {
|
||||||
|
return toResponse(template.elements(), template.updatedAt());
|
||||||
}
|
}
|
||||||
|
|
||||||
private InvoiceTemplateElementPayload toPayload(InvoiceTemplateElement element) {
|
private InvoiceTemplateElementPayload toPayload(InvoiceTemplateElement element) {
|
||||||
@@ -152,6 +168,17 @@ public class InvoiceTemplateService {
|
|||||||
return value == null ? "" : value;
|
return value == null ? "" : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private InvoiceTemplateResponse toResponse(List<InvoiceTemplateElement> elements, LocalDateTime updatedAt) {
|
||||||
|
List<InvoiceTemplateElementPayload> payloads = elements == null
|
||||||
|
? List.of()
|
||||||
|
: elements.stream().map(this::toPayload).toList();
|
||||||
|
return new InvoiceTemplateResponse(true, payloads, updatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String templateId(String userId) {
|
||||||
|
return userId + ":" + TEMPLATE_TYPE.name().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
public record InvoiceTemplateElementPayload(
|
public record InvoiceTemplateElementPayload(
|
||||||
String id,
|
String id,
|
||||||
String paletteId,
|
String paletteId,
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package de.svencarstensen.muh.service;
|
||||||
|
|
||||||
|
import de.svencarstensen.muh.domain.AppUser;
|
||||||
|
import de.svencarstensen.muh.domain.InvoiceTemplateElement;
|
||||||
|
import de.svencarstensen.muh.domain.ReportTemplate;
|
||||||
|
import de.svencarstensen.muh.domain.Template;
|
||||||
|
import de.svencarstensen.muh.domain.TemplateType;
|
||||||
|
import de.svencarstensen.muh.repository.AppUserRepository;
|
||||||
|
import de.svencarstensen.muh.repository.ReportTemplateRepository;
|
||||||
|
import de.svencarstensen.muh.repository.TemplateRepository;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ReportTemplateService {
|
||||||
|
|
||||||
|
private static final TemplateType TEMPLATE_TYPE = TemplateType.REPORT;
|
||||||
|
|
||||||
|
private final AppUserRepository appUserRepository;
|
||||||
|
private final TemplateRepository templateRepository;
|
||||||
|
private final ReportTemplateRepository reportTemplateRepository;
|
||||||
|
|
||||||
|
public ReportTemplateService(
|
||||||
|
AppUserRepository appUserRepository,
|
||||||
|
TemplateRepository templateRepository,
|
||||||
|
ReportTemplateRepository reportTemplateRepository
|
||||||
|
) {
|
||||||
|
this.appUserRepository = appUserRepository;
|
||||||
|
this.templateRepository = templateRepository;
|
||||||
|
this.reportTemplateRepository = reportTemplateRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvoiceTemplateService.InvoiceTemplateResponse currentTemplate(String actorId) {
|
||||||
|
String userId = requireActorId(actorId);
|
||||||
|
requireActiveUser(userId);
|
||||||
|
return templateRepository.findByUserIdAndType(userId, TEMPLATE_TYPE)
|
||||||
|
.map(this::toResponse)
|
||||||
|
.or(() -> reportTemplateRepository.findById(userId).map(this::toLegacyResponse))
|
||||||
|
.orElseGet(() -> new InvoiceTemplateService.InvoiceTemplateResponse(false, List.of(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvoiceTemplateService.InvoiceTemplateResponse saveTemplate(
|
||||||
|
String actorId,
|
||||||
|
List<InvoiceTemplateService.InvoiceTemplateElementPayload> payloadElements
|
||||||
|
) {
|
||||||
|
String userId = requireActorId(actorId);
|
||||||
|
requireActiveUser(userId);
|
||||||
|
Template existing = templateRepository.findByUserIdAndType(userId, TEMPLATE_TYPE).orElse(null);
|
||||||
|
ReportTemplate legacyTemplate = existing == null
|
||||||
|
? reportTemplateRepository.findById(userId).orElse(null)
|
||||||
|
: null;
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
Template saved = templateRepository.save(new Template(
|
||||||
|
existing != null ? existing.id() : templateId(userId),
|
||||||
|
userId,
|
||||||
|
TEMPLATE_TYPE,
|
||||||
|
sanitizeElements(payloadElements),
|
||||||
|
existing != null ? existing.createdAt() : legacyTemplate != null ? legacyTemplate.createdAt() : now,
|
||||||
|
now
|
||||||
|
));
|
||||||
|
return toResponse(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppUser requireActiveUser(@NonNull String actorId) {
|
||||||
|
return appUserRepository.findById(actorId)
|
||||||
|
.filter(AppUser::active)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN, "Nicht berechtigt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull String requireActorId(String actorId) {
|
||||||
|
if (isBlank(actorId)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nicht berechtigt");
|
||||||
|
}
|
||||||
|
String sanitized = Objects.requireNonNull(actorId).trim();
|
||||||
|
return Objects.requireNonNull(sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<InvoiceTemplateElement> sanitizeElements(
|
||||||
|
List<InvoiceTemplateService.InvoiceTemplateElementPayload> payloadElements
|
||||||
|
) {
|
||||||
|
if (payloadElements == null || payloadElements.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return payloadElements.stream()
|
||||||
|
.map(this::sanitizeElement)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvoiceTemplateElement sanitizeElement(InvoiceTemplateService.InvoiceTemplateElementPayload payload) {
|
||||||
|
if (payload == null
|
||||||
|
|| isBlank(payload.id())
|
||||||
|
|| isBlank(payload.paletteId())
|
||||||
|
|| isBlank(payload.kind())
|
||||||
|
|| payload.x() == null
|
||||||
|
|| payload.y() == null
|
||||||
|
|| payload.width() == null
|
||||||
|
|| payload.fontSize() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InvoiceTemplateElement(
|
||||||
|
payload.id().trim(),
|
||||||
|
payload.paletteId().trim(),
|
||||||
|
payload.kind().trim(),
|
||||||
|
trimOrEmpty(payload.label()),
|
||||||
|
nullToEmpty(payload.content()),
|
||||||
|
payload.x(),
|
||||||
|
payload.y(),
|
||||||
|
payload.width(),
|
||||||
|
payload.height(),
|
||||||
|
payload.fontSize(),
|
||||||
|
payload.fontWeight(),
|
||||||
|
blankToNull(payload.textAlign()),
|
||||||
|
blankToNull(payload.lineOrientation()),
|
||||||
|
blankToNull(payload.imageSrc()),
|
||||||
|
payload.imageNaturalWidth(),
|
||||||
|
payload.imageNaturalHeight()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvoiceTemplateService.InvoiceTemplateResponse toResponse(Template template) {
|
||||||
|
return toResponse(template.elements(), template.updatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvoiceTemplateService.InvoiceTemplateResponse toLegacyResponse(ReportTemplate template) {
|
||||||
|
return toResponse(template.elements(), template.updatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvoiceTemplateService.InvoiceTemplateElementPayload toPayload(InvoiceTemplateElement element) {
|
||||||
|
return new InvoiceTemplateService.InvoiceTemplateElementPayload(
|
||||||
|
element.id(),
|
||||||
|
element.paletteId(),
|
||||||
|
element.kind(),
|
||||||
|
element.label(),
|
||||||
|
element.content(),
|
||||||
|
element.x(),
|
||||||
|
element.y(),
|
||||||
|
element.width(),
|
||||||
|
element.height(),
|
||||||
|
element.fontSize(),
|
||||||
|
element.fontWeight(),
|
||||||
|
element.textAlign(),
|
||||||
|
element.lineOrientation(),
|
||||||
|
element.imageSrc(),
|
||||||
|
element.imageNaturalWidth(),
|
||||||
|
element.imageNaturalHeight()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBlank(String value) {
|
||||||
|
return value == null || value.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String blankToNull(String value) {
|
||||||
|
return isBlank(value) ? null : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimOrEmpty(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nullToEmpty(String value) {
|
||||||
|
return value == null ? "" : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvoiceTemplateService.InvoiceTemplateResponse toResponse(
|
||||||
|
List<InvoiceTemplateElement> elements,
|
||||||
|
LocalDateTime updatedAt
|
||||||
|
) {
|
||||||
|
List<InvoiceTemplateService.InvoiceTemplateElementPayload> payloads = elements == null
|
||||||
|
? List.of()
|
||||||
|
: elements.stream().map(this::toPayload).toList();
|
||||||
|
return new InvoiceTemplateService.InvoiceTemplateResponse(true, payloads, updatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String templateId(String userId) {
|
||||||
|
return userId + ":" + TEMPLATE_TYPE.name().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package de.svencarstensen.muh.web;
|
|||||||
|
|
||||||
import de.svencarstensen.muh.service.CatalogService;
|
import de.svencarstensen.muh.service.CatalogService;
|
||||||
import de.svencarstensen.muh.service.InvoiceTemplateService;
|
import de.svencarstensen.muh.service.InvoiceTemplateService;
|
||||||
|
import de.svencarstensen.muh.service.ReportTemplateService;
|
||||||
import de.svencarstensen.muh.security.SecuritySupport;
|
import de.svencarstensen.muh.security.SecuritySupport;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -19,15 +20,18 @@ public class SessionController {
|
|||||||
|
|
||||||
private final CatalogService catalogService;
|
private final CatalogService catalogService;
|
||||||
private final InvoiceTemplateService invoiceTemplateService;
|
private final InvoiceTemplateService invoiceTemplateService;
|
||||||
|
private final ReportTemplateService reportTemplateService;
|
||||||
private final SecuritySupport securitySupport;
|
private final SecuritySupport securitySupport;
|
||||||
|
|
||||||
public SessionController(
|
public SessionController(
|
||||||
CatalogService catalogService,
|
CatalogService catalogService,
|
||||||
InvoiceTemplateService invoiceTemplateService,
|
InvoiceTemplateService invoiceTemplateService,
|
||||||
|
ReportTemplateService reportTemplateService,
|
||||||
SecuritySupport securitySupport
|
SecuritySupport securitySupport
|
||||||
) {
|
) {
|
||||||
this.catalogService = catalogService;
|
this.catalogService = catalogService;
|
||||||
this.invoiceTemplateService = invoiceTemplateService;
|
this.invoiceTemplateService = invoiceTemplateService;
|
||||||
|
this.reportTemplateService = reportTemplateService;
|
||||||
this.securitySupport = securitySupport;
|
this.securitySupport = securitySupport;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +45,11 @@ public class SessionController {
|
|||||||
return invoiceTemplateService.currentTemplate(securitySupport.currentUser().id());
|
return invoiceTemplateService.currentTemplate(securitySupport.currentUser().id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/report-template")
|
||||||
|
public InvoiceTemplateService.InvoiceTemplateResponse currentReportTemplate() {
|
||||||
|
return reportTemplateService.currentTemplate(securitySupport.currentUser().id());
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/password-login")
|
@PostMapping("/password-login")
|
||||||
public CatalogService.SessionResponse passwordLogin(@RequestBody PasswordLoginRequest request) {
|
public CatalogService.SessionResponse passwordLogin(@RequestBody PasswordLoginRequest request) {
|
||||||
return catalogService.loginWithPassword(request.email(), request.password());
|
return catalogService.loginWithPassword(request.email(), request.password());
|
||||||
@@ -62,7 +71,7 @@ public class SessionController {
|
|||||||
|
|
||||||
@PutMapping("/invoice-template")
|
@PutMapping("/invoice-template")
|
||||||
public InvoiceTemplateService.InvoiceTemplateResponse saveInvoiceTemplate(
|
public InvoiceTemplateService.InvoiceTemplateResponse saveInvoiceTemplate(
|
||||||
@RequestBody InvoiceTemplateRequest request
|
@RequestBody TemplateRequest request
|
||||||
) {
|
) {
|
||||||
return invoiceTemplateService.saveTemplate(
|
return invoiceTemplateService.saveTemplate(
|
||||||
securitySupport.currentUser().id(),
|
securitySupport.currentUser().id(),
|
||||||
@@ -70,6 +79,16 @@ public class SessionController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/report-template")
|
||||||
|
public InvoiceTemplateService.InvoiceTemplateResponse saveReportTemplate(
|
||||||
|
@RequestBody TemplateRequest request
|
||||||
|
) {
|
||||||
|
return reportTemplateService.saveTemplate(
|
||||||
|
securitySupport.currentUser().id(),
|
||||||
|
request.elements()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public record PasswordLoginRequest(@NotBlank String email, @NotBlank String password) {
|
public record PasswordLoginRequest(@NotBlank String email, @NotBlank String password) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +104,7 @@ public class SessionController {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record InvoiceTemplateRequest(
|
public record TemplateRequest(
|
||||||
List<InvoiceTemplateService.InvoiceTemplateElementPayload> elements
|
List<InvoiceTemplateService.InvoiceTemplateElementPayload> elements
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import SearchFarmerPage from "./pages/SearchFarmerPage";
|
|||||||
import SearchCalendarPage from "./pages/SearchCalendarPage";
|
import SearchCalendarPage from "./pages/SearchCalendarPage";
|
||||||
import UserManagementPage from "./pages/UserManagementPage";
|
import UserManagementPage from "./pages/UserManagementPage";
|
||||||
import InvoiceTemplatePage from "./pages/InvoiceTemplatePage";
|
import InvoiceTemplatePage from "./pages/InvoiceTemplatePage";
|
||||||
|
import ReportTemplatePage from "./pages/ReportTemplatePage";
|
||||||
|
|
||||||
function ProtectedRoutes() {
|
function ProtectedRoutes() {
|
||||||
const { user, ready } = useSession();
|
const { user, ready } = useSession();
|
||||||
@@ -37,6 +38,7 @@ function ProtectedRoutes() {
|
|||||||
<Route path="/samples/:sampleId/antibiogram" element={<AntibiogramPage />} />
|
<Route path="/samples/:sampleId/antibiogram" element={<AntibiogramPage />} />
|
||||||
<Route path="/samples/:sampleId/therapy" element={<TherapyPage />} />
|
<Route path="/samples/:sampleId/therapy" element={<TherapyPage />} />
|
||||||
<Route path="/invoice-template" element={<InvoiceTemplatePage />} />
|
<Route path="/invoice-template" element={<InvoiceTemplatePage />} />
|
||||||
|
<Route path="/report-template" element={<ReportTemplatePage />} />
|
||||||
<Route path="/admin" element={<Navigate to={isAdmin ? "/admin/landwirte" : "/admin/benutzer"} replace />} />
|
<Route path="/admin" element={<Navigate to={isAdmin ? "/admin/landwirte" : "/admin/benutzer"} replace />} />
|
||||||
<Route path="/admin/benutzer" element={<UserManagementPage />} />
|
<Route path="/admin/benutzer" element={<UserManagementPage />} />
|
||||||
<Route path="/admin/landwirte" element={<AdministrationPage />} />
|
<Route path="/admin/landwirte" element={<AdministrationPage />} />
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ const PAGE_TITLES: Record<string, string> = {
|
|||||||
"/home": "Startseite",
|
"/home": "Startseite",
|
||||||
"/samples/new": "Neuanlage einer Probe",
|
"/samples/new": "Neuanlage einer Probe",
|
||||||
"/portal": "MUH-Portal",
|
"/portal": "MUH-Portal",
|
||||||
"/invoice-template": "Rechnungstemplate",
|
"/invoice-template": "Rechnung",
|
||||||
|
"/report-template": "Bericht",
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolvePageTitle(pathname: string) {
|
function resolvePageTitle(pathname: string) {
|
||||||
@@ -80,9 +81,17 @@ export default function AppShell() {
|
|||||||
<div className="nav-group">
|
<div className="nav-group">
|
||||||
<div className="nav-group__label">Verwaltung</div>
|
<div className="nav-group__label">Verwaltung</div>
|
||||||
<div className="nav-subnav">
|
<div className="nav-subnav">
|
||||||
|
<div className="nav-subgroup">
|
||||||
|
<div className="nav-subgroup__label">Vorlagen</div>
|
||||||
|
<div className="nav-subnav nav-subnav--nested">
|
||||||
<NavLink to="/invoice-template" className={({ isActive }) => `nav-sublink ${isActive ? "is-active" : ""}`}>
|
<NavLink to="/invoice-template" className={({ isActive }) => `nav-sublink ${isActive ? "is-active" : ""}`}>
|
||||||
Rechnungstemplate
|
Rechnung
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink to="/report-template" className={({ isActive }) => `nav-sublink ${isActive ? "is-active" : ""}`}>
|
||||||
|
Bericht
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<NavLink to="/admin/landwirte" className={({ isActive }) => `nav-sublink ${isActive ? "is-active" : ""}`}>
|
<NavLink to="/admin/landwirte" className={({ isActive }) => `nav-sublink ${isActive ? "is-active" : ""}`}>
|
||||||
Landwirte
|
Landwirte
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const MIN_LINE_THICKNESS = 1;
|
|||||||
const MIN_LINE_LENGTH = 40;
|
const MIN_LINE_LENGTH = 40;
|
||||||
const MIN_FONT_SIZE = 12;
|
const MIN_FONT_SIZE = 12;
|
||||||
const MAX_FONT_SIZE = 44;
|
const MAX_FONT_SIZE = 44;
|
||||||
const LOCKED_TEXT_PALETTE_IDS = new Set([
|
const INVOICE_LOCKED_TEXT_PALETTE_IDS = new Set([
|
||||||
"invoice-title",
|
"invoice-title",
|
||||||
"company-name",
|
"company-name",
|
||||||
"contact-person",
|
"contact-person",
|
||||||
@@ -50,12 +50,39 @@ const LOCKED_TEXT_PALETTE_IDS = new Set([
|
|||||||
"bic",
|
"bic",
|
||||||
"footer",
|
"footer",
|
||||||
]);
|
]);
|
||||||
|
const REPORT_LOCKED_TEXT_PALETTE_IDS = new Set([
|
||||||
|
"report-title",
|
||||||
|
"report-farmer",
|
||||||
|
"report-cow",
|
||||||
|
"report-clinical-findings",
|
||||||
|
"report-treatment",
|
||||||
|
"report-examination-start",
|
||||||
|
"report-examination-end",
|
||||||
|
"report-sample-number",
|
||||||
|
"report-date",
|
||||||
|
"report-quarter-vl-label",
|
||||||
|
"report-quarter-vr-label",
|
||||||
|
"report-quarter-hl-label",
|
||||||
|
"report-quarter-hr-label",
|
||||||
|
"report-quarter-vl-result",
|
||||||
|
"report-quarter-vr-result",
|
||||||
|
"report-quarter-hl-result",
|
||||||
|
"report-quarter-hr-result",
|
||||||
|
"report-antibiogram-heading",
|
||||||
|
"report-antibiogram-summary",
|
||||||
|
"report-antibiogram-details",
|
||||||
|
"report-therapy-heading",
|
||||||
|
"report-therapy-text",
|
||||||
|
"report-misc-heading",
|
||||||
|
"report-misc-note",
|
||||||
|
"report-lab-note",
|
||||||
|
]);
|
||||||
|
|
||||||
type ElementKind = "text" | "line" | "image";
|
type ElementKind = "text" | "line" | "image";
|
||||||
type LineOrientation = "horizontal" | "vertical";
|
type LineOrientation = "horizontal" | "vertical";
|
||||||
type TextAlign = "left" | "center" | "right";
|
type TextAlign = "left" | "center" | "right";
|
||||||
type FontWeight = 400 | 500 | 600 | 700;
|
type FontWeight = 400 | 500 | 600 | 700;
|
||||||
type PaletteCategory = "master-data" | "customer-data" | "free-elements";
|
type PaletteCategory = string;
|
||||||
|
|
||||||
interface PaletteItem {
|
interface PaletteItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -118,6 +145,17 @@ interface InvoiceTemplateResponse {
|
|||||||
updatedAt: string | null;
|
updatedAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InvoiceTemplatePageProps {
|
||||||
|
buildStarterLayout?: (user: UserOption | null, paletteItems: PaletteItem[]) => TemplateElement[];
|
||||||
|
lockedTextPaletteIds?: ReadonlySet<string>;
|
||||||
|
paletteGroups?: Array<{ category: PaletteCategory; title: string }>;
|
||||||
|
paletteItems?: PaletteItem[];
|
||||||
|
pdfDownloadName?: string;
|
||||||
|
pdfPreviewTitle?: string;
|
||||||
|
templateApiPath?: string;
|
||||||
|
templateTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
type DragPayload =
|
type DragPayload =
|
||||||
| {
|
| {
|
||||||
kind: "palette";
|
kind: "palette";
|
||||||
@@ -158,7 +196,7 @@ const CP1252_MAP: Record<number, number> = {
|
|||||||
8482: 153,
|
8482: 153,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PALETTE_ITEMS: PaletteItem[] = [
|
const INVOICE_PALETTE_ITEMS: PaletteItem[] = [
|
||||||
{
|
{
|
||||||
id: "invoice-title",
|
id: "invoice-title",
|
||||||
category: "master-data",
|
category: "master-data",
|
||||||
@@ -459,12 +497,338 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const PALETTE_GROUPS: Array<{ category: PaletteCategory; title: string }> = [
|
const INVOICE_PALETTE_GROUPS: Array<{ category: PaletteCategory; title: string }> = [
|
||||||
{ category: "master-data", title: "Stammdaten" },
|
{ category: "master-data", title: "Stammdaten" },
|
||||||
{ category: "customer-data", title: "Kundendaten" },
|
{ category: "customer-data", title: "Kundendaten" },
|
||||||
{ category: "free-elements", title: "Freie Elemente" },
|
{ category: "free-elements", title: "Freie Elemente" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const REPORT_PALETTE_ITEMS: PaletteItem[] = [
|
||||||
|
{
|
||||||
|
id: "report-title",
|
||||||
|
category: "report-header",
|
||||||
|
label: "Berichtstitel",
|
||||||
|
description: "Titel der Probenauswertung",
|
||||||
|
width: 330,
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Milchprobenauswertung",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-sample-number",
|
||||||
|
category: "report-header",
|
||||||
|
label: "Probe",
|
||||||
|
description: "Probenummer",
|
||||||
|
width: 120,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Probe: 10307",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-date",
|
||||||
|
category: "report-header",
|
||||||
|
label: "Datum",
|
||||||
|
description: "Berichtsdatum",
|
||||||
|
width: 120,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => `Datum:\n${new Intl.DateTimeFormat("de-DE", { dateStyle: "medium" }).format(new Date())}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-farmer",
|
||||||
|
category: "report-data",
|
||||||
|
label: "Landwirt",
|
||||||
|
description: "Name des Landwirts",
|
||||||
|
width: 270,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Landwirt: VoS, Dirk Schwissel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-cow",
|
||||||
|
category: "report-data",
|
||||||
|
label: "Kuh",
|
||||||
|
description: "Kuh- oder Tiernummer",
|
||||||
|
width: 220,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Kuh: 36",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-clinical-findings",
|
||||||
|
category: "report-data",
|
||||||
|
label: "Klinische Untersuchung",
|
||||||
|
description: "Klinischer Befund",
|
||||||
|
width: 320,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Klinische Untersuchung:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-treatment",
|
||||||
|
category: "report-data",
|
||||||
|
label: "Vorbehandelt",
|
||||||
|
description: "Vorbehandlung oder Medikation",
|
||||||
|
width: 340,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Vorbehandelt mit: Ubrolexin; Betamox",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-examination-start",
|
||||||
|
category: "report-data",
|
||||||
|
label: "Untersuchungsbeginn",
|
||||||
|
description: "Startdatum der Untersuchung",
|
||||||
|
width: 280,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Untersuchungsbeginn: 11.03.2026",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-examination-end",
|
||||||
|
category: "report-data",
|
||||||
|
label: "Untersuchungsende",
|
||||||
|
description: "Enddatum der Untersuchung",
|
||||||
|
width: 260,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Untersuchungsende: 13.03.2026",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-quarter-vl-label",
|
||||||
|
category: "report-findings",
|
||||||
|
label: "VL",
|
||||||
|
description: "Linkes Vorderviertel",
|
||||||
|
width: 32,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: "center",
|
||||||
|
defaultContent: () => "VL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-quarter-vr-label",
|
||||||
|
category: "report-findings",
|
||||||
|
label: "VR",
|
||||||
|
description: "Rechtes Vorderviertel",
|
||||||
|
width: 32,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: "center",
|
||||||
|
defaultContent: () => "VR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-quarter-hl-label",
|
||||||
|
category: "report-findings",
|
||||||
|
label: "HL",
|
||||||
|
description: "Linkes Hinterviertel",
|
||||||
|
width: 32,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: "center",
|
||||||
|
defaultContent: () => "HL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-quarter-hr-label",
|
||||||
|
category: "report-findings",
|
||||||
|
label: "HR",
|
||||||
|
description: "Rechtes Hinterviertel",
|
||||||
|
width: 32,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: "center",
|
||||||
|
defaultContent: () => "HR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-quarter-vl-result",
|
||||||
|
category: "report-findings",
|
||||||
|
label: "Befund VL",
|
||||||
|
description: "Befundtext fuer VL",
|
||||||
|
width: 120,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "kein bakt.\nWachstum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-quarter-vr-result",
|
||||||
|
category: "report-findings",
|
||||||
|
label: "Befund VR",
|
||||||
|
description: "Befundtext fuer VR",
|
||||||
|
width: 120,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "KNS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-quarter-hl-result",
|
||||||
|
category: "report-findings",
|
||||||
|
label: "Befund HL",
|
||||||
|
description: "Befundtext fuer HL",
|
||||||
|
width: 120,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "kein bakt.\nWachstum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-quarter-hr-result",
|
||||||
|
category: "report-findings",
|
||||||
|
label: "Befund HR",
|
||||||
|
description: "Befundtext fuer HR",
|
||||||
|
width: 120,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "kein bakt.\nWachstum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-antibiogram-heading",
|
||||||
|
category: "report-sections",
|
||||||
|
label: "Antibiogramm Titel",
|
||||||
|
description: "Ueberschrift des Antibiogramms",
|
||||||
|
width: 170,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Antibiogramm 4/4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-antibiogram-summary",
|
||||||
|
category: "report-sections",
|
||||||
|
label: "Antibiogramm Ergebnis",
|
||||||
|
description: "Kurzfassung zum Viertel",
|
||||||
|
width: 520,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 600,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "VR (sensibel): Penicillin, Cefalexin ...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-antibiogram-details",
|
||||||
|
category: "report-sections",
|
||||||
|
label: "Antibiogramm Details",
|
||||||
|
description: "Ausfuehrliche Antibiogramm-Liste",
|
||||||
|
width: 610,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () =>
|
||||||
|
"Cefquinom, Enrofloxacin, Sulfadoxin Trimethoprim, Amoxicillin Clavulansaeure, Oxacillin, Ampicillin, Sulfadoxin Trimethoprim",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-therapy-heading",
|
||||||
|
category: "report-sections",
|
||||||
|
label: "Empfehlung/Therapie",
|
||||||
|
description: "Ueberschrift der Therapieempfehlung",
|
||||||
|
width: 230,
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Empfehlung/Therapie:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-therapy-text",
|
||||||
|
category: "report-sections",
|
||||||
|
label: "Therapietext",
|
||||||
|
description: "Empfehlungstext",
|
||||||
|
width: 400,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "weiter wie begonnen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-misc-heading",
|
||||||
|
category: "report-sections",
|
||||||
|
label: "Sonstiges",
|
||||||
|
description: "Ueberschrift fuer Hinweise",
|
||||||
|
width: 140,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Sonstiges",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-misc-note",
|
||||||
|
category: "report-sections",
|
||||||
|
label: "Hinweis",
|
||||||
|
description: "Allgemeiner Hinweistext",
|
||||||
|
width: 620,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () =>
|
||||||
|
"· Bei Rueckfragen setzen Sie sich bitte mit Ihrem behandelnden Tierarzt in Verbindung.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-lab-note",
|
||||||
|
category: "report-sections",
|
||||||
|
label: "Laborvermerk",
|
||||||
|
description: "Interner Vermerk",
|
||||||
|
width: 220,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Interner Vermerk: Labor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "free-text",
|
||||||
|
category: "free-elements",
|
||||||
|
label: "Freitext",
|
||||||
|
description: "Beliebig editierbarer Text",
|
||||||
|
width: 220,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Freitext",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "line",
|
||||||
|
category: "free-elements",
|
||||||
|
label: "Linie",
|
||||||
|
description: "Linie mit umschaltbarer Ausrichtung",
|
||||||
|
kind: "line",
|
||||||
|
width: 260,
|
||||||
|
height: 3,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
lineOrientation: "horizontal",
|
||||||
|
defaultContent: () => "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "image",
|
||||||
|
category: "free-elements",
|
||||||
|
label: "Bild",
|
||||||
|
description: "Logo, Kopfgrafik oder Gestaltungselement",
|
||||||
|
kind: "image",
|
||||||
|
width: 180,
|
||||||
|
height: 120,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 400,
|
||||||
|
textAlign: "left",
|
||||||
|
defaultContent: () => "Bild",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const REPORT_PALETTE_GROUPS: Array<{ category: PaletteCategory; title: string }> = [
|
||||||
|
{ category: "report-header", title: "Kopfbereich" },
|
||||||
|
{ category: "report-data", title: "Probendaten" },
|
||||||
|
{ category: "report-findings", title: "Befunde" },
|
||||||
|
{ category: "report-sections", title: "Abschnitte" },
|
||||||
|
{ category: "free-elements", title: "Freie Elemente" },
|
||||||
|
];
|
||||||
|
|
||||||
function clamp(value: number, minimum: number, maximum: number) {
|
function clamp(value: number, minimum: number, maximum: number) {
|
||||||
return Math.min(Math.max(value, minimum), maximum);
|
return Math.min(Math.max(value, minimum), maximum);
|
||||||
}
|
}
|
||||||
@@ -915,12 +1279,12 @@ function normalizeElement(element: TemplateElement): TemplateElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPaletteItem(paletteId: string) {
|
function findPaletteItem(paletteItems: PaletteItem[], paletteId: string) {
|
||||||
return PALETTE_ITEMS.find((entry) => entry.id === paletteId) ?? null;
|
return paletteItems.find((entry) => entry.id === paletteId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function requirePaletteItem(paletteId: string) {
|
function requirePaletteItem(paletteItems: PaletteItem[], paletteId: string) {
|
||||||
const paletteItem = findPaletteItem(paletteId);
|
const paletteItem = findPaletteItem(paletteItems, paletteId);
|
||||||
if (!paletteItem) {
|
if (!paletteItem) {
|
||||||
throw new Error(`Palette item ${paletteId} is not configured.`);
|
throw new Error(`Palette item ${paletteId} is not configured.`);
|
||||||
}
|
}
|
||||||
@@ -937,8 +1301,11 @@ function getPalettePreviewText(paletteItem: PaletteItem, user: UserOption | null
|
|||||||
return paletteItem.defaultContent(user);
|
return paletteItem.defaultContent(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isElementContentEditable(element: Pick<TemplateElement, "kind" | "paletteId">) {
|
function isElementContentEditable(
|
||||||
return element.kind === "text" && !LOCKED_TEXT_PALETTE_IDS.has(element.paletteId);
|
element: Pick<TemplateElement, "kind" | "paletteId">,
|
||||||
|
lockedTextPaletteIds: ReadonlySet<string>,
|
||||||
|
) {
|
||||||
|
return element.kind === "text" && !lockedTextPaletteIds.has(element.paletteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createElementFromPalette(
|
function createElementFromPalette(
|
||||||
@@ -966,18 +1333,66 @@ function createElementFromPalette(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStarterLayout(user: UserOption | null) {
|
function createInvoiceStarterLayout(user: UserOption | null, paletteItems: PaletteItem[]) {
|
||||||
return [
|
return [
|
||||||
createElementFromPalette(requirePaletteItem("company-name"), user, { x: 56, y: 56 }),
|
createElementFromPalette(requirePaletteItem(paletteItems, "company-name"), user, { x: 56, y: 56 }),
|
||||||
createElementFromPalette(requirePaletteItem("street"), user, { x: 56, y: 104 }),
|
createElementFromPalette(requirePaletteItem(paletteItems, "street"), user, { x: 56, y: 104 }),
|
||||||
createElementFromPalette(requirePaletteItem("house-number"), user, { x: 288, y: 104 }),
|
createElementFromPalette(requirePaletteItem(paletteItems, "house-number"), user, { x: 288, y: 104 }),
|
||||||
createElementFromPalette(requirePaletteItem("postal-code"), user, { x: 56, y: 132 }),
|
createElementFromPalette(requirePaletteItem(paletteItems, "postal-code"), user, { x: 56, y: 132 }),
|
||||||
createElementFromPalette(requirePaletteItem("city"), user, { x: 164, y: 132 }),
|
createElementFromPalette(requirePaletteItem(paletteItems, "city"), user, { x: 164, y: 132 }),
|
||||||
createElementFromPalette(requirePaletteItem("invoice-title"), user, { x: 56, y: 238 }),
|
createElementFromPalette(requirePaletteItem(paletteItems, "invoice-title"), user, { x: 56, y: 238 }),
|
||||||
createElementFromPalette(requirePaletteItem("invoice-date"), user, { x: 538, y: 64 }),
|
createElementFromPalette(requirePaletteItem(paletteItems, "invoice-date"), user, { x: 538, y: 64 }),
|
||||||
createElementFromPalette(requirePaletteItem("invoice-number"), user, { x: 538, y: 96 }),
|
createElementFromPalette(requirePaletteItem(paletteItems, "invoice-number"), user, { x: 538, y: 96 }),
|
||||||
createElementFromPalette(requirePaletteItem("amount"), user, { x: 558, y: 432 }),
|
createElementFromPalette(requirePaletteItem(paletteItems, "amount"), user, { x: 558, y: 432 }),
|
||||||
createElementFromPalette(requirePaletteItem("footer"), user, { x: 56, y: 1008 }),
|
createElementFromPalette(requirePaletteItem(paletteItems, "footer"), user, { x: 56, y: 1008 }),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createReportStarterLayout(user: UserOption | null, paletteItems: PaletteItem[]) {
|
||||||
|
const horizontalLine = (x: number, y: number, width: number) =>
|
||||||
|
normalizeElement({
|
||||||
|
...createElementFromPalette(requirePaletteItem(paletteItems, "line"), user, { x, y }),
|
||||||
|
width,
|
||||||
|
});
|
||||||
|
const verticalLine = (x: number, y: number, height: number) =>
|
||||||
|
normalizeElement({
|
||||||
|
...createElementFromPalette(requirePaletteItem(paletteItems, "line"), user, { x, y }),
|
||||||
|
height,
|
||||||
|
lineOrientation: "vertical",
|
||||||
|
width: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-title"), user, { x: 56, y: 108 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-farmer"), user, { x: 56, y: 154 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-cow"), user, { x: 56, y: 186 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-clinical-findings"), user, { x: 56, y: 218 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-treatment"), user, { x: 56, y: 250 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-examination-start"), user, { x: 56, y: 282 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-examination-end"), user, { x: 56, y: 314 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-sample-number"), user, { x: 430, y: 108 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-date"), user, { x: 572, y: 108 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-quarter-vl-label"), user, { x: 538, y: 206 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-quarter-vr-label"), user, { x: 628, y: 206 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-quarter-hl-label"), user, { x: 538, y: 236 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-quarter-hr-label"), user, { x: 628, y: 236 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-quarter-vl-result"), user, { x: 430, y: 168 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-quarter-vr-result"), user, { x: 618, y: 184 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-quarter-hl-result"), user, { x: 430, y: 272 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-quarter-hr-result"), user, { x: 618, y: 272 }),
|
||||||
|
horizontalLine(520, 254, 170),
|
||||||
|
verticalLine(606, 196, 106),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-antibiogram-heading"), user, { x: 56, y: 426 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-antibiogram-summary"), user, { x: 56, y: 458 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-antibiogram-details"), user, { x: 56, y: 490 }),
|
||||||
|
horizontalLine(56, 396, 646),
|
||||||
|
horizontalLine(56, 548, 646),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-therapy-heading"), user, { x: 56, y: 574 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-therapy-text"), user, { x: 292, y: 574 }),
|
||||||
|
horizontalLine(56, 628, 646),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-misc-heading"), user, { x: 56, y: 656 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-misc-note"), user, { x: 56, y: 702 }),
|
||||||
|
createElementFromPalette(requirePaletteItem(paletteItems, "report-lab-note"), user, { x: 56, y: 754 }),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1130,7 +1545,16 @@ function readDragPayload(event: DragEvent<HTMLElement>): DragPayload | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InvoiceTemplatePage() {
|
export default function InvoiceTemplatePage({
|
||||||
|
buildStarterLayout = createInvoiceStarterLayout,
|
||||||
|
lockedTextPaletteIds = INVOICE_LOCKED_TEXT_PALETTE_IDS,
|
||||||
|
paletteGroups = INVOICE_PALETTE_GROUPS,
|
||||||
|
paletteItems = INVOICE_PALETTE_ITEMS,
|
||||||
|
pdfDownloadName = "rechnung-template.pdf",
|
||||||
|
pdfPreviewTitle = "PDF-Vorschau Rechnungsvorlage",
|
||||||
|
templateApiPath = "/session/invoice-template",
|
||||||
|
templateTitle = "Rechnungsvorlage",
|
||||||
|
}: InvoiceTemplatePageProps) {
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
const pageRef = useRef<HTMLDivElement | null>(null);
|
const pageRef = useRef<HTMLDivElement | null>(null);
|
||||||
const cardRef = useRef<HTMLElement | null>(null);
|
const cardRef = useRef<HTMLElement | null>(null);
|
||||||
@@ -1140,7 +1564,9 @@ export default function InvoiceTemplatePage() {
|
|||||||
const mouseDragCleanupRef = useRef<(() => void) | null>(null);
|
const mouseDragCleanupRef = useRef<(() => void) | null>(null);
|
||||||
const moveOffsetRef = useRef<{ x: number; y: number } | null>(null);
|
const moveOffsetRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
|
|
||||||
const [elements, setElements] = useState<TemplateElement[]>(() => createStarterLayout(user));
|
const [elements, setElements] = useState<TemplateElement[]>(() =>
|
||||||
|
buildStarterLayout(user, paletteItems),
|
||||||
|
);
|
||||||
const [selectedElementId, setSelectedElementId] = useState<string | null>(null);
|
const [selectedElementId, setSelectedElementId] = useState<string | null>(null);
|
||||||
const [isCanvasActive, setIsCanvasActive] = useState(false);
|
const [isCanvasActive, setIsCanvasActive] = useState(false);
|
||||||
const [draggingElementId, setDraggingElementId] = useState<string | null>(null);
|
const [draggingElementId, setDraggingElementId] = useState<string | null>(null);
|
||||||
@@ -1163,7 +1589,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
const selectedElement = elements.find((entry) => entry.id === selectedElementId) ?? null;
|
const selectedElement = elements.find((entry) => entry.id === selectedElementId) ?? null;
|
||||||
const selectedElementHeight = selectedElement ? getElementHeight(selectedElement) : null;
|
const selectedElementHeight = selectedElement ? getElementHeight(selectedElement) : null;
|
||||||
const selectedElementContentEditable = selectedElement
|
const selectedElementContentEditable = selectedElement
|
||||||
? isElementContentEditable(selectedElement)
|
? isElementContentEditable(selectedElement, lockedTextPaletteIds)
|
||||||
: false;
|
: false;
|
||||||
const selectedElementMinimumWidth = selectedElement
|
const selectedElementMinimumWidth = selectedElement
|
||||||
? getMinimumWidthForElement(selectedElement.kind, selectedElement.lineOrientation)
|
? getMinimumWidthForElement(selectedElement.kind, selectedElement.lineOrientation)
|
||||||
@@ -1176,10 +1602,11 @@ export default function InvoiceTemplatePage() {
|
|||||||
)
|
)
|
||||||
: MIN_MEDIA_HEIGHT;
|
: MIN_MEDIA_HEIGHT;
|
||||||
const templateTimestampLabel = formatTemplateTimestamp(templateUpdatedAt);
|
const templateTimestampLabel = formatTemplateTimestamp(templateUpdatedAt);
|
||||||
|
const emptyCanvasMessage = `Ziehen Sie links ein Element auf diese Fläche, um Ihre ${templateTitle} zu starten.`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const starterLayout = createStarterLayout(null);
|
const starterLayout = buildStarterLayout(null, paletteItems);
|
||||||
setElements(starterLayout);
|
setElements(starterLayout);
|
||||||
setSelectedElementId(starterLayout[0]?.id ?? null);
|
setSelectedElementId(starterLayout[0]?.id ?? null);
|
||||||
setResizingElementId(null);
|
setResizingElementId(null);
|
||||||
@@ -1194,7 +1621,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
setTemplateError(null);
|
setTemplateError(null);
|
||||||
setIsTemplateLoading(true);
|
setIsTemplateLoading(true);
|
||||||
|
|
||||||
void apiGet<InvoiceTemplateResponse>("/session/invoice-template")
|
void apiGet<InvoiceTemplateResponse>(templateApiPath)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return;
|
return;
|
||||||
@@ -1203,7 +1630,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
setIsTemplateApiAvailable(true);
|
setIsTemplateApiAvailable(true);
|
||||||
setTemplateUpdatedAt(response.updatedAt);
|
setTemplateUpdatedAt(response.updatedAt);
|
||||||
if (!response.stored) {
|
if (!response.stored) {
|
||||||
const starterLayout = createStarterLayout(user);
|
const starterLayout = buildStarterLayout(user, paletteItems);
|
||||||
setElements(starterLayout);
|
setElements(starterLayout);
|
||||||
setSelectedElementId(starterLayout[0]?.id ?? null);
|
setSelectedElementId(starterLayout[0]?.id ?? null);
|
||||||
setResizingElementId(null);
|
setResizingElementId(null);
|
||||||
@@ -1222,7 +1649,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
if (error instanceof ApiError && error.status === 404) {
|
if (error instanceof ApiError && error.status === 404) {
|
||||||
const starterLayout = createStarterLayout(user);
|
const starterLayout = buildStarterLayout(user, paletteItems);
|
||||||
setElements(starterLayout);
|
setElements(starterLayout);
|
||||||
setSelectedElementId(starterLayout[0]?.id ?? null);
|
setSelectedElementId(starterLayout[0]?.id ?? null);
|
||||||
setResizingElementId(null);
|
setResizingElementId(null);
|
||||||
@@ -1243,7 +1670,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [user]);
|
}, [buildStarterLayout, paletteItems, templateApiPath, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedElementId && elements.some((entry) => entry.id === selectedElementId)) {
|
if (selectedElementId && elements.some((entry) => entry.id === selectedElementId)) {
|
||||||
@@ -1390,7 +1817,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
entry.id === elementId
|
entry.id === elementId
|
||||||
? normalizeElement({
|
? normalizeElement({
|
||||||
...entry,
|
...entry,
|
||||||
...(!isElementContentEditable(entry) &&
|
...(!isElementContentEditable(entry, lockedTextPaletteIds) &&
|
||||||
Object.prototype.hasOwnProperty.call(patch, "content")
|
Object.prototype.hasOwnProperty.call(patch, "content")
|
||||||
? { ...patch, content: entry.content }
|
? { ...patch, content: entry.content }
|
||||||
: patch),
|
: patch),
|
||||||
@@ -1408,7 +1835,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function placePaletteElement(paletteId: string, clientX: number, clientY: number) {
|
function placePaletteElement(paletteId: string, clientX: number, clientY: number) {
|
||||||
const paletteItem = findPaletteItem(paletteId);
|
const paletteItem = findPaletteItem(paletteItems, paletteId);
|
||||||
const canvasElement = canvasRef.current;
|
const canvasElement = canvasRef.current;
|
||||||
if (!paletteItem || !canvasElement) {
|
if (!paletteItem || !canvasElement) {
|
||||||
return;
|
return;
|
||||||
@@ -1481,7 +1908,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
|
|
||||||
function handleResetToStarter() {
|
function handleResetToStarter() {
|
||||||
mouseDragCleanupRef.current?.();
|
mouseDragCleanupRef.current?.();
|
||||||
const starterLayout = createStarterLayout(user);
|
const starterLayout = buildStarterLayout(user, paletteItems);
|
||||||
setElements(starterLayout);
|
setElements(starterLayout);
|
||||||
setSelectedElementId(starterLayout[0]?.id ?? null);
|
setSelectedElementId(starterLayout[0]?.id ?? null);
|
||||||
setResizingElementId(null);
|
setResizingElementId(null);
|
||||||
@@ -1523,7 +1950,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
setIsTemplateSaving(true);
|
setIsTemplateSaving(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiPut<InvoiceTemplateResponse>("/session/invoice-template", {
|
const response = await apiPut<InvoiceTemplateResponse>(templateApiPath, {
|
||||||
elements,
|
elements,
|
||||||
});
|
});
|
||||||
const savedElements = normalizeTemplateElements(response.elements);
|
const savedElements = normalizeTemplateElements(response.elements);
|
||||||
@@ -1808,14 +2235,14 @@ export default function InvoiceTemplatePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="invoice-template__palette">
|
<div className="invoice-template__palette">
|
||||||
{PALETTE_GROUPS.map((group) => (
|
{paletteGroups.map((group) => (
|
||||||
<section key={group.category} className="invoice-template__palette-group">
|
<section key={group.category} className="invoice-template__palette-group">
|
||||||
<div className="invoice-template__palette-group-header">
|
<div className="invoice-template__palette-group-header">
|
||||||
<h5>{group.title}</h5>
|
<h5>{group.title}</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="invoice-template__palette-grid">
|
<div className="invoice-template__palette-grid">
|
||||||
{PALETTE_ITEMS.filter((item) => item.category === group.category).map((item) => (
|
{paletteItems.filter((item) => item.category === group.category).map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1844,7 +2271,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
<div className="invoice-template__canvas-column">
|
<div className="invoice-template__canvas-column">
|
||||||
<div className="invoice-template__canvas-header">
|
<div className="invoice-template__canvas-header">
|
||||||
<div>
|
<div>
|
||||||
<h4>Rechnungsvorlage</h4>
|
<h4>{templateTitle}</h4>
|
||||||
<span className="invoice-template__panel-note">
|
<span className="invoice-template__panel-note">
|
||||||
{isTemplateLoading
|
{isTemplateLoading
|
||||||
? "Gespeicherte Vorlage wird geladen..."
|
? "Gespeicherte Vorlage wird geladen..."
|
||||||
@@ -1931,10 +2358,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="invoice-template__canvas-empty">
|
<div className="invoice-template__canvas-empty">{emptyCanvasMessage}</div>
|
||||||
Ziehen Sie links ein Element auf diese Fläche, um Ihr Rechnungstemplate zu
|
|
||||||
starten.
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2174,7 +2598,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
<div className="dialog__actions">
|
<div className="dialog__actions">
|
||||||
<a
|
<a
|
||||||
href={pdfPreviewUrl}
|
href={pdfPreviewUrl}
|
||||||
download="rechnung-template.pdf"
|
download={pdfDownloadName}
|
||||||
className="secondary-button"
|
className="secondary-button"
|
||||||
>
|
>
|
||||||
PDF herunterladen
|
PDF herunterladen
|
||||||
@@ -2193,7 +2617,7 @@ export default function InvoiceTemplatePage() {
|
|||||||
<iframe
|
<iframe
|
||||||
className="dialog__frame"
|
className="dialog__frame"
|
||||||
src={pdfPreviewUrl}
|
src={pdfPreviewUrl}
|
||||||
title="PDF-Vorschau Rechnungstemplate"
|
title={pdfPreviewTitle}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2202,3 +2626,10 @@ export default function InvoiceTemplatePage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
REPORT_LOCKED_TEXT_PALETTE_IDS,
|
||||||
|
REPORT_PALETTE_GROUPS,
|
||||||
|
REPORT_PALETTE_ITEMS,
|
||||||
|
createReportStarterLayout,
|
||||||
|
};
|
||||||
|
|||||||
21
frontend/src/pages/ReportTemplatePage.tsx
Normal file
21
frontend/src/pages/ReportTemplatePage.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import InvoiceTemplatePage, {
|
||||||
|
REPORT_LOCKED_TEXT_PALETTE_IDS,
|
||||||
|
REPORT_PALETTE_GROUPS,
|
||||||
|
REPORT_PALETTE_ITEMS,
|
||||||
|
createReportStarterLayout,
|
||||||
|
} from "./InvoiceTemplatePage";
|
||||||
|
|
||||||
|
export default function ReportTemplatePage() {
|
||||||
|
return (
|
||||||
|
<InvoiceTemplatePage
|
||||||
|
buildStarterLayout={createReportStarterLayout}
|
||||||
|
lockedTextPaletteIds={REPORT_LOCKED_TEXT_PALETTE_IDS}
|
||||||
|
paletteGroups={REPORT_PALETTE_GROUPS}
|
||||||
|
paletteItems={REPORT_PALETTE_ITEMS}
|
||||||
|
pdfDownloadName="bericht-template.pdf"
|
||||||
|
pdfPreviewTitle="PDF-Vorschau Berichtsvorlage"
|
||||||
|
templateApiPath="/session/report-template"
|
||||||
|
templateTitle="Berichtsvorlage"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -137,6 +137,23 @@ a {
|
|||||||
padding-left: 14px;
|
padding-left: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-subgroup {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-subgroup__label {
|
||||||
|
padding: 4px 14px 0;
|
||||||
|
color: rgba(248, 243, 237, 0.56);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-subnav--nested {
|
||||||
|
gap: 6px;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-sublink {
|
.nav-sublink {
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
|||||||
Reference in New Issue
Block a user