730 lines
22 KiB
Dart
730 lines
22 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import '../models/bean.dart';
|
|
import '../models/machine.dart';
|
|
import '../models/recipe.dart';
|
|
import '../models/drink.dart';
|
|
import '../models/journal_entry.dart';
|
|
|
|
class CsvDataService {
|
|
static final CsvDataService _instance = CsvDataService._internal();
|
|
factory CsvDataService() => _instance;
|
|
CsvDataService._internal();
|
|
|
|
List<Bean>? _cachedBeans;
|
|
List<Machine>? _cachedMachines;
|
|
List<Recipe>? _cachedRecipes;
|
|
List<Drink>? _cachedDrinks;
|
|
List<JournalEntry>? _cachedJournalEntries;
|
|
|
|
Map<String, OriginCountry>? _cachedOriginCountries;
|
|
|
|
Future<List<Bean>> getBeans() async {
|
|
if (_cachedBeans != null) return _cachedBeans!;
|
|
|
|
final csvBeans = await _loadBeansFromCsv();
|
|
_cachedBeans = [...csvBeans, ..._customBeans];
|
|
|
|
return _cachedBeans!;
|
|
}
|
|
|
|
Future<List<Bean>> _loadBeansFromCsv() async {
|
|
await _loadLookupData();
|
|
|
|
final csvData = await rootBundle.loadString('lib/database/Coffee_Beans.csv');
|
|
final lines = csvData.split('\n');
|
|
final headers = lines[0].split(',');
|
|
|
|
final beans = <Bean>[];
|
|
|
|
for (int i = 1; i < lines.length; i++) {
|
|
final line = lines[i].trim();
|
|
if (line.isEmpty) continue;
|
|
|
|
try {
|
|
final values = _parseCsvLine(line);
|
|
if (values.length >= headers.length) {
|
|
final bean = _createBeanFromCsv(headers, values);
|
|
beans.add(bean);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error parsing bean line $i: $e');
|
|
}
|
|
}
|
|
|
|
return beans;
|
|
}
|
|
|
|
Future<List<Machine>> getMachines() async {
|
|
if (_cachedMachines != null) return _cachedMachines!;
|
|
|
|
final csvMachines = await _loadMachinesFromCsv();
|
|
_cachedMachines = [...csvMachines, ..._customMachines];
|
|
|
|
return _cachedMachines!;
|
|
}
|
|
|
|
Future<List<Machine>> _loadMachinesFromCsv() async {
|
|
final csvData = await rootBundle.loadString('lib/database/Coffee_Machines.csv');
|
|
final lines = csvData.split('\n');
|
|
final headers = lines[0].split(',');
|
|
|
|
final machines = <Machine>[];
|
|
|
|
for (int i = 1; i < lines.length; i++) {
|
|
final line = lines[i].trim();
|
|
if (line.isEmpty) continue;
|
|
|
|
try {
|
|
final values = _parseCsvLine(line);
|
|
if (values.length >= headers.length) {
|
|
final machine = _createMachineFromCsv(headers, values);
|
|
machines.add(machine);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error parsing machine line $i: $e');
|
|
}
|
|
}
|
|
|
|
return machines;
|
|
}
|
|
|
|
Future<List<Recipe>> getRecipes() async {
|
|
if (_cachedRecipes != null) return _cachedRecipes!;
|
|
|
|
final csvRecipes = await _loadRecipesFromCsv();
|
|
_cachedRecipes = [...csvRecipes, ..._customRecipes];
|
|
|
|
return _cachedRecipes!;
|
|
}
|
|
|
|
Future<List<Recipe>> _loadRecipesFromCsv() async {
|
|
final csvData = await rootBundle.loadString('lib/database/Brew_Recipes.csv');
|
|
final lines = csvData.split('\n');
|
|
final headers = lines[0].split(',');
|
|
|
|
final recipes = <Recipe>[];
|
|
|
|
for (int i = 1; i < lines.length; i++) {
|
|
final line = lines[i].trim();
|
|
if (line.isEmpty) continue;
|
|
|
|
try {
|
|
final values = _parseCsvLine(line);
|
|
if (values.length >= headers.length) {
|
|
final recipe = _createRecipeFromCsv(headers, values);
|
|
recipes.add(recipe);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error parsing recipe line $i: $e');
|
|
}
|
|
}
|
|
|
|
return recipes;
|
|
}
|
|
|
|
Future<List<Drink>> getDrinks() async {
|
|
if (_cachedDrinks != null) return _cachedDrinks!;
|
|
_cachedDrinks = []; // Empty for now - user will add their own
|
|
return _cachedDrinks!;
|
|
}
|
|
|
|
Future<List<JournalEntry>> getJournalEntries() async {
|
|
if (_cachedJournalEntries != null) return _cachedJournalEntries!;
|
|
_cachedJournalEntries = []; // Empty for now - user will add their own
|
|
return _cachedJournalEntries!;
|
|
}
|
|
|
|
Future<void> _loadLookupData() async {
|
|
if (_cachedOriginCountries != null) return;
|
|
|
|
// Load Origin Countries
|
|
final countriesData = await rootBundle.loadString('lib/database/Origin_Countries.csv');
|
|
final countryLines = countriesData.split('\n');
|
|
final countryHeaders = countryLines[0].split(',');
|
|
|
|
_cachedOriginCountries = {};
|
|
for (int i = 1; i < countryLines.length; i++) {
|
|
final line = countryLines[i].trim();
|
|
if (line.isEmpty) continue;
|
|
|
|
try {
|
|
final values = _parseCsvLine(line);
|
|
if (values.length >= countryHeaders.length) {
|
|
final country = _createOriginCountryFromCsv(countryHeaders, values);
|
|
_cachedOriginCountries![country.id] = country;
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error parsing country line $i: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
List<String> _parseCsvLine(String line) {
|
|
final List<String> result = [];
|
|
bool inQuotes = false;
|
|
String current = '';
|
|
|
|
for (int i = 0; i < line.length; i++) {
|
|
final char = line[i];
|
|
|
|
if (char == '"') {
|
|
inQuotes = !inQuotes;
|
|
} else if (char == ',' && !inQuotes) {
|
|
result.add(current.trim());
|
|
current = '';
|
|
} else {
|
|
current += char;
|
|
}
|
|
}
|
|
|
|
result.add(current.trim());
|
|
return result;
|
|
}
|
|
|
|
Bean _createBeanFromCsv(List<String> headers, List<String> values) {
|
|
final Map<String, String> row = {};
|
|
for (int i = 0; i < headers.length && i < values.length; i++) {
|
|
row[headers[i]] = values[i];
|
|
}
|
|
|
|
return Bean(
|
|
id: row['id'] ?? '',
|
|
name: row['name'] ?? '',
|
|
origin: row['origin'] ?? '',
|
|
farm: row['farm'] ?? '',
|
|
producer: row['producer'] ?? '',
|
|
varietal: row['varietal'] ?? '',
|
|
altitude: _parseIntSafe(row['altitude']) ?? 1500,
|
|
processingMethod: row['processingMethod'] ?? '',
|
|
harvestSeason: row['harvestSeason'] ?? '',
|
|
flavorNotes: _parseJsonList(row['flavorNotes']).map((e) => _parseTastingNote(e)).toList(),
|
|
acidity: _parseAcidity(row['acidity']),
|
|
body: _parseBody(row['body']),
|
|
sweetness: _parseIntSafe(row['sweetness']) ?? 5,
|
|
roastLevel: _parseRoastLevel(row['roastLevel']),
|
|
cupScore: _parseDoubleSafe(row['cupScore']) ?? 85.0,
|
|
price: _parseDoubleSafe(row['price']) ?? 15.0,
|
|
availability: _parseAvailability(row['availability']),
|
|
certifications: _parseJsonList(row['certifications']),
|
|
roaster: row['roaster'] ?? '',
|
|
roastDate: _parseDateSafe(row['roastDate']) ?? DateTime.now(),
|
|
bestByDate: _parseDateSafe(row['bestByDate']) ?? DateTime.now().add(Duration(days: 365)),
|
|
brewingMethods: _parseJsonList(row['brewingMethods']),
|
|
isOwned: row['isOwned']?.toLowerCase() == 'true',
|
|
quantity: _parseDoubleSafe(row['quantity']) ?? 0.0,
|
|
notes: row['notes'] ?? '',
|
|
);
|
|
}
|
|
|
|
Machine _createMachineFromCsv(List<String> headers, List<String> values) {
|
|
final Map<String, String> row = {};
|
|
for (int i = 0; i < headers.length && i < values.length; i++) {
|
|
row[headers[i]] = values[i];
|
|
}
|
|
|
|
return Machine(
|
|
id: row['id'] ?? '',
|
|
manufacturer: row['manufacturer'] ?? '',
|
|
model: row['model'] ?? '',
|
|
year: _parseIntSafe(row['year']) ?? DateTime.now().year,
|
|
type: _parseMachineType(row['type']),
|
|
steamWand: row['steamWand']?.toLowerCase() == 'true',
|
|
details: row['details'] ?? '',
|
|
isOwned: row['isOwned']?.toLowerCase() == 'true',
|
|
rating: _parseDoubleSafe(row['rating']) ?? 4.0,
|
|
popularity: _parseIntSafe(row['popularity']) ?? 50,
|
|
portafilters: _parsePortafilters(row['portafilters']),
|
|
specifications: _parseJsonMap(row['specifications']),
|
|
);
|
|
}
|
|
|
|
Recipe _createRecipeFromCsv(List<String> headers, List<String> values) {
|
|
final Map<String, String> row = {};
|
|
for (int i = 0; i < headers.length && i < values.length; i++) {
|
|
row[headers[i]] = values[i];
|
|
}
|
|
|
|
return Recipe(
|
|
id: row['id'] ?? '',
|
|
name: row['name'] ?? '',
|
|
servingTemp: _parseServingTemp(row['servingTemp']),
|
|
milkType: _parseMilkType(row['milkType']),
|
|
brewMethod: _parseBrewMethod(row['brewMethod']),
|
|
grindSize: _parseGrindSize(row['grindSize']),
|
|
coffeeAmount: _parseDoubleSafe(row['coffeeAmount']) ?? 0,
|
|
waterAmount: _parseDoubleSafe(row['waterAmount']) ?? 0,
|
|
brewTime: _parseIntSafe(row['brewTime']) ?? 0,
|
|
instructions: row['instructions'] ?? '',
|
|
notes: row['notes'],
|
|
difficulty: _parseDifficulty(row['difficulty']),
|
|
equipmentNeeded: _parseJsonList(row['equipmentNeeded']),
|
|
yieldAmount: _parseDoubleSafe(row['yieldAmount']) ?? 0,
|
|
caffeinePer100ml: _parseDoubleSafe(row['caffeinePer100ml']) ?? 0,
|
|
waterTemperature: _parseIntSafe(row['waterTemperature']) ?? 93,
|
|
bloomTime: _parseIntSafe(row['bloomTime']) ?? 30,
|
|
totalExtractionTime: _parseIntSafe(row['totalExtractionTime']) ?? 240,
|
|
grindToWaterRatio: row['grindToWaterRatio'] ?? '1:16',
|
|
tags: _parseJsonList(row['tags']),
|
|
origin: row['origin'] ?? '',
|
|
rating: _parseDoubleSafe(row['rating']) ?? 4.0,
|
|
popularity: _parseIntSafe(row['popularity']) ?? 50,
|
|
createdBy: row['createdBy'] ?? '',
|
|
isPublic: row['isPublic']?.toLowerCase() == 'true',
|
|
lastModified: _parseDateSafe(row['lastModified']) ?? DateTime.now(),
|
|
);
|
|
}
|
|
|
|
OriginCountry _createOriginCountryFromCsv(List<String> headers, List<String> values) {
|
|
final Map<String, String> row = {};
|
|
for (int i = 0; i < headers.length && i < values.length; i++) {
|
|
row[headers[i]] = values[i];
|
|
}
|
|
|
|
return OriginCountry(
|
|
id: row['id'] ?? '',
|
|
continent: row['continent'] ?? '',
|
|
avgElevation: _parseIntSafe(row['altitudeRange']?.split('-').first.replaceAll('m', '')) ?? 1500,
|
|
details: row['characteristics'] ?? '',
|
|
notes: row['flavorProfile'] ?? '',
|
|
rating: _parseDoubleSafe(row['rating']) ?? 4.0,
|
|
);
|
|
}
|
|
|
|
// Parsing helper methods
|
|
int? _parseIntSafe(String? value) {
|
|
if (value == null || value.isEmpty) return null;
|
|
return int.tryParse(value.replaceAll(RegExp(r'[^0-9]'), ''));
|
|
}
|
|
|
|
double? _parseDoubleSafe(String? value) {
|
|
if (value == null || value.isEmpty) return null;
|
|
return double.tryParse(value);
|
|
}
|
|
|
|
DateTime? _parseDateSafe(String? value) {
|
|
if (value == null || value.isEmpty) return null;
|
|
try {
|
|
return DateTime.parse(value);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
List<String> _parseJsonList(String? value) {
|
|
if (value == null || value.isEmpty) return [];
|
|
try {
|
|
// Convert Python-style list to proper JSON
|
|
String jsonValue = value.replaceAll("'", '"');
|
|
final decoded = json.decode(jsonValue);
|
|
return List<String>.from(decoded);
|
|
} catch (e) {
|
|
debugPrint('Error parsing JSON list: $e');
|
|
return [];
|
|
}
|
|
}
|
|
|
|
TastingNotes _parseTastingNote(String note) {
|
|
switch (note) {
|
|
case 'Chocolate':
|
|
return TastingNotes.chocolate;
|
|
case 'Fruity':
|
|
return TastingNotes.fruity;
|
|
case 'Floral':
|
|
return TastingNotes.floral;
|
|
case 'Nutty':
|
|
return TastingNotes.nutty;
|
|
case 'Spicy':
|
|
return TastingNotes.spicy;
|
|
case 'Citrus':
|
|
return TastingNotes.citrus;
|
|
case 'Berry':
|
|
return TastingNotes.berry;
|
|
case 'Caramel':
|
|
return TastingNotes.caramel;
|
|
case 'Honey':
|
|
return TastingNotes.honey;
|
|
case 'Vanilla':
|
|
return TastingNotes.vanilla;
|
|
case 'Cocoa':
|
|
return TastingNotes.cocoa;
|
|
case 'Tobacco':
|
|
return TastingNotes.tobacco;
|
|
case 'Leather':
|
|
return TastingNotes.leather;
|
|
case 'Spice':
|
|
return TastingNotes.spice;
|
|
case 'Clove':
|
|
return TastingNotes.clove;
|
|
default:
|
|
return TastingNotes.chocolate;
|
|
}
|
|
}
|
|
|
|
RoastLevel _parseRoastLevel(String? value) {
|
|
switch (value) {
|
|
case 'Light':
|
|
return RoastLevel.light;
|
|
case 'Medium':
|
|
return RoastLevel.medium;
|
|
case 'Medium-Dark':
|
|
return RoastLevel.mediumDark;
|
|
case 'Dark':
|
|
return RoastLevel.dark;
|
|
case 'Medium-Light':
|
|
return RoastLevel.mediumLight;
|
|
default:
|
|
return RoastLevel.medium;
|
|
}
|
|
}
|
|
|
|
Acidity _parseAcidity(String? value) {
|
|
switch (value) {
|
|
case 'High':
|
|
return Acidity.high;
|
|
case 'Medium-High':
|
|
return Acidity.mediumHigh;
|
|
case 'Medium':
|
|
return Acidity.medium;
|
|
case 'Medium-Low':
|
|
return Acidity.mediumLow;
|
|
case 'Low':
|
|
return Acidity.low;
|
|
default:
|
|
return Acidity.medium;
|
|
}
|
|
}
|
|
|
|
Body _parseBody(String? value) {
|
|
switch (value) {
|
|
case 'Light':
|
|
return Body.light;
|
|
case 'Medium-Light':
|
|
return Body.mediumLight;
|
|
case 'Medium':
|
|
return Body.medium;
|
|
case 'Medium-Full':
|
|
return Body.mediumFull;
|
|
case 'Full':
|
|
return Body.full;
|
|
default:
|
|
return Body.medium;
|
|
}
|
|
}
|
|
|
|
Availability _parseAvailability(String? value) {
|
|
switch (value) {
|
|
case 'Available':
|
|
return Availability.available;
|
|
case 'Limited':
|
|
return Availability.limited;
|
|
case 'Seasonal':
|
|
return Availability.seasonal;
|
|
case 'Sold Out':
|
|
return Availability.soldOut;
|
|
default:
|
|
return Availability.available;
|
|
}
|
|
}
|
|
|
|
MachineType _parseMachineType(String? value) {
|
|
switch (value) {
|
|
case 'Espresso':
|
|
return MachineType.espresso;
|
|
case 'Drip':
|
|
return MachineType.drip;
|
|
case 'French Press':
|
|
return MachineType.frenchPress;
|
|
case 'Grinder':
|
|
return MachineType.grinder;
|
|
case 'Espresso Pod':
|
|
return MachineType.espressoPod;
|
|
case 'E61':
|
|
return MachineType.e61;
|
|
case 'Pod':
|
|
return MachineType.pod;
|
|
case 'Cold Brew':
|
|
return MachineType.coldBrew;
|
|
case 'Percolation':
|
|
return MachineType.percolation;
|
|
default:
|
|
return MachineType.espresso;
|
|
}
|
|
}
|
|
|
|
ServingTemp _parseServingTemp(String? value) {
|
|
switch (value) {
|
|
case 'Hot':
|
|
return ServingTemp.hot;
|
|
case 'Cold':
|
|
return ServingTemp.cold;
|
|
case 'Iced':
|
|
return ServingTemp.iced;
|
|
default:
|
|
return ServingTemp.hot;
|
|
}
|
|
}
|
|
|
|
MilkType? _parseMilkType(String? value) {
|
|
if (value == null || value.isEmpty) return null;
|
|
switch (value) {
|
|
case 'Whole':
|
|
return MilkType.whole;
|
|
case 'Skim':
|
|
return MilkType.skim;
|
|
case 'Almond':
|
|
return MilkType.almond;
|
|
case 'Soy':
|
|
return MilkType.soy;
|
|
case 'Coconut':
|
|
return MilkType.coconut;
|
|
default:
|
|
return MilkType.whole;
|
|
}
|
|
}
|
|
|
|
BrewMethod _parseBrewMethod(String? value) {
|
|
switch (value) {
|
|
case 'Espresso':
|
|
return BrewMethod.espresso;
|
|
case 'Pour Over':
|
|
return BrewMethod.pourOver;
|
|
case 'French Press':
|
|
return BrewMethod.frenchPress;
|
|
case 'Drip':
|
|
return BrewMethod.drip;
|
|
default:
|
|
return BrewMethod.espresso;
|
|
}
|
|
}
|
|
|
|
GrindSize _parseGrindSize(String? value) {
|
|
switch (value) {
|
|
case 'Fine':
|
|
return GrindSize.fine;
|
|
case 'Medium':
|
|
return GrindSize.medium;
|
|
case 'Coarse':
|
|
return GrindSize.coarse;
|
|
default:
|
|
return GrindSize.medium;
|
|
}
|
|
}
|
|
|
|
Difficulty _parseDifficulty(String? value) {
|
|
switch (value) {
|
|
case 'Beginner':
|
|
return Difficulty.beginner;
|
|
case 'Intermediate':
|
|
return Difficulty.intermediate;
|
|
case 'Advanced':
|
|
return Difficulty.advanced;
|
|
default:
|
|
return Difficulty.intermediate;
|
|
}
|
|
}
|
|
|
|
List<Portafilter> _parsePortafilters(String? value) {
|
|
if (value == null || value.isEmpty) return [];
|
|
try {
|
|
// Convert Python-style list to proper JSON
|
|
String jsonValue = value.replaceAll("'", '"');
|
|
final decoded = json.decode(jsonValue);
|
|
if (decoded is List) {
|
|
return decoded.map((item) => Portafilter(
|
|
id: item['id'] ?? '',
|
|
size: item['size'] ?? '58mm',
|
|
material: item['material'] ?? 'Stainless Steel',
|
|
)).toList();
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error parsing portafilters: $e');
|
|
}
|
|
return [];
|
|
}
|
|
|
|
Map<String, dynamic> _parseJsonMap(String? value) {
|
|
if (value == null || value.isEmpty) return {};
|
|
try {
|
|
// Convert Python-style dict to proper JSON
|
|
String jsonValue = value.replaceAll("'", '"');
|
|
final decoded = json.decode(jsonValue);
|
|
if (decoded is Map<String, dynamic>) {
|
|
return decoded;
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error parsing JSON map: $e');
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// Custom entries (user-created items that extend the CSV catalog)
|
|
final List<Bean> _customBeans = [];
|
|
final List<Machine> _customMachines = [];
|
|
final List<Recipe> _customRecipes = [];
|
|
|
|
// Methods for managing custom catalog entries
|
|
Future<void> saveBean(Bean bean) async {
|
|
// Add to custom beans (user-created entries)
|
|
final index = _customBeans.indexWhere((b) => b.id == bean.id);
|
|
if (index >= 0) {
|
|
_customBeans[index] = bean;
|
|
} else {
|
|
_customBeans.add(bean);
|
|
}
|
|
|
|
// Update cached beans to include custom entries
|
|
if (_cachedBeans != null) {
|
|
final csvBeans = await _loadBeansFromCsv();
|
|
_cachedBeans = [...csvBeans, ..._customBeans];
|
|
}
|
|
}
|
|
|
|
Future<void> deleteBean(String id) async {
|
|
// Only allow deletion of custom beans, not CSV beans
|
|
final wasCustom = _customBeans.any((b) => b.id == id);
|
|
if (wasCustom) {
|
|
_customBeans.removeWhere((bean) => bean.id == id);
|
|
|
|
// Update cached beans
|
|
if (_cachedBeans != null) {
|
|
final csvBeans = await _loadBeansFromCsv();
|
|
_cachedBeans = [...csvBeans, ..._customBeans];
|
|
}
|
|
} else {
|
|
throw Exception('Cannot delete catalog items. Only custom entries can be deleted.');
|
|
}
|
|
}
|
|
|
|
Future<void> saveMachine(Machine machine) async {
|
|
// Add to custom machines (user-created entries)
|
|
final index = _customMachines.indexWhere((m) => m.id == machine.id);
|
|
if (index >= 0) {
|
|
_customMachines[index] = machine;
|
|
} else {
|
|
_customMachines.add(machine);
|
|
}
|
|
|
|
// Update cached machines to include custom entries
|
|
if (_cachedMachines != null) {
|
|
final csvMachines = await _loadMachinesFromCsv();
|
|
_cachedMachines = [...csvMachines, ..._customMachines];
|
|
}
|
|
}
|
|
|
|
Future<void> deleteMachine(String id) async {
|
|
// Only allow deletion of custom machines, not CSV machines
|
|
final wasCustom = _customMachines.any((m) => m.id == id);
|
|
if (wasCustom) {
|
|
_customMachines.removeWhere((machine) => machine.id == id);
|
|
|
|
// Update cached machines
|
|
if (_cachedMachines != null) {
|
|
final csvMachines = await _loadMachinesFromCsv();
|
|
_cachedMachines = [...csvMachines, ..._customMachines];
|
|
}
|
|
} else {
|
|
throw Exception('Cannot delete catalog items. Only custom entries can be deleted.');
|
|
}
|
|
}
|
|
|
|
Future<void> saveRecipe(Recipe recipe) async {
|
|
// Add to custom recipes (user-created entries)
|
|
final index = _customRecipes.indexWhere((r) => r.id == recipe.id);
|
|
if (index >= 0) {
|
|
_customRecipes[index] = recipe;
|
|
} else {
|
|
_customRecipes.add(recipe);
|
|
}
|
|
|
|
// Update cached recipes to include custom entries
|
|
if (_cachedRecipes != null) {
|
|
final csvRecipes = await _loadRecipesFromCsv();
|
|
_cachedRecipes = [...csvRecipes, ..._customRecipes];
|
|
}
|
|
}
|
|
|
|
Future<void> deleteRecipe(String id) async {
|
|
// Only allow deletion of custom recipes, not CSV recipes
|
|
final wasCustom = _customRecipes.any((r) => r.id == id);
|
|
if (wasCustom) {
|
|
_customRecipes.removeWhere((recipe) => recipe.id == id);
|
|
|
|
// Update cached recipes
|
|
if (_cachedRecipes != null) {
|
|
final csvRecipes = await _loadRecipesFromCsv();
|
|
_cachedRecipes = [...csvRecipes, ..._customRecipes];
|
|
}
|
|
} else {
|
|
throw Exception('Cannot delete catalog items. Only custom entries can be deleted.');
|
|
}
|
|
}
|
|
|
|
Future<void> saveDrink(Drink drink) async {
|
|
// For CSV mode, add to cached list
|
|
_cachedDrinks ??= [];
|
|
final index = _cachedDrinks!.indexWhere((d) => d.id == drink.id);
|
|
if (index >= 0) {
|
|
_cachedDrinks![index] = drink;
|
|
} else {
|
|
_cachedDrinks!.add(drink);
|
|
}
|
|
}
|
|
|
|
Future<void> deleteDrink(String id) async {
|
|
_cachedDrinks?.removeWhere((drink) => drink.id == id);
|
|
}
|
|
|
|
Future<void> saveJournalEntry(JournalEntry entry) async {
|
|
// For CSV mode, add to cached list
|
|
_cachedJournalEntries ??= [];
|
|
final index = _cachedJournalEntries!.indexWhere((e) => e.id == entry.id);
|
|
if (index >= 0) {
|
|
_cachedJournalEntries![index] = entry;
|
|
} else {
|
|
_cachedJournalEntries!.add(entry);
|
|
}
|
|
}
|
|
|
|
Future<void> deleteJournalEntry(String id) async {
|
|
_cachedJournalEntries?.removeWhere((entry) => entry.id == id);
|
|
}
|
|
|
|
Future<void> clearAllData() async {
|
|
_cachedBeans = null;
|
|
_cachedMachines = null;
|
|
_cachedRecipes = null;
|
|
_cachedDrinks = [];
|
|
_cachedJournalEntries = [];
|
|
|
|
// Clear custom entries
|
|
_customBeans.clear();
|
|
_customMachines.clear();
|
|
_customRecipes.clear();
|
|
}
|
|
|
|
// Helper methods to check if an item is from CSV or custom
|
|
bool isCsvBean(String id) {
|
|
return !_customBeans.any((bean) => bean.id == id);
|
|
}
|
|
|
|
bool isCsvMachine(String id) {
|
|
return !_customMachines.any((machine) => machine.id == id);
|
|
}
|
|
|
|
bool isCsvRecipe(String id) {
|
|
return !_customRecipes.any((recipe) => recipe.id == id);
|
|
}
|
|
|
|
// Get only custom entries
|
|
List<Bean> getCustomBeans() => List.from(_customBeans);
|
|
List<Machine> getCustomMachines() => List.from(_customMachines);
|
|
List<Recipe> getCustomRecipes() => List.from(_customRecipes);
|
|
|
|
// Get only CSV entries
|
|
Future<List<Bean>> getCsvBeans() async => await _loadBeansFromCsv();
|
|
Future<List<Machine>> getCsvMachines() async => await _loadMachinesFromCsv();
|
|
Future<List<Recipe>> getCsvRecipes() async => await _loadRecipesFromCsv();
|
|
}
|