Skip to content

Conversation

@jezweb
Copy link

@jezweb jezweb commented Nov 8, 2025

Problem

The createCategory action was throwing errors instead of returning a success/error object, making it inconsistent with other mutation actions in the codebase.

Inconsistency:

  • deleteTodoAction → Returns { success, error? }
  • updateTodoFieldAction → Returns { success, data?, error? }
  • createCategoryThrows errors

This meant client code needed try/catch blocks for createCategory but not for other mutations.

Solution

Standardize createCategory to return a success/error object pattern, matching other programmatic mutations.

Changes

Action (create-category.action.ts):

  • Before: Promise<Category> - throws on error
  • After: Promise<{ success: boolean, data?: Category, error?: string }>
  • Return error objects instead of throwing
  • Added authentication error handling for consistency

Component (add-category.tsx):

  • Before: Required try/catch block
  • After: Simple if-check on result.success
  • Cleaner, more predictable error handling

Pattern Strategy

The codebase now follows clear use-case patterns:

1️⃣ Form Actions with Redirect → Throw pattern

  • createTodoAction
  • updateTodoAction
  • Uses redirect() which throws NEXT_REDIRECT

2️⃣ Programmatic Mutations → Success/error object

  • deleteTodoAction
  • updateTodoFieldAction
  • createCategory ✓ (fixed in this PR)
  • No try/catch needed on client

3️⃣ Data Fetching → Silent failure (return empty)

  • getAllTodos
  • getTodoById
  • getAllCategories

Benefits

  • ✅ Consistent error handling across all programmatic mutations
  • ✅ Cleaner client code (no try/catch needed)
  • ✅ Better error propagation to UI toast notifications
  • ✅ Matches established patterns in the codebase

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Full CRM: Contacts and Deals UIs (list, create, edit, delete), contact tagging, tag management, and a seed-data command.
    • New CRM dashboard with metrics and quick-action cards; navigation updated to include Contacts and Deals.
  • Bug Fixes

    • Improved error handling for category creation with clearer failure responses.
    • Removed success notification after creating a category.

Update createCategory action to return success/error object instead of throwing errors, making it consistent with other mutation actions like deleteTodoAction and updateTodoFieldAction.

Changes:
- create-category.action.ts: Return { success, data?, error? } instead of throwing
- add-category.tsx: Update to handle new response structure without try/catch
- Added authentication error handling for consistency

Benefits:
- Consistent error handling across all non-form mutation actions
- Cleaner client code (no try/catch needed)
- Better error propagation to UI
- Matches established pattern used by delete and update field actions

This brings all programmatic mutations to use the same response pattern, while form actions with redirect() continue to use throw pattern (as required by Next.js).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Nov 8, 2025

Walkthrough

Major CRM feature additions: contacts, tags, deals, dashboard metrics, seed data, DB migrations/schema updates, routes, UI components, and many server actions. Also a small todos category action refactor from throw-based to result-based responses and related UI adjustment.

Changes

Cohort / File(s) Summary
Todos: category action & UI
src/modules/todos/actions/create-category.action.ts, src/modules/todos/components/add-category.tsx
Refactor createCategory to return { success:boolean; data?:Category; error?:string } instead of throwing; Add component result-based handling and error toast path; remove success toast.
CRM DB schema & migrations
src/drizzle/0001_fantastic_captain_flint.sql, src/drizzle/meta/0001_snapshot.json, src/drizzle/meta/_journal.json, src/db/schema.ts
Add contacts, contact_tags, contacts_to_tags, deals tables; update snapshot/journal; export new schema tables from central schema file.
Database config & deployment
drizzle.config.ts, wrangler.jsonc
Update D1 database_id and binding/database_name to new CRM target.
Seeding & scripts
src/lib/seed.ts, package.json
Add CRM seed script (seedCRM) and npm script db:seed to run it.
Contacts: schemas, routes, actions, components, pages
src/modules/contacts/schemas/contact.schema.ts, src/modules/contacts/actions/*.ts (create-contact.action.ts, get-contacts.action.ts, update-contact.action.ts, delete-contact.action.ts, tag-management.actions.ts), src/modules/contacts/components/* (contact-form.tsx, contact-card.tsx, delete-contact.tsx), src/modules/contacts/contacts.route.ts, src/app/dashboard/contacts/*.tsx (page.tsx, new/page.tsx, [id]/edit/page.tsx)
New contacts domain: Drizzle table + Zod schemas and types; CRUD server actions with auth/ownership checks, tag management, query with tags, routes helper; client components (form, card, delete UI) and app pages for list/new/edit.
Deals: schemas, models, routes, actions, components, pages
src/modules/deals/schemas/deal.schema.ts, src/modules/deals/models/deal.enum.ts, src/modules/deals/actions/*.ts (create-deal.action.ts, get-deals.action.ts, update-deal.action.ts, delete-deal.action.ts), src/modules/deals/components/* (deal-form.tsx, deal-card.tsx, delete-deal.tsx), src/modules/deals/deals.route.ts, src/app/dashboard/deals/*.tsx (page.tsx, new/page.tsx, [id]/edit/page.tsx)
New deals domain: table + Zod schemas/types, enum of stages, CRUD server actions parsing/normalizing FormData, ownership checks, UI components (form, card, delete), routes helpers, and pages (board, new, edit).
Dashboard: metrics & UI
src/modules/dashboard/actions/get-dashboard-metrics.action.ts, src/modules/dashboard/components/* (stat-card.tsx, quick-action-card.tsx), src/modules/dashboard/dashboard.page.tsx
Add getDashboardMetricsAction returning typed metrics; new StatCard and QuickActionCard components; replace Todo-centric dashboard with CRM metrics grid and quick actions linking to contacts/deals.
Navigation & app wiring
src/components/navigation.tsx, src/app/dashboard/* (dashboard integration)
Add Contacts and Deals nav items and update app title to "CRM"; wire dashboard to new metrics and routes.
Drizzle config & snapshots
src/drizzle/meta/0001_snapshot.json, src/drizzle/0001_fantastic_captain_flint.sql
Snapshot and migration reflecting new CRM schema and metadata.
Docs & process
README.md, SESSION.md, docs/DATABASE_SCHEMA.md, docs/IMPLEMENTATION_PHASES.md, docs/TESTING.md
Large documentation additions: CRM feature docs, session log, full DB schema doc, phased implementation plan, and testing guide (some duplicated sections observed).

Sequence Diagram(s)

%%{init: {"themeVariables": {"sequenceActorBorder":"#6b7280","actorBackground":"#f8fafc"}}}%%
sequenceDiagram
    participant User
    participant UI as Client Components
    participant Action as Server Actions
    participant DB as Database (D1/Drizzle)

    User->>UI: submit Contact/Deal form
    UI->>Action: call create/update action (FormData)
    Note right of Action: requireAuth, parse & validate (Zod), ownership checks
    Action->>DB: insert/update rows (contacts/deals, tags, junctions)
    DB-->>Action: success / error
    alt success
        Action->>Action: revalidatePath("/dashboard/...") 
        Action-->>UI: redirect / success response
        UI->>User: show updated list / navigate
    else error
        Action-->>UI: throw or return structured error
        UI->>User: show error message
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~180 minutes

  • Rationale: large, heterogeneous changes spanning DB migrations, schema/types, many new server actions with auth/ownership logic, parsing/validation, plus numerous UI components and pages; requires cross-layer validation.
  • Areas needing extra attention:
    • Migration correctness and foreign-key behaviors (ON DELETE/SET NULL) vs. Drizzle schema.
    • Authentication and ownership checks in all server actions (edge cases, NEXT_REDIRECT handling).
    • FormData parsing/normalization (dates, numbers, optional fields) in create/update actions for contacts and deals.
    • Revalidation and redirect usage for Next.js app router (correct paths and SSR/SSR boundaries).
    • Seed script safety (hard-coded userId) and idempotency.
    • README/docs duplication and consistency.

Poem

🐰
From burrows deep of schema ground, I hop —
New contacts, deals, and tags I dropped,
Seeds sown, routes stitched, and dashboards bright,
Migrations march through day and night,
A tiny rabbit cheers the CRM crop!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: standardizing the createCategory action's error response pattern to return a success/error object instead of throwing.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link

@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: 0

🧹 Nitpick comments (1)
src/modules/todos/actions/create-category.action.ts (1)

13-17: Shape the action result as a discriminated union

Using success: boolean with optional data/error allows impossible states (success: true + missing data, or success: false + missing error), so every caller still needs defensive runtime checks. A discriminated union keeps the API intent but lets TypeScript prove that success responses always include payload and error responses always include a message. That removes redundant checks in the client and aligns with the pattern other actions can share. Example:

+type CreateCategoryResult =
+    | { success: true; data: Category }
+    | { success: false; error: string };
+
-export async function createCategory(data: unknown): Promise<{
-    success: boolean;
-    data?: Category;
-    error?: string;
-}> {
+export async function createCategory(
+    data: unknown,
+): Promise<CreateCategoryResult> {

Return statements inside the function already match these shapes, so no further logic changes are needed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a44687a and 3acc741.

📒 Files selected for processing (2)
  • src/modules/todos/actions/create-category.action.ts (2 hunks)
  • src/modules/todos/components/add-category.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/modules/todos/components/add-category.tsx (1)
src/modules/todos/actions/create-category.action.ts (1)
  • createCategory (13-72)
src/modules/todos/actions/create-category.action.ts (1)
src/modules/todos/schemas/category.schema.ts (1)
  • Category (39-39)

jezweb pushed a commit to jezweb/full-flare-stack that referenced this pull request Nov 8, 2025
Jez and others added 9 commits November 8, 2025 20:34
Phase 1 complete: Configured new D1 database and planning docs

Changes:
- Created new D1 database: fullstack-crm (a1d231c7-b7e7-4e7a-aa0e-78a56c2e123a)
- Updated wrangler.jsonc with new database ID
- Updated drizzle.config.ts with new database ID
- Applied initial migrations (user, session, todos, categories tables)
- Added planning documentation:
  * docs/IMPLEMENTATION_PHASES.md (6 phases, ~6-8 hours)
  * docs/DATABASE_SCHEMA.md (CRM schema documentation)
- Verified dev environment works (Next.js + D1 connection)

Next: Phase 2 - Create CRM database schema (contacts, tags, deals)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Phase: 1 - Project Setup
Status: Complete
Session: Completed initial project setup including D1 database creation, configuration updates, migrations, and dev environment verification

Files Changed:
- SESSION.md (session tracking initialized)

Next: Phase 2 - Create CRM database schema (contacts, tags, deals tables)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Phase: 2 - Database Schema
Status: Complete
Session: Created all CRM schemas and applied migration successfully

Files Changed:
- src/modules/contacts/schemas/contact.schema.ts (contacts, tags, junction)
- src/modules/deals/models/deal.enum.ts (deal stages enum)
- src/modules/deals/schemas/deal.schema.ts (deals schema)
- src/db/schema.ts (export new schemas)
- src/drizzle/0001_fantastic_captain_flint.sql (migration)
- SESSION.md (marked Phase 2 complete)

Database Changes:
- Created contacts table (11 columns, user_id FK CASCADE)
- Created contact_tags table (5 columns, user_id FK CASCADE)
- Created contacts_to_tags junction table (composite PK)
- Created deals table (11 columns, contact_id FK SET NULL, user_id FK CASCADE)
- Applied migration locally and verified all tables exist

Next: Phase 3 - Contacts Module (Server Actions + UI components)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Implemented complete contacts CRUD functionality with search and tag management.

Server Actions (5 files):
- create-contact.action.ts: Create new contact with validation
- get-contacts.action.ts: Fetch contacts with search (LIKE queries) and tag filtering
- update-contact.action.ts: Update contact with ownership verification
- delete-contact.action.ts: Delete contact with cascade tag removal
- tag-management.actions.ts: Full tag CRUD (create, list, assign, remove, delete)

UI Components (3 files):
- contact-form.tsx: React Hook Form + Zod validation for create/edit
- contact-card.tsx: Display contact with tags, email, phone, company
- delete-contact.tsx: AlertDialog confirmation for delete

Pages (3 routes):
- /dashboard/contacts: List page with search input
- /dashboard/contacts/new: Create contact form
- /dashboard/contacts/[id]/edit: Edit contact form

Navigation:
- Added "Contacts" link to main navigation with Users icon

Features:
- Search contacts by name, email, company (case-insensitive LIKE)
- Tag support (many-to-many via junction table)
- Ownership verification (users can only edit/delete own contacts)
- Form validation (at least one name required, email format validation)
- Responsive grid layout (1/2/3 columns)
- Empty state with call-to-action

Build: ✅ No TypeScript errors, all pages compile successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Implemented complete deals/pipeline management with Kanban board view.

Server Actions (4 files):
- create-deal.action.ts: Create deal with contact linking
- get-deals.action.ts: Fetch deals with LEFT JOIN to contacts
- update-deal.action.ts: Update deal with ownership verification
- delete-deal.action.ts: Delete deal with ownership check

UI Components (3 files):
- deal-form.tsx: React Hook Form with contact dropdown, currency selector, date picker
- deal-card.tsx: Display deal with currency formatting, contact info, stage badge
- delete-deal.tsx: AlertDialog confirmation for delete

Pages (3 routes):
- /dashboard/deals: Pipeline Kanban board (6 stage columns)
- /dashboard/deals/new: Create deal form with contact selector
- /dashboard/deals/[id]/edit: Edit deal form

Navigation:
- Added "Deals" link to main navigation with TrendingUp icon

Features:
- Pipeline board with 6 stages (Prospecting → Closed Won/Lost)
- Group deals by stage in responsive grid columns
- Link deals to contacts (optional, dropdown selector)
- Currency formatting (AUD, USD, EUR, GBP)
- Expected close date (HTML date input → unix timestamp)
- Pipeline value calculation (excludes closed deals)
- Stage-specific color badges
- Ownership verification on update/delete

Build: ✅ No TypeScript errors, all pages compile successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Phase: 5 - Dashboard Integration
Status: Complete
Session: Transformed dashboard from TodoApp to CRM command center with live metrics

Files Changed:
- src/modules/dashboard/actions/get-dashboard-metrics.action.ts (new, 6 SQL queries)
- src/modules/dashboard/components/stat-card.tsx (new, reusable metric card)
- src/modules/dashboard/components/quick-action-card.tsx (new, action link card)
- src/modules/dashboard/dashboard.page.tsx (redesigned for CRM)
- src/components/navigation.tsx (title: TodoApp → CRM)
- SESSION.md (Phase 5 complete)

Key Features:
- 6 CRM metrics: total contacts, new contacts this month, active deals, pipeline value, deals won this month, win rate
- Responsive 3-column metrics grid (1/2/3 columns)
- Quick action cards: New Contact, New Deal, View Pipeline
- Currency formatting (AUD), percentage formatting
- Semantic colors only (no raw Tailwind colors)
- Graceful error handling (zero values on failure)

Build: ✅ Successful, all pages compile

Next: Phase 6 - Testing & Documentation (seed data, testing guide)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Phase: 6 - Testing & Documentation
Status: Complete
Session: Created comprehensive testing suite, seed data, and documentation

Files Changed:
- src/lib/seed.ts (new, 10 contacts + 5 tags + 5 deals seed data)
- docs/TESTING.md (new, 60+ manual test cases)
- docs/DATABASE_SCHEMA.md (verified accurate)
- README.md (added CRM features section)
- package.json (added db:seed script)
- SESSION.md (Phase 6 complete, all 6 phases done)

Key Deliverables:
- Seed script with realistic CRM data across all pipeline stages
- Comprehensive testing guide with manual test procedures
- Complete documentation suite (schema, implementation, testing)
- README updated with CRM module structure and features

Testing Coverage:
- 60+ test cases covering all CRM features
- Security tests (auth, ownership, data isolation)
- UI/UX tests (forms, responsive, errors)
- Edge cases (data integrity, formatting, empty states)

Documentation:
- Implementation phases guide (IMPLEMENTATION_PHASES.md)
- Database schema with ERD and query patterns (DATABASE_SCHEMA.md)
- Testing checklist with procedures (TESTING.md)
- README with CRM overview and module structure

Build: ✅ Successful, all pages compile

🎉 CRM PROJECT COMPLETE - All 6 phases finished
Ready for deployment and production use

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Issue: Deal form crashed when opening contact dropdown
Root Cause: Radix UI Select doesn't support empty string values
Solution: Use sentinel value "__none__" instead of empty string

Changes:
- src/modules/deals/components/deal-form.tsx
  * Line 133: Updated onValueChange to handle "__none__" sentinel
  * Line 136: Set default value to "__none__" when contactId is undefined
  * Line 144: Changed SelectItem value from "" to "__none__"

- SESSION.md
  * Added "Post-Launch Bug Fixes" section
  * Documented bug fix details

Tested: Deal form now opens without errors, "None" option works correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link

@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: 12

🧹 Nitpick comments (3)
docs/TESTING.md (1)

374-386: Optional: Fix markdown formatting issues.

The static analysis tool flags minor markdown issues:

  1. Lines 374-379: Spaces inside emphasis markers in the pass/fail summary template
  2. Line 384: Missing language identifier for the notes code block

Apply this diff to fix the emphasis marker spacing:

-**Pass/Fail Summary**:
-- Contacts: ___ / ___ passed
-- Deals: ___ / ___ passed
-- Dashboard: ___ / ___ passed
-- Security: ___ / ___ passed
-- UI/UX: ___ / ___ passed
-- Edge Cases: ___ / ___ passed
+**Pass/Fail Summary**:
+- Contacts: `___` / `___` passed
+- Deals: `___` / `___` passed
+- Dashboard: `___` / `___` passed
+- Security: `___` / `___` passed
+- UI/UX: `___` / `___` passed
+- Edge Cases: `___` / `___` passed

And specify the language for the code block:

-```
+```text
 [Add any issues found, bugs to fix, or improvements to make]

</blockquote></details>
<details>
<summary>src/app/dashboard/contacts/[id]/edit/page.tsx (1)</summary><blockquote>

`8-14`: **Make `params` a plain object**

Next.js delivers route params synchronously; typing them as a `Promise` and awaiting them is misleading and can skew future type inference. Please accept `{ params: { id: string } }` and read it directly.  

```diff
-export default async function EditContactPage({
-    params,
-}: {
-    params: Promise<{ id: string }>;
-}) {
-    const { id } = await params;
+export default async function EditContactPage({
+    params,
+}: {
+    params: { id: string };
+}) {
+    const { id } = params;
src/app/dashboard/deals/[id]/edit/page.tsx (1)

9-16: Keep Next.js route params synchronous

Same as the contacts edit page: Next hands you a plain object, not a promise. Adjust the signature so future readers (and TS tooling) see the correct contract.

-export default async function EditDealPage({
-    params,
-}: {
-    params: Promise<{ id: string }>;
-}) {
-    const { id } = await params;
+export default async function EditDealPage({
+    params,
+}: {
+    params: { id: string };
+}) {
+    const { id } = params;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3acc741 and 69fa202.

📒 Files selected for processing (44)
  • README.md (1 hunks)
  • SESSION.md (1 hunks)
  • docs/DATABASE_SCHEMA.md (1 hunks)
  • docs/IMPLEMENTATION_PHASES.md (1 hunks)
  • docs/TESTING.md (1 hunks)
  • drizzle.config.ts (1 hunks)
  • package.json (1 hunks)
  • src/app/dashboard/contacts/[id]/edit/page.tsx (1 hunks)
  • src/app/dashboard/contacts/new/page.tsx (1 hunks)
  • src/app/dashboard/contacts/page.tsx (1 hunks)
  • src/app/dashboard/deals/[id]/edit/page.tsx (1 hunks)
  • src/app/dashboard/deals/new/page.tsx (1 hunks)
  • src/app/dashboard/deals/page.tsx (1 hunks)
  • src/components/navigation.tsx (3 hunks)
  • src/db/schema.ts (1 hunks)
  • src/drizzle/0001_fantastic_captain_flint.sql (1 hunks)
  • src/drizzle/meta/0001_snapshot.json (1 hunks)
  • src/drizzle/meta/_journal.json (1 hunks)
  • src/lib/seed.ts (1 hunks)
  • src/modules/contacts/actions/create-contact.action.ts (1 hunks)
  • src/modules/contacts/actions/delete-contact.action.ts (1 hunks)
  • src/modules/contacts/actions/get-contacts.action.ts (1 hunks)
  • src/modules/contacts/actions/tag-management.actions.ts (1 hunks)
  • src/modules/contacts/actions/update-contact.action.ts (1 hunks)
  • src/modules/contacts/components/contact-card.tsx (1 hunks)
  • src/modules/contacts/components/contact-form.tsx (1 hunks)
  • src/modules/contacts/components/delete-contact.tsx (1 hunks)
  • src/modules/contacts/contacts.route.ts (1 hunks)
  • src/modules/contacts/schemas/contact.schema.ts (1 hunks)
  • src/modules/dashboard/actions/get-dashboard-metrics.action.ts (1 hunks)
  • src/modules/dashboard/components/quick-action-card.tsx (1 hunks)
  • src/modules/dashboard/components/stat-card.tsx (1 hunks)
  • src/modules/dashboard/dashboard.page.tsx (1 hunks)
  • src/modules/deals/actions/create-deal.action.ts (1 hunks)
  • src/modules/deals/actions/delete-deal.action.ts (1 hunks)
  • src/modules/deals/actions/get-deals.action.ts (1 hunks)
  • src/modules/deals/actions/update-deal.action.ts (1 hunks)
  • src/modules/deals/components/deal-card.tsx (1 hunks)
  • src/modules/deals/components/deal-form.tsx (1 hunks)
  • src/modules/deals/components/delete-deal.tsx (1 hunks)
  • src/modules/deals/deals.route.ts (1 hunks)
  • src/modules/deals/models/deal.enum.ts (1 hunks)
  • src/modules/deals/schemas/deal.schema.ts (1 hunks)
  • wrangler.jsonc (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • SESSION.md
🧰 Additional context used
🧬 Code graph analysis (32)
src/app/dashboard/contacts/new/page.tsx (1)
src/modules/contacts/components/contact-form.tsx (1)
  • ContactForm (30-245)
src/modules/contacts/contacts.route.ts (1)
src/modules/todos/todos.route.ts (1)
  • id (5-5)
src/modules/deals/components/deal-form.tsx (6)
src/modules/deals/actions/get-deals.action.ts (1)
  • DealWithContact (10-13)
src/modules/contacts/schemas/contact.schema.ts (1)
  • Contact (108-108)
src/modules/deals/schemas/deal.schema.ts (1)
  • insertDealSchema (39-58)
src/modules/deals/actions/update-deal.action.ts (1)
  • updateDealAction (11-81)
src/modules/deals/actions/create-deal.action.ts (1)
  • createDealAction (10-70)
src/modules/deals/models/deal.enum.ts (1)
  • dealStageEnum (12-19)
src/app/dashboard/deals/new/page.tsx (3)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/deals/components/deal-form.tsx (1)
  • DealForm (41-316)
src/modules/deals/schemas/deal.schema.ts (2)
src/modules/contacts/schemas/contact.schema.ts (1)
  • contacts (7-25)
src/modules/deals/models/deal.enum.ts (3)
  • DealStageType (10-10)
  • DealStage (1-8)
  • dealStageEnum (12-19)
src/modules/contacts/actions/delete-contact.action.ts (3)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/todos/actions/delete-todo.action.ts (1)
  • deleteTodoAction (10-59)
src/app/dashboard/contacts/page.tsx (2)
src/modules/contacts/actions/get-contacts.action.ts (1)
  • getContactsAction (17-112)
src/modules/contacts/components/contact-card.tsx (1)
  • ContactCard (14-104)
src/lib/seed.ts (2)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/deals/models/deal.enum.ts (1)
  • DealStage (1-8)
src/modules/contacts/components/delete-contact.tsx (2)
src/modules/contacts/actions/delete-contact.action.ts (1)
  • deleteContactAction (9-46)
src/components/ui/alert-dialog.tsx (12)
  • AlertDialog (154-154)
  • AlertDialogTrigger (157-157)
  • AlertDialogContent (158-158)
  • AlertDialogDescription (162-162)
  • AlertDialogCancel (164-164)
  • AlertDialogAction (163-163)
  • AlertDialogContent (52-69)
  • AlertDialogCancel (141-151)
  • AlertDialogAction (129-139)
  • AlertDialogDescription (116-127)
  • AlertDialogOverlay (36-50)
  • AlertDialogTrigger (14-23)
src/modules/deals/actions/delete-deal.action.ts (3)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/todos/actions/delete-todo.action.ts (1)
  • deleteTodoAction (10-59)
src/modules/contacts/actions/update-contact.action.ts (3)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/contacts/schemas/contact.schema.ts (1)
  • updateContactSchema (80-84)
src/modules/contacts/actions/create-contact.action.ts (3)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/modules/contacts/schemas/contact.schema.ts (1)
  • insertContactSchema (57-76)
src/db/index.ts (1)
  • getDb (5-10)
src/app/dashboard/deals/page.tsx (3)
src/modules/deals/actions/get-deals.action.ts (1)
  • getDealsAction (15-63)
src/modules/deals/models/deal.enum.ts (1)
  • dealStageEnum (12-19)
src/modules/deals/components/deal-card.tsx (1)
  • DealCard (23-103)
src/modules/deals/components/delete-deal.tsx (2)
src/modules/deals/actions/delete-deal.action.ts (1)
  • deleteDealAction (10-47)
src/components/ui/alert-dialog.tsx (10)
  • AlertDialog (154-154)
  • AlertDialogContent (158-158)
  • AlertDialogDescription (162-162)
  • AlertDialogCancel (164-164)
  • AlertDialogAction (163-163)
  • AlertDialogContent (52-69)
  • AlertDialogDescription (116-127)
  • AlertDialogOverlay (36-50)
  • AlertDialogAction (129-139)
  • AlertDialogCancel (141-151)
src/modules/deals/actions/create-deal.action.ts (3)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/modules/deals/schemas/deal.schema.ts (1)
  • insertDealSchema (39-58)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/deals/actions/update-deal.action.ts (3)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/deals/schemas/deal.schema.ts (1)
  • updateDealSchema (62-66)
src/modules/contacts/components/contact-card.tsx (3)
src/modules/contacts/actions/get-contacts.action.ts (1)
  • ContactWithTags (13-15)
src/components/ui/card.tsx (6)
  • Card (85-85)
  • CardHeader (86-86)
  • CardContent (91-91)
  • Card (5-16)
  • CardHeader (18-29)
  • CardContent (64-72)
src/modules/contacts/components/delete-contact.tsx (1)
  • DeleteContact (23-78)
src/modules/dashboard/dashboard.page.tsx (3)
src/modules/dashboard/actions/get-dashboard-metrics.action.ts (1)
  • getDashboardMetricsAction (18-145)
src/modules/dashboard/components/stat-card.tsx (1)
  • StatCard (19-78)
src/modules/dashboard/components/quick-action-card.tsx (1)
  • QuickActionCard (18-43)
src/modules/dashboard/components/quick-action-card.tsx (1)
src/components/ui/card.tsx (10)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardContent (91-91)
  • CardDescription (90-90)
  • CardHeader (18-29)
  • Card (5-16)
  • CardDescription (41-49)
  • CardTitle (31-39)
  • CardContent (64-72)
drizzle.config.ts (1)
drizzle.local.config.ts (1)
  • getLocalD1DB (5-21)
src/components/navigation.tsx (1)
src/modules/todos/new-todo.page.tsx (1)
  • NewTodoPage (9-31)
src/modules/contacts/components/contact-form.tsx (3)
src/modules/contacts/schemas/contact.schema.ts (2)
  • Contact (108-108)
  • insertContactSchema (57-76)
src/modules/contacts/actions/update-contact.action.ts (1)
  • updateContactAction (14-73)
src/modules/contacts/actions/create-contact.action.ts (1)
  • createContactAction (13-60)
src/modules/dashboard/actions/get-dashboard-metrics.action.ts (2)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/dashboard/components/stat-card.tsx (2)
src/components/ui/card.tsx (8)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardContent (91-91)
  • Card (5-16)
  • CardHeader (18-29)
  • CardTitle (31-39)
  • CardContent (64-72)
src/lib/utils.ts (1)
  • cn (4-6)
src/app/dashboard/deals/[id]/edit/page.tsx (3)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/deals/components/deal-form.tsx (1)
  • DealForm (41-316)
src/modules/deals/actions/get-deals.action.ts (4)
src/modules/deals/schemas/deal.schema.ts (1)
  • Deal (69-69)
src/modules/deals/models/deal.enum.ts (1)
  • DealStageType (10-10)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/contacts/actions/get-contacts.action.ts (3)
src/modules/contacts/schemas/contact.schema.ts (1)
  • Contact (108-108)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/contacts/actions/tag-management.actions.ts (3)
src/modules/contacts/schemas/contact.schema.ts (3)
  • ContactTag (110-110)
  • insertContactTagSchema (86-92)
  • insertContactToTagSchema (102-105)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/deals/components/deal-card.tsx (3)
src/modules/deals/actions/get-deals.action.ts (1)
  • DealWithContact (10-13)
src/components/ui/card.tsx (6)
  • Card (85-85)
  • CardHeader (86-86)
  • CardContent (91-91)
  • Card (5-16)
  • CardHeader (18-29)
  • CardContent (64-72)
src/modules/deals/components/delete-deal.tsx (1)
  • DeleteDeal (23-76)
src/modules/contacts/schemas/contact.schema.ts (1)
src/db/schema.ts (4)
  • contacts (10-10)
  • user (4-4)
  • contactTags (11-11)
  • contactsToTags (12-12)
src/app/dashboard/contacts/[id]/edit/page.tsx (3)
src/modules/auth/utils/auth-utils.ts (1)
  • requireAuth (76-84)
src/db/index.ts (1)
  • getDb (5-10)
src/modules/contacts/components/contact-form.tsx (1)
  • ContactForm (30-245)
src/modules/deals/deals.route.ts (1)
src/modules/todos/todos.route.ts (1)
  • id (5-5)
🪛 LanguageTool
docs/IMPLEMENTATION_PHASES.md

[style] ~326-~326: The double modal “requires LEFT” is nonstandard (only accepted in certain dialects). Consider “to be LEFT”.
Context: ... - Fetching contacts with tags requires LEFT JOIN on junction table - Drizzle syntax...

(NEEDS_FIXED)

🪛 markdownlint-cli2 (0.18.1)
docs/DATABASE_SCHEMA.md

752-752: Bare URL used

(MD034, no-bare-urls)


753-753: Bare URL used

(MD034, no-bare-urls)


754-754: Bare URL used

(MD034, no-bare-urls)


755-755: Bare URL used

(MD034, no-bare-urls)

docs/IMPLEMENTATION_PHASES.md

40-40: Bare URL used

(MD034, no-bare-urls)

README.md

687-687: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/TESTING.md

374-374: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


374-374: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


375-375: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


375-375: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


376-376: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


376-376: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


377-377: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


377-377: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


378-378: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


378-378: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


379-379: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


379-379: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)


384-384: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (14)
README.md (3)

622-759: Well-structured CRM documentation with comprehensive module coverage.

The new CRM features section provides excellent detail on contacts management, deals pipeline, dashboard metrics, module structure, and implementation patterns. The documentation is thorough and follows the template's existing style and quality standards.


670-670: All referenced documentation files exist in the repository.

Verification confirms that docs/DATABASE_SCHEMA.md, docs/IMPLEMENTATION_PHASES.md, and docs/TESTING.md are all present. The links in README.md will not break.


622-759: No duplication detected—review comment is incorrect.

The README contains only one ## 📊 CRM Features section (line 622). The ### CRM Documentation reference at line 753 is a subsection within that single section that lists documentation links, not a duplicate section. The review comment's premise about accidental duplication is unfounded.

Likely an incorrect or invalid review comment.

drizzle.config.ts (1)

16-16: LGTM: Database ID update aligns with wrangler.jsonc.

The database ID change matches the corresponding update in wrangler.jsonc (line 22), ensuring consistent targeting of the new fullstack-crm database instance.

wrangler.jsonc (1)

21-22: LGTM: Database configuration updated for CRM.

The database name and ID changes align with the new fullstack-crm database setup while maintaining the existing binding name next_cf_app for code compatibility.

src/components/navigation.tsx (3)

1-1: LGTM: Icon imports match usage.

The newly imported TrendingUp and Users icons are correctly used in the Contacts and Deals navigation items.


16-16: LGTM: App title updated to reflect CRM features.

The title change from "TodoApp" to "CRM" aligns with the expanded feature set introduced in this PR.


31-42: LGTM: Navigation items added for CRM modules.

The new Contacts and Deals navigation items follow the existing pattern and use appropriate icons. The routes reference the new CRM modules added in this PR.

docs/TESTING.md (1)

1-386: Comprehensive testing guide with good coverage.

The testing documentation is thorough and well-structured, covering:

  • Feature-specific tests (Contacts, Deals, Dashboard)
  • Security tests (authentication, authorization, data isolation)
  • UI/UX and responsive design tests
  • Edge cases and data integrity scenarios

This provides a solid manual testing framework for the CRM features.

docs/IMPLEMENTATION_PHASES.md (1)

1-836: Excellent implementation guide with comprehensive phase breakdown.

This documentation provides a detailed roadmap for CRM implementation with:

  • Clear phase structure (setup, database, contacts, deals, dashboard, testing)
  • Data flow diagrams and ER diagrams
  • Critical dependencies and gotchas sections
  • Verification criteria for each phase
  • Context management and deployment strategies

This will be invaluable for understanding the architecture and guiding future development.

src/drizzle/meta/_journal.json (1)

12-18: LGTM: Migration journal entry added.

The new migration entry follows the correct Drizzle journal format and corresponds to the CRM schema migration (0001_fantastic_captain_flint).

src/db/schema.ts (1)

9-14: LGTM: CRM schema exports added.

The new exports for CRM schemas (contacts, contactTags, contactsToTags, deals) follow the existing pattern and centralize schema access for the new modules.

src/modules/contacts/actions/create-contact.action.ts (1)

13-58: Solid auth + validation flow

Nice job pulling only string entries from the FormData, validating with insertContactSchema, and overriding userId server-side before insert—this matches the standardized action pattern cleanly.

src/modules/dashboard/components/stat-card.tsx (1)

19-77: Reusable metrics card looks great

API stays tight, icon + trend helpers keep the JSX tidy, and the styling hooks into our Card primitives cleanly. Nicely done.

"db:inspect:preview": "wrangler d1 execute next-cf-app --env preview --command=\"SELECT name FROM sqlite_master WHERE type='table';\"",
"db:inspect:prod": "wrangler d1 execute next-cf-app --remote --command=\"SELECT name FROM sqlite_master WHERE type='table';\"",
"db:reset:local": "wrangler d1 execute next-cf-app --local --command=\"DROP TABLE IF EXISTS todos;\" && pnpm run db:migrate:local",
"db:seed": "tsx src/lib/seed.ts",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add tsx to devDependencies.

The new db:seed script uses tsx to execute TypeScript directly, but tsx is not listed in the project's dependencies. This will cause the script to fail unless tsx is globally installed.

Apply this diff to add tsx to devDependencies:

     "devDependencies": {
         "@biomejs/biome": "2.2.4",
         "@tailwindcss/postcss": "^4",
         "@types/node": "^20.19.13",
         "@types/react": "^19",
         "@types/react-dom": "^19",
         "drizzle-kit": "^0.31.4",
         "tailwindcss": "^4",
         "tw-animate-css": "^1.3.8",
+        "tsx": "^4.19.2",
         "typescript": "^5",
         "wrangler": "^4.35.0"
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In package.json around line 28, the new "db:seed" script uses the tsx binary but
tsx is not declared in devDependencies; add "tsx" to devDependencies (prefer
latest stable or a project-consistent version) and run npm install (or yarn) to
update lockfile so the script works in fresh environments and CI.


### CRM Module Structure

```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language specification to fenced code block.

Fenced code blocks should declare a language for proper syntax highlighting. The module structure block at line 687 should specify a language or use a plain text representation.

Apply this diff to add language specification:

- ```
- src/modules/
+ ```
+ src/modules/

Alternatively, use triple backticks with a language identifier if this represents a file tree:

- ```
+ ```tree
  src/modules/
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

687-687: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In README.md around line 687, the fenced code block closing/backticks lack a
language specifier; update that block to include either a plain-text fenced
block or a language identifier (e.g., tree) so the snippet is properly
highlighted — replace the current triple backticks with either ``` src/modules/
``` wrapped in ``` ``` or use a language tag like ```tree src/modules/ ``` to
mark the block as a tree listing.

Comment on lines +42 to +47
{ name: "Customer", color: "green", userId },
{ name: "Lead", color: "blue", userId },
{ name: "Partner", color: "purple", userId },
{ name: "Inactive", color: "gray", userId },
{ name: "VIP", color: "yellow", userId },
];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix invalid tag colors in seed data.

ContactCard builds background colors by appending "20" to each tag color for alpha tinting, so seeding plain names like "green" produces invalid CSS ("green20"). Please switch these to hex codes with leading # to match the documented validation and keep the CRM UI rendering correctly.

-            { name: "Customer", color: "green", userId },
-            { name: "Lead", color: "blue", userId },
-            { name: "Partner", color: "purple", userId },
-            { name: "Inactive", color: "gray", userId },
-            { name: "VIP", color: "yellow", userId },
+            { name: "Customer", color: "#10B981", userId },
+            { name: "Lead", color: "#3B82F6", userId },
+            { name: "Partner", color: "#8B5CF6", userId },
+            { name: "Inactive", color: "#6B7280", userId },
+            { name: "VIP", color: "#F59E0B", userId },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ name: "Customer", color: "green", userId },
{ name: "Lead", color: "blue", userId },
{ name: "Partner", color: "purple", userId },
{ name: "Inactive", color: "gray", userId },
{ name: "VIP", color: "yellow", userId },
];
{ name: "Customer", color: "#10B981", userId },
{ name: "Lead", color: "#3B82F6", userId },
{ name: "Partner", color: "#8B5CF6", userId },
{ name: "Inactive", color: "#6B7280", userId },
{ name: "VIP", color: "#F59E0B", userId },
];
🤖 Prompt for AI Agents
In src/lib/seed.ts around lines 42–47, the seed tag colors are plain names
(e.g., "green") which become invalid CSS when the UI appends "20"; replace those
values with hex color strings including the leading "#" so the appended alpha
tint yields valid CSS — for example use hex values like #22c55e for green,
#3b82f6 for blue, #7c3aed for purple, #9ca3af for gray, and #facc15 for yellow
(update each corresponding tag entry accordingly).

Comment on lines +323 to +333
if (require.main === module) {
seedCRM()
.then(() => {
console.log("✅ Seed script completed successfully");
process.exit(0);
})
.catch((error) => {
console.error("❌ Seed script failed:", error);
process.exit(1);
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Switch to an ESM-safe main guard.

This module runs under ESM (via tsx), so require is undefined and the script exits immediately with a ReferenceError. Replace the CommonJS guard with import.meta.main to let pnpm run db:seed execute correctly.

-if (require.main === module) {
+if (import.meta.main) {
     seedCRM()
         .then(() => {
             console.log("✅ Seed script completed successfully");
             process.exit(0);
🤖 Prompt for AI Agents
In src/lib/seed.ts around lines 323 to 333, the module uses the CommonJS guard
`if (require.main === module)` which throws under ESM (require is undefined);
replace that guard with the ESM-safe `if (import.meta.main)` and keep the same
Promise handling (call seedCRM().then(...).catch(...)) so the script runs
correctly when executed via tsx/pnpm run db:seed; ensure no other CommonJS-only
globals remain in this file.

Comment on lines +15 to +44
const existingContact = await db.query.contacts.findFirst({
where: eq(contacts.id, id),
});

if (!existingContact) {
throw new Error("Contact not found");
}

if (existingContact.userId !== Number.parseInt(user.id)) {
throw new Error("Forbidden: You do not own this contact");
}

// Delete contact (tags will be cascade deleted)
await db.delete(contacts).where(eq(contacts.id, id));

revalidatePath("/dashboard/contacts");
return true;
} catch (error) {
console.error("Error deleting contact:", error);

if (
error instanceof Error &&
error.message === "Authentication required"
) {
throw new Error("Authentication required");
}

throw new Error(
error instanceof Error ? error.message : "Failed to delete contact",
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent contact enumeration and enforce user-scoped deletion

The current flow fetches by id alone, then throws “Forbidden” when the contact belongs to someone else. That reveals the existence of other users’ contacts and the delete query isn’t scoped by userId, leaving a small TOCTOU window. Please bind both the lookup and delete to the authenticated user so unauthorized callers just get a single not-found outcome.

-import { eq } from "drizzle-orm";
+import { and, eq } from "drizzle-orm";
@@
-        const existingContact = await db.query.contacts.findFirst({
-            where: eq(contacts.id, id),
-        });
-
-        if (!existingContact) {
-            throw new Error("Contact not found");
-        }
-
-        if (existingContact.userId !== Number.parseInt(user.id)) {
-            throw new Error("Forbidden: You do not own this contact");
-        }
+        const userId = Number.parseInt(user.id);
+        const existingContact = await db.query.contacts.findFirst({
+            where: and(eq(contacts.id, id), eq(contacts.userId, userId)),
+        });
+
+        if (!existingContact) {
+            throw new Error("Contact not found or unauthorized");
+        }
@@
-        await db.delete(contacts).where(eq(contacts.id, id));
+        await db
+            .delete(contacts)
+            .where(and(eq(contacts.id, id), eq(contacts.userId, userId)));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const existingContact = await db.query.contacts.findFirst({
where: eq(contacts.id, id),
});
if (!existingContact) {
throw new Error("Contact not found");
}
if (existingContact.userId !== Number.parseInt(user.id)) {
throw new Error("Forbidden: You do not own this contact");
}
// Delete contact (tags will be cascade deleted)
await db.delete(contacts).where(eq(contacts.id, id));
revalidatePath("/dashboard/contacts");
return true;
} catch (error) {
console.error("Error deleting contact:", error);
if (
error instanceof Error &&
error.message === "Authentication required"
) {
throw new Error("Authentication required");
}
throw new Error(
error instanceof Error ? error.message : "Failed to delete contact",
);
const userId = Number.parseInt(user.id);
const existingContact = await db.query.contacts.findFirst({
where: and(eq(contacts.id, id), eq(contacts.userId, userId)),
});
if (!existingContact) {
throw new Error("Contact not found or unauthorized");
}
// Delete contact (tags will be cascade deleted)
await db
.delete(contacts)
.where(and(eq(contacts.id, id), eq(contacts.userId, userId)));
revalidatePath("/dashboard/contacts");
return true;
} catch (error) {
console.error("Error deleting contact:", error);
if (
error instanceof Error &&
error.message === "Authentication required"
) {
throw new Error("Authentication required");
}
throw new Error(
error instanceof Error ? error.message : "Failed to delete contact",
);
🤖 Prompt for AI Agents
In src/modules/contacts/actions/delete-contact.action.ts around lines 15 to 44,
the current code first looks up a contact by id alone and returns "Forbidden" if
it belongs to another user, which leaks existence and leaves a TOCTOU window
because the delete query is not scoped to the authenticated user; change both
the lookup and the delete to include userId equality (e.g., where:
and(eq(contacts.id, id), eq(contacts.userId, Number.parseInt(user.id)))), parse
user.id once into a const, treat any absence as "Contact not found" (do not
return "Forbidden"), perform the delete with the same combined where clause and
optionally check the delete result to decide success, and keep error handling
identical but ensure unauthorized cases surface as not-found to avoid
enumeration.

Comment on lines +65 to +73
onClick={(e) => {
e.preventDefault();
handleDelete();
}}
disabled={isPending}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isPending ? "Deleting..." : "Delete"}
</AlertDialogAction>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Close the dialog programmatically after successful deletion.
event.preventDefault() stops Radix from auto-closing, but without a controlled open state the dialog remains visible even after the contact is deleted. Mirror the controlled-dialog approach so success sets open to false while failures keep the dialog up to show the error message.

 export function DeleteContact({ contactId }: DeleteContactProps) {
-    const [isPending, startTransition] = useTransition();
-    const [error, setError] = useState<string | null>(null);
+    const [open, setOpen] = useState(false);
+    const [isPending, startTransition] = useTransition();
+    const [error, setError] = useState<string | null>(null);
@@
-    const handleDelete = () => {
+    const handleDelete = () => {
         setError(null);
         startTransition(async () => {
             try {
-                await deleteContactAction(contactId);
+                await deleteContactAction(contactId);
+                setOpen(false);
             } catch (err) {
@@
-        <AlertDialog>
+        <AlertDialog
+            open={open}
+            onOpenChange={(nextOpen) => {
+                if (!nextOpen) setError(null);
+                setOpen(nextOpen);
+            }}
+        >

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +16 to +45
const existingDeal = await db.query.deals.findFirst({
where: eq(deals.id, id),
});

if (!existingDeal) {
throw new Error("Deal not found");
}

if (existingDeal.userId !== Number.parseInt(user.id)) {
throw new Error("Forbidden: You do not own this deal");
}

// Delete deal
await db.delete(deals).where(eq(deals.id, id));

revalidatePath(dealsRoutes.board);
return true;
} catch (error) {
console.error("Error deleting deal:", error);

if (
error instanceof Error &&
error.message === "Authentication required"
) {
throw new Error("Authentication required");
}

throw new Error(
error instanceof Error ? error.message : "Failed to delete deal",
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid leaking deal existence and scope DB operations by user

Fetching by id alone and then throwing “Forbidden” when userId mismatches lets any authenticated user distinguish between “deal exists but isn’t mine” and “deal never existed,” which is an unnecessary information leak. It also leaves a TOCTOU gap because the subsequent delete is not scoped by userId. Please bind both the lookup and deletion to the authenticated user so we return a single “not found or unauthorized” outcome and ensure we never delete someone else’s deal.

Apply this diff to tighten the auth checks:

-import { eq } from "drizzle-orm";
+import { and, eq } from "drizzle-orm";
@@
-        // Verify ownership
-        const existingDeal = await db.query.deals.findFirst({
-            where: eq(deals.id, id),
-        });
-
-        if (!existingDeal) {
-            throw new Error("Deal not found");
-        }
-
-        if (existingDeal.userId !== Number.parseInt(user.id)) {
-            throw new Error("Forbidden: You do not own this deal");
-        }
+        const userId = Number.parseInt(user.id);
+        const existingDeal = await db.query.deals.findFirst({
+            where: and(eq(deals.id, id), eq(deals.userId, userId)),
+        });
+
+        if (!existingDeal) {
+            throw new Error("Deal not found or unauthorized");
+        }
@@
-        await db.delete(deals).where(eq(deals.id, id));
+        await db
+            .delete(deals)
+            .where(and(eq(deals.id, id), eq(deals.userId, userId)));

Comment on lines +50 to +52
contactName: sql<string | null>`${contacts.firstName} || ' ' || ${contacts.lastName}`,
contactEmail: contacts.email,
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix contactName so single-name contacts don’t disappear

contacts.firstName || ' ' || contacts.lastName evaluates to NULL in SQLite whenever either side is NULL. As a result, contacts that only have a first name or only a last name come back with contactName = null, blanking their label in the UI. Please coalesce the fields (and trim the result) so we always emit the available portion of the name.

-                contactName: sql<string | null>`${contacts.firstName} || ' ' || ${contacts.lastName}`,
+                contactName: sql<string | null>`trim(
+                    coalesce(${contacts.firstName}, '') || ' ' || coalesce(${contacts.lastName}, '')
+                )`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
contactName: sql<string | null>`${contacts.firstName} || ' ' || ${contacts.lastName}`,
contactEmail: contacts.email,
})
contactName: sql<string | null>`trim(
coalesce(${contacts.firstName}, '') || ' ' || coalesce(${contacts.lastName}, '')
)`,
contactEmail: contacts.email,
})
🤖 Prompt for AI Agents
In src/modules/deals/actions/get-deals.action.ts around lines 50 to 52, the
contactName expression currently uses a plain concatenation which yields NULL if
either side is NULL; replace it with a SQL expression that COALESCEs firstName
and lastName to empty strings, concatenates them with a space, trims the result,
and then NULLIFs an empty trimmed string back to NULL so single-name contacts
are preserved and entirely-empty names remain NULL; update the sql template for
contactName accordingly.

Comment on lines +75 to +87
startTransition(async () => {
try {
if (initialData) {
await updateDealAction(initialData.id, formData);
} else {
await createDealAction(formData);
}
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to save deal",
);
}
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don’t swallow redirect() errors
Catching every error here prevents the Next.js redirect from ever firing on a successful create/update, so the user stays on the form and sees an error instead of navigating away. Make sure to rethrow redirect errors (use isRedirectError from next/navigation) before you set the local error state.

-import { createDealAction } from "../actions/create-deal.action";
-import { updateDealAction } from "../actions/update-deal.action";
+import { isRedirectError } from "next/navigation";
+import { createDealAction } from "../actions/create-deal.action";
+import { updateDealAction } from "../actions/update-deal.action";
@@
         startTransition(async () => {
             try {
                 if (initialData) {
                     await updateDealAction(initialData.id, formData);
                 } else {
                     await createDealAction(formData);
                 }
             } catch (err) {
+                if (isRedirectError(err)) {
+                    throw err;
+                }
                 setError(
                     err instanceof Error ? err.message : "Failed to save deal",
                 );
             }
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
startTransition(async () => {
try {
if (initialData) {
await updateDealAction(initialData.id, formData);
} else {
await createDealAction(formData);
}
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to save deal",
);
}
});
import { isRedirectError } from "next/navigation";
import { createDealAction } from "../actions/create-deal.action";
import { updateDealAction } from "../actions/update-deal.action";
startTransition(async () => {
try {
if (initialData) {
await updateDealAction(initialData.id, formData);
} else {
await createDealAction(formData);
}
} catch (err) {
if (isRedirectError(err)) {
throw err;
}
setError(
err instanceof Error ? err.message : "Failed to save deal",
);
}
});
🤖 Prompt for AI Agents
In src/modules/deals/components/deal-form.tsx around lines 75 to 87, the catch
block swallows all errors and prevents Next.js redirects from occurring; import
isRedirectError from next/navigation and inside the catch check if
(isRedirectError(err)) throw err; otherwise keep the existing setError logic
(preserving the err instanceof Error ? err.message : "Failed to save deal") so
redirect errors are rethrown and other errors still set local state.

Comment on lines +63 to +71
onClick={(e) => {
e.preventDefault();
handleDelete();
}}
disabled={isPending}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isPending ? "Deleting..." : "Delete"}
</AlertDialogAction>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Ensure the dialog closes after a successful delete.
Because the AlertDialogAction handler always calls event.preventDefault() and the dialog isn’t controlled, the sheet never closes on success—users must click “Cancel” even after the deletion succeeds. Control the AlertDialog’s open state and call setOpen(false) once deleteDealAction resolves so the dialog closes while still allowing errors to surface.

 export function DeleteDeal({ dealId }: DeleteDealProps) {
-    const [isPending, startTransition] = useTransition();
-    const [error, setError] = useState<string | null>(null);
+    const [open, setOpen] = useState(false);
+    const [isPending, startTransition] = useTransition();
+    const [error, setError] = useState<string | null>(null);
@@
-    const handleDelete = () => {
+    const handleDelete = () => {
         setError(null);
         startTransition(async () => {
             try {
-                await deleteDealAction(dealId);
+                await deleteDealAction(dealId);
+                setOpen(false);
             } catch (err) {
@@
-        <AlertDialog>
+        <AlertDialog
+            open={open}
+            onOpenChange={(nextOpen) => {
+                if (!nextOpen) setError(null);
+                setOpen(nextOpen);
+            }}
+        >

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/modules/deals/components/delete-deal.tsx around lines 63 to 71, the
AlertDialog is uncontrolled and the onClick handler always calls
event.preventDefault(), so the dialog never closes after a successful delete;
make the dialog controlled with a local state (const [open, setOpen] =
useState(false)), pass open and onOpenChange={setOpen} to the AlertDialog, and
change the delete button handler to asynchronously call the delete action and
only call setOpen(false) after the delete resolves successfully (e.g., try {
await deleteDealAction(); setOpen(false); } catch (err) { /* surface error, do
not close */ }), keeping any preventDefault only if needed.

jezweb pushed a commit to jezweb/full-flare-stack that referenced this pull request Nov 9, 2025
…ev#20

**Phase 1: Remove CRM Content Leak**
- Delete docs/DATABASE_SCHEMA.md (CRM contacts/deals/tags from separate project)
- Delete docs/IMPLEMENTATION_PHASES.md (CRM implementation phases)

**Phase 2: Critical Accessibility Fixes (PR ifindev#28)**
- Add ref forwarding to Popover components (PopoverTrigger, PopoverContent, PopoverAnchor)
- Prevents accessibility issues with asChild pattern
- Follows shadcn/ui standards with React.forwardRef + displayName

**Phase 3: UX Improvements (PR ifindev#28)**
- Add visual validation feedback to color picker input
- Show red border when hex color is invalid
- Allow partial input while typing
- Remove redundant backgroundColor from button (keep only on inner div)

**Phase 4: Markdown Fixes (PR ifindev#29)**
- Add 'plaintext' language identifier to code block (line 67)
- Fix hyphenation: "export to PDF" → "export-to-PDF" (line 1030)

**Note on PR ifindev#20:**
- createCategory already uses correct discriminated union pattern
- No changes needed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant