546 lines
24 KiB
Dart
546 lines
24 KiB
Dart
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<ClientViewFullwidth> createState() => _ClientViewFullwidthState();
|
|
}
|
|
|
|
class _ClientViewFullwidthState extends State<ClientViewFullwidth>
|
|
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<AppState>(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 = <String, List<GatewayTokenEntity>>{};
|
|
final gatewayMap = <String, CompanyGatewayEntity>{};
|
|
final linkMap = <String, String>{};
|
|
|
|
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: <String, dynamic>{
|
|
'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: <String, dynamic>{
|
|
'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),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
}
|