diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Customer.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Customer.java index 6189c77..d15f2f5 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Customer.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Customer.java @@ -20,12 +20,10 @@ import jakarta.persistence.*; @Entity @Table(name = "customer") -@NamedQuery(name = Customer.FIND_ALL, query = "select c from Customer c order by c.name") @NamedQuery(name = Customer.FIND_BY_NUMBER, query = "select c from Customer c where c.customerNumber = :cutomerNumber") public class Customer extends AbstractDateEntity { private static final long serialVersionUID = 1L; public static final String SEQUENCE = "customer_seq"; - public static final String FIND_ALL = "Customer.findAll"; public static final String FIND_BY_NUMBER = "Customer.findByNumber"; public static final String PARAM_NUMBER = "cutomerNumber"; diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Questionnaire.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Questionnaire.java new file mode 100644 index 0000000..425f2c7 --- /dev/null +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/Questionnaire.java @@ -0,0 +1,181 @@ +package marketing.heyday.hartmann.fotodocumentation.core.model; + +import java.util.Date; + +import org.apache.commons.lang.builder.HashCodeBuilder; + +import jakarta.persistence.*; + +/** + * + * + *

Copyright: Copyright (c) 2024

+ *

Company: heyday Marketing GmbH

+ * @author Patrick Verboom + * @version 1.0 + * + * created: 19 Jan 2026 + */ + +@Entity +@Table(name = "questionnaire") +public class Questionnaire extends AbstractDateEntity { + private static final long serialVersionUID = 1L; + public static final String SEQUENCE = "questionnaire_seq"; + + @Id + @Column(name = "questionnaire_id", length = 22) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE) + @SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1) + private Long questionnaireId; + + // username from the person that shot the picture + @Column(name = "username") + private String username; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "questionnaire_date", nullable = false) + private Date questionnaireDate; + + @Basic(fetch = FetchType.LAZY) + private String comment; + + @Column(name = "evaluation") + private Integer evaluation; + + @Column + private String category; + + @Column(name = "questions") + @Basic(fetch = FetchType.LAZY) + private String questions; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "customer_id_fk") + private QuestionnaireCustomer customer; + + + public Long getQuestionnaireId() { + return questionnaireId; + } + + public void setQuestionnaireId(Long questionnaireId) { + this.questionnaireId = questionnaireId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Date getQuestionnaireDate() { + return questionnaireDate; + } + + public void setQuestionnaireDate(Date questionnaireDate) { + this.questionnaireDate = questionnaireDate; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public Integer getEvaluation() { + return evaluation; + } + + public void setEvaluation(Integer evaluation) { + this.evaluation = evaluation; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getQuestions() { + return questions; + } + + public void setQuestions(String questions) { + this.questions = questions; + } + + public QuestionnaireCustomer getCustomer() { + return customer; + } + + public void setCustomer(QuestionnaireCustomer customer) { + this.customer = customer; + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(questionnaireId).toHashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass() || questionnaireId == null) { + return false; + } + return this.questionnaireId.equals(((Questionnaire) obj).getQuestionnaireId()); + } + + public static class Builder { + private Questionnaire instance = new Questionnaire(); + + public Builder(){ + instance.evaluation = 0; + } + + public Builder username(String username) { + instance.setUsername(username); + return this; + } + + public Builder questionnaireDate(Date date) { + instance.setQuestionnaireDate(date); + return this; + } + + public Builder comment(String comment) { + instance.setComment(comment); + return this; + } + + public Builder category(String category) { + instance.setCategory(category); + return this; + } + + public Builder questions(String questions) { + instance.setQuestions(questions); + return this; + } + + public Builder evaluation(Integer evaluation) { + instance.setEvaluation(evaluation); + return this; + } + + public Builder customer(QuestionnaireCustomer customer) { + instance.setCustomer(customer); + return this; + } + + public Questionnaire build() { + return instance; + } + } +} diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/QuestionnaireCustomer.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/QuestionnaireCustomer.java new file mode 100644 index 0000000..82ce87b --- /dev/null +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/model/QuestionnaireCustomer.java @@ -0,0 +1,140 @@ +package marketing.heyday.hartmann.fotodocumentation.core.model; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.lang.builder.HashCodeBuilder; + +import jakarta.persistence.*; + +/** + * + * + *

Copyright: Copyright (c) 2024

+ *

Company: heyday Marketing GmbH

+ * @author Patrick Verboom + * @version 1.0 + * + * created: 19 Jan 2026 + */ + +@Entity +@Table(name = "questionnaire_customer") +@NamedQuery(name = QuestionnaireCustomer.FIND_BY_NUMBER, query = "select c from QuestionnaireCustomer c where c.customerNumber = :customerNumber") +public class QuestionnaireCustomer extends AbstractDateEntity { + private static final long serialVersionUID = 1L; + public static final String SEQUENCE = "questionnaire_customer_seq"; + + public static final String FIND_BY_NUMBER = "QuestionnaireCustomer.findByNumber"; + public static final String PARAM_NUMBER = "customerNumber"; + + @Id + @Column(name = "customer_id", length = 22) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE) + @SequenceGenerator(name = SEQUENCE, sequenceName = SEQUENCE, allocationSize = 1) + private Long customerId; + + @Column(name = "customer_number", unique = true, nullable = false) + private String customerNumber; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "city") + private String city; + + @Column(name = "zip") + private String zip; + + @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true) + private Set questionnaires = new HashSet<>(); + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getCustomerNumber() { + return customerNumber; + } + + public void setCustomerNumber(String customerNumber) { + this.customerNumber = customerNumber; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getZip() { + return zip; + } + + public void setZip(String zip) { + this.zip = zip; + } + + public Set getQuestionnaires() { + return questionnaires; + } + + public void setQuestionnaires(Set questionnaires) { + this.questionnaires = questionnaires; + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(customerNumber).toHashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + return this.customerNumber.equals(((QuestionnaireCustomer) obj).getCustomerNumber()); + } + + public static class Builder { + private QuestionnaireCustomer instance = new QuestionnaireCustomer(); + + public Builder customerNumber(String customerNumber) { + instance.setCustomerNumber(customerNumber); + return this; + } + + public Builder name(String name) { + instance.setName(name); + return this; + } + + public Builder city(String city) { + instance.setCity(city); + return this; + } + + public Builder zip(String zip) { + instance.setZip(zip); + return this; + } + + public QuestionnaireCustomer build() { + return instance; + } + } +} diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnaireCustomerService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnaireCustomerService.java new file mode 100644 index 0000000..da4b14a --- /dev/null +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnaireCustomerService.java @@ -0,0 +1,121 @@ +package marketing.heyday.hartmann.fotodocumentation.core.service; + +import java.util.*; + +import org.apache.commons.lang3.StringUtils; + +import jakarta.annotation.security.PermitAll; +import jakarta.ejb.LocalBean; +import jakarta.ejb.Stateless; +import jakarta.inject.Inject; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.*; +import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire; +import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer; +import marketing.heyday.hartmann.fotodocumentation.core.utils.CalendarUtil; +import marketing.heyday.hartmann.fotodocumentation.core.utils.ExcelUtils; +import marketing.heyday.hartmann.fotodocumentation.core.utils.PdfUtils; +import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerListValue; +import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerValue; +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: 18 Feb 2026 + */ +@Stateless +@LocalBean +@PermitAll +public class QuestionnaireCustomerService extends AbstractService { + + @Inject + private ExcelUtils excelUtils; + + @Inject + private CalendarUtil calendarUtil; + + // query = search for name, number and date + public List getAll(String queryStr, String startsWith) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = builder.createQuery(QuestionnaireCustomer.class); + Root customerRoot = criteriaQuery.from(QuestionnaireCustomer.class); + + criteriaQuery = criteriaQuery.select(customerRoot).distinct(true); + + List predicates = new ArrayList<>(); + + if (StringUtils.isNotBlank(startsWith)) { + String param = startsWith.toLowerCase() + "%"; + var pred = builder.like(builder.lower(customerRoot.get("name")), param); + predicates.add(pred); + } + + if (StringUtils.isNotBlank(queryStr)) { + + // check if it contains a date + Date date = calendarUtil.parse(queryStr); + if (date != null) { + Date startOfDay = calendarUtil.getStartOfDay(date); + Date endOfDay = calendarUtil.getEndOfDay(date); + + Fetch picturesFetch = customerRoot.fetch("pictures", JoinType.LEFT); + @SuppressWarnings("unchecked") + Join pictures = (Join) picturesFetch; + + var predicateDate = builder.between(pictures.get("pictureDate"), startOfDay, endOfDay); + predicates.add(predicateDate); + + } else { + String param = "%" + StringUtils.trimToEmpty(queryStr).toLowerCase() + "%"; + var predicateName = builder.like(builder.lower(customerRoot.get("name")), param); + var predicateNr = builder.like(builder.lower(customerRoot.get("customerNumber")), param); + + var pred = builder.or(predicateName, predicateNr); + predicates.add(pred); + } + } + + if (predicates.size() == 1) { + criteriaQuery = criteriaQuery.where(predicates.getFirst()); + } else if (predicates.size() > 1) { + criteriaQuery = criteriaQuery.where(builder.and(predicates.toArray(new Predicate[0]))); + } + + TypedQuery typedQuery = entityManager.createQuery(criteriaQuery); + List customers = typedQuery.getResultList(); + customers.forEach(c -> c.getQuestionnaires().size()); + return customers.parallelStream().map(QuestionnaireCustomerListValue::builder).toList(); + } + + public QuestionnaireCustomerValue get(Long id) { + QuestionnaireCustomer customer = entityManager.find(QuestionnaireCustomer.class, id); + if (customer == null) { + return null; + } + + return QuestionnaireCustomerValue.builder(customer); + } + + public byte[] getExport(Long id, Long questionnaireId) { + QuestionnaireCustomer customer = entityManager.find(QuestionnaireCustomer.class, id); + if (customer == null) { + return new byte[0]; + } + + List questionnaires = customer.getQuestionnaires().stream().sorted((x, y) -> x.getQuestionnaireDate().compareTo(y.getQuestionnaireDate())).toList(); + + if (questionnaireId != null) { + Optional 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