Files
filament-tracker/lib/widgets/person_weight_card.dart

435 lines
15 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:intl/intl.dart';
import '../models/home_model.dart';
import 'outlined_text.dart';
class PersonWeightCard extends StatelessWidget {
/// Alle Einträge einer Person, absteigend nach Datum sortiert.
final String personName;
final List<WeightModel> entries;
final VoidCallback? onAddWeight;
/// Wird aufgerufen wenn ein Verlaufseintrag oder der Header-Eintrag
/// zum Bearbeiten angeklickt wird.
final void Function(WeightModel entry)? onEditEntry;
const PersonWeightCard({
super.key,
required this.personName,
required this.entries,
this.onAddWeight,
this.onEditEntry,
});
@override
Widget build(BuildContext context) {
// Defensiv: sicher sortiert (neuestes zuerst)
final sorted = [...entries]..sort((a, b) => b.date.compareTo(a.date));
final current = sorted.first;
final history = sorted.skip(1).toList();
return ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.15),
width: 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Header ──────────────────────────────────────────
_CardHeader(
current: current,
name: personName,
onAddWeight: onAddWeight,
onEdit: onEditEntry != null
? () => onEditEntry!(current)
: null,
),
// ── Historische Einträge ─────────────────────────────
if (history.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
child: Text(
'Verlauf',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.45),
fontSize: 12,
letterSpacing: 1.2,
fontWeight: FontWeight.w600,
),
),
),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 220),
child: ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.only(
left: 12,
right: 12,
bottom: 12,
),
itemCount: history.length,
separatorBuilder: (_, _) => Divider(
color: Colors.white.withValues(alpha: 0.08),
height: 1,
),
itemBuilder: (context, i) => _HistoryRow(
entry: history[i],
onTap: onEditEntry != null
? () => onEditEntry!(history[i])
: null,
),
),
),
] else ...[
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Text(
'Noch keine älteren Einträge.',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.35),
fontSize: 13,
),
),
),
],
],
),
),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Header
// ─────────────────────────────────────────────────────────────────────────────
class _CardHeader extends StatelessWidget {
final WeightModel current;
final String name;
final VoidCallback? onAddWeight;
final VoidCallback? onEdit;
const _CardHeader({
required this.current,
required this.name,
this.onAddWeight,
this.onEdit,
});
@override
Widget build(BuildContext context) {
final changeColor = current.weightChange < 0
? const Color(0xFF4FFFB0)
: current.weightChange > 0
? const Color(0xFFFF6B6B)
: Colors.white54;
final changeIcon = current.weightChange < 0
? Icons.trending_down_rounded
: current.weightChange > 0
? Icons.trending_up_rounded
: Icons.remove_rounded;
return Container(
padding: const EdgeInsets.fromLTRB(16, 18, 16, 14),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
border: Border(
bottom: BorderSide(color: Colors.white.withValues(alpha: 0.1)),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Avatar
CircleAvatar(
radius: 26,
backgroundColor: Colors.white.withValues(alpha: 0.12),
child: OutlinedText(
name[0].toUpperCase(),
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w900),
fillColor: Colors.white,
strokeColor: Colors.black,
strokeWidth: 1.5,
),
),
const SizedBox(width: 14),
// Name + Datum
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
OutlinedText(
name,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w900,
letterSpacing: 0.4,
),
fillColor: Colors.white,
strokeColor: Colors.black,
strokeWidth: 2,
),
const SizedBox(height: 2),
Text(
'Zuletzt: ${DateFormat('dd.MM.yyyy').format(current.date)}',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 12,
letterSpacing: 0.3,
),
),
],
),
),
// Add-Button
if (onAddWeight != null) _GlassAddButton(onPressed: onAddWeight!),
if (onEdit != null) ...[
const SizedBox(width: 6),
_GlassEditButton(onPressed: onEdit!),
],
const SizedBox(width: 8),
// Aktuelles Gewicht + Änderung
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
OutlinedText(
'${current.weight.toStringAsFixed(1)} kg',
style: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.w900,
letterSpacing: 0.2,
),
fillColor: Colors.white,
strokeColor: Colors.black,
strokeWidth: 2,
),
const SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(changeIcon, color: changeColor, size: 16),
const SizedBox(width: 3),
Text(
current.weightChange == 0
? '±0.0 kg'
: '${current.weightChange > 0 ? '+' : ''}${current.weightChange.toStringAsFixed(1)} kg',
style: TextStyle(
color: changeColor,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Verlaufs-Zeile
// ─────────────────────────────────────────────────────────────────────────────
class _HistoryRow extends StatelessWidget {
final WeightModel entry;
final VoidCallback? onTap;
const _HistoryRow({required this.entry, this.onTap});
@override
Widget build(BuildContext context) {
final changeColor = entry.weightChange < 0
? const Color(0xFF4FFFB0)
: entry.weightChange > 0
? const Color(0xFFFF6B6B)
: Colors.white54;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 6),
child: Row(
children: [
// Datum
Expanded(
flex: 3,
child: Text(
DateFormat('dd.MM.yyyy').format(entry.date),
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6),
fontSize: 14,
),
),
),
// Gewicht
Expanded(
flex: 2,
child: Text(
'${entry.weight.toStringAsFixed(1)} kg',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
// Änderung
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(
entry.weightChange < 0
? Icons.arrow_downward_rounded
: entry.weightChange > 0
? Icons.arrow_upward_rounded
: Icons.remove_rounded,
color: changeColor,
size: 14,
),
const SizedBox(width: 2),
Text(
entry.weightChange == 0
? '±0.0'
: '${entry.weightChange > 0 ? '+' : ''}${entry.weightChange.toStringAsFixed(1)}',
style: TextStyle(
color: changeColor,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
],
),
),
// Edit-Button gut sichtbar auf Mobile
if (onTap != null) ...[
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.white.withValues(alpha: 0.25),
),
),
child: const Icon(
Icons.edit_outlined,
size: 16,
color: Colors.white,
),
),
],
],
),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Glas-Add-Button (runder "+"-Button passend zum Karten-Header)
// ─────────────────────────────────────────────────────────────────────────────
/// Ein runder Bearbeiten-Button im Glas-Stil.
class _GlassEditButton extends StatelessWidget {
final VoidCallback onPressed;
const _GlassEditButton({required this.onPressed});
@override
Widget build(BuildContext context) {
return Tooltip(
message: 'Eintrag bearbeiten',
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(50),
child: ClipRRect(
borderRadius: BorderRadius.circular(50),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.10),
shape: BoxShape.circle,
border: Border.all(color: Colors.white.withValues(alpha: 0.25)),
),
child: const Icon(
Icons.edit_outlined,
color: Colors.white,
size: 18,
),
),
),
),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Glas-Add-Button
// ─────────────────────────────────────────────────────────────────────────────
class _GlassAddButton extends StatelessWidget {
final VoidCallback onPressed;
const _GlassAddButton({required this.onPressed});
@override
Widget build(BuildContext context) {
return Tooltip(
message: 'Gewicht hinzufügen',
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(50),
child: ClipRRect(
borderRadius: BorderRadius.circular(50),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.14),
shape: BoxShape.circle,
border: Border.all(color: Colors.white.withValues(alpha: 0.3)),
),
child: const Icon(
Icons.add_rounded,
color: Colors.white,
size: 20,
),
),
),
),
),
);
}
}