From 350d60302b4a73679d73529176e271e331fa580b Mon Sep 17 00:00:00 2001 From: CelaniDe Date: Thu, 12 Feb 2026 21:04:43 +0100 Subject: [PATCH] create README,md --- README.md | 995 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 973 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e215bc4..5b91956 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,987 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Icostpro - Developer Guide -## Getting Started +> **Purpose**: This README helps developers understand where to find code and how to write new features that align with the existing architecture. -First, run the development server: +--- -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +## 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 ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +**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`) -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +--- -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +### `/components` - Reusable React Components -## Learn More +``` +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 +``` -To learn more about Next.js, take a look at the following resources: +**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) -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +**Where to add new components**: Add to `/components/` with descriptive names +**Naming convention**: PascalCase, suffix with type (e.g., `UserModal.tsx`, `OrderTable.tsx`) -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +--- -## Deploy on Vercel +### `/lib` - Utilities & Shared Logic -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +``` +lib/ +├── mongodb.ts # MongoDB connection pool (singleton pattern) +└── types.ts # TypeScript interfaces for all entities +``` -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +**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 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 +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/` +- [ ] 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