cmms/frontend/CLAUDE.md

23 KiB

Claude Code Guidelines for Atlas CMMS

This document provides guidelines for Claude Code when working on this Vue.js 3 CMMS application.

⚠️ CRITICAL: STRICT ENFORCEMENT OF PATTERNS

NEVER REINVENT THE WHEEL - ALWAYS COPY EXISTING WORKING IMPLEMENTATIONS

Before writing ANY new code:

  1. SEARCH for existing similar implementations in the codebase
  2. COPY the exact pattern from working examples
  3. DO NOT modify or "improve" existing patterns
  4. DO NOT mix business logic with view components

Token Conservation Rules:

  • Violating separation of concerns = wasted tokens on rewrites
  • Reinventing existing patterns = wasted tokens on rewrites
  • Not following established UI patterns = wasted tokens on rewrites

When implementing any new feature:

  1. Find the most similar existing feature (vendors, assets, work orders, etc.)
  2. Copy the EXACT file structure and implementation
  3. Only change the entity-specific details (names, fields, etc.)
  4. NEVER change the architectural patterns

If you violate these rules, you are wasting the user's tokens and usage limits.

Architecture Guidelines

Separation of Concerns

  • Always create composables for business logic - Use the use* pattern (e.g., useQRCode, useAssetManagement)
  • Components should be thin - Components handle UI rendering, user interactions, and state presentation only
  • Extract business logic to composables - Data processing, API calls, complex calculations belong in composables
  • Follow Vue 3 Composition API best practices - Use reactive refs, computed properties, and watchers appropriately

Code Organization Patterns

Composables Structure

src/composables/
├── assets/
│   ├── useAssetManagement.js
│   ├── useQRCode.js
│   └── useAssetAnalytics.js
├── workorders/
└── shared/

Component Structure

<script setup>
// 1. Imports (composables, components, utilities)
// 2. Props and emits definitions
// 3. Composable usage (business logic)
// 4. UI-specific reactive state
// 5. UI event handlers
// 6. Lifecycle hooks
</script>

What Goes Where

Composables Should Handle:

  • Business logic and calculations
  • Data transformation and validation
  • Local reactive state (component-specific refs, computed)
  • Complex operations (QR generation, file processing, etc.)
  • Reusable functionality across components
  • Orchestrating between repositories and stores

Components Should Handle:

  • Template rendering and UI structure
  • User interaction events (clicks, inputs)
  • UI-specific state (modals, loading states)
  • Props validation and emitting events
  • Basic data formatting for display
  • Component lifecycle management

Repositories Should Handle:

  • All API calls and HTTP requests
  • Data fetching and posting to backend
  • Request/response transformation
  • API error handling and retry logic
  • Authentication headers and request configuration
  • Endpoint management and URL construction

Pinia Stores Should Handle:

  • Global application state
  • Cross-component data sharing
  • Persistent state management
  • State mutations and actions
  • Computed state (getters)
  • State synchronization with backend data

Test-Driven Development (TDD) Process

ALWAYS follow TDD - Write tests FIRST, then implementation:

  1. Write the test - Define expected behavior with failing tests
  2. Write minimal code - Make the test pass with simplest implementation
  3. Refactor - Improve code while keeping tests green
  4. Repeat - Continue with next test case

Implementation Checklist

Before implementing new features, ask:

  1. Have you written the test first? → TDD is mandatory
  2. Is there business logic? → Create a composable (with tests)
  3. Will this be reused? → Create a composable (with tests)
  4. Does it involve data processing? → Use a composable (with tests)
  5. Is it purely UI-related? → Keep in component (with tests)
  6. Does it need API calls? → Use a repository (with tests)
  7. Does it manage global state? → Use a Pinia store (with tests)
  8. Does it need cross-component data sharing? → Use a Pinia store (with tests)
  9. Is it local component state? → Use reactive refs in composable or component (with tests)

Examples

Good: Proper Separation of Concerns

<script setup>
import { useAssetManagement } from '@/composables/assets/useAssetManagement'
import { useAssetStore } from '@/stores/assets'

// Store for global state
const assetStore = useAssetStore()

// Composable for business logic
const { processAssetData, validateAsset } = useAssetManagement()

// Local UI state
const showModal = ref(false)

const handleSaveAsset = async () => {
  // Business logic in composable
  const processedData = processAssetData(formData.value)
  
  // Validation in composable
  if (!validateAsset(processedData)) return
  
  // API call through store (which uses repository)
  await assetStore.createAsset(processedData)
  
  // UI logic in component
  showModal.value = false
}
</script>

Good: TDD Example - Write Test First

// __tests__/useQRCode.test.js
import { describe, it, expect, vi } from 'vitest'
import { useQRCode } from '@/composables/assets/useQRCode'

describe('useQRCode', () => {
  it('should generate QR code with valid asset data', async () => {
    const { generateQRCode } = useQRCode()
    const assetData = { name: 'Test Asset', assetNumber: 'AST-001' }
    
    const result = await generateQRCode(assetData)
    
    expect(result.qrCode).toContain('data:image/png;base64')
    expect(result.data.name).toBe('Test Asset')
  })
  
  it('should throw error for invalid asset data', async () => {
    const { generateQRCode } = useQRCode()
    const invalidData = {}
    
    await expect(generateQRCode(invalidData)).rejects.toThrow()
  })
})

Good: Repository Pattern with JSDoc

// AssetRepository.js
/**
 * Repository for handling asset-related API operations
 */
export class AssetRepository extends BaseRepository {
  /**
   * Create a new asset
   * @param {Object} assetData - The asset data to create
   * @param {string} assetData.name - Asset name
   * @param {string} assetData.category - Asset category
   * @returns {Promise<Object>} Created asset data
   */
  async createAsset(assetData) {
    return this.post('/assets', assetData)
  }
  
  /**
   * Update an existing asset
   * @param {string} id - Asset ID to update
   * @param {Object} assetData - Updated asset data
   * @returns {Promise<Object>} Updated asset data
   */
  async updateAsset(id, assetData) {
    return this.put(`/assets/${id}`, assetData)
  }
}

Good: Pinia Store Pattern with JSDoc

// stores/assets.js
import { defineStore } from 'pinia'
import { AssetRepository } from '@/services/repositories/AssetRepository'

/**
 * Asset store for managing global asset state
 */
export const useAssetStore = defineStore('assets', {
  state: () => ({
    /** @type {Array<Object>} */
    assets: [],
    /** @type {Object|null} */
    currentAsset: null,
    /** @type {boolean} */
    loading: false,
    /** @type {string|null} */
    error: null
  }),
  
  getters: {
    /**
     * Get asset by ID
     * @param {Object} state
     * @returns {function(string): Object|undefined}
     */
    getAssetById: (state) => (id) => {
      return state.assets.find(asset => asset.id === id)
    }
  },
  
  actions: {
    /**
     * Create a new asset
     * @param {Object} assetData - Asset data to create
     * @returns {Promise<Object>} Created asset
     */
    async createAsset(assetData) {
      this.loading = true
      this.error = null
      try {
        const newAsset = await AssetRepository.createAsset(assetData)
        this.assets.push(newAsset)
        return newAsset
      } catch (error) {
        this.error = error.message
        throw error
      } finally {
        this.loading = false
      }
    }
  }
})

Good: Composable with JSDoc and TDD

// composables/assets/useAssetManagement.js
import { ref, computed } from 'vue'

/**
 * Composable for asset management business logic
 * @returns {Object} Asset management methods and state
 */
export function useAssetManagement() {
  /** @type {import('vue').Ref<string|null>} */
  const error = ref(null)
  
  /**
   * Validate asset data
   * @param {Object} assetData - Asset data to validate
   * @param {string} assetData.name - Asset name
   * @param {string} assetData.category - Asset category
   * @returns {boolean} Whether asset data is valid
   */
  const validateAsset = (assetData) => {
    error.value = null
    
    if (!assetData.name || assetData.name.trim().length === 0) {
      error.value = 'Asset name is required'
      return false
    }
    
    if (!assetData.category) {
      error.value = 'Asset category is required'
      return false
    }
    
    return true
  }
  
  /**
   * Process asset data before saving
   * @param {Object} rawData - Raw form data
   * @returns {Object} Processed asset data
   */
  const processAssetData = (rawData) => {
    return {
      ...rawData,
      name: rawData.name?.trim(),
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    }
  }
  
  return {
    error: computed(() => error.value),
    validateAsset,
    processAssetData
  }
}

Bad: No Tests Written First

// Don't write implementation without tests first
export function useAssetManagement() {
  // Implementation without tests = bad practice
  const validateAsset = (assetData) => {
    // Complex logic without tests to verify behavior
  }
}

Bad: API Calls in Components

<script setup>
// Don't make API calls directly in components
const saveAsset = async () => {
  const response = await fetch('/api/assets', {
    method: 'POST',
    body: JSON.stringify(assetData)
  })
  // ... handling response
}
</script>

Bad: No JSDoc Documentation

// Don't skip documentation
export function useAssetManagement() {
  const validateAsset = (assetData) => {
    // No documentation about parameters or return values
  }
}

Vue.js 3 + Pinia Specific Guidelines

  • Use JavaScript consistently - No TypeScript, pure JavaScript with JSDoc for type hints
  • Use Composition API consistently
  • Prefer <script setup> syntax
  • Use JSDoc comments for better type safety and documentation
  • Follow Vue 3 reactivity patterns (avoid Vue 2 patterns)
  • Use computed for derived state, watch for side effects
  • Implement proper error handling in composables and stores
  • Use Pinia stores for global state, not reactive() in composables
  • Repository pattern for all API interactions

File Naming Conventions

  • Composables: use*.js (camelCase)
  • Components: PascalCase.vue
  • Views: *View.vue
  • Repositories: *Repository.js (PascalCase)
  • Stores: camelCase.js (lowercase with camelCase export)
  • Utilities: camelCase.js

Error Handling

  • Repositories should handle HTTP errors and throw meaningful exceptions
  • Stores should catch repository errors and expose error state
  • Composables should handle business logic errors via reactive refs
  • Components should display error states appropriately from stores/composables
  • Always provide user-friendly error messages
  • Log detailed errors for debugging

Data Flow Architecture

Component → Composable → Store → Repository → API
    ↑           ↑         ↑         ↑
   UI Logic   Business   Global    HTTP
            Logic      State     Calls

Testing Framework & TDD Requirements

Test Framework Setup

  • Use Vitest for unit testing (not Jest)
  • Use @vue/test-utils for component testing
  • Use MSW (Mock Service Worker) for API mocking in integration tests
  • Test files should be in __tests__ directories or *.test.js files

TDD Implementation Rules

MANDATORY: Always write tests before implementation:

  1. Red Phase: Write a failing test that describes the expected behavior
  2. Green Phase: Write minimal code to make the test pass
  3. Refactor Phase: Improve code quality while keeping tests green

Test Structure by Layer

Repository Tests (with MSW)

// __tests__/AssetRepository.test.js
import { describe, it, expect, beforeEach } from 'vitest'
import { setupServer } from 'msw/node'
import { rest } from 'msw'
import { AssetRepository } from '@/services/repositories/AssetRepository'

const server = setupServer(
  rest.post('/api/assets', (req, res, ctx) => {
    return res(ctx.json({ id: '1', name: 'Test Asset' }))
  })
)

describe('AssetRepository', () => {
  beforeEach(() => server.listen())
  
  it('should create asset via API', async () => {
    const repository = new AssetRepository()
    const assetData = { name: 'Test Asset' }
    
    const result = await repository.createAsset(assetData)
    
    expect(result.id).toBe('1')
    expect(result.name).toBe('Test Asset')
  })
})

Store Tests (with mocked repositories)

// __tests__/assets.store.test.js  
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useAssetStore } from '@/stores/assets'

vi.mock('@/services/repositories/AssetRepository', () => ({
  AssetRepository: {
    createAsset: vi.fn()
  }
}))

describe('Asset Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    vi.clearAllMocks()
  })
  
  it('should create asset and update state', async () => {
    const store = useAssetStore()
    const mockAsset = { id: '1', name: 'Test Asset' }
    
    AssetRepository.createAsset.mockResolvedValue(mockAsset)
    
    await store.createAsset({ name: 'Test Asset' })
    
    expect(store.assets).toContain(mockAsset)
    expect(store.loading).toBe(false)
  })
})

Composable Tests (with mocked stores)

// __tests__/useAssetManagement.test.js
import { describe, it, expect } from 'vitest'
import { useAssetManagement } from '@/composables/assets/useAssetManagement'

describe('useAssetManagement', () => {
  it('should validate asset with required fields', () => {
    const { validateAsset } = useAssetManagement()
    const validAsset = { name: 'Test Asset', category: 'Equipment' }
    
    const result = validateAsset(validAsset)
    
    expect(result).toBe(true)
  })
  
  it('should reject asset without name', () => {
    const { validateAsset, error } = useAssetManagement()
    const invalidAsset = { category: 'Equipment' }
    
    const result = validateAsset(invalidAsset)
    
    expect(result).toBe(false)
    expect(error.value).toBe('Asset name is required')
  })
})

Component Tests (with mocked composables)

// __tests__/AssetForm.test.js
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import AssetForm from '@/components/assets/AssetForm.vue'

vi.mock('@/composables/assets/useAssetManagement', () => ({
  useAssetManagement: () => ({
    validateAsset: vi.fn(() => true),
    error: { value: null }
  })
}))

describe('AssetForm', () => {
  it('should render form fields', () => {
    const wrapper = mount(AssetForm)
    
    expect(wrapper.find('input[name="name"]').exists()).toBe(true)
    expect(wrapper.find('select[name="category"]').exists()).toBe(true)
  })
})

TDD Workflow Summary

For every new feature, follow this exact order:

  1. Write failing test describing expected behavior
  2. Run test to confirm it fails (Red phase)
  3. Write minimal implementation to make test pass (Green phase)
  4. Run test to confirm it passes
  5. Refactor code while keeping tests green
  6. Repeat for next requirement

Mandatory Requirements Checklist

JavaScript only - No TypeScript
JSDoc documentation - All functions and complex types
TDD first - Tests written before implementation
Repository pattern - All API calls go through repositories
Pinia stores - All global state management
Composables - All business logic
Vitest + MSW - Testing frameworks

Remember:

  • Tests first → MANDATORY for all code
  • JavaScript + JSDoc → No TypeScript allowed
  • API calls → Always use repositories with MSW tests
  • Global state → Always use Pinia stores with mocked repository tests
  • Business logic → Always use composables with unit tests
  • UI logic → Keep in components with mocked composable tests

Data Flow: Component → Composable → Store → Repository → API
Test Flow: Component Tests → Composable Tests → Store Tests → Repository Tests

UI/UX Consistency Guidelines

Modal Forms Pattern (MANDATORY)

All create and edit forms MUST use the modal pattern established in AssetsListView.vue:

  • Modal-based forms - No separate pages for create/edit operations
  • Auto-save functionality - Implemented using localStorage with user interaction tracking
  • Tab-based organization - Multi-step forms organized in tabs (Basic, Details, etc.)
  • Query parameter navigation - Use ?edit=123 or ?create=true&assetId=456 patterns
  • Form restoration - Show notifications for unsaved data with restore/dismiss options
  • Consistent actions - Edit buttons navigate with query params to open modals

Detail View Pattern (MANDATORY)

All detail views MUST follow the structure established in AssetDetailView.vue:

<template>
  <FPLayout>
    <template #header>
      <FPPageHeader
        :title="entity.name"
        :description="entity.description"
        :breadcrumbs="breadcrumbsArray"
      >
        <template #actions>
          <FPButton variant="secondary" @click="editEntity">Edit</FPButton>
          <FPButton variant="primary" @click="createRelatedEntity">Create Related</FPButton>
        </template>
      </FPPageHeader>
    </template>

    <div class="px-6 py-8">
      <!-- Loading, Content, Error states with v-if/v-else-if/v-else -->
    </div>
  </FPLayout>
</template>

Required elements:

  • FPLayout with header slot - Consistent layout structure
  • FPPageHeader with actions - Title, description, breadcrumbs, action buttons
  • Proper spacing - px-6 py-8 for content padding
  • Three-state rendering - Loading, content, error states
  • Action buttons - Edit and create related entity buttons in header

List View Pattern (MANDATORY)

All listing pages MUST follow the structure established in WorkOrdersListView.vue:

<template>
  <FPLayout>
    <template #header>
      <FPPageHeader title="Entities" description="Description">
        <template #actions>
          <FPButton @click="refreshData">Refresh</FPButton>
          <FPButton variant="primary" @click="createEntity">Create Entity</FPButton>
        </template>
        <template #stats>
          <FPStats :stats="entityStats" />
        </template>
        <template #tabs>
          <FPTabs v-model="activeTab" :tabs="statusTabs" />
        </template>
      </FPPageHeader>
    </template>

    <div class="p-6">
      <!-- Filters -->
      <!-- FPTable with actions -->
      <!-- Create/Edit Modal -->
    </div>
  </FPLayout>
</template>

Required elements:

  • Header with stats and tabs - Statistics row and filter tabs
  • Filter controls - Search, selects, date pickers above table
  • FPTable component - Consistent table with actions column
  • Inline actions - Edit, View, Delete buttons in table rows
  • Modal integration - Create/edit modals in same component

Navigation Patterns (MANDATORY)

Edit Actions:

const editEntity = (id) => {
  router.push(`/entities?edit=${id}`)
}

Create Related Actions:

const createRelatedEntity = (parentId) => {
  router.push(`/related-entities?create=true&parentId=${parentId}`)
}

Query Parameter Handling:

// In onMounted of list views
const editEntityId = route.query.edit
if (editEntityId) {
  setTimeout(() => {
    editEntity(parseInt(editEntityId))
    router.replace({ path: '/entities' })
  }, 500)
}

const shouldCreate = route.query.create === 'true'
if (shouldCreate) {
  const parentId = route.query.parentId
  setTimeout(() => {
    if (parentId) {
      entityForm.parentId = parseInt(parentId)
    }
    createEntityAction()
    router.replace({ path: '/entities' })
  }, 500)
}

Form Structure Requirements

Modal Configuration:

  • Size: size="xl" for complex forms
  • Auto-save: localStorage with user interaction tracking
  • Restore notifications: Show unsaved data restore options
  • Tab navigation: Multi-step forms with next/previous buttons
  • Validation: Real-time validation with error display

Form Tabs Pattern:

const formTabs = [
  { key: 'basic', label: 'Basic Information', icon: 'info' },
  { key: 'details', label: 'Details', icon: 'document' },
  { key: 'advanced', label: 'Advanced', icon: 'cog' }
]

Implementation Checklist

For every new entity (Users, Locations, Parts, etc.):

List View (EntitiesListView.vue):

  • Uses FPLayout with header slot
  • Implements FPPageHeader with stats and tabs
  • Has filter controls above FPTable
  • Contains create/edit modal in same component
  • Handles query parameters for edit/create
  • Uses consistent action buttons (Edit, View, Delete)

Detail View (EntityDetailView.vue):

  • Uses FPLayout with header slot
  • Implements FPPageHeader with breadcrumbs and actions
  • Has proper content spacing (px-6 py-8)
  • Implements three-state rendering (loading/content/error)
  • Edit button uses query parameter navigation

Modal Forms:

  • Size="xl" for complex forms
  • Tab-based organization for multi-step forms
  • Auto-save with localStorage integration
  • Form restoration notifications
  • Real-time validation and error handling
  • Consistent button placement and styling

Examples Directory Structure

views/
├── entities/
│   ├── EntitiesView.vue          # Parent route component
│   ├── EntitiesListView.vue      # List with modal (primary pattern)
│   └── EntityDetailView.vue      # Detail view with header actions

Anti-Patterns (AVOID)

Separate create/edit pages - Use modals instead No auto-save - All forms must have auto-save Inconsistent navigation - Always use query parameters Missing breadcrumbs - Detail views must have navigation context No loading states - Always implement three-state rendering Inline edit forms - Use modals for consistency


Remember:

  • All forms → Modal-based with auto-save and tabs
  • All detail views → FPLayout with header slot and actions
  • All list views → Stats, tabs, filters, and integrated modals
  • All navigation → Query parameter based for edit/create actions

These patterns ensure consistency, better UX, and maintainable code across the entire application.