feat: Implement inventory management features with status tracking and checkout functionality

- Updated layout metadata to reflect "Inventory Management" title and description.
- Enhanced inventory item model to include status (active, checked-out, used-up).
- Added status filter to inventory items display.
- Introduced CheckoutBasket component for managing selected items during checkout.
- Implemented checkout process to update inventory based on item status and location.
- Modified database schema to include status column with migration for existing items.
- Updated API endpoints to support fetching and updating inventory items by status.
This commit is contained in:
2025-08-04 15:53:07 +02:00
parent 738f42f3b4
commit ec1dbaa2a7
9 changed files with 841 additions and 115 deletions

View File

@@ -34,7 +34,7 @@ export async function PUT(
) { ) {
try { try {
const { id } = await params; const { id } = await params;
const { name, quantity, value, location } = await request.json(); const { name, quantity, value, location, status } = await request.json();
if (!name || quantity === undefined || value === undefined) { if (!name || quantity === undefined || value === undefined) {
return NextResponse.json( return NextResponse.json(
@@ -58,6 +58,7 @@ export async function PUT(
quantity, quantity,
value, value,
location || '', location || '',
status || 'active',
id id
); );
@@ -81,13 +82,17 @@ export async function PUT(
if (currentItem.location !== (location || '')) { if (currentItem.location !== (location || '')) {
auditQueries.create.run(id, 'UPDATE', 'location', currentItem.location || '', location || ''); auditQueries.create.run(id, 'UPDATE', 'location', currentItem.location || '', location || '');
} }
if (currentItem.status !== (status || 'active')) {
auditQueries.create.run(id, 'UPDATE', 'status', currentItem.status || 'active', status || 'active');
}
return NextResponse.json({ return NextResponse.json({
id: id, id: id,
name, name,
quantity, quantity,
value, value,
location: location || '' location: location || '',
status: status || 'active'
}); });
} catch (error) { } catch (error) {
console.error('Error updating inventory item:', error); console.error('Error updating inventory item:', error);

View File

@@ -2,9 +2,18 @@ import { NextRequest, NextResponse } from 'next/server';
import { inventoryQueries, auditQueries } from '@/lib/database'; import { inventoryQueries, auditQueries } from '@/lib/database';
// GET /api/inventory - Get all inventory items // GET /api/inventory - Get all inventory items
export async function GET() { export async function GET(request: NextRequest) {
try { try {
const items = inventoryQueries.getAll.all(); const { searchParams } = new URL(request.url);
const status = searchParams.get('status');
let items;
if (status) {
items = inventoryQueries.getByStatus.all(status);
} else {
items = inventoryQueries.getActive.all(); // Default to active items only
}
return NextResponse.json(items); return NextResponse.json(items);
} catch (error) { } catch (error) {
console.error('Error fetching inventory:', error); console.error('Error fetching inventory:', error);
@@ -18,7 +27,7 @@ export async function GET() {
// POST /api/inventory - Create new inventory item // POST /api/inventory - Create new inventory item
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const { name, quantity, value, location } = await request.json(); const { name, quantity, value, location, status } = await request.json();
if (!name || quantity === undefined || value === undefined) { if (!name || quantity === undefined || value === undefined) {
return NextResponse.json( return NextResponse.json(
@@ -27,7 +36,7 @@ export async function POST(request: NextRequest) {
); );
} }
const result = inventoryQueries.create.run(name, quantity, value, location || ''); const result = inventoryQueries.create.run(name, quantity, value, location || '', status || 'active');
const itemId = result.lastInsertRowid; const itemId = result.lastInsertRowid;
// Log the creation // Log the creation
@@ -39,7 +48,8 @@ export async function POST(request: NextRequest) {
name, name,
quantity, quantity,
value, value,
location: location || '' location: location || '',
status: status || 'active'
}, },
{ status: 201 } { status: 201 }
); );

View File

@@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from 'next/server';
import { userQueries } from '@/lib/database';
export async function GET() {
try {
const users = userQueries.getAll.all();
return NextResponse.json(users);
} catch (error) {
console.error('Error fetching users:', error);
return NextResponse.json(
{ error: 'Failed to fetch users' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const { name, email } = await request.json();
if (!name || !email) {
return NextResponse.json(
{ error: 'Name and email are required' },
{ status: 400 }
);
}
// Check if user already exists
const existingUser = userQueries.getByEmail.get(email);
if (existingUser) {
return NextResponse.json(
{ error: 'User with this email already exists' },
{ status: 409 }
);
}
const result = userQueries.create.run(name, email);
const newUser = userQueries.getById.get(result.lastInsertRowid);
return NextResponse.json(newUser, { status: 201 });
} catch (error) {
console.error('Error creating user:', error);
return NextResponse.json(
{ error: 'Failed to create user' },
{ status: 500 }
);
}
}

View File

@@ -3,6 +3,56 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
/* Mobile-friendly touch targets and scrolling */
html {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overscroll-behavior-y: none;
}
/* Better touch targets */
.touch-manipulation {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* Smooth scrolling for mobile */
* {
-webkit-overflow-scrolling: touch;
}
/* Prevent zoom on inputs on iOS */
input[type="text"],
input[type="number"],
input[type="email"],
input[type="password"],
select,
textarea {
font-size: 16px;
}
@media (max-width: 640px) {
/* Larger touch targets on mobile */
button,
input[type="button"],
input[type="submit"],
input[type="checkbox"],
select {
min-height: 44px;
}
/* Better spacing for mobile */
.container {
padding-left: 1rem;
padding-right: 1rem;
}
}
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);

View File

@@ -9,6 +9,7 @@ interface InventoryItem {
quantity: number; quantity: number;
value: number; value: number;
location: string; location: string;
status: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
@@ -34,7 +35,8 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
name: '', name: '',
quantity: 0, quantity: 0,
value: 0, value: 0,
location: '' location: '',
status: 'active'
}); });
// Initialize item ID from params // Initialize item ID from params
@@ -59,7 +61,8 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
name: data.name, name: data.name,
quantity: data.quantity, quantity: data.quantity,
value: data.value, value: data.value,
location: data.location location: data.location,
status: data.status || 'active'
}); });
} else { } else {
console.error('Item not found'); console.error('Item not found');
@@ -129,7 +132,8 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
name: item.name, name: item.name,
quantity: item.quantity, quantity: item.quantity,
value: item.value, value: item.value,
location: item.location location: item.location,
status: item.status || 'active'
}); });
} }
setIsEditing(false); setIsEditing(false);
@@ -183,33 +187,33 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
} }
return ( return (
<div className="container mx-auto p-4 max-w-4xl"> <div className="container mx-auto p-3 sm:p-4 max-w-4xl min-h-screen">
{/* Navigation */} {/* Navigation */}
<div className="mb-6"> <div className="mb-4 sm:mb-6">
<button <button
onClick={() => router.push('/')} onClick={() => router.push('/')}
className="text-blue-500 hover:text-blue-600 font-medium" className="text-blue-500 hover:text-blue-600 font-medium text-sm touch-manipulation"
> >
Back to Inventory Back to Inventory
</button> </button>
</div> </div>
{/* Item Details */} {/* Item Details */}
<div className="bg-white shadow-md rounded-lg p-6 mb-6"> <div className="bg-white shadow-md rounded-lg p-4 sm:p-6 mb-4 sm:mb-6">
<div className="flex justify-between items-start mb-4"> <div className="flex flex-col sm:flex-row justify-between items-start mb-4 gap-3">
<h1 className="text-2xl font-bold">{item.name}</h1> <h1 className="text-xl sm:text-2xl font-bold">{item.name}</h1>
<div className="flex gap-2"> <div className="flex flex-wrap gap-2 w-full sm:w-auto">
{!isEditing ? ( {!isEditing ? (
<> <>
<button <button
onClick={() => setIsEditing(true)} onClick={() => setIsEditing(true)}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded font-medium" className="flex-1 sm:flex-initial bg-blue-500 hover:bg-blue-600 text-white px-3 sm:px-4 py-2 rounded font-medium text-sm touch-manipulation"
> >
Edit Edit
</button> </button>
<button <button
onClick={deleteItem} onClick={deleteItem}
className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded font-medium" className="flex-1 sm:flex-initial bg-red-500 hover:bg-red-600 text-white px-3 sm:px-4 py-2 rounded font-medium text-sm touch-manipulation"
> >
Delete Delete
</button> </button>
@@ -218,13 +222,13 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
<> <>
<button <button
onClick={saveItem} onClick={saveItem}
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded font-medium" className="flex-1 sm:flex-initial bg-green-500 hover:bg-green-600 text-white px-3 sm:px-4 py-2 rounded font-medium text-sm touch-manipulation"
> >
Save Save
</button> </button>
<button <button
onClick={cancelEdit} onClick={cancelEdit}
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded font-medium" className="flex-1 sm:flex-initial bg-gray-300 hover:bg-gray-400 text-gray-700 px-3 sm:px-4 py-2 rounded font-medium text-sm touch-manipulation"
> >
Cancel Cancel
</button> </button>
@@ -234,7 +238,7 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
</div> </div>
{isEditing ? ( {isEditing ? (
<form onSubmit={saveItem} className="grid grid-cols-1 md:grid-cols-2 gap-4"> <form onSubmit={saveItem} className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
<div> <div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Name Name
@@ -242,7 +246,7 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
<input <input
id="name" id="name"
type="text" type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required required
@@ -255,7 +259,7 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
<input <input
id="quantity" id="quantity"
type="number" type="number"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={formData.quantity} value={formData.quantity}
onChange={(e) => setFormData({ ...formData, quantity: parseInt(e.target.value) || 0 })} onChange={(e) => setFormData({ ...formData, quantity: parseInt(e.target.value) || 0 })}
required required
@@ -269,7 +273,7 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
id="value" id="value"
type="number" type="number"
step="0.01" step="0.01"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={formData.value} value={formData.value}
onChange={(e) => setFormData({ ...formData, value: parseFloat(e.target.value) || 0 })} onChange={(e) => setFormData({ ...formData, value: parseFloat(e.target.value) || 0 })}
required required
@@ -282,14 +286,69 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
<input <input
id="location" id="location"
type="text" type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={formData.location} value={formData.location}
onChange={(e) => setFormData({ ...formData, location: e.target.value })} onChange={(e) => setFormData({ ...formData, location: e.target.value })}
/> />
</div> </div>
<div className="sm:col-span-2">
<label htmlFor="status" className="block text-sm font-medium text-gray-700 mb-1">
Status
</label>
<select
id="status"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value })}
>
<option value="active">Active</option>
<option value="checked-out">Checked Out</option>
<option value="used-up">Used Up</option>
</select>
</div>
</form> </form>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="space-y-4">
{/* Mobile Layout */}
<div className="sm:hidden space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-700">Quantity</label>
<p className="text-lg font-semibold">{item.quantity}</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-700">Value each</label>
<p className="text-lg font-semibold">${item.value.toFixed(2)}</p>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-700">Location</label>
<p className="text-sm font-semibold">{item.location || 'Not specified'}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-700">Status</label>
<p className={`text-sm font-semibold ${
item.status === 'active' ? 'text-green-600' :
item.status === 'checked-out' ? 'text-blue-600' :
item.status === 'used-up' ? 'text-red-600' :
'text-gray-600'
}`}>
{item.status === 'checked-out' ? 'Checked Out' :
item.status === 'used-up' ? 'Used Up' :
item.status === 'active' ? 'Active' :
item.status}
</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-700">Total Value</label>
<p className="text-lg font-semibold text-green-600">${(item.quantity * item.value).toFixed(2)}</p>
</div>
</div>
</div>
{/* Desktop Layout */}
<div className="hidden sm:grid sm:grid-cols-2 gap-6">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700">Quantity</label> <label className="block text-sm font-medium text-gray-700">Quantity</label>
@@ -305,12 +364,27 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
<label className="block text-sm font-medium text-gray-700">Location</label> <label className="block text-sm font-medium text-gray-700">Location</label>
<p className="text-lg font-semibold">{item.location || 'Not specified'}</p> <p className="text-lg font-semibold">{item.location || 'Not specified'}</p>
</div> </div>
<div>
<label className="block text-sm font-medium text-gray-700">Status</label>
<p className={`text-lg font-semibold ${
item.status === 'active' ? 'text-green-600' :
item.status === 'checked-out' ? 'text-blue-600' :
item.status === 'used-up' ? 'text-red-600' :
'text-gray-600'
}`}>
{item.status === 'checked-out' ? 'Checked Out' :
item.status === 'used-up' ? 'Used Up' :
item.status === 'active' ? 'Active' :
item.status}
</p>
</div>
<div> <div>
<label className="block text-sm font-medium text-gray-700">Total Value</label> <label className="block text-sm font-medium text-gray-700">Total Value</label>
<p className="text-lg font-semibold text-green-600">${(item.quantity * item.value).toFixed(2)}</p> <p className="text-lg font-semibold text-green-600">${(item.quantity * item.value).toFixed(2)}</p>
</div> </div>
</div> </div>
</div> </div>
</div>
)} )}
<div className="mt-6 pt-4 border-t text-sm text-gray-500"> <div className="mt-6 pt-4 border-t text-sm text-gray-500">
@@ -322,28 +396,28 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
</div> </div>
{/* Change History */} {/* Change History */}
<div className="bg-white shadow-md rounded-lg p-6"> <div className="bg-white shadow-md rounded-lg p-4 sm:p-6">
<h2 className="text-xl font-semibold mb-4">Change History</h2> <h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Change History</h2>
{auditLogs.length === 0 ? ( {auditLogs.length === 0 ? (
<p className="text-gray-500">No changes recorded for this item.</p> <p className="text-gray-500 text-center py-6 sm:py-8 text-sm">No changes recorded for this item.</p>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-2 sm:space-y-4">
{auditLogs.map((log) => ( {auditLogs.map((log) => (
<div <div
key={log.id} key={log.id}
className="border-l-4 border-blue-400 bg-blue-50 p-4 rounded-r" className="border-l-4 border-blue-400 bg-blue-50 p-3 sm:p-4 rounded-r"
> >
<div className="flex justify-between items-start"> <div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-1 sm:gap-2">
<div> <div className="flex-1">
<div className="font-medium text-gray-800 mb-1"> <div className="font-medium text-gray-800 mb-1 text-sm">
{formatAuditMessage(log)} {formatAuditMessage(log)}
</div> </div>
<div className="text-sm text-gray-600"> <div className="text-xs sm:text-sm text-gray-600">
{formatTimestamp(log.timestamp)} {formatTimestamp(log.timestamp)}
</div> </div>
</div> </div>
<div className="text-xs bg-blue-200 text-blue-800 px-2 py-1 rounded"> <div className="text-xs bg-blue-200 text-blue-800 px-2 py-1 rounded self-start">
{log.action} {log.action}
</div> </div>
</div> </div>

View File

@@ -13,8 +13,14 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "Inventory Management",
description: "Generated by create next app", description: "Simple inventory management system with item tracking",
};
export const viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
}; };
export default function RootLayout({ export default function RootLayout({

View File

@@ -2,6 +2,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import CheckoutBasket from '@/components/CheckoutBasket';
interface InventoryItem { interface InventoryItem {
id: number; id: number;
@@ -9,6 +10,7 @@ interface InventoryItem {
quantity: number; quantity: number;
value: number; value: number;
location: string; location: string;
status: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
@@ -18,17 +20,24 @@ export default function Home() {
const [items, setItems] = useState<InventoryItem[]>([]); const [items, setItems] = useState<InventoryItem[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [editingItem, setEditingItem] = useState<InventoryItem | null>(null); const [editingItem, setEditingItem] = useState<InventoryItem | null>(null);
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
const [showBasket, setShowBasket] = useState(false);
const [statusFilter, setStatusFilter] = useState<string>('active');
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
quantity: 0, quantity: 0,
value: 0, value: 0,
location: '' location: '',
status: 'active'
}); });
// Fetch all inventory items // Fetch items by status
const fetchItems = async () => { const fetchItems = async () => {
try { try {
const response = await fetch('/api/inventory'); const url = statusFilter === 'all'
? '/api/inventory?status=all'
: `/api/inventory?status=${statusFilter}`;
const response = await fetch(url);
const data = await response.json(); const data = await response.json();
setItems(data); setItems(data);
} catch (error) { } catch (error) {
@@ -85,14 +94,15 @@ export default function Home() {
name: item.name, name: item.name,
quantity: item.quantity, quantity: item.quantity,
value: item.value, value: item.value,
location: item.location location: item.location,
status: item.status || 'active'
}); });
}; };
// Reset form // Reset form
const resetForm = () => { const resetForm = () => {
setEditingItem(null); setEditingItem(null);
setFormData({ name: '', quantity: 0, value: 0, location: '' }); setFormData({ name: '', quantity: 0, value: 0, location: '', status: 'active' });
}; };
// Navigate to item page // Navigate to item page
@@ -100,9 +110,30 @@ export default function Home() {
router.push(`/inventory/${itemId}`); router.push(`/inventory/${itemId}`);
}; };
// Handle item selection for checkout
const toggleItemSelection = (itemId: number) => {
const newSelected = new Set(selectedItems);
if (newSelected.has(itemId)) {
newSelected.delete(itemId);
} else {
newSelected.add(itemId);
}
setSelectedItems(newSelected);
};
// Clear all selections
const clearSelections = () => {
setSelectedItems(new Set());
};
// Get selected items data
const getSelectedItemsData = () => {
return items.filter(item => selectedItems.has(item.id));
};
useEffect(() => { useEffect(() => {
fetchItems(); fetchItems();
}, []); }, [statusFilter]);
if (isLoading) { if (isLoading) {
return ( return (
@@ -113,23 +144,66 @@ export default function Home() {
} }
return ( return (
<div className="container mx-auto p-4 max-w-6xl"> <div className="container mx-auto p-3 sm:p-4 max-w-6xl min-h-screen">
<h1 className="text-3xl font-bold mb-6">Inventory Management</h1> <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4 sm:mb-6 gap-3 sm:gap-4">
<h1 className="text-2xl sm:text-3xl font-bold">Inventory Management</h1>
{/* Status Filter */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 w-full sm:w-auto">
<div className="flex items-center gap-2 w-full sm:w-auto">
<label htmlFor="statusFilter" className="text-sm font-medium text-gray-700 whitespace-nowrap">
Show:
</label>
<select
id="statusFilter"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="flex-1 sm:flex-initial px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
>
<option value="active">Active Items</option>
<option value="checked-out">Checked Out</option>
<option value="used-up">Used Up</option>
<option value="all">All Items</option>
</select>
</div>
{/* Checkout Actions */}
{selectedItems.size > 0 && statusFilter === 'active' && (
<div className="flex gap-2 items-center w-full sm:w-auto">
<span className="text-xs sm:text-sm text-gray-600 whitespace-nowrap">
{selectedItems.size} item{selectedItems.size !== 1 ? 's' : ''} selected
</span>
<button
onClick={() => setShowBasket(true)}
className="flex-1 sm:flex-initial bg-green-500 hover:bg-green-600 text-white px-3 sm:px-4 py-2 rounded font-medium text-sm whitespace-nowrap"
>
Basket ({selectedItems.size})
</button>
<button
onClick={clearSelections}
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-3 py-2 rounded font-medium text-sm whitespace-nowrap"
>
Clear
</button>
</div>
)}
</div>
</div>
{/* Add/Edit Form */} {/* Add/Edit Form */}
<div className="bg-white shadow-md rounded-lg p-6 mb-6"> <div className="bg-white shadow-md rounded-lg p-4 sm:p-6 mb-4 sm:mb-6">
<h2 className="text-xl font-semibold mb-4"> <h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">
{editingItem ? 'Edit Item' : 'Add New Item'} {editingItem ? 'Edit Item' : 'Add New Item'}
</h2> </h2>
<form onSubmit={saveItem} className="grid grid-cols-1 md:grid-cols-4 gap-4"> <form onSubmit={saveItem} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
<div> <div className="sm:col-span-2 lg:col-span-1">
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Name Name
</label> </label>
<input <input
id="name" id="name"
type="text" type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required required
@@ -142,7 +216,7 @@ export default function Home() {
<input <input
id="quantity" id="quantity"
type="number" type="number"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={formData.quantity} value={formData.quantity}
onChange={(e) => setFormData({ ...formData, quantity: parseInt(e.target.value) || 0 })} onChange={(e) => setFormData({ ...formData, quantity: parseInt(e.target.value) || 0 })}
required required
@@ -156,35 +230,35 @@ export default function Home() {
id="value" id="value"
type="number" type="number"
step="0.01" step="0.01"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={formData.value} value={formData.value}
onChange={(e) => setFormData({ ...formData, value: parseFloat(e.target.value) || 0 })} onChange={(e) => setFormData({ ...formData, value: parseFloat(e.target.value) || 0 })}
required required
/> />
</div> </div>
<div> <div className="sm:col-span-2 lg:col-span-1">
<label htmlFor="location" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="location" className="block text-sm font-medium text-gray-700 mb-1">
Location Location
</label> </label>
<input <input
id="location" id="location"
type="text" type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={formData.location} value={formData.location}
onChange={(e) => setFormData({ ...formData, location: e.target.value })} onChange={(e) => setFormData({ ...formData, location: e.target.value })}
/> />
</div> </div>
<div className="md:col-span-4 flex gap-2"> <div className="sm:col-span-2 lg:col-span-4 flex flex-col sm:flex-row gap-2 sm:gap-2 pt-2">
<button <button
type="submit" type="submit"
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md font-medium" className="flex-1 sm:flex-initial bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md font-medium text-sm"
> >
{editingItem ? 'Update' : 'Add'} Item {editingItem ? 'Update' : 'Add'} Item
</button> </button>
{editingItem && ( {editingItem && (
<button <button
type="button" type="button"
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded-md font-medium" className="flex-1 sm:flex-initial bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded-md font-medium text-sm"
onClick={resetForm} onClick={resetForm}
> >
Cancel Cancel
@@ -195,50 +269,160 @@ export default function Home() {
</div> </div>
{/* Items List */} {/* Items List */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="bg-white shadow-md rounded-lg overflow-hidden">
<div className="px-4 sm:px-6 py-3 sm:py-4 bg-gray-50 border-b">
<h2 className="text-base sm:text-lg font-semibold">Inventory Items</h2>
</div>
{items.length === 0 ? (
<div className="text-center text-gray-500 py-8 px-4">
No inventory items found. Add your first item above!
</div>
) : (
<div className="divide-y divide-gray-200">
{items.map((item) => ( {items.map((item) => (
<div key={item.id} className="bg-white shadow-md rounded-lg p-6">
<div <div
className="cursor-pointer hover:bg-gray-50 -m-6 p-6 rounded-lg transition-colors" key={item.id}
className="px-3 sm:px-6 py-3 sm:py-4 hover:bg-gray-50 transition-colors"
>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex items-start gap-3 flex-1">
{/* Only show checkbox for active items */}
{statusFilter === 'active' && (
<input
type="checkbox"
checked={selectedItems.has(item.id)}
onChange={() => toggleItemSelection(item.id)}
className="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500 mt-1 sm:mt-0"
/>
)}
<div
className="flex-1 cursor-pointer"
onClick={() => viewItem(item.id)} onClick={() => viewItem(item.id)}
> >
<h3 className="text-lg font-semibold mb-3">{item.name}</h3> {/* Mobile Layout */}
<div className="space-y-2"> <div className="sm:hidden">
<p><strong>Quantity:</strong> {item.quantity}</p> <div className="flex justify-between items-start mb-2">
<p><strong>Value:</strong> ${item.value.toFixed(2)}</p> <h3 className="font-medium text-gray-900 text-sm">{item.name}</h3>
<p><strong>Location:</strong> {item.location || 'Not specified'}</p> <div className={`text-xs font-medium px-2 py-1 rounded ${
<p><strong>Total Value:</strong> ${(item.quantity * item.value).toFixed(2)}</p> item.status === 'active' ? 'bg-green-100 text-green-600' :
item.status === 'checked-out' ? 'bg-blue-100 text-blue-600' :
item.status === 'used-up' ? 'bg-red-100 text-red-600' :
'bg-gray-100 text-gray-600'
}`}>
{item.status === 'checked-out' ? 'Checked Out' :
item.status === 'used-up' ? 'Used Up' :
item.status === 'active' ? 'Active' :
item.status}
</div> </div>
</div> </div>
<div className="flex gap-2 mt-4"> <div className="grid grid-cols-2 gap-2 text-xs text-gray-500">
<div>Qty: <span className="font-medium text-gray-900">{item.quantity}</span></div>
<div>Value: <span className="font-medium text-gray-900">${item.value.toFixed(2)}</span></div>
<div className="col-span-2">Location: <span className="font-medium text-gray-900">{item.location || 'Not specified'}</span></div>
<div className="col-span-2">Total: <span className="font-semibold text-green-600">${(item.quantity * item.value).toFixed(2)}</span></div>
</div>
</div>
{/* Desktop Layout */}
<div className="hidden sm:grid sm:grid-cols-5 gap-4">
<div>
<div className="text-sm font-medium text-gray-900">{item.name}</div>
<div className="text-sm text-gray-500">ID: {item.id}</div>
</div>
<div>
<div className="text-sm text-gray-500">Quantity</div>
<div className="text-sm font-medium text-gray-900">{item.quantity}</div>
</div>
<div>
<div className="text-sm text-gray-500">Value</div>
<div className="text-sm font-medium text-gray-900">${item.value.toFixed(2)}</div>
</div>
<div>
<div className="text-sm text-gray-500">Location</div>
<div className="text-sm font-medium text-gray-900">{item.location || 'Not specified'}</div>
</div>
<div>
<div className="text-sm text-gray-500">Status</div>
<div className={`text-sm font-medium ${
item.status === 'active' ? 'text-green-600' :
item.status === 'checked-out' ? 'text-blue-600' :
item.status === 'used-up' ? 'text-red-600' :
'text-gray-600'
}`}>
{item.status === 'checked-out' ? 'Checked Out' :
item.status === 'used-up' ? 'Used Up' :
item.status === 'active' ? 'Active' :
item.status}
</div>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-between sm:justify-end gap-2 sm:ml-4">
{/* Mobile: Show total value prominently */}
<div className="sm:hidden">
<div className="text-xs text-gray-500">Total</div>
<div className="text-sm font-semibold text-green-600">${(item.quantity * item.value).toFixed(2)}</div>
</div>
{/* Desktop: Show total value */}
<div className="hidden sm:block text-right mr-4">
<div className="text-sm text-gray-500">Total Value</div>
<div className="text-sm font-semibold text-green-600">${(item.quantity * item.value).toFixed(2)}</div>
</div>
{/* Action Buttons */}
<div className="flex gap-1">
<button <button
className="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded text-sm font-medium" className="bg-blue-500 hover:bg-blue-600 text-white px-2 py-1 rounded text-xs font-medium touch-manipulation"
onClick={() => editItem(item)} onClick={(e) => {
e.stopPropagation();
editItem(item);
}}
> >
Edit Edit
</button> </button>
<button <button
className="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded text-sm font-medium" className="bg-green-500 hover:bg-green-600 text-white px-2 py-1 rounded text-xs font-medium touch-manipulation"
onClick={() => viewItem(item.id)} onClick={(e) => {
e.stopPropagation();
viewItem(item.id);
}}
> >
View Details View
</button> </button>
<button <button
className="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-sm font-medium" className="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-xs font-medium touch-manipulation"
onClick={() => deleteItem(item.id)} onClick={(e) => {
e.stopPropagation();
deleteItem(item.id);
}}
> >
Delete Del
</button> </button>
</div> </div>
</div> </div>
</div>
</div>
))} ))}
</div> </div>
{items.length === 0 && (
<div className="text-center text-gray-500 mt-8">
No inventory items found. Add your first item above!
</div>
)} )}
</div> </div>
{/* Checkout Basket Modal */}
<CheckoutBasket
isOpen={showBasket}
onClose={() => setShowBasket(false)}
selectedItems={getSelectedItemsData()}
onCheckout={() => {
fetchItems(); // Refresh the list after checkout
setSelectedItems(new Set()); // Clear selections
setShowBasket(false); // Close basket
}}
/>
</div>
); );
} }

View File

@@ -0,0 +1,322 @@
'use client';
import { useState, useEffect } from 'react';
interface InventoryItem {
id: number;
name: string;
quantity: number;
value: number;
location: string;
created_at: string;
updated_at: string;
}
interface CheckoutItem {
item: InventoryItem;
requestedQuantity: number;
}
interface CheckoutBasketProps {
isOpen: boolean;
onClose: () => void;
selectedItems: InventoryItem[];
onCheckout: () => void;
}
export default function CheckoutBasket({ isOpen, onClose, selectedItems, onCheckout }: CheckoutBasketProps) {
const [checkoutItems, setCheckoutItems] = useState<CheckoutItem[]>([]);
const [newLocation, setNewLocation] = useState('');
const [markAsUsedUp, setMarkAsUsedUp] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
// Initialize checkout items when basket opens
useEffect(() => {
if (isOpen && selectedItems.length > 0) {
setCheckoutItems(
selectedItems.map(item => ({
item,
requestedQuantity: 1 // Default to 1
}))
);
}
}, [isOpen, selectedItems]);
// Update requested quantity for an item
const updateQuantity = (itemId: number, quantity: number) => {
setCheckoutItems(prev =>
prev.map(checkoutItem =>
checkoutItem.item.id === itemId
? { ...checkoutItem, requestedQuantity: Math.max(0, Math.min(quantity, checkoutItem.item.quantity)) }
: checkoutItem
)
);
};
// Remove item from checkout
const removeItem = (itemId: number) => {
setCheckoutItems(prev => prev.filter(checkoutItem => checkoutItem.item.id !== itemId));
};
// Process checkout
const processCheckout = async () => {
if (!newLocation.trim()) {
alert('Please specify a new location');
return;
}
if (checkoutItems.length === 0) {
alert('No items to checkout');
return;
}
setIsProcessing(true);
try {
// Process each item
for (const checkoutItem of checkoutItems) {
const { item, requestedQuantity } = checkoutItem;
if (requestedQuantity > 0) {
const remainingQuantity = item.quantity - requestedQuantity;
const newStatus = markAsUsedUp ? 'used-up' : 'active';
if (remainingQuantity > 0) {
// Partial checkout: Update original item with remaining quantity
await fetch(`/api/inventory/${item.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: item.name,
quantity: remainingQuantity,
value: item.value,
location: item.location, // Keep original location
status: 'active' // Remains active
})
});
// Create new item for checked-out portion
await fetch('/api/inventory', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: markAsUsedUp ? `${item.name} (Used Up)` : `${item.name} (Moved)`,
quantity: requestedQuantity,
value: item.value,
location: newLocation,
status: newStatus
})
});
} else {
// Full checkout: Update the entire item
await fetch(`/api/inventory/${item.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: item.name,
quantity: requestedQuantity,
value: item.value,
location: newLocation,
status: newStatus
})
});
}
}
}
// Success
alert('Checkout completed successfully! Items have been properly tracked.');
onCheckout();
setCheckoutItems([]);
setNewLocation('');
setMarkAsUsedUp(false);
} catch (error) {
console.error('Checkout failed:', error);
alert('Checkout failed. Please try again.');
} finally {
setIsProcessing(false);
}
};
// Calculate total value
const totalValue = checkoutItems.reduce(
(sum, checkoutItem) => sum + (checkoutItem.requestedQuantity * checkoutItem.item.value),
0
);
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-3 sm:p-4 z-50">
<div className="bg-white rounded-lg p-4 sm:p-6 w-full max-w-4xl max-h-[90vh] sm:max-h-[80vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4 sm:mb-6">
<h2 className="text-xl sm:text-2xl font-semibold">Checkout Basket</h2>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 text-2xl p-1 touch-manipulation"
>
×
</button>
</div>
{checkoutItems.length === 0 ? (
<div className="text-center text-gray-500 py-8">
No items in basket
</div>
) : (
<>
{/* New Location Input */}
<div className="mb-4 sm:mb-6 p-3 sm:p-4 bg-blue-50 rounded-lg">
<label htmlFor="newLocation" className="block text-sm font-medium text-gray-700 mb-2">
New Location (where items will be moved to):
</label>
<input
id="newLocation"
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
value={newLocation}
onChange={(e) => setNewLocation(e.target.value)}
placeholder="e.g., Office Desk, Storage Room A, etc."
required
/>
</div>
{/* Items List */}
<div className="space-y-3 sm:space-y-4 mb-4 sm:mb-6">
{checkoutItems.map((checkoutItem) => (
<div key={checkoutItem.item.id} className="border rounded-lg p-3 sm:p-4">
{/* Mobile Layout */}
<div className="sm:hidden">
<div className="flex justify-between items-start mb-3">
<div className="flex-1">
<h3 className="font-medium text-gray-900 text-sm">{checkoutItem.item.name}</h3>
<div className="text-xs text-gray-500 mt-1">
Available: {checkoutItem.item.quantity} |
${checkoutItem.item.value.toFixed(2)} each
</div>
<div className="text-xs text-gray-500">
From: {checkoutItem.item.location || 'Not specified'}
</div>
</div>
<button
onClick={() => removeItem(checkoutItem.item.id)}
className="text-red-500 hover:text-red-700 px-2 py-1 text-xs touch-manipulation"
>
Remove
</button>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<label className="text-xs font-medium text-gray-700">Qty:</label>
<input
type="number"
min="0"
max={checkoutItem.item.quantity}
value={checkoutItem.requestedQuantity}
onChange={(e) => updateQuantity(checkoutItem.item.id, parseInt(e.target.value) || 0)}
className="w-16 px-2 py-1 border border-gray-300 rounded text-center text-sm"
/>
</div>
<div className="text-sm font-medium text-green-600">
${(checkoutItem.requestedQuantity * checkoutItem.item.value).toFixed(2)}
</div>
</div>
</div>
{/* Desktop Layout */}
<div className="hidden sm:flex sm:items-center sm:justify-between">
<div className="flex-1">
<h3 className="font-medium text-gray-900">{checkoutItem.item.name}</h3>
<div className="text-sm text-gray-500 mt-1">
Available: {checkoutItem.item.quantity} |
Value: ${checkoutItem.item.value.toFixed(2)} each |
Current Location: {checkoutItem.item.location || 'Not specified'}
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<label className="text-sm font-medium text-gray-700">Quantity:</label>
<input
type="number"
min="0"
max={checkoutItem.item.quantity}
value={checkoutItem.requestedQuantity}
onChange={(e) => updateQuantity(checkoutItem.item.id, parseInt(e.target.value) || 0)}
className="w-20 px-2 py-1 border border-gray-300 rounded text-center"
/>
</div>
<div className="text-sm font-medium text-green-600">
${(checkoutItem.requestedQuantity * checkoutItem.item.value).toFixed(2)}
</div>
<button
onClick={() => removeItem(checkoutItem.item.id)}
className="text-red-500 hover:text-red-700 px-2 py-1"
>
Remove
</button>
</div>
</div>
</div>
))}
</div>
{/* Summary */}
<div className="border-t pt-3 sm:pt-4 mb-4 sm:mb-6">
<div className="flex justify-between items-center text-base sm:text-lg font-semibold">
<span>Total Value:</span>
<span className="text-green-600">${totalValue.toFixed(2)}</span>
</div>
<div className="text-xs sm:text-sm text-gray-600 mt-1">
{checkoutItems.reduce((sum, item) => sum + item.requestedQuantity, 0)} item(s) selected
</div>
</div>
{/* Status Options */}
<div className="mb-4 sm:mb-6 p-3 sm:p-4 bg-yellow-50 rounded-lg">
<div className="flex items-start sm:items-center space-x-2">
<input
id="markAsUsedUp"
type="checkbox"
checked={markAsUsedUp}
onChange={(e) => setMarkAsUsedUp(e.target.checked)}
className="h-4 w-4 text-red-600 rounded border-gray-300 focus:ring-red-500 mt-0.5 sm:mt-0"
/>
<div className="flex-1">
<label htmlFor="markAsUsedUp" className="text-sm font-medium text-gray-700 block">
Mark items as "Used Up" (consumed/disposed)
</label>
<div className="text-xs text-gray-500 mt-1">
{markAsUsedUp
? "Items will be marked as 'Used Up' and won't be available for future checkout"
: "Items will remain 'Active' and available for future checkout from their new location"
}
</div>
</div>
</div>
</div>
{/* Actions */}
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
<button
onClick={processCheckout}
disabled={isProcessing || !newLocation.trim() || checkoutItems.length === 0}
className="flex-1 bg-green-500 hover:bg-green-600 disabled:bg-gray-300 text-white px-4 py-3 sm:py-2 rounded font-medium text-sm touch-manipulation"
>
{isProcessing ? 'Processing...' : 'Complete Checkout'}
</button>
<button
onClick={onClose}
className="flex-1 sm:flex-initial px-4 py-3 sm:py-2 bg-gray-300 hover:bg-gray-400 text-gray-700 rounded font-medium text-sm touch-manipulation"
>
Cancel
</button>
</div>
</>
)}
</div>
</div>
);
}

View File

@@ -18,6 +18,7 @@ export function initializeDatabase() {
quantity INTEGER NOT NULL DEFAULT 0, quantity INTEGER NOT NULL DEFAULT 0,
value REAL NOT NULL DEFAULT 0, value REAL NOT NULL DEFAULT 0,
location TEXT, location TEXT,
status TEXT DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
) )
@@ -40,6 +41,23 @@ export function initializeDatabase() {
db.exec(createInventoryTable); db.exec(createInventoryTable);
db.exec(createAuditLogTable); db.exec(createAuditLogTable);
// Migration: Add status column if it doesn't exist
try {
const checkColumn = db.prepare("PRAGMA table_info(inventory)");
const columns = checkColumn.all() as any[];
const hasStatusColumn = columns.some(col => col.name === 'status');
if (!hasStatusColumn) {
console.log('Adding status column to inventory table...');
db.exec("ALTER TABLE inventory ADD COLUMN status TEXT DEFAULT 'active'");
// Update all existing items to have active status
db.exec("UPDATE inventory SET status = 'active' WHERE status IS NULL");
console.log('Status column added successfully');
}
} catch (error) {
console.error('Error during migration:', error);
}
console.log('Database initialized successfully'); console.log('Database initialized successfully');
} }
@@ -51,6 +69,12 @@ export const inventoryQueries = {
// Get all inventory items // Get all inventory items
getAll: db.prepare('SELECT * FROM inventory ORDER BY name ASC'), getAll: db.prepare('SELECT * FROM inventory ORDER BY name ASC'),
// Get active inventory items only
getActive: db.prepare("SELECT * FROM inventory WHERE status = 'active' ORDER BY name ASC"),
// Get items by status
getByStatus: db.prepare('SELECT * FROM inventory WHERE status = ? ORDER BY name ASC'),
// Get inventory item by ID // Get inventory item by ID
getById: db.prepare('SELECT * FROM inventory WHERE id = ?'), getById: db.prepare('SELECT * FROM inventory WHERE id = ?'),
@@ -58,16 +82,19 @@ export const inventoryQueries = {
searchByName: db.prepare('SELECT * FROM inventory WHERE name LIKE ? ORDER BY name ASC'), searchByName: db.prepare('SELECT * FROM inventory WHERE name LIKE ? ORDER BY name ASC'),
// Create new inventory item // Create new inventory item
create: db.prepare('INSERT INTO inventory (name, quantity, value, location) VALUES (?, ?, ?, ?)'), create: db.prepare('INSERT INTO inventory (name, quantity, value, location, status) VALUES (?, ?, ?, ?, ?)'),
// Update inventory item // Update inventory item
update: db.prepare('UPDATE inventory SET name = ?, quantity = ?, value = ?, location = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'), update: db.prepare('UPDATE inventory SET name = ?, quantity = ?, value = ?, location = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'),
// Delete inventory item // Delete inventory item
delete: db.prepare('DELETE FROM inventory WHERE id = ?'), delete: db.prepare('DELETE FROM inventory WHERE id = ?'),
// Update quantity only // Update quantity only
updateQuantity: db.prepare('UPDATE inventory SET quantity = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'), updateQuantity: db.prepare('UPDATE inventory SET quantity = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'),
// Update location and status
updateLocationAndStatus: db.prepare('UPDATE inventory SET location = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'),
}; };
// Audit log operations // Audit log operations