mobile_skt/lib/screens/sale_quotation/sale_quotation.dart

571 lines
21 KiB
Dart
Raw Normal View History

import 'dart:ffi';
2024-04-01 14:27:44 +08:00
import 'package:collection/collection.dart';
2024-04-01 08:41:52 +08:00
import 'package:flutter/material.dart';
2024-04-01 14:27:44 +08:00
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/app_theme.dart';
2024-04-01 17:35:34 +08:00
import 'package:sk_base_mobile/models/sale_quotation.model.dart';
import 'package:sk_base_mobile/screens/sale_quotation/components/sale_quotation_drawer.dart';
2024-04-01 17:35:34 +08:00
import 'package:sk_base_mobile/screens/sale_quotation/sale_quotation.controller.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/util/snack_bar.util.dart';
2024-04-01 17:35:34 +08:00
import 'package:sk_base_mobile/widgets/core/sk_number_input.dart';
import 'package:sk_base_mobile/widgets/core/sk_text_input.dart';
2024-04-02 12:24:08 +08:00
import 'package:sk_base_mobile/widgets/empty.dart';
2024-04-01 08:41:52 +08:00
import 'package:sk_base_mobile/widgets/sk_appbar.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:math_expressions/math_expressions.dart';
import 'dart:math' as math;
2024-04-01 08:41:52 +08:00
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 headerTitleStyle = TextStyle(
2024-04-01 17:35:34 +08:00
fontSize: ScreenAdaper.height(30),
fontWeight: FontWeight.w600,
2024-04-01 14:27:44 +08:00
color: AppTheme.nearlyBlack);
final headerBgcolor = const Color.fromARGB(255, 238, 238, 238);
2024-04-01 17:35:34 +08:00
2024-04-01 08:41:52 +08:00
@override
Widget build(BuildContext context) {
return Scaffold(
key: controller.scaffoldKey,
endDrawer: Drawer(
// 从右到左出现
child: SaleQuotationEndDrawer(),
2024-04-02 12:24:08 +08:00
),
2024-04-01 14:27:44 +08:00
appBar: SkAppbar(
title: '报价计算',
action: [
IconButton(
onPressed: () {
controller.scaffoldKey.currentState?.openEndDrawer();
},
icon: const Icon(Icons.more_horiz_outlined, color: AppTheme.white),
)
2024-04-01 14:27:44 +08:00
],
),
2024-04-01 14:59:20 +08:00
body: SafeArea(
bottom: ScreenAdaper.isLandspace() ? false : true,
child: Stack(children: [
Column(
children: [
builderHeader(),
Expanded(
child: Obx(() => controller.groups.isEmpty
? Empty(text: '请先添加分组')
: 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(),
)
],
),
// Positioned(
// bottom: ScreenAdaper.height(10),
// right: ScreenAdaper.height(10),
// child: IconButton(
// padding: EdgeInsets.all(ScreenAdaper.height(20)),
// style: ButtonStyle(
// backgroundColor: MaterialStateProperty.all<Color>(
// AppTheme.primaryColorLight)),
// onPressed: () {
// controller.addGroup();
// },
// icon: Icon(
// Icons.add,
// size: ScreenAdaper.height(40),
// ),
// ))
]),
),
);
}
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]!),
2024-04-01 14:59:20 +08:00
),
),
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),
),
)
],
),
2024-04-01 08:41:52 +08:00
);
}
2024-04-02 12:24:08 +08:00
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(
children: [
Text(
'名称',
style: headerTitleStyle,
),
const Spacer(),
Container(
alignment: Alignment.center,
width: ScreenAdaper.width(quantityWidth),
2024-04-02 12:24:08 +08:00
// constraints: BoxConstraints(minWidth: quantityWidth),
child: Text(
'数量',
style: headerTitleStyle,
),
),
Container(
alignment: Alignment.center,
width: ScreenAdaper.width(unitPriceWidth),
// constraints: BoxConstraints(minWidth: ScreenAdaper.width(unitPriceWidth)),
2024-04-02 12:24:08 +08:00
child: Text(
'单价',
style: headerTitleStyle,
),
),
Container(
alignment: Alignment.center,
width: ScreenAdaper.width(amountWidth),
2024-04-02 12:24:08 +08:00
child: Text(
'总价',
style: headerTitleStyle,
),
),
],
));
}
Widget buildBody(int groupIndex) {
return SliverStickyHeader.builder(
builder: (context, state) => buildGroupHeader(groupIndex, state),
2024-04-02 16:22:21 +08:00
sliver: Obx(() => !controller.groups[groupIndex].isExpanded.value
2024-04-01 14:27:44 +08:00
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) => const SizedBox(),
2024-04-01 14:27:44 +08:00
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(
height: ScreenAdaper.height(80),
color: headerBgcolor.withOpacity(1.0 - state.scrollPercentage),
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(
2024-04-01 14:27:44 +08:00
padding: EdgeInsets.zero,
onPressed: () {
controller.groups[groupIndex].isExpanded.value =
false;
2024-04-02 16:22:21 +08:00
},
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.confirm(
title: '确定要删除此组吗?',
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(25));
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)),
),
2024-04-01 14:27:44 +08:00
),
],
),
if (controller.groups[groupIndex].items[rowIndex].spec !=
null)
Row(
children: [
Text(
'规格、型号: ',
style: subTextStyle,
),
Text(
controller.groups[groupIndex].items[rowIndex].spec ??
'',
style: subTextStyle,
),
],
2024-04-01 14:27:44 +08:00
),
if (controller.groups[groupIndex].items[rowIndex].unit !=
null)
Row(
children: [
Text(
'单位: ',
style: subTextStyle,
),
Text(
controller.groups[groupIndex].items[rowIndex].unit ??
'',
style: subTextStyle,
),
],
2024-04-01 14:27:44 +08:00
),
if (controller.groups[groupIndex].items[rowIndex].remark !=
null)
Row(
2024-04-01 14:27:44 +08:00
children: [
Text(
'备注: ',
style: subTextStyle,
),
2024-04-01 14:27:44 +08:00
Expanded(
child: Text(
'${controller.groups[groupIndex].items[rowIndex].remark ?? ''}${controller.groups[groupIndex].items[rowIndex].remark ?? ''}${controller.products[rowIndex].remark ?? ''}',
overflow: TextOverflow.ellipsis,
style: subTextStyle,
),
2024-04-01 14:27:44 +08:00
),
],
),
],
),
),
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)),
2024-04-01 14:27:44 +08:00
),
),
groupIndex: groupIndex,
rowIndex: rowIndex,
field: 'quantity',
inputWidth: ScreenAdaper.width(quantityWidth),
value: controller.groups[groupIndex].items[rowIndex].quantity,
func: (value) {
// 取消失去焦点,并且弹窗警告
bool isValid = value > 0;
if (!isValid) {
SnackBarUtil().error('数量必须>0,若想删除产品0请左滑');
}
return isValid;
}),
buildEditCell<int>(
Container(
alignment: Alignment.center,
width: ScreenAdaper.width(unitPriceWidth),
child: Text(
'${controller.groups[groupIndex].items[rowIndex].cost}',
style: TextStyle(fontSize: ScreenAdaper.height(25)),
),
),
groupIndex: groupIndex,
rowIndex: rowIndex,
field: 'cost',
inputWidth: ScreenAdaper.width(unitPriceWidth),
value: controller.groups[groupIndex].items[rowIndex].cost),
buildEditCell<int>(
Container(
alignment: Alignment.center,
width: ScreenAdaper.width(amountWidth),
child: Text(
'${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),
],
),
),
);
2024-04-01 08:41:52 +08:00
}
2024-04-01 14:27:44 +08:00
Widget buildEditCell<T>(
Widget content, {
required int groupIndex,
required int rowIndex,
required String field,
required double inputWidth,
required dynamic value,
Function(dynamic)? func,
}) {
2024-04-01 17:35:34 +08:00
return Obx(() => controller.editingcell[0] == groupIndex &&
controller.editingcell[1] == rowIndex &&
controller.editingcell[2] == field
? SizedBox(
2024-04-01 17:35:34 +08:00
width: inputWidth,
child: SkNumberInput<T>(
2024-04-01 17:35:34 +08:00
hint: '',
isDense: true,
autoFocus: true,
validator: func,
onFieldSubmitted: (value) {
final editingData =
controller.groups[groupIndex].items[rowIndex].toJson();
editingData[field] = value as T;
controller.saveChanges(groupIndex, rowIndex,
SaleQuotationItemModel.fromJson(editingData), field);
},
onTapOutside: (dynamic value) {
final editingData =
controller.groups[groupIndex].items[rowIndex].toJson();
editingData[field] = value as T;
controller.saveChanges(groupIndex, rowIndex,
SaleQuotationItemModel.fromJson(editingData), field);
},
2024-04-01 17:35:34 +08:00
contentPadding: EdgeInsets.symmetric(
horizontal: ScreenAdaper.width(20),
vertical: ScreenAdaper.height(10)),
textController:
TextEditingController(text: '${value == 0 ? '' : value}')),
2024-04-01 17:35:34 +08:00
)
: InkWell(
onTap: () {
controller.editingcell.value = [groupIndex, rowIndex, field];
},
child: content,
));
2024-04-01 14:27:44 +08:00
}
}