diff --git a/.claude/agents/github-issue-manager.md b/.claude/agents/github-issue-manager.md index 47df77972..fec81f975 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. diff --git a/.github/prompts/github-issue-new.prompt.md b/.github/prompts/github-issue-new.prompt.md index 4c7739ab9..857fb6cd2 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. diff --git a/.github/prompts/validate-pr.prompt.md b/.github/prompts/validate-pr.prompt.md new file mode 100644 index 000000000..a49884f65 --- /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. diff --git a/.zed/settings.json b/.zed/settings.json index aac05863b..1bc550fe0 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/biome.jsonc b/biome.jsonc index 8076600c4..b1d95cb6c 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,10 +1,362 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, "linter": { "rules": { - "style": { - "useImportType": "off" + "recommended": false + }, + "includes": [ + "**", + "!node_modules", + "!src/sections/datepicker", + "!.vercel", + "!.vinxi", + "!dist", + "!build", + "!coverage", + "!public", + "!out", + "!.output" + ] + }, + "overrides": [ + { + "assist": { + "actions": { + "source": { + "organizeImports": "off" + } + } + }, + "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": { + "level": "error", + "options": { + "style": "type" + } + }, + "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": {} } } - } + ] } diff --git a/memory.jsonl b/memory.jsonl index 7befdfd16..441dabbe8 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/di/useCases.ts b/src/di/useCases.ts index 4b93c49a3..405706143 100644 --- a/src/di/useCases.ts +++ b/src/di/useCases.ts @@ -23,7 +23,6 @@ import { createWeightUseCases, type WeightUseCases, } from '~/modules/weight/application/weight/usecases/weightUseCases' -import { GUEST_USER_ID } from '~/shared/guest/guestConstants' import { createGuestUseCases } from '~/shared/guest/guestUseCases' export type AppMode = 'guest' | 'normal' @@ -67,9 +66,7 @@ const coreContainer = createRoot(() => { ) createEffect(() => { - const isGuest = - authUseCases().currentUserIdOrGuestId() === GUEST_USER_ID && - guestUseCases().hasAcceptedGuestTerms() + const isGuest = guestUseCases().isGuestMode() setMode(isGuest ? 'guest' : 'normal') }) diff --git a/src/modules/diet/recent-food/application/services/recentFoodCrudService.ts b/src/modules/diet/recent-food/application/services/recentFoodCrudService.ts new file mode 100644 index 000000000..b46080b82 --- /dev/null +++ b/src/modules/diet/recent-food/application/services/recentFoodCrudService.ts @@ -0,0 +1,56 @@ +import { + type NewRecentFood, + 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 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 fetchUserRecentFoodsAsTemplates( + 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/diet/recent-food/application/usecases/deps.ts b/src/modules/diet/recent-food/application/usecases/deps.ts new file mode 100644 index 000000000..6463e5b40 --- /dev/null +++ b/src/modules/diet/recent-food/application/usecases/deps.ts @@ -0,0 +1,26 @@ +import { createRoot } from 'solid-js' + +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 +// 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) => undefined, + onUpdate: (_: unknown) => undefined, + onDelete: (_: unknown) => undefined, + }) + + return { recentFoodCrudService } +}) + +export { recentFoodCrudService } diff --git a/src/modules/diet/recent-food/application/usecases/extractRecentFoodReference.ts b/src/modules/diet/recent-food/application/usecases/extractRecentFoodReference.ts new file mode 100644 index 000000000..614b905bc --- /dev/null +++ b/src/modules/diet/recent-food/application/usecases/extractRecentFoodReference.ts @@ -0,0 +1,71 @@ +import { + isFoodItem, + isGroupItem, + 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' + +/** + * Result of extracting recent food reference from an item. + */ +export type RecentFoodReference = { + type: RecentFood['type'] + referenceId: number +} + +/** + * Extracts the recent food reference (type and referenceId) from an item. + * This is used to track recently used foods/recipes. + * + * For FoodItem: returns the food reference directly + * For RecipeItem: returns the recipe reference directly + * For GroupItem: returns the reference of the first trackable child (food or recipe) + * + * @param item - The item to extract reference from + * @returns The recent food reference, or null if the item cannot be tracked + */ +export function extractRecentFoodReferenceFromItem( + item: Item, +): RecentFoodReference[] { + if (isFoodItem(item)) { + return [ + { + type: item.reference.type, + referenceId: item.reference.id, + }, + ] + } + + if (isRecipeItem(item)) { + return [ + { + type: item.reference.type, + referenceId: item.reference.id, + }, + ] + } + + if (isGroupItem(item)) { + const references: RecentFoodReference[] = [] + for (const child of item.reference.children) { + const childReferences = extractRecentFoodReferenceFromItem(child) + if (childReferences.length > 0) { + references.push(...childReferences) + } + } + return references + } + + // Should never reach here + item satisfies never + return [] +} + +export function extractRecentFoodReferenceFromTemplate( + template: Template, +): RecentFoodReference[] { + return extractRecentFoodReferenceFromItem(templateToItem(template)) +} diff --git a/src/modules/diet/recent-food/application/usecases/recentFoodUseCases.ts b/src/modules/diet/recent-food/application/usecases/recentFoodUseCases.ts new file mode 100644 index 000000000..a80f37e21 --- /dev/null +++ b/src/modules/diet/recent-food/application/usecases/recentFoodUseCases.ts @@ -0,0 +1,128 @@ +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 { + extractRecentFoodReferenceFromTemplate, + type RecentFoodReference, +} 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/diet/recent-food/domain/recentFood' +import { type Template } from '~/modules/diet/template/domain/template' +import { + showError, + showPromise, +} from '~/modules/toast/application/toastManager' +import { type User } from '~/modules/user/domain/user' +import { logging } from '~/shared/utils/logging' + +export const recentFoodUseCases = { + fetchUserRecentFoodsAsTemplates: async ( + userId: User['uuid'], + search: string, + opts?: { limit?: number }, + ) => { + 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 ( + 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' }, + ) + }, + + async deleteRecentFoodOfTemplate(template: Template) { + const [recentFoodReference, ...rest] = + extractRecentFoodReferenceFromTemplate(template) + + const authUseCases = useCases.authUseCases() + + if (recentFoodReference === undefined) { + return + } + if (rest.length > 0) { + logging.warn( + 'Expected only one recent food reference for template, but found multiple.', + { + template, + recentFoodReferences: [recentFoodReference, ...rest], + }, + ) + } + + await recentFoodUseCases.deleteRecentFoodByReference( + authUseCases.currentUserIdOrGuestId(), + recentFoodReference.type, + recentFoodReference.referenceId, + ) + }, + + touchRecentFood: async (recentFoodRef: RecentFoodReference) => + await touchRecentFood(recentFoodRef), + + touchRecentFoodForItem: async (item: Item) => + await touchRecentFoodForItem(item), +} diff --git a/src/modules/recent-food/application/tests/extractRecentFoodReference.test.ts b/src/modules/diet/recent-food/application/usecases/tests/extractRecentFoodReference.test.ts similarity index 84% rename from src/modules/recent-food/application/tests/extractRecentFoodReference.test.ts rename to src/modules/diet/recent-food/application/usecases/tests/extractRecentFoodReference.test.ts index 239a772ad..a3c3d45d7 100644 --- a/src/modules/recent-food/application/tests/extractRecentFoodReference.test.ts +++ b/src/modules/diet/recent-food/application/usecases/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/diet/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/diet/recent-food/application/usecases/touchRecentFood.ts b/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts new file mode 100644 index 000000000..d9a7c973a --- /dev/null +++ b/src/modules/diet/recent-food/application/usecases/touchRecentFood.ts @@ -0,0 +1,54 @@ +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 { + createNewRecentFood, + type NewRecentFood, +} from '~/modules/diet/recent-food/domain/recentFood' + +export async function touchRecentFood(recentFoodRef: RecentFoodReference) { + const authUseCases = useCases.authUseCases() + 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) { + 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()) { + 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') + } + + const updateResult = await recentFoodCrudService.updateRecentFood( + currentRecentFood.id, + newRecentFoodData, + ) + if (updateResult === null) { + throw new Error('Failed to update recent food record') + } + } +} diff --git a/src/modules/diet/recent-food/application/usecases/touchRecentFoodForItem.ts b/src/modules/diet/recent-food/application/usecases/touchRecentFoodForItem.ts new file mode 100644 index 000000000..20098d500 --- /dev/null +++ b/src/modules/diet/recent-food/application/usecases/touchRecentFoodForItem.ts @@ -0,0 +1,21 @@ +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' +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) + } +} diff --git a/src/modules/recent-food/domain/recentFood.ts b/src/modules/diet/recent-food/domain/recentFood.ts similarity index 88% rename from src/modules/recent-food/domain/recentFood.ts rename to src/modules/diet/recent-food/domain/recentFood.ts index 4f1e43e7a..e2beb18b8 100644 --- a/src/modules/recent-food/domain/recentFood.ts +++ b/src/modules/diet/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' diff --git a/src/modules/recent-food/domain/recentFoodRepository.ts b/src/modules/diet/recent-food/domain/recentFoodRepository.ts similarity index 85% rename from src/modules/recent-food/domain/recentFoodRepository.ts rename to src/modules/diet/recent-food/domain/recentFoodRepository.ts index c75a34285..83cd70e28 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 5e1129eea..cadcdee78 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/diet/recent-food/infrastructure/recentFoodRepository.ts b/src/modules/diet/recent-food/infrastructure/recentFoodRepository.ts new file mode 100644 index 000000000..fac06fec6 --- /dev/null +++ b/src/modules/diet/recent-food/infrastructure/recentFoodRepository.ts @@ -0,0 +1,80 @@ +import { + type NewRecentFood, + type RecentFood, +} 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' + +export function createRecentFoodRepository( + gateway: RecentFoodGateway, +): RecentFoodRepository { + return { + async fetchByUserTypeAndReferenceId( + userId: User['uuid'], + type: RecentFood['type'], + referenceId: number, + ): Promise { + try { + return await gateway.fetchByUserTypeAndReferenceId( + userId, + type, + referenceId, + ) + } catch (error) { + logging.error('[NON-FATAL] fetchByUserTypeAndReferenceId:', error) + return null + } + }, + + async fetchUserRecentFoodsAsTemplates( + userId: User['uuid'], + search: string, + opts?: { limit?: number }, + ): Promise { + try { + return await gateway.fetchUserRecentFoodsAsTemplates( + userId, + search, + opts, + ) + } catch (error) { + logging.error('[NON-FATAL] fetchUserRecentFoodsAsTemplates:', error) + return [] + } + }, + + async insert(input: NewRecentFood): Promise { + try { + return await gateway.insert(input) + } catch (error) { + logging.error('[NON-FATAL] insert:', error) + return null + } + }, + + async update(id: number, input: NewRecentFood): Promise { + try { + return await gateway.update(id, input) + } catch (error) { + logging.error('[NON-FATAL] update:', error) + return null + } + }, + + async deleteByReference( + userId: User['uuid'], + type: RecentFood['type'], + referenceId: number, + ): Promise { + try { + return await gateway.deleteByReference(userId, type, referenceId) + } catch (error) { + logging.error('[NON-FATAL] deleteByReference:', error) + return false + } + }, + } +} 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 58% rename from src/modules/recent-food/infrastructure/supabase/realtime.ts rename to src/modules/diet/recent-food/infrastructure/supabase/realtime.ts index fa6fd3544..bd5cb42e2 100644 --- a/src/modules/recent-food/infrastructure/supabase/realtime.ts +++ b/src/modules/diet/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 { SUPABASE_TABLE_RECENT_FOODS } from '~/modules/recent-food/infrastructure/supabase/constants' +import { + type RecentFood, + recentFoodSchema, +} 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' 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/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts b/src/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts similarity index 94% rename from src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts rename to src/modules/diet/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts index 7e91d9dff..baf745e67 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' @@ -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/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 cfd5867c9..903e199f5 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/diet/recent-food/ui/RemoveFromRecentButton.tsx b/src/modules/diet/recent-food/ui/RemoveFromRecentButton.tsx new file mode 100644 index 000000000..a36020fa0 --- /dev/null +++ b/src/modules/diet/recent-food/ui/RemoveFromRecentButton.tsx @@ -0,0 +1,35 @@ +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 { debouncedTab } from '~/modules/template-search/application/usecases/templateSearchState' +import { TrashIcon } from '~/sections/common/components/icons/TrashIcon' + +type RemoveFromRecentButtonProps = { + template: Template + refetch: (info?: unknown) => unknown +} + +export function RemoveFromRecentButton(props: RemoveFromRecentButtonProps) { + const handleClick = (e: MouseEvent) => { + e.stopPropagation() + e.preventDefault() + + void recentFoodUseCases + .deleteRecentFoodOfTemplate(props.template) + .then(props.refetch) + } + + return ( + + + + ) +} diff --git a/src/sections/common/components/buttons/RemoveFromRecentButton.test.tsx b/src/modules/diet/recent-food/ui/tests/RemoveFromRecentButton.test.tsx similarity index 67% rename from src/sections/common/components/buttons/RemoveFromRecentButton.test.tsx rename to src/modules/diet/recent-food/ui/tests/RemoveFromRecentButton.test.tsx index aea64ae33..ae2318b8d 100644 --- a/src/sections/common/components/buttons/RemoveFromRecentButton.test.tsx +++ b/src/modules/diet/recent-food/ui/tests/RemoveFromRecentButton.test.tsx @@ -17,9 +17,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', @@ -46,19 +49,16 @@ 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) describe('RemoveFromRecentButton Logic', () => { const mockRefetch = vi.fn() - const mockUserId = '42' const mockFoodTemplate: Food = promoteNewFoodToFood( createNewFood({ @@ -87,7 +87,6 @@ describe('RemoveFromRecentButton Logic', () => { vi.clearAllMocks() mockDebouncedTab.mockReturnValue('recent') mockShowPromise.mockImplementation((promise) => promise) - mockDeleteRecentFoodByReference.mockResolvedValue(true) }) afterEach(() => { @@ -128,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/recent-food/application/usecases/extractRecentFoodReference.ts b/src/modules/recent-food/application/usecases/extractRecentFoodReference.ts deleted file mode 100644 index faa2da2c6..000000000 --- a/src/modules/recent-food/application/usecases/extractRecentFoodReference.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - isFoodItem, - isGroupItem, - isRecipeItem, - type Item, -} from '~/modules/diet/item/schema/itemSchema' -import { type RecentFood } from '~/modules/recent-food/domain/recentFood' - -/** - * Result of extracting recent food reference from an item. - */ -export type RecentFoodReference = { - type: RecentFood['type'] - referenceId: number -} - -/** - * Extracts the recent food reference (type and referenceId) from an item. - * This is used to track recently used foods/recipes. - * - * For FoodItem: returns the food reference directly - * For RecipeItem: returns the recipe reference directly - * For GroupItem: returns the reference of the first trackable child (food or recipe) - * - * @param item - The item to extract reference from - * @returns The recent food reference, or null if the item cannot be tracked - */ -export function extractRecentFoodReference( - item: Item, -): RecentFoodReference | null { - if (isFoodItem(item)) { - return { - type: 'food', - referenceId: item.reference.id, - } - } - - if (isRecipeItem(item)) { - return { - type: 'recipe', - 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, - } - } - // Cannot track - no trackable children - return null - } - - // Unknown item type (should never happen due to exhaustive checks) - return null -} 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 606307432..000000000 --- a/src/modules/recent-food/application/usecases/recentFoodCrud.ts +++ /dev/null @@ -1,119 +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' - -/** - * Factory that creates recent-food CRUD use-cases. - * - * Allows injecting a repository and `showPromise` helper for DI and testing. - */ -export function createRecentFoodCrud(deps?: { - recentFoodRepository?: ReturnType - showPromise?: typeof showPromise -}) { - const recentFoodRepository = - deps?.recentFoodRepository ?? createRecentFoodRepository() - const _showPromise = deps?.showPromise ?? showPromise - - async function fetchRecentFoodByUserTypeAndReferenceId( - userId: User['uuid'], - type: RecentFood['type'], - referenceId: number, - ): Promise { - return await recentFoodRepository.fetchByUserTypeAndReferenceId( - userId, - type, - referenceId, - ) - } - - 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 }, - ) - } - - 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' }, - ) - } - - 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' }, - ) - } - - 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' }, - ) - } - - return { - fetchRecentFoodByUserTypeAndReferenceId, - fetchUserRecentFoods, - insertRecentFood, - updateRecentFood, - deleteRecentFoodByReference, - } -} - -/** - * Backward-compatible shim: keep existing named exports working while consumers migrate. - * Wired to default repository and showPromise. - */ -const _defaultRecentFoodCrud = createRecentFoodCrud() - -export const fetchRecentFoodByUserTypeAndReferenceId = - _defaultRecentFoodCrud.fetchRecentFoodByUserTypeAndReferenceId -export const fetchUserRecentFoods = _defaultRecentFoodCrud.fetchUserRecentFoods -export const insertRecentFood = _defaultRecentFoodCrud.insertRecentFood -export const updateRecentFood = _defaultRecentFoodCrud.updateRecentFood -export const deleteRecentFoodByReference = - _defaultRecentFoodCrud.deleteRecentFoodByReference - -// Also export the factory for DI consumers -export { _defaultRecentFoodCrud as recentFoodCrud } -export type RecentFoodCrud = ReturnType diff --git a/src/modules/recent-food/infrastructure/recentFoodRepository.ts b/src/modules/recent-food/infrastructure/recentFoodRepository.ts deleted file mode 100644 index 9fa6c8512..000000000 --- a/src/modules/recent-food/infrastructure/recentFoodRepository.ts +++ /dev/null @@ -1,89 +0,0 @@ -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 { createSupabaseRecentFoodGateway } 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 { - return { - fetchByUserTypeAndReferenceId, - fetchUserRecentFoodsAsTemplates, - insert, - update, - deleteByReference, - } -} - -export 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 - } -} - -export 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 [] - } -} - -export async function insert(input: NewRecentFood): Promise { - try { - return await supabaseGateway.insert(input) - } catch (error) { - logging.error('RecentFood operation error:', error) - return null - } -} - -export 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 - } -} - -export 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 - } -} 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 6665029a8..000000000 --- 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/template-search/application/templateSearchLogic.ts b/src/modules/template-search/application/templateSearchLogic.ts index 63eedd523..5e23dc15b 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 4589c8793..78532ab1c 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 381160a23..78920814d 100644 --- a/src/modules/template-search/application/usecases/templateSearchState.ts +++ b/src/modules/template-search/application/usecases/templateSearchState.ts @@ -11,11 +11,11 @@ import { type FoodCrud, } from '~/modules/diet/food/application/usecases/foodCrud' import { createSupabaseFoodRepository } from '~/modules/diet/food/infrastructure/api/infrastructure/supabase/supabaseFoodRepository' +import { recentFoodUseCases } from '~/modules/diet/recent-food/application/usecases/recentFoodUseCases' import { fetchUserRecipeByName, fetchUserRecipes, } from '~/modules/diet/recipe/application/usecases/recipeCrud' -import { fetchUserRecentFoods } from '~/modules/recent-food/application/usecases/recentFoodCrud' import { fetchTemplatesByTabLogic } from '~/modules/template-search/application/templateSearchLogic' // userUseCases will be accessed via the DI container to avoid init order issues import { type TemplateSearchTab } from '~/sections/search/components/TemplateSearchTabs' @@ -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() }) @@ -72,22 +73,16 @@ export function createTemplateSearchState(deps?: { 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 diff --git a/src/sections/common/components/buttons/RemoveFromRecentButton.tsx b/src/sections/common/components/buttons/RemoveFromRecentButton.tsx deleted file mode 100644 index 893e91efd..000000000 --- a/src/sections/common/components/buttons/RemoveFromRecentButton.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Show } from 'solid-js' - -import { useCases } from '~/di/useCases' -import { - isTemplateFood, - type Template, -} from '~/modules/diet/template/domain/template' -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 { TrashIcon } from '~/sections/common/components/icons/TrashIcon' -import { logging } from '~/shared/utils/logging' - -type RemoveFromRecentButtonProps = { - template: Template - refetch: (info?: unknown) => unknown -} - -export function RemoveFromRecentButton(props: RemoveFromRecentButtonProps) { - const authUseCases = useCases.authUseCases() - const handleClick = (e: MouseEvent) => { - e.stopPropagation() - e.preventDefault() - - const templateType = isTemplateFood(props.template) ? 'food' : 'recipe' - const templateId = props.template.id - - 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.' - }, - }, - ) - .then(props.refetch) - .catch(() => {}) - } - - return ( - - - - ) -} diff --git a/src/sections/search/components/TemplateSearchModal.tsx b/src/sections/search/components/TemplateSearchModal.tsx index 2ccb31cfa..599e63a59 100644 --- a/src/sections/search/components/TemplateSearchModal.tsx +++ b/src/sections/search/components/TemplateSearchModal.tsx @@ -1,10 +1,10 @@ import { onMount, Suspense } from 'solid-js' -import { useCases } from '~/di/useCases' import { dayUseCases } from '~/modules/diet/day-diet/application/usecases/dayUseCases' import { type Item } from '~/modules/diet/item/schema/itemSchema' import { createMacroOverflow } from '~/modules/diet/macro-nutrients/application/macroOverflow' import { macroTargetUseCases } from '~/modules/diet/macro-target/application/macroTargetUseCases' +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 { @@ -14,13 +14,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 { 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 { debouncedSearch, refetchTemplates, @@ -96,54 +89,12 @@ export function TemplateSearchModal(props: TemplateSearchModalProps) { originalAddedItem: Item, closeEditModal: () => void, ) => { - const authUseCases = useCases.authUseCases() 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) + .then(refetchTemplates) const confirmModalId = openConfirmModal( 'Deseja adicionar outro item ou finalizar a inclusão?', diff --git a/src/sections/search/components/TemplateSearchResultItem.tsx b/src/sections/search/components/TemplateSearchResultItem.tsx index f9bd7603d..d9138a8a8 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 '~/sections/common/components/buttons/RemoveFromRecentButton' import { ItemView } from '~/sections/item/components/ItemView' import { ItemFavorite } from '~/sections/item/components/UnifiedItemFavorite' import { openDeleteConfirmModal } from '~/shared/modal/ui/DeleteConfirmModal' diff --git a/src/sections/search/ui/openTemplateSearchModal.tsx b/src/sections/search/ui/openTemplateSearchModal.tsx index 5031716e1..5538e7334 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/sections/settings/components/GuestDataWarning.tsx b/src/sections/settings/components/GuestDataWarning.tsx index c4329d671..f8e2be0c9 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 diff --git a/src/shared/guest/guestUseCases.ts b/src/shared/guest/guestUseCases.ts index 34b64129d..547b642af 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() }, diff --git a/src/shared/supabase/database.types.ts b/src/shared/supabase/database.types.ts index 4d2a7d51f..03dbceedf 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 0b9fdaa21..5b01765e2 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,