diff --git a/src/app/api/inventory/[id]/route.ts b/src/app/api/inventory/[id]/route.ts index b2ed152..9b58e2e 100644 --- a/src/app/api/inventory/[id]/route.ts +++ b/src/app/api/inventory/[id]/route.ts @@ -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); diff --git a/src/app/api/inventory/route.ts b/src/app/api/inventory/route.ts index fa36d6b..564eca3 100644 --- a/src/app/api/inventory/route.ts +++ b/src/app/api/inventory/route.ts @@ -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 } ); diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts new file mode 100644 index 0000000..70bc98c --- /dev/null +++ b/src/app/api/users/route.ts @@ -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 } + ); + } +} diff --git a/src/app/globals.css b/src/app/globals.css index dc98be7..efa8047 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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); diff --git a/src/app/inventory/[id]/page.tsx b/src/app/inventory/[id]/page.tsx index f158435..5227711 100644 --- a/src/app/inventory/[id]/page.tsx +++ b/src/app/inventory/[id]/page.tsx @@ -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 ( -
+
{/* Navigation */} -
+
{/* Item Details */} -
-
-

{item.name}

-
+
+
+

{item.name}

+
{!isEditing ? ( <> @@ -218,13 +222,13 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> } <> @@ -234,7 +238,7 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
{isEditing ? ( -
+
+
+ + +
) : ( -
-
-
- -

{item.quantity}

+
+ {/* Mobile Layout */} +
+
+
+ +

{item.quantity}

+
+
+ +

${item.value.toFixed(2)}

+
- -

${item.value.toFixed(2)}

+ +

{item.location || 'Not specified'}

+
+
+
+ +

+ {item.status === 'checked-out' ? 'Checked Out' : + item.status === 'used-up' ? 'Used Up' : + item.status === 'active' ? 'Active' : + item.status} +

+
+
+ +

${(item.quantity * item.value).toFixed(2)}

+
-
-
- -

{item.location || 'Not specified'}

+ + {/* Desktop Layout */} +
+
+
+ +

{item.quantity}

+
+
+ +

${item.value.toFixed(2)}

+
-
- -

${(item.quantity * item.value).toFixed(2)}

+
+
+ +

{item.location || 'Not specified'}

+
+
+ +

+ {item.status === 'checked-out' ? 'Checked Out' : + item.status === 'used-up' ? 'Used Up' : + item.status === 'active' ? 'Active' : + item.status} +

+
+
+ +

${(item.quantity * item.value).toFixed(2)}

+
@@ -322,28 +396,28 @@ export default function ItemPage({ params }: { params: Promise<{ id: string }> }
{/* Change History */} -
-

Change History

+
+

Change History

{auditLogs.length === 0 ? ( -

No changes recorded for this item.

+

No changes recorded for this item.

) : ( -
+
{auditLogs.map((log) => (
-
-
-
+
+
+
{formatAuditMessage(log)}
-
+
{formatTimestamp(log.timestamp)}
-
+
{log.action}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..86eafde 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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({ diff --git a/src/app/page.tsx b/src/app/page.tsx index 2e06026..1c04082 100644 --- a/src/app/page.tsx +++ b/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([]); const [isLoading, setIsLoading] = useState(true); const [editingItem, setEditingItem] = useState(null); + const [selectedItems, setSelectedItems] = useState>(new Set()); + const [showBasket, setShowBasket] = useState(false); + const [statusFilter, setStatusFilter] = useState('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 ( -
-

Inventory Management

+
+
+

Inventory Management

+ + {/* Status Filter */} +
+
+ + +
+ + {/* Checkout Actions */} + {selectedItems.size > 0 && statusFilter === 'active' && ( +
+ + {selectedItems.size} item{selectedItems.size !== 1 ? 's' : ''} selected + + + +
+ )} +
+
{/* Add/Edit Form */} -
-

+
+

{editingItem ? 'Edit Item' : 'Add New Item'}

-
-
+ +
setFormData({ ...formData, name: e.target.value })} required @@ -142,7 +216,7 @@ export default function Home() { 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 />
-
+
setFormData({ ...formData, location: e.target.value })} />
-
+
{editingItem && (
{/* Items List */} -
- {items.map((item) => ( -
-
viewItem(item.id)} - > -

{item.name}

-
-

Quantity: {item.quantity}

-

Value: ${item.value.toFixed(2)}

-

Location: {item.location || 'Not specified'}

-

Total Value: ${(item.quantity * item.value).toFixed(2)}

-
-
-
- - - -
+
+
+

Inventory Items

+
+ + {items.length === 0 ? ( +
+ No inventory items found. Add your first item above!
- ))} + ) : ( +
+ {items.map((item) => ( +
+
+
+ {/* Only show checkbox for active items */} + {statusFilter === 'active' && ( + toggleItemSelection(item.id)} + className="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500 mt-1 sm:mt-0" + /> + )} + +
viewItem(item.id)} + > + {/* Mobile Layout */} +
+
+

{item.name}

+
+ {item.status === 'checked-out' ? 'Checked Out' : + item.status === 'used-up' ? 'Used Up' : + item.status === 'active' ? 'Active' : + item.status} +
+
+
+
Qty: {item.quantity}
+
Value: ${item.value.toFixed(2)}
+
Location: {item.location || 'Not specified'}
+
Total: ${(item.quantity * item.value).toFixed(2)}
+
+
+ + {/* Desktop Layout */} +
+
+
{item.name}
+
ID: {item.id}
+
+
+
Quantity
+
{item.quantity}
+
+
+
Value
+
${item.value.toFixed(2)}
+
+
+
Location
+
{item.location || 'Not specified'}
+
+
+
Status
+
+ {item.status === 'checked-out' ? 'Checked Out' : + item.status === 'used-up' ? 'Used Up' : + item.status === 'active' ? 'Active' : + item.status} +
+
+
+
+
+ +
+ {/* Mobile: Show total value prominently */} +
+
Total
+
${(item.quantity * item.value).toFixed(2)}
+
+ + {/* Desktop: Show total value */} +
+
Total Value
+
${(item.quantity * item.value).toFixed(2)}
+
+ + {/* Action Buttons */} +
+ + + +
+
+
+
+ ))} +
+ )}
- {items.length === 0 && ( -
- No inventory items found. Add your first item above! -
- )} + {/* Checkout Basket Modal */} + setShowBasket(false)} + selectedItems={getSelectedItemsData()} + onCheckout={() => { + fetchItems(); // Refresh the list after checkout + setSelectedItems(new Set()); // Clear selections + setShowBasket(false); // Close basket + }} + />
); } diff --git a/src/components/CheckoutBasket.tsx b/src/components/CheckoutBasket.tsx new file mode 100644 index 0000000..e2b41b1 --- /dev/null +++ b/src/components/CheckoutBasket.tsx @@ -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([]); + 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 ( +
+
+
+

Checkout Basket

+ +
+ + {checkoutItems.length === 0 ? ( +
+ No items in basket +
+ ) : ( + <> + {/* New Location Input */} +
+ + setNewLocation(e.target.value)} + placeholder="e.g., Office Desk, Storage Room A, etc." + required + /> +
+ + {/* Items List */} +
+ {checkoutItems.map((checkoutItem) => ( +
+ {/* Mobile Layout */} +
+
+
+

{checkoutItem.item.name}

+
+ Available: {checkoutItem.item.quantity} | + ${checkoutItem.item.value.toFixed(2)} each +
+
+ From: {checkoutItem.item.location || 'Not specified'} +
+
+ +
+
+
+ + 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" + /> +
+
+ ${(checkoutItem.requestedQuantity * checkoutItem.item.value).toFixed(2)} +
+
+
+ + {/* Desktop Layout */} +
+
+

{checkoutItem.item.name}

+
+ Available: {checkoutItem.item.quantity} | + Value: ${checkoutItem.item.value.toFixed(2)} each | + Current Location: {checkoutItem.item.location || 'Not specified'} +
+
+ +
+
+ + updateQuantity(checkoutItem.item.id, parseInt(e.target.value) || 0)} + className="w-20 px-2 py-1 border border-gray-300 rounded text-center" + /> +
+ +
+ ${(checkoutItem.requestedQuantity * checkoutItem.item.value).toFixed(2)} +
+ + +
+
+
+ ))} +
+ + {/* Summary */} +
+
+ Total Value: + ${totalValue.toFixed(2)} +
+
+ {checkoutItems.reduce((sum, item) => sum + item.requestedQuantity, 0)} item(s) selected +
+
+ + {/* Status Options */} +
+
+ 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" + /> +
+ +
+ {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" + } +
+
+
+
+ + {/* Actions */} +
+ + +
+ + )} +
+
+ ); +} diff --git a/src/lib/database.ts b/src/lib/database.ts index ff1bd2d..66918f1 100644 --- a/src/lib/database.ts +++ b/src/lib/database.ts @@ -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