update order detail

This commit is contained in:
Phuoc Nguyen
2025-11-25 11:57:56 +07:00
parent c3b5653420
commit 039dfb9fb5
22 changed files with 1587 additions and 288 deletions

View File

@@ -221,14 +221,14 @@ class AppTheme {
// ==================== Switch Theme ====================
switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
thumbColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primaryBlue;
}
return AppColors.grey500;
}),
trackColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
trackColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.lightBlue;
}
return AppColors.grey100;
@@ -237,20 +237,20 @@ class AppTheme {
// ==================== Checkbox Theme ====================
checkboxTheme: CheckboxThemeData(
fillColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primaryBlue;
}
return AppColors.white;
}),
checkColor: MaterialStateProperty.all(AppColors.white),
checkColor: WidgetStateProperty.all(AppColors.white),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
),
// ==================== Radio Theme ====================
radioTheme: RadioThemeData(
fillColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primaryBlue;
}
return AppColors.grey500;

View File

@@ -7,6 +7,8 @@ library;
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
// ============================================================================
// String Extensions
@@ -422,26 +424,26 @@ extension BuildContextExtensions on BuildContext {
}
/// Navigate to route
Future<T?> push<T>(Widget page) {
return Navigator.of(this).push<T>(MaterialPageRoute(builder: (_) => page));
}
// Future<T?> push<T>(Widget page) {
// return Navigator.of(this).push<T>(MaterialPageRoute(builder: (_) => page));
// }
/// Navigate and replace current route
Future<T?> pushReplacement<T>(Widget page) {
return Navigator.of(
this,
).pushReplacement<T, void>(MaterialPageRoute(builder: (_) => page));
}
// Future<T?> pushReplacement<T>(Widget page) {
// return Navigator.of(
// this,
// ).pushReplacement<T, void>(MaterialPageRoute(builder: (_) => page));
// }
/// Pop current route
void pop<T>([T? result]) {
Navigator.of(this).pop(result);
}
// void pop<T>([T? result]) {
// GoRouter.of(this).pop(result);
// }
/// Pop until first route
void popUntilFirst() {
Navigator.of(this).popUntil((route) => route.isFirst);
}
// void popUntilFirst() {
// Navigator.of(this).popUntil((route) => route.isFirst);
// }
}
// ============================================================================
@@ -466,4 +468,26 @@ extension NumExtensions on num {
final mod = math.pow(10.0, places);
return ((this * mod).round().toDouble() / mod);
}
/// Format as Vietnamese currency (đồng)
/// Returns formatted string like "1.153.434đ"
String get toVNCurrency {
final formatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
return formatter.format(this);
}
/// Format as Vietnamese currency with custom symbol
/// Returns formatted string with custom symbol
String toCurrency({String symbol = 'đ', int decimalDigits = 0}) {
final formatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: symbol,
decimalDigits: decimalDigits,
);
return formatter.format(this);
}
}

View File

@@ -657,7 +657,7 @@ class AddressFormPage extends HookConsumerWidget {
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: items.containsKey(value) ? value : null,
initialValue: items.containsKey(value) ? value : null,
isExpanded: true,
validator: validator,
decoration: InputDecoration(
@@ -796,7 +796,7 @@ class AddressFormPage extends HookConsumerWidget {
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: items.containsKey(value) ? value : null,
initialValue: items.containsKey(value) ? value : null,
isExpanded: true,
validator: validator,
decoration: InputDecoration(

View File

@@ -453,7 +453,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
@@ -759,7 +759,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
return customerGroupsAsync.when(
data: (groups) {
return DropdownButtonFormField<CustomerGroup>(
value: _selectedRole,
initialValue: _selectedRole,
decoration: _buildInputDecoration(
hintText: 'Chọn vai trò',
prefixIcon: FontAwesomeIcons.briefcase,
@@ -831,7 +831,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
return citiesAsync.when(
data: (cities) {
return DropdownButtonFormField<City>(
value: _selectedCity,
initialValue: _selectedCity,
decoration: _buildInputDecoration(
hintText: 'Chọn tỉnh/thành phố',
prefixIcon: Icons.location_city,

View File

@@ -55,7 +55,7 @@ class RoleDropdown extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DropdownButtonFormField<String>(
value: value,
initialValue: value,
decoration: InputDecoration(
hintText: 'Chọn vai trò của bạn',
hintStyle: const TextStyle(

View File

@@ -167,7 +167,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
// Price
Text(
'${currencyFormatter.format(widget.item.product.basePrice)}/${widget.item.product.unit ?? ''}',
'${currencyFormatter.format(widget.item.product.basePrice)}/',
style: AppTypography.titleMedium.copyWith(
color: AppColors.primaryBlue,
fontWeight: FontWeight.bold,
@@ -252,7 +252,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
// Unit label
Text(
widget.item.product.unit ?? '',
'',
style: AppTypography.bodySmall.copyWith(
color: AppColors.grey500,
),
@@ -273,7 +273,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
const TextSpan(text: '(Quy đổi: '),
TextSpan(
text:
'${widget.item.quantityConverted.toStringAsFixed(2)} ${widget.item.product.unit ?? ''}',
'${widget.item.quantityConverted.toStringAsFixed(2)} ',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const TextSpan(text: ' = '),

View File

@@ -31,7 +31,7 @@ class MemberCardWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -64,7 +64,7 @@ class MemberCardWidget extends StatelessWidget {
Text(
memberCard.memberType.displayName,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
color: Colors.white.withValues(alpha: 0.9),
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.8,
@@ -79,7 +79,7 @@ class MemberCardWidget extends StatelessWidget {
Text(
'Valid through',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
color: Colors.white.withValues(alpha: 0.8),
fontSize: 11,
),
),
@@ -123,7 +123,7 @@ class MemberCardWidget extends StatelessWidget {
Text(
'CLASS: ${memberCard.tier.displayName}',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
color: Colors.white.withValues(alpha: 0.9),
fontSize: 12,
fontWeight: FontWeight.w600,
),
@@ -132,7 +132,7 @@ class MemberCardWidget extends StatelessWidget {
Text(
'Points: ${_formatPoints(memberCard.points)}',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
color: Colors.white.withValues(alpha: 0.9),
fontSize: 12,
fontWeight: FontWeight.w600,
),

View File

@@ -24,8 +24,8 @@ class OrderDetailModel {
final List<OrderItemDetailModel> items;
final PaymentTermsInfoModel paymentTerms;
final List<TimelineItemModel> timeline;
final List<dynamic> payments;
final List<dynamic> invoices;
final List<PaymentInfoModel> payments;
final List<InvoiceInfoModel> invoices;
/// Create from JSON
factory OrderDetailModel.fromJson(Map<String, dynamic> json) {
@@ -50,8 +50,14 @@ class OrderDetailModel {
.map((item) =>
TimelineItemModel.fromJson(item as Map<String, dynamic>))
.toList(),
payments: json['payments'] as List<dynamic>? ?? [],
invoices: json['invoices'] as List<dynamic>? ?? [],
payments: (json['payments'] as List<dynamic>? ?? [])
.map((item) =>
PaymentInfoModel.fromJson(item as Map<String, dynamic>))
.toList(),
invoices: (json['invoices'] as List<dynamic>? ?? [])
.map((item) =>
InvoiceInfoModel.fromJson(item as Map<String, dynamic>))
.toList(),
);
}
@@ -64,8 +70,8 @@ class OrderDetailModel {
'items': items.map((item) => item.toJson()).toList(),
'payment_terms': paymentTerms.toJson(),
'timeline': timeline.map((item) => item.toJson()).toList(),
'payments': payments,
'invoices': invoices,
'payments': payments.map((item) => item.toJson()).toList(),
'invoices': invoices.map((item) => item.toJson()).toList(),
};
}
@@ -78,8 +84,8 @@ class OrderDetailModel {
items: items.map((item) => item.toEntity()).toList(),
paymentTerms: paymentTerms.toEntity(),
timeline: timeline.map((item) => item.toEntity()).toList(),
payments: payments,
invoices: invoices,
payments: payments.map((item) => item.toEntity()).toList(),
invoices: invoices.map((item) => item.toEntity()).toList(),
);
}
@@ -96,8 +102,12 @@ class OrderDetailModel {
timeline: entity.timeline
.map((item) => TimelineItemModel.fromEntity(item))
.toList(),
payments: entity.payments,
invoices: entity.invoices,
payments: entity.payments
.map((item) => PaymentInfoModel.fromEntity(item))
.toList(),
invoices: entity.invoices
.map((item) => InvoiceInfoModel.fromEntity(item))
.toList(),
);
}
}
@@ -500,3 +510,93 @@ class TimelineItemModel {
);
}
}
/// Payment Info Model
class PaymentInfoModel {
const PaymentInfoModel({
required this.name,
required this.creationDate,
required this.amount,
});
final String name;
final String creationDate;
final double amount;
factory PaymentInfoModel.fromJson(Map<String, dynamic> json) {
return PaymentInfoModel(
name: json['name'] as String,
creationDate: json['creation_date'] as String,
amount: (json['amount'] as num).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'creation_date': creationDate,
'amount': amount,
};
}
PaymentInfo toEntity() {
return PaymentInfo(
name: name,
creationDate: creationDate,
amount: amount,
);
}
factory PaymentInfoModel.fromEntity(PaymentInfo entity) {
return PaymentInfoModel(
name: entity.name,
creationDate: entity.creationDate,
amount: entity.amount,
);
}
}
/// Invoice Info Model
class InvoiceInfoModel {
const InvoiceInfoModel({
required this.name,
required this.postingDate,
required this.grandTotal,
});
final String name;
final String postingDate;
final double grandTotal;
factory InvoiceInfoModel.fromJson(Map<String, dynamic> json) {
return InvoiceInfoModel(
name: json['name'] as String,
postingDate: json['posting_date'] as String,
grandTotal: (json['grand_total'] as num).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'posting_date': postingDate,
'grand_total': grandTotal,
};
}
InvoiceInfo toEntity() {
return InvoiceInfo(
name: name,
postingDate: postingDate,
grandTotal: grandTotal,
);
}
factory InvoiceInfoModel.fromEntity(InvoiceInfo entity) {
return InvoiceInfoModel(
name: entity.name,
postingDate: entity.postingDate,
grandTotal: entity.grandTotal,
);
}
}

View File

@@ -24,8 +24,8 @@ class OrderDetail extends Equatable {
final List<OrderItemDetail> items;
final PaymentTermsInfo paymentTerms;
final List<TimelineItem> timeline;
final List<dynamic> payments; // Payment entities can be added later
final List<dynamic> invoices; // Invoice entities can be added later
final List<PaymentInfo> payments;
final List<InvoiceInfo> invoices;
@override
List<Object?> get props => [
@@ -219,3 +219,35 @@ class TimelineItem extends Equatable {
@override
List<Object?> get props => [label, value, status];
}
/// Payment Info
class PaymentInfo extends Equatable {
const PaymentInfo({
required this.name,
required this.creationDate,
required this.amount,
});
final String name;
final String creationDate;
final double amount;
@override
List<Object?> get props => [name, creationDate, amount];
}
/// Invoice Info
class InvoiceInfo extends Equatable {
const InvoiceInfo({
required this.name,
required this.postingDate,
required this.grandTotal,
});
final String name;
final String postingDate;
final double grandTotal;
@override
List<Object?> get props => [name, postingDate, grandTotal];
}

View File

@@ -12,6 +12,7 @@ import 'package:intl/intl.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/enums/status_color.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/utils/extensions.dart';
import 'package:worker/features/orders/domain/entities/order_detail.dart';
import 'package:worker/features/orders/presentation/providers/orders_provider.dart';
@@ -82,16 +83,24 @@ class OrderDetailPage extends ConsumerWidget {
_buildStatusTimelineCard(orderDetail),
// Delivery/Address Information Card
_buildAddressInfoCard(orderDetail),
_buildAddressInfoCard(context, orderDetail),
// Customer Information Card
_buildCustomerInfoCard(orderDetail),
// Invoice Information Card
_buildInvoiceInfoCard(context, orderDetail),
// Invoices List Card
_buildInvoicesListCard(context, orderDetail),
// Products List Card
_buildProductsListCard(orderDetail),
// Order Summary Card
_buildOrderSummaryCard(orderDetail),
// Payment History Card
_buildPaymentHistoryCard(context, orderDetail),
const SizedBox(height: 16),
],
),
),
@@ -327,7 +336,7 @@ class OrderDetailPage extends ConsumerWidget {
}
/// Build Address Info Card
Widget _buildAddressInfoCard(OrderDetail orderDetail) {
Widget _buildAddressInfoCard(BuildContext context, OrderDetail orderDetail) {
final order = orderDetail.order;
final shippingAddress = orderDetail.shippingAddress;
final dateFormatter = DateFormat('dd/MM/yyyy');
@@ -341,15 +350,15 @@ class OrderDetailPage extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
const Row(
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.truck,
color: AppColors.primaryBlue,
size: 18,
),
const SizedBox(width: 8),
const Text(
SizedBox(width: 8),
Text(
'Thông tin giao hàng',
style: TextStyle(
fontSize: 16,
@@ -362,86 +371,139 @@ class OrderDetailPage extends ConsumerWidget {
const SizedBox(height: 16),
// Delivery Date
_buildInfoRow(
icon: FontAwesomeIcons.calendar,
label: 'Ngày giao hàng',
value: dateFormatter.format(DateTime.parse(order.deliveryDate)),
// Address Section with Label + Button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Địa chỉ nhận hàng',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.grey500,
),
),
TextButton(
onPressed: () {
// TODO: Navigate to address update
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chức năng đang phát triển')),
);
},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: const BorderSide(color: AppColors.grey100),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Cập nhật',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryBlue,
),
),
),
],
),
const SizedBox(height: 12),
const SizedBox(height: 8),
_buildInfoRow(
icon: FontAwesomeIcons.locationDot,
label: 'Địa chỉ giao hàng',
value:
'${shippingAddress.addressLine1}\n${shippingAddress.wardName}, ${shippingAddress.cityName}',
// Address Box
Container(
padding: const EdgeInsets.all(12),
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey100),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
shippingAddress.addressTitle,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 4),
Text(
shippingAddress.phone,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(height: 4),
Text(
'${shippingAddress.addressLine1}\n${shippingAddress.wardName}, ${shippingAddress.cityName}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
),
const SizedBox(height: 12),
const SizedBox(height: 16),
_buildInfoRow(
icon: FontAwesomeIcons.user,
label: 'Người nhận',
value: '${shippingAddress.addressTitle} - ${shippingAddress.phone}',
// Pickup Date
const Text(
'Ngày lấy hàng',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey500,
),
),
const SizedBox(height: 4),
Text(
dateFormatter.format(DateTime.parse(order.deliveryDate)),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
),
),
if (order.description.isNotEmpty) ...[ const SizedBox(height: 16),
// Notes
const Text(
'Ghi chú',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey500,
),
),
const SizedBox(height: 4),
Text(
order.description,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
),
),
],
],
),
),
);
}
/// Build Info Row
Widget _buildInfoRow({
required IconData icon,
required String label,
required String value,
Color? valueColor,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Row(
children: [
FaIcon(icon, size: 14, color: AppColors.grey500),
const SizedBox(width: 6),
Expanded(
child: Text(
label,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: Text(
value,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 14,
fontWeight: valueColor != null
? FontWeight.w600
: FontWeight.w500,
color: valueColor ?? AppColors.grey900,
),
),
),
],
);
}
/// Build Customer Info Card
Widget _buildCustomerInfoCard(OrderDetail orderDetail) {
final order = orderDetail.order;
/// Build Invoice Info Card
Widget _buildInvoiceInfoCard(BuildContext context, OrderDetail orderDetail) {
final billingAddress = orderDetail.billingAddress;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 1,
@@ -451,16 +513,146 @@ class OrderDetailPage extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title + Update Button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const FaIcon(
FontAwesomeIcons.user,
const Row(
children: [
FaIcon(
FontAwesomeIcons.fileInvoice,
color: AppColors.primaryBlue,
size: 18,
),
SizedBox(width: 8),
Text(
'Thông tin hóa đơn',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
],
),
TextButton(
onPressed: () {
// TODO: Navigate to invoice update
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chức năng đang phát triển')),
);
},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: AppColors.grey100),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Cập nhật',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryBlue,
),
),
),
],
),
const SizedBox(height: 12),
// Invoice Address Box
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey100),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
billingAddress.addressTitle,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
if (billingAddress.taxCode.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
'Mã số thuế: ${billingAddress.taxCode}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
const SizedBox(height: 2),
Text(
'Số điện thoại: ${billingAddress.phone}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(height: 2),
Text(
'Email: ${billingAddress.email}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(height: 2),
Text(
'Địa chỉ: ${billingAddress.addressLine1}, ${billingAddress.wardName}, ${billingAddress.cityName}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
),
],
),
),
);
}
/// Build Invoices List Card
Widget _buildInvoicesListCard(BuildContext context, OrderDetail orderDetail) {
final invoices = orderDetail.invoices;
if (invoices.isEmpty) {
return const SizedBox.shrink();
}
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
FaIcon(
FontAwesomeIcons.fileInvoiceDollar,
color: AppColors.primaryBlue,
size: 18,
),
const SizedBox(width: 8),
const Text(
'Thông tin khách hàng',
SizedBox(width: 8),
Text(
'Hóa đơn đã xuất',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
@@ -472,54 +664,109 @@ class OrderDetailPage extends ConsumerWidget {
const SizedBox(height: 16),
_buildCustomerRow('Tên khách hàng:', order.customer),
const SizedBox(height: 12),
_buildCustomerRow('Số điện thoại:', billingAddress.phone),
const SizedBox(height: 12),
_buildCustomerRow('Email:', billingAddress.email),
const SizedBox(height: 12),
if (billingAddress.taxCode.isNotEmpty) ...[
_buildCustomerRow('Mã số thuế:', billingAddress.taxCode),
const SizedBox(height: 12),
],
// Invoice Items (Mock data for now)
...invoices.map((e) => _buildInvoiceItem(
invoiceId: e.name,
date: e.postingDate,
amount: e.grandTotal.toVNCurrency,
onTap: () {
// TODO: Navigate to invoice detail
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chức năng đang phát triển')),
);
},
),),
],
),
),
);
}
/// Build Customer Row
Widget _buildCustomerRow(String label, String value) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
/// Build Invoice Item
Widget _buildInvoiceItem({
required String invoiceId,
required String date,
required String amount,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey100),
borderRadius: BorderRadius.circular(8),
),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppColors.primaryBlue, Color(0xFF1d4ed8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: FaIcon(
FontAwesomeIcons.fileInvoice,
color: Colors.white,
size: 18,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
invoiceId,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 2),
Text(
'Ngày xuất: $date',
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
),
),
const SizedBox(width: 4,),
Text(
amount,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.danger,
),
),
const SizedBox(width: 8),
const FaIcon(
FontAwesomeIcons.chevronRight,
size: 14,
color: AppColors.grey500,
),
],
),
],
),
);
}
/// Build Products List Card
Widget _buildProductsListCard(OrderDetail orderDetail) {
final items = orderDetail.items;
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@@ -530,15 +777,15 @@ class OrderDetailPage extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
const Row(
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.box,
color: AppColors.primaryBlue,
size: 18,
),
const SizedBox(width: 8),
const Text(
SizedBox(width: 8),
Text(
'Sản phẩm đặt hàng',
style: TextStyle(
fontSize: 16,
@@ -657,14 +904,14 @@ class OrderDetailPage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${currencyFormatter.format(item.price)}/m²',
'${item.price.toVNCurrency}/m²',
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
Text(
currencyFormatter.format(item.totalAmount),
item.totalAmount.toVNCurrency,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
@@ -692,11 +939,6 @@ class OrderDetailPage extends ConsumerWidget {
Widget _buildOrderSummaryCard(OrderDetail orderDetail) {
final order = orderDetail.order;
final paymentTerms = orderDetail.paymentTerms;
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@@ -728,13 +970,13 @@ class OrderDetailPage extends ConsumerWidget {
const SizedBox(height: 16),
_buildSummaryRow('Tổng tiền hàng:', currencyFormatter.format(order.total)),
_buildSummaryRow('Tổng tiền hàng:', order.total.toVNCurrency),
const SizedBox(height: 8),
if (order.totalRemaining > 0) ...[
_buildSummaryRow(
'Còn lại:',
currencyFormatter.format(order.totalRemaining),
order.totalRemaining.toVNCurrency,
valueColor: AppColors.warning,
),
const SizedBox(height: 8),
@@ -744,7 +986,7 @@ class OrderDetailPage extends ConsumerWidget {
_buildSummaryRow(
'Tổng cộng:',
currencyFormatter.format(order.grandTotal),
order.grandTotal.toVNCurrency,
isTotal: true,
),
@@ -784,31 +1026,169 @@ class OrderDetailPage extends ConsumerWidget {
),
),
if (order.description.isNotEmpty) ...[
const Divider(height: 24),
],
),
),
);
}
// Order Notes
Row(
/// Build Payment History Card
Widget _buildPaymentHistoryCard(BuildContext context, OrderDetail orderDetail) {
final order = orderDetail.order;
final payments = orderDetail.payments;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [
const Row(
children: [
FaIcon(
FontAwesomeIcons.clockRotateLeft,
color: AppColors.primaryBlue,
size: 18,
),
SizedBox(width: 8),
Text(
'Lịch sử thanh toán',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
],
),
...payments.map((e) => _buildPaymentItem(
paymentId: e.name,
date: e.creationDate,
amount: e.amount.toVNCurrency,
onTap: () {
// TODO: Show payment detail modal
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chi tiết thanh toán')),
);
},
)),
// Payment Summary
Container(
padding: const EdgeInsets.only(top: 12),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: AppColors.grey100, width: 1),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const FaIcon(FontAwesomeIcons.noteSticky, size: 14, color: AppColors.grey500),
const SizedBox(width: 6),
const Text(
'Ghi chú đơn hàng:',
style: TextStyle(fontSize: 14, color: AppColors.grey500),
'Còn lại:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey500,
),
),
Text(
order.totalRemaining.toVNCurrency,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.danger,
),
),
],
),
const SizedBox(height: 4),
Text(
order.description,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
height: 1.4,
),
],
),
),
);
}
/// Build Payment Item
Widget _buildPaymentItem({
required String paymentId,
required String date,
required String amount,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey100),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Color(0xFFD1FAE5),
shape: BoxShape.circle,
),
child: const Center(
child: FaIcon(
FontAwesomeIcons.check,
color: Color(0xFF065F46),
size: 16,
),
),
],
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
paymentId,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 2),
Text(
date,
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
),
),
const SizedBox(width: 4),
Text(
amount,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF065F46),
),
),
const SizedBox(width: 8),
const FaIcon(
FontAwesomeIcons.chevronRight,
size: 14,
color: AppColors.grey500,
),
],
),
),
@@ -848,8 +1228,6 @@ class OrderDetailPage extends ConsumerWidget {
/// Build Action Buttons
Widget _buildActionButtons(BuildContext context, OrderDetail orderDetail) {
final shippingAddress = orderDetail.shippingAddress;
return Container(
decoration: BoxDecoration(
color: AppColors.white,
@@ -866,23 +1244,22 @@ class OrderDetailPage extends ConsumerWidget {
spacing: 12,
children: [
Expanded(
child: OutlinedButton.icon(
child: ElevatedButton.icon(
onPressed: () {
// TODO: Navigate to payment page
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gọi ${shippingAddress.phone}...'),
const SnackBar(
content: Text('Chức năng thanh toán đang phát triển'),
),
);
},
icon: const FaIcon(FontAwesomeIcons.phone, size: 18),
label: const Text('Liên hệ'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
side: const BorderSide(
color: AppColors.grey100,
width: 2,
),
foregroundColor: AppColors.grey900,
icon: const FaIcon(FontAwesomeIcons.creditCard, size: 18),
label: const Text('Thanh toán'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
@@ -890,21 +1267,24 @@ class OrderDetailPage extends ConsumerWidget {
),
),
Expanded(
child: ElevatedButton.icon(
child: OutlinedButton.icon(
onPressed: () {
// TODO: Navigate to chat/support
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Chức năng đang phát triển...'),
content: Text('Liên hệ hỗ trợ...'),
),
);
},
icon: const FaIcon(FontAwesomeIcons.penToSquare, size: 18),
label: const Text('Cập nhật'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
elevation: 0,
icon: const FaIcon(FontAwesomeIcons.comments, size: 18),
label: const Text('Liên hệ hỗ trợ'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: BorderSide(
color: AppColors.grey100,
width: 2,
),
foregroundColor: AppColors.grey900,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),

View File

@@ -23,7 +23,7 @@ class DocumentCard extends StatelessWidget {
border: Border.all(color: AppColors.grey100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),

View File

@@ -28,7 +28,6 @@ class ProductModel extends HiveObject {
this.specifications,
this.itemGroupName,
this.brand,
this.unit,
this.conversionOfSm,
this.introAttributes,
required this.isActive,
@@ -83,10 +82,6 @@ class ProductModel extends HiveObject {
@HiveField(10)
final String? brand;
/// Unit of measurement (m2, box, piece, etc.)
@HiveField(11)
final String? unit;
/// Conversion factor for Square Meter UOM (tiles per m²)
/// Used to calculate: Số viên = Số lượng × conversionOfSm
@HiveField(17)
@@ -204,7 +199,6 @@ class ProductModel extends HiveObject {
: null,
itemGroupName: json['item_group_name'] as String?,
brand: json['brand'] as String?,
unit: json['currency'] as String?, // Use currency as unit for now
conversionOfSm: json['conversion_of_sm'] != null
? (json['conversion_of_sm'] as num).toDouble()
: null,
@@ -260,7 +254,6 @@ class ProductModel extends HiveObject {
specifications: null,
itemGroupName: json['item_group_name'] as String?,
brand: null, // Not provided by wishlist API
unit: json['currency'] as String? ?? '',
conversionOfSm: json['conversion_of_sm'] != null
? (json['conversion_of_sm'] as num).toDouble()
: null,
@@ -291,7 +284,6 @@ class ProductModel extends HiveObject {
: null,
'item_group_name': itemGroupName,
'brand': brand,
'unit': unit,
'conversion_of_sm': conversionOfSm,
'intro_attributes': introAttributes != null
? jsonDecode(introAttributes!)
@@ -406,7 +398,6 @@ class ProductModel extends HiveObject {
String? specifications,
String? itemGroupName,
String? brand,
String? unit,
double? conversionOfSm,
String? introAttributes,
bool? isActive,
@@ -427,7 +418,6 @@ class ProductModel extends HiveObject {
specifications: specifications ?? this.specifications,
itemGroupName: itemGroupName ?? this.itemGroupName,
brand: brand ?? this.brand,
unit: unit ?? this.unit,
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
introAttributes: introAttributes ?? this.introAttributes,
isActive: isActive ?? this.isActive,
@@ -471,7 +461,6 @@ class ProductModel extends HiveObject {
specifications: specificationsMap ?? {},
itemGroupName: itemGroupName,
brand: brand,
unit: unit,
conversionOfSm: conversionOfSm,
introAttributes: introAttributesList,
isActive: isActive,

View File

@@ -28,7 +28,6 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
specifications: fields[8] as String?,
itemGroupName: fields[9] as String?,
brand: fields[10] as String?,
unit: fields[11] as String?,
conversionOfSm: (fields[17] as num?)?.toDouble(),
introAttributes: fields[18] as String?,
isActive: fields[12] as bool,
@@ -42,7 +41,7 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
@override
void write(BinaryWriter writer, ProductModel obj) {
writer
..writeByte(19)
..writeByte(18)
..writeByte(0)
..write(obj.productId)
..writeByte(1)
@@ -65,8 +64,6 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
..write(obj.itemGroupName)
..writeByte(10)
..write(obj.brand)
..writeByte(11)
..write(obj.unit)
..writeByte(12)
..write(obj.isActive)
..writeByte(13)

View File

@@ -22,7 +22,6 @@ class Product {
required this.specifications,
this.itemGroupName,
this.brand,
this.unit,
this.conversionOfSm,
this.introAttributes,
required this.isActive,
@@ -64,9 +63,6 @@ class Product {
/// Brand name
final String? brand;
/// Unit of measurement (e.g., "m²", "viên", "hộp")
final String? unit;
/// Conversion factor for Square Meter UOM (tiles per m²)
/// Used to calculate: Số viên = Số lượng × conversionOfSm
final double? conversionOfSm;
@@ -154,7 +150,6 @@ class Product {
Map<String, dynamic>? specifications,
String? itemGroupName,
String? brand,
String? unit,
double? conversionOfSm,
List<Map<String, String>>? introAttributes,
bool? isActive,
@@ -175,7 +170,6 @@ class Product {
specifications: specifications ?? this.specifications,
itemGroupName: itemGroupName ?? this.itemGroupName,
brand: brand ?? this.brand,
unit: unit ?? this.unit,
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
introAttributes: introAttributes ?? this.introAttributes,
isActive: isActive ?? this.isActive,
@@ -203,7 +197,6 @@ class Product {
other.basePrice == basePrice &&
other.itemGroupName == itemGroupName &&
other.brand == brand &&
other.unit == unit &&
other.isActive == isActive &&
other.isFeatured == isFeatured &&
other.erpnextItemCode == erpnextItemCode;
@@ -218,7 +211,6 @@ class Product {
basePrice,
itemGroupName,
brand,
unit,
isActive,
isFeatured,
erpnextItemCode,

View File

@@ -156,7 +156,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Đã thêm $_quantity ${product.unit} ${product.name} vào giỏ hàng!',
'Đã thêm $_quantity ${product.name} vào giỏ hàng!',
),
duration: const Duration(seconds: 2),
action: SnackBarAction(
@@ -246,7 +246,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
right: 0,
child: StickyActionBar(
quantity: _quantity,
unit: product.unit ?? '',
unit: '',
conversionOfSm: product.conversionOfSm,
uomFromIntroAttributes: product.getIntroAttribute('UOM'),
onIncrease: _increaseQuantity,

View File

@@ -220,7 +220,7 @@ class ProductCard extends ConsumerWidget {
// Price
Text(
'${_formatPrice(product.effectivePrice)}/${product.unit}',
'${_formatPrice(product.effectivePrice)}/',
style: const TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,

View File

@@ -61,7 +61,7 @@ class ProductInfoSection extends StatelessWidget {
children: [
// Current Price
Text(
'${_formatPrice(product.basePrice)}/${product.unit ?? ''}',
'${_formatPrice(product.basePrice)}/',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,

View File

@@ -271,7 +271,7 @@ class DesignRequestCreatePage extends HookConsumerWidget {
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: selectedStyle.value.isEmpty
initialValue: selectedStyle.value.isEmpty
? null
: selectedStyle.value,
decoration: InputDecoration(
@@ -365,7 +365,7 @@ class DesignRequestCreatePage extends HookConsumerWidget {
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: selectedBudget.value.isEmpty
initialValue: selectedBudget.value.isEmpty
? null
: selectedBudget.value,
decoration: InputDecoration(

View File

@@ -124,7 +124,7 @@ class SearchAppBar extends StatelessWidget implements PreferredSizeWidget {
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: hintText,
hintStyle: TextStyle(color: Colors.white.withOpacity(0.7)),
hintStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
border: InputBorder.none,
suffixIcon: controller?.text.isNotEmpty ?? false
? IconButton(

View File

@@ -45,7 +45,7 @@ class GradientCard extends StatelessWidget {
shadows ??
[
BoxShadow(
color: Colors.black.withOpacity(0.1 * (elevation / 4)),
color: Colors.black.withValues(alpha: 0.1 * (elevation / 4)),
blurRadius: elevation,
offset: Offset(0, elevation / 2),
),
@@ -243,9 +243,9 @@ class _ShimmerGradientCardState extends State<ShimmerGradientCard>
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(0.1),
Colors.white.withOpacity(0.3),
Colors.white.withOpacity(0.1),
Colors.white.withValues(alpha: 0.1),
Colors.white.withValues(alpha: 0.3),
Colors.white.withValues(alpha: 0.1),
],
stops: [
_controller.value - 0.3,