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

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

View File

@@ -124,5 +124,25 @@
"backButtonLabel": "zurück",
"@backButtonLabel": {
"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:
/// **'zurück'**
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

View File

@@ -96,4 +96,19 @@ class AppLocalizationsDe extends AppLocalizations {
@override
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 evaluationColor = _generalStyle.evaluationColor(value: pictureDto.evaluation); // FIXME: set to color from DB
final evaluationColor = _generalStyle.evaluationColor(value: pictureDto.evaluation);
return InkWell(
key: Key("table_row_${customerDto.id}"),
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/dto/customer_dto.dart';
import 'package:fotodocumentation/dto/picture_dto.dart';
import 'package:fotodocumentation/l10n/app_localizations.dart';
import 'package:fotodocumentation/pages/ui_utils/component/customer_back_button.dart';
import 'package:fotodocumentation/pages/customer/picture_fullscreen_dialog.dart';
import 'package:fotodocumentation/pages/ui_utils/component/general_error_widget.dart';
@@ -200,7 +201,7 @@ class _PictureWidgetState extends State<PictureWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"KUNDENNUMMER",
AppLocalizations.of(context)!.pictureWidgetLabelCustomerNumber,
style: labelStyle,
),
Padding(
@@ -210,10 +211,32 @@ class _PictureWidgetState extends State<PictureWidget> {
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: const EdgeInsets.only(top: 20.0),
child: Text(
"KOMMENTAR",
AppLocalizations.of(context)!.pictureWidgetLabelComment,
style: labelStyle,
),
),
@@ -258,7 +281,7 @@ class _PictureWidgetState extends State<PictureWidget> {
mainAxisSize: MainAxisSize.min,
children: [
Text(
"BEWERTUNG",
AppLocalizations.of(context)!.pictureWidgetLabelEvaluation,
style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 16,
@@ -333,6 +356,7 @@ class _PictureWidgetState extends State<PictureWidget> {
children: [
// Previous button
IconButton(
key: Key("navigate_left"),
onPressed: hasPrevious ? () => _actionNavigateToPicture(currentIndex - 1) : null,
icon: Icon(Icons.chevron_left, color: _generalStyle.nextTextColor, size: 32),
),
@@ -372,6 +396,7 @@ class _PictureWidgetState extends State<PictureWidget> {
const SizedBox(width: 24),
// Next button
IconButton(
key: Key("navigate_right"),
onPressed: hasNext ? () => _actionNavigateToPicture(currentIndex + 1) : null,
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 {
_selectedPicture?.evaluation = value;
_pictureController.updateEvaluation(_selectedPicture!);
setState(() {
});
setState(() {});
}
void _actionNavigateToPicture(int index) {

View File

@@ -90,6 +90,7 @@ class _LoginWidgetState extends State<LoginWidget> {
children: [
Center(
child: Text(
key: Key("login_title"),
AppLocalizations.of(context)!.loginTitle,
style: TextStyle(
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();
// 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 {
@@ -162,7 +162,7 @@ void main() {
when(mockLoginController.authenticate('testuser', 'wrongpassword'))
.thenAnswer((_) async => (jwtTokenPairDto: null));
await pumpAppConfig(tester, GlobalRouter.pathLogin);
await pumpApp(tester, const LoginWidget());
await tester.pumpAndSettle();
// Enter credentials

View File

@@ -1,7 +1,7 @@
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/customer_dto.dart' show CustomerDto, CustomerListDto;
import 'package:fotodocumentation/dto/picture_dto.dart';
import 'package:fotodocumentation/pages/customer/picture_fullscreen_dialog.dart';
import 'package:fotodocumentation/utils/di_container.dart';
@@ -13,8 +13,7 @@ import '../testing/test_utils.dart';
import '../testing/test_utils.mocks.dart';
// Minimal valid base64 encoded 1x1 pixel PNG image
const String _testImage =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
const String _testImage = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@@ -74,8 +73,9 @@ void main() {
setScreenSize(tester, 1024, 1024);
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();
// Verify customer name is displayed
@@ -93,8 +93,9 @@ void main() {
setScreenSize(tester, 1024, 1024);
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();
// Verify the comment is displayed
@@ -105,8 +106,9 @@ void main() {
setScreenSize(tester, 1024, 1024);
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();
// The comment field should be empty but the label should exist
@@ -117,47 +119,51 @@ void main() {
setScreenSize(tester, 1024, 1024);
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();
// Both navigation buttons should always be visible at the bottom
expect(find.byIcon(Icons.chevron_left), findsOneWidget);
expect(find.byIcon(Icons.chevron_right), findsOneWidget);
expect(find.byKey(Key("navigate_left")), findsOneWidget);
expect(find.byKey(Key("navigate_right")), findsOneWidget);
});
testWidgets('shows both navigation buttons when on middle picture', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024);
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();
// Both navigation buttons should be visible
expect(find.byIcon(Icons.chevron_left), findsOneWidget);
expect(find.byIcon(Icons.chevron_right), findsOneWidget);
expect(find.byKey(Key("navigate_left")), findsOneWidget);
expect(find.byKey(Key("navigate_right")), findsOneWidget);
});
testWidgets('shows both navigation buttons on last picture', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024);
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();
// Both navigation buttons should be visible (right one is disabled)
expect(find.byIcon(Icons.chevron_left), findsOneWidget);
expect(find.byIcon(Icons.chevron_right), findsOneWidget);
expect(find.byKey(Key("navigate_left")), findsOneWidget);
expect(find.byKey(Key("navigate_right")), findsOneWidget);
});
testWidgets('navigates to next picture when right button is tapped', (WidgetTester tester) async {
setScreenSize(tester, 1024, 1024);
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();
// Verify first picture comment is shown
@@ -176,15 +182,16 @@ void main() {
setScreenSize(tester, 1024, 1024);
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();
// Verify second picture comment is shown
expect(find.text('Second picture comment'), findsOneWidget);
// Tap left navigation button
await tester.tap(find.byIcon(Icons.chevron_left));
await tester.tap(find.byKey(Key("navigate_left")));
await tester.pumpAndSettle();
// Verify first picture comment is now shown
@@ -203,21 +210,23 @@ void main() {
);
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();
// Both navigation buttons should be shown but disabled
expect(find.byIcon(Icons.chevron_left), findsOneWidget);
expect(find.byIcon(Icons.chevron_right), findsOneWidget);
expect(find.byKey(Key("navigate_left")), findsOneWidget);
expect(find.byKey(Key("navigate_right")), findsOneWidget);
});
testWidgets('opens fullscreen dialog when image is tapped', (WidgetTester tester) async {
setScreenSize(tester, 2048, 2048);
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();
// 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),
) 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].