Add Docker support and enhance data models for tender details

- Introduced a .dockerignore file to optimize Docker builds by excluding unnecessary files.
- Updated the Dockerfile to use a Flutter base image, enabling web builds for the application.
- Added new data models for `Lot` and `OrganizationAddress` to represent tender details more accurately.
- Enhanced the `Organization` model to include `companyId` and a nested `address` field.
- Updated the `TenderData` model to include new fields such as `noticeTypeCode`, `formType`, and `lots`, reflecting the API response structure.
- Regenerated necessary code for data models to ensure compatibility with the updated structures.
This commit is contained in:
AmirReza Jamali
2026-06-03 13:32:42 +03:30
parent 83b77c05ef
commit dde66521f6
35 changed files with 1362 additions and 659 deletions
@@ -1,10 +1,21 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/date_utils.dart';
import 'package:tm_app/core/utils/price_extension.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
import 'package:tm_app/views/detail/strings/tender_details_strings.dart';
import 'package:tm_app/views/detail/widgets/deadline_item.dart';
import 'package:tm_app/views/detail/widgets/info_item.dart';
// AI-NOTE (for PR reviewer): This widget was rewritten to render ONLY the
// backend-driven fields requested for the tender-details page (notice type,
// form type, languages, issue/publication dates, procedure, value, duration,
// deadlines, buyer org/address, lots, status, ids). Design is unchanged — it
// still uses InfoItem/DeadlineItem. The _infoItem() helper returns an empty
// list when a value is null/blank so empty fields don't render as blank rows
// (this is why each entry is spread with `...`). The old hard-coded
// "Reference Number" row was dropped because it isn't in the requested set.
class TenderDetailInfoSection extends StatelessWidget {
final bool isScreenBig;
final TenderData detail;
@@ -17,24 +28,114 @@ class TenderDetailInfoSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final buyer = detail.buyerOrganization;
final address = buyer?.address;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DeadlineItem(detail: detail, isScreenBig: isScreenBig),
InfoItem(
title: TenderDetailsStrings.tenderClientLabel,
value: detail.buyerOrganization!.name ?? '',
..._infoItem(TenderDetailsStrings.noticeTypeCode, detail.noticeTypeCode),
..._infoItem(TenderDetailsStrings.formType, detail.formType),
..._infoItem(
TenderDetailsStrings.noticeLanguageCode,
detail.noticeLanguageCode,
),
InfoItem(
title: TenderDetailsStrings.estimatedValue,
value:
'${detail.estimatedValue?.formattedPrice} ${detail.currency ?? ''}',
..._infoItem(
TenderDetailsStrings.issueDate,
_date(detail.issueDate),
),
InfoItem(
title: TenderDetailsStrings.tenderReferenceNumberLabel,
value: detail.noticePublicationId ?? '',
..._infoItem(
TenderDetailsStrings.issueTime,
_dateTime(detail.issueTime),
),
..._infoItem(TenderDetailsStrings.procedureCode, detail.procedureCode),
..._infoItem(
TenderDetailsStrings.estimatedValue,
detail.estimatedValue == null
? null
: '${detail.estimatedValue?.formattedPrice} ${detail.currency ?? ''}',
),
..._infoItem(TenderDetailsStrings.durationLabel, _duration()),
..._infoItem(
TenderDetailsStrings.publicationDate,
_date(detail.publicationDate),
),
..._infoItem(
TenderDetailsStrings.tenderDeadlineDate,
_date(detail.tenderDeadline),
),
..._infoItem(TenderDetailsStrings.countryCode, detail.countryCode),
..._infoItem(TenderDetailsStrings.buyerName, buyer?.name),
..._infoItem(TenderDetailsStrings.companyId, buyer?.companyId),
..._infoItem(TenderDetailsStrings.buyerCity, address?.cityName),
..._infoItem(TenderDetailsStrings.buyerCountry, address?.countryCode),
..._infoItem(
TenderDetailsStrings.officialLanguages,
(detail.officialLanguages == null ||
detail.officialLanguages!.isEmpty)
? null
: detail.officialLanguages!.join(', '),
),
..._infoItem(TenderDetailsStrings.status, detail.status),
..._infoItem(TenderDetailsStrings.tenderId, detail.tenderId),
..._lotsSection(),
],
);
}
/// Returns an [InfoItem] only when [value] has content, so empty fields are
/// not rendered as blank rows.
List<Widget> _infoItem(String title, String? value) {
if (value == null || value.trim().isEmpty) {
return const [];
}
return [InfoItem(title: title, value: value)];
}
List<Widget> _lotsSection() {
final lots = detail.lots;
if (lots == null || lots.isEmpty) {
return const [];
}
return [
SizedBox(height: 8.0.h()),
Text(
TenderDetailsStrings.lots,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
SizedBox(height: 8.0.h()),
for (final lot in lots) ...[
..._infoItem(TenderDetailsStrings.lotId, lot.lotId),
..._infoItem('Title', lot.title),
..._infoItem('Description', lot.description),
],
];
}
String? _duration() {
final duration = detail.duration?.trim() ?? '';
final unit = detail.durationUnit?.trim() ?? '';
final combined = '$duration $unit'.trim();
return combined.isEmpty ? null : combined;
}
String? _date(int? unix) {
if (unix == null) {
return null;
}
return timeConvertor(unix);
}
String? _dateTime(int? unix) {
if (unix == null) {
return null;
}
return timeConvertor(unix, hasTime: true);
}
}