Files
ertl_filament_verwaltung/lib/widgets/entry_panel.dart
T
2026-10-27 15:00:16 +01:00

241 lines
6.4 KiB
Dart

import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class EntryPanel extends StatelessWidget {
const EntryPanel({
super.key,
required this.title,
required this.subtitle,
required this.child,
});
final String title;
final String subtitle;
final Widget child;
@override
Widget build(BuildContext context) => Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppTheme.inputSurface,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: const Color(0xFFCEE6DF)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
title,
style: const TextStyle(fontSize: 25, fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(
subtitle,
style: const TextStyle(color: AppTheme.muted, height: 1.5),
),
const SizedBox(height: 24),
child,
],
),
);
}
class EntryMessage extends StatelessWidget {
const EntryMessage({
super.key,
required this.message,
required this.hasError,
});
final String? message;
final bool hasError;
@override
Widget build(BuildContext context) => message == null
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.only(top: 16),
child: Semantics(
liveRegion: true,
child: Text(
message!,
style: TextStyle(
color: hasError
? Theme.of(context).colorScheme.error
: AppTheme.teal,
),
),
),
);
}
class EntryNumberField extends StatelessWidget {
const EntryNumberField({
super.key,
required this.controller,
required this.label,
required this.unit,
required this.validator,
});
final TextEditingController controller;
final String label;
final String unit;
final FormFieldValidator<String> validator;
@override
Widget build(BuildContext context) => TextFormField(
controller: controller,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(labelText: label, suffixText: unit),
validator: validator,
);
}
class EntryFieldGrid extends StatelessWidget {
const EntryFieldGrid({super.key, required this.children});
final List<Widget> children;
@override
Widget build(BuildContext context) => LayoutBuilder(
builder: (context, constraints) {
final columns = constraints.maxWidth >= 600 ? 2 : 1;
final width = (constraints.maxWidth - (columns - 1) * 16) / columns;
return Wrap(
spacing: 16,
runSpacing: 16,
children: children
.map((child) => SizedBox(width: width, child: child))
.toList(),
);
},
);
}
/// Lazily builds saved entries below the input form.
class SavedEntries extends StatelessWidget {
const SavedEntries({
super.key,
required this.title,
required this.count,
required this.emptyText,
required this.itemBuilder,
});
final String title;
final int count;
final String emptyText;
final IndexedWidgetBuilder itemBuilder;
@override
Widget build(BuildContext context) => DecoratedSliver(
decoration: BoxDecoration(
color: AppTheme.inventorySurface,
borderRadius: BorderRadius.circular(24),
),
sliver: SliverPadding(
padding: const EdgeInsets.all(24),
sliver: SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Text(
'$title ($count)',
style: const TextStyle(
color: Colors.white,
fontSize: 23,
fontWeight: FontWeight.w700,
),
),
),
),
if (count == 0)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Text(
emptyText,
style: const TextStyle(color: Color(0xFFB9CCD9), height: 1.5),
),
),
)
else
SliverList.separated(
itemCount: count,
itemBuilder: itemBuilder,
separatorBuilder: (context, index) => const SizedBox(height: 12),
),
],
),
),
);
}
class SavedEntryCard extends StatelessWidget {
const SavedEntryCard({
super.key,
required this.title,
required this.subtitle,
required this.details,
this.notes = '',
});
final String title;
final String subtitle;
final List<String> details;
final String notes;
@override
Widget build(BuildContext context) => Card(
margin: EdgeInsets.zero,
elevation: 0,
color: Colors.white,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
title,
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 18),
),
const SizedBox(height: 6),
Text(
subtitle,
style: const TextStyle(color: AppTheme.muted, height: 1.5),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: details
.map(
(detail) => Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
decoration: BoxDecoration(
color: AppTheme.inputSurface,
borderRadius: BorderRadius.circular(8),
),
child: Text(
detail,
style: const TextStyle(color: AppTheme.teal),
),
),
)
.toList(),
),
if (notes.isNotEmpty) ...[
const SizedBox(height: 14),
Text(
notes,
style: const TextStyle(color: AppTheme.muted, height: 1.5),
),
],
],
),
),
);
}