import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/company_gateway_model.dart'; import 'package:invoiceninja_flutter/data/models/company_model.dart'; import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/data/models/gateway_token_model.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/ui/app/copy_to_clipboard.dart'; import 'package:invoiceninja_flutter/ui/app/entity_header.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/app/forms/app_tab_bar.dart'; import 'package:invoiceninja_flutter/ui/app/icon_text.dart'; import 'package:invoiceninja_flutter/ui/app/portal_links.dart'; import 'package:invoiceninja_flutter/ui/client/view/client_view_activity.dart'; import 'package:invoiceninja_flutter/ui/client/view/client_view_documents.dart'; import 'package:invoiceninja_flutter/ui/client/view/client_view_ledger.dart'; import 'package:invoiceninja_flutter/ui/client/view/client_view_payment_methods.dart'; import 'package:invoiceninja_flutter/ui/client/view/client_view_system_logs.dart'; import 'package:invoiceninja_flutter/ui/client/view/client_view_vm.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/strings.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; class ClientViewFullwidth extends StatefulWidget { const ClientViewFullwidth({ Key? key, required this.viewModel, }) : super(key: key); final ClientViewVM viewModel; @override State createState() => _ClientViewFullwidthState(); } class _ClientViewFullwidthState extends State with TickerProviderStateMixin { ScrollController? _scrollController1; ScrollController? _scrollController2; ScrollController? _scrollController3; @override void initState() { super.initState(); _scrollController1 = ScrollController(); _scrollController2 = ScrollController(); _scrollController3 = ScrollController(); } @override void dispose() { _scrollController1!.dispose(); _scrollController2!.dispose(); _scrollController3!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final localization = AppLocalization.of(context); final store = StoreProvider.of(context); final state = store.state; final company = state.company; final client = state.clientState.get(state.uiState.filterEntityId!); final documents = client.documents; final viewModel = widget.viewModel; final billingAddress = formatAddress(state, object: client); final shippingAddress = formatAddress(state, object: client, isShipping: true); final hasMultipleContacts = client.contacts.length > 1; final showStanding = !state.prefState.isPreviewVisible && !state.uiState.isEditing && state.prefState.isModuleTable; // Group gateway tokens by the customerReference final tokenMap = >{}; final gatewayMap = {}; final linkMap = {}; client.gatewayTokens.forEach((gatewayToken) { final companyGateway = state.companyGatewayState.get(gatewayToken.companyGatewayId); if (companyGateway.isOld && !companyGateway.isDeleted!) { final customerReference = gatewayToken.customerReference; gatewayMap[customerReference] = companyGateway; final clientUrl = GatewayEntity.getClientUrl( gatewayId: companyGateway.gatewayId, customerReference: customerReference, ); if (clientUrl != null) { linkMap[customerReference] = clientUrl; } if (tokenMap.containsKey(customerReference)) { tokenMap[customerReference]!.add(gatewayToken); } else { tokenMap[customerReference] = [gatewayToken]; } } }); return LayoutBuilder(builder: (context, layout) { final minHeight = layout.maxHeight - (kMobileDialogPadding * 2) - 43; return Row( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: FormCard( isLast: true, constraints: BoxConstraints(minHeight: minHeight), crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.only( top: kMobileDialogPadding, right: kMobileDialogPadding / 2, bottom: kMobileDialogPadding, left: kMobileDialogPadding), child: ListView( controller: _scrollController1, children: [ Text( localization!.details, style: Theme.of(context).textTheme.titleLarge, ), SizedBox(height: 8), if (client.isTaxExempt) Text(localization.isTaxExempt), if (client.paymentBalance != 0) Text(localization.payments + ': ' + formatNumber(client.paymentBalance, context, clientId: client.id)!), if (client.creditBalance != 0) Text(localization.credit + ': ' + formatNumber(client.creditBalance, context, clientId: client.id)!), if (client.idNumber.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 1), child: CopyToClipboard( value: client.idNumber, prefix: localization.id, ), ), if (client.vatNumber.isNotEmpty) CopyToClipboard( value: client.vatNumber, prefix: localization.vat, ), SizedBox(height: 4), if (client.phone.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: CopyToClipboard( value: client.phone, child: IconText(icon: Icons.phone, text: client.phone), ), ), if (client.website.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: InkWell( onTap: () => launchUrl(Uri.parse(untrimUrl(client.website))), child: IconText( icon: MdiIcons.earth, text: trimUrl(client.website), ), ), ), SizedBox(height: 4), if (client.currencyId != state.company.currencyId) Padding( padding: const EdgeInsets.only(bottom: 1), child: Text( state.staticState.currencyMap[client.currencyId] ?.name ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if ((client.languageId ?? '').isNotEmpty && client.languageId != state.company.languageId) Padding( padding: const EdgeInsets.only(bottom: 1), child: Text( state.staticState.languageMap[client.languageId] ?.name ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if ((client.settings.defaultTaskRate ?? 0) != 0) Text( '${localization.taskRate}: ${client.settings.defaultTaskRate}'), if (client.customValue1.isNotEmpty) Text(company.formatCustomFieldValue( CustomFieldType.client1, client.customValue1)), if (client.customValue2.isNotEmpty) Text(company.formatCustomFieldValue( CustomFieldType.client2, client.customValue2)), if (client.customValue3.isNotEmpty) Text(company.formatCustomFieldValue( CustomFieldType.client3, client.customValue3)), if (client.customValue4.isNotEmpty) Text(company.formatCustomFieldValue( CustomFieldType.client4, client.customValue4)), ], ), ), ), Expanded( child: FormCard( isLast: true, constraints: BoxConstraints(minHeight: minHeight), crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.only( top: kMobileDialogPadding, right: kMobileDialogPadding / 2, bottom: kMobileDialogPadding, left: kMobileDialogPadding / 2), child: ListView( controller: _scrollController2, children: [ Text( localization.address, style: Theme.of(context).textTheme.titleLarge, ), SizedBox(height: 8), if (billingAddress.isNotEmpty) ...[ Text( localization.billingAddress, style: TextStyle(color: Colors.grey), ), Row( children: [ Expanded( child: CopyToClipboard( value: billingAddress, child: Row( children: [ Flexible(child: Text(billingAddress)), ], ), ), ), SizedBox(width: 8), IconButton( onPressed: () { launchUrl(Uri( scheme: 'https', host: 'maps.google.com', queryParameters: { 'daddr': billingAddress })); }, icon: Icon(Icons.map), tooltip: state.prefState.enableTooltips ? localization.viewMap : '', ) ], ), SizedBox(height: 8), ], if (shippingAddress.isNotEmpty) ...[ Text( localization.shippingAddress, style: TextStyle(color: Colors.grey), ), Row( children: [ Expanded( child: CopyToClipboard( value: shippingAddress, child: Row( children: [ Flexible(child: Text(shippingAddress)), ], ), ), ), SizedBox(width: 8), IconButton( onPressed: () { launchUrl(Uri( scheme: 'https', host: 'maps.google.com', queryParameters: { 'daddr': shippingAddress })); }, icon: Icon(Icons.map)), ], ), SizedBox(height: 8), ], if (client.publicNotes.isNotEmpty) CopyToClipboard(value: client.publicNotes), ], ), )), Expanded( child: FormCard( isLast: true, constraints: BoxConstraints(minHeight: minHeight), crossAxisAlignment: CrossAxisAlignment.start, padding: EdgeInsets.only( top: kMobileDialogPadding, right: kMobileDialogPadding / (!showStanding ? 1 : 2), bottom: kMobileDialogPadding, left: kMobileDialogPadding / 2), child: Scrollbar( thumbVisibility: true, controller: _scrollController3, child: ListView( controller: _scrollController3, children: [ Text( localization.contacts + (hasMultipleContacts ? ' (${client.contacts.length})' : ''), style: Theme.of(context).textTheme.titleLarge, ), SizedBox(height: 8), ...client.contacts.map((contact) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( contact.fullName, style: Theme.of(context).textTheme.titleMedium, ), if (contact.email.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: CopyToClipboard( value: contact.email, child: IconText( icon: Icons.email, text: contact.email), ), ), if (contact.phone.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: CopyToClipboard( value: contact.phone, child: IconText( icon: Icons.phone, text: contact.phone), ), ), if (company.hasCustomField( CustomFieldType.contact1) && contact.customValue1.isNotEmpty) Text(company.formatCustomFieldValue( CustomFieldType.contact1, contact.customValue1)), if (company.hasCustomField( CustomFieldType.contact2) && contact.customValue2.isNotEmpty) Text(company.formatCustomFieldValue( CustomFieldType.contact2, contact.customValue2)), if (company.hasCustomField( CustomFieldType.contact3) && contact.customValue3.isNotEmpty) Text(company.formatCustomFieldValue( CustomFieldType.contact3, contact.customValue3)), if (company.hasCustomField( CustomFieldType.contact4) && contact.customValue4.isNotEmpty) Text(company.formatCustomFieldValue( CustomFieldType.contact4, contact.customValue4)), SizedBox(height: 8), if (!hasMultipleContacts) ...[ PortalLinks( viewLink: contact.silentLink, copyLink: contact.link, client: client, style: PortalLinkStyle.buttons, ), SizedBox(height: 16), ] else SizedBox(height: 8), ], ), ), if (hasMultipleContacts) PortalLinks( client: client, viewLink: contact.silentLink, copyLink: contact.link, style: PortalLinkStyle.dropdown, ) ], ); }).toList(), ], ), ), )), if (showStanding) Expanded( flex: 2, child: FormCard( isLast: true, crossAxisAlignment: CrossAxisAlignment.start, constraints: BoxConstraints(minHeight: minHeight, maxHeight: 600), padding: const EdgeInsets.only( top: kMobileDialogPadding, right: kMobileDialogPadding, bottom: kMobileDialogPadding, left: kMobileDialogPadding / 2), child: DefaultTabController( length: company.isModuleEnabled(EntityType.document) ? 5 : 4, child: SizedBox( height: minHeight, child: Column( mainAxisSize: MainAxisSize.min, children: [ AppTabBar( isScrollable: true, tabs: [ Tab( child: Text(localization.standing), ), if (tokenMap.keys.isNotEmpty) Tab( text: '${localization.paymentMethods} (${tokenMap.keys.length})', ), if (company.isModuleEnabled(EntityType.document)) Tab( text: documents.isEmpty ? localization.documents : '${localization.documents} (${documents.length})', ), Tab( text: localization.ledger, ), Tab( text: localization.activity, ), Tab( text: localization.systemLogs, ), ], ), Flexible( child: TabBarView( children: [ ListView( children: [ EntityHeader( entity: client, label: localization.paidToDate, value: formatNumber( client.paidToDate, context, clientId: client.id), secondLabel: localization.balanceDue, secondValue: formatNumber( client.balance, context, clientId: client.id), ), if (client.privateNotes.isNotEmpty) IconText( text: client.privateNotes, icon: Icons.lock, copyToClipboard: true, ) ], ), if (tokenMap.keys.isNotEmpty) RefreshIndicator( onRefresh: () => viewModel.onRefreshed(context), child: ClientViewPaymentMethods( viewModel: viewModel, key: ValueKey(viewModel.client.id), gatewayMap: gatewayMap, linkMap: linkMap, tokenMap: tokenMap, ), ), if (company.isModuleEnabled(EntityType.document)) RefreshIndicator( onRefresh: () => viewModel.onRefreshed(context), child: ClientViewDocuments( viewModel: viewModel, key: ValueKey(viewModel.client.id), ), ), RefreshIndicator( onRefresh: () => viewModel.onRefreshed(context), child: ClientViewLedger( viewModel: viewModel, key: ValueKey(viewModel.client.id), ), ), RefreshIndicator( onRefresh: () => viewModel.onRefreshed(context), child: ClientViewActivity( viewModel: viewModel, key: ValueKey(viewModel.client.id), ), ), RefreshIndicator( onRefresh: () => viewModel.onRefreshed(context), child: ClientViewSystemLogs( viewModel: viewModel, key: ValueKey(viewModel.client.id), ), ) ], ), ), ], ), ), ), ), ), ], ); }); } }