import 'package:drift/drift.dart'; import 'package:drift_postgres/drift_postgres.dart'; import '../database.dart'; import '../tables/packs.dart'; import '../tables/relations.dart'; part 'pack_dao.g.dart'; @DriftAccessor( tables: [ CardPacks, GameCards, VoiceModels, PreviewCards, CardPackCards, CardVoices, ], ) class PackDao extends DatabaseAccessor with _$PackDaoMixin { PackDao(super.db); // ==================== CardPacks ==================== /// Получить пак по ID Future getPackById(String id) async { final query = select(cardPacks) ..where((p) => p.id.equals(id)) ..limit(1); final results = await query.get(); return results.isNotEmpty ? results.first : null; } /// Получить все паки Future> getAllPacks({ bool enabledOnly = false, String? orderByField, bool orderDesc = false, }) { final query = select(cardPacks); if (enabledOnly) { query.where((p) => p.enabled.equals(true)); } query.where((p) => p.isDeleted.equals(false)); if (orderByField == 'order') { if (orderDesc) { query.orderBy([(p) => OrderingTerm.desc(p.order)]); } else { query.orderBy([(p) => OrderingTerm.asc(p.order)]); } } else { query.orderBy([(p) => OrderingTerm.asc(p.order)]); } return query.get(); } /// Создать пак Future createPack(CardPacksCompanion pack) async { final inserted = await into(cardPacks).insertReturning(pack); return inserted.id; } /// Обновить пак Future updatePack(CardPack pack) { return update(cardPacks).replace(pack); } /// Обновить пак частично Future updatePackPartial(CardPacksCompanion updates) { if (!updates.id.present) throw ArgumentError('Pack ID is required'); final packId = updates.id.value; return (update(cardPacks)..where((p) => p.id.equals(packId))).write( updates.copyWith(updatedAt: Value(PgDateTime(DateTime.now()))), ); } /// Удалить пак (soft delete) Future softDeletePack(String packId) { return (update(cardPacks)..where((p) => p.id.equals(packId))).write( CardPacksCompanion( isDeleted: const Value(true), updatedAt: Value(PgDateTime(DateTime.now())), ), ); } /// Подсчитать паки Future countPacks({bool enabledOnly = false}) async { final countExpr = cardPacks.id.count(); final query = selectOnly(cardPacks)..addColumns([countExpr]); if (enabledOnly) { query.where(cardPacks.enabled.equals(true)); } query.where(cardPacks.isDeleted.equals(false)); return await query.map((row) => row.read(countExpr)!).getSingle(); } // ==================== GameCards ==================== /// Получить карточку по ID (только активные) Future getCardById(String id) { return (select(gameCards) ..where((c) => c.id.equals(id) & c.isDeleted.equals(false))) .getSingleOrNull(); } /// Получить паки для карточки /// Связь через CardPackCards (packId удален из GameCards) Future> getPacksForCard(String cardId) async { final query = select(cardPacks).join([ innerJoin( cardPackCards, cardPackCards.packId.equalsExp(cardPacks.id), ), ])..where( cardPackCards.cardId.equals(cardId) & cardPacks.isDeleted.equals(false), ); final rows = await query.get(); return rows.map((row) => row.readTable(cardPacks)).toList(); } /// Получить все карточки пака /// Связь теперь только через CardPackCards (packId удален из GameCards) Future> getPackCards(String packId) async { final query = select(gameCards).join([ innerJoin( cardPackCards, cardPackCards.cardId.equalsExp(gameCards.id) & cardPackCards.packId.equals(packId), ), ])..where(gameCards.isDeleted.equals(false)); final results = await query.get(); // Сортируем по order из junction table, если есть, иначе по ID results.sort((a, b) { final orderA = a.read(cardPackCards.order) ?? 999999; final orderB = a.read(cardPackCards.order) ?? 999999; if (orderA != orderB) return orderA.compareTo(orderB); return a.readTable(gameCards).id.compareTo(b.readTable(gameCards).id); }); return results.map((row) => row.readTable(gameCards)).toList(); } /// Получить карточки по списку ID Future> getCardsByIds(List ids) { if (ids.isEmpty) return Future.value([]); return (select(gameCards)..where((c) => c.id.isIn(ids))).get(); } /// Найти карточки по original (слову) /// Возвращает все карточки с указанным original (может быть несколько) Future> searchCardsByOriginal(String original) { return (select(gameCards) ..where( (c) => c.original.equals(original) & c.isDeleted.equals(false), ) ..limit(10) // Ограничиваем для производительности ) .get(); } /// Создать карточку Future createCard(GameCardsCompanion card) async { final inserted = await into(gameCards).insertReturning(card); return inserted.id; } /// Обновить карточку Future updateCard(GameCard card) { return update(gameCards).replace(card); } /// Удалить карточку (soft delete) Future softDeleteCard(String cardId) { return (update(gameCards)..where((c) => c.id.equals(cardId))).write( GameCardsCompanion( isDeleted: const Value(true), updatedAt: Value(PgDateTime(DateTime.now())), ), ); } /// Удалить карточку (hard delete) Future deleteCard(String cardId) { return (delete(gameCards)..where((c) => c.id.equals(cardId))).go(); } /// Получить все карточки с пагинацией Future> getAllCards({int? limit, int? offset}) { final query = select(gameCards); if (limit != null) { query.limit(limit, offset: offset); } return query.get(); } /// Подсчитать карточки Future countCards() async { final countExpr = gameCards.id.count(); final query = selectOnly(gameCards)..addColumns([countExpr]); return await query.map((row) => row.read(countExpr)!).getSingle(); } /// Добавить карточку в пак /// Если связь уже существует, метод завершится успешно без ошибки Future addCardToPack({ required String packId, required String cardId, int order = 0, }) async { // Проверить, существует ли уже связь final existing = await (select(cardPackCards)..where( (cpc) => cpc.packId.equals(packId) & cpc.cardId.equals(cardId), )) .getSingleOrNull(); // Если связь уже существует, ничего не делаем if (existing != null) { return; } // Иначе создаем новую связь await into(cardPackCards).insert( CardPackCardsCompanion.insert( packId: packId, cardId: cardId, order: Value(order), ), ); } /// Удалить карточку из пака Future removeCardFromPack(String packId, String cardId) async { await (delete(cardPackCards)..where( (cpc) => cpc.packId.equals(packId) & cpc.cardId.equals(cardId), )) .go(); } /// Обновить порядок карточек в паке Future updatePackCardsOrder(String packId, List cardIds) async { await transaction(() async { // Удалить старые связи await (delete( cardPackCards, )..where((cpc) => cpc.packId.equals(packId))).go(); // Создать новые связи с порядком for (var i = 0; i < cardIds.length; i++) { await into(cardPackCards).insert( CardPackCardsCompanion.insert( packId: packId, cardId: cardIds[i], order: Value(i), ), ); } }); } // ==================== PreviewCards ==================== /// Получить preview карточки пака Future> getPreviewCards(String packId) async { final query = select(gameCards).join([ innerJoin( db.previewCards, db.previewCards.cardId.equalsExp(gameCards.id) & db.previewCards.packId.equals(packId), ), ]); final results = await query.get(); // Сортируем вручную по order из junction table results.sort((a, b) { final orderA = a.read(db.previewCards.order) ?? 0; final orderB = b.read(db.previewCards.order) ?? 0; return orderA.compareTo(orderB); }); return results.map((row) => row.readTable(gameCards)).toList(); } /// Установить preview карточки для пака Future setPreviewCards(String packId, List cardIds) async { await transaction(() async { // Удалить старые preview карточки await (delete( previewCards, )..where((pc) => pc.packId.equals(packId))).go(); // Добавить новые for (var i = 0; i < cardIds.length; i++) { await into(previewCards).insert( PreviewCardsCompanion.insert( packId: packId, cardId: cardIds[i], order: Value(i), ), ); } }); } // ==================== VoiceModels ==================== /// Получить голосовые модели карточки Future> getCardVoices(String cardId) async { print('🔍 DAO getCardVoices: Querying voices for card $cardId'); // First, check if there are any voices in VoiceModels table with direct cardId link final directVoicesQuery = select(voiceModels) ..where((v) => v.cardId.equals(cardId)); final directVoices = await directVoicesQuery.get(); print( '🔍 DAO getCardVoices: Found ${directVoices.length} voices in VoiceModels with direct cardId link', ); // Check if there are any voices in CardVoices table for this card final countExpr = cardVoices.cardId.count(); final countQuery = selectOnly(cardVoices) ..addColumns([countExpr]) ..where(cardVoices.cardId.equals(cardId)); final countResult = await countQuery.getSingle(); final count = countResult.read(countExpr) ?? 0; print( '🔍 DAO getCardVoices: Found $count entries in CardVoices junction table for card $cardId', ); // Query through junction table final query = select(voiceModels).join([ innerJoin( cardVoices, cardVoices.voiceId.equalsExp(voiceModels.id) & cardVoices.cardId.equals(cardId), ), ]); final result = await query.map((row) => row.readTable(voiceModels)).get(); print( '🔍 DAO getCardVoices: Query through junction table returned ${result.length} voices', ); // If junction table query returns nothing but direct link exists, return direct voices if (result.isEmpty && directVoices.isNotEmpty) { print( '⚠️ DAO getCardVoices: Junction table is empty but direct link exists! Using direct voices.', ); return directVoices; } return result; } /// Добавить голосовую модель к карточке Future addVoiceToCard(String cardId, String voiceId) async { print('🔍 DAO addVoiceToCard: Linking voice $voiceId to card $cardId'); try { // Check if voice exists final voice = await getVoiceById(voiceId); if (voice == null) { print('❌ DAO addVoiceToCard: Voice $voiceId does not exist!'); throw Exception('Voice $voiceId does not exist'); } print( '✅ DAO addVoiceToCard: Voice $voiceId exists with cardId=${voice.cardId}', ); // Check if card exists final card = await getCardById(cardId); if (card == null) { print('❌ DAO addVoiceToCard: Card $cardId does not exist!'); throw Exception('Card $cardId does not exist'); } print('✅ DAO addVoiceToCard: Card $cardId exists'); // Insert into junction table final result = await into(cardVoices).insert( CardVoicesCompanion.insert(cardId: cardId, voiceId: voiceId), mode: InsertMode.insertOrIgnore, ); print('✅ DAO addVoiceToCard: Insert result: $result'); // Verify the insert final verifyQuery = select(cardVoices) ..where((cv) => cv.cardId.equals(cardId) & cv.voiceId.equals(voiceId)); final verifyResult = await verifyQuery.getSingleOrNull(); if (verifyResult == null) { print( '❌ DAO addVoiceToCard: Verification failed! Link was not created.', ); } else { print( '✅ DAO addVoiceToCard: Successfully linked and verified voice $voiceId to card $cardId', ); } } catch (e, stackTrace) { print( '❌ DAO addVoiceToCard: Error linking voice $voiceId to card $cardId: $e', ); print('Stack trace: $stackTrace'); rethrow; } } /// Удалить голосовую модель из карточки Future removeVoiceFromCard(String cardId, String voiceId) async { await (delete( cardVoices, )..where((cv) => cv.cardId.equals(cardId) & cv.voiceId.equals(voiceId))) .go(); } /// Создать голосовую модель Future createVoice(VoiceModelsCompanion voice) async { final inserted = await into(voiceModels).insertReturning(voice); return inserted.id; } /// Получить голосовую модель по ID Future getVoiceById(String id) { return (select( voiceModels, )..where((v) => v.id.equals(id))).getSingleOrNull(); } /// Обновить путь/URL аудиофайла голосовой модели Future updateVoiceUrl(String voiceId, String voiceUrl) async { await (update(voiceModels)..where((v) => v.id.equals(voiceId))).write( VoiceModelsCompanion(voiceUrl: Value(voiceUrl)), ); } /// Удалить голосовую модель Future deleteVoice(String voiceId) async { await (delete(voiceModels)..where((v) => v.id.equals(voiceId))).go(); } }