From e7508a4c8e5979f464bcd0fd8fab3c90586468c3 Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 28 Aug 2026 16:14:42 +0200 Subject: [PATCH] Calling API in LatestReportColumn.dart and improving UI --- ac_web/lib/Api/Api.dart | 1 + ac_web/lib/Api/Dtos/EReportType.dart | 50 ++++++++ ac_web/lib/Api/Dtos/ReportDto.dart | 22 +++- ac_web/lib/Widgets/LatestReportColumn.dart | 139 +++++++++++++++++---- ac_web/test/Api/ApiLayerTest.dart | 6 +- 5 files changed, 183 insertions(+), 35 deletions(-) create mode 100644 ac_web/lib/Api/Dtos/EReportType.dart diff --git a/ac_web/lib/Api/Api.dart b/ac_web/lib/Api/Api.dart index 4fa1fad..c5e0beb 100644 --- a/ac_web/lib/Api/Api.dart +++ b/ac_web/lib/Api/Api.dart @@ -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/EAgentStatus.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/Services/AgentsService.dart'; export 'package:ac_web/Api/Services/ApiServiceBase.dart'; diff --git a/ac_web/lib/Api/Dtos/EReportType.dart b/ac_web/lib/Api/Dtos/EReportType.dart new file mode 100644 index 0000000..39b4a66 --- /dev/null +++ b/ac_web/lib/Api/Dtos/EReportType.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'; + } + } +} diff --git a/ac_web/lib/Api/Dtos/ReportDto.dart b/ac_web/lib/Api/Dtos/ReportDto.dart index 8605e72..157fbd5 100644 --- a/ac_web/lib/Api/Dtos/ReportDto.dart +++ b/ac_web/lib/Api/Dtos/ReportDto.dart @@ -1,9 +1,11 @@ +import 'package:ac_web/Api/Dtos/EReportType.dart'; import 'package:ac_web/Api/JsonReader.dart'; /// Dart counterpart of the backend `ReportDto` returned by `GET /reports`. class ReportDto { const ReportDto({ required this.id, + required this.title, required this.author, required this.timestamp, required this.type, @@ -11,21 +13,27 @@ class ReportDto { }); final String id; + final String title; final String author; final DateTime timestamp; - /// Free form report type. The backend sends this as a plain string, so it is - /// intentionally not modelled as an enum here. - final String type; + final EReportType type; final String text; factory ReportDto.fromJson(Map json) { return ReportDto( id: JsonReader.requireString(json, 'Id'), + title: JsonReader.optionalString(json, 'Title') ?? '', author: JsonReader.requireString(json, 'Author'), 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') ?? '', ); } @@ -33,13 +41,15 @@ class ReportDto { Map toJson() { return { 'Id': id, + 'Title': title, 'Author': author, 'Timestamp': timestamp.toIso8601String(), - 'Type': type, + 'Type': type.wireName, 'Text': text, }; } @override - String toString() => 'ReportDto(id: $id, author: $author, type: $type)'; + String toString() => + 'ReportDto(id: $id, title: $title, author: $author, type: ${type.wireName})'; } diff --git a/ac_web/lib/Widgets/LatestReportColumn.dart b/ac_web/lib/Widgets/LatestReportColumn.dart index a9c5a32..af19dff 100644 --- a/ac_web/lib/Widgets/LatestReportColumn.dart +++ b/ac_web/lib/Widgets/LatestReportColumn.dart @@ -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/LatestReportHeader.dart'; import 'package:flutter/material.dart'; -class LatestReportColumn extends StatelessWidget{ +class LatestReportColumn extends StatefulWidget{ const LatestReportColumn({super.key}); + @override + State createState() => _LatestReportColumnState(); +} + +class _LatestReportColumnState extends State{ + + late Future> mReportsFuture; + + @override + void initState() { + super.initState(); + mReportsFuture = AcApi.instance.reports.getReports(); + } @override Widget build(BuildContext context) { + return FutureBuilder>( + 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 reports = snapshot.data ?? []; + + return _buildList(reports); + }, + ); + } + + Widget _buildList(List reports) { List widgets = []; + // Title Entry widgets.add(LatestReportHeader(text: "Latest Reports", bigHeader: true,)); - widgets.add( - LatestReportHeader(text: "Today") - ); + DateTime? lastDay; - widgets.add(LatestReportEntry( - text: "Entry 0", - icon: Icons.code, - onTap: () => {widgets.add( - LatestReportHeader(text: "WOOOD") - )}, - )); - widgets.add(LatestReportEntry( - text: "Entry 1", - icon: Icons.newspaper, - )); + // Iterate through the reports and create Report entries accordingly + for(int i = 0; i < reports.length; i++){ - widgets.add( - LatestReportHeader(text: "Yesterday") - ); - widgets.add(LatestReportEntry( - text: "Entry -1", - icon: Icons.code, - )); + // Add a day header above the first entry of every day + final DateTime day = _dayOf(reports[i].timestamp); - widgets.add( - LatestReportHeader(text: "15. August") - ); + 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( + text: reports[i].title, + icon: icon, + onTap: () => {widgets.add( + LatestReportHeader(text: "WOOOD") + )}, + )); + } return ListView( padding: EdgeInsetsGeometry.all(8), children: widgets, ); } -} \ No newline at end of file + + /// 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 _monthNames = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; +} diff --git a/ac_web/test/Api/ApiLayerTest.dart b/ac_web/test/Api/ApiLayerTest.dart index ee5befe..fb9ad64 100644 --- a/ac_web/test/Api/ApiLayerTest.dart +++ b/ac_web/test/Api/ApiLayerTest.dart @@ -98,14 +98,14 @@ void main() { 'Id': 'older', 'Author': 'Ada', 'Timestamp': '2026-08-20T10:00:00Z', - 'Type': 'Info', + 'Type': 'LOG', 'Text': 'first', }, { 'Id': 'newer', 'Author': 'Grace', 'Timestamp': '2026-08-27T10:00:00Z', - 'Type': 'Warning', + 'Type': 'CODE_REVIEW', 'Text': 'second', }, ]), @@ -115,7 +115,7 @@ void main() { expect(reports.map((report) => report.id), ['newer', 'older']); expect(reports.first.author, 'Grace'); - expect(reports.first.type, 'Warning'); + expect(reports.first.type, EReportType.codeReview); }); test('maps error status codes onto ApiHttpException', () async {