Manage categories and locations
This commit is contained in:
@@ -2,6 +2,8 @@ import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom';
|
|||||||
import ItemsPage from './pages/ItemsPage';
|
import ItemsPage from './pages/ItemsPage';
|
||||||
import StatsPage from './pages/StatsPage';
|
import StatsPage from './pages/StatsPage';
|
||||||
import DeletedItemsPage from './pages/DeletedItemsPage';
|
import DeletedItemsPage from './pages/DeletedItemsPage';
|
||||||
|
import LocationsPage from './pages/LocationsPage';
|
||||||
|
import CategoriesPage from './pages/CategoriesPage';
|
||||||
import { useTheme } from './useTheme';
|
import { useTheme } from './useTheme';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -29,6 +31,22 @@ export default function App() {
|
|||||||
>
|
>
|
||||||
Stats
|
Stats
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
to="/categories"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`text-sm font-medium px-3 py-1.5 rounded-md ${isActive ? 'bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900' : 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Categories
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
to="/locations"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`text-sm font-medium px-3 py-1.5 rounded-md ${isActive ? 'bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900' : 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Locations
|
||||||
|
</NavLink>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/deleted"
|
to="/deleted"
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
@@ -57,6 +75,8 @@ export default function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<ItemsPage />} />
|
<Route path="/" element={<ItemsPage />} />
|
||||||
<Route path="/stats" element={<StatsPage dark={dark} />} />
|
<Route path="/stats" element={<StatsPage dark={dark} />} />
|
||||||
|
<Route path="/categories" element={<CategoriesPage />} />
|
||||||
|
<Route path="/locations" element={<LocationsPage />} />
|
||||||
<Route path="/deleted" element={<DeletedItemsPage />} />
|
<Route path="/deleted" element={<DeletedItemsPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -55,6 +55,34 @@ export const api = {
|
|||||||
return request('/suggestions/locations');
|
return request('/suggestions/locations');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Locations
|
||||||
|
listLocations(): Promise<{ location: string; item_count: number }[]> {
|
||||||
|
return request('/locations');
|
||||||
|
},
|
||||||
|
renameLocation(oldName: string, newName: string): Promise<{ updated: number }> {
|
||||||
|
return request(`/locations/${encodeURIComponent(oldName)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ name: newName }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteLocation(name: string): Promise<{ updated: number }> {
|
||||||
|
return request(`/locations/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// Categories
|
||||||
|
listCategories(): Promise<{ category: string; item_count: number }[]> {
|
||||||
|
return request('/categories');
|
||||||
|
},
|
||||||
|
renameCategory(oldName: string, newName: string): Promise<{ updated: number }> {
|
||||||
|
return request(`/categories/${encodeURIComponent(oldName)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ name: newName }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteCategory(name: string): Promise<{ updated: number }> {
|
||||||
|
return request(`/categories/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
||||||
|
},
|
||||||
|
|
||||||
// Files
|
// Files
|
||||||
async uploadFiles(itemId: string, files: File[], type: 'photo' | 'receipt'): Promise<ItemFile[]> {
|
async uploadFiles(itemId: string, files: File[], type: 'photo' | 'receipt'): Promise<ItemFile[]> {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
|
|||||||
143
client/src/pages/CategoriesPage.tsx
Normal file
143
client/src/pages/CategoriesPage.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { api } from '../api';
|
||||||
|
|
||||||
|
interface CategoryRow {
|
||||||
|
category: string;
|
||||||
|
item_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CategoriesPage() {
|
||||||
|
const [categories, setCategories] = useState<CategoryRow[]>([]);
|
||||||
|
const [editingName, setEditingName] = useState<string | null>(null);
|
||||||
|
const [newName, setNewName] = useState('');
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchCategories = useCallback(() => {
|
||||||
|
api.listCategories().then(setCategories);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCategories();
|
||||||
|
}, [fetchCategories]);
|
||||||
|
|
||||||
|
const startRename = (category: string) => {
|
||||||
|
setEditingName(category);
|
||||||
|
setNewName(category);
|
||||||
|
setConfirmDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelRename = () => {
|
||||||
|
setEditingName(null);
|
||||||
|
setNewName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = async (oldName: string) => {
|
||||||
|
const trimmed = newName.trim();
|
||||||
|
if (!trimmed || trimmed === oldName) {
|
||||||
|
cancelRename();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.renameCategory(oldName, trimmed);
|
||||||
|
setEditingName(null);
|
||||||
|
setNewName('');
|
||||||
|
fetchCategories();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (name: string) => {
|
||||||
|
if (confirmDelete !== name) {
|
||||||
|
setConfirmDelete(name);
|
||||||
|
setEditingName(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.deleteCategory(name);
|
||||||
|
setConfirmDelete(null);
|
||||||
|
fetchCategories();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Categories</h2>
|
||||||
|
|
||||||
|
{categories.length === 0 ? (
|
||||||
|
<div className="text-sm text-gray-400 text-center py-12">
|
||||||
|
No categories found. Assign categories to items to see them here.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-100 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||||
|
<th className="px-3 py-2 font-medium">Category</th>
|
||||||
|
<th className="px-3 py-2 font-medium">Items</th>
|
||||||
|
<th className="px-3 py-2 font-medium text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<tr key={cat.category} className="border-b border-gray-50 dark:border-gray-800">
|
||||||
|
<td className="px-3 py-2 font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{editingName === cat.category ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newName}
|
||||||
|
onChange={e => setNewName(e.target.value)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') handleRename(cat.category);
|
||||||
|
if (e.key === 'Escape') cancelRename();
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
className="w-full px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
cat.category
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-gray-500 dark:text-gray-400">{cat.item_count}</td>
|
||||||
|
<td className="px-3 py-2 text-right">
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
{editingName === cat.category ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRename(cat.category)}
|
||||||
|
className="text-xs font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 px-2 py-1 rounded hover:bg-blue-50 dark:hover:bg-blue-900/30"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={cancelRename}
|
||||||
|
className="text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => startRename(cat.category)}
|
||||||
|
className="text-xs font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 px-2 py-1 rounded hover:bg-blue-50 dark:hover:bg-blue-900/30"
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(cat.category)}
|
||||||
|
className={`text-xs font-medium px-2 py-1 rounded ${
|
||||||
|
confirmDelete === cat.category
|
||||||
|
? 'bg-red-600 text-white'
|
||||||
|
: 'text-red-500 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{confirmDelete === cat.category ? 'Confirm' : 'Delete'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
143
client/src/pages/LocationsPage.tsx
Normal file
143
client/src/pages/LocationsPage.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { api } from '../api';
|
||||||
|
|
||||||
|
interface LocationRow {
|
||||||
|
location: string;
|
||||||
|
item_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LocationsPage() {
|
||||||
|
const [locations, setLocations] = useState<LocationRow[]>([]);
|
||||||
|
const [editingName, setEditingName] = useState<string | null>(null);
|
||||||
|
const [newName, setNewName] = useState('');
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchLocations = useCallback(() => {
|
||||||
|
api.listLocations().then(setLocations);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLocations();
|
||||||
|
}, [fetchLocations]);
|
||||||
|
|
||||||
|
const startRename = (location: string) => {
|
||||||
|
setEditingName(location);
|
||||||
|
setNewName(location);
|
||||||
|
setConfirmDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelRename = () => {
|
||||||
|
setEditingName(null);
|
||||||
|
setNewName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = async (oldName: string) => {
|
||||||
|
const trimmed = newName.trim();
|
||||||
|
if (!trimmed || trimmed === oldName) {
|
||||||
|
cancelRename();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.renameLocation(oldName, trimmed);
|
||||||
|
setEditingName(null);
|
||||||
|
setNewName('');
|
||||||
|
fetchLocations();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (name: string) => {
|
||||||
|
if (confirmDelete !== name) {
|
||||||
|
setConfirmDelete(name);
|
||||||
|
setEditingName(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.deleteLocation(name);
|
||||||
|
setConfirmDelete(null);
|
||||||
|
fetchLocations();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Locations</h2>
|
||||||
|
|
||||||
|
{locations.length === 0 ? (
|
||||||
|
<div className="text-sm text-gray-400 text-center py-12">
|
||||||
|
No locations found. Assign locations to items to see them here.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-100 dark:border-gray-800 text-left text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||||
|
<th className="px-3 py-2 font-medium">Location</th>
|
||||||
|
<th className="px-3 py-2 font-medium">Items</th>
|
||||||
|
<th className="px-3 py-2 font-medium text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{locations.map(loc => (
|
||||||
|
<tr key={loc.location} className="border-b border-gray-50 dark:border-gray-800">
|
||||||
|
<td className="px-3 py-2 font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{editingName === loc.location ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newName}
|
||||||
|
onChange={e => setNewName(e.target.value)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') handleRename(loc.location);
|
||||||
|
if (e.key === 'Escape') cancelRename();
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
className="w-full px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
loc.location
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-gray-500 dark:text-gray-400">{loc.item_count}</td>
|
||||||
|
<td className="px-3 py-2 text-right">
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
{editingName === loc.location ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRename(loc.location)}
|
||||||
|
className="text-xs font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 px-2 py-1 rounded hover:bg-blue-50 dark:hover:bg-blue-900/30"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={cancelRename}
|
||||||
|
className="text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => startRename(loc.location)}
|
||||||
|
className="text-xs font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 px-2 py-1 rounded hover:bg-blue-50 dark:hover:bg-blue-900/30"
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(loc.location)}
|
||||||
|
className={`text-xs font-medium px-2 py-1 rounded ${
|
||||||
|
confirmDelete === loc.location
|
||||||
|
? 'bg-red-600 text-white'
|
||||||
|
: 'text-red-500 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{confirmDelete === loc.location ? 'Confirm' : 'Delete'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import itemsRouter from './routes/items.js';
|
|||||||
import uploadsRouter, { createFileUploadRouter, createFileDeleteRouter } from './routes/uploads.js';
|
import uploadsRouter, { createFileUploadRouter, createFileDeleteRouter } from './routes/uploads.js';
|
||||||
import statsRouter from './routes/stats.js';
|
import statsRouter from './routes/stats.js';
|
||||||
import suggestionsRouter from './routes/suggestions.js';
|
import suggestionsRouter from './routes/suggestions.js';
|
||||||
|
import locationsRouter from './routes/locations.js';
|
||||||
|
import categoriesRouter from './routes/categories.js';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -26,6 +28,8 @@ app.use('/api/files', createFileDeleteRouter());
|
|||||||
app.use('/api/uploads', uploadsRouter);
|
app.use('/api/uploads', uploadsRouter);
|
||||||
app.use('/api/stats', statsRouter);
|
app.use('/api/stats', statsRouter);
|
||||||
app.use('/api/suggestions', suggestionsRouter);
|
app.use('/api/suggestions', suggestionsRouter);
|
||||||
|
app.use('/api/locations', locationsRouter);
|
||||||
|
app.use('/api/categories', categoriesRouter);
|
||||||
|
|
||||||
// Serve static client in production
|
// Serve static client in production
|
||||||
const clientDist = join(__dirname, '..', '..', 'client', 'dist');
|
const clientDist = join(__dirname, '..', '..', 'client', 'dist');
|
||||||
|
|||||||
41
server/src/routes/categories.ts
Normal file
41
server/src/routes/categories.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import db from '../db.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// List all categories with item counts
|
||||||
|
router.get('/', (_req, res) => {
|
||||||
|
const rows = db.prepare(
|
||||||
|
`SELECT category, COUNT(*) as item_count
|
||||||
|
FROM items WHERE deleted_at IS NULL AND category != ''
|
||||||
|
GROUP BY category ORDER BY category`
|
||||||
|
).all() as { category: string; item_count: number }[];
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rename a category (bulk update all items)
|
||||||
|
router.put('/:name', (req, res) => {
|
||||||
|
const oldName = req.params.name;
|
||||||
|
const { name } = req.body;
|
||||||
|
if (!name || typeof name !== 'string' || !name.trim()) {
|
||||||
|
res.status(400).json({ error: 'New name is required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = db.prepare(
|
||||||
|
`UPDATE items SET category = ?, updated_at = datetime('now')
|
||||||
|
WHERE category = ? AND deleted_at IS NULL`
|
||||||
|
).run(name.trim(), oldName);
|
||||||
|
res.json({ updated: result.changes });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete a category (clear from all items)
|
||||||
|
router.delete('/:name', (req, res) => {
|
||||||
|
const name = req.params.name;
|
||||||
|
const result = db.prepare(
|
||||||
|
`UPDATE items SET category = '', updated_at = datetime('now')
|
||||||
|
WHERE category = ? AND deleted_at IS NULL`
|
||||||
|
).run(name);
|
||||||
|
res.json({ updated: result.changes });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
41
server/src/routes/locations.ts
Normal file
41
server/src/routes/locations.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import db from '../db.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// List all locations with item counts
|
||||||
|
router.get('/', (_req, res) => {
|
||||||
|
const rows = db.prepare(
|
||||||
|
`SELECT location, COUNT(*) as item_count
|
||||||
|
FROM items WHERE deleted_at IS NULL AND location != ''
|
||||||
|
GROUP BY location ORDER BY location`
|
||||||
|
).all() as { location: string; item_count: number }[];
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rename a location (bulk update all items)
|
||||||
|
router.put('/:name', (req, res) => {
|
||||||
|
const oldName = req.params.name;
|
||||||
|
const { name } = req.body;
|
||||||
|
if (!name || typeof name !== 'string' || !name.trim()) {
|
||||||
|
res.status(400).json({ error: 'New name is required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = db.prepare(
|
||||||
|
`UPDATE items SET location = ?, updated_at = datetime('now')
|
||||||
|
WHERE location = ? AND deleted_at IS NULL`
|
||||||
|
).run(name.trim(), oldName);
|
||||||
|
res.json({ updated: result.changes });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete a location (clear from all items)
|
||||||
|
router.delete('/:name', (req, res) => {
|
||||||
|
const name = req.params.name;
|
||||||
|
const result = db.prepare(
|
||||||
|
`UPDATE items SET location = '', updated_at = datetime('now')
|
||||||
|
WHERE location = ? AND deleted_at IS NULL`
|
||||||
|
).run(name);
|
||||||
|
res.json({ updated: result.changes });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
Reference in New Issue
Block a user