diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index ec19d77..e02d22e 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -1126,3 +1126,13 @@ export const measuringtoolsApi = { } } } + +// 3D printed parts (printedparts plugin) +export const printedpartsApi = { + list(params = {}) { + return api.get('/printedparts/items', { params }) + }, + get(printeditemid) { + return api.get(`/printedparts/items/${printeditemid}`) + } +} diff --git a/frontend/src/router/routes/printedparts.js b/frontend/src/router/routes/printedparts.js index 244544c..4646e0c 100644 --- a/frontend/src/router/routes/printedparts.js +++ b/frontend/src/router/routes/printedparts.js @@ -11,25 +11,25 @@ export default [ { path: 'printedparts', name: 'printedparts', - component: () => import('../../views/printedparts/PrintedpartsList.vue'), + component: () => import('../../views/printedparts/PrintedItemsList.vue'), meta: { plugin: 'printedparts' } }, { path: 'printedparts/new', name: 'printedparts-new', - component: () => import('../../views/printedparts/PrintedpartsForm.vue'), + component: () => import('../../views/printedparts/PrintedItemForm.vue'), meta: { requiresAuth: true, plugin: 'printedparts' } }, { path: 'printedparts/:id', name: 'printedparts-detail', - component: () => import('../../views/printedparts/PrintedpartsDetail.vue'), + component: () => import('../../views/printedparts/PrintedItemDetail.vue'), meta: { plugin: 'printedparts' } }, { path: 'printedparts/:id/edit', name: 'printedparts-edit', - component: () => import('../../views/printedparts/PrintedpartsForm.vue'), + component: () => import('../../views/printedparts/PrintedItemForm.vue'), meta: { requiresAuth: true, plugin: 'printedparts' } } ] diff --git a/frontend/src/views/AppLayout.vue b/frontend/src/views/AppLayout.vue index 44e61ee..0159b3b 100644 --- a/frontend/src/views/AppLayout.vue +++ b/frontend/src/views/AppLayout.vue @@ -101,7 +101,7 @@ import ToastHost from '../components/ToastHost.vue' import { Sun, Moon, LayoutDashboard, Calendar, Map, Cog, Monitor, Printer, Globe, Usb, AppWindow, BookOpen, BarChart3, Bell, Image, ShieldCheck, Ruler, - KeyRound, LogOut + Box, KeyRound, LogOut } from 'lucide-vue-next' import { useAuthStore } from '../stores/auth' import { currentTheme, toggleTheme } from '../stores/theme' @@ -147,6 +147,7 @@ const iconMap = { 'image': Image, 'shield': ShieldCheck, 'ruler': Ruler, + 'box': Box, } // Default navigation (used as fallback if API fails) diff --git a/frontend/src/views/printedparts/PrintedpartsDetail.vue b/frontend/src/views/printedparts/PrintedItemDetail.vue similarity index 100% rename from frontend/src/views/printedparts/PrintedpartsDetail.vue rename to frontend/src/views/printedparts/PrintedItemDetail.vue diff --git a/frontend/src/views/printedparts/PrintedpartsForm.vue b/frontend/src/views/printedparts/PrintedItemForm.vue similarity index 100% rename from frontend/src/views/printedparts/PrintedpartsForm.vue rename to frontend/src/views/printedparts/PrintedItemForm.vue diff --git a/frontend/src/views/printedparts/PrintedItemsList.vue b/frontend/src/views/printedparts/PrintedItemsList.vue new file mode 100644 index 0000000..c444dc8 --- /dev/null +++ b/frontend/src/views/printedparts/PrintedItemsList.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/frontend/src/views/printedparts/PrintedpartsList.vue b/frontend/src/views/printedparts/PrintedpartsList.vue deleted file mode 100644 index f7e18d8..0000000 --- a/frontend/src/views/printedparts/PrintedpartsList.vue +++ /dev/null @@ -1,142 +0,0 @@ - - - - - diff --git a/plugins/printedparts/api/routes.py b/plugins/printedparts/api/routes.py index 73d11ff..6028e8d 100644 --- a/plugins/printedparts/api/routes.py +++ b/plugins/printedparts/api/routes.py @@ -1,18 +1,66 @@ """Printedparts plugin API routes. -Stage 2 placeholder: the blueprint must import cleanly for plugin discovery -and migrations (the alembic env imports the models package, which pulls in -plugin.py and this module). Real endpoints land in the next stage. +Reads are open (jwt optional) like every list surface; mutations arrive in +later stages with permission gates. The kiosk endpoints (unauthenticated by +explicit decision - see the proposal) also land later. """ -from flask import Blueprint +from flask import Blueprint, request +from flask_jwt_extended import jwt_required +from sqlalchemy import or_ -from shopdb.api import success_response +from shopdb.api import ( + db, + success_response, + error_response, + paginated_response, + ErrorCodes, + get_pagination_params, + paginate_query, +) + +from ..models import PrintedItem printedparts_bp = Blueprint('printedparts', __name__) -@printedparts_bp.route('/ping', methods=['GET']) -def ping(): - """Liveness probe for the lab: proves the blueprint is registered.""" - return success_response({'plugin': 'printedparts', 'status': 'ok'}) +@printedparts_bp.route('/items', methods=['GET']) +@jwt_required(optional=True) +def list_items(): + """List printed items, paginated; search + low-stock filter.""" + page, per_page = get_pagination_params(request) + query = PrintedItem.query + if request.args.get('active', 'true').lower() != 'false': + query = query.filter(PrintedItem.isactive == True) + if search := request.args.get('search'): + like = f'%{search}%' + query = query.filter(or_( + PrintedItem.itemcode.ilike(like), + PrintedItem.itemname.ilike(like), + PrintedItem.itemdescription.ilike(like), + PrintedItem.binlocation.ilike(like), + )) + if request.args.get('lowstock', '').lower() == 'true': + query = query.filter( + PrintedItem.quantityonhand <= PrintedItem.lowstockthreshold) + query = query.order_by(PrintedItem.itemname) + items, total = paginate_query(query, page, per_page) + return paginated_response( + [item.to_dict() for item in items], page, per_page, total) + + +@printedparts_bp.route('/items/', methods=['GET']) +@jwt_required(optional=True) +def get_item(item_id: int): + """Get one printed item with its recent transactions.""" + item = db.session.get(PrintedItem, item_id) + if not item: + return error_response(ErrorCodes.NOT_FOUND, + f'Printed item {item_id} not found', + http_code=404) + data = item.to_dict() + recent = (item.transactions + .order_by(db.desc('transactiondate')) + .limit(25).all()) + data['recenttransactions'] = [t.to_dict() for t in recent] + return success_response(data) diff --git a/plugins/printedparts/plugin.py b/plugins/printedparts/plugin.py index 89cf221..e62b30c 100644 --- a/plugins/printedparts/plugin.py +++ b/plugins/printedparts/plugin.py @@ -51,6 +51,16 @@ class PrintedpartsPlugin(BasePlugin): def init_app(self, app: Flask, db_instance) -> None: logger.info(f'Printedparts plugin initialized (v{self.meta.version})') + def get_navigation_items(self) -> List[dict]: + return [ + { + 'name': '3D Parts', + 'icon': 'box', + 'route': '/printedparts', + 'position': 46, + }, + ] + def on_install(self, app: Flask) -> None: with app.app_context(): self._seed_settings()