Files
shopdb-flask/plugins/employees/plugin.py
cproudlock 56b7874f8d Self-hosted employee directory (in-app management + CSV import)
Most sites have no external HR database, so add a self-hosted directory mode.

- New employee_directory_mode setting: 'external' (default; read a separate HR
  DB, unchanged) or 'selfhosted' (app-owned table).
- DirectoryEmployee model + directoryemployees table (migration 7d16). to_dict
  emits the same keys the external contract uses (SSO/First_Name/...), so both
  modes share one response shape and the frontend is unchanged.
- Employee search / single / batch lookup branch on the mode.
- Self-hosted-only management endpoints: list, create, update, delete, and CSV
  import (upsert by SSO). Guarded so they only work in self-hosted mode.
- EmployeeDirectory.vue management page (Settings > Locations & Organization):
  table + search + pagination, add/edit/delete, CSV import (file or paste).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 08:56:12 -04:00

84 lines
3.3 KiB
Python

"""Employees plugin main class.
Blueprint-only plugin: a read-only employee directory lookup over a separate HR
database. No model (the directory is an external DB) and no AssetType. The
connection helper (employee_connection) stays core infrastructure in shopdb.api,
shared with search and the notifications shopfloor feed.
"""
import json
import logging
from pathlib import Path
from typing import List, Dict, Optional, Type
from flask import Flask, Blueprint
from shopdb.plugins.base import BasePlugin, PluginMeta
from .api import employees_bp
from .models import DirectoryEmployee
logger = logging.getLogger(__name__)
class EmployeesPlugin(BasePlugin):
"""Employees plugin - read-only HR directory lookup."""
def __init__(self):
self._manifest = self._load_manifest()
def _load_manifest(self) -> Dict:
"""Load plugin manifest from JSON file."""
manifest_path = Path(__file__).parent / 'manifest.json'
if manifest_path.exists():
with open(manifest_path, 'r') as f:
return json.load(f)
return {}
@property
def meta(self) -> PluginMeta:
"""Return plugin metadata."""
return PluginMeta(
name=self._manifest.get('name', 'employees'),
version=self._manifest.get('version', '1.0.0'),
description=self._manifest.get(
'description',
'Read-only employee directory lookup'
),
author=self._manifest.get('author', 'ShopDB Team'),
dependencies=self._manifest.get('dependencies', []),
core_version=self._manifest.get('core_version', '>=0.1.0,<1.0.0'),
api_prefix=self._manifest.get('api_prefix', '/api/employees'),
)
def get_blueprint(self) -> Optional[Blueprint]:
"""Return Flask Blueprint with API routes."""
return employees_bp
def get_models(self) -> List[Type]:
"""Self-hosted directory table (used when directory_mode=selfhosted).
External mode reads a separate DB via employee_connection instead."""
return [DirectoryEmployee]
def get_config_schema(self) -> List[Dict]:
"""Employee directory DB connection. Host/name/user are settings the
wizard can edit; the password stays in .env (emitted, not stored)."""
return [
{'key': 'employee_db_host', 'label': 'Employee DB host', 'type': 'text',
'secret': False, 'default': 'localhost',
'help': 'This DB must expose an "employees" table or view with columns '
'SSO, First_Name, Last_Name, Team, Role, Picture. '
'See plugins/employees/README.md.'},
{'key': 'employee_db_name', 'label': 'Employee DB name', 'type': 'text',
'secret': False, 'default': 'wjf_employees'},
{'key': 'employee_db_user', 'label': 'Employee DB user', 'type': 'text',
'secret': False},
{'key': 'employee_db_password', 'label': 'Employee DB password', 'type': 'password',
'secret': True, 'envvar': 'EMPLOYEE_DB_PASSWORD',
'help': 'Stored in .env, not the database. The wizard shows the line to paste.'},
]
def init_app(self, app: Flask, db_instance) -> None:
"""Initialize plugin with Flask app."""
logger.info(f"Employees plugin initialized (v{self.meta.version})")