Skip to content

refactor!: migrate from Supabase client to internal API for various components#4480

Merged
vhpx merged 3 commits intomainfrom
refactor/migration/supabase-client
Mar 20, 2026
Merged

refactor!: migrate from Supabase client to internal API for various components#4480
vhpx merged 3 commits intomainfrom
refactor/migration/supabase-client

Conversation

@khangronky
Copy link
Collaborator

@khangronky khangronky commented Mar 20, 2026

  • Updated delete-resource.tsx to use deleteWorkspaceStorageObject instead of Supabase client.
  • Refactored file-display.tsx to utilize createWorkspaceStorageSignedUrl from internal API.
  • Changed delete-link-button.tsx to use updateWorkspaceCourseModule for link deletion.
  • Replaced Supabase client calls in category-spending-chart.tsx with getCategoryBreakdown API.
  • Updated spending-trends-chart.tsx to fetch data using getSpendingTrends API.
  • Refactored recurring transaction form to use internal API for creating and updating transactions.
  • Modified recurring-transactions-page.tsx to list and delete recurring transactions via internal API.
  • Updated category-filter.tsx to fetch transaction categories using internal API.
  • Refactored category-breakdown-dialog.tsx to use getCategoryBreakdown API.
  • Changed category-donut-chart.tsx to fetch category breakdown data using internal API.
  • Updated period-breakdown-panel.tsx to use getTransactionStats API.
  • Refactored user-filter.tsx to list workspace members using internal API.
  • Updated wallet-role-access.tsx to fetch workspace roles using internal API.
  • Refactored google-calendar-settings.tsx to delete calendar connections using internal API.
  • Updated hour-settings.tsx to manage workspace calendar hours via internal API.
  • Refactored use-workspace-permission.ts to check permissions using internal API.
  • Updated use-workspace-user.ts to fetch user profile using internal API.
  • Added tests to ensure deprecated Supabase client imports are removed from various files.

Description

What?

Why?

How?

Screenshots for proof (must have)


Summary by cubic

Migrates client-side Supabase calls to server-backed endpoints and @tuturuuu/internal-api helpers. Centralizes auth/permission checks and standardizes data access across AI, education, finance, roles, mail, posts, promotions, settings, storage, and users, with improved type safety in calendar hour settings.

  • Refactors

    • Added API routes for: AI model favorites; finance recurring transactions (CRUD + upcoming) and filter-users; finance analytics (category breakdown, spending trends); mail; posts filter options; quiz-set link/unlink; roles (list, members); permissions check/setup status; storage deletes; promotions referral settings.
    • Introduced @tuturuuu/internal-api modules: ai, education, finance, mail, promotions, roles, settings, users, storage (new exports in packages/internal-api).
    • AI: model selector and gateway favorites now use /ai/model-favorites and @tuturuuu/internal-api/ai to list/toggle favorites.
    • Finance: charts use getCategoryBreakdown/getSpendingTrends; recurring transactions use create/update/list/delete/upcoming APIs; filters use transaction categories and filter-users APIs.
    • Education: modules, flashcards, quizzes, and YouTube links use updateWorkspaceCourseModule, link/unlink APIs, and storage signed URLs/deletes; components now receive wsId where needed.
    • Roles/permissions/calendar: permission banner and use-workspace-permission/use-workspace-user use settings/users APIs; calendar preferences/hours use settings APIs with improved type safety in HoursSettings; wallet role access uses roles API.
    • Mail and posts filters now read via internal API routes.
    • Removed @tuturuuu/supabase/next/client from client components; expanded tests to block deprecated imports.
  • Migration

    • Stop importing @tuturuuu/supabase/next/client in client code; use @tuturuuu/internal-api/* helpers.
    • Pass wsId to updated components: ModuleToggles, ModuleContentEditor, DeleteLinkButton, QuizsetModuleLinker.

Written for commit 67f39c4. Summary will update on new commits. Review in cubic

…mponents

- Updated delete-resource.tsx to use deleteWorkspaceStorageObject instead of Supabase client.
- Refactored file-display.tsx to utilize createWorkspaceStorageSignedUrl from internal API.
- Changed delete-link-button.tsx to use updateWorkspaceCourseModule for link deletion.
- Replaced Supabase client calls in category-spending-chart.tsx with getCategoryBreakdown API.
- Updated spending-trends-chart.tsx to fetch data using getSpendingTrends API.
- Refactored recurring transaction form to use internal API for creating and updating transactions.
- Modified recurring-transactions-page.tsx to list and delete recurring transactions via internal API.
- Updated category-filter.tsx to fetch transaction categories using internal API.
- Refactored category-breakdown-dialog.tsx to use getCategoryBreakdown API.
- Changed category-donut-chart.tsx to fetch category breakdown data using internal API.
- Updated period-breakdown-panel.tsx to use getTransactionStats API.
- Refactored user-filter.tsx to list workspace members using internal API.
- Updated wallet-role-access.tsx to fetch workspace roles using internal API.
- Refactored google-calendar-settings.tsx to delete calendar connections using internal API.
- Updated hour-settings.tsx to manage workspace calendar hours via internal API.
- Refactored use-workspace-permission.ts to check permissions using internal API.
- Updated use-workspace-user.ts to fetch user profile using internal API.
- Added tests to ensure deprecated Supabase client imports are removed from various files.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added recurring transactions management with upcoming transaction forecasting.
    • Added financial analytics endpoints for category spending breakdown and spending trends visualization.
  • Refactor

    • Consolidated data fetching across workspace permissions, email, calendar settings, finance, and storage management to internal API layer for improved consistency and maintainability.

Walkthrough

The PR systematically refactors the application to replace direct Supabase client calls with a centralized internal API abstraction layer. New REST endpoints are added for workspaces across AI models, finance, mail, posts, roles, permissions, and storage. Corresponding internal-api wrapper modules are created and exposed via package.json exports. Client-side components and hooks are updated to call these new wrapper functions instead of querying Supabase directly.

Changes

Cohort / File(s) Summary
AI Model Favorites
apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/components/mira-gateway-models.ts, apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/components/mira-model-selector/use-mira-model-selector-data.ts, apps/web/src/app/api/v1/workspaces/[wsId]/ai/model-favorites/route.ts, packages/internal-api/src/ai.ts
Migrated AI model favorites from direct Supabase queries to new REST API endpoint (GET, PATCH) with corresponding internal API wrapper functions (listWorkspaceAiModelFavorites, toggleWorkspaceAiModelFavorite).
Education - Courses & Modules
apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/..., apps/web/src/app/api/v1/workspaces/[wsId]/quiz-sets/[setId]/modules/..., packages/ui/src/components/ui/custom/education/modules/..., packages/internal-api/src/education.ts
Added REST endpoints for updating/deleting course modules and linking quiz-set modules; created internal API wrappers; updated UI components to use new APIs instead of Supabase client.
Education - Quizzes & Flashcards
apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/quizzes/client-quizzes.tsx, apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/flashcards/client-flashcards.tsx, packages/internal-api/src/education.ts
Replaced Supabase deletion logic with deleteWorkspaceQuiz and deleteWorkspaceFlashcard internal API functions, using try/catch error handling.
Finance - Recurring Transactions
apps/web/src/app/api/v1/workspaces/[wsId]/finance/recurring-transactions/..., packages/ui/src/components/ui/finance/recurring/..., packages/internal-api/src/finance.ts
Added REST endpoints (GET, POST, PUT, DELETE) for recurring transactions; created internal API functions with typed payload/response interfaces; refactored UI forms and pages to use new APIs.
Finance - Transaction Analytics
apps/web/src/app/api/workspaces/[wsId]/transactions/category-breakdown/route.ts, apps/web/src/app/api/workspaces/[wsId]/transactions/spending-trends/route.ts, packages/ui/src/components/ui/finance/analytics/..., packages/ui/src/components/ui/finance/transactions/period-charts/..., packages/internal-api/src/finance.ts
Added analytics endpoints for category breakdown and spending trends; created corresponding internal API wrappers (getCategoryBreakdown, getSpendingTrends, getTransactionStats); updated chart components to use new APIs.
Finance - User & Category Filtering
apps/web/src/app/api/v1/workspaces/[wsId]/finance/filter-users/route.ts, packages/ui/src/components/ui/finance/transactions/user-filter.tsx, packages/ui/src/components/ui/finance/transactions/category-filter.tsx
Added REST endpoint for filtering transaction creators/users; replaced Supabase queries with internal API calls in filter components.
Mail
apps/web/src/app/api/v1/workspaces/[wsId]/mail/route.ts, apps/web/src/app/[locale]/(dashboard)/[wsId]/mail/client.tsx, packages/internal-api/src/mail.ts
Added REST endpoint for listing workspace emails with pagination; created listWorkspaceEmails internal API wrapper; updated client component to use new API.
Posts & Filters
apps/web/src/app/api/v1/workspaces/[wsId]/posts/filter-options/route.ts, apps/web/src/app/[locale]/(dashboard)/[wsId]/posts/filters.tsx, packages/internal-api/src/settings.ts
Added REST endpoint for post filter options (user groups, excluded groups, users); created getPostsFilterOptions internal API wrapper; simplified client-side filter logic.
Roles & Members
apps/web/src/app/api/v1/workspaces/[wsId]/roles/..., apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/..., packages/internal-api/src/roles.ts
Added/updated REST endpoints for roles and members; created listWorkspaceRoles and listRoleMembers internal API wrappers; refactored form components to fetch via new APIs.
Workspace Permissions
apps/web/src/app/api/v1/workspaces/[wsId]/settings/permissions/..., apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/permission-setup-banner.tsx, packages/ui/src/hooks/use-workspace-permission.ts, packages/internal-api/src/settings.ts
Added REST endpoints for permission checks and setup status; created internal API wrappers (checkWorkspacePermission, getWorkspacePermissionSetupStatus, getWorkspacePermissionsSummary); updated banner and hooks to use new APIs.
Workspace Settings & Calendar
apps/web/src/lib/calendar-preferences-provider.tsx, packages/ui/src/components/ui/legacy/calendar/settings/..., packages/internal-api/src/settings.ts, packages/internal-api/src/users.ts
Migrated calendar settings and user profile fetching from Supabase to internal APIs (getWorkspaceCalendarHours, updateWorkspaceCalendarHours, getUserCalendarSettings, getCurrentUserProfile); refactored hour validation logic; removed direct Supabase client dependencies.
Storage
apps/web/src/app/api/v1/workspaces/[wsId]/storage/object/route.ts, packages/ui/src/components/ui/custom/education/modules/resources/..., packages/internal-api/src/education.ts
Added REST endpoint for deleting storage objects; created internal API wrapper; updated resource components to use new signed URL and deletion APIs.
Promotions & Referral Settings
apps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.ts, apps/web/src/app/[locale]/(dashboard)/[wsId]/inventory/promotions/settings-form.tsx, packages/internal-api/src/promotions.ts
Added REST endpoint for referral settings with typed payload validation and best-effort user link migration; created updateWorkspaceReferralSettings wrapper; removed in-component migration logic from client form.
Internal API Package
packages/internal-api/package.json, packages/internal-api/src/index.ts, packages/internal-api/src/*.ts (new modules)
Extended internal-api package exports to expose new modules (ai, education, mail, promotions, roles, settings, storage) and re-exported all new wrapper functions and types.
Migration Tracking
scripts/internal-api-migration.test.js
Extended test assertions to verify migrated files no longer import deprecated Supabase browser client and properly import from @tuturuuu/internal-api/*.
Other Refactored Components
apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/migrations/hooks/use-migration-state.ts, packages/ui/src/components/ui/finance/wallets/walletId/wallet-role-access.tsx, packages/ui/src/hooks/use-workspace-user.ts
Migrated workspace lookups, role retrieval, and user profile fetching to use internal API functions instead of direct Supabase queries.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Browser Client
    participant RestAPI as Next.js REST API<br/>/api/v1/*
    participant InternalAPI as Internal API<br/>Module Wrapper
    participant Supabase as Supabase<br/>Database

    rect rgba(100, 150, 200, 0.5)
    Note over Client,Supabase: Before: Direct Supabase Access
    Client->>Client: Create Supabase client
    Client->>Supabase: Query table directly<br/>e.g., ai_model_favorites
    Supabase-->>Client: Return data
    end

    rect rgba(100, 200, 100, 0.5)
    Note over Client,Supabase: After: Abstracted via Internal API
    Client->>InternalAPI: Call wrapper function<br/>e.g., listWorkspaceAiModelFavorites()
    InternalAPI->>RestAPI: HTTP GET/POST/PUT/DELETE<br/>/api/v1/workspaces/*/...
    RestAPI->>RestAPI: Validate permissions<br/>Resolve auth user
    RestAPI->>Supabase: Query/mutate as needed
    Supabase-->>RestAPI: Return result
    RestAPI-->>InternalAPI: Return JSON response
    InternalAPI-->>Client: Return typed data
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement, platform, refactor

Suggested reviewers

  • vhpx

Poem

🐰 From Supabase queries deep,
We bundle changes vast and steep—
REST endpoints guard each request's way,
Internal APIs seize the day!
With types so clean and routes so neat,
Our migration is now complete! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description lacks the structured sections required by the template (What?, Why?, How?, Screenshots for proof), though it contains a detailed list of changes and a comprehensive auto-generated summary. Complete the required template sections: clearly articulate What changes are being made, Why they are necessary, How they were implemented, and provide evidence with screenshots or test results.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor!: migrate from Supabase client to internal API for various components' accurately and concisely summarizes the main change—a large-scale migration from direct Supabase client calls to centralized internal API endpoints.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/migration/supabase-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 20, 2026

🔧 Biome Check Report

Total Issues Found: 0

🔧 Format Check Results
Metric Value Status
📁 Files Checked 4997 ✅ Complete
Errors 0 ✅ None
⚠️ Warnings 0 ✅ None
ℹ️ Info 0 ✅ None
📝 Total Issues 0 ✅ Passed
🔍 Lint Check Results
Metric Value Status
📁 Files Checked 5002 ✅ Complete
Errors 0 ✅ None
⚠️ Warnings 0 ✅ None
ℹ️ Info 0 ✅ None
📝 Total Issues 0 ✅ Passed

🎉 All Issues Resolved!

Your code is now 100% clean! Great job! 🏆

🤖 Auto-generated by Biome Check workflow • Last updated: 3/20/2026, 5:10:21 PM

@github-actions
Copy link
Contributor

github-actions bot commented Mar 20, 2026

🌐 i18n Check Report

✅ All i18n checks passed!

Check Status
🔤 Translation Sorting ✅ Sorted
🔑 Translation Keys ✅ All in sync
🔄 Key Parity (en ↔ vi) ✅ Keys in sync
📦 Namespace Check (shared → apps) ✅ All present
🤖 Auto-generated by i18n Check workflow • Last updated: 3/20/2026, 5:10:25 PM

@codecov
Copy link

codecov bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 0% with 106 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.14%. Comparing base (18e6c06) to head (67f39c4).
⚠️ Report is 17 commits behind head on main.

Files with missing lines Patch % Lines
packages/internal-api/src/finance.ts 0.00% 24 Missing and 2 partials ⚠️
packages/internal-api/src/settings.ts 0.00% 23 Missing and 2 partials ⚠️
packages/internal-api/src/education.ts 0.00% 21 Missing ⚠️
packages/internal-api/src/ai.ts 0.00% 6 Missing and 1 partial ⚠️
packages/internal-api/src/roles.ts 0.00% 7 Missing ⚠️
[...sId]/(dashboard)/components/mira-gateway-models.ts](https://app.codecov.io/gh/tutur3u/platform/pull/4480?src=pr&el=tree&filepath=apps%2Fweb%2Fsrc%2Fapp%2F%5Blocale%5D%2F%28dashboard%29%2F%5BwsId%5D%2F%28dashboard%29%2Fcomponents%2Fmira-gateway-models.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u#diff-YXBwcy93ZWIvc3JjL2FwcC9bbG9jYWxlXS8oZGFzaGJvYXJkKS9bd3NJZF0vKGRhc2hib2FyZCkvY29tcG9uZW50cy9taXJhLWdhdGV3YXktbW9kZWxzLnRz) 0.00% 3 Missing and 3 partials ⚠️
packages/internal-api/src/users.ts 0.00% 6 Missing ⚠️
packages/internal-api/src/mail.ts 0.00% 3 Missing and 1 partial ⚠️
packages/internal-api/src/promotions.ts 0.00% 3 Missing ⚠️
packages/ui/src/hooks/use-workspace-user.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4480      +/-   ##
==========================================
- Coverage   47.26%   47.14%   -0.13%     
==========================================
  Files         501      507       +6     
  Lines       34318    34408      +90     
  Branches    12315    12320       +5     
==========================================
  Hits        16220    16220              
- Misses      14763    14848      +85     
- Partials     3335     3340       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 65 files

Confidence score: 2/5

  • There is meaningful merge risk because multiple high-confidence issues are user-impacting, including a permission gap in apps/web/src/app/api/v1/workspaces/[wsId]/storage/object/route.ts where users with any workspace permission may be able to delete storage objects without manage_drive.
  • apps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.ts can incorrectly relink all referred users when affectedUserIds is empty, which can misapply promotions and create broad data integrity/regression impact.
  • Several medium-severity regressions are also present (calendar disconnect success shown on failed DELETE, workspace scoping missing in finance filter-users, and banner behavior changing on API errors), so this is not just a single isolated defect.
  • Pay close attention to apps/web/src/app/api/v1/workspaces/[wsId]/storage/object/route.ts, apps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.ts, apps/web/src/app/api/v1/workspaces/[wsId]/finance/filter-users/route.ts - authorization, cross-workspace data scope, and incorrect bulk-linking behavior need to be fixed before relying on this safely.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.ts">

<violation number="1" location="apps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.ts:81">
P1: When no users have the old promotion linked (`affectedUserIds` is empty), this falls back to linking **all** referred users to the new promotion — even those who never had the old one. If no one had the old promo, the migration should be a no-op rather than assigning the new promo to everyone.</violation>
</file>

<file name="apps/web/src/app/api/v1/workspaces/[wsId]/mail/route.ts">

<violation number="1" location="apps/web/src/app/api/v1/workspaces/[wsId]/mail/route.ts:13">
P2: Validate and cap `page`/`pageSize` before using them in `.range(...)` to avoid NaN errors and unbounded queries when clients pass invalid or huge values.</violation>
</file>

<file name="packages/ui/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx">

<violation number="1" location="packages/ui/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx:358">
P2: Check the DELETE responses for calendar connections and surface failures; otherwise a failed deletion still reports a successful disconnect.</violation>
</file>

<file name="apps/web/src/app/api/v1/workspaces/[wsId]/finance/filter-users/route.ts">

<violation number="1" location="apps/web/src/app/api/v1/workspaces/[wsId]/finance/filter-users/route.ts:23">
P2: Filter creator views by `ws_id` so the endpoint only returns users for the requested workspace.</violation>
</file>

<file name="apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/permission-setup-banner.tsx">

<violation number="1" location="apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/permission-setup-banner.tsx:26">
P2: Handle API errors in `queryFn` to preserve previous behavior; without a fallback, fetch failures can incorrectly show the permission setup banner.</violation>
</file>

<file name="apps/web/src/app/api/v1/workspaces/[wsId]/storage/object/route.ts">

<violation number="1" location="apps/web/src/app/api/v1/workspaces/[wsId]/storage/object/route.ts:25">
P1: Add a `manage_drive` permission check before deleting storage objects; otherwise any user with any workspace permission can delete files.</violation>
</file>

<file name="apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/role-members.tsx">

<violation number="1" location="apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/role-members.tsx:343">
P2: listWorkspaceMembers doesn’t provide full_name, but the new cast to WorkspaceUser[] makes the UI keep using user.full_name. That field will always be undefined, so full-name search and display regress for users without display_name. Update the mapping or the UI to align with InternalApiWorkspaceMember.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant UI as Client Component
    participant SDK as @tuturuuu/internal-api
    participant API as Next.js API Route
    participant Perms as Workspace Helper (Auth)
    participant DB as Supabase (Server-side)

    Note over UI,DB: Refactored Data/Control Flow (Migration from Client-side Supabase to API Proxy)

    UI->>SDK: NEW: call internal API helper (e.g. getTransactionStats)
    SDK->>API: NEW: fetch("/api/v1/workspaces/{wsId}/...")
    
    rect rgb(240, 240, 240)
        Note right of API: Server-side Boundary
        API->>Perms: NEW: getPermissions({ wsId, request })
        Perms->>DB: Get user session & workspace roles
        DB-->>Perms: session / role data
        Perms-->>API: permissions object
    end

    alt Has Required Permissions
        API->>API: Validate request body (Zod)
        
        alt Database Operation
            API->>DB: CHANGED: supabase.from(...).select/upsert()
            DB-->>API: data / error
        else Storage Operation (e.g. delete resource)
            API->>DB: NEW: supabase.storage.from('workspaces').remove()
            Note right of API: Uses Admin client for privileged deletes
            DB-->>API: storage result
        end

        API-->>SDK: 200 OK (JSON response)
        SDK-->>UI: Return typed data
    else Unauthorized / Forbidden
        API-->>SDK: 401 / 403 Error
        SDK-->>UI: Throw error / Toast notification
    end

    Note over UI,SDK: UI updates state via React Query / router.refresh()
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@khangronky khangronky marked this pull request as ready for review March 20, 2026 17:09
@coderabbitai coderabbitai bot added enhancement New feature or request platform Infrastructure changes tudo labels Mar 20, 2026
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

@khangronky khangronky requested a review from vhpx March 20, 2026 17:25
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 36

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (17)
apps/web/src/app/api/v1/workspaces/[wsId]/roles/route.ts (2)

11-13: ⚠️ Potential issue | 🟠 Major

Pass request to createClient to support mobile Bearer token auth.

The handler uses createClient() without the request parameter, which prevents mobile clients using Authorization headers from authenticating. This also affects the POST handler at line 33.

🔧 Proposed fix
-export async function GET(_: Request, { params }: Params) {
-  const supabase = await createClient();
+export async function GET(request: Request, { params }: Params) {
+  const supabase = await createClient(request);

And for the POST handler:

-export async function POST(req: Request, { params }: Params) {
-  const supabase = await createClient();
+export async function POST(req: Request, { params }: Params) {
+  const supabase = await createClient(req);

As per coding guidelines: "For API routes that must serve web sessions and mobile Bearer tokens, initialize Supabase with createClient(request) so Authorization headers are honored."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/v1/workspaces/`[wsId]/roles/route.ts around lines 11 -
13, The GET and POST route handlers call createClient() without the incoming
Request, so mobile Bearer Authorization headers aren’t honored; update both
handlers to call createClient(request) (i.e., pass the handler's Request object
into createClient) so Supabase will accept Authorization headers for mobile
auth, and keep the rest of the logic in the GET and POST functions (e.g., the
existing params/wsId handling) unchanged.

15-19: ⚠️ Potential issue | 🟡 Minor

Add normalizeWorkspaceId call to handle "personal" workspace identifier.

The route uses wsId directly without normalization. Import normalizeWorkspaceId from @tuturuuu/utils/workspace-helper and normalize the ID before querying, consistent with other workspace-scoped routes in the codebase.

Current code (lines 15-19)
  const { data, error } = await supabase
    .from('workspace_roles')
    .select('*')
    .eq('ws_id', wsId)
    .order('name', { ascending: true });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/v1/workspaces/`[wsId]/roles/route.ts around lines 15 -
19, The route currently uses the raw wsId when querying workspace_roles; import
normalizeWorkspaceId from '@tuturuuu/utils/workspace-helper' and call it on wsId
before the Supabase query (replace usages of wsId in the .eq('ws_id', ...) call
with the normalized value), ensuring the normalized ID is used when building the
query in the handler in route.ts.
apps/web/src/app/api/v1/workspaces/[wsId]/roles/[roleId]/members/route.ts (2)

4-8: ⚠️ Potential issue | 🔴 Critical

Extract wsId from params and verify role belongs to workspace.

The route path includes [wsId] but the handler only extracts roleId. Without verifying that the role belongs to the specified workspace, this could allow unauthorized cross-workspace access.

🔧 Proposed fix
 interface Params {
   params: Promise<{
+    wsId: string;
     roleId: string;
   }>;
 }

-export async function GET(_: Request, { params }: Params) {
-  const supabase = await createClient();
-  const { roleId } = await params;
+export async function GET(request: Request, { params }: Params) {
+  const supabase = await createClient(request);
+  const { wsId, roleId } = await params;

   const { data, error, count } = await supabase
     .from('workspace_role_members')
     .select(
       '...users!inner(id, display_name, full_name, avatar_url, ...user_private_details(email))',
       {
         count: 'exact',
       }
     )
-    .eq('role_id', roleId);
+    .eq('role_id', roleId)
+    .eq('ws_id', wsId);

As per coding guidelines: "For API routes that must serve web sessions and mobile Bearer tokens, initialize Supabase with createClient(request) so Authorization headers are honored."

Also applies to: 10-12

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/v1/workspaces/`[wsId]/roles/[roleId]/members/route.ts
around lines 4 - 8, The Params interface and route handler currently only
extract roleId; update Params to include wsId and in the route handler extract
both params.params.wsId and params.params.roleId, initialize Supabase with
createClient(request) so Authorization headers (web sessions and mobile Bearer
tokens) are honored, then query the roles table (e.g., SELECT * FROM roles WHERE
id = roleId) and verify the returned role's workspace_id (or ws_id) equals the
extracted wsId before returning members; if the check fails, return a 403/404.
Ensure you reference the Params interface and the route handler in route.ts and
apply the same change to the related handlers noted in the file.

35-53: ⚠️ Potential issue | 🟠 Major

POST handler also needs createClient(request) and workspace verification.

The same issues apply to the POST handler: it should use createClient(req) and verify the role belongs to the workspace before inserting members.

🔧 Proposed fix
-export async function POST(req: Request, { params }: Params) {
-  const supabase = await createClient();
-  const { roleId } = await params;
+export async function POST(req: Request, { params }: Params) {
+  const supabase = await createClient(req);
+  const { wsId, roleId } = await params;
+
+  // Verify role belongs to workspace
+  const { data: role, error: verifyError } = await supabase
+    .from('workspace_roles')
+    .select('id')
+    .eq('id', roleId)
+    .eq('ws_id', wsId)
+    .maybeSingle();
+
+  if (verifyError || !role) {
+    return NextResponse.json({ message: 'Role not found' }, { status: 404 });
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/v1/workspaces/`[wsId]/roles/[roleId]/members/route.ts
around lines 35 - 53, Update the POST handler to call createClient(req) (not
createClient()) and properly read params (extract wsId and roleId from params,
don't await params), then verify the role belongs to the workspace by querying
workspace_roles for role_id = roleId and workspace_id = wsId before inserting;
if the role is missing return an appropriate 404/403 NextResponse. After
verification, perform the insert into workspace_role_members (as currently
done), handle and return any insertion errors (roleError) in the response, and
return success when complete.
apps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/linked-modules/page.tsx (1)

161-164: ⚠️ Potential issue | 🟠 Major

Bug: Recursive retry calls wrong function.

The retry logic calls getData(wsId, ...) but should call getModules(wsId, ...). This appears to be a pre-existing bug but will cause incorrect behavior on retry.

🐛 Proposed fix
   const { data, error, count } = await queryBuilder;
   if (error) {
     if (!retry) throw error;
-    return getData(wsId, { q, pageSize, retry: false });
+    return getModules(wsId, { q, pageSize, retry: false });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/app/`[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/linked-modules/page.tsx
around lines 161 - 164, The retry branch currently calls getData(wsId, { q,
pageSize, retry: false }) but should call getModules(wsId, { q, pageSize, retry:
false }) so retries invoke the correct fetch function; update the error handling
block to call getModules instead of getData (referencing the getData and
getModules identifiers) and keep the same parameters and retry: false behavior.
apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/layout.tsx (1)

184-191: 🧹 Nitpick | 🔵 Trivial

createDynamicClient usage is flagged as deprecated.

The getResources function uses createDynamicClient() which is deprecated per coding guidelines. Consider migrating this to use the internal API pattern in a follow-up.

Based on learnings: "When tuturuuu/supabase/next/client or createDynamicClient is flagged as deprecated, migrate to an authenticated Internal API route and consume it through tuturuuu/internal-api".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/app/`[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/layout.tsx
around lines 184 - 191, getResources currently calls the deprecated
createDynamicClient(); replace this client usage by calling an authenticated
internal API route that performs the Supabase storage.list operation
server-side. Create an internal API endpoint (e.g.
/api/internal/workspace-resources) that accepts the path, uses the
non-deprecated server-side Supabase client to list storage from 'workspaces',
and returns the data/error; then update getResources to call that internal
endpoint via the tuturuuu/internal-api fetch pattern instead of
createDynamicClient. Ensure the endpoint handles auth and propagates errors so
getResources can return the same data shape.
apps/web/src/app/[locale]/(dashboard)/[wsId]/mail/client.tsx (1)

48-61: 🧹 Nitpick | 🔵 Trivial

Consider using useInfiniteQuery for pagination.

The loadMore callback performs client-side data fetching outside of TanStack Query. While this pattern is common for infinite scroll, TanStack Query's useInfiniteQuery would provide better caching, error handling, and state management for paginated data.

♻️ Suggested approach using useInfiniteQuery
const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['workspace-emails', wsId],
  queryFn: ({ pageParam = 1 }) => listWorkspaceEmails(wsId, { page: pageParam, pageSize: PAGE_SIZE }),
  getNextPageParam: (lastPage, pages) => 
    lastPage.length === PAGE_SIZE ? pages.length + 1 : undefined,
  initialData: { pages: [data], pageParams: [1] },
});

As per coding guidelines: "All client-side fetching/mutation must use useQuery/useMutation from TanStack Query."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/`[locale]/(dashboard)/[wsId]/mail/client.tsx around lines 48
- 61, Replace the manual loadMore handler that calls getWorkspaceMails and
mutates setEmails/page/hasMore with a TanStack Query useInfiniteQuery
implementation: create an infinite query keyed by ['workspace-emails', wsId]
whose queryFn calls getWorkspaceMails(wsId, { page: pageParam, pageSize:
PAGE_SIZE }), provide getNextPageParam that returns next page when
lastPage.length === PAGE_SIZE, and wire
fetchNextPage/hasNextPage/isFetchingNextPage to trigger loading more instead of
calling loadMore; remove the client-side page/hasMore state and use the
paginated data structure returned by useInfiniteQuery to render emails. Ensure
you reference fetchNextPage where loadMore was used and use the query result
pages to derive the flattened email list for rendering.
packages/ui/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx (1)

4-5: ⚠️ Potential issue | 🟠 Major

Migrate deprecated Supabase client and consolidate calendar disconnect into an internal API route.

This shared UI component (packages/ui/) still uses the deprecated createClient() at line 4 to call auth.getUser() within a useEffect hook (lines 92–104). Per the migration pattern, replace this browser Supabase call with an authenticated internal API helper that returns whether the current user is a Tuturuuu employee.

Additionally, the disconnect flow at lines 346–367 performs a raw GET and then a fan-out DELETE sequence on individual calendar connections without consolidated error handling. The Promise.all on individual DELETE requests at line 362–367 does not validate that each DELETE succeeded, risking orphaned connection records if one or more fail. Consolidate the entire disconnect and cleanup into a single authenticated API route and consume it through the internal API pattern, which also simplifies error handling and transaction safety.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/ui/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx`
around lines 4 - 5, The component still imports and uses the deprecated
createClient() to call auth.getUser() inside the useEffect and also performs
per-connection GET + Promise.all DELETEs for calendar disconnects; replace the
client-side auth.getUser() call with an authenticated internal API helper (e.g.,
an internal endpoint and helper like isEmployee/getCurrentUser for the existing
useEffect) and call that from the component instead of createClient(); remove
the per-connection fan-out DELETE logic and instead implement a single
authenticated internal API route (e.g., calendar.disconnectAll or
calendar.cleanupConnections) that performs the DELETEs transactionally and
returns success/failure, then call that single route from the component’s
disconnect handler and surface its error state—update references to
createClient(), the useEffect that reads auth.getUser(), and the disconnect
handler that currently uses Promise.all to use the new internal API helpers.
packages/ui/src/components/ui/custom/education/modules/resources/delete-resource.tsx (1)

9-10: ⚠️ Potential issue | 🟡 Minor

Thread wsId explicitly into DeleteResourceButton instead of deriving it from the path.

The component receives a workspace-prefixed path (${wsId}/courses/...) and extracts the workspace ID via path.split('/')[0]. While this works in the current call site, it creates an implicit contract that all paths must be workspace-prefixed. Since wsId is available at the call site (from route params), pass it explicitly for clarity and to align with the codebase pattern of threading workspace IDs explicitly through helper functions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/ui/src/components/ui/custom/education/modules/resources/delete-resource.tsx`
around lines 9 - 10, The component DeleteResourceButton currently derives wsId
from the path via path.split('/')[0]; instead, add an explicit wsId prop (e.g.,
DeleteResourceButton({ wsId, path })) and stop extracting workspace id inside
the component; update the component to use the passed wsId wherever workspace
identification is needed (remove path.split usage) and change all call sites to
pass the route param wsId (from useRouter or parent props) so the workspace id
is threaded explicitly consistent with the codebase.
apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/migrations/hooks/use-migration-state.ts (1)

338-374: ⚠️ Potential issue | 🟠 Major

Replace this useEffect with useQuery to eliminate the stale-response race condition.

This effect performs client-side data fetching in useEffect, which violates the mandatory standard. Additionally, when targetWorkspaceId changes, an older getWorkspace(wsId) call can resolve after the newer fetch and overwrite the displayed name with stale data. Migrate to TanStack Query's useQuery with proper debouncing, or add a stale-response guard (e.g., capture and check the current targetWorkspaceId before setting state).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/app/`[locale]/(dashboard)/[wsId]/(workspace-settings)/migrations/hooks/use-migration-state.ts
around lines 338 - 374, Replace the client-side
useEffect/fetchTargetWorkspaceName flow with a TanStack Query useQuery keyed on
targetWorkspaceId to handle loading and caching (or, if you prefer to keep
manual fetching, add a stale-response guard): remove the setTimeout/useEffect
block and instead call useQuery({ queryKey: ['workspaceName',
targetWorkspaceId], queryFn: () => getWorkspace(targetWorkspaceId), enabled:
Boolean(targetWorkspaceId), staleTime: 0, refetchOnWindowFocus: false }) and
derive setTargetWorkspaceName and setLoadingTargetWorkspaceName from the query
state; if you choose the guard approach keep
fetchTargetWorkspaceName/getWorkspace but capture the current targetWorkspaceId
in a local variable before awaiting and verify it still equals the latest
targetWorkspaceId before calling setTargetWorkspaceName or
setLoadingTargetWorkspaceName to prevent stale-response overwrites.
apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/content-editor.tsx (1)

26-41: 🧹 Nitpick | 🔵 Trivial

Consider debouncing the save operation.

The saveContentToDB function is called on every onChange event, which fires on each keystroke. This can cause:

  1. Excessive API calls overwhelming the server
  2. Race conditions where rapid changes cause out-of-order saves
  3. Poor UX if the network is slow
♻️ Suggested debounce implementation
+import { useDebouncedCallback } from 'use-debounce';
 import { useState } from 'react';

 export function ModuleContentEditor({ wsId, courseId, moduleId, content }: Props) {
   const [post, setPost] = useState<JSONContent | null>(content || null);
   const t = useTranslations();

+  const debouncedSave = useDebouncedCallback(
+    async (content: JSONContent | null) => {
+      try {
+        await updateWorkspaceCourseModule(wsId, moduleId, {
+          course_id: courseId,
+          content,
+        });
+      } catch (error) {
+        console.log(error);
+        toast.error(t('common.error_saving_content'));
+      }
+    },
+    1000 // 1 second debounce
+  );

   const onChange = (content: JSONContent | null) => {
     setPost(content);
-    saveContentToDB(content);
+    debouncedSave(content);
   };

-  const saveContentToDB = async (content: JSONContent | null) => {
-    try {
-      await updateWorkspaceCourseModule(wsId, moduleId, {
-        course_id: courseId,
-        content,
-      });
-    } catch (error) {
-      console.log(error);
-      toast.error(t('common.error_saving_content'));
-    }
-  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/app/`[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/content-editor.tsx
around lines 26 - 41, onChange currently calls saveContentToDB on every
keystroke causing excessive API calls and potential race conditions; wrap the
save logic in a debounced function (e.g., create a debouncedSaveContent using
lodash.debounce or a useDebouncedCallback hook) and call that from onChange
while still calling setPost immediately; implement the debounced function with
useCallback/useRef so it preserves identity, cancel/flush it in a cleanup effect
(on unmount) and consider flushing on explicit save or blur to ensure latest
content is persisted; keep updateWorkspaceCourseModule, wsId, moduleId and
courseId usage unchanged inside the debounced call.
packages/ui/src/components/ui/finance/recurring/recurring-transactions-page.tsx (1)

294-294: 🧹 Nitpick | 🔵 Trivial

Consider typing the upcoming transaction instead of using any.

The transaction: any parameter weakens type safety. Consider defining or reusing an interface for the upcoming transaction shape returned by listUpcomingRecurringTransactions.

-{upcomingTransactions.map((transaction: any, index: number) => (
+{upcomingTransactions.map((transaction, index) => (

If the return type of listUpcomingRecurringTransactions is properly typed, TypeScript can infer the type here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/ui/src/components/ui/finance/recurring/recurring-transactions-page.tsx`
at line 294, The map callback currently types its parameter as "transaction:
any", losing type safety; replace any by a concrete type (either create/reuse an
interface such as UpcomingRecurringTransaction or use the return type from
listUpcomingRecurringTransactions) so TypeScript can infer/validate fields used
inside upcomingTransactions.map in recurring-transactions-page.tsx; update the
upstream function signature for listUpcomingRecurringTransactions if needed or
import the existing type and use it in the map callback and related variables.
apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/role-members.tsx (1)

82-117: 🛠️ Refactor suggestion | 🟠 Major

Mutation handlers still use raw fetch() instead of useMutation with internal API helpers.

The read operations have been migrated to use @tuturuuu/internal-api helpers, but handleAddMember and handleRemoveMember still use raw fetch() calls. This is inconsistent with the migration pattern and violates the coding guideline requiring TanStack Query for all client-side mutations.

Consider creating internal API helpers like addRoleMember(wsId, roleId, memberIds) and removeRoleMember(wsId, roleId, memberId) in @tuturuuu/internal-api/roles, then wrap them with useMutation.

♻️ Suggested refactor pattern
+import { addRoleMember, removeRoleMember } from '@tuturuuu/internal-api/roles';
+
+const addMemberMutation = useMutation({
+  mutationFn: (memberId: string) => addRoleMember(wsId, roleId!, [memberId]),
+  onSuccess: () => {
+    toast.success(t('ws-roles.member_added_successfully'));
+    roleMembersQuery.refetch();
+    setAddMemberQuery('');
+  },
+  onError: () => {
+    toast.error(t('ws-roles.failed_to_add_member'));
+  },
+});
+
+const removeMemberMutation = useMutation({
+  mutationFn: (memberId: string) => removeRoleMember(wsId, roleId!, memberId),
+  onSuccess: () => {
+    toast.success(t('ws-roles.member_removed_successfully'));
+    roleMembersQuery.refetch();
+  },
+  onError: () => {
+    toast.error(t('ws-roles.failed_to_remove_member'));
+  },
+});
-
-const handleAddMember = async (memberId: string) => {
-  const res = await fetch(
-    `/api/v1/workspaces/${wsId}/roles/${roleId}/members`,
-    ...
-  );
-  ...
-};
-
-const handleRemoveMember = async (memberId: string) => {
-  const res = await fetch(
-    `/api/v1/workspaces/${wsId}/roles/${roleId}/members/${memberId}`,
-    ...
-  );
-  ...
-};

As per coding guidelines: "All client-side fetching/mutation must use useQuery/useMutation from TanStack Query" and "When client or shared UI code needs app API access, add or extend helpers in packages/internal-api/src/*".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/app/`[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/role-members.tsx
around lines 82 - 117, Replace the raw fetch calls in handleAddMember and
handleRemoveMember with TanStack Query mutations: add two API helpers (e.g.,
addRoleMember(wsId, roleId, memberIds) and removeRoleMember(wsId, roleId,
memberId) in `@tuturuuu/internal-api/roles`) and then use useMutation to call
those helpers from this component; on success call roleMembersQuery.refetch()
(and show the same toast messages), and on error show the existing error
toasts—update handleAddMember and handleRemoveMember to trigger the
corresponding mutations instead of using fetch().
packages/ui/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx (1)

25-44: ⚠️ Potential issue | 🟡 Minor

Silent error handling provides no user feedback on failure.

The function catches errors and logs to console but provides no user-facing feedback. Consider adding a toast notification on failure:

♻️ Suggested improvement
+import { toast } from '@tuturuuu/ui/sonner';
+
   const updateYoutubeLinks = async (
     moduleId: string,
     courseId: string,
     links: string[]
   ) => {
     setLoading(true);
     try {
       await updateWorkspaceCourseModule(wsId, moduleId, {
         course_id: courseId,
         youtube_links: links,
       });
       router.refresh();
       setLoading(false);
-      return null;
     } catch (error) {
       console.error('error', error);
+      toast.error('Failed to delete link');
       setLoading(false);
-      return null;
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/ui/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx`
around lines 25 - 44, The updateYoutubeLinks function currently swallows errors
by only logging and returning null; update it to show user-facing feedback by
invoking the project's toast/notification utility on failure (and optionally on
success). Specifically, inside the catch block of updateYoutubeLinks (which
calls updateWorkspaceCourseModule and router.refresh), replace or augment
console.error with a call to the app's toast (or notification) API with a clear
error message and perhaps error.message, ensure setLoading(false) still runs,
and consider a success toast after a successful update before or after
router.refresh so users get visible confirmation.
packages/ui/src/components/ui/legacy/calendar/settings/hour-settings.tsx (2)

66-90: ⚠️ Potential issue | 🟡 Minor

Missing state rollback on API failure for optimistic updates.

The handler updates local state optimistically before the API call, but doesn't restore the previous value if the update fails. The user sees the new value even after an error toast.

Proposed fix pattern
   const handlePersonalHoursChange = async (
     newHours?: WeekTimeRanges | null
   ) => {
     if (!newHours) {
       toast.error('No hours provided');
       return;
     }

+    const previousHours = value.personalHours;
     setValue((prev) => ({
       ...prev,
       personalHours: newHours,
     }));

     try {
       await updateWorkspaceCalendarHours(wsId, {
         type: 'PERSONAL',
         hours: newHours,
       });
       toast.success('Personal hours updated');
     } catch (error) {
       console.error('Error updating personal hours:', error);
       toast.error('Failed to update personal hours');
+      setValue((prev) => ({
+        ...prev,
+        personalHours: previousHours,
+      }));
       return;
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/ui/legacy/calendar/settings/hour-settings.tsx`
around lines 66 - 90, The handler handlePersonalHoursChange performs an
optimistic update via setValue before calling updateWorkspaceCalendarHours(wsId,
...), but on API failure it never restores the previous personalHours; capture
the previous state (prev.personalHours) before calling setValue, then in the
catch block call setValue to rollback to that captured value and still show the
error toast/console.error; ensure the captured value is referenced inside the
async flow so the state is restored only on failure.

36-60: ⚠️ Potential issue | 🟠 Major

Replace useEffect data fetching with TanStack Query.

The component uses useEffect for fetching calendar hours, which violates the project's data fetching guidelines. Migrate to useQuery for better caching and state management.

As per coding guidelines: "NEVER use useEffect for data fetching. TanStack Query (useQuery/useMutation) is the mandatory standard."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/ui/legacy/calendar/settings/hour-settings.tsx`
around lines 36 - 60, The useEffect-based fetchHours should be replaced with a
TanStack Query hook: remove the useEffect and fetchHours, and instead call
useQuery (e.g., useQuery(['workspaceCalendarHours', wsId], () =>
getWorkspaceCalendarHours(wsId))) to load data; on success map the returned data
into setValue (use the query's onSuccess) and on error show the toast.error and
console.error via the query's onError or error state; ensure you reference
getWorkspaceCalendarHours, setValue, and wsId and use the query key
['workspaceCalendarHours', wsId] so caching/invalidations work correctly.
apps/web/src/app/[locale]/(dashboard)/[wsId]/posts/filters.tsx (1)

36-55: ⚠️ Potential issue | 🟠 Major

Replace useEffect data fetching with TanStack Query.

Using useEffect for data fetching violates project guidelines. Migrate to useQuery for proper caching, error handling, and loading states.

Proposed refactor using useQuery
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
+import { useQuery } from '@tanstack/react-query';

 // In the component:
-  const [userGroups, setUserGroups] = useState<UserGroup[]>([]);
-  const [excludedUserGroups, setExcludedUserGroups] = useState<UserGroup[]>([]);
-  const [users, setUsers] = useState<WorkspaceUser[]>([]);
-
-  useEffect(() => {
-    const loadData = async () => {
-      try {
-        const [userGroupsData, excludedGroupsData, usersData] =
-          await Promise.all([
-            getUserGroups(wsId),
-            getExcludedUserGroups(wsId, searchParams),
-            getUsers(wsId),
-          ]);
-
-        setUserGroups(userGroupsData.data);
-        setExcludedUserGroups(excludedGroupsData.data);
-        setUsers(usersData.data);
-      } catch (error) {
-        console.error('Failed to load filter data:', error);
-      }
-    };
-
-    loadData();
-  }, [wsId, searchParams]);
+  const includedGroupsParam = Array.isArray(searchParams.includedGroups)
+    ? searchParams.includedGroups
+    : searchParams.includedGroups
+      ? [searchParams.includedGroups]
+      : [];
+
+  const { data } = useQuery({
+    queryKey: ['posts-filter-options', wsId, includedGroupsParam],
+    queryFn: async () => {
+      const [base, excluded] = await Promise.all([
+        getPostsFilterOptions(wsId),
+        getPostsFilterOptions(wsId, { includedGroups: includedGroupsParam }),
+      ]);
+      return {
+        userGroups: base.userGroups as UserGroup[],
+        excludedUserGroups: excluded.excludedUserGroups as UserGroup[],
+        users: base.users as WorkspaceUser[],
+      };
+    },
+  });
+
+  const userGroups = data?.userGroups ?? [];
+  const excludedUserGroups = data?.excludedUserGroups ?? [];
+  const users = data?.users ?? [];

As per coding guidelines: "NEVER use useEffect for data fetching. TanStack Query (useQuery/useMutation) is the mandatory standard."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/`[locale]/(dashboard)/[wsId]/posts/filters.tsx around lines
36 - 55, Replace the useEffect/loadData pattern with TanStack Query hooks:
remove the useEffect and the loadData async function and instead create useQuery
(or useQueries) hooks that call getUserGroups(wsId), getExcludedUserGroups(wsId,
searchParams), and getUsers(wsId); map their results into
setUserGroups/setExcludedUserGroups/setUsers or preferably derive state directly
from query.data, handle loading/error states from each query, and ensure each
query key includes wsId and searchParams so caching/invalidation works
correctly; keep function names getUserGroups, getExcludedUserGroups, getUsers
and state setters (or replace them) and ensure queries refetch when wsId or
searchParams change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1605837b-d647-4a82-8eb5-c218411be7ff

📥 Commits

Reviewing files that changed from the base of the PR and between 18e6c06 and 67f39c4.

📒 Files selected for processing (65)
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/components/mira-gateway-models.ts
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/components/mira-model-selector/use-mira-model-selector-data.ts
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/(dashboard)/permission-setup-banner.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/migrations/hooks/use-migration-state.ts
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/index.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/roles/form/role-members.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/content-editor.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/page.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/flashcards/client-flashcards.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/layout.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/quizzes/client-quizzes.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/youtube-links/page.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/linked-modules/linker.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/linked-modules/page.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/linked-modules/row-actions.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/inventory/promotions/settings-form.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/mail/client.tsx
  • apps/web/src/app/[locale]/(dashboard)/[wsId]/posts/filters.tsx
  • apps/web/src/app/api/v1/workspaces/[wsId]/ai/model-favorites/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/finance/filter-users/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/finance/recurring-transactions/[recurringTransactionId]/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/finance/recurring-transactions/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/finance/recurring-transactions/upcoming/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/mail/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/posts/filter-options/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/promotions/referral-settings/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/quiz-sets/[setId]/modules/[moduleId]/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/quiz-sets/[setId]/modules/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/roles/[roleId]/members/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/roles/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/settings/permissions/check/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/settings/permissions/setup-status/route.ts
  • apps/web/src/app/api/v1/workspaces/[wsId]/storage/object/route.ts
  • apps/web/src/app/api/workspaces/[wsId]/transactions/category-breakdown/route.ts
  • apps/web/src/app/api/workspaces/[wsId]/transactions/spending-trends/route.ts
  • apps/web/src/lib/calendar-preferences-provider.tsx
  • packages/internal-api/package.json
  • packages/internal-api/src/ai.ts
  • packages/internal-api/src/education.ts
  • packages/internal-api/src/finance.ts
  • packages/internal-api/src/index.ts
  • packages/internal-api/src/mail.ts
  • packages/internal-api/src/promotions.ts
  • packages/internal-api/src/roles.ts
  • packages/internal-api/src/settings.ts
  • packages/internal-api/src/users.ts
  • packages/ui/src/components/ui/custom/education/modules/module-toggle.tsx
  • packages/ui/src/components/ui/custom/education/modules/resources/delete-resource.tsx
  • packages/ui/src/components/ui/custom/education/modules/resources/file-display.tsx
  • packages/ui/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx
  • packages/ui/src/components/ui/finance/analytics/category-spending-chart.tsx
  • packages/ui/src/components/ui/finance/analytics/spending-trends-chart.tsx
  • packages/ui/src/components/ui/finance/recurring/form.tsx
  • packages/ui/src/components/ui/finance/recurring/recurring-transactions-page.tsx
  • packages/ui/src/components/ui/finance/transactions/category-filter.tsx
  • packages/ui/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx
  • packages/ui/src/components/ui/finance/transactions/period-charts/category-donut-chart.tsx
  • packages/ui/src/components/ui/finance/transactions/period-charts/period-breakdown-panel.tsx
  • packages/ui/src/components/ui/finance/transactions/user-filter.tsx
  • packages/ui/src/components/ui/finance/wallets/walletId/wallet-role-access.tsx
  • packages/ui/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx
  • packages/ui/src/components/ui/legacy/calendar/settings/hour-settings.tsx
  • packages/ui/src/hooks/use-workspace-permission.ts
  • packages/ui/src/hooks/use-workspace-user.ts
  • scripts/internal-api-migration.test.js

@vhpx vhpx merged commit 507efd6 into main Mar 20, 2026
63 checks passed
@vhpx vhpx deleted the refactor/migration/supabase-client branch March 20, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request platform Infrastructure changes tudo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants