Calling API in LatestReportColumn.dart
and improving UI
This commit is contained in:
@@ -12,6 +12,7 @@ export 'package:ac_web/Api/Dtos/AgentDto.dart';
|
|||||||
export 'package:ac_web/Api/Dtos/AgentTaskDto.dart';
|
export 'package:ac_web/Api/Dtos/AgentTaskDto.dart';
|
||||||
export 'package:ac_web/Api/Dtos/EAgentStatus.dart';
|
export 'package:ac_web/Api/Dtos/EAgentStatus.dart';
|
||||||
export 'package:ac_web/Api/Dtos/EAgentType.dart';
|
export 'package:ac_web/Api/Dtos/EAgentType.dart';
|
||||||
|
export 'package:ac_web/Api/Dtos/EReportType.dart';
|
||||||
export 'package:ac_web/Api/Dtos/ReportDto.dart';
|
export 'package:ac_web/Api/Dtos/ReportDto.dart';
|
||||||
export 'package:ac_web/Api/Services/AgentsService.dart';
|
export 'package:ac_web/Api/Services/AgentsService.dart';
|
||||||
export 'package:ac_web/Api/Services/ApiServiceBase.dart';
|
export 'package:ac_web/Api/Services/ApiServiceBase.dart';
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/// Mirror of the backend `EReportType` enum.
|
||||||
|
///
|
||||||
|
/// The order of the values has to match the order in the .Net enum, because
|
||||||
|
/// the api may serialize enums as their numeric value. [unknown] is kept last
|
||||||
|
/// so that it never shadows a real backend value and acts as fallback for
|
||||||
|
/// values this client does not know yet.
|
||||||
|
enum EReportType {
|
||||||
|
log,
|
||||||
|
codeReview,
|
||||||
|
projectPlan,
|
||||||
|
research,
|
||||||
|
newspaper,
|
||||||
|
unknown;
|
||||||
|
|
||||||
|
/// Name as it is written in the .Net enum, used for json (de)serialization.
|
||||||
|
String get wireName {
|
||||||
|
switch (this) {
|
||||||
|
case EReportType.log:
|
||||||
|
return 'LOG';
|
||||||
|
case EReportType.codeReview:
|
||||||
|
return 'CODE_REVIEW';
|
||||||
|
case EReportType.projectPlan:
|
||||||
|
return 'PROJECT_PLAN';
|
||||||
|
case EReportType.research:
|
||||||
|
return 'RESEARCH';
|
||||||
|
case EReportType.newspaper:
|
||||||
|
return 'NEWSPAPER';
|
||||||
|
case EReportType.unknown:
|
||||||
|
return 'UNKNOWN';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Human readable label, e.g. for chips or table cells.
|
||||||
|
String get displayName {
|
||||||
|
switch (this) {
|
||||||
|
case EReportType.log:
|
||||||
|
return 'Log';
|
||||||
|
case EReportType.codeReview:
|
||||||
|
return 'Code Review';
|
||||||
|
case EReportType.projectPlan:
|
||||||
|
return 'Project Plan';
|
||||||
|
case EReportType.research:
|
||||||
|
return 'Research';
|
||||||
|
case EReportType.newspaper:
|
||||||
|
return 'Newspaper';
|
||||||
|
case EReportType.unknown:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import 'package:ac_web/Api/Dtos/EReportType.dart';
|
||||||
import 'package:ac_web/Api/JsonReader.dart';
|
import 'package:ac_web/Api/JsonReader.dart';
|
||||||
|
|
||||||
/// Dart counterpart of the backend `ReportDto` returned by `GET /reports`.
|
/// Dart counterpart of the backend `ReportDto` returned by `GET /reports`.
|
||||||
class ReportDto {
|
class ReportDto {
|
||||||
const ReportDto({
|
const ReportDto({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.title,
|
||||||
required this.author,
|
required this.author,
|
||||||
required this.timestamp,
|
required this.timestamp,
|
||||||
required this.type,
|
required this.type,
|
||||||
@@ -11,21 +13,27 @@ class ReportDto {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
|
final String title;
|
||||||
final String author;
|
final String author;
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
|
|
||||||
/// Free form report type. The backend sends this as a plain string, so it is
|
final EReportType type;
|
||||||
/// intentionally not modelled as an enum here.
|
|
||||||
final String type;
|
|
||||||
|
|
||||||
final String text;
|
final String text;
|
||||||
|
|
||||||
factory ReportDto.fromJson(Map<String, dynamic> json) {
|
factory ReportDto.fromJson(Map<String, dynamic> json) {
|
||||||
return ReportDto(
|
return ReportDto(
|
||||||
id: JsonReader.requireString(json, 'Id'),
|
id: JsonReader.requireString(json, 'Id'),
|
||||||
|
title: JsonReader.optionalString(json, 'Title') ?? '',
|
||||||
author: JsonReader.requireString(json, 'Author'),
|
author: JsonReader.requireString(json, 'Author'),
|
||||||
timestamp: JsonReader.requireDateTime(json, 'Timestamp'),
|
timestamp: JsonReader.requireDateTime(json, 'Timestamp'),
|
||||||
type: JsonReader.optionalString(json, 'Type') ?? '',
|
type: JsonReader.enumValue(
|
||||||
|
json,
|
||||||
|
'Type',
|
||||||
|
EReportType.values,
|
||||||
|
(value) => value.wireName,
|
||||||
|
EReportType.unknown,
|
||||||
|
),
|
||||||
text: JsonReader.optionalString(json, 'Text') ?? '',
|
text: JsonReader.optionalString(json, 'Text') ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -33,13 +41,15 @@ class ReportDto {
|
|||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'Id': id,
|
'Id': id,
|
||||||
|
'Title': title,
|
||||||
'Author': author,
|
'Author': author,
|
||||||
'Timestamp': timestamp.toIso8601String(),
|
'Timestamp': timestamp.toIso8601String(),
|
||||||
'Type': type,
|
'Type': type.wireName,
|
||||||
'Text': text,
|
'Text': text,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ReportDto(id: $id, author: $author, type: $type)';
|
String toString() =>
|
||||||
|
'ReportDto(id: $id, title: $title, author: $author, type: ${type.wireName})';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,136 @@
|
|||||||
|
import 'package:ac_web/Api/AcApi.dart';
|
||||||
|
import 'package:ac_web/Api/Dtos/EReportType.dart';
|
||||||
|
import 'package:ac_web/Api/Dtos/ReportDto.dart';
|
||||||
import 'package:ac_web/Widgets/LatestReportEntry.dart';
|
import 'package:ac_web/Widgets/LatestReportEntry.dart';
|
||||||
import 'package:ac_web/Widgets/LatestReportHeader.dart';
|
import 'package:ac_web/Widgets/LatestReportHeader.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class LatestReportColumn extends StatelessWidget{
|
class LatestReportColumn extends StatefulWidget{
|
||||||
const LatestReportColumn({super.key});
|
const LatestReportColumn({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LatestReportColumn> createState() => _LatestReportColumnState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LatestReportColumnState extends State<LatestReportColumn>{
|
||||||
|
|
||||||
|
late Future<List<ReportDto>> mReportsFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
mReportsFuture = AcApi.instance.reports.getReports();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<List<ReportDto>>(
|
||||||
|
future: mReportsFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Center(child: Text('Could not load reports: ${snapshot.error}'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The list of reports fetched from the ReportsController.
|
||||||
|
final List<ReportDto> reports = snapshot.data ?? <ReportDto>[];
|
||||||
|
|
||||||
|
return _buildList(reports);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildList(List<ReportDto> reports) {
|
||||||
|
|
||||||
List<Widget> widgets = <Widget>[];
|
List<Widget> widgets = <Widget>[];
|
||||||
|
|
||||||
|
// Title Entry
|
||||||
widgets.add(LatestReportHeader(text: "Latest Reports", bigHeader: true,));
|
widgets.add(LatestReportHeader(text: "Latest Reports", bigHeader: true,));
|
||||||
|
|
||||||
widgets.add(
|
DateTime? lastDay;
|
||||||
LatestReportHeader(text: "Today")
|
|
||||||
);
|
// Iterate through the reports and create Report entries accordingly
|
||||||
|
for(int i = 0; i < reports.length; i++){
|
||||||
|
|
||||||
|
// Add a day header above the first entry of every day
|
||||||
|
final DateTime day = _dayOf(reports[i].timestamp);
|
||||||
|
|
||||||
|
if(lastDay == null || day != lastDay){
|
||||||
|
widgets.add(LatestReportHeader(text: _dayLabel(day)));
|
||||||
|
lastDay = day;
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData icon;
|
||||||
|
|
||||||
|
switch(reports[i].type){
|
||||||
|
|
||||||
|
case EReportType.log:
|
||||||
|
icon = Icons.receipt_long;
|
||||||
|
case EReportType.codeReview:
|
||||||
|
icon = Icons.code;
|
||||||
|
case EReportType.projectPlan:
|
||||||
|
icon = Icons.assignment;
|
||||||
|
case EReportType.research:
|
||||||
|
icon = Icons.science;
|
||||||
|
case EReportType.newspaper:
|
||||||
|
icon = Icons.newspaper;
|
||||||
|
case EReportType.unknown:
|
||||||
|
icon = Icons.question_mark;
|
||||||
|
}
|
||||||
|
|
||||||
widgets.add(LatestReportEntry(
|
widgets.add(LatestReportEntry(
|
||||||
text: "Entry 0",
|
text: reports[i].title,
|
||||||
icon: Icons.code,
|
icon: icon,
|
||||||
onTap: () => {widgets.add(
|
onTap: () => {widgets.add(
|
||||||
LatestReportHeader(text: "WOOOD")
|
LatestReportHeader(text: "WOOOD")
|
||||||
)},
|
)},
|
||||||
));
|
));
|
||||||
widgets.add(LatestReportEntry(
|
}
|
||||||
text: "Entry 1",
|
|
||||||
icon: Icons.newspaper,
|
|
||||||
));
|
|
||||||
|
|
||||||
widgets.add(
|
|
||||||
LatestReportHeader(text: "Yesterday")
|
|
||||||
);
|
|
||||||
widgets.add(LatestReportEntry(
|
|
||||||
text: "Entry -1",
|
|
||||||
icon: Icons.code,
|
|
||||||
));
|
|
||||||
|
|
||||||
widgets.add(
|
|
||||||
LatestReportHeader(text: "15. August")
|
|
||||||
);
|
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: EdgeInsetsGeometry.all(8),
|
padding: EdgeInsetsGeometry.all(8),
|
||||||
children: widgets,
|
children: widgets,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strips the time part, so that two timestamps of the same day are equal.
|
||||||
|
DateTime _dayOf(DateTime timestamp) {
|
||||||
|
final DateTime local = timestamp.toLocal();
|
||||||
|
return DateTime(local.year, local.month, local.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "Today", "Yesterday" or e.g. "15. August" for all other days.
|
||||||
|
String _dayLabel(DateTime day) {
|
||||||
|
final DateTime today = _dayOf(DateTime.now());
|
||||||
|
final DateTime yesterday = DateTime(today.year, today.month, today.day - 1);
|
||||||
|
|
||||||
|
if (day == today) {
|
||||||
|
return "Today";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (day == yesterday) {
|
||||||
|
return "Yesterday";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "${day.day}. ${_monthNames[day.month - 1]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
static const List<String> _monthNames = <String>[
|
||||||
|
"January",
|
||||||
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
@@ -98,14 +98,14 @@ void main() {
|
|||||||
'Id': 'older',
|
'Id': 'older',
|
||||||
'Author': 'Ada',
|
'Author': 'Ada',
|
||||||
'Timestamp': '2026-08-20T10:00:00Z',
|
'Timestamp': '2026-08-20T10:00:00Z',
|
||||||
'Type': 'Info',
|
'Type': 'LOG',
|
||||||
'Text': 'first',
|
'Text': 'first',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'Id': 'newer',
|
'Id': 'newer',
|
||||||
'Author': 'Grace',
|
'Author': 'Grace',
|
||||||
'Timestamp': '2026-08-27T10:00:00Z',
|
'Timestamp': '2026-08-27T10:00:00Z',
|
||||||
'Type': 'Warning',
|
'Type': 'CODE_REVIEW',
|
||||||
'Text': 'second',
|
'Text': 'second',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@@ -115,7 +115,7 @@ void main() {
|
|||||||
|
|
||||||
expect(reports.map((report) => report.id), ['newer', 'older']);
|
expect(reports.map((report) => report.id), ['newer', 'older']);
|
||||||
expect(reports.first.author, 'Grace');
|
expect(reports.first.author, 'Grace');
|
||||||
expect(reports.first.type, 'Warning');
|
expect(reports.first.type, EReportType.codeReview);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('maps error status codes onto ApiHttpException', () async {
|
test('maps error status codes onto ApiHttpException', () async {
|
||||||
|
|||||||
Reference in New Issue
Block a user