feat: add admin menu configuration and sidebar improvements

- Add AdminMenuConfig model for per-platform menu customization
- Add menu registry for centralized menu configuration
- Add my-menu-config and platform-menu-config admin pages
- Update sidebar with improved layout and Alpine.js interactions
- Add FrontendType enum for admin/vendor menu separation
- Document self-contained module patterns in session note

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 20:34:58 +01:00
parent 32e43efb3c
commit 9a828999fe
14 changed files with 2346 additions and 619 deletions

View File

@@ -0,0 +1,128 @@
"""Add admin menu configuration table
Revision ID: za0k1l2m3n4o5
Revises: z9j0k1l2m3n4
Create Date: 2026-01-25
Adds configurable admin sidebar menus:
- Platform-level config: Controls which menu items platform admins see
- User-level config: Controls which menu items super admins see
- Opt-out model: All items visible by default
- Mandatory items enforced at application level (companies, vendors, users, settings)
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "za0k1l2m3n4o5"
down_revision = "z9j0k1l2m3n4"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create admin_menu_configs table
op.create_table(
"admin_menu_configs",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column(
"platform_id",
sa.Integer(),
nullable=True,
comment="Platform scope - applies to all platform admins of this platform",
),
sa.Column(
"user_id",
sa.Integer(),
nullable=True,
comment="User scope - applies to this specific super admin",
),
sa.Column(
"menu_item_id",
sa.String(50),
nullable=False,
comment="Menu item identifier from registry (e.g., 'products', 'inventory')",
),
sa.Column(
"is_visible",
sa.Boolean(),
nullable=False,
server_default="true",
comment="Whether this menu item is visible (False = hidden)",
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
onupdate=sa.func.now(),
),
# Foreign keys
sa.ForeignKeyConstraint(
["platform_id"],
["platforms.id"],
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id"),
# Unique constraints
sa.UniqueConstraint("platform_id", "menu_item_id", name="uq_platform_menu_config"),
sa.UniqueConstraint("user_id", "menu_item_id", name="uq_user_menu_config"),
# Check constraint: exactly one scope must be set
sa.CheckConstraint(
"(platform_id IS NOT NULL AND user_id IS NULL) OR "
"(platform_id IS NULL AND user_id IS NOT NULL)",
name="ck_admin_menu_config_scope",
),
)
# Create indexes for performance
op.create_index(
"idx_admin_menu_configs_platform_id",
"admin_menu_configs",
["platform_id"],
)
op.create_index(
"idx_admin_menu_configs_user_id",
"admin_menu_configs",
["user_id"],
)
op.create_index(
"idx_admin_menu_configs_menu_item_id",
"admin_menu_configs",
["menu_item_id"],
)
op.create_index(
"idx_admin_menu_config_platform_visible",
"admin_menu_configs",
["platform_id", "is_visible"],
)
op.create_index(
"idx_admin_menu_config_user_visible",
"admin_menu_configs",
["user_id", "is_visible"],
)
def downgrade() -> None:
# Drop indexes
op.drop_index("idx_admin_menu_config_user_visible", table_name="admin_menu_configs")
op.drop_index("idx_admin_menu_config_platform_visible", table_name="admin_menu_configs")
op.drop_index("idx_admin_menu_configs_menu_item_id", table_name="admin_menu_configs")
op.drop_index("idx_admin_menu_configs_user_id", table_name="admin_menu_configs")
op.drop_index("idx_admin_menu_configs_platform_id", table_name="admin_menu_configs")
# Drop table
op.drop_table("admin_menu_configs")

View File

@@ -0,0 +1,117 @@
"""Add frontend_type to admin_menu_configs
Revision ID: zb1l2m3n4o5p6
Revises: za0k1l2m3n4o5
Create Date: 2026-01-25
Adds frontend_type column to support both admin and vendor menu configuration:
- 'admin': Admin panel menus (super admins, platform admins)
- 'vendor': Vendor dashboard menus (configured per platform)
Also updates unique constraints to include frontend_type and adds
a check constraint ensuring user_id scope is only used for admin frontend.
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "zb1l2m3n4o5p6"
down_revision = "za0k1l2m3n4o5"
branch_labels = None
depends_on = None
def upgrade() -> None:
# 1. Create the enum type for frontend_type
frontend_type_enum = sa.Enum('admin', 'vendor', name='frontendtype')
frontend_type_enum.create(op.get_bind(), checkfirst=True)
# 2. Add frontend_type column with default value
op.add_column(
"admin_menu_configs",
sa.Column(
"frontend_type",
sa.Enum('admin', 'vendor', name='frontendtype'),
nullable=False,
server_default="admin",
comment="Which frontend this config applies to (admin or vendor)",
),
)
# 3. Create index on frontend_type
op.create_index(
"idx_admin_menu_configs_frontend_type",
"admin_menu_configs",
["frontend_type"],
)
# 4. Drop old unique constraints
op.drop_constraint("uq_platform_menu_config", "admin_menu_configs", type_="unique")
op.drop_constraint("uq_user_menu_config", "admin_menu_configs", type_="unique")
# 5. Create new unique constraints that include frontend_type
op.create_unique_constraint(
"uq_frontend_platform_menu_config",
"admin_menu_configs",
["frontend_type", "platform_id", "menu_item_id"],
)
op.create_unique_constraint(
"uq_frontend_user_menu_config",
"admin_menu_configs",
["frontend_type", "user_id", "menu_item_id"],
)
# 6. Add check constraint: user_id scope only allowed for admin frontend
op.create_check_constraint(
"ck_user_scope_admin_only",
"admin_menu_configs",
"(user_id IS NULL) OR (frontend_type = 'admin')",
)
# 7. Create composite indexes for common queries
op.create_index(
"idx_admin_menu_config_frontend_platform",
"admin_menu_configs",
["frontend_type", "platform_id"],
)
op.create_index(
"idx_admin_menu_config_frontend_user",
"admin_menu_configs",
["frontend_type", "user_id"],
)
def downgrade() -> None:
# Drop new indexes
op.drop_index("idx_admin_menu_config_frontend_user", table_name="admin_menu_configs")
op.drop_index("idx_admin_menu_config_frontend_platform", table_name="admin_menu_configs")
# Drop check constraint
op.drop_constraint("ck_user_scope_admin_only", "admin_menu_configs", type_="check")
# Drop new unique constraints
op.drop_constraint("uq_frontend_user_menu_config", "admin_menu_configs", type_="unique")
op.drop_constraint("uq_frontend_platform_menu_config", "admin_menu_configs", type_="unique")
# Restore old unique constraints
op.create_unique_constraint(
"uq_platform_menu_config",
"admin_menu_configs",
["platform_id", "menu_item_id"],
)
op.create_unique_constraint(
"uq_user_menu_config",
"admin_menu_configs",
["user_id", "menu_item_id"],
)
# Drop frontend_type index
op.drop_index("idx_admin_menu_configs_frontend_type", table_name="admin_menu_configs")
# Drop frontend_type column
op.drop_column("admin_menu_configs", "frontend_type")
# Drop the enum type
sa.Enum('admin', 'vendor', name='frontendtype').drop(op.get_bind(), checkfirst=True)