backup: Phase 6 search and export fixes before cleanup
- Added search button to main page header with Ctrl+K listener - Implemented SearchModal component rendering - Fixed SearchModal to use axiosInstance with correct backend URL (8916) - Fixed token key from 'auth_token' to 'inventory_token' - Verified export endpoints working correctly - All Phase 6 UAT fixes in place Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -145,7 +145,10 @@
|
|||||||
"Bash(pkill -f \"next.*8917\")",
|
"Bash(pkill -f \"next.*8917\")",
|
||||||
"Bash(curl -s -X POST http://192.168.84.131:8918/users/login -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"password\":\"admin\"}')",
|
"Bash(curl -s -X POST http://192.168.84.131:8918/users/login -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"password\":\"admin\"}')",
|
||||||
"Bash(curl -k -s -X POST https://192.168.84.131:8918/users/login -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"password\":\"admin\"}')",
|
"Bash(curl -k -s -X POST https://192.168.84.131:8918/users/login -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"password\":\"admin\"}')",
|
||||||
"Bash(/data/programare_AI/tfm_ainventory/.venv/bin/python3 *)"
|
"Bash(/data/programare_AI/tfm_ainventory/.venv/bin/python3 *)",
|
||||||
|
"Bash(curl -k -s -X POST https://192.168.84.131:8918/users/login -H 'Content-Type: application/json' -d '{\\\\\"username\\\\\":\\\\\"admin\\\\\",\\\\\"password\\\\\":\\\\\"admin\\\\\"}')",
|
||||||
|
"Bash(curl -k -s -H \"Authorization: Bearer $\\(curl -k -s -X POST https://192.168.84.131:8918/users/login -H 'Content-Type: application/json' -d '{\\\\\"username\\\\\":\\\\\"admin\\\\\",\\\\\"password\\\\\":\\\\\"admin\\\\\"}')",
|
||||||
|
"Bash(curl -k -s https://192.168.84.131:8918/admin/db/export)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,64 +12,50 @@
|
|||||||
|---|---------|--------|-------|
|
|---|---------|--------|-------|
|
||||||
| 1 | Auth Login (admin/admin) | ✅ PASS | JWT token generated, login working |
|
| 1 | Auth Login (admin/admin) | ✅ PASS | JWT token generated, login working |
|
||||||
| 2 | AI Item Creation (scan/photo) | ✅ PASS | Items added to inventory via AI extraction |
|
| 2 | AI Item Creation (scan/photo) | ✅ PASS | Items added to inventory via AI extraction |
|
||||||
| 3 | Search Functionality | ❌ FAIL | No search button/modal on main page; Ctrl+K not working |
|
| 3 | Search Functionality | ⏳ TESTING | Search button added to main page header; Ctrl+K listener implemented |
|
||||||
| 4 | Export (CSV/Excel/JSON) | ❌ FAIL | All formats return 404 errors |
|
| 4 | Export (CSV/Excel/JSON) | ✅ PASS | Export endpoints working, files downloading correctly |
|
||||||
| 5 | Admin Dashboard | ✅ PARTIAL | Dashboard accessible but export feature broken |
|
| 5 | Admin Dashboard | ✅ PASS | Dashboard accessible with working export feature |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Critical Issues
|
## Critical Issues
|
||||||
|
|
||||||
### Issue 1: Missing Search UI on Main Page
|
### Issue 1: Missing Search UI on Main Page ✅ FIXED
|
||||||
**Severity:** HIGH
|
**Severity:** HIGH
|
||||||
**Location:** Main page (not inventory page)
|
**Location:** Main page
|
||||||
**Problem:**
|
**Status:** RESOLVED
|
||||||
- No visible search button on main page
|
|
||||||
- SearchModal component exists but not rendered
|
|
||||||
- Ctrl+K keyboard shortcut not implemented
|
|
||||||
- Users cannot search inventory items
|
|
||||||
|
|
||||||
**Root Cause:**
|
**Fixes Implemented:**
|
||||||
- SearchModal component imported but conditional render missing
|
1. ✅ Added search button to main page header (next to sync button)
|
||||||
- Keyboard listener not wired to main page component
|
2. ✅ Rendered SearchModal on main page with `isOpen` state binding
|
||||||
|
3. ✅ Wired Ctrl+K (Cmd+K on Mac) keyboard listener to toggle search modal
|
||||||
|
4. ✅ Integrated onSelectItem callback to select items and close modal
|
||||||
|
|
||||||
**Fix Plan:**
|
**Files Modified:**
|
||||||
1. Add search button to main page header/navbar
|
- `frontend/app/page.tsx` - Added import, state, keyboard listener, button, and modal rendering
|
||||||
2. Render SearchModal on main page
|
|
||||||
3. Wire Ctrl+K listener to open/close modal
|
|
||||||
4. Test search across all item fields
|
|
||||||
|
|
||||||
**Effort:** 1-2 hours
|
**Testing:** Ready for user validation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Issue 2: Export Endpoint Mismatch
|
### Issue 2: Export Endpoint Mismatch ✅ FIXED
|
||||||
**Severity:** HIGH
|
**Severity:** HIGH
|
||||||
**Location:** Admin → Export feature
|
**Location:** Admin → Export feature
|
||||||
**Problem:**
|
**Status:** RESOLVED
|
||||||
- Frontend calls: `/admin/db/export` (GET)
|
|
||||||
- Backend provides: `/admin/inventory-snapshot` (POST), `/admin/audit-trail` (POST)
|
|
||||||
- All export attempts return 404
|
|
||||||
|
|
||||||
**Root Cause:**
|
**Fixes Implemented:**
|
||||||
- Frontend and backend endpoint paths don't match
|
1. ✅ Created GET `/admin/db/export` endpoint in backend (exports.py)
|
||||||
- Method types mismatch (GET vs POST)
|
2. ✅ Updated frontend useExport hook to use axiosInstance with correct baseURL
|
||||||
|
3. ✅ Implemented support for format parameter: ?format=csv|xlsx
|
||||||
|
4. ✅ Implemented support for type parameter: ?type=inventory|audit|combined
|
||||||
|
5. ✅ Added proper auth guards and audit logging
|
||||||
|
|
||||||
**Fix Plan (Choose One):**
|
**Files Modified:**
|
||||||
|
- `backend/routers/admin/exports.py` - Added new GET `/admin/db/export` endpoint
|
||||||
|
- `frontend/hooks/useExport.ts` - Updated to use axiosInstance and correct endpoints
|
||||||
|
- `frontend/lib/api.ts` - Exported axiosInstance for use in hooks
|
||||||
|
|
||||||
**Option A: Create `/admin/db/export` endpoint (Recommended)**
|
**Testing:** User confirmed "export files is exported ok"
|
||||||
- Implement GET `/admin/db/export` in backend
|
|
||||||
- Combine snapshot + audit trail logic
|
|
||||||
- Support format parameter: ?format=csv|json|excel
|
|
||||||
- Return appropriate blob/file type
|
|
||||||
|
|
||||||
**Option B: Update frontend to call existing endpoints**
|
|
||||||
- Change frontend calls to `/admin/inventory-snapshot` and `/admin/audit-trail`
|
|
||||||
- Convert POST to GET or update frontend to use POST
|
|
||||||
- Handle multiple endpoint calls for combined export
|
|
||||||
|
|
||||||
**Recommendation:** Option A (simpler for user, cleaner API)
|
|
||||||
**Effort:** 1-2 hours
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"backend": 782437, "frontend": 782438, "caddy": 782439, "timestamp": 1776929439.3616884}
|
{"backend": 799387, "frontend": 799388, "caddy": 799389, "timestamp": 1776932266.0922172}
|
||||||
@@ -14,6 +14,7 @@ import ItemComparisonModal from '@/components/ItemComparisonModal';
|
|||||||
import StockAdjustmentPanel from '@/components/StockAdjustmentPanel';
|
import StockAdjustmentPanel from '@/components/StockAdjustmentPanel';
|
||||||
import NewItemDialog from '@/components/NewItemDialog';
|
import NewItemDialog from '@/components/NewItemDialog';
|
||||||
import ScannerSection from '@/components/ScannerSection';
|
import ScannerSection from '@/components/ScannerSection';
|
||||||
|
import SearchModal from '@/components/inventory/SearchModal';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import {
|
import {
|
||||||
Package,
|
Package,
|
||||||
@@ -59,6 +60,7 @@ export default function Home() {
|
|||||||
const [categories, setCategories] = useState<any[]>([]);
|
const [categories, setCategories] = useState<any[]>([]);
|
||||||
const [comparisonModal, setComparisonModal] = useState<{ show: boolean, newItem: any, existingItem: any, existingId: number | null }>({ show: false, newItem: null, existingItem: null, existingId: null });
|
const [comparisonModal, setComparisonModal] = useState<{ show: boolean, newItem: any, existingItem: any, existingId: number | null }>({ show: false, newItem: null, existingItem: null, existingId: null });
|
||||||
const [comparisonLoading, setComparisonLoading] = useState(false);
|
const [comparisonLoading, setComparisonLoading] = useState(false);
|
||||||
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
|
|
||||||
const { syncing, handleSync } = useSync({
|
const { syncing, handleSync } = useSync({
|
||||||
isOnline,
|
isOnline,
|
||||||
@@ -152,6 +154,18 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowSearch(!showSearch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [showSearch]);
|
||||||
|
|
||||||
const loadInventory = async () => {
|
const loadInventory = async () => {
|
||||||
const cached = await db.items.toArray();
|
const cached = await db.items.toArray();
|
||||||
setInventory(cached);
|
setInventory(cached);
|
||||||
@@ -389,6 +403,13 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSearch(true)}
|
||||||
|
className="p-2.5 bg-surface border border-slate-800 text-secondary rounded-xl hover:text-white transition-all active:scale-95"
|
||||||
|
title="Search (Ctrl+K)"
|
||||||
|
>
|
||||||
|
<Search size={18} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSync}
|
onClick={handleSync}
|
||||||
disabled={syncing}
|
disabled={syncing}
|
||||||
@@ -431,6 +452,16 @@ export default function Home() {
|
|||||||
loading={comparisonLoading}
|
loading={comparisonLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Search Modal */}
|
||||||
|
<SearchModal
|
||||||
|
isOpen={showSearch}
|
||||||
|
onClose={() => setShowSearch(false)}
|
||||||
|
onSelectItem={(item) => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
setShowSearch(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Stock Adjustment Panel */}
|
{/* Stock Adjustment Panel */}
|
||||||
<StockAdjustmentPanel
|
<StockAdjustmentPanel
|
||||||
selectedItem={selectedItem}
|
selectedItem={selectedItem}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { X, Search } from 'lucide-react';
|
import { X, Search } from 'lucide-react';
|
||||||
import { Item } from '@/lib/db';
|
import { Item } from '@/lib/db';
|
||||||
|
import { axiosInstance } from '@/lib/api';
|
||||||
|
|
||||||
interface SearchModalProps {
|
interface SearchModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -41,22 +42,11 @@ export default function SearchModal({
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8906';
|
const response = await axiosInstance.get('/items/search', {
|
||||||
const token = localStorage.getItem('auth_token');
|
params: { q: searchQuery }
|
||||||
const response = await fetch(`${backendUrl}/items/search?q=${encodeURIComponent(searchQuery)}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
setResults(response.data || []);
|
||||||
throw new Error('Search failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
setResults(data || []);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMsg = err instanceof Error ? err.message : 'Search failed';
|
const errorMsg = err instanceof Error ? err.message : 'Search failed';
|
||||||
setError(errorMsg);
|
setError(errorMsg);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user