Files
submanager/lib/widgets/payment_card/payment_card_form_sheet.dart
2025-11-14 16:53:41 +09:00

291 lines
9.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../l10n/app_localizations.dart';
import '../../models/payment_card_model.dart';
import '../../providers/payment_card_provider.dart';
import '../../utils/payment_card_utils.dart';
class PaymentCardFormSheet extends StatefulWidget {
final PaymentCardModel? card;
final String? initialIssuerName;
final String? initialLast4;
final String? initialColorHex;
final String? initialIconName;
const PaymentCardFormSheet({
super.key,
this.card,
this.initialIssuerName,
this.initialLast4,
this.initialColorHex,
this.initialIconName,
});
static Future<String?> show(
BuildContext context, {
PaymentCardModel? card,
String? initialIssuerName,
String? initialLast4,
String? initialColorHex,
String? initialIconName,
}) async {
return showModalBottomSheet<String>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (_) => PaymentCardFormSheet(
card: card,
initialIssuerName: initialIssuerName,
initialLast4: initialLast4,
initialColorHex: initialColorHex,
initialIconName: initialIconName,
),
);
}
@override
State<PaymentCardFormSheet> createState() => _PaymentCardFormSheetState();
}
class _PaymentCardFormSheetState extends State<PaymentCardFormSheet> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _issuerController;
late TextEditingController _last4Controller;
late String _selectedColor;
late String _selectedIcon;
late bool _isDefault;
bool _isSaving = false;
@override
void initState() {
super.initState();
_issuerController = TextEditingController(
text: widget.card?.issuerName ?? widget.initialIssuerName ?? '',
);
_last4Controller = TextEditingController(
text: widget.card?.last4 ?? widget.initialLast4 ?? '',
);
_selectedColor = widget.card?.colorHex ??
widget.initialColorHex ??
PaymentCardUtils.colorPalette.first;
_selectedIcon = widget.card?.iconName ??
widget.initialIconName ??
PaymentCardUtils.iconMap.keys.first;
_isDefault = widget.card?.isDefault ?? false;
}
@override
void dispose() {
_issuerController.dispose();
_last4Controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
final isEditing = widget.card != null;
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
isEditing ? loc.editPaymentCard : loc.addPaymentCard,
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: 16),
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _issuerController,
decoration: InputDecoration(
labelText: loc.paymentCardIssuer,
border: const OutlineInputBorder(),
),
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return loc.requiredFieldsError;
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _last4Controller,
decoration: InputDecoration(
labelText: loc.paymentCardLast4,
border: const OutlineInputBorder(),
counterText: '',
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
],
validator: (value) {
if (value == null || value.length != 4) {
return loc.paymentCardLast4;
}
return null;
},
),
const SizedBox(height: 16),
Text(
loc.paymentCardColor,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: PaymentCardUtils.colorPalette.map((hex) {
final color = PaymentCardUtils.colorFromHex(hex);
final selected = _selectedColor == hex;
return GestureDetector(
onTap: () {
setState(() {
_selectedColor = hex;
});
},
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: selected
? Theme.of(context).colorScheme.onSurface
: Colors.transparent,
width: 2,
),
),
),
);
}).toList(),
),
const SizedBox(height: 16),
Text(
loc.paymentCardIcon,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: PaymentCardUtils.iconMap.entries.map((entry) {
final selected = _selectedIcon == entry.key;
return ChoiceChip(
label: Icon(entry.value,
color: selected
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurface),
selected: selected,
onSelected: (_) {
setState(() {
_selectedIcon = entry.key;
});
},
selectedColor: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
);
}).toList(),
),
const SizedBox(height: 16),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(loc.setAsDefaultCard),
value: _isDefault,
onChanged: (value) {
setState(() {
_isDefault = value;
});
},
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: _isSaving ? null : _handleSubmit,
child: _isSaving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(loc.save),
),
),
],
),
),
],
),
),
);
}
Future<void> _handleSubmit() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isSaving = true;
});
try {
final provider = context.read<PaymentCardProvider>();
String cardId;
if (widget.card == null) {
final card = await provider.addCard(
issuerName: _issuerController.text.trim(),
last4: _last4Controller.text.trim(),
colorHex: _selectedColor,
iconName: _selectedIcon,
isDefault: _isDefault,
);
cardId = card.id;
} else {
widget.card!
..issuerName = _issuerController.text.trim()
..last4 = _last4Controller.text.trim()
..colorHex = _selectedColor
..iconName = _selectedIcon
..isDefault = _isDefault;
await provider.updateCard(widget.card!);
cardId = widget.card!.id;
}
if (mounted) {
Navigator.of(context).pop(cardId);
}
} finally {
if (mounted) {
setState(() {
_isSaving = false;
});
}
}
}
}