myCRM/CLAUDE.md
olli 8a132d2fb9 feat: Implement ProjectTask module with full CRUD functionality
- Added ProjectTask entity with fields for name, description, budget, hour contingent, hourly rate, and total price.
- Created ProjectTaskRepository with methods for querying tasks by project and user access.
- Implemented ProjectTaskVoter for fine-grained access control based on user roles and project membership.
- Developed ProjectTaskSecurityListener to enforce permission checks during task creation.
- Introduced custom ProjectTaskProjectFilter for filtering tasks based on project existence.
- Integrated ProjectTask management in the frontend with Vue.js components, including CRUD operations and filtering capabilities.
- Added API endpoints for ProjectTask with appropriate security measures.
- Created migration for project_tasks table in the database.
- Updated documentation to reflect new module features and usage.
2025-11-14 17:12:40 +01:00

11 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

myCRM is a modern, modular CRM application built with Symfony 7.1 LTS, Vue.js 3, and PrimeVue. It features a sophisticated permission system, API Platform for REST APIs, and a single-page application architecture.

Common Development Commands

Initial Setup

composer install
php bin/console doctrine:database:create
php bin/console doctrine:migrations:migrate
php bin/console doctrine:fixtures:load  # Load test data (admin@mycrm.local/admin123)
npm install
npm run dev

Development Workflow

# Terminal 1: Backend server
symfony serve -d              # OR: php -S localhost:8000 -t public/

# Terminal 2: Frontend with hot reload
npm run watch

# Production build
npm run build

Database Operations

# Create new migration after entity changes
php bin/console make:migration
php bin/console doctrine:migrations:migrate

# Validate schema matches entities
php bin/console doctrine:schema:validate

# Reset database (dev only)
php bin/console doctrine:database:drop --force
php bin/console doctrine:database:create
php bin/console doctrine:migrations:migrate
php bin/console doctrine:fixtures:load

Testing

# Backend tests
php bin/phpunit

# Validate Doctrine schema
php bin/console doctrine:schema:validate

# Check user permissions (custom command)
php bin/console app:user:permissions <email>

Code Generation

# Entity with API Platform support
php bin/console make:entity

# Migration after entity changes
php bin/console make:migration

# Security voter
php bin/console make:voter

# API Platform state processor/provider
php bin/console make:state-processor
php bin/console make:state-provider

Cache Management

php bin/console cache:clear
APP_ENV=prod php bin/console cache:warmup

Architecture Overview

Security System (6 Layers)

The application uses a sophisticated multi-layer security architecture:

  1. Authentication Layer: Symfony form login + OAuth (PocketId integration)
  2. Standard Roles: ROLE_ADMIN, ROLE_USER (stored in User.roles as JSON array)
  3. Module Permission System: Custom fine-grained permissions
    • User → UserRoles (many-to-many) → Role → RolePermissions → Module
    • 6 permission types: view, create, edit, delete, export, manage
    • Checked via: $user->hasModulePermission('contacts', 'view')
  4. Custom Voters: Entity-level access control (4 voters)
    • ModuleVoter: Routes permission checks to module system
    • ProjectVoter: Project ownership + team member checks
    • ProjectTaskVoter: Inherits project access + admin bypass
    • GitRepositoryVoter: Requires associated project access
  5. Event Listeners: Pre-persist security validation
    • ProjectTaskSecurityListener: Validates project tasks can only be created if user has project access
  6. Query Extensions: Auto-filter collections
    • ProjectAccessExtension: Automatically filters project collections based on user access

Permission Check Flow

// 1. Check via Security component
$this->denyAccessUnlessGranted('view', $contact);  // Uses ContactVoter → ModuleVoter

// 2. Direct module check
if ($user->hasModulePermission('contacts', 'edit')) { }

// 3. API Platform security
#[ApiResource(
    operations: [
        new Get(security: "is_granted('VIEW', object)")
    ]
)]

Entity Relationships

Core Entities:

  • User: Authentication + role management (dual system: Symfony roles + custom UserRoles)
  • Role: Groups of module permissions
  • Module: CRM modules (contacts, projects, tasks, etc.)
  • RolePermission: Joins Role ↔ Module with 6 boolean flags
  • Contact: Business contacts (implements ModuleAwareInterface, module='contacts')
  • ContactPerson: Linked to Contact, inherits permissions
  • Project: Project management (owner + team members, module='projects')
  • ProjectTask: Subtasks under projects (standalone tasks admin-only, module='tasks')
  • GitRepository: Linked to projects, inherits project access

Key Patterns:

  • ModuleAwareInterface: Entities that tie to permission modules
  • Project Access Model: $project->hasAccess($user) returns true if owner OR team member
  • Permission Inheritance: ContactPerson inherits Contact permissions, GitRepository inherits Project permissions
  • Dual Role System: User.roles (Symfony standard array) + User.userRoles (ManyToMany with Role entity)

API Platform Configuration

Per-Entity Security:

#[ApiResource(
    operations: [
        new GetCollection(security: "is_granted('VIEW', 'contacts')"),
        new Get(security: "is_granted('VIEW', object)"),
        new Post(security: "is_granted('CREATE', 'contacts')"),
        new Put(security: "is_granted('EDIT', object)"),
        new Delete(security: "is_granted('DELETE', object)")
    ]
)]

Serialization Groups:

  • {entity}:read: Fields exposed in GET responses
  • {entity}:write: Fields accepted in POST/PUT
  • Used to control field visibility based on context

Common Filters:

  • SearchFilter: Text search on specific fields
  • BooleanFilter: Filter by boolean fields (active, isDebtor, etc.)
  • DateFilter: Filter by date ranges
  • Custom filters for complex queries (e.g., ProjectTaskProjectFilter)

Frontend Architecture (Vue.js 3)

Directory Structure:

/assets/js
  /components   - Reusable components (CrudDataTable, AppMenu, AppTopbar)
  /views        - Page-level components (ContactManagement, ProjectManagement, Dashboard)
  /layout       - Sakai layout components (AppLayout, AppSidebar)
  /composables  - Composition API functions (useAuth, usePermissions)
  /stores       - Pinia state management
  /api          - API client wrappers
  /router.js    - Vue Router SPA navigation

/assets/styles
  /layout       - Sakai SCSS (topbar, sidebar, menu, responsive)
  tailwind.css  - Tailwind CSS v4 with PrimeUI plugin
  sakai.scss    - Sakai layout imports

Component Pattern:

<script setup>
import { ref } from 'vue'
import CrudDataTable from '@/components/CrudDataTable.vue'

const items = ref([])
const apiEndpoint = '/api/contacts'

async function loadData() {
  const response = await fetch(apiEndpoint)
  items.value = await response.json()
}
</script>

PrimeVue Usage:

  • DataTable: Main component for entity lists (server-side pagination, filtering, sorting)
  • Dialog: Modal forms for create/edit
  • Button, InputText, Dropdown: Form components
  • Chart: Dashboard visualizations (Chart.js integration)
  • Theme: Aura theme with dark mode support

Webpack Encore:

  • Entry point: assets/app.js
  • Aliases: @assets/js, @imagesassets/images
  • Hot reload: npm run watch
  • Production build: npm run build

Development Conventions

Backend (Symfony)

Controllers: Keep thin, delegate to services

// Good
public function create(Request $request, ContactService $service): JsonResponse
{
    $this->denyAccessUnlessGranted('CREATE', 'contacts');
    return $this->json($service->createContact($request->toArray()));
}

Services: Constructor injection, use interfaces

class ContactService
{
    public function __construct(
        private EntityManagerInterface $em,
        private EventDispatcherInterface $dispatcher
    ) {}
}

Entities: Doctrine attributes, define relationships

#[ORM\Entity]
#[ORM\Table(name: 'contacts')]
#[ApiResource(/* ... */)]
class Contact implements ModuleAwareInterface
{
    public function getModuleName(): string { return 'contacts'; }
}

Voters: Implement granular permissions

class ContactVoter extends Voter
{
    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, ['VIEW', 'EDIT', 'DELETE'])
            && ($subject instanceof Contact || $subject === 'contacts');
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        // Delegate to ModuleVoter via hasModulePermission()
    }
}

Frontend (Vue.js)

Composables: Extract reusable logic

// composables/usePermissions.js
export function usePermissions() {
  const hasPermission = (module, action) => {
    // Check user permissions
  }
  return { hasPermission }
}

Components: Single File Components with Composition API

<script setup>
import { ref, onMounted } from 'vue'

const props = defineProps({
  apiEndpoint: String
})

const emit = defineEmits(['save', 'cancel'])

// Component logic here
</script>

Testing Strategy

  • PHPUnit: Backend unit and functional tests
  • Doctrine Schema Validation: Run in CI to catch schema drift
  • Voter Tests: Explicitly test permission logic
  • Frontend: Vitest/Jest for Vue components (when configured)

Important Files

Security:

  • src/Security/Voter/ - All voter implementations
  • src/EventListener/ProjectTaskSecurityListener.php - Pre-persist validation
  • src/Filter/ProjectAccessExtension.php - Query auto-filtering
  • src/Entity/User.php - hasModulePermission() method (lines ~200-220)

API Configuration:

  • Entity #[ApiResource] attributes - Security, operations, filters
  • config/packages/api_platform.yaml - Global API settings

Frontend Entry:

  • assets/app.js - Vue app initialization
  • assets/js/router.js - Route definitions and navigation guards
  • webpack.config.js - Encore configuration

Key Workflows

Adding a New Module

  1. Create entity with ModuleAwareInterface
  2. Add to Module entity fixtures/database
  3. Create voter (or rely on ModuleVoter)
  4. Configure API Platform security
  5. Add Vue.js view component
  6. Add route to router.js
  7. Add menu item to AppMenu.vue

Adding a New Permission-Controlled Feature

  1. Determine permission level (module vs. entity)
  2. If entity-level: create/update Voter
  3. Add security checks in controller/API
  4. Update frontend to check permissions before showing UI
  5. Test with different user roles

Database Schema Changes

  1. Modify entity attributes
  2. php bin/console make:migration
  3. Review generated migration
  4. php bin/console doctrine:migrations:migrate
  5. php bin/console doctrine:schema:validate to verify
  6. Update fixtures if needed

Common Patterns

Check Module Permission (Backend):

if (!$user->hasModulePermission('contacts', 'view')) {
    throw new AccessDeniedException();
}

Check Entity Permission (Backend):

$this->denyAccessUnlessGranted('EDIT', $contact);

API Platform Security:

#[ApiResource(
    security: "is_granted('ROLE_USER')",
    operations: [
        new Get(security: "is_granted('VIEW', object)")
    ]
)]

Project Access Check:

if (!$project->hasAccess($user)) {
    throw new AccessDeniedException();
}

Frontend Permission Check:

import { usePermissions } from '@/composables/usePermissions'

const { hasPermission } = usePermissions()

if (hasPermission('contacts', 'create')) {
  // Show create button
}

Additional Documentation

  • docs/LOGIN.md - Authentication system details
  • docs/PERMISSIONS.md - Modular permission system
  • docs/USER-CRUD.md - User management with API Platform
  • docs/PROJECT_TASKS_MODULE.md - Project tasks implementation
  • .github/copilot-instructions.md - Detailed development guidelines