diff --git a/plugins/tools/__init__.py b/plugins/tools/__init__.py new file mode 100644 index 00000000..0c0692f3 --- /dev/null +++ b/plugins/tools/__init__.py @@ -0,0 +1,5 @@ +"""Tech Tools plugin: browser-side utilities for the shop floor.""" + +from .plugin import ToolsPlugin + +__all__ = ['ToolsPlugin'] diff --git a/plugins/tools/frontend/routes.js b/plugins/tools/frontend/routes.js new file mode 100644 index 00000000..aa152858 --- /dev/null +++ b/plugins/tools/frontend/routes.js @@ -0,0 +1,26 @@ +/** + * Tech Tools plugin routes. + * + * `default` = AppLayout child routes; `toplevel` = full-screen routes. + * + * The index sits inside the app shell. Each tool that PRINTS is toplevel, the + * way every other label page in this repo is: printing from inside AppLayout + * would put the sidebar and header on the label stock. + */ +export default [ + { + path: 'tools', + name: 'tools', + component: () => import('./views/ToolsIndex.vue'), + meta: { requiresAuth: true, plugin: 'tools' } + }, +] + +export const toplevel = [ + { + path: '/tools/codes', + name: 'tools-codes', + component: () => import('./views/CodeGenerator.vue'), + meta: { requiresAuth: true, plugin: 'tools' } + }, +] diff --git a/plugins/tools/frontend/tools.js b/plugins/tools/frontend/tools.js new file mode 100644 index 00000000..aa93420b --- /dev/null +++ b/plugins/tools/frontend/tools.js @@ -0,0 +1,67 @@ +// The Tech Tools catalog. +// +// This is the ONLY place a tool has to be declared. Adding one is: write the +// view, add a route in routes.js, add an entry here. The index page groups by +// category and searches over name + description + keywords. +// +// Fields: +// id - stable slug, also the key the index uses +// name - card title +// description - one line, says what it does, not how +// category - group heading on the index +// route - path to open +// standalone - true when the route lives OUTSIDE AppLayout (print pages), +// so the index opens it as a normal link rather than a +// router-link and the user keeps this tab +// keywords - extra search terms someone might type instead of the name +export const TOOLS = [ + { + id: 'code-generator', + name: 'Barcode / QR Generator', + description: 'Make QR or CODE128 labels from typed text, a URL, or a CSV, sized for your label stock.', + category: 'labels', + route: '/tools/codes', + standalone: true, + keywords: ['qr', 'barcode', 'code128', 'label', 'zebra', 'sticker', 'csv', 'print'], + }, +] + +export const CATEGORY_LABELS = { + labels: 'Labels & Printing', + convert: 'Conversion', + network: 'Network', +} + +export function categoryLabel(category) { + return CATEGORY_LABELS[category] || category +} + +// Tools matching a search string, or all of them when the box is empty. +export function searchTools(query) { + const needle = (query || '').trim().toLowerCase() + if (!needle) return TOOLS + return TOOLS.filter(tool => { + const haystack = [tool.name, tool.description, ...(tool.keywords || [])] + .join(' ') + .toLowerCase() + return haystack.includes(needle) + }) +} + +// Tools grouped for the index, categories in CATEGORY_LABELS order so the +// page does not reshuffle as tools are added. +export function groupTools(query) { + const matched = searchTools(query) + const order = Object.keys(CATEGORY_LABELS) + const seen = [...new Set(matched.map(tool => tool.category))] + seen.sort((a, b) => { + const ai = order.indexOf(a) + const bi = order.indexOf(b) + return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi) + }) + return seen.map(category => ({ + category, + label: categoryLabel(category), + tools: matched.filter(tool => tool.category === category), + })) +} diff --git a/plugins/tools/frontend/views/CodeGenerator.vue b/plugins/tools/frontend/views/CodeGenerator.vue new file mode 100644 index 00000000..3b15fca6 --- /dev/null +++ b/plugins/tools/frontend/views/CodeGenerator.vue @@ -0,0 +1,678 @@ + + + + + diff --git a/plugins/tools/frontend/views/ToolsIndex.vue b/plugins/tools/frontend/views/ToolsIndex.vue new file mode 100644 index 00000000..ec547dca --- /dev/null +++ b/plugins/tools/frontend/views/ToolsIndex.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/plugins/tools/manifest.json b/plugins/tools/manifest.json new file mode 100644 index 00000000..3e7b40a9 --- /dev/null +++ b/plugins/tools/manifest.json @@ -0,0 +1,11 @@ +{ + "name": "tools", + "version": "1.0.0", + "description": "Tech Tools: shop-floor utilities that run entirely in the browser (barcode/QR label generator, with room for more)", + "author": "ShopDB Team", + "dependencies": [], + "core_version": ">=0.16.0,<1.0.0", + "provides": { + "features": ["code_generator"] + } +} diff --git a/plugins/tools/plugin.py b/plugins/tools/plugin.py new file mode 100644 index 00000000..ae9c3610 --- /dev/null +++ b/plugins/tools/plugin.py @@ -0,0 +1,56 @@ +"""Tech Tools plugin. + +A section for small technician utilities. Every tool here runs entirely in the +browser: no API, no tables, no state. That is deliberate - these are the things +someone reaches for at a bench with no network guarantee, and an air-gapped +site gets them for free. + +The plugin exists so the section can carry a nav entry and be included in or +left out of a per-site build (ADR-013). The tools themselves are frontend-only, +which is why get_blueprint and get_models are both empty. +""" + +import logging +from typing import List, Optional, Type + +from flask import Blueprint, Flask + +from shopdb.plugins.base import BasePlugin, PluginMeta + +logger = logging.getLogger(__name__) + + +class ToolsPlugin(BasePlugin): + """Registers the Tech Tools section. All tools are client-side.""" + + @property + def meta(self) -> PluginMeta: + return PluginMeta( + name='tools', + version='1.0.0', + description='Tech Tools: browser-side technician utilities', + author='ShopDB Team', + dependencies=[], + core_version='>=0.16.0,<1.0.0', + ) + + def get_blueprint(self) -> Optional[Blueprint]: + # No API surface. The tools never leave the browser. + return None + + def get_models(self) -> List[Type]: + # No tables. Nothing here is persisted. + return [] + + def get_navigation_items(self) -> List[dict]: + return [ + { + 'name': 'Tech Tools', + 'icon': 'wrench', + 'route': '/tools', + 'position': 47, + }, + ] + + def on_install(self, app: Flask) -> None: + logger.info('Tools plugin installed')