Replace hardcoded demo data in the config page with real API calls. Branches and employees now load from /pos/api/config/* endpoints, with create modals for both. Business data tab reads tenant_config (read-only). Theme selector was already working and is preserved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
173 lines
6.4 KiB
Python
173 lines
6.4 KiB
Python
# /home/Autopartes/pos/blueprints/config_bp.py
|
|
"""Config blueprint: tenant configuration, branches, theming."""
|
|
|
|
from flask import Blueprint, request, jsonify, g
|
|
from middleware import require_auth, has_permission
|
|
from tenant_db import get_tenant_conn
|
|
|
|
config_bp = Blueprint('config', __name__, url_prefix='/pos/api/config')
|
|
|
|
|
|
@config_bp.route('/branches', methods=['GET'])
|
|
@require_auth()
|
|
def list_branches():
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT id, name, address, phone, is_active FROM branches ORDER BY id")
|
|
branches = []
|
|
for r in cur.fetchall():
|
|
branches.append({'id': r[0], 'name': r[1], 'address': r[2], 'phone': r[3], 'is_active': r[4]})
|
|
cur.close()
|
|
conn.close()
|
|
return jsonify({'data': branches})
|
|
|
|
|
|
@config_bp.route('/branches', methods=['POST'])
|
|
@require_auth('config.edit')
|
|
def create_branch():
|
|
data = request.get_json() or {}
|
|
if not data.get('name'):
|
|
return jsonify({'error': 'name required'}), 400
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
cur = conn.cursor()
|
|
cur.execute("""
|
|
INSERT INTO branches (name, address, phone)
|
|
VALUES (%s, %s, %s) RETURNING id
|
|
""", (data['name'], data.get('address'), data.get('phone')))
|
|
branch_id = cur.fetchone()[0]
|
|
conn.commit()
|
|
cur.close()
|
|
conn.close()
|
|
return jsonify({'id': branch_id, 'message': 'Branch created'}), 201
|
|
|
|
|
|
@config_bp.route('/employees', methods=['GET'])
|
|
@require_auth('config.view')
|
|
def list_employees():
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
cur = conn.cursor()
|
|
cur.execute("""
|
|
SELECT e.id, e.name, e.email, e.phone, e.role, e.branch_id,
|
|
b.name as branch_name, e.max_discount_pct, e.is_active
|
|
FROM employees e
|
|
LEFT JOIN branches b ON e.branch_id = b.id
|
|
ORDER BY e.id
|
|
""")
|
|
employees = []
|
|
for r in cur.fetchall():
|
|
employees.append({
|
|
'id': r[0], 'name': r[1], 'email': r[2], 'phone': r[3],
|
|
'role': r[4], 'branch_id': r[5], 'branch_name': r[6],
|
|
'max_discount_pct': float(r[7]) if r[7] else 0, 'is_active': r[8]
|
|
})
|
|
cur.close()
|
|
conn.close()
|
|
return jsonify({'data': employees})
|
|
|
|
|
|
@config_bp.route('/employees', methods=['POST'])
|
|
@require_auth('config.edit')
|
|
def create_employee():
|
|
import bcrypt
|
|
data = request.get_json() or {}
|
|
required = ['name', 'role', 'pin']
|
|
for f in required:
|
|
if not data.get(f):
|
|
return jsonify({'error': f'{f} required'}), 400
|
|
|
|
valid_roles = ['admin', 'cashier', 'warehouse', 'accountant']
|
|
if data['role'] not in valid_roles:
|
|
return jsonify({'error': f'role must be one of: {", ".join(valid_roles)}'}), 400
|
|
|
|
pin_hash = bcrypt.hashpw(data['pin'].encode(), bcrypt.gensalt()).decode()
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
cur = conn.cursor()
|
|
cur.execute("""
|
|
INSERT INTO employees (name, email, phone, pin, role, branch_id, max_discount_pct, is_active)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, true) RETURNING id
|
|
""", (data['name'], data.get('email'), data.get('phone'), pin_hash,
|
|
data['role'], data.get('branch_id'), data.get('max_discount_pct', 0)))
|
|
emp_id = cur.fetchone()[0]
|
|
|
|
# Set default permissions by role
|
|
role_permissions = {
|
|
'admin': ['pos.sell', 'pos.discount', 'pos.cancel', 'pos.view_cost',
|
|
'inventory.view', 'inventory.create', 'inventory.edit', 'inventory.adjust', 'inventory.transfer',
|
|
'catalog.view', 'catalog.edit',
|
|
'customers.view', 'customers.create', 'customers.edit', 'customers.edit_credit',
|
|
'invoicing.view', 'invoicing.create',
|
|
'reports.view', 'reports.financial',
|
|
'config.view', 'config.edit', 'config.edit_prices'],
|
|
'cashier': ['pos.sell', 'pos.discount', 'pos.cancel',
|
|
'catalog.view', 'customers.view', 'customers.create'],
|
|
'warehouse': ['inventory.view', 'inventory.create', 'inventory.edit',
|
|
'inventory.adjust', 'inventory.transfer', 'catalog.view'],
|
|
'accountant': ['accounting.view', 'accounting.create',
|
|
'invoicing.view', 'invoicing.create', 'invoicing.cancel',
|
|
'reports.view', 'reports.financial',
|
|
'customers.view'],
|
|
}
|
|
|
|
for perm in role_permissions.get(data['role'], []):
|
|
cur.execute(
|
|
"INSERT INTO employee_permissions (employee_id, permission) VALUES (%s, %s) ON CONFLICT DO NOTHING",
|
|
(emp_id, perm)
|
|
)
|
|
|
|
from services.audit import log_action
|
|
log_action(conn, 'EMPLOYEE_CREATE', 'employee', emp_id,
|
|
new_value={'name': data['name'], 'role': data['role']})
|
|
|
|
conn.commit()
|
|
cur.close()
|
|
conn.close()
|
|
return jsonify({'id': emp_id, 'message': 'Employee created'}), 201
|
|
|
|
|
|
@config_bp.route('/business', methods=['GET'])
|
|
@require_auth()
|
|
def get_business():
|
|
"""Read-only tenant business info from tenant_config."""
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT key, value FROM tenant_config WHERE key LIKE 'tenant_%'")
|
|
cfg = {}
|
|
for row in cur.fetchall():
|
|
cfg[row[0]] = row[1]
|
|
cur.close()
|
|
conn.close()
|
|
return jsonify({
|
|
'razon_social': cfg.get('tenant_razon_social', ''),
|
|
'nombre': cfg.get('tenant_nombre', cfg.get('tenant_razon_social', '')),
|
|
'rfc': cfg.get('tenant_rfc', ''),
|
|
'regimen_fiscal': cfg.get('tenant_regimen_fiscal', ''),
|
|
'direccion': cfg.get('tenant_direccion', ''),
|
|
'telefono': cfg.get('tenant_telefono', ''),
|
|
'email': cfg.get('tenant_email', ''),
|
|
})
|
|
|
|
|
|
@config_bp.route('/theme', methods=['GET'])
|
|
@require_auth()
|
|
def get_theme():
|
|
"""Get current theme for this tenant. Returns CSS variables."""
|
|
# For v1, return a default theme. The design team will add more.
|
|
return jsonify({
|
|
'theme': 'default',
|
|
'variables': {
|
|
'--color-primary': '#1a73e8',
|
|
'--color-secondary': '#5f6368',
|
|
'--color-accent': '#ff6b35',
|
|
'--color-bg': '#ffffff',
|
|
'--color-surface': '#f8f9fa',
|
|
'--color-text': '#202124',
|
|
'--color-border': '#dadce0',
|
|
'--font-display': "'Sora', sans-serif",
|
|
'--font-body': "'Plus Jakarta Sans', sans-serif",
|
|
'--font-mono': "'JetBrains Mono', monospace",
|
|
'--radius': '8px',
|
|
}
|
|
})
|