- Constitution and project spec (spec.yml) - 7 comprehensive feature specs (tasks, batches, labor, compliance, inventory, integrations, comms) - Phase 1 implementation plan (6-week roadmap) - Week 1 task breakdown (15 concrete tasks) - Architecture and compliance documentation - Backend and frontend setup guides - Deployment guide for nexus-vector - CI/CD workflows (Forgejo Actions) - Quick start guide for developers Project is ready for implementation with: - Automated testing on every push - Automatic deployment to nexus-vector on push to main - Database migrations handled automatically - Health checks and monitoring Stack: TypeScript, Fastify, React, Vite, PostgreSQL, Prisma, Docker
442 lines
11 KiB
Markdown
442 lines
11 KiB
Markdown
# CA Grow Ops Manager — Frontend
|
||
|
||
**Version**: 0.1.0
|
||
**Stack**: TypeScript, React, Vite/Next.js, Radix UI/shadcn
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
The frontend is a **mobile-first, accessible** web application optimized for low-friction daily use in cultivation facilities. It provides intuitive interfaces for tasks, batches, labor tracking, compliance, and team communication.
|
||
|
||
---
|
||
|
||
## Tech Stack
|
||
|
||
- **Language**: TypeScript 5.x
|
||
- **Framework**: React 18.x
|
||
- **Build Tool**: Vite or Next.js (to be decided during implementation)
|
||
- **Component Library**: Radix UI or shadcn-style accessible primitives
|
||
- **Styling**: Tailwind CSS or modern CSS-in-JS
|
||
- **State Management**: React Context + hooks (Zustand or Redux Toolkit for complex state)
|
||
- **Routing**: React Router (Vite) or Next.js App Router
|
||
- **Forms**: React Hook Form + Zod validation
|
||
- **API Client**: Axios or Fetch with TypeScript types
|
||
- **Testing**: Vitest + React Testing Library
|
||
|
||
---
|
||
|
||
## Project Structure
|
||
|
||
### Vite Option
|
||
|
||
```
|
||
frontend/
|
||
├── src/
|
||
│ ├── app/ # App shell
|
||
│ │ ├── App.tsx
|
||
│ │ ├── Router.tsx
|
||
│ │ └── Layout.tsx
|
||
│ ├── pages/ # Page components
|
||
│ │ ├── Dashboard.tsx
|
||
│ │ ├── Tasks/
|
||
│ │ │ ├── TasksPage.tsx
|
||
│ │ │ ├── TaskDetail.tsx
|
||
│ │ │ └── TodayView.tsx
|
||
│ │ ├── Batches/
|
||
│ │ ├── Labor/
|
||
│ │ ├── Compliance/
|
||
│ │ └── Settings/
|
||
│ ├── components/ # Reusable components
|
||
│ │ ├── ui/ # Base UI components (buttons, inputs, etc.)
|
||
│ │ ├── tasks/ # Task-specific components
|
||
│ │ ├── batches/
|
||
│ │ └── shared/ # Shared components (nav, header, etc.)
|
||
│ ├── hooks/ # Custom React hooks
|
||
│ │ ├── useAuth.ts
|
||
│ │ ├── useTasks.ts
|
||
│ │ └── useApi.ts
|
||
│ ├── lib/ # Utilities and helpers
|
||
│ │ ├── api.ts # API client
|
||
│ │ ├── auth.ts # Auth helpers
|
||
│ │ └── utils.ts
|
||
│ ├── types/ # TypeScript types
|
||
│ │ ├── api.ts
|
||
│ │ └── models.ts
|
||
│ ├── styles/ # Global styles
|
||
│ │ ├── index.css
|
||
│ │ └── tailwind.css
|
||
│ └── main.tsx # Entry point
|
||
├── public/ # Static assets
|
||
├── .env.example
|
||
├── package.json
|
||
├── tsconfig.json
|
||
├── vite.config.ts
|
||
└── README.md
|
||
```
|
||
|
||
### Next.js Option
|
||
|
||
```
|
||
frontend/
|
||
├── app/ # Next.js App Router
|
||
│ ├── layout.tsx
|
||
│ ├── page.tsx # Dashboard
|
||
│ ├── tasks/
|
||
│ │ ├── page.tsx
|
||
│ │ └── [id]/page.tsx
|
||
│ ├── batches/
|
||
│ ├── labor/
|
||
│ └── api/ # API routes (if needed)
|
||
├── components/ # Same as Vite
|
||
├── hooks/
|
||
├── lib/
|
||
├── types/
|
||
├── styles/
|
||
└── public/
|
||
```
|
||
|
||
---
|
||
|
||
## Design System
|
||
|
||
### Component Library (Radix UI / shadcn)
|
||
|
||
Use accessible primitives for:
|
||
|
||
- **Buttons**: Primary, secondary, destructive
|
||
- **Inputs**: Text, number, date, select
|
||
- **Dialogs**: Modals, drawers, alerts
|
||
- **Dropdowns**: Menus, selects, comboboxes
|
||
- **Tabs**: Navigation, content switching
|
||
- **Cards**: Content containers
|
||
- **Badges**: Status indicators
|
||
|
||
### Styling (Tailwind CSS)
|
||
|
||
```css
|
||
/* Design tokens */
|
||
:root {
|
||
--color-primary: #10b981; /* Green for cultivation */
|
||
--color-secondary: #3b82f6; /* Blue for actions */
|
||
--color-danger: #ef4444; /* Red for alerts */
|
||
--color-warning: #f59e0b; /* Orange for warnings */
|
||
--color-success: #10b981; /* Green for success */
|
||
|
||
--spacing-xs: 0.25rem;
|
||
--spacing-sm: 0.5rem;
|
||
--spacing-md: 1rem;
|
||
--spacing-lg: 1.5rem;
|
||
--spacing-xl: 2rem;
|
||
|
||
--font-sans: 'Inter', system-ui, sans-serif;
|
||
--font-mono: 'Fira Code', monospace;
|
||
}
|
||
```
|
||
|
||
### Mobile-First Design
|
||
|
||
- **Big Tap Targets**: Minimum 44×44px for all interactive elements
|
||
- **Dark Mode**: Default dark theme with light mode option
|
||
- **Responsive Breakpoints**:
|
||
- Mobile: < 640px
|
||
- Tablet: 640px - 1024px
|
||
- Desktop: > 1024px
|
||
|
||
---
|
||
|
||
## Key Features
|
||
|
||
### 1. Today View (Mobile-Optimized)
|
||
|
||
```tsx
|
||
// TodayView.tsx
|
||
export const TodayView = () => {
|
||
const { tasks, loading } = useTasks({ dueToday: true });
|
||
|
||
return (
|
||
<div className="p-4 space-y-4">
|
||
<h1 className="text-2xl font-bold">Today's Tasks</h1>
|
||
{tasks.map(task => (
|
||
<TaskCard key={task.id} task={task} />
|
||
))}
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
### 2. Timeclock Widget
|
||
|
||
```tsx
|
||
// TimeclockWidget.tsx
|
||
export const TimeclockWidget = () => {
|
||
const { currentShift, clockIn, clockOut } = useTimeclock();
|
||
|
||
return (
|
||
<Card className="p-6">
|
||
{currentShift ? (
|
||
<div>
|
||
<p>Clocked in: {formatTime(currentShift.clockIn)}</p>
|
||
<Button onClick={clockOut} size="lg">Clock Out</Button>
|
||
</div>
|
||
) : (
|
||
<Button onClick={clockIn} size="lg">Clock In</Button>
|
||
)}
|
||
</Card>
|
||
);
|
||
};
|
||
```
|
||
|
||
### 3. Batch Timeline
|
||
|
||
```tsx
|
||
// BatchTimeline.tsx
|
||
export const BatchTimeline = ({ batchId }: { batchId: string }) => {
|
||
const { events } = useBatchTimeline(batchId);
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{events.map(event => (
|
||
<TimelineEvent key={event.id} event={event} />
|
||
))}
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## API Integration
|
||
|
||
### API Client
|
||
|
||
```typescript
|
||
// lib/api.ts
|
||
import axios from 'axios';
|
||
|
||
const api = axios.create({
|
||
baseURL: import.meta.env.VITE_API_URL,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
});
|
||
|
||
// Add auth token to requests
|
||
api.interceptors.request.use(config => {
|
||
const token = localStorage.getItem('accessToken');
|
||
if (token) {
|
||
config.headers.Authorization = `Bearer ${token}`;
|
||
}
|
||
return config;
|
||
});
|
||
|
||
// Handle token refresh
|
||
api.interceptors.response.use(
|
||
response => response,
|
||
async error => {
|
||
if (error.response?.status === 401) {
|
||
// Attempt token refresh
|
||
const refreshToken = localStorage.getItem('refreshToken');
|
||
if (refreshToken) {
|
||
const { data } = await axios.post('/api/auth/refresh', { refreshToken });
|
||
localStorage.setItem('accessToken', data.accessToken);
|
||
error.config.headers.Authorization = `Bearer ${data.accessToken}`;
|
||
return axios(error.config);
|
||
}
|
||
}
|
||
return Promise.reject(error);
|
||
}
|
||
);
|
||
|
||
export default api;
|
||
```
|
||
|
||
### Custom Hooks
|
||
|
||
```typescript
|
||
// hooks/useTasks.ts
|
||
export const useTasks = (filters?: TaskFilters) => {
|
||
const [tasks, setTasks] = useState<Task[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
const fetchTasks = async () => {
|
||
const { data } = await api.get('/api/tasks', { params: filters });
|
||
setTasks(data.data);
|
||
setLoading(false);
|
||
};
|
||
fetchTasks();
|
||
}, [filters]);
|
||
|
||
return { tasks, loading };
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Authentication
|
||
|
||
### Auth Context
|
||
|
||
```typescript
|
||
// hooks/useAuth.ts
|
||
const AuthContext = createContext<AuthContextType | null>(null);
|
||
|
||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||
const [user, setUser] = useState<User | null>(null);
|
||
|
||
const login = async (email: string, password: string) => {
|
||
const { data } = await api.post('/api/auth/login', { email, password });
|
||
localStorage.setItem('accessToken', data.accessToken);
|
||
localStorage.setItem('refreshToken', data.refreshToken);
|
||
setUser(data.user);
|
||
};
|
||
|
||
const logout = () => {
|
||
localStorage.removeItem('accessToken');
|
||
localStorage.removeItem('refreshToken');
|
||
setUser(null);
|
||
};
|
||
|
||
return (
|
||
<AuthContext.Provider value={{ user, login, logout }}>
|
||
{children}
|
||
</AuthContext.Provider>
|
||
);
|
||
};
|
||
|
||
export const useAuth = () => {
|
||
const context = useContext(AuthContext);
|
||
if (!context) throw new Error('useAuth must be used within AuthProvider');
|
||
return context;
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Testing
|
||
|
||
### Component Tests (Vitest + React Testing Library)
|
||
|
||
```typescript
|
||
// TaskCard.test.tsx
|
||
import { render, screen } from '@testing-library/react';
|
||
import { TaskCard } from './TaskCard';
|
||
|
||
describe('TaskCard', () => {
|
||
it('renders task name', () => {
|
||
const task = { id: '1', name: 'Water plants', status: 'PENDING' };
|
||
render(<TaskCard task={task} />);
|
||
expect(screen.getByText('Water plants')).toBeInTheDocument();
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## Environment Variables
|
||
|
||
```bash
|
||
# API
|
||
VITE_API_URL=http://localhost:3000/api
|
||
|
||
# Auth
|
||
VITE_AUTH_ENABLED=true
|
||
|
||
# Features
|
||
VITE_METRC_ENABLED=false
|
||
```
|
||
|
||
---
|
||
|
||
## Development Workflow
|
||
|
||
### Setup
|
||
|
||
```bash
|
||
# Install dependencies
|
||
npm install
|
||
|
||
# Set up environment variables
|
||
cp .env.example .env
|
||
|
||
# Start dev server
|
||
npm run dev
|
||
```
|
||
|
||
### Scripts
|
||
|
||
```json
|
||
{
|
||
"scripts": {
|
||
"dev": "vite",
|
||
"build": "tsc && vite build",
|
||
"preview": "vite preview",
|
||
"test": "vitest",
|
||
"test:ui": "vitest --ui",
|
||
"lint": "eslint src --ext .ts,.tsx",
|
||
"format": "prettier --write src"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Accessibility
|
||
|
||
- **WCAG 2.1 AA Compliance**: All components meet accessibility standards
|
||
- **Keyboard Navigation**: All interactive elements keyboard-accessible
|
||
- **Screen Reader Support**: Proper ARIA labels and roles
|
||
- **Focus Management**: Clear focus indicators and logical tab order
|
||
- **Color Contrast**: Minimum 4.5:1 contrast ratio for text
|
||
|
||
---
|
||
|
||
## Mobile & PWA
|
||
|
||
### Progressive Web App (Stretch Goal)
|
||
|
||
```typescript
|
||
// vite.config.ts
|
||
import { VitePWA } from 'vite-plugin-pwa';
|
||
|
||
export default defineConfig({
|
||
plugins: [
|
||
VitePWA({
|
||
registerType: 'autoUpdate',
|
||
manifest: {
|
||
name: 'CA Grow Ops Manager',
|
||
short_name: 'Grow Ops',
|
||
theme_color: '#10b981',
|
||
icons: [
|
||
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
||
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
|
||
],
|
||
},
|
||
}),
|
||
],
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## Next Steps
|
||
|
||
1. **Choose Build Tool**: Decide between Vite and Next.js
|
||
2. **Set Up Project**: Initialize React project with TypeScript
|
||
3. **Configure Tailwind**: Set up Tailwind CSS and design tokens
|
||
4. **Build Design System**: Implement base UI components
|
||
5. **Implement Pages**: Build page components per spec
|
||
6. **Write Tests**: Component and integration tests
|
||
7. **Deploy**: Set up CI/CD and deploy to staging
|
||
|
||
---
|
||
|
||
## Resources
|
||
|
||
- [React Documentation](https://react.dev)
|
||
- [Vite Documentation](https://vitejs.dev)
|
||
- [Next.js Documentation](https://nextjs.org/docs)
|
||
- [Radix UI Documentation](https://www.radix-ui.com)
|
||
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
||
- [React Testing Library](https://testing-library.com/react)
|