pictureOpt = customer.getQuestionnaires().stream().filter(p -> p.getQuestionnaireId().equals(questionnaireId)).findFirst();
+ questionnaires = pictureOpt.map(Arrays::asList).orElse(questionnaires);
+ }
+
+ return excelUtils.create(customer, questionnaires);
+ }
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnairePublishService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnairePublishService.java
new file mode 100644
index 0000000..5acffcf
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnairePublishService.java
@@ -0,0 +1,26 @@
+package marketing.heyday.hartmann.fotodocumentation.core.service;
+
+import jakarta.annotation.security.PermitAll;
+import jakarta.ejb.LocalBean;
+import jakarta.ejb.Stateless;
+import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnairePublishValue;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 18 Feb 2026
+ */
+@Stateless
+@LocalBean
+@PermitAll
+public class QuestionnairePublishService extends AbstractService {
+
+ public boolean publish(QuestionnairePublishValue value) {
+ // FIXME: implement me
+ return false;
+ }
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnaireService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnaireService.java
new file mode 100644
index 0000000..edeefca
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnaireService.java
@@ -0,0 +1,44 @@
+package marketing.heyday.hartmann.fotodocumentation.core.service;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import jakarta.annotation.security.PermitAll;
+import jakarta.ejb.LocalBean;
+import jakarta.ejb.Stateless;
+import jakarta.persistence.EntityNotFoundException;
+import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
+import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 19 Jan 2026
+ */
+@Stateless
+@LocalBean
+@PermitAll
+public class QuestionnaireService extends AbstractService {
+ private static final Log LOG = LogFactory.getLog(QuestionnaireService.class);
+
+ public StorageState delete(Long id) {
+ return super.delete(Questionnaire.class, id);
+ }
+
+ public StorageState updateEvaluationStatus(Long id, Integer value) {
+ try {
+ Questionnaire entity = entityManager.getReference(Questionnaire.class, id);
+ entity.setEvaluation(value);
+ entityManager.flush();
+ return StorageState.OK;
+ } catch (EntityNotFoundException e) {
+ LOG.warn("Failed to update evaluation value 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/utils/ExcelUtils.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ExcelUtils.java
new file mode 100644
index 0000000..435474d
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ExcelUtils.java
@@ -0,0 +1,29 @@
+package marketing.heyday.hartmann.fotodocumentation.core.utils;
+
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
+import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 2 Feb 2026
+ */
+@SuppressWarnings({ "java:S818", "squid:S818", "squid:S109" })
+public class ExcelUtils {
+ private static final Log LOG = LogFactory.getLog(ExcelUtils.class);
+
+ public byte[] create(QuestionnaireCustomer customer, List questionnaires) {
+ // FIXME: implement excel export
+ return new byte[0];
+ }
+
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnaireCustomerResource.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnaireCustomerResource.java
new file mode 100644
index 0000000..23911aa
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnaireCustomerResource.java
@@ -0,0 +1,88 @@
+package marketing.heyday.hartmann.fotodocumentation.rest;
+
+import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT;
+
+import java.io.OutputStream;
+
+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.*;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+import jakarta.ws.rs.core.StreamingOutput;
+import marketing.heyday.hartmann.fotodocumentation.core.service.QuestionnaireCustomerService;
+import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnaireCustomerListValue;
+import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnaireCustomerValue;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 21 Jan 2026
+ */
+@RequestScoped
+@Path("questionnairecustomer")
+public class QuestionnaireCustomerResource {
+ private static final Log LOG = LogFactory.getLog(QuestionnaireCustomerResource.class);
+
+ @EJB
+ private QuestionnaireCustomerService questionnaireCustomerService;
+
+ @GZIP
+ @GET
+ @Path("")
+ @Produces(JSON_OUT)
+ @Operation(summary = "Get customer list")
+ @ApiResponse(responseCode = "200", description = "Successfully retrieved customer list", content = @Content(mediaType = JSON_OUT, array = @ArraySchema(schema = @Schema(implementation = QuestionnaireCustomerListValue.class))))
+ public Response doGetCustomerList(@QueryParam("query") String query, @QueryParam("startsWith") String startsWith) {
+ LOG.debug("Query customers for query " + query + " startsWith: " + startsWith);
+ var retVal = questionnaireCustomerService.getAll(query, startsWith);
+ return Response.ok().entity(retVal).build();
+ }
+
+ @GZIP
+ @GET
+ @Path("{id}")
+ @Produces(JSON_OUT)
+ @Operation(summary = "Get customer value")
+ @ApiResponse(responseCode = "200", description = "Successfully retrieved customer value", content = @Content(mediaType = JSON_OUT, array = @ArraySchema(schema = @Schema(implementation = QuestionnaireCustomerValue.class))))
+ public Response doGetDetailCustomer(@PathParam("id") Long id) {
+ LOG.debug("Get Customer details for id " + id);
+
+ var retVal = questionnaireCustomerService.get(id);
+ return Response.ok().entity(retVal).build();
+ }
+
+ @GZIP
+ @GET
+ @Path("export/{id}")
+ @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ @Operation(summary = "Get Export")
+ @ApiResponse(responseCode = "200", description = "Successfully retrieved export")
+ public Response doExport(@PathParam("id") Long id, @QueryParam("questionnaire") Long questionnaireId) {
+ LOG.debug("Create export for customer " + id + " with optional param " + questionnaireId);
+ byte[] pdf = questionnaireCustomerService.getExport(id, questionnaireId);
+
+ if (pdf.length == 0) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ StreamingOutput streamingOutput = (OutputStream output) -> {
+ LOG.debug("Start writing content to OutputStream available bytes");
+ output.write(pdf);
+ };
+ return Response.status(Status.OK).entity(streamingOutput).build();
+ }
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnairePublishResource.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnairePublishResource.java
new file mode 100644
index 0000000..021ef0c
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnairePublishResource.java
@@ -0,0 +1,46 @@
+package marketing.heyday.hartmann.fotodocumentation.rest;
+
+import org.jboss.resteasy.annotations.GZIP;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import jakarta.ejb.EJB;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+import marketing.heyday.hartmann.fotodocumentation.core.service.QuestionnairePublishService;
+import marketing.heyday.hartmann.fotodocumentation.rest.jackson.JsonSchemaValidate;
+import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnairePublishValue;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 18 Feb 2026
+ */
+
+@RequestScoped
+@Path("questionnaire-publish")
+public class QuestionnairePublishResource {
+
+ @EJB
+ private QuestionnairePublishService questionnairePublishService;
+
+ @GZIP
+ @POST
+ @Path("")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Add questionnaire to database")
+ @ApiResponse(responseCode = "200", description = "Add successfull")
+ public Response doAddQuestionnaire(@JsonSchemaValidate("schema/questionnaire_publish.json") QuestionnairePublishValue value) {
+ boolean success = questionnairePublishService.publish(value);
+ return success ? Response.ok().build() : Response.status(Status.BAD_REQUEST).build();
+ }
+}
\ No newline at end of file
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnaireResource.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnaireResource.java
new file mode 100644
index 0000000..714944b
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnaireResource.java
@@ -0,0 +1,70 @@
+package marketing.heyday.hartmann.fotodocumentation.rest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import jakarta.ejb.EJB;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.*;
+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.QuestionnaireService;
+import marketing.heyday.hartmann.fotodocumentation.core.utils.EvaluationUtil;
+import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 21 Jan 2026
+ */
+@RequestScoped
+@Path("questionnaire")
+public class QuestionnaireResource {
+ private static final Log LOG = LogFactory.getLog(QuestionnaireResource.class);
+
+ @EJB
+ private QuestionnaireService questionnaireService;
+
+ @Inject
+ private EvaluationUtil evaluationUtil;
+
+ @DELETE
+ @Path("{id}")
+ @Operation(summary = "Delete questionnaire 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 questionnaire with id " + id);
+ var state = questionnaireService.delete(id);
+ return deleteResponse(state).build();
+ }
+
+ @PUT
+ @Path("evaluation/{id}")
+ @Operation(summary = "Update evaluation for questionnaire data to database")
+ @ApiResponse(responseCode = "200", description = "Task successfully updated")
+ public Response doUpdateEvaluation(@PathParam("id") Long id, @QueryParam("evaluation") Integer value) {
+ if (evaluationUtil.isInValid(value)) {
+ return Response.status(Status.BAD_REQUEST).build();
+ }
+ StorageState state = questionnaireService.updateEvaluationStatus(id, value);
+ return deleteResponse(state).build();
+ }
+
+ protected ResponseBuilder deleteResponse(StorageState state) {
+ return switch (state) {
+ case OK -> Response.status(Status.OK);
+ case NOT_FOUND -> Response.status(Status.NOT_FOUND);
+ default -> Response.status(Status.INTERNAL_SERVER_ERROR);
+ };
+ }
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ApplicationConfigApi.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ApplicationConfigApi.java
index 50d38e2..7083e6f 100644
--- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ApplicationConfigApi.java
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ApplicationConfigApi.java
@@ -46,6 +46,10 @@ public class ApplicationConfigApi extends Application {
retVal.add(CustomerPictureResource.class);
retVal.add(CustomerResource.class);
retVal.add(PictureResource.class);
+
+ retVal.add(QuestionnairePublishResource.class);
+ retVal.add(QuestionnaireCustomerResource.class);
+ retVal.add(QuestionnaireResource.class);
LOG.info("returning rest api classes " + retVal);
return retVal;
}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnaireCustomerListValue.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnaireCustomerListValue.java
new file mode 100644
index 0000000..2c03f4a
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnaireCustomerListValue.java
@@ -0,0 +1,29 @@
+package marketing.heyday.hartmann.fotodocumentation.rest.vo;
+
+import java.util.Date;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
+import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 19 Jan 2026
+ */
+
+@Schema(name = "QuestionnaireCustomerList")
+public record QuestionnaireCustomerListValue(Long id, String name, String customerNumber, String zip, String city, Date lastUpdateDate) {
+
+ public static QuestionnaireCustomerListValue builder(QuestionnaireCustomer customer) {
+ if (customer == null) {
+ return null;
+ }
+ Date date = customer.getQuestionnaires().stream().map(Questionnaire::getQuestionnaireDate).sorted((p1, p2) -> p2.compareTo(p1)).findFirst().orElse(null);
+ return new QuestionnaireCustomerListValue(customer.getCustomerId(), customer.getName(), customer.getCustomerNumber(), customer.getZip(), customer.getCity(), date);
+ }
+}
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnaireCustomerValue.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnaireCustomerValue.java
new file mode 100644
index 0000000..5026480
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnaireCustomerValue.java
@@ -0,0 +1,27 @@
+package marketing.heyday.hartmann.fotodocumentation.rest.vo;
+
+import java.util.List;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 22 Jan 2026
+ */
+@Schema(name = "QuestionnaireCustomer")
+public record QuestionnaireCustomerValue(Long id, String name, String customerNumber, String city, String zip, List questionnaires) {
+
+ public static QuestionnaireCustomerValue builder(QuestionnaireCustomer customer) {
+ if (customer == null) {
+ return null;
+ }
+ List questionnaires = customer.getQuestionnaires().parallelStream().map(QuestionnaireValue::builder).filter(p -> p != null).toList();
+ return new QuestionnaireCustomerValue(customer.getCustomerId(), customer.getName(), customer.getCustomerNumber(), customer.getCity(), customer.getZip(), questionnaires);
+ }
+}
\ No newline at end of file
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnairePublishValue.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnairePublishValue.java
new file mode 100644
index 0000000..be41a72
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnairePublishValue.java
@@ -0,0 +1,21 @@
+package marketing.heyday.hartmann.fotodocumentation.rest.vo;
+
+import java.util.Date;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import marketing.heyday.hartmann.fotodocumentation.rest.jackson.SchemaValidated;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 18 Feb 2026
+ */
+
+@Schema(name = "QuestionnairePublish")
+public record QuestionnairePublishValue(String username, String pharmacyName, String customerNumber, Date date, String zip, String city, String comment, String category, String base64String) implements SchemaValidated {
+
+}
\ No newline at end of file
diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnaireValue.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnaireValue.java
new file mode 100644
index 0000000..a543531
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/vo/QuestionnaireValue.java
@@ -0,0 +1,28 @@
+package marketing.heyday.hartmann.fotodocumentation.rest.vo;
+
+import java.util.Date;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
+
+/**
+ *
+ * Copyright: Copyright (c) 2024
+ * Company: heyday Marketing GmbH
+ * @author Patrick Verboom
+ * @version 1.0
+ *
+ * created: 22 Jan 2026
+ */
+
+@Schema(name = "Questionnaire")
+public record QuestionnaireValue(Long id, String comment, String category, Date questionnaireDate, String username, Integer evaluation) {
+
+ public static QuestionnaireValue builder(Questionnaire questionnaire) {
+ if (questionnaire == null) {
+ return null;
+ }
+
+ return new QuestionnaireValue(questionnaire.getQuestionnaireId(), questionnaire.getComment(), questionnaire.getCategory(), questionnaire.getQuestionnaireDate(), questionnaire.getUsername(), questionnaire.getEvaluation());
+ }
+}
diff --git a/hartmann-foto-documentation-app/src/main/resources/META-INF/persistence.xml b/hartmann-foto-documentation-app/src/main/resources/META-INF/persistence.xml
index 17c2443..888382a 100644
--- a/hartmann-foto-documentation-app/src/main/resources/META-INF/persistence.xml
+++ b/hartmann-foto-documentation-app/src/main/resources/META-INF/persistence.xml
@@ -12,6 +12,9 @@
marketing.heyday.hartmann.fotodocumentation.core.model.Customer
marketing.heyday.hartmann.fotodocumentation.core.model.Picture
marketing.heyday.hartmann.fotodocumentation.core.model.JwtRefreshToken
+
+ marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer
+ marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire
diff --git a/hartmann-foto-documentation-app/src/main/resources/marketing/heyday/hartmann/fotodocumentation/core/db/migration/V5__questionnaire.sql b/hartmann-foto-documentation-app/src/main/resources/marketing/heyday/hartmann/fotodocumentation/core/db/migration/V5__questionnaire.sql
new file mode 100644
index 0000000..1baec19
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/resources/marketing/heyday/hartmann/fotodocumentation/core/db/migration/V5__questionnaire.sql
@@ -0,0 +1,37 @@
+
+create sequence IF NOT EXISTS questionnaire_customer_seq start 25;
+
+CREATE TABLE IF NOT EXISTS questionnaire_customer
+(
+ customer_id bigint PRIMARY KEY,
+ customer_number varchar(150) NOT NULL,
+ name varchar(150) NOT NULL,
+ zip varchar(150),
+ city varchar(150),
+ jpa_active boolean NOT NULL,
+ jpa_created timestamp NOT NULL,
+ jpa_updated timestamp NOT NULL,
+ jpa_version integer NOT NULL,
+ CONSTRAINT unq_questionnaire_customer_number UNIQUE(customer_number)
+);
+
+
+create sequence IF NOT EXISTS questionnaire_seq start 25;
+
+
+CREATE TABLE IF NOT EXISTS questionnaire
+(
+ questionnaire_id bigint PRIMARY KEY,
+ username varchar(150),
+ questionnaire_date timestamp NOT NULL,
+ comment text,
+ questions text,
+ category varchar(250),
+ evaluation bigint NOT NULL,
+ jpa_active boolean NOT NULL,
+ jpa_created timestamp NOT NULL,
+ jpa_updated timestamp NOT NULL,
+ jpa_version integer NOT NULL,
+ customer_id_fk bigint REFERENCES questionnaire_customer
+);
+
diff --git a/hartmann-foto-documentation-app/src/main/resources/schema/questionnaire_publish.json b/hartmann-foto-documentation-app/src/main/resources/schema/questionnaire_publish.json
new file mode 100644
index 0000000..fb08682
--- /dev/null
+++ b/hartmann-foto-documentation-app/src/main/resources/schema/questionnaire_publish.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Add Customer Picture",
+ "description": "Add a Customer Picture to the system",
+ "type": "object",
+ "properties": {
+ "username": {
+ "description": "The username from the user who uploads the picture",
+ "type": "string"
+ },
+ "pharmacyName": {
+ "description": "The Name from the pharmacy customer ",
+ "type": "string"
+ },
+ "customerNumber": {
+ "description": "The unique number from the pharmacy customer ",
+ "type": "string"
+ },
+ "date": {
+ "description": "The date when the picture is taken ",
+ "type": "string"
+ },
+ "comment": {
+ "description": "A free text comment field ",
+ "type": "string"
+ },
+ "zip": {
+ "description": "The zip from the customer",
+ "type": "string"
+ },
+ "city": {
+ "description": "The city from the customer",
+ "type": "string"
+ },
+ "base64String": {
+ "description": "The Picture content as base64 ",
+ "type": "string"
+ }
+ },
+ "required": [
+ "username",
+ "pharmacyName",
+ "customerNumber",
+ "date",
+ "comment",
+ "base64String"
+ ]
+}
\ No newline at end of file
diff --git a/hartmann-foto-documentation-frontend/lib/controller/questionnaire_controller.dart b/hartmann-foto-documentation-frontend/lib/controller/questionnaire_controller.dart
new file mode 100644
index 0000000..291ac46
--- /dev/null
+++ b/hartmann-foto-documentation-frontend/lib/controller/questionnaire_controller.dart
@@ -0,0 +1,23 @@
+import 'package:fotodocumentation/controller/base_controller.dart';
+import 'package:fotodocumentation/dto/questionnaire_dto.dart';
+
+abstract interface class QuestionnaireController {
+ Future delete(QuestionnaireDto dto);
+ Future updateEvaluation(QuestionnaireDto dto);
+}
+
+class QuestionnaireControllerImpl extends BaseController implements QuestionnaireController {
+ final String path = "questionnaire";
+
+ @override
+ Future delete(QuestionnaireDto dto) {
+ String uriStr = '${uriUtils.getBaseUrl()}$path/${dto.id}';
+ return runDeleteWithAuth(uriStr);
+ }
+
+ @override
+ Future updateEvaluation(QuestionnaireDto dto) {
+ String uriStr = '${uriUtils.getBaseUrl()}$path/evaluation/${dto.id}?evaluation=${dto.evaluation}';
+ return runPutWithAuth(uriStr);
+ }
+}
diff --git a/hartmann-foto-documentation-frontend/lib/controller/questionnaire_customer_controller.dart b/hartmann-foto-documentation-frontend/lib/controller/questionnaire_customer_controller.dart
new file mode 100644
index 0000000..6aebfde
--- /dev/null
+++ b/hartmann-foto-documentation-frontend/lib/controller/questionnaire_customer_controller.dart
@@ -0,0 +1,42 @@
+import 'package:fotodocumentation/controller/base_controller.dart';
+import 'package:fotodocumentation/dto/questionnaire_customer_dto.dart';
+
+abstract interface class QuestionnaireCustomerController {
+ Future> getAll(String query, String startsWith);
+
+ Future get({required int id});
+
+ Future> export({required int customerId, int? questionnaireId});
+}
+
+class QuestionnaireCustomerControllerImpl extends BaseController implements QuestionnaireCustomerController {
+ final String path = "questionnairecustomer";
+
+ @override
+ Future> getAll(String query, String startsWith) async {
+ String uriStr = '${uriUtils.getBaseUrl()}$path?query=$query&startsWith=$startsWith';
+ return runGetListWithAuth(uriStr, (p0) {
+ List retVal = [];
+ for (var elem in p0) {
+ var entity = QuestionnaireCustomerListDto.fromJson(elem);
+ retVal.add(entity);
+ }
+ return retVal;
+ });
+ }
+
+ @override
+ Future get({required int id}) {
+ String uriStr = '${uriUtils.getBaseUrl()}$path/$id';
+ return runGetWithAuth(uriStr, (json) => QuestionnaireCustomerDto.fromJson(json));
+ }
+
+ @override
+ Future> export({required int customerId, int? questionnaireId}) {
+ String uriStr = '${uriUtils.getBaseUrl()}$path/export/$customerId';
+ if (questionnaireId != null) {
+ uriStr += '?questionnaire=$questionnaireId';
+ }
+ return runGetBytesWithAuth(uriStr);
+ }
+}
diff --git a/hartmann-foto-documentation-frontend/lib/dto/customer_dto.dart b/hartmann-foto-documentation-frontend/lib/dto/customer_dto.dart
index 317e675..daa10b0 100644
--- a/hartmann-foto-documentation-frontend/lib/dto/customer_dto.dart
+++ b/hartmann-foto-documentation-frontend/lib/dto/customer_dto.dart
@@ -46,3 +46,5 @@ class CustomerDto {
);
}
}
+
+
diff --git a/hartmann-foto-documentation-frontend/lib/dto/questionnaire_customer_dto.dart b/hartmann-foto-documentation-frontend/lib/dto/questionnaire_customer_dto.dart
new file mode 100644
index 0000000..4b834d1
--- /dev/null
+++ b/hartmann-foto-documentation-frontend/lib/dto/questionnaire_customer_dto.dart
@@ -0,0 +1,49 @@
+import 'package:fotodocumentation/dto/questionnaire_dto.dart';
+import 'package:fotodocumentation/utils/date_time_utils.dart';
+
+class QuestionnaireCustomerListDto {
+ final int id;
+ final String name;
+ final String customerNumber;
+ final String? zip;
+ final String? city;
+ final DateTime? lastUpdateDate;
+
+ QuestionnaireCustomerListDto({required this.id, required this.name, required this.customerNumber, this.zip, this.city, this.lastUpdateDate});
+
+ /// Create from JSON response
+ factory QuestionnaireCustomerListDto.fromJson(Map json) {
+ return QuestionnaireCustomerListDto(
+ 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']),
+ );
+ }
+}
+
+
+class QuestionnaireCustomerDto {
+ final int id;
+ final String name;
+ final String customerNumber;
+ final String? zip;
+ final String? city;
+ final List questionnaires;
+
+ QuestionnaireCustomerDto({required this.id, required this.name, required this.customerNumber, required this.questionnaires, this.zip, this.city});
+
+ /// Create from JSON response
+ factory QuestionnaireCustomerDto.fromJson(Map json) {
+ return QuestionnaireCustomerDto(
+ 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?,
+ questionnaires: List.from(json["questionnaires"].map((x) => QuestionnaireDto.fromJson(x))),
+ );
+ }
+}
\ No newline at end of file
diff --git a/hartmann-foto-documentation-frontend/lib/dto/questionnaire_dto.dart b/hartmann-foto-documentation-frontend/lib/dto/questionnaire_dto.dart
new file mode 100644
index 0000000..8d8ccc3
--- /dev/null
+++ b/hartmann-foto-documentation-frontend/lib/dto/questionnaire_dto.dart
@@ -0,0 +1,30 @@
+import 'package:fotodocumentation/utils/date_time_utils.dart';
+
+class QuestionnaireDto {
+ final int id;
+ final String? comment;
+ final String? category;
+ int evaluation;
+ final DateTime questionnaireDate;
+ final String? username;
+
+ QuestionnaireDto(
+ {required this.id, required this.comment, required this.category, required this.evaluation, required this.questionnaireDate, required this.username});
+
+ /// Create from JSON response
+ factory QuestionnaireDto.fromJson(Map json) {
+ return QuestionnaireDto(
+ id: json['id'] as int,
+ comment: json['comment'] as String?,
+ category: json['category'] as String?,
+ evaluation: json["evaluation"] as int,
+ questionnaireDate: DateTimeUtils.toDateTime(json['questionnaireDate']) ?? DateTime.now(),
+ username: json['username'] as String?,
+ );
+ }
+
+ @override
+ String toString() {
+ return 'QuestionnaireDto{id: $id}';
+ }
+}
\ No newline at end of file
diff --git a/hartmann-foto-documentation-frontend/lib/l10n/app_de.arb b/hartmann-foto-documentation-frontend/lib/l10n/app_de.arb
index 0a603d9..b659238 100644
--- a/hartmann-foto-documentation-frontend/lib/l10n/app_de.arb
+++ b/hartmann-foto-documentation-frontend/lib/l10n/app_de.arb
@@ -151,5 +151,9 @@
"customerWidgetDownloadInProgress": "Download wird vorbereitet…",
"@customerWidgetDownloadInProgress": {
"description": "Download in progress message"
+ },
+ "questionnaireLoginTitle": "FRAGEBOGEN",
+ "@questionnaireLoginTitle": {
+ "description": "Questionnaire login page title"
}
}
\ No newline at end of file
diff --git a/hartmann-foto-documentation-frontend/lib/l10n/app_localizations.dart b/hartmann-foto-documentation-frontend/lib/l10n/app_localizations.dart
index 0ec558e..c5009f1 100644
--- a/hartmann-foto-documentation-frontend/lib/l10n/app_localizations.dart
+++ b/hartmann-foto-documentation-frontend/lib/l10n/app_localizations.dart
@@ -297,6 +297,12 @@ abstract class AppLocalizations {
/// In de, this message translates to:
/// **'Download wird vorbereitet…'**
String get customerWidgetDownloadInProgress;
+
+ /// Questionnaire login page title
+ ///
+ /// In de, this message translates to:
+ /// **'FRAGEBOGEN'**
+ String get questionnaireLoginTitle;
}
class _AppLocalizationsDelegate
diff --git a/hartmann-foto-documentation-frontend/lib/l10n/app_localizations_de.dart b/hartmann-foto-documentation-frontend/lib/l10n/app_localizations_de.dart
index ca7717a..ae1c58e 100644
--- a/hartmann-foto-documentation-frontend/lib/l10n/app_localizations_de.dart
+++ b/hartmann-foto-documentation-frontend/lib/l10n/app_localizations_de.dart
@@ -117,4 +117,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get customerWidgetDownloadInProgress => 'Download wird vorbereitet…';
+
+ @override
+ String get questionnaireLoginTitle => 'FRAGEBOGEN';
}
diff --git a/hartmann-foto-documentation-frontend/lib/pages/customer/customer_list_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_customer_list_widget.dart
similarity index 96%
rename from hartmann-foto-documentation-frontend/lib/pages/customer/customer_list_widget.dart
rename to hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_customer_list_widget.dart
index 9d55833..9a28985 100644
--- a/hartmann-foto-documentation-frontend/lib/pages/customer/customer_list_widget.dart
+++ b/hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_customer_list_widget.dart
@@ -13,14 +13,14 @@ import 'package:fotodocumentation/utils/global_router.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
-class CustomerListWidget extends StatefulWidget {
- const CustomerListWidget({super.key});
+class FotoCustomerListWidget extends StatefulWidget {
+ const FotoCustomerListWidget({super.key});
@override
- State createState() => _CustomerListWidgetState();
+ State createState() => _FotoCustomerListWidgetState();
}
-class _CustomerListWidgetState extends State {
+class _FotoCustomerListWidgetState extends State {
CustomerController get _customerController => DiContainer.get();
GeneralStyle get _generalStyle => DiContainer.get();
@@ -242,7 +242,7 @@ class _CustomerListWidgetState extends State {
}
Future _actionSelect(BuildContext context, CustomerListDto dto) async {
- String uri = "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/${dto.id}";
+ String uri = "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/${dto.id}";
context.go(uri);
}
diff --git a/hartmann-foto-documentation-frontend/lib/pages/customer/customer_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_customer_widget.dart
similarity index 93%
rename from hartmann-foto-documentation-frontend/lib/pages/customer/customer_widget.dart
rename to hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_customer_widget.dart
index ce74166..d6636c1 100644
--- a/hartmann-foto-documentation-frontend/lib/pages/customer/customer_widget.dart
+++ b/hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_customer_widget.dart
@@ -6,7 +6,7 @@ import 'package:fotodocumentation/dto/customer_dto.dart';
import 'package:fotodocumentation/dto/picture_dto.dart';
import 'package:fotodocumentation/l10n/app_localizations.dart';
import 'package:fotodocumentation/pages/ui_utils/component/customer_back_button.dart';
-import 'package:fotodocumentation/pages/customer/picture_delete_dialog.dart';
+import 'package:fotodocumentation/pages/foto/customer/foto_picture_delete_dialog.dart';
import 'package:fotodocumentation/pages/ui_utils/component/general_error_widget.dart';
import 'package:fotodocumentation/pages/ui_utils/component/page_header_widget.dart';
import 'package:fotodocumentation/pages/ui_utils/component/waiting_widget.dart';
@@ -17,15 +17,15 @@ import 'package:go_router/go_router.dart';
import 'package:fotodocumentation/utils/file_download.dart';
import 'package:intl/intl.dart';
-class CustomerWidget extends StatefulWidget {
+class FotoCustomerWidget extends StatefulWidget {
final int customerId;
- const CustomerWidget({super.key, required this.customerId});
+ const FotoCustomerWidget({super.key, required this.customerId});
@override
- State createState() => _CustomerWidgetState();
+ State createState() => _FotoCustomerWidgetState();
}
-class _CustomerWidgetState extends State {
+class _FotoCustomerWidgetState extends State {
CustomerController get _customerController => DiContainer.get();
PictureController get _pictureController => DiContainer.get();
GeneralStyle get _generalStyle => DiContainer.get();
@@ -40,6 +40,14 @@ class _CustomerWidgetState extends State {
_dto = _customerController.get(id: widget.customerId);
}
+ @override
+ void didUpdateWidget(covariant FotoCustomerWidget oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.customerId != widget.customerId) {
+ _dto = _customerController.get(id: widget.customerId);
+ }
+ }
+
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -90,7 +98,7 @@ class _CustomerWidgetState extends State {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PageHeaderWidget(text: dto.name, subText: subText),
- CustomerBackButton(path: GlobalRouter.pathHome),
+ CustomerBackButton(path: GlobalRouter.pathFotoHome),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
@@ -316,7 +324,7 @@ class _CustomerWidgetState extends State {
final confirmed = await showDialog(
context: context,
builder: (BuildContext context) {
- return PictureDeleteDialog();
+ return FotoPictureDeleteDialog();
},
);
@@ -329,7 +337,7 @@ class _CustomerWidgetState extends State {
}
Future _actionSelect(BuildContext context, CustomerDto customerDto, PictureDto pictureDto) async {
- String uri = "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/${customerDto.id}${GlobalRouter.pathPicture}/${pictureDto.id}";
+ String uri = "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/${customerDto.id}${GlobalRouter.pathFotoPicture}/${pictureDto.id}";
context.go(uri);
setState(() {
_dto = _customerController.get(id: widget.customerId);
diff --git a/hartmann-foto-documentation-frontend/lib/pages/customer/picture_delete_dialog.dart b/hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_picture_delete_dialog.dart
similarity index 96%
rename from hartmann-foto-documentation-frontend/lib/pages/customer/picture_delete_dialog.dart
rename to hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_picture_delete_dialog.dart
index 5c45aad..708ebcd 100644
--- a/hartmann-foto-documentation-frontend/lib/pages/customer/picture_delete_dialog.dart
+++ b/hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_picture_delete_dialog.dart
@@ -3,10 +3,10 @@ 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 {
+class FotoPictureDeleteDialog extends StatelessWidget {
GeneralStyle get _generalStyle => DiContainer.get();
- const PictureDeleteDialog({super.key});
+ const FotoPictureDeleteDialog({super.key});
@override
Widget build(BuildContext context) {
diff --git a/hartmann-foto-documentation-frontend/lib/pages/customer/picture_fullscreen_dialog.dart b/hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_picture_fullscreen_dialog.dart
similarity index 94%
rename from hartmann-foto-documentation-frontend/lib/pages/customer/picture_fullscreen_dialog.dart
rename to hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_picture_fullscreen_dialog.dart
index 0b4b3ff..a580f8b 100644
--- a/hartmann-foto-documentation-frontend/lib/pages/customer/picture_fullscreen_dialog.dart
+++ b/hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_picture_fullscreen_dialog.dart
@@ -4,12 +4,12 @@ import 'package:fotodocumentation/dto/picture_dto.dart';
import 'package:fotodocumentation/pages/ui_utils/general_style.dart';
import 'package:fotodocumentation/utils/di_container.dart';
-class PictureFullscreenDialog extends StatelessWidget {
+class FotoPictureFullscreenDialog extends StatelessWidget {
GeneralStyle get _generalStyle => DiContainer.get();
final PictureDto dto;
- const PictureFullscreenDialog({super.key, required this.dto});
+ const FotoPictureFullscreenDialog({super.key, required this.dto});
@override
Widget build(BuildContext context) {
diff --git a/hartmann-foto-documentation-frontend/lib/pages/customer/picture_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_picture_widget.dart
similarity index 94%
rename from hartmann-foto-documentation-frontend/lib/pages/customer/picture_widget.dart
rename to hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_picture_widget.dart
index e9f71dd..3668730 100644
--- a/hartmann-foto-documentation-frontend/lib/pages/customer/picture_widget.dart
+++ b/hartmann-foto-documentation-frontend/lib/pages/foto/customer/foto_picture_widget.dart
@@ -6,7 +6,7 @@ import 'package:fotodocumentation/dto/customer_dto.dart';
import 'package:fotodocumentation/dto/picture_dto.dart';
import 'package:fotodocumentation/l10n/app_localizations.dart';
import 'package:fotodocumentation/pages/ui_utils/component/customer_back_button.dart';
-import 'package:fotodocumentation/pages/customer/picture_fullscreen_dialog.dart';
+import 'package:fotodocumentation/pages/foto/customer/foto_picture_fullscreen_dialog.dart';
import 'package:fotodocumentation/pages/ui_utils/component/general_error_widget.dart';
import 'package:fotodocumentation/pages/ui_utils/component/page_header_widget.dart';
import 'package:fotodocumentation/pages/ui_utils/component/waiting_widget.dart';
@@ -15,16 +15,16 @@ import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/global_router.dart';
import 'package:intl/intl.dart';
-class PictureWidget extends StatefulWidget {
+class FotoPictureWidget extends StatefulWidget {
final int customerId;
final int pictureId;
- const PictureWidget({super.key, required this.customerId, required this.pictureId});
+ const FotoPictureWidget({super.key, required this.customerId, required this.pictureId});
@override
- State createState() => _PictureWidgetState();
+ State createState() => _FotoPictureWidgetState();
}
-class _PictureWidgetState extends State {
+class _FotoPictureWidgetState extends State {
GeneralStyle get _generalStyle => DiContainer.get();
CustomerController get _customerController => DiContainer.get();
PictureController get _pictureController => DiContainer.get();
@@ -43,6 +43,14 @@ class _PictureWidgetState extends State {
_dto = _customerController.get(id: widget.customerId);
}
+ @override
+ void didUpdateWidget(covariant FotoPictureWidget oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.customerId != widget.customerId || oldWidget.pictureId != widget.pictureId) {
+ _dto = _customerController.get(id: widget.customerId);
+ }
+ }
+
@override
void dispose() {
_commentScrollController.dispose();
@@ -97,7 +105,7 @@ class _PictureWidgetState extends State {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PageHeaderWidget(text: _customerDto.name),
- CustomerBackButton(path: GlobalRouter.pathCustomer),
+ CustomerBackButton(path: GlobalRouter.pathFotoCustomer),
const SizedBox(height: 24),
Expanded(
child: _mainWidget(context, selectedPicture),
@@ -415,7 +423,7 @@ class _PictureWidgetState extends State {
showDialog(
context: context,
builder: (BuildContext context) {
- return PictureFullscreenDialog(dto: dto);
+ return FotoPictureFullscreenDialog(dto: dto);
},
);
}
diff --git a/hartmann-foto-documentation-frontend/lib/pages/login/login_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/foto/login/foto_login_widget.dart
similarity index 95%
rename from hartmann-foto-documentation-frontend/lib/pages/login/login_widget.dart
rename to hartmann-foto-documentation-frontend/lib/pages/foto/login/foto_login_widget.dart
index 641f31b..eef1261 100644
--- a/hartmann-foto-documentation-frontend/lib/pages/login/login_widget.dart
+++ b/hartmann-foto-documentation-frontend/lib/pages/foto/login/foto_login_widget.dart
@@ -10,14 +10,14 @@ import 'package:fotodocumentation/pages/ui_utils/general_style.dart';
import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/login_credentials.dart';
-class LoginWidget extends StatefulWidget {
- const LoginWidget({super.key});
+class FotoLoginWidget extends StatefulWidget {
+ const FotoLoginWidget({super.key});
@override
- State createState() => _LoginWidgetState();
+ State createState() => _FotoLoginWidgetState();
}
-class _LoginWidgetState extends State {
+class _FotoLoginWidgetState extends State {
LoginController get _loginController => DiContainer.get();
LoginCredentials get _loginCredentials => DiContainer.get();
GeneralStyle get _generalStyle => DiContainer.get();
@@ -201,7 +201,9 @@ class _LoginWidgetState extends State {
_loginCredentials.setLoggedIn(true);
if (context.mounted) {
- context.go("/");
+ // Get the redirect URL from query parameters
+ final redirect = GoRouterState.of(context).uri.queryParameters['redirect'];
+ context.go(redirect ?? '/');
}
}
}
diff --git a/hartmann-foto-documentation-frontend/lib/pages/questionnaire/customer/questionnaire_customer_list_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/questionnaire/customer/questionnaire_customer_list_widget.dart
new file mode 100644
index 0000000..824bd18
--- /dev/null
+++ b/hartmann-foto-documentation-frontend/lib/pages/questionnaire/customer/questionnaire_customer_list_widget.dart
@@ -0,0 +1,291 @@
+import 'package:flutter/material.dart';
+import 'package:fotodocumentation/controller/base_controller.dart';
+import 'package:fotodocumentation/controller/questionnaire_customer_controller.dart';
+import 'package:fotodocumentation/dto/questionnaire_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/page_header_widget.dart';
+import 'package:fotodocumentation/pages/ui_utils/component/search_bar_widget.dart';
+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:go_router/go_router.dart';
+import 'package:intl/intl.dart';
+
+class QuestionaireCustomerListWidget extends StatefulWidget {
+ const QuestionaireCustomerListWidget({super.key});
+
+ @override
+ State createState() => _QuestionaireCustomerListWidgetState();
+}
+
+class _QuestionaireCustomerListWidgetState extends State {
+ QuestionnaireCustomerController get _questionnaireCustomerController => DiContainer.get();
+ GeneralStyle get _generalStyle => DiContainer.get();
+
+ final _searchController = TextEditingController();
+ late Future> _dtos;
+ String? _selectedLetter;
+ late DateFormat _dateFormat;
+
+ @override
+ void initState() {
+ super.initState();
+ _dateFormat = DateFormat('dd MMMM yyyy');
+ _dtos = _questionnaireCustomerController.getAll(_searchController.text, "");
+ }
+
+ @override
+ void dispose() {
+ _searchController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: _body(context),
+ );
+ }
+
+ Widget _body(BuildContext context) {
+ return Container(
+ color: _generalStyle.pageBackgroundColor,
+ child: Padding(
+ padding: const EdgeInsets.only(top: 8.0, left: 50.0, right: 50.0, bottom: 8.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ PageHeaderWidget(text: "FRAGENBOGEN AUSWERTUNG"),
+ //_abcHeaderBar(),
+ const SizedBox(width: 48),
+ SearchBarWidget(
+ searchController: _searchController,
+ onSearch: (text) async => actionSearch(text),
+ ),
+ Expanded(
+ child: _customerListWidget(),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _customerListWidget() {
+ return FutureBuilder>(
+ future: _dtos,
+ builder: (BuildContext context, AsyncSnapshot> snapshot) {
+ if (snapshot.connectionState != ConnectionState.done) {
+ return const WaitingWidget();
+ }
+ if (snapshot.hasData) {
+ List dtos = snapshot.data ?? [];
+
+ return _listWidget(dtos);
+ } else if (snapshot.hasError) {
+ var error = snapshot.error;
+ return (error is ServerError) ? GeneralErrorWidget.fromServerError(error) : GeneralErrorWidget(error: snapshot.error.toString());
+ }
+ return const WaitingWidget();
+ },
+ );
+ }
+
+ Widget _listWidget(List dtos) {
+ if (dtos.isEmpty) {
+ return Text(AppLocalizations.of(context)!.customerListEmpty,
+ 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: Column(
+ children: [
+ _tableHeaderRow(context),
+ Divider(thickness: 2, height: 1, color: _generalStyle.secondaryWidgetBackgroundColor),
+ Expanded(
+ child: ListView.separated(
+ padding: const EdgeInsets.all(0),
+ itemCount: dtos.length,
+ itemBuilder: (BuildContext context, int index) {
+ return _tableDataRow(context, dtos[index]);
+ },
+ separatorBuilder: (BuildContext context, int index) => Divider(
+ color: _generalStyle.secondaryWidgetBackgroundColor,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _tableHeaderRow(BuildContext context) {
+ final headerStyle = TextStyle(
+ fontFamily: _generalStyle.fontFamily,
+ fontWeight: FontWeight.bold,
+ fontSize: 20,
+ color: _generalStyle.secondaryWidgetBackgroundColor,
+ );
+
+ return Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Row(
+ spacing: 8.0,
+ children: [
+ const SizedBox(width: 48),
+ Expanded(
+ flex: 1,
+ child: Text(
+ AppLocalizations.of(context)!.customerListHeaderCustomerNumber,
+ style: headerStyle,
+ ),
+ ),
+ Expanded(
+ flex: 3,
+ child: Text(
+ AppLocalizations.of(context)!.customerListHeaderName,
+ 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,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _tableDataRow(BuildContext context, QuestionnaireCustomerListDto dto) {
+ final dataStyle = TextStyle(
+ fontFamily: _generalStyle.fontFamily,
+ fontSize: 16.0,
+ color: _generalStyle.secondaryTextLabelColor,
+ );
+
+ final dateStr = dto.lastUpdateDate == null ? "" : _dateFormat.format(dto.lastUpdateDate!);
+ return InkWell(
+ onTap: () => _actionSelect(context, dto),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Row(
+ spacing: 8.0,
+ children: [
+ SizedBox(
+ width: 48,
+ child: Icon(Icons.folder_open_outlined, color: _generalStyle.secondaryWidgetBackgroundColor),
+ ),
+ Expanded(
+ flex: 1,
+ child: Text(dto.customerNumber, style: dataStyle),
+ ),
+ Expanded(
+ 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),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Future actionSearch(String text) async {
+ _reloadData();
+ }
+
+ Future _actionSelect(BuildContext context, QuestionnaireCustomerListDto dto) async {
+ String uri = "${GlobalRouter.pathQuestionnaireHome}/${GlobalRouter.pathQuestionnaireCustomer}/${dto.id}";
+ context.go(uri);
+ }
+
+ void _reloadData() {
+ _dtos = _questionnaireCustomerController.getAll(_searchController.text, _selectedLetter ?? "");
+ setState(() {});
+ }
+/*
+ Widget _abcHeaderBar() {
+ const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: letters.split('').map((letter) {
+ final isSelected = _selectedLetter == letter;
+ return InkWell(
+ onTap: () {
+ setState(() {
+ _selectedLetter = isSelected ? null : letter;
+ _reloadData();
+ });
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ decoration: BoxDecoration(
+ color: isSelected ? _generalStyle.secondaryWidgetBackgroundColor : Colors.white,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(
+ letter,
+ style: TextStyle(
+ fontFamily: _generalStyle.fontFamily,
+ fontWeight: FontWeight.bold,
+ fontSize: 20.0,
+ color: isSelected ? Colors.white : _generalStyle.secondaryWidgetBackgroundColor,
+ ),
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ );
+ }*/
+}
diff --git a/hartmann-foto-documentation-frontend/lib/pages/questionnaire/customer/questionnaire_customer_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/questionnaire/customer/questionnaire_customer_widget.dart
new file mode 100644
index 0000000..9703d14
--- /dev/null
+++ b/hartmann-foto-documentation-frontend/lib/pages/questionnaire/customer/questionnaire_customer_widget.dart
@@ -0,0 +1,380 @@
+import 'package:flutter/material.dart';
+import 'package:fotodocumentation/controller/base_controller.dart';
+import 'package:fotodocumentation/controller/questionnaire_controller.dart';
+import 'package:fotodocumentation/controller/questionnaire_customer_controller.dart';
+import 'package:fotodocumentation/dto/questionnaire_customer_dto.dart';
+import 'package:fotodocumentation/dto/questionnaire_dto.dart';
+import 'package:fotodocumentation/l10n/app_localizations.dart';
+import 'package:fotodocumentation/pages/questionnaire/customer/questionnaire_delete_dialog.dart';
+import 'package:fotodocumentation/pages/ui_utils/component/customer_back_button.dart';
+import 'package:fotodocumentation/pages/ui_utils/component/general_error_widget.dart';
+import 'package:fotodocumentation/pages/ui_utils/component/page_header_widget.dart';
+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/utils/file_download.dart';
+import 'package:intl/intl.dart';
+
+class QuestionaireCustomerWidget extends StatefulWidget {
+ final int customerId;
+ const QuestionaireCustomerWidget({super.key, required this.customerId});
+
+ @override
+ State createState() => _QuestionaireCustomerWidgetState();
+}
+
+class _QuestionaireCustomerWidgetState extends State {
+ QuestionnaireCustomerController get _customerController => DiContainer.get();
+ QuestionnaireController get _pictureController => DiContainer.get();
+ GeneralStyle get _generalStyle => DiContainer.get();
+
+ late Future _dto;
+ late DateFormat _dateFormat;
+
+ @override
+ void initState() {
+ super.initState();
+ _dateFormat = DateFormat('dd MMMM yyyy');
+ _dto = _customerController.get(id: widget.customerId);
+ }
+
+ @override
+ void didUpdateWidget(covariant QuestionaireCustomerWidget oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.customerId != widget.customerId) {
+ _dto = _customerController.get(id: widget.customerId);
+ }
+ }
+
+ @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),
+ ),
+ ),
+ );
+ }
+
+ Widget _body(BuildContext context) {
+ return FutureBuilder(
+ future: _dto,
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ if (snapshot.connectionState != ConnectionState.done) {
+ return const WaitingWidget();
+ }
+ if (snapshot.hasData) {
+ QuestionnaireCustomerDto? dto = snapshot.data;
+ if (dto == null) {
+ return Text(
+ AppLocalizations.of(context)!.customerWidgetNotFound,
+ style: TextStyle(
+ fontFamily: _generalStyle.fontFamily,
+ fontWeight: FontWeight.bold,
+ fontSize: 20,
+ color: _generalStyle.secondaryWidgetBackgroundColor,
+ ),
+ );
+ }
+ 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 _mainWidget(QuestionnaireCustomerDto dto) {
+ var subText = AppLocalizations.of(context)!.customerWidgetCustomerNumberPrefix(dto.customerNumber);
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ PageHeaderWidget(text: dto.name, subText: subText),
+ CustomerBackButton(path: GlobalRouter.pathFotoHome),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ _downloadButton(context, dto),
+ ],
+ ),
+ const SizedBox(height: 24),
+ Expanded(
+ child: _customerWidget(dto),
+ ),
+ ],
+ );
+ }
+
+ Widget _customerWidget(QuestionnaireCustomerDto customerDto) {
+ var questionnaireDtos = customerDto.questionnaires;
+
+ questionnaireDtos.sort((a, b) => b.questionnaireDate.compareTo(a.questionnaireDate));
+
+ return Card(
+ margin: EdgeInsets.zero,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Column(
+ children: [
+ _tableHeaderRow(context),
+ Divider(thickness: 2, height: 1, color: _generalStyle.secondaryWidgetBackgroundColor),
+ Expanded(
+ child: ListView.separated(
+ padding: const EdgeInsets.only(top: 8.0),
+ itemCount: questionnaireDtos.length,
+ itemBuilder: (BuildContext context, int index) {
+ return _tableDataRow(context, customerDto, questionnaireDtos[index]);
+ },
+ separatorBuilder: (BuildContext context, int index) => Divider(color: _generalStyle.secondaryWidgetBackgroundColor),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _tableHeaderRow(BuildContext context) {
+ final headerStyle = TextStyle(
+ fontFamily: _generalStyle.fontFamily,
+ fontWeight: FontWeight.bold,
+ fontSize: 20,
+ color: _generalStyle.secondaryWidgetBackgroundColor,
+ );
+
+ return Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 8.0,
+ children: [
+ Expanded(
+ flex: 1,
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ AppLocalizations.of(context)!.customerWidgetHeaderFoto,
+ style: headerStyle,
+ ),
+ ),
+ ),
+ Expanded(
+ flex: 3,
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ AppLocalizations.of(context)!.customerWidgetHeaderComment,
+ style: headerStyle,
+ ),
+ ),
+ ),
+ Expanded(
+ flex: 1,
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ 'Bewertung',
+ style: headerStyle,
+ ),
+ ),
+ ),
+ Expanded(
+ flex: 2,
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ AppLocalizations.of(context)!.customerWidgetHeaderUploadDate,
+ style: headerStyle,
+ ),
+ ),
+ ),
+ const SizedBox(width: 96),
+ ],
+ ),
+ );
+ }
+
+ Widget _tableDataRow(BuildContext context, QuestionnaireCustomerDto customerDto, QuestionnaireDto questionnaireDto) {
+ final dataStyle = TextStyle(
+ fontFamily: _generalStyle.fontFamily,
+ fontSize: 16.0,
+ color: _generalStyle.secondaryTextLabelColor,
+ );
+
+ final dateStr = _dateFormat.format(questionnaireDto.questionnaireDate);
+ final evaluationColor = _generalStyle.evaluationColor(value: questionnaireDto.evaluation);
+ return InkWell(
+ key: Key("table_row_${customerDto.id}"),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ spacing: 8.0,
+ children: [
+ Expanded(
+ flex: 1,
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 70, maxHeight: 70),
+ child:
+ Icon(Icons.abc),
+ /*
+ FIXME: remove me
+ Image.network(
+ headers: {cred.name: cred.value},
+ pictureDto.thumbnailSizeUrl,
+ fit: BoxFit.contain,
+ ),*/
+ ),
+ ),
+ ),
+ Expanded(
+ flex: 3,
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Text(questionnaireDto.comment ?? "", style: dataStyle),
+ ),
+ ),
+ Expanded(
+ flex: 1,
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Container(
+ width: 20,
+ height: 20,
+ decoration: BoxDecoration(
+ color: evaluationColor,
+ shape: BoxShape.circle,
+ ),
+ ),
+ ),
+ ),
+ Expanded(
+ flex: 2,
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Text(dateStr, style: dataStyle),
+ ),
+ ),
+ SizedBox(
+ width: 48,
+ child: IconButton(
+ key: Key("table_row_download_${questionnaireDto.id}"),
+ icon: Icon(
+ Icons.file_download_outlined,
+ color: _generalStyle.loginFormTextLabelColor,
+ ),
+ onPressed: () => _actionDownload(context, customerDto, questionnaireDto),
+ ),
+ ),
+ SizedBox(
+ width: 48,
+ child: IconButton(
+ key: Key("table_row_delete_${customerDto.id}"),
+ icon: Icon(
+ Icons.delete_outline,
+ color: _generalStyle.errorColor,
+ ),
+ onPressed: () => _actionDelete(context, customerDto, questionnaireDto),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _downloadButton(BuildContext context, QuestionnaireCustomerDto customerDto) {
+ return ElevatedButton.icon(
+ key: Key("download_all_button"),
+ onPressed: () => _actionDownload(context, customerDto, null),
+ iconAlignment: IconAlignment.end,
+ icon: Icon(
+ Icons.file_download_outlined,
+ color: _generalStyle.primaryButtonTextColor,
+ size: 24,
+ ),
+ label: Text(
+ "Alle herunterladen",
+ style: TextStyle(
+ fontFamily: _generalStyle.fontFamily,
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ color: _generalStyle.primaryButtonTextColor,
+ ),
+ ),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _generalStyle.secondaryWidgetBackgroundColor,
+ elevation: 0,
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
+ shape: const StadiumBorder(),
+ ),
+ );
+ }
+
+ Future _actionDelete(BuildContext context, QuestionnaireCustomerDto customerDto, QuestionnaireDto questionnaireDto) async {
+ final confirmed = await showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return QuestionaireDeleteDialog();
+ },
+ );
+
+ if (confirmed == true) {
+ _pictureController.delete(questionnaireDto);
+ setState(() {
+ _dto = _customerController.get(id: widget.customerId);
+ });
+ }
+ }
+
+ Future _actionDownload(BuildContext context, QuestionnaireCustomerDto customerDto, QuestionnaireDto? questionnaireDto) async {
+ 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, questionnaireId: questionnaireDto?.id);
+ final fileName = questionnaireDto != null ? '${customerDto.customerNumber}_${questionnaireDto.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;
+ }
+ }
+}
diff --git a/hartmann-foto-documentation-frontend/lib/pages/questionnaire/customer/questionnaire_delete_dialog.dart b/hartmann-foto-documentation-frontend/lib/pages/questionnaire/customer/questionnaire_delete_dialog.dart
new file mode 100644
index 0000000..6e83ba7
--- /dev/null
+++ b/hartmann-foto-documentation-frontend/lib/pages/questionnaire/customer/questionnaire_delete_dialog.dart
@@ -0,0 +1,91 @@
+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 QuestionaireDeleteDialog extends StatelessWidget {
+ GeneralStyle get _generalStyle => DiContainer.get();
+
+ const QuestionaireDeleteDialog({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ actionsAlignment: MainAxisAlignment.center,
+ actionsPadding: EdgeInsets.only(bottom: 50),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 50.0),
+ child: Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: _generalStyle.errorColor,
+ shape: BoxShape.circle,
+ ),
+ child: const Icon(
+ Icons.delete_outline,
+ size: 32,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ const SizedBox(height: 16),
+ Padding(
+ padding: const EdgeInsets.only(left:50, right: 50, bottom: 50),
+ child: Text(
+ AppLocalizations.of(context)!.deleteDialogText,
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontFamily: _generalStyle.fontFamily,
+ color: _generalStyle.secondaryTextLabelColor,
+ fontSize: 28.71,
+ fontWeight: FontWeight.normal,
+
+ ),
+ ),
+ ),
+ ],
+ ),
+ actions: [
+ ElevatedButton(
+ key: Key("questionnaire_delete_no"),
+ onPressed: () => Navigator.of(context).pop(false),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _generalStyle.deleteCancelButtonBackgroundColor,
+ shape: const StadiumBorder(),
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
+ ),
+ child: Text(
+ AppLocalizations.of(context)!.deleteDialogButtonCancel,
+ style: TextStyle(
+ fontFamily: _generalStyle.fontFamily,
+ color: _generalStyle.deleteCancelTextColor,
+ fontWeight: FontWeight.bold,
+ fontSize: 18.37,
+ ),
+ ),
+ ),
+ ElevatedButton(
+ key: Key("questionnaire_delete_yes"),
+ onPressed: () => Navigator.of(context).pop(true),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _generalStyle.primaryButtonBackgroundColor,
+ shape: const StadiumBorder(),
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
+ ),
+ child: Text(
+ AppLocalizations.of(context)!.deleteDialogButtonApprove,
+ style: TextStyle(
+ fontFamily: _generalStyle.fontFamily,
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ fontSize: 18.37,
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/hartmann-foto-documentation-frontend/lib/pages/questionnaire/login/questionnaire_login_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/questionnaire/login/questionnaire_login_widget.dart
new file mode 100644
index 0000000..d30889e
--- /dev/null
+++ b/hartmann-foto-documentation-frontend/lib/pages/questionnaire/login/questionnaire_login_widget.dart
@@ -0,0 +1,209 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import 'package:go_router/go_router.dart';
+
+import 'package:fotodocumentation/controller/login_controller.dart';
+import 'package:fotodocumentation/dto/jwt_token_pair_dto.dart';
+import 'package:fotodocumentation/l10n/app_localizations.dart';
+import 'package:fotodocumentation/pages/ui_utils/general_style.dart';
+import 'package:fotodocumentation/utils/di_container.dart';
+import 'package:fotodocumentation/utils/login_credentials.dart';
+
+class QuestionaireLoginWidget extends StatefulWidget {
+ const QuestionaireLoginWidget({super.key});
+
+ @override
+ State createState() => _QuestionaireLoginWidgetState();
+}
+
+class _QuestionaireLoginWidgetState extends State {
+ LoginController get _loginController => DiContainer.get();
+ LoginCredentials get _loginCredentials => DiContainer.get();
+ GeneralStyle get _generalStyle => DiContainer.get();
+
+ final GlobalKey _formKey = GlobalKey();
+
+ final _usernameController = TextEditingController();
+ final _passwordController = TextEditingController();
+
+ String? _error;
+ final FocusNode _focusNode = FocusNode();
+
+ @override
+ void dispose() {
+ _focusNode.dispose();
+ _usernameController.dispose();
+ _passwordController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: _body(context),
+ );
+ }
+
+ Widget _body(BuildContext context) {
+ return Container(
+ color: _generalStyle.pageBackgroundColor,
+ child: _content(context),
+ );
+ }
+
+ Widget _content(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Form(
+ key: _formKey,
+ child: KeyboardListener(
+ focusNode: _focusNode,
+ onKeyEvent: (event) {
+ if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) {
+ _actionSubmit(context);
+ }
+ },
+ child: ListView(
+ children: [
+ Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 40.0),
+ child: Image.asset(
+ 'assets/images/logo.png',
+ height: 120,
+ ),
+ ),
+ ),
+ Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 500),
+ child: Card(
+ elevation: 0,
+ margin: EdgeInsets.zero,
+ clipBehavior: Clip.antiAlias,
+ child: Padding(
+ padding: const EdgeInsets.all(70.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Center(
+ child: Text(
+ key: Key("login_title"),
+ AppLocalizations.of(context)!.questionnaireLoginTitle,
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: _generalStyle.primaryTextLabelColor,
+ fontFamily: _generalStyle.fontFamily,
+ fontSize: 50,
+ ),
+ ),
+ ),
+ const SizedBox(height: 40),
+ if (_error != null) ...[
+ _errorWidget(),
+ const SizedBox(height: 40),
+ ],
+ TextFormField(
+ key: Key("username"),
+ controller: _usernameController,
+ decoration: _formFieldInputDecoration(AppLocalizations.of(context)!.loginUsernameTFLabel),
+ ),
+ const SizedBox(height: 25),
+ TextFormField(
+ key: Key("password"),
+ controller: _passwordController,
+ obscureText: true,
+ decoration: _formFieldInputDecoration(AppLocalizations.of(context)!.loginPasswordTFLabel),
+ ),
+ const SizedBox(height: 25),
+ Center(
+ child: ElevatedButton(
+ key: Key("SubmitWidgetButton"),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _generalStyle.primaryButtonBackgroundColor,
+ foregroundColor: _generalStyle.primaryButtonTextColor,
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
+ ),
+ onPressed: () async => await _actionSubmit(context),
+ child: Text(
+ AppLocalizations.of(context)!.loginLoginButtonLabel,
+ style: TextStyle(fontWeight: FontWeight.bold, fontFamily: _generalStyle.fontFamily),
+ ),
+ ),
+ ),
+ const SizedBox(height: 30),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _errorWidget() {
+ return Center(
+ child: Text(
+ _error!,
+ style: TextStyle(
+ color: _generalStyle.errorColor,
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ fontFamily: _generalStyle.fontFamily,
+ ),
+ ),
+ );
+ }
+
+ InputDecoration _formFieldInputDecoration(String text) {
+ var formLabelTextStyle = TextStyle(
+ color: _generalStyle.loginFormTextLabelColor,
+ fontFamily: _generalStyle.fontFamily,
+ fontSize: 14,
+ fontStyle: FontStyle.normal,
+ fontWeight: FontWeight.normal,
+ );
+ return InputDecoration(
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: _error != null ? BorderSide(color: _generalStyle.errorColor) : BorderSide(),
+ ),
+ enabledBorder: _error != null
+ ? OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide(color: _generalStyle.errorColor),
+ )
+ : null,
+ labelText: text.toUpperCase(),
+ labelStyle: formLabelTextStyle,
+ floatingLabelBehavior: FloatingLabelBehavior.always,
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ );
+ }
+
+ Future _actionSubmit(BuildContext context) async {
+ String username = _usernameController.text;
+ String password = _passwordController.text;
+
+ AuthenticateReply authenticateReply = await _loginController.authenticate(username, password);
+
+ JwtTokenPairDto? jwtTokenPairDto = authenticateReply.jwtTokenPairDto;
+ if (jwtTokenPairDto == null) {
+ setState(() => _error = AppLocalizations.of(context)!.loginErrorMessage);
+ return;
+ }
+
+ _loginCredentials.setLoggedIn(true);
+ if (context.mounted) {
+ // Get the redirect URL from query parameters
+ final redirect = GoRouterState.of(context).uri.queryParameters['redirect'];
+ context.go(redirect ?? '/');
+ }
+ }
+}
diff --git a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/page_header_widget.dart b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/page_header_widget.dart
index 8e24832..33d047e 100644
--- a/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/page_header_widget.dart
+++ b/hartmann-foto-documentation-frontend/lib/pages/ui_utils/component/page_header_widget.dart
@@ -94,7 +94,7 @@ class PageHeaderWidget extends StatelessWidget {
return ElevatedButton.icon(
onPressed: () {
loginCredentials.logout();
- context.go(GlobalRouter.pathLogin);
+ context.go(GlobalRouter.pathFotoLogin);
},
icon: Icon(
Icons.logout_rounded,
diff --git a/hartmann-foto-documentation-frontend/lib/utils/di_container.dart b/hartmann-foto-documentation-frontend/lib/utils/di_container.dart
index a2409c6..504e6d4 100644
--- a/hartmann-foto-documentation-frontend/lib/utils/di_container.dart
+++ b/hartmann-foto-documentation-frontend/lib/utils/di_container.dart
@@ -1,6 +1,8 @@
import 'package:fotodocumentation/controller/customer_controller.dart';
import 'package:fotodocumentation/controller/login_controller.dart';
import 'package:fotodocumentation/controller/picture_controller.dart';
+import 'package:fotodocumentation/controller/questionnaire_controller.dart';
+import 'package:fotodocumentation/controller/questionnaire_customer_controller.dart';
import 'package:fotodocumentation/pages/ui_utils/general_style.dart';
import 'package:fotodocumentation/utils/http_client_utils.dart';
import 'package:fotodocumentation/utils/jwt_token_storage.dart';
@@ -26,6 +28,9 @@ class DiContainer {
DiContainer.instance.put(LoginController, LoginControllerImpl());
DiContainer.instance.put(CustomerController, CustomerControllerImpl());
DiContainer.instance.put(PictureController, PictureControllerImpl());
+
+ DiContainer.instance.put(QuestionnaireCustomerController, QuestionnaireCustomerControllerImpl());
+ DiContainer.instance.put(QuestionnaireController, QuestionnaireControllerImpl());
}
void put(Type key, T object) {
diff --git a/hartmann-foto-documentation-frontend/lib/utils/global_router.dart b/hartmann-foto-documentation-frontend/lib/utils/global_router.dart
index 9a9b277..8100a7f 100644
--- a/hartmann-foto-documentation-frontend/lib/utils/global_router.dart
+++ b/hartmann-foto-documentation-frontend/lib/utils/global_router.dart
@@ -1,26 +1,35 @@
// needed for web horizontal scroll behavior
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/pages/foto/customer/foto_customer_list_widget.dart';
+import 'package:fotodocumentation/pages/foto/customer/foto_customer_widget.dart';
+import 'package:fotodocumentation/pages/foto/customer/foto_picture_widget.dart';
+import 'package:fotodocumentation/pages/foto/login/foto_login_widget.dart';
+import 'package:fotodocumentation/pages/questionnaire/customer/questionnaire_customer_list_widget.dart';
+import 'package:fotodocumentation/pages/questionnaire/customer/questionnaire_customer_widget.dart';
+import 'package:fotodocumentation/pages/questionnaire/login/questionnaire_login_widget.dart';
import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/login_credentials.dart';
import 'package:go_router/go_router.dart';
class GlobalRouter {
static final GlobalKey rootNavigatorKey = GlobalKey(debugLabel: 'root');
- static final GlobalKey bottomBarNavigatorKey = GlobalKey(debugLabel: 'bottombar');
- static final GlobalKey adminNavigatorKey = GlobalKey(debugLabel: 'admin');
- static final GlobalKey skillEditorNavigatorKey = GlobalKey(debugLabel: 'skillEditor');
- static final String pathHome = "/home";
- static final String pathCustomer = "/customer";
- static final String pathPicture = "/picture";
- static final String pathLogin = "/login";
+ static final String pathRoot = "/";
- static final GoRouter router = createRouter(pathHome);
+ static final String pathFoto = "/foto";
+ static final String pathQuestionnaire = "/fragenbogen";
+
+ static final String pathFotoHome = "$pathFoto/home";
+ static final String pathFotoCustomer = "$pathFoto/customer";
+ static final String pathFotoPicture = "$pathFoto/picture";
+ static final String pathFotoLogin = "$pathFoto/login";
+
+ static final String pathQuestionnaireHome = "$pathQuestionnaire/home";
+ static final String pathQuestionnaireCustomer = "customer";
+ static final String pathQuestionnaireLogin = "$pathQuestionnaire/login";
+
+ static final GoRouter router = createRouter(pathRoot);
static GoRouter createRouter(String initialLocation) {
return GoRouter(
@@ -28,40 +37,80 @@ class GlobalRouter {
initialLocation: initialLocation,
routes: [
GoRoute(
- path: "/",
- redirect: (_, __) => pathHome,
+ path: "/",
+ redirect: (_, __) {//nvlev4YnTi
+ logger.t("uri / redirect to $pathFotoHome");
+ return pathFotoHome;
+ }),
+ GoRoute(
+ path: pathFoto,
+ redirect: (_, __) {
+ logger.t("uri $pathFoto redirect to $pathFotoHome");
+ return pathFotoHome;
+ }),
+ GoRoute(
+ path: pathFotoLogin,
+ builder: (_, __) {
+ logger.t("uri $pathFotoLogin show foto login");
+ return const FotoLoginWidget();
+ },
),
GoRoute(
- path: pathLogin,
- builder: (BuildContext context, GoRouterState state) => const LoginWidget(),
- ),
- GoRoute(
- path: pathHome,
- builder: (context, state) => CustomerListWidget(),
+ path: pathFotoHome,
+ builder: (context, state) => FotoCustomerListWidget(),
routes: [
GoRoute(
- path: "$pathCustomer/:customerId",
+ path: "$pathFotoCustomer/:customerId",
builder: (context, state) {
var idStr = state.pathParameters['customerId'];
var id = idStr == null ? null : int.tryParse(idStr);
- return CustomerWidget(customerId: id ?? -1);
+ return FotoCustomerWidget(customerId: id ?? -1);
},
routes: [
GoRoute(
- path: "$pathPicture/:pictureId",
+ path: "$pathFotoPicture/:pictureId",
builder: (context, state) {
var customerIdStr = state.pathParameters['customerId'];
var customerId = customerIdStr == null ? null : int.tryParse(customerIdStr);
var pictureIdStr = state.pathParameters['pictureId'];
var pictureId = pictureIdStr == null ? null : int.tryParse(pictureIdStr);
- return PictureWidget(customerId: customerId ?? -1, pictureId: pictureId ?? -1);
+ return FotoPictureWidget(customerId: customerId ?? -1, pictureId: pictureId ?? -1);
},
),
],
),
],
),
+ GoRoute(
+ path: pathQuestionnaire,
+ redirect: (_, __) {
+ logger.t("uri $pathQuestionnaire redirect to $pathQuestionnaireHome");
+ return pathQuestionnaireHome;
+ }),
+ GoRoute(
+ path: pathQuestionnaireLogin,
+ builder: (_, __) {
+ logger.t("uri $pathQuestionnaireLogin show questionnaire login");
+ return const QuestionaireLoginWidget();
+ },
+ ),
+ GoRoute(
+ path: pathQuestionnaireHome,
+ builder: (context, state) => QuestionaireCustomerListWidget(),
+ routes: [
+ GoRoute(
+ path: "$pathQuestionnaireCustomer/:customerId",
+ builder: (context, state) {
+ var uriStr = state.uri.toString();
+ logger.t("uri $uriStr show questionnaire");
+ var idStr = state.pathParameters['customerId'];
+ var id = idStr == null ? null : int.tryParse(idStr);
+ return QuestionaireCustomerWidget(customerId: id ?? -1);
+ },
+ ),
+ ],
+ ),
],
redirect: (context, state) {
var uriStr = state.uri.toString();
@@ -69,7 +118,18 @@ class GlobalRouter {
LoginCredentials loginCredentials = DiContainer.get();
if (!loginCredentials.isLoggedIn) {
- return pathLogin;
+ if (uriStr != '/') {
+ if (uriStr.startsWith(pathFoto) && !uriStr.startsWith(pathFotoLogin)) {
+ var url = '$pathFotoLogin?redirect=${Uri.encodeComponent(uriStr)}';
+ logger.t("foto redirect to $url");
+ return url;
+ }
+ if (uriStr.startsWith(pathQuestionnaire) && !uriStr.startsWith(pathQuestionnaireLogin)) {
+ var url = '$pathQuestionnaireLogin?redirect=${Uri.encodeComponent(uriStr)}';
+ logger.t("questionnaire redirect to $url");
+ return url;
+ }
+ }
}
return null;
},
diff --git a/hartmann-foto-documentation-frontend/lib/utils/http_client_interceptor.dart b/hartmann-foto-documentation-frontend/lib/utils/http_client_interceptor.dart
index cec5ca8..ba57e18 100644
--- a/hartmann-foto-documentation-frontend/lib/utils/http_client_interceptor.dart
+++ b/hartmann-foto-documentation-frontend/lib/utils/http_client_interceptor.dart
@@ -31,7 +31,7 @@ class HttpClientInterceptor extends http.BaseClient {
// Navigate to login page using GoRouter
final context = GlobalRouter.rootNavigatorKey.currentContext;
if (context != null) {
- context.go(GlobalRouter.pathLogin);
+ context.go(GlobalRouter.pathFotoLogin);
}
}
}
\ No newline at end of file
diff --git a/hartmann-foto-documentation-frontend/test/pages/customer_list_widget_test.dart b/hartmann-foto-documentation-frontend/test/pages/foto/customer/customer_list_widget_test.dart
similarity index 91%
rename from hartmann-foto-documentation-frontend/test/pages/customer_list_widget_test.dart
rename to hartmann-foto-documentation-frontend/test/pages/foto/customer/customer_list_widget_test.dart
index c6608f1..8baaa52 100644
--- a/hartmann-foto-documentation-frontend/test/pages/customer_list_widget_test.dart
+++ b/hartmann-foto-documentation-frontend/test/pages/foto/customer/customer_list_widget_test.dart
@@ -10,8 +10,8 @@ import 'package:fotodocumentation/controller/customer_controller.dart';
import 'package:fotodocumentation/dto/customer_dto.dart';
import 'package:fotodocumentation/utils/di_container.dart';
-import '../testing/test_utils.dart';
-import '../testing/test_utils.mocks.dart';
+import '../../../testing/test_utils.dart';
+import '../../../testing/test_utils.mocks.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@@ -36,7 +36,7 @@ Future _searchtest(WidgetTester tester) async {
when(controller.getAll("", "")).thenAnswer((_) async => _list);
when(controller.getAll(searchText, "")).thenAnswer((_) async => [_list.first]);
- await pumpAppConfig(tester, GlobalRouter.pathHome);
+ await pumpAppConfig(tester, GlobalRouter.pathFotoHome);
verify(controller.getAll(argThat(equals("")), argThat(equals("")))).called(1);
await tester.enterText(find.byKey(Key("Search_text_field")), searchText);
diff --git a/hartmann-foto-documentation-frontend/test/pages/customer_widget_test.dart b/hartmann-foto-documentation-frontend/test/pages/foto/customer/customer_widget_test.dart
similarity index 86%
rename from hartmann-foto-documentation-frontend/test/pages/customer_widget_test.dart
rename to hartmann-foto-documentation-frontend/test/pages/foto/customer/customer_widget_test.dart
index 9d9c480..1fb1ae0 100644
--- a/hartmann-foto-documentation-frontend/test/pages/customer_widget_test.dart
+++ b/hartmann-foto-documentation-frontend/test/pages/foto/customer/customer_widget_test.dart
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fotodocumentation/dto/picture_dto.dart';
-import 'package:fotodocumentation/pages/customer/picture_delete_dialog.dart';
-import 'package:fotodocumentation/pages/customer/picture_widget.dart';
+import 'package:fotodocumentation/pages/foto/customer/foto_picture_delete_dialog.dart';
+import 'package:fotodocumentation/pages/foto/customer/foto_picture_widget.dart';
import 'package:fotodocumentation/utils/global_router.dart';
import 'package:fotodocumentation/utils/login_credentials.dart';
import 'package:mockito/mockito.dart';
@@ -14,8 +14,8 @@ import 'package:fotodocumentation/dto/customer_dto.dart';
import 'package:fotodocumentation/utils/di_container.dart';
import 'package:flutter_image_test_utils/flutter_image_test_utils.dart';
-import '../testing/test_utils.dart';
-import '../testing/test_utils.mocks.dart';
+import '../../../testing/test_utils.dart';
+import '../../../testing/test_utils.mocks.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@@ -33,7 +33,7 @@ void main() {
when(controller.get(id: 1)).thenAnswer((_) async => _dto);
when(controller.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1");
verify(controller.get(id: 1)).called(1);
// Click on the first row (InkWell) to open the picture popup
@@ -41,7 +41,7 @@ void main() {
await tester.pumpAndSettle();
// Verify that the popup is shown by checking for the PictureWidget
- expect(find.byType(PictureWidget), findsOneWidget);
+ expect(find.byType(FotoPictureWidget), findsOneWidget);
});
});
@@ -58,7 +58,7 @@ void main() {
when(pictureController.delete(argThat(isA()))).thenAnswer((_) async => true);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1");
verify(controller.get(id: 1)).called(1);
// Click on the first row (InkWell) to open the picture popup
@@ -66,7 +66,7 @@ void main() {
await tester.pumpAndSettle();
// Verify that the popup is shown by checking for the PictureWidget
- expect(find.byType(PictureDeleteDialog), findsOneWidget);
+ expect(find.byType(FotoPictureDeleteDialog), findsOneWidget);
// Click the yes button to confirm delete
await tester.tap(find.byKey(Key("picture_delete_yes")));
@@ -89,7 +89,7 @@ void main() {
when(controller.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1");
verify(controller.get(id: 1)).called(1);
// Click on the first row (InkWell) to open the picture popup
@@ -97,7 +97,7 @@ void main() {
await tester.pumpAndSettle();
// Verify that the popup is shown by checking for the PictureWidget
- expect(find.byType(PictureDeleteDialog), findsOneWidget);
+ expect(find.byType(FotoPictureDeleteDialog), findsOneWidget);
// Click the yes button to confirm delete
await tester.tap(find.byKey(Key("picture_delete_no")));
diff --git a/hartmann-foto-documentation-frontend/test/pages/picture_widget_test.dart b/hartmann-foto-documentation-frontend/test/pages/foto/customer/picture_widget_test.dart
similarity index 85%
rename from hartmann-foto-documentation-frontend/test/pages/picture_widget_test.dart
rename to hartmann-foto-documentation-frontend/test/pages/foto/customer/picture_widget_test.dart
index e271513..2af1068 100644
--- a/hartmann-foto-documentation-frontend/test/pages/picture_widget_test.dart
+++ b/hartmann-foto-documentation-frontend/test/pages/foto/customer/picture_widget_test.dart
@@ -4,14 +4,14 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:fotodocumentation/controller/customer_controller.dart';
import 'package:fotodocumentation/dto/customer_dto.dart' show CustomerDto, CustomerListDto;
import 'package:fotodocumentation/dto/picture_dto.dart';
-import 'package:fotodocumentation/pages/customer/picture_fullscreen_dialog.dart';
+import 'package:fotodocumentation/pages/foto/customer/foto_picture_fullscreen_dialog.dart';
import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/global_router.dart';
import 'package:fotodocumentation/utils/login_credentials.dart';
import 'package:mockito/mockito.dart';
-import '../testing/test_utils.dart';
-import '../testing/test_utils.mocks.dart';
+import '../../../testing/test_utils.dart';
+import '../../../testing/test_utils.mocks.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@@ -80,7 +80,7 @@ void main() {
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1${GlobalRouter.pathPicture}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1${GlobalRouter.pathFotoPicture}/1");
await tester.pumpAndSettle();
// Verify customer name is displayed
@@ -102,7 +102,7 @@ void main() {
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1${GlobalRouter.pathPicture}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1${GlobalRouter.pathFotoPicture}/1");
await tester.pumpAndSettle();
// Verify the comment is displayed
@@ -116,7 +116,7 @@ void main() {
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1${GlobalRouter.pathPicture}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1${GlobalRouter.pathFotoPicture}/1");
await tester.pumpAndSettle();
// The comment field should be empty but the label should exist
@@ -131,7 +131,7 @@ void main() {
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1${GlobalRouter.pathPicture}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1${GlobalRouter.pathFotoPicture}/1");
await tester.pumpAndSettle();
// Both navigation buttons should always be visible at the bottom
@@ -146,7 +146,7 @@ void main() {
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1${GlobalRouter.pathPicture}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1${GlobalRouter.pathFotoPicture}/1");
await tester.pumpAndSettle();
// Both navigation buttons should be visible
@@ -161,7 +161,7 @@ void main() {
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1${GlobalRouter.pathPicture}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1${GlobalRouter.pathFotoPicture}/1");
await tester.pumpAndSettle();
// Both navigation buttons should be visible (right one is disabled)
@@ -176,7 +176,7 @@ void main() {
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1${GlobalRouter.pathPicture}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1${GlobalRouter.pathFotoPicture}/1");
await tester.pumpAndSettle();
// Verify first picture comment is shown
@@ -199,7 +199,7 @@ void main() {
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1${GlobalRouter.pathPicture}/2");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1${GlobalRouter.pathFotoPicture}/2");
await tester.pumpAndSettle();
// Verify second picture comment is shown
@@ -229,7 +229,7 @@ void main() {
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1${GlobalRouter.pathPicture}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1${GlobalRouter.pathFotoPicture}/1");
await tester.pumpAndSettle();
// Both navigation buttons should be shown but disabled
@@ -244,7 +244,7 @@ void main() {
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
provideMockedNetworkImages(() async {
- await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/1${GlobalRouter.pathPicture}/1");
+ await pumpAppConfig(tester, "${GlobalRouter.pathFotoHome}${GlobalRouter.pathFotoCustomer}/1${GlobalRouter.pathFotoPicture}/1");
await tester.pumpAndSettle();
// Find the GestureDetector with the image key
@@ -261,7 +261,7 @@ void main() {
await tester.pumpAndSettle();
// Verify fullscreen dialog is shown
- expect(find.byType(PictureFullscreenDialog), findsOneWidget);
+ expect(find.byType(FotoPictureFullscreenDialog), findsOneWidget);
});
});
});
diff --git a/hartmann-foto-documentation-frontend/test/pages/foto/customer_widget_test.dart b/hartmann-foto-documentation-frontend/test/pages/foto/customer_widget_test.dart
new file mode 100644
index 0000000..e69de29
diff --git a/hartmann-foto-documentation-frontend/test/pages/login_widget_test.dart b/hartmann-foto-documentation-frontend/test/pages/foto/login/foto_login_widget_test.dart
similarity index 90%
rename from hartmann-foto-documentation-frontend/test/pages/login_widget_test.dart
rename to hartmann-foto-documentation-frontend/test/pages/foto/login/foto_login_widget_test.dart
index 5df168a..31f329f 100644
--- a/hartmann-foto-documentation-frontend/test/pages/login_widget_test.dart
+++ b/hartmann-foto-documentation-frontend/test/pages/foto/login/foto_login_widget_test.dart
@@ -2,14 +2,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fotodocumentation/controller/login_controller.dart';
import 'package:fotodocumentation/dto/jwt_token_pair_dto.dart';
-import 'package:fotodocumentation/pages/login/login_widget.dart';
+import 'package:fotodocumentation/pages/foto/login/foto_login_widget.dart';
import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/global_router.dart';
import 'package:fotodocumentation/utils/login_credentials.dart';
import 'package:mockito/mockito.dart';
-import '../testing/test_utils.dart';
-import '../testing/test_utils.mocks.dart';
+import '../../../testing/test_utils.dart';
+import '../../../testing/test_utils.mocks.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@@ -32,7 +32,7 @@ void main() {
testWidgets('displays login title', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024);
- await pumpApp(tester, const LoginWidget());
+ await pumpApp(tester, const FotoLoginWidget());
await tester.pumpAndSettle();
// Verify the login title is displayed (German localization)
@@ -42,7 +42,7 @@ void main() {
testWidgets('displays username and password fields', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024);
- await pumpApp(tester, const LoginWidget());
+ await pumpApp(tester, const FotoLoginWidget());
await tester.pumpAndSettle();
// Verify username field exists
@@ -55,7 +55,7 @@ void main() {
testWidgets('displays login button', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024);
- await pumpApp(tester, const LoginWidget());
+ await pumpApp(tester, const FotoLoginWidget());
await tester.pumpAndSettle();
// Verify login button exists
@@ -65,7 +65,7 @@ void main() {
testWidgets('can enter username and password', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024);
- await pumpApp(tester, const LoginWidget());
+ await pumpApp(tester, const FotoLoginWidget());
await tester.pumpAndSettle();
// Enter username
@@ -91,7 +91,7 @@ void main() {
when(mockLoginController.authenticate('testuser', 'testpassword'))
.thenAnswer((_) async => (jwtTokenPairDto: jwtTokenPairDto));
- await pumpAppConfig(tester, GlobalRouter.pathLogin);
+ await pumpAppConfig(tester, GlobalRouter.pathFotoLogin);
await tester.pumpAndSettle();
// Enter credentials
@@ -118,7 +118,7 @@ void main() {
when(mockLoginController.authenticate('testuser', 'testpassword'))
.thenAnswer((_) async => (jwtTokenPairDto: jwtTokenPairDto));
- await pumpAppConfig(tester, GlobalRouter.pathLogin);
+ await pumpAppConfig(tester, GlobalRouter.pathFotoLogin);
await tester.pumpAndSettle();
// Enter credentials
@@ -140,7 +140,7 @@ void main() {
when(mockLoginController.authenticate('testuser', 'wrongpassword'))
.thenAnswer((_) async => (jwtTokenPairDto: null));
- await pumpAppConfig(tester, GlobalRouter.pathLogin);
+ await pumpAppConfig(tester, GlobalRouter.pathFotoLogin);
await tester.pumpAndSettle();
// Enter credentials
@@ -162,7 +162,7 @@ void main() {
when(mockLoginController.authenticate('testuser', 'wrongpassword'))
.thenAnswer((_) async => (jwtTokenPairDto: null));
- await pumpApp(tester, const LoginWidget());
+ await pumpApp(tester, const FotoLoginWidget());
await tester.pumpAndSettle();
// Enter credentials
@@ -181,7 +181,7 @@ void main() {
testWidgets('password field obscures text', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024);
- await pumpApp(tester, const LoginWidget());
+ await pumpApp(tester, const FotoLoginWidget());
await tester.pumpAndSettle();
// Find the EditableText descendant of the password field
diff --git a/hartmann-foto-documentation-frontend/test/pages/questionnaire/login/questionnaire_login_widget_test.dart b/hartmann-foto-documentation-frontend/test/pages/questionnaire/login/questionnaire_login_widget_test.dart
new file mode 100644
index 0000000..763aadc
--- /dev/null
+++ b/hartmann-foto-documentation-frontend/test/pages/questionnaire/login/questionnaire_login_widget_test.dart
@@ -0,0 +1,200 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fotodocumentation/controller/login_controller.dart';
+import 'package:fotodocumentation/dto/jwt_token_pair_dto.dart';
+import 'package:fotodocumentation/pages/questionnaire/login/questionnaire_login_widget.dart';
+import 'package:fotodocumentation/utils/di_container.dart';
+import 'package:fotodocumentation/utils/global_router.dart';
+import 'package:fotodocumentation/utils/login_credentials.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../../testing/test_utils.dart';
+import '../../../testing/test_utils.mocks.dart';
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+ DiContainer.instance.initState();
+
+ late MockLoginController mockLoginController;
+ late MockLoginCredentials mockLoginCredentials;
+
+ setUp(() {
+ mockLoginController = MockLoginController();
+ mockLoginCredentials = MockLoginCredentials();
+
+ when(mockLoginCredentials.isLoggedIn).thenReturn(false);
+
+ DiContainer.instance.put(LoginController, mockLoginController);
+ DiContainer.instance.put(LoginCredentials, mockLoginCredentials);
+ });
+
+ group('LoginWidget', () {
+ testWidgets('displays login title', (WidgetTester tester) async {
+ setScreenSize(tester, 1024, 1024);
+
+ await pumpApp(tester, const QuestionaireLoginWidget());
+ await tester.pumpAndSettle();
+
+ // Verify the login title is displayed (German localization)
+ expect(find.byKey(const Key('login_title')), findsOneWidget);
+ });
+
+ testWidgets('displays username and password fields', (WidgetTester tester) async {
+ setScreenSize(tester, 1024, 1024);
+
+ await pumpApp(tester, const QuestionaireLoginWidget());
+ await tester.pumpAndSettle();
+
+ // Verify username field exists
+ expect(find.byKey(const Key("username")), findsOneWidget);
+
+ // Verify password field exists
+ expect(find.byKey(const Key("password")), findsOneWidget);
+ });
+
+ testWidgets('displays login button', (WidgetTester tester) async {
+ setScreenSize(tester, 1024, 1024);
+
+ await pumpApp(tester, const QuestionaireLoginWidget());
+ await tester.pumpAndSettle();
+
+ // Verify login button exists
+ expect(find.byKey(const Key("SubmitWidgetButton")), findsOneWidget);
+ });
+
+ testWidgets('can enter username and password', (WidgetTester tester) async {
+ setScreenSize(tester, 1024, 1024);
+
+ await pumpApp(tester, const QuestionaireLoginWidget());
+ await tester.pumpAndSettle();
+
+ // Enter username
+ await tester.enterText(find.byKey(const Key("username")), 'testuser');
+ await tester.pumpAndSettle();
+
+ // Enter password
+ await tester.enterText(find.byKey(const Key("password")), 'testpassword');
+ await tester.pumpAndSettle();
+
+ // Verify text was entered
+ expect(find.text('testuser'), findsOneWidget);
+ });
+
+ testWidgets('calls authenticate on login button tap', (WidgetTester tester) async {
+ setScreenSize(tester, 1024, 1024);
+
+ final jwtTokenPairDto = JwtTokenPairDto(
+ accessToken: 'test_access_token',
+ refreshToken: 'test_refresh_token',
+ );
+
+ when(mockLoginController.authenticate('testuser', 'testpassword'))
+ .thenAnswer((_) async => (jwtTokenPairDto: jwtTokenPairDto));
+
+ await pumpAppConfig(tester, GlobalRouter.pathFotoLogin);
+ await tester.pumpAndSettle();
+
+ // Enter credentials
+ await tester.enterText(find.byKey(const Key("username")), 'testuser');
+ await tester.enterText(find.byKey(const Key("password")), 'testpassword');
+ await tester.pumpAndSettle();
+
+ // Tap login button
+ await tester.tap(find.byKey(const Key("SubmitWidgetButton")));
+ await tester.pumpAndSettle();
+
+ // Verify authenticate was called with correct credentials
+ verify(mockLoginController.authenticate('testuser', 'testpassword')).called(1);
+ });
+
+ testWidgets('sets logged in on successful authentication', (WidgetTester tester) async {
+ setScreenSize(tester, 1024, 1024);
+
+ final jwtTokenPairDto = JwtTokenPairDto(
+ accessToken: 'test_access_token',
+ refreshToken: 'test_refresh_token',
+ );
+
+ when(mockLoginController.authenticate('testuser', 'testpassword'))
+ .thenAnswer((_) async => (jwtTokenPairDto: jwtTokenPairDto));
+
+ await pumpAppConfig(tester, GlobalRouter.pathFotoLogin);
+ await tester.pumpAndSettle();
+
+ // Enter credentials
+ await tester.enterText(find.byKey(const Key("username")), 'testuser');
+ await tester.enterText(find.byKey(const Key("password")), 'testpassword');
+ await tester.pumpAndSettle();
+
+ // Tap login button
+ await tester.tap(find.byKey(const Key("SubmitWidgetButton")));
+ await tester.pumpAndSettle();
+
+ // Verify setLoggedIn was called
+ verify(mockLoginCredentials.setLoggedIn(true)).called(1);
+ });
+
+ testWidgets('displays error message on failed authentication', (WidgetTester tester) async {
+ setScreenSize(tester, 1024, 1024);
+
+ when(mockLoginController.authenticate('testuser', 'wrongpassword'))
+ .thenAnswer((_) async => (jwtTokenPairDto: null));
+
+ await pumpAppConfig(tester, GlobalRouter.pathFotoLogin);
+ await tester.pumpAndSettle();
+
+ // Enter credentials
+ await tester.enterText(find.byKey(const Key("username")), 'testuser');
+ await tester.enterText(find.byKey(const Key("password")), 'wrongpassword');
+ await tester.pumpAndSettle();
+
+ // Tap login button
+ await tester.tap(find.byKey(const Key("SubmitWidgetButton")));
+ await tester.pumpAndSettle();
+
+ // Verify error message is displayed (German localization)
+ expect(find.text('Falscher Benutzername oder Passwort'), findsOneWidget);
+ });
+
+ testWidgets('does not call setLoggedIn on failed authentication', (WidgetTester tester) async {
+ setScreenSize(tester, 1024, 1024);
+
+ when(mockLoginController.authenticate('testuser', 'wrongpassword'))
+ .thenAnswer((_) async => (jwtTokenPairDto: null));
+
+ await pumpApp(tester, const QuestionaireLoginWidget());
+ await tester.pumpAndSettle();
+
+ // Enter credentials
+ await tester.enterText(find.byKey(const Key("username")), 'testuser');
+ await tester.enterText(find.byKey(const Key("password")), 'wrongpassword');
+ await tester.pumpAndSettle();
+
+ // Tap login button
+ await tester.tap(find.byKey(const Key("SubmitWidgetButton")));
+ await tester.pumpAndSettle();
+
+ // Verify setLoggedIn was NOT called
+ verifyNever(mockLoginCredentials.setLoggedIn(any));
+ });
+
+ testWidgets('password field obscures text', (WidgetTester tester) async {
+ setScreenSize(tester, 1024, 1024);
+
+ await pumpApp(tester, const QuestionaireLoginWidget());
+ await tester.pumpAndSettle();
+
+ // Find the EditableText descendant of the password field
+ // TextFormField wraps TextField which contains EditableText
+ final passwordFieldFinder = find.byKey(const Key("password"));
+ final editableTextFinder = find.descendant(
+ of: passwordFieldFinder,
+ matching: find.byType(EditableText),
+ );
+ final editableText = tester.widget(editableTextFinder);
+
+ // Verify obscureText is true
+ expect(editableText.obscureText, isTrue);
+ });
+ });
+}
diff --git a/hartmann-foto-documentation-web/src/main/webapp/WEB-INF/web.xml b/hartmann-foto-documentation-web/src/main/webapp/WEB-INF/web.xml
index 160e16e..5c0dd39 100644
--- a/hartmann-foto-documentation-web/src/main/webapp/WEB-INF/web.xml
+++ b/hartmann-foto-documentation-web/src/main/webapp/WEB-INF/web.xml
@@ -9,6 +9,12 @@
/api/picture
/api/picture/*
+ /api/questionnairecustomer
+ /api/questionnairecustomer/*
+
+ /api/questionnaire
+ /api/questionnaire/*
+
GET
POST
PUT