Enhance UI button and order card features

Added fullWidth support to UiButton and updated usages to ensure buttons span container width. Improved ViewOrderCard with expanded worker details, avatar stack, formatted date/time, and an order edit bottom sheet. Introduced new icons and typography style in the design system.
This commit is contained in:
Achintha Isuru
2026-01-23 11:53:36 -05:00
parent 960b21ec8c
commit 868688fb02
7 changed files with 543 additions and 100 deletions

View File

@@ -163,6 +163,12 @@ class UiIcons {
/// Eye off icon for hidden visibility /// Eye off icon for hidden visibility
static const IconData eyeOff = _IconLib.eyeOff; static const IconData eyeOff = _IconLib.eyeOff;
/// Phone icon for calls
static const IconData phone = _IconLib.phone;
/// Message circle icon for chat
static const IconData messageCircle = _IconLib.messageCircle;
/// Building icon for companies /// Building icon for companies
static const IconData building = _IconLib.building2; static const IconData building = _IconLib.building2;

View File

@@ -12,8 +12,8 @@ class UiTheme {
/// Returns the light theme for the Staff application. /// Returns the light theme for the Staff application.
static ThemeData get light { static ThemeData get light {
final colorScheme = UiColors.colorScheme; final ColorScheme colorScheme = UiColors.colorScheme;
final textTheme = UiTypography.textTheme; final TextTheme textTheme = UiTypography.textTheme;
return ThemeData( return ThemeData(
useMaterial3: true, useMaterial3: true,
@@ -68,7 +68,6 @@ class UiTheme {
horizontal: UiConstants.space6, horizontal: UiConstants.space6,
vertical: UiConstants.space3, vertical: UiConstants.space3,
), ),
minimumSize: const Size(double.infinity, 54),
maximumSize: const Size(double.infinity, 54), maximumSize: const Size(double.infinity, 54),
).copyWith( ).copyWith(
side: WidgetStateProperty.resolveWith<BorderSide?>((states) { side: WidgetStateProperty.resolveWith<BorderSide?>((states) {
@@ -99,7 +98,6 @@ class UiTheme {
horizontal: UiConstants.space4, horizontal: UiConstants.space4,
vertical: UiConstants.space2, vertical: UiConstants.space2,
), ),
minimumSize: const Size(double.infinity, 52),
maximumSize: const Size(double.infinity, 52), maximumSize: const Size(double.infinity, 52),
), ),
), ),
@@ -117,7 +115,6 @@ class UiTheme {
horizontal: UiConstants.space4, horizontal: UiConstants.space4,
vertical: UiConstants.space3, vertical: UiConstants.space3,
), ),
minimumSize: const Size(double.infinity, 52),
maximumSize: const Size(double.infinity, 52), maximumSize: const Size(double.infinity, 52),
), ),
), ),

View File

@@ -320,6 +320,15 @@ class UiTypography {
color: UiColors.textPrimary, color: UiColors.textPrimary,
); );
/// Body 3 Medium - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: -0.1 (#121826)
static final TextStyle body3m = _primaryBase.copyWith(
fontWeight: FontWeight.w500,
fontSize: 12,
height: 1.5,
letterSpacing: -0.1,
color: UiColors.textPrimary,
);
/// Body 4 Regular - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: 0.05 (#121826) /// Body 4 Regular - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: 0.05 (#121826)
static final TextStyle body4r = _primaryBase.copyWith( static final TextStyle body4r = _primaryBase.copyWith(
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,

View File

@@ -27,6 +27,9 @@ class UiButton extends StatelessWidget {
/// The size of the button. /// The size of the button.
final UiButtonSize size; final UiButtonSize size;
/// Whether the button should take up the full width of its container.
final bool fullWidth;
/// The button widget to use (ElevatedButton, OutlinedButton, or TextButton). /// The button widget to use (ElevatedButton, OutlinedButton, or TextButton).
final Widget Function( final Widget Function(
BuildContext context, BuildContext context,
@@ -48,6 +51,7 @@ class UiButton extends StatelessWidget {
this.style, this.style,
this.iconSize = 20, this.iconSize = 20,
this.size = UiButtonSize.medium, this.size = UiButtonSize.medium,
this.fullWidth = false,
}) : assert( }) : assert(
text != null || child != null, text != null || child != null,
'Either text or child must be provided', 'Either text or child must be provided',
@@ -64,6 +68,7 @@ class UiButton extends StatelessWidget {
this.style, this.style,
this.iconSize = 20, this.iconSize = 20,
this.size = UiButtonSize.medium, this.size = UiButtonSize.medium,
this.fullWidth = false,
}) : buttonBuilder = _elevatedButtonBuilder, }) : buttonBuilder = _elevatedButtonBuilder,
assert( assert(
text != null || child != null, text != null || child != null,
@@ -81,6 +86,7 @@ class UiButton extends StatelessWidget {
this.style, this.style,
this.iconSize = 20, this.iconSize = 20,
this.size = UiButtonSize.medium, this.size = UiButtonSize.medium,
this.fullWidth = false,
}) : buttonBuilder = _outlinedButtonBuilder, }) : buttonBuilder = _outlinedButtonBuilder,
assert( assert(
text != null || child != null, text != null || child != null,
@@ -98,6 +104,25 @@ class UiButton extends StatelessWidget {
this.style, this.style,
this.iconSize = 20, this.iconSize = 20,
this.size = UiButtonSize.medium, this.size = UiButtonSize.medium,
this.fullWidth = false,
}) : buttonBuilder = _textButtonBuilder,
assert(
text != null || child != null,
'Either text or child must be provided',
);
/// Creates a ghost button (transparent background).
UiButton.ghost({
super.key,
this.text,
this.child,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.medium,
this.fullWidth = false,
}) : buttonBuilder = _textButtonBuilder, }) : buttonBuilder = _textButtonBuilder,
assert( assert(
text != null || child != null, text != null || child != null,
@@ -107,7 +132,18 @@ class UiButton extends StatelessWidget {
@override @override
/// Builds the button UI. /// Builds the button UI.
Widget build(BuildContext context) { Widget build(BuildContext context) {
return buttonBuilder(context, onPressed, style, _buildButtonContent()); final Widget button = buttonBuilder(
context,
onPressed,
style,
_buildButtonContent(),
);
if (fullWidth) {
return SizedBox(width: double.infinity, child: button);
}
return button;
} }
/// Builds the button content with optional leading and trailing icons. /// Builds the button content with optional leading and trailing icons.
@@ -116,27 +152,40 @@ class UiButton extends StatelessWidget {
return child!; return child!;
} }
// Single icon or text case final String buttonText = text ?? '';
// Optimization: If no icons, return plain text to avoid Row layout overhead
if (leadingIcon == null && trailingIcon == null) { if (leadingIcon == null && trailingIcon == null) {
return Text(text!); return Text(buttonText, textAlign: TextAlign.center);
} }
if (leadingIcon != null && text == null && trailingIcon == null) { // Multiple elements case: Use a Row with MainAxisSize.min
return Icon(leadingIcon, size: iconSize);
}
// Multiple elements case
final List<Widget> children = []; final List<Widget> children = [];
if (leadingIcon != null) { if (leadingIcon != null) {
children.add(Icon(leadingIcon, size: iconSize)); children.add(Icon(leadingIcon, size: iconSize));
children.add(const SizedBox(width: UiConstants.space2));
} }
children.add(Text(text!)); if (buttonText.isNotEmpty) {
if (leadingIcon != null) {
children.add(const SizedBox(width: UiConstants.space2));
}
// Use flexible to ensure text doesn't force infinite width in flex parents
children.add(
Flexible(
child: Text(
buttonText,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
);
}
if (trailingIcon != null) { if (trailingIcon != null) {
children.add(const SizedBox(width: UiConstants.space2)); if (buttonText.isNotEmpty || leadingIcon != null) {
children.add(const SizedBox(width: UiConstants.space2));
}
children.add(Icon(trailingIcon, size: iconSize)); children.add(Icon(trailingIcon, size: iconSize));
} }

View File

@@ -97,6 +97,7 @@ class ClientGetStartedPage extends StatelessWidget {
.get_started_page .get_started_page
.sign_in_button, .sign_in_button,
onPressed: () => Modular.to.pushClientSignIn(), onPressed: () => Modular.to.pushClientSignIn(),
fullWidth: true,
), ),
const SizedBox(height: UiConstants.space3), const SizedBox(height: UiConstants.space3),
@@ -108,6 +109,7 @@ class ClientGetStartedPage extends StatelessWidget {
.get_started_page .get_started_page
.create_account_button, .create_account_button,
onPressed: () => Modular.to.pushClientSignUp(), onPressed: () => Modular.to.pushClientSignUp(),
fullWidth: true,
), ),
], ],
), ),

View File

@@ -96,6 +96,7 @@ class _ClientSignInFormState extends State<ClientSignInForm> {
UiButton.primary( UiButton.primary(
text: widget.isLoading ? null : i18n.sign_in_button, text: widget.isLoading ? null : i18n.sign_in_button,
onPressed: widget.isLoading ? null : _handleSubmit, onPressed: widget.isLoading ? null : _handleSubmit,
fullWidth: true,
child: widget.isLoading child: widget.isLoading
? const SizedBox( ? const SizedBox(
height: 24, height: 24,

View File

@@ -20,7 +20,16 @@ class ViewOrderCard extends StatefulWidget {
} }
class _ViewOrderCardState extends State<ViewOrderCard> { class _ViewOrderCardState extends State<ViewOrderCard> {
bool _expanded = false; bool _expanded = true;
void _openEditSheet({required OrderItem order}) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) => _OrderEditSheet(order: order),
);
}
/// Returns the semantic color for the given status. /// Returns the semantic color for the given status.
Color _getStatusColor({required String status}) { Color _getStatusColor({required String status}) {
@@ -65,6 +74,13 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
String _formatDate({required String dateStr}) { String _formatDate({required String dateStr}) {
try { try {
final DateTime date = DateTime.parse(dateStr); final DateTime date = DateTime.parse(dateStr);
final DateTime now = DateTime.now();
final DateTime today = DateTime(now.year, now.month, now.day);
final DateTime tomorrow = today.add(const Duration(days: 1));
final DateTime checkDate = DateTime(date.year, date.month, date.day);
if (checkDate == today) return 'Today';
if (checkDate == tomorrow) return 'Tomorrow';
return DateFormat('EEE, MMM d').format(date); return DateFormat('EEE, MMM d').format(date);
} catch (_) { } catch (_) {
return dateStr; return dateStr;
@@ -73,7 +89,18 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
/// Formats the time string for display. /// Formats the time string for display.
String _formatTime({required String timeStr}) { String _formatTime({required String timeStr}) {
return timeStr; if (timeStr.isEmpty) return '';
try {
final List<String> parts = timeStr.split(':');
int hour = int.parse(parts[0]);
final int minute = int.parse(parts[1]);
final String ampm = hour >= 12 ? 'PM' : 'AM';
hour = hour % 12;
if (hour == 0) hour = 12;
return '$hour:${minute.toString().padLeft(2, '0')} $ampm';
} catch (_) {
return timeStr;
}
} }
@override @override
@@ -205,7 +232,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
const SizedBox(width: UiConstants.space1), const SizedBox(width: UiConstants.space1),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
// TODO: Get directions // TODO: Handle location
}, },
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
@@ -236,9 +263,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
icon: UiIcons.edit, icon: UiIcons.edit,
color: UiColors.primary, color: UiColors.primary,
bgColor: UiColors.tagInProgress, bgColor: UiColors.tagInProgress,
onTap: () { onTap: () => _openEditSheet(order: order),
// TODO: Open edit sheet
},
), ),
const SizedBox(width: UiConstants.space2), const SizedBox(width: UiConstants.space2),
_buildHeaderIconButton( _buildHeaderIconButton(
@@ -319,120 +344,129 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
const SizedBox(height: UiConstants.space3), const SizedBox(height: UiConstants.space3),
// Coverage Bar // Coverage Section
if (order.status != 'completed') ...<Widget>[ if (order.status != 'completed') ...<Widget>[
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Row( Row(
children: <Widget>[ children: <Widget>[
Text( const Icon(
t.client_view_orders.card.coverage, UiIcons.success,
style: UiTypography.footnote2b.copyWith( size: 16,
color: UiColors.textSecondary, color: UiColors.textSuccess,
),
), ),
const SizedBox(width: UiConstants.space1), const SizedBox(width: 6),
Container( Text(
padding: const EdgeInsets.symmetric( t.client_view_orders.card.workers_label(
horizontal: 4, filled: order.filled,
vertical: 1, needed: order.workersNeeded,
), ),
decoration: BoxDecoration( style: UiTypography.body2m.copyWith(
color: coveragePercent == 100 color: UiColors.textPrimary,
? UiColors.tagSuccess
: UiColors.tagInProgress,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'$coveragePercent%',
style: UiTypography.footnote2b.copyWith(
fontSize: 9,
color: coveragePercent == 100
? UiColors.textSuccess
: UiColors.primary,
),
), ),
), ),
], ],
), ),
Text( Text(
t.client_view_orders.card.workers_label( '$coveragePercent%',
filled: order.filled, style: UiTypography.body2b.copyWith(
needed: order.workersNeeded, color: UiColors.primary,
),
style: UiTypography.footnote2m.copyWith(
color: UiColors.textSecondary,
), ),
), ),
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 8),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: coveragePercent / 100, value: coveragePercent / 100,
backgroundColor: UiColors.separatorSecondary, backgroundColor: UiColors.separatorSecondary,
valueColor: AlwaysStoppedAnimation<Color>( valueColor: const AlwaysStoppedAnimation<Color>(
coveragePercent == 100 UiColors.primary,
? UiColors.textSuccess
: UiColors.primary,
), ),
minHeight: 4, minHeight: 8,
), ),
), ),
// Avatar Stack Preview (if not expanded)
if (!_expanded && order.confirmedApps.isNotEmpty) ...<Widget>[
const SizedBox(height: UiConstants.space3),
Row(
children: <Widget>[
_buildAvatarStack(order.confirmedApps),
if (order.confirmedApps.length > 3)
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'+${order.confirmedApps.length - 3} more',
style: UiTypography.footnote2r.copyWith(
color: UiColors.textSecondary,
),
),
),
],
),
],
], ],
], ],
), ),
), ),
// Worker Avatars and more details (Expanded section) // Assigned Workers (Expanded section)
if (_expanded) ...<Widget>[ if (_expanded && order.confirmedApps.isNotEmpty) ...<Widget>[
const Divider(height: 1, color: UiColors.separatorSecondary), Container(
Padding( decoration: const BoxDecoration(
color: UiColors.bgSecondary,
border: Border(
top: BorderSide(color: UiColors.separatorSecondary),
),
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(UiConstants.radiusBase),
),
),
padding: const EdgeInsets.all(UiConstants.space4), padding: const EdgeInsets.all(UiConstants.space4),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Row(
t.client_view_orders.card.confirmed_workers, mainAxisAlignment: MainAxisAlignment.spaceBetween,
style: UiTypography.body2b.copyWith( children: <Widget>[
color: UiColors.textPrimary, Text(
), t.client_view_orders.card.confirmed_workers,
style: UiTypography.footnote2b.copyWith(
color: UiColors.textSecondary,
),
),
UiButton.primary(
text: 'Message All',
leadingIcon: UiIcons.messageCircle,
size: UiButtonSize.small,
// style: ElevatedButton.styleFrom(
// minimumSize: const Size(0, 32),
// maximumSize: const Size(0, 32),
// ),
onPressed: () {
// TODO: Message all workers
},
),
],
), ),
const SizedBox(height: UiConstants.space3), const SizedBox(height: UiConstants.space3),
if (order.confirmedApps.isEmpty) ...order.confirmedApps
Text( .take(5)
t.client_view_orders.card.no_workers, .map((Map<String, dynamic> app) => _buildWorkerRow(app)),
style: UiTypography.body3r.copyWith( if (order.confirmedApps.length > 5)
color: UiColors.textInactive, Center(
child: TextButton(
onPressed: () => setState(() => _expanded = !_expanded),
child: Text(
'Show ${order.confirmedApps.length - 5} more workers',
style: UiTypography.body3m.copyWith(
color: UiColors.primary,
),
),
), ),
)
else
Wrap(
spacing: -8,
children: order.confirmedApps
.map(
(Map<String, dynamic> app) => Tooltip(
message: app['worker_name'] as String,
child: CircleAvatar(
radius: 14,
backgroundColor: UiColors.white,
child: CircleAvatar(
radius: 12,
backgroundColor: UiColors.bgSecondary,
child: Text(
(app['worker_name'] as String).substring(
0,
1,
),
style: UiTypography.footnote2b,
),
),
),
),
)
.toList(),
), ),
], ],
), ),
@@ -443,6 +477,163 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
); );
} }
/// Builds a stacked avatar UI for a list of applications.
Widget _buildAvatarStack(List<Map<String, dynamic>> apps) {
const double size = 28.0;
const double overlap = 20.0;
final int count = apps.length > 3 ? 3 : apps.length;
return SizedBox(
height: size,
width: size + (count - 1) * overlap,
child: Stack(
children: <Widget>[
for (int i = 0; i < count; i++)
Positioned(
left: i * overlap,
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: UiColors.white, width: 2),
color: UiColors.tagInProgress,
),
child: Center(
child: Text(
(apps[i]['worker_name'] as String)[0],
style: UiTypography.footnote2b.copyWith(
color: UiColors.primary,
),
),
),
),
),
],
),
);
}
/// Builds a detailed row for a worker.
Widget _buildWorkerRow(Map<String, dynamic> app) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
// Avatar
CircleAvatar(
radius: 18,
backgroundColor: UiColors.tagInProgress,
child: Text(
(app['worker_name'] as String)[0],
style: UiTypography.body2b.copyWith(
color: UiColors.primary,
),
),
),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
app['worker_name'] as String,
style: UiTypography.body2m.copyWith(
color: UiColors.textPrimary,
),
),
const SizedBox(height: 2),
Row(
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
),
decoration: BoxDecoration(
border: Border.all(
color: UiColors.separatorPrimary,
),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'⭐ 4.8',
style: UiTypography.titleUppercase4m.copyWith(
color: UiColors.textSecondary,
),
),
),
if (app['check_in_time'] != null) ...<Widget>[
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
),
decoration: BoxDecoration(
color: UiColors.tagSuccess,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Checked In',
style: UiTypography.titleUppercase4m.copyWith(
color: UiColors.textSuccess,
),
),
),
],
],
),
],
),
],
),
Row(
children: <Widget>[
_buildActionIconButton(icon: UiIcons.phone, onTap: () {}),
const SizedBox(width: 8),
_buildActionIconButton(
icon: UiIcons.messageCircle,
onTap: () {},
),
const SizedBox(width: 8),
const Icon(
UiIcons.success,
size: 20,
color: UiColors.textSuccess,
),
],
),
],
),
),
);
}
/// Specialized action button for worker rows.
Widget _buildActionIconButton({
required IconData icon,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 32,
height: 32,
decoration: const BoxDecoration(color: UiColors.transparent),
child: Icon(icon, size: 16, color: UiColors.primary),
),
);
}
/// Builds a small icon button used in row headers. /// Builds a small icon button used in row headers.
Widget _buildHeaderIconButton({ Widget _buildHeaderIconButton({
required IconData icon, required IconData icon,
@@ -523,3 +714,191 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
); );
} }
} }
/// A bottom sheet for editing an existing order.
class _OrderEditSheet extends StatefulWidget {
const _OrderEditSheet({required this.order});
final OrderItem order;
@override
State<_OrderEditSheet> createState() => _OrderEditSheetState();
}
class _OrderEditSheetState extends State<_OrderEditSheet> {
late TextEditingController _titleController;
late TextEditingController _dateController;
late TextEditingController _locationController;
late TextEditingController _workersNeededController;
@override
void initState() {
super.initState();
_titleController = TextEditingController(text: widget.order.title);
_dateController = TextEditingController(text: widget.order.date);
_locationController = TextEditingController(
text: widget.order.locationAddress,
);
_workersNeededController = TextEditingController(
text: widget.order.workersNeeded.toString(),
);
}
@override
void dispose() {
_titleController.dispose();
_dateController.dispose();
_locationController.dispose();
_workersNeededController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.9,
decoration: const BoxDecoration(
color: UiColors.bgSecondary,
borderRadius: BorderRadius.vertical(
top: Radius.circular(UiConstants.radiusBase * 2),
),
),
child: Column(
children: <Widget>[
// Header
Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
decoration: const BoxDecoration(
color: UiColors.primary,
borderRadius: BorderRadius.vertical(
top: Radius.circular(UiConstants.radiusBase * 2),
),
),
child: Row(
children: <Widget>[
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: UiColors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Icon(
UiIcons.chevronLeft,
color: UiColors.white,
size: 24,
),
),
),
),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Edit Order',
style: UiTypography.title1m.copyWith(
color: UiColors.white,
),
),
Text(
'Refine your staffing needs',
style: UiTypography.body3r.copyWith(
color: UiColors.white.withValues(alpha: 0.7),
),
),
],
),
],
),
),
// Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildSectionLabel('Position Title'),
UiTextField(
controller: _titleController,
hintText: 'e.g. Server, Bartender',
prefixIcon: UiIcons.briefcase,
),
const SizedBox(height: UiConstants.space4),
_buildSectionLabel('Date'),
UiTextField(
controller: _dateController,
hintText: 'Select Date',
prefixIcon: UiIcons.calendar,
readOnly: true,
onTap: () {
// TODO: Show date picker
},
),
const SizedBox(height: UiConstants.space4),
_buildSectionLabel('Location'),
UiTextField(
controller: _locationController,
hintText: 'Business address',
prefixIcon: UiIcons.mapPin,
),
const SizedBox(height: UiConstants.space4),
_buildSectionLabel('Workers Needed'),
Row(
children: <Widget>[
Expanded(
child: UiTextField(
controller: _workersNeededController,
hintText: 'Quantity',
prefixIcon: UiIcons.users,
keyboardType: TextInputType.number,
),
),
],
),
const SizedBox(height: UiConstants.space6),
UiButton.primary(
text: 'Save Changes',
fullWidth: true,
onPressed: () {
// TODO: Implement save logic
Navigator.pop(context);
},
),
const SizedBox(height: UiConstants.space3),
UiButton.ghost(
text: 'Cancel',
fullWidth: true,
onPressed: () => Navigator.pop(context),
),
],
),
),
),
],
),
);
}
Widget _buildSectionLabel(String label) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
label.toUpperCase(),
style: UiTypography.titleUppercase4m.copyWith(
color: UiColors.textSecondary,
fontWeight: FontWeight.bold,
),
),
);
}
}