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:
@@ -34,7 +34,7 @@ export async function PUT(
|
||||
) {
|
||||
try {
|
||||
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) {
|
||||
return NextResponse.json(
|
||||
@@ -58,6 +58,7 @@ export async function PUT(
|
||||
quantity,
|
||||
value,
|
||||
location || '',
|
||||
status || 'active',
|
||||
id
|
||||
);
|
||||
|
||||
@@ -81,13 +82,17 @@ export async function PUT(
|
||||
if (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({
|
||||
id: id,
|
||||
name,
|
||||
quantity,
|
||||
value,
|
||||
location: location || ''
|
||||
location: location || '',
|
||||
status: status || 'active'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating inventory item:', error);
|
||||
|
||||
@@ -2,9 +2,18 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { inventoryQueries, auditQueries } from '@/lib/database';
|
||||
|
||||
// GET /api/inventory - Get all inventory items
|
||||
export async function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error('Error fetching inventory:', error);
|
||||
@@ -18,7 +27,7 @@ export async function GET() {
|
||||
// POST /api/inventory - Create new inventory item
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { name, quantity, value, location } = await request.json();
|
||||
const { name, quantity, value, location, status } = await request.json();
|
||||
|
||||
if (!name || quantity === undefined || value === undefined) {
|
||||
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;
|
||||
|
||||
// Log the creation
|
||||
@@ -39,7 +48,8 @@ export async function POST(request: NextRequest) {
|
||||
name,
|
||||
quantity,
|
||||
value,
|
||||
location: location || ''
|
||||
location: location || '',
|
||||
status: status || 'active'
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
|
||||
48
src/app/api/users/route.ts
Normal file
48
src/app/api/users/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,56 @@
|
||||
|
||||
@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 {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
||||
@@ -9,6 +9,7 @@ interface InventoryItem {
|
||||
quantity: number;
|
||||
value: number;
|
||||
location: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -34,7 +35,8 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
|
||||
name: '',
|
||||
quantity: 0,
|
||||
value: 0,
|
||||
location: ''
|
||||
location: '',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// Initialize item ID from params
|
||||
@@ -59,7 +61,8 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
|
||||
name: data.name,
|
||||
quantity: data.quantity,
|
||||
value: data.value,
|
||||
location: data.location
|
||||
location: data.location,
|
||||
status: data.status || 'active'
|
||||
});
|
||||
} else {
|
||||
console.error('Item not found');
|
||||
@@ -129,7 +132,8 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
|
||||
name: item.name,
|
||||
quantity: item.quantity,
|
||||
value: item.value,
|
||||
location: item.location
|
||||
location: item.location,
|
||||
status: item.status || 'active'
|
||||
});
|
||||
}
|
||||
setIsEditing(false);
|
||||
@@ -183,33 +187,33 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
|
||||
}
|
||||
|
||||
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 */}
|
||||
<div className="mb-6">
|
||||
<div className="mb-4 sm:mb-6">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Item Details */}
|
||||
<div className="bg-white shadow-md rounded-lg p-6 mb-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h1 className="text-2xl font-bold">{item.name}</h1>
|
||||
<div className="flex gap-2">
|
||||
<div className="bg-white shadow-md rounded-lg p-4 sm:p-6 mb-4 sm:mb-6">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start mb-4 gap-3">
|
||||
<h1 className="text-xl sm:text-2xl font-bold">{item.name}</h1>
|
||||
<div className="flex flex-wrap gap-2 w-full sm:w-auto">
|
||||
{!isEditing ? (
|
||||
<>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -218,13 +222,13 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
|
||||
<>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -234,7 +238,7 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
|
||||
</div>
|
||||
|
||||
{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>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Name
|
||||
@@ -242,7 +246,7 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
|
||||
<input
|
||||
id="name"
|
||||
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}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
required
|
||||
@@ -255,7 +259,7 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
|
||||
<input
|
||||
id="quantity"
|
||||
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}
|
||||
onChange={(e) => setFormData({ ...formData, quantity: parseInt(e.target.value) || 0 })}
|
||||
required
|
||||
@@ -269,7 +273,7 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
|
||||
id="value"
|
||||
type="number"
|
||||
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}
|
||||
onChange={(e) => setFormData({ ...formData, value: parseFloat(e.target.value) || 0 })}
|
||||
required
|
||||
@@ -282,32 +286,102 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
|
||||
<input
|
||||
id="location"
|
||||
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}
|
||||
onChange={(e) => setFormData({ ...formData, location: e.target.value })}
|
||||
/>
|
||||
</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>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Quantity</label>
|
||||
<p className="text-lg font-semibold">{item.quantity}</p>
|
||||
<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-sm font-medium text-gray-700">Value per Item</label>
|
||||
<p className="text-lg font-semibold">${item.value.toFixed(2)}</p>
|
||||
<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>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Location</label>
|
||||
<p className="text-lg font-semibold">{item.location || 'Not specified'}</p>
|
||||
|
||||
{/* Desktop Layout */}
|
||||
<div className="hidden sm:grid sm:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Quantity</label>
|
||||
<p className="text-lg font-semibold">{item.quantity}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Value per Item</label>
|
||||
<p className="text-lg font-semibold">${item.value.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Location</label>
|
||||
<p className="text-lg font-semibold">{item.location || 'Not specified'}</p>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -322,28 +396,28 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
|
||||
</div>
|
||||
|
||||
{/* Change History */}
|
||||
<div className="bg-white shadow-md rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Change History</h2>
|
||||
<div className="bg-white shadow-md rounded-lg p-4 sm:p-6">
|
||||
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Change History</h2>
|
||||
|
||||
{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) => (
|
||||
<div
|
||||
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>
|
||||
<div className="font-medium text-gray-800 mb-1">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-1 sm:gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-800 mb-1 text-sm">
|
||||
{formatAuditMessage(log)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
<div className="text-xs sm:text-sm text-gray-600">
|
||||
{formatTimestamp(log.timestamp)}
|
||||
</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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,8 +13,14 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Inventory Management",
|
||||
description: "Simple inventory management system with item tracking",
|
||||
};
|
||||
|
||||
export const viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
306
src/app/page.tsx
306
src/app/page.tsx
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import CheckoutBasket from '@/components/CheckoutBasket';
|
||||
|
||||
interface InventoryItem {
|
||||
id: number;
|
||||
@@ -9,6 +10,7 @@ interface InventoryItem {
|
||||
quantity: number;
|
||||
value: number;
|
||||
location: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -18,17 +20,24 @@ export default function Home() {
|
||||
const [items, setItems] = useState<InventoryItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
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({
|
||||
name: '',
|
||||
quantity: 0,
|
||||
value: 0,
|
||||
location: ''
|
||||
location: '',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// Fetch all inventory items
|
||||
// Fetch items by status
|
||||
const fetchItems = async () => {
|
||||
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();
|
||||
setItems(data);
|
||||
} catch (error) {
|
||||
@@ -85,14 +94,15 @@ export default function Home() {
|
||||
name: item.name,
|
||||
quantity: item.quantity,
|
||||
value: item.value,
|
||||
location: item.location
|
||||
location: item.location,
|
||||
status: item.status || 'active'
|
||||
});
|
||||
};
|
||||
|
||||
// Reset form
|
||||
const resetForm = () => {
|
||||
setEditingItem(null);
|
||||
setFormData({ name: '', quantity: 0, value: 0, location: '' });
|
||||
setFormData({ name: '', quantity: 0, value: 0, location: '', status: 'active' });
|
||||
};
|
||||
|
||||
// Navigate to item page
|
||||
@@ -100,9 +110,30 @@ export default function Home() {
|
||||
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(() => {
|
||||
fetchItems();
|
||||
}, []);
|
||||
}, [statusFilter]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -113,23 +144,66 @@ export default function Home() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 max-w-6xl">
|
||||
<h1 className="text-3xl font-bold mb-6">Inventory Management</h1>
|
||||
<div className="container mx-auto p-3 sm:p-4 max-w-6xl min-h-screen">
|
||||
<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 */}
|
||||
<div className="bg-white shadow-md rounded-lg p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
<div className="bg-white shadow-md rounded-lg p-4 sm:p-6 mb-4 sm:mb-6">
|
||||
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">
|
||||
{editingItem ? 'Edit Item' : 'Add New Item'}
|
||||
</h2>
|
||||
<form onSubmit={saveItem} className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<form onSubmit={saveItem} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
||||
<div className="sm:col-span-2 lg:col-span-1">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
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}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
required
|
||||
@@ -142,7 +216,7 @@ export default function Home() {
|
||||
<input
|
||||
id="quantity"
|
||||
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}
|
||||
onChange={(e) => setFormData({ ...formData, quantity: parseInt(e.target.value) || 0 })}
|
||||
required
|
||||
@@ -156,35 +230,35 @@ export default function Home() {
|
||||
id="value"
|
||||
type="number"
|
||||
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}
|
||||
onChange={(e) => setFormData({ ...formData, value: parseFloat(e.target.value) || 0 })}
|
||||
required
|
||||
/>
|
||||
</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">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="location"
|
||||
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}
|
||||
onChange={(e) => setFormData({ ...formData, location: e.target.value })}
|
||||
/>
|
||||
</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
|
||||
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
|
||||
</button>
|
||||
{editingItem && (
|
||||
<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}
|
||||
>
|
||||
Cancel
|
||||
@@ -195,50 +269,160 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
{/* Items List */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="bg-white shadow-md rounded-lg p-6">
|
||||
<div
|
||||
className="cursor-pointer hover:bg-gray-50 -m-6 p-6 rounded-lg transition-colors"
|
||||
onClick={() => viewItem(item.id)}
|
||||
>
|
||||
<h3 className="text-lg font-semibold mb-3">{item.name}</h3>
|
||||
<div className="space-y-2">
|
||||
<p><strong>Quantity:</strong> {item.quantity}</p>
|
||||
<p><strong>Value:</strong> ${item.value.toFixed(2)}</p>
|
||||
<p><strong>Location:</strong> {item.location || 'Not specified'}</p>
|
||||
<p><strong>Total Value:</strong> ${(item.quantity * item.value).toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4">
|
||||
<button
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded text-sm font-medium"
|
||||
onClick={() => editItem(item)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded text-sm font-medium"
|
||||
onClick={() => viewItem(item.id)}
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
<button
|
||||
className="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-sm font-medium"
|
||||
onClick={() => deleteItem(item.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<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) => (
|
||||
<div
|
||||
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)}
|
||||
>
|
||||
{/* Mobile Layout */}
|
||||
<div className="sm:hidden">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="font-medium text-gray-900 text-sm">{item.name}</h3>
|
||||
<div className={`text-xs font-medium px-2 py-1 rounded ${
|
||||
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 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
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white px-2 py-1 rounded text-xs font-medium touch-manipulation"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
editItem(item);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="bg-green-500 hover:bg-green-600 text-white px-2 py-1 rounded text-xs font-medium touch-manipulation"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
viewItem(item.id);
|
||||
}}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
className="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-xs font-medium touch-manipulation"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteItem(item.id);
|
||||
}}
|
||||
>
|
||||
Del
|
||||
</button>
|
||||
</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>
|
||||
)}
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
322
src/components/CheckoutBasket.tsx
Normal file
322
src/components/CheckoutBasket.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export function initializeDatabase() {
|
||||
quantity INTEGER NOT NULL DEFAULT 0,
|
||||
value REAL NOT NULL DEFAULT 0,
|
||||
location TEXT,
|
||||
status TEXT DEFAULT 'active',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
@@ -40,6 +41,23 @@ export function initializeDatabase() {
|
||||
db.exec(createInventoryTable);
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -51,6 +69,12 @@ export const inventoryQueries = {
|
||||
// Get all inventory items
|
||||
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
|
||||
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'),
|
||||
|
||||
// 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: 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: db.prepare('DELETE FROM inventory WHERE id = ?'),
|
||||
|
||||
// Update quantity only
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user