@@ -90,6 +90,19 @@ class _MyReservationsPageState extends State<MyReservationsPage> {
9090 }
9191 }
9292
93+ Color _statusColor (ReservationStatus status) {
94+ switch (status) {
95+ case ReservationStatus .reserved:
96+ return const Color (0xFF2D6A4F );
97+ case ReservationStatus .completed:
98+ return const Color (0xFF1D8348 );
99+ case ReservationStatus .expired:
100+ return const Color (0xFF8E7D63 );
101+ case ReservationStatus .cancelled:
102+ return const Color (0xFF9E9E9E );
103+ }
104+ }
105+
93106 @override
94107 Widget build (BuildContext context) {
95108 final s = AppStrings .of (context);
@@ -127,43 +140,148 @@ class _MyReservationsPageState extends State<MyReservationsPage> {
127140
128141 final items = snapshot.data ?? const < _ReservationWithListing > [];
129142 if (items.isEmpty) {
130- return Center (child: Text (s.noMyReservations));
143+ return Center (
144+ child: Padding (
145+ padding: const EdgeInsets .all (24 ),
146+ child: Column (
147+ mainAxisSize: MainAxisSize .min,
148+ children: [
149+ const Icon (Icons .receipt_long_outlined, size: 44 ),
150+ const SizedBox (height: 10 ),
151+ Text (s.noMyReservations),
152+ const SizedBox (height: 12 ),
153+ FilledButton .tonalIcon (
154+ onPressed: () => context.go ('/' ),
155+ icon: const Icon (Icons .search_outlined),
156+ label: Text (
157+ AppScope .of (context).localeController.isZhTw
158+ ? '去找可領取餐點'
159+ : 'Browse listings' ,
160+ ),
161+ ),
162+ ],
163+ ),
164+ ),
165+ );
131166 }
132167
133- return ListView .builder (
168+ final aliasFrequency = < String , int > {};
169+ for (final item in items) {
170+ final alias = item.listing? .displayNameOptional? .trim () ?? '' ;
171+ if (alias.isEmpty) {
172+ continue ;
173+ }
174+ final normalized = alias.toLowerCase ();
175+ aliasFrequency[normalized] = (aliasFrequency[normalized] ?? 0 ) + 1 ;
176+ }
177+
178+ return ListView (
134179 padding: const EdgeInsets .all (12 ),
135- itemCount: items.length,
136- itemBuilder: (context, index) {
137- final item = items[index];
138- final reservation = item.reservation;
139- final listing = item.listing;
140- final statusLabel = s.statusLabel (switch (reservation.status) {
141- ReservationStatus .reserved => AppStatusLabel .reserved,
142- ReservationStatus .completed => AppStatusLabel .completed,
143- ReservationStatus .expired => AppStatusLabel .expired,
144- ReservationStatus .cancelled => AppStatusLabel .cancelled,
145- });
146- return Card (
147- child: ListTile (
148- title: Text ('${listing ?.itemType ?? 'Item' } · $statusLabel ' ),
149- subtitle: Text (
150- 'Code: ${reservation .pickupCode }\n '
151- 'Pickup: ${listing ?.pickupPointText ?? '-' }\n '
152- 'Expires: ${formatDateTime (reservation .expiresAt )}' ,
153- ),
154- isThreeLine: true ,
155- trailing: reservation.status == ReservationStatus .reserved
156- ? OutlinedButton (
157- onPressed: () => _cancelReservation (reservation),
158- child: Text (s.cancelReservation),
159- )
160- : null ,
161- onTap: () => context.go (
162- '/listing/${reservation .listingId }/reservation/${reservation .id }' ,
180+ children: [
181+ Card (
182+ color: Theme .of (context).colorScheme.surfaceContainerLow,
183+ child: Padding (
184+ padding: const EdgeInsets .all (12 ),
185+ child: Column (
186+ crossAxisAlignment: CrossAxisAlignment .start,
187+ children: [
188+ Text (
189+ s.privacyFaqTitle,
190+ style: Theme .of (context).textTheme.titleSmall,
191+ ),
192+ const SizedBox (height: 4 ),
193+ Text (
194+ s.privacyNotice,
195+ style: Theme .of (context).textTheme.bodySmall,
196+ ),
197+ const SizedBox (height: 4 ),
198+ Text (
199+ s.faqNotice,
200+ style: Theme .of (context).textTheme.bodySmall,
201+ ),
202+ ],
163203 ),
164204 ),
165- );
166- },
205+ ),
206+ const SizedBox (height: 8 ),
207+ ...items.map ((item) {
208+ final reservation = item.reservation;
209+ final listing = item.listing;
210+ final alias = listing? .displayNameOptional? .trim () ?? '' ;
211+ final isFrequentEnterprise =
212+ alias.isNotEmpty &&
213+ (aliasFrequency[alias.toLowerCase ()] ?? 0 ) >= 2 ;
214+ final statusLabel = s.statusLabel (switch (reservation.status) {
215+ ReservationStatus .reserved => AppStatusLabel .reserved,
216+ ReservationStatus .completed => AppStatusLabel .completed,
217+ ReservationStatus .expired => AppStatusLabel .expired,
218+ ReservationStatus .cancelled => AppStatusLabel .cancelled,
219+ });
220+ return Card (
221+ child: ListTile (
222+ title: Wrap (
223+ spacing: 8 ,
224+ runSpacing: 6 ,
225+ crossAxisAlignment: WrapCrossAlignment .center,
226+ children: [
227+ Text (listing? .itemType ?? 'Item' ),
228+ Container (
229+ padding: const EdgeInsets .symmetric (
230+ horizontal: 8 ,
231+ vertical: 2 ,
232+ ),
233+ decoration: BoxDecoration (
234+ color: _statusColor (
235+ reservation.status,
236+ ).withValues (alpha: 0.12 ),
237+ borderRadius: BorderRadius .circular (999 ),
238+ ),
239+ child: Text (
240+ statusLabel,
241+ style: TextStyle (
242+ color: _statusColor (reservation.status),
243+ fontWeight: FontWeight .w600,
244+ ),
245+ ),
246+ ),
247+ ],
248+ ),
249+ subtitle: Column (
250+ crossAxisAlignment: CrossAxisAlignment .start,
251+ mainAxisSize: MainAxisSize .min,
252+ children: [
253+ Text (
254+ 'Code: ${reservation .pickupCode }\n '
255+ 'Pickup: ${listing ?.pickupPointText ?? '-' }\n '
256+ 'Expires: ${formatDateTime (reservation .expiresAt )}' ,
257+ ),
258+ if (isFrequentEnterprise) ...[
259+ const SizedBox (height: 6 ),
260+ Chip (
261+ visualDensity: VisualDensity .compact,
262+ avatar: const Icon (
263+ Icons .verified,
264+ size: 16 ,
265+ color: Color (0xFF2D6A4F ),
266+ ),
267+ label: Text (s.frequentEnterprise),
268+ ),
269+ ],
270+ ],
271+ ),
272+ trailing: reservation.status == ReservationStatus .reserved
273+ ? OutlinedButton (
274+ onPressed: () => _cancelReservation (reservation),
275+ child: Text (s.cancelReservation),
276+ )
277+ : null ,
278+ onTap: () => context.go (
279+ '/listing/${reservation .listingId }/reservation/${reservation .id }' ,
280+ ),
281+ ),
282+ );
283+ }),
284+ ],
167285 );
168286 },
169287 ),
0 commit comments