cmms/frontend/CLAUDE.md

758 lines
23 KiB
Markdown

# 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
```vue
<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
```vue
<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
```javascript
// __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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```javascript
// 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
```vue
<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
```javascript
// 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)
```javascript
// __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)
```javascript
// __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)
```javascript
// __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)
```javascript
// __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:**
```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:**
```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:**
```javascript
const editEntity = (id) => {
router.push(`/entities?edit=${id}`)
}
```
**Create Related Actions:**
```javascript
const createRelatedEntity = (parentId) => {
router.push(`/related-entities?create=true&parentId=${parentId}`)
}
```
**Query Parameter Handling:**
```javascript
// 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:**
```javascript
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.