Add persistent invoice template management
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("invoiceTemplates")
|
||||||
|
public record InvoiceTemplate(
|
||||||
|
@Id String userId,
|
||||||
|
List<InvoiceTemplateElement> elements,
|
||||||
|
LocalDateTime createdAt,
|
||||||
|
LocalDateTime updatedAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package de.svencarstensen.muh.domain;
|
||||||
|
|
||||||
|
public record InvoiceTemplateElement(
|
||||||
|
String id,
|
||||||
|
String paletteId,
|
||||||
|
String kind,
|
||||||
|
String label,
|
||||||
|
String content,
|
||||||
|
Integer x,
|
||||||
|
Integer y,
|
||||||
|
Integer width,
|
||||||
|
Integer height,
|
||||||
|
Integer fontSize,
|
||||||
|
Integer fontWeight,
|
||||||
|
String textAlign,
|
||||||
|
String lineOrientation,
|
||||||
|
String imageSrc,
|
||||||
|
Integer imageNaturalWidth,
|
||||||
|
Integer imageNaturalHeight
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.svencarstensen.muh.repository;
|
||||||
|
|
||||||
|
import de.svencarstensen.muh.domain.InvoiceTemplate;
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
|
||||||
|
public interface InvoiceTemplateRepository extends MongoRepository<InvoiceTemplate, String> {
|
||||||
|
}
|
||||||
@@ -4,9 +4,12 @@ import de.svencarstensen.muh.domain.AppUser;
|
|||||||
import de.svencarstensen.muh.domain.UserRole;
|
import de.svencarstensen.muh.domain.UserRole;
|
||||||
import de.svencarstensen.muh.repository.AppUserRepository;
|
import de.svencarstensen.muh.repository.AppUserRepository;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class AuthorizationService {
|
public class AuthorizationService {
|
||||||
|
|
||||||
@@ -43,10 +46,11 @@ public class AuthorizationService {
|
|||||||
return user.accountId().trim();
|
return user.accountId().trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String requireText(String value, String message) {
|
private @NonNull String requireText(String value, String message) {
|
||||||
if (value == null || value.isBlank()) {
|
if (value == null || value.isBlank()) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||||
}
|
}
|
||||||
return value.trim();
|
String sanitized = Objects.requireNonNull(value).trim();
|
||||||
|
return Objects.requireNonNull(sanitized);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
@@ -15,6 +16,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
|
public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
|
||||||
@@ -28,7 +30,11 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
protected void doFilterInternal(
|
||||||
|
@NonNull HttpServletRequest request,
|
||||||
|
@NonNull HttpServletResponse response,
|
||||||
|
@NonNull FilterChain filterChain
|
||||||
|
)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
|
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||||
if (authorization == null || !authorization.startsWith("Bearer ")) {
|
if (authorization == null || !authorization.startsWith("Bearer ")) {
|
||||||
@@ -38,7 +44,7 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
AuthenticatedUser tokenUser = authTokenService.parseToken(authorization.substring(7));
|
AuthenticatedUser tokenUser = authTokenService.parseToken(authorization.substring(7));
|
||||||
AppUser user = appUserRepository.findById(tokenUser.id())
|
AppUser user = appUserRepository.findById(Objects.requireNonNull(tokenUser.id()))
|
||||||
.filter(AppUser::active)
|
.filter(AppUser::active)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Benutzer ungueltig"));
|
.orElseThrow(() -> new IllegalArgumentException("Benutzer ungueltig"));
|
||||||
|
|
||||||
|
|||||||
@@ -485,7 +485,7 @@ public class CatalogService {
|
|||||||
if (isPrimaryUser(existing) && actor.role() != UserRole.ADMIN) {
|
if (isPrimaryUser(existing) && actor.role() != UserRole.ADMIN) {
|
||||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Der Hauptbenutzer kann nicht geloescht werden");
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Der Hauptbenutzer kann nicht geloescht werden");
|
||||||
}
|
}
|
||||||
appUserRepository.deleteById(existing.id());
|
appUserRepository.deleteById(requireText(existing.id(), "Benutzer-ID fehlt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changePassword(String actorId, String id, String newPassword) {
|
public void changePassword(String actorId, String id, String newPassword) {
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package de.svencarstensen.muh.service;
|
||||||
|
|
||||||
|
import de.svencarstensen.muh.domain.AppUser;
|
||||||
|
import de.svencarstensen.muh.domain.InvoiceTemplate;
|
||||||
|
import de.svencarstensen.muh.domain.InvoiceTemplateElement;
|
||||||
|
import de.svencarstensen.muh.repository.AppUserRepository;
|
||||||
|
import de.svencarstensen.muh.repository.InvoiceTemplateRepository;
|
||||||
|
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.Objects;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class InvoiceTemplateService {
|
||||||
|
|
||||||
|
private final AppUserRepository appUserRepository;
|
||||||
|
private final InvoiceTemplateRepository invoiceTemplateRepository;
|
||||||
|
|
||||||
|
public InvoiceTemplateService(
|
||||||
|
AppUserRepository appUserRepository,
|
||||||
|
InvoiceTemplateRepository invoiceTemplateRepository
|
||||||
|
) {
|
||||||
|
this.appUserRepository = appUserRepository;
|
||||||
|
this.invoiceTemplateRepository = invoiceTemplateRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvoiceTemplateResponse currentTemplate(String actorId) {
|
||||||
|
String userId = requireActorId(actorId);
|
||||||
|
requireActiveUser(userId);
|
||||||
|
return invoiceTemplateRepository.findById(userId)
|
||||||
|
.map(this::toResponse)
|
||||||
|
.orElseGet(() -> new InvoiceTemplateResponse(false, List.of(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvoiceTemplateResponse saveTemplate(String actorId, List<InvoiceTemplateElementPayload> payloadElements) {
|
||||||
|
String userId = requireActorId(actorId);
|
||||||
|
requireActiveUser(userId);
|
||||||
|
InvoiceTemplate existing = invoiceTemplateRepository.findById(userId).orElse(null);
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
InvoiceTemplate saved = invoiceTemplateRepository.save(new InvoiceTemplate(
|
||||||
|
userId,
|
||||||
|
sanitizeElements(payloadElements),
|
||||||
|
existing != null ? existing.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<InvoiceTemplateElementPayload> payloadElements) {
|
||||||
|
if (payloadElements == null || payloadElements.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return payloadElements.stream()
|
||||||
|
.map(this::sanitizeElement)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvoiceTemplateElement sanitizeElement(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 InvoiceTemplateResponse toResponse(InvoiceTemplate template) {
|
||||||
|
List<InvoiceTemplateElementPayload> elements = template.elements() == null
|
||||||
|
? List.of()
|
||||||
|
: template.elements().stream().map(this::toPayload).toList();
|
||||||
|
return new InvoiceTemplateResponse(true, elements, template.updatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvoiceTemplateElementPayload toPayload(InvoiceTemplateElement element) {
|
||||||
|
return new 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record InvoiceTemplateElementPayload(
|
||||||
|
String id,
|
||||||
|
String paletteId,
|
||||||
|
String kind,
|
||||||
|
String label,
|
||||||
|
String content,
|
||||||
|
Integer x,
|
||||||
|
Integer y,
|
||||||
|
Integer width,
|
||||||
|
Integer height,
|
||||||
|
Integer fontSize,
|
||||||
|
Integer fontWeight,
|
||||||
|
String textAlign,
|
||||||
|
String lineOrientation,
|
||||||
|
String imageSrc,
|
||||||
|
Integer imageNaturalWidth,
|
||||||
|
Integer imageNaturalHeight
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record InvoiceTemplateResponse(
|
||||||
|
boolean stored,
|
||||||
|
List<InvoiceTemplateElementPayload> elements,
|
||||||
|
LocalDateTime updatedAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,7 +102,7 @@ public class ReportService {
|
|||||||
helper.setFrom(requireText(mailFrom, "Absender fehlt"));
|
helper.setFrom(requireText(mailFrom, "Absender fehlt"));
|
||||||
helper.setTo(requireText(sample.farmerEmail(), "Empfänger fehlt"));
|
helper.setTo(requireText(sample.farmerEmail(), "Empfänger fehlt"));
|
||||||
helper.setSubject("MUH-Bericht Probe " + sample.sampleNumber());
|
helper.setSubject("MUH-Bericht Probe " + sample.sampleNumber());
|
||||||
helper.setText(buildMailBody(sample, customerSignature), false);
|
helper.setText(Objects.requireNonNull(buildMailBody(sample, customerSignature)), false);
|
||||||
helper.addAttachment("MUH-Bericht-" + sample.sampleNumber() + ".pdf", new ByteArrayResource(Objects.requireNonNull(pdf)));
|
helper.addAttachment("MUH-Bericht-" + sample.sampleNumber() + ".pdf", new ByteArrayResource(Objects.requireNonNull(pdf)));
|
||||||
sender.send(message);
|
sender.send(message);
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
@@ -171,7 +171,8 @@ public class ReportService {
|
|||||||
return defaultCustomerSignature();
|
return defaultCustomerSignature();
|
||||||
}
|
}
|
||||||
|
|
||||||
AppUser actor = appUserRepository.findById(actorId.trim()).orElse(null);
|
String normalizedActorId = Objects.requireNonNull(actorId).trim();
|
||||||
|
AppUser actor = appUserRepository.findById(Objects.requireNonNull(normalizedActorId)).orElse(null);
|
||||||
if (actor == null) {
|
if (actor == null) {
|
||||||
return defaultCustomerSignature();
|
return defaultCustomerSignature();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import org.springframework.web.bind.annotation.PathVariable;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.PathVariable;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
package de.svencarstensen.muh.web;
|
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.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;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/session")
|
@RequestMapping("/api/session")
|
||||||
public class SessionController {
|
public class SessionController {
|
||||||
|
|
||||||
private final CatalogService catalogService;
|
private final CatalogService catalogService;
|
||||||
|
private final InvoiceTemplateService invoiceTemplateService;
|
||||||
private final SecuritySupport securitySupport;
|
private final SecuritySupport securitySupport;
|
||||||
|
|
||||||
public SessionController(CatalogService catalogService, SecuritySupport securitySupport) {
|
public SessionController(
|
||||||
|
CatalogService catalogService,
|
||||||
|
InvoiceTemplateService invoiceTemplateService,
|
||||||
|
SecuritySupport securitySupport
|
||||||
|
) {
|
||||||
this.catalogService = catalogService;
|
this.catalogService = catalogService;
|
||||||
|
this.invoiceTemplateService = invoiceTemplateService;
|
||||||
this.securitySupport = securitySupport;
|
this.securitySupport = securitySupport;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +36,11 @@ public class SessionController {
|
|||||||
return catalogService.currentUser(securitySupport.currentUser().id());
|
return catalogService.currentUser(securitySupport.currentUser().id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/invoice-template")
|
||||||
|
public InvoiceTemplateService.InvoiceTemplateResponse currentInvoiceTemplate() {
|
||||||
|
return invoiceTemplateService.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());
|
||||||
@@ -46,6 +60,16 @@ public class SessionController {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/invoice-template")
|
||||||
|
public InvoiceTemplateService.InvoiceTemplateResponse saveInvoiceTemplate(
|
||||||
|
@RequestBody InvoiceTemplateRequest request
|
||||||
|
) {
|
||||||
|
return invoiceTemplateService.saveTemplate(
|
||||||
|
securitySupport.currentUser().id(),
|
||||||
|
request.elements()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public record PasswordLoginRequest(@NotBlank String email, @NotBlank String password) {
|
public record PasswordLoginRequest(@NotBlank String email, @NotBlank String password) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,4 +84,9 @@ public class SessionController {
|
|||||||
@NotBlank String password
|
@NotBlank String password
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record InvoiceTemplateRequest(
|
||||||
|
List<InvoiceTemplateService.InvoiceTemplateElementPayload> elements
|
||||||
|
) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export const USER_STORAGE_KEY = "muh.current-user";
|
export const USER_STORAGE_KEY = "muh.current-user";
|
||||||
export const AUTH_TOKEN_STORAGE_KEY = "muh.auth-token";
|
export const AUTH_TOKEN_STORAGE_KEY = "muh.auth-token";
|
||||||
export const INVOICE_TEMPLATE_STORAGE_KEY = "muh.invoice-template";
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ export default function LoginPage() {
|
|||||||
const [showRegistration, setShowRegistration] = useState(false);
|
const [showRegistration, setShowRegistration] = useState(false);
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
const [loginInputsUnlocked, setLoginInputsUnlocked] = useState(false);
|
||||||
const [showLoginValidation, setShowLoginValidation] = useState(false);
|
const [showLoginValidation, setShowLoginValidation] = useState(false);
|
||||||
const [showRegisterValidation, setShowRegisterValidation] = useState(false);
|
const [showRegisterValidation, setShowRegisterValidation] = useState(false);
|
||||||
const [registration, setRegistration] = useState({
|
const [registration, setRegistration] = useState({
|
||||||
@@ -28,6 +29,10 @@ export default function LoginPage() {
|
|||||||
const [feedback, setFeedback] = useState<FeedbackState>(null);
|
const [feedback, setFeedback] = useState<FeedbackState>(null);
|
||||||
const { setSession } = useSession();
|
const { setSession } = useSession();
|
||||||
|
|
||||||
|
function unlockLoginInputs() {
|
||||||
|
setLoginInputsUnlocked(true);
|
||||||
|
}
|
||||||
|
|
||||||
async function handlePasswordLogin(event: FormEvent<HTMLFormElement>) {
|
async function handlePasswordLogin(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setShowLoginValidation(true);
|
setShowLoginValidation(true);
|
||||||
@@ -116,14 +121,23 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
<div className="auth-grid">
|
<div className="auth-grid">
|
||||||
{!showRegistration ? (
|
{!showRegistration ? (
|
||||||
<form className={`login-panel__section ${showLoginValidation ? "show-validation" : ""}`} onSubmit={handlePasswordLogin}>
|
<form
|
||||||
|
className={`login-panel__section ${showLoginValidation ? "show-validation" : ""}`}
|
||||||
|
onSubmit={handlePasswordLogin}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<label className="field field--required">
|
<label className="field field--required">
|
||||||
<span>E-Mail</span>
|
<span>E-Mail</span>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
|
name="login-email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(event) => setEmail(event.target.value)}
|
onChange={(event) => setEmail(event.target.value)}
|
||||||
|
onFocus={unlockLoginInputs}
|
||||||
|
onPointerDown={unlockLoginInputs}
|
||||||
placeholder="z. B. name@hof.de"
|
placeholder="z. B. name@hof.de"
|
||||||
|
autoComplete="off"
|
||||||
|
readOnly={!loginInputsUnlocked}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -131,8 +145,13 @@ export default function LoginPage() {
|
|||||||
<span>Passwort</span>
|
<span>Passwort</span>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
|
name="login-password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(event) => setPassword(event.target.value)}
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
|
onFocus={unlockLoginInputs}
|
||||||
|
onPointerDown={unlockLoginInputs}
|
||||||
|
autoComplete="new-password"
|
||||||
|
readOnly={!loginInputsUnlocked}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -751,6 +751,9 @@ a {
|
|||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
background: var(--surface-strong);
|
||||||
|
box-shadow: none;
|
||||||
|
backdrop-filter: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,13 +857,14 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.invoice-template__canvas-shell {
|
.invoice-template__canvas-shell {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 0 8px;
|
padding: 0 0 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -883,7 +887,6 @@ a {
|
|||||||
linear-gradient(90deg, transparent 31px, rgba(37, 49, 58, 0.05) 32px),
|
linear-gradient(90deg, transparent 31px, rgba(37, 49, 58, 0.05) 32px),
|
||||||
#fffdf9;
|
#fffdf9;
|
||||||
background-size: auto, 32px 32px, 32px 32px, auto;
|
background-size: auto, 32px 32px, 32px 32px, auto;
|
||||||
box-shadow: 0 32px 60px rgba(54, 44, 27, 0.12);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
@@ -891,7 +894,7 @@ a {
|
|||||||
|
|
||||||
.invoice-template__canvas.is-active {
|
.invoice-template__canvas.is-active {
|
||||||
border-color: rgba(17, 109, 99, 0.48);
|
border-color: rgba(17, 109, 99, 0.48);
|
||||||
box-shadow: 0 32px 60px rgba(54, 44, 27, 0.12), 0 0 0 6px rgba(17, 109, 99, 0.08);
|
box-shadow: 0 0 0 6px rgba(17, 109, 99, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoice-template__canvas::before {
|
.invoice-template__canvas::before {
|
||||||
@@ -924,9 +927,15 @@ a {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
cursor: move;
|
cursor: move;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
overflow: visible;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-template__canvas-element--line,
|
||||||
|
.invoice-template__canvas-element--image {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.invoice-template__canvas-element:hover,
|
.invoice-template__canvas-element:hover,
|
||||||
.invoice-template__canvas-element.is-selected {
|
.invoice-template__canvas-element.is-selected {
|
||||||
border-color: rgba(17, 109, 99, 0.28);
|
border-color: rgba(17, 109, 99, 0.28);
|
||||||
@@ -938,6 +947,11 @@ a {
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-template__canvas-element.is-resizing {
|
||||||
|
cursor: nwse-resize;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
.invoice-template__canvas-element-text {
|
.invoice-template__canvas-element-text {
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
@@ -945,6 +959,56 @@ a {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-template__canvas-line {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(37, 49, 58, 0.82);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-template__canvas-image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 16px;
|
||||||
|
object-fit: contain;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-template__canvas-image-placeholder {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px dashed rgba(17, 109, 99, 0.28);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(17, 109, 99, 0.05);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
pointer-events: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-template__canvas-element-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
right: -7px;
|
||||||
|
bottom: -7px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 1px solid rgba(17, 109, 99, 0.48);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface-strong);
|
||||||
|
box-shadow: 0 0 0 3px rgba(17, 109, 99, 0.12);
|
||||||
|
cursor: nwse-resize;
|
||||||
|
pointer-events: auto;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
.invoice-template__inspector {
|
.invoice-template__inspector {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -957,7 +1021,9 @@ a {
|
|||||||
|
|
||||||
.invoice-template__inspector-actions {
|
.invoice-template__inspector-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoice-template-page .field-grid {
|
.invoice-template-page .field-grid {
|
||||||
|
|||||||
Reference in New Issue
Block a user