|
|
|
|
@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
|
|
|
import 'package:geolocator/geolocator.dart';
|
|
|
|
|
import 'package:mohem_flutter_app/api/login_api_client.dart';
|
|
|
|
|
import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart';
|
|
|
|
|
import 'package:mohem_flutter_app/app_state/app_state.dart';
|
|
|
|
|
@ -20,6 +21,9 @@ import 'package:mohem_flutter_app/models/post_params_model.dart';
|
|
|
|
|
import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart';
|
|
|
|
|
import 'package:mohem_flutter_app/widgets/button/default_button.dart';
|
|
|
|
|
import 'package:mohem_flutter_app/widgets/info_chip_widget.dart';
|
|
|
|
|
import 'package:mohem_flutter_app/widgets/location/Location.dart' as location;
|
|
|
|
|
import 'package:mohem_flutter_app/widgets/toast/app_toast.dart';
|
|
|
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
|
import 'package:shimmer/shimmer.dart';
|
|
|
|
|
|
|
|
|
|
class DashboardScreenForDependet extends StatefulWidget {
|
|
|
|
|
@ -43,15 +47,15 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
bool isSubcategoriesLoading = false;
|
|
|
|
|
String? _selectedSubcategoryId;
|
|
|
|
|
|
|
|
|
|
// Filters bottom sheet state. Availability is single-select and functional - it drives the
|
|
|
|
|
// same selectedTypeFilter/offerType param the old chip row used. Sort/Distance/Minimum
|
|
|
|
|
// discount still have no backend support, so those remain UI-only for now. Committed on
|
|
|
|
|
// "Apply filters"; sort/distance/discount are persisted here so reopening the sheet shows
|
|
|
|
|
// the last-applied selection (availability's source of truth is selectedTypeFilter itself).
|
|
|
|
|
// Filters bottom sheet state - all fully wired to the API now. Availability drives
|
|
|
|
|
// WP_Offer_Type (via selectedTypeFilter), _filterSortBy drives WP_Sort_By ('nearest'
|
|
|
|
|
// requires WP_Geo_Location, see _ensureLocationReadyForNearestSort), _filterDistance drives
|
|
|
|
|
// WP_Radius (null when "any"), _filterMinDiscount drives WP_Min_Discount (null when "any").
|
|
|
|
|
// Committed on "Apply filters"/"Reset"; persisted here so reopening the sheet shows the
|
|
|
|
|
// last-applied selection (availability's source of truth is selectedTypeFilter itself).
|
|
|
|
|
String _filterSortBy = 'nearest';
|
|
|
|
|
String _filterDistance = '5km';
|
|
|
|
|
String _filterMinDiscount = 'any';
|
|
|
|
|
bool isNearestOffers = true;
|
|
|
|
|
bool isLoading = true;
|
|
|
|
|
bool isLoadingMore = false;
|
|
|
|
|
bool isCategoriesLoading = true;
|
|
|
|
|
@ -284,6 +288,11 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
searchQuery = query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool wantsNearest = _filterSortBy == 'nearest';
|
|
|
|
|
if (wantsNearest && !await _ensureLocationReadyForNearestSort()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
|
isLoading = true;
|
|
|
|
|
currentPage = 1;
|
|
|
|
|
@ -294,14 +303,19 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
String catParam = (targetCategoryId == "all" || targetCategoryId == "0") ? "" : targetCategoryId;
|
|
|
|
|
String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter(selectedTypeFilter);
|
|
|
|
|
String? queryParam = searchQuery.trim().isNotEmpty ? searchQuery.trim() : null;
|
|
|
|
|
print("Fetching WalaPlus retailers: category='$catParam', offerType='$offerTypeParam', query='$queryParam', page=1, per_page=$pageSize");
|
|
|
|
|
int? radiusParam = _radiusFromFilterDistance(_filterDistance);
|
|
|
|
|
int? minDiscountParam = _filterMinDiscount == 'any' ? null : int.tryParse(_filterMinDiscount);
|
|
|
|
|
print("Fetching WalaPlus retailers: category='$catParam', offerType='$offerTypeParam', query='$queryParam', sortBy='$_filterSortBy', radius=$radiusParam, minDiscount=$minDiscountParam, page=1, per_page=$pageSize");
|
|
|
|
|
WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers(
|
|
|
|
|
categoryId: catParam,
|
|
|
|
|
page: 1,
|
|
|
|
|
perPage: pageSize,
|
|
|
|
|
offerType: offerTypeParam,
|
|
|
|
|
query: queryParam,
|
|
|
|
|
context: isNearestOffers ? context : null,
|
|
|
|
|
sortBy: _filterSortBy,
|
|
|
|
|
radius: radiusParam,
|
|
|
|
|
minDiscount: minDiscountParam,
|
|
|
|
|
context: wantsNearest ? context : null,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
retailersList = response?.retailers ?? <WalaPlusRetailerModel>[];
|
|
|
|
|
@ -325,6 +339,11 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
Future<void> fetchNextPage() async {
|
|
|
|
|
if (isLoading || isLoadingMore || _isFetchingPage || !hasMore) return;
|
|
|
|
|
|
|
|
|
|
bool wantsNearest = _filterSortBy == 'nearest';
|
|
|
|
|
if (wantsNearest && !await _ensureLocationReadyForNearestSort()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_isFetchingPage = true;
|
|
|
|
|
setState(() {
|
|
|
|
|
isLoadingMore = true;
|
|
|
|
|
@ -335,8 +354,10 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
String catParam = (currentCategoryId == "all" || currentCategoryId == "0") ? "" : currentCategoryId;
|
|
|
|
|
String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter(selectedTypeFilter);
|
|
|
|
|
String? queryParam = searchQuery.trim().isNotEmpty ? searchQuery.trim() : null;
|
|
|
|
|
int? radiusParam = _radiusFromFilterDistance(_filterDistance);
|
|
|
|
|
int? minDiscountParam = _filterMinDiscount == 'any' ? null : int.tryParse(_filterMinDiscount);
|
|
|
|
|
|
|
|
|
|
print("Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam', offerType: '$offerTypeParam', query: '$queryParam'");
|
|
|
|
|
print("Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam', offerType: '$offerTypeParam', query: '$queryParam', sortBy: '$_filterSortBy'");
|
|
|
|
|
|
|
|
|
|
WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers(
|
|
|
|
|
categoryId: catParam,
|
|
|
|
|
@ -344,7 +365,10 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
perPage: pageSize,
|
|
|
|
|
offerType: offerTypeParam,
|
|
|
|
|
query: queryParam,
|
|
|
|
|
context: isNearestOffers ? context : null,
|
|
|
|
|
sortBy: _filterSortBy,
|
|
|
|
|
radius: radiusParam,
|
|
|
|
|
minDiscount: minDiscountParam,
|
|
|
|
|
context: wantsNearest ? context : null,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
List<WalaPlusRetailerModel> newRetailers = response?.retailers ?? [];
|
|
|
|
|
@ -373,6 +397,59 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
await Future.wait<dynamic>(<Future<void>>[fetchCategories(), fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId, query: _searchController.text)]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// '5km'/'10km'/'25km' -> 5/10/25 for WP_Radius; 'any' -> null (omit the param - no distance cap).
|
|
|
|
|
int? _radiusFromFilterDistance(String distance) {
|
|
|
|
|
if (distance == 'any') return null;
|
|
|
|
|
return int.tryParse(distance.replaceAll('km', ''));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WP_Sort_By=nearest requires WP_Geo_Location to be passed alongside it, so before fetching
|
|
|
|
|
// with "Nearest" selected we make sure location services + permission are actually available.
|
|
|
|
|
// Mirrors the existing attendance-flow location-permission pattern (see
|
|
|
|
|
// mark_attendance_widget.dart's checkHuaweiLocationPermission): services-off -> prompt to open
|
|
|
|
|
// settings, permission-not-granted -> request it, still denied -> toast. Never auto-retries the
|
|
|
|
|
// fetch - the user must grant access and trigger the fetch again themselves.
|
|
|
|
|
Future<bool> _ensureLocationReadyForNearestSort() async {
|
|
|
|
|
Completer<bool> enabledCompleter = Completer<bool>();
|
|
|
|
|
location.Location.isEnabled((bool isEnabled) {
|
|
|
|
|
if (!enabledCompleter.isCompleted) enabledCompleter.complete(isEnabled);
|
|
|
|
|
});
|
|
|
|
|
bool isEnabled = await enabledCompleter.future;
|
|
|
|
|
if (!isEnabled) {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
ConfirmBottomSheet.show(
|
|
|
|
|
context,
|
|
|
|
|
message: LocaleKeys.locationServicesRequiredForNearestOffers.tr(),
|
|
|
|
|
confirmText: LocaleKeys.openSettings.tr(),
|
|
|
|
|
showCancelButton: false,
|
|
|
|
|
onConfirm: () async {
|
|
|
|
|
await Geolocator.openLocationSettings();
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Completer<bool> permissionCompleter = Completer<bool>();
|
|
|
|
|
location.Location.havePermission((bool hasPermission) {
|
|
|
|
|
if (!permissionCompleter.isCompleted) permissionCompleter.complete(hasPermission);
|
|
|
|
|
});
|
|
|
|
|
bool hasPermission = await permissionCompleter.future;
|
|
|
|
|
if (!hasPermission) {
|
|
|
|
|
bool granted = await _requestLocationPermission();
|
|
|
|
|
if (!granted) {
|
|
|
|
|
AppToast.show(LocaleKeys.locationPermissionRequiredForNearestOffers.tr());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<bool> _requestLocationPermission() async {
|
|
|
|
|
Map<Permission, PermissionStatus> result = await <Permission>[Permission.location].request();
|
|
|
|
|
return result[Permission.location] == PermissionStatus.granted || result[Permission.locationAlways] == PermissionStatus.granted;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _onSearchChanged(String query) {
|
|
|
|
|
_debounceTimer?.cancel();
|
|
|
|
|
searchQuery = query;
|
|
|
|
|
@ -642,7 +719,16 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
GridView.builder(
|
|
|
|
|
shrinkWrap: true,
|
|
|
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
|
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, childAspectRatio: 0.7, crossAxisSpacing: 12, mainAxisSpacing: 16),
|
|
|
|
|
// Fixed pixel height instead of childAspectRatio - the card's content is
|
|
|
|
|
// fixed in absolute pixels, not proportional to screen width, so an aspect
|
|
|
|
|
// ratio made the computed cell height inconsistent across device sizes.
|
|
|
|
|
// _buildOfferCard now also standardizes every card's content height (the
|
|
|
|
|
// subtitle line and badges row are always reserved/fixed-height, not
|
|
|
|
|
// conditional/wrapping), so 235 is a real, stable fit - not a worst-case
|
|
|
|
|
// guess - with a non-scrollable SingleChildScrollView backstop in the card
|
|
|
|
|
// itself for the rare case a device still needs a bit more (e.g. large
|
|
|
|
|
// system text scaling), which clips instead of overflowing.
|
|
|
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, mainAxisExtent: 235, crossAxisSpacing: 12, mainAxisSpacing: 16),
|
|
|
|
|
itemCount: _filteredRetailersList.length,
|
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
|
return _buildOfferCard(_filteredRetailersList[index], isAr);
|
|
|
|
|
@ -864,9 +950,9 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Matches the shared Filters bottom sheet design. Availability is single-select and
|
|
|
|
|
// functional, driving selectedTypeFilter/offerType exactly like the old chip row did.
|
|
|
|
|
// Sort/Distance/Minimum discount still have no backend support, so those remain UI-only.
|
|
|
|
|
// Matches the shared Filters bottom sheet design. All four sections are fully wired to the
|
|
|
|
|
// API: Availability -> WP_Offer_Type (via selectedTypeFilter), Sort by -> WP_Sort_By, Distance
|
|
|
|
|
// -> WP_Radius, Minimum discount -> WP_Min_Discount. "Any"/"All" maps to null (param omitted).
|
|
|
|
|
void _showFiltersBottomSheet() {
|
|
|
|
|
String draftAvailability = selectedTypeFilter;
|
|
|
|
|
String draftSortBy = _filterSortBy;
|
|
|
|
|
@ -1221,11 +1307,17 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
boxShadow: [BoxShadow(color: const Color(0xFF0D1720).withOpacity(0.0392), offset: const Offset(0, 5), blurRadius: 18, spreadRadius: 0)],
|
|
|
|
|
),
|
|
|
|
|
padding: const EdgeInsets.all(10),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
// Inset image with rounded corners
|
|
|
|
|
ClipRRect(
|
|
|
|
|
// Hard backstop against RenderFlex overflow: if content ever needs more height than
|
|
|
|
|
// mainAxisExtent provides (e.g. a device with large system text scaling), this clips
|
|
|
|
|
// the excess instead of showing a debug overflow error. Not meant to be scrolled by
|
|
|
|
|
// the user - just a safety net, hence NeverScrollableScrollPhysics.
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
// Inset image with rounded corners
|
|
|
|
|
ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(14),
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
height: 120,
|
|
|
|
|
@ -1293,29 +1385,34 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
|
|
|
|
|
// Retailer Name / Bundle Title
|
|
|
|
|
(item.name ?? "").toText14(isBold: true, maxlines: 1, overflow: TextOverflow.ellipsis, color: const Color(0xFF1E293B)),
|
|
|
|
|
if (offerSubtitle.isNotEmpty) ...[const SizedBox(height: 2), offerSubtitle.toText14(maxlines: 1, overflow: TextOverflow.ellipsis, isBold: true, color: const Color(0xFF1E293B))],
|
|
|
|
|
|
|
|
|
|
// Badges Row: Type, Validity, Location & Usage
|
|
|
|
|
if (offerTypes.isNotEmpty || validityText.trim().isNotEmpty || cityText.trim().isNotEmpty || usageText.trim().isNotEmpty) ...[
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
Wrap(
|
|
|
|
|
spacing: 5,
|
|
|
|
|
runSpacing: 4,
|
|
|
|
|
// Subtitle line is always rendered (even empty) so every card reserves the same
|
|
|
|
|
// vertical space here, instead of shorter-content cards ending up shorter overall.
|
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
|
offerSubtitle.toText14(maxlines: 1, overflow: TextOverflow.ellipsis, isBold: true, color: const Color(0xFF1E293B)),
|
|
|
|
|
|
|
|
|
|
// Badges row: Type, Location & Usage - always exactly one row, fixed height.
|
|
|
|
|
// Horizontally scrollable instead of wrapping to a 2nd row, so every card's badge
|
|
|
|
|
// section is the same height regardless of how many badges it has.
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: 28,
|
|
|
|
|
child: ListView(
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
physics: const BouncingScrollPhysics(),
|
|
|
|
|
children: <Widget>[
|
|
|
|
|
// One Type Chip per distinct type - Online (green) and/or In Store (blue).
|
|
|
|
|
for (String offerType in offerTypes)
|
|
|
|
|
for (String offerType in offerTypes) ...<Widget>[
|
|
|
|
|
InfoChipWidget(
|
|
|
|
|
text: _getTypeFilterTitle(offerType),
|
|
|
|
|
icon: _getTypeFilterIcon(offerType),
|
|
|
|
|
backgroundColor: offerType == 'online' ? const Color(0xFFE6F7F0) : const Color(0xFFEFF6FF),
|
|
|
|
|
textColor: offerType == 'online' ? const Color(0xFF00A651) : const Color(0xFF2563EB),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// Validity badge
|
|
|
|
|
// if (validityText.trim().isNotEmpty) InfoChipWidget(text: validityText, backgroundColor: const Color(0xFFE6F7F0), textColor: const Color(0xFF00A651)),
|
|
|
|
|
const SizedBox(width: 5),
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
// Location Badge
|
|
|
|
|
if (cityText.trim().isNotEmpty)
|
|
|
|
|
if (cityText.trim().isNotEmpty) ...<Widget>[
|
|
|
|
|
InfoChipWidget(
|
|
|
|
|
text: cityText,
|
|
|
|
|
svgIcon: "assets/icons/location.svg",
|
|
|
|
|
@ -1325,15 +1422,18 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
iconColor: const Color(0xFF64748B),
|
|
|
|
|
iconTextGap: 2.5,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 5),
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
// Usage badge
|
|
|
|
|
if (usageText.trim().isNotEmpty) InfoChipWidget(text: usageText, backgroundColor: const Color(0xFFFEF3C7), textColor: const Color(0xFFB45309)),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1344,7 +1444,7 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
child: GridView.builder(
|
|
|
|
|
shrinkWrap: true,
|
|
|
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
|
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, childAspectRatio: 0.7, crossAxisSpacing: 12, mainAxisSpacing: 16),
|
|
|
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: isTablet ? 4 : 2, mainAxisExtent: 235, crossAxisSpacing: 12, mainAxisSpacing: 16),
|
|
|
|
|
itemCount: 4,
|
|
|
|
|
itemBuilder: (_, __) {
|
|
|
|
|
return Container(
|
|
|
|
|
@ -1353,19 +1453,17 @@ class _DashboardScreenForDependetState extends State<DashboardScreenForDependet>
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: <Widget>[
|
|
|
|
|
Container(height: 105, width: double.infinity, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14))),
|
|
|
|
|
Container(height: 120, width: double.infinity, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14))),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Container(height: 14, width: 110, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
Container(height: 12, width: 80, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
Wrap(
|
|
|
|
|
spacing: 5,
|
|
|
|
|
runSpacing: 4,
|
|
|
|
|
Container(height: 14, width: 80, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
Row(
|
|
|
|
|
children: <Widget>[
|
|
|
|
|
Container(height: 16, width: 50, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10))),
|
|
|
|
|
Container(height: 16, width: 45, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10))),
|
|
|
|
|
Container(height: 16, width: 45, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10))),
|
|
|
|
|
Container(height: 28, width: 50, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))),
|
|
|
|
|
const SizedBox(width: 5),
|
|
|
|
|
Container(height: 28, width: 45, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
|