Initial commit: SIBU 2.0 MISSION

This commit is contained in:
2026-02-21 09:53:31 -05:00
commit 0c7aa53c8b
400 changed files with 67708 additions and 0 deletions

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sizer/sizer.dart';
import '../../../core/app_export.dart';
import '../../../services/coupon_service.dart';
import '../../../theme/app_theme.dart';
class CategoryFilterChips extends StatelessWidget {
final String selectedCategory;
final ValueChanged<String> onCategorySelected;
const CategoryFilterChips({
super.key,
required this.selectedCategory,
required this.onCategorySelected,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final categories = CouponService.getCategoryOptions();
return Container(
height: 6.h,
padding: EdgeInsets.symmetric(vertical: 1.h),
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 4.w),
itemCount: categories.length,
separatorBuilder: (context, index) => SizedBox(width: 2.w),
itemBuilder: (context, index) {
final category = categories[index];
final isSelected = selectedCategory == category;
return GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
onCategorySelected(category);
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 4.w,
vertical: 1.h,
),
decoration: BoxDecoration(
color: isSelected
? AppTheme.accentYellow
: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected
? AppTheme.accentYellow
: theme.colorScheme.outline,
width: 1,
),
),
child: Text(
category,
style: theme.textTheme.labelMedium?.copyWith(
color: isSelected
? AppTheme.primaryBlack
: theme.colorScheme.onSurface,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
),
),
),
);
},
),
);
}
}

View File

@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sizer/sizer.dart';
import '../../../core/app_export.dart';
import '../../../models/coupon_model.dart';
class CouponCardWidget extends StatelessWidget {
final CouponModel coupon;
final VoidCallback onTap;
const CouponCardWidget({
super.key,
required this.coupon,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: () {
HapticFeedback.lightImpact();
onTap();
},
borderRadius: BorderRadius.circular(12),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: theme.colorScheme.surface,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image Section (16:9 aspect ratio)
Expanded(
flex: 4,
child: Container(
width: double.infinity,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: coupon.imageUrl != null
? CustomImageWidget(
imageUrl: coupon.imageUrl!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
semanticLabel: 'Imagen de ${coupon.businessName}',
)
: Container(
color: theme.colorScheme.surfaceContainerHighest,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomIconWidget(
iconName: 'image',
color: theme.colorScheme.onSurfaceVariant,
size: 24,
),
SizedBox(height: 1.h),
Text(
'Sin imagen',
style:
theme.textTheme.bodySmall?.copyWith(
color:
theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
),
),
),
),
),
// Content Section
Expanded(
flex: 3,
child: Padding(
padding: EdgeInsets.all(3.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Business Name (bold)
Text(
coupon.businessName,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 0.5.h),
// Title (subtitle style)
Text(
coupon.title,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 0.5.h),
// Description (2 lines max, small text)
Expanded(
child: Text(
coupon.description,
style: theme.textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(height: 1.h),
// Valid until text
Text(
coupon.validUntil != null
? 'Válido hasta: ${coupon.validUntilFormatted}'
: 'Sin fecha de vencimiento',
style: theme.textTheme.bodySmall?.copyWith(
color: coupon.isExpired
? AppTheme.errorRed
: coupon.isExpiringSoon
? Colors.orange
: theme.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sizer/sizer.dart';
import '../../../core/app_export.dart';
import '../../../models/coupon_model.dart';
class CouponDetailModal extends StatelessWidget {
final CouponModel coupon;
final VoidCallback? onUseCoupon;
const CouponDetailModal({
super.key,
required this.coupon,
this.onUseCoupon,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.all(4.w),
child: Container(
constraints: BoxConstraints(
maxHeight: 85.h,
),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Close Button
Align(
alignment: Alignment.topRight,
child: Padding(
padding: EdgeInsets.all(2.w),
child: IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: CustomIconWidget(
iconName: 'close',
color: theme.colorScheme.onSurface,
size: 24,
),
),
),
),
// Content
Flexible(
child: Padding(
padding: EdgeInsets.fromLTRB(6.w, 0, 6.w, 6.w),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Full Image
Container(
width: double.infinity,
height: 25.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: theme.colorScheme.surfaceContainerHighest,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: coupon.imageUrl != null
? CustomImageWidget(
imageUrl: coupon.imageUrl!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
semanticLabel:
'Imagen de ${coupon.businessName}',
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomIconWidget(
iconName: 'image',
color: theme.colorScheme.onSurfaceVariant,
size: 32,
),
SizedBox(height: 1.h),
Text(
'Sin imagen',
style:
theme.textTheme.bodyMedium?.copyWith(
color:
theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
),
),
SizedBox(height: 4.w),
// Business Name
Text(
coupon.businessName,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 2.w),
// Title
Text(
coupon.title,
style: theme.textTheme.titleMedium?.copyWith(
color: AppTheme.accentYellow,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 3.w),
// Description
Text(
coupon.description,
style: theme.textTheme.bodyMedium,
),
SizedBox(height: 3.w),
// Valid Until
Container(
padding: EdgeInsets.all(3.w),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
CustomIconWidget(
iconName: 'access_time',
color: theme.colorScheme.onSurfaceVariant,
size: 20,
),
SizedBox(width: 2.w),
Text(
coupon.validUntil != null
? 'Válido hasta: ${coupon.validUntilFormatted}'
: 'Sin fecha de vencimiento',
style: theme.textTheme.bodyMedium?.copyWith(
color: coupon.isExpired
? AppTheme.errorRed
: coupon.isExpiringSoon
? Colors.orange
: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
),
SizedBox(height: 4.w),
// Action Buttons Row
Row(
children: [
// Call Button (placeholder)
Expanded(
child: OutlinedButton.icon(
onPressed: () {
HapticFeedback.lightImpact();
// TODO: Implement call functionality
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Función de llamada próximamente'),
),
);
},
icon: CustomIconWidget(
iconName: 'call',
color: theme.colorScheme.primary,
size: 20,
),
label: const Text('Llamar'),
style: OutlinedButton.styleFrom(
foregroundColor: theme.colorScheme.primary,
side:
BorderSide(color: theme.colorScheme.outline),
),
),
),
SizedBox(width: 3.w),
// WhatsApp Button (placeholder)
Expanded(
child: OutlinedButton.icon(
onPressed: () {
HapticFeedback.lightImpact();
// TODO: Implement WhatsApp functionality
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Función de WhatsApp próximamente'),
),
);
},
icon: CustomIconWidget(
iconName: 'chat',
color: Colors.green,
size: 20,
),
label: const Text('WhatsApp'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.green,
side:
BorderSide(color: theme.colorScheme.outline),
),
),
),
SizedBox(width: 3.w),
// Location Button (placeholder)
Expanded(
child: OutlinedButton.icon(
onPressed: () {
HapticFeedback.lightImpact();
// TODO: Implement location functionality
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Función de ubicación próximamente'),
),
);
},
icon: CustomIconWidget(
iconName: 'location_on',
color: AppTheme.errorRed,
size: 20,
),
label: const Text('Ubicación'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.errorRed,
side:
BorderSide(color: theme.colorScheme.outline),
),
),
),
],
),
],
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../core/app_export.dart';
class EmptyStateWidget extends StatelessWidget {
final String title;
final String subtitle;
final String? actionText;
final VoidCallback? onActionPressed;
const EmptyStateWidget({
super.key,
required this.title,
required this.subtitle,
this.actionText,
this.onActionPressed,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: EdgeInsets.all(8.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Empty state icon
Container(
padding: EdgeInsets.all(6.w),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
shape: BoxShape.circle,
),
child: CustomIconWidget(
iconName: 'local_offer',
color: theme.colorScheme.onSurfaceVariant,
size: 48,
),
),
SizedBox(height: 4.h),
// Title
Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
SizedBox(height: 2.h),
// Subtitle
Text(
subtitle,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
if (actionText != null && onActionPressed != null) ...[
SizedBox(height: 4.h),
// Action button
ElevatedButton(
onPressed: onActionPressed,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.accentYellow,
foregroundColor: AppTheme.primaryBlack,
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 1.5.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
actionText!,
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
],
],
),
),
);
}
}

View File

@ -0,0 +1,273 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sizer/sizer.dart';
import '../../../core/app_export.dart';
import '../../../theme/app_theme.dart';
class FilterBottomSheet extends StatefulWidget {
final Map<String, dynamic> currentFilters;
final Function(Map<String, dynamic>) onFiltersChanged;
const FilterBottomSheet({
super.key,
required this.currentFilters,
required this.onFiltersChanged,
});
@override
State<FilterBottomSheet> createState() => _FilterBottomSheetState();
}
class _FilterBottomSheetState extends State<FilterBottomSheet> {
late Map<String, dynamic> _filters;
final List<String> _categories = [
'Todos',
'Restaurantes',
'Tiendas',
'Servicios',
'Entretenimiento',
'Salud',
'Belleza',
];
final List<String> _sortOptions = [
'Más recientes',
'Por vencer',
'Mayor descuento',
'Distancia',
];
@override
void initState() {
super.initState();
_filters = Map<String, dynamic>.from(widget.currentFilters);
}
void _applyFilters() {
HapticFeedback.lightImpact();
widget.onFiltersChanged(_filters);
Navigator.of(context).pop();
}
void _resetFilters() {
HapticFeedback.lightImpact();
setState(() {
_filters = {
'category': 'Todos',
'sortBy': 'Más recientes',
'showUsed': false,
'showExpired': false,
};
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle bar
Container(
margin: EdgeInsets.only(top: 2.h),
width: 12.w,
height: 0.5.h,
decoration: BoxDecoration(
color: theme.colorScheme.outline,
borderRadius: BorderRadius.circular(2),
),
),
// Header
Padding(
padding: EdgeInsets.all(4.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Filtros',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
TextButton(
onPressed: _resetFilters,
child: Text(
'Limpiar',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
Divider(height: 1, color: theme.colorScheme.outline),
// Content
Flexible(
child: SingleChildScrollView(
padding: EdgeInsets.all(4.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Category Filter
Text(
'Categoría',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 2.h),
Wrap(
spacing: 2.w,
runSpacing: 1.h,
children: _categories.map((category) {
final isSelected = _filters['category'] == category;
return FilterChip(
label: Text(category),
selected: isSelected,
onSelected: (selected) {
HapticFeedback.lightImpact();
setState(() {
_filters['category'] = category;
});
},
backgroundColor: theme.colorScheme.surface,
selectedColor:
AppTheme.accentYellow.withValues(alpha: 0.2),
checkmarkColor: AppTheme.primaryBlack,
labelStyle: theme.textTheme.bodyMedium?.copyWith(
color: isSelected
? AppTheme.primaryBlack
: theme.colorScheme.onSurface,
fontWeight:
isSelected ? FontWeight.w600 : FontWeight.w400,
),
side: BorderSide(
color: isSelected
? AppTheme.accentYellow
: theme.colorScheme.outline,
),
);
}).toList(),
),
SizedBox(height: 3.h),
// Sort Options
Text(
'Ordenar por',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 2.h),
Column(
children: _sortOptions.map((option) {
final isSelected = _filters['sortBy'] == option;
return RadioListTile<String>(
title: Text(
option,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight:
isSelected ? FontWeight.w600 : FontWeight.w400,
),
),
value: option,
groupValue: _filters['sortBy'],
onChanged: (value) {
HapticFeedback.lightImpact();
setState(() {
_filters['sortBy'] = value;
});
},
activeColor: AppTheme.primaryBlack,
contentPadding: EdgeInsets.zero,
);
}).toList(),
),
SizedBox(height: 2.h),
// Additional Options
Text(
'Mostrar',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 1.h),
SwitchListTile(
title: Text(
'Cupones usados',
style: theme.textTheme.bodyMedium,
),
value: _filters['showUsed'] ?? false,
onChanged: (value) {
HapticFeedback.lightImpact();
setState(() {
_filters['showUsed'] = value;
});
},
activeColor: AppTheme.accentYellow,
activeTrackColor: AppTheme.primaryBlack,
contentPadding: EdgeInsets.zero,
),
SwitchListTile(
title: Text(
'Cupones vencidos',
style: theme.textTheme.bodyMedium,
),
value: _filters['showExpired'] ?? false,
onChanged: (value) {
HapticFeedback.lightImpact();
setState(() {
_filters['showExpired'] = value;
});
},
activeColor: AppTheme.accentYellow,
activeTrackColor: AppTheme.primaryBlack,
contentPadding: EdgeInsets.zero,
),
],
),
),
),
// Apply Button
Padding(
padding: EdgeInsets.all(4.w),
child: SizedBox(
width: double.infinity,
height: 6.h,
child: ElevatedButton(
onPressed: _applyFilters,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.accentYellow,
foregroundColor: AppTheme.primaryBlack,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Aplicar Filtros',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
),
),
SizedBox(height: MediaQuery.of(context).padding.bottom),
],
),
);
}
}

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sizer/sizer.dart';
import '../../../core/app_export.dart';
import '../../../services/coupon_service.dart';
import '../../../widgets/custom_icon_widget.dart';
class SortDropdown extends StatelessWidget {
final String selectedSort;
final ValueChanged<String> onSortChanged;
const SortDropdown({
super.key,
required this.selectedSort,
required this.onSortChanged,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final sortOptions = CouponService.getSortOptions();
return Container(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Ordenar por:',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 3.w),
decoration: BoxDecoration(
border: Border.all(color: theme.colorScheme.outline),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedSort,
isDense: true,
icon: CustomIconWidget(
iconName: 'keyboard_arrow_down',
color: theme.colorScheme.onSurface,
size: 20,
),
items: sortOptions.map((String option) {
return DropdownMenuItem<String>(
value: option,
child: Text(
option,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
HapticFeedback.lightImpact();
onSortChanged(newValue);
}
},
),
),
),
],
),
);
}
}