diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/AbstractService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/AbstractService.java index 0a1d7f7..8bbb990 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/AbstractService.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/AbstractService.java @@ -1,12 +1,17 @@ package marketing.heyday.hartmann.fotodocumentation.core.service; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import jakarta.annotation.Resource; import jakarta.ejb.EJB; import jakarta.ejb.EJBContext; import jakarta.ejb.SessionContext; import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.PersistenceContext; import marketing.heyday.hartmann.fotodocumentation.core.query.QueryService; +import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState; /** * @@ -19,7 +24,8 @@ import marketing.heyday.hartmann.fotodocumentation.core.query.QueryService; */ public abstract class AbstractService { - + private static final Log LOG = LogFactory.getLog(AbstractService.class); + @Resource protected EJBContext ejbContext; @@ -31,5 +37,17 @@ public abstract class AbstractService { @EJB protected QueryService queryService; - + + protected StorageState delete(Class type, Long id) { + try { + T entity = entityManager.getReference(type, id); + entityManager.remove(entity); + entityManager.flush(); + return StorageState.OK; + } catch (EntityNotFoundException e) { + LOG.warn("Failed to delete entity " + type + " not found " + id, e); + ejbContext.setRollbackOnly(); + return StorageState.NOT_FOUND; + } + } } diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/PictureService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/PictureService.java index 62213cd..671f567 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/PictureService.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/PictureService.java @@ -4,7 +4,7 @@ import jakarta.annotation.security.PermitAll; import jakarta.ejb.LocalBean; import jakarta.ejb.Stateless; import marketing.heyday.hartmann.fotodocumentation.core.model.Picture; -import marketing.heyday.hartmann.fotodocumentation.rest.vo.PictureValue; +import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState; /** * @@ -20,12 +20,7 @@ import marketing.heyday.hartmann.fotodocumentation.rest.vo.PictureValue; @PermitAll public class PictureService extends AbstractService { - public PictureValue get(Long id) { - Picture picture = entityManager.find(Picture.class, id); - if (picture == null) { - return null; - } - - return PictureValue.builder(picture); + public StorageState delete(Long id) { + return super.delete(Picture.class, id); } } diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/StorageUtils.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/StorageUtils.java new file mode 100644 index 0000000..431c4f6 --- /dev/null +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/StorageUtils.java @@ -0,0 +1,23 @@ +package marketing.heyday.hartmann.fotodocumentation.core.utils; + +/** + * + *

Copyright: Copyright (c) 2024

+ *

Company: heyday Marketing GmbH

+ * @author Patrick Verboom + * @version 1.0 + * + * created: 27 Jan 2026 + */ + +public class StorageUtils { + + public enum StorageState { + OK, + DUPLICATE, + FORBIDDEN, + NOT_FOUND, + ERROR, + ; + } +} diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/PictureResource.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/PictureResource.java index f9d2cea..265e773 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/PictureResource.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/PictureResource.java @@ -1,25 +1,20 @@ package marketing.heyday.hartmann.fotodocumentation.rest; -import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.jboss.resteasy.annotations.GZIP; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.ejb.EJB; import jakarta.enterprise.context.RequestScoped; -import jakarta.ws.rs.GET; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.ResponseBuilder; +import jakarta.ws.rs.core.Response.Status; import marketing.heyday.hartmann.fotodocumentation.core.service.PictureService; -import marketing.heyday.hartmann.fotodocumentation.rest.vo.PictureValue; +import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState; /** * @@ -38,15 +33,25 @@ public class PictureResource { @EJB private PictureService pictureService; - @GZIP - @GET + @DELETE @Path("{id}") - @Produces(JSON_OUT) - @Operation(summary = "Get picture value") - @ApiResponse(responseCode = "200", description = "Successfully retrieved picture value", content = @Content(mediaType = JSON_OUT, array = @ArraySchema(schema = @Schema(implementation = PictureValue.class)))) - public Response doGetDetailCustomer(@PathParam("id") Long id) { - LOG.debug("Get Picture details for id " + id); - var retVal = pictureService.get(id); - return Response.ok().entity(retVal).build(); + @Operation(summary = "Delete picture from database") + @ApiResponse(responseCode = "200", description = "Task successfully deleted") + @ApiResponse(responseCode = "404", description = "Task not found") + @ApiResponse(responseCode = "403", description = "Insufficient permissions") + public Response doDelete(@PathParam("id") Long id) { + LOG.debug("Delete picture with id " + id); + var state = pictureService.delete(id); + return deleteResponse(state).build(); + } + + protected ResponseBuilder deleteResponse(StorageState state) { + return switch(state) { + case OK -> Response.status(Status.OK); + case DUPLICATE -> Response.status(Status.CONFLICT); + case NOT_FOUND -> Response.status(Status.NOT_FOUND); + case FORBIDDEN -> Response.status(Status.FORBIDDEN); + default -> Response.status(Status.INTERNAL_SERVER_ERROR); + }; } } diff --git a/hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-1.0.0-SNAPSHOT.war b/hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-1.0.0-SNAPSHOT.war index 133c95d..9d71d4a 100644 Binary files a/hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-1.0.0-SNAPSHOT.war and b/hartmann-foto-documentation-docker/src/main/docker/hartmann-foto-documentation-web-1.0.0-SNAPSHOT.war differ diff --git a/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerResourceTest.java b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerResourceTest.java index 0388450..9d97bf3 100644 --- a/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerResourceTest.java +++ b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/CustomerResourceTest.java @@ -27,7 +27,6 @@ import org.junit.jupiter.api.TestMethodOrder; public class CustomerResourceTest extends AbstractRestTest { private static final Log LOG = LogFactory.getLog(CustomerResourceTest.class); private static final String PATH = "api/customer"; - private static final String BASE_UPLOAD = "src/test/resources/upload/"; private static final String BASE_DOWNLOAD = "json/CustomerResourceTest-"; @BeforeAll diff --git a/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/PictureResourceTest.java b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/PictureResourceTest.java new file mode 100644 index 0000000..3d35da3 --- /dev/null +++ b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/PictureResourceTest.java @@ -0,0 +1,75 @@ +package marketing.heyday.hartmann.fotodocumentation.rest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.client.fluent.Request; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +/** + * + *

Copyright: Copyright (c) 2024

+ *

Company: heyday Marketing GmbH

+ * @author Patrick Verboom + * @version 1.0 + * + * created: 14 Nov 2024 + */ +@TestMethodOrder(OrderAnnotation.class) +public class PictureResourceTest extends AbstractRestTest { + private static final Log LOG = LogFactory.getLog(PictureResourceTest.class); + private static final String PATH = "api/picture"; + + @BeforeAll + public static void init() { + initDB(); + } + + @Test + @Order(1) + public void doDelete() throws IOException { + LOG.info("doDelete"); + + assertEquals(5, pictureCount()); + + String path = deploymentURL + PATH + "/1"; + Request request = Request.Delete(path).addHeader("Accept", "application/json; charset=utf-8") + .addHeader("Authorization", getAuthorization()); + + HttpResponse httpResponse = executeRequest(request); + int code = httpResponse.getStatusLine().getStatusCode(); + assertEquals(200, code); + + assertEquals(4, pictureCount()); + } + + @Test + @Order(1) + public void doDeleteNotFound() throws IOException { + LOG.info("doDeleteNotFound"); + + assertEquals(5, pictureCount()); + + String path = deploymentURL + PATH + "/6000"; + Request request = Request.Delete(path).addHeader("Accept", "application/json; charset=utf-8") + .addHeader("Authorization", getAuthorization()); + + HttpResponse httpResponse = executeRequest(request); + int code = httpResponse.getStatusLine().getStatusCode(); + assertEquals(404, code); + + assertEquals(5, pictureCount()); + } + + private int pictureCount() { + return getCount("select count(*) from picture"); + } +} diff --git a/hartmann-foto-documentation-docker/src/test/resources/json/CustomerResourceTest-dogetAll.json b/hartmann-foto-documentation-docker/src/test/resources/json/CustomerResourceTest-dogetAll.json index b5aab6b..3c12325 100644 --- a/hartmann-foto-documentation-docker/src/test/resources/json/CustomerResourceTest-dogetAll.json +++ b/hartmann-foto-documentation-docker/src/test/resources/json/CustomerResourceTest-dogetAll.json @@ -1,17 +1,20 @@ [ { - "name": "Meier Apotheke", - "customerNumber": "2345", - "amountOfPicture": 1 - }, - { + "id": 1, "name": "Müller Apotheke", "customerNumber": "1234", - "amountOfPicture": 2 + "lastUpdateDate": 1729764570000 }, { + "id": 2, + "name": "Meier Apotheke", + "customerNumber": "2345", + "lastUpdateDate": 1729764570000 + }, + { + "id": 3, "name": "Schmidt Apotheke", "customerNumber": "3456", - "amountOfPicture": 2 + "lastUpdateDate": 1729764570000 } ] \ No newline at end of file diff --git a/hartmann-foto-documentation-frontend/lib/controller/picture_controller.dart b/hartmann-foto-documentation-frontend/lib/controller/picture_controller.dart index af168ce..4f08a95 100644 --- a/hartmann-foto-documentation-frontend/lib/controller/picture_controller.dart +++ b/hartmann-foto-documentation-frontend/lib/controller/picture_controller.dart @@ -2,22 +2,15 @@ import 'package:fotodocumentation/controller/base_controller.dart'; import 'package:fotodocumentation/dto/customer_dto.dart'; abstract interface class PictureController { - Future get({required int id}); Future delete(PictureDto dto); } class PictureControllerImpl extends BaseController implements PictureController { final String path = "picture"; - @override - Future get({required int id}) { - String uriStr = '${uriUtils.getBaseUrl()}$path/$id'; - return runGetWithAuth(uriStr, (json) => PictureDto.fromJson(json)); - } - @override Future delete(PictureDto dto) { - // TODO: implement delete - throw UnimplementedError(); + String uriStr = '${uriUtils.getBaseUrl()}$path/${dto.id}'; + return runDeleteWithAuth(uriStr); } } diff --git a/hartmann-foto-documentation-frontend/lib/dto/customer_dto.dart b/hartmann-foto-documentation-frontend/lib/dto/customer_dto.dart index 8f78a18..0178b31 100644 --- a/hartmann-foto-documentation-frontend/lib/dto/customer_dto.dart +++ b/hartmann-foto-documentation-frontend/lib/dto/customer_dto.dart @@ -6,7 +6,7 @@ class CustomerListDto { final String customerNumber; final DateTime? lastUpdateDate; - CustomerListDto({required this.id, required this.name, required this.customerNumber, required this.lastUpdateDate}); + CustomerListDto({required this.id, required this.name, required this.customerNumber, this.lastUpdateDate}); /// Create from JSON response factory CustomerListDto.fromJson(Map json) { @@ -45,8 +45,10 @@ class PictureDto { final String image; final DateTime pictureDate; final String? username; + final CustomerListDto customerListDto; -PictureDto({required this.id, required this.comment, required this.category, required this.image, required this.pictureDate, required this.username}); + PictureDto( + {required this.id, required this.comment, required this.category, required this.image, required this.pictureDate, required this.username, required this.customerListDto}); /// Create from JSON response factory PictureDto.fromJson(Map json) { @@ -57,6 +59,7 @@ PictureDto({required this.id, required this.comment, required this.category, req image: json['image'] as String, pictureDate: DateTimeUtils.toDateTime(json['pictureDate']) ?? DateTime.now(), username: json['username'] as String?, + customerListDto: CustomerListDto.fromJson(json['customer']), ); } } diff --git a/hartmann-foto-documentation-frontend/lib/pages/customer/customer_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/customer/customer_widget.dart index de766e3..b9b3749 100644 --- a/hartmann-foto-documentation-frontend/lib/pages/customer/customer_widget.dart +++ b/hartmann-foto-documentation-frontend/lib/pages/customer/customer_widget.dart @@ -1,6 +1,8 @@ import 'dart:convert' show base64Decode; import 'package:flutter/material.dart'; +import 'package:fotodocumentation/controller/picture_controller.dart'; +import 'package:fotodocumentation/pages/customer/picture_delete_dialog.dart'; import 'package:go_router/go_router.dart'; @@ -16,6 +18,7 @@ import 'package:fotodocumentation/pages/ui_utils/component/waiting_widget.dart'; import 'package:fotodocumentation/pages/ui_utils/general_style.dart'; import 'package:fotodocumentation/utils/di_container.dart'; import 'package:fotodocumentation/utils/global_router.dart'; +import 'package:fotodocumentation/pages/customer/picture_widget.dart'; class CustomerWidget extends StatefulWidget { final int customerId; @@ -27,6 +30,7 @@ class CustomerWidget extends StatefulWidget { class _CustomerWidgetState extends State { CustomerController get _customerController => DiContainer.get(); + PictureController get _pictureController => DiContainer.get(); GeneralStyle get _generalStyle => DiContainer.get(); late Future _dto; @@ -103,8 +107,10 @@ class _CustomerWidgetState extends State { ); } - Widget _customerWidget(CustomerDto dto) { - var dtos = dto.pictures; + Widget _customerWidget(CustomerDto customerDto) { + var pictureDtos = customerDto.pictures; + + pictureDtos.sort((a, b) => b.pictureDate.compareTo(a.pictureDate)); return Card( margin: EdgeInsets.zero, @@ -118,9 +124,9 @@ class _CustomerWidgetState extends State { Expanded( child: ListView.separated( padding: const EdgeInsets.only(top: 8.0), - itemCount: dtos.length, + itemCount: pictureDtos.length, itemBuilder: (BuildContext context, int index) { - return _tableDataRow(context, dtos[index]); + return _tableDataRow(context, customerDto, pictureDtos[index]); }, separatorBuilder: (BuildContext context, int index) => Divider(color: _generalStyle.secondaryWidgetBackgroundColor), ), @@ -173,21 +179,22 @@ class _CustomerWidgetState extends State { ), ), ), + const SizedBox(width: 48), ], ), ); } - Widget _tableDataRow(BuildContext context, PictureDto dto) { + Widget _tableDataRow(BuildContext context, CustomerDto customerDto, PictureDto pictureDto) { final dataStyle = TextStyle( fontFamily: _generalStyle.fontFamily, fontSize: 16.0, color: _generalStyle.secondaryTextLabelColor, ); - final dateStr = _dateFormat.format(dto.pictureDate); + final dateStr = _dateFormat.format(pictureDto.pictureDate); return InkWell( - onTap: () => _actionSelect(context, dto), + onTap: () => _actionSelect(context, customerDto, pictureDto), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Row( @@ -201,7 +208,7 @@ class _CustomerWidgetState extends State { child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 70, maxHeight: 70), child: Image.memory( - base64Decode(dto.image), + base64Decode(pictureDto.image), fit: BoxFit.contain, ), ), @@ -211,7 +218,7 @@ class _CustomerWidgetState extends State { flex: 3, child: Align( alignment: Alignment.centerLeft, - child: Text(dto.comment ?? "", style: dataStyle), + child: Text(pictureDto.comment ?? "", style: dataStyle), ), ), Expanded( @@ -221,14 +228,56 @@ class _CustomerWidgetState extends State { child: Text(dateStr, style: dataStyle), ), ), + SizedBox( + width: 48, + child: IconButton( + icon: Icon( + Icons.delete_outline, + color: _generalStyle.errorColor, + ), + onPressed: () => _actionDelete(context, customerDto, pictureDto), + ), + ), ], ), ), ); } - Future _actionSelect(BuildContext context, PictureDto dto) async { - context.go("${GlobalRouter.pathPicture}/${dto.id}"); + Future _actionDelete(BuildContext context, CustomerDto customerDto, PictureDto pictureDto) async { + final confirmed = await showDialog( + context: context, + builder: (BuildContext context) { + return PictureDeleteDialog(); + }, + ); + + if (confirmed == true) { + _pictureController.delete(pictureDto); + setState(() { + _dto = _customerController.get(id: widget.customerId); + }); + } + } + + Future _actionSelect(BuildContext context, CustomerDto customerDto, PictureDto pictureDto) async { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + backgroundColor: _generalStyle.pageBackgroundColor, + insetPadding: const EdgeInsets.all(24), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.9, + child: PictureWidget(customerDto: customerDto, pictureDto: pictureDto), + ), + ), + ); + }, + ); } Widget _backButton(BuildContext context) { diff --git a/hartmann-foto-documentation-frontend/lib/pages/customer/picture_delete_dialog.dart b/hartmann-foto-documentation-frontend/lib/pages/customer/picture_delete_dialog.dart new file mode 100644 index 0000000..e501113 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/customer/picture_delete_dialog.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:fotodocumentation/l10n/app_localizations.dart'; +import 'package:fotodocumentation/pages/ui_utils/general_style.dart'; +import 'package:fotodocumentation/utils/di_container.dart'; + +class PictureDeleteDialog extends StatelessWidget { + GeneralStyle get _generalStyle => DiContainer.get(); + + const PictureDeleteDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text( + AppLocalizations.of(context)!.deleteDialogTitle, + style: TextStyle( + fontFamily: _generalStyle.fontFamily, + fontWeight: FontWeight.bold, + color: _generalStyle.secondaryWidgetBackgroundColor, + ), + ), + content: Text( + AppLocalizations.of(context)!.deleteDialogText, + style: TextStyle( + fontFamily: _generalStyle.fontFamily, + color: _generalStyle.secondaryTextLabelColor, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text( + AppLocalizations.of(context)!.deleteDialogButtonCancel, + style: TextStyle( + fontFamily: _generalStyle.fontFamily, + color: _generalStyle.secondaryTextLabelColor, + ), + ), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text( + AppLocalizations.of(context)!.deleteDialogButtonApprove, + style: TextStyle( + fontFamily: _generalStyle.fontFamily, + color: _generalStyle.errorColor, + ), + ), + ), + ], + ); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/customer/picture_fullscreen_dialog.dart b/hartmann-foto-documentation-frontend/lib/pages/customer/picture_fullscreen_dialog.dart new file mode 100644 index 0000000..6b7fdf1 --- /dev/null +++ b/hartmann-foto-documentation-frontend/lib/pages/customer/picture_fullscreen_dialog.dart @@ -0,0 +1,54 @@ +import 'dart:convert' show base64Decode; + +import 'package:flutter/material.dart'; +import 'package:fotodocumentation/dto/customer_dto.dart'; +import 'package:fotodocumentation/pages/ui_utils/general_style.dart'; +import 'package:fotodocumentation/utils/di_container.dart'; + +class PictureFullscreenDialog extends StatelessWidget { + GeneralStyle get _generalStyle => DiContainer.get(); + + final PictureDto dto; + + const PictureFullscreenDialog({super.key, required this.dto}); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: _generalStyle.pageBackgroundColor, + insetPadding: const EdgeInsets.all(24), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.9, + child: Stack( + fit: StackFit.expand, + children: [ + InteractiveViewer( + panEnabled: true, + scaleEnabled: true, + minScale: 0.5, + maxScale: 5.0, + child: Center( + child: Image.memory( + base64Decode(dto.image), + fit: BoxFit.contain, + ), + ), + ), + Positioned( + top: 16, + right: 16, + child: IconButton( + icon: Icon(Icons.close, color: _generalStyle.primaryButtonBackgroundColor, size: 32), + onPressed: () => Navigator.of(context).pop(), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/hartmann-foto-documentation-frontend/lib/pages/customer/picture_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/customer/picture_widget.dart index 30877b7..d30537a 100644 --- a/hartmann-foto-documentation-frontend/lib/pages/customer/picture_widget.dart +++ b/hartmann-foto-documentation-frontend/lib/pages/customer/picture_widget.dart @@ -1,31 +1,25 @@ import 'dart:convert' show base64Decode; import 'package:flutter/material.dart'; -import 'package:fotodocumentation/controller/picture_controller.dart'; - -import 'package:intl/intl.dart'; - -import 'package:fotodocumentation/controller/base_controller.dart'; import 'package:fotodocumentation/dto/customer_dto.dart'; -import 'package:fotodocumentation/l10n/app_localizations.dart'; -import 'package:fotodocumentation/pages/ui_utils/component/general_error_widget.dart'; -import 'package:fotodocumentation/pages/ui_utils/component/waiting_widget.dart'; +import 'package:fotodocumentation/pages/customer/picture_fullscreen_dialog.dart'; import 'package:fotodocumentation/pages/ui_utils/general_style.dart'; import 'package:fotodocumentation/utils/di_container.dart'; +import 'package:intl/intl.dart'; class PictureWidget extends StatefulWidget { - final int id; - const PictureWidget({super.key, required this.id}); + final CustomerDto customerDto; + final PictureDto pictureDto; + const PictureWidget({super.key, required this.customerDto, required this.pictureDto}); @override State createState() => _PictureWidgetState(); } class _PictureWidgetState extends State { - PictureController get _pictureController => DiContainer.get(); GeneralStyle get _generalStyle => DiContainer.get(); - late Future _dto; + late PictureDto _selectedPicture; late DateFormat _dateFormat; final ScrollController _commentScrollController = ScrollController(); @@ -33,7 +27,7 @@ class _PictureWidgetState extends State { void initState() { super.initState(); _dateFormat = DateFormat('dd MMMM yyyy'); - _dto = _pictureController.get(id: widget.id); + _selectedPicture = widget.pictureDto; } @override @@ -44,91 +38,164 @@ class _PictureWidgetState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Container( - color: _generalStyle.pageBackgroundColor, - child: Padding( - padding: const EdgeInsets.only(top: 8.0, left: 50.0, right: 50.0, bottom: 8.0), - child: _body(context), + final pictures = widget.customerDto.pictures; + final currentIndex = pictures.indexWhere((p) => p.id == _selectedPicture.id); + final hasPrevious = currentIndex > 0; + final hasNext = currentIndex < pictures.length - 1; + + return Stack( + children: [ + Container( + width: double.infinity, + height: double.infinity, + color: _generalStyle.pageBackgroundColor, + child: Padding( + padding: const EdgeInsets.only(top: 50.0, left: 50.0, right: 50.0, bottom: 8.0), + child: _body(context), + ), ), - ), + // Left navigation button + if (hasPrevious) + Positioned( + left: 0, + top: 0, + bottom: 0, + child: GestureDetector( + onTap: () => _navigateToPicture(currentIndex - 1), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Container( + width: 50, + color: Colors.transparent, + child: Center( + child: Container( + decoration: BoxDecoration( + color: _generalStyle.primaryButtonBackgroundColor, + shape: BoxShape.circle, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.chevron_left, + color: _generalStyle.primaryButtonTextColor, + size: 32, + ), + ), + ), + ), + ), + ), + ), + ), + // Right navigation button + if (hasNext) + Positioned( + right: 0, + top: 0, + bottom: 0, + child: GestureDetector( + onTap: () => _navigateToPicture(currentIndex + 1), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Container( + width: 50, + color: Colors.transparent, + child: Center( + child: Container( + decoration: BoxDecoration( + color: _generalStyle.primaryButtonBackgroundColor, + shape: BoxShape.circle, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.chevron_right, + color: _generalStyle.primaryButtonTextColor, + size: 32, + ), + ), + ), + ), + ), + ), + ), + ), + // Close button + Positioned( + top: 16, + right: 16, + child: Container( + decoration: BoxDecoration( + color: _generalStyle.primaryButtonBackgroundColor, + shape: BoxShape.circle, + ), + child: IconButton( + icon: Icon(Icons.close, color: _generalStyle.primaryButtonTextColor, size: 24), + onPressed: () => Navigator.of(context).pop(), + ), + ), + ), + ], ); } - Widget _body(BuildContext context) { - return FutureBuilder( - future: _dto, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const WaitingWidget(); - } - if (snapshot.hasData) { - PictureDto? dto = snapshot.data; + void _navigateToPicture(int index) { + final pictures = widget.customerDto.pictures; + if (index >= 0 && index < pictures.length) { + setState(() { + _selectedPicture = pictures[index]; + }); + } + } - return _mainWidget(dto); - } else if (snapshot.hasError) { - var error = snapshot.error; - return (error is ServerError) ? GeneralErrorWidget.fromServerError(error) : GeneralErrorWidget(error: snapshot.error.toString()); - } - return const WaitingWidget(); + Widget _body(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final isNarrow = constraints.maxWidth < 800; + return SingleChildScrollView( + child: isNarrow + ? Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _imageWidget(_selectedPicture), + const SizedBox(height: 32), + _contentWidget(_selectedPicture), + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _imageWidget(_selectedPicture), + const SizedBox(width: 32), + Expanded(child: _contentWidget(_selectedPicture)), + ], + ), + ); }, ); } - Widget _mainWidget(PictureDto? dto) { - if (dto == null) { - return Text( - AppLocalizations.of(context)!.customerWidgetNotFound, - style: TextStyle( - fontFamily: _generalStyle.fontFamily, - fontWeight: FontWeight.bold, - fontSize: 20, - color: _generalStyle.secondaryWidgetBackgroundColor, - ), - ); - } - - return Card( - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(24.0), - child: LayoutBuilder( - builder: (context, constraints) { - final isNarrow = constraints.maxWidth < 800; - return SingleChildScrollView( - child: isNarrow - ? Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _imageWidget(dto), - const SizedBox(height: 32), - _contentWidget(dto), - ], - ) - : Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _imageWidget(dto), - const SizedBox(width: 32), - _contentWidget(dto), - ], - ), - ); - }, + Widget _imageWidget(PictureDto dto) { + return GestureDetector( + onTap: () => _showFullscreenImage(dto), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Image.memory( + base64Decode(dto.image), + fit: BoxFit.contain, ), ), ); } - Widget _imageWidget(PictureDto dto) { - return Image.memory( - base64Decode(dto.image), - fit: BoxFit.contain, + void _showFullscreenImage(PictureDto dto) { + showDialog( + context: context, + builder: (BuildContext context) { + return PictureFullscreenDialog(dto: dto); + }, ); } @@ -167,7 +234,7 @@ class _PictureWidgetState extends State { Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( - "Name of apotheke", + dto.customerListDto.name, style: contentStyle, ), ), @@ -181,7 +248,7 @@ class _PictureWidgetState extends State { Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( - "123445587474873", + dto.customerListDto.customerNumber, style: contentStyle, ), ), @@ -209,7 +276,7 @@ class _PictureWidgetState extends State { Padding( padding: const EdgeInsets.only(top: 4.0), child: Container( - width: 300, + width: double.infinity, height: 150, decoration: BoxDecoration( border: Border.all(color: _generalStyle.secondaryTextLabelColor.withValues(alpha: 0.3)), diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/general_style.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/general_style.dart index 99da76d..546db65 100644 --- a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/general_style.dart +++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/general_style.dart @@ -14,6 +14,8 @@ abstract interface class GeneralStyle { Color get pageBackgroundColor; + Color get primaryCardColor; + Color get errorColor; String get fontFamily; @@ -44,6 +46,9 @@ class GeneralStyleImpl implements GeneralStyle { @override Color get primaryButtonTextColor => Colors.white; + @override + Color get primaryCardColor => Colors.white; + @override Color get pageBackgroundColor => const Color(0xFFF5F5F5); diff --git a/hartmann-foto-documentation-frontend/lib/utils/global_router.dart b/hartmann-foto-documentation-frontend/lib/utils/global_router.dart index 37d99d3..e0c403b 100644 --- a/hartmann-foto-documentation-frontend/lib/utils/global_router.dart +++ b/hartmann-foto-documentation-frontend/lib/utils/global_router.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:fotodocumentation/main.dart'; import 'package:fotodocumentation/pages/customer/customer_list_widget.dart'; import 'package:fotodocumentation/pages/customer/customer_widget.dart'; -import 'package:fotodocumentation/pages/customer/picture_widget.dart'; import 'package:fotodocumentation/pages/login/login_widget.dart'; import 'package:fotodocumentation/utils/di_container.dart'; import 'package:fotodocumentation/utils/login_credentials.dart'; @@ -17,7 +16,6 @@ class GlobalRouter { static final String pathHome = "/home"; static final String pathCustomer = "/customer"; - static final String pathPicture = "/picture"; static final String pathLogin = "/login"; static final GoRouter router = createRouter(pathHome); @@ -47,14 +45,6 @@ class GlobalRouter { return CustomerWidget(customerId: id ?? -1); }, ), - GoRoute( - path: "$pathPicture/:id", - builder: (context, state) { - var idStr = state.pathParameters['id']; - var id = idStr == null ? null : int.tryParse(idStr); - return PictureWidget(id: id ?? -1); - }, - ), ], redirect: (context, state) { var uriStr = state.uri.toString();