474 lines
20 KiB
Dart
474 lines
20 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../../../core/theme/app_theme.dart';
|
|
import '../models/trade_model.dart';
|
|
import '../models/close_trade_request_dto.dart';
|
|
|
|
class TradeClosingCockpit extends StatefulWidget {
|
|
final TradeModel trade;
|
|
final String defaultSymbol;
|
|
final void Function(CloseTradeRequestDto) onClose;
|
|
|
|
const TradeClosingCockpit({
|
|
super.key,
|
|
required this.trade,
|
|
required this.defaultSymbol,
|
|
required this.onClose,
|
|
});
|
|
|
|
static Future<void> show(
|
|
BuildContext context, {
|
|
required TradeModel trade,
|
|
required String defaultSymbol,
|
|
required void Function(CloseTradeRequestDto) onClose,
|
|
}) {
|
|
return showDialog(
|
|
context: context,
|
|
barrierDismissible: true,
|
|
builder: (dialogContext) => TradeClosingCockpit(
|
|
trade: trade,
|
|
defaultSymbol: defaultSymbol,
|
|
onClose: onClose,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
State<TradeClosingCockpit> createState() => _TradeClosingCockpitState();
|
|
}
|
|
|
|
class _TradeClosingCockpitState extends State<TradeClosingCockpit> {
|
|
late TextEditingController _exitPriceCtrl;
|
|
late TextEditingController _exitFeeCtrl;
|
|
late TextEditingController _notesCtrl;
|
|
|
|
DateTime _exitTimestamp = DateTime.now();
|
|
String _selectedReasonTag = 'Manuell in TR verkauft';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final defaultPrice = widget.trade.currentPrice > 0
|
|
? widget.trade.currentPrice
|
|
: (widget.trade.actualEntryPrice > 0 ? widget.trade.actualEntryPrice : widget.trade.entryPrice);
|
|
|
|
_exitPriceCtrl = TextEditingController(text: defaultPrice.toStringAsFixed(2));
|
|
_exitFeeCtrl = TextEditingController(text: '1.00');
|
|
_notesCtrl = TextEditingController();
|
|
|
|
_exitPriceCtrl.addListener(() => setState(() {}));
|
|
_exitFeeCtrl.addListener(() => setState(() {}));
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_exitPriceCtrl.dispose();
|
|
_exitFeeCtrl.dispose();
|
|
_notesCtrl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
double _parse(TextEditingController ctrl, double fallback) {
|
|
final clean = ctrl.text.replaceAll(',', '.').trim();
|
|
return double.tryParse(clean) ?? fallback;
|
|
}
|
|
|
|
double get _exitPrice => _parse(_exitPriceCtrl, widget.trade.entryPrice);
|
|
double get _exitFee => _parse(_exitFeeCtrl, 1.0);
|
|
|
|
double get _entryPrice => widget.trade.actualEntryPrice > 0 ? widget.trade.actualEntryPrice : (widget.trade.entryPrice > 0 ? widget.trade.entryPrice : 1.0);
|
|
double get _posSize => widget.trade.positionSize > 0 ? widget.trade.positionSize : 1000.0;
|
|
double get _quantity => widget.trade.quantity > 0 ? widget.trade.quantity : (_posSize / _entryPrice);
|
|
|
|
double get _calculatedProceeds {
|
|
if (_exitPrice <= 0 || _quantity <= 0) return 0.0;
|
|
return _quantity * _exitPrice;
|
|
}
|
|
|
|
double get _calculatedPnlAbs {
|
|
if (_entryPrice <= 0 || _exitPrice <= 0) return 0.0;
|
|
final isShort = widget.trade.signalType.toUpperCase() == 'SELL' || widget.trade.signalType.toUpperCase() == 'SHORT';
|
|
final movePct = isShort ? ((_entryPrice - _exitPrice) / _entryPrice) : ((_exitPrice - _entryPrice) / _entryPrice);
|
|
final lev = (widget.trade.instrumentType.toLowerCase().contains('knock') || widget.trade.instrumentType.toLowerCase().contains('option'))
|
|
? 1.0
|
|
: (widget.trade.leverageUsed > 0 ? widget.trade.leverageUsed : 1.0);
|
|
final totalFees = (widget.trade.entryFee > 0 ? widget.trade.entryFee : 1.0) + _exitFee;
|
|
return (movePct * _posSize * lev) - totalFees;
|
|
}
|
|
|
|
double get _calculatedPnlPct {
|
|
if (_posSize <= 0) return 0.0;
|
|
return (_calculatedPnlAbs / _posSize) * 100.0;
|
|
}
|
|
|
|
void _selectTimeOption(String option) {
|
|
final now = DateTime.now();
|
|
setState(() {
|
|
if (option == 'now') {
|
|
_exitTimestamp = now;
|
|
} else if (option == 'today_morning') {
|
|
_exitTimestamp = DateTime(now.year, now.month, now.day, 9, 15);
|
|
} else if (option == 'today_noon') {
|
|
_exitTimestamp = DateTime(now.year, now.month, now.day, 13, 0);
|
|
} else if (option == 'yesterday') {
|
|
final yest = now.subtract(const Duration(days: 1));
|
|
_exitTimestamp = DateTime(yest.year, yest.month, yest.day, 17, 30);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _pickCustomDateTime() async {
|
|
final pickedDate = await showDatePicker(
|
|
context: context,
|
|
initialDate: _exitTimestamp,
|
|
firstDate: DateTime.now().subtract(const Duration(days: 90)),
|
|
lastDate: DateTime.now(),
|
|
);
|
|
|
|
if (pickedDate != null && mounted) {
|
|
final pickedTime = await showTimePicker(
|
|
context: context,
|
|
initialTime: TimeOfDay.fromDateTime(_exitTimestamp),
|
|
);
|
|
|
|
if (pickedTime != null && mounted) {
|
|
setState(() {
|
|
_exitTimestamp = DateTime(
|
|
pickedDate.year,
|
|
pickedDate.month,
|
|
pickedDate.day,
|
|
pickedTime.hour,
|
|
pickedTime.minute,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isWin = _calculatedPnlAbs >= 0;
|
|
final pnlColor = isWin ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
|
|
|
return Dialog(
|
|
backgroundColor: Colors.transparent,
|
|
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
|
child: Container(
|
|
width: 580,
|
|
constraints: const BoxConstraints(maxHeight: 780),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.cardSurface,
|
|
borderRadius: BorderRadius.circular(24),
|
|
border: Border.all(color: AppTheme.glassBorder),
|
|
boxShadow: [
|
|
BoxShadow(color: Colors.black.withValues(alpha: 0.6), blurRadius: 30, offset: const Offset(0, 10)),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Header
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 18, 16, 14),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.accentRed.withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(Icons.flag_outlined, color: AppTheme.accentRed, size: 22),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Position Schließen & Nacherfassen',
|
|
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
|
),
|
|
Text(
|
|
'Trade #${widget.trade.id} • ${widget.trade.companyName.isNotEmpty ? widget.trade.companyName : widget.defaultSymbol}',
|
|
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
icon: const Icon(Icons.close, color: Colors.white54),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(color: Colors.white12, height: 1),
|
|
|
|
// Body
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Entry Recap Box
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.glassSurface,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: AppTheme.glassBorder),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
_summaryCol('Einstiegskurs', '€${_entryPrice.toStringAsFixed(2)}'),
|
|
_summaryCol('Investition', '€${_posSize.toStringAsFixed(0)}'),
|
|
_summaryCol('Stückzahl', '${_quantity.toStringAsFixed(2)} Stk.'),
|
|
_summaryCol('Instrument', widget.trade.instrumentType),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// SECTION 1: VERKAUFSKURS
|
|
const Text('1. TATSÄCHLICHER VERKAUFSKURS (€)', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
|
|
const SizedBox(height: 8),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _exitPriceCtrl,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold),
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.sell_outlined, size: 18, color: Colors.white54),
|
|
labelText: 'Verkaufskurs in Trade Republic',
|
|
filled: true,
|
|
fillColor: AppTheme.glassSurface,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppTheme.glassBorder)),
|
|
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppTheme.glassBorder)),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _exitFeeCtrl,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
|
decoration: InputDecoration(
|
|
prefixIcon: const Icon(Icons.receipt_long, size: 16, color: Colors.white54),
|
|
labelText: 'Ausstiegsgebühr (€)',
|
|
filled: true,
|
|
fillColor: AppTheme.glassSurface,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppTheme.glassBorder)),
|
|
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppTheme.glassBorder)),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// SECTION 2: ZEITPUNKT
|
|
const Text('2. WANN WURDE DER TRADE GESCHLOSSEN?', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
|
|
const SizedBox(height: 8),
|
|
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
_timeChip('Jetzt', 'now'),
|
|
_timeChip('Heute Morgen (09:15)', 'today_morning'),
|
|
_timeChip('Heute Mittag (13:00)', 'today_noon'),
|
|
_timeChip('Gestern (17:30)', 'yesterday'),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
// Custom Date/Time Picker Trigger
|
|
GestureDetector(
|
|
onTap: _pickCustomDateTime,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.glassSurface,
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: AppTheme.glassBorder),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.calendar_today, size: 16, color: AppTheme.accentCyan),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Ausführungszeit: ${_formatDateTime(_exitTimestamp)}',
|
|
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
Text('Ändern', style: TextStyle(color: AppTheme.accentCyan, fontSize: 12, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// SECTION 3: GRUND / NOTIZ
|
|
const Text('3. AUSSTIEGSGRUND', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.5)),
|
|
const SizedBox(height: 8),
|
|
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
_reasonChip('🎯 Take-Profit gegriffen'),
|
|
_reasonChip('📱 Manuell in TR verkauft'),
|
|
_reasonChip('🛑 Stop-Loss ausgelöst'),
|
|
_reasonChip('🕒 Vor Wochenende / Time-Stop'),
|
|
_reasonChip('⚠️ Risiko minimiert'),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// LIVE REALISIERTER PNL VORSCHAU
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: pnlColor.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: pnlColor.withValues(alpha: 0.4)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('Realisierter Gewinn / Verlust (PnL):', style: const TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.bold)),
|
|
Text(
|
|
'${(isWin ? "+€" : "-€")}${_calculatedPnlAbs.abs().toStringAsFixed(2)} (${isWin ? "+" : ""}${_calculatedPnlPct.toStringAsFixed(2)}%)',
|
|
style: TextStyle(color: pnlColor, fontSize: 17, fontWeight: FontWeight.w900),
|
|
),
|
|
],
|
|
),
|
|
const Divider(color: Colors.white12, height: 16),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('Verkaufserlös (Gesamt): €${_calculatedProceeds.toStringAsFixed(2)}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
|
Text('Netto nach Gebühren: €${(_posSize + _calculatedPnlAbs).toStringAsFixed(2)}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Actions Footer
|
|
Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text('Abbrechen', style: TextStyle(color: AppTheme.textMuted)),
|
|
),
|
|
const Spacer(),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
final req = CloseTradeRequestDto(
|
|
userExitPrice: _exitPrice,
|
|
userExitTimestamp: _exitTimestamp,
|
|
exitFee: _exitFee,
|
|
closeReason: _selectedReasonTag,
|
|
);
|
|
Navigator.pop(context);
|
|
widget.onClose(req);
|
|
},
|
|
icon: const Icon(Icons.check_circle, size: 18),
|
|
label: const Text('Position Exakt So Buchen', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: pnlColor,
|
|
foregroundColor: isWin ? Colors.black : Colors.white,
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _summaryCol(String label, String val) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 10)),
|
|
const SizedBox(height: 2),
|
|
Text(val, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _timeChip(String label, String option) {
|
|
return GestureDetector(
|
|
onTap: () => _selectTimeOption(option),
|
|
child: Container(
|
|
margin: const EdgeInsets.only(right: 8),
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.glassSurface,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: AppTheme.glassBorder),
|
|
),
|
|
child: Text(label, style: const TextStyle(color: Colors.white70, fontSize: 11, fontWeight: FontWeight.bold)),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _reasonChip(String label) {
|
|
final isSelected = _selectedReasonTag == label;
|
|
return GestureDetector(
|
|
onTap: () => setState(() => _selectedReasonTag = label),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? AppTheme.accentCyan.withValues(alpha: 0.2) : AppTheme.glassSurface,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: isSelected ? AppTheme.accentCyan : AppTheme.glassBorder),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: isSelected ? AppTheme.accentCyan : Colors.white70,
|
|
fontSize: 11,
|
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatDateTime(DateTime dt) {
|
|
final d = dt.day.toString().padLeft(2, '0');
|
|
final m = dt.month.toString().padLeft(2, '0');
|
|
final y = dt.year;
|
|
final h = dt.hour.toString().padLeft(2, '0');
|
|
final min = dt.minute.toString().padLeft(2, '0');
|
|
return '$d.$m.$y um $h:$min Uhr';
|
|
}
|
|
}
|