refactor: save current workspace state including FinlyticAnalyzer fixes, FinlyticApp trade route alignment, and DTO audit documentation
This commit is contained in:
@@ -20,10 +20,10 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
Future<void> _onFetchEvents(FetchCalendarEvents event, Emitter<CalendarState> emit) async {
|
||||
emit(CalendarLoading());
|
||||
try {
|
||||
final events = await repository.fetchEvents();
|
||||
final events = await repository.fetchEvents(event.year, event.month);
|
||||
emit(CalendarLoaded(
|
||||
allEvents: events,
|
||||
currentMonth: DateTime.now(),
|
||||
currentMonth: DateTime(event.year, event.month),
|
||||
));
|
||||
} catch (e) {
|
||||
emit(const CalendarError("Fehler beim Laden des Kalenders."));
|
||||
@@ -60,6 +60,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
currentMonth: event.newMonth,
|
||||
clearSelectedDate: true,
|
||||
));
|
||||
|
||||
// Trigger new fetch for the selected month
|
||||
add(FetchCalendarEvents(year: event.newMonth.year, month: event.newMonth.month));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,15 @@ abstract class CalendarEvent extends Equatable {
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class FetchCalendarEvents extends CalendarEvent {}
|
||||
class FetchCalendarEvents extends CalendarEvent {
|
||||
final int year;
|
||||
final int month;
|
||||
|
||||
const FetchCalendarEvents({required this.year, required this.month});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [year, month];
|
||||
}
|
||||
|
||||
class FilterCategoryChanged extends CalendarEvent {
|
||||
final String category;
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
|
||||
|
||||
class CorporateEventModel extends Equatable {
|
||||
final String id;
|
||||
final String isin;
|
||||
final String symbol;
|
||||
final String companyName;
|
||||
final String eventType;
|
||||
@@ -15,6 +16,7 @@ class CorporateEventModel extends Equatable {
|
||||
required this.eventType,
|
||||
required this.eventDate,
|
||||
required this.description,
|
||||
required this.isin,
|
||||
});
|
||||
|
||||
factory CorporateEventModel.fromJson(Map<String, dynamic> json) {
|
||||
@@ -25,7 +27,8 @@ class CorporateEventModel extends Equatable {
|
||||
if (str.contains('.')) {
|
||||
final parts = str.split('.');
|
||||
if (parts.length >= 3) {
|
||||
return DateTime(int.parse(parts[2]), int.parse(parts[1]), int.parse(parts[0]));
|
||||
return DateTime(
|
||||
int.parse(parts[2]), int.parse(parts[1]), int.parse(parts[0]));
|
||||
}
|
||||
}
|
||||
return DateTime.parse(str);
|
||||
@@ -36,17 +39,24 @@ class CorporateEventModel extends Equatable {
|
||||
|
||||
return CorporateEventModel(
|
||||
id: json['id']?.toString() ?? json['Id']?.toString() ?? '',
|
||||
isin: json['isin']?.toString() ?? json['Isin']?.toString() ?? '',
|
||||
symbol: json['symbol']?.toString() ?? json['Symbol']?.toString() ?? '',
|
||||
companyName: json['companyName']?.toString() ?? json['CompanyName']?.toString() ?? '',
|
||||
eventType: json['eventType']?.toString() ?? json['EventType']?.toString() ?? '',
|
||||
companyName: json['companyName']?.toString() ??
|
||||
json['CompanyName']?.toString() ??
|
||||
'',
|
||||
eventType:
|
||||
json['eventType']?.toString() ?? json['EventType']?.toString() ?? '',
|
||||
eventDate: parseDate(json['eventDate'] ?? json['EventDate']),
|
||||
description: json['description']?.toString() ?? json['Description']?.toString() ?? '',
|
||||
description: json['description']?.toString() ??
|
||||
json['Description']?.toString() ??
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'isin': isin,
|
||||
'symbol': symbol,
|
||||
'companyName': companyName,
|
||||
'eventType': eventType,
|
||||
@@ -56,5 +66,6 @@ class CorporateEventModel extends Equatable {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, symbol, companyName, eventType, eventDate, description];
|
||||
List<Object?> get props =>
|
||||
[id, symbol, companyName, eventType, eventDate, description];
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ class CalendarRepository {
|
||||
|
||||
CalendarRepository({required this.apiClient});
|
||||
|
||||
Future<List<CorporateEventModel>> fetchEvents() async {
|
||||
Future<List<CorporateEventModel>> fetchEvents(int year, int month) async {
|
||||
try {
|
||||
final res = await apiClient.get('/api/v1/calendar');
|
||||
final monthStr = month.toString().padLeft(2, '0');
|
||||
final res = await apiClient.get('/api/v1/calendar/events/$year/$monthStr');
|
||||
if (res.statusCode == 200 && res.data != null) {
|
||||
final List<dynamic> data = res.data;
|
||||
return data.map((json) => CorporateEventModel.fromJson(json)).toList();
|
||||
|
||||
@@ -17,9 +17,12 @@ class CorporateCalendarScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => CalendarBloc(
|
||||
repository: CalendarRepository(apiClient: apiClient),
|
||||
)..add(FetchCalendarEvents()),
|
||||
create: (context) {
|
||||
final now = DateTime.now();
|
||||
return CalendarBloc(
|
||||
repository: CalendarRepository(apiClient: apiClient),
|
||||
)..add(FetchCalendarEvents(year: now.year, month: now.month));
|
||||
},
|
||||
child: _CorporateCalendarScreenContent(apiClient: apiClient),
|
||||
);
|
||||
}
|
||||
@@ -50,125 +53,130 @@ class _CorporateCalendarScreenContent extends StatelessWidget {
|
||||
if (state is CalendarLoaded) {
|
||||
final filtered = state.filteredEvents;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MonthCalendarWidget(
|
||||
currentMonth: state.currentMonth,
|
||||
selectedDate: state.selectedDate,
|
||||
events: state.allEvents.map((e) => e.toJson()).toList(),
|
||||
onDateSelected: (date) {
|
||||
context.read<CalendarBloc>().add(FilterDateSelected(date));
|
||||
},
|
||||
onMonthChanged: (newMonth) {
|
||||
context.read<CalendarBloc>().add(MonthChanged(newMonth));
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: categories.map((cat) {
|
||||
final isSelected = state.selectedCategory == cat;
|
||||
String label = 'Alle';
|
||||
if (cat == 'Earnings') label = 'Quartalsergebnisse';
|
||||
if (cat == 'ExDividend') label = 'Ex-Dividendentage';
|
||||
if (cat == 'Payout') label = 'Zahlungstage';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ChoiceChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
selectedColor: AppTheme.primaryEmerald.withValues(alpha: 0.25),
|
||||
backgroundColor: AppTheme.glassSurface,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? AppTheme.primaryEmerald : AppTheme.textSecondary,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
fontSize: 12,
|
||||
),
|
||||
side: BorderSide(color: isSelected ? AppTheme.primaryEmerald : AppTheme.glassBorder),
|
||||
onSelected: (_) {
|
||||
context.read<CalendarBloc>().add(FilterCategoryChanged(cat));
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.selectedDate != null)
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
context.read<CalendarBloc>().add(const FilterDateSelected(null));
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MonthCalendarWidget(
|
||||
currentMonth: state.currentMonth,
|
||||
selectedDate: state.selectedDate,
|
||||
events: state.allEvents.map((e) => e.toJson()).toList(),
|
||||
onDateSelected: (date) {
|
||||
context.read<CalendarBloc>().add(FilterDateSelected(date));
|
||||
},
|
||||
onMonthChanged: (newMonth) {
|
||||
context.read<CalendarBloc>().add(MonthChanged(newMonth));
|
||||
},
|
||||
icon: Icon(Icons.clear, size: 14, color: AppTheme.accentCyan),
|
||||
label: Text('Alle Tage', style: TextStyle(color: AppTheme.accentCyan, fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
state.selectedDate != null
|
||||
? 'Termine am ${state.selectedDate!.day.toString().padLeft(2, '0')}.${state.selectedDate!.month.toString().padLeft(2, '0')}.${state.selectedDate!.year} (${filtered.length})'
|
||||
: 'Anstehende Termine (${filtered.length})',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: AppTheme.textPrimary),
|
||||
),
|
||||
Text('Kachelansicht', style: TextStyle(fontSize: 11, color: AppTheme.textMuted)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: categories.map((cat) {
|
||||
final isSelected = state.selectedCategory == cat;
|
||||
String label = 'Alle';
|
||||
if (cat == 'Earnings') label = 'Quartalsergebnisse';
|
||||
if (cat == 'ExDividend') label = 'Ex-Dividendentage';
|
||||
if (cat == 'Payout') label = 'Zahlungstage';
|
||||
|
||||
filtered.isEmpty
|
||||
? Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.glassSurface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.glassBorder),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Keine Unternehmenstermine für diesen Filter/Tag gefunden.',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 13),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ChoiceChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
selectedColor: AppTheme.primaryEmerald.withValues(alpha: 0.25),
|
||||
backgroundColor: AppTheme.glassSurface,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? AppTheme.primaryEmerald : AppTheme.textSecondary,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
fontSize: 12,
|
||||
),
|
||||
side: BorderSide(color: isSelected ? AppTheme.primaryEmerald : AppTheme.glassBorder),
|
||||
onSelected: (_) {
|
||||
context.read<CalendarBloc>().add(FilterCategoryChanged(cat));
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.selectedDate != null)
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
context.read<CalendarBloc>().add(const FilterDateSelected(null));
|
||||
},
|
||||
icon: Icon(Icons.clear, size: 14, color: AppTheme.accentCyan),
|
||||
label: Text('Alle Tage', style: TextStyle(color: AppTheme.accentCyan, fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
state.selectedDate != null
|
||||
? 'Termine am ${state.selectedDate!.day.toString().padLeft(2, '0')}.${state.selectedDate!.month.toString().padLeft(2, '0')}.${state.selectedDate!.year} (${filtered.length})'
|
||||
: 'Anstehende Termine (${filtered.length})',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: AppTheme.textPrimary),
|
||||
),
|
||||
Text('Kachelansicht', style: TextStyle(fontSize: 11, color: AppTheme.textMuted)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
if (filtered.isEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.glassSurface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.glassBorder),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Keine Unternehmenstermine für diesen Filter/Tag gefunden.',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final crossAxisCount = constraints.maxWidth > 750 ? 4 : (constraints.maxWidth > 480 ? 2 : 1);
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: filtered.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
childAspectRatio: 3,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return CalendarEventTile(
|
||||
event: filtered[index].toJson(),
|
||||
apiClient: apiClient,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (filtered.isNotEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 300,
|
||||
mainAxisExtent: 135,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return CalendarEventTile(
|
||||
event: filtered[index].toJson(),
|
||||
apiClient: apiClient,
|
||||
);
|
||||
},
|
||||
childCount: filtered.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/utils/asset_utils.dart';
|
||||
import '../../../core/widgets/asset_logo_widget.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
import '../../../core/widgets/status_badge.dart';
|
||||
@@ -20,12 +19,13 @@ class CalendarEventTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rawSymbol = event['symbol']?.toString() ?? event['Symbol']?.toString() ?? 'ASSET';
|
||||
final rawCompany = event['companyName']?.toString() ?? event['CompanyName']?.toString() ?? rawSymbol;
|
||||
final displayName = AssetUtils.getAssetName(rawCompany.isNotEmpty ? rawCompany : rawSymbol);
|
||||
final isin = event['isin']!;
|
||||
final rawSymbol = event['ticker']?.toString() ?? event['Ticker']?.toString() ?? event['symbol']?.toString() ?? 'ASSET';
|
||||
final companyName = event['companyName']?.toString() ?? event['CompanyName']?.toString() ?? rawSymbol;
|
||||
final type = event['eventType']?.toString() ?? event['EventType']?.toString() ?? 'Earnings';
|
||||
final desc = event['description']?.toString() ?? event['Description']?.toString() ?? '';
|
||||
final dateStr = event['eventDate']?.toString() ?? event['EventDate']?.toString() ?? '';
|
||||
final desc = event['description']?.toString() ?? event['Description']?.toString() ?? '$companyName $type Termin';
|
||||
final dateStr = event['date']?.toString() ?? event['Date']?.toString() ?? event['eventDate']?.toString() ?? '';
|
||||
final image = event['image']?.toString() ?? (isin.isNotEmpty ? '/api/v1/logo/$isin' : null);
|
||||
|
||||
String formattedDate = dateStr;
|
||||
try {
|
||||
@@ -50,7 +50,9 @@ class CalendarEventTile extends StatelessWidget {
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => AssetDetailScreen(
|
||||
symbol: displayName,
|
||||
isin: isin,
|
||||
name: companyName,
|
||||
symbol: rawSymbol,
|
||||
apiClient: apiClient,
|
||||
),
|
||||
),
|
||||
@@ -66,14 +68,14 @@ class CalendarEventTile extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
AssetLogoWidget(symbolOrName: displayName, size: 28),
|
||||
AssetLogoWidget(symbolOrName: companyName, imageUrl: image, size: 28),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayName,
|
||||
companyName,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13.5),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
Reference in New Issue
Block a user