mobile_skt/lib/screens/sale_quotation/sale_quotation.dart

622 lines
23 KiB
Dart
Raw Normal View History

2024-10-16 09:48:17 +08:00
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/models/sale_quotation.model.dart';
import 'package:sk_base_mobile/router/router.util.dart';
import 'package:sk_base_mobile/screens/sale_quotation/components/sale_quotation_drawer.dart';
import 'package:sk_base_mobile/screens/sale_quotation/sale_quotation.controller.dart';
import 'package:sk_base_mobile/util/common.util.dart';
import 'package:sk_base_mobile/util/modal.util.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/form_item/sk_number_input.dart';
import 'package:sk_base_mobile/widgets/form_item/sk_text_input.dart';
import 'package:sk_base_mobile/widgets/core/sk_appbar.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:math_expressions/math_expressions.dart';
class SaleQuotationPage extends StatelessWidget {
SaleQuotationPage({super.key});
final controller = Get.put(SaleQuotationController());
final quantityWidth = 140.0;
final unitPriceWidth = 140.0;
final amountWidth = 140.0;
final TextStyle? headerTitleStyle = TextStyle(
fontSize: ScreenAdaper.height(30),
fontWeight: FontWeight.w600,
color: Theme.of(Get.context!).colorScheme.onPrimary);
final headerBgcolor = AppTheme.appbarBgColor;
@override
Widget build(BuildContext context) {
return Obx(() => Scaffold(
key: controller.scaffoldKey,
backgroundColor: AppTheme.nearlyWhite,
endDrawer: Drawer(
// 从右到左出现
child: SaleQuotationEndDrawer(),
),
appBar: SkAppbar(
onPop: () async {
// 在这里判断是否允许用户返回
// 如果返回 true用户可以返回
// 如果返回 false用户不能返回
if (controller.templateName.value == '默认' &&
controller.groups.isNotEmpty) {
await ModalUtil.alert(
confirmText: '保存',
cancelText: '继续退出',
contentText: '是否保存当前模板?',
onConfirm: () {
controller.openTemplateNameEditPopup(
onConfirm: (title) async {
controller.templateName.value = title;
await controller.saveToLocal();
await controller.saveToDatabase();
RouterUtil.back();
});
},
onCancel: () {
RouterUtil.back();
});
} else {
RouterUtil.back();
}
},
title: '报价计算-${controller.templateName}',
action: [
IconButton(
onPressed: () {
controller.scaffoldKey.currentState?.openEndDrawer();
},
icon: const Icon(Icons.more_horiz_outlined,
color: AppTheme.white),
),
],
),
body: SafeArea(
bottom: ScreenAdaper.isLandspace() ? false : true,
child: Stack(children: [
Column(
children: [
builderHeader(),
Expanded(
child: Obx(() => controller.groups.isEmpty
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'请右上角选择模板',
style: TextStyle(
fontSize: ScreenAdaper.height(30)),
),
Text('或者左上角加号添加分组',
style: TextStyle(
fontSize: ScreenAdaper.height(30)))
],
)
: CustomScrollView(
slivers: controller.groups
.mapIndexed<Widget>(
(index, e) => buildBody(index))
.toList(),
))),
// 当键盘弹起时,不显示
SizedBox(
height: ScreenAdaper.height(100),
child: buildTotalCostRow(),
),
SizedBox(
height: ScreenAdaper.height(100),
child: buildTotalSalesPriceRow(),
)
],
),
]),
),
));
}
Widget buildTotalSalesPriceRow() {
final formulaController =
TextEditingController(text: controller.formula.value);
return Container(
padding: EdgeInsets.symmetric(
horizontal: ScreenAdaper.width(20),
vertical: ScreenAdaper.height(10)),
decoration: BoxDecoration(
border: Border(
top: BorderSide(width: 1, color: Colors.grey[200]!),
),
),
child: Row(
children: [
Text(
'合计',
style: TextStyle(
fontSize: ScreenAdaper.height(30), fontWeight: FontWeight.w600),
),
Expanded(
child: Obx(
() => Container(
alignment: Alignment.centerLeft,
padding:
EdgeInsets.symmetric(horizontal: ScreenAdaper.width(10)),
height: ScreenAdaper.height(100),
width: ScreenAdaper.width(100),
child: controller.isFormulaEditing.value
? SkTextInput(
hint: '',
isDense: true,
autoFocus: true,
onFieldSubmitted: (value) {
controller.formula.value = value;
controller.isFormulaEditing.value = false;
},
validator: (String? value) {
if (value == null) {
return null;
}
try {
Parser p = Parser();
Expression exp = p.parse(value);
Variable x = Variable('成本');
ContextModel cm = ContextModel()
..bindVariable(
x, Number(controller.totalCost.value));
exp.evaluate(EvaluationType.REAL, cm);
return null;
} catch (e) {
return '公式错误';
}
},
onChanged: (String formula) {
try {
controller.calculateTotal(newFormula: formula);
} catch (e) {}
},
onTapOutside: (String value) {
controller.formula.value = value;
controller.isFormulaEditing.value = false;
},
contentPadding: EdgeInsets.symmetric(
horizontal: ScreenAdaper.width(20),
vertical: ScreenAdaper.height(10)),
textController: formulaController)
: InkWell(
onTap: () {
controller.isFormulaEditing.value = true;
},
child: Text('${controller.formula}',
style: TextStyle(
fontSize: ScreenAdaper.height(30),
color: AppTheme.grey)),
),
),
),
),
// IconButton(
// onPressed: () {
// controller.isFormulaEditing.value =
// !controller.isFormulaEditing.value;
// },
// icon: Icon(Icons.edit, size: ScreenAdaper.height(30)),
// ),
Obx(
() => Text(
'${controller.totalPrice.value}',
textAlign: TextAlign.right,
style: TextStyle(
fontSize: ScreenAdaper.height(30),
fontWeight: FontWeight.w600),
),
)
],
),
);
}
Widget buildTotalCostRow() {
return Container(
padding: EdgeInsets.symmetric(
horizontal: ScreenAdaper.width(20),
vertical: ScreenAdaper.height(10)),
decoration: BoxDecoration(
border: Border(
top: BorderSide(width: 1, color: Colors.grey[200]!),
),
),
child: Row(
children: [
Text(
'成本',
style: TextStyle(
fontSize: ScreenAdaper.height(30), fontWeight: FontWeight.w600),
),
const Spacer(),
Obx(
() => Text(
'${controller.totalCost.value}',
style: TextStyle(
fontSize: ScreenAdaper.height(30),
fontWeight: FontWeight.w600),
),
)
],
),
);
}
Widget builderHeader() {
return Container(
decoration: BoxDecoration(
color: headerBgcolor,
border: Border(
bottom:
BorderSide(color: AppTheme.nearlyBlack.withOpacity(0.5)))),
padding: EdgeInsets.only(
left: ScreenAdaper.width(20),
top: ScreenAdaper.height(10),
bottom: ScreenAdaper.height(10)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
GestureDetector(
onTap: () {
controller.addGroup();
},
child: Icon(
Icons.add_circle_outline_outlined,
size: ScreenAdaper.height(40),
color: Theme.of(Get.context!).colorScheme.onPrimary,
),
),
SizedBox(
width: ScreenAdaper.width(5),
),
Text(
'名称',
style: headerTitleStyle,
),
const Spacer(),
Container(
alignment: Alignment.center,
width: ScreenAdaper.width(quantityWidth),
// constraints: BoxConstraints(minWidth: quantityWidth),
child: Text(
'数量',
style: headerTitleStyle,
),
),
Container(
alignment: Alignment.center,
width: ScreenAdaper.width(unitPriceWidth),
// constraints: BoxConstraints(minWidth: ScreenAdaper.width(unitPriceWidth)),
child: Text(
'单价',
style: headerTitleStyle,
),
),
Container(
alignment: Alignment.center,
width: ScreenAdaper.width(amountWidth),
child: Text(
'总价',
style: headerTitleStyle,
),
),
],
));
}
Widget buildBody(int groupIndex) {
return SliverStickyHeader.builder(
builder: (context, state) => buildGroupHeader(groupIndex, state),
sliver: Obx(() => !controller.groups[groupIndex].isExpanded.value
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) => const SizedBox(),
childCount: 0,
))
: SliverList(
delegate: SliverChildBuilderDelegate(
(context, int i) => buildRow(groupIndex, i),
childCount: !controller.groups[groupIndex].isExpanded.value
? 0
: controller.groups[groupIndex].items.length,
),
)),
);
}
Widget buildGroupHeader(int groupIndex, SliverStickyHeaderState state) {
final titleStyle = TextStyle(
fontSize: ScreenAdaper.height(30),
color: AppTheme.nearlyBlack,
fontWeight: FontWeight.w600);
return Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 235, 235, 235)
.withOpacity(1.0 - state.scrollPercentage),
border: const Border(
top: BorderSide(width: 1, color: AppTheme.dividerColor),
bottom: BorderSide(width: 1, color: AppTheme.dividerColor))),
height: ScreenAdaper.height(80),
alignment: Alignment.centerLeft,
child: InkWell(
onTap: () {
controller.groups[groupIndex].isExpanded.value =
!controller.groups[groupIndex].isExpanded.value;
},
child: Row(
children: [
Obx(() => controller.groups[groupIndex].isExpanded.value
? IconButton(
padding: EdgeInsets.zero,
onPressed: () {
controller.groups[groupIndex].isExpanded.value =
false;
},
icon: const Icon(
Icons.expand_more,
color: AppTheme.nearlyBlack,
),
)
: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
controller.groups[groupIndex].isExpanded.value = true;
},
icon: const Icon(
Icons.chevron_right,
color: AppTheme.nearlyBlack,
),
)),
Text(
controller.groups[groupIndex].name,
style: titleStyle,
),
const Spacer(),
IconButton(
onPressed: () {
ModalUtil.alert(
contentText: '确定要删除此组吗?',
onConfirm: () {
controller.removeGroup(groupIndex);
});
},
icon: const Icon(
Icons.delete_outline,
color: AppTheme.nearlyBlack,
)),
IconButton(
onPressed: () {
controller.addItems(groupIndex);
},
icon: const Icon(
Icons.add,
color: AppTheme.nearlyBlack,
)),
],
)));
}
Widget buildRow(int groupIndex, int rowIndex) {
final subTextStyle =
TextStyle(color: AppTheme.grey, fontSize: ScreenAdaper.height(23));
return Slidable(
key: UniqueKey(),
endActionPane: ActionPane(
extentRatio: 0.2,
motion: const DrawerMotion(),
children: [
SlidableAction(
padding: EdgeInsets.zero,
onPressed: (_) {
controller.removeItem(groupIndex, rowIndex);
},
backgroundColor: AppTheme.dangerColor,
foregroundColor: Colors.white,
icon: Icons.delete,
label: '删除',
),
],
),
child: Container(
padding: EdgeInsets.only(
left: ScreenAdaper.width(20),
top: ScreenAdaper.height(15),
bottom: ScreenAdaper.height(15),
),
constraints: BoxConstraints(minHeight: ScreenAdaper.height(100)),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1, color: Colors.grey[200]!),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
controller.groups[groupIndex].items[rowIndex].name,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: ScreenAdaper.height(30)),
),
),
],
),
if (controller.groups[groupIndex].items[rowIndex]
.componentSpecification !=
null)
Row(
children: [
// Text(
// '规格、型号: ',
// style: subTextStyle,
// ),
Text(
controller.groups[groupIndex].items[rowIndex]
.componentSpecification ??
'',
style: subTextStyle,
),
],
),
if (controller.groups[groupIndex].items[rowIndex].unit !=
null)
Row(
children: [
Text(
'单位: ',
style: subTextStyle,
),
Text(
controller.groups[groupIndex].items[rowIndex].unit ??
'',
style: subTextStyle,
),
],
),
if (controller.groups[groupIndex].items[rowIndex].remark !=
null)
Row(
children: [
Text(
'备注: ',
style: subTextStyle,
),
Expanded(
child: Text(
controller.groups[groupIndex].items[rowIndex]
.remark ??
'',
overflow: TextOverflow.ellipsis,
style: subTextStyle,
),
),
],
),
],
),
),
const VerticalDivider(),
buildEditCell<int>(
Container(
alignment: Alignment.center,
width: ScreenAdaper.width(quantityWidth),
child: Text(
'${controller.groups[groupIndex].items[rowIndex].quantity}',
style: TextStyle(fontSize: ScreenAdaper.height(25)),
),
),
groupIndex: groupIndex,
rowIndex: rowIndex,
field: 'quantity',
inputWidth: ScreenAdaper.width(quantityWidth),
value: controller.groups[groupIndex].items[rowIndex].quantity,
func: (value) {
// 取消失去焦点,并且弹窗警告
// bool isValid = (value ?? 0) > 0;
// if (!isValid) {
// SnackBarUtil().error('数量必须>0,若想删除产品0请左滑');
// }
return true;
}),
buildEditCell<double>(
Container(
alignment: Alignment.center,
width: ScreenAdaper.width(unitPriceWidth),
child: Text(
CommonUtil.toNumberWithoutZero(controller
.groups[groupIndex].items[rowIndex].unitPrice),
style: TextStyle(fontSize: ScreenAdaper.height(25)),
),
),
groupIndex: groupIndex,
rowIndex: rowIndex,
field: 'unitPrice',
inputWidth: ScreenAdaper.width(unitPriceWidth),
value: controller.groups[groupIndex].items[rowIndex].unitPrice),
buildEditCell<double>(
Container(
alignment: Alignment.center,
width: ScreenAdaper.width(amountWidth),
child: Text(
CommonUtil.toNumberWithoutZero(
controller.groups[groupIndex].items[rowIndex].amount),
style: TextStyle(fontSize: ScreenAdaper.height(25)),
),
),
groupIndex: groupIndex,
rowIndex: rowIndex,
field: 'amount',
inputWidth: ScreenAdaper.width(amountWidth),
value: controller.groups[groupIndex].items[rowIndex].amount),
],
),
),
);
}
Widget buildEditCell<T>(
Widget content, {
required int groupIndex,
required int rowIndex,
required String field,
required double inputWidth,
required dynamic value,
Function(dynamic)? func,
}) {
return Obx(() => controller.editingcell[0] == groupIndex &&
controller.editingcell[1] == rowIndex &&
controller.editingcell[2] == field
? SizedBox(
width: inputWidth,
child: SkNumberInput<T>(
hint: '',
isDense: true,
autoFocus: true,
validator: func,
onFieldSubmitted: (value) {
final editingData =
controller.groups[groupIndex].items[rowIndex].toJson();
editingData[field] = value;
controller.afterRowChanges(groupIndex, rowIndex,
SaleQuotationItemModel.fromJson(editingData), field);
},
onTapOutside: (dynamic value) {
value = value ?? 0;
final editingData =
controller.groups[groupIndex].items[rowIndex].toJson();
editingData[field] = value as T;
controller.afterRowChanges(groupIndex, rowIndex,
SaleQuotationItemModel.fromJson(editingData), field);
},
contentPadding: EdgeInsets.symmetric(
horizontal: ScreenAdaper.width(20),
vertical: ScreenAdaper.height(10)),
textController: TextEditingController(
text: value == 0
? ''
: CommonUtil.toNumberWithoutZero(value))),
)
: InkWell(
onTap: () {
controller.editingcell.value = [groupIndex, rowIndex, field];
},
child: content,
));
}
}