IcostPro/README.md
2026-02-12 21:04:43 +01:00

988 lines
23 KiB
Markdown

# Icostpro - Developer Guide
> **Purpose**: This README helps developers understand where to find code and how to write new features that align with the existing architecture.
---
## Table of Contents
1. [Project Overview](#project-overview)
2. [Folder Structure](#folder-structure)
3. [Coding Patterns & Conventions](#coding-patterns--conventions)
4. [Component Architecture](#component-architecture)
5. [API Design & Data Flow](#api-design--data-flow)
6. [Adding New Features](#adding-new-features)
---
## Project Overview
**Tech Stack**: Next.js 16 (App Router) + React 19 + TypeScript 5 + MongoDB 7 + Tailwind CSS 4
**Application Type**: Full-stack restaurant inventory management system for tracking ingredients, pricing, suppliers, and categories.
**Key Patterns**:
- Client components for interactive pages (`'use client'`)
- Server-side API routes for data operations
- MongoDB with aggregation pipelines for joins
- Modal-based workflows for CRUD operations
- Responsive design (mobile-first with Tailwind)
---
## Folder Structure
### `/app` - Next.js App Router
```
app/
├── api/ # API Routes (server-side only)
│ ├── categories/route.ts # GET, POST for categories
│ ├── subcategories/route.ts # GET, POST with category filtering
│ ├── suppliers/route.ts # GET, POST for suppliers
│ └── ingredients/
│ ├── route.ts # GET (list with filters), POST
│ └── [id]/route.ts # GET, PUT, DELETE by ID
├── dashboard/
│ └── ingredients/
│ └── page.tsx # Main ingredients page (client component)
├── layout.tsx # Root layout with metadata
├── page.tsx # Home page (redirects to /dashboard/ingredients)
└── globals.css # Tailwind imports + theme CSS variables
```
**Where to add new pages**: Create folders under `/app/dashboard/` (e.g., `/app/dashboard/reports/page.tsx`)
**Where to add new API routes**: Create folders under `/app/api/` (e.g., `/app/api/orders/route.ts`)
---
### `/components` - Reusable React Components
```
components/
├── AddIngredientModal.tsx # Add/Edit modal with complex form
├── ViewIngredientModal.tsx # Read-only detail modal
├── DeleteConfirmModal.tsx # Confirmation dialog
├── IngredientTable.tsx # Responsive table/card display
└── Sidebar.tsx # Navigation sidebar
```
**Component Types**:
- **Modals**: Components ending in `Modal.tsx` (handle open/close state via props)
- **Data Display**: Components like `IngredientTable.tsx` (receive data via props)
- **Layout**: Components like `Sidebar.tsx` (navigation, structure)
**Where to add new components**: Add to `/components/` with descriptive names
**Naming convention**: PascalCase, suffix with type (e.g., `UserModal.tsx`, `OrderTable.tsx`)
---
### `/lib` - Utilities & Shared Logic
```
lib/
├── mongodb.ts # MongoDB connection pool (singleton pattern)
└── types.ts # TypeScript interfaces for all entities
```
**Where to add**:
- **Database connections**: Add to or extend `mongodb.ts`
- **Type definitions**: Add to `types.ts` (interfaces for entities, API responses, props)
- **Utility functions**: Create new files (e.g., `lib/formatting.ts`, `lib/validation.ts`)
---
## Coding Patterns & Conventions
### TypeScript Interfaces
**Location**: `/lib/types.ts`
All entity interfaces are defined here. Key patterns:
```typescript
export interface Ingredient {
_id?: string; // Optional for new documents
code: string; // Required fields
name: string;
// ... all fields
createdAt?: Date; // Auto-generated by MongoDB
updatedAt?: Date;
}
export interface IngredientWithRefs {
_id: string;
code: string;
category?: string; // Populated name (not ID)
subcategory?: string;
supplier?: string;
// ... rest of fields
}
```
**Convention**:
- Base interface for database schema (`Ingredient`)
- `WithRefs` variant for aggregated/joined data (`IngredientWithRefs`)
---
### Component Patterns
#### Client Components
All pages and interactive components use `'use client'` directive:
```typescript
'use client'
import { useState, useEffect } from 'react'
export default function MyPage() {
const [data, setData] = useState([])
// Component logic
}
```
#### State Management
- **Local state**: Use `useState` for component-specific data
- **Derived state**: Use `useMemo` for computed values
- **Side effects**: Use `useEffect` for data fetching
- **Callbacks**: Use `useCallback` to memoize functions passed as props
Example from `page.tsx`:
```typescript
const [ingredients, setIngredients] = useState<IngredientWithRefs[]>([])
const [loading, setLoading] = useState(true)
const fetchIngredients = useCallback(async () => {
setLoading(true)
try {
const response = await fetch(`/api/ingredients?${params}`)
const data = await response.json()
setIngredients(data.ingredients)
} finally {
setLoading(false)
}
}, [currentPage, searchTerm, categoryFilter, subcategoryFilter])
useEffect(() => {
fetchIngredients()
}, [fetchIngredients])
```
---
#### Modal Patterns
All modals follow this prop interface:
```typescript
interface ModalProps {
open: boolean; // Controls visibility
onClose: () => void; // Called when modal closes
onSaved?: () => void; // Called after successful save
ingredientId?: string; // For edit mode (optional)
}
```
**Usage**:
1. Parent component manages `open` state
2. Pass `onSaved` to refresh data after changes
3. Include `ingredientId` for edit mode
Example:
```typescript
const [modalOpen, setModalOpen] = useState(false)
const [editingId, setEditingId] = useState<string | undefined>()
<AddIngredientModal
open={modalOpen}
onClose={() => {
setModalOpen(false)
setEditingId(undefined)
}}
onSaved={() => {
fetchIngredients() // Refresh data
setModalOpen(false)
}}
ingredientId={editingId}
/>
```
---
### Styling Conventions
**Tailwind CSS with Reusable Classes**:
```typescript
const inputClass = "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
const labelClass = "block text-sm font-medium text-gray-700 mb-1"
const selectClass = "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
```
**Responsive Design**:
- Mobile-first: Default styles are for mobile
- Breakpoints: `md:`, `lg:`, `xl:` for larger screens
- Example: `<div className="block md:hidden">` (show only on mobile)
**Color Palette**:
- Primary: `blue-500` (#2563eb)
- Success: `green-500`
- Danger: `red-500`
- Backgrounds: `gray-50`, `gray-100`
- Text: `gray-700`, `gray-900`
---
### Form Validation
**Pattern**: Client-side validation before API calls
```typescript
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Validation
if (!name.trim()) {
alert('Name is required')
return
}
if (quantity <= 0) {
alert('Quantity must be greater than 0')
return
}
// API call
const response = await fetch('/api/ingredients', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
if (response.ok) {
onSaved?.()
}
}
```
---
## Component Architecture
### Page Components (`/app/dashboard/*/page.tsx`)
**Responsibilities**:
1. Fetch data from API routes
2. Manage filter/pagination state
3. Orchestrate modals
4. Pass data to display components
**Structure**:
```typescript
'use client'
export default function IngredientsPage() {
// State
const [data, setData] = useState([])
const [filters, setFilters] = useState({})
const [modals, setModals] = useState({ add: false, view: false })
// Data fetching
useEffect(() => { /* fetch */ }, [filters])
// Render
return (
<>
<FilterBar />
<DataTable data={data} />
<Modals />
</>
)
}
```
---
### Modal Components
#### Add/Edit Modal Pattern
```typescript
export default function AddEntityModal({ open, onClose, onSaved, entityId }: ModalProps) {
const [formData, setFormData] = useState({ /* fields */ })
const isEditMode = !!entityId
useEffect(() => {
if (isEditMode && open) {
// Fetch existing data
fetch(`/api/entities/${entityId}`).then(/* populate form */)
} else {
// Reset form
setFormData({ /* defaults */ })
}
}, [entityId, open])
const handleSubmit = async () => {
const method = isEditMode ? 'PUT' : 'POST'
const url = isEditMode ? `/api/entities/${entityId}` : '/api/entities'
const response = await fetch(url, {
method,
body: JSON.stringify(formData)
})
if (response.ok) {
onSaved?.()
}
}
if (!open) return null
return (
<div className="modal-overlay">
<form onSubmit={handleSubmit}>
{/* Form fields */}
</form>
</div>
)
}
```
---
#### View Modal Pattern
```typescript
export default function ViewEntityModal({ open, onClose, entityId }: ModalProps) {
const [entity, setEntity] = useState<Entity | null>(null)
useEffect(() => {
if (open && entityId) {
fetch(`/api/entities/${entityId}`)
.then(res => res.json())
.then(setEntity)
}
}, [open, entityId])
if (!open || !entity) return null
return (
<div className="modal-overlay">
{/* Read-only display */}
</div>
)
}
```
---
### Table/Display Components
**Props Pattern**:
```typescript
interface TableProps {
items: Item[]
onView: (id: string) => void
onEdit: (id: string) => void
onDelete: (id: string) => void
}
```
**Responsive Pattern**:
```tsx
<div>
{/* Desktop: Table */}
<div className="hidden md:block">
<table>{/* table rows */}</table>
</div>
{/* Mobile: Cards */}
<div className="md:hidden space-y-4">
{items.map(item => (
<div key={item._id} className="card">
{/* card content */}
</div>
))}
</div>
</div>
```
---
## API Design & Data Flow
### API Route Structure
**Location**: `/app/api/[resource]/route.ts`
**Pattern**: Export HTTP method handlers
```typescript
import { NextRequest, NextResponse } from 'next/server'
import clientPromise from '@/lib/mongodb'
// GET /api/entities
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const page = parseInt(searchParams.get('page') || '1')
const client = await clientPromise
const db = client.db('icostpro')
const items = await db.collection('entities')
.find({})
.skip((page - 1) * 10)
.limit(10)
.toArray()
return NextResponse.json({ items })
}
// POST /api/entities
export async function POST(request: NextRequest) {
const body = await request.json()
const client = await clientPromise
const db = client.db('icostpro')
const result = await db.collection('entities').insertOne({
...body,
createdAt: new Date(),
updatedAt: new Date()
})
return NextResponse.json({ insertedId: result.insertedId }, { status: 201 })
}
```
---
### Dynamic Route Pattern (`/api/[resource]/[id]/route.ts`)
```typescript
interface RouteParams {
params: Promise<{ id: string }>
}
// GET /api/entities/:id
export async function GET(
request: NextRequest,
{ params }: RouteParams
) {
const { id } = await params
const client = await clientPromise
const db = client.db('icostpro')
const item = await db.collection('entities')
.findOne({ _id: new ObjectId(id) })
if (!item) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
return NextResponse.json(item)
}
// PUT /api/entities/:id
export async function PUT(
request: NextRequest,
{ params }: RouteParams
) {
const { id } = await params
const body = await request.json()
const client = await clientPromise
const db = client.db('icostpro')
const result = await db.collection('entities').updateOne(
{ _id: new ObjectId(id) },
{ $set: { ...body, updatedAt: new Date() } }
)
return NextResponse.json({ modifiedCount: result.modifiedCount })
}
// DELETE /api/entities/:id
export async function DELETE(
request: NextRequest,
{ params }: RouteParams
) {
const { id } = await params
const client = await clientPromise
const db = client.db('icostpro')
await db.collection('entities').deleteOne({ _id: new ObjectId(id) })
return NextResponse.json({ success: true })
}
```
---
### MongoDB Aggregation Pattern
**Use Case**: Joining referenced data (e.g., populate category names)
**Pattern from `/api/ingredients/[id]/route.ts`**:
```typescript
const pipeline = [
{ $match: { _id: new ObjectId(id) } },
{
$lookup: {
from: 'categories',
localField: 'categoryId',
foreignField: '_id',
as: 'categoryData'
}
},
{
$lookup: {
from: 'subcategories',
localField: 'subcategoryId',
foreignField: '_id',
as: 'subcategoryData'
}
},
{
$lookup: {
from: 'suppliers',
localField: 'supplierId',
foreignField: '_id',
as: 'supplierData'
}
},
{
$project: {
_id: 1,
code: 1,
name: 1,
category: { $arrayElemAt: ['$categoryData.name', 0] },
subcategory: { $arrayElemAt: ['$subcategoryData.name', 0] },
supplier: { $arrayElemAt: ['$supplierData.name', 0] },
categoryId: 1,
subcategoryId: 1,
supplierId: 1,
quantity: 1,
// ... rest of fields
}
}
]
const result = await db.collection('ingredients').aggregate(pipeline).toArray()
const ingredient = result[0] as IngredientWithRefs
```
**When to use aggregation**:
- When you need to display related entity names (not just IDs)
- For GET by ID endpoints that show full details
- For list endpoints where joins improve UX
**When NOT to use aggregation**:
- POST/PUT/DELETE operations (use IDs only)
- Simple list endpoints where IDs are sufficient
---
### API Response Patterns
**Success Responses**:
```typescript
// List
{ items: [...], total: 100, page: 1 }
// Single item
{ _id: '...', name: '...', ... }
// Create
{ insertedId: '...' }
// Update
{ modifiedCount: 1 }
// Delete
{ success: true }
```
**Error Responses**:
```typescript
{ error: 'Not found' } // 404
{ error: 'Invalid request' } // 400
{ error: 'Internal server error' } // 500
```
---
### Data Flow Diagram
```
User Action
[Page Component] ← useState/useEffect
API Call (fetch)
[API Route Handler] (/app/api/*/route.ts)
MongoDB Query/Aggregation
[Response] NextResponse.json()
[Page Component] setData(response)
[Display Component] receives data via props
UI Update
```
---
## Adding New Features
### 1. Adding a New Entity (e.g., "Orders")
**Step 1**: Define TypeScript interface in `/lib/types.ts`
```typescript
export interface Order {
_id?: string
orderNumber: string
supplierId: string
items: OrderItem[]
totalAmount: number
status: 'pending' | 'received' | 'cancelled'
createdAt?: Date
updatedAt?: Date
}
export interface OrderWithRefs extends Omit<Order, 'supplierId'> {
supplier?: string
}
```
**Step 2**: Create API routes in `/app/api/orders/`
- `/app/api/orders/route.ts` (GET list, POST)
- `/app/api/orders/[id]/route.ts` (GET, PUT, DELETE)
**Step 3**: Create page component in `/app/dashboard/orders/page.tsx`
```typescript
'use client'
export default function OrdersPage() {
const [orders, setOrders] = useState<OrderWithRefs[]>([])
// Fetch, filter, pagination logic
}
```
**Step 4**: Create modal components in `/components/`
- `AddOrderModal.tsx`
- `ViewOrderModal.tsx`
- `DeleteConfirmModal.tsx` (can reuse existing)
**Step 5**: Create display component
- `OrderTable.tsx` (responsive table/card view)
**Step 6**: Add navigation link in `Sidebar.tsx`
```typescript
<Link href="/dashboard/orders">Orders</Link>
```
---
### 2. Adding Filters to Existing Lists
**Example**: Add date range filter to ingredients
**Step 1**: Add state to page component
```typescript
const [dateFrom, setDateFrom] = useState<string>('')
const [dateTo, setDateTo] = useState<string>('')
```
**Step 2**: Include in fetch params
```typescript
const params = new URLSearchParams({
page: currentPage.toString(),
dateFrom,
dateTo
})
```
**Step 3**: Update API route to handle new params
```typescript
const dateFrom = searchParams.get('dateFrom')
const dateTo = searchParams.get('dateTo')
const query: any = {}
if (dateFrom || dateTo) {
query.createdAt = {}
if (dateFrom) query.createdAt.$gte = new Date(dateFrom)
if (dateTo) query.createdAt.$lte = new Date(dateTo)
}
const items = await db.collection('ingredients').find(query)
```
**Step 4**: Add filter UI
```tsx
<input
type="date"
value={dateFrom}
onChange={e => setDateFrom(e.target.value)}
/>
```
---
### 3. Adding Computed Fields to Forms
**Example**: Add "total price" calculation to ingredients
**Step 1**: Add derived state with `useMemo`
```typescript
const totalPrice = useMemo(() => {
return quantity * unitPrice * (1 + vat / 100)
}, [quantity, unitPrice, vat])
```
**Step 2**: Display in form
```tsx
<div className="bg-gray-50 p-3 rounded">
<span className="font-medium">Total: {totalPrice.toFixed(2)}</span>
</div>
```
**Step 3**: Optional - Save to database
```typescript
const handleSubmit = async () => {
const data = {
quantity,
unitPrice,
vat,
totalPrice // Include computed value
}
await fetch('/api/ingredients', { method: 'POST', body: JSON.stringify(data) })
}
```
---
### 4. Adding Inline Creation to Dropdowns
**Pattern from `AddIngredientModal.tsx`**:
**Step 1**: Add state for inline form
```typescript
const [showInlineCategory, setShowInlineCategory] = useState(false)
const [newCategoryName, setNewCategoryName] = useState('')
```
**Step 2**: Conditional rendering
```tsx
{showInlineCategory ? (
<div className="flex gap-2">
<input
value={newCategoryName}
onChange={e => setNewCategoryName(e.target.value)}
placeholder="New category name"
/>
<button onClick={handleCreateCategory}>Save</button>
<button onClick={() => setShowInlineCategory(false)}>Cancel</button>
</div>
) : (
<>
<select value={selectedCategory} onChange={e => setSelectedCategory(e.target.value)}>
{categories.map(cat => <option key={cat._id} value={cat._id}>{cat.name}</option>)}
</select>
<button onClick={() => setShowInlineCategory(true)}>+ New</button>
</>
)}
```
**Step 3**: Handle creation
```typescript
const handleCreateCategory = async () => {
const response = await fetch('/api/categories', {
method: 'POST',
body: JSON.stringify({ name: newCategoryName })
})
const data = await response.json()
setCategories([...categories, { _id: data.insertedId, name: newCategoryName }])
setSelectedCategory(data.insertedId)
setShowInlineCategory(false)
}
```
---
### 5. Adding Search Functionality
**Step 1**: Add search state
```typescript
const [searchTerm, setSearchTerm] = useState('')
```
**Step 2**: Debounced API call
```typescript
useEffect(() => {
const timer = setTimeout(() => {
fetchData() // Includes searchTerm in API call
}, 300)
return () => clearTimeout(timer)
}, [searchTerm])
```
**Step 3**: API route with case-insensitive search
```typescript
const searchTerm = searchParams.get('search')
const query: any = {}
if (searchTerm) {
query.$or = [
{ name: { $regex: searchTerm, $options: 'i' } },
{ code: { $regex: searchTerm, $options: 'i' } }
]
}
const items = await db.collection('entities').find(query)
```
---
### 6. Adding Pagination
**Pattern** (already implemented in ingredients):
**State**:
```typescript
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const itemsPerPage = 10
```
**API call**:
```typescript
const params = new URLSearchParams({
page: currentPage.toString(),
limit: itemsPerPage.toString()
})
const response = await fetch(`/api/entities?${params}`)
const data = await response.json()
setTotalPages(Math.ceil(data.total / itemsPerPage))
```
**API route**:
```typescript
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const items = await db.collection('entities')
.find(query)
.skip((page - 1) * limit)
.limit(limit)
.toArray()
const total = await db.collection('entities').countDocuments(query)
return NextResponse.json({ items, total })
```
**UI**:
```tsx
<div className="flex gap-2">
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage(p => p - 1)}
>
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button
disabled={currentPage === totalPages}
onClick={() => setCurrentPage(p => p + 1)}
>
Next
</button>
</div>
```
---
## Best Practices Checklist
When adding new code, ensure you follow these practices:
**TypeScript**:
- [ ] All interfaces defined in `/lib/types.ts`
- [ ] Proper typing for props, state, and API responses
- [ ] Use `WithRefs` pattern for aggregated data
**Components**:
- [ ] Use `'use client'` for interactive components
- [ ] Modal components follow standard prop interface
- [ ] Responsive design with Tailwind breakpoints
- [ ] Reusable class variables for consistent styling
**API Routes**:
- [ ] Proper HTTP methods (GET, POST, PUT, DELETE)
- [ ] Error handling with appropriate status codes
- [ ] Timestamps (`createdAt`, `updatedAt`) on write operations
- [ ] Use aggregation for joins when displaying related data
**Data Flow**:
- [ ] `useState` for component state
- [ ] `useEffect` for data fetching
- [ ] `useCallback` for memoized functions
- [ ] `useMemo` for derived/computed values
**Forms**:
- [ ] Client-side validation before API calls
- [ ] Loading states during submissions
- [ ] Success/error feedback to users
- [ ] Reset form after successful submission
**Code Organization**:
- [ ] Page components orchestrate, display components render
- [ ] API routes in `/app/api/[resource]/`
- [ ] Shared components in `/components/`
- [ ] Utilities/types in `/lib/`
---
## Common Patterns Reference
### Auto-Generated Codes
```typescript
// Get next code number
const lastIngredient = await db.collection('ingredients')
.find({})
.sort({ code: -1 })
.limit(1)
.toArray()
const nextNumber = lastIngredient.length > 0
? parseInt(lastIngredient[0].code.split('-')[1]) + 1
: 1
const code = `ING-${String(nextNumber).padStart(3, '0')}`
```
### Price Calculations
```typescript
const netPrice = grossPrice * (1 + vat / 100)
// With discount
const effectiveGross = applyDiscountToGross
? grossPrice - discount
: grossPrice
const effectiveNet = applyDiscountToGross
? effectiveGross * (1 + vat / 100)
: netPrice - discount
```
### Conditional Rendering
```typescript
{loading ? (
<div>Loading...</div>
) : items.length === 0 ? (
<div>No items found</div>
) : (
<Table items={items} />
)}
```
---
**Last Updated**: 2026-02-12
**Version**: 1.0