Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
168fc986f2 | ||
|
|
a6216f6e81 | ||
|
|
2955a7eb1c | ||
|
|
7819b963f2 | ||
|
|
5e941753e2 | ||
|
|
9c439e21ee | ||
|
|
b6158d933f | ||
|
|
bb4d7fbf68 |
4
Jenkinsfile
vendored
4
Jenkinsfile
vendored
@@ -7,7 +7,7 @@ def defaultRocketChatChannel = "#builds"
|
||||
def rocketChatColor = "#e813c8"
|
||||
def rocketChatEmoji = ":skunk:"
|
||||
|
||||
def numOfArtifactsToKeep = env.GIT_BRANCH == "main" ? "10" : "5"
|
||||
def numOfArtifactsToKeep = env.GIT_BRANCH == "master" ? "10" : "5"
|
||||
|
||||
|
||||
def result = []
|
||||
@@ -189,7 +189,7 @@ pipeline {
|
||||
|
||||
stage ('Release') {
|
||||
when {
|
||||
branch 'main'
|
||||
branch 'master'
|
||||
}
|
||||
|
||||
steps {
|
||||
|
||||
@@ -44,6 +44,13 @@
|
||||
<version>3.0.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- EXIF metadata extraction for image orientation -->
|
||||
<dependency>
|
||||
<groupId>com.drewnoakes</groupId>
|
||||
<artifactId>metadata-extractor</artifactId>
|
||||
<version>2.19.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Elytron secrity used for username/password login -->
|
||||
<dependency>
|
||||
<groupId>org.wildfly.security</groupId>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.core.utils;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import com.drew.imaging.ImageMetadataReader;
|
||||
import com.drew.imaging.ImageProcessingException;
|
||||
import com.drew.metadata.Metadata;
|
||||
import com.drew.metadata.MetadataException;
|
||||
import com.drew.metadata.exif.ExifIFD0Directory;
|
||||
|
||||
/**
|
||||
*
|
||||
* <p>Copyright: Copyright (c) 2024</p>
|
||||
* <p>Company: heyday Marketing GmbH</p>
|
||||
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||
* @version 1.0
|
||||
*
|
||||
* created: 6 Feb 2026
|
||||
*/
|
||||
|
||||
public interface ImageHandler {
|
||||
static final Log LOG = LogFactory.getLog(ImageHandler.class);
|
||||
|
||||
/**
|
||||
* Reads image bytes and returns a BufferedImage with correct EXIF orientation applied.
|
||||
*/
|
||||
default BufferedImage readImageWithCorrectOrientation(byte[] imageBytes) throws IOException {
|
||||
int orientation = getExifOrientation(imageBytes);
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes));
|
||||
if (image == null) {
|
||||
throw new IOException("Failed to read image from byte array");
|
||||
}
|
||||
return applyOrientation(image, orientation);
|
||||
}
|
||||
|
||||
default int getExifOrientation(byte[] imageBytes) {
|
||||
try {
|
||||
Metadata metadata = ImageMetadataReader.readMetadata(new ByteArrayInputStream(imageBytes));
|
||||
ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
|
||||
if (directory != null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
|
||||
return directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
|
||||
}
|
||||
} catch (ImageProcessingException | IOException | MetadataException e) {
|
||||
LOG.debug("Could not read EXIF orientation: " + e.getMessage());
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
default BufferedImage applyOrientation(BufferedImage image, int orientation) {
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
|
||||
AffineTransform transform = new AffineTransform();
|
||||
int newWidth = width;
|
||||
int newHeight = height;
|
||||
|
||||
switch (orientation) {
|
||||
case 1 -> {
|
||||
return image;
|
||||
}
|
||||
case 2 -> {
|
||||
transform.scale(-1.0, 1.0);
|
||||
transform.translate(-width, 0);
|
||||
}
|
||||
case 3 -> {
|
||||
transform.translate(width, height);
|
||||
transform.rotate(Math.PI);
|
||||
}
|
||||
case 4 -> {
|
||||
transform.scale(1.0, -1.0);
|
||||
transform.translate(0, -height);
|
||||
}
|
||||
case 5 -> {
|
||||
newWidth = height;
|
||||
newHeight = width;
|
||||
transform.rotate(-Math.PI / 2);
|
||||
transform.scale(-1.0, 1.0);
|
||||
}
|
||||
case 6 -> {
|
||||
newWidth = height;
|
||||
newHeight = width;
|
||||
transform.translate(height, 0);
|
||||
transform.rotate(Math.PI / 2);
|
||||
}
|
||||
case 7 -> {
|
||||
newWidth = height;
|
||||
newHeight = width;
|
||||
transform.scale(-1.0, 1.0);
|
||||
transform.translate(-height, 0);
|
||||
transform.translate(0, width);
|
||||
transform.rotate(3 * Math.PI / 2);
|
||||
}
|
||||
case 8 -> {
|
||||
newWidth = height;
|
||||
newHeight = width;
|
||||
transform.translate(0, width);
|
||||
transform.rotate(-Math.PI / 2);
|
||||
}
|
||||
default -> {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
BufferedImage rotated = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = rotated.createGraphics();
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2d.drawImage(image, transform, null);
|
||||
g2d.dispose();
|
||||
|
||||
return rotated;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
* created: 2 Feb 2026
|
||||
*/
|
||||
|
||||
public class ImageUtil {
|
||||
public class ImageUtil implements ImageHandler {
|
||||
private static final Log LOG = LogFactory.getLog(ImageUtil.class);
|
||||
|
||||
private static final int NORMAL_MAX_WIDTH = 1200;
|
||||
@@ -44,10 +44,10 @@ public class ImageUtil {
|
||||
public byte[] getImage(String base64, int size) {
|
||||
byte[] original = Base64.getDecoder().decode(base64);
|
||||
return switch (size) {
|
||||
case 1 -> original;
|
||||
case 1 -> applyExifOrientation(original);
|
||||
case 2 -> normal(original);
|
||||
case 3 -> thumbnail(original);
|
||||
default -> original;
|
||||
default -> applyExifOrientation(original);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,16 +61,27 @@ public class ImageUtil {
|
||||
|
||||
private byte[] resize(byte[] original, int maxWidth, float quality) {
|
||||
try {
|
||||
int orientation = getExifOrientation(original);
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(original));
|
||||
if (image == null) {
|
||||
LOG.error("Failed to read image from byte array");
|
||||
return original;
|
||||
}
|
||||
|
||||
// For rotated images (orientation 5, 6, 7, 8), width and height are swapped
|
||||
int effectiveWidth = (orientation >= 5 && orientation <= 8) ? image.getHeight() : image.getWidth();
|
||||
|
||||
// If no resize needed and no orientation fix needed, return original
|
||||
if (effectiveWidth <= maxWidth && orientation == 1) {
|
||||
return original;
|
||||
}
|
||||
|
||||
image = applyOrientation(image, orientation);
|
||||
|
||||
int originalWidth = image.getWidth();
|
||||
|
||||
if (originalWidth <= maxWidth) {
|
||||
return original;
|
||||
return writeJpeg(image, quality);
|
||||
}
|
||||
|
||||
double scale = (double) maxWidth / originalWidth;
|
||||
@@ -92,6 +103,26 @@ public class ImageUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] applyExifOrientation(byte[] original) {
|
||||
try {
|
||||
int orientation = getExifOrientation(original);
|
||||
if (orientation == 1) {
|
||||
return original;
|
||||
}
|
||||
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(original));
|
||||
if (image == null) {
|
||||
return original;
|
||||
}
|
||||
|
||||
BufferedImage rotated = applyOrientation(image, orientation);
|
||||
return writeJpeg(rotated, 1.0F);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to apply EXIF orientation", e);
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] writeJpeg(BufferedImage image, float quality) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package marketing.heyday.hartmann.fotodocumentation.core.utils;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -19,6 +20,7 @@ import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
|
||||
import marketing.heyday.hartmann.fotodocumentation.core.model.Customer;
|
||||
@@ -34,7 +36,7 @@ import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
|
||||
* created: 2 Feb 2026
|
||||
*/
|
||||
@SuppressWarnings({ "java:S818", "squid:S818", "squid:S109" })
|
||||
public class PdfUtils {
|
||||
public class PdfUtils implements ImageHandler {
|
||||
private static final Log LOG = LogFactory.getLog(PdfUtils.class);
|
||||
|
||||
private static final String FONT_PANTON_REGULAR = "fonts/Panton-Regular.ttf";
|
||||
@@ -70,7 +72,8 @@ public class PdfUtils {
|
||||
float pageWidth = page.getMediaBox().getWidth();
|
||||
float pageHeight = page.getMediaBox().getHeight();
|
||||
float contentWidth = pageWidth - 2 * PAGE_MARGIN;
|
||||
float halfWidth = contentWidth / 2F;
|
||||
float imageWidth = contentWidth * 0.75F;
|
||||
float metadataWidth = contentWidth * 0.25F;
|
||||
|
||||
try (PDPageContentStream cs = new PDPageContentStream(document, page)) {
|
||||
float yPosition = pageHeight - 50F;
|
||||
@@ -87,16 +90,17 @@ public class PdfUtils {
|
||||
firstPage = false;
|
||||
}
|
||||
|
||||
// Left side: image (50% of content width)
|
||||
// Left side: image (75% of content width)
|
||||
float imageX = PAGE_MARGIN;
|
||||
float imageY = yPosition;
|
||||
float imageMaxWidth = halfWidth - 10F;
|
||||
float imageMaxWidth = imageWidth - 10F;
|
||||
float imageMaxHeight = pageHeight - 2 * PAGE_MARGIN - 40F;
|
||||
|
||||
if (picture.getImage() != null) {
|
||||
try {
|
||||
byte[] imageBytes = Base64.getDecoder().decode(picture.getImage());
|
||||
PDImageXObject pdImage = PDImageXObject.createFromByteArray(document, imageBytes, "picture");
|
||||
BufferedImage correctedImage = readImageWithCorrectOrientation(imageBytes);
|
||||
PDImageXObject pdImage = LosslessFactory.createFromImage(document, correctedImage);
|
||||
|
||||
float scale = Math.min(imageMaxWidth / pdImage.getWidth(), imageMaxHeight / pdImage.getHeight());
|
||||
float drawWidth = pdImage.getWidth() * scale;
|
||||
@@ -108,30 +112,25 @@ public class PdfUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// Right side: metadata (top-aligned with image)
|
||||
float rightX = PAGE_MARGIN + halfWidth + 10F;
|
||||
float rightY = imageY - 32F;
|
||||
// Right side: metadata (25% of content width, top-aligned with image)
|
||||
float rightX = PAGE_MARGIN + imageWidth + 10F;
|
||||
float rightY = imageY - 10F;
|
||||
|
||||
// Date (no label, bold, size 44)
|
||||
// Date (bold, size 10 - matching labels)
|
||||
String dateStr = picture.getPictureDate() != null ? (DATE_FORMAT.format(picture.getPictureDate()) + " UHR") : "";
|
||||
cs.setFont(fontBold, 32);
|
||||
cs.setFont(fontBold, 10);
|
||||
cs.setNonStrokingColor(COLOR_DATE);
|
||||
cs.beginText();
|
||||
cs.newLineAtOffset(rightX, rightY);
|
||||
cs.showText(dateStr);
|
||||
cs.endText();
|
||||
rightY -= 54F;
|
||||
rightY -= 24F;
|
||||
|
||||
// Customer number
|
||||
float kundenNummerY = rightY;
|
||||
rightY = drawLabel(cs, fontBold, "KUNDENNUMMER", rightX, rightY);
|
||||
rightY = drawValue(cs, fontRegular, nullSafe(customer.getCustomerNumber()), rightX, rightY);
|
||||
rightY -= 10F;
|
||||
|
||||
// Evaluation card with circles
|
||||
float circlesX = rightX + 140F;
|
||||
drawEvaluationCard(cs, fontBold, circlesX, kundenNummerY, picture.getEvaluation());
|
||||
|
||||
// ZIP
|
||||
rightY = drawLabel(cs, fontBold, "PLZ", rightX, rightY);
|
||||
rightY = drawValue(cs, fontRegular, nullSafe(customer.getZip()), rightX, rightY);
|
||||
@@ -142,9 +141,27 @@ public class PdfUtils {
|
||||
rightY = drawValue(cs, fontRegular, nullSafe(customer.getCity()), rightX, rightY);
|
||||
rightY -= 10F;
|
||||
|
||||
// Evaluation card with circles
|
||||
float evaluationY = rightY;
|
||||
drawEvaluationCard(cs, fontBold, rightX, evaluationY, picture.getEvaluation());
|
||||
rightY -= 80F;
|
||||
|
||||
// Comment
|
||||
rightY = drawLabel(cs, fontBold, "KOMMENTAR", rightX, rightY);
|
||||
drawWrappedText(cs, fontRegular, nullSafe(picture.getComment()), rightX, rightY, halfWidth - 20f);
|
||||
String remainingComment = drawWrappedText(cs, fontRegular, nullSafe(picture.getComment()), rightX, rightY, metadataWidth - 20f, 50F);
|
||||
|
||||
// Continue comment on additional pages if needed
|
||||
while (remainingComment != null && !remainingComment.isEmpty()) {
|
||||
PDPage continuationPage = new PDPage(new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()));
|
||||
document.addPage(continuationPage);
|
||||
|
||||
try (PDPageContentStream continuationCs = new PDPageContentStream(document, continuationPage)) {
|
||||
float continuationY = continuationPage.getMediaBox().getHeight() - PAGE_MARGIN;
|
||||
float continuationWidth = continuationPage.getMediaBox().getWidth() - 2 * PAGE_MARGIN;
|
||||
continuationY = drawLabel(continuationCs, fontBold, "KOMMENTAR (FORTSETZUNG)", PAGE_MARGIN, continuationY);
|
||||
remainingComment = drawWrappedText(continuationCs, fontRegular, remainingComment, PAGE_MARGIN, continuationY, continuationWidth, 50F);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,9 +206,9 @@ public class PdfUtils {
|
||||
return y - 14F;
|
||||
}
|
||||
|
||||
private void drawWrappedText(PDPageContentStream cs, PDFont font, String text, float x, float y, float maxWidth) throws IOException {
|
||||
private String drawWrappedText(PDPageContentStream cs, PDFont font, String text, float x, float y, float maxWidth, float minY) throws IOException {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
cs.setFont(font, 10);
|
||||
cs.setNonStrokingColor(COLOR_TEXT_GRAY);
|
||||
@@ -199,11 +216,23 @@ public class PdfUtils {
|
||||
String[] words = text.split("\\s+");
|
||||
StringBuilder line = new StringBuilder();
|
||||
float currentY = y;
|
||||
int wordIndex = 0;
|
||||
|
||||
for (String word : words) {
|
||||
for (wordIndex = 0; wordIndex < words.length; wordIndex++) {
|
||||
String word = words[wordIndex];
|
||||
String testLine = line.isEmpty() ? word : (line + " " + word);
|
||||
float textWidth = font.getStringWidth(testLine) / 1000F * 10F;
|
||||
if (textWidth > maxWidth && !line.isEmpty()) {
|
||||
// Check if we have room for this line
|
||||
if (currentY < minY) {
|
||||
// Return remaining text starting from current word
|
||||
StringBuilder remaining = new StringBuilder(line);
|
||||
for (int i = wordIndex; i < words.length; i++) {
|
||||
if (!remaining.isEmpty()) remaining.append(" ");
|
||||
remaining.append(words[i]);
|
||||
}
|
||||
return remaining.toString();
|
||||
}
|
||||
cs.beginText();
|
||||
cs.newLineAtOffset(x, currentY);
|
||||
cs.showText(line.toString());
|
||||
@@ -215,45 +244,36 @@ public class PdfUtils {
|
||||
}
|
||||
}
|
||||
if (!line.isEmpty()) {
|
||||
if (currentY < minY) {
|
||||
return line.toString();
|
||||
}
|
||||
cs.beginText();
|
||||
cs.newLineAtOffset(x, currentY);
|
||||
cs.showText(line.toString());
|
||||
cs.endText();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void drawEvaluationCard(PDPageContentStream cs, PDFont fontBold, float x, float y, Integer evaluation) throws IOException {
|
||||
int eval = evaluation != null ? evaluation : 0;
|
||||
Color[] colors = { COLOR_GREEN, COLOR_YELLOW, COLOR_RED };
|
||||
|
||||
float cardPadding = 10F;
|
||||
float cardWidth = 2 * CIRCLE_SPACING + 2 * HIGHLIGHT_RADIUS + 2 * cardPadding;
|
||||
float labelHeight = 14F;
|
||||
float cardHeight = labelHeight + 2 * HIGHLIGHT_RADIUS + 2 * cardPadding + 4F;
|
||||
float cardX = x - HIGHLIGHT_RADIUS - cardPadding;
|
||||
float cardY = y - cardHeight + cardPadding;
|
||||
|
||||
// Draw card background (rounded rectangle)
|
||||
cs.setStrokingColor(new Color(0xDD, 0xDD, 0xDD));
|
||||
cs.setNonStrokingColor(new Color(0xF8, 0xF8, 0xF8));
|
||||
cs.setLineWidth(1f);
|
||||
drawRoundedRect(cs, cardX, cardY, cardWidth, cardHeight, 6F);
|
||||
cs.fillAndStroke();
|
||||
|
||||
// Draw "BEWERTUNG" label above circles
|
||||
float labelX = x;
|
||||
float labelY = y - labelHeight;
|
||||
cs.setFont(fontBold, 9);
|
||||
cs.setFont(fontBold, 10);
|
||||
cs.setNonStrokingColor(COLOR_CUSTOMER_NAME);
|
||||
cs.beginText();
|
||||
cs.newLineAtOffset(labelX, labelY);
|
||||
cs.newLineAtOffset(x, labelY);
|
||||
cs.showText("BEWERTUNG");
|
||||
cs.endText();
|
||||
|
||||
// Draw circles below the label
|
||||
float circleY = labelY - cardPadding - HIGHLIGHT_RADIUS - 2F;
|
||||
float circleY = labelY - HIGHLIGHT_RADIUS - 10F;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
float cx = x + i * CIRCLE_SPACING;
|
||||
float cx = x + CIRCLE_RADIUS + i * CIRCLE_SPACING;
|
||||
|
||||
// Highlight circle if this matches the evaluation (1=green, 2=yellow, 3=red)
|
||||
if (eval == i + 1) {
|
||||
@@ -270,19 +290,6 @@ public class PdfUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private void drawRoundedRect(PDPageContentStream cs, float x, float y, float w, float h, float r) throws IOException {
|
||||
cs.moveTo(x + r, y);
|
||||
cs.lineTo(x + w - r, y);
|
||||
cs.curveTo(x + w, y, x + w, y, x + w, y + r);
|
||||
cs.lineTo(x + w, y + h - r);
|
||||
cs.curveTo(x + w, y + h, x + w, y + h, x + w - r, y + h);
|
||||
cs.lineTo(x + r, y + h);
|
||||
cs.curveTo(x, y + h, x, y + h, x, y + h - r);
|
||||
cs.lineTo(x, y + r);
|
||||
cs.curveTo(x, y, x, y, x + r, y);
|
||||
cs.closePath();
|
||||
}
|
||||
|
||||
private void drawCircle(PDPageContentStream cs, float cx, float cy, float r) throws IOException {
|
||||
float k = 0.5523f; // Bezier approximation for circle
|
||||
cs.moveTo(cx - r, cy);
|
||||
|
||||
@@ -17,13 +17,13 @@ import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
|
||||
*/
|
||||
|
||||
@Schema(name = "CustomerList")
|
||||
public record CustomerListValue(Long id, String name, String customerNumber, Date lastUpdateDate) {
|
||||
public record CustomerListValue(Long id, String name, String customerNumber, String zip, String city, Date lastUpdateDate) {
|
||||
|
||||
public static CustomerListValue builder(Customer customer) {
|
||||
if (customer == null) {
|
||||
return null;
|
||||
}
|
||||
Date date = customer.getPictures().stream().map(Picture::getPictureDate).sorted((p1, p2) -> p2.compareTo(p1)).findFirst().orElse(null);
|
||||
return new CustomerListValue(customer.getCustomerId(), customer.getName(), customer.getCustomerNumber(), date);
|
||||
return new CustomerListValue(customer.getCustomerId(), customer.getName(), customer.getCustomerNumber(), customer.getZip(), customer.getCity(), date);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class PdfUtilsTest {
|
||||
byte[] pdfBytes = pdfUtils.createPdf(customer, pictures);
|
||||
|
||||
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
|
||||
assertEquals(3, document.getNumberOfPages());
|
||||
assertEquals(4, document.getNumberOfPages());
|
||||
}
|
||||
writeToFile(pdfBytes, "createPdf_multiplePictures_createsOnPagePerPicture.pdf");
|
||||
}
|
||||
|
||||
@@ -178,4 +178,16 @@ public class PictureResourceTest extends AbstractRestTest {
|
||||
int code = httpResponse.getStatusLine().getStatusCode();
|
||||
assertEquals(404, code);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
|
||||
var test = new PictureResourceTest();
|
||||
|
||||
test.deploymentURL = "http://localhost:8080/";
|
||||
test.deploymentURL = "https://hartmann-cue.heydevelop.de/";
|
||||
test.username = "adm";
|
||||
test.password = "x1t0e7Pb49";
|
||||
|
||||
test.doGetPicture();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ class CustomerListDto {
|
||||
final int id;
|
||||
final String name;
|
||||
final String customerNumber;
|
||||
final String? zip;
|
||||
final String? city;
|
||||
final DateTime? lastUpdateDate;
|
||||
|
||||
CustomerListDto({required this.id, required this.name, required this.customerNumber, this.lastUpdateDate});
|
||||
CustomerListDto({required this.id, required this.name, required this.customerNumber, this.zip, this.city, this.lastUpdateDate});
|
||||
|
||||
/// Create from JSON response
|
||||
factory CustomerListDto.fromJson(Map<String, dynamic> json) {
|
||||
@@ -15,6 +17,8 @@ class CustomerListDto {
|
||||
id: json['id'] as int,
|
||||
name: json['name'] as String,
|
||||
customerNumber: json['customerNumber'] as String,
|
||||
zip: json['zip'] as String?,
|
||||
city: json['city'] as String?,
|
||||
lastUpdateDate: DateTimeUtils.toDateTime(json['lastUpdateDate']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"errorWidget": "Fehler: {name}",
|
||||
"@errorWidget": {
|
||||
"description": "Error widget text",
|
||||
@@ -80,7 +79,7 @@
|
||||
"@customerListHeaderName": {
|
||||
"description": "Customer list table header for name"
|
||||
},
|
||||
"customerListHeaderLastDate": "Datum Bilder",
|
||||
"customerListHeaderLastDate": "Datum",
|
||||
"@customerListHeaderLastDate": {
|
||||
"description": "Customer list table header for last date"
|
||||
},
|
||||
@@ -144,5 +143,13 @@
|
||||
"pictureWidgetLabelEvaluation": "BEWERTUNG",
|
||||
"@pictureWidgetLabelEvaluation": {
|
||||
"description": "Picture widget label for evaluation"
|
||||
},
|
||||
"pictureWidgetNotFound": "Das Bild konnte nicht gefunden werden.",
|
||||
"@pictureWidgetNotFound": {
|
||||
"description": "Picture not found error message"
|
||||
},
|
||||
"customerWidgetDownloadInProgress": "Download wird vorbereitet…",
|
||||
"@customerWidgetDownloadInProgress": {
|
||||
"description": "Download in progress message"
|
||||
}
|
||||
}
|
||||
@@ -199,7 +199,7 @@ abstract class AppLocalizations {
|
||||
/// Customer list table header for last date
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Datum Bilder'**
|
||||
/// **'Datum'**
|
||||
String get customerListHeaderLastDate;
|
||||
|
||||
/// Customer list table header for ladt date
|
||||
@@ -285,6 +285,18 @@ abstract class AppLocalizations {
|
||||
/// In de, this message translates to:
|
||||
/// **'BEWERTUNG'**
|
||||
String get pictureWidgetLabelEvaluation;
|
||||
|
||||
/// Picture not found error message
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Das Bild konnte nicht gefunden werden.'**
|
||||
String get pictureWidgetNotFound;
|
||||
|
||||
/// Download in progress message
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Download wird vorbereitet…'**
|
||||
String get customerWidgetDownloadInProgress;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -65,7 +65,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get customerListHeaderName => 'Apothekenname';
|
||||
|
||||
@override
|
||||
String get customerListHeaderLastDate => 'Datum Bilder';
|
||||
String get customerListHeaderLastDate => 'Datum';
|
||||
|
||||
@override
|
||||
String get customerListHeaderLastDateSuffix => ' (zuletzt aktualisiert)';
|
||||
@@ -111,4 +111,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pictureWidgetLabelEvaluation => 'BEWERTUNG';
|
||||
|
||||
@override
|
||||
String get pictureWidgetNotFound => 'Das Bild konnte nicht gefunden werden.';
|
||||
|
||||
@override
|
||||
String get customerWidgetDownloadInProgress => 'Download wird vorbereitet…';
|
||||
}
|
||||
|
||||
@@ -138,15 +138,10 @@ class _CustomerListWidgetState extends State<CustomerListWidget> {
|
||||
color: _generalStyle.secondaryWidgetBackgroundColor,
|
||||
);
|
||||
|
||||
final headerStyleSuffix = TextStyle(
|
||||
fontFamily: _generalStyle.fontFamily,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 20,
|
||||
color: _generalStyle.secondaryWidgetBackgroundColor,
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
const SizedBox(width: 48),
|
||||
Expanded(
|
||||
@@ -163,18 +158,32 @@ class _CustomerListWidgetState extends State<CustomerListWidget> {
|
||||
style: headerStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
"PLZ/Ort",
|
||||
style: headerStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
"Ort",
|
||||
style: headerStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Wrap(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Icon(Icons.calendar_month, color: _generalStyle.secondaryWidgetBackgroundColor),
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.customerListHeaderLastDate,
|
||||
style: headerStyle,
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.customerListHeaderLastDateSuffix,
|
||||
style: headerStyleSuffix,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -196,6 +205,7 @@ class _CustomerListWidgetState extends State<CustomerListWidget> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 48,
|
||||
@@ -209,6 +219,14 @@ class _CustomerListWidgetState extends State<CustomerListWidget> {
|
||||
flex: 3,
|
||||
child: Text(dto.name, style: dataStyle),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(dto.zip ?? "", style: dataStyle),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(dto.city ?? "", style: dataStyle),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(dateStr, style: dataStyle),
|
||||
|
||||
@@ -337,8 +337,43 @@ class _CustomerWidgetState extends State<CustomerWidget> {
|
||||
}
|
||||
|
||||
Future<void> _actionDownload(BuildContext context, CustomerDto customerDto, PictureDto? pictureDto) async {
|
||||
final bytes = await _customerController.export(customerId: customerDto.id, pictureId: pictureDto?.id);
|
||||
final fileName = pictureDto != null ? '${customerDto.customerNumber}_${pictureDto.id}.pdf' : '${customerDto.customerNumber}.pdf';
|
||||
await downloadFile(bytes, fileName);
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => Dialog(
|
||||
backgroundColor: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.customerWidgetDownloadInProgress,
|
||||
style: TextStyle(
|
||||
fontFamily: _generalStyle.fontFamily,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final bytes = await _customerController.export(customerId: customerDto.id, pictureId: pictureDto?.id);
|
||||
final fileName = pictureDto != null ? '${customerDto.customerNumber}_${pictureDto.id}.pdf' : '${customerDto.customerNumber}.pdf';
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
await downloadFile(bytes, fileName);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,13 +72,13 @@ class _PictureWidgetState extends State<PictureWidget> {
|
||||
if (snapshot.hasData) {
|
||||
CustomerDto? dto = snapshot.data;
|
||||
if (dto == null) {
|
||||
return GeneralErrorWidget(error: "FIXME"); // FIXME: set error text data not found
|
||||
return GeneralErrorWidget(error: AppLocalizations.of(context)!.customerWidgetNotFound);
|
||||
}
|
||||
_customerDto = dto;
|
||||
_selectedPicture ??= dto.pictures.firstWhere((p) => p.id == widget.pictureId);
|
||||
_selectedPicture ??= _customerDto.pictures.firstOrNull;
|
||||
if (_selectedPicture == null) {
|
||||
return GeneralErrorWidget(error: "FIXME"); // FIXME: set error text data not found
|
||||
return GeneralErrorWidget(error: AppLocalizations.of(context)!.pictureWidgetNotFound);
|
||||
}
|
||||
return _body(context, _selectedPicture!);
|
||||
} else if (snapshot.hasError) {
|
||||
|
||||
@@ -117,7 +117,6 @@ void main() {
|
||||
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
|
||||
provideMockedNetworkImages(() async {
|
||||
await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1${GlobalRouter.pathPicture}/1");
|
||||
await tester.pump(Duration(seconds: 10));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The comment field should be empty but the label should exist
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
20d9c068b272d7a2718d0d8e3e04271e
|
||||
Reference in New Issue
Block a user