rework ui
This commit is contained in:
@@ -40,6 +40,12 @@ public class Customer extends AbstractDateEntity {
|
||||
|
||||
@Column(name = "name", nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "city")
|
||||
private String city;
|
||||
|
||||
@Column(name = "zip")
|
||||
private String zip;
|
||||
|
||||
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private Set<Picture> pictures = new HashSet<>();
|
||||
@@ -68,6 +74,22 @@ public class Customer extends AbstractDateEntity {
|
||||
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() {
|
||||
return pictures;
|
||||
}
|
||||
@@ -101,6 +123,16 @@ public class Customer extends AbstractDateEntity {
|
||||
instance.setName(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder city(String city) {
|
||||
instance.setCity(city);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder zip(String zip) {
|
||||
instance.setZip(zip);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Customer build() {
|
||||
return instance;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
-- customer
|
||||
|
||||
alter table customer add column zip varchar(150);
|
||||
|
||||
alter table customer add column city varchar(150);
|
||||
|
||||
@@ -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))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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")];
|
||||
|
||||
@@ -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].
|
||||
|
||||
Reference in New Issue
Block a user