diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index a8d7826..d0de2dd 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -793,6 +793,12 @@ export const settingsApi = {
},
create(data) {
return api.post('/settings', data)
+ },
+ uploadMapBlueprint(theme, file) {
+ const form = new FormData()
+ form.append('theme', theme)
+ form.append('file', file)
+ return api.post('/settings/map-blueprint', form, { headers: { 'Content-Type': 'multipart/form-data' } })
}
}
diff --git a/frontend/src/views/settings/SystemSettings.vue b/frontend/src/views/settings/SystemSettings.vue
index c117ba1..7cd84a3 100644
--- a/frontend/src/views/settings/SystemSettings.vue
+++ b/frontend/src/views/settings/SystemSettings.vue
@@ -358,7 +358,7 @@
@@ -788,6 +796,7 @@ const computerTypes = ref([]) // ComputerType names for the dropdown
const loading = ref(true)
const saving = ref(false)
+const mapUploading = ref(false)
const testingEmail = ref(false)
const error = ref('')
const success = ref('')
@@ -908,6 +917,27 @@ async function changePcTypeMapping(pxetype, computertype) {
}
}
+async function uploadBlueprint(theme, event) {
+ const file = event.target.files[0]
+ if (!file) return
+ mapUploading.value = true
+ error.value = ''
+ success.value = ''
+ try {
+ const { data } = await settingsApi.uploadMapBlueprint(theme, file)
+ const url = data?.data?.value
+ if (theme === 'light') settings.map_blueprint_light = url
+ else settings.map_blueprint_dark = url
+ success.value = 'Blueprint uploaded'
+ setTimeout(() => { success.value = '' }, 2000)
+ } catch (e) {
+ error.value = apiError(e, 'Upload failed')
+ } finally {
+ mapUploading.value = false
+ event.target.value = ''
+ }
+}
+
async function toggleSetting(key) {
const newValue = !settings[key]
await saveSetting(key, newValue)
@@ -1319,4 +1349,7 @@ onMounted(loadSettings)
.identifier-matrix .identifier-name {
color: var(--text);
}
+.map-upload-row { display: flex; align-items: center; gap: 0.75rem; margin-top: 0.4rem; }
+.map-thumb { height: 40px; border: 1px solid var(--border); border-radius: 4px; background: #fff; }
+.map-thumb-dark { background: #222; }
diff --git a/shopdb/core/api/settings.py b/shopdb/core/api/settings.py
index 7744852..a81bf20 100644
--- a/shopdb/core/api/settings.py
+++ b/shopdb/core/api/settings.py
@@ -1,7 +1,10 @@
"""Settings API routes."""
-from flask import Blueprint, request
+import os
+
+from flask import Blueprint, request, current_app, send_from_directory
from flask_jwt_extended import jwt_required
+from werkzeug.utils import secure_filename
from shopdb.extensions import db, cache
from shopdb.core.models import Setting, AuditLog
@@ -11,6 +14,14 @@ from shopdb.utils.authz import require_permission, require_role
settings_bp = Blueprint('settings', __name__)
+# Floor-map blueprint uploads live in the instance dir and are served publicly
+# (the kiosk dashboards read them without auth).
+MAP_IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
+
+
+def _map_dir():
+ return os.path.join(current_app.instance_path, 'maps')
+
# Cache key for settings
SETTINGS_CACHE_KEY = 'system_settings'
SETTINGS_CACHE_TTL = 300 # 5 minutes
@@ -74,6 +85,48 @@ def invalidate_settings_cache():
cache.delete(SETTINGS_CACHE_KEY)
+@settings_bp.route('/map-blueprint', methods=['POST'])
+@jwt_required()
+@require_role('admin')
+def upload_map_blueprint():
+ """Upload a floor-map blueprint image and point the setting at it.
+
+ multipart/form-data: file=, theme=light|dark. Saves to the instance
+ maps dir and sets map_blueprint_ to the served URL.
+ """
+ theme = (request.form.get('theme') or '').strip().lower()
+ if theme not in ('light', 'dark'):
+ return error_response(ErrorCodes.VALIDATION_ERROR, 'theme must be light or dark')
+ upload = request.files.get('file')
+ if not upload or not upload.filename:
+ return error_response(ErrorCodes.VALIDATION_ERROR, 'No file provided')
+ ext = os.path.splitext(upload.filename)[1].lower()
+ if ext not in MAP_IMAGE_EXTENSIONS:
+ return error_response(ErrorCodes.VALIDATION_ERROR,
+ f'Unsupported image type {ext}')
+
+ os.makedirs(_map_dir(), exist_ok=True)
+ filename = secure_filename(f'blueprint-{theme}{ext}')
+ upload.save(os.path.join(_map_dir(), filename))
+
+ url = f'/api/settings/map-blueprint/{filename}'
+ key = f'map_blueprint_{theme}'
+ setting = Setting.query.filter_by(key=key).first()
+ if setting:
+ setting.value = url
+ else:
+ db.session.add(Setting(key=key, value=url, valuetype='string', category='map'))
+ db.session.commit()
+ invalidate_settings_cache()
+ return success_response({'key': key, 'value': url}, message='Blueprint uploaded')
+
+
+@settings_bp.route('/map-blueprint/', methods=['GET'])
+def serve_map_blueprint(filename):
+ """Serve an uploaded blueprint image (public - kiosks read it)."""
+ return send_from_directory(_map_dir(), filename)
+
+
@settings_bp.route('', methods=['GET'])
@jwt_required(optional=True)
def list_settings():