diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index c8f7095..60b119d 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -8,13 +8,13 @@ class AppConfig { // این متد بر اساس محیط فعال، URL مناسب را برمی‌گرداند. static String get apiBaseUrl { - // return 'http://10.0.2.2:8081'; + return 'http://10.0.2.2:8081'; // return '192.168.1.103:8081'; // if (isDevelopment) { // // Handle different platforms for local development // if (kIsWeb) { // For web, use localhost - return 'https://app.opplens.com'; + // return 'https://app.opplens.com'; // return 'http://localhost:8081'; // } else { // For Android emulator, use 10.0.2.2 (special IP for host machine) diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart index 401d615..fd5edb0 100644 --- a/lib/core/routes/app_routes.dart +++ b/lib/core/routes/app_routes.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:tm_app/views/completion_of_documents/pages/m_completion_of_documents_page.dart'; import 'package:tm_app/views/detail/pages/detail_screen.dart'; import 'package:tm_app/views/home/pages/home_screen.dart'; import 'package:tm_app/views/liked_tenders/pages/liked_tenders_screen.dart'; @@ -182,6 +183,16 @@ class LikedTendersRouteData extends GoRouteData with _$LikedTendersRouteData { } } +@TypedGoRoute(path: '/completion-of-documents') +class CompletionOfDocumentsMobileRouteData extends GoRouteData with _$CompletionOfDocumentsMobileRouteData { + const CompletionOfDocumentsMobileRouteData(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return CompletionOfDocumentsMobilePage(); + } +} + @TypedGoRoute(path: '/tender-detail') class TenderDetailRouteData extends GoRouteData with _$TenderDetailRouteData { final String tenderId; diff --git a/lib/core/routes/app_routes.g.dart b/lib/core/routes/app_routes.g.dart index 225597b..778d11f 100644 --- a/lib/core/routes/app_routes.g.dart +++ b/lib/core/routes/app_routes.g.dart @@ -11,6 +11,7 @@ List get $appRoutes => [ $appShellRouteData, $yourTendersRouteData, $likedTendersRouteData, + $completionOfDocumentsMobileRouteData, $tenderDetailRouteData, $forgotPasswordRouteData, ]; @@ -228,6 +229,33 @@ mixin _$LikedTendersRouteData on GoRouteData { void replace(BuildContext context) => context.replace(location); } +RouteBase get $completionOfDocumentsMobileRouteData => GoRouteData.$route( + path: '/completion-of-documents', + + factory: _$CompletionOfDocumentsMobileRouteData._fromState, +); + +mixin _$CompletionOfDocumentsMobileRouteData on GoRouteData { + static CompletionOfDocumentsMobileRouteData _fromState(GoRouterState state) => + const CompletionOfDocumentsMobileRouteData(); + + @override + String get location => GoRouteData.$location('/completion-of-documents'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + RouteBase get $tenderDetailRouteData => GoRouteData.$route( path: '/tender-detail', diff --git a/lib/core/theme/colors.dart b/lib/core/theme/colors.dart index 0d87b8e..615afb1 100644 --- a/lib/core/theme/colors.dart +++ b/lib/core/theme/colors.dart @@ -113,6 +113,9 @@ class AppColors { ? const Color.fromRGBO(131, 120, 105, 0.2) : const Color(0xFFFFF2DE); + static Color get orange0 => + _isDarkMode ? const Color(0xFF8379694D) : const Color(0xFFFEFBF8); + static Color get white => _isDarkMode ? const Color(0xFFFFFFFF) : const Color(0xFFFFFFFF); @@ -140,6 +143,12 @@ class AppColors { static Color get error => _isDarkMode ? const Color(0xFFFF3232) : const Color(0xFFC02525); + static Color get purple => + _isDarkMode ? const Color(0xFF270059) : const Color(0xFF270059); + + static Color get purpleLight => + _isDarkMode ? const Color(0xFFEAE6EF) : const Color(0xFFEAE6EF); + static Color get red0 => _isDarkMode ? const Color(0xFF312F2F) diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 107333f..a59db8c 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -47,18 +47,27 @@ class $AssetsIconsGen { /// File path: assets/icons/close-circle.svg String get closeCircle => 'assets/icons/close-circle.svg'; + /// File path: assets/icons/danger.svg + String get danger => 'assets/icons/danger.svg'; + /// File path: assets/icons/dislike.svg String get dislike => 'assets/icons/dislike.svg'; /// File path: assets/icons/edit.svg String get edit => 'assets/icons/edit.svg'; + /// File path: assets/icons/filter.svg + String get filter => 'assets/icons/filter.svg'; + /// File path: assets/icons/home.svg String get home => 'assets/icons/home.svg'; /// File path: assets/icons/home_active.svg String get homeActive => 'assets/icons/home_active.svg'; + /// File path: assets/icons/icon_contracts.svg + String get iconContracts => 'assets/icons/icon_contracts.svg'; + /// File path: assets/icons/like.svg String get like => 'assets/icons/like.svg'; @@ -80,6 +89,12 @@ class $AssetsIconsGen { /// File path: assets/icons/notification.svg String get notification => 'assets/icons/notification.svg'; + /// File path: assets/icons/notify_icon.svg + String get notifyIcon => 'assets/icons/notify_icon.svg'; + + /// File path: assets/icons/notify_icon_active.svg + String get notifyIconActive => 'assets/icons/notify_icon_active.svg'; + /// File path: assets/icons/profile-circle.svg String get profileCircle => 'assets/icons/profile-circle.svg'; @@ -112,7 +127,10 @@ class $AssetsIconsGen { String get thumbLike => 'assets/icons/thumb_like.svg'; /// File path: assets/icons/tick-circle.svg - String get tickCircle => 'assets/icons/tick-circle.svg'; + String get tickCircleSvg => 'assets/icons/tick-circle.svg'; + + /// File path: assets/icons/tick_circle.svg + String get tickCircleSvg_ => 'assets/icons/tick_circle.svg'; /// File path: assets/icons/tick.svg String get tick => 'assets/icons/tick.svg'; @@ -136,10 +154,13 @@ class $AssetsIconsGen { arrows, calendar, closeCircle, + danger, dislike, edit, + filter, home, homeActive, + iconContracts, like, location, logoBig, @@ -147,6 +168,8 @@ class $AssetsIconsGen { menu, moon, notification, + notifyIcon, + notifyIconActive, profileCircle, profileCircleActive, shield, @@ -157,7 +180,8 @@ class $AssetsIconsGen { tenderLogoSvg, thumb, thumbLike, - tickCircle, + tickCircleSvg, + tickCircleSvg_, tick, trailingIcon, trash, diff --git a/lib/views/completion_of_documents/pages/m_completion_of_documents_page.dart b/lib/views/completion_of_documents/pages/m_completion_of_documents_page.dart new file mode 100644 index 0000000..6c1f4a2 --- /dev/null +++ b/lib/views/completion_of_documents/pages/m_completion_of_documents_page.dart @@ -0,0 +1,327 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:tm_app/core/constants/assets.dart'; +import 'package:tm_app/core/theme/colors.dart'; +import 'package:tm_app/core/utils/size_config.dart'; +import 'package:tm_app/views/completion_of_documents/strings/completion_of_documents_string.dart'; +import 'package:tm_app/views/shared/base_button.dart'; +import 'package:tm_app/views/shared/tender_app_bar.dart'; + +class CompletionOfDocumentsMobilePage extends StatelessWidget { + const CompletionOfDocumentsMobilePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.backgroundColor, + appBar: appBar( + context: context, + title: CompletionOfDocumentsStrings.completionOfDocuments, + ), + body: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 16.0.w(), vertical: 12.0.h()), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 24.0.h(), + padding: EdgeInsets.symmetric(horizontal: 8.0.w()), + decoration: BoxDecoration( + color: AppColors.primary20, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + CompletionOfDocumentsStrings.submissionDeadline, + style: TextStyle( + color: AppColors.textBlue, + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + ), + ), + Text( + '2025-06-15', + style: TextStyle( + color: AppColors.grey80, + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + SizedBox(height: 12.0.h()), + Container( + height: 58.0.h(), + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 12.0.w(), + vertical: 8.0.h(), + ), + decoration: BoxDecoration( + border: Border.all(color: AppColors.orange10), + borderRadius: BorderRadius.circular(8), + color: AppColors.orange0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Match with your profile', + style: TextStyle( + fontSize: 12.0.sp(), + color: AppColors.grey80, + fontWeight: FontWeight.w400, + ), + ), + Spacer(), + Text( + '75%', + style: TextStyle( + fontSize: 12.0.sp(), + color: AppColors.cyanTeal, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + SizedBox(height: 4.0.h()), + Row( + children: [ + Expanded( + child: LinearProgressIndicator( + value: 0.75, + backgroundColor: Color(0xFFE0E0E0), + color: AppColors.cyanTeal, + minHeight: 6, + borderRadius: BorderRadius.circular(10), + ), + ), + ], + ), + ], + ), + ), + SizedBox(height: 16.0.h()), + _docCard( + title: 'Company Registration Certificate', + subtitle: 'Legal document proving business registration', + status: 'Completed', + statusColor: AppColors.green30, + statusBackgroundColor: AppColors.green10, + backgroundColor: AppColors.primary10, + borderSideColor: AppColors.primary20, + ), + _docCard( + title: 'Tax Compliance Certificate', + subtitle: 'Proof of tax registration and compliance', + status: 'Completed', + statusColor: AppColors.green30, + statusBackgroundColor: AppColors.green10, + backgroundColor: AppColors.primary10, + borderSideColor: AppColors.primary30, + ), + _docCard( + title: 'Technical Proposal', + subtitle: 'Detailed technical solution for the tender', + status: 'Incomplete', + statusColor: AppColors.red20, + statusBackgroundColor: AppColors.red10, + backgroundColor: AppColors.red0, + tag: 'Self apply', + tagColor: AppColors.green30, + borderSideColor: AppColors.red10, + tagBackgroundColor: AppColors.green20, + ), + _docCard( + title: 'Financial Proposal', + subtitle: 'Detailed cost breakdown and pricing', + status: 'Incomplete', + statusColor: AppColors.red20, + statusBackgroundColor: AppColors.red10, + backgroundColor: AppColors.red0, + tag: 'Partnership', + tagColor: AppColors.purple, + borderSideColor: AppColors.red10, + tagBackgroundColor: AppColors.purpleLight, + ), + _docCard( + title: 'Financial Proposal', + subtitle: 'Detailed cost breakdown and pricing', + status: 'Incomplete', + statusColor: AppColors.red20, + statusBackgroundColor: AppColors.red10, + backgroundColor: AppColors.red0, + borderSideColor: AppColors.red10, + ), + SizedBox(height: 16.0.h()), + + Text( + CompletionOfDocumentsStrings.setDeadline, + style: TextStyle( + fontSize: 16.0.sp(), + color: AppColors.grey80, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8.0.h()), + Container( + height: 48.0.h(), + padding: EdgeInsets.symmetric(horizontal: 12.0.w()), + decoration: BoxDecoration( + border: Border.all(color: AppColors.grey40), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '2025/04/10', + style: TextStyle( + fontSize: 16.0.sp(), + fontWeight: FontWeight.w700, + color: AppColors.grey60, + ), + ), + SvgPicture.asset( + AssetsManager.calendar, + height: 24.0.h(), + width: 24.0.w(), + ), + ], + ), + ), + SizedBox(height: 16.0.h()), + + Container( + padding: EdgeInsets.all(12), + height: 100.0.h(), + decoration: BoxDecoration( + border: Border.all(color: AppColors.borderColor), + borderRadius: BorderRadius.circular(8), + color: AppColors.grey0, + ), + child: TextField( + maxLines: null, + decoration: InputDecoration.collapsed( + hintText: CompletionOfDocumentsStrings.note, + ), + style: TextStyle( + fontSize: 16.0.sp(), + color: AppColors.grey60, + fontWeight: FontWeight.w400, + ), + ), + ), + SizedBox(height: 24.0.h()), + BaseButton( + isEnabled: true, + onPressed: () {}, + text: CompletionOfDocumentsStrings.submitForCompletion, + textColor: AppColors.textBlue, + backgroundColor: AppColors.primary30, + ), + ], + ), + ), + ); + } + + Widget _docCard({ + required String title, + required String subtitle, + required String status, + required Color statusColor, + required Color backgroundColor, + required Color borderSideColor, + required Color statusBackgroundColor, + String? tag, + Color? tagColor, + Color? tagBackgroundColor, + }) { + return Container( + width: double.infinity, + margin: EdgeInsets.only(bottom: 12.0.h()), + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: backgroundColor, + border: Border( + left: BorderSide(color: borderSideColor, width: 2.0.w()), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (tag != null) ...[ + Spacer(), + Container( + margin: EdgeInsets.only(right: 8), + height: 32.0.h(), + width: 91.0.w(), + decoration: BoxDecoration( + color: tagBackgroundColor, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + tag, + style: TextStyle( + fontSize: 12.0.sp(), + fontWeight: FontWeight.w500, + color: tagColor, + ), + ), + ), + ), + ], + if (tag == null) Spacer(), + Container( + height: 32.0.h(), + width: 91.0.w(), + decoration: BoxDecoration( + color: statusBackgroundColor, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + status, + style: TextStyle( + fontSize: 12.0.sp(), + fontWeight: FontWeight.w500, + color: statusColor, + ), + ), + ), + ), + ], + ), + SizedBox(height: 8.0.h()), + Text( + title, + style: TextStyle( + fontSize: 20.0.sp(), + fontWeight: FontWeight.w600, + color: AppColors.grey70, + ), + ), + SizedBox(height: 4.0.h()), + Text( + subtitle, + style: TextStyle( + fontSize: 16.0.sp(), + color: AppColors.grey, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/completion_of_documents/strings/completion_of_documents_string.dart b/lib/views/completion_of_documents/strings/completion_of_documents_string.dart new file mode 100644 index 0000000..14af095 --- /dev/null +++ b/lib/views/completion_of_documents/strings/completion_of_documents_string.dart @@ -0,0 +1,10 @@ +class CompletionOfDocumentsStrings { + CompletionOfDocumentsStrings._(); + + static const String completionOfDocuments = 'Completion of documents'; + static const String submissionDeadline = 'Submission deadline:'; + static const String setDeadline = + 'Set Deadline for Completion of documents :'; + static const String note = 'Note:'; + static const String submitForCompletion = 'Submit for completion'; +} diff --git a/lib/views/detail/strings/tender_details_strings.dart b/lib/views/detail/strings/tender_details_strings.dart index 5713b88..53c02b5 100644 --- a/lib/views/detail/strings/tender_details_strings.dart +++ b/lib/views/detail/strings/tender_details_strings.dart @@ -31,4 +31,6 @@ class TenderDetailsStrings { static const String tenderUnsubmittedSuccessfully = 'tender unsubmitted successfully!'; static const String tenderNoData = 'No tender details available'; + static const String meetingTime = 'Meeting Time'; + static const String apply = 'Apply'; } diff --git a/lib/views/detail/widgets/select_meeting_time_bottom_sheet.dart b/lib/views/detail/widgets/select_meeting_time_bottom_sheet.dart new file mode 100644 index 0000000..5b0e7f4 --- /dev/null +++ b/lib/views/detail/widgets/select_meeting_time_bottom_sheet.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:tm_app/core/routes/app_routes.dart'; +import 'package:tm_app/core/theme/colors.dart'; +import 'package:tm_app/core/utils/size_config.dart'; +import 'package:tm_app/views/detail/strings/tender_details_strings.dart'; + +class SelectMeetingTimeBottomSheet extends StatefulWidget { + const SelectMeetingTimeBottomSheet({required this.onConfirm, super.key}); + final ValueChanged onConfirm; + + @override + State createState() => + _SelectMeetingTimeBottomSheetState(); +} + +class _SelectMeetingTimeBottomSheetState + extends State { + int selectedType = 0; + late String type = ''; + + final List> times = [ + {'title': 'Sun 2025-06-15', 'value': 'mode1', 'time': '10:00'}, + {'title': 'Sun 2025-06-16', 'value': 'mode2', 'time': '12:00'}, + {'title': 'Sun 2025-06-17', 'value': 'mode3', 'time': '14:00'}, + {'title': 'Sun 2025-06-18', 'value': 'mode4', 'time': '16:00'}, + {'title': 'Sun 2025-06-19', 'value': 'mode5', 'time': '18:00'}, + ]; + + @override + Widget build(BuildContext context) { + return Wrap( + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.fromLTRB(24.0.w(), 24.0.h(), 24.0.w(), 33.0.h()), + decoration: BoxDecoration( + color: AppColors.backgroundColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + ), + ), + child: Column( + children: [ + Text( + TenderDetailsStrings.meetingTime, + style: TextStyle( + fontSize: 20.0.sp(), + fontWeight: FontWeight.w600, + color: AppColors.grey70, + ), + ), + SizedBox(height: 15.0.h()), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: times.length, + itemBuilder: (context, index) { + final item = times[index]; + return Padding( + padding: EdgeInsets.only(bottom: 8.0.h()), + child: _submissionType( + title: item['title']!, + time: item['time']!, + isSelected: type == item['value'], + onTap: () { + selectedType = index; + type = item['value']!; + setState(() {}); + }, + ), + ); + }, + ), + SizedBox(height: 8.0.h()), + SizedBox(height: 8.0.h()), + InkWell( + onTap: () { + CompletionOfDocumentsMobileRouteData().push(context); + //context.pop(); + }, + child: Container( + width: double.infinity, + height: 56.0.h(), + decoration: BoxDecoration( + color: AppColors.primary20, + borderRadius: BorderRadius.circular(100), + ), + alignment: Alignment.center, + child: Text( + TenderDetailsStrings.apply, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.mainBlue, + ), + ), + ), + ), + ], + ), + ), + ], + ); + } + + Widget _submissionType({ + required VoidCallback onTap, + required String title, + required String time, + required bool isSelected, + }) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.0), + border: Border.all( + color: isSelected ? AppColors.mainBlue : AppColors.grey40, + ), + ), + child: InkWell( + onTap: onTap, + child: SizedBox( + width: double.infinity, + height: 40.0.h(), + child: Row( + children: [ + Container( + width: 20.0.w(), + height: 20.0.w(), + margin: EdgeInsets.symmetric( + horizontal: 10.0.w(), + vertical: 10.0.h(), + ), + padding: EdgeInsets.all(3), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? AppColors.mainBlue : AppColors.grey70, + width: 2.0, + ), + ), + alignment: Alignment.center, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? AppColors.mainBlue : Colors.transparent, + ), + ), + ), + Expanded( + child: Row( + children: [ + Text( + title, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.grey80, + ), + ), + Spacer(), + Text( + time, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.grey80, + ), + ), + SizedBox(width: 10.0.w()), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/detail/widgets/tender_detail_action.dart b/lib/views/detail/widgets/tender_detail_action.dart index 79050f6..33793c5 100644 --- a/lib/views/detail/widgets/tender_detail_action.dart +++ b/lib/views/detail/widgets/tender_detail_action.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/view_models/tender_detail_view_model.dart'; +import 'package:tm_app/views/detail/widgets/select_meeting_time_bottom_sheet.dart'; import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/shared/select_submission_bottom_sheet.dart'; @@ -49,24 +50,29 @@ class TenderDetailActions extends StatelessWidget { backgroundColor: AppColors.primary30, textColor: AppColors.mainBlue, onPressed: () { - if (viewModel.status == - TenderApprovalStatus.submitted.value) { - viewModel.submitTenderApproval( - tenderId: detail.id!, - submissionMode: - TenderSubmissionMode.selfApply.value, - ); - } else { + { showModalBottomSheet( isDismissible: true, context: context, - builder: (context) { + builder: (bottomSheetContext) { return SelectSubmissionBottomSheet( onConfirm: (value) { - viewModel.submitTenderApproval( - tenderId: detail.id!, - submissionMode: value, - ); + // viewModel.submitTenderApproval( + // tenderId: detail.id!, + // submissionMode: value, + // ); + Future.delayed(Duration.zero, () { + showModalBottomSheet( + isDismissible: true, + // ignore: use_build_context_synchronously + context: context, + builder: (context) { + return SelectMeetingTimeBottomSheet( + onConfirm: (meetingTime) {}, + ); + }, + ); + }); }, ); }, diff --git a/pubspec.lock b/pubspec.lock index 68cbca8..59ac43a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -316,7 +316,7 @@ packages: source: sdk version: "0.0.0" flutter_web_plugins: - dependency: transitive + dependency: "direct main" description: flutter source: sdk version: "0.0.0" @@ -909,14 +909,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - url_strategy: - dependency: "direct main" - description: - name: url_strategy - sha256: "42b68b42a9864c4d710401add17ad06e28f1c1d5500c93b98c431f6b0ea4ab87" - url: "https://pub.dev" - source: hosted - version: "0.2.0" uuid: dependency: transitive description: