Skip to content

Commit

Permalink
Merge pull request #11 from styrix560/4-handle-duplicate-seats
Browse files Browse the repository at this point in the history
4 handle duplicate seats
  • Loading branch information
styrix560 authored Jun 2, 2024
2 parents efd22ee + b386670 commit 206cfbc
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 54 deletions.
3 changes: 1 addition & 2 deletions integration_test/app_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import "package:bbs4/api/mock_api.dart";
import "package:bbs4/main.dart";
import "package:bbs4/types/booking.dart";
import "package:bbs4/types/booking_time.dart";
import "package:bbs4/types/global_data.dart";
import "package:bbs4/types/price_type.dart";
import "package:bbs4/types/seat.dart";
Expand Down Expand Up @@ -50,7 +49,7 @@ Future<void> main() async {
home: const MainApp(),
));

final globalData = GlobalData(BookingTime.afternoon);
final globalData = GlobalData();

await clickSeat(tester, "R1 P1");

Expand Down
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "widgets/bookings_view.dart";
import "widgets/overview.dart";

final scaffoldKey = GlobalKey<ScaffoldMessengerState>();
final navigatorKey = GlobalKey<NavigatorState>();

Future<bool> appInitialization() async {
await initSupernova(
Expand All @@ -29,6 +30,7 @@ Future<void> main() async {

runApp(MaterialApp(
scaffoldMessengerKey: scaffoldKey,
navigatorKey: navigatorKey,
home: configFound ? const MainApp() : const ConfigMissingApp(),
));
}
Expand Down
52 changes: 26 additions & 26 deletions lib/types/global_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,31 @@ import "price_type.dart";
import "seat.dart";

class GlobalData {
factory GlobalData([BookingTime? bookingTime]) =>
GlobalData.fromTime(bookingTime ?? currentBookingTime.value);
factory GlobalData() {
if (!globalDataInstances.containsKey(currentBookingTime.value)) {
globalDataInstances[currentBookingTime.value] = GlobalData._internal(
ValueNotifier([]),
ValueNotifier(null),
);
}
return globalDataInstances[currentBookingTime.value]!;
}

GlobalData._internal(
this._bookings,
this._activeBooking,
this.bookingTime,
) {
loadBookings();
}

factory GlobalData.fromTime(BookingTime bookingTime) {
if (!globalDataInstances.containsKey(bookingTime)) {
globalDataInstances[bookingTime] = GlobalData._internal(
ValueNotifier([]),
ValueNotifier(null),
bookingTime,
);
}
return globalDataInstances[bookingTime]!;
}

static final Map<BookingTime, GlobalData> globalDataInstances = {};
static late final Api api;

Future<void> pushBookings() async {
isTransactionInProgress.value = true;
try {
await api.writeBookings(bookingTime.germanName, bookings.value);
await api.writeBookings(
currentBookingTime.value.germanName, bookings.value);
} on Exception catch (error, stacktrace) {
logger.error("error pushing changes to db", error, stacktrace);
snackbar("Fehler beim Schreiben der Buchungen");
Expand All @@ -50,26 +46,26 @@ class GlobalData {
isTransactionInProgress.value = true;
late final List<Booking> newBookings;
try {
newBookings = await api.getBookings(bookingTime.germanName);
newBookings = await api.getBookings(currentBookingTime.value.germanName);
} on Exception catch (error, stacktrace) {
logger.error("error loading bookings", error, stacktrace);
snackbar(
"Fehler beim Laden der Buchungen. Ist Internet aktiviert und "
"geht die Uhr richtig?",
);
}
bookings.value = mergeBookings(bookings.value, newBookings);
bookings.value = mergeBookings(
bookings.value,
newBookings,
preferExternal: true,
);
snackbar("Buchungen erfolgreich geladen");
isTransactionInProgress.value = false;
}

static final ValueNotifier<BookingTime> currentBookingTime =
ValueNotifier(BookingTime.afternoon);

set bookingTime(BookingTime newBookingTime) =>
currentBookingTime.value = newBookingTime;

final BookingTime bookingTime;
final ValueNotifier<List<Booking>> _bookings;
final ValueNotifier<Booking?> _activeBooking;

Expand Down Expand Up @@ -141,18 +137,22 @@ class GlobalData {
Booking(uuid.v4(), "", "", "", {firstSeat}, 0, PriceType.normal, "");
}

// Local changes take absolute precedence. Only if the external bookings
// contain a booking, that we do not know at all, we add it to the list
/// Merges two Lists of [Booking]s together. [preferExternal] signifies,
/// whether changes from the external source (i.e. the database) should
/// override local changes. This is effectively the case, when pulling.
/// Otherwise the local changes are more important.
static List<Booking> mergeBookings(
List<Booking> internal,
List<Booking> external,
) {
List<Booking> external, {
required bool preferExternal,
}) {
final keysToBookings = <String, Booking>{
for (final booking in internal) booking.id: booking,
};
keysToBookings.addAll({
for (final booking in external)
if (!keysToBookings.containsKey(booking.id)) booking.id: booking,
if (preferExternal || !keysToBookings.containsKey(booking.id))
booking.id: booking,
});
return keysToBookings.values.toList();
}
Expand Down
21 changes: 17 additions & 4 deletions lib/widgets/bookings_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class BookingsView extends HookWidget {
for (final (y, width) in rowWidths.indexed) {
for (var x = 0; x < width; x++) {
final seat = Seat(y, x);
seatCells[seat] = SeatCellWidget(x, y, bookingTime);
seatCells[seat] = SeatCellWidget(x, y);
}
}

Expand All @@ -31,7 +31,7 @@ class BookingsView extends HookWidget {
@override
Widget build(BuildContext context) {
final globalData = GlobalData();
final bookingTime = globalData.bookingTime;
final bookingTime = GlobalData.currentBookingTime.value;
final seatCells = useMemoized(() => initSeatCells(bookingTime), [
bookingTime,
]);
Expand All @@ -47,7 +47,6 @@ class BookingsView extends HookWidget {
final transactionsDisabled =
globalData.isBookingActive || globalData.isTransactionInProgress.value;

// TODO(styrix): get feedback for the EditBookingsWidget
return Column(
children: [
Row(
Expand All @@ -58,11 +57,25 @@ class BookingsView extends HookWidget {
(p0) => p0.germanName,
(newBookingTime) {
if (newBookingTime == null) return;
globalData.bookingTime = newBookingTime;
GlobalData.currentBookingTime.value = newBookingTime;
},
disabled: transactionsDisabled,
),
const Spacer(),
// FilledButton(
// onPressed: () async {
// final Booking? booking = await showDialog(
// context: context,
// builder: (context) => SeatDisambiguationWidget([
// Booking("id1", "Max", "Mustermann", "Musterklasse", {}, 0,
// PriceType.reduced, ""),
// Booking("id2", "Klaus", "Kinski", "sehr sauer", {}, 0,
// PriceType.reduced, ""),
// ]),
// );
// },
// child: const Text("test"),
// ),
FilledButton(
onPressed: transactionsDisabled ? null : globalData.loadBookings,
child: const Text("Buchungen zurücksetzen"),
Expand Down
7 changes: 4 additions & 3 deletions lib/widgets/edit_booking.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ class EditBookingWidget extends HookWidget {

globalData.activeBooking.addListener(listenable);
return () => globalData.activeBooking.removeListener(listenable);
});
}, [GlobalData.currentBookingTime.value]);
useListenable(GlobalData.currentBookingTime);

Form buildForm() {
final globalData = GlobalData();
Expand Down Expand Up @@ -115,7 +116,7 @@ class EditBookingWidget extends HookWidget {
const VerticalDivider(
width: 32,
),
PaidPriceWidget(bookingTime.value),
const PaidPriceWidget(),
const VerticalDivider(
width: 32,
),
Expand Down Expand Up @@ -164,7 +165,7 @@ class EditBookingWidget extends HookWidget {
);
}

if (!GlobalData().isBookingActive) {
if (!globalData.isBookingActive) {
return const SizedBox();
}
return Column(
Expand Down
4 changes: 2 additions & 2 deletions lib/widgets/overview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ class OverviewWidget extends HookWidget {
Row(
children: [
CustomMenuButton(
globalData.bookingTime,
GlobalData.currentBookingTime.value,
BookingTime.values,
(bookingTime) => bookingTime.germanName,
(newBookingTime) {
if (newBookingTime == null) return;
globalData.bookingTime = newBookingTime;
GlobalData.currentBookingTime.value = newBookingTime;
},
disabled: globalData.isTransactionInProgress.value,
),
Expand Down
11 changes: 4 additions & 7 deletions lib/widgets/price_paid.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@ import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:supernova/supernova.dart";

import "../types/booking_time.dart";
import "../types/global_data.dart";
import "../types/price_type.dart";

class PaidPriceWidget extends HookWidget {
const PaidPriceWidget(this.bookingTime, {super.key});
const PaidPriceWidget({super.key});

final BookingTime bookingTime;

int get pricePerSeat => GlobalData.fromTime(bookingTime)
int get pricePerSeat => GlobalData()
.activeBooking
.value!
.priceType
.calculatePrice(bookingTime);
.calculatePrice(GlobalData.currentBookingTime.value);

@override
Widget build(BuildContext context) {
final globalData = GlobalData.fromTime(bookingTime);
final globalData = GlobalData();
final activeBooking = globalData.activeBooking.value;
final pricePaid = useState(activeBooking?.pricePaid ?? 0);
final controller =
Expand Down
26 changes: 16 additions & 10 deletions lib/widgets/seat_cell.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,25 @@ import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";

import "../types/booking.dart";
import "../types/booking_time.dart";
import "../types/global_data.dart";
import "../types/price_type.dart";
import "../types/seat.dart";
import "seat_disambiguation.dart";

class SeatCellWidget extends HookWidget {
const SeatCellWidget(
this.x,
this.y,
this.bookingTime, {
this.y, {
super.key,
});

final int x;
final int y;
final BookingTime bookingTime;

@override
Widget build(BuildContext context) {
final seat = Seat(y, x);
final globalData = GlobalData.fromTime(bookingTime);
final globalData = GlobalData();
final activeBooking = globalData.activeBooking.value;
// final rebuild = useRebuild();
// final previousBooking = useRef<Booking?>(null);
Expand All @@ -48,7 +46,8 @@ class SeatCellWidget extends HookWidget {
Color getColor() {
Color getColorForBooking(
Booking booking, Color colorIfPaid, Color colorIfNotPaid) {
final pricePerSeat = booking.priceType.calculatePrice(bookingTime);
final pricePerSeat = booking.priceType
.calculatePrice(GlobalData.currentBookingTime.value);
final amountOfPaidSeats =
pricePerSeat == 0 ? 1000 : booking.pricePaid ~/ pricePerSeat;

Expand Down Expand Up @@ -91,7 +90,7 @@ class SeatCellWidget extends HookWidget {
flex: 2,
child: GestureDetector(
onTapDown: (_) {
onClick(seat);
onClick(context, seat);
},
child: DecoratedBox(
decoration: BoxDecoration(
Expand All @@ -108,11 +107,18 @@ class SeatCellWidget extends HookWidget {
);
}

void onClick(Seat seat) {
final globalData = GlobalData.fromTime(bookingTime);
Future<void> onClick(BuildContext context, Seat seat) async {
final globalData = GlobalData();
final activeBooking = globalData.activeBooking.value;
final clickedBookings = globalData.getBookingsContainingSeat(seat);
assert(clickedBookings.length < 2);
if (clickedBookings.length > 1) {
final selectedBooking = await showDialog<Booking>(
context: context,
builder: (context) => SeatDisambiguationWidget(clickedBookings),
);
globalData.changeActiveBooking(selectedBooking);
return;
}

if (clickedBookings.length == 1) {
final newBooking = clickedBookings.first;
Expand Down
40 changes: 40 additions & 0 deletions lib/widgets/seat_disambiguation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";

import "../main.dart";
import "../types/booking.dart";

// When the bookings are merged from the database it is possible, that some
// seats are booked more than once. This dialog allows the user to choose, which
// one of the multiple Bookings attached to a seat they want to edit
class SeatDisambiguationWidget extends HookWidget {
final List<Booking> bookings;

const SeatDisambiguationWidget(this.bookings);

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("Zu diesem Sitz gehören mehr als eine Buchung."),
content: SingleChildScrollView(
child: Column(
children: [
Text("Bitte die zu bearbeitende Buchung auswählen:"),
for (final booking in bookings)
Row(
children: [
Text("${booking.lastName}, ${booking.firstName}"),
Spacer(),
IconButton(
onPressed: () {
navigatorKey.currentState!.pop(booking);
},
icon: Icon(Icons.edit)),
],
)
],
),
),
);
}
}

0 comments on commit 206cfbc

Please sign in to comment.