8 Commits

Author SHA1 Message Date
verboomp
168fc986f2 Tweaking cusomter list 2026-02-10 11:13:16 +01:00
verboomp
a6216f6e81 tweaking download 2026-02-06 14:41:16 +01:00
verboomp
2955a7eb1c tweaking download 2026-02-06 13:45:35 +01:00
verboomp
7819b963f2 Fix image in pdf using the EXIF image header 2026-02-06 12:11:04 +01:00
verboomp
5e941753e2 Tweaking padding in customer list and update look and feel pdf export 2026-02-06 11:09:25 +01:00
verboomp
9c439e21ee Fix bug wrong orientation caused by EXIF 2026-02-06 10:01:45 +01:00
verboomp
b6158d933f Fix bug wrong orientation caused by EXIF 2026-02-06 09:37:03 +01:00
verboomp
bb4d7fbf68 Improved by resolving the last two fixme 2026-02-05 09:25:13 +01:00
17 changed files with 346 additions and 84 deletions

4
Jenkinsfile vendored
View File

@@ -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 {

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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");
}

View File

@@ -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();
}
}

View File

@@ -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']),
);
}

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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…';
}

View File

@@ -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),

View File

@@ -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;
}
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -0,0 +1 @@
20d9c068b272d7a2718d0d8e3e04271e