Calling API in LatestReportColumn.dart

and improving UI
This commit is contained in:
2026-08-28 16:14:42 +02:00
parent b3a13cd5a2
commit e7508a4c8e
5 changed files with 183 additions and 35 deletions
+1
View File
@@ -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';
+50
View File
@@ -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';
}
}
}
+16 -6
View File
@@ -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})';
} }
+113 -26
View File
@@ -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")
);
widgets.add(LatestReportEntry( // Iterate through the reports and create Report entries accordingly
text: "Entry 0", for(int i = 0; i < reports.length; i++){
icon: Icons.code,
onTap: () => {widgets.add(
LatestReportHeader(text: "WOOOD")
)},
));
widgets.add(LatestReportEntry(
text: "Entry 1",
icon: Icons.newspaper,
));
widgets.add( // Add a day header above the first entry of every day
LatestReportHeader(text: "Yesterday") final DateTime day = _dayOf(reports[i].timestamp);
);
widgets.add(LatestReportEntry(
text: "Entry -1",
icon: Icons.code,
));
widgets.add( if(lastDay == null || day != lastDay){
LatestReportHeader(text: "15. August") 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(
text: reports[i].title,
icon: icon,
onTap: () => {widgets.add(
LatestReportHeader(text: "WOOOD")
)},
));
}
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",
];
}
+3 -3
View File
@@ -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 {