758 lines
23 KiB
Markdown
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. |