Files
weight-tracker/lib/widgets/add_weight_dialog.dart
2026-02-24 22:38:01 +01:00

566 lines
20 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../models/home_model.dart';
class AddWeightDialog extends StatefulWidget {
final String userId;
final String personName;
/// Letztes bekanntes Gewicht dieser Person wird für weightChange benötigt.
final double lastWeight;
/// Wenn gesetzt → Edit-Modus: Felder werden vorausgefüllt.
final WeightModel? existingEntry;
const AddWeightDialog({
super.key,
required this.userId,
required this.personName,
required this.lastWeight,
this.existingEntry,
});
/// Öffnet den Dialog zum Hinzufügen eines neuen Eintrags.
static Future<WeightModel?> show({
required String userId,
required String personName,
required double lastWeight,
}) {
return Get.dialog<WeightModel>(
AddWeightDialog(
userId: userId,
personName: personName,
lastWeight: lastWeight,
),
barrierColor: Colors.black.withValues(alpha: 0.55),
);
}
/// Öffnet den Dialog zum Bearbeiten eines vorhandenen Eintrags.
/// [previousWeight] = Gewicht des Eintrags VOR dem zu bearbeitenden.
static Future<WeightModel?> showEdit({
required WeightModel entry,
required double previousWeight,
}) {
return Get.dialog<WeightModel>(
AddWeightDialog(
userId: entry.documentId,
personName: entry.name,
lastWeight: previousWeight,
existingEntry: entry,
),
barrierColor: Colors.black.withValues(alpha: 0.55),
);
}
@override
State<AddWeightDialog> createState() => _AddWeightDialogState();
}
class _AddWeightDialogState extends State<AddWeightDialog>
with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _weightController = TextEditingController();
late final TextEditingController _nameController;
DateTime _selectedDate = DateTime.now();
double? _parsedWeight;
late AnimationController _anim;
late Animation<double> _fadeScale;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.personName);
// Edit-Modus: Felder vorausfüllen
if (widget.existingEntry != null) {
final e = widget.existingEntry!;
_weightController.text = e.weight.toString().replaceAll('.', ',');
_parsedWeight = e.weight;
_selectedDate = e.date;
}
_anim = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 220),
);
_fadeScale = CurvedAnimation(parent: _anim, curve: Curves.easeOutBack);
_anim.forward();
}
@override
void dispose() {
_weightController.dispose();
_nameController.dispose();
_anim.dispose();
super.dispose();
}
double get _weightChange {
if (_parsedWeight == null || widget.lastWeight == 0) return 0;
return double.parse(
(_parsedWeight! - widget.lastWeight).toStringAsFixed(2),
);
}
Color get _changeColor {
if (_weightChange < 0) return const Color(0xFF4FFFB0);
if (_weightChange > 0) return const Color(0xFFFF6B6B);
return Colors.white54;
}
String get _changeLabel {
if (_parsedWeight == null) return '';
if (widget.lastWeight == 0) return 'Erster Eintrag';
final sign = _weightChange > 0 ? '+' : '';
return '$sign${_weightChange.toStringAsFixed(1)} kg';
}
Future<void> _pickDate() async {
final picked = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime(2100),
builder: (context, child) => Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.dark(
primary: Color(0xFF7B9FFF),
onSurface: Colors.white,
surface: Color(0xFF1A2035),
),
),
child: child!,
),
);
if (picked != null) setState(() => _selectedDate = picked);
}
void _submit() {
if (!_formKey.currentState!.validate()) return;
final name = _nameController.text.trim();
final entry = WeightModel(
// Im Edit-Modus dieselbe ID behalten
documentId:
widget.existingEntry?.documentId ??
DateTime.now().millisecondsSinceEpoch.toString(),
name: name,
weight: _parsedWeight!,
date: _selectedDate,
weightChange: widget.lastWeight == 0 ? 0 : _weightChange,
);
Navigator.of(context).pop(entry);
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _fadeScale,
child: FadeTransition(
opacity: _anim,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 460),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 24, sigmaY: 24),
child: Material(
type: MaterialType.transparency,
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
width: 1,
),
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Titelzeile ──────────────────────────
_DialogHeader(
personName: widget.personName,
isEdit: widget.existingEntry != null,
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Name
_GlassField(
label: 'Name',
icon: Icons.person_outline_rounded,
child: TextFormField(
controller: _nameController,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
decoration: _inputDecoration('z. B. Joe'),
validator: (v) =>
(v == null || v.trim().isEmpty)
? 'Name eingeben'
: null,
),
),
const SizedBox(height: 14),
// Gewicht
_GlassField(
label: 'Gewicht',
icon: Icons.monitor_weight_outlined,
child: TextFormField(
controller: _weightController,
keyboardType:
const TextInputType.numberWithOptions(
decimal: true,
),
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d*[.,]?\d*'),
),
],
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
decoration: _inputDecoration(
'z. B. 72,5',
),
onChanged: (v) {
final parsed = double.tryParse(
v.replaceAll(',', '.'),
);
setState(() => _parsedWeight = parsed);
},
validator: (v) {
if (v == null || v.isEmpty) {
return 'Gewicht eingeben';
}
if (double.tryParse(
v.replaceAll(',', '.'),
) ==
null) {
return 'Ungültige Zahl';
}
return null;
},
),
),
const SizedBox(height: 14),
// Datum
_GlassField(
label: 'Datum',
icon: Icons.calendar_today_outlined,
child: InkWell(
onTap: _pickDate,
borderRadius: BorderRadius.circular(10),
child: InputDecorator(
decoration: _inputDecoration(''),
child: Text(
DateFormat(
'dd.MM.yyyy',
).format(_selectedDate),
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
),
),
),
const SizedBox(height: 20),
// Vorschau weightChange
if (_parsedWeight != null)
_ChangePreview(
label: _changeLabel,
color: _changeColor,
lastWeight: widget.lastWeight,
),
if (_parsedWeight != null)
const SizedBox(height: 20),
// Buttons
Row(
children: [
Expanded(
child: _GlassButton(
label: 'Abbrechen',
onPressed: () =>
Navigator.of(context).pop(),
primary: false,
),
),
const SizedBox(width: 12),
Expanded(
child: _GlassButton(
label: widget.existingEntry != null
? 'Speichern'
: 'Hinzufügen',
onPressed: _submit,
primary: true,
),
),
],
),
const SizedBox(height: 20),
],
),
),
],
),
), // Form
), // Container
), // Material
), // BackdropFilter
), // ClipRRect
), // Padding
), // ConstrainedBox
), // Center
), // FadeTransition
); // ScaleTransition
}
InputDecoration _inputDecoration(String hint) => InputDecoration(
hintText: hint,
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.35),
fontSize: 14,
),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.07),
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.2)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.2)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFF7B9FFF), width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFFFF6B6B), width: 1.2),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFFFF6B6B), width: 1.5),
),
errorStyle: const TextStyle(color: Color(0xFFFF6B6B), fontSize: 11),
);
}
// ─────────────────────────────────────────────────────────────────────────────
// Hilfs-Widgets
// ─────────────────────────────────────────────────────────────────────────────
class _DialogHeader extends StatelessWidget {
final String personName;
final bool isEdit;
const _DialogHeader({required this.personName, this.isEdit = false});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(20, 18, 12, 14),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.06),
border: Border(
bottom: BorderSide(color: Colors.white.withValues(alpha: 0.12)),
),
),
child: Row(
children: [
Icon(
isEdit ? Icons.edit_outlined : Icons.add_circle_outline_rounded,
color: const Color(0xFF7B9FFF),
size: 22,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isEdit ? 'Eintrag bearbeiten' : 'Gewicht hinzufügen',
style: const TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
Text(
personName,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.55),
fontSize: 13,
),
),
],
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(
Icons.close_rounded,
color: Colors.white.withValues(alpha: 0.5),
size: 20,
),
splashRadius: 18,
),
],
),
);
}
}
class _GlassField extends StatelessWidget {
final String label;
final IconData icon;
final Widget child;
const _GlassField({
required this.label,
required this.icon,
required this.child,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: Colors.white.withValues(alpha: 0.5), size: 14),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6),
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 0.8,
),
),
],
),
const SizedBox(height: 6),
child,
],
);
}
}
class _ChangePreview extends StatelessWidget {
final String label;
final Color color;
final double lastWeight;
const _ChangePreview({
required this.label,
required this.color,
required this.lastWeight,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: color.withValues(alpha: 0.35)),
),
child: Row(
children: [
Icon(Icons.swap_vert_rounded, color: color, size: 18),
const SizedBox(width: 8),
Text(
lastWeight == 0 ? label : 'Änderung zum letzten Eintrag: ',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 13,
),
),
if (lastWeight != 0)
Text(
label,
style: TextStyle(
color: color,
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
class _GlassButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
final bool primary;
const _GlassButton({
required this.label,
required this.onPressed,
required this.primary,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 13),
decoration: BoxDecoration(
color: primary
? const Color(0xFF7B9FFF).withValues(alpha: 0.25)
: Colors.white.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: primary
? const Color(0xFF7B9FFF).withValues(alpha: 0.6)
: Colors.white.withValues(alpha: 0.15),
width: 1,
),
),
alignment: Alignment.center,
child: Text(
label,
style: TextStyle(
color: primary ? const Color(0xFF7B9FFF) : Colors.white60,
fontWeight: FontWeight.w700,
fontSize: 14,
letterSpacing: 0.3,
),
),
),
),
);
}
}