From 90f2c3f89b6483623173839d44826adbb231744b Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Wed, 3 Dec 2025 08:49:06 -0300 Subject: [PATCH 01/23] refactor(recent-food): move RemoveFromRecentButton to recent-food module --- .../recent-food/ui}/RemoveFromRecentButton.tsx | 0 .../recent-food/ui/tests}/RemoveFromRecentButton.test.tsx | 0 src/sections/search/components/TemplateSearchResultItem.tsx | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename src/{sections/common/components/buttons => modules/recent-food/ui}/RemoveFromRecentButton.tsx (100%) rename src/{sections/common/components/buttons => modules/recent-food/ui/tests}/RemoveFromRecentButton.test.tsx (100%) diff --git a/src/sections/common/components/buttons/RemoveFromRecentButton.tsx b/src/modules/recent-food/ui/RemoveFromRecentButton.tsx similarity index 100% rename from src/sections/common/components/buttons/RemoveFromRecentButton.tsx rename to src/modules/recent-food/ui/RemoveFromRecentButton.tsx diff --git a/src/sections/common/components/buttons/RemoveFromRecentButton.test.tsx b/src/modules/recent-food/ui/tests/RemoveFromRecentButton.test.tsx similarity index 100% rename from src/sections/common/components/buttons/RemoveFromRecentButton.test.tsx rename to src/modules/recent-food/ui/tests/RemoveFromRecentButton.test.tsx diff --git a/src/sections/search/components/TemplateSearchResultItem.tsx b/src/sections/search/components/TemplateSearchResultItem.tsx index f9bd7603..3a457cd6 100644 --- a/src/sections/search/components/TemplateSearchResultItem.tsx +++ b/src/sections/search/components/TemplateSearchResultItem.tsx @@ -6,7 +6,7 @@ import { isTemplateRecipe, type Template, } from '~/modules/diet/template/domain/template' -import { RemoveFromRecentButton } from '~/sections/common/components/buttons/RemoveFromRecentButton' +import { RemoveFromRecentButton } from '~/modules/recent-food/ui/RemoveFromRecentButton' import { ItemView } from '~/sections/item/components/ItemView' import { ItemFavorite } from '~/sections/item/components/UnifiedItemFavorite' import { openDeleteConfirmModal } from '~/shared/modal/ui/DeleteConfirmModal' From 6a18937e911ae9477da5bdc59e5626b694caa816 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Wed, 3 Dec 2025 08:49:36 -0300 Subject: [PATCH 02/23] refactor(recent-food): remove export from internal functions in recentFoodRepository --- .../recent-food/infrastructure/recentFoodRepository.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/recent-food/infrastructure/recentFoodRepository.ts b/src/modules/recent-food/infrastructure/recentFoodRepository.ts index 9fa6c851..0cab28d5 100644 --- a/src/modules/recent-food/infrastructure/recentFoodRepository.ts +++ b/src/modules/recent-food/infrastructure/recentFoodRepository.ts @@ -20,7 +20,7 @@ export function createRecentFoodRepository(): RecentFoodRepository { } } -export async function fetchByUserTypeAndReferenceId( +async function fetchByUserTypeAndReferenceId( userId: User['uuid'], type: RecentFood['type'], referenceId: number, @@ -37,7 +37,7 @@ export async function fetchByUserTypeAndReferenceId( } } -export async function fetchUserRecentFoodsAsTemplates( +async function fetchUserRecentFoodsAsTemplates( userId: User['uuid'], search: string, opts?: { limit?: number }, @@ -54,7 +54,7 @@ export async function fetchUserRecentFoodsAsTemplates( } } -export async function insert(input: NewRecentFood): Promise { +async function insert(input: NewRecentFood): Promise { try { return await supabaseGateway.insert(input) } catch (error) { @@ -63,7 +63,7 @@ export async function insert(input: NewRecentFood): Promise { } } -export async function update( +async function update( id: number, input: NewRecentFood, ): Promise { @@ -75,7 +75,7 @@ export async function update( } } -export async function deleteByReference( +async function deleteByReference( userId: User['uuid'], type: RecentFood['type'], referenceId: number, From e84c9904b2b0089d9252d4871d9a516c93734c65 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Wed, 3 Dec 2025 09:21:10 -0300 Subject: [PATCH 03/23] refactor(recent-food): restructure module with service and use cases --- .../services/recentFoodCrudService.ts | 56 +++++++ .../application/usecases/recentFoodCrud.ts | 81 ---------- .../usecases/recentFoodUseCases.ts | 138 ++++++++++++++++++ .../infrastructure/recentFoodRepository.ts | 137 ++++++++--------- .../supabase/supabaseRecentFoodGateway.ts | 4 + .../recent-food/ui/RemoveFromRecentButton.tsx | 18 +-- .../ui/tests/RemoveFromRecentButton.test.tsx | 62 -------- .../usecases/templateSearchState.ts | 5 +- .../search/components/TemplateSearchModal.tsx | 54 +------ 9 files changed, 270 insertions(+), 285 deletions(-) create mode 100644 src/modules/recent-food/application/services/recentFoodCrudService.ts delete mode 100644 src/modules/recent-food/application/usecases/recentFoodCrud.ts create mode 100644 src/modules/recent-food/application/usecases/recentFoodUseCases.ts diff --git a/src/modules/recent-food/application/services/recentFoodCrudService.ts b/src/modules/recent-food/application/services/recentFoodCrudService.ts new file mode 100644 index 00000000..ba5a09b1 --- /dev/null +++ b/src/modules/recent-food/application/services/recentFoodCrudService.ts @@ -0,0 +1,56 @@ +import type { Template } from '~/modules/diet/template/domain/template' +import { + type NewRecentFood, + type RecentFood, +} from '~/modules/recent-food/domain/recentFood' +import { type RecentFoodRepository } from '~/modules/recent-food/domain/recentFoodRepository' +import { type User } from '~/modules/user/domain/user' +import env from '~/shared/config/env' + +export function createRecentFoodCrudService(repository: RecentFoodRepository) { + return { + async fetchRecentFoodByUserTypeAndReferenceId( + userId: User['uuid'], + type: RecentFood['type'], + referenceId: number, + ): Promise { + return await repository.fetchByUserTypeAndReferenceId( + userId, + type, + referenceId, + ) + }, + + async fetchUserRecentFoods( + userId: User['uuid'], + search: string, + opts?: { limit?: number }, + ): Promise { + const limit = opts?.limit ?? env.VITE_RECENT_FOODS_DEFAULT_LIMIT + return await repository.fetchUserRecentFoodsAsTemplates(userId, search, { + limit, + }) + }, + + async insertRecentFood( + recentFoodInput: NewRecentFood, + ): Promise { + return await repository.insert(recentFoodInput) + }, + + async updateRecentFood( + recentFoodId: number, + recentFoodInput: NewRecentFood, + ): Promise { + return await repository.update(recentFoodId, recentFoodInput) + }, + + async deleteRecentFoodByReference( + userId: User['uuid'], + type: RecentFood['type'], + referenceId: number, + ): Promise { + return await repository.deleteByReference(userId, type, referenceId) + }, + } +} diff --git a/src/modules/recent-food/application/usecases/recentFoodCrud.ts b/src/modules/recent-food/application/usecases/recentFoodCrud.ts deleted file mode 100644 index 37bf4318..00000000 --- a/src/modules/recent-food/application/usecases/recentFoodCrud.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { Template } from '~/modules/diet/template/domain/template' -import { - type NewRecentFood, - type RecentFood, -} from '~/modules/recent-food/domain/recentFood' -import { createRecentFoodRepository } from '~/modules/recent-food/infrastructure/recentFoodRepository' -import { showPromise } from '~/modules/toast/application/toastManager' -import { type User } from '~/modules/user/domain/user' -import env from '~/shared/config/env' - -const recentFoodRepository = createRecentFoodRepository() - -export async function fetchRecentFoodByUserTypeAndReferenceId( - userId: User['uuid'], - type: RecentFood['type'], - referenceId: number, -): Promise { - return await recentFoodRepository.fetchByUserTypeAndReferenceId( - userId, - type, - referenceId, - ) -} - -export async function fetchUserRecentFoods( - userId: User['uuid'], - search: string, - opts?: { limit?: number }, -): Promise { - const limit = opts?.limit ?? env.VITE_RECENT_FOODS_DEFAULT_LIMIT - return await recentFoodRepository.fetchUserRecentFoodsAsTemplates( - userId, - search, - { limit }, - ) -} - -export async function insertRecentFood( - recentFoodInput: NewRecentFood, -): Promise { - return await showPromise( - recentFoodRepository.insert(recentFoodInput), - { - loading: 'Salvando alimento recente...', - success: 'Alimento recente salvo com sucesso', - error: 'Erro ao salvar alimento recente', - }, - { context: 'user-action' }, - ) -} - -export async function updateRecentFood( - recentFoodId: number, - recentFoodInput: NewRecentFood, -): Promise { - return await showPromise( - recentFoodRepository.update(recentFoodId, recentFoodInput), - { - loading: 'Atualizando alimento recente...', - success: 'Alimento recente atualizado com sucesso', - error: 'Erro ao atualizar alimento recente', - }, - { context: 'user-action' }, - ) -} - -export async function deleteRecentFoodByReference( - userId: User['uuid'], - type: RecentFood['type'], - referenceId: number, -): Promise { - return await showPromise( - recentFoodRepository.deleteByReference(userId, type, referenceId), - { - loading: 'Removendo alimento recente...', - success: 'Alimento recente removido com sucesso', - error: 'Erro ao remover alimento recente', - }, - { context: 'user-action' }, - ) -} diff --git a/src/modules/recent-food/application/usecases/recentFoodUseCases.ts b/src/modules/recent-food/application/usecases/recentFoodUseCases.ts new file mode 100644 index 00000000..58c12430 --- /dev/null +++ b/src/modules/recent-food/application/usecases/recentFoodUseCases.ts @@ -0,0 +1,138 @@ +import { createRoot } from 'solid-js' + +import { authUseCases } from '~/modules/auth/application/usecases/authUseCases' +import { type Item } from '~/modules/diet/item/schema/itemSchema' +import { createRecentFoodCrudService } from '~/modules/recent-food/application/services/recentFoodCrudService' +import { extractRecentFoodReference } from '~/modules/recent-food/application/usecases/extractRecentFoodReference' +import { + createNewRecentFood, + type NewRecentFood, + type RecentFood, +} from '~/modules/recent-food/domain/recentFood' +import { createRecentFoodRepository } from '~/modules/recent-food/infrastructure/recentFoodRepository' +import { createSupabaseRecentFoodGateway } from '~/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway' +import { + showError, + showPromise, +} from '~/modules/toast/application/toastManager' +import { type User } from '~/modules/user/domain/user' +import { logging } from '~/shared/utils/logging' + +const { recentFoodCrudService } = createRoot(() => { + const supabaseRecentFoodGateway = createSupabaseRecentFoodGateway() + const repository = createRecentFoodRepository(supabaseRecentFoodGateway) + const recentFoodCrudService = createRecentFoodCrudService(repository) + return { recentFoodCrudService } +}) + +export const recentFoodUseCases = { + fetchUserRecentFoods: async ( + userId: User['uuid'], + search: string, + opts?: { limit?: number }, + ) => { + return await recentFoodCrudService.fetchUserRecentFoods( + userId, + search, + opts, + ) + }, + + insertRecentFood: async ( + recentFoodInput: NewRecentFood, + ): Promise => { + return await showPromise( + recentFoodCrudService.insertRecentFood(recentFoodInput), + { + loading: 'Salvando alimento recente...', + success: 'Alimento recente salvo com sucesso', + error: 'Erro ao salvar alimento recente', + }, + { context: 'user-action' }, + ) + }, + + updateRecentFood: async ( + recentFoodId: number, + recentFoodInput: NewRecentFood, + ): Promise => { + return await showPromise( + recentFoodCrudService.updateRecentFood(recentFoodId, recentFoodInput), + { + loading: 'Atualizando alimento recente...', + success: 'Alimento recente atualizado com sucesso', + error: 'Erro ao atualizar alimento recente', + }, + { context: 'user-action' }, + ) + }, + + deleteRecentFoodByReference: async ( + userId: User['uuid'], + type: RecentFood['type'], + referenceId: number, + ): Promise => { + return await showPromise( + recentFoodCrudService.deleteRecentFoodByReference( + userId, + type, + referenceId, + ), + { + loading: 'Removendo alimento dos recentes...', + success: 'Alimento removido dos recentes com sucesso', + error: 'Erro ao remover alimento dos recentes', + }, + { context: 'user-action' }, + ) + }, + + touchRecentFoodForItem: async (item: Item) => { + const recentFoodRef = extractRecentFoodReference(item) + if (recentFoodRef === null) { + logging.warn( + 'Cannot touch recent food for item - no trackable reference found', + { item }, + ) + showError('Não foi possível adicionar alimento aos alimentos recentes.') + return + } + + const currentRecentFood = + await recentFoodCrudService.fetchRecentFoodByUserTypeAndReferenceId( + authUseCases.currentUserIdOrGuestId(), + recentFoodRef.type, + recentFoodRef.referenceId, + ) + + const timesCurrentlyUsed = currentRecentFood?.times_used ?? 0 + const newRecentFoodData = createNewRecentFood({ + user_id: authUseCases.currentUserIdOrGuestId(), + type: recentFoodRef.type, + reference_id: recentFoodRef.referenceId, + last_used: new Date(), + times_used: timesCurrentlyUsed + 1, + }) + + if (currentRecentFood === null) { + await recentFoodUseCases.insertRecentFood(newRecentFoodData) + } else { + // TODO: Remove client-side user check after implementing row-level security (RLS) + if (currentRecentFood.user_id !== authUseCases.currentUserIdOrGuestId()) { + throw new Error('BUG: recentFood fetched does not match current user') + } + + if ( + currentRecentFood.type !== recentFoodRef.type || + currentRecentFood.reference_id !== recentFoodRef.referenceId + ) { + throw new Error('BUG: recentFood fetched does not match type/reference') + } + + await recentFoodUseCases.updateRecentFood( + currentRecentFood.id, + newRecentFoodData, + ) + } + }, +} diff --git a/src/modules/recent-food/infrastructure/recentFoodRepository.ts b/src/modules/recent-food/infrastructure/recentFoodRepository.ts index 0cab28d5..a48284f6 100644 --- a/src/modules/recent-food/infrastructure/recentFoodRepository.ts +++ b/src/modules/recent-food/infrastructure/recentFoodRepository.ts @@ -4,86 +4,77 @@ import { type RecentFood, } from '~/modules/recent-food/domain/recentFood' import { type RecentFoodRepository } from '~/modules/recent-food/domain/recentFoodRepository' -import { createSupabaseRecentFoodGateway } from '~/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway' +import { type RecentFoodGateway } from '~/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway' import { type User } from '~/modules/user/domain/user' import { logging } from '~/shared/utils/logging' -const supabaseGateway = createSupabaseRecentFoodGateway() - -export function createRecentFoodRepository(): RecentFoodRepository { +export function createRecentFoodRepository( + gateway: RecentFoodGateway, +): RecentFoodRepository { return { - fetchByUserTypeAndReferenceId, - fetchUserRecentFoodsAsTemplates, - insert, - update, - deleteByReference, - } -} - -async function fetchByUserTypeAndReferenceId( - userId: User['uuid'], - type: RecentFood['type'], - referenceId: number, -): Promise { - try { - return await supabaseGateway.fetchByUserTypeAndReferenceId( - userId, - type, - referenceId, - ) - } catch (error) { - logging.error('RecentFood operation error:', error) - return null - } -} + async fetchByUserTypeAndReferenceId( + userId: User['uuid'], + type: RecentFood['type'], + referenceId: number, + ): Promise { + try { + return await gateway.fetchByUserTypeAndReferenceId( + userId, + type, + referenceId, + ) + } catch (error) { + logging.error('RecentFood operation error:', error) + return null + } + }, -async function fetchUserRecentFoodsAsTemplates( - userId: User['uuid'], - search: string, - opts?: { limit?: number }, -): Promise { - try { - return await supabaseGateway.fetchUserRecentFoodsAsTemplates( - userId, - search, - opts, - ) - } catch (error) { - logging.error('RecentFood operation error:', error) - return [] - } -} + async fetchUserRecentFoodsAsTemplates( + userId: User['uuid'], + search: string, + opts?: { limit?: number }, + ): Promise { + try { + return await gateway.fetchUserRecentFoodsAsTemplates( + userId, + search, + opts, + ) + } catch (error) { + logging.error('RecentFood operation error:', error) + return [] + } + }, -async function insert(input: NewRecentFood): Promise { - try { - return await supabaseGateway.insert(input) - } catch (error) { - logging.error('RecentFood operation error:', error) - return null - } -} + async insert(input: NewRecentFood): Promise { + try { + return await gateway.insert(input) + } catch (error) { + logging.error('RecentFood operation error:', error) + return null + } + }, -async function update( - id: number, - input: NewRecentFood, -): Promise { - try { - return await supabaseGateway.update(id, input) - } catch (error) { - logging.error('RecentFood operation error:', error) - return null - } -} + async update(id: number, input: NewRecentFood): Promise { + try { + return await gateway.update(id, input) + } catch (error) { + logging.error('RecentFood operation error:', error) + return null + } + }, -async function deleteByReference( - userId: User['uuid'], - type: RecentFood['type'], - referenceId: number, -): Promise { - try { - return await supabaseGateway.deleteByReference(userId, type, referenceId) - } catch (error) { - logging.error('RecentFood operation error:', error) - return false + async deleteByReference( + userId: User['uuid'], + type: RecentFood['type'], + referenceId: number, + ): Promise { + try { + return await gateway.deleteByReference(userId, type, referenceId) + } catch (error) { + logging.error('RecentFood operation error:', error) + return false + } + }, } } diff --git a/src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts b/src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts index 7e91d9df..03ef8267 100644 --- a/src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts +++ b/src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts @@ -110,6 +110,10 @@ export function createSupabaseRecentFoodGateway() { } } +export type RecentFoodGateway = ReturnType< + typeof createSupabaseRecentFoodGateway +> + async function fetchByUserTypeAndReferenceId( userId: User['uuid'], type: RecentFood['type'], diff --git a/src/modules/recent-food/ui/RemoveFromRecentButton.tsx b/src/modules/recent-food/ui/RemoveFromRecentButton.tsx index cc905921..ea5d3ad9 100644 --- a/src/modules/recent-food/ui/RemoveFromRecentButton.tsx +++ b/src/modules/recent-food/ui/RemoveFromRecentButton.tsx @@ -5,11 +5,9 @@ import { isTemplateFood, type Template, } from '~/modules/diet/template/domain/template' -import { deleteRecentFoodByReference } from '~/modules/recent-food/application/usecases/recentFoodCrud' +import { recentFoodUseCases } from '~/modules/recent-food/application/usecases/recentFoodUseCases' import { debouncedTab } from '~/modules/template-search/application/usecases/templateSearchState' -import { showPromise } from '~/modules/toast/application/toastManager' import { TrashIcon } from '~/sections/common/components/icons/TrashIcon' -import { logging } from '~/shared/utils/logging' type RemoveFromRecentButtonProps = { template: Template @@ -26,19 +24,9 @@ export function RemoveFromRecentButton(props: RemoveFromRecentButtonProps) { const userId = authUseCases.currentUserIdOrGuestId() - void showPromise( - deleteRecentFoodByReference(userId, templateType, templateId), - { - loading: 'Removendo item da lista de recentes...', - success: 'Item removido da lista de recentes com sucesso!', - error: (err: unknown) => { - logging.error('RemoveFromRecentButton error:', err) - return 'Erro ao remover item da lista de recentes.' - }, - }, - ) + void recentFoodUseCases + .deleteRecentFoodByReference(userId, templateType, templateId) .then(props.refetch) - .catch(() => {}) } return ( diff --git a/src/modules/recent-food/ui/tests/RemoveFromRecentButton.test.tsx b/src/modules/recent-food/ui/tests/RemoveFromRecentButton.test.tsx index 192e5f76..1e298a51 100644 --- a/src/modules/recent-food/ui/tests/RemoveFromRecentButton.test.tsx +++ b/src/modules/recent-food/ui/tests/RemoveFromRecentButton.test.tsx @@ -47,12 +47,10 @@ vi.mock('~/shared/utils/logging', () => ({ })) // Import the mocked modules -import { deleteRecentFoodByReference } from '~/modules/recent-food/application/usecases/recentFoodCrud' import { debouncedTab } from '~/modules/template-search/application/usecases/templateSearchState' import { showPromise } from '~/modules/toast/application/toastManager' import { logging } from '~/shared/utils/logging' -const mockDeleteRecentFoodByReference = vi.mocked(deleteRecentFoodByReference) const mockDebouncedTab = vi.mocked(debouncedTab) const mockShowPromise = vi.mocked(showPromise) const mockLogging = vi.mocked(logging) @@ -89,7 +87,6 @@ describe('RemoveFromRecentButton Logic', () => { vi.spyOn(authUseCases, 'currentUserIdOrGuestId').mockReturnValue(mockUserId) mockDebouncedTab.mockReturnValue('recent') mockShowPromise.mockImplementation((promise) => promise) - mockDeleteRecentFoodByReference.mockResolvedValue(true) }) afterEach(() => { @@ -130,65 +127,6 @@ describe('RemoveFromRecentButton Logic', () => { }) }) - describe('API Integration Logic', () => { - it('calls deleteRecentFoodByReference with correct parameters for food template', async () => { - const templateType = isTemplateFood(mockFoodTemplate) ? 'food' : 'recipe' - const templateId = mockFoodTemplate.id - - await deleteRecentFoodByReference(mockUserId, templateType, templateId) - - expect(mockDeleteRecentFoodByReference).toHaveBeenCalledWith( - mockUserId, - 'food', - mockFoodTemplate.id, - ) - }) - - it('calls deleteRecentFoodByReference with correct parameters for recipe template', async () => { - const templateType = isTemplateFood(mockRecipeTemplate) - ? 'food' - : 'recipe' - const templateId = mockRecipeTemplate.id - - await deleteRecentFoodByReference(mockUserId, templateType, templateId) - - expect(mockDeleteRecentFoodByReference).toHaveBeenCalledWith( - mockUserId, - 'recipe', - mockRecipeTemplate.id, - ) - }) - }) - - describe('Toast Promise Integration', () => { - it('configures showPromise with correct parameters', async () => { - const promise = deleteRecentFoodByReference( - mockUserId, - 'food', - mockFoodTemplate.id, - ) - - await showPromise(promise, { - loading: 'Removendo item da lista de recentes...', - success: 'Item removido da lista de recentes com sucesso!', - error: (err: unknown) => { - mockLogging.error('RemoveFromRecentButton error:', err) - return 'Erro ao remover item da lista de recentes.' - }, - }) - - expect(mockShowPromise).toHaveBeenCalledWith( - expect.any(Promise), - expect.objectContaining({ - loading: 'Removendo item da lista de recentes...', - success: 'Item removido da lista de recentes com sucesso!', - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - error: expect.any(Function), - }), - ) - }) - }) - describe('Error Handling Logic', () => { it('handles API errors correctly', () => { const mockError = new Error('API Error') diff --git a/src/modules/template-search/application/usecases/templateSearchState.ts b/src/modules/template-search/application/usecases/templateSearchState.ts index f54220d3..b095ada5 100644 --- a/src/modules/template-search/application/usecases/templateSearchState.ts +++ b/src/modules/template-search/application/usecases/templateSearchState.ts @@ -9,7 +9,7 @@ import { fetchUserRecipeByName, fetchUserRecipes, } from '~/modules/diet/recipe/application/usecases/recipeCrud' -import { fetchUserRecentFoods } from '~/modules/recent-food/application/usecases/recentFoodCrud' +import { recentFoodUseCases } from '~/modules/recent-food/application/usecases/recentFoodUseCases' import { fetchTemplatesByTabLogic } from '~/modules/template-search/application/templateSearchLogic' import { userUseCases } from '~/modules/user/application/usecases/userUseCases' import { type TemplateSearchTab } from '~/sections/search/components/TemplateSearchTabs' @@ -35,9 +35,10 @@ export const [templates, { refetch: refetchTemplates }] = createResource( signals.search, signals.userId, { + // TODO: Convert fetchTemplatesByTabLogic deps to reactive signals? fetchUserRecipes, fetchUserRecipeByName, - fetchUserRecentFoods, + fetchUserRecentFoods: recentFoodUseCases.fetchUserRecentFoods, fetchFoods, fetchFoodsByName, getFavoriteFoods, diff --git a/src/sections/search/components/TemplateSearchModal.tsx b/src/sections/search/components/TemplateSearchModal.tsx index d1ca2a43..83000e75 100644 --- a/src/sections/search/components/TemplateSearchModal.tsx +++ b/src/sections/search/components/TemplateSearchModal.tsx @@ -1,6 +1,5 @@ import { onMount, Suspense } from 'solid-js' -import { authUseCases } from '~/modules/auth/application/usecases/authUseCases' import { type Item } from '~/modules/diet/item/schema/itemSchema' import { isOverflow } from '~/modules/diet/macro-nutrients/application/macroOverflow' import { getRecipePreparedQuantity } from '~/modules/diet/recipe/domain/recipeOperations' @@ -12,13 +11,7 @@ import { import { type Template } from '~/modules/diet/template/domain/template' import { isTemplateRecipe } from '~/modules/diet/template/domain/template' import { type TemplateItem } from '~/modules/diet/template-item/domain/templateItem' -import { extractRecentFoodReference } from '~/modules/recent-food/application/usecases/extractRecentFoodReference' -import { - fetchRecentFoodByUserTypeAndReferenceId, - insertRecentFood, - updateRecentFood, -} from '~/modules/recent-food/application/usecases/recentFoodCrud' -import { createNewRecentFood } from '~/modules/recent-food/domain/recentFood' +import { recentFoodUseCases } from '~/modules/recent-food/application/usecases/recentFoodUseCases' import { debouncedSearch, refetchTemplates, @@ -89,52 +82,9 @@ export function TemplateSearchModal(props: TemplateSearchModalProps) { closeEditModal: () => void, ) => { const handleConfirm = async () => { - const userId = authUseCases.currentUserIdOrGuestId() - props.onNewItem?.(newItem, originalAddedItem) - // Extract recent food reference from the item - const recentFoodRef = extractRecentFoodReference(originalAddedItem) - if (recentFoodRef === null) { - logging.warn( - 'Cannot track recent food for item without trackable reference', - { originalAddedItem }, - ) - // Continue with the rest of the flow, just skip recent food tracking - } else { - const { type, referenceId } = recentFoodRef - - const recentFood = await fetchRecentFoodByUserTypeAndReferenceId( - userId, - type, - referenceId, - ) - - if ( - recentFood !== null && - (recentFood.user_id !== authUseCases.currentUserIdOrGuestId() || - recentFood.type !== type || - recentFood.reference_id !== referenceId) - ) { - throw new Error( - 'BUG: recentFood fetched does not match user/type/reference', - ) - } - - const recentFoodInput = createNewRecentFood({ - user_id: userId, - type, - reference_id: referenceId, - last_used: new Date(), - times_used: (recentFood?.times_used ?? 0) + 1, - }) - - if (recentFood !== null) { - await updateRecentFood(recentFood.id, recentFoodInput) - } else { - await insertRecentFood(recentFoodInput) - } - } + void recentFoodUseCases.touchRecentFoodForItem(originalAddedItem) const confirmModalId = openConfirmModal( 'Deseja adicionar outro item ou finalizar a inclusão?', From ac559569dcd7c356d35316b70f56c7ba6dc4c0de Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Wed, 3 Dec 2025 14:49:22 -0300 Subject: [PATCH 04/23] refactor(recent-food): simplify data management and realtime updates --- .../services/recentFoodCrudService.ts | 2 +- .../usecases/recentFoodUseCases.ts | 34 ++++++++-- .../infrastructure/recentFoodRepository.ts | 10 +-- .../signals/recentFoodCacheStore.ts | 62 ------------------- .../infrastructure/supabase/realtime.ts | 21 ++++--- .../application/templateSearchLogic.ts | 7 ++- .../tests/templateSearchLogic.test.ts | 20 ++++-- .../usecases/templateSearchState.ts | 3 +- .../search/components/TemplateSearchModal.tsx | 6 +- .../search/ui/openTemplateSearchModal.tsx | 2 + src/shared/supabase/database.types.ts | 52 ++++++++++++---- src/shared/supabase/supabase.ts | 1 + 12 files changed, 115 insertions(+), 105 deletions(-) delete mode 100644 src/modules/recent-food/infrastructure/signals/recentFoodCacheStore.ts diff --git a/src/modules/recent-food/application/services/recentFoodCrudService.ts b/src/modules/recent-food/application/services/recentFoodCrudService.ts index ba5a09b1..e24d1a33 100644 --- a/src/modules/recent-food/application/services/recentFoodCrudService.ts +++ b/src/modules/recent-food/application/services/recentFoodCrudService.ts @@ -21,7 +21,7 @@ export function createRecentFoodCrudService(repository: RecentFoodRepository) { ) }, - async fetchUserRecentFoods( + async fetchUserRecentFoodsAsTemplates( userId: User['uuid'], search: string, opts?: { limit?: number }, diff --git a/src/modules/recent-food/application/usecases/recentFoodUseCases.ts b/src/modules/recent-food/application/usecases/recentFoodUseCases.ts index 58c12430..031f257b 100644 --- a/src/modules/recent-food/application/usecases/recentFoodUseCases.ts +++ b/src/modules/recent-food/application/usecases/recentFoodUseCases.ts @@ -10,6 +10,7 @@ import { type RecentFood, } from '~/modules/recent-food/domain/recentFood' import { createRecentFoodRepository } from '~/modules/recent-food/infrastructure/recentFoodRepository' +import { initializeRecentFoodRealtime } from '~/modules/recent-food/infrastructure/supabase/realtime' import { createSupabaseRecentFoodGateway } from '~/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway' import { showError, @@ -22,20 +23,41 @@ const { recentFoodCrudService } = createRoot(() => { const supabaseRecentFoodGateway = createSupabaseRecentFoodGateway() const repository = createRecentFoodRepository(supabaseRecentFoodGateway) const recentFoodCrudService = createRecentFoodCrudService(repository) + + // TODO: Implement recent food cache using realtime updates + initializeRecentFoodRealtime({ + onInsert: (_: RecentFood) => {}, + onUpdate: (_: RecentFood) => {}, + onDelete: (_: RecentFood) => {}, + }) + return { recentFoodCrudService } }) export const recentFoodUseCases = { - fetchUserRecentFoods: async ( + fetchUserRecentFoodsAsTemplates: async ( userId: User['uuid'], search: string, opts?: { limit?: number }, ) => { - return await recentFoodCrudService.fetchUserRecentFoods( - userId, - search, - opts, - ) + try { + const recentFoods = + await recentFoodCrudService.fetchUserRecentFoodsAsTemplates( + userId, + search, + opts, + ) + + return recentFoods + } catch (error) { + logging.error('Error fetching user recent foods', { + error, + userId, + search, + }) + showError(error, {}, 'Não foi possível carregar alimentos recentes.') + return [] + } }, insertRecentFood: async ( diff --git a/src/modules/recent-food/infrastructure/recentFoodRepository.ts b/src/modules/recent-food/infrastructure/recentFoodRepository.ts index a48284f6..71b57a5f 100644 --- a/src/modules/recent-food/infrastructure/recentFoodRepository.ts +++ b/src/modules/recent-food/infrastructure/recentFoodRepository.ts @@ -24,7 +24,7 @@ export function createRecentFoodRepository( referenceId, ) } catch (error) { - logging.error('RecentFood operation error:', error) + logging.error('[NON-FATAL] fetchByUserTypeAndReferenceId:', error) return null } }, @@ -41,7 +41,7 @@ export function createRecentFoodRepository( opts, ) } catch (error) { - logging.error('RecentFood operation error:', error) + logging.error('[NON-FATAL] fetchUserRecentFoodsAsTemplates:', error) return [] } }, @@ -50,7 +50,7 @@ export function createRecentFoodRepository( try { return await gateway.insert(input) } catch (error) { - logging.error('RecentFood operation error:', error) + logging.error('[NON-FATAL] insert:', error) return null } }, @@ -59,7 +59,7 @@ export function createRecentFoodRepository( try { return await gateway.update(id, input) } catch (error) { - logging.error('RecentFood operation error:', error) + logging.error('[NON-FATAL] update:', error) return null } }, @@ -72,7 +72,7 @@ export function createRecentFoodRepository( try { return await gateway.deleteByReference(userId, type, referenceId) } catch (error) { - logging.error('RecentFood operation error:', error) + logging.error('[NON-FATAL] deleteByReference:', error) return false } }, diff --git a/src/modules/recent-food/infrastructure/signals/recentFoodCacheStore.ts b/src/modules/recent-food/infrastructure/signals/recentFoodCacheStore.ts deleted file mode 100644 index 6665029a..00000000 --- a/src/modules/recent-food/infrastructure/signals/recentFoodCacheStore.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { createEffect, createSignal, untrack } from 'solid-js' - -import { type RecentFood } from '~/modules/recent-food/domain/recentFood' -import { logging } from '~/shared/utils/logging' - -const [recentFoods, setRecentFoods] = createSignal([]) - -function clearCache() { - logging.debug(`Clearing cache`) - setRecentFoods([]) -} - -function upsertToCache(recentFood: RecentFood) { - logging.debug(`Upserting recent food:`, recentFood) - const existingIndex = untrack(recentFoods).findIndex( - (rf) => rf.id === recentFood.id, - ) - setRecentFoods((existingRecentFoods) => { - const foods = [...existingRecentFoods] - if (existingIndex >= 0) { - foods[existingIndex] = recentFood - } else { - foods.push(recentFood) - // Sort by last_used descending (most recent first) - foods.sort((a, b) => b.last_used.getTime() - a.last_used.getTime()) - } - return foods - }) -} - -function removeFromCache(filter: { - by: T - value: RecentFood[T] -}) { - setRecentFoods((foods) => - foods.filter((rf) => rf[filter.by] !== filter.value), - ) -} - -function createCacheItemSignal(filter: { - by: T - value: RecentFood[T] -}) { - logging.debug(`findInCache filter=`, filter) - const result = - recentFoods().find((rf) => rf[filter.by] === filter.value) ?? null - logging.debug(`findInCache result=`, { result }) - return result -} - -export const recentFoodCacheStore = { - recentFoods, - setRecentFoods, - clearCache, - upsertToCache, - removeFromCache, - createCacheItemSignal, -} - -createEffect(() => { - logging.debug(`Recent foods cache size: `, { length: recentFoods().length }) -}) diff --git a/src/modules/recent-food/infrastructure/supabase/realtime.ts b/src/modules/recent-food/infrastructure/supabase/realtime.ts index fa6fd354..0c35d6e0 100644 --- a/src/modules/recent-food/infrastructure/supabase/realtime.ts +++ b/src/modules/recent-food/infrastructure/supabase/realtime.ts @@ -1,11 +1,17 @@ -import { recentFoodSchema } from '~/modules/recent-food/domain/recentFood' -import { recentFoodCacheStore } from '~/modules/recent-food/infrastructure/signals/recentFoodCacheStore' +import { + type RecentFood, + recentFoodSchema, +} from '~/modules/recent-food/domain/recentFood' import { SUPABASE_TABLE_RECENT_FOODS } from '~/modules/recent-food/infrastructure/supabase/constants' import { registerSubapabaseRealtimeCallback } from '~/shared/supabase/supabase' import { logging } from '~/shared/utils/logging' let initialized = false -export function initializeRecentFoodRealtime() { +export function initializeRecentFoodRealtime(callbacks: { + onInsert: (data: RecentFood) => void + onUpdate: (data: RecentFood) => void + onDelete: (data: RecentFood) => void +}) { if (initialized) { return } @@ -21,24 +27,21 @@ export function initializeRecentFoodRealtime() { switch (event.eventType) { case 'INSERT': { if (event.new !== undefined) { - recentFoodCacheStore.upsertToCache(event.new) + callbacks.onInsert(event.new) } break } case 'UPDATE': { if (event.new) { - recentFoodCacheStore.upsertToCache(event.new) + callbacks.onUpdate(event.new) } break } case 'DELETE': { if (event.old) { - recentFoodCacheStore.removeFromCache({ - by: 'id', - value: event.old.id, - }) + callbacks.onDelete(event.old) } break } diff --git a/src/modules/template-search/application/templateSearchLogic.ts b/src/modules/template-search/application/templateSearchLogic.ts index 63eedd52..5e23dc15 100644 --- a/src/modules/template-search/application/templateSearchLogic.ts +++ b/src/modules/template-search/application/templateSearchLogic.ts @@ -17,7 +17,7 @@ export type FetchTemplatesDeps = { userId: User['uuid'], name: string, ) => Promise - fetchUserRecentFoods: ( + fetchUserRecentFoodsAsTemplates: ( userId: User['uuid'], search: string, opts?: { limit?: number }, @@ -54,7 +54,10 @@ export async function fetchTemplatesByTabLogic( if (userId === undefined) { return [] } - const templates = await deps.fetchUserRecentFoods(userId, search) + const templates = await deps.fetchUserRecentFoodsAsTemplates( + userId, + search, + ) // Apply additional client-side filtering if needed (for EAN search) if (lowerSearch !== '') { diff --git a/src/modules/template-search/application/tests/templateSearchLogic.test.ts b/src/modules/template-search/application/tests/templateSearchLogic.test.ts index 4589c879..78532ab1 100644 --- a/src/modules/template-search/application/tests/templateSearchLogic.test.ts +++ b/src/modules/template-search/application/tests/templateSearchLogic.test.ts @@ -44,7 +44,9 @@ describe('fetchTemplatesByTabLogic', () => { deps = { fetchUserRecipes: vi.fn().mockResolvedValue([mockRecipe]), fetchUserRecipeByName: vi.fn().mockResolvedValue([mockRecipe]), - fetchUserRecentFoods: vi.fn().mockResolvedValue([mockFood, mockRecipe]), + fetchUserRecentFoodsAsTemplates: vi + .fn() + .mockResolvedValue([mockFood, mockRecipe]), fetchFoods: vi.fn().mockResolvedValue([mockFood]), fetchFoodsByName: vi.fn().mockResolvedValue([mockFood]), getFavoriteFoods: () => [1], @@ -80,7 +82,10 @@ describe('fetchTemplatesByTabLogic', () => { ) expect(result).toEqual([mockFood, mockRecipe]) // Verify that fetchUserRecentFoods was called with correct parameters - expect(deps.fetchUserRecentFoods).toHaveBeenCalledWith(userId, '') + expect(deps.fetchUserRecentFoodsAsTemplates).toHaveBeenCalledWith( + userId, + '', + ) }) it('fetches favorite foods for Favoritos tab', async () => { @@ -105,7 +110,7 @@ describe('fetchTemplatesByTabLogic', () => { it('filters by search string in Recentes tab', async () => { // Mock the function to return only the food template for search "Banana" - deps.fetchUserRecentFoods = vi.fn().mockResolvedValue([mockFood]) + deps.fetchUserRecentFoodsAsTemplates = vi.fn().mockResolvedValue([mockFood]) const result = await fetchTemplatesByTabLogic( availableTabs.Recentes.id, @@ -114,13 +119,16 @@ describe('fetchTemplatesByTabLogic', () => { deps, ) expect(result).toEqual([mockFood]) - expect(deps.fetchUserRecentFoods).toHaveBeenCalledWith(userId, 'Banana') + expect(deps.fetchUserRecentFoodsAsTemplates).toHaveBeenCalledWith( + userId, + 'Banana', + ) }) it('filters by EAN in Recentes tab', async () => { // The EAN search should filter client-side since the database function only searches by name // Mock the function to return both templates, then expect client-side filtering to work - deps.fetchUserRecentFoods = vi + deps.fetchUserRecentFoodsAsTemplates = vi .fn() .mockResolvedValue([mockFood, mockRecipe]) @@ -131,7 +139,7 @@ describe('fetchTemplatesByTabLogic', () => { deps, ) expect(result).toEqual([mockFood]) - expect(deps.fetchUserRecentFoods).toHaveBeenCalledWith( + expect(deps.fetchUserRecentFoodsAsTemplates).toHaveBeenCalledWith( userId, mockFood.ean!, ) diff --git a/src/modules/template-search/application/usecases/templateSearchState.ts b/src/modules/template-search/application/usecases/templateSearchState.ts index b095ada5..8f10d654 100644 --- a/src/modules/template-search/application/usecases/templateSearchState.ts +++ b/src/modules/template-search/application/usecases/templateSearchState.ts @@ -38,7 +38,8 @@ export const [templates, { refetch: refetchTemplates }] = createResource( // TODO: Convert fetchTemplatesByTabLogic deps to reactive signals? fetchUserRecipes, fetchUserRecipeByName, - fetchUserRecentFoods: recentFoodUseCases.fetchUserRecentFoods, + fetchUserRecentFoodsAsTemplates: + recentFoodUseCases.fetchUserRecentFoodsAsTemplates, fetchFoods, fetchFoodsByName, getFavoriteFoods, diff --git a/src/sections/search/components/TemplateSearchModal.tsx b/src/sections/search/components/TemplateSearchModal.tsx index 83000e75..8dd22ab8 100644 --- a/src/sections/search/components/TemplateSearchModal.tsx +++ b/src/sections/search/components/TemplateSearchModal.tsx @@ -84,7 +84,11 @@ export function TemplateSearchModal(props: TemplateSearchModalProps) { const handleConfirm = async () => { props.onNewItem?.(newItem, originalAddedItem) - void recentFoodUseCases.touchRecentFoodForItem(originalAddedItem) + void recentFoodUseCases + .touchRecentFoodForItem(originalAddedItem) + .then(() => { + void refetchTemplates() + }) const confirmModalId = openConfirmModal( 'Deseja adicionar outro item ou finalizar a inclusão?', diff --git a/src/sections/search/ui/openTemplateSearchModal.tsx b/src/sections/search/ui/openTemplateSearchModal.tsx index 5031716e..5538e733 100644 --- a/src/sections/search/ui/openTemplateSearchModal.tsx +++ b/src/sections/search/ui/openTemplateSearchModal.tsx @@ -3,6 +3,7 @@ * Opens the TemplateSearchModal using the modal system. */ +import { refetchTemplates } from '~/modules/template-search/application/usecases/templateSearchState' import { TemplateSearchModal, type TemplateSearchModalProps, @@ -43,6 +44,7 @@ export function openTemplateSearchModal(config: TemplateSearchModalConfig) { { title, onClose: () => { + void refetchTemplates() config.onClose?.() }, }, diff --git a/src/shared/supabase/database.types.ts b/src/shared/supabase/database.types.ts index 4d2a7d51..03dbceed 100644 --- a/src/shared/supabase/database.types.ts +++ b/src/shared/supabase/database.types.ts @@ -221,7 +221,6 @@ export type Database = { times_used: number type: string user_id: string | null - user_id_old: number | null } Insert: { created_at?: string @@ -231,7 +230,6 @@ export type Database = { times_used: number type?: string user_id?: string | null - user_id_old?: number | null } Update: { created_at?: string @@ -241,17 +239,8 @@ export type Database = { times_used?: number type?: string user_id?: string | null - user_id_old?: number | null } - Relationships: [ - { - foreignKeyName: 'recent_foods_user_id_old_fkey' - columns: ['user_id_old'] - isOneToOne: false - referencedRelation: 'users' - referencedColumns: ['id'] - }, - ] + Relationships: [] } recipes: { Row: { @@ -330,6 +319,45 @@ export type Database = { } Relationships: [] } + users_duplicate: { + Row: { + birthdate: string + created_at: string + desired_weight: number + diet: string + favorite_foods: number[] | null + gender: string + id: number + macro_profile: Json | null + name: string + uuid: string + } + Insert: { + birthdate?: string + created_at?: string + desired_weight: number + diet?: string + favorite_foods?: number[] | null + gender?: string + id?: number + macro_profile?: Json | null + name: string + uuid: string + } + Update: { + birthdate?: string + created_at?: string + desired_weight?: number + diet?: string + favorite_foods?: number[] | null + gender?: string + id?: number + macro_profile?: Json | null + name?: string + uuid?: string + } + Relationships: [] + } weights: { Row: { created_at: string diff --git a/src/shared/supabase/supabase.ts b/src/shared/supabase/supabase.ts index 0b9fdaa2..5b01765e 100644 --- a/src/shared/supabase/supabase.ts +++ b/src/shared/supabase/supabase.ts @@ -64,6 +64,7 @@ export function registerSubapabaseRealtimeCallback( supabase .channel(table) .on( + // TODO: Make supabase realtime only subscribe to events of the current user (how?) (Note: remember to use reactive current user ID changes) 'postgres_changes', { event: '*', schema: 'public', table }, handleCallback, From 3be16d7cc464289530565104555c5dca7bce1487 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Wed, 3 Dec 2025 14:52:08 -0300 Subject: [PATCH 05/23] style(recent-food): remove unnecessary comment from recentFood.ts --- src/modules/recent-food/domain/recentFood.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/recent-food/domain/recentFood.ts b/src/modules/recent-food/domain/recentFood.ts index 4f1e43e7..e2beb18b 100644 --- a/src/modules/recent-food/domain/recentFood.ts +++ b/src/modules/recent-food/domain/recentFood.ts @@ -1,5 +1,3 @@ -// Domain layer for recent food - pure business logic without external dependencies - import { z } from 'zod/v4' import { createZodEntity } from '~/shared/domain/validation' From 6c0b60289b62292db3b500a3f5e78f134be1ea93 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Wed, 3 Dec 2025 15:41:38 -0300 Subject: [PATCH 06/23] feat(pr-validation): add automated PR vs issue verification prompt --- .github/prompts/validate-pr.prompt.md | 102 ++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/prompts/validate-pr.prompt.md diff --git a/.github/prompts/validate-pr.prompt.md b/.github/prompts/validate-pr.prompt.md new file mode 100644 index 00000000..a49884f6 --- /dev/null +++ b/.github/prompts/validate-pr.prompt.md @@ -0,0 +1,102 @@ +--- +description: Review a GitHub Pull Request (PR) against its referenced issue and acceptance criteria. The prompt takes a PR number (or full PR URL), fetches the PR description and changed files, finds the referenced issue (look for Fixes or explicit issue link), and performs a focused verification: does the PR implement the issue correctly? Are acceptance criteria met? Are there missing implementations, regressions, or scope creeps? Produce a concise human-readable review report and a machine-friendly checklist and suggestions. +agent: github-issue-manager +tools: ['changes', 'codebase', 'editFiles', 'extensions', 'fetch', 'findTestFiles', 'githubRepo', 'new', 'openSimpleBrowser', 'problems', 'runCommands', 'runNotebooks', 'runTasks', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure', 'usages', 'vscodeAPI', 'activePullRequest'] +--- + +# PR vs Issue Verification Prompt + +Goal +- Given a PR identifier, automatically evaluate whether the PR implements the + referenced issue and meets its acceptance criteria. Point out missing items, + acceptance violations, potential regressions, and provide actionable fixes. + +Input +- Required: PR identifier (one of: PR number, full PR URL, or "owner/repo#PR"). +- Optional: "source" hint (github api, local git, or manual copy) if automatic fetch is not possible. +- Optional: "strictness" level: quick | thorough (default thorough). + +Primary steps the assistant must perform +1. Fetch PR metadata: + - PR title and full description/body. + - All files changed in the PR, including diffs/patches. + - Any linked issues mentioned in the PR description (look for "Fixes #", "Closes #", or explicit issue URLs). + - CI status (if available), existing test results. +2. Identify the referenced issue: + - If PR body contains "Fixes #" or "Closes #" or an issue URL, open that issue. + - If multiple issues referenced, list them and prioritize ones marked as "Fixes" or "Closes". +3. Fetch issue content: + - Title, body, labels, and especially acceptance criteria or checklist items in the issue description. + - Any related comments that modify or clarify requirements (scan last N comments, N=8). +4. Compare PR changes with issue acceptance criteria: + - For each acceptance criterion, determine whether the PR: + - Fully implements it (point to file(s)/diff lines that satisfy it). + - Partially implements it (explain what's missing). + - Does not implement it. + - Detect regressions or unrelated large scope changes (files that don't appear relevant to the issue). + - Detect potential security or privacy regressions if code touches auth/permissions/data-export. +5. Check documentation and tests: + - Are tests added/updated for new behavior? Point to test files. + - Are relevant docs/README/CHANGELOG updated if required by acceptance criteria? + - Run static checks if available (or recommend commands to run). +6. Produce outputs: + - A summary verdict (Accept / Needs changes / Reject) and short reason. + - A checklist mapping each acceptance criterion to status (Done / Partial / Missing) with evidence: file paths, code snippets (small), or diff references. + - A list of detected issues (bugs, missing tests, missing docs, scope creep, style/format problems). + - Suggested, prioritized actionable changes (patch-level guidance or sample code). + - Commands to run locally to reproduce tests/linters and quick steps for the author (e.g., "run: pnpm test -w", "npm run lint", or CI link). + - If PR description lacks "Fixes #", recommend adding the issue reference and where it should be added. +7. Output formats + - Primary: human-readable markdown report. + - Secondary: machine-friendly JSON object at the bottom with keys: + - pr: { number, title, url } + - issue: { number, title, url } + - verdict: Accept|ChangesRequested|Reject + - acceptanceChecklist: [{ criterion, status, evidence: [{file,line,excerpt}] }] + - findings: [{ severity, path, message, suggestion }] + - runCommands: [string] + - Keep JSON compact and valid for programmatic parsing. + +Behavior rules and heuristics +- When matching acceptance criteria to code, prefer exact code references (file + function + line range) over vague statements. +- If acceptance criteria are ambiguous or missing, ask one clarifying question instead of guessing. +- If multiple files implement a feature, ensure they are consistent (no duplicate logic or contradictory behavior). +- If tests fail or CI is red, mark as "Needs changes" and include failing test names and error snippets. +- Flag changes that touch unrelated modules as potential scope creep—explain risk. +- Respect repository conventions (e.g., presence of a testing framework, command names). If unknown, include recommended commands and ask for confirmation. +- Be concise. Produce an executive summary no longer than ~6 sentences, then the detailed checklist and findings. + +Formatting requirements +- Start with a one-line summary verdict. +- Then an "Executive summary" (1–4 short paragraphs). +- Then "Acceptance checklist" with bullet items referencing files/line ranges. +- Then "Findings & Recommendations" with prioritized actionable items. +- Close with the compact JSON block suitable for tooling. + +Example invocation (user -> assistant) +- "PR: 1438" +- "PR: https://github.com/owner/repo/pull/1438" +- "Please run a thorough review of PR #1438 against its referenced issue." + +Example output (outline) +- Verdict line: "Verdict: Needs changes — missing tests for X and acceptance item Y." +- Executive summary. +- Acceptance checklist: + - [Done] Criterion A — evidence: src/modules/foo/bar.ts:32-54 + - [Partial] Criterion B — evidence: src/modules/foo/baz.ts:12-18, missing: input validation + - [Missing] Criterion C — not implemented +- Findings & Recommendations (with suggested code changes or tests). +- Run commands. +- JSON blob. + +When to ask follow-up questions +- If PR description does not reference an issue explicitly, ask: "Which issue should I validate this PR against?" +- If acceptance criteria are unclear or not present in the issue, ask a clarifying question listing the ambiguous points. +- If the repo requires private access for fetching PR data, ask user to provide PR body and file diffs. + +Security & privacy +- Never leak secrets or environment variables. +- If the code touches credentials or tokens, flag it and recommend rotating secrets if necessary. + +End of prompt +- Return the full markdown report plus the JSON blob, using the format described above. From 801412d33a0bf80643a443e27a9623d2a8c14daa Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Wed, 3 Dec 2025 15:44:07 -0300 Subject: [PATCH 07/23] feat(github-issue-manager): add tools section for enhanced functionality --- .claude/agents/github-issue-manager.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.claude/agents/github-issue-manager.md b/.claude/agents/github-issue-manager.md index 47df7797..fec81f97 100644 --- a/.claude/agents/github-issue-manager.md +++ b/.claude/agents/github-issue-manager.md @@ -2,6 +2,8 @@ name: github-issue-manager description: Use this agent when you need to manage GitHub repository issues, including viewing existing issues, creating new issues with proper templates and labels, updating issue status, managing milestones, or coordinating issue workflows. Examples: Context: User wants to create a new feature request issue for adding dark mode support. user: "I want to create an issue for adding dark mode to the app" assistant: "I'll use the github-issue-manager agent to create a properly formatted feature request issue with the correct labels and template." Context: User needs to review all open bugs before a release. user: "Show me all open bug issues that need to be fixed before v2.0 release" assistant: "Let me use the github-issue-manager agent to query and analyze all open bug issues filtered by the v2.0 milestone." Context: User wants to update an issue's labels and milestone after reviewing it. user: "Issue #123 should be labeled as high complexity and assigned to the v2.1 milestone" assistant: "I'll use the github-issue-manager agent to update issue #123 with the appropriate complexity label and milestone assignment." color: purple +tools: + ['search', 'new', 'github/*', 'runCommands', 'runTasks', 'usages', 'vscodeAPI', 'problems', 'changes', 'testFailure', 'openSimpleBrowser', 'fetch', 'githubRepo', 'runTests'] --- You are an expert GitHub Issue Manager with comprehensive knowledge of repository management, issue workflows, and GitHub CLI operations. You specialize in efficiently managing the complete issue lifecycle using gh commands and understanding repository standards. From 084fabc4a760a38b3cdd8557dbbc359710b59525 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Wed, 3 Dec 2025 19:42:39 -0300 Subject: [PATCH 08/23] feat(recent-food): support multiple references for group items and template deletion --- .../tests/extractRecentFoodReference.test.ts | 40 ++++++------- .../usecases/extractRecentFoodReference.ts | 57 ++++++++++--------- .../usecases/recentFoodUseCases.ts | 51 ++++++++++++++--- .../recent-food/ui/RemoveFromRecentButton.tsx | 13 +---- .../search/components/TemplateSearchModal.tsx | 4 +- 5 files changed, 97 insertions(+), 68 deletions(-) diff --git a/src/modules/recent-food/application/tests/extractRecentFoodReference.test.ts b/src/modules/recent-food/application/tests/extractRecentFoodReference.test.ts index 239a772a..0cd69b19 100644 --- a/src/modules/recent-food/application/tests/extractRecentFoodReference.test.ts +++ b/src/modules/recent-food/application/tests/extractRecentFoodReference.test.ts @@ -13,9 +13,9 @@ import { type RecipeItem, } from '~/modules/diet/item/schema/itemSchema' import { createMacroNutrients } from '~/modules/diet/macro-nutrients/domain/macroNutrients' -import { extractRecentFoodReference } from '~/modules/recent-food/application/usecases/extractRecentFoodReference' +import { extractRecentFoodReferenceFromItem } from '~/modules/recent-food/application/usecases/extractRecentFoodReference' -describe('extractRecentFoodReference', () => { +describe('extractRecentFoodReferenceFromItem', () => { const mockMacros = createMacroNutrients({ carbsInGrams: 25, proteinInGrams: 2, @@ -44,7 +44,7 @@ describe('extractRecentFoodReference', () => { }, }) - const result = extractRecentFoodReference(foodItem) + const [result] = extractRecentFoodReferenceFromItem(foodItem) expect(result).toEqual({ type: 'food', @@ -65,7 +65,7 @@ describe('extractRecentFoodReference', () => { }, }) - const result = extractRecentFoodReference(eanFoodItem) + const [result] = extractRecentFoodReferenceFromItem(eanFoodItem) expect(result).toEqual({ type: 'food', @@ -87,7 +87,7 @@ describe('extractRecentFoodReference', () => { }, }) - const result = extractRecentFoodReference(recipeItem) + const [result] = extractRecentFoodReferenceFromItem(recipeItem) expect(result).toEqual({ type: 'recipe', @@ -118,7 +118,7 @@ describe('extractRecentFoodReference', () => { }, }) - const result = extractRecentFoodReference(recipeItem) + const [result] = extractRecentFoodReferenceFromItem(recipeItem) expect(result).toEqual({ type: 'recipe', @@ -150,7 +150,7 @@ describe('extractRecentFoodReference', () => { }, }) - const result = extractRecentFoodReference(groupItem) + const [result] = extractRecentFoodReferenceFromItem(groupItem) expect(result).toEqual({ type: 'food', @@ -180,7 +180,7 @@ describe('extractRecentFoodReference', () => { }, }) - const result = extractRecentFoodReference(groupItem) + const [result] = extractRecentFoodReferenceFromItem(groupItem) expect(result).toEqual({ type: 'recipe', @@ -221,7 +221,7 @@ describe('extractRecentFoodReference', () => { }, }) - const result = extractRecentFoodReference(groupItem) + const [result] = extractRecentFoodReferenceFromItem(groupItem) expect(result).toEqual({ type: 'food', @@ -229,7 +229,7 @@ describe('extractRecentFoodReference', () => { }) }) - it('should return null for GroupItem with empty children', () => { + it('should return undefined for GroupItem with empty children', () => { const groupItem: GroupItem = createGroupItem({ id: 7, name: 'Empty Group', @@ -240,12 +240,12 @@ describe('extractRecentFoodReference', () => { }, }) - const result = extractRecentFoodReference(groupItem) + const [result] = extractRecentFoodReferenceFromItem(groupItem) - expect(result).toBeNull() + expect(result).toBeUndefined() }) - it('should return null for GroupItem with nested group child (no trackable reference)', () => { + it('should return undefined for GroupItem with nested group child (no trackable reference)', () => { const nestedGroup: GroupItem = createGroupItem({ id: 40, name: 'Nested Group', @@ -266,9 +266,9 @@ describe('extractRecentFoodReference', () => { }, }) - const result = extractRecentFoodReference(groupItem) + const [result] = extractRecentFoodReferenceFromItem(groupItem) - expect(result).toBeNull() + expect(result).toBeUndefined() }) }) @@ -277,7 +277,7 @@ describe('extractRecentFoodReference', () => { // This simulates the exact flow when a food is added via EAN scan: // 1. Food is fetched/imported from API with a database ID // 2. templateToItem creates a FoodItem with reference.id = food's database ID - // 3. extractRecentFoodReference should extract that reference for tracking + // 3. extractRecentFoodReferenceFromItem should extract that reference for tracking const eanScannedFood = promoteNewFoodToFood( createNewFood({ @@ -304,9 +304,9 @@ describe('extractRecentFoodReference', () => { }, }) - const result = extractRecentFoodReference(itemFromEanFood) + const [result] = extractRecentFoodReferenceFromItem(itemFromEanFood) - expect(result).not.toBeNull() + expect(result).not.toBeUndefined() expect(result?.type).toBe('food') expect(result?.referenceId).toBe(98765) // Must match the database ID }) @@ -335,9 +335,9 @@ describe('extractRecentFoodReference', () => { }, }) - const result = extractRecentFoodReference(groupifiedItem) + const [result] = extractRecentFoodReferenceFromItem(groupifiedItem) - expect(result).not.toBeNull() + expect(result).not.toBeUndefined() expect(result?.type).toBe('food') expect(result?.referenceId).toBe(5555) // Should still track the original food }) diff --git a/src/modules/recent-food/application/usecases/extractRecentFoodReference.ts b/src/modules/recent-food/application/usecases/extractRecentFoodReference.ts index faa2da2c..dc9c6901 100644 --- a/src/modules/recent-food/application/usecases/extractRecentFoodReference.ts +++ b/src/modules/recent-food/application/usecases/extractRecentFoodReference.ts @@ -4,6 +4,8 @@ import { isRecipeItem, type Item, } from '~/modules/diet/item/schema/itemSchema' +import { templateToItem } from '~/modules/diet/template/application/templateToItem' +import { type Template } from '~/modules/diet/template/domain/template' import { type RecentFood } from '~/modules/recent-food/domain/recentFood' /** @@ -25,42 +27,45 @@ export type RecentFoodReference = { * @param item - The item to extract reference from * @returns The recent food reference, or null if the item cannot be tracked */ -export function extractRecentFoodReference( +export function extractRecentFoodReferenceFromItem( item: Item, -): RecentFoodReference | null { +): RecentFoodReference[] { if (isFoodItem(item)) { - return { - type: 'food', - referenceId: item.reference.id, - } + return [ + { + type: item.reference.type, + referenceId: item.reference.id, + }, + ] } if (isRecipeItem(item)) { - return { - type: 'recipe', - referenceId: item.reference.id, - } + return [ + { + type: item.reference.type, + referenceId: item.reference.id, + }, + ] } if (isGroupItem(item)) { - // GroupItem: track using the first trackable child's reference - const firstChild = item.reference.children[0] - if (firstChild !== undefined && isFoodItem(firstChild)) { - return { - type: 'food', - referenceId: firstChild.reference.id, - } - } - if (firstChild !== undefined && isRecipeItem(firstChild)) { - return { - type: 'recipe', - referenceId: firstChild.reference.id, + const references: RecentFoodReference[] = [] + for (const child of item.reference.children) { + const childReferences = extractRecentFoodReferenceFromItem(child) + if (childReferences.length > 0) { + references.push(...childReferences) } } - // Cannot track - no trackable children - return null + return references } - // Unknown item type (should never happen due to exhaustive checks) - return null + // Should never reach here + item satisfies never + return [] +} + +export function extractRecentFoodReferenceFromTemplate( + template: Template, +): RecentFoodReference[] { + return extractRecentFoodReferenceFromItem(templateToItem(template)) } diff --git a/src/modules/recent-food/application/usecases/recentFoodUseCases.ts b/src/modules/recent-food/application/usecases/recentFoodUseCases.ts index 031f257b..57236e43 100644 --- a/src/modules/recent-food/application/usecases/recentFoodUseCases.ts +++ b/src/modules/recent-food/application/usecases/recentFoodUseCases.ts @@ -2,8 +2,13 @@ import { createRoot } from 'solid-js' import { authUseCases } from '~/modules/auth/application/usecases/authUseCases' import { type Item } from '~/modules/diet/item/schema/itemSchema' +import { type Template } from '~/modules/diet/template/domain/template' import { createRecentFoodCrudService } from '~/modules/recent-food/application/services/recentFoodCrudService' -import { extractRecentFoodReference } from '~/modules/recent-food/application/usecases/extractRecentFoodReference' +import { + extractRecentFoodReferenceFromItem, + extractRecentFoodReferenceFromTemplate, + type RecentFoodReference, +} from '~/modules/recent-food/application/usecases/extractRecentFoodReference' import { createNewRecentFood, type NewRecentFood, @@ -109,17 +114,31 @@ export const recentFoodUseCases = { ) }, - touchRecentFoodForItem: async (item: Item) => { - const recentFoodRef = extractRecentFoodReference(item) - if (recentFoodRef === null) { + async deleteRecentFoodOfTemplate(template: Template) { + const [recentFoodReference, ...rest] = + extractRecentFoodReferenceFromTemplate(template) + + if (recentFoodReference === undefined) { + return + } + if (rest.length > 0) { logging.warn( - 'Cannot touch recent food for item - no trackable reference found', - { item }, + 'Expected only one recent food reference for template, but found multiple.', + { + template, + recentFoodReferences: [recentFoodReference, ...rest], + }, ) - showError('Não foi possível adicionar alimento aos alimentos recentes.') - return } + await recentFoodUseCases.deleteRecentFoodByReference( + authUseCases.currentUserIdOrGuestId(), + recentFoodReference.type, + recentFoodReference.referenceId, + ) + }, + + touchRecentFood: async (recentFoodRef: RecentFoodReference) => { const currentRecentFood = await recentFoodCrudService.fetchRecentFoodByUserTypeAndReferenceId( authUseCases.currentUserIdOrGuestId(), @@ -157,4 +176,20 @@ export const recentFoodUseCases = { ) } }, + + touchRecentFoodForItem: async (item: Item) => { + const [recentFoodRef] = extractRecentFoodReferenceFromItem(item) + if (recentFoodRef === undefined) { + logging.warn( + 'Cannot touch recent food for item - no trackable reference found', + { item }, + ) + showError('Não foi possível adicionar alimento aos alimentos recentes.') + return + } + + for (const recentFoodRef of extractRecentFoodReferenceFromItem(item)) { + await recentFoodUseCases.touchRecentFood(recentFoodRef) + } + }, } diff --git a/src/modules/recent-food/ui/RemoveFromRecentButton.tsx b/src/modules/recent-food/ui/RemoveFromRecentButton.tsx index ea5d3ad9..f6c1fd27 100644 --- a/src/modules/recent-food/ui/RemoveFromRecentButton.tsx +++ b/src/modules/recent-food/ui/RemoveFromRecentButton.tsx @@ -1,10 +1,6 @@ import { Show } from 'solid-js' -import { authUseCases } from '~/modules/auth/application/usecases/authUseCases' -import { - isTemplateFood, - type Template, -} from '~/modules/diet/template/domain/template' +import { type Template } from '~/modules/diet/template/domain/template' import { recentFoodUseCases } from '~/modules/recent-food/application/usecases/recentFoodUseCases' import { debouncedTab } from '~/modules/template-search/application/usecases/templateSearchState' import { TrashIcon } from '~/sections/common/components/icons/TrashIcon' @@ -19,13 +15,8 @@ export function RemoveFromRecentButton(props: RemoveFromRecentButtonProps) { e.stopPropagation() e.preventDefault() - const templateType = isTemplateFood(props.template) ? 'food' : 'recipe' - const templateId = props.template.id - - const userId = authUseCases.currentUserIdOrGuestId() - void recentFoodUseCases - .deleteRecentFoodByReference(userId, templateType, templateId) + .deleteRecentFoodOfTemplate(props.template) .then(props.refetch) } diff --git a/src/sections/search/components/TemplateSearchModal.tsx b/src/sections/search/components/TemplateSearchModal.tsx index 8dd22ab8..a8d2ada2 100644 --- a/src/sections/search/components/TemplateSearchModal.tsx +++ b/src/sections/search/components/TemplateSearchModal.tsx @@ -86,9 +86,7 @@ export function TemplateSearchModal(props: TemplateSearchModalProps) { void recentFoodUseCases .touchRecentFoodForItem(originalAddedItem) - .then(() => { - void refetchTemplates() - }) + .then(refetchTemplates) const confirmModalId = openConfirmModal( 'Deseja adicionar outro item ou finalizar a inclusão?', From 7d56c0880ef333675aed03221214455ef9472d56 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 19:30:13 -0300 Subject: [PATCH 09/23] refactor(recent-food): extract touch functions and centralize dependencies --- .../recent-food/application/usecases/deps.ts | 26 ++++++ .../usecases/recentFoodUseCases.ts | 83 ++----------------- .../application/usecases/touchRecentFood.ts | 46 ++++++++++ .../usecases/touchRecentFoodForItem.ts | 21 +++++ 4 files changed, 100 insertions(+), 76 deletions(-) create mode 100644 src/modules/recent-food/application/usecases/deps.ts create mode 100644 src/modules/recent-food/application/usecases/touchRecentFood.ts create mode 100644 src/modules/recent-food/application/usecases/touchRecentFoodForItem.ts diff --git a/src/modules/recent-food/application/usecases/deps.ts b/src/modules/recent-food/application/usecases/deps.ts new file mode 100644 index 00000000..b1fca564 --- /dev/null +++ b/src/modules/recent-food/application/usecases/deps.ts @@ -0,0 +1,26 @@ +import { createRoot } from 'solid-js' + +import { createRecentFoodCrudService } from '~/modules/recent-food/application/services/recentFoodCrudService' +import { createRecentFoodRepository } from '~/modules/recent-food/infrastructure/recentFoodRepository' +import { initializeRecentFoodRealtime } from '~/modules/recent-food/infrastructure/supabase/realtime' +import { createSupabaseRecentFoodGateway } from '~/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway' + +// Centralized dependency wiring for recent-food use-cases. +// This file performs the minimal initialization (createRoot) once and +// exports the constructed service for use by individual use-case modules. +const { recentFoodCrudService } = createRoot(() => { + const supabaseRecentFoodGateway = createSupabaseRecentFoodGateway() + const repository = createRecentFoodRepository(supabaseRecentFoodGateway) + const recentFoodCrudService = createRecentFoodCrudService(repository) + + // TODO: Implement recent food cache using realtime updates + initializeRecentFoodRealtime({ + onInsert: (_: unknown) => {}, + onUpdate: (_: unknown) => {}, + onDelete: (_: unknown) => {}, + }) + + return { recentFoodCrudService } +}) + +export { recentFoodCrudService } diff --git a/src/modules/recent-food/application/usecases/recentFoodUseCases.ts b/src/modules/recent-food/application/usecases/recentFoodUseCases.ts index 57236e43..8f3b8d27 100644 --- a/src/modules/recent-food/application/usecases/recentFoodUseCases.ts +++ b/src/modules/recent-food/application/usecases/recentFoodUseCases.ts @@ -1,22 +1,17 @@ -import { createRoot } from 'solid-js' - import { authUseCases } from '~/modules/auth/application/usecases/authUseCases' import { type Item } from '~/modules/diet/item/schema/itemSchema' import { type Template } from '~/modules/diet/template/domain/template' -import { createRecentFoodCrudService } from '~/modules/recent-food/application/services/recentFoodCrudService' +import { recentFoodCrudService } from '~/modules/recent-food/application/usecases/deps' import { - extractRecentFoodReferenceFromItem, extractRecentFoodReferenceFromTemplate, type RecentFoodReference, } from '~/modules/recent-food/application/usecases/extractRecentFoodReference' +import { touchRecentFood } from '~/modules/recent-food/application/usecases/touchRecentFood' +import { touchRecentFoodForItem } from '~/modules/recent-food/application/usecases/touchRecentFoodForItem' import { - createNewRecentFood, type NewRecentFood, type RecentFood, } from '~/modules/recent-food/domain/recentFood' -import { createRecentFoodRepository } from '~/modules/recent-food/infrastructure/recentFoodRepository' -import { initializeRecentFoodRealtime } from '~/modules/recent-food/infrastructure/supabase/realtime' -import { createSupabaseRecentFoodGateway } from '~/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway' import { showError, showPromise, @@ -24,21 +19,6 @@ import { import { type User } from '~/modules/user/domain/user' import { logging } from '~/shared/utils/logging' -const { recentFoodCrudService } = createRoot(() => { - const supabaseRecentFoodGateway = createSupabaseRecentFoodGateway() - const repository = createRecentFoodRepository(supabaseRecentFoodGateway) - const recentFoodCrudService = createRecentFoodCrudService(repository) - - // TODO: Implement recent food cache using realtime updates - initializeRecentFoodRealtime({ - onInsert: (_: RecentFood) => {}, - onUpdate: (_: RecentFood) => {}, - onDelete: (_: RecentFood) => {}, - }) - - return { recentFoodCrudService } -}) - export const recentFoodUseCases = { fetchUserRecentFoodsAsTemplates: async ( userId: User['uuid'], @@ -138,58 +118,9 @@ export const recentFoodUseCases = { ) }, - touchRecentFood: async (recentFoodRef: RecentFoodReference) => { - const currentRecentFood = - await recentFoodCrudService.fetchRecentFoodByUserTypeAndReferenceId( - authUseCases.currentUserIdOrGuestId(), - recentFoodRef.type, - recentFoodRef.referenceId, - ) - - const timesCurrentlyUsed = currentRecentFood?.times_used ?? 0 - const newRecentFoodData = createNewRecentFood({ - user_id: authUseCases.currentUserIdOrGuestId(), - type: recentFoodRef.type, - reference_id: recentFoodRef.referenceId, - last_used: new Date(), - times_used: timesCurrentlyUsed + 1, - }) - - if (currentRecentFood === null) { - await recentFoodUseCases.insertRecentFood(newRecentFoodData) - } else { - // TODO: Remove client-side user check after implementing row-level security (RLS) - if (currentRecentFood.user_id !== authUseCases.currentUserIdOrGuestId()) { - throw new Error('BUG: recentFood fetched does not match current user') - } - - if ( - currentRecentFood.type !== recentFoodRef.type || - currentRecentFood.reference_id !== recentFoodRef.referenceId - ) { - throw new Error('BUG: recentFood fetched does not match type/reference') - } + touchRecentFood: async (recentFoodRef: RecentFoodReference) => + await touchRecentFood(recentFoodRef), - await recentFoodUseCases.updateRecentFood( - currentRecentFood.id, - newRecentFoodData, - ) - } - }, - - touchRecentFoodForItem: async (item: Item) => { - const [recentFoodRef] = extractRecentFoodReferenceFromItem(item) - if (recentFoodRef === undefined) { - logging.warn( - 'Cannot touch recent food for item - no trackable reference found', - { item }, - ) - showError('Não foi possível adicionar alimento aos alimentos recentes.') - return - } - - for (const recentFoodRef of extractRecentFoodReferenceFromItem(item)) { - await recentFoodUseCases.touchRecentFood(recentFoodRef) - } - }, + touchRecentFoodForItem: async (item: Item) => + await touchRecentFoodForItem(item), } diff --git a/src/modules/recent-food/application/usecases/touchRecentFood.ts b/src/modules/recent-food/application/usecases/touchRecentFood.ts new file mode 100644 index 00000000..38664535 --- /dev/null +++ b/src/modules/recent-food/application/usecases/touchRecentFood.ts @@ -0,0 +1,46 @@ +import { authUseCases } from '~/modules/auth/application/usecases/authUseCases' +import { recentFoodCrudService } from '~/modules/recent-food/application/usecases/deps' +import type { RecentFoodReference } from '~/modules/recent-food/application/usecases/extractRecentFoodReference' +import { + createNewRecentFood, + type NewRecentFood, +} from '~/modules/recent-food/domain/recentFood' + +export async function touchRecentFood(recentFoodRef: RecentFoodReference) { + const currentRecentFood = + await recentFoodCrudService.fetchRecentFoodByUserTypeAndReferenceId( + authUseCases.currentUserIdOrGuestId(), + recentFoodRef.type, + recentFoodRef.referenceId, + ) + + const timesCurrentlyUsed = currentRecentFood?.times_used ?? 0 + const newRecentFoodData: NewRecentFood = createNewRecentFood({ + user_id: authUseCases.currentUserIdOrGuestId(), + type: recentFoodRef.type, + reference_id: recentFoodRef.referenceId, + last_used: new Date(), + times_used: timesCurrentlyUsed + 1, + }) + + if (currentRecentFood === null) { + await recentFoodCrudService.insertRecentFood(newRecentFoodData) + } else { + // TODO: Remove client-side user check after implementing row-level security (RLS) + if (currentRecentFood.user_id !== authUseCases.currentUserIdOrGuestId()) { + throw new Error('BUG: recentFood fetched does not match current user') + } + + if ( + currentRecentFood.type !== recentFoodRef.type || + currentRecentFood.reference_id !== recentFoodRef.referenceId + ) { + throw new Error('BUG: recentFood fetched does not match type/reference') + } + + await recentFoodCrudService.updateRecentFood( + currentRecentFood.id, + newRecentFoodData, + ) + } +} diff --git a/src/modules/recent-food/application/usecases/touchRecentFoodForItem.ts b/src/modules/recent-food/application/usecases/touchRecentFoodForItem.ts new file mode 100644 index 00000000..9803eecb --- /dev/null +++ b/src/modules/recent-food/application/usecases/touchRecentFoodForItem.ts @@ -0,0 +1,21 @@ +import type { Item } from '~/modules/diet/item/schema/itemSchema' +import { extractRecentFoodReferenceFromItem } from '~/modules/recent-food/application/usecases/extractRecentFoodReference' +import { touchRecentFood } from '~/modules/recent-food/application/usecases/touchRecentFood' +import { showError } from '~/modules/toast/application/toastManager' +import { logging } from '~/shared/utils/logging' + +export async function touchRecentFoodForItem(item: Item) { + const [recentFoodRef] = extractRecentFoodReferenceFromItem(item) + if (recentFoodRef === undefined) { + logging.warn( + 'Cannot touch recent food for item - no trackable reference found', + { item }, + ) + showError('Não foi possível adicionar alimento aos alimentos recentes.') + return + } + + for (const recentFoodRef of extractRecentFoodReferenceFromItem(item)) { + await touchRecentFood(recentFoodRef) + } +} From 0081997e409d34e12809cf937c446b4d13e4bea5 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 19:33:51 -0300 Subject: [PATCH 10/23] test: Move recent-food test to usecases tests folder --- .../{ => usecases}/tests/extractRecentFoodReference.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/modules/recent-food/application/{ => usecases}/tests/extractRecentFoodReference.test.ts (100%) diff --git a/src/modules/recent-food/application/tests/extractRecentFoodReference.test.ts b/src/modules/recent-food/application/usecases/tests/extractRecentFoodReference.test.ts similarity index 100% rename from src/modules/recent-food/application/tests/extractRecentFoodReference.test.ts rename to src/modules/recent-food/application/usecases/tests/extractRecentFoodReference.test.ts From 0c1f870252f5f8b6545dc133ffa0a22da650b1df Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 19:42:18 -0300 Subject: [PATCH 11/23] refactor(recent-food): move module to diet subdirectory --- memory.jsonl | 4 ++-- .../application/services/recentFoodCrudService.ts | 6 +++--- .../recent-food/application/usecases/deps.ts | 8 ++++---- .../usecases/extractRecentFoodReference.ts | 2 +- .../application/usecases/recentFoodUseCases.ts | 12 ++++++------ .../tests/extractRecentFoodReference.test.ts | 2 +- .../application/usecases/touchRecentFood.ts | 6 +++--- .../application/usecases/touchRecentFoodForItem.ts | 4 ++-- .../{ => diet}/recent-food/domain/recentFood.ts | 0 .../recent-food/domain/recentFoodRepository.ts | 4 ++-- .../recent-food/domain/tests/recentFood.test.ts | 2 +- .../infrastructure/recentFoodRepository.ts | 8 ++++---- .../recent-food/infrastructure/supabase/constants.ts | 0 .../recent-food/infrastructure/supabase/realtime.ts | 4 ++-- .../supabase/supabaseRecentFoodGateway.ts | 10 +++++----- .../supabase/supabaseRecentFoodMapper.ts | 2 +- .../recent-food/ui/RemoveFromRecentButton.tsx | 2 +- .../ui/tests/RemoveFromRecentButton.test.tsx | 9 ++++++--- .../application/usecases/templateSearchState.ts | 2 +- .../search/components/TemplateSearchModal.tsx | 2 +- .../search/components/TemplateSearchResultItem.tsx | 2 +- 21 files changed, 47 insertions(+), 44 deletions(-) rename src/modules/{ => diet}/recent-food/application/services/recentFoodCrudService.ts (91%) rename src/modules/{ => diet}/recent-food/application/usecases/deps.ts (63%) rename src/modules/{ => diet}/recent-food/application/usecases/extractRecentFoodReference.ts (95%) rename src/modules/{ => diet}/recent-food/application/usecases/recentFoodUseCases.ts (88%) rename src/modules/{ => diet}/recent-food/application/usecases/tests/extractRecentFoodReference.test.ts (98%) rename src/modules/{ => diet}/recent-food/application/usecases/touchRecentFood.ts (84%) rename src/modules/{ => diet}/recent-food/application/usecases/touchRecentFoodForItem.ts (75%) rename src/modules/{ => diet}/recent-food/domain/recentFood.ts (100%) rename src/modules/{ => diet}/recent-food/domain/recentFoodRepository.ts (93%) rename src/modules/{ => diet}/recent-food/domain/tests/recentFood.test.ts (97%) rename src/modules/{ => diet}/recent-food/infrastructure/recentFoodRepository.ts (88%) rename src/modules/{ => diet}/recent-food/infrastructure/supabase/constants.ts (100%) rename src/modules/{ => diet}/recent-food/infrastructure/supabase/realtime.ts (87%) rename src/modules/{ => diet}/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts (95%) rename src/modules/{ => diet}/recent-food/infrastructure/supabase/supabaseRecentFoodMapper.ts (96%) rename src/modules/{ => diet}/recent-food/ui/RemoveFromRecentButton.tsx (90%) rename src/modules/{ => diet}/recent-food/ui/tests/RemoveFromRecentButton.test.tsx (97%) diff --git a/memory.jsonl b/memory.jsonl index 7befdfd1..441dabbe 100644 --- a/memory.jsonl +++ b/memory.jsonl @@ -1,4 +1,4 @@ -{"type":"entity","entityType":"user","name":"marcuscastelo","observations":["User identified as marcuscastelo for this session.","Attempted to use mcp_memory_add_observations for session logging; error due to missing entity, so entity creation was proposed and now completed.","System prompt for agent memory management was reviewed and confirmed as a guide for using the memory tool.","All phases were tracked via issues #882 (epic), #883 (schema/domain), #887 (infra), #884 (app), #885 (UI), and PR #908 (UI migration in progress).","Barrel index.ts files are now banned in this codebase. See #file:copilot-instructions.md for rationale. All such files must be removed or left blank with a comment.","Successfully completed Issue #941: Error Detail Modal System migration to unified architecture on June 29, 2025.","Implemented feature-flagged compatibility bridge that maintains backward compatibility for openErrorModal, closeErrorModal, and getOpenModals APIs.","Updated all production imports to use new bridge while preserving legacy fallback functionality.","All tests passing (242 passed, 3 skipped) after migration with comprehensive validation completed.","Migration maintains complete backward compatibility and provides safe rollout/rollback mechanism via feature flags.","Successfully analyzed and resolved modal duplication issue in macroflows project on July 2, 2025.","Identified redundant modal architecture where TemplateSearchModal was creating its own modal structure inside the unified modal system.","Refactored TemplateSearchModal to be pure content component, removing legacy Modal wrapper structure.","Updated modal titles from English to Portuguese for better UX consistency.","All tests passing after modal architecture cleanup - validated with copilot:check script.","Fixed modal responsiveness issue in TemplateSearchModal on July 2, 2025.","Identified that fixed height `h-[60vh] sm:h-[80vh]` in TemplateSearchModal was causing mobile layout problems after modal system migration.","Changed height from fixed `h-[60vh] sm:h-[80vh]` to flexible `max-h-[60vh] sm:max-h-[70vh]` with `min-h-0` for better responsive behavior.","Adjusted TemplateSearchResults max-height from `max-h-[60vh]` to `max-h-[40vh] sm:max-h-[50vh]` to accommodate mobile viewport constraints.","All tests continue passing (244 passed, 3 skipped) after responsive fixes.","Issue was caused by modal structure change where TemplateSearchModal now renders directly inside UnifiedModalContainer instead of being wrapped in its own modal.","Fixed styles ensure modal content doesn't exceed screen bounds on mobile devices while maintaining proper desktop experience.","Successfully eliminated duplicated modal closing logic in ItemEditModal.tsx on July 2, 2025.","Identified 3 patterns of duplication in modal handlers: setChildEditModalVisible/setEditingChild (lines 390-395), setRecipeEditModalVisible (lines 406, 409, 412), and setAddItemModalVisible (lines 451, 453, 454).","Created helper functions closeChildEditModal(), closeRecipeEditModal(), and closeAddItemModal() to centralize modal closing logic.","Replaced duplicated inline modal closing calls in onApply, onClose, onCancel, onSaveRecipe, onDelete, onFinish, and onNewItem handlers.","Reduced code duplication by ~15 lines while maintaining identical functionality and improving maintainability.","All validation scripts passed (244 tests passed, 3 skipped) with TypeScript compilation, ESLint, and code quality checks successful.","Refactoring follows DRY principle and clean code practices without changing user-facing behavior.","Successfully fixed ConfirmModalContext migration blocking issue on July 3, 2025.","Migrated three recipe components (RecipeEditModal.tsx, RecipeEditView.tsx, UnifiedRecipeEditView.tsx) from deleted ConfirmModalContext to unified modal system.","Replaced useConfirmModalContext() pattern with openConfirmModal() helper from unified system.","All TypeScript compilation, ESLint checks, and tests now passing (244 passed, 3 skipped).","Modal migration plan Phase 3 can now proceed with ItemEditModal and RecipeEditModal migrations.","Unified modal system infrastructure confirmed working correctly with UnifiedModalProvider integrated.","Successfully completed RecipeEditModal.tsx migration to unified modal system on July 3, 2025.","Converted RecipeEditModal from legacy modal wrapper to pure content component, removing Modal, ModalContextProvider, and useModalContext dependencies.","Updated RecipeEditModalProps interface to remove show, onVisibilityChange props and add onClose prop for unified modal integration.","Replaced legacy modal structure (Modal.Header, Modal.Content, Modal.Footer) with direct div-based layout suitable for unified modal container.","Updated Actions component to use onClose callback instead of direct closeModal calls, ensuring proper modal lifecycle management.","Fixed ItemEditModal usage to pass onClose prop to RecipeEditModal component for seamless integration.","All tests passing (244 passed, 3 skipped) with TypeScript compilation, ESLint, and code quality checks successful.","RecipeEditModal migration represents completion of high-complexity modal in Phase 3 of modal migration plan.","Modal migration plan Phase 3 critical modals (ItemEditModal and RecipeEditModal) now fully migrated to unified system.","Successfully completed PHASE 4: Integration & Infrastructure of modal migration plan on July 3, 2025.","Updated TestModal in test-app.tsx to use unified modal system instead of legacy ModalContextProvider pattern.","Removed legacy context fallbacks from Modal component, making onClose and visible props required instead of optional.","Updated ModalHeader component to require explicit onClose prop, removing useModalContext dependency.","Fixed UnifiedModalContainer to handle conditional Modal.Header rendering based on showCloseButton setting.","Updated Modal component imports to remove unused Accessor and Setter types after legacy context removal.","All TypeScript compilation, ESLint checks, and tests passing (244 passed, 3 skipped) after legacy modal context removal.","Modal component now operates purely on explicit props without any legacy context fallbacks.","PHASE 4 Integration & Infrastructure completed: all modal patterns use unified system, legacy context fallbacks removed, UnifiedModalContainer fully integrated.","Successfully completed CLEANUP phase and entire modal migration plan on July 3, 2025.","Removed all legacy modal context files: ModalContext.tsx, legacyModalContextBridge.tsx, and associated test files.","Updated MODAL_MIGRATION_PLAN.md to reflect completed status with all phases marked as ACHIEVED.","Modal migration completed ahead of schedule: 3 days actual vs 4 weeks planned timeline.","Final validation: 242 tests passing, 3 skipped, with TypeScript compilation and ESLint checks successful.","Unified modal system now fully operational: openModal(), openEditModal(), openConfirmModal() patterns implemented throughout codebase.","Eliminated ~100+ lines of duplicated modal code while preserving all critical callbacks and user interaction flows.","No legacy modal dependencies remain - complete architectural migration achieved.","Documentation updated to serve as successful implementation reference for future projects.","Successfully fixed modal closing issues in macroflows project on July 3, 2025.","Identified and resolved 3 modal closing problems: TemplateSearchModal→ItemEditModal flow, MacroTargets 'Apagar e restaurar perfil antigo' modal, and EANInsertModal flow.","TemplateSearchModal ItemEditModal onClose was only calling props.onFinish() but not props.onClose(), leaving parent modal open.","MacroTargets modal opened via openContentModal but footer buttons (Cancelar, Apagar atual e restaurar antigo) weren't closing the modal after actions.","EANInsertModal in TemplateSearchModal had empty onClose callback and wasn't closing modal on template selection.","Solutions implemented: stored modal IDs from openContentModal calls, used closeModal from useUnifiedModal hook, ensured proper callback chaining.","All fixes maintain existing success/error handling and confirmation flows while ensuring modals close appropriately.","All validation scripts continue passing (242 tests passed, 3 skipped) with TypeScript compilation and ESLint checks successful.","Modal system now provides consistent UX where all modals close after their respective actions are completed.","Fixed critical modal closing issues by implementing proper modal ID storage on July 3, 2025.","Root cause identified: openEditModal and openConfirmModal calls weren't storing modal IDs, preventing proper modal closure.","TemplateSearchModal 'Aplicar' issue: Modified to store editModalId from openEditModal and close it after successful onApply completion.","TemplateSearchModal 'Finalizar' issue: Modified confirmation modal to store confirmModalId and close after both onConfirm and onCancel actions.","Macro overflow modal issue: Added overflowModalId storage and proper closeModal calls for both confirm and cancel scenarios.","Removed dependency on legacy closeAllModals() function, replaced with specific modal ID-based closing.","All modal operations now use proper async/await pattern with error handling that closes modals on both success and failure.","Solution ensures each modal is closed individually by its specific ID rather than global modal clearing.","All validation scripts continue passing (242 tests passed, 3 skipped) after implementing proper modal ID management.","Completed comprehensive modal ID storage audit across entire codebase on July 3, 2025.","Identified 8+ files with modal closing issues: DayMeals.tsx (2 openEditModal problems), ItemEditModal.tsx (1 problem), RecipeEditModal.tsx (2 problems), BottomNavigation.tsx, ConsoleDumpButton.tsx, RecipeEditView.tsx, MealEditView.tsx, WeightView.tsx.","Fixed DayMeals.tsx: handleEditItem and handleNewItemButton now store modal IDs and call closeModal after successful actions.","Fixed ItemEditModal.tsx: handleEditChild now stores childModalId and closes modal properly after onApply.","Started fixing RecipeEditModal.tsx by adding useUnifiedModal hook - remaining fixes needed for two openEditModal calls.","All corrected modals now follow proper pattern: store modalId from open calls, use closeModal(modalId) in onApply/onConfirm/onCancel callbacks.","Maintained async/await error handling while ensuring modals close on both success and failure scenarios.","Systematic approach identified modal closing issues by searching for openEditModal, openContentModal, openConfirmModal calls without ID storage.","All validation scripts continue passing (242 tests passed, 3 skipped) after implementing critical modal ID fixes.","Successfully completed final modal ID storage fixes in ItemEditModal.tsx and ExternalTemplateToItemModal.tsx on July 3, 2025.","Fixed ItemEditModal.tsx: Added recipeEditModalId and addItemModalId signals with helper functions closeRecipeEditModal() and closeAddItemModal().","Updated both openEditModal calls in ItemEditModal.tsx to store modal IDs and use closeModal(modalId) for proper modal closing.","Fixed ExternalTemplateToItemModal.tsx: Added useUnifiedModal hook and updated openEditModal call to store modal ID and use it in onClose callbacks.","All modal flows now properly store modal IDs and close modals after actions complete, eliminating the remaining modal closing issues.","Final validation successful: All 242 tests passing, 3 skipped, with TypeScript compilation and ESLint checks successful.","Modal system refactor is now complete with all critical modal closing issues resolved across the entire codebase.","The new (modalId: string) => JSXElement pattern is now consistently implemented throughout the application.","Successfully continued modal refactoring task on July 4, 2025 - refactored 8 additional files to use specialized modal helpers.","Files refactored: DayMeals.tsx (openItemEditModal, openTemplateSearchModal), RecipeEditModal.tsx (openTemplateSearchModal, openItemEditModal, openDeleteConfirmModal), ItemEditModal.tsx (openItemEditModal, openRecipeEditModal, openTemplateSearchModal), RecipeEditView.tsx (openClearItemsConfirmModal), UnifiedRecipeEditView.tsx (openClearItemsConfirmModal), WeightView.tsx (openDeleteConfirmModal), MealEditView.tsx (openClearItemsConfirmModal, openDeleteConfirmModal), TemplateSearchResults.tsx (openDeleteConfirmModal).","Modal refactoring demonstrates successful elimination of code duplication: replaced openEditModal + JSX with openItemEditModal, openContentModal + TemplateSearchModal with openTemplateSearchModal, openConfirmModal + manual confirm text with openDeleteConfirmModal and openClearItemsConfirmModal.","Refactoring reduced code complexity: removed manual modalId management, eliminated duplicated confirmation patterns, standardized delete/clear modal messages and buttons.","All TypeScript compilation passing after refactoring - main application code correctly using specialized modal helpers.","Modal system test failures persist but are unrelated to refactoring - appear to be test isolation issues with modalManager state not resetting between tests.","Remaining files with open*Modal calls include: CopyLastDayButton.tsx, PreviousDayCard.tsx, ExternalEANInsertModal.tsx, EANSearch.tsx, MacroTargets.tsx, plus test files and routes/test-app.tsx.","Custom complex modals like MacroTargets profile restoration modal intentionally left unrefactored due to highly specific custom content and footer requirements.","Modal refactoring demonstrates clean architecture principles: specialized helpers abstract common patterns while preserving flexibility for edge cases.","Successfully completed Issue #935: RecentFood entity and related infrastructure cleanup on July 6, 2025.","Removed the unused RecentFood domain entity and RecentTemplate types that were no longer serving business purposes.","Eliminated the fetchUserRecentTemplates function that was redundant since fetchUserRecentFoods now returns Template[] directly.","Deleted the entire src/modules/recent-food/domain/ directory as it contained only unused RecentFood business entities.","Maintained necessary database infrastructure (RecentFoodRecord, RecentFoodInput types) for recent_foods table operations.","All TypeScript compilation, ESLint checks, and tests passing (286 passed, 3 skipped) after cleanup.","Successfully completed the entire recent food refactoring initiative (Issues #931-935) eliminating ~50+ lines of unnecessary domain code.","The codebase now uses Template objects directly throughout the recent food system, eliminating the intermediate RecentFood entity layer.","Core insight: Distinguished between unnecessary business domain entities vs necessary database infrastructure - removed only the business layer while preserving data access operations.","Successfully completed Issue #936: test(performance): validate refactored recent foods performance and cleanup on July 6, 2025.","Verified Recent tab functionality working correctly with enhanced performance.","Cleaned up TODO comment from recent-food application layer that was no longer needed.","Updated docs/audit_domain_recent-food.md to reflect the successful refactoring achievements.","Documented achieved performance metrics: ~60% reduction in database queries, faster response time for Recent tab, simplified architecture.","Validated all tests passing: 286 passed, 3 skipped with TypeScript compilation and ESLint checks successful.","Confirmed the entire recent food refactoring initiative (Issues #931-935) was successful with performance improvements as expected.","The refactoring successfully eliminated the unnecessary RecentFood domain entity and now uses Template objects directly throughout the system.","Enhanced database function search_recent_foods_with_names() returns complete Template objects directly, reducing network overhead and improving performance.","Fixed critical database function error in search_recent_foods_with_names on July 6, 2025.","Resolved 'structure of query does not match function result type' error by casting f.macros from json to jsonb.","The error was caused by foods.macros being json type but function return type declaring jsonb.","Added type casting (f.macros::jsonb) to both query branches in the database function.","Updated database README.md to document the type casting approach for future reference.","This fix ensures the recent food search functionality works correctly without breaking existing database schema.","All tests continue passing (286 passed, 3 skipped) after the database function fix."]} +{"type":"entity","entityType":"user","name":"marcuscastelo","observations":["User identified as marcuscastelo for this session.","Attempted to use mcp_memory_add_observations for session logging; error due to missing entity, so entity creation was proposed and now completed.","System prompt for agent memory management was reviewed and confirmed as a guide for using the memory tool.","All phases were tracked via issues #882 (epic), #883 (schema/domain), #887 (infra), #884 (app), #885 (UI), and PR #908 (UI migration in progress).","Barrel index.ts files are now banned in this codebase. See #file:copilot-instructions.md for rationale. All such files must be removed or left blank with a comment.","Successfully completed Issue #941: Error Detail Modal System migration to unified architecture on June 29, 2025.","Implemented feature-flagged compatibility bridge that maintains backward compatibility for openErrorModal, closeErrorModal, and getOpenModals APIs.","Updated all production imports to use new bridge while preserving legacy fallback functionality.","All tests passing (242 passed, 3 skipped) after migration with comprehensive validation completed.","Migration maintains complete backward compatibility and provides safe rollout/rollback mechanism via feature flags.","Successfully analyzed and resolved modal duplication issue in macroflows project on July 2, 2025.","Identified redundant modal architecture where TemplateSearchModal was creating its own modal structure inside the unified modal system.","Refactored TemplateSearchModal to be pure content component, removing legacy Modal wrapper structure.","Updated modal titles from English to Portuguese for better UX consistency.","All tests passing after modal architecture cleanup - validated with copilot:check script.","Fixed modal responsiveness issue in TemplateSearchModal on July 2, 2025.","Identified that fixed height `h-[60vh] sm:h-[80vh]` in TemplateSearchModal was causing mobile layout problems after modal system migration.","Changed height from fixed `h-[60vh] sm:h-[80vh]` to flexible `max-h-[60vh] sm:max-h-[70vh]` with `min-h-0` for better responsive behavior.","Adjusted TemplateSearchResults max-height from `max-h-[60vh]` to `max-h-[40vh] sm:max-h-[50vh]` to accommodate mobile viewport constraints.","All tests continue passing (244 passed, 3 skipped) after responsive fixes.","Issue was caused by modal structure change where TemplateSearchModal now renders directly inside UnifiedModalContainer instead of being wrapped in its own modal.","Fixed styles ensure modal content doesn't exceed screen bounds on mobile devices while maintaining proper desktop experience.","Successfully eliminated duplicated modal closing logic in ItemEditModal.tsx on July 2, 2025.","Identified 3 patterns of duplication in modal handlers: setChildEditModalVisible/setEditingChild (lines 390-395), setRecipeEditModalVisible (lines 406, 409, 412), and setAddItemModalVisible (lines 451, 453, 454).","Created helper functions closeChildEditModal(), closeRecipeEditModal(), and closeAddItemModal() to centralize modal closing logic.","Replaced duplicated inline modal closing calls in onApply, onClose, onCancel, onSaveRecipe, onDelete, onFinish, and onNewItem handlers.","Reduced code duplication by ~15 lines while maintaining identical functionality and improving maintainability.","All validation scripts passed (244 tests passed, 3 skipped) with TypeScript compilation, ESLint, and code quality checks successful.","Refactoring follows DRY principle and clean code practices without changing user-facing behavior.","Successfully fixed ConfirmModalContext migration blocking issue on July 3, 2025.","Migrated three recipe components (RecipeEditModal.tsx, RecipeEditView.tsx, UnifiedRecipeEditView.tsx) from deleted ConfirmModalContext to unified modal system.","Replaced useConfirmModalContext() pattern with openConfirmModal() helper from unified system.","All TypeScript compilation, ESLint checks, and tests now passing (244 passed, 3 skipped).","Modal migration plan Phase 3 can now proceed with ItemEditModal and RecipeEditModal migrations.","Unified modal system infrastructure confirmed working correctly with UnifiedModalProvider integrated.","Successfully completed RecipeEditModal.tsx migration to unified modal system on July 3, 2025.","Converted RecipeEditModal from legacy modal wrapper to pure content component, removing Modal, ModalContextProvider, and useModalContext dependencies.","Updated RecipeEditModalProps interface to remove show, onVisibilityChange props and add onClose prop for unified modal integration.","Replaced legacy modal structure (Modal.Header, Modal.Content, Modal.Footer) with direct div-based layout suitable for unified modal container.","Updated Actions component to use onClose callback instead of direct closeModal calls, ensuring proper modal lifecycle management.","Fixed ItemEditModal usage to pass onClose prop to RecipeEditModal component for seamless integration.","All tests passing (244 passed, 3 skipped) with TypeScript compilation, ESLint, and code quality checks successful.","RecipeEditModal migration represents completion of high-complexity modal in Phase 3 of modal migration plan.","Modal migration plan Phase 3 critical modals (ItemEditModal and RecipeEditModal) now fully migrated to unified system.","Successfully completed PHASE 4: Integration & Infrastructure of modal migration plan on July 3, 2025.","Updated TestModal in test-app.tsx to use unified modal system instead of legacy ModalContextProvider pattern.","Removed legacy context fallbacks from Modal component, making onClose and visible props required instead of optional.","Updated ModalHeader component to require explicit onClose prop, removing useModalContext dependency.","Fixed UnifiedModalContainer to handle conditional Modal.Header rendering based on showCloseButton setting.","Updated Modal component imports to remove unused Accessor and Setter types after legacy context removal.","All TypeScript compilation, ESLint checks, and tests passing (244 passed, 3 skipped) after legacy modal context removal.","Modal component now operates purely on explicit props without any legacy context fallbacks.","PHASE 4 Integration & Infrastructure completed: all modal patterns use unified system, legacy context fallbacks removed, UnifiedModalContainer fully integrated.","Successfully completed CLEANUP phase and entire modal migration plan on July 3, 2025.","Removed all legacy modal context files: ModalContext.tsx, legacyModalContextBridge.tsx, and associated test files.","Updated MODAL_MIGRATION_PLAN.md to reflect completed status with all phases marked as ACHIEVED.","Modal migration completed ahead of schedule: 3 days actual vs 4 weeks planned timeline.","Final validation: 242 tests passing, 3 skipped, with TypeScript compilation and ESLint checks successful.","Unified modal system now fully operational: openModal(), openEditModal(), openConfirmModal() patterns implemented throughout codebase.","Eliminated ~100+ lines of duplicated modal code while preserving all critical callbacks and user interaction flows.","No legacy modal dependencies remain - complete architectural migration achieved.","Documentation updated to serve as successful implementation reference for future projects.","Successfully fixed modal closing issues in macroflows project on July 3, 2025.","Identified and resolved 3 modal closing problems: TemplateSearchModal→ItemEditModal flow, MacroTargets 'Apagar e restaurar perfil antigo' modal, and EANInsertModal flow.","TemplateSearchModal ItemEditModal onClose was only calling props.onFinish() but not props.onClose(), leaving parent modal open.","MacroTargets modal opened via openContentModal but footer buttons (Cancelar, Apagar atual e restaurar antigo) weren't closing the modal after actions.","EANInsertModal in TemplateSearchModal had empty onClose callback and wasn't closing modal on template selection.","Solutions implemented: stored modal IDs from openContentModal calls, used closeModal from useUnifiedModal hook, ensured proper callback chaining.","All fixes maintain existing success/error handling and confirmation flows while ensuring modals close appropriately.","All validation scripts continue passing (242 tests passed, 3 skipped) with TypeScript compilation and ESLint checks successful.","Modal system now provides consistent UX where all modals close after their respective actions are completed.","Fixed critical modal closing issues by implementing proper modal ID storage on July 3, 2025.","Root cause identified: openEditModal and openConfirmModal calls weren't storing modal IDs, preventing proper modal closure.","TemplateSearchModal 'Aplicar' issue: Modified to store editModalId from openEditModal and close it after successful onApply completion.","TemplateSearchModal 'Finalizar' issue: Modified confirmation modal to store confirmModalId and close after both onConfirm and onCancel actions.","Macro overflow modal issue: Added overflowModalId storage and proper closeModal calls for both confirm and cancel scenarios.","Removed dependency on legacy closeAllModals() function, replaced with specific modal ID-based closing.","All modal operations now use proper async/await pattern with error handling that closes modals on both success and failure.","Solution ensures each modal is closed individually by its specific ID rather than global modal clearing.","All validation scripts continue passing (242 tests passed, 3 skipped) after implementing proper modal ID management.","Completed comprehensive modal ID storage audit across entire codebase on July 3, 2025.","Identified 8+ files with modal closing issues: DayMeals.tsx (2 openEditModal problems), ItemEditModal.tsx (1 problem), RecipeEditModal.tsx (2 problems), BottomNavigation.tsx, ConsoleDumpButton.tsx, RecipeEditView.tsx, MealEditView.tsx, WeightView.tsx.","Fixed DayMeals.tsx: handleEditItem and handleNewItemButton now store modal IDs and call closeModal after successful actions.","Fixed ItemEditModal.tsx: handleEditChild now stores childModalId and closes modal properly after onApply.","Started fixing RecipeEditModal.tsx by adding useUnifiedModal hook - remaining fixes needed for two openEditModal calls.","All corrected modals now follow proper pattern: store modalId from open calls, use closeModal(modalId) in onApply/onConfirm/onCancel callbacks.","Maintained async/await error handling while ensuring modals close on both success and failure scenarios.","Systematic approach identified modal closing issues by searching for openEditModal, openContentModal, openConfirmModal calls without ID storage.","All validation scripts continue passing (242 tests passed, 3 skipped) after implementing critical modal ID fixes.","Successfully completed final modal ID storage fixes in ItemEditModal.tsx and ExternalTemplateToItemModal.tsx on July 3, 2025.","Fixed ItemEditModal.tsx: Added recipeEditModalId and addItemModalId signals with helper functions closeRecipeEditModal() and closeAddItemModal().","Updated both openEditModal calls in ItemEditModal.tsx to store modal IDs and use closeModal(modalId) for proper modal closing.","Fixed ExternalTemplateToItemModal.tsx: Added useUnifiedModal hook and updated openEditModal call to store modal ID and use it in onClose callbacks.","All modal flows now properly store modal IDs and close modals after actions complete, eliminating the remaining modal closing issues.","Final validation successful: All 242 tests passing, 3 skipped, with TypeScript compilation and ESLint checks successful.","Modal system refactor is now complete with all critical modal closing issues resolved across the entire codebase.","The new (modalId: string) => JSXElement pattern is now consistently implemented throughout the application.","Successfully continued modal refactoring task on July 4, 2025 - refactored 8 additional files to use specialized modal helpers.","Files refactored: DayMeals.tsx (openItemEditModal, openTemplateSearchModal), RecipeEditModal.tsx (openTemplateSearchModal, openItemEditModal, openDeleteConfirmModal), ItemEditModal.tsx (openItemEditModal, openRecipeEditModal, openTemplateSearchModal), RecipeEditView.tsx (openClearItemsConfirmModal), UnifiedRecipeEditView.tsx (openClearItemsConfirmModal), WeightView.tsx (openDeleteConfirmModal), MealEditView.tsx (openClearItemsConfirmModal, openDeleteConfirmModal), TemplateSearchResults.tsx (openDeleteConfirmModal).","Modal refactoring demonstrates successful elimination of code duplication: replaced openEditModal + JSX with openItemEditModal, openContentModal + TemplateSearchModal with openTemplateSearchModal, openConfirmModal + manual confirm text with openDeleteConfirmModal and openClearItemsConfirmModal.","Refactoring reduced code complexity: removed manual modalId management, eliminated duplicated confirmation patterns, standardized delete/clear modal messages and buttons.","All TypeScript compilation passing after refactoring - main application code correctly using specialized modal helpers.","Modal system test failures persist but are unrelated to refactoring - appear to be test isolation issues with modalManager state not resetting between tests.","Remaining files with open*Modal calls include: CopyLastDayButton.tsx, PreviousDayCard.tsx, ExternalEANInsertModal.tsx, EANSearch.tsx, MacroTargets.tsx, plus test files and routes/test-app.tsx.","Custom complex modals like MacroTargets profile restoration modal intentionally left unrefactored due to highly specific custom content and footer requirements.","Modal refactoring demonstrates clean architecture principles: specialized helpers abstract common patterns while preserving flexibility for edge cases.","Successfully completed Issue #935: RecentFood entity and related infrastructure cleanup on July 6, 2025.","Removed the unused RecentFood domain entity and RecentTemplate types that were no longer serving business purposes.","Eliminated the fetchUserRecentTemplates function that was redundant since fetchUserRecentFoods now returns Template[] directly.","Deleted the entire src/modules/diet/recent-food/domain/ directory as it contained only unused RecentFood business entities.","Maintained necessary database infrastructure (RecentFoodRecord, RecentFoodInput types) for recent_foods table operations.","All TypeScript compilation, ESLint checks, and tests passing (286 passed, 3 skipped) after cleanup.","Successfully completed the entire recent food refactoring initiative (Issues #931-935) eliminating ~50+ lines of unnecessary domain code.","The codebase now uses Template objects directly throughout the recent food system, eliminating the intermediate RecentFood entity layer.","Core insight: Distinguished between unnecessary business domain entities vs necessary database infrastructure - removed only the business layer while preserving data access operations.","Successfully completed Issue #936: test(performance): validate refactored recent foods performance and cleanup on July 6, 2025.","Verified Recent tab functionality working correctly with enhanced performance.","Cleaned up TODO comment from recent-food application layer that was no longer needed.","Updated docs/audit_domain_recent-food.md to reflect the successful refactoring achievements.","Documented achieved performance metrics: ~60% reduction in database queries, faster response time for Recent tab, simplified architecture.","Validated all tests passing: 286 passed, 3 skipped with TypeScript compilation and ESLint checks successful.","Confirmed the entire recent food refactoring initiative (Issues #931-935) was successful with performance improvements as expected.","The refactoring successfully eliminated the unnecessary RecentFood domain entity and now uses Template objects directly throughout the system.","Enhanced database function search_recent_foods_with_names() returns complete Template objects directly, reducing network overhead and improving performance.","Fixed critical database function error in search_recent_foods_with_names on July 6, 2025.","Resolved 'structure of query does not match function result type' error by casting f.macros from json to jsonb.","The error was caused by foods.macros being json type but function return type declaring jsonb.","Added type casting (f.macros::jsonb) to both query branches in the database function.","Updated database README.md to document the type casting approach for future reference.","This fix ensures the recent food search functionality works correctly without breaking existing database schema.","All tests continue passing (286 passed, 3 skipped) after the database function fix."]} {"type":"entity","entityType":"epic","name":"Item/ItemGroup Unification for Recursive Recipes","observations":["Unify Item and ItemGroup entities into a single hierarchical structure to support recursive recipes and eliminate semantic contradictions.","Strategic goals: prepare for recursive recipes, eliminate semantic contradictions, reduce technical debt, simplify architecture.","Proposed solution: Item type with recursive reference supporting food, recipe, and group.","Success criteria: single entity supports all behaviors, recursive recipes via config, no breaking changes, performance maintained, 100% test coverage.","Risk mitigation: feature flags, comprehensive testing, rollback plan, performance monitoring.","Session on June 27, 2025: User requested to migrate Recipe entity and all related flows from legacy Item[] to Item[] in-memory, while maintaining Item[] (food only) for database compatibility. Migration included auditing, refactoring, conversion utilities, persistence adaptation, test updates, and documentation.","Item type introduced: recursive reference for food, recipe, group; enables recursive recipes and eliminates semantic contradictions.","Migration strategy included: feature flags, comprehensive testing, rollback plans, and performance monitoring.","Each phase included detailed acceptance criteria, risk mitigation, and documentation updates.","EPIC-882: This epic will be suspended for v0.13.0 and resumed as soon as v0.14.0 starts being the rc/ branch."]} {"type":"entity","entityType":"phase","name":"#883 Phase 1: Schema & Domain Layer","observations":["Refactor domain and schema layers to unify Item and ItemGroup into Item.","Scope: domain logic, schema definitions, migration utilities, macro calculation, hierarchy utilities, conversion utilities, unit and performance tests.","Motivation: eliminate contradictions, technical debt, enable recursive recipes, simplify architecture.","Acceptance: Item schema, new domain ops, migration utilities, feature flag, 100% test coverage, performance, docs.","Dependencies: current Item/ItemGroup schemas, MacroNutrients schema, existing domain ops."]} {"type":"entity","entityType":"phase","name":"#887 Phase 2: Infrastructure Layer","observations":["Refactor infrastructure layer for unified Item/ItemGroup structure: DB schema, repos, migration utilities, rollback, query optimization, validation, backup/restore, monitoring.","Motivation: enable recursive recipes, eliminate technical debt, enable future app/UI migrations.","Acceptance: unified schema, data migration, queries work, performance, rollback, tests, migration scripts, benchmarks, docs.","Dependencies: Phase 1 (domain), blocks Phase 3 (app), Phase 4 (UI)."]} @@ -25,4 +25,4 @@ {"type":"relation","from":"marcuscastelo","relationType":"implemented","to":"Issue #851"} {"type":"relation","from":"marcuscastelo","to":"macroflows","relationType":"owns"} {"type":"relation","from":"marcuscastelo","to":"modal-system-refactor","relationType":"completed"} -{"type":"relation","from":"modal-system-refactor","to":"macroflows","relationType":"belongs to"} \ No newline at end of file +{"type":"relation","from":"modal-system-refactor","to":"macroflows","relationType":"belongs to"} diff --git a/src/modules/recent-food/application/services/recentFoodCrudService.ts b/src/modules/diet/recent-food/application/services/recentFoodCrudService.ts similarity index 91% rename from src/modules/recent-food/application/services/recentFoodCrudService.ts rename to src/modules/diet/recent-food/application/services/recentFoodCrudService.ts index e24d1a33..aeabeee9 100644 --- a/src/modules/recent-food/application/services/recentFoodCrudService.ts +++ b/src/modules/diet/recent-food/application/services/recentFoodCrudService.ts @@ -1,9 +1,9 @@ -import type { Template } from '~/modules/diet/template/domain/template' import { type NewRecentFood, type RecentFood, -} from '~/modules/recent-food/domain/recentFood' -import { type RecentFoodRepository } from '~/modules/recent-food/domain/recentFoodRepository' +} from '~/modules/diet/recent-food/domain/recentFood' +import { type RecentFoodRepository } from '~/modules/diet/recent-food/domain/recentFoodRepository' +import type { Template } from '~/modules/diet/template/domain/template' import { type User } from '~/modules/user/domain/user' import env from '~/shared/config/env' diff --git a/src/modules/recent-food/application/usecases/deps.ts b/src/modules/diet/recent-food/application/usecases/deps.ts similarity index 63% rename from src/modules/recent-food/application/usecases/deps.ts rename to src/modules/diet/recent-food/application/usecases/deps.ts index b1fca564..3cf1cdfa 100644 --- a/src/modules/recent-food/application/usecases/deps.ts +++ b/src/modules/diet/recent-food/application/usecases/deps.ts @@ -1,9 +1,9 @@ import { createRoot } from 'solid-js' -import { createRecentFoodCrudService } from '~/modules/recent-food/application/services/recentFoodCrudService' -import { createRecentFoodRepository } from '~/modules/recent-food/infrastructure/recentFoodRepository' -import { initializeRecentFoodRealtime } from '~/modules/recent-food/infrastructure/supabase/realtime' -import { createSupabaseRecentFoodGateway } from '~/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway' +import { createRecentFoodCrudService } from '~/modules/diet/recent-food/application/services/recentFoodCrudService' +import { createRecentFoodRepository } from '~/modules/diet/recent-food/infrastructure/recentFoodRepository' +import { initializeRecentFoodRealtime } from '~/modules/diet/recent-food/infrastructure/supabase/realtime' +import { createSupabaseRecentFoodGateway } from '~/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodGateway' // Centralized dependency wiring for recent-food use-cases. // This file performs the minimal initialization (createRoot) once and diff --git a/src/modules/recent-food/application/usecases/extractRecentFoodReference.ts b/src/modules/diet/recent-food/application/usecases/extractRecentFoodReference.ts similarity index 95% rename from src/modules/recent-food/application/usecases/extractRecentFoodReference.ts rename to src/modules/diet/recent-food/application/usecases/extractRecentFoodReference.ts index dc9c6901..614b905b 100644 --- a/src/modules/recent-food/application/usecases/extractRecentFoodReference.ts +++ b/src/modules/diet/recent-food/application/usecases/extractRecentFoodReference.ts @@ -4,9 +4,9 @@ import { isRecipeItem, type Item, } from '~/modules/diet/item/schema/itemSchema' +import { type RecentFood } from '~/modules/diet/recent-food/domain/recentFood' import { templateToItem } from '~/modules/diet/template/application/templateToItem' import { type Template } from '~/modules/diet/template/domain/template' -import { type RecentFood } from '~/modules/recent-food/domain/recentFood' /** * Result of extracting recent food reference from an item. diff --git a/src/modules/recent-food/application/usecases/recentFoodUseCases.ts b/src/modules/diet/recent-food/application/usecases/recentFoodUseCases.ts similarity index 88% rename from src/modules/recent-food/application/usecases/recentFoodUseCases.ts rename to src/modules/diet/recent-food/application/usecases/recentFoodUseCases.ts index 8f3b8d27..76f0f892 100644 --- a/src/modules/recent-food/application/usecases/recentFoodUseCases.ts +++ b/src/modules/diet/recent-food/application/usecases/recentFoodUseCases.ts @@ -1,17 +1,17 @@ import { authUseCases } from '~/modules/auth/application/usecases/authUseCases' import { type Item } from '~/modules/diet/item/schema/itemSchema' -import { type Template } from '~/modules/diet/template/domain/template' -import { recentFoodCrudService } from '~/modules/recent-food/application/usecases/deps' +import { recentFoodCrudService } from '~/modules/diet/recent-food/application/usecases/deps' import { extractRecentFoodReferenceFromTemplate, type RecentFoodReference, -} from '~/modules/recent-food/application/usecases/extractRecentFoodReference' -import { touchRecentFood } from '~/modules/recent-food/application/usecases/touchRecentFood' -import { touchRecentFoodForItem } from '~/modules/recent-food/application/usecases/touchRecentFoodForItem' +} from '~/modules/diet/recent-food/application/usecases/extractRecentFoodReference' +import { touchRecentFood } from '~/modules/diet/recent-food/application/usecases/touchRecentFood' +import { touchRecentFoodForItem } from '~/modules/diet/recent-food/application/usecases/touchRecentFoodForItem' import { type NewRecentFood, type RecentFood, -} from '~/modules/recent-food/domain/recentFood' +} from '~/modules/diet/recent-food/domain/recentFood' +import { type Template } from '~/modules/diet/template/domain/template' import { showError, showPromise, diff --git a/src/modules/recent-food/application/usecases/tests/extractRecentFoodReference.test.ts b/src/modules/diet/recent-food/application/usecases/tests/extractRecentFoodReference.test.ts similarity index 98% rename from src/modules/recent-food/application/usecases/tests/extractRecentFoodReference.test.ts rename to src/modules/diet/recent-food/application/usecases/tests/extractRecentFoodReference.test.ts index 0cd69b19..a3c3d45d 100644 --- a/src/modules/recent-food/application/usecases/tests/extractRecentFoodReference.test.ts +++ b/src/modules/diet/recent-food/application/usecases/tests/extractRecentFoodReference.test.ts @@ -13,7 +13,7 @@ import { type RecipeItem, } from '~/modules/diet/item/schema/itemSchema' import { createMacroNutrients } from '~/modules/diet/macro-nutrients/domain/macroNutrients' -import { extractRecentFoodReferenceFromItem } from '~/modules/recent-food/application/usecases/extractRecentFoodReference' +import { extractRecentFoodReferenceFromItem } from '~/modules/diet/recent-food/application/usecases/extractRecentFoodReference' describe('extractRecentFoodReferenceFromItem', () => { const mockMacros = createMacroNutrients({ diff --git a/src/modules/recent-food/application/usecases/touchRecentFood.ts b/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts similarity index 84% rename from src/modules/recent-food/application/usecases/touchRecentFood.ts rename to src/modules/diet/recent-food/application/usecases/touchRecentFood.ts index 38664535..4c381034 100644 --- a/src/modules/recent-food/application/usecases/touchRecentFood.ts +++ b/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts @@ -1,10 +1,10 @@ import { authUseCases } from '~/modules/auth/application/usecases/authUseCases' -import { recentFoodCrudService } from '~/modules/recent-food/application/usecases/deps' -import type { RecentFoodReference } from '~/modules/recent-food/application/usecases/extractRecentFoodReference' +import { recentFoodCrudService } from '~/modules/diet/recent-food/application/usecases/deps' +import type { RecentFoodReference } from '~/modules/diet/recent-food/application/usecases/extractRecentFoodReference' import { createNewRecentFood, type NewRecentFood, -} from '~/modules/recent-food/domain/recentFood' +} from '~/modules/diet/recent-food/domain/recentFood' export async function touchRecentFood(recentFoodRef: RecentFoodReference) { const currentRecentFood = diff --git a/src/modules/recent-food/application/usecases/touchRecentFoodForItem.ts b/src/modules/diet/recent-food/application/usecases/touchRecentFoodForItem.ts similarity index 75% rename from src/modules/recent-food/application/usecases/touchRecentFoodForItem.ts rename to src/modules/diet/recent-food/application/usecases/touchRecentFoodForItem.ts index 9803eecb..8fc70b33 100644 --- a/src/modules/recent-food/application/usecases/touchRecentFoodForItem.ts +++ b/src/modules/diet/recent-food/application/usecases/touchRecentFoodForItem.ts @@ -1,6 +1,6 @@ import type { Item } from '~/modules/diet/item/schema/itemSchema' -import { extractRecentFoodReferenceFromItem } from '~/modules/recent-food/application/usecases/extractRecentFoodReference' -import { touchRecentFood } from '~/modules/recent-food/application/usecases/touchRecentFood' +import { extractRecentFoodReferenceFromItem } from '~/modules/diet/recent-food/application/usecases/extractRecentFoodReference' +import { touchRecentFood } from '~/modules/diet/recent-food/application/usecases/touchRecentFood' import { showError } from '~/modules/toast/application/toastManager' import { logging } from '~/shared/utils/logging' diff --git a/src/modules/recent-food/domain/recentFood.ts b/src/modules/diet/recent-food/domain/recentFood.ts similarity index 100% rename from src/modules/recent-food/domain/recentFood.ts rename to src/modules/diet/recent-food/domain/recentFood.ts diff --git a/src/modules/recent-food/domain/recentFoodRepository.ts b/src/modules/diet/recent-food/domain/recentFoodRepository.ts similarity index 93% rename from src/modules/recent-food/domain/recentFoodRepository.ts rename to src/modules/diet/recent-food/domain/recentFoodRepository.ts index c75a3428..cd79d63e 100644 --- a/src/modules/recent-food/domain/recentFoodRepository.ts +++ b/src/modules/diet/recent-food/domain/recentFoodRepository.ts @@ -1,8 +1,8 @@ -import type { Template } from '~/modules/diet/template/domain/template' import { type NewRecentFood, type RecentFood, -} from '~/modules/recent-food/domain/recentFood' +} from '~/modules/diet/recent-food/domain/recentFood' +import type { Template } from '~/modules/diet/template/domain/template' import { type User } from '~/modules/user/domain/user' export type RecentFoodRepository = { diff --git a/src/modules/recent-food/domain/tests/recentFood.test.ts b/src/modules/diet/recent-food/domain/tests/recentFood.test.ts similarity index 97% rename from src/modules/recent-food/domain/tests/recentFood.test.ts rename to src/modules/diet/recent-food/domain/tests/recentFood.test.ts index 5e1129ee..cadcdee7 100644 --- a/src/modules/recent-food/domain/tests/recentFood.test.ts +++ b/src/modules/diet/recent-food/domain/tests/recentFood.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { createNewRecentFood } from '~/modules/recent-food/domain/recentFood' +import { createNewRecentFood } from '~/modules/diet/recent-food/domain/recentFood' describe('Recent Food Domain', () => { describe('createRecentFoodInput', () => { diff --git a/src/modules/recent-food/infrastructure/recentFoodRepository.ts b/src/modules/diet/recent-food/infrastructure/recentFoodRepository.ts similarity index 88% rename from src/modules/recent-food/infrastructure/recentFoodRepository.ts rename to src/modules/diet/recent-food/infrastructure/recentFoodRepository.ts index 71b57a5f..66ed4307 100644 --- a/src/modules/recent-food/infrastructure/recentFoodRepository.ts +++ b/src/modules/diet/recent-food/infrastructure/recentFoodRepository.ts @@ -1,10 +1,10 @@ -import type { Template } from '~/modules/diet/template/domain/template' import { type NewRecentFood, type RecentFood, -} from '~/modules/recent-food/domain/recentFood' -import { type RecentFoodRepository } from '~/modules/recent-food/domain/recentFoodRepository' -import { type RecentFoodGateway } from '~/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway' +} from '~/modules/diet/recent-food/domain/recentFood' +import { type RecentFoodRepository } from '~/modules/diet/recent-food/domain/recentFoodRepository' +import { type RecentFoodGateway } from '~/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodGateway' +import type { Template } from '~/modules/diet/template/domain/template' import { type User } from '~/modules/user/domain/user' import { logging } from '~/shared/utils/logging' diff --git a/src/modules/recent-food/infrastructure/supabase/constants.ts b/src/modules/diet/recent-food/infrastructure/supabase/constants.ts similarity index 100% rename from src/modules/recent-food/infrastructure/supabase/constants.ts rename to src/modules/diet/recent-food/infrastructure/supabase/constants.ts diff --git a/src/modules/recent-food/infrastructure/supabase/realtime.ts b/src/modules/diet/recent-food/infrastructure/supabase/realtime.ts similarity index 87% rename from src/modules/recent-food/infrastructure/supabase/realtime.ts rename to src/modules/diet/recent-food/infrastructure/supabase/realtime.ts index 0c35d6e0..bd5cb42e 100644 --- a/src/modules/recent-food/infrastructure/supabase/realtime.ts +++ b/src/modules/diet/recent-food/infrastructure/supabase/realtime.ts @@ -1,8 +1,8 @@ import { type RecentFood, recentFoodSchema, -} from '~/modules/recent-food/domain/recentFood' -import { SUPABASE_TABLE_RECENT_FOODS } from '~/modules/recent-food/infrastructure/supabase/constants' +} from '~/modules/diet/recent-food/domain/recentFood' +import { SUPABASE_TABLE_RECENT_FOODS } from '~/modules/diet/recent-food/infrastructure/supabase/constants' import { registerSubapabaseRealtimeCallback } from '~/shared/supabase/supabase' import { logging } from '~/shared/utils/logging' diff --git a/src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts b/src/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts similarity index 95% rename from src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts rename to src/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts index 03ef8267..baf745e6 100644 --- a/src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts +++ b/src/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts @@ -3,14 +3,14 @@ import { z } from 'zod/v4' import { foodSchema } from '~/modules/diet/food/domain/food' import { supabaseItemMapper } from '~/modules/diet/item/infrastructure/supabase/supabaseItemMapper' import { supabaseMacroNutrientsMapper } from '~/modules/diet/macro-nutrients/infrastructure/supabase/supabaseMacroNutrientsMapper' -import { recipeSchema } from '~/modules/diet/recipe/domain/recipe' -import { type Template } from '~/modules/diet/template/domain/template' import { type NewRecentFood, type RecentFood, -} from '~/modules/recent-food/domain/recentFood' -import { SUPABASE_TABLE_RECENT_FOODS } from '~/modules/recent-food/infrastructure/supabase/constants' -import { supabaseRecentFoodMapper } from '~/modules/recent-food/infrastructure/supabase/supabaseRecentFoodMapper' +} from '~/modules/diet/recent-food/domain/recentFood' +import { SUPABASE_TABLE_RECENT_FOODS } from '~/modules/diet/recent-food/infrastructure/supabase/constants' +import { supabaseRecentFoodMapper } from '~/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodMapper' +import { recipeSchema } from '~/modules/diet/recipe/domain/recipe' +import { type Template } from '~/modules/diet/template/domain/template' import { type User } from '~/modules/user/domain/user' import { type Json } from '~/shared/supabase/database.types' import { supabase } from '~/shared/supabase/supabase' diff --git a/src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodMapper.ts b/src/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodMapper.ts similarity index 96% rename from src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodMapper.ts rename to src/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodMapper.ts index cfd5867c..903e199f 100644 --- a/src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodMapper.ts +++ b/src/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodMapper.ts @@ -2,7 +2,7 @@ import { type NewRecentFood, type RecentFood, recentFoodSchema, -} from '~/modules/recent-food/domain/recentFood' +} from '~/modules/diet/recent-food/domain/recentFood' import { type Database } from '~/shared/supabase/database.types' import { parseWithStack } from '~/shared/utils/parseWithStack' diff --git a/src/modules/recent-food/ui/RemoveFromRecentButton.tsx b/src/modules/diet/recent-food/ui/RemoveFromRecentButton.tsx similarity index 90% rename from src/modules/recent-food/ui/RemoveFromRecentButton.tsx rename to src/modules/diet/recent-food/ui/RemoveFromRecentButton.tsx index f6c1fd27..a36020fa 100644 --- a/src/modules/recent-food/ui/RemoveFromRecentButton.tsx +++ b/src/modules/diet/recent-food/ui/RemoveFromRecentButton.tsx @@ -1,7 +1,7 @@ import { Show } from 'solid-js' +import { recentFoodUseCases } from '~/modules/diet/recent-food/application/usecases/recentFoodUseCases' import { type Template } from '~/modules/diet/template/domain/template' -import { recentFoodUseCases } from '~/modules/recent-food/application/usecases/recentFoodUseCases' import { debouncedTab } from '~/modules/template-search/application/usecases/templateSearchState' import { TrashIcon } from '~/sections/common/components/icons/TrashIcon' diff --git a/src/modules/recent-food/ui/tests/RemoveFromRecentButton.test.tsx b/src/modules/diet/recent-food/ui/tests/RemoveFromRecentButton.test.tsx similarity index 97% rename from src/modules/recent-food/ui/tests/RemoveFromRecentButton.test.tsx rename to src/modules/diet/recent-food/ui/tests/RemoveFromRecentButton.test.tsx index 1e298a51..493a74e0 100644 --- a/src/modules/recent-food/ui/tests/RemoveFromRecentButton.test.tsx +++ b/src/modules/diet/recent-food/ui/tests/RemoveFromRecentButton.test.tsx @@ -18,9 +18,12 @@ import { } from '~/modules/diet/template/domain/template' // Mock the modules -vi.mock('~/modules/recent-food/application/usecases/recentFoodCrud', () => ({ - deleteRecentFoodByReference: vi.fn(), -})) +vi.mock( + '~/modules/diet/recent-food/application/usecases/recentFoodCrud', + () => ({ + deleteRecentFoodByReference: vi.fn(), + }), +) vi.mock( '~/modules/template-search/application/usecases/templateSearchState', diff --git a/src/modules/template-search/application/usecases/templateSearchState.ts b/src/modules/template-search/application/usecases/templateSearchState.ts index 8f10d654..c2d3f012 100644 --- a/src/modules/template-search/application/usecases/templateSearchState.ts +++ b/src/modules/template-search/application/usecases/templateSearchState.ts @@ -5,11 +5,11 @@ import { fetchFoods, fetchFoodsByName, } from '~/modules/diet/food/application/usecases/foodCrud' +import { recentFoodUseCases } from '~/modules/diet/recent-food/application/usecases/recentFoodUseCases' import { fetchUserRecipeByName, fetchUserRecipes, } from '~/modules/diet/recipe/application/usecases/recipeCrud' -import { recentFoodUseCases } from '~/modules/recent-food/application/usecases/recentFoodUseCases' import { fetchTemplatesByTabLogic } from '~/modules/template-search/application/templateSearchLogic' import { userUseCases } from '~/modules/user/application/usecases/userUseCases' import { type TemplateSearchTab } from '~/sections/search/components/TemplateSearchTabs' diff --git a/src/sections/search/components/TemplateSearchModal.tsx b/src/sections/search/components/TemplateSearchModal.tsx index a8d2ada2..c4a8b1e2 100644 --- a/src/sections/search/components/TemplateSearchModal.tsx +++ b/src/sections/search/components/TemplateSearchModal.tsx @@ -2,6 +2,7 @@ import { onMount, Suspense } from 'solid-js' import { type Item } from '~/modules/diet/item/schema/itemSchema' import { isOverflow } from '~/modules/diet/macro-nutrients/application/macroOverflow' +import { recentFoodUseCases } from '~/modules/diet/recent-food/application/usecases/recentFoodUseCases' import { getRecipePreparedQuantity } from '~/modules/diet/recipe/domain/recipeOperations' import { createItemFromTemplate } from '~/modules/diet/template/application/createGroupFromTemplate' import { @@ -11,7 +12,6 @@ import { import { type Template } from '~/modules/diet/template/domain/template' import { isTemplateRecipe } from '~/modules/diet/template/domain/template' import { type TemplateItem } from '~/modules/diet/template-item/domain/templateItem' -import { recentFoodUseCases } from '~/modules/recent-food/application/usecases/recentFoodUseCases' import { debouncedSearch, refetchTemplates, diff --git a/src/sections/search/components/TemplateSearchResultItem.tsx b/src/sections/search/components/TemplateSearchResultItem.tsx index 3a457cd6..d9138a8a 100644 --- a/src/sections/search/components/TemplateSearchResultItem.tsx +++ b/src/sections/search/components/TemplateSearchResultItem.tsx @@ -1,3 +1,4 @@ +import { RemoveFromRecentButton } from '~/modules/diet/recent-food/ui/RemoveFromRecentButton' import { deleteRecipe } from '~/modules/diet/recipe/application/usecases/recipeCrud' import { getRecipePreparedQuantity } from '~/modules/diet/recipe/domain/recipeOperations' import { templateToItem } from '~/modules/diet/template/application/templateToItem' @@ -6,7 +7,6 @@ import { isTemplateRecipe, type Template, } from '~/modules/diet/template/domain/template' -import { RemoveFromRecentButton } from '~/modules/recent-food/ui/RemoveFromRecentButton' import { ItemView } from '~/sections/item/components/ItemView' import { ItemFavorite } from '~/sections/item/components/UnifiedItemFavorite' import { openDeleteConfirmModal } from '~/shared/modal/ui/DeleteConfirmModal' From 4cf99ec5174d322c0847471050a13c16c6ca754e Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 20:08:56 -0300 Subject: [PATCH 12/23] feat(biome): add initial VCS configuration for Git --- biome.jsonc | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 biome.jsonc diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 00000000..19090646 --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,7 @@ +{ + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + } +} From 52bcecb5f76583e936c66846599c463885a1c8e5 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 20:19:23 -0300 Subject: [PATCH 13/23] feat(biome): enhance linter configuration with detailed rules and overrides --- biome.jsonc | 345 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 344 insertions(+), 1 deletion(-) diff --git a/biome.jsonc b/biome.jsonc index 19090646..fee2393f 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -3,5 +3,348 @@ "enabled": true, "clientKind": "git", "useIgnoreFile": true - } + }, + "linter": { + "rules": { + "recommended": false + }, + "includes": [ + "**", + "!node_modules", + "!src/sections/datepicker", + "!.vercel", + "!.vinxi", + "!dist", + "!build", + "!coverage", + "!public", + "!out", + "!.output" + ] + }, + "overrides": [ + { + "includes": [ + "**/*.ts", + "**/*.tsx" + ], + "javascript": { + "globals": [ + "onanimationend", + "ongamepadconnected", + "onlostpointercapture", + "onanimationiteration", + "onkeyup", + "onmousedown", + "onanimationstart", + "onslotchange", + "onprogress", + "ontransitionstart", + "onpause", + "onended", + "onpointerover", + "onscrollend", + "onformdata", + "ontransitionrun", + "onanimationcancel", + "ondrag", + "onchange", + "onbeforeinstallprompt", + "onbeforexrselect", + "onmessage", + "ontransitioncancel", + "onpointerdown", + "onabort", + "onpointerout", + "oncuechange", + "ongotpointercapture", + "onscrollsnapchanging", + "onsearch", + "onsubmit", + "onstalled", + "onsuspend", + "onreset", + "ApexCharts", + "onmouseenter", + "ongamepaddisconnected", + "onerror", + "onresize", + "onbeforetoggle", + "onmouseover", + "ondragover", + "onpagehide", + "onmousemove", + "onratechange", + "oncommand", + "process", + "onmessageerror", + "onwheel", + "ondevicemotion", + "onauxclick", + "ontransitionend", + "onpaste", + "onpageswap", + "ononline", + "ondeviceorientationabsolute", + "onkeydown", + "onclose", + "onselect", + "onpageshow", + "onpointercancel", + "onbeforematch", + "onpointerrawupdate", + "ondragleave", + "onscrollsnapchange", + "onseeked", + "onwaiting", + "onbeforeunload", + "onplaying", + "onvolumechange", + "ondragend", + "onstorage", + "onloadeddata", + "onfocus", + "onoffline", + "onplay", + "onafterprint", + "onclick", + "oncut", + "onmouseout", + "ondblclick", + "oncanplay", + "onloadstart", + "onappinstalled", + "onpointermove", + "ontoggle", + "oncontextmenu", + "NodeJS", + "onblur", + "oncancel", + "onbeforeprint", + "oncontextrestored", + "onloadedmetadata", + "onpointerup", + "onlanguagechange", + "oncopy", + "onselectstart", + "onscroll", + "onload", + "ondragstart", + "onbeforeinput", + "oncanplaythrough", + "oninput", + "oninvalid", + "ontimeupdate", + "ondurationchange", + "onselectionchange", + "onmouseup", + "location", + "onkeypress", + "onpointerleave", + "oncontextlost", + "ondrop", + "onsecuritypolicyviolation", + "oncontentvisibilityautostatechange", + "ondeviceorientation", + "onseeking", + "onrejectionhandled", + "onunload", + "onmouseleave", + "onhashchange", + "onpointerenter", + "onmousewheel", + "onunhandledrejection", + "ondragenter", + "onpopstate", + "onpagereveal", + "onemptied" + ] + }, + "linter": { + "rules": { + "a11y": { + "noAriaUnsupportedElements": "warn", + "useAltText": "warn", + "useAriaPropsForRole": "warn", + "useAriaPropsSupportedByRole": "warn", + "useValidAriaProps": "warn", + "useValidAriaValues": "warn" + }, + "complexity": { + "noAdjacentSpacesInRegex": "error", + "noExtraBooleanCast": "error", + "noUselessCatch": "error", + "noUselessEscapeInRegex": "error", + "noUselessThisAlias": "error", + "noUselessTypeConstraint": "error" + }, + "correctness": { + "noConstAssign": "error", + "noConstantCondition": "error", + "noEmptyCharacterClassInRegex": "error", + "noEmptyPattern": "error", + "noGlobalObjectCalls": "error", + "noInvalidBuiltinInstantiation": "error", + "noInvalidConstructorSuper": "error", + "noNonoctalDecimalEscape": "error", + "noPrecisionLoss": "error", + "noSelfAssign": "error", + "noSetterReturn": "error", + "noSolidDestructuredProps": "error", + "noSwitchDeclarations": "error", + "noUndeclaredVariables": "error", + "noUnreachable": "error", + "noUnreachableSuper": "error", + "noUnsafeFinally": "error", + "noUnsafeOptionalChaining": "error", + "noUnusedLabels": "error", + "noUnusedPrivateClassMembers": "error", + "noUnusedVariables": "error", + "useIsNan": "error", + "useValidForDirection": "error", + "useValidTypeof": "error", + "useYield": "error" + }, + "performance": { + "useSolidForComponent": "error" + }, + "style": { + "noCommonJs": "error", + "noNamespace": "error", + "noRestrictedImports": "error", + "useArrayLiterals": "error", + "useAsConstAssertion": "error", + "useConsistentObjectDefinitions": "warn", + "useConsistentTypeDefinitions": "error", + "useImportType": { + "level": "error", + "options": { + "style": "inlineType" + } + }, + "useThrowOnlyError": "error" + }, + "suspicious": { + "noAssignInExpressions": "error", + "noAsyncPromiseExecutor": "error", + "noCatchAssign": "error", + "noClassAssign": "error", + "noCompareNegZero": "error", + "noConsole": "off", + "noConstantBinaryExpressions": "error", + "noControlCharactersInRegex": "error", + "noDebugger": "error", + "noDoubleEquals": "error", + "noDuplicateCase": "error", + "noDuplicateClassMembers": "error", + "noDuplicateElseIf": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noEmptyBlockStatements": "error", + "noExplicitAny": "error", + "noExtraNonNullAssertion": "error", + "noFallthroughSwitchClause": "error", + "noFunctionAssign": "error", + "noGlobalAssign": "error", + "noImportAssign": "error", + "noIrregularWhitespace": "error", + "noMisleadingCharacterClass": "error", + "noMisleadingInstantiator": "error", + "noNonNullAssertedOptionalChain": "error", + "noPrototypeBuiltins": "error", + "noReactSpecificProps": "warn", + "noRedeclare": "error", + "noShadowRestrictedNames": "error", + "noSparseArray": "error", + "noTsIgnore": "error", + "noUnsafeDeclarationMerging": "error", + "noUnsafeNegation": "error", + "noUselessRegexBackrefs": "error", + "noWith": "error", + "useAwait": "off", + "useGetterReturn": "error", + "useNamespaceKeyword": "error" + } + } + } + }, + { + "includes": [ + "src/shared/error/**/*.ts", + "src/shared/error/**/*.tsx", + "src/modules/observability/**/*.ts", + "src/modules/observability/**/*.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "vitest.setup.ts" + ], + "linter": { + "rules": { + "suspicious": { + "noConsole": "off" + } + } + } + }, + { + "includes": [ + "**/infrastructure/**/*.ts", + "**/infrastructure/**/*.tsx", + "src/shared/utils/supabase.ts", + "src/shared/console/**/*.ts", + "src/shared/hooks/**/*.ts", + "src/shared/utils/**/*.ts", + "src/sections/**/*.tsx", + "vitest.setup.ts" + ], + "linter": { + "rules": { + "style": { + "noRestrictedImports": "error" + } + } + } + }, + { + "includes": [ + ".eslintrc.js", + ".eslintrc.cjs", + "eslint.config.js" + ], + "javascript": { + "globals": [ + "exports" + ] + } + }, + { + "includes": [ + "src/app-version.ts" + ], + "linter": { + "rules": { + "style": { + "noRestrictedImports": "off" + }, + "suspicious": { + "noExplicitAny": "off" + } + } + } + }, + { + "includes": [ + "src/shared/solid/lazyImport.ts", + "src/app.tsx", + "src/routes/**/*.tsx", + "src/sections/**/*.tsx", + "src/modules/observability/**/*.ts", + "**/*.test.ts", + "**/*.test.tsx" + ], + "linter": { + "rules": {} + } + } + ] } From 23fbec8a1672b1799442f4d6af20972b807f091e Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 20:25:25 -0300 Subject: [PATCH 14/23] feat(biome): add assist actions to organize imports and enhance type definition rules --- biome.jsonc | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/biome.jsonc b/biome.jsonc index fee2393f..b1d95cb6 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -24,6 +24,13 @@ }, "overrides": [ { + "assist": { + "actions": { + "source": { + "organizeImports": "off" + } + } + }, "includes": [ "**/*.ts", "**/*.tsx" @@ -215,7 +222,12 @@ "useArrayLiterals": "error", "useAsConstAssertion": "error", "useConsistentObjectDefinitions": "warn", - "useConsistentTypeDefinitions": "error", + "useConsistentTypeDefinitions": { + "level": "error", + "options": { + "style": "type" + } + }, "useImportType": { "level": "error", "options": { From 4db4ec883372f1f5fb3a84f8af7eadd593a7af9d Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 20:28:55 -0300 Subject: [PATCH 15/23] refactor(diet): update import statements to use type imports consistently --- .zed/settings.json | 6 ++++++ .../application/services/recentFoodCrudService.ts | 2 +- src/modules/diet/recent-food/application/usecases/deps.ts | 6 +++--- .../recent-food/application/usecases/touchRecentFood.ts | 2 +- .../application/usecases/touchRecentFoodForItem.ts | 2 +- src/modules/diet/recent-food/domain/recentFoodRepository.ts | 2 +- .../diet/recent-food/infrastructure/recentFoodRepository.ts | 2 +- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.zed/settings.json b/.zed/settings.json index 5566f922..0c52fa67 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -6,11 +6,17 @@ "tab_size": 2, "languages": { "TypeScript": { + "code_actions_on_format": { + "source.fixAll.biome": true + }, "formatter": { "code_action": "source.fixAll.eslint" } }, "TSX": { + "code_actions_on_format": { + "source.fixAll.biome": true + }, "formatter": { "code_action": "source.fixAll.eslint" } diff --git a/src/modules/diet/recent-food/application/services/recentFoodCrudService.ts b/src/modules/diet/recent-food/application/services/recentFoodCrudService.ts index aeabeee9..b46080b8 100644 --- a/src/modules/diet/recent-food/application/services/recentFoodCrudService.ts +++ b/src/modules/diet/recent-food/application/services/recentFoodCrudService.ts @@ -3,7 +3,7 @@ import { type RecentFood, } from '~/modules/diet/recent-food/domain/recentFood' import { type RecentFoodRepository } from '~/modules/diet/recent-food/domain/recentFoodRepository' -import type { Template } from '~/modules/diet/template/domain/template' +import { type Template } from '~/modules/diet/template/domain/template' import { type User } from '~/modules/user/domain/user' import env from '~/shared/config/env' diff --git a/src/modules/diet/recent-food/application/usecases/deps.ts b/src/modules/diet/recent-food/application/usecases/deps.ts index 3cf1cdfa..6463e5b4 100644 --- a/src/modules/diet/recent-food/application/usecases/deps.ts +++ b/src/modules/diet/recent-food/application/usecases/deps.ts @@ -15,9 +15,9 @@ const { recentFoodCrudService } = createRoot(() => { // TODO: Implement recent food cache using realtime updates initializeRecentFoodRealtime({ - onInsert: (_: unknown) => {}, - onUpdate: (_: unknown) => {}, - onDelete: (_: unknown) => {}, + onInsert: (_: unknown) => undefined, + onUpdate: (_: unknown) => undefined, + onDelete: (_: unknown) => undefined, }) return { recentFoodCrudService } diff --git a/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts b/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts index 4c381034..a5c5e2ef 100644 --- a/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts +++ b/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts @@ -1,6 +1,6 @@ import { authUseCases } from '~/modules/auth/application/usecases/authUseCases' import { recentFoodCrudService } from '~/modules/diet/recent-food/application/usecases/deps' -import type { RecentFoodReference } from '~/modules/diet/recent-food/application/usecases/extractRecentFoodReference' +import { type RecentFoodReference } from '~/modules/diet/recent-food/application/usecases/extractRecentFoodReference' import { createNewRecentFood, type NewRecentFood, diff --git a/src/modules/diet/recent-food/application/usecases/touchRecentFoodForItem.ts b/src/modules/diet/recent-food/application/usecases/touchRecentFoodForItem.ts index 8fc70b33..20098d50 100644 --- a/src/modules/diet/recent-food/application/usecases/touchRecentFoodForItem.ts +++ b/src/modules/diet/recent-food/application/usecases/touchRecentFoodForItem.ts @@ -1,4 +1,4 @@ -import type { Item } from '~/modules/diet/item/schema/itemSchema' +import { type Item } from '~/modules/diet/item/schema/itemSchema' import { extractRecentFoodReferenceFromItem } from '~/modules/diet/recent-food/application/usecases/extractRecentFoodReference' import { touchRecentFood } from '~/modules/diet/recent-food/application/usecases/touchRecentFood' import { showError } from '~/modules/toast/application/toastManager' diff --git a/src/modules/diet/recent-food/domain/recentFoodRepository.ts b/src/modules/diet/recent-food/domain/recentFoodRepository.ts index cd79d63e..83cd70e2 100644 --- a/src/modules/diet/recent-food/domain/recentFoodRepository.ts +++ b/src/modules/diet/recent-food/domain/recentFoodRepository.ts @@ -2,7 +2,7 @@ import { type NewRecentFood, type RecentFood, } from '~/modules/diet/recent-food/domain/recentFood' -import type { Template } from '~/modules/diet/template/domain/template' +import { type Template } from '~/modules/diet/template/domain/template' import { type User } from '~/modules/user/domain/user' export type RecentFoodRepository = { diff --git a/src/modules/diet/recent-food/infrastructure/recentFoodRepository.ts b/src/modules/diet/recent-food/infrastructure/recentFoodRepository.ts index 66ed4307..fac06fec 100644 --- a/src/modules/diet/recent-food/infrastructure/recentFoodRepository.ts +++ b/src/modules/diet/recent-food/infrastructure/recentFoodRepository.ts @@ -4,7 +4,7 @@ import { } from '~/modules/diet/recent-food/domain/recentFood' import { type RecentFoodRepository } from '~/modules/diet/recent-food/domain/recentFoodRepository' import { type RecentFoodGateway } from '~/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodGateway' -import type { Template } from '~/modules/diet/template/domain/template' +import { type Template } from '~/modules/diet/template/domain/template' import { type User } from '~/modules/user/domain/user' import { logging } from '~/shared/utils/logging' From 4b3220c5516acfc8fff6e8b007e0e6c1a04af08d Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 20:31:51 -0300 Subject: [PATCH 16/23] docs: enhance GitHub issue creation guidelines with robust examples and shell safety notes --- .github/prompts/github-issue-new.prompt.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/prompts/github-issue-new.prompt.md b/.github/prompts/github-issue-new.prompt.md index 4c7739ab..857fb6cd 100644 --- a/.github/prompts/github-issue-new.prompt.md +++ b/.github/prompts/github-issue-new.prompt.md @@ -17,7 +17,19 @@ Core rules (short) - Always confirm issue type if ambiguous: bug, feature, improvement, refactor, task, subissue. - Use the matching template from `docs/` and produce Markdown output that fills the chosen template sections. - For bugs, include a `Related Files` section listing relevant paths discovered by searching the codebase. -- Use `printf` with a heredoc to write the issue body to a temp file and call `gh issue create --body-file` (zsh-compatible; prefer double quotes). +- Use `printf` with a heredoc to write the issue body to a temp file and call `gh issue create --body-file` (zsh-compatible). Use a single-quoted heredoc marker to avoid unwanted shell expansion and avoid backticks or legacy `\`...\`` command substitution. +- +- Robust, zsh-safe example (recommended): + +- printf "%s\n" "$(cat <<'ISSUE_BODY' )" > /tmp/issue-body.md +- +- ISSUE_BODY +- cat /tmp/issue-body.md +- gh issue create --title "..." --label "..." --body-file /tmp/issue-body.md + +- Notes: +- - Use a single-quoted heredoc marker (<<'ISSUE_BODY') so the shell does not expand variables or backticks inside the body. +- - Avoid using backticks (``) anywhere in the generated shell snippet. Also avoid unquoted here-doc markers that allow expansion unless expansion is explicitly desired. - Output only the final `gh` command in a fenced markdown code block delimited by four backticks. - Never include "Additional context" sections or any agent-personal offers in the issue body. Do not append sentences like "If you want, I can open and inspect..." or other invitations to inspect code — the issue body must contain only the structured template content and investigation-derived facts. @@ -68,7 +80,7 @@ Special rules - Improvements should state justification, urgency, impact, and suggested actions. Safety and shell notes -- Use double-quoted printf to avoid zsh heredoc quoting issues; if that fails, retry and document fallback. + - Use the zsh-safe pattern above (single-quoted heredoc within a command-substitution passed to printf) to avoid quoting pitfalls. If that fails, document a fallback. - Preserve Unicode and accented characters. - When creating files in `/tmp`, handle permissions and check write success. From 5240eaf056e4ad57d28936e17f6ce093603e0434 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 20:38:39 -0300 Subject: [PATCH 17/23] refactor(diet-recent-food): use dependency injection for auth use cases --- .../recent-food/application/usecases/recentFoodUseCases.ts | 4 +++- .../diet/recent-food/application/usecases/touchRecentFood.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/modules/diet/recent-food/application/usecases/recentFoodUseCases.ts b/src/modules/diet/recent-food/application/usecases/recentFoodUseCases.ts index 76f0f892..a80f37e2 100644 --- a/src/modules/diet/recent-food/application/usecases/recentFoodUseCases.ts +++ b/src/modules/diet/recent-food/application/usecases/recentFoodUseCases.ts @@ -1,4 +1,4 @@ -import { authUseCases } from '~/modules/auth/application/usecases/authUseCases' +import { useCases } from '~/di/useCases' import { type Item } from '~/modules/diet/item/schema/itemSchema' import { recentFoodCrudService } from '~/modules/diet/recent-food/application/usecases/deps' import { @@ -98,6 +98,8 @@ export const recentFoodUseCases = { const [recentFoodReference, ...rest] = extractRecentFoodReferenceFromTemplate(template) + const authUseCases = useCases.authUseCases() + if (recentFoodReference === undefined) { return } diff --git a/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts b/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts index a5c5e2ef..fa149197 100644 --- a/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts +++ b/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts @@ -1,4 +1,4 @@ -import { authUseCases } from '~/modules/auth/application/usecases/authUseCases' +import { useCases } from '~/di/useCases' import { recentFoodCrudService } from '~/modules/diet/recent-food/application/usecases/deps' import { type RecentFoodReference } from '~/modules/diet/recent-food/application/usecases/extractRecentFoodReference' import { @@ -7,6 +7,7 @@ import { } from '~/modules/diet/recent-food/domain/recentFood' export async function touchRecentFood(recentFoodRef: RecentFoodReference) { + const authUseCases = useCases.authUseCases() const currentRecentFood = await recentFoodCrudService.fetchRecentFoodByUserTypeAndReferenceId( authUseCases.currentUserIdOrGuestId(), From 9d9046700382aa5f7ac75976e22e70bdaf4acdf5 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 21:39:27 -0300 Subject: [PATCH 18/23] refactor(test): remove unused mockUserId variable --- .../diet/recent-food/ui/tests/RemoveFromRecentButton.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/diet/recent-food/ui/tests/RemoveFromRecentButton.test.tsx b/src/modules/diet/recent-food/ui/tests/RemoveFromRecentButton.test.tsx index 4e9add74..ae2318b8 100644 --- a/src/modules/diet/recent-food/ui/tests/RemoveFromRecentButton.test.tsx +++ b/src/modules/diet/recent-food/ui/tests/RemoveFromRecentButton.test.tsx @@ -59,7 +59,6 @@ const mockLogging = vi.mocked(logging) describe('RemoveFromRecentButton Logic', () => { const mockRefetch = vi.fn() - const mockUserId = '42' const mockFoodTemplate: Food = promoteNewFoodToFood( createNewFood({ From 5cc587fd6bd60f9f5e62aea98e1d7b19828d4e76 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 21:51:28 -0300 Subject: [PATCH 19/23] refactor(guest): simplify guest mode determination logic --- src/di/useCases.ts | 4 +--- src/shared/guest/guestUseCases.ts | 3 --- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/di/useCases.ts b/src/di/useCases.ts index 37783c9b..3f13ea28 100644 --- a/src/di/useCases.ts +++ b/src/di/useCases.ts @@ -34,9 +34,7 @@ const container = createRoot(() => { ) createEffect(() => { - const isGuest = - authUseCases().currentUserIdOrGuestId() === GUEST_USER_ID && - guestUseCases().hasAcceptedGuestTerms() + const isGuest = guestUseCases().isGuestMode() setMode(isGuest ? 'guest' : 'normal') }) diff --git a/src/shared/guest/guestUseCases.ts b/src/shared/guest/guestUseCases.ts index 34b64129..547b642a 100644 --- a/src/shared/guest/guestUseCases.ts +++ b/src/shared/guest/guestUseCases.ts @@ -45,9 +45,6 @@ export function createGuestUseCases(di: GuestDI) { const guestUseCases = { isGuestMode: () => guestStore.guestModeEnabled() && guestUseCases.hasAcceptedGuestTerms(), - setGuestModeEnabled: (enabled: boolean) => { - guestStore.setGuestModeEnabled(enabled) - }, hasAcceptedGuestTerms: () => { return guestStore.acceptedGuestTerms() }, From 66834a9c44cedbaf00ea3b5eaf171b27e6382630 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Fri, 5 Dec 2025 21:52:36 -0300 Subject: [PATCH 20/23] refactor(useCases): remove unused GUEST_USER_ID import --- src/di/useCases.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/di/useCases.ts b/src/di/useCases.ts index 3f13ea28..10bf1960 100644 --- a/src/di/useCases.ts +++ b/src/di/useCases.ts @@ -5,7 +5,6 @@ import { createUserUseCases } from '~/modules/user/application/usecases/userUseC import { type UserRepository } from '~/modules/user/domain/userRepository' import { createGuestUserRepository } from '~/modules/user/infrastructure/guest/guestUserRepository' import { createSupabaseUserRepository } from '~/modules/user/infrastructure/supabase/supabaseUserRepository' -import { GUEST_USER_ID } from '~/shared/guest/guestConstants' import { createGuestUseCases } from '~/shared/guest/guestUseCases' export type AppMode = 'guest' | 'normal' From 2c969cc4f6b08a5dfa357953fd9ce7790bbd5559 Mon Sep 17 00:00:00 2001 From: Marcus Vinicius Castelo Branco Martins Date: Sat, 6 Dec 2025 12:07:46 -0300 Subject: [PATCH 21/23] Remove guest terms acceptance from warning message Removed the display of guest terms acceptance status in the warning message. --- src/sections/settings/components/GuestDataWarning.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sections/settings/components/GuestDataWarning.tsx b/src/sections/settings/components/GuestDataWarning.tsx index c4329d67..f8e2be0c 100644 --- a/src/sections/settings/components/GuestDataWarning.tsx +++ b/src/sections/settings/components/GuestDataWarning.tsx @@ -45,7 +45,7 @@ export function GuestDataWarning() {

- Modo Demo ({guestUseCases.hasAcceptedGuestTerms()}) + Modo Demo

Você está usando o aplicativo em modo demo. Os dados são armazenados From 79cb6d97d1300999064472757bcbcba2afa95890 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Sat, 6 Dec 2025 15:39:08 -0300 Subject: [PATCH 22/23] refactor(day-diet): simplify dayUseCases export to direct function call --- .../application/usecases/dayUseCases.ts | 7 ++- .../usecases/templateSearchState.ts | 53 +++++-------------- 2 files changed, 17 insertions(+), 43 deletions(-) diff --git a/src/modules/diet/day-diet/application/usecases/dayUseCases.ts b/src/modules/diet/day-diet/application/usecases/dayUseCases.ts index 7d8601fc..90721968 100644 --- a/src/modules/diet/day-diet/application/usecases/dayUseCases.ts +++ b/src/modules/diet/day-diet/application/usecases/dayUseCases.ts @@ -243,9 +243,8 @@ export function createDayUseCases(deps: { authUseCases: () => AuthUseCases }) { * Backward-compatible shim kept for legacy consumers. * Consumers may continue to import `dayUseCases` while migration proceeds. */ -export const dayUseCases = () => - createDayUseCases({ - authUseCases: () => useCases.authUseCases(), - }) +export const dayUseCases = createDayUseCases({ + authUseCases: () => useCases.authUseCases(), +}) export type DayUseCases = ReturnType diff --git a/src/modules/template-search/application/usecases/templateSearchState.ts b/src/modules/template-search/application/usecases/templateSearchState.ts index b2841e1a..78920814 100644 --- a/src/modules/template-search/application/usecases/templateSearchState.ts +++ b/src/modules/template-search/application/usecases/templateSearchState.ts @@ -10,8 +10,8 @@ import { createFoodCrud, type FoodCrud, } from '~/modules/diet/food/application/usecases/foodCrud' -import { recentFoodUseCases } from '~/modules/diet/recent-food/application/usecases/recentFoodUseCases' import { createSupabaseFoodRepository } from '~/modules/diet/food/infrastructure/api/infrastructure/supabase/supabaseFoodRepository' +import { recentFoodUseCases } from '~/modules/diet/recent-food/application/usecases/recentFoodUseCases' import { fetchUserRecipeByName, fetchUserRecipes, @@ -33,7 +33,7 @@ export function createTemplateSearchState(deps?: { currentUser: () => { favorite_foods?: number[] } | null } } - fetchUserRecentFoods?: typeof fetchUserRecentFoods + fetchUserRecentFoods?: typeof recentFoodUseCases.fetchUserRecentFoodsAsTemplates foodCrud?: FoodCrud fetchUserRecipes?: typeof fetchUserRecipes fetchUserRecipeByName?: typeof fetchUserRecipeByName @@ -45,7 +45,8 @@ export function createTemplateSearchState(deps?: { return createRoot(() => { const injectedUseCases = deps?.useCases ?? useCases const injectedFetchUserRecentFoods = - deps?.fetchUserRecentFoods ?? fetchUserRecentFoods + deps?.fetchUserRecentFoods ?? + recentFoodUseCases.fetchUserRecentFoodsAsTemplates const foodCrud = deps?.foodCrud ?? createFoodCrud({ repository: () => createSupabaseFoodRepository() }) @@ -63,26 +64,6 @@ export function createTemplateSearchState(deps?: { createSignal('hidden') const [debouncedTab] = localCreateDebouncedSignal(templateSearchTab, 500) -export const [templates, { refetch: refetchTemplates }] = createResource( - () => ({ - tab: debouncedTab(), - search: debouncedSearch(), - userId: useCases.authUseCases().currentUserIdOrGuestId(), - }), - (signals) => { - return fetchTemplatesByTabLogic( - signals.tab, - signals.search, - signals.userId, - { - // TODO: Convert fetchTemplatesByTabLogic deps to reactive signals? - fetchUserRecipes, - fetchUserRecipeByName, - fetchUserRecentFoodsAsTemplates: - recentFoodUseCases.fetchUserRecentFoodsAsTemplates, - fetchFoods, - fetchFoodsByName, - getFavoriteFoods, const getFavoriteFoods = () => injectedUseCases.userUseCases().currentUser()?.favorite_foods ?? [] @@ -92,22 +73,16 @@ export const [templates, { refetch: refetchTemplates }] = createResource( search: debouncedSearch(), userId: injectedUseCases.authUseCases().currentUserIdOrGuestId(), }), - (signals) => { - return fetchTemplatesByTabLogic( - signals.tab, - signals.search, - signals.userId, - { - fetchUserRecipes: injectedFetchUserRecipes, - fetchUserRecipeByName: injectedFetchUserRecipeByName, - fetchUserRecentFoods: injectedFetchUserRecentFoods, - fetchFoods: (params) => foodCrud.fetchFoods(params), - fetchFoodsByName: (name, params) => - foodCrud.fetchFoodsByName(name, params), - getFavoriteFoods, - }, - ) - }, + (signals) => + fetchTemplatesByTabLogic(signals.tab, signals.search, signals.userId, { + fetchUserRecipes: injectedFetchUserRecipes, + fetchUserRecipeByName: injectedFetchUserRecipeByName, + fetchUserRecentFoodsAsTemplates: injectedFetchUserRecentFoods, + fetchFoods: (params) => foodCrud.fetchFoods(params), + fetchFoodsByName: (name, params) => + foodCrud.fetchFoodsByName(name, params), + getFavoriteFoods, + }), ) // Ensure the reactive signals are referenced inside a tracked scope so the From c82ce0902a1d0f87eb861ae436ba7e575b4b2441 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Sat, 6 Dec 2025 15:49:27 -0300 Subject: [PATCH 23/23] refactor(recent-food): streamline insert and update logic in touchRecentFood use case --- .../application/usecases/touchRecentFood.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts b/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts index fa149197..d9a7c973 100644 --- a/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts +++ b/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts @@ -25,7 +25,11 @@ export async function touchRecentFood(recentFoodRef: RecentFoodReference) { }) if (currentRecentFood === null) { - await recentFoodCrudService.insertRecentFood(newRecentFoodData) + const insertResult = + await recentFoodCrudService.insertRecentFood(newRecentFoodData) + if (insertResult === null) { + throw new Error('Failed to insert recent food record') + } } else { // TODO: Remove client-side user check after implementing row-level security (RLS) if (currentRecentFood.user_id !== authUseCases.currentUserIdOrGuestId()) { @@ -39,9 +43,12 @@ export async function touchRecentFood(recentFoodRef: RecentFoodReference) { throw new Error('BUG: recentFood fetched does not match type/reference') } - await recentFoodCrudService.updateRecentFood( + const updateResult = await recentFoodCrudService.updateRecentFood( currentRecentFood.id, newRecentFoodData, ) + if (updateResult === null) { + throw new Error('Failed to update recent food record') + } } }