feat: implement staff availability, clock-in, payments and fix UI navigation
This commit is contained in:
@@ -1,93 +1,75 @@
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../domain/repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Implementation of [ClockInRepositoryInterface].
|
||||
/// Implementation of [ClockInRepositoryInterface] using Mock Data.
|
||||
///
|
||||
/// Delegates shift data retrieval to [ShiftsRepositoryMock] and manages purely
|
||||
/// local state for attendance (check-in/out) for the prototype phase.
|
||||
/// This implementation uses hardcoded data to match the prototype UI.
|
||||
class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
final ShiftsRepositoryMock _shiftsMock;
|
||||
|
||||
// Local state for the session (mocking backend state)
|
||||
ClockInRepositoryImpl();
|
||||
|
||||
// Local state for the mock implementation
|
||||
bool _isCheckedIn = false;
|
||||
DateTime? _checkInTime;
|
||||
DateTime? _checkOutTime;
|
||||
String? _activeShiftId;
|
||||
|
||||
ClockInRepositoryImpl({ShiftsRepositoryMock? shiftsMock})
|
||||
: _shiftsMock = shiftsMock ?? ShiftsRepositoryMock();
|
||||
|
||||
@override
|
||||
Future<Shift?> getTodaysShift() async {
|
||||
final shifts = await _shiftsMock.getMyShifts();
|
||||
|
||||
if (shifts.isEmpty) return null;
|
||||
// Simulate network delay
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final now = DateTime.now();
|
||||
final todayStr = DateFormat('yyyy-MM-dd').format(now);
|
||||
|
||||
// Find a shift effectively for today, or mock one
|
||||
try {
|
||||
return shifts.firstWhere((s) => s.date == todayStr);
|
||||
} catch (_) {
|
||||
final original = shifts.first;
|
||||
// Mock "today's" shift based on the first available shift
|
||||
return Shift(
|
||||
id: original.id,
|
||||
title: original.title,
|
||||
clientName: original.clientName,
|
||||
logoUrl: original.logoUrl,
|
||||
hourlyRate: original.hourlyRate,
|
||||
location: original.location,
|
||||
locationAddress: original.locationAddress,
|
||||
date: todayStr,
|
||||
startTime: original.startTime, // Use original times or calculate
|
||||
endTime: original.endTime,
|
||||
createdDate: original.createdDate,
|
||||
status: 'assigned',
|
||||
latitude: original.latitude,
|
||||
longitude: original.longitude,
|
||||
description: original.description,
|
||||
managers: original.managers,
|
||||
);
|
||||
}
|
||||
// Mock Shift matching the prototype
|
||||
return Shift(
|
||||
id: '1',
|
||||
title: 'Warehouse Assistant',
|
||||
clientName: 'Amazon Warehouse',
|
||||
logoUrl:
|
||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Amazon_2024.svg/500px-Amazon_2024.svg.png',
|
||||
hourlyRate: 22.50,
|
||||
location: 'San Francisco, CA',
|
||||
locationAddress: '123 Market St, San Francisco, CA 94105',
|
||||
date: DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||
startTime: '09:00',
|
||||
endTime: '17:00',
|
||||
createdDate: DateTime.now().subtract(const Duration(days: 2)).toIso8601String(),
|
||||
status: 'assigned',
|
||||
description: 'General warehouse duties including packing and sorting.',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getAttendanceStatus() async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
return _getCurrentStatusMap();
|
||||
return {
|
||||
'isCheckedIn': _isCheckedIn,
|
||||
'checkInTime': _checkInTime,
|
||||
'checkOutTime': _checkOutTime,
|
||||
'activeShiftId': '1',
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> clockIn({required String shiftId, String? notes}) async {
|
||||
await Future.delayed(const Duration(seconds: 1)); // Simulate network
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
_isCheckedIn = true;
|
||||
_checkInTime = DateTime.now();
|
||||
_activeShiftId = shiftId;
|
||||
_checkOutTime = null; // Reset for new check-in? Or keep for history?
|
||||
// Simple mock logic: reset check-out on new check-in.
|
||||
|
||||
return _getCurrentStatusMap();
|
||||
|
||||
return getAttendanceStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> clockOut({String? notes, int? breakTimeMinutes}) async {
|
||||
await Future.delayed(const Duration(seconds: 1)); // Simulate network
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
_isCheckedIn = false;
|
||||
_checkOutTime = DateTime.now();
|
||||
|
||||
return _getCurrentStatusMap();
|
||||
return getAttendanceStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getActivityLog() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
// Mock data
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
return [
|
||||
{
|
||||
'date': DateTime.now().subtract(const Duration(days: 1)),
|
||||
@@ -101,15 +83,13 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
'end': '05:00 PM',
|
||||
'hours': '8h',
|
||||
},
|
||||
{
|
||||
'date': DateTime.now().subtract(const Duration(days: 3)),
|
||||
'start': '09:00 AM',
|
||||
'end': '05:00 PM',
|
||||
'hours': '8h',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getCurrentStatusMap() {
|
||||
return {
|
||||
'isCheckedIn': _isCheckedIn,
|
||||
'checkInTime': _checkInTime,
|
||||
'checkOutTime': _checkOutTime,
|
||||
'activeShiftId': _activeShiftId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
// --- State ---
|
||||
class ClockInState extends Equatable {
|
||||
final bool isLoading;
|
||||
final bool isLocationVerified;
|
||||
final String? error;
|
||||
final Position? currentLocation;
|
||||
final double? distanceFromVenue;
|
||||
final bool isClockedIn;
|
||||
final DateTime? clockInTime;
|
||||
|
||||
const ClockInState({
|
||||
this.isLoading = false,
|
||||
this.isLocationVerified = false,
|
||||
this.error,
|
||||
this.currentLocation,
|
||||
this.distanceFromVenue,
|
||||
this.isClockedIn = false,
|
||||
this.clockInTime,
|
||||
});
|
||||
|
||||
ClockInState copyWith({
|
||||
bool? isLoading,
|
||||
bool? isLocationVerified,
|
||||
String? error,
|
||||
Position? currentLocation,
|
||||
double? distanceFromVenue,
|
||||
bool? isClockedIn,
|
||||
DateTime? clockInTime,
|
||||
}) {
|
||||
return ClockInState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isLocationVerified: isLocationVerified ?? this.isLocationVerified,
|
||||
error: error, // Clear error if not provided
|
||||
currentLocation: currentLocation ?? this.currentLocation,
|
||||
distanceFromVenue: distanceFromVenue ?? this.distanceFromVenue,
|
||||
isClockedIn: isClockedIn ?? this.isClockedIn,
|
||||
clockInTime: clockInTime ?? this.clockInTime,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
isLoading,
|
||||
isLocationVerified,
|
||||
error,
|
||||
currentLocation,
|
||||
distanceFromVenue,
|
||||
isClockedIn,
|
||||
clockInTime,
|
||||
];
|
||||
}
|
||||
|
||||
// --- Cubit ---
|
||||
class ClockInCubit extends Cubit<ClockInState> {
|
||||
// Mock Venue Location (e.g., Grand Hotel, NYC)
|
||||
static const double venueLat = 40.7128;
|
||||
static const double venueLng = -74.0060;
|
||||
static const double allowedRadiusMeters = 500; // 500m radius
|
||||
|
||||
ClockInCubit() : super(const ClockInState());
|
||||
|
||||
Future<void> checkLocationPermission() async {
|
||||
emit(state.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Location permissions are denied',
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Location permissions are permanently denied, we cannot request permissions.',
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
_getCurrentLocation();
|
||||
} catch (e) {
|
||||
emit(state.copyWith(isLoading: false, error: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getCurrentLocation() async {
|
||||
try {
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
);
|
||||
|
||||
final distance = Geolocator.distanceBetween(
|
||||
position.latitude,
|
||||
position.longitude,
|
||||
venueLat,
|
||||
venueLng,
|
||||
);
|
||||
|
||||
final isWithinRadius = distance <= allowedRadiusMeters;
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
currentLocation: position,
|
||||
distanceFromVenue: distance,
|
||||
isLocationVerified: isWithinRadius,
|
||||
error: isWithinRadius ? null : 'You are ${distance.toStringAsFixed(0)}m away. You must be within ${allowedRadiusMeters}m.',
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(isLoading: false, error: 'Failed to get location: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clockIn() async {
|
||||
if (state.currentLocation == null) {
|
||||
await checkLocationPermission();
|
||||
if (state.currentLocation == null) return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
isClockedIn: true,
|
||||
clockInTime: DateTime.now(),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> clockOut() async {
|
||||
if (state.currentLocation == null) {
|
||||
await checkLocationPermission();
|
||||
if (state.currentLocation == null) return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
isClockedIn: false,
|
||||
clockInTime: null,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
: '--:-- --';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -96,7 +97,6 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
distanceMeters: 500, // Mock value for demo
|
||||
etaMinutes: 8, // Mock value for demo
|
||||
),
|
||||
|
||||
// Date Selector
|
||||
DateSelector(
|
||||
selectedDate: state.selectedDate,
|
||||
@@ -149,12 +149,15 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
AttendanceCard(
|
||||
type: AttendanceType.breaks,
|
||||
title: "Break Time",
|
||||
// TODO: Connect to Data Connect when 'breakDuration' field is added to Shift schema.
|
||||
value: "00:30 min",
|
||||
subtitle: "Scheduled 00:30 min",
|
||||
),
|
||||
const AttendanceCard(
|
||||
type: AttendanceType.days,
|
||||
title: "Total Days",
|
||||
// TODO: Connect to Data Connect when 'staffStats' or similar aggregation API is available.
|
||||
// Currently avoided to prevent fetching full shift history for a simple count.
|
||||
value: "28",
|
||||
subtitle: "Working Days",
|
||||
),
|
||||
@@ -162,6 +165,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Your Activity Header
|
||||
// Your Activity Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -178,15 +182,17 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
onTap: () {
|
||||
debugPrint('Navigating to shifts...');
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
child: Row(
|
||||
children: const [
|
||||
Text(
|
||||
"View all",
|
||||
style: TextStyle(
|
||||
color: AppColors.krowBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 16,
|
||||
@@ -221,7 +227,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
child: Row(
|
||||
children: [
|
||||
_buildModeTab("Swipe", LucideIcons.mapPin, 'swipe', state.checkInMode),
|
||||
_buildModeTab("NFC Tap", LucideIcons.wifi, 'nfc', state.checkInMode),
|
||||
// _buildModeTab("NFC Tap", LucideIcons.wifi, 'nfc', state.checkInMode),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -467,7 +473,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Recent Activity List
|
||||
...state.activityLog.map(
|
||||
if (state.activityLog.isNotEmpty) ...state.activityLog.map(
|
||||
(activity) => Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
@@ -530,11 +536,12 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -24,7 +24,7 @@ class AttendanceCard extends StatelessWidget {
|
||||
final styles = _getStyles(type);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -39,31 +39,37 @@ class AttendanceCard extends StatelessWidget {
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: styles.bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(styles.icon, size: 16, color: styles.iconColor),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: 11,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
const SizedBox(height: 2),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
),
|
||||
),
|
||||
if (scheduledTime != null) ...[
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class LocationMapPlaceholder extends StatelessWidget {
|
||||
final bool isVerified;
|
||||
final double? distance;
|
||||
|
||||
const LocationMapPlaceholder({
|
||||
super.key,
|
||||
required this.isVerified,
|
||||
this.distance,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE2E8F0),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
image: DecorationImage(
|
||||
image: const NetworkImage(
|
||||
'https://maps.googleapis.com/maps/api/staticmap?center=40.7128,-74.0060&zoom=15&size=600x300&maptype=roadmap&markers=color:red%7C40.7128,-74.0060&key=YOUR_API_KEY',
|
||||
),
|
||||
// In a real app with keys, this would verify visually.
|
||||
// For now we use a generic placeholder color/icon to avoid broken images.
|
||||
fit: BoxFit.cover,
|
||||
onError: (_, __) {},
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Fallback UI if image fails (which it will without key)
|
||||
const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(LucideIcons.mapPin, size: 48, color: UiColors.iconSecondary),
|
||||
SizedBox(height: 8),
|
||||
Text('Map View (GPS)', style: TextStyle(color: UiColors.textSecondary)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Status Overlay
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isVerified ? LucideIcons.checkCircle : LucideIcons.alertCircle,
|
||||
color: isVerified ? UiColors.textSuccess : UiColors.destructive,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isVerified ? 'Location Verified' : 'Location Check',
|
||||
style: UiTypography.body1b.copyWith(color: UiColors.textPrimary),
|
||||
),
|
||||
if (distance != null)
|
||||
Text(
|
||||
'${distance!.toStringAsFixed(0)}m from venue',
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -106,26 +106,28 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_tookLunch = false;
|
||||
_step = 102; // Go to No Lunch Reason
|
||||
});
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
shape: RoundedRectangleBorder(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"No",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF121826),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
"No",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF121826),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -180,19 +182,27 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
isExpanded: true,
|
||||
value: _breakStart,
|
||||
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(),
|
||||
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
|
||||
onChanged: (v) => setState(() => _breakStart = v),
|
||||
decoration: const InputDecoration(labelText: 'Start'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Start',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
isExpanded: true,
|
||||
value: _breakEnd,
|
||||
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(),
|
||||
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
|
||||
onChanged: (v) => setState(() => _breakEnd = v),
|
||||
decoration: const InputDecoration(labelText: 'End'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'End',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@ publish_to: 'none'
|
||||
resolution: workspace
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
sdk: '^3.10.7'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
@@ -28,3 +28,4 @@ dependencies:
|
||||
path: ../../../data_connect
|
||||
krow_core:
|
||||
path: ../../../core
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
|
||||
Reference in New Issue
Block a user