280 lines
12 KiB
Dart
280 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import '../../../core/theme/app_theme.dart';
|
|
import '../../../core/widgets/glass_container.dart';
|
|
import '../../favorites/cubit/favorites_cubit.dart';
|
|
import '../../favorites/models/favorite_asset_model.dart';
|
|
import '../models/trade_model.dart';
|
|
import 'trade_detail_modal.dart';
|
|
|
|
class TradeCard extends StatelessWidget {
|
|
final TradeModel trade;
|
|
final VoidCallback? onAccept;
|
|
final VoidCallback? onClose;
|
|
final VoidCallback? onSettings;
|
|
|
|
const TradeCard({
|
|
super.key,
|
|
required this.trade,
|
|
this.onAccept,
|
|
this.onClose,
|
|
this.onSettings,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isBuy = trade.signalType == 'BUY';
|
|
final signalColor = isBuy ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
|
final isProposed = trade.isProposed;
|
|
final isActive = trade.isActive;
|
|
final isClosed = trade.isClosed;
|
|
|
|
return BlocBuilder<FavoritesCubit, FavoritesState>(
|
|
builder: (context, favState) {
|
|
double livePrice = 0.0;
|
|
final keyUpper = (trade.isin.isNotEmpty ? trade.isin : trade.symbol).toUpperCase();
|
|
final match = favState.favoriteDetails.firstWhere(
|
|
(f) => f.isin.toUpperCase() == keyUpper || f.symbol.toUpperCase() == keyUpper,
|
|
orElse: () => const FavoriteAssetModel(isin: '', symbol: '', name: '', currentPrice: 0.0, change24h: 0.0),
|
|
);
|
|
if (match.currentPrice > 0) {
|
|
livePrice = match.currentPrice;
|
|
}
|
|
|
|
final pnlAbs = livePrice > 0 ? trade.calculateLivePnlAbs(livePrice) : trade.calculatedPnlAbs;
|
|
final pnlPct = livePrice > 0 ? trade.calculateLivePnlPct(livePrice) : trade.calculatedPnlPct;
|
|
final isPnlPos = pnlAbs >= 0;
|
|
final pnlColor = isPnlPos ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
|
final currPrice = livePrice > 0 ? livePrice : trade.effectiveCurrentPrice;
|
|
|
|
return GestureDetector(
|
|
onTap: () => TradeDetailModal.show(
|
|
context,
|
|
trade: trade,
|
|
onAccept: onAccept,
|
|
onClose: onClose,
|
|
),
|
|
child: GlassContainer(
|
|
margin: const EdgeInsets.only(bottom: 14),
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header Row: Signal, Symbol, Status & Live PnL
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: signalColor.withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: signalColor.withValues(alpha: 0.4)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
isBuy ? Icons.trending_up : Icons.trending_down,
|
|
size: 14,
|
|
color: signalColor,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
trade.signalType,
|
|
style: TextStyle(color: signalColor, fontWeight: FontWeight.bold, fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
trade.companyName.isNotEmpty && trade.companyName != 'UNKNOWN'
|
|
? trade.companyName
|
|
: (trade.symbol.isNotEmpty && trade.symbol != 'UNKNOWN' ? trade.symbol : (trade.isin.isNotEmpty ? trade.isin : 'Aktie')),
|
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white),
|
|
),
|
|
if (trade.isin.isNotEmpty || (trade.symbol.isNotEmpty && trade.symbol != 'UNKNOWN' && trade.symbol != trade.companyName))
|
|
Text(
|
|
trade.isin.isNotEmpty ? trade.isin : trade.symbol,
|
|
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (isActive || isClosed)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: pnlColor.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: pnlColor.withValues(alpha: 0.3)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Text(
|
|
'${isPnlPos ? '+' : ''}${pnlAbs.toStringAsFixed(2)} €',
|
|
style: TextStyle(color: pnlColor, fontWeight: FontWeight.bold, fontSize: 14),
|
|
),
|
|
Text(
|
|
'${isPnlPos ? '+' : ''}${pnlPct.toStringAsFixed(2)}%',
|
|
style: TextStyle(color: pnlColor, fontSize: 11),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
else if (trade.isRejected)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.accentRed.withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: AppTheme.accentRed.withValues(alpha: 0.4)),
|
|
),
|
|
child: Text(
|
|
'ABGELEHNT',
|
|
style: TextStyle(color: AppTheme.accentRed, fontWeight: FontWeight.bold, fontSize: 10),
|
|
),
|
|
)
|
|
else
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.amber.withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: Colors.amber.withValues(alpha: 0.4)),
|
|
),
|
|
child: const Text(
|
|
'VORSCHLAG',
|
|
style: TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontSize: 10),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
// Price Metrics Grid with Live Kurs
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withValues(alpha: 0.2),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.05)),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
_priceItem(
|
|
isActive || isClosed ? 'Ausführung' : 'Ziel-Einstieg',
|
|
trade.actualEntryPrice > 0
|
|
? '${trade.actualEntryPrice.toStringAsFixed(2)} €'
|
|
: (trade.entryPrice > 0 ? '${trade.entryPrice.toStringAsFixed(2)} €' : '-'),
|
|
Colors.white
|
|
),
|
|
_priceItem('Live-Kurs', '${currPrice.toStringAsFixed(2)} €', AppTheme.accentCyan),
|
|
_priceItem('Stop-Loss', '${trade.stopLoss.toStringAsFixed(2)} €', AppTheme.accentRed),
|
|
_priceItem('Take-Profit', '${trade.takeProfit.toStringAsFixed(2)} €', AppTheme.primaryEmerald),
|
|
],
|
|
),
|
|
),
|
|
|
|
if (trade.reasoning.isNotEmpty) ...[
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
trade.reasoning,
|
|
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// Footer Action Row
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'${trade.instrumentType.isNotEmpty ? trade.instrumentType : "Stock"}${trade.derivativeIsin.isNotEmpty ? " (${trade.derivativeIsin})" : ""} • ${trade.timeframe.isNotEmpty ? trade.timeframe : "1D"}${trade.leverageUsed > 1 ? " • ${trade.leverageUsed.toStringAsFixed(0)}x Hebel" : ""}',
|
|
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
|
|
),
|
|
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
onPressed: () => TradeDetailModal.show(
|
|
context,
|
|
trade: trade,
|
|
onAccept: onAccept,
|
|
onClose: onClose,
|
|
),
|
|
icon: Icon(Icons.info_outline, size: 18, color: AppTheme.accentCyan),
|
|
tooltip: 'KI-Begründung & Details',
|
|
),
|
|
if (isProposed && onAccept != null) ...[
|
|
const SizedBox(width: 6),
|
|
ElevatedButton.icon(
|
|
onPressed: onAccept,
|
|
icon: const Icon(Icons.check_circle_outline, size: 16),
|
|
label: const Text('Trade Übernehmen'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppTheme.primaryEmerald,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
),
|
|
],
|
|
if (isActive && onClose != null) ...[
|
|
const SizedBox(width: 6),
|
|
OutlinedButton.icon(
|
|
onPressed: onClose,
|
|
icon: Icon(Icons.close, size: 14, color: AppTheme.accentRed),
|
|
label: Text('Position Schließen', style: TextStyle(color: AppTheme.accentRed, fontSize: 12)),
|
|
style: OutlinedButton.styleFrom(
|
|
side: BorderSide(color: AppTheme.accentRed.withValues(alpha: 0.5)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
),
|
|
],
|
|
if (isActive && onSettings != null) ...[
|
|
const SizedBox(width: 6),
|
|
IconButton(
|
|
onPressed: onSettings,
|
|
icon: const Icon(Icons.settings, size: 16, color: Colors.white),
|
|
tooltip: 'Einstellungen',
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: AppTheme.glassSurface,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _priceItem(String label, String val, Color valColor) {
|
|
return Column(
|
|
children: [
|
|
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 10)),
|
|
const SizedBox(height: 2),
|
|
Text(val, style: TextStyle(color: valColor, fontWeight: FontWeight.bold, fontSize: 12)),
|
|
],
|
|
);
|
|
}
|
|
}
|