refactor: Extract SakePricingSection from sake_detail_screen.dart, bump to v1.0.16+27
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3502694d89
commit
1a50c739a1
|
|
@ -0,0 +1,351 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,8 @@ import '../widgets/analyzing_dialog.dart';
|
|||
import '../widgets/sake_3d_carousel_with_reason.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import '../providers/sake_list_provider.dart';
|
||||
import '../services/pricing_calculator.dart';
|
||||
import '../providers/theme_provider.dart';
|
||||
import '../models/user_profile.dart';
|
||||
import 'sake_detail/sections/sake_pricing_section.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
import '../widgets/common/munyun_like_button.dart';
|
||||
|
|
@ -579,20 +578,14 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
),
|
||||
),
|
||||
|
||||
// Phase 2-3: Business Pricing Section
|
||||
// Phase 2-3: Business Pricing Section (Extracted)
|
||||
SliverToBoxAdapter(
|
||||
child: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final userProfile = ref.watch(userProfileProvider);
|
||||
if (!userProfile.isBusinessMode) return const SizedBox.shrink();
|
||||
|
||||
return _buildPricingSection(context, userProfile);
|
||||
},
|
||||
child: SakePricingSection(
|
||||
sake: _sake,
|
||||
onUpdated: (updated) => setState(() => _sake = updated),
|
||||
),
|
||||
),
|
||||
|
||||
// End of Pricing Section
|
||||
|
||||
// Gap with Safe Area
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom),
|
||||
|
|
@ -815,340 +808,6 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
setState(() => _sake = newItem);
|
||||
}
|
||||
|
||||
// Phase 2-3: Business Pricing UI (Simplified)
|
||||
Widget _buildPricingSection(BuildContext context, UserProfile userProfile) {
|
||||
final appColors = Theme.of(context).extension<AppColors>()!;
|
||||
|
||||
// Calculated Price
|
||||
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(userProfile),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: appColors.brandPrimary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
child: const Text('編集'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showPriceSettingsDialog(UserProfile argProfile) async {
|
||||
// final userProfile = ref.read(userProfileProvider); // Now passed as arg
|
||||
if (!argProfile.isBusinessMode) return;
|
||||
|
||||
int? cost = _sake.userData.costPrice;
|
||||
int? manual = _sake.userData.price;
|
||||
double markup = _sake.userData.markup;
|
||||
// Copy existing variants
|
||||
Map<String, int> variants = Map.from(_sake.userData.priceVariants ?? {});
|
||||
|
||||
// Tiny State for Inline Adding
|
||||
String tempName = '';
|
||||
String tempPrice = ''; // String to handle empty better
|
||||
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;
|
||||
// Clear inputs
|
||||
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: [
|
||||
// 1. Manual Price (Top Priority)
|
||||
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),
|
||||
|
||||
// 2. Variants (Inline Entry)
|
||||
const Text('提供サイズ選択', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Presets Chips
|
||||
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(() {
|
||||
// Auto-fill logic
|
||||
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),
|
||||
|
||||
// Inline Inputs
|
||||
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, // Using controller to clear
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '価格',
|
||||
suffixText: '円',
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (v) => tempPrice = v,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
|
||||
// List of Added Variants
|
||||
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),
|
||||
|
||||
// 3. Auto Calculation (Accordion)
|
||||
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);
|
||||
setState(() => _sake = newItem);
|
||||
}
|
||||
|
||||
Future<void> _showDeleteDialog(BuildContext context) async {
|
||||
final navigator = Navigator.of(context);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.15+26
|
||||
version: 1.0.16+27
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.1
|
||||
|
|
|
|||
Loading…
Reference in New Issue