connect with mongodb
This commit is contained in:
parent
9507e8d6d6
commit
75bafa619f
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npx tsc:*)",
|
||||||
|
"Bash(npx tsx:*)",
|
||||||
|
"Bash(npm run build:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/api/categories/route.ts
Normal file
31
app/api/categories/route.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getDb } from '@/lib/mongodb';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const db = await getDb();
|
||||||
|
const categories = await db.collection('categories')
|
||||||
|
.find()
|
||||||
|
.sort({ name: 1 })
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return NextResponse.json(categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const body = await request.json();
|
||||||
|
const { name } = body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
const now = new Date();
|
||||||
|
const result = await db.collection('categories').insertOne({
|
||||||
|
name,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ _id: result.insertedId, name, createdAt: now, updatedAt: now }, { status: 201 });
|
||||||
|
}
|
||||||
121
app/api/products/[id]/route.ts
Normal file
121
app/api/products/[id]/route.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
import { getDb } from '@/lib/mongodb';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_request: Request,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
if (!ObjectId.isValid(id)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
const product = await db.collection('products').aggregate([
|
||||||
|
{ $match: { _id: new ObjectId(id) } },
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'categories',
|
||||||
|
localField: 'categoryId',
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'categoryDoc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'subcategories',
|
||||||
|
localField: 'subcategoryId',
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'subcategoryDoc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'suppliers',
|
||||||
|
localField: 'supplierId',
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'supplierDoc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
code: 1,
|
||||||
|
name: 1,
|
||||||
|
category: { $arrayElemAt: ['$categoryDoc.name', 0] },
|
||||||
|
subcategory: { $arrayElemAt: ['$subcategoryDoc.name', 0] },
|
||||||
|
quantity: 1,
|
||||||
|
unit: 1,
|
||||||
|
unitPrice: 1,
|
||||||
|
vat: 1,
|
||||||
|
supplier: { $arrayElemAt: ['$supplierDoc.name', 0] },
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]).next();
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return NextResponse.json({ error: 'Product not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
if (!ObjectId.isValid(id)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const update: Record<string, unknown> = { updatedAt: new Date() };
|
||||||
|
|
||||||
|
if (body.code !== undefined) update.code = body.code;
|
||||||
|
if (body.name !== undefined) update.name = body.name;
|
||||||
|
if (body.categoryId !== undefined) update.categoryId = new ObjectId(body.categoryId);
|
||||||
|
if (body.subcategoryId !== undefined) update.subcategoryId = new ObjectId(body.subcategoryId);
|
||||||
|
if (body.quantity !== undefined) update.quantity = body.quantity;
|
||||||
|
if (body.unit !== undefined) update.unit = body.unit;
|
||||||
|
if (body.unitPrice !== undefined) update.unitPrice = body.unitPrice;
|
||||||
|
if (body.vat !== undefined) update.vat = body.vat;
|
||||||
|
if (body.supplierId !== undefined) update.supplierId = new ObjectId(body.supplierId);
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
const result = await db.collection('products').findOneAndUpdate(
|
||||||
|
{ _id: new ObjectId(id) },
|
||||||
|
{ $set: update },
|
||||||
|
{ returnDocument: 'after' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return NextResponse.json({ error: 'Product not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
_request: Request,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
if (!ObjectId.isValid(id)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
const result = await db.collection('products').deleteOne({ _id: new ObjectId(id) });
|
||||||
|
|
||||||
|
if (result.deletedCount === 0) {
|
||||||
|
return NextResponse.json({ error: 'Product not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
}
|
||||||
119
app/api/products/route.ts
Normal file
119
app/api/products/route.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
import { getDb } from '@/lib/mongodb';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = request.nextUrl;
|
||||||
|
const page = Math.max(1, parseInt(searchParams.get('page') || '1'));
|
||||||
|
const limit = Math.min(100, Math.max(1, parseInt(searchParams.get('limit') || '10')));
|
||||||
|
const search = searchParams.get('search') || '';
|
||||||
|
const categoryId = searchParams.get('categoryId');
|
||||||
|
const subcategoryId = searchParams.get('subcategoryId');
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
|
||||||
|
const matchStage: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (categoryId) {
|
||||||
|
matchStage.categoryId = new ObjectId(categoryId);
|
||||||
|
}
|
||||||
|
if (subcategoryId) {
|
||||||
|
matchStage.subcategoryId = new ObjectId(subcategoryId);
|
||||||
|
}
|
||||||
|
if (search) {
|
||||||
|
matchStage.$or = [
|
||||||
|
{ name: { $regex: search, $options: 'i' } },
|
||||||
|
{ code: { $regex: search, $options: 'i' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipeline = [
|
||||||
|
{ $match: matchStage },
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'categories',
|
||||||
|
localField: 'categoryId',
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'categoryDoc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'subcategories',
|
||||||
|
localField: 'subcategoryId',
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'subcategoryDoc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'suppliers',
|
||||||
|
localField: 'supplierId',
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'supplierDoc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
code: 1,
|
||||||
|
name: 1,
|
||||||
|
category: { $arrayElemAt: ['$categoryDoc.name', 0] },
|
||||||
|
subcategory: { $arrayElemAt: ['$subcategoryDoc.name', 0] },
|
||||||
|
quantity: 1,
|
||||||
|
unit: 1,
|
||||||
|
unitPrice: 1,
|
||||||
|
vat: 1,
|
||||||
|
supplier: { $arrayElemAt: ['$supplierDoc.name', 0] },
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $sort: { createdAt: -1 as const } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [data, countResult] = await Promise.all([
|
||||||
|
db.collection('products')
|
||||||
|
.aggregate(pipeline)
|
||||||
|
.skip((page - 1) * limit)
|
||||||
|
.limit(limit)
|
||||||
|
.toArray(),
|
||||||
|
db.collection('products')
|
||||||
|
.countDocuments(matchStage),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data,
|
||||||
|
total: countResult,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const body = await request.json();
|
||||||
|
const { code, name, categoryId, subcategoryId, quantity, unit, unitPrice, vat, supplierId } = body;
|
||||||
|
|
||||||
|
if (!code || !name || !categoryId || !subcategoryId || !quantity || !unit || unitPrice == null || vat == null || !supplierId) {
|
||||||
|
return NextResponse.json({ error: 'All fields are required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
const now = new Date();
|
||||||
|
const doc = {
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
categoryId: new ObjectId(categoryId),
|
||||||
|
subcategoryId: new ObjectId(subcategoryId),
|
||||||
|
quantity,
|
||||||
|
unit,
|
||||||
|
unitPrice,
|
||||||
|
vat,
|
||||||
|
supplierId: new ObjectId(supplierId),
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await db.collection('products').insertOne(doc);
|
||||||
|
|
||||||
|
return NextResponse.json({ _id: result.insertedId, ...doc }, { status: 201 });
|
||||||
|
}
|
||||||
42
app/api/subcategories/route.ts
Normal file
42
app/api/subcategories/route.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
import { getDb } from '@/lib/mongodb';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = request.nextUrl;
|
||||||
|
const categoryId = searchParams.get('categoryId');
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
const filter: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (categoryId) {
|
||||||
|
filter.categoryId = new ObjectId(categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subcategories = await db.collection('subcategories')
|
||||||
|
.find(filter)
|
||||||
|
.sort({ name: 1 })
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return NextResponse.json(subcategories);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const body = await request.json();
|
||||||
|
const { name, categoryId } = body;
|
||||||
|
|
||||||
|
if (!name || !categoryId) {
|
||||||
|
return NextResponse.json({ error: 'Name and categoryId are required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
const now = new Date();
|
||||||
|
const result = await db.collection('subcategories').insertOne({
|
||||||
|
name,
|
||||||
|
categoryId: new ObjectId(categoryId),
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ _id: result.insertedId, name, categoryId, createdAt: now, updatedAt: now }, { status: 201 });
|
||||||
|
}
|
||||||
31
app/api/suppliers/route.ts
Normal file
31
app/api/suppliers/route.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getDb } from '@/lib/mongodb';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const db = await getDb();
|
||||||
|
const suppliers = await db.collection('suppliers')
|
||||||
|
.find()
|
||||||
|
.sort({ name: 1 })
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return NextResponse.json(suppliers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const body = await request.json();
|
||||||
|
const { name } = body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
|
const now = new Date();
|
||||||
|
const result = await db.collection('suppliers').insertOne({
|
||||||
|
name,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ _id: result.insertedId, name, createdAt: now, updatedAt: now }, { status: 201 });
|
||||||
|
}
|
||||||
@ -1,74 +1,116 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } 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 ProductTable, { Product } from '@/components/ProductTable';
|
||||||
|
|
||||||
const sampleProducts: Product[] = [
|
interface Category {
|
||||||
{
|
_id: string;
|
||||||
id: 'PRD-001',
|
name: string;
|
||||||
name: 'Organic Butter',
|
}
|
||||||
category: 'Dairy',
|
|
||||||
subcategory: 'Butter & Spreads',
|
interface Subcategory {
|
||||||
quantity: 25,
|
_id: string;
|
||||||
unit: 'kg',
|
name: string;
|
||||||
unitPrice: 4.50,
|
categoryId: string;
|
||||||
vat: 9,
|
}
|
||||||
supplier: 'DairyFresh Co.',
|
|
||||||
lastUpdated: '2026-02-10',
|
interface ProductsResponse {
|
||||||
},
|
data: Product[];
|
||||||
{
|
total: number;
|
||||||
id: 'PRD-002',
|
page: number;
|
||||||
name: 'All-Purpose Flour',
|
limit: number;
|
||||||
category: 'Dry Goods',
|
}
|
||||||
subcategory: 'Flours',
|
|
||||||
quantity: 150,
|
|
||||||
unit: 'kg',
|
|
||||||
unitPrice: 0.85,
|
|
||||||
vat: 9,
|
|
||||||
supplier: 'GrainMasters',
|
|
||||||
lastUpdated: '2026-02-09',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'PRD-003',
|
|
||||||
name: 'Atlantic Salmon Fillet',
|
|
||||||
category: 'Seafood',
|
|
||||||
subcategory: 'Fresh Fish',
|
|
||||||
quantity: 12,
|
|
||||||
unit: 'kg',
|
|
||||||
unitPrice: 18.90,
|
|
||||||
vat: 9,
|
|
||||||
supplier: 'OceanHarvest',
|
|
||||||
lastUpdated: '2026-02-11',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'PRD-004',
|
|
||||||
name: 'Madagascar Vanilla Extract',
|
|
||||||
category: 'Dry Goods',
|
|
||||||
subcategory: 'Spices & Extracts',
|
|
||||||
quantity: 8,
|
|
||||||
unit: 'L',
|
|
||||||
unitPrice: 45.00,
|
|
||||||
vat: 21,
|
|
||||||
supplier: 'SpiceWorld Ltd',
|
|
||||||
lastUpdated: '2026-02-08',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'PRD-005',
|
|
||||||
name: 'Fresh Avocados',
|
|
||||||
category: 'Produce',
|
|
||||||
subcategory: 'Fruits',
|
|
||||||
quantity: 40,
|
|
||||||
unit: 'pcs',
|
|
||||||
unitPrice: 1.20,
|
|
||||||
vat: 9,
|
|
||||||
supplier: 'FreshFarm Direct',
|
|
||||||
lastUpdated: '2026-02-11',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ProductsPage() {
|
export default function ProductsPage() {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [searchInput, setSearchInput] = useState('');
|
||||||
|
const [categoryId, setCategoryId] = useState('');
|
||||||
|
const [subcategoryId, setSubcategoryId] = useState('');
|
||||||
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
|
const [allSubcategories, setAllSubcategories] = useState<Subcategory[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const limit = 10;
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
// Fetch categories and subcategories on mount
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([
|
||||||
|
fetch('/api/categories').then(r => r.json()),
|
||||||
|
fetch('/api/subcategories').then(r => r.json()),
|
||||||
|
]).then(([cats, subs]) => {
|
||||||
|
setCategories(cats);
|
||||||
|
setAllSubcategories(subs);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredSubcategories = categoryId
|
||||||
|
? allSubcategories.filter(s => s.categoryId === categoryId)
|
||||||
|
: allSubcategories;
|
||||||
|
|
||||||
|
// Fetch products
|
||||||
|
const fetchProducts = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('page', String(page));
|
||||||
|
params.set('limit', String(limit));
|
||||||
|
if (search) params.set('search', search);
|
||||||
|
if (categoryId) params.set('categoryId', categoryId);
|
||||||
|
if (subcategoryId) params.set('subcategoryId', subcategoryId);
|
||||||
|
|
||||||
|
const res = await fetch(`/api/products?${params}`);
|
||||||
|
const data: ProductsResponse = await res.json();
|
||||||
|
setProducts(data.data);
|
||||||
|
setTotal(data.total);
|
||||||
|
setLoading(false);
|
||||||
|
}, [page, search, categoryId, subcategoryId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProducts();
|
||||||
|
}, [fetchProducts]);
|
||||||
|
|
||||||
|
// Reset page when filters change
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1);
|
||||||
|
}, [search, categoryId, subcategoryId]);
|
||||||
|
|
||||||
|
// Clear subcategory when category changes
|
||||||
|
useEffect(() => {
|
||||||
|
setSubcategoryId('');
|
||||||
|
}, [categoryId]);
|
||||||
|
|
||||||
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSearch(searchInput);
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeFilters: { label: string; onClear: () => void }[] = [];
|
||||||
|
if (categoryId) {
|
||||||
|
const cat = categories.find(c => c._id === categoryId);
|
||||||
|
if (cat) activeFilters.push({ label: `Category: ${cat.name}`, onClear: () => setCategoryId('') });
|
||||||
|
}
|
||||||
|
if (subcategoryId) {
|
||||||
|
const sub = allSubcategories.find(s => s._id === subcategoryId);
|
||||||
|
if (sub) activeFilters.push({ label: `Subcategory: ${sub.name}`, onClear: () => setSubcategoryId('') });
|
||||||
|
}
|
||||||
|
if (search) {
|
||||||
|
activeFilters.push({ label: `Search: "${search}"`, onClear: () => { setSearch(''); setSearchInput(''); } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearAllFilters = () => {
|
||||||
|
setCategoryId('');
|
||||||
|
setSubcategoryId('');
|
||||||
|
setSearch('');
|
||||||
|
setSearchInput('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const startItem = total === 0 ? 0 : (page - 1) * limit + 1;
|
||||||
|
const endItem = Math.min(page * limit, total);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-gray-50">
|
<div className="flex min-h-screen bg-gray-50">
|
||||||
@ -87,7 +129,7 @@ export default function ProductsPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<h1 className="text-lg font-bold text-gray-900">KitchenOS</h1>
|
<h1 className="text-lg font-bold text-gray-900">KitchenOS</h1>
|
||||||
<div className="w-6" /> {/* Spacer for centering */}
|
<div className="w-6" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -109,11 +151,14 @@ 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 */}
|
||||||
<div className="w-full sm:flex-1 sm:min-w-[250px]">
|
<form onSubmit={handleSearch} className="w-full sm:flex-1 sm:min-w-[250px]">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by name, ID, or supplier…"
|
placeholder="Search by name or code…"
|
||||||
|
value={searchInput}
|
||||||
|
onChange={(e) => setSearchInput(e.target.value)}
|
||||||
|
onBlur={() => setSearch(searchInput)}
|
||||||
className="w-full px-4 py-2.5 pl-10 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm sm:text-base"
|
className="w-full px-4 py-2.5 pl-10 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm sm:text-base"
|
||||||
/>
|
/>
|
||||||
<svg
|
<svg
|
||||||
@ -130,27 +175,32 @@ export default function ProductsPage() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
{/* Filters Row */}
|
{/* Filters Row */}
|
||||||
<div className="grid grid-cols-2 sm:flex gap-2 sm:gap-3">
|
<div className="grid grid-cols-2 sm:flex gap-2 sm:gap-3">
|
||||||
{/* Category Filter */}
|
{/* Category Filter */}
|
||||||
<select className="px-3 sm:px-4 py-2.5 rounded-lg border border-gray-300 bg-white text-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm sm:text-base">
|
<select
|
||||||
<option>Categories</option>
|
value={categoryId}
|
||||||
<option>Dairy</option>
|
onChange={(e) => setCategoryId(e.target.value)}
|
||||||
<option>Dry Goods</option>
|
className="px-3 sm:px-4 py-2.5 rounded-lg border border-gray-300 bg-white text-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm sm:text-base"
|
||||||
<option>Seafood</option>
|
>
|
||||||
<option>Produce</option>
|
<option value="">Categories</option>
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<option key={cat._id} value={cat._id}>{cat.name}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{/* Subcategory Filter */}
|
{/* Subcategory Filter */}
|
||||||
<select className="px-3 sm:px-4 py-2.5 rounded-lg border border-gray-300 bg-white text-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm sm:text-base">
|
<select
|
||||||
<option>Subcategories</option>
|
value={subcategoryId}
|
||||||
<option>Butter & Spreads</option>
|
onChange={(e) => setSubcategoryId(e.target.value)}
|
||||||
<option>Flours</option>
|
className="px-3 sm:px-4 py-2.5 rounded-lg border border-gray-300 bg-white text-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm sm:text-base"
|
||||||
<option>Fresh Fish</option>
|
>
|
||||||
<option>Spices & Extracts</option>
|
<option value="">Subcategories</option>
|
||||||
<option>Fruits</option>
|
{filteredSubcategories.map((sub) => (
|
||||||
|
<option key={sub._id} value={sub._id}>{sub.name}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{/* Bulk Actions */}
|
{/* Bulk Actions */}
|
||||||
@ -161,12 +211,14 @@ export default function ProductsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Active Filters */}
|
{/* Active Filters */}
|
||||||
|
{activeFilters.length > 0 && (
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
<span className="text-sm text-gray-600">Active filters:</span>
|
<span className="text-sm text-gray-600">Active filters:</span>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 text-blue-700 text-xs sm:text-sm font-medium">
|
{activeFilters.map((filter) => (
|
||||||
Category: Dry Goods
|
<span key={filter.label} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 text-blue-700 text-xs sm:text-sm font-medium">
|
||||||
<button className="hover:text-blue-900">
|
{filter.label}
|
||||||
|
<button onClick={filter.onClear} className="hover:text-blue-900">
|
||||||
<svg className="w-3 h-3 sm:w-4 sm:h-4" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-3 h-3 sm:w-4 sm:h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
@ -176,41 +228,78 @@ export default function ProductsPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<button className="text-xs sm:text-sm text-blue-600 hover:text-blue-800 font-medium sm:ml-2 self-start">
|
<button onClick={clearAllFilters} className="text-xs sm:text-sm text-blue-600 hover:text-blue-800 font-medium sm:ml-2 self-start">
|
||||||
Clear all
|
Clear all
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Product Table */}
|
{/* Product Table */}
|
||||||
<ProductTable products={sampleProducts} />
|
{loading ? (
|
||||||
|
<div className="bg-white rounded-lg border border-gray-200 p-12 text-center">
|
||||||
|
<p className="text-gray-500">Loading products...</p>
|
||||||
|
</div>
|
||||||
|
) : products.length === 0 ? (
|
||||||
|
<div className="bg-white rounded-lg border border-gray-200 p-12 text-center">
|
||||||
|
<p className="text-gray-500">No products found.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ProductTable products={products} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
|
{total > 0 && (
|
||||||
<div className="mt-4 sm:mt-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
<div className="mt-4 sm:mt-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<p className="text-xs sm:text-sm text-gray-600 text-center sm:text-left">
|
<p className="text-xs sm:text-sm text-gray-600 text-center sm:text-left">
|
||||||
Showing <span className="font-medium">1</span> to{' '}
|
Showing <span className="font-medium">{startItem}</span> to{' '}
|
||||||
<span className="font-medium">5</span> of{' '}
|
<span className="font-medium">{endItem}</span> of{' '}
|
||||||
<span className="font-medium">5</span> results
|
<span className="font-medium">{total}</span> results
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-1 sm:gap-2">
|
<div className="flex items-center gap-1 sm:gap-2">
|
||||||
<button className="px-2 sm:px-3 py-2 rounded-lg border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-xs sm:text-sm" disabled>
|
<button
|
||||||
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||||
|
disabled={page === 1}
|
||||||
|
className="px-2 sm:px-3 py-2 rounded-lg border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-xs sm:text-sm"
|
||||||
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<button className="px-2 sm:px-3 py-2 rounded-lg bg-blue-600 text-white font-medium text-xs sm:text-sm min-w-[32px] sm:min-w-[36px]">
|
{Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||||
1
|
.filter(p => {
|
||||||
|
// Show first, last, current, and neighbors
|
||||||
|
if (p === 1 || p === totalPages) return true;
|
||||||
|
if (Math.abs(p - page) <= 1) return true;
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.map((p, idx, arr) => (
|
||||||
|
<React.Fragment key={p}>
|
||||||
|
{idx > 0 && arr[idx - 1] !== p - 1 && (
|
||||||
|
<span className="px-1 text-gray-400 text-xs">…</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
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] ${
|
||||||
|
p === page
|
||||||
|
? 'bg-blue-600 text-white font-medium'
|
||||||
|
: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
</button>
|
</button>
|
||||||
<button className="px-2 sm:px-3 py-2 rounded-lg border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors text-xs sm:text-sm min-w-[32px] sm:min-w-[36px]">
|
</React.Fragment>
|
||||||
2
|
))}
|
||||||
</button>
|
<button
|
||||||
<button className="hidden sm:block px-3 py-2 rounded-lg border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors text-sm">
|
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||||
3
|
disabled={page === totalPages}
|
||||||
</button>
|
className="px-2 sm:px-3 py-2 rounded-lg border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-xs sm:text-sm"
|
||||||
<button className="px-2 sm:px-3 py-2 rounded-lg border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 transition-colors text-xs sm:text-sm">
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
id: string;
|
_id: string;
|
||||||
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
category: string;
|
||||||
subcategory: string;
|
subcategory: string;
|
||||||
@ -12,7 +13,8 @@ export interface Product {
|
|||||||
unitPrice: number;
|
unitPrice: number;
|
||||||
vat: number;
|
vat: number;
|
||||||
supplier: string;
|
supplier: string;
|
||||||
lastUpdated: string;
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductTableProps {
|
interface ProductTableProps {
|
||||||
@ -36,7 +38,7 @@ export default function ProductTable({ products }: ProductTableProps) {
|
|||||||
if (selectedProducts.size === products.length) {
|
if (selectedProducts.size === products.length) {
|
||||||
setSelectedProducts(new Set());
|
setSelectedProducts(new Set());
|
||||||
} else {
|
} else {
|
||||||
setSelectedProducts(new Set(products.map(p => p.id)));
|
setSelectedProducts(new Set(products.map(p => p._id)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -55,7 +57,7 @@ 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}
|
checked={selectedProducts.size === products.length && products.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"
|
||||||
/>
|
/>
|
||||||
@ -91,19 +93,19 @@ export default function ProductTable({ products }: ProductTableProps) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{products.map((product) => (
|
{products.map((product) => (
|
||||||
<tr key={product.id} className="hover:bg-gray-50 transition-colors">
|
<tr key={product._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={selectedProducts.has(product._id)}
|
||||||
onChange={() => toggleProduct(product.id)}
|
onChange={() => toggleProduct(product._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">{product.name}</p>
|
||||||
<p className="text-xs text-gray-500">ID: {product.id}</p>
|
<p className="text-xs text-gray-500">ID: {product.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}
|
{product.category} • {product.quantity} {product.unit}
|
||||||
</p>
|
</p>
|
||||||
@ -141,19 +143,19 @@ 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) => (
|
{products.map((product) => (
|
||||||
<div key={product.id} className="bg-white rounded-lg border border-gray-200 p-4">
|
<div key={product._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={selectedProducts.has(product._id)}
|
||||||
onChange={() => toggleProduct(product.id)}
|
onChange={() => toggleProduct(product._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">{product.name}</h3>
|
||||||
<p className="text-xs text-gray-500 mt-0.5">ID: {product.id}</p>
|
<p className="text-xs text-gray-500 mt-0.5">ID: {product.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">
|
||||||
@ -200,7 +202,7 @@ export default function ProductTable({ products }: ProductTableProps) {
|
|||||||
</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">{product.lastUpdated}</p>
|
<p className="text-xs text-gray-700 mt-0.5">{new Date(product.updatedAt).toLocaleDateString()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
mongo:
|
||||||
|
image: mongo:7.0
|
||||||
|
container_name: mongo-icostpro
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: root
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: rootpassword
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db
|
||||||
|
command: ["mongod", "--auth"]
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo_data:
|
||||||
29
lib/mongodb.ts
Normal file
29
lib/mongodb.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { MongoClient, Db } from 'mongodb';
|
||||||
|
|
||||||
|
const uri = process.env.MONGODB_URI!;
|
||||||
|
|
||||||
|
let client: MongoClient;
|
||||||
|
let db: Db;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var _mongoClient: MongoClient | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDb(): Promise<Db> {
|
||||||
|
if (db) return db;
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
if (!global._mongoClient) {
|
||||||
|
global._mongoClient = new MongoClient(uri);
|
||||||
|
await global._mongoClient.connect();
|
||||||
|
}
|
||||||
|
client = global._mongoClient;
|
||||||
|
} else {
|
||||||
|
client = new MongoClient(uri);
|
||||||
|
await client.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
db = client.db();
|
||||||
|
return db;
|
||||||
|
}
|
||||||
53
lib/types.ts
Normal file
53
lib/types.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
|
||||||
|
export interface Category {
|
||||||
|
_id: ObjectId;
|
||||||
|
name: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subcategory {
|
||||||
|
_id: ObjectId;
|
||||||
|
name: string;
|
||||||
|
categoryId: ObjectId;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Supplier {
|
||||||
|
_id: ObjectId;
|
||||||
|
name: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductDoc {
|
||||||
|
_id: ObjectId;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
categoryId: ObjectId;
|
||||||
|
subcategoryId: ObjectId;
|
||||||
|
quantity: number;
|
||||||
|
unit: string;
|
||||||
|
unitPrice: number;
|
||||||
|
vat: number;
|
||||||
|
supplierId: ObjectId;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductWithRefs {
|
||||||
|
_id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
subcategory: string;
|
||||||
|
quantity: number;
|
||||||
|
unit: string;
|
||||||
|
unitPrice: number;
|
||||||
|
vat: number;
|
||||||
|
supplier: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
143
package-lock.json
generated
143
package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "icostpro",
|
"name": "icostpro",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"mongodb": "^7.1.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
@ -1021,6 +1022,15 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mongodb-js/saslprep": {
|
||||||
|
"version": "1.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
|
||||||
|
"integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sparse-bitfield": "^3.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||||
@ -1575,6 +1585,21 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/webidl-conversions": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/whatwg-url": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/webidl-conversions": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.55.0",
|
"version": "8.55.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz",
|
||||||
@ -2473,6 +2498,15 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bson": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
@ -4853,6 +4887,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/memory-pager": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@ -4900,6 +4940,65 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mongodb": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@mongodb-js/saslprep": "^1.3.0",
|
||||||
|
"bson": "^7.1.1",
|
||||||
|
"mongodb-connection-string-url": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@aws-sdk/credential-providers": "^3.806.0",
|
||||||
|
"@mongodb-js/zstd": "^7.0.0",
|
||||||
|
"gcp-metadata": "^7.0.1",
|
||||||
|
"kerberos": "^7.0.0",
|
||||||
|
"mongodb-client-encryption": ">=7.0.0 <7.1.0",
|
||||||
|
"snappy": "^7.3.2",
|
||||||
|
"socks": "^2.8.6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@aws-sdk/credential-providers": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@mongodb-js/zstd": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"gcp-metadata": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"kerberos": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"mongodb-client-encryption": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"snappy": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"socks": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mongodb-connection-string-url": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/whatwg-url": "^13.0.0",
|
||||||
|
"whatwg-url": "^14.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -5351,7 +5450,6 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@ -5812,6 +5910,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sparse-bitfield": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"memory-pager": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable-hash": {
|
"node_modules/stable-hash": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||||
@ -6100,6 +6207,18 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||||
@ -6376,6 +6495,28 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "14.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||||
|
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.1.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"mongodb": "^7.1.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
|
|||||||
151
scripts/seed.ts
Normal file
151
scripts/seed.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { MongoClient, ObjectId } from 'mongodb';
|
||||||
|
|
||||||
|
const uri = 'mongodb://root:rootpassword@localhost:27017/icostpro?authSource=admin';
|
||||||
|
|
||||||
|
async function seed() {
|
||||||
|
const client = new MongoClient(uri);
|
||||||
|
await client.connect();
|
||||||
|
const db = client.db();
|
||||||
|
|
||||||
|
console.log('Connected to MongoDB. Seeding...');
|
||||||
|
|
||||||
|
// Clear existing data
|
||||||
|
await Promise.all([
|
||||||
|
db.collection('categories').deleteMany({}),
|
||||||
|
db.collection('subcategories').deleteMany({}),
|
||||||
|
db.collection('suppliers').deleteMany({}),
|
||||||
|
db.collection('products').deleteMany({}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Insert categories
|
||||||
|
const categoryIds = {
|
||||||
|
dairy: new ObjectId(),
|
||||||
|
dryGoods: new ObjectId(),
|
||||||
|
seafood: new ObjectId(),
|
||||||
|
produce: new ObjectId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.collection('categories').insertMany([
|
||||||
|
{ _id: categoryIds.dairy, name: 'Dairy', createdAt: now, updatedAt: now },
|
||||||
|
{ _id: categoryIds.dryGoods, name: 'Dry Goods', createdAt: now, updatedAt: now },
|
||||||
|
{ _id: categoryIds.seafood, name: 'Seafood', createdAt: now, updatedAt: now },
|
||||||
|
{ _id: categoryIds.produce, name: 'Produce', createdAt: now, updatedAt: now },
|
||||||
|
]);
|
||||||
|
console.log('Inserted 4 categories');
|
||||||
|
|
||||||
|
// Insert subcategories
|
||||||
|
const subcategoryIds = {
|
||||||
|
butterSpreads: new ObjectId(),
|
||||||
|
flours: new ObjectId(),
|
||||||
|
freshFish: new ObjectId(),
|
||||||
|
spicesExtracts: new ObjectId(),
|
||||||
|
fruits: new ObjectId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.collection('subcategories').insertMany([
|
||||||
|
{ _id: subcategoryIds.butterSpreads, name: 'Butter & Spreads', categoryId: categoryIds.dairy, createdAt: now, updatedAt: now },
|
||||||
|
{ _id: subcategoryIds.flours, name: 'Flours', categoryId: categoryIds.dryGoods, createdAt: now, updatedAt: now },
|
||||||
|
{ _id: subcategoryIds.freshFish, name: 'Fresh Fish', categoryId: categoryIds.seafood, createdAt: now, updatedAt: now },
|
||||||
|
{ _id: subcategoryIds.spicesExtracts, name: 'Spices & Extracts', categoryId: categoryIds.dryGoods, createdAt: now, updatedAt: now },
|
||||||
|
{ _id: subcategoryIds.fruits, name: 'Fruits', categoryId: categoryIds.produce, createdAt: now, updatedAt: now },
|
||||||
|
]);
|
||||||
|
console.log('Inserted 5 subcategories');
|
||||||
|
|
||||||
|
// Insert suppliers
|
||||||
|
const supplierIds = {
|
||||||
|
dairyFresh: new ObjectId(),
|
||||||
|
grainMasters: new ObjectId(),
|
||||||
|
oceanHarvest: new ObjectId(),
|
||||||
|
spiceWorld: new ObjectId(),
|
||||||
|
freshFarm: new ObjectId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.collection('suppliers').insertMany([
|
||||||
|
{ _id: supplierIds.dairyFresh, name: 'DairyFresh Co.', createdAt: now, updatedAt: now },
|
||||||
|
{ _id: supplierIds.grainMasters, name: 'GrainMasters', createdAt: now, updatedAt: now },
|
||||||
|
{ _id: supplierIds.oceanHarvest, name: 'OceanHarvest', createdAt: now, updatedAt: now },
|
||||||
|
{ _id: supplierIds.spiceWorld, name: 'SpiceWorld Ltd', createdAt: now, updatedAt: now },
|
||||||
|
{ _id: supplierIds.freshFarm, name: 'FreshFarm Direct', createdAt: now, updatedAt: now },
|
||||||
|
]);
|
||||||
|
console.log('Inserted 5 suppliers');
|
||||||
|
|
||||||
|
// Insert products
|
||||||
|
await db.collection('products').insertMany([
|
||||||
|
{
|
||||||
|
code: 'PRD-001',
|
||||||
|
name: 'Organic Butter',
|
||||||
|
categoryId: categoryIds.dairy,
|
||||||
|
subcategoryId: subcategoryIds.butterSpreads,
|
||||||
|
quantity: 25,
|
||||||
|
unit: 'kg',
|
||||||
|
unitPrice: 4.50,
|
||||||
|
vat: 9,
|
||||||
|
supplierId: supplierIds.dairyFresh,
|
||||||
|
createdAt: new Date('2026-02-10'),
|
||||||
|
updatedAt: new Date('2026-02-10'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PRD-002',
|
||||||
|
name: 'All-Purpose Flour',
|
||||||
|
categoryId: categoryIds.dryGoods,
|
||||||
|
subcategoryId: subcategoryIds.flours,
|
||||||
|
quantity: 150,
|
||||||
|
unit: 'kg',
|
||||||
|
unitPrice: 0.85,
|
||||||
|
vat: 9,
|
||||||
|
supplierId: supplierIds.grainMasters,
|
||||||
|
createdAt: new Date('2026-02-09'),
|
||||||
|
updatedAt: new Date('2026-02-09'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PRD-003',
|
||||||
|
name: 'Atlantic Salmon Fillet',
|
||||||
|
categoryId: categoryIds.seafood,
|
||||||
|
subcategoryId: subcategoryIds.freshFish,
|
||||||
|
quantity: 12,
|
||||||
|
unit: 'kg',
|
||||||
|
unitPrice: 18.90,
|
||||||
|
vat: 9,
|
||||||
|
supplierId: supplierIds.oceanHarvest,
|
||||||
|
createdAt: new Date('2026-02-11'),
|
||||||
|
updatedAt: new Date('2026-02-11'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PRD-004',
|
||||||
|
name: 'Madagascar Vanilla Extract',
|
||||||
|
categoryId: categoryIds.dryGoods,
|
||||||
|
subcategoryId: subcategoryIds.spicesExtracts,
|
||||||
|
quantity: 8,
|
||||||
|
unit: 'L',
|
||||||
|
unitPrice: 45.00,
|
||||||
|
vat: 21,
|
||||||
|
supplierId: supplierIds.spiceWorld,
|
||||||
|
createdAt: new Date('2026-02-08'),
|
||||||
|
updatedAt: new Date('2026-02-08'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PRD-005',
|
||||||
|
name: 'Fresh Avocados',
|
||||||
|
categoryId: categoryIds.produce,
|
||||||
|
subcategoryId: subcategoryIds.fruits,
|
||||||
|
quantity: 40,
|
||||||
|
unit: 'pcs',
|
||||||
|
unitPrice: 1.20,
|
||||||
|
vat: 9,
|
||||||
|
supplierId: supplierIds.freshFarm,
|
||||||
|
createdAt: new Date('2026-02-11'),
|
||||||
|
updatedAt: new Date('2026-02-11'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
console.log('Inserted 5 products');
|
||||||
|
|
||||||
|
console.log('Seeding complete!');
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
seed().catch((err) => {
|
||||||
|
console.error('Seed failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user