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,265 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../../models/taxi_model.dart';
import '../../services/taxi_service.dart';
import '../../widgets/custom_app_bar.dart';
import '../../widgets/custom_bottom_bar.dart';
import './widgets/taxi_card_widget.dart';
import './widgets/taxi_empty_state_widget.dart';
import './widgets/taxi_filters_widget.dart';
/// Main taxi screen with filtering, search, and favorites functionality
class TaxiScreen extends StatefulWidget {
const TaxiScreen({super.key});
@override
State<TaxiScreen> createState() => _TaxiScreenState();
}
class _TaxiScreenState extends State<TaxiScreen> {
final TaxiService _taxiService = TaxiService.instance;
Timer? _searchDebouncer;
// State variables
List<String> _corregimientos = [];
List<String> _shifts = [];
List<TaxiModel> _taxis = [];
Set<String> _favoriteTaxiIds = {};
String? _selectedCorregimiento;
String? _selectedShift;
String _searchText = '';
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadInitialData();
}
@override
void dispose() {
_searchDebouncer?.cancel();
super.dispose();
}
/// Load corregimientos, shifts and initial taxi data
Future<void> _loadInitialData() async {
try {
setState(() {
_isLoading = true;
_error = null;
});
// Load corregimientos, shifts and favorites
final results = await Future.wait([
_taxiService.getCorregimientos(),
_taxiService.getFavoriteTaxiIds(),
]);
final corregimientos = results[0];
final favoriteIds = results[1];
setState(() {
_corregimientos = corregimientos;
_shifts = _taxiService.getShifts();
_favoriteTaxiIds = favoriteIds.toSet();
_isLoading = false;
});
// Load taxis for all corregimientos initially
await _searchTaxis();
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
/// Search taxis with current filters
Future<void> _searchTaxis() async {
try {
final taxis = await _taxiService.searchTaxis(
selectedCorregimiento: _selectedCorregimiento,
selectedShift: _selectedShift,
searchText: _searchText.isEmpty ? null : _searchText,
);
setState(() {
_taxis = taxis;
_error = null;
});
} catch (e) {
setState(() {
_error = e.toString();
});
}
}
/// Handle corregimiento selection change
void _onCorregimientoChanged(String? corregimiento) {
setState(() {
_selectedCorregimiento = corregimiento;
});
_searchTaxis();
}
/// Handle shift selection change
void _onShiftChanged(String? shift) {
setState(() {
_selectedShift = shift;
});
_searchTaxis();
}
/// Handle search text change with debouncing
void _onSearchChanged(String text) {
setState(() {
_searchText = text;
});
// Cancel previous timer
_searchDebouncer?.cancel();
// Start new timer for debounced search
_searchDebouncer = Timer(const Duration(milliseconds: 300), () {
_searchTaxis();
});
}
/// Clear all filters
void _onClearFilters() {
setState(() {
_selectedCorregimiento = null;
_selectedShift = null;
_searchText = '';
});
_searchTaxis();
}
/// Toggle favorite status for a taxi
Future<void> _onFavoriteToggle(String taxiId) async {
try {
final isFavorite = await _taxiService.toggleFavorite(taxiId);
setState(() {
if (isFavorite) {
_favoriteTaxiIds.add(taxiId);
} else {
_favoriteTaxiIds.remove(taxiId);
}
});
// Show feedback
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
isFavorite
? 'Taxi agregado a favoritos'
: 'Taxi removido de favoritos',
),
duration: const Duration(seconds: 2),
backgroundColor: isFavorite ? Colors.green : Colors.orange,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error al actualizar favoritos: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
/// Handle pull to refresh
Future<void> _onRefresh() async {
await _loadInitialData();
}
/// Get current empty state widget based on context
Widget _getEmptyStateWidget() {
if (_error != null) {
return TaxiEmptyStateWidget.error(
error: _error!,
onRetry: _loadInitialData,
);
}
if (_selectedCorregimiento == null &&
_selectedShift == null &&
_searchText.isEmpty) {
return TaxiEmptyStateWidget.noFiltersSelected();
}
return TaxiEmptyStateWidget.noResultsFound(onClearFilters: _onClearFilters);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: theme.colorScheme.surface,
appBar: const CustomAppBar(
title: 'Taxi Directory',
backgroundColor: Color(0xFF101820),
foregroundColor: Color(0xFFFEE715),
),
body: Column(
children: [
// Filters Section
TaxiFiltersWidget(
corregimientos: _corregimientos,
shifts: _shifts,
selectedCorregimiento: _selectedCorregimiento,
selectedShift: _selectedShift,
searchText: _searchText,
onCorregimientoChanged: _onCorregimientoChanged,
onShiftChanged: _onShiftChanged,
onSearchChanged: _onSearchChanged,
onClearFilters: _onClearFilters,
),
// Results Section
Expanded(
child:
_isLoading
? const Center(child: CircularProgressIndicator())
: _taxis.isEmpty
? _getEmptyStateWidget()
: RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.builder(
itemCount: _taxis.length,
itemBuilder: (context, index) {
final taxi = _taxis[index];
return TaxiCardWidget(
taxi: taxi,
isFavorite: _favoriteTaxiIds.contains(taxi.id),
onFavoriteToggle: _onFavoriteToggle,
);
},
),
),
),
],
),
bottomNavigationBar: CustomBottomBar(
currentIndex: 3, // Taxi tab index
onTap: _onBottomNavTap,
),
);
}
/// Handle bottom navigation tap
void _onBottomNavTap(int index) {
// Navigation is handled by CustomBottomBar
}
}

View File

@ -0,0 +1,159 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../models/taxi_model.dart';
/// Individual taxi card widget displaying taxi information with call and favorite functionality
class TaxiCardWidget extends StatelessWidget {
final TaxiModel taxi;
final bool isFavorite;
final Function(String) onFavoriteToggle;
const TaxiCardWidget({
super.key,
required this.taxi,
required this.isFavorite,
required this.onFavoriteToggle,
});
/// Launch phone call
Future<void> _makePhoneCall(String phoneNumber) async {
final Uri launchUri = Uri(scheme: 'tel', path: phoneNumber);
try {
if (await canLaunchUrl(launchUri)) {
await launchUrl(launchUri);
}
} catch (e) {
// Handle error silently or show user feedback
}
}
/// Capitalize first letter of shift for display
String _formatShift(String shift) {
if (shift.isEmpty) return '';
return shift[0].toUpperCase() + shift.substring(1);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Row: Name and Favorite Button
Row(
children: [
Expanded(
child: Text(
taxi.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: const Color(0xFF101820),
),
),
),
IconButton(
onPressed: () => onFavoriteToggle(taxi.id),
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color:
isFavorite
? const Color(0xFFFEE715)
: theme.iconTheme.color,
),
tooltip:
isFavorite ? 'Remove from favorites' : 'Add to favorites',
),
],
),
const SizedBox(height: 8),
// Phone Row with Call Button
Row(
children: [
Icon(Icons.phone, size: 20, color: theme.iconTheme.color),
const SizedBox(width: 8),
Expanded(
child: Text(
taxi.phone,
style: theme.textTheme.bodyMedium?.copyWith(
color: const Color(0xFF101820),
),
),
),
ElevatedButton.icon(
onPressed: () => _makePhoneCall(taxi.phone),
icon: const Icon(Icons.call, size: 18),
label: const Text('Call'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFEE715),
foregroundColor: const Color(0xFF101820),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
),
const SizedBox(height: 8),
// Location and Shift Info
Row(
children: [
// Corregimiento
Expanded(
child: Row(
children: [
Icon(
Icons.location_on,
size: 16,
color: theme.iconTheme.color?.withAlpha(153),
),
const SizedBox(width: 4),
Text(
taxi.corregimiento,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.textTheme.bodySmall?.color?.withAlpha(
153,
),
),
),
],
),
),
// Shift
Row(
children: [
Icon(
Icons.schedule,
size: 16,
color: theme.iconTheme.color?.withAlpha(153),
),
const SizedBox(width: 4),
Text(
_formatShift(taxi.shift),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.textTheme.bodySmall?.color?.withAlpha(153),
),
),
],
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
/// Widget for displaying various empty states in the taxi screen
class TaxiEmptyStateWidget extends StatelessWidget {
final String title;
final String message;
final IconData icon;
final String? buttonText;
final VoidCallback? onButtonPressed;
const TaxiEmptyStateWidget({
super.key,
required this.title,
required this.message,
required this.icon,
this.buttonText,
this.onButtonPressed,
});
/// Empty state when no filters are selected
factory TaxiEmptyStateWidget.noFiltersSelected() {
return const TaxiEmptyStateWidget(
title: 'Select Filters',
message: 'Choose a corregimiento and/or shift to see available taxis.',
icon: Icons.filter_list,
);
}
/// Empty state when no results match the current filters
factory TaxiEmptyStateWidget.noResultsFound({
required VoidCallback onClearFilters,
}) {
return TaxiEmptyStateWidget(
title: 'No taxis available for this selection.',
message: 'Try adjusting your filters or search terms.',
icon: Icons.search_off,
buttonText: 'Clear Filters',
onButtonPressed: onClearFilters,
);
}
/// Empty state when there's no data at all
factory TaxiEmptyStateWidget.noData() {
return const TaxiEmptyStateWidget(
title: 'No taxis registered yet.',
message: 'There are no taxi services available at the moment.',
icon: Icons.local_taxi,
);
}
/// Empty state for error conditions
factory TaxiEmptyStateWidget.error({
required String error,
required VoidCallback onRetry,
}) {
return TaxiEmptyStateWidget(
title: 'Error Loading Taxis',
message: error,
icon: Icons.error_outline,
buttonText: 'Retry',
onButtonPressed: onRetry,
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 80, color: theme.iconTheme.color?.withAlpha(128)),
const SizedBox(height: 24),
Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
color: const Color(0xFF101820),
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
message,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodyMedium?.color?.withAlpha(153),
),
textAlign: TextAlign.center,
),
if (buttonText != null && onButtonPressed != null) ...[
const SizedBox(height: 24),
ElevatedButton(
onPressed: onButtonPressed,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFEE715),
foregroundColor: const Color(0xFF101820),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(buttonText!),
),
],
],
),
),
);
}
}

View File

@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
/// Widget for taxi search filters including corregimiento dropdown, shift dropdown and search input
class TaxiFiltersWidget extends StatefulWidget {
final List<String> corregimientos; // Changed from districts
final List<String> shifts; // Added shifts list
final String? selectedCorregimiento; // Changed from selectedDistrict
final String? selectedShift; // Added shift selection
final String searchText;
final Function(String?)
onCorregimientoChanged; // Changed from onDistrictChanged
final Function(String?) onShiftChanged; // Added shift handler
final Function(String) onSearchChanged;
final VoidCallback onClearFilters;
const TaxiFiltersWidget({
super.key,
required this.corregimientos, // Changed from districts
required this.shifts, // Added shifts
required this.selectedCorregimiento, // Changed from selectedDistrict
required this.selectedShift, // Added shift selection
required this.searchText,
required this.onCorregimientoChanged, // Changed from onDistrictChanged
required this.onShiftChanged, // Added shift handler
required this.onSearchChanged,
required this.onClearFilters,
});
@override
State<TaxiFiltersWidget> createState() => _TaxiFiltersWidgetState();
}
class _TaxiFiltersWidgetState extends State<TaxiFiltersWidget> {
late TextEditingController _searchController;
@override
void initState() {
super.initState();
_searchController = TextEditingController(text: widget.searchText);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
boxShadow: [
BoxShadow(
color: theme.shadowColor.withAlpha(26),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
// Filter Dropdowns Row
Row(
children: [
// Corregimiento Dropdown
Expanded(
child: DropdownButtonFormField<String?>(
value:
widget
.selectedCorregimiento, // Changed from selectedDistrict
hint: const Text(
'Seleccionar Corregimiento',
), // Updated hint text
decoration: InputDecoration(
labelText:
'Filter by Corregimiento', // Updated label as requested
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('Todos los corregimientos'), // Updated text
),
...widget.corregimientos.map(
// Changed from districts
(corregimiento) => DropdownMenuItem<String?>(
value: corregimiento,
child: Text(corregimiento),
),
),
],
onChanged:
widget
.onCorregimientoChanged, // Changed from onDistrictChanged
),
),
const SizedBox(width: 12),
// Shift Dropdown
Expanded(
child: DropdownButtonFormField<String?>(
value: widget.selectedShift,
hint: const Text('Seleccionar Turno'),
decoration: InputDecoration(
labelText: 'Filter by Shift', // Label as requested
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('Todos los turnos'),
),
...widget.shifts.map(
(shift) => DropdownMenuItem<String?>(
value: shift,
child: Text(shift),
),
),
],
onChanged: widget.onShiftChanged,
),
),
if (widget.selectedCorregimiento !=
null || // Changed from selectedDistrict
widget.selectedShift != null || // Added shift condition
widget.searchText.isNotEmpty) ...[
const SizedBox(width: 8),
IconButton(
onPressed: widget.onClearFilters,
icon: const Icon(Icons.clear),
tooltip: 'Clear Filters', // Updated tooltip text as requested
style: IconButton.styleFrom(
backgroundColor: theme.colorScheme.errorContainer,
foregroundColor: theme.colorScheme.onErrorContainer,
),
),
],
],
),
const SizedBox(height: 12),
// Search Input
TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'Buscar taxi o teléfono',
hintText: 'Escribe el nombre o número...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixIcon:
widget.searchText.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
widget.onSearchChanged('');
},
icon: const Icon(Icons.clear),
tooltip: 'Limpiar búsqueda',
)
: null,
),
onChanged: widget.onSearchChanged,
textInputAction: TextInputAction.search,
),
],
),
);
}
}