rename products to ingredients
This commit is contained in:
parent
75bafa619f
commit
af49388593
@ -13,7 +13,7 @@ export async function GET(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const product = await db.collection('products').aggregate([
|
const ingredient = await db.collection('ingredients').aggregate([
|
||||||
{ $match: { _id: new ObjectId(id) } },
|
{ $match: { _id: new ObjectId(id) } },
|
||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
@ -56,11 +56,11 @@ export async function GET(
|
|||||||
},
|
},
|
||||||
]).next();
|
]).next();
|
||||||
|
|
||||||
if (!product) {
|
if (!ingredient) {
|
||||||
return NextResponse.json({ error: 'Product not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Ingredient not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(product);
|
return NextResponse.json(ingredient);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
@ -87,14 +87,14 @@ export async function PUT(
|
|||||||
if (body.supplierId !== undefined) update.supplierId = new ObjectId(body.supplierId);
|
if (body.supplierId !== undefined) update.supplierId = new ObjectId(body.supplierId);
|
||||||
|
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const result = await db.collection('products').findOneAndUpdate(
|
const result = await db.collection('ingredients').findOneAndUpdate(
|
||||||
{ _id: new ObjectId(id) },
|
{ _id: new ObjectId(id) },
|
||||||
{ $set: update },
|
{ $set: update },
|
||||||
{ returnDocument: 'after' }
|
{ returnDocument: 'after' }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return NextResponse.json({ error: 'Product not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Ingredient not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(result);
|
return NextResponse.json(result);
|
||||||
@ -111,10 +111,10 @@ export async function DELETE(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const result = await db.collection('products').deleteOne({ _id: new ObjectId(id) });
|
const result = await db.collection('ingredients').deleteOne({ _id: new ObjectId(id) });
|
||||||
|
|
||||||
if (result.deletedCount === 0) {
|
if (result.deletedCount === 0) {
|
||||||
return NextResponse.json({ error: 'Product not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Ingredient not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
@ -72,12 +72,12 @@ export async function GET(request: NextRequest) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const [data, countResult] = await Promise.all([
|
const [data, countResult] = await Promise.all([
|
||||||
db.collection('products')
|
db.collection('ingredients')
|
||||||
.aggregate(pipeline)
|
.aggregate(pipeline)
|
||||||
.skip((page - 1) * limit)
|
.skip((page - 1) * limit)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.toArray(),
|
.toArray(),
|
||||||
db.collection('products')
|
db.collection('ingredients')
|
||||||
.countDocuments(matchStage),
|
.countDocuments(matchStage),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ export async function POST(request: Request) {
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await db.collection('products').insertOne(doc);
|
const result = await db.collection('ingredients').insertOne(doc);
|
||||||
|
|
||||||
return NextResponse.json({ _id: result.insertedId, ...doc }, { status: 201 });
|
return NextResponse.json({ _id: result.insertedId, ...doc }, { status: 201 });
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import Sidebar from '@/components/Sidebar';
|
import Sidebar from '@/components/Sidebar';
|
||||||
import ProductTable, { Product } from '@/components/ProductTable';
|
import IngredientTable, { Ingredient } from '@/components/IngredientTable';
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
_id: string;
|
_id: string;
|
||||||
@ -15,16 +15,16 @@ interface Subcategory {
|
|||||||
categoryId: string;
|
categoryId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductsResponse {
|
interface IngredientsResponse {
|
||||||
data: Product[];
|
data: Ingredient[];
|
||||||
total: number;
|
total: number;
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductsPage() {
|
export default function IngredientsPage() {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [ingredients, setIngredients] = useState<Ingredient[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@ -53,8 +53,8 @@ export default function ProductsPage() {
|
|||||||
? allSubcategories.filter(s => s.categoryId === categoryId)
|
? allSubcategories.filter(s => s.categoryId === categoryId)
|
||||||
: allSubcategories;
|
: allSubcategories;
|
||||||
|
|
||||||
// Fetch products
|
// Fetch ingredients
|
||||||
const fetchProducts = useCallback(async () => {
|
const fetchIngredients = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('page', String(page));
|
params.set('page', String(page));
|
||||||
@ -63,16 +63,16 @@ export default function ProductsPage() {
|
|||||||
if (categoryId) params.set('categoryId', categoryId);
|
if (categoryId) params.set('categoryId', categoryId);
|
||||||
if (subcategoryId) params.set('subcategoryId', subcategoryId);
|
if (subcategoryId) params.set('subcategoryId', subcategoryId);
|
||||||
|
|
||||||
const res = await fetch(`/api/products?${params}`);
|
const res = await fetch(`/api/ingredients?${params}`);
|
||||||
const data: ProductsResponse = await res.json();
|
const data: IngredientsResponse = await res.json();
|
||||||
setProducts(data.data);
|
setIngredients(data.data);
|
||||||
setTotal(data.total);
|
setTotal(data.total);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [page, search, categoryId, subcategoryId]);
|
}, [page, search, categoryId, subcategoryId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProducts();
|
fetchIngredients();
|
||||||
}, [fetchProducts]);
|
}, [fetchIngredients]);
|
||||||
|
|
||||||
// Reset page when filters change
|
// Reset page when filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -136,13 +136,13 @@ export default function ProductsPage() {
|
|||||||
<div className="p-4 sm:p-6 lg:p-8">
|
<div className="p-4 sm:p-6 lg:p-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6 lg:mb-8">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6 lg:mb-8">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Product Management</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Ingredient Management</h1>
|
||||||
<div className="flex gap-2 sm:gap-3">
|
<div className="flex gap-2 sm:gap-3">
|
||||||
<button className="flex-1 sm:flex-none px-4 sm:px-5 py-2.5 rounded-lg border border-gray-300 bg-white text-gray-700 font-medium hover:bg-gray-50 transition-colors text-sm sm:text-base">
|
<button className="flex-1 sm:flex-none px-4 sm:px-5 py-2.5 rounded-lg border border-gray-300 bg-white text-gray-700 font-medium hover:bg-gray-50 transition-colors text-sm sm:text-base">
|
||||||
Import
|
Import
|
||||||
</button>
|
</button>
|
||||||
<button className="flex-1 sm:flex-none px-4 sm:px-5 py-2.5 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 transition-colors text-sm sm:text-base whitespace-nowrap">
|
<button className="flex-1 sm:flex-none px-4 sm:px-5 py-2.5 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 transition-colors text-sm sm:text-base whitespace-nowrap">
|
||||||
+ Create Product
|
+ Add Ingredient
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -151,7 +151,7 @@ export default function ProductsPage() {
|
|||||||
<div className="bg-white rounded-lg border border-gray-200 p-4 sm:p-6 mb-4 sm:mb-6">
|
<div className="bg-white rounded-lg border border-gray-200 p-4 sm:p-6 mb-4 sm:mb-6">
|
||||||
<div className="flex flex-col sm:flex-row sm:flex-wrap items-stretch sm:items-center gap-3 sm:gap-4 mb-4">
|
<div className="flex flex-col sm:flex-row sm:flex-wrap items-stretch sm:items-center gap-3 sm:gap-4 mb-4">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<form onSubmit={handleSearch} className="w-full sm:flex-1 sm:min-w-[250px]">
|
<form onSubmit={handleSearch} className="w-full sm:flex-1 sm:min-w-62.5">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -237,17 +237,17 @@ export default function ProductsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Product Table */}
|
{/* Ingredient Table */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-12 text-center">
|
<div className="bg-white rounded-lg border border-gray-200 p-12 text-center">
|
||||||
<p className="text-gray-500">Loading products...</p>
|
<p className="text-gray-500">Loading ingredients...</p>
|
||||||
</div>
|
</div>
|
||||||
) : products.length === 0 ? (
|
) : ingredients.length === 0 ? (
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-12 text-center">
|
<div className="bg-white rounded-lg border border-gray-200 p-12 text-center">
|
||||||
<p className="text-gray-500">No products found.</p>
|
<p className="text-gray-500">No ingredients found.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ProductTable products={products} />
|
<IngredientTable ingredients={ingredients} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
@ -268,7 +268,6 @@ export default function ProductsPage() {
|
|||||||
</button>
|
</button>
|
||||||
{Array.from({ length: totalPages }, (_, i) => i + 1)
|
{Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||||
.filter(p => {
|
.filter(p => {
|
||||||
// Show first, last, current, and neighbors
|
|
||||||
if (p === 1 || p === totalPages) return true;
|
if (p === 1 || p === totalPages) return true;
|
||||||
if (Math.abs(p - page) <= 1) return true;
|
if (Math.abs(p - page) <= 1) return true;
|
||||||
return false;
|
return false;
|
||||||
@ -280,7 +279,7 @@ export default function ProductsPage() {
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(p)}
|
onClick={() => setPage(p)}
|
||||||
className={`px-2 sm:px-3 py-2 rounded-lg text-xs sm:text-sm min-w-[32px] sm:min-w-[36px] ${
|
className={`px-2 sm:px-3 py-2 rounded-lg text-xs sm:text-sm min-w-8 sm:min-w-9 ${
|
||||||
p === page
|
p === page
|
||||||
? 'bg-blue-600 text-white font-medium'
|
? 'bg-blue-600 text-white font-medium'
|
||||||
: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors'
|
: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors'
|
||||||
@ -14,7 +14,7 @@ const geistMono = Geist_Mono({
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "KitchenOS - Restaurant Inventory Management",
|
title: "KitchenOS - Restaurant Inventory Management",
|
||||||
description: "Professional restaurant inventory and product management system",
|
description: "Professional restaurant inventory and ingredient management system",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
redirect('/dashboard/products');
|
redirect('/dashboard/ingredients');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
export interface Product {
|
export interface Ingredient {
|
||||||
_id: string;
|
_id: string;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -17,28 +17,28 @@ export interface Product {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductTableProps {
|
interface IngredientTableProps {
|
||||||
products: Product[];
|
ingredients: Ingredient[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductTable({ products }: ProductTableProps) {
|
export default function IngredientTable({ ingredients }: IngredientTableProps) {
|
||||||
const [selectedProducts, setSelectedProducts] = useState<Set<string>>(new Set());
|
const [selectedIngredients, setSelectedIngredients] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const toggleProduct = (id: string) => {
|
const toggleIngredient = (id: string) => {
|
||||||
const newSelected = new Set(selectedProducts);
|
const newSelected = new Set(selectedIngredients);
|
||||||
if (newSelected.has(id)) {
|
if (newSelected.has(id)) {
|
||||||
newSelected.delete(id);
|
newSelected.delete(id);
|
||||||
} else {
|
} else {
|
||||||
newSelected.add(id);
|
newSelected.add(id);
|
||||||
}
|
}
|
||||||
setSelectedProducts(newSelected);
|
setSelectedIngredients(newSelected);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAll = () => {
|
const toggleAll = () => {
|
||||||
if (selectedProducts.size === products.length) {
|
if (selectedIngredients.size === ingredients.length) {
|
||||||
setSelectedProducts(new Set());
|
setSelectedIngredients(new Set());
|
||||||
} else {
|
} else {
|
||||||
setSelectedProducts(new Set(products.map(p => p._id)));
|
setSelectedIngredients(new Set(ingredients.map(i => i._id)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,13 +57,13 @@ export default function ProductTable({ products }: ProductTableProps) {
|
|||||||
<th className="px-4 lg:px-6 py-3 text-left">
|
<th className="px-4 lg:px-6 py-3 text-left">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedProducts.size === products.length && products.length > 0}
|
checked={selectedIngredients.size === ingredients.length && ingredients.length > 0}
|
||||||
onChange={toggleAll}
|
onChange={toggleAll}
|
||||||
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 lg:px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
<th className="px-4 lg:px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
Product
|
Ingredient
|
||||||
</th>
|
</th>
|
||||||
<th className="hidden lg:table-cell px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
<th className="hidden lg:table-cell px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
Category
|
Category
|
||||||
@ -92,38 +92,38 @@ export default function ProductTable({ products }: ProductTableProps) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{products.map((product) => (
|
{ingredients.map((ingredient) => (
|
||||||
<tr key={product._id} className="hover:bg-gray-50 transition-colors">
|
<tr key={ingredient._id} className="hover:bg-gray-50 transition-colors">
|
||||||
<td className="px-4 lg:px-6 py-4">
|
<td className="px-4 lg:px-6 py-4">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedProducts.has(product._id)}
|
checked={selectedIngredients.has(ingredient._id)}
|
||||||
onChange={() => toggleProduct(product._id)}
|
onChange={() => toggleIngredient(ingredient._id)}
|
||||||
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 lg:px-6 py-4">
|
<td className="px-4 lg:px-6 py-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-900">{product.name}</p>
|
<p className="text-sm font-medium text-gray-900">{ingredient.name}</p>
|
||||||
<p className="text-xs text-gray-500">ID: {product.code}</p>
|
<p className="text-xs text-gray-500">ID: {ingredient.code}</p>
|
||||||
<p className="text-xs text-gray-500 lg:hidden mt-1">
|
<p className="text-xs text-gray-500 lg:hidden mt-1">
|
||||||
{product.category} • {product.quantity} {product.unit}
|
{ingredient.category} • {ingredient.quantity} {ingredient.unit}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="hidden lg:table-cell px-6 py-4 text-sm text-gray-700">{product.category}</td>
|
<td className="hidden lg:table-cell px-6 py-4 text-sm text-gray-700">{ingredient.category}</td>
|
||||||
<td className="hidden xl:table-cell px-6 py-4 text-sm text-gray-700">{product.subcategory}</td>
|
<td className="hidden xl:table-cell px-6 py-4 text-sm text-gray-700">{ingredient.subcategory}</td>
|
||||||
<td className="px-4 lg:px-6 py-4 text-sm text-gray-700">
|
<td className="px-4 lg:px-6 py-4 text-sm text-gray-700">
|
||||||
{product.quantity} {product.unit}
|
{ingredient.quantity} {ingredient.unit}
|
||||||
</td>
|
</td>
|
||||||
<td className="hidden lg:table-cell px-6 py-4 text-sm text-gray-700">€{product.unitPrice.toFixed(2)}</td>
|
<td className="hidden lg:table-cell px-6 py-4 text-sm text-gray-700">€{ingredient.unitPrice.toFixed(2)}</td>
|
||||||
<td className="hidden xl:table-cell px-6 py-4 text-sm text-gray-700">{product.vat}%</td>
|
<td className="hidden xl:table-cell px-6 py-4 text-sm text-gray-700">{ingredient.vat}%</td>
|
||||||
<td className="px-4 lg:px-6 py-4 text-sm font-bold text-gray-900">
|
<td className="px-4 lg:px-6 py-4 text-sm font-bold text-gray-900">
|
||||||
€{calculateNetPrice(product.unitPrice, product.vat).toFixed(2)}
|
€{calculateNetPrice(ingredient.unitPrice, ingredient.vat).toFixed(2)}
|
||||||
</td>
|
</td>
|
||||||
<td className="hidden lg:table-cell px-6 py-4">
|
<td className="hidden lg:table-cell px-6 py-4">
|
||||||
<a href="#" className="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
<a href="#" className="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
||||||
{product.supplier}
|
{ingredient.supplier}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 lg:px-6 py-4">
|
<td className="px-4 lg:px-6 py-4">
|
||||||
@ -142,20 +142,20 @@ export default function ProductTable({ products }: ProductTableProps) {
|
|||||||
|
|
||||||
{/* Mobile Card View */}
|
{/* Mobile Card View */}
|
||||||
<div className="md:hidden space-y-4">
|
<div className="md:hidden space-y-4">
|
||||||
{products.map((product) => (
|
{ingredients.map((ingredient) => (
|
||||||
<div key={product._id} className="bg-white rounded-lg border border-gray-200 p-4">
|
<div key={ingredient._id} className="bg-white rounded-lg border border-gray-200 p-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedProducts.has(product._id)}
|
checked={selectedIngredients.has(ingredient._id)}
|
||||||
onChange={() => toggleProduct(product._id)}
|
onChange={() => toggleIngredient(ingredient._id)}
|
||||||
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
|
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-gray-900">{product.name}</h3>
|
<h3 className="text-sm font-semibold text-gray-900">{ingredient.name}</h3>
|
||||||
<p className="text-xs text-gray-500 mt-0.5">ID: {product.code}</p>
|
<p className="text-xs text-gray-500 mt-0.5">ID: {ingredient.code}</p>
|
||||||
</div>
|
</div>
|
||||||
<button className="text-gray-400 hover:text-gray-600">
|
<button className="text-gray-400 hover:text-gray-600">
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
@ -167,28 +167,28 @@ export default function ProductTable({ products }: ProductTableProps) {
|
|||||||
<div className="mt-3 grid grid-cols-2 gap-3">
|
<div className="mt-3 grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Category</p>
|
<p className="text-xs text-gray-500">Category</p>
|
||||||
<p className="text-sm text-gray-900 mt-0.5">{product.category}</p>
|
<p className="text-sm text-gray-900 mt-0.5">{ingredient.category}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Subcategory</p>
|
<p className="text-xs text-gray-500">Subcategory</p>
|
||||||
<p className="text-sm text-gray-900 mt-0.5">{product.subcategory}</p>
|
<p className="text-sm text-gray-900 mt-0.5">{ingredient.subcategory}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Quantity</p>
|
<p className="text-xs text-gray-500">Quantity</p>
|
||||||
<p className="text-sm text-gray-900 mt-0.5">{product.quantity} {product.unit}</p>
|
<p className="text-sm text-gray-900 mt-0.5">{ingredient.quantity} {ingredient.unit}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Unit Price</p>
|
<p className="text-xs text-gray-500">Unit Price</p>
|
||||||
<p className="text-sm text-gray-900 mt-0.5">€{product.unitPrice.toFixed(2)}</p>
|
<p className="text-sm text-gray-900 mt-0.5">€{ingredient.unitPrice.toFixed(2)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">VAT</p>
|
<p className="text-xs text-gray-500">VAT</p>
|
||||||
<p className="text-sm text-gray-900 mt-0.5">{product.vat}%</p>
|
<p className="text-sm text-gray-900 mt-0.5">{ingredient.vat}%</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Net Price</p>
|
<p className="text-xs text-gray-500">Net Price</p>
|
||||||
<p className="text-sm font-bold text-gray-900 mt-0.5">
|
<p className="text-sm font-bold text-gray-900 mt-0.5">
|
||||||
€{calculateNetPrice(product.unitPrice, product.vat).toFixed(2)}
|
€{calculateNetPrice(ingredient.unitPrice, ingredient.vat).toFixed(2)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -197,12 +197,12 @@ export default function ProductTable({ products }: ProductTableProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Supplier</p>
|
<p className="text-xs text-gray-500">Supplier</p>
|
||||||
<a href="#" className="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
<a href="#" className="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
||||||
{product.supplier}
|
{ingredient.supplier}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-xs text-gray-500">Updated</p>
|
<p className="text-xs text-gray-500">Updated</p>
|
||||||
<p className="text-xs text-gray-700 mt-0.5">{new Date(product.updatedAt).toLocaleDateString()}</p>
|
<p className="text-xs text-gray-700 mt-0.5">{new Date(ingredient.updatedAt).toLocaleDateString()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -11,7 +11,7 @@ interface NavItem {
|
|||||||
|
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ label: 'Dashboard', icon: '📊', href: '/dashboard' },
|
{ label: 'Dashboard', icon: '📊', href: '/dashboard' },
|
||||||
{ label: 'Products', icon: '📦', active: true, href: '/dashboard/products' },
|
{ label: 'Ingredients', icon: '📦', active: true, href: '/dashboard/ingredients' },
|
||||||
{ label: 'Categories', icon: '🏷️', href: '/dashboard/categories' },
|
{ label: 'Categories', icon: '🏷️', href: '/dashboard/categories' },
|
||||||
{ label: 'Subcategories', icon: '🔖', href: '/dashboard/subcategories' },
|
{ label: 'Subcategories', icon: '🔖', href: '/dashboard/subcategories' },
|
||||||
{ label: 'Suppliers', icon: '🚚', href: '/dashboard/suppliers' },
|
{ label: 'Suppliers', icon: '🚚', href: '/dashboard/suppliers' },
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export interface Supplier {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductDoc {
|
export interface IngredientDoc {
|
||||||
_id: ObjectId;
|
_id: ObjectId;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -37,7 +37,7 @@ export interface ProductDoc {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductWithRefs {
|
export interface IngredientWithRefs {
|
||||||
_id: string;
|
_id: string;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ async function seed() {
|
|||||||
db.collection('categories').deleteMany({}),
|
db.collection('categories').deleteMany({}),
|
||||||
db.collection('subcategories').deleteMany({}),
|
db.collection('subcategories').deleteMany({}),
|
||||||
db.collection('suppliers').deleteMany({}),
|
db.collection('suppliers').deleteMany({}),
|
||||||
db.collection('products').deleteMany({}),
|
db.collection('ingredients').deleteMany({}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -71,10 +71,10 @@ async function seed() {
|
|||||||
]);
|
]);
|
||||||
console.log('Inserted 5 suppliers');
|
console.log('Inserted 5 suppliers');
|
||||||
|
|
||||||
// Insert products
|
// Insert ingredients
|
||||||
await db.collection('products').insertMany([
|
await db.collection('ingredients').insertMany([
|
||||||
{
|
{
|
||||||
code: 'PRD-001',
|
code: 'ING-001',
|
||||||
name: 'Organic Butter',
|
name: 'Organic Butter',
|
||||||
categoryId: categoryIds.dairy,
|
categoryId: categoryIds.dairy,
|
||||||
subcategoryId: subcategoryIds.butterSpreads,
|
subcategoryId: subcategoryIds.butterSpreads,
|
||||||
@ -87,7 +87,7 @@ async function seed() {
|
|||||||
updatedAt: new Date('2026-02-10'),
|
updatedAt: new Date('2026-02-10'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'PRD-002',
|
code: 'ING-002',
|
||||||
name: 'All-Purpose Flour',
|
name: 'All-Purpose Flour',
|
||||||
categoryId: categoryIds.dryGoods,
|
categoryId: categoryIds.dryGoods,
|
||||||
subcategoryId: subcategoryIds.flours,
|
subcategoryId: subcategoryIds.flours,
|
||||||
@ -100,7 +100,7 @@ async function seed() {
|
|||||||
updatedAt: new Date('2026-02-09'),
|
updatedAt: new Date('2026-02-09'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'PRD-003',
|
code: 'ING-003',
|
||||||
name: 'Atlantic Salmon Fillet',
|
name: 'Atlantic Salmon Fillet',
|
||||||
categoryId: categoryIds.seafood,
|
categoryId: categoryIds.seafood,
|
||||||
subcategoryId: subcategoryIds.freshFish,
|
subcategoryId: subcategoryIds.freshFish,
|
||||||
@ -113,7 +113,7 @@ async function seed() {
|
|||||||
updatedAt: new Date('2026-02-11'),
|
updatedAt: new Date('2026-02-11'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'PRD-004',
|
code: 'ING-004',
|
||||||
name: 'Madagascar Vanilla Extract',
|
name: 'Madagascar Vanilla Extract',
|
||||||
categoryId: categoryIds.dryGoods,
|
categoryId: categoryIds.dryGoods,
|
||||||
subcategoryId: subcategoryIds.spicesExtracts,
|
subcategoryId: subcategoryIds.spicesExtracts,
|
||||||
@ -126,7 +126,7 @@ async function seed() {
|
|||||||
updatedAt: new Date('2026-02-08'),
|
updatedAt: new Date('2026-02-08'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'PRD-005',
|
code: 'ING-005',
|
||||||
name: 'Fresh Avocados',
|
name: 'Fresh Avocados',
|
||||||
categoryId: categoryIds.produce,
|
categoryId: categoryIds.produce,
|
||||||
subcategoryId: subcategoryIds.fruits,
|
subcategoryId: subcategoryIds.fruits,
|
||||||
@ -139,7 +139,7 @@ async function seed() {
|
|||||||
updatedAt: new Date('2026-02-11'),
|
updatedAt: new Date('2026-02-11'),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
console.log('Inserted 5 products');
|
console.log('Inserted 5 ingredients');
|
||||||
|
|
||||||
console.log('Seeding complete!');
|
console.log('Seeding complete!');
|
||||||
await client.close();
|
await client.close();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user