# 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**: - **Global `/components/`**: ONLY for components reused across multiple pages (e.g., `Sidebar.tsx`, `DeleteConfirmModal.tsx`) - **Page-specific components**: Keep inside the page's own folder (e.g., `/app/dashboard/orders/OrderSummaryCard.tsx`) **⚠️ Important Rule**: If a component is only used by ONE specific page, it should live in that page's folder, NOT in the global `/components/` directory. This keeps the codebase organized and prevents component bloat. **Naming convention**: PascalCase, suffix with type (e.g., `UserModal.tsx`, `OrderTable.tsx`) **Example Structure**: ``` app/dashboard/orders/ ├── page.tsx # Main orders page ├── OrderSummaryCard.tsx # Page-specific component └── OrderFilters.tsx # Page-specific component ``` --- ### `/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([]) 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() { 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: `
` (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 ( <> ) } ``` --- ### 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 (
{/* Form fields */}
) } ``` --- #### View Modal Pattern ```typescript export default function ViewEntityModal({ open, onClose, entityId }: ModalProps) { const [entity, setEntity] = useState(null) useEffect(() => { if (open && entityId) { fetch(`/api/entities/${entityId}`) .then(res => res.json()) .then(setEntity) } }, [open, entityId]) if (!open || !entity) return null return (
{/* Read-only display */}
) } ``` --- ### 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
{/* Desktop: Table */}
{/* table rows */}
{/* Mobile: Cards */}
{items.map(item => (
{/* card content */}
))}
``` --- ## 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 { 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([]) // Fetch, filter, pagination logic } ``` **Step 4**: Create page-specific components in `/app/dashboard/orders/` - `AddOrderModal.tsx` (orders-specific modal) - `ViewOrderModal.tsx` (orders-specific modal) - `OrderTable.tsx` (responsive table/card view) - Note: Only move to `/components/` if these will be reused across multiple pages **Step 5**: Reuse global components if applicable - `DeleteConfirmModal.tsx` (already exists in `/components/` - reusable across all pages) **Step 6**: Add navigation link in `Sidebar.tsx` ```typescript Orders ``` --- ### 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('') const [dateTo, setDateTo] = useState('') ``` **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 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
Total: €{totalPrice.toFixed(2)}
``` **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 ? (
setNewCategoryName(e.target.value)} placeholder="New category name" />
) : ( <> )} ``` **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
Page {currentPage} of {totalPages}
``` --- ## 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/` (only if reused across multiple pages) - [ ] Page-specific components stay in the page's folder, NOT in global `/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 ? (
Loading...
) : items.length === 0 ? (
No items found
) : ( )} ``` --- **Last Updated**: 2026-02-12 **Version**: 1.0