rework ui

This commit is contained in:
verboomp
2026-01-29 14:50:06 +01:00
parent e062b4c688
commit 2587a9c3c8
13 changed files with 197 additions and 41 deletions

View File

@@ -41,6 +41,12 @@ public class Customer extends AbstractDateEntity {
@Column(name = "name", nullable = false) @Column(name = "name", nullable = false)
private String name; private String name;
@Column(name = "city")
private String city;
@Column(name = "zip")
private String zip;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Picture> pictures = new HashSet<>(); private Set<Picture> pictures = new HashSet<>();
@@ -68,6 +74,22 @@ public class Customer extends AbstractDateEntity {
this.name = 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<Picture> getPictures() { public Set<Picture> getPictures() {
return pictures; return pictures;
} }
@@ -102,6 +124,16 @@ public class Customer extends AbstractDateEntity {
return this; return this;
} }
public Builder city(String city) {
instance.setCity(city);
return this;
}
public Builder zip(String zip) {
instance.setZip(zip);
return this;
}
public Customer build() { public Customer build() {
return instance; return instance;
} }

View File

@@ -0,0 +1,7 @@
-- customer
alter table customer add column zip varchar(150);
alter table customer add column city varchar(150);

View File

@@ -24,9 +24,11 @@ class CustomerDto {
final int id; final int id;
final String name; final String name;
final String customerNumber; final String customerNumber;
final String? zip;
final String? city;
final List<PictureDto> pictures; final List<PictureDto> pictures;
CustomerDto({required this.id, required this.name, required this.customerNumber, required this.pictures}); CustomerDto({required this.id, required this.name, required this.customerNumber, required this.pictures, this.zip, this.city});
/// Create from JSON response /// Create from JSON response
factory CustomerDto.fromJson(Map<String, dynamic> json) { factory CustomerDto.fromJson(Map<String, dynamic> json) {
@@ -34,9 +36,9 @@ class CustomerDto {
id: json['id'] as int, id: json['id'] as int,
name: json['name'] as String, name: json['name'] as String,
customerNumber: json['customerNumber'] as String, customerNumber: json['customerNumber'] as String,
zip: json['zip'] as String?,
city: json['city'] as String?,
pictures: List<PictureDto>.from(json["pictures"].map((x) => PictureDto.fromJson(x))), pictures: List<PictureDto>.from(json["pictures"].map((x) => PictureDto.fromJson(x))),
); );
} }
} }

View File

@@ -124,5 +124,25 @@
"backButtonLabel": "zurück", "backButtonLabel": "zurück",
"@backButtonLabel": { "@backButtonLabel": {
"description": "Back button label" "description": "Back button label"
},
"pictureWidgetLabelCustomerNumber": "KUNDENNUMMER",
"@pictureWidgetLabelCustomerNumber": {
"description": "Picture widget label for customer number"
},
"pictureWidgetLabelZip": "PLZ",
"@pictureWidgetLabelZip": {
"description": "Picture widget label for zip code"
},
"pictureWidgetLabelCity": "ORT",
"@pictureWidgetLabelCity": {
"description": "Picture widget label for city"
},
"pictureWidgetLabelComment": "KOMMENTAR",
"@pictureWidgetLabelComment": {
"description": "Picture widget label for comment"
},
"pictureWidgetLabelEvaluation": "BEWERTUNG",
"@pictureWidgetLabelEvaluation": {
"description": "Picture widget label for evaluation"
} }
} }

View File

@@ -255,6 +255,36 @@ abstract class AppLocalizations {
/// In de, this message translates to: /// In de, this message translates to:
/// **'zurück'** /// **'zurück'**
String get backButtonLabel; String get backButtonLabel;
/// Picture widget label for customer number
///
/// In de, this message translates to:
/// **'KUNDENNUMMER'**
String get pictureWidgetLabelCustomerNumber;
/// Picture widget label for zip code
///
/// In de, this message translates to:
/// **'PLZ'**
String get pictureWidgetLabelZip;
/// Picture widget label for city
///
/// In de, this message translates to:
/// **'ORT'**
String get pictureWidgetLabelCity;
/// Picture widget label for comment
///
/// In de, this message translates to:
/// **'KOMMENTAR'**
String get pictureWidgetLabelComment;
/// Picture widget label for evaluation
///
/// In de, this message translates to:
/// **'BEWERTUNG'**
String get pictureWidgetLabelEvaluation;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View File

@@ -96,4 +96,19 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get backButtonLabel => 'zurück'; String get backButtonLabel => 'zurück';
@override
String get pictureWidgetLabelCustomerNumber => 'KUNDENNUMMER';
@override
String get pictureWidgetLabelZip => 'PLZ';
@override
String get pictureWidgetLabelCity => 'ORT';
@override
String get pictureWidgetLabelComment => 'KOMMENTAR';
@override
String get pictureWidgetLabelEvaluation => 'BEWERTUNG';
} }

View File

@@ -204,7 +204,7 @@ class _CustomerWidgetState extends State<CustomerWidget> {
); );
final dateStr = _dateFormat.format(pictureDto.pictureDate); final dateStr = _dateFormat.format(pictureDto.pictureDate);
final evaluationColor = _generalStyle.evaluationColor(value: pictureDto.evaluation); // FIXME: set to color from DB final evaluationColor = _generalStyle.evaluationColor(value: pictureDto.evaluation);
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),

View File

@@ -6,6 +6,7 @@ import 'package:fotodocumentation/controller/customer_controller.dart';
import 'package:fotodocumentation/controller/picture_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/l10n/app_localizations.dart';
import 'package:fotodocumentation/pages/ui_utils/component/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/general_error_widget.dart';
@@ -200,7 +201,7 @@ class _PictureWidgetState extends State<PictureWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"KUNDENNUMMER", AppLocalizations.of(context)!.pictureWidgetLabelCustomerNumber,
style: labelStyle, style: labelStyle,
), ),
Padding( Padding(
@@ -210,10 +211,32 @@ class _PictureWidgetState extends State<PictureWidget> {
style: contentStyle, style: contentStyle,
), ),
), ),
Text(
AppLocalizations.of(context)!.pictureWidgetLabelZip,
style: labelStyle,
),
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
_customerDto.zip ?? "",
style: contentStyle,
),
),
Text(
AppLocalizations.of(context)!.pictureWidgetLabelCity,
style: labelStyle,
),
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
_customerDto.city ?? "",
style: contentStyle,
),
),
Padding( Padding(
padding: const EdgeInsets.only(top: 20.0), padding: const EdgeInsets.only(top: 20.0),
child: Text( child: Text(
"KOMMENTAR", AppLocalizations.of(context)!.pictureWidgetLabelComment,
style: labelStyle, style: labelStyle,
), ),
), ),
@@ -258,7 +281,7 @@ class _PictureWidgetState extends State<PictureWidget> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
"BEWERTUNG", AppLocalizations.of(context)!.pictureWidgetLabelEvaluation,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
fontSize: 16, fontSize: 16,
@@ -333,6 +356,7 @@ class _PictureWidgetState extends State<PictureWidget> {
children: [ children: [
// Previous button // Previous button
IconButton( IconButton(
key: Key("navigate_left"),
onPressed: hasPrevious ? () => _actionNavigateToPicture(currentIndex - 1) : null, onPressed: hasPrevious ? () => _actionNavigateToPicture(currentIndex - 1) : null,
icon: Icon(Icons.chevron_left, color: _generalStyle.nextTextColor, size: 32), icon: Icon(Icons.chevron_left, color: _generalStyle.nextTextColor, size: 32),
), ),
@@ -372,6 +396,7 @@ class _PictureWidgetState extends State<PictureWidget> {
const SizedBox(width: 24), const SizedBox(width: 24),
// Next button // Next button
IconButton( IconButton(
key: Key("navigate_right"),
onPressed: hasNext ? () => _actionNavigateToPicture(currentIndex + 1) : null, onPressed: hasNext ? () => _actionNavigateToPicture(currentIndex + 1) : null,
icon: Icon(Icons.chevron_right, color: _generalStyle.nextTextColor, size: 32), icon: Icon(Icons.chevron_right, color: _generalStyle.nextTextColor, size: 32),
), ),
@@ -383,9 +408,7 @@ class _PictureWidgetState extends State<PictureWidget> {
Future<void> _actionUpdateEvaluation(int value) async { Future<void> _actionUpdateEvaluation(int value) async {
_selectedPicture?.evaluation = value; _selectedPicture?.evaluation = value;
_pictureController.updateEvaluation(_selectedPicture!); _pictureController.updateEvaluation(_selectedPicture!);
setState(() { setState(() {});
});
} }
void _actionNavigateToPicture(int index) { void _actionNavigateToPicture(int index) {

View File

@@ -90,6 +90,7 @@ class _LoginWidgetState extends State<LoginWidget> {
children: [ children: [
Center( Center(
child: Text( child: Text(
key: Key("login_title"),
AppLocalizations.of(context)!.loginTitle, AppLocalizations.of(context)!.loginTitle,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

File diff suppressed because one or more lines are too long

View File

@@ -36,7 +36,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Verify the login title is displayed (German localization) // Verify the login title is displayed (German localization)
expect(find.text('BILDERUPLOAD'), findsOneWidget); expect(find.byKey(const Key('login_title')), findsOneWidget);
}); });
testWidgets('displays username and password fields', (WidgetTester tester) async { testWidgets('displays username and password fields', (WidgetTester tester) async {
@@ -162,7 +162,7 @@ void main() {
when(mockLoginController.authenticate('testuser', 'wrongpassword')) when(mockLoginController.authenticate('testuser', 'wrongpassword'))
.thenAnswer((_) async => (jwtTokenPairDto: null)); .thenAnswer((_) async => (jwtTokenPairDto: null));
await pumpAppConfig(tester, GlobalRouter.pathLogin); await pumpApp(tester, const LoginWidget());
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Enter credentials // Enter credentials

View File

@@ -1,7 +1,7 @@
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/controller/customer_controller.dart';
import 'package:fotodocumentation/dto/customer_dto.dart' show CustomerDto; import 'package:fotodocumentation/dto/customer_dto.dart' show CustomerDto, CustomerListDto;
import 'package:fotodocumentation/dto/picture_dto.dart'; import 'package:fotodocumentation/dto/picture_dto.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';
@@ -13,8 +13,7 @@ import '../testing/test_utils.dart';
import '../testing/test_utils.mocks.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 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@@ -74,8 +73,9 @@ void main() {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto); when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1"); await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathPicture}/1/1");
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Verify customer name is displayed // Verify customer name is displayed
@@ -93,8 +93,9 @@ void main() {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto); when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1"); await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathPicture}/1/1");
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Verify the comment is displayed // Verify the comment is displayed
@@ -105,8 +106,9 @@ void main() {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto); when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/3"); await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathPicture}/1/1");
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,47 +119,51 @@ void main() {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto); when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1"); await pumpAppConfig(tester, "${GlobalRouter.pathHome}${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
expect(find.byIcon(Icons.chevron_left), findsOneWidget); expect(find.byKey(Key("navigate_left")), findsOneWidget);
expect(find.byIcon(Icons.chevron_right), findsOneWidget); expect(find.byKey(Key("navigate_right")), findsOneWidget);
}); });
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);
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto); when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/2"); await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathPicture}/1/1");
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Both navigation buttons should be visible // Both navigation buttons should be visible
expect(find.byIcon(Icons.chevron_left), findsOneWidget); expect(find.byKey(Key("navigate_left")), findsOneWidget);
expect(find.byIcon(Icons.chevron_right), findsOneWidget); expect(find.byKey(Key("navigate_right")), findsOneWidget);
}); });
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);
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto); when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/3"); await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathPicture}/1/1");
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)
expect(find.byIcon(Icons.chevron_left), findsOneWidget); expect(find.byKey(Key("navigate_left")), findsOneWidget);
expect(find.byIcon(Icons.chevron_right), findsOneWidget); expect(find.byKey(Key("navigate_right")), findsOneWidget);
}); });
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);
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto); when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1"); await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathPicture}/1/1");
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Verify first picture comment is shown // Verify first picture comment is shown
@@ -176,15 +182,16 @@ void main() {
setScreenSize(tester, 1024, 1024); setScreenSize(tester, 1024, 1024);
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto); when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/2"); await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathPicture}/1/2");
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Verify second picture comment is shown // Verify second picture comment is shown
expect(find.text('Second picture comment'), findsOneWidget); expect(find.text('Second picture comment'), findsOneWidget);
// Tap left navigation button // Tap left navigation button
await tester.tap(find.byIcon(Icons.chevron_left)); await tester.tap(find.byKey(Key("navigate_left")));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Verify first picture comment is now shown // Verify first picture comment is now shown
@@ -203,21 +210,23 @@ void main() {
); );
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => singlePictureCustomer); when(mockCustomerController.get(id: 1)).thenAnswer((_) async => singlePictureCustomer);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1"); await pumpAppConfig(tester, "${GlobalRouter.pathHome}${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
expect(find.byIcon(Icons.chevron_left), findsOneWidget); expect(find.byKey(Key("navigate_left")), findsOneWidget);
expect(find.byIcon(Icons.chevron_right), findsOneWidget); expect(find.byKey(Key("navigate_right")), findsOneWidget);
}); });
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);
when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto); when(mockCustomerController.get(id: 1)).thenAnswer((_) async => customerDto);
when(mockCustomerController.getAll("", "")).thenAnswer((_) async => _list);
await pumpAppConfig(tester, "${GlobalRouter.pathPicture}/1/1"); await pumpAppConfig(tester, "${GlobalRouter.pathHome}${GlobalRouter.pathPicture}/1/1");
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Find the GestureDetector with the image key // Find the GestureDetector with the image key
@@ -238,3 +247,5 @@ void main() {
}); });
}); });
} }
List<CustomerListDto> _list = [CustomerListDto(id: 1, customerNumber: "CODE1", name: "Customer 1"), CustomerListDto(id: 2, customerNumber: "CODE2", name: "Customer 2")];

View File

@@ -237,6 +237,16 @@ class MockPictureController extends _i1.Mock implements _i11.PictureController {
), ),
returnValue: _i7.Future<bool>.value(false), returnValue: _i7.Future<bool>.value(false),
) as _i7.Future<bool>); ) as _i7.Future<bool>);
@override
_i7.Future<bool> updateEvaluation(_i12.PictureDto? dto) =>
(super.noSuchMethod(
Invocation.method(
#updateEvaluation,
[dto],
),
returnValue: _i7.Future<bool>.value(false),
) as _i7.Future<bool>);
} }
/// A class which mocks [JwtTokenStorage]. /// A class which mocks [JwtTokenStorage].