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:
@@ -163,6 +163,12 @@ class UiIcons {
|
||||
/// Eye off icon for hidden visibility
|
||||
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
|
||||
static const IconData building = _IconLib.building2;
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ class UiTheme {
|
||||
|
||||
/// Returns the light theme for the Staff application.
|
||||
static ThemeData get light {
|
||||
final colorScheme = UiColors.colorScheme;
|
||||
final textTheme = UiTypography.textTheme;
|
||||
final ColorScheme colorScheme = UiColors.colorScheme;
|
||||
final TextTheme textTheme = UiTypography.textTheme;
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
@@ -68,7 +68,6 @@ class UiTheme {
|
||||
horizontal: UiConstants.space6,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 54),
|
||||
maximumSize: const Size(double.infinity, 54),
|
||||
).copyWith(
|
||||
side: WidgetStateProperty.resolveWith<BorderSide?>((states) {
|
||||
@@ -99,7 +98,6 @@ class UiTheme {
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 52),
|
||||
maximumSize: const Size(double.infinity, 52),
|
||||
),
|
||||
),
|
||||
@@ -117,7 +115,6 @@ class UiTheme {
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 52),
|
||||
maximumSize: const Size(double.infinity, 52),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -320,6 +320,15 @@ class UiTypography {
|
||||
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)
|
||||
static final TextStyle body4r = _primaryBase.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
|
||||
@@ -27,6 +27,9 @@ class UiButton extends StatelessWidget {
|
||||
/// The size of the button.
|
||||
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).
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
@@ -48,6 +51,7 @@ class UiButton extends StatelessWidget {
|
||||
this.style,
|
||||
this.iconSize = 20,
|
||||
this.size = UiButtonSize.medium,
|
||||
this.fullWidth = false,
|
||||
}) : assert(
|
||||
text != null || child != null,
|
||||
'Either text or child must be provided',
|
||||
@@ -64,6 +68,7 @@ class UiButton extends StatelessWidget {
|
||||
this.style,
|
||||
this.iconSize = 20,
|
||||
this.size = UiButtonSize.medium,
|
||||
this.fullWidth = false,
|
||||
}) : buttonBuilder = _elevatedButtonBuilder,
|
||||
assert(
|
||||
text != null || child != null,
|
||||
@@ -81,6 +86,7 @@ class UiButton extends StatelessWidget {
|
||||
this.style,
|
||||
this.iconSize = 20,
|
||||
this.size = UiButtonSize.medium,
|
||||
this.fullWidth = false,
|
||||
}) : buttonBuilder = _outlinedButtonBuilder,
|
||||
assert(
|
||||
text != null || child != null,
|
||||
@@ -98,6 +104,25 @@ class UiButton extends StatelessWidget {
|
||||
this.style,
|
||||
this.iconSize = 20,
|
||||
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,
|
||||
assert(
|
||||
text != null || child != null,
|
||||
@@ -107,7 +132,18 @@ class UiButton extends StatelessWidget {
|
||||
@override
|
||||
/// Builds the button UI.
|
||||
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.
|
||||
@@ -116,27 +152,40 @@ class UiButton extends StatelessWidget {
|
||||
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) {
|
||||
return Text(text!);
|
||||
return Text(buttonText, textAlign: TextAlign.center);
|
||||
}
|
||||
|
||||
if (leadingIcon != null && text == null && trailingIcon == null) {
|
||||
return Icon(leadingIcon, size: iconSize);
|
||||
}
|
||||
|
||||
// Multiple elements case
|
||||
// Multiple elements case: Use a Row with MainAxisSize.min
|
||||
final List<Widget> children = [];
|
||||
|
||||
if (leadingIcon != null) {
|
||||
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) {
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ class ClientGetStartedPage extends StatelessWidget {
|
||||
.get_started_page
|
||||
.sign_in_button,
|
||||
onPressed: () => Modular.to.pushClientSignIn(),
|
||||
fullWidth: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
@@ -108,6 +109,7 @@ class ClientGetStartedPage extends StatelessWidget {
|
||||
.get_started_page
|
||||
.create_account_button,
|
||||
onPressed: () => Modular.to.pushClientSignUp(),
|
||||
fullWidth: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -96,6 +96,7 @@ class _ClientSignInFormState extends State<ClientSignInForm> {
|
||||
UiButton.primary(
|
||||
text: widget.isLoading ? null : i18n.sign_in_button,
|
||||
onPressed: widget.isLoading ? null : _handleSubmit,
|
||||
fullWidth: true,
|
||||
child: widget.isLoading
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
|
||||
@@ -20,7 +20,16 @@ class ViewOrderCard extends StatefulWidget {
|
||||
}
|
||||
|
||||
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.
|
||||
Color _getStatusColor({required String status}) {
|
||||
@@ -65,6 +74,13 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
String _formatDate({required String dateStr}) {
|
||||
try {
|
||||
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);
|
||||
} catch (_) {
|
||||
return dateStr;
|
||||
@@ -73,7 +89,18 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
|
||||
/// Formats the time string for display.
|
||||
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
|
||||
@@ -205,7 +232,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// TODO: Get directions
|
||||
// TODO: Handle location
|
||||
},
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
@@ -236,9 +263,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
icon: UiIcons.edit,
|
||||
color: UiColors.primary,
|
||||
bgColor: UiColors.tagInProgress,
|
||||
onTap: () {
|
||||
// TODO: Open edit sheet
|
||||
},
|
||||
onTap: () => _openEditSheet(order: order),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
_buildHeaderIconButton(
|
||||
@@ -319,120 +344,129 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
|
||||
// Coverage Bar
|
||||
// Coverage Section
|
||||
if (order.status != 'completed') ...<Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_view_orders.card.coverage,
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
const Icon(
|
||||
UiIcons.success,
|
||||
size: 16,
|
||||
color: UiColors.textSuccess,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
t.client_view_orders.card.workers_label(
|
||||
filled: order.filled,
|
||||
needed: order.workersNeeded,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: coveragePercent == 100
|
||||
? UiColors.tagSuccess
|
||||
: UiColors.tagInProgress,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'$coveragePercent%',
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
fontSize: 9,
|
||||
color: coveragePercent == 100
|
||||
? UiColors.textSuccess
|
||||
: UiColors.primary,
|
||||
),
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
t.client_view_orders.card.workers_label(
|
||||
filled: order.filled,
|
||||
needed: order.workersNeeded,
|
||||
),
|
||||
style: UiTypography.footnote2m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
'$coveragePercent%',
|
||||
style: UiTypography.body2b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: coveragePercent / 100,
|
||||
backgroundColor: UiColors.separatorSecondary,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
coveragePercent == 100
|
||||
? UiColors.textSuccess
|
||||
: UiColors.primary,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
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)
|
||||
if (_expanded) ...<Widget>[
|
||||
const Divider(height: 1, color: UiColors.separatorSecondary),
|
||||
Padding(
|
||||
// Assigned Workers (Expanded section)
|
||||
if (_expanded && order.confirmedApps.isNotEmpty) ...<Widget>[
|
||||
Container(
|
||||
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),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_view_orders.card.confirmed_workers,
|
||||
style: UiTypography.body2b.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
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),
|
||||
if (order.confirmedApps.isEmpty)
|
||||
Text(
|
||||
t.client_view_orders.card.no_workers,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textInactive,
|
||||
...order.confirmedApps
|
||||
.take(5)
|
||||
.map((Map<String, dynamic> app) => _buildWorkerRow(app)),
|
||||
if (order.confirmedApps.length > 5)
|
||||
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.
|
||||
Widget _buildHeaderIconButton({
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user