rework ui
This commit is contained in:
@@ -74,6 +74,14 @@ abstract class BaseController {
|
||||
var response = await client.delete(uri, headers: {cred.name: cred.value});
|
||||
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 {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:fotodocumentation/dto/picture_dto.dart';
|
||||
|
||||
abstract interface class PictureController {
|
||||
Future<bool> delete(PictureDto dto);
|
||||
Future<bool> updateEvaluation(PictureDto dto);
|
||||
}
|
||||
|
||||
class PictureControllerImpl extends BaseController implements PictureController {
|
||||
@@ -13,4 +14,10 @@ class PictureControllerImpl extends BaseController implements PictureController
|
||||
String uriStr = '${uriUtils.getBaseUrl()}$path/${dto.id}';
|
||||
return runDeleteWithAuth(uriStr);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> updateEvaluation(PictureDto dto) {
|
||||
String uriStr = '${uriUtils.getBaseUrl()}$path/evaluation/${dto.id}?evaluation=${dto.evaluation}';
|
||||
return runPutWithAuth(uriStr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ class PictureDto {
|
||||
final String? comment;
|
||||
final String? category;
|
||||
final String image;
|
||||
final int evaluation;
|
||||
int evaluation;
|
||||
final DateTime pictureDate;
|
||||
final String? username;
|
||||
|
||||
|
||||
@@ -60,15 +60,15 @@
|
||||
"@deleteDialogTitle": {
|
||||
"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": {
|
||||
"description": "Delete dialog text"
|
||||
},
|
||||
"deleteDialogButtonCancel": "Nein",
|
||||
"deleteDialogButtonCancel": "Abbrechen",
|
||||
"@deleteDialogButtonCancel": {
|
||||
"description": "Cancel Button text"
|
||||
},
|
||||
"deleteDialogButtonApprove": "Ja",
|
||||
"deleteDialogButtonApprove": "Ja, fortfahren",
|
||||
"@deleteDialogButtonApprove": {
|
||||
"description": "Approve Button text"
|
||||
},
|
||||
|
||||
@@ -169,19 +169,19 @@ abstract class AppLocalizations {
|
||||
/// Delete dialog text
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// Cancel Button text
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Nein'**
|
||||
/// **'Abbrechen'**
|
||||
String get deleteDialogButtonCancel;
|
||||
|
||||
/// Approve Button text
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Ja'**
|
||||
/// **'Ja, fortfahren'**
|
||||
String get deleteDialogButtonApprove;
|
||||
|
||||
/// Customer list table header for customer number
|
||||
|
||||
@@ -50,13 +50,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get deleteDialogText =>
|
||||
'Sind Sie sicher, dass Sie diese Eintrag löschen möchten?';
|
||||
'Sicher, dass Sie den Eintrag löschen möchten?';
|
||||
|
||||
@override
|
||||
String get deleteDialogButtonCancel => 'Nein';
|
||||
String get deleteDialogButtonCancel => 'Abbrechen';
|
||||
|
||||
@override
|
||||
String get deleteDialogButtonApprove => 'Ja';
|
||||
String get deleteDialogButtonApprove => 'Ja, fortfahren';
|
||||
|
||||
@override
|
||||
String get customerListHeaderCustomerNumber => 'Kunden-Nr.';
|
||||
|
||||
@@ -224,7 +224,8 @@ class _CustomerListWidgetState extends State<CustomerListWidget> {
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import 'dart:convert' show base64Decode;
|
||||
|
||||
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/l10n/app_localizations.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/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/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/pages/customer/picture_widget.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class CustomerWidget extends StatefulWidget {
|
||||
final int customerId;
|
||||
@@ -98,7 +94,6 @@ class _CustomerWidgetState extends State<CustomerWidget> {
|
||||
children: [
|
||||
PageHeaderWidget(text: dto.name, subText: subText),
|
||||
CustomerBackButton(path: GlobalRouter.pathHome),
|
||||
//_backButton(context),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@@ -209,7 +204,7 @@ class _CustomerWidgetState extends State<CustomerWidget> {
|
||||
);
|
||||
|
||||
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(
|
||||
key: Key("table_row_${customerDto.id}"),
|
||||
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) {
|
||||
return ElevatedButton.icon(
|
||||
key: Key("download_all_button"),
|
||||
@@ -360,7 +328,11 @@ class _CustomerWidgetState extends State<CustomerWidget> {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -11,41 +11,77 @@ class PictureDeleteDialog extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.deleteDialogTitle,
|
||||
style: TextStyle(
|
||||
fontFamily: _generalStyle.fontFamily,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _generalStyle.secondaryWidgetBackgroundColor,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!.deleteDialogText,
|
||||
style: TextStyle(
|
||||
fontFamily: _generalStyle.fontFamily,
|
||||
color: _generalStyle.secondaryTextLabelColor,
|
||||
),
|
||||
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: [
|
||||
TextButton(
|
||||
ElevatedButton(
|
||||
key: Key("picture_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.secondaryTextLabelColor,
|
||||
color: _generalStyle.deleteCancelTextColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.37,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
ElevatedButton(
|
||||
key: Key("picture_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: _generalStyle.errorColor,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.37,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import 'dart:convert' show base64Decode;
|
||||
|
||||
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/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/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:intl/intl.dart';
|
||||
|
||||
class PictureWidgetHolder {
|
||||
final CustomerDto customerDto;
|
||||
final PictureDto pictureDto;
|
||||
|
||||
const PictureWidgetHolder(this.customerDto, this.pictureDto);
|
||||
}
|
||||
|
||||
class PictureWidget extends StatefulWidget {
|
||||
final CustomerDto customerDto;
|
||||
final PictureDto pictureDto;
|
||||
const PictureWidget({super.key, required this.customerDto, required this.pictureDto});
|
||||
final int customerId;
|
||||
final int pictureId;
|
||||
const PictureWidget({super.key, required this.customerId, required this.pictureId});
|
||||
|
||||
@override
|
||||
State<PictureWidget> createState() => _PictureWidgetState();
|
||||
@@ -29,16 +27,21 @@ class PictureWidget extends StatefulWidget {
|
||||
|
||||
class _PictureWidgetState extends State<PictureWidget> {
|
||||
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;
|
||||
final ScrollController _commentScrollController = ScrollController();
|
||||
|
||||
late Future<CustomerDto?> _dto;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_dateFormat = DateFormat('dd.MM.yyyy, hh:mm');
|
||||
_selectedPicture = widget.pictureDto;
|
||||
_dto = _customerController.get(id: widget.customerId);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -54,30 +57,58 @@ class _PictureWidgetState extends State<PictureWidget> {
|
||||
color: _generalStyle.pageBackgroundColor,
|
||||
child: Padding(
|
||||
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) {
|
||||
final pictures = widget.customerDto.pictures;
|
||||
Widget _future(BuildContext context) {
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PageHeaderWidget(text: widget.customerDto.name),
|
||||
PageHeaderWidget(text: _customerDto.name),
|
||||
CustomerBackButton(path: GlobalRouter.pathCustomer),
|
||||
const SizedBox(height: 24),
|
||||
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(
|
||||
builder: (context, constraints) {
|
||||
final isNarrow = constraints.maxWidth < 800;
|
||||
@@ -87,18 +118,18 @@ class _PictureWidgetState extends State<PictureWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_imageWidget(_selectedPicture),
|
||||
_imageWidget(selectedPicture),
|
||||
const SizedBox(height: 32),
|
||||
_contentWidget(_selectedPicture),
|
||||
_contentWidget(selectedPicture),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_imageWidget(_selectedPicture),
|
||||
_imageWidget(selectedPicture),
|
||||
const SizedBox(width: 32),
|
||||
Expanded(child: _contentWidget(_selectedPicture)),
|
||||
Expanded(child: _contentWidget(selectedPicture)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -148,63 +179,150 @@ class _PictureWidgetState extends State<PictureWidget> {
|
||||
color: _generalStyle.secondaryTextLabelColor,
|
||||
);
|
||||
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(
|
||||
_dateFormat.format(dto.pictureDate),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 44,
|
||||
fontFamily: _generalStyle.fontFamily,
|
||||
color: _generalStyle.primaryTextLabelColor,
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20.0),
|
||||
child: Text(
|
||||
"KUNDENNUMMER",
|
||||
style: labelStyle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text(
|
||||
widget.customerDto.customerNumber,
|
||||
style: contentStyle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20.0),
|
||||
child: Text(
|
||||
"KOMMENTAR",
|
||||
style: labelStyle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 150,
|
||||
child: Scrollbar(
|
||||
controller: _commentScrollController,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: _commentScrollController,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
dto.comment ?? "",
|
||||
style: contentStyle,
|
||||
),
|
||||
),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_dateFormat.format(dto.pictureDate),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 44,
|
||||
fontFamily: _generalStyle.fontFamily,
|
||||
color: _generalStyle.primaryTextLabelColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"KUNDENNUMMER",
|
||||
style: labelStyle,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text(
|
||||
_customerDto.customerNumber,
|
||||
style: contentStyle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20.0),
|
||||
child: Text(
|
||||
"KOMMENTAR",
|
||||
style: labelStyle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 150,
|
||||
child: Scrollbar(
|
||||
controller: _commentScrollController,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: _commentScrollController,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
dto.comment ?? "",
|
||||
style: contentStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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
|
||||
Widget _bottomNavigationWidget(List<PictureDto> pictures) {
|
||||
final currentIndex = pictures.indexWhere((p) => p.id == _selectedPicture.id);
|
||||
Widget _bottomNavigationWidget(List<PictureDto> pictures, PictureDto selectedPicture) {
|
||||
final currentIndex = pictures.indexWhere((p) => p.id == selectedPicture.id);
|
||||
final hasPrevious = currentIndex > 0;
|
||||
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) {
|
||||
final pictures = widget.customerDto.pictures;
|
||||
final pictures = _customerDto.pictures;
|
||||
if (index >= 0 && index < pictures.length) {
|
||||
setState(() {
|
||||
_selectedPicture = pictures[index];
|
||||
|
||||
@@ -15,7 +15,7 @@ class CustomerBackButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton.icon(
|
||||
onPressed: () => context.go(path),
|
||||
onPressed: () => context.pop(),
|
||||
icon: Icon(
|
||||
Icons.chevron_left,
|
||||
color: _generalStyle.secondaryTextLabelColor,
|
||||
@@ -20,42 +20,72 @@ class PageHeaderWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0, bottom: 24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isNarrow = constraints.maxWidth < 600;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
key: Key("PageHeaderTextHeadline"),
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 50,
|
||||
fontFamily: _generalStyle.fontFamily,
|
||||
color: _generalStyle.primaryTextLabelColor,
|
||||
if (isNarrow) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
_logoutButton(context),
|
||||
Image.asset(
|
||||
'assets/images/logo.png',
|
||||
height: 48,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_logoutButton(context),
|
||||
Image.asset(
|
||||
'assets/images/logo.png',
|
||||
height: 48,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
key: Key("PageHeaderTextHeadline"),
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 50,
|
||||
fontFamily: _generalStyle.fontFamily,
|
||||
color: _generalStyle.primaryTextLabelColor,
|
||||
),
|
||||
),
|
||||
] 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),
|
||||
Image.asset(
|
||||
'assets/images/logo.png',
|
||||
height: 48,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (subText.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
key: Key("PageHeaderTextSubHeadline"),
|
||||
subText,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: _generalStyle.fontFamily,
|
||||
color: _generalStyle.secondaryTextLabelColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (subText.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
key: Key("PageHeaderTextSubHeadline"),
|
||||
subText,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: _generalStyle.fontFamily,
|
||||
color: _generalStyle.secondaryTextLabelColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract interface class GeneralStyle {
|
||||
Color? evaluationColor({int value = 0});
|
||||
Color? evaluationColor({required int value});
|
||||
|
||||
Color get evaluationGoodColor;
|
||||
Color get evaluationMiddleColor;
|
||||
@@ -20,6 +20,8 @@ abstract interface class GeneralStyle {
|
||||
Color get secondaryWidgetBackgroundColor;
|
||||
|
||||
Color get pageBackgroundColor;
|
||||
Color get deleteCancelButtonBackgroundColor;
|
||||
Color get deleteCancelTextColor;
|
||||
|
||||
Color get errorColor;
|
||||
|
||||
@@ -28,7 +30,7 @@ abstract interface class GeneralStyle {
|
||||
|
||||
class GeneralStyleImpl implements GeneralStyle {
|
||||
@override
|
||||
Color? evaluationColor({int value = 0}) {
|
||||
Color? evaluationColor({required int value}) {
|
||||
switch (value) {
|
||||
case 1:
|
||||
return evaluationGoodColor;
|
||||
@@ -70,6 +72,12 @@ class GeneralStyleImpl implements GeneralStyle {
|
||||
@override
|
||||
Color get pageBackgroundColor => const Color(0xFFF5F5F5);
|
||||
|
||||
@override
|
||||
Color get deleteCancelButtonBackgroundColor => const Color(0xFFD9D9D9);
|
||||
|
||||
@override
|
||||
Color get deleteCancelTextColor => const Color(0xFF1E1E1E);
|
||||
|
||||
@override
|
||||
Color get nextTextColor => const Color(0xFF757575);
|
||||
|
||||
|
||||
@@ -38,21 +38,27 @@ class GlobalRouter {
|
||||
GoRoute(
|
||||
path: pathHome,
|
||||
builder: (context, state) => CustomerListWidget(),
|
||||
),
|
||||
GoRoute(
|
||||
path: "$pathCustomer/:id",
|
||||
builder: (context, state) {
|
||||
var idStr = state.pathParameters['id'];
|
||||
var id = idStr == null ? null : int.tryParse(idStr);
|
||||
return CustomerWidget(customerId: id ?? -1);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: pathPicture,
|
||||
builder: (context, state) {
|
||||
PictureWidgetHolder holder = state.extra as PictureWidgetHolder;
|
||||
return PictureWidget(customerDto: holder.customerDto, pictureDto: holder.pictureDto);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "$pathCustomer/:id",
|
||||
builder: (context, state) {
|
||||
var idStr = state.pathParameters['id'];
|
||||
var id = idStr == null ? null : int.tryParse(idStr);
|
||||
return CustomerWidget(customerId: id ?? -1);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: "$pathPicture/:customerId/: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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
redirect: (context, state) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,13 +1,16 @@
|
||||
import 'package:flutter/material.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/picture_dto.dart';
|
||||
import 'package:fotodocumentation/pages/customer/picture_widget.dart';
|
||||
import 'package:fotodocumentation/pages/customer/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';
|
||||
|
||||
// Minimal valid base64 encoded 1x1 pixel PNG image
|
||||
const String _testImage =
|
||||
@@ -22,6 +25,7 @@ void main() {
|
||||
late PictureDto pictureDto1;
|
||||
late PictureDto pictureDto2;
|
||||
late PictureDto pictureDto3;
|
||||
late MockCustomerController mockCustomerController;
|
||||
|
||||
setUp(() {
|
||||
pictureDto1 = PictureDto(
|
||||
@@ -60,20 +64,19 @@ void main() {
|
||||
customerNumber: 'CUST001',
|
||||
pictures: [pictureDto1, pictureDto2, pictureDto3],
|
||||
);
|
||||
|
||||
mockCustomerController = MockCustomerController();
|
||||
DiContainer.instance.put(CustomerController, mockCustomerController);
|
||||
});
|
||||
|
||||
group('PictureWidget', () {
|
||||
testWidgets('displays customer information correctly', (WidgetTester tester) async {
|
||||
setScreenSize(tester, 1024, 1024);
|
||||
|
||||
await pumpApp(
|
||||
tester,
|
||||
PictureWidget(customerDto: customerDto, pictureDto: pictureDto1),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
|
||||
|
||||
// Verify that the header is displayed
|
||||
expect(find.text('INFORMATIONEN'), findsOneWidget);
|
||||
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify customer name is displayed
|
||||
expect(find.text('Test Apotheke'), findsOneWidget);
|
||||
@@ -82,19 +85,16 @@ void main() {
|
||||
expect(find.text('CUST001'), findsOneWidget);
|
||||
|
||||
// Verify labels are displayed
|
||||
expect(find.text('APOTHEKE'), findsOneWidget);
|
||||
expect(find.text('KUNDENNUMMER'), findsOneWidget);
|
||||
expect(find.text('DATUM'), findsOneWidget);
|
||||
expect(find.text('KOMMENTAR'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays picture comment', (WidgetTester tester) async {
|
||||
setScreenSize(tester, 1024, 1024);
|
||||
|
||||
await pumpApp(
|
||||
tester,
|
||||
PictureWidget(customerDto: customerDto, pictureDto: pictureDto1),
|
||||
);
|
||||
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
|
||||
|
||||
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify the comment is displayed
|
||||
@@ -104,10 +104,9 @@ void main() {
|
||||
testWidgets('displays empty string when comment is null', (WidgetTester tester) async {
|
||||
setScreenSize(tester, 1024, 1024);
|
||||
|
||||
await pumpApp(
|
||||
tester,
|
||||
PictureWidget(customerDto: customerDto, pictureDto: pictureDto3),
|
||||
);
|
||||
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
|
||||
|
||||
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/3");
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 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 {
|
||||
setScreenSize(tester, 1024, 1024);
|
||||
|
||||
await pumpApp(
|
||||
tester,
|
||||
PictureWidget(customerDto: customerDto, pictureDto: pictureDto1),
|
||||
);
|
||||
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
|
||||
|
||||
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 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 {
|
||||
setScreenSize(tester, 1024, 1024);
|
||||
|
||||
await pumpApp(
|
||||
tester,
|
||||
PictureWidget(customerDto: customerDto, pictureDto: pictureDto2),
|
||||
);
|
||||
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
|
||||
|
||||
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/2");
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Both navigation buttons should be visible
|
||||
@@ -145,10 +142,9 @@ void main() {
|
||||
testWidgets('shows both navigation buttons on last picture', (WidgetTester tester) async {
|
||||
setScreenSize(tester, 1024, 1024);
|
||||
|
||||
await pumpApp(
|
||||
tester,
|
||||
PictureWidget(customerDto: customerDto, pictureDto: pictureDto3),
|
||||
);
|
||||
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
|
||||
|
||||
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/3");
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 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 {
|
||||
setScreenSize(tester, 1024, 1024);
|
||||
|
||||
await pumpApp(
|
||||
tester,
|
||||
PictureWidget(customerDto: customerDto, pictureDto: pictureDto1),
|
||||
);
|
||||
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
|
||||
|
||||
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 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 {
|
||||
setScreenSize(tester, 1024, 1024);
|
||||
|
||||
await pumpApp(
|
||||
tester,
|
||||
PictureWidget(customerDto: customerDto, pictureDto: pictureDto2),
|
||||
);
|
||||
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
|
||||
|
||||
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/2");
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify second picture comment is shown
|
||||
@@ -208,10 +202,9 @@ void main() {
|
||||
pictures: [pictureDto1],
|
||||
);
|
||||
|
||||
await pumpApp(
|
||||
tester,
|
||||
PictureWidget(customerDto: singlePictureCustomer, pictureDto: pictureDto1),
|
||||
);
|
||||
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => singlePictureCustomer);
|
||||
|
||||
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 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 {
|
||||
setScreenSize(tester, 2048, 2048);
|
||||
|
||||
await pumpApp(
|
||||
tester,
|
||||
PictureWidget(customerDto: customerDto, pictureDto: pictureDto1),
|
||||
);
|
||||
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
|
||||
|
||||
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1");
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Find the GestureDetector with the image key
|
||||
|
||||
Reference in New Issue
Block a user