ponshu-room-lite/lib/screens/sake_detail/sections/sake_pricing_section.dart

352 lines
15 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../../models/sake_item.dart';
import '../../../models/user_profile.dart';
import '../../../providers/theme_provider.dart';
import '../../../services/pricing_calculator.dart';
import '../../../theme/app_colors.dart';
/// Business pricing section for sake detail screen.
/// Displays pricing info and provides price editing dialog.
class SakePricingSection extends ConsumerWidget {
final SakeItem sake;
final ValueChanged<SakeItem> onUpdated;
const SakePricingSection({
super.key,
required this.sake,
required this.onUpdated,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userProfile = ref.watch(userProfileProvider);
if (!userProfile.isBusinessMode) return const SizedBox.shrink();
return _buildPricingContent(context, userProfile);
}
Widget _buildPricingContent(BuildContext context, UserProfile userProfile) {
final appColors = Theme.of(context).extension<AppColors>()!;
final calculatedPrice = PricingCalculator.calculatePrice(sake);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Theme.of(context).primaryColor.withValues(alpha: 0.2)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(LucideIcons.coins, color: appColors.brandPrimary, size: 18),
const SizedBox(width: 6),
Text(
'価格設定',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: appColors.brandPrimary),
),
],
),
const SizedBox(height: 4),
Text(
calculatedPrice > 0
? '現在$calculatedPrice円'
: '未設定',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: calculatedPrice > 0
? appColors.brandPrimary
: appColors.textTertiary,
),
),
],
),
ElevatedButton(
onPressed: () => _showPriceSettingsDialog(context, userProfile),
style: ElevatedButton.styleFrom(
backgroundColor: appColors.brandPrimary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
child: const Text('編集'),
),
],
),
);
}
Future<void> _showPriceSettingsDialog(BuildContext context, UserProfile argProfile) async {
if (!argProfile.isBusinessMode) return;
int? cost = sake.userData.costPrice;
int? manual = sake.userData.price;
double markup = sake.userData.markup;
Map<String, int> variants = Map.from(sake.userData.priceVariants ?? {});
String tempName = '';
String tempPrice = '';
final TextEditingController nameController = TextEditingController();
final TextEditingController priceController = TextEditingController();
await showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setModalState) {
void addVariant() {
if (tempName.isNotEmpty && tempPrice.isNotEmpty) {
final parsedPrice = int.tryParse(tempPrice);
if (parsedPrice != null) {
setModalState(() {
variants[tempName] = parsedPrice;
tempName = '';
tempPrice = '';
nameController.clear();
priceController.clear();
});
}
}
}
return AlertDialog(
title: const Text('価格設定', style: TextStyle(fontWeight: FontWeight.bold)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
initialValue: manual?.toString() ?? '',
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '販売価格 (税込)',
hintText: '手動で設定する場合に入力',
suffixText: '',
border: OutlineInputBorder(),
),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
onChanged: (v) => setModalState(() => manual = int.tryParse(v)),
),
const SizedBox(height: 24),
const Text('提供サイズ選択', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Wrap(
spacing: 8.0,
runSpacing: 4.0,
children: [
for (var preset in ['グラス (90ml)', '一合 (180ml)', 'ボトル (720ml)'])
ChoiceChip(
label: Text(preset),
selected: tempName == preset,
onSelected: (selected) {
setModalState(() {
if (selected) {
tempName = preset;
nameController.text = preset;
}
});
},
backgroundColor: Theme.of(context).extension<AppColors>()!.surfaceSubtle,
selectedColor: Theme.of(context).extension<AppColors>()!.brandAccent.withValues(alpha: 0.3),
labelStyle: TextStyle(
color: (tempName == preset)
? Theme.of(context).extension<AppColors>()!.brandPrimary
: Theme.of(context).extension<AppColors>()!.textPrimary,
fontWeight: (tempName == preset) ? FontWeight.bold : null,
),
),
],
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: '名称',
hintText: '例: 徳利',
isDense: true,
border: OutlineInputBorder(),
),
onChanged: (v) => tempName = v,
),
),
const SizedBox(width: 8),
Expanded(
flex: 2,
child: TextField(
controller: priceController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '価格',
suffixText: '',
isDense: true,
border: OutlineInputBorder(),
),
onChanged: (v) => tempPrice = v,
onSubmitted: (_) => addVariant(),
),
),
],
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(LucideIcons.plus),
label: const Text('リストに追加'),
style: ElevatedButton.styleFrom(
backgroundColor: (tempName.isNotEmpty && tempPrice.isNotEmpty)
? Theme.of(context).extension<AppColors>()!.brandAccent
: Theme.of(context).extension<AppColors>()!.surfaceSubtle,
foregroundColor: Colors.white,
),
onPressed: (tempName.isNotEmpty && tempPrice.isNotEmpty)
? addVariant
: null,
),
),
const SizedBox(height: 16),
if (variants.isNotEmpty)
Container(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).extension<AppColors>()!.divider),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
...variants.entries.map((e) => Column(
children: [
ListTile(
dense: true,
title: Text(e.key, style: const TextStyle(fontWeight: FontWeight.w500)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('${PricingCalculator.formatPrice(e.value)}', style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
IconButton(
icon: Icon(LucideIcons.x, color: Theme.of(context).extension<AppColors>()!.iconSubtle, size: 18),
onPressed: () {
setModalState(() {
variants.remove(e.key);
});
},
),
],
),
),
if (e.key != variants.keys.last) const Divider(height: 1),
],
)),
],
),
),
const SizedBox(height: 24),
ExpansionTile(
title: Text('原価・掛率設定 (自動計算)', style: TextStyle(fontSize: 14, color: Theme.of(context).extension<AppColors>()!.textSecondary)),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8),
child: Column(
children: [
TextFormField(
initialValue: cost?.toString() ?? '',
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '仕入れ値 (円)',
suffixText: '',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.currency_yen),
),
onChanged: (v) => setModalState(() => cost = int.tryParse(v)),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('掛率: ${markup.toStringAsFixed(1)}'),
TextButton(
child: const Text('リセット'),
onPressed: () => setModalState(() => markup = argProfile.defaultMarkup),
)
],
),
Slider(
value: markup,
min: 1.0,
max: 5.0,
divisions: 40,
label: markup.toStringAsFixed(1),
activeColor: Theme.of(context).extension<AppColors>()!.brandAccent,
onChanged: (v) => setModalState(() => markup = v),
),
Text(
'参考計算価格: ${PricingCalculator.formatPrice(PricingCalculator.roundUpTo50((cost ?? 0) * markup))}円 (50円切上)',
style: TextStyle(color: Theme.of(context).extension<AppColors>()!.textSecondary, fontSize: 12),
),
],
),
),
],
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('キャンセル'),
),
ElevatedButton(
onPressed: () {
_updatePricing(
costPrice: cost,
manualPrice: manual,
markup: markup,
priceVariants: variants.isEmpty ? null : variants,
);
Navigator.pop(context);
},
child: const Text('保存'),
),
],
);
}
),
);
}
Future<void> _updatePricing({int? costPrice, int? manualPrice, double? markup, Map<String, int>? priceVariants}) async {
final box = Hive.box<SakeItem>('sake_items');
final newItem = sake.copyWith(
costPrice: costPrice,
manualPrice: manualPrice,
markup: markup ?? sake.userData.markup,
priceVariants: priceVariants,
isUserEdited: true,
);
await box.put(sake.key, newItem);
onUpdated(newItem);
}
}