rework ui

This commit is contained in:
verboomp
2026-01-29 12:37:44 +01:00
parent 38979c99e5
commit e062b4c688
18 changed files with 462 additions and 246 deletions

View File

@@ -1,8 +1,12 @@
package marketing.heyday.hartmann.fotodocumentation.core.service; 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.annotation.security.PermitAll;
import jakarta.ejb.LocalBean; import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless; import jakarta.ejb.Stateless;
import jakarta.persistence.EntityNotFoundException;
import marketing.heyday.hartmann.fotodocumentation.core.model.Picture; import marketing.heyday.hartmann.fotodocumentation.core.model.Picture;
import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState; import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.StorageState;
@@ -19,8 +23,22 @@ import marketing.heyday.hartmann.fotodocumentation.core.utils.StorageUtils.Stora
@LocalBean @LocalBean
@PermitAll @PermitAll
public class PictureService extends AbstractService { public class PictureService extends AbstractService {
private static final Log LOG = LogFactory.getLog(PictureService.class);
public StorageState delete(Long id) { public StorageState delete(Long id) {
return super.delete(Picture.class, id); return super.delete(Picture.class, id);
} }
public StorageState updateEvaluationStatus(Long id, Integer value) {
try {
Picture entity = entityManager.getReference(Picture.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;
}
}
} }

View File

@@ -7,9 +7,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ejb.EJB; import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.DELETE; import jakarta.ws.rs.*;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder; import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.Response.Status;
@@ -45,6 +43,18 @@ public class PictureResource {
return deleteResponse(state).build(); return deleteResponse(state).build();
} }
@PUT
@Path("evaluation/{id}")
@Operation(summary = "Update evaluation for picture data to database")
@ApiResponse(responseCode = "200", description = "Task successfully updated")
public Response doUpdateTask(@PathParam("id") Long id, @QueryParam("evaluation") Integer value) {
if(value == null || value < 1 || value >3) {
return Response.status(Status.BAD_REQUEST).build();
}
StorageState state = pictureService.updateEvaluationStatus(id, value);
return deleteResponse(state).build();
}
protected ResponseBuilder deleteResponse(StorageState state) { protected ResponseBuilder deleteResponse(StorageState state) {
return switch(state) { return switch(state) {
case OK -> Response.status(Status.OK); case OK -> Response.status(Status.OK);

View File

@@ -74,6 +74,14 @@ abstract class BaseController {
var response = await client.delete(uri, headers: {cred.name: cred.value}); var response = await client.delete(uri, headers: {cred.name: cred.value});
return response.statusCode == 200; return response.statusCode == 200;
} }
Future<bool> runPutWithAuth(String uriStr) async {
http.Client client = httpClientUtils.client;
Header cred = await getAuthHeader();
Uri uri = Uri.parse(uriStr);
var response = await client.put(uri, headers: {cred.name: cred.value});
return response.statusCode == 200;
}
} }
class Header { class Header {

View File

@@ -3,6 +3,7 @@ import 'package:fotodocumentation/dto/picture_dto.dart';
abstract interface class PictureController { abstract interface class PictureController {
Future<bool> delete(PictureDto dto); Future<bool> delete(PictureDto dto);
Future<bool> updateEvaluation(PictureDto dto);
} }
class PictureControllerImpl extends BaseController implements PictureController { class PictureControllerImpl extends BaseController implements PictureController {
@@ -13,4 +14,10 @@ class PictureControllerImpl extends BaseController implements PictureController
String uriStr = '${uriUtils.getBaseUrl()}$path/${dto.id}'; String uriStr = '${uriUtils.getBaseUrl()}$path/${dto.id}';
return runDeleteWithAuth(uriStr); return runDeleteWithAuth(uriStr);
} }
@override
Future<bool> updateEvaluation(PictureDto dto) {
String uriStr = '${uriUtils.getBaseUrl()}$path/evaluation/${dto.id}?evaluation=${dto.evaluation}';
return runPutWithAuth(uriStr);
}
} }

View File

@@ -5,7 +5,7 @@ class PictureDto {
final String? comment; final String? comment;
final String? category; final String? category;
final String image; final String image;
final int evaluation; int evaluation;
final DateTime pictureDate; final DateTime pictureDate;
final String? username; final String? username;

View File

@@ -60,15 +60,15 @@
"@deleteDialogTitle": { "@deleteDialogTitle": {
"description": "Delete dialog title" "description": "Delete dialog title"
}, },
"deleteDialogText": "Sind Sie sicher, dass Sie diese Eintrag löschen möchten?", "deleteDialogText": "Sicher, dass Sie den Eintrag löschen möchten?",
"@deleteDialogText": { "@deleteDialogText": {
"description": "Delete dialog text" "description": "Delete dialog text"
}, },
"deleteDialogButtonCancel": "Nein", "deleteDialogButtonCancel": "Abbrechen",
"@deleteDialogButtonCancel": { "@deleteDialogButtonCancel": {
"description": "Cancel Button text" "description": "Cancel Button text"
}, },
"deleteDialogButtonApprove": "Ja", "deleteDialogButtonApprove": "Ja, fortfahren",
"@deleteDialogButtonApprove": { "@deleteDialogButtonApprove": {
"description": "Approve Button text" "description": "Approve Button text"
}, },

View File

@@ -169,19 +169,19 @@ abstract class AppLocalizations {
/// Delete dialog text /// Delete dialog text
/// ///
/// In de, this message translates to: /// In de, this message translates to:
/// **'Sind Sie sicher, dass Sie diese Eintrag löschen möchten?'** /// **'Sicher, dass Sie den Eintrag löschen möchten?'**
String get deleteDialogText; String get deleteDialogText;
/// Cancel Button text /// Cancel Button text
/// ///
/// In de, this message translates to: /// In de, this message translates to:
/// **'Nein'** /// **'Abbrechen'**
String get deleteDialogButtonCancel; String get deleteDialogButtonCancel;
/// Approve Button text /// Approve Button text
/// ///
/// In de, this message translates to: /// In de, this message translates to:
/// **'Ja'** /// **'Ja, fortfahren'**
String get deleteDialogButtonApprove; String get deleteDialogButtonApprove;
/// Customer list table header for customer number /// Customer list table header for customer number

View File

@@ -50,13 +50,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get deleteDialogText => String get deleteDialogText =>
'Sind Sie sicher, dass Sie diese Eintrag löschen möchten?'; 'Sicher, dass Sie den Eintrag löschen möchten?';
@override @override
String get deleteDialogButtonCancel => 'Nein'; String get deleteDialogButtonCancel => 'Abbrechen';
@override @override
String get deleteDialogButtonApprove => 'Ja'; String get deleteDialogButtonApprove => 'Ja, fortfahren';
@override @override
String get customerListHeaderCustomerNumber => 'Kunden-Nr.'; String get customerListHeaderCustomerNumber => 'Kunden-Nr.';

View File

@@ -224,7 +224,8 @@ class _CustomerListWidgetState extends State<CustomerListWidget> {
} }
Future<void> _actionSelect(BuildContext context, CustomerListDto dto) async { Future<void> _actionSelect(BuildContext context, CustomerListDto dto) async {
context.push("${GlobalRouter.pathCustomer}/${dto.id}"); String uri = "${GlobalRouter.pathHome}${GlobalRouter.pathCustomer}/${dto.id}";
context.go(uri);
} }
void _reloadData() { void _reloadData() {

View File

@@ -1,26 +1,22 @@
import 'dart:convert' show base64Decode; import 'dart:convert' show base64Decode;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fotodocumentation/controller/picture_controller.dart';
import 'package:fotodocumentation/dto/picture_dto.dart';
import 'package:fotodocumentation/pages/customer/back_button.dart';
import 'package:fotodocumentation/pages/customer/picture_delete_dialog.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:fotodocumentation/controller/base_controller.dart'; import 'package:fotodocumentation/controller/base_controller.dart';
import 'package:fotodocumentation/l10n/app_localizations.dart';
import 'package:fotodocumentation/controller/customer_controller.dart'; import 'package:fotodocumentation/controller/customer_controller.dart';
import 'package:fotodocumentation/controller/picture_controller.dart';
import 'package:fotodocumentation/dto/customer_dto.dart'; 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/ui_utils/component/general_error_widget.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/page_header_widget.dart';
import 'package:fotodocumentation/pages/ui_utils/component/waiting_widget.dart'; import 'package:fotodocumentation/pages/ui_utils/component/waiting_widget.dart';
import 'package:fotodocumentation/pages/ui_utils/general_style.dart'; import 'package:fotodocumentation/pages/ui_utils/general_style.dart';
import 'package:fotodocumentation/utils/di_container.dart'; import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/global_router.dart'; import 'package:fotodocumentation/utils/global_router.dart';
import 'package:fotodocumentation/pages/customer/picture_widget.dart'; import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
class CustomerWidget extends StatefulWidget { class CustomerWidget extends StatefulWidget {
final int customerId; final int customerId;
@@ -98,7 +94,6 @@ class _CustomerWidgetState extends State<CustomerWidget> {
children: [ children: [
PageHeaderWidget(text: dto.name, subText: subText), PageHeaderWidget(text: dto.name, subText: subText),
CustomerBackButton(path: GlobalRouter.pathHome), CustomerBackButton(path: GlobalRouter.pathHome),
//_backButton(context),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@@ -209,7 +204,7 @@ class _CustomerWidgetState extends State<CustomerWidget> {
); );
final dateStr = _dateFormat.format(pictureDto.pictureDate); final dateStr = _dateFormat.format(pictureDto.pictureDate);
final evaluationColor = _generalStyle.evaluationColor(); // FIXME: set to color from DB final evaluationColor = _generalStyle.evaluationColor(value: pictureDto.evaluation); // FIXME: set to color from DB
return InkWell( return InkWell(
key: Key("table_row_${customerDto.id}"), key: Key("table_row_${customerDto.id}"),
onTap: () => _actionSelect(context, customerDto, pictureDto), onTap: () => _actionSelect(context, customerDto, pictureDto),
@@ -288,33 +283,6 @@ class _CustomerWidgetState extends State<CustomerWidget> {
); );
} }
Widget _backButton(BuildContext context) {
return ElevatedButton.icon(
onPressed: () => context.push(GlobalRouter.pathHome),
icon: Icon(
Icons.chevron_left,
color: _generalStyle.secondaryTextLabelColor,
size: 24,
),
label: Text(
AppLocalizations.of(context)!.backButtonLabel,
style: TextStyle(
fontFamily: _generalStyle.fontFamily,
fontWeight: FontWeight.normal,
fontSize: 16,
color: _generalStyle.secondaryTextLabelColor,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
Widget _downloadButton(BuildContext context) { Widget _downloadButton(BuildContext context) {
return ElevatedButton.icon( return ElevatedButton.icon(
key: Key("download_all_button"), key: Key("download_all_button"),
@@ -360,7 +328,11 @@ class _CustomerWidgetState extends State<CustomerWidget> {
} }
Future<void> _actionSelect(BuildContext context, CustomerDto customerDto, PictureDto pictureDto) async { Future<void> _actionSelect(BuildContext context, CustomerDto customerDto, PictureDto pictureDto) async {
context.go(GlobalRouter.pathPicture, extra: PictureWidgetHolder(customerDto, pictureDto)); String uri = "${GlobalRouter.pathHome}${GlobalRouter.pathPicture}/${customerDto.id}/${pictureDto.id}";
context.go(uri);
setState(() {
_dto = _customerController.get(id: widget.customerId);
});
} }
Future<void> _actionDownload(BuildContext context) async { Future<void> _actionDownload(BuildContext context) async {

View File

@@ -11,41 +11,77 @@ class PictureDeleteDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text( actionsAlignment: MainAxisAlignment.center,
AppLocalizations.of(context)!.deleteDialogTitle, actionsPadding: EdgeInsets.only(bottom: 50),
style: TextStyle( content: Column(
fontFamily: _generalStyle.fontFamily, mainAxisSize: MainAxisSize.min,
fontWeight: FontWeight.bold, children: [
color: _generalStyle.secondaryWidgetBackgroundColor, 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,
), ),
), ),
content: Text( ),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.only(left:50, right: 50, bottom: 50),
child: Text(
AppLocalizations.of(context)!.deleteDialogText, AppLocalizations.of(context)!.deleteDialogText,
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontFamily: _generalStyle.fontFamily, fontFamily: _generalStyle.fontFamily,
color: _generalStyle.secondaryTextLabelColor, color: _generalStyle.secondaryTextLabelColor,
fontSize: 28.71,
fontWeight: FontWeight.normal,
), ),
), ),
),
],
),
actions: [ actions: [
TextButton( ElevatedButton(
key: Key("picture_delete_no"), key: Key("picture_delete_no"),
onPressed: () => Navigator.of(context).pop(false), 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( child: Text(
AppLocalizations.of(context)!.deleteDialogButtonCancel, AppLocalizations.of(context)!.deleteDialogButtonCancel,
style: TextStyle( style: TextStyle(
fontFamily: _generalStyle.fontFamily, fontFamily: _generalStyle.fontFamily,
color: _generalStyle.secondaryTextLabelColor, color: _generalStyle.deleteCancelTextColor,
fontWeight: FontWeight.bold,
fontSize: 18.37,
), ),
), ),
), ),
TextButton( ElevatedButton(
key: Key("picture_delete_yes"), key: Key("picture_delete_yes"),
onPressed: () => Navigator.of(context).pop(true), 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( child: Text(
AppLocalizations.of(context)!.deleteDialogButtonApprove, AppLocalizations.of(context)!.deleteDialogButtonApprove,
style: TextStyle( style: TextStyle(
fontFamily: _generalStyle.fontFamily, fontFamily: _generalStyle.fontFamily,
color: _generalStyle.errorColor, color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18.37,
), ),
), ),
), ),

View File

@@ -1,27 +1,25 @@
import 'dart:convert' show base64Decode; import 'dart:convert' show base64Decode;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fotodocumentation/controller/base_controller.dart';
import 'package:fotodocumentation/controller/customer_controller.dart';
import 'package:fotodocumentation/controller/picture_controller.dart';
import 'package:fotodocumentation/dto/customer_dto.dart'; import 'package:fotodocumentation/dto/customer_dto.dart';
import 'package:fotodocumentation/dto/picture_dto.dart'; import 'package:fotodocumentation/dto/picture_dto.dart';
import 'package:fotodocumentation/pages/customer/customer_back_button.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/customer/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/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/pages/ui_utils/general_style.dart';
import 'package:fotodocumentation/utils/di_container.dart'; import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/global_router.dart'; import 'package:fotodocumentation/utils/global_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class PictureWidgetHolder {
final CustomerDto customerDto;
final PictureDto pictureDto;
const PictureWidgetHolder(this.customerDto, this.pictureDto);
}
class PictureWidget extends StatefulWidget { class PictureWidget extends StatefulWidget {
final CustomerDto customerDto; final int customerId;
final PictureDto pictureDto; final int pictureId;
const PictureWidget({super.key, required this.customerDto, required this.pictureDto}); const PictureWidget({super.key, required this.customerId, required this.pictureId});
@override @override
State<PictureWidget> createState() => _PictureWidgetState(); State<PictureWidget> createState() => _PictureWidgetState();
@@ -29,16 +27,21 @@ class PictureWidget extends StatefulWidget {
class _PictureWidgetState extends State<PictureWidget> { class _PictureWidgetState extends State<PictureWidget> {
GeneralStyle get _generalStyle => DiContainer.get(); GeneralStyle get _generalStyle => DiContainer.get();
CustomerController get _customerController => DiContainer.get();
PictureController get _pictureController => DiContainer.get();
late PictureDto _selectedPicture; late CustomerDto _customerDto;
PictureDto? _selectedPicture;
late DateFormat _dateFormat; late DateFormat _dateFormat;
final ScrollController _commentScrollController = ScrollController(); final ScrollController _commentScrollController = ScrollController();
late Future<CustomerDto?> _dto;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_dateFormat = DateFormat('dd.MM.yyyy, hh:mm'); _dateFormat = DateFormat('dd.MM.yyyy, hh:mm');
_selectedPicture = widget.pictureDto; _dto = _customerController.get(id: widget.customerId);
} }
@override @override
@@ -54,30 +57,58 @@ class _PictureWidgetState extends State<PictureWidget> {
color: _generalStyle.pageBackgroundColor, color: _generalStyle.pageBackgroundColor,
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 8.0, left: 50.0, right: 50.0, bottom: 8.0), padding: const EdgeInsets.only(top: 8.0, left: 50.0, right: 50.0, bottom: 8.0),
child: _body(context), child: _future(context),
), ),
), ),
); );
} }
Widget _body(BuildContext context) { Widget _future(BuildContext context) {
final pictures = widget.customerDto.pictures; return FutureBuilder<CustomerDto?>(
future: _dto,
builder: (BuildContext context, AsyncSnapshot<CustomerDto?> snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const WaitingWidget();
}
if (snapshot.hasData) {
CustomerDto? dto = snapshot.data;
if (dto == null) {
return GeneralErrorWidget(error: "FIXME"); // FIXME: set error text data not found
}
_customerDto = dto;
_selectedPicture ??= dto.pictures.firstWhere((p) => p.id == widget.pictureId);
_selectedPicture ??= _customerDto.pictures.firstOrNull;
if (_selectedPicture == null) {
return GeneralErrorWidget(error: "FIXME"); // FIXME: set error text data not found
}
return _body(context, _selectedPicture!);
} else if (snapshot.hasError) {
var error = snapshot.error;
return (error is ServerError) ? GeneralErrorWidget.fromServerError(error) : GeneralErrorWidget(error: snapshot.error.toString());
}
return const WaitingWidget();
},
);
}
Widget _body(BuildContext context, PictureDto selectedPicture) {
final pictures = _customerDto.pictures;
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
PageHeaderWidget(text: widget.customerDto.name), PageHeaderWidget(text: _customerDto.name),
CustomerBackButton(path: GlobalRouter.pathCustomer), CustomerBackButton(path: GlobalRouter.pathCustomer),
const SizedBox(height: 24), const SizedBox(height: 24),
Expanded( Expanded(
child: _mainWidget(context), child: _mainWidget(context, selectedPicture),
), ),
_bottomNavigationWidget(pictures), _bottomNavigationWidget(pictures, selectedPicture),
], ],
); );
} }
Widget _mainWidget(BuildContext context) { Widget _mainWidget(BuildContext context, PictureDto selectedPicture) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final isNarrow = constraints.maxWidth < 800; final isNarrow = constraints.maxWidth < 800;
@@ -87,18 +118,18 @@ class _PictureWidgetState extends State<PictureWidget> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_imageWidget(_selectedPicture), _imageWidget(selectedPicture),
const SizedBox(height: 32), const SizedBox(height: 32),
_contentWidget(_selectedPicture), _contentWidget(selectedPicture),
], ],
) )
: Row( : Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_imageWidget(_selectedPicture), _imageWidget(selectedPicture),
const SizedBox(width: 32), const SizedBox(width: 32),
Expanded(child: _contentWidget(_selectedPicture)), Expanded(child: _contentWidget(selectedPicture)),
], ],
), ),
); );
@@ -148,7 +179,9 @@ class _PictureWidgetState extends State<PictureWidget> {
color: _generalStyle.secondaryTextLabelColor, color: _generalStyle.secondaryTextLabelColor,
); );
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text( Text(
_dateFormat.format(dto.pictureDate), _dateFormat.format(dto.pictureDate),
style: TextStyle( style: TextStyle(
@@ -158,18 +191,22 @@ class _PictureWidgetState extends State<PictureWidget> {
color: _generalStyle.primaryTextLabelColor, color: _generalStyle.primaryTextLabelColor,
), ),
), ),
const SizedBox(height: 20),
Padding( Row(
padding: const EdgeInsets.only(top: 20.0), crossAxisAlignment: CrossAxisAlignment.start,
child: Text( children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"KUNDENNUMMER", "KUNDENNUMMER",
style: labelStyle, style: labelStyle,
), ),
),
Padding( Padding(
padding: const EdgeInsets.only(top: 4.0), padding: const EdgeInsets.only(top: 4.0),
child: Text( child: Text(
widget.customerDto.customerNumber, _customerDto.customerNumber,
style: contentStyle, style: contentStyle,
), ),
), ),
@@ -199,12 +236,93 @@ class _PictureWidgetState extends State<PictureWidget> {
), ),
), ),
), ),
]); ],
),
),
const SizedBox(width: 24),
_evaluationCard(dto),
],
),
],
);
}
Widget _evaluationCard(PictureDto dto) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"BEWERTUNG",
style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 16,
fontFamily: _generalStyle.fontFamily,
color: _generalStyle.primaryTextLabelColor,
),
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
_evaluationCircle(
key: const Key("evaluation_good"),
color: _generalStyle.evaluationGoodColor,
value: 1,
selected: dto.evaluation == 1,
),
const SizedBox(width: 12),
_evaluationCircle(
key: const Key("evaluation_middle"),
color: _generalStyle.evaluationMiddleColor,
value: 2,
selected: dto.evaluation == 2,
),
const SizedBox(width: 12),
_evaluationCircle(
key: const Key("evaluation_bad"),
color: _generalStyle.evaluationBadColor,
value: 3,
selected: dto.evaluation == 3,
),
],
),
],
),
),
);
}
Widget _evaluationCircle({
required Key key,
required Color color,
required int value,
required bool selected,
}) {
return InkWell(
key: key,
onTap: () async => _actionUpdateEvaluation(value),
customBorder: const CircleBorder(),
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: selected ? Border.all(color: _generalStyle.secondaryWidgetBackgroundColor, width: 3) : null,
),
),
);
} }
// Bottom navigation buttons // Bottom navigation buttons
Widget _bottomNavigationWidget(List<PictureDto> pictures) { Widget _bottomNavigationWidget(List<PictureDto> pictures, PictureDto selectedPicture) {
final currentIndex = pictures.indexWhere((p) => p.id == _selectedPicture.id); final currentIndex = pictures.indexWhere((p) => p.id == selectedPicture.id);
final hasPrevious = currentIndex > 0; final hasPrevious = currentIndex > 0;
final hasNext = currentIndex < pictures.length - 1; final hasNext = currentIndex < pictures.length - 1;
@@ -262,8 +380,16 @@ class _PictureWidgetState extends State<PictureWidget> {
); );
} }
Future<void> _actionUpdateEvaluation(int value) async {
_selectedPicture?.evaluation = value;
_pictureController.updateEvaluation(_selectedPicture!);
setState(() {
});
}
void _actionNavigateToPicture(int index) { void _actionNavigateToPicture(int index) {
final pictures = widget.customerDto.pictures; final pictures = _customerDto.pictures;
if (index >= 0 && index < pictures.length) { if (index >= 0 && index < pictures.length) {
setState(() { setState(() {
_selectedPicture = pictures[index]; _selectedPicture = pictures[index];

View File

@@ -15,7 +15,7 @@ class CustomerBackButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ElevatedButton.icon( return ElevatedButton.icon(
onPressed: () => context.go(path), onPressed: () => context.pop(),
icon: Icon( icon: Icon(
Icons.chevron_left, Icons.chevron_left,
color: _generalStyle.secondaryTextLabelColor, color: _generalStyle.secondaryTextLabelColor,

View File

@@ -20,11 +20,24 @@ class PageHeaderWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 24.0, bottom: 24.0), padding: const EdgeInsets.only(top: 24.0, bottom: 24.0),
child: Column( child: LayoutBuilder(
builder: (context, constraints) {
final isNarrow = constraints.maxWidth < 600;
return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (isNarrow) ...[
Row( Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
_logoutButton(context),
Image.asset(
'assets/images/logo.png',
height: 48,
),
],
),
const SizedBox(height: 16),
Text( Text(
key: Key("PageHeaderTextHeadline"), key: Key("PageHeaderTextHeadline"),
text, text,
@@ -35,7 +48,21 @@ class PageHeaderWidget extends StatelessWidget {
color: _generalStyle.primaryTextLabelColor, color: _generalStyle.primaryTextLabelColor,
), ),
), ),
const Spacer(), ] else ...[
Row(
children: [
Expanded(
child: Text(
key: Key("PageHeaderTextHeadline"),
text,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 50,
fontFamily: _generalStyle.fontFamily,
color: _generalStyle.primaryTextLabelColor,
),
),
),
_logoutButton(context), _logoutButton(context),
Image.asset( Image.asset(
'assets/images/logo.png', 'assets/images/logo.png',
@@ -43,6 +70,7 @@ class PageHeaderWidget extends StatelessWidget {
), ),
], ],
), ),
],
if (subText.isNotEmpty) ...[ if (subText.isNotEmpty) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
@@ -56,6 +84,8 @@ class PageHeaderWidget extends StatelessWidget {
), ),
], ],
], ],
);
},
), ),
); );
} }

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
abstract interface class GeneralStyle { abstract interface class GeneralStyle {
Color? evaluationColor({int value = 0}); Color? evaluationColor({required int value});
Color get evaluationGoodColor; Color get evaluationGoodColor;
Color get evaluationMiddleColor; Color get evaluationMiddleColor;
@@ -20,6 +20,8 @@ abstract interface class GeneralStyle {
Color get secondaryWidgetBackgroundColor; Color get secondaryWidgetBackgroundColor;
Color get pageBackgroundColor; Color get pageBackgroundColor;
Color get deleteCancelButtonBackgroundColor;
Color get deleteCancelTextColor;
Color get errorColor; Color get errorColor;
@@ -28,7 +30,7 @@ abstract interface class GeneralStyle {
class GeneralStyleImpl implements GeneralStyle { class GeneralStyleImpl implements GeneralStyle {
@override @override
Color? evaluationColor({int value = 0}) { Color? evaluationColor({required int value}) {
switch (value) { switch (value) {
case 1: case 1:
return evaluationGoodColor; return evaluationGoodColor;
@@ -70,6 +72,12 @@ class GeneralStyleImpl implements GeneralStyle {
@override @override
Color get pageBackgroundColor => const Color(0xFFF5F5F5); Color get pageBackgroundColor => const Color(0xFFF5F5F5);
@override
Color get deleteCancelButtonBackgroundColor => const Color(0xFFD9D9D9);
@override
Color get deleteCancelTextColor => const Color(0xFF1E1E1E);
@override @override
Color get nextTextColor => const Color(0xFF757575); Color get nextTextColor => const Color(0xFF757575);

View File

@@ -38,7 +38,7 @@ class GlobalRouter {
GoRoute( GoRoute(
path: pathHome, path: pathHome,
builder: (context, state) => CustomerListWidget(), builder: (context, state) => CustomerListWidget(),
), routes: [
GoRoute( GoRoute(
path: "$pathCustomer/:id", path: "$pathCustomer/:id",
builder: (context, state) { builder: (context, state) {
@@ -48,13 +48,19 @@ class GlobalRouter {
}, },
), ),
GoRoute( GoRoute(
path: pathPicture, path: "$pathPicture/:customerId/:pictureId",
builder: (context, state) { builder: (context, state) {
PictureWidgetHolder holder = state.extra as PictureWidgetHolder; var customerIdStr = state.pathParameters['customerId'];
return PictureWidget(customerDto: holder.customerDto, pictureDto: holder.pictureDto); 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);
}, },
), ),
], ],
),
],
redirect: (context, state) { redirect: (context, state) {
var uriStr = state.uri.toString(); var uriStr = state.uri.toString();
logger.t("uri $uriStr"); logger.t("uri $uriStr");

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:fotodocumentation/controller/customer_controller.dart';
import 'package:fotodocumentation/dto/customer_dto.dart' show CustomerDto; import 'package:fotodocumentation/dto/customer_dto.dart' show CustomerDto;
import 'package:fotodocumentation/dto/picture_dto.dart'; import 'package:fotodocumentation/dto/picture_dto.dart';
import 'package:fotodocumentation/pages/customer/picture_widget.dart';
import 'package:fotodocumentation/pages/customer/picture_fullscreen_dialog.dart'; import 'package:fotodocumentation/pages/customer/picture_fullscreen_dialog.dart';
import 'package:fotodocumentation/utils/di_container.dart'; import 'package:fotodocumentation/utils/di_container.dart';
import 'package:fotodocumentation/utils/global_router.dart';
import 'package:fotodocumentation/utils/login_credentials.dart'; import 'package:fotodocumentation/utils/login_credentials.dart';
import 'package:mockito/mockito.dart';
import '../testing/test_utils.dart'; import '../testing/test_utils.dart';
import '../testing/test_utils.mocks.dart';
// Minimal valid base64 encoded 1x1 pixel PNG image // Minimal valid base64 encoded 1x1 pixel PNG image
const String _testImage = const String _testImage =
@@ -22,6 +25,7 @@ void main() {
late PictureDto pictureDto1; late PictureDto pictureDto1;
late PictureDto pictureDto2; late PictureDto pictureDto2;
late PictureDto pictureDto3; late PictureDto pictureDto3;
late MockCustomerController mockCustomerController;
setUp(() { setUp(() {
pictureDto1 = PictureDto( pictureDto1 = PictureDto(
@@ -60,20 +64,19 @@ void main() {
customerNumber: 'CUST001', customerNumber: 'CUST001',
pictures: [pictureDto1, pictureDto2, pictureDto3], pictures: [pictureDto1, pictureDto2, pictureDto3],
); );
mockCustomerController = MockCustomerController();
DiContainer.instance.put(CustomerController, mockCustomerController);
}); });
group('PictureWidget', () { group('PictureWidget', () {
testWidgets('displays customer information correctly', (WidgetTester tester) async { testWidgets('displays customer information correctly', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
await pumpApp( when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
tester,
PictureWidget(customerDto: customerDto, pictureDto: pictureDto1),
);
await tester.pumpAndSettle();
// Verify that the header is displayed await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
expect(find.text('INFORMATIONEN'), findsOneWidget); await tester.pumpAndSettle();
// Verify customer name is displayed // Verify customer name is displayed
expect(find.text('Test Apotheke'), findsOneWidget); expect(find.text('Test Apotheke'), findsOneWidget);
@@ -82,19 +85,16 @@ void main() {
expect(find.text('CUST001'), findsOneWidget); expect(find.text('CUST001'), findsOneWidget);
// Verify labels are displayed // Verify labels are displayed
expect(find.text('APOTHEKE'), findsOneWidget);
expect(find.text('KUNDENNUMMER'), findsOneWidget); expect(find.text('KUNDENNUMMER'), findsOneWidget);
expect(find.text('DATUM'), findsOneWidget);
expect(find.text('KOMMENTAR'), findsOneWidget); expect(find.text('KOMMENTAR'), findsOneWidget);
}); });
testWidgets('displays picture comment', (WidgetTester tester) async { testWidgets('displays picture comment', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
await pumpApp( when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
tester,
PictureWidget(customerDto: customerDto, pictureDto: pictureDto1), await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Verify the comment is displayed // Verify the comment is displayed
@@ -104,10 +104,9 @@ void main() {
testWidgets('displays empty string when comment is null', (WidgetTester tester) async { testWidgets('displays empty string when comment is null', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
await pumpApp( when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
tester,
PictureWidget(customerDto: customerDto, pictureDto: pictureDto3), await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/3");
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// The comment field should be empty but the label should exist // The comment field should be empty but the label should exist
@@ -117,10 +116,9 @@ void main() {
testWidgets('shows both navigation buttons at bottom', (WidgetTester tester) async { testWidgets('shows both navigation buttons at bottom', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
await pumpApp( when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
tester,
PictureWidget(customerDto: customerDto, pictureDto: pictureDto1), await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Both navigation buttons should always be visible at the bottom // Both navigation buttons should always be visible at the bottom
@@ -131,10 +129,9 @@ void main() {
testWidgets('shows both navigation buttons when on middle picture', (WidgetTester tester) async { testWidgets('shows both navigation buttons when on middle picture', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
await pumpApp( when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
tester,
PictureWidget(customerDto: customerDto, pictureDto: pictureDto2), await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/2");
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Both navigation buttons should be visible // Both navigation buttons should be visible
@@ -145,10 +142,9 @@ void main() {
testWidgets('shows both navigation buttons on last picture', (WidgetTester tester) async { testWidgets('shows both navigation buttons on last picture', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
await pumpApp( when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
tester,
PictureWidget(customerDto: customerDto, pictureDto: pictureDto3), await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/3");
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Both navigation buttons should be visible (right one is disabled) // Both navigation buttons should be visible (right one is disabled)
@@ -159,10 +155,9 @@ void main() {
testWidgets('navigates to next picture when right button is tapped', (WidgetTester tester) async { testWidgets('navigates to next picture when right button is tapped', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
await pumpApp( when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
tester,
PictureWidget(customerDto: customerDto, pictureDto: pictureDto1), await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Verify first picture comment is shown // Verify first picture comment is shown
@@ -180,10 +175,9 @@ void main() {
testWidgets('navigates to previous picture when left button is tapped', (WidgetTester tester) async { testWidgets('navigates to previous picture when left button is tapped', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
await pumpApp( when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
tester,
PictureWidget(customerDto: customerDto, pictureDto: pictureDto2), await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/2");
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Verify second picture comment is shown // Verify second picture comment is shown
@@ -208,10 +202,9 @@ void main() {
pictures: [pictureDto1], pictures: [pictureDto1],
); );
await pumpApp( when(mockCustomerController.get(id: 1)).thenAnswer((_) async => singlePictureCustomer);
tester,
PictureWidget(customerDto: singlePictureCustomer, pictureDto: pictureDto1), await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Both navigation buttons should be shown but disabled // Both navigation buttons should be shown but disabled
@@ -222,10 +215,9 @@ void main() {
testWidgets('opens fullscreen dialog when image is tapped', (WidgetTester tester) async { testWidgets('opens fullscreen dialog when image is tapped', (WidgetTester tester) async {
setScreenSize(tester, 2048, 2048); setScreenSize(tester, 2048, 2048);
await pumpApp( when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
tester,
PictureWidget(customerDto: customerDto, pictureDto: pictureDto1), await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Find the GestureDetector with the image key // Find the GestureDetector with the image key