Skip to content

Commit 5ff7bcd

Browse files
author
Luca
committed
feat: show availability calendar on item detail screen
1 parent 7b1b495 commit 5ff7bcd

2 files changed

Lines changed: 230 additions & 3 deletions

File tree

‎app/lib/screens/item_screen.dart‎

Lines changed: 219 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:shareloop/screens/edit_item_screen.dart';
77
import 'package:shareloop/screens/login_screen.dart';
88
import 'package:shareloop/screens/rent_request_chat_screen.dart';
99
import 'package:shareloop/state/auth.dart' show authProvider;
10+
import 'package:shareloop/state/booked_dates.dart';
1011
import 'package:shareloop/state/item_detail.dart';
1112

1213
class ItemScreen extends ConsumerWidget {
@@ -40,6 +41,7 @@ class ItemScreen extends ConsumerWidget {
4041
),
4142
body: asyncItem.when(
4243
data: (item) => _Content(
44+
itemId: itemId,
4345
item: item,
4446
userId: asyncUser.hasValue ? asyncUser.value?.id : null,
4547
),
@@ -50,15 +52,22 @@ class ItemScreen extends ConsumerWidget {
5052
}
5153
}
5254

53-
class _Content extends StatelessWidget {
55+
class _Content extends ConsumerWidget {
56+
final int itemId;
5457
final ItemDetail item;
5558
final int? userId;
5659

57-
const _Content({required this.item, required this.userId});
60+
const _Content({
61+
required this.itemId,
62+
required this.item,
63+
required this.userId,
64+
});
5865

5966
@override
60-
Widget build(BuildContext context) {
67+
Widget build(BuildContext context, WidgetRef ref) {
6168
final isOwnItem = userId != null && item.author.id == userId;
69+
final asyncBooked = ref.watch(bookedDatesProvider(itemId));
70+
6271
return SingleChildScrollView(
6372
child: Column(
6473
crossAxisAlignment: CrossAxisAlignment.start,
@@ -118,6 +127,24 @@ class _Content extends StatelessWidget {
118127
),
119128
),
120129
],
130+
const SizedBox(height: 24),
131+
Text(
132+
'Verfügbarkeit',
133+
style: Theme.of(context).textTheme.titleMedium,
134+
),
135+
const SizedBox(height: 8),
136+
asyncBooked.when(
137+
data: (booked) => _AvailabilityCalendar(
138+
bookedRanges: booked,
139+
),
140+
loading: () => const SizedBox(
141+
height: 300,
142+
child: Center(child: CircularProgressIndicator()),
143+
),
144+
error: (_, __) => const Text(
145+
'Verfügbarkeit konnte nicht geladen werden',
146+
),
147+
),
121148
const SizedBox(height: 32),
122149
const Divider(),
123150
const SizedBox(height: 16),
@@ -131,6 +158,195 @@ class _Content extends StatelessWidget {
131158
}
132159
}
133160

161+
class _AvailabilityCalendar extends StatefulWidget {
162+
final List<DateRange> bookedRanges;
163+
164+
const _AvailabilityCalendar({required this.bookedRanges});
165+
166+
@override
167+
State<_AvailabilityCalendar> createState() => _AvailabilityCalendarState();
168+
}
169+
170+
class _AvailabilityCalendarState extends State<_AvailabilityCalendar> {
171+
late DateTime _currentMonth;
172+
173+
@override
174+
void initState() {
175+
super.initState();
176+
_currentMonth = DateTime.now();
177+
}
178+
179+
@override
180+
Widget build(BuildContext context) {
181+
final now = DateTime.now();
182+
return Card(
183+
child: Padding(
184+
padding: const EdgeInsets.all(8),
185+
child: Column(
186+
children: [
187+
_MonthHeader(
188+
currentMonth: _currentMonth,
189+
onPrevious: _currentMonth.month == now.month &&
190+
_currentMonth.year == now.year
191+
? null
192+
: () => setState(() {
193+
_currentMonth = DateTime(
194+
_currentMonth.year,
195+
_currentMonth.month - 1,
196+
);
197+
}),
198+
onNext: () => setState(() {
199+
_currentMonth = DateTime(
200+
_currentMonth.year,
201+
_currentMonth.month + 1,
202+
);
203+
}),
204+
),
205+
_CalendarGrid(
206+
currentMonth: _currentMonth,
207+
bookedRanges: widget.bookedRanges,
208+
),
209+
],
210+
),
211+
),
212+
);
213+
}
214+
}
215+
216+
class _MonthHeader extends StatelessWidget {
217+
final DateTime currentMonth;
218+
final VoidCallback? onPrevious;
219+
final VoidCallback onNext;
220+
221+
const _MonthHeader({
222+
required this.currentMonth,
223+
required this.onPrevious,
224+
required this.onNext,
225+
});
226+
227+
@override
228+
Widget build(BuildContext context) {
229+
final loc = MaterialLocalizations.of(context);
230+
return Row(
231+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
232+
children: [
233+
IconButton(
234+
icon: const Icon(Icons.chevron_left),
235+
onPressed: onPrevious,
236+
),
237+
Text(
238+
loc.formatMonthYear(currentMonth),
239+
style: Theme.of(context).textTheme.titleSmall,
240+
),
241+
IconButton(
242+
icon: const Icon(Icons.chevron_right),
243+
onPressed: onNext,
244+
),
245+
],
246+
);
247+
}
248+
}
249+
250+
class _CalendarGrid extends StatelessWidget {
251+
final DateTime currentMonth;
252+
final List<DateRange> bookedRanges;
253+
254+
const _CalendarGrid({
255+
required this.currentMonth,
256+
required this.bookedRanges,
257+
});
258+
259+
bool _isBooked(DateTime day) {
260+
for (final r in bookedRanges) {
261+
final start = DateTime(r.startDate.year, r.startDate.month, r.startDate.day);
262+
final end = DateTime(r.endDate.year, r.endDate.month, r.endDate.day);
263+
if (!day.isBefore(start) && !day.isAfter(end)) return true;
264+
}
265+
return false;
266+
}
267+
268+
@override
269+
Widget build(BuildContext context) {
270+
final cs = Theme.of(context).colorScheme;
271+
final loc = MaterialLocalizations.of(context);
272+
final now = DateTime.now();
273+
final today = DateTime(now.year, now.month, now.day);
274+
275+
final firstDayOfMonth = DateTime(currentMonth.year, currentMonth.month, 1);
276+
final lastDayOfMonth = DateTime(currentMonth.year, currentMonth.month + 1, 0);
277+
final weekdayOffset = loc.firstDayOfWeekIndex;
278+
final startWeekday = (firstDayOfMonth.weekday - weekdayOffset + 7) % 7;
279+
280+
final weekdays = List.generate(7, (i) {
281+
return loc.narrowWeekdays[(i + weekdayOffset) % 7];
282+
});
283+
284+
return Column(
285+
children: [
286+
Row(
287+
mainAxisAlignment: MainAxisAlignment.spaceAround,
288+
children: weekdays
289+
.map((d) => SizedBox(
290+
width: 36,
291+
child: Text(
292+
d,
293+
textAlign: TextAlign.center,
294+
style: TextStyle(
295+
fontSize: 12,
296+
color: cs.onSurfaceVariant,
297+
),
298+
),
299+
))
300+
.toList(),
301+
),
302+
const SizedBox(height: 4),
303+
...List.generate(_totalWeeks(firstDayOfMonth, lastDayOfMonth, startWeekday), (weekIndex) {
304+
return Row(
305+
mainAxisAlignment: MainAxisAlignment.spaceAround,
306+
children: List.generate(7, (dayIndex) {
307+
final dayNumber = weekIndex * 7 + dayIndex - startWeekday + 1;
308+
if (dayNumber < 1 || dayNumber > lastDayOfMonth.day) {
309+
return const SizedBox(width: 36, height: 36);
310+
}
311+
final date = DateTime(currentMonth.year, currentMonth.month, dayNumber);
312+
final isPast = date.isBefore(today);
313+
final booked = _isBooked(date);
314+
final isToday = date == today;
315+
316+
return Container(
317+
width: 36,
318+
height: 36,
319+
alignment: Alignment.center,
320+
decoration: isToday
321+
? BoxDecoration(
322+
color: cs.primary.withValues(alpha: 0.15),
323+
shape: BoxShape.circle,
324+
)
325+
: null,
326+
child: Text(
327+
'$dayNumber',
328+
style: TextStyle(
329+
fontSize: 14,
330+
color: booked || isPast
331+
? cs.onSurface.withValues(alpha: 0.3)
332+
: cs.onSurface,
333+
fontWeight: isToday ? FontWeight.bold : null,
334+
),
335+
),
336+
);
337+
}),
338+
);
339+
}),
340+
],
341+
);
342+
}
343+
344+
int _totalWeeks(DateTime first, DateTime last, int startWeekday) {
345+
final totalDays = last.day + startWeekday;
346+
return (totalDays / 7).ceil();
347+
}
348+
}
349+
134350
class _ItemRatingsSection extends StatelessWidget {
135351
final ItemDetail item;
136352

‎app/lib/state/booked_dates.dart‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:openapi/api.dart';
3+
import 'package:shareloop/app_config.dart';
4+
5+
final bookedDatesProvider =
6+
FutureProvider.autoDispose.family<List<DateRange>, int>(
7+
(ref, itemId) async {
8+
final result = await AppConfig.apiClient.getBookedDates(itemId);
9+
return result ?? [];
10+
},
11+
);

0 commit comments

Comments
 (0)