mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 12:50:11 -05:00
feat(admin): restore admin-defined module visibility
- Add settings.disabled_module_ids (JSON) to store admin-disabled module IDs - Migration 110: add disabled_module_ids column to settings - ModuleRegistry.is_enabled() respects settings.disabled_module_ids - Admin > System Settings: new 'Module visibility' section with toggles for all non-core modules; disabled modules are hidden from all users. - Core modules stay always on; default empty list keeps current behavior.
This commit is contained in:
@@ -55,6 +55,9 @@ class Settings(db.Model):
|
||||
# Privacy and analytics settings
|
||||
allow_analytics = db.Column(db.Boolean, default=True, nullable=False) # Controls system info sharing for analytics
|
||||
|
||||
# Module visibility: admin-disabled module IDs (e.g. ["gantt", "leads"]). Empty/None = all enabled.
|
||||
disabled_module_ids = db.Column(db.JSON, default=list, nullable=True)
|
||||
|
||||
# Kiosk mode settings
|
||||
kiosk_mode_enabled = db.Column(db.Boolean, default=False, nullable=False)
|
||||
kiosk_auto_logout_minutes = db.Column(db.Integer, default=15, nullable=False)
|
||||
@@ -374,6 +377,7 @@ class Settings(db.Model):
|
||||
"invoice_pdf_template_css": self.invoice_pdf_template_css,
|
||||
"invoice_pdf_design_json": self.invoice_pdf_design_json,
|
||||
"allow_analytics": self.allow_analytics,
|
||||
"disabled_module_ids": (self.disabled_module_ids if self.disabled_module_ids is not None else []),
|
||||
"mail_enabled": self.mail_enabled,
|
||||
"mail_server": self.mail_server,
|
||||
"mail_port": self.mail_port,
|
||||
|
||||
@@ -949,6 +949,16 @@ def settings():
|
||||
"kiosk_default_movement_type": getattr(settings_obj, "kiosk_default_movement_type", "adjustment"),
|
||||
}
|
||||
|
||||
# Module visibility: non-CORE modules for admin toggles
|
||||
from app.utils.module_registry import ModuleRegistry, ModuleCategory
|
||||
|
||||
ModuleRegistry.initialize_defaults()
|
||||
modules_by_category = {}
|
||||
for cat in ModuleCategory:
|
||||
mods = [m for m in ModuleRegistry.get_by_category(cat) if m.category != ModuleCategory.CORE]
|
||||
if mods:
|
||||
modules_by_category[cat] = mods
|
||||
|
||||
if request.method == "POST":
|
||||
# Validate timezone
|
||||
timezone = request.form.get("timezone") or settings_obj.timezone
|
||||
@@ -964,6 +974,8 @@ def settings():
|
||||
timezones=timezones,
|
||||
kiosk_settings=kiosk_settings,
|
||||
peppol_env_enabled=peppol_env_enabled,
|
||||
modules_by_category=modules_by_category,
|
||||
ModuleCategory=ModuleCategory,
|
||||
)
|
||||
|
||||
# Update basic settings
|
||||
@@ -1059,6 +1071,15 @@ def settings():
|
||||
app_module.log_event("admin.analytics_toggled", user_id=current_user.id, new_state=allow_analytics)
|
||||
app_module.track_event(current_user.id, "admin.analytics_toggled", {"enabled": allow_analytics})
|
||||
|
||||
# Module visibility: build disabled_module_ids from unchecked module_enabled_* checkboxes
|
||||
if hasattr(settings_obj, "disabled_module_ids"):
|
||||
disabled = []
|
||||
for mods in modules_by_category.values():
|
||||
for m in mods:
|
||||
if ("module_enabled_" + m.id) not in request.form:
|
||||
disabled.append(m.id)
|
||||
settings_obj.disabled_module_ids = disabled
|
||||
|
||||
# Ensure settings object is in the session (important for new instances)
|
||||
if settings_obj not in db.session:
|
||||
db.session.add(settings_obj)
|
||||
@@ -1071,6 +1092,8 @@ def settings():
|
||||
timezones=timezones,
|
||||
kiosk_settings=kiosk_settings,
|
||||
peppol_env_enabled=peppol_env_enabled,
|
||||
modules_by_category=modules_by_category,
|
||||
ModuleCategory=ModuleCategory,
|
||||
)
|
||||
# #region agent log
|
||||
try:
|
||||
@@ -1099,6 +1122,8 @@ def settings():
|
||||
timezones=timezones,
|
||||
kiosk_settings=kiosk_settings,
|
||||
peppol_env_enabled=peppol_env_enabled,
|
||||
modules_by_category=modules_by_category,
|
||||
ModuleCategory=ModuleCategory,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -263,6 +263,49 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Module visibility -->
|
||||
{% if modules_by_category is defined and modules_by_category %}
|
||||
<div class="border-b border-border-light dark:border-border-dark pb-6">
|
||||
<h2 class="text-lg font-semibold mb-4">{{ _('Module Visibility') }}</h2>
|
||||
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mb-4">
|
||||
{{ _('Disable modules to hide them from all users. Disabled modules will not appear in the sidebar. Core modules (Dashboard, Projects, Timer, etc.) are always enabled.') }}
|
||||
</p>
|
||||
{% set _d = (settings.disabled_module_ids or []) %}
|
||||
<div class="space-y-6">
|
||||
{% for category, modules in modules_by_category.items() %}
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark mb-3">
|
||||
{% if category == ModuleCategory.TIME_TRACKING %}{{ _('Time Tracking') }}
|
||||
{% elif category == ModuleCategory.PROJECT_MANAGEMENT %}{{ _('Project Management') }}
|
||||
{% elif category == ModuleCategory.CRM %}{{ _('CRM') }}
|
||||
{% elif category == ModuleCategory.FINANCE %}{{ _('Finance & Expenses') }}
|
||||
{% elif category == ModuleCategory.INVENTORY %}{{ _('Inventory') }}
|
||||
{% elif category == ModuleCategory.ANALYTICS %}{{ _('Analytics') }}
|
||||
{% elif category == ModuleCategory.TOOLS %}{{ _('Tools & Data') }}
|
||||
{% elif category == ModuleCategory.ADVANCED %}{{ _('Advanced') }}
|
||||
{% else %}{{ category.value|title }}{% endif %}
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{% for module in modules %}
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" name="module_enabled_{{ module.id }}" id="module_enabled_{{ module.id }}"
|
||||
{% if module.id not in _d %}checked{% endif %}
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<label for="module_enabled_{{ module.id }}" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">
|
||||
{{ module.name }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-text-muted-light dark:text-text-muted-dark">
|
||||
{{ _('Disabling "Reports" hides the whole Reports submenu including Report Builder and Scheduled Reports.') }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Analytics Settings -->
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-4">{{ _('Privacy & Analytics') }}</h2>
|
||||
|
||||
@@ -107,7 +107,13 @@ class ModuleRegistry:
|
||||
for dep_id in module.dependencies:
|
||||
if not cls.is_enabled(dep_id, settings, user):
|
||||
return False
|
||||
|
||||
|
||||
# Admin-disabled modules (settings.disabled_module_ids)
|
||||
if settings:
|
||||
disabled = getattr(settings, "disabled_module_ids", None) or []
|
||||
if isinstance(disabled, list) and module_id in disabled:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Add disabled_module_ids to settings for admin module visibility control
|
||||
|
||||
Revision ID: 110_add_disabled_module_ids
|
||||
Revises: 109_add_pdf_template_date_format
|
||||
Create Date: 2025-01-30
|
||||
|
||||
Admin can disable modules system-wide via settings.disabled_module_ids (JSON array).
|
||||
Empty or NULL means no modules are disabled (all enabled).
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
|
||||
revision = "110_add_disabled_module_ids"
|
||||
down_revision = "109_add_pdf_template_date_format"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
"""Add disabled_module_ids (JSON/JSONB) to settings table."""
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
if "settings" not in inspector.get_table_names():
|
||||
return
|
||||
|
||||
settings_cols = {c["name"] for c in inspector.get_columns("settings")}
|
||||
if "disabled_module_ids" in settings_cols:
|
||||
return
|
||||
|
||||
# Use JSON for SQLite/MySQL, JSONB for PostgreSQL
|
||||
is_pg = bind.dialect.name == "postgresql"
|
||||
col_type = JSONB() if is_pg else sa.JSON()
|
||||
|
||||
op.add_column(
|
||||
"settings",
|
||||
sa.Column("disabled_module_ids", col_type, nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
"""Remove disabled_module_ids from settings table."""
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
is_sqlite = bind.dialect.name == "sqlite"
|
||||
|
||||
if "settings" not in inspector.get_table_names():
|
||||
return
|
||||
|
||||
settings_cols = {c["name"] for c in inspector.get_columns("settings")}
|
||||
if "disabled_module_ids" not in settings_cols:
|
||||
return
|
||||
|
||||
if is_sqlite:
|
||||
with op.batch_alter_table("settings", schema=None) as batch_op:
|
||||
batch_op.drop_column("disabled_module_ids")
|
||||
else:
|
||||
op.drop_column("settings", "disabled_module_ids")
|
||||
Reference in New Issue
Block a user