refactor: migrate templates and static files to self-contained modules

Templates Migration:
- Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.)
- Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.)
- Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms)
- Migrate public templates to modules (billing, marketplace, cms)
- Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/)
- Migrate letzshop partials to marketplace module

Static Files Migration:
- Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file)
- Migrate vendor JS to modules: tenancy (4 files), core (2 files)
- Migrate shared JS: vendor-selector.js to core, media-picker.js to cms
- Migrate storefront JS: storefront-layout.js to core
- Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/)
- Update all template references to use module_static paths

Naming Consistency:
- Rename static/platform/ to static/public/
- Rename app/templates/platform/ to app/templates/public/
- Update all extends and static references

Documentation:
- Update module-system.md with shared templates documentation
- Update frontend-structure.md with new module JS organization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 14:34:16 +01:00
parent 843703258f
commit 4e28d91a78
542 changed files with 11603 additions and 9037 deletions

View File

@@ -0,0 +1,238 @@
{# app/templates/admin/admin-user-detail.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/headers.html' import detail_page_header %}
{% block title %}Admin User Details{% endblock %}
{% block alpine_data %}adminUserDetailPage(){% endblock %}
{% block content %}
{% call detail_page_header("adminUser?.full_name || adminUser?.username || 'Admin User Details'", '/admin/admin-users', subtitle_show='adminUser') %}
@<span x-text="adminUser?.username"></span>
<span class="text-gray-400 mx-2">|</span>
<span x-text="adminUser?.email"></span>
{% endcall %}
{{ loading_state('Loading admin user details...') }}
{{ error_state('Error loading admin user') }}
<!-- Admin User Details -->
<div x-show="!loading && adminUser">
<!-- Quick Actions Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Quick Actions
</h3>
<div class="flex flex-wrap items-center gap-3">
<a
:href="`/admin/admin-users/${userId}/edit`"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
<span x-html="$icon('edit', 'w-4 h-4 mr-2')"></span>
Edit Admin User
</a>
<button
@click="toggleStatus()"
:disabled="saving || adminUser?.id === currentUserId"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 border border-transparent rounded-lg focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
:class="adminUser?.is_active ? 'bg-orange-600 hover:bg-orange-700' : 'bg-green-600 hover:bg-green-700'"
:title="adminUser?.id === currentUserId ? 'Cannot deactivate yourself' : ''">
<span x-html="$icon(adminUser?.is_active ? 'user-x' : 'user-check', 'w-4 h-4 mr-2')"></span>
<span x-text="adminUser?.is_active ? 'Deactivate' : 'Activate'"></span>
</button>
<button
@click="deleteAdminUser()"
:disabled="saving || adminUser?.id === currentUserId"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:shadow-outline-red disabled:opacity-50 disabled:cursor-not-allowed"
:title="adminUser?.id === currentUserId ? 'Cannot delete yourself' : 'Delete admin user'">
<span x-html="$icon('delete', 'w-4 h-4 mr-2')"></span>
Delete Admin User
</button>
</div>
</div>
<!-- Status Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-4">
<!-- Admin Type -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 rounded-full"
:class="adminUser?.is_super_admin
? 'text-orange-500 bg-orange-100 dark:text-orange-100 dark:bg-orange-500'
: 'text-purple-500 bg-purple-100 dark:text-purple-100 dark:bg-purple-500'">
<span x-html="$icon(adminUser?.is_super_admin ? 'star' : 'shield', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Admin Type
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="adminUser?.is_super_admin ? 'Super Admin' : 'Platform Admin'">
-
</p>
</div>
</div>
<!-- Active Status -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 rounded-full"
:class="adminUser?.is_active ? 'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500' : 'text-red-500 bg-red-100 dark:text-red-100 dark:bg-red-500'">
<span x-html="$icon(adminUser?.is_active ? 'check-circle' : 'x-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Status
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="adminUser?.is_active ? 'Active' : 'Inactive'">
-
</p>
</div>
</div>
<!-- Platforms Access -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('globe-alt', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Platforms
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="adminUser?.is_super_admin ? 'All' : ((adminUser?.platforms || []).length || 0)">
0
</p>
</div>
</div>
<!-- Created Date -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('calendar', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Created
</p>
<p class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="formatDate(adminUser?.created_at)">
-
</p>
</div>
</div>
</div>
<!-- Main Info Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Account Information -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Account Information
</h3>
<div class="space-y-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Username</p>
<p class="text-sm text-gray-700 dark:text-gray-300">@<span x-text="adminUser?.username || '-'"></span></p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Email</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="adminUser?.email || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Email Verified</p>
<span
class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full"
:class="adminUser?.is_email_verified ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100'"
x-text="adminUser?.is_email_verified ? 'Verified' : 'Not Verified'">
</span>
</div>
</div>
</div>
<!-- Personal Information -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Personal Information
</h3>
<div class="space-y-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Full Name</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="adminUser?.full_name || 'Not provided'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">First Name</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="adminUser?.first_name || 'Not provided'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Last Name</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="adminUser?.last_name || 'Not provided'">-</p>
</div>
</div>
</div>
</div>
<!-- Platform Access (for platform admins) -->
<template x-if="!adminUser?.is_super_admin">
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Platform Access
</h3>
<template x-if="(adminUser?.platforms || []).length === 0">
<p class="text-sm text-gray-500 dark:text-gray-400">No platforms assigned. This admin cannot access any platform.</p>
</template>
<template x-if="(adminUser?.platforms || []).length > 0">
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<template x-for="platform in (adminUser?.platforms || [])" :key="platform.id">
<div class="flex items-center p-3 bg-gray-50 rounded-lg dark:bg-gray-700">
<div class="p-2 mr-3 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-600">
<span x-html="$icon('globe-alt', 'w-4 h-4')"></span>
</div>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-200" x-text="platform.name"></p>
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="platform.code"></p>
</div>
</div>
</template>
</div>
</template>
</div>
</template>
<!-- Super Admin Notice -->
<template x-if="adminUser?.is_super_admin">
<div class="px-4 py-3 mb-8 bg-orange-50 dark:bg-orange-900/20 rounded-lg border border-orange-200 dark:border-orange-800">
<div class="flex items-center">
<span x-html="$icon('star', 'w-5 h-5 text-orange-500 mr-3')"></span>
<div>
<h4 class="text-sm font-medium text-orange-800 dark:text-orange-300">Super Admin Access</h4>
<p class="text-sm text-orange-600 dark:text-orange-400">
This user has full access to all platforms and can manage other admin users.
</p>
</div>
</div>
</div>
</template>
<!-- Activity Information -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Activity Information
</h3>
<div class="grid gap-6 md:grid-cols-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Last Login</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="adminUser?.last_login ? formatDate(adminUser.last_login) : 'Never'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Created At</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="formatDate(adminUser?.created_at)">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Last Updated</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="formatDate(adminUser?.updated_at)">-</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/admin-user-detail.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,272 @@
{# app/templates/admin/admin-user-edit.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import loading_state %}
{% from 'shared/macros/headers.html' import edit_page_header %}
{% from 'shared/macros/modals.html' import confirm_modal_dynamic %}
{% block title %}Edit Admin User{% endblock %}
{% block alpine_data %}adminUserEditPage(){% endblock %}
{% block content %}
{% call edit_page_header('Edit Admin User', '/admin/admin-users', subtitle_show='adminUser', back_label='Back to Admin Users') %}
@<span x-text="adminUser?.username"></span>
{% endcall %}
{{ loading_state('Loading admin user...', show_condition='loading') }}
<!-- Edit Content -->
<div x-show="!loading && adminUser">
<!-- Quick Actions Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Quick Actions
</h3>
<div class="flex flex-wrap items-center gap-3">
<!-- Toggle Active Status -->
<button
@click="toggleStatus()"
:disabled="saving || adminUser?.id === currentUserId"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none disabled:opacity-50"
:class="adminUser?.is_active ? 'bg-orange-600 hover:bg-orange-700' : 'bg-green-600 hover:bg-green-700'"
:title="adminUser?.id === currentUserId ? 'Cannot deactivate yourself' : ''">
<span x-html="$icon(adminUser?.is_active ? 'user-x' : 'user-check', 'w-4 h-4 mr-2')"></span>
<span x-text="adminUser?.is_active ? 'Deactivate' : 'Activate'"></span>
</button>
<!-- Toggle Super Admin -->
<button
@click="toggleSuperAdmin()"
:disabled="saving || (adminUser?.id === currentUserId && adminUser?.is_super_admin)"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none disabled:opacity-50"
:class="adminUser?.is_super_admin ? 'bg-yellow-600 hover:bg-yellow-700' : 'bg-purple-600 hover:bg-purple-700'"
:title="adminUser?.id === currentUserId && adminUser?.is_super_admin ? 'Cannot demote yourself' : ''">
<span x-html="$icon(adminUser?.is_super_admin ? 'shield-x' : 'shield-check', 'w-4 h-4 mr-2')"></span>
<span x-text="adminUser?.is_super_admin ? 'Demote from Super Admin' : 'Promote to Super Admin'"></span>
</button>
<!-- Status Badges -->
<div class="ml-auto flex items-center gap-2">
<span
x-show="adminUser?.is_super_admin"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-yellow-700 bg-yellow-100 rounded-full dark:bg-yellow-700 dark:text-yellow-100">
Super Admin
</span>
<span
x-show="!adminUser?.is_super_admin"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-purple-700 bg-purple-100 rounded-full dark:bg-purple-700 dark:text-purple-100">
Platform Admin
</span>
<span
x-show="adminUser?.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
Active
</span>
<span
x-show="!adminUser?.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-red-700 bg-red-100 rounded-full dark:bg-red-700 dark:text-red-100">
Inactive
</span>
</div>
</div>
</div>
<!-- Admin Info Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Admin Information
</h3>
<div class="grid gap-6 md:grid-cols-2">
<!-- Left Column -->
<div class="space-y-4">
<div>
<span class="text-sm text-gray-600 dark:text-gray-400">User ID</span>
<p class="text-gray-800 dark:text-gray-200 font-medium" x-text="adminUser?.id"></p>
</div>
<div>
<span class="text-sm text-gray-600 dark:text-gray-400">Username</span>
<p class="text-gray-800 dark:text-gray-200 font-medium" x-text="adminUser?.username"></p>
</div>
<div>
<span class="text-sm text-gray-600 dark:text-gray-400">Email</span>
<p class="text-gray-800 dark:text-gray-200 font-medium" x-text="adminUser?.email"></p>
</div>
</div>
<!-- Right Column -->
<div class="space-y-4">
<div>
<span class="text-sm text-gray-600 dark:text-gray-400">First Name</span>
<p class="text-gray-800 dark:text-gray-200 font-medium" x-text="adminUser?.first_name || '-'"></p>
</div>
<div>
<span class="text-sm text-gray-600 dark:text-gray-400">Last Name</span>
<p class="text-gray-800 dark:text-gray-200 font-medium" x-text="adminUser?.last_name || '-'"></p>
</div>
</div>
</div>
</div>
<!-- Platform Assignments Card (Only for Platform Admins) -->
<template x-if="!adminUser?.is_super_admin">
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
Platform Assignments
</h3>
<button
@click="openPlatformModal()"
:disabled="saving || availablePlatformsForAssignment.length === 0"
class="flex items-center px-3 py-1.5 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 focus:outline-none disabled:opacity-50">
<span x-html="$icon('plus', 'w-4 h-4 mr-1')"></span>
Add Platform
</button>
</div>
<!-- Assigned Platforms List -->
<div x-show="adminUser?.platforms?.length > 0" class="space-y-2">
<template x-for="platform in adminUser?.platforms" :key="platform.id">
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg dark:bg-gray-700">
<div class="flex items-center">
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center mr-3">
<span class="text-lg font-bold text-purple-600 dark:text-purple-400" x-text="platform.code.charAt(0).toUpperCase()"></span>
</div>
<div>
<p class="font-medium text-gray-800 dark:text-gray-200" x-text="platform.name"></p>
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="platform.code"></p>
</div>
</div>
<button
@click="removePlatform(platform.id)"
:disabled="saving || adminUser?.platforms?.length <= 1"
class="p-2 text-red-600 hover:bg-red-100 dark:hover:bg-red-900/20 rounded-lg transition-colors disabled:opacity-50"
:title="adminUser?.platforms?.length <= 1 ? 'Must have at least one platform' : 'Remove platform'">
<span x-html="$icon('x', 'w-5 h-5')"></span>
</button>
</div>
</template>
</div>
<!-- No Platforms Message -->
<div x-show="!adminUser?.platforms?.length" class="text-center py-6">
<span x-html="$icon('exclamation', 'mx-auto h-12 w-12 text-gray-400')"></span>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">No platforms assigned</p>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
<span x-html="$icon('information-circle', 'w-4 h-4 inline mr-1')"></span>
Platform admins must be assigned to at least one platform.
</p>
</div>
</template>
<!-- Super Admin Notice -->
<template x-if="adminUser?.is_super_admin">
<div class="px-4 py-3 mb-6 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
<div class="flex items-center">
<span x-html="$icon('shield-check', 'w-6 h-6 text-yellow-600 dark:text-yellow-400 mr-3')"></span>
<div>
<h4 class="font-medium text-yellow-800 dark:text-yellow-200">Super Admin Access</h4>
<p class="text-sm text-yellow-700 dark:text-yellow-300">This user has full access to all platforms and administrative functions.</p>
</div>
</div>
</div>
</template>
<!-- Danger Zone Card -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border border-red-200 dark:border-red-800">
<h3 class="mb-4 text-lg font-semibold text-red-700 dark:text-red-400">
Danger Zone
</h3>
<div class="flex flex-wrap items-center gap-3">
<!-- Delete Admin User Button -->
<button
@click="deleteAdminUser()"
:disabled="saving || adminUser?.id === currentUserId"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-150 bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:shadow-outline-red disabled:opacity-50"
:title="adminUser?.id === currentUserId ? 'Cannot delete yourself' : 'Delete this admin user'">
<span x-html="$icon('delete', 'w-4 h-4 mr-2')"></span>
Delete Admin User
</button>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
<span x-html="$icon('exclamation', 'w-4 h-4 inline mr-1 text-red-500')"></span>
Deleting an admin user is permanent and cannot be undone.
</p>
</div>
</div>
<!-- Platform Assignment Modal -->
<div
x-show="showPlatformModal"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
@click.self="showPlatformModal = false"
@keydown.escape.window="showPlatformModal = false">
<div class="w-full max-w-md p-6 bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
Assign Platform
</h3>
<button
@click="showPlatformModal = false"
class="p-1 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<span x-html="$icon('x', 'w-5 h-5')"></span>
</button>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Select Platform
</label>
<select
x-model="selectedPlatformId"
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select">
<option value="">Choose a platform...</option>
<template x-for="platform in availablePlatformsForAssignment" :key="platform.id">
<option :value="platform.id" x-text="`${platform.name} (${platform.code})`"></option>
</template>
</select>
</div>
<div x-show="availablePlatformsForAssignment.length === 0" class="mb-4 p-3 bg-gray-100 dark:bg-gray-700 rounded-lg">
<p class="text-sm text-gray-600 dark:text-gray-400">
All available platforms have been assigned to this admin.
</p>
</div>
<div class="flex justify-end gap-3">
<button
@click="showPlatformModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600">
Cancel
</button>
<button
@click="assignPlatform(selectedPlatformId)"
:disabled="!selectedPlatformId || saving"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 focus:outline-none disabled:opacity-50">
<span x-show="!saving">Assign Platform</span>
<span x-show="saving" class="flex items-center">
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
Assigning...
</span>
</button>
</div>
</div>
</div>
<!-- Remove Platform Confirmation Modal -->
{{ confirm_modal_dynamic(
'removePlatformModal',
'Remove Platform',
"'Are you sure you want to remove \"' + (platformToRemove?.name || '') + '\" from this admin?'",
'confirmRemovePlatform()',
'showRemovePlatformModal',
'Remove',
'Cancel',
'warning'
) }}
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/admin-user-edit.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,262 @@
{# app/templates/admin/admin-users.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/pagination.html' import pagination %}
{% from 'shared/macros/headers.html' import page_header %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
{% block title %}Admin Users{% endblock %}
{% block alpine_data %}adminUsersPage(){% endblock %}
{% block content %}
{{ page_header('Admin User Management', subtitle='Manage super admins and platform admins', action_label='Create Admin User', action_url='/admin/admin-users/create', action_icon='user-plus') }}
{{ loading_state('Loading admin users...') }}
{{ error_state('Error loading admin users') }}
<!-- Stats Cards -->
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
<!-- Card: Total Admin Users -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('shield', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Total Admins
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total_admins || 0">
0
</p>
</div>
</div>
<!-- Card: Super Admins -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
<span x-html="$icon('star', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Super Admins
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.super_admins || 0">
0
</p>
</div>
</div>
<!-- Card: Platform Admins -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('globe-alt', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Platform Admins
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.platform_admins || 0">
0
</p>
</div>
</div>
<!-- Card: Active Admins -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
<span x-html="$icon('user-check', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Active
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.active_admins || 0">
0
</p>
</div>
</div>
</div>
<!-- Search and Filters -->
<div x-show="!loading" class="mb-6 px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<!-- Search Bar -->
<div class="flex-1 max-w-md">
<div class="relative">
<input
type="text"
x-model="filters.search"
@input="debouncedSearch()"
placeholder="Search by name, email, or username..."
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:bg-gray-700 dark:text-gray-300"
>
<div class="absolute inset-y-0 left-0 flex items-center pl-3">
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
</div>
</div>
</div>
<!-- Filters -->
<div class="flex flex-wrap gap-3">
<!-- Admin Type Filter -->
<select
x-model="filters.is_super_admin"
@change="pagination.page = 1; loadAdminUsers()"
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
>
<option value="">All Admin Types</option>
<option value="true">Super Admins</option>
<option value="false">Platform Admins</option>
</select>
<!-- Status Filter -->
<select
x-model="filters.is_active"
@change="pagination.page = 1; loadAdminUsers()"
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
>
<option value="">All Status</option>
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
<!-- Refresh Button -->
<button
@click="loadAdminUsers(); loadStats()"
class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="Refresh admin users"
>
<span x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
Refresh
</button>
</div>
</div>
</div>
<!-- Admin Users Table -->
<div x-show="!loading">
{% call table_wrapper() %}
{{ table_header(['Admin', 'Email', 'Type', 'Platforms', 'Status', 'Last Login', 'Actions']) }}
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<!-- Empty State -->
<template x-if="adminUsers.length === 0">
<tr>
<td colspan="7" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
<div class="flex flex-col items-center">
<span x-html="$icon('shield', 'w-12 h-12 text-gray-400 mb-4')"></span>
<p class="text-lg font-medium">No admin users found</p>
<p class="text-sm" x-text="filters.search ? 'Try adjusting your search or filters' : 'Create your first admin user to get started'"></p>
</div>
</td>
</tr>
</template>
<!-- Admin User Rows -->
<template x-for="admin in adminUsers" :key="admin.id">
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700">
<!-- Admin Info -->
<td class="px-4 py-3">
<div class="flex items-center text-sm">
<div class="relative hidden w-8 h-8 mr-3 rounded-full md:block">
<div class="absolute inset-0 rounded-full flex items-center justify-center text-white font-semibold text-sm"
:class="admin.is_super_admin ? 'bg-orange-500' : 'bg-purple-500'"
x-text="(admin.username || 'A').charAt(0).toUpperCase()">
</div>
</div>
<div>
<p class="font-semibold" x-text="admin.username"></p>
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="admin.full_name || ''"></p>
</div>
</div>
</td>
<!-- Email -->
<td class="px-4 py-3 text-sm" x-text="admin.email"></td>
<!-- Type -->
<td class="px-4 py-3 text-xs">
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
:class="admin.is_super_admin
? 'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100'
: 'text-purple-700 bg-purple-100 dark:bg-purple-700 dark:text-purple-100'"
x-text="admin.is_super_admin ? 'Super Admin' : 'Platform Admin'">
</span>
</td>
<!-- Platforms -->
<td class="px-4 py-3 text-sm">
<template x-if="admin.is_super_admin">
<span class="text-gray-500 dark:text-gray-400 italic">All platforms</span>
</template>
<template x-if="!admin.is_super_admin">
<div class="flex flex-wrap gap-1">
<template x-for="platform in (admin.platforms || []).slice(0, 3)" :key="platform.id">
<span class="px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-700 rounded" x-text="platform.code"></span>
</template>
<template x-if="(admin.platforms || []).length > 3">
<span class="px-2 py-0.5 text-xs text-gray-500" x-text="'+' + ((admin.platforms || []).length - 3) + ' more'"></span>
</template>
<template x-if="(admin.platforms || []).length === 0">
<span class="text-gray-500 dark:text-gray-400 italic">None assigned</span>
</template>
</div>
</template>
</td>
<!-- Status -->
<td class="px-4 py-3 text-xs">
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
:class="admin.is_active ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100'"
x-text="admin.is_active ? 'Active' : 'Inactive'">
</span>
</td>
<!-- Last Login -->
<td class="px-4 py-3 text-sm" x-text="admin.last_login ? formatDate(admin.last_login) : 'Never'"></td>
<!-- Actions -->
<td class="px-4 py-3">
<div class="flex items-center space-x-2 text-sm">
<!-- View Button -->
<a
:href="'/admin/admin-users/' + admin.id"
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="View admin user"
>
<span x-html="$icon('eye', 'w-5 h-5')"></span>
</a>
<!-- Edit Button -->
<a
:href="'/admin/admin-users/' + admin.id + '/edit'"
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="Edit admin user"
>
<span x-html="$icon('edit', 'w-5 h-5')"></span>
</a>
<!-- Delete Button (disabled for self) -->
<button
@click="deleteAdminUser(admin)"
:disabled="admin.id === currentUserId"
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700 focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
:title="admin.id === currentUserId ? 'Cannot delete yourself' : 'Delete admin user'"
>
<span x-html="$icon('delete', 'w-5 h-5')"></span>
</button>
</div>
</td>
</tr>
</template>
</tbody>
{% endcall %}
{{ pagination() }}
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/admin-users.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,249 @@
{# app/templates/admin/companies.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/pagination.html' import pagination %}
{% from 'shared/macros/headers.html' import page_header %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
{% block title %}Companies{% endblock %}
{% block alpine_data %}adminCompanies(){% endblock %}
{% block content %}
{{ page_header('Company Management', action_label='Create Company', action_url='/admin/companies/create') }}
{{ loading_state('Loading companies...') }}
{{ error_state('Error loading companies') }}
<!-- Stats Cards -->
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
<!-- Card: Total Companies -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('office-building', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Total Companies
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total || 0">
0
</p>
</div>
</div>
<!-- Card: Verified Companies -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
<span x-html="$icon('badge-check', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Verified
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.verified || 0">
0
</p>
</div>
</div>
<!-- Card: Active Companies -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Active
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.active || 0">
0
</p>
</div>
</div>
<!-- Card: Total Vendors -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
<span x-html="$icon('shopping-bag', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Total Vendors
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.totalVendors || 0">
0
</p>
</div>
</div>
</div>
<!-- Search and Filters Bar -->
<div x-show="!loading" class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<!-- Search Input -->
<div class="flex-1 max-w-md">
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
</span>
<input
type="text"
x-model="filters.search"
@input="debouncedSearch()"
placeholder="Search by name or email..."
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
>
</div>
</div>
<!-- Filters -->
<div class="flex flex-wrap gap-3">
<!-- Status Filter -->
<select
x-model="filters.is_active"
@change="pagination.page = 1; loadCompanies()"
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
>
<option value="">All Status</option>
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
<!-- Verification Filter -->
<select
x-model="filters.is_verified"
@change="pagination.page = 1; loadCompanies()"
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
>
<option value="">All Verification</option>
<option value="true">Verified</option>
<option value="false">Unverified</option>
</select>
<!-- Refresh Button -->
<button
@click="loadCompanies()"
class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="Refresh companies"
>
<span x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
Refresh
</button>
</div>
</div>
</div>
<!-- Companies Table -->
<div x-show="!loading">
{% call table_wrapper() %}
{{ table_header(['Company', 'Owner', 'Vendors', 'Status', 'Created', 'Actions']) }}
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<!-- Empty State -->
<template x-if="paginatedCompanies.length === 0">
<tr>
<td colspan="6" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
<div class="flex flex-col items-center">
<span x-html="$icon('office-building', 'w-12 h-12 mb-2 text-gray-300')"></span>
<p class="font-medium">No companies found</p>
<p class="text-xs mt-1" x-text="filters.search || filters.is_active || filters.is_verified ? 'Try adjusting your search or filters' : 'Create your first company to get started'"></p>
</div>
</td>
</tr>
</template>
<!-- Company Rows -->
<template x-for="company in paginatedCompanies" :key="company.id">
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<!-- Company Info -->
<td class="px-4 py-3">
<div class="flex items-center text-sm">
<div class="relative hidden w-8 h-8 mr-3 rounded-full md:block">
<div class="absolute inset-0 rounded-full bg-blue-100 dark:bg-blue-600 flex items-center justify-center">
<span class="text-xs font-semibold text-blue-600 dark:text-blue-100"
x-text="company.name?.charAt(0).toUpperCase() || '?'"></span>
</div>
</div>
<div>
<p class="font-semibold" x-text="company.name"></p>
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="company.contact_email"></p>
</div>
</div>
</td>
<!-- Owner Email -->
<td class="px-4 py-3 text-sm">
<p x-text="company.owner_email || 'N/A'"></p>
</td>
<!-- Vendor Count -->
<td class="px-4 py-3 text-sm">
<span class="px-2 py-1 font-semibold leading-tight text-blue-700 bg-blue-100 rounded-full dark:bg-blue-700 dark:text-blue-100"
x-text="company.vendor_count || 0">
0
</span>
</td>
<!-- Status Badges -->
<td class="px-4 py-3 text-xs">
<div class="flex gap-1">
<span class="inline-flex items-center px-2 py-1 font-semibold leading-tight rounded-full"
:class="company.is_active ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700'">
<span x-text="company.is_active ? 'Active' : 'Inactive'"></span>
</span>
<span x-show="company.is_verified" class="inline-flex items-center px-2 py-1 font-semibold leading-tight text-blue-700 bg-blue-100 rounded-full dark:bg-blue-700 dark:text-blue-100">
<span x-html="$icon('badge-check', 'w-3 h-3 mr-1')"></span>
Verified
</span>
</div>
</td>
<!-- Created Date -->
<td class="px-4 py-3 text-sm" x-text="formatDate(company.created_at)"></td>
<!-- Actions -->
<td class="px-4 py-3">
<div class="flex items-center space-x-2 text-sm">
<!-- View Button -->
<a
:href="'/admin/companies/' + company.id"
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="View company"
>
<span x-html="$icon('eye', 'w-5 h-5')"></span>
</a>
<!-- Edit Button -->
<button
@click="editCompany(company.id)"
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="Edit company"
>
<span x-html="$icon('edit', 'w-5 h-5')"></span>
</button>
<!-- Delete Button -->
<button
@click="deleteCompany(company)"
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="Delete company"
:disabled="company.vendor_count > 0"
:class="company.vendor_count > 0 ? 'opacity-50 cursor-not-allowed' : ''"
>
<span x-html="$icon('delete', 'w-5 h-5')"></span>
</button>
</div>
</td>
</tr>
</template>
</tbody>
{% endcall %}
{{ pagination() }}
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/companies.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,285 @@
{# app/templates/admin/company-create.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/headers.html' import page_header %}
{% from 'shared/macros/alerts.html' import error_state %}
{% block title %}Create Company{% endblock %}
{% block alpine_data %}adminCompanyCreate(){% endblock %}
{% block content %}
{{ page_header('Create New Company', subtitle='Create a company account with an owner user', back_url='/admin/companies', back_label='Back to Companies') }}
<!-- Success Message -->
<div x-show="successMessage" x-cloak class="mb-6 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg">
<div class="flex items-start">
<span x-html="$icon('check-circle', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
<div class="flex-1">
<p class="font-semibold">Company Created Successfully!</p>
<template x-if="ownerCredentials">
<div class="mt-2 p-3 bg-white rounded border border-green-300">
<p class="text-sm font-semibold mb-2">Owner Login Credentials (Save these!):</p>
<div class="space-y-1 text-sm font-mono">
<div><span class="font-bold">Email:</span> <span x-text="ownerCredentials.email"></span></div>
<div><span class="font-bold">Password:</span> <span x-text="ownerCredentials.password" class="bg-yellow-100 px-2 py-1 rounded"></span></div>
<div><span class="font-bold">Login URL:</span> <span x-text="ownerCredentials.login_url"></span></div>
</div>
<p class="mt-2 text-xs text-red-600">⚠️ The password will only be shown once. Please save it now!</p>
</div>
</template>
</div>
</div>
</div>
{{ error_state('Error Creating Company', error_var='errorMessage', show_condition='errorMessage') }}
<!-- Create Company Form -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<form @submit.prevent="createCompany">
<!-- Company Information Section -->
<div class="mb-6">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Company Information</h3>
<div class="grid gap-6 mb-4 md:grid-cols-2">
<!-- Company Name -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Company Name <span class="text-red-500">*</span>
</label>
<input
type="text"
x-model="formData.name"
required
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="ACME Corporation"
/>
</div>
<!-- Contact Email (Business) -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Business Contact Email <span class="text-red-500">*</span>
</label>
<input
type="email"
x-model="formData.contact_email"
required
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="info@acmecorp.com"
/>
<p class="mt-1 text-xs text-gray-500">Public business contact email</p>
</div>
</div>
<!-- Description -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Description
</label>
<textarea
x-model="formData.description"
rows="3"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="Brief description of the company..."
></textarea>
</div>
<div class="grid gap-6 mb-4 md:grid-cols-2">
<!-- Contact Phone -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Phone Number
</label>
<input
type="tel"
x-model="formData.contact_phone"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="+352 123 456 789"
/>
</div>
<!-- Website -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Website
</label>
<input
type="url"
x-model="formData.website"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="https://www.acmecorp.com"
/>
</div>
</div>
<div class="grid gap-6 mb-4 md:grid-cols-2">
<!-- Business Address -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Business Address
</label>
<textarea
x-model="formData.business_address"
rows="2"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="123 Main Street, Luxembourg City"
></textarea>
</div>
<!-- Tax Number -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Tax/VAT Number
</label>
<input
type="text"
x-model="formData.tax_number"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="LU12345678"
/>
</div>
</div>
</div>
<!-- Owner Information Section -->
<div class="mb-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Owner Account</h3>
<div class="p-4 mb-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
<p class="text-sm text-blue-800 dark:text-blue-300">
<span x-html="$icon('information-circle', 'w-4 h-4 inline mr-1')"></span>
A user account will be created for the company owner. If the email already exists, that user will be assigned as owner.
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Owner Email (Login) <span class="text-red-500">*</span>
</label>
<input
type="email"
x-model="formData.owner_email"
required
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="john.smith@acmecorp.com"
/>
<p class="mt-1 text-xs text-gray-500">This email will be used for owner login. Can be different from business contact email.</p>
</div>
</div>
<!-- Form Actions -->
<div class="flex items-center justify-between pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
@click="window.location.href='/admin/companies'"
class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-500 focus:outline-none focus:shadow-outline-gray"
>
Cancel
</button>
<button
type="submit"
:disabled="loading"
class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!loading">Create Company</span>
<span x-show="loading" class="flex items-center">
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
Creating...
</span>
</button>
</div>
</form>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
// Company Create Alpine Component
function adminCompanyCreate() {
return {
// Inherit base layout functionality
...data(),
// Page identifier
currentPage: 'companies',
// Form data
formData: {
name: '',
description: '',
owner_email: '',
contact_email: '',
contact_phone: '',
website: '',
business_address: '',
tax_number: ''
},
// UI state
loading: false,
successMessage: false,
errorMessage: '',
ownerCredentials: null,
// Initialize
init() {
console.log('Company Create page initialized');
},
// Create company
async createCompany() {
this.loading = true;
this.errorMessage = '';
this.successMessage = false;
this.ownerCredentials = null;
try {
console.log('Creating company:', this.formData);
const response = await apiClient.post('/admin/companies', this.formData);
console.log('Company created successfully:', response);
// Store owner credentials
if (response.temporary_password && response.temporary_password !== 'N/A (Existing user)') {
this.ownerCredentials = {
email: response.owner_email,
password: response.temporary_password,
login_url: response.login_url
};
}
this.successMessage = true;
// Reset form
this.formData = {
name: '',
description: '',
owner_email: '',
contact_email: '',
contact_phone: '',
website: '',
business_address: '',
tax_number: ''
};
// Scroll to top to show success message
window.scrollTo({ top: 0, behavior: 'smooth' });
// Redirect after 10 seconds if credentials shown, or 3 seconds otherwise
const redirectDelay = this.ownerCredentials ? 10000 : 3000;
setTimeout(() => {
window.location.href = '/admin/companies';
}, redirectDelay);
} catch (error) {
console.error('Failed to create company:', error);
this.errorMessage = error.message || 'Failed to create company';
window.scrollTo({ top: 0, behavior: 'smooth' });
} finally {
this.loading = false;
}
}
};
}
</script>
{% endblock %}

View File

@@ -0,0 +1,269 @@
{# app/templates/admin/company-detail.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/headers.html' import detail_page_header %}
{% block title %}Company Details{% endblock %}
{% block alpine_data %}adminCompanyDetail(){% endblock %}
{% block content %}
{% call detail_page_header("company?.name || 'Company Details'", '/admin/companies', subtitle_show='company') %}
ID: <span x-text="companyId"></span>
<span class="text-gray-400 mx-2">|</span>
<span x-text="company?.vendor_count || 0"></span> vendor(s)
{% endcall %}
{{ loading_state('Loading company details...') }}
{{ error_state('Error loading company') }}
<!-- Company Details -->
<div x-show="!loading && company">
<!-- Quick Actions Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Quick Actions
</h3>
<div class="flex flex-wrap items-center gap-3">
<a
:href="`/admin/companies/${companyId}/edit`"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
<span x-html="$icon('edit', 'w-4 h-4 mr-2')"></span>
Edit Company
</a>
<button
@click="deleteCompany()"
:disabled="company?.vendor_count > 0"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:shadow-outline-red disabled:opacity-50 disabled:cursor-not-allowed"
:title="company?.vendor_count > 0 ? 'Cannot delete company with vendors' : 'Delete company'">
<span x-html="$icon('delete', 'w-4 h-4 mr-2')"></span>
Delete Company
</button>
</div>
</div>
<!-- Status Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-4">
<!-- Verification Status -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 rounded-full"
:class="company?.is_verified ? 'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500' : 'text-orange-500 bg-orange-100 dark:text-orange-100 dark:bg-orange-500'">
<span x-html="$icon(company?.is_verified ? 'badge-check' : 'clock', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Verification
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="company?.is_verified ? 'Verified' : 'Pending'">
-
</p>
</div>
</div>
<!-- Active Status -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 rounded-full"
:class="company?.is_active ? 'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500' : 'text-red-500 bg-red-100 dark:text-red-100 dark:bg-red-500'">
<span x-html="$icon(company?.is_active ? 'check-circle' : 'x-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Status
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="company?.is_active ? 'Active' : 'Inactive'">
-
</p>
</div>
</div>
<!-- Vendor Count -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('shopping-bag', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Vendors
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="company?.vendor_count || 0">
0
</p>
</div>
</div>
<!-- Created Date -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('calendar', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Created
</p>
<p class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="formatDate(company?.created_at)">
-
</p>
</div>
</div>
</div>
<!-- Main Info Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Basic Information -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Basic Information
</h3>
<div class="space-y-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Company Name</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.name || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Description</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.description || 'No description provided'">-</p>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Contact Information
</h3>
<div class="space-y-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Contact Email</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.contact_email || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Phone</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.contact_phone || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Website</p>
<a
x-show="company?.website"
:href="company?.website"
target="_blank"
class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400"
x-text="company?.website">
</a>
<span x-show="!company?.website" class="text-sm text-gray-700 dark:text-gray-300">-</span>
</div>
</div>
</div>
</div>
<!-- Business Details -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Business Details
</h3>
<div class="grid gap-6 md:grid-cols-2">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Business Address</p>
<p class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line" x-text="company?.business_address || 'No address provided'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Tax Number</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.tax_number || 'Not provided'">-</p>
</div>
</div>
</div>
<!-- Owner Information -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Owner Information
</h3>
<div class="grid gap-6 md:grid-cols-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Owner User ID</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.owner_user_id || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Owner Username</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.owner_username || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Owner Email</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="company?.owner_email || '-'">-</p>
</div>
</div>
</div>
<!-- Vendors Section -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="company?.vendors && company?.vendors.length > 0">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('shopping-bag', 'inline w-5 h-5 mr-2')"></span>
Vendors (<span x-text="company?.vendors?.length || 0"></span>)
</h3>
<div class="overflow-x-auto">
<table class="w-full whitespace-no-wrap">
<thead>
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-700">
<th class="px-4 py-3">Vendor</th>
<th class="px-4 py-3">Subdomain</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<template x-for="vendor in company?.vendors || []" :key="vendor.id">
<tr class="text-gray-700 dark:text-gray-400">
<td class="px-4 py-3">
<div class="flex items-center text-sm">
<div>
<p class="font-semibold" x-text="vendor.name"></p>
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="vendor.vendor_code"></p>
</div>
</div>
</td>
<td class="px-4 py-3 text-sm" x-text="vendor.subdomain"></td>
<td class="px-4 py-3 text-xs">
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
:class="vendor.is_active ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100'"
x-text="vendor.is_active ? 'Active' : 'Inactive'">
</span>
</td>
<td class="px-4 py-3">
<a :href="'/admin/vendors/' + vendor.vendor_code"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 text-sm">
View
</a>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- More Actions -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
More Actions
</h3>
<div class="flex flex-wrap gap-3">
<!-- Create Vendor Button -->
<a
href="/admin/vendors/create"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-150 bg-green-600 border border-transparent rounded-lg hover:bg-green-700 focus:outline-none focus:shadow-outline-green"
>
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
Create Vendor
</a>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
<span x-html="$icon('information-circle', 'w-4 h-4 inline mr-1')"></span>
Vendors created will be associated with this company.
</p>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/company-detail.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,449 @@
{# app/templates/admin/company-edit.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import loading_state %}
{% from 'shared/macros/inputs.html' import search_autocomplete, selected_item_display %}
{% from 'shared/macros/headers.html' import edit_page_header %}
{% block title %}Edit Company{% endblock %}
{% block alpine_data %}adminCompanyEdit(){% endblock %}
{% block content %}
{% call edit_page_header('Edit Company', '/admin/companies', subtitle_show='company', back_label='Back to Companies') %}
<span x-text="company?.name"></span>
{% endcall %}
{{ loading_state('Loading company...', show_condition='loadingCompany') }}
<!-- Edit Form -->
<div x-show="!loadingCompany && company">
<!-- Quick Actions Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Quick Actions
</h3>
<div class="flex flex-wrap items-center gap-3">
<button
@click="toggleVerification()"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
:class="{ 'bg-orange-600 hover:bg-orange-700': company && company.is_verified, 'bg-green-600 hover:bg-green-700': company && !company.is_verified }">
<span x-html="$icon(company?.is_verified ? 'x-circle' : 'badge-check', 'w-4 h-4 mr-2')"></span>
<span x-text="company?.is_verified ? 'Unverify Company' : 'Verify Company'"></span>
</button>
<button
@click="toggleActive()"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
:class="{ 'bg-red-600 hover:bg-red-700': company && company.is_active, 'bg-green-600 hover:bg-green-700': company && !company.is_active }">
<span x-html="$icon(company?.is_active ? 'lock-closed' : 'lock-open', 'w-4 h-4 mr-2')"></span>
<span x-text="company?.is_active ? 'Deactivate' : 'Activate'"></span>
</button>
<!-- Status Badges -->
<div class="ml-auto flex items-center gap-2">
<span
x-show="company?.is_verified"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
<span x-html="$icon('badge-check', 'w-3 h-3 mr-1')"></span>
Verified
</span>
<span
x-show="!company?.is_verified"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-orange-700 bg-orange-100 rounded-full dark:bg-orange-700 dark:text-orange-100">
<span x-html="$icon('clock', 'w-3 h-3 mr-1')"></span>
Pending
</span>
<span
x-show="company?.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
Active
</span>
<span
x-show="!company?.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-red-700 bg-red-100 rounded-full dark:bg-red-700 dark:text-red-100">
Inactive
</span>
</div>
</div>
</div>
<!-- Form Card -->
<form @submit.prevent="handleSubmit" class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Left Column: Basic Info -->
<div>
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Basic Information
</h3>
<!-- Company ID (readonly) -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Company ID
</span>
<input
type="text"
:value="company?.id"
disabled
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
System-generated identifier
</span>
</label>
<!-- Name -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Company Name <span class="text-red-600">*</span>
</span>
<input
type="text"
x-model="formData.name"
required
maxlength="255"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.name }"
>
<span x-show="errors.name" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.name"></span>
</label>
<!-- Description -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Description
</span>
<textarea
x-model="formData.description"
rows="3"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
></textarea>
</label>
</div>
<!-- Right Column: Contact Info -->
<div>
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Contact Information
</h3>
<!-- Owner Info (readonly) -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Owner
</span>
<input
type="text"
:value="company?.owner_username ? company.owner_username + ' (' + company.owner_email + ')' : 'User ID: ' + company?.owner_user_id"
disabled
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Use "Transfer Ownership" in More Actions to change
</span>
</label>
<!-- Contact Email -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Contact Email <span class="text-red-600">*</span>
</span>
<input
type="email"
x-model="formData.contact_email"
required
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.contact_email }"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Public business contact email
</span>
<span x-show="errors.contact_email" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.contact_email"></span>
</label>
<!-- Phone -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Phone
</span>
<input
type="tel"
x-model="formData.contact_phone"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
<!-- Website -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Website
</span>
<input
type="url"
x-model="formData.website"
:disabled="saving"
placeholder="https://example.com"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
</div>
</div>
<!-- Business Details -->
<div class="mb-8">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Business Details
</h3>
<div class="grid gap-6 md:grid-cols-2">
<!-- Business Address -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">
Business Address
</span>
<textarea
x-model="formData.business_address"
rows="3"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
></textarea>
</label>
<!-- Tax Number -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">
Tax Number
</span>
<input
type="text"
x-model="formData.tax_number"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
</div>
</div>
<!-- Company Statistics (readonly) -->
<template x-if="company?.vendor_count !== undefined">
<div class="mb-8">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Company Statistics
</h3>
<div class="grid gap-4 md:grid-cols-2">
<div class="p-4 bg-gray-50 rounded-lg dark:bg-gray-700">
<p class="text-sm text-gray-600 dark:text-gray-400">Total Vendors</p>
<p class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="company.vendor_count || 0"></p>
</div>
<div class="p-4 bg-gray-50 rounded-lg dark:bg-gray-700">
<p class="text-sm text-gray-600 dark:text-gray-400">Active Vendors</p>
<p class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="company.active_vendor_count || 0"></p>
</div>
</div>
</div>
</template>
<!-- Save Button -->
<div class="flex items-center justify-end gap-3 pt-6 border-t dark:border-gray-700">
<a
href="/admin/companies"
class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none">
Cancel
</a>
<button
type="submit"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!saving">
<span x-html="$icon('check', 'w-4 h-4 mr-2 inline')"></span>
Save Changes
</span>
<span x-show="saving" class="flex items-center">
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
Saving...
</span>
</button>
</div>
</form>
<!-- More Actions Section -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
More Actions
</h3>
<div class="flex flex-wrap gap-3">
<!-- Transfer Ownership Button -->
<button
@click="showTransferOwnershipModal = true"
:disabled="saving"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-150 bg-orange-600 border border-transparent rounded-lg hover:bg-orange-700 focus:outline-none focus:shadow-outline-orange disabled:opacity-50"
>
<span x-html="$icon('switch-horizontal', 'w-4 h-4 mr-2')"></span>
Transfer Ownership
</button>
<!-- Delete Company Button -->
<button
@click="deleteCompany()"
:disabled="saving || (company?.vendor_count > 0)"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-150 bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:shadow-outline-red disabled:opacity-50"
:title="company?.vendor_count > 0 ? 'Cannot delete company with vendors' : 'Delete this company'"
>
<span x-html="$icon('delete', 'w-4 h-4 mr-2')"></span>
Delete Company
</button>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
<span x-html="$icon('information-circle', 'w-4 h-4 inline mr-1')"></span>
Ownership transfer affects all vendors under this company.
<span x-show="company?.vendor_count > 0" class="text-orange-600 dark:text-orange-400">
Company cannot be deleted while it has vendors (<span x-text="company?.vendor_count"></span> vendors).
</span>
</p>
</div>
{# noqa: FE-004 - Complex form modal with dynamic user search and transfer functionality #}
<!-- Transfer Ownership Modal -->
<div
x-show="showTransferOwnershipModal"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50"
@keydown.escape.window="showTransferOwnershipModal = false"
>
<div
@click.away="showTransferOwnershipModal = false"
class="w-full max-w-md bg-white rounded-lg shadow-lg dark:bg-gray-800"
>
<!-- Modal Header -->
<div class="flex items-center justify-between p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
Transfer Company Ownership
</h3>
<button
@click="showTransferOwnershipModal = false"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<span x-html="$icon('x', 'w-5 h-5')"></span>
</button>
</div>
<!-- Modal Body -->
<div class="p-4">
<div class="mb-4 p-3 bg-orange-100 border border-orange-300 text-orange-700 rounded-lg dark:bg-orange-900 dark:border-orange-700 dark:text-orange-300">
<p class="flex items-start text-sm">
<span x-html="$icon('exclamation', 'w-5 h-5 mr-2 flex-shrink-0')"></span>
<span>
<strong>Warning:</strong> This will transfer ownership of the company
"<span x-text="company?.name"></span>" and all its vendors to another user.
</span>
</p>
</div>
<form @submit.prevent="transferOwnership()">
<!-- New Owner Search -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
New Owner <span class="text-red-600">*</span>
</span>
{{ search_autocomplete(
search_var='userSearchQuery',
results_var='userSearchResults',
show_dropdown_var='showUserDropdown',
loading_var='searchingUsers',
disabled_var='transferring',
search_action='searchUsers()',
select_action='selectUser(item)',
selected_check='transferData.new_owner_user_id === item.id',
display_field='username',
secondary_field='email',
placeholder='Search by name or email...',
no_results_text='No users found'
) }}
{{ selected_item_display(
selected_var='selectedUser',
display_field='username',
secondary_field='email',
clear_action='clearSelectedUser()'
) }}
<span class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Current owner: <span x-text="company?.owner_username || 'User ID ' + company?.owner_user_id"></span>
</span>
<p x-show="showOwnerError && !transferData.new_owner_user_id" class="mt-1 text-xs text-red-600 dark:text-red-400">
<span x-html="$icon('exclamation', 'w-4 h-4 inline mr-1')"></span>
Please select a new owner
</p>
</label>
<!-- Transfer Reason -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Reason (optional)
</span>
<textarea
x-model="transferData.transfer_reason"
rows="2"
:disabled="transferring"
placeholder="Enter reason for transfer (for audit log)"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
></textarea>
</label>
<!-- Confirmation Checkbox -->
<label class="flex items-center text-sm">
<input
type="checkbox"
x-model="transferData.confirm_transfer"
:disabled="transferring"
class="form-checkbox text-purple-600"
>
<span class="ml-2 text-gray-700 dark:text-gray-400">
I confirm I want to transfer ownership
</span>
</label>
<p x-show="showConfirmError && !transferData.confirm_transfer" class="mt-1 mb-4 text-xs text-red-600 dark:text-red-400">
<span x-html="$icon('exclamation', 'w-4 h-4 inline mr-1')"></span>
Please confirm the transfer by checking the box above
</p>
<div x-show="!showConfirmError || transferData.confirm_transfer" class="mb-4"></div>
<!-- Modal Actions -->
<div class="flex justify-end gap-3">
<button
type="button"
@click="showTransferOwnershipModal = false"
:disabled="transferring"
class="px-4 py-2 text-sm font-medium text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
:disabled="transferring"
class="flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-150 bg-orange-600 border border-transparent rounded-lg hover:bg-orange-700 focus:outline-none focus:shadow-outline-orange disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!transferring">
<span x-html="$icon('switch-horizontal', 'w-4 h-4 mr-2 inline')"></span>
Transfer Ownership
</span>
<span x-show="transferring" class="flex items-center">
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
Transferring...
</span>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/company-edit.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,140 @@
{# app/templates/admin/login.html #}
<!DOCTYPE html>
<html :class="{ 'dark': dark }" x-data="adminLogin()" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin Login - Multi-Tenant Platform</title>
<!-- Fonts: Local fallback + Google Fonts -->
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="{{ url_for('static', path='admin/css/tailwind.output.css') }}" />
<style>
[x-cloak] { display: none !important; }
</style>
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
<div class="flex-1 h-full max-w-4xl mx-auto overflow-hidden bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="flex flex-col overflow-y-auto md:flex-row">
<div class="h-32 md:h-auto md:w-1/2">
<img aria-hidden="true" class="object-cover w-full h-full dark:hidden"
src="{{ url_for('static', path='admin/img/login-office.jpeg') }}" alt="Office" />
<img aria-hidden="true" class="hidden object-cover w-full h-full dark:block"
src="{{ url_for('static', path='admin/img/login-office-dark.jpeg') }}" alt="Office" />
</div>
<div class="flex items-center justify-center p-6 sm:p-12 md:w-1/2">
<div class="w-full">
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Admin Login
</h1>
<!-- Alert Messages -->
<div x-show="error" x-text="error"
class="px-4 py-3 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800"
x-transition></div>
<div x-show="success" x-text="success"
class="px-4 py-3 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800"
x-transition></div>
<!-- Login Form -->
<form @submit.prevent="handleLogin">
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Username</span>
<input x-model="credentials.username"
:disabled="loading"
@input="clearErrors"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600': errors.username }"
placeholder="Enter your username"
autocomplete="username"
required />
<span x-show="errors.username" x-text="errors.username"
class="text-xs text-red-600 dark:text-red-400"></span>
</label>
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">Password</span>
<input x-model="credentials.password"
:disabled="loading"
@input="clearErrors"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600': errors.password }"
placeholder="***************"
type="password"
autocomplete="current-password"
required />
<span x-show="errors.password" x-text="errors.password"
class="text-xs text-red-600 dark:text-red-400"></span>
</label>
{# noqa: FE-002 - Inline spinner SVG for loading state #}
<button type="submit" :disabled="loading"
class="block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!loading">Sign in</span>
<span x-show="loading" class="flex items-center justify-center">
<svg class="inline w-4 h-4 mr-2 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Signing in...
</span>
</button>
</form>
<hr class="my-8" />
<p class="mt-4">
<a class="text-sm font-medium text-purple-600 dark:text-purple-400 hover:underline"
href="#">
Forgot your password?
</a>
</p>
<p class="mt-2">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="/">
← Back to Platform
</a>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Scripts - ORDER MATTERS! -->
<!-- 1. Log Configuration -->
<script src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>
<!-- 2. Icons -->
<script src="{{ url_for('static', path='shared/js/icons.js') }}"></script>
<!-- 3. Utils -->
<script src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
<!-- 4. API Client -->
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
<!-- 5. Alpine.js v3 with CDN fallback -->
<script>
(function() {
var script = document.createElement('script');
script.defer = true;
script.src = 'https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js';
script.onerror = function() {
console.warn('Alpine.js CDN failed, loading local copy...');
var fallbackScript = document.createElement('script');
fallbackScript.defer = true;
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/alpine.min.js") }}';
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
})();
</script>
<!-- 6. Login Logic -->
<script src="{{ url_for('tenancy_static', path='admin/js/login.js') }}"></script>
</body>
</html>

View File

@@ -0,0 +1,148 @@
{# app/templates/admin/module-config.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
{% from 'shared/macros/headers.html' import page_header %}
{% block title %}Module Configuration{% endblock %}
{% block alpine_data %}adminModuleConfig('{{ platform_code }}', '{{ module_code }}'){% endblock %}
{% block content %}
{{ page_header('Module Configuration', back_url='/admin/platforms/' + platform_code + '/modules') }}
{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }}
{{ error_state('Error', show_condition='error') }}
<!-- Module Info -->
<div class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="moduleInfo?.module_name || 'Loading...'"></h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
Configure settings for this module on <span x-text="platformName" class="font-medium"></span>.
</p>
</div>
<span class="px-3 py-1 text-sm font-medium rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200" x-text="moduleInfo?.module_code?.toUpperCase()"></span>
</div>
</div>
<!-- Loading State -->
<div x-show="loading" class="flex items-center justify-center py-12 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<span x-html="$icon('refresh', 'w-8 h-8 animate-spin text-purple-600')"></span>
<span class="ml-3 text-gray-500 dark:text-gray-400">Loading configuration...</span>
</div>
<!-- Configuration Form -->
<div x-show="!loading" class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200">Configuration Options</h3>
</div>
<!-- Config Fields -->
<div class="p-4 space-y-6">
<template x-if="moduleInfo?.schema_info?.length > 0">
<div class="space-y-6">
<template x-for="field in moduleInfo.schema_info" :key="field.key">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2" x-text="field.label"></label>
<!-- Boolean field -->
<template x-if="field.type === 'boolean'">
<div class="flex items-center">
<button
@click="config[field.key] = !config[field.key]"
:class="{
'bg-purple-600': config[field.key],
'bg-gray-200 dark:bg-gray-600': !config[field.key]
}"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
role="switch"
:aria-checked="config[field.key]"
>
<span
:class="{
'translate-x-5': config[field.key],
'translate-x-0': !config[field.key]
}"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
></span>
</button>
<span class="ml-3 text-sm text-gray-500 dark:text-gray-400" x-text="config[field.key] ? 'Enabled' : 'Disabled'"></span>
</div>
</template>
<!-- Number field (dynamic Alpine.js template - cannot use static macro) --> {# noqa: FE-008 #}
<template x-if="field.type === 'number'">
<input
type="number"
x-model.number="config[field.key]"
:min="field.min"
:max="field.max"
class="block w-full max-w-xs px-3 py-2 text-sm leading-5 text-gray-700 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring focus:ring-purple-400"
>
</template>
<!-- String field -->
<template x-if="field.type === 'string'">
<input
type="text"
x-model="config[field.key]"
class="block w-full max-w-md px-3 py-2 text-sm leading-5 text-gray-700 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring focus:ring-purple-400"
>
</template>
<!-- Select field -->
<template x-if="field.type === 'select'">
<select
x-model="config[field.key]"
class="block w-full max-w-xs px-3 py-2 text-sm leading-5 text-gray-700 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring focus:ring-purple-400"
>
<template x-for="option in field.options" :key="option">
<option :value="option" x-text="option"></option>
</template>
</select>
</template>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="field.description"></p>
</div>
</template>
</div>
</template>
<!-- No config options -->
<template x-if="!moduleInfo?.schema_info?.length">
<div class="text-center py-8">
<span x-html="$icon('cog', 'w-12 h-12 mx-auto text-gray-400')"></span>
<p class="mt-4 text-gray-500 dark:text-gray-400">No configuration options available for this module.</p>
</div>
</template>
</div>
<!-- Actions -->
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700/50 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
<button
@click="resetToDefaults()"
:disabled="saving"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600 disabled:opacity-50"
>
<span x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
Reset to Defaults
</button>
<button
@click="saveConfig()"
:disabled="saving"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 focus:outline-none focus:ring focus:ring-purple-400 disabled:opacity-50"
>
<span x-show="saving" x-html="$icon('refresh', 'w-4 h-4 mr-2 animate-spin')"></span>
<span x-show="!saving" x-html="$icon('check', 'w-4 h-4 mr-2')"></span>
<span x-text="saving ? 'Saving...' : 'Save Configuration'"></span>
</button>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/module-config.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,274 @@
{# app/templates/admin/module-info.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state, loading_state %}
{% from 'shared/macros/headers.html' import page_header %}
{% block title %}Module Details{% endblock %}
{% block alpine_data %}adminModuleInfo('{{ platform_code }}', '{{ module_code }}'){% endblock %}
{% block content %}
<!-- Header with Back Button -->
<div class="flex items-center justify-between my-6">
<div class="flex items-center">
<a :href="`/admin/platforms/${platformCode}/modules`" class="mr-4 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<span x-html="$icon('arrow-left', 'w-5 h-5 text-gray-600 dark:text-gray-400')"></span>
</a>
<div>
<div class="flex items-center">
<span x-html="$icon(getModuleIcon(moduleCode), 'w-8 h-8 text-purple-600 dark:text-purple-400 mr-3')"></span>
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="module?.name || 'Loading...'"></h2>
<code class="ml-3 text-sm bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded" x-text="moduleCode"></code>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-text="module?.description || ''"></p>
</div>
</div>
<div class="flex items-center gap-3">
<!-- Configure Button -->
<a x-show="hasConfig(moduleCode)"
:href="`/admin/platforms/${platformCode}/modules/${moduleCode}/config`"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 focus:outline-none focus:ring focus:ring-purple-400">
<span x-html="$icon('cog', 'w-4 h-4 mr-2')"></span>
Configure
</a>
<!-- Status Badge & Toggle -->
<div class="flex items-center gap-3">
<span x-show="module?.is_enabled"
class="px-3 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Enabled
</span>
<span x-show="!module?.is_enabled && !module?.is_core"
class="px-3 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
Disabled
</span>
<span x-show="module?.is_core"
class="px-3 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
Core
</span>
<!-- Toggle (only for non-core modules) -->
<button
x-show="!module?.is_core"
@click="toggleModule()"
:disabled="saving"
:class="{
'bg-purple-600': module?.is_enabled,
'bg-gray-200 dark:bg-gray-600': !module?.is_enabled
}"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
role="switch"
:aria-checked="module?.is_enabled"
>
<span
:class="{
'translate-x-5': module?.is_enabled,
'translate-x-0': !module?.is_enabled
}"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
></span>
</button>
</div>
</div>
</div>
{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }}
{{ error_state('Error', show_condition='error') }}
{{ loading_state('Loading module details...') }}
<!-- Main Content -->
<div x-show="!loading && module" class="space-y-6">
<!-- Description Card -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">Description</h3>
<p class="text-gray-600 dark:text-gray-400" x-text="module?.description || 'No description available.'"></p>
</div>
<!-- Features -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Features</h3>
<div x-show="module?.features?.length > 0" class="flex flex-wrap gap-2">
<template x-for="feature in module?.features" :key="feature">
<span class="px-3 py-1 text-sm rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300" x-text="feature"></span>
</template>
</div>
<p x-show="!module?.features?.length" class="text-gray-500 dark:text-gray-400 text-sm">No features defined for this module.</p>
</div>
<!-- Menu Items -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Admin Menu Items -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span x-html="$icon('office-building', 'w-5 h-5 inline mr-2 text-purple-600 dark:text-purple-400')"></span>
Admin Menu Items
</h3>
<div x-show="module?.admin_menu_items?.length > 0" class="space-y-2">
<template x-for="item in module?.admin_menu_items" :key="item">
<div class="flex items-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded">
<span x-html="$icon('menu-alt-2', 'w-4 h-4 text-gray-500 mr-2')"></span>
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="item"></span>
</div>
</template>
</div>
<p x-show="!module?.admin_menu_items?.length" class="text-gray-500 dark:text-gray-400 text-sm">No admin menu items.</p>
</div>
<!-- Vendor Menu Items -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span x-html="$icon('building-storefront', 'w-5 h-5 inline mr-2 text-teal-600 dark:text-teal-400')"></span>
Vendor Menu Items
</h3>
<div x-show="module?.vendor_menu_items?.length > 0" class="space-y-2">
<template x-for="item in module?.vendor_menu_items" :key="item">
<div class="flex items-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded">
<span x-html="$icon('menu-alt-2', 'w-4 h-4 text-gray-500 mr-2')"></span>
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="item"></span>
</div>
</template>
</div>
<p x-show="!module?.vendor_menu_items?.length" class="text-gray-500 dark:text-gray-400 text-sm">No vendor menu items.</p>
</div>
</div>
<!-- Dependencies -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Requires (Dependencies) -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span x-html="$icon('link', 'w-5 h-5 inline mr-2 text-amber-600 dark:text-amber-400')"></span>
Dependencies
<span class="text-xs font-normal text-gray-500 dark:text-gray-400 ml-2">(requires)</span>
</h3>
<div x-show="module?.requires?.length > 0" class="flex flex-wrap gap-2">
<template x-for="dep in module?.requires" :key="dep">
<a :href="`/admin/platforms/${platformCode}/modules/${dep}`"
class="px-3 py-1 text-sm rounded-full bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-900/50 transition-colors"
x-text="dep"></a>
</template>
</div>
<p x-show="!module?.requires?.length" class="text-gray-500 dark:text-gray-400 text-sm">No dependencies.</p>
</div>
<!-- Dependents (Required By) -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span x-html="$icon('users', 'w-5 h-5 inline mr-2 text-blue-600 dark:text-blue-400')"></span>
Dependents
<span class="text-xs font-normal text-gray-500 dark:text-gray-400 ml-2">(required by)</span>
</h3>
<div x-show="module?.dependent_modules?.length > 0" class="flex flex-wrap gap-2">
<template x-for="dep in module?.dependent_modules" :key="dep">
<a :href="`/admin/platforms/${platformCode}/modules/${dep}`"
class="px-3 py-1 text-sm rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-900/50 transition-colors"
x-text="dep"></a>
</template>
</div>
<p x-show="!module?.dependent_modules?.length" class="text-gray-500 dark:text-gray-400 text-sm">No modules depend on this one.</p>
</div>
</div>
<!-- Self-Contained Module Info (if applicable) -->
<div x-show="module?.is_self_contained" class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span x-html="$icon('puzzle', 'w-5 h-5 inline mr-2 text-green-600 dark:text-green-400')"></span>
Self-Contained Module
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Services -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.services_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.services_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Services</p>
<code x-show="module?.self_contained_info?.services_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.services_path"></code>
<p x-show="!module?.self_contained_info?.services_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
<!-- Models -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.models_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.models_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Models</p>
<code x-show="module?.self_contained_info?.models_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.models_path"></code>
<p x-show="!module?.self_contained_info?.models_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
<!-- Templates -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.templates_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.templates_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Templates</p>
<code x-show="module?.self_contained_info?.templates_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.templates_path"></code>
<p x-show="!module?.self_contained_info?.templates_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
<!-- Locales -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.locales_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.locales_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Locales</p>
<code x-show="module?.self_contained_info?.locales_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.locales_path"></code>
<p x-show="!module?.self_contained_info?.locales_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
<!-- Static Files -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.static_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.static_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Static Files</p>
<code x-show="module?.self_contained_info?.static_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.static_path"></code>
<p x-show="!module?.self_contained_info?.static_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
<!-- API Routes -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.api_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.api_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">API Routes</p>
<code x-show="module?.self_contained_info?.api_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.api_path"></code>
<p x-show="!module?.self_contained_info?.api_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
</div>
</div>
<!-- Module Type Info -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Module Information</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
<span class="text-gray-600 dark:text-gray-400">Module Type</span>
<span x-show="module?.is_core" class="px-2 py-1 text-xs font-medium rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">Core</span>
<span x-show="!module?.is_core" class="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">Optional</span>
</div>
<div class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
<span class="text-gray-600 dark:text-gray-400">Self-Contained</span>
<span x-show="module?.is_self_contained" class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">Yes</span>
<span x-show="!module?.is_self_contained" class="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">No</span>
</div>
<div class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
<span class="text-gray-600 dark:text-gray-400">Has Configuration</span>
<span x-show="hasConfig(moduleCode)" class="px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">Yes</span>
<span x-show="!hasConfig(moduleCode)" class="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">No</span>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/module-info.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,301 @@
{# app/templates/admin/platform-detail.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/headers.html' import page_header %}
{% block title %}Platform Details{% endblock %}
{% block alpine_data %}platformDetail(){% endblock %}
{% block content %}
<!-- Header with Back Button -->
<div class="flex items-center justify-between my-6">
<div class="flex items-center">
<a href="/admin/platforms" class="mr-4 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<span x-html="$icon('arrow-left', 'w-5 h-5 text-gray-600 dark:text-gray-400')"></span>
</a>
<div>
<div class="flex items-center">
<span x-html="$icon(getPlatformIcon(platformCode), 'w-8 h-8 text-purple-600 dark:text-purple-400 mr-3')"></span>
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="platform?.name || 'Loading...'"></h2>
<code class="ml-3 text-sm bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded" x-text="platformCode"></code>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-text="platform?.description || ''"></p>
</div>
</div>
<div class="flex items-center gap-2">
<!-- Status Badges -->
<span
x-show="platform?.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Active
</span>
<span
x-show="!platform?.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
Inactive
</span>
</div>
</div>
{{ loading_state('Loading platform details...') }}
{{ error_state('Error loading platform') }}
<!-- Main Content -->
<div x-show="!loading && platform" class="space-y-6">
<!-- Quick Actions -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Quick Actions</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- Edit Settings -->
<a :href="`/admin/platforms/${platformCode}/edit`"
class="flex items-center p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg hover:bg-purple-100 dark:hover:bg-purple-900/40 transition-colors">
<span x-html="$icon('cog', 'w-8 h-8 text-purple-600 dark:text-purple-400')"></span>
<div class="ml-3">
<p class="font-semibold text-gray-900 dark:text-white">Edit Settings</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Branding, domain, languages</p>
</div>
</a>
<!-- Edit Homepage -->
<a :href="`/admin/content-pages?platform_code=${platformCode}&slug=home`"
class="flex items-center p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg hover:bg-blue-100 dark:hover:bg-blue-900/40 transition-colors">
<span x-html="$icon('home', 'w-8 h-8 text-blue-600 dark:text-blue-400')"></span>
<div class="ml-3">
<p class="font-semibold text-gray-900 dark:text-white">Edit Homepage</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Platform landing page</p>
</div>
</a>
<!-- Manage Pages -->
<a :href="`/admin/content-pages?platform_code=${platformCode}`"
class="flex items-center p-4 bg-teal-50 dark:bg-teal-900/20 rounded-lg hover:bg-teal-100 dark:hover:bg-teal-900/40 transition-colors">
<span x-html="$icon('document-text', 'w-8 h-8 text-teal-600 dark:text-teal-400')"></span>
<div class="ml-3">
<p class="font-semibold text-gray-900 dark:text-white">Manage Pages</p>
<p class="text-sm text-gray-600 dark:text-gray-400">All content pages</p>
</div>
</a>
<!-- View Platform -->
<a :href="getPlatformUrl()" target="_blank"
class="flex items-center p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<span x-html="$icon('external-link', 'w-8 h-8 text-gray-600 dark:text-gray-400')"></span>
<div class="ml-3">
<p class="font-semibold text-gray-900 dark:text-white">View Platform</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Open in new tab</p>
</div>
</a>
</div>
</div>
<!-- Super Admin Actions (Menu Configuration) -->
<div x-show="isSuperAdmin" class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span class="inline-flex items-center">
Super Admin
<span class="ml-2 px-2 py-0.5 text-xs font-medium rounded bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">Admin Only</span>
</span>
</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- Module Configuration -->
<a :href="`/admin/platforms/${platformCode}/modules`"
class="flex items-center p-4 bg-green-50 dark:bg-green-900/20 rounded-lg hover:bg-green-100 dark:hover:bg-green-900/40 transition-colors">
<span x-html="$icon('puzzle', 'w-8 h-8 text-green-600 dark:text-green-400')"></span>
<div class="ml-3">
<p class="font-semibold text-gray-900 dark:text-white">Module Configuration</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Enable/disable features</p>
</div>
</a>
<!-- Menu Configuration -->
<a :href="`/admin/platforms/${platformCode}/menu-config`"
class="flex items-center p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg hover:bg-amber-100 dark:hover:bg-amber-900/40 transition-colors">
<span x-html="$icon('view-grid', 'w-8 h-8 text-amber-600 dark:text-amber-400')"></span>
<div class="ml-3">
<p class="font-semibold text-gray-900 dark:text-white">Menu Configuration</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Admin & vendor menus</p>
</div>
</a>
</div>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<!-- Vendors -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Vendors</p>
<p class="text-3xl font-bold text-purple-600 dark:text-purple-400" x-text="platform?.vendor_count || 0"></p>
</div>
<div class="p-3 bg-purple-100 dark:bg-purple-900/50 rounded-full">
<span x-html="$icon('building-storefront', 'w-6 h-6 text-purple-600 dark:text-purple-400')"></span>
</div>
</div>
</div>
<!-- Marketing Pages -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Marketing Pages</p>
<p class="text-3xl font-bold text-blue-600 dark:text-blue-400" x-text="platform?.platform_pages_count || 0"></p>
</div>
<div class="p-3 bg-blue-100 dark:bg-blue-900/50 rounded-full">
<span x-html="$icon('megaphone', 'w-6 h-6 text-blue-600 dark:text-blue-400')"></span>
</div>
</div>
</div>
<!-- Vendor Defaults -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Vendor Defaults</p>
<p class="text-3xl font-bold text-teal-600 dark:text-teal-400" x-text="platform?.vendor_defaults_count || 0"></p>
</div>
<div class="p-3 bg-teal-100 dark:bg-teal-900/50 rounded-full">
<span x-html="$icon('document-duplicate', 'w-6 h-6 text-teal-600 dark:text-teal-400')"></span>
</div>
</div>
</div>
<!-- Language -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Default Language</p>
<p class="text-3xl font-bold text-gray-700 dark:text-gray-300" x-text="platform?.default_language?.toUpperCase() || '—'"></p>
</div>
<div class="p-3 bg-gray-100 dark:bg-gray-700 rounded-full">
<span x-html="$icon('language', 'w-6 h-6 text-gray-600 dark:text-gray-400')"></span>
</div>
</div>
</div>
</div>
<!-- Platform Configuration -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Routing Info -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Routing Configuration</h3>
<div class="space-y-3">
<div class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
<span class="text-gray-600 dark:text-gray-400">Production Domain</span>
<code x-show="platform?.domain" class="text-sm bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded" x-text="platform?.domain"></code>
<span x-show="!platform?.domain" class="text-gray-400 dark:text-gray-500 text-sm">Not configured</span>
</div>
<div class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
<span class="text-gray-600 dark:text-gray-400">Dev Path Prefix</span>
<code x-show="platform?.path_prefix" class="text-sm bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded" x-text="`/platforms/${platform?.path_prefix}/`"></code>
<span x-show="!platform?.path_prefix" class="text-gray-400 dark:text-gray-500 text-sm">Root path</span>
</div>
<div class="flex justify-between items-center py-2">
<span class="text-gray-600 dark:text-gray-400">Supported Languages</span>
<div class="flex gap-1">
<template x-for="lang in (platform?.supported_languages || [])" :key="lang">
<span class="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded" x-text="lang.toUpperCase()"></span>
</template>
</div>
</div>
</div>
</div>
<!-- Branding -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Branding</h3>
<div class="space-y-3">
<div class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
<span class="text-gray-600 dark:text-gray-400">Logo</span>
<template x-if="platform?.logo">
<img :src="platform.logo" alt="Logo" class="h-8 max-w-32 object-contain">
</template>
<span x-show="!platform?.logo" class="text-gray-400 dark:text-gray-500 text-sm">Not configured</span>
</div>
<div class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
<span class="text-gray-600 dark:text-gray-400">Logo (Dark)</span>
<template x-if="platform?.logo_dark">
<img :src="platform.logo_dark" alt="Logo Dark" class="h-8 max-w-32 object-contain bg-gray-800 rounded p-1">
</template>
<span x-show="!platform?.logo_dark" class="text-gray-400 dark:text-gray-500 text-sm">Not configured</span>
</div>
<div class="flex justify-between items-center py-2">
<span class="text-gray-600 dark:text-gray-400">Favicon</span>
<template x-if="platform?.favicon">
<img :src="platform.favicon" alt="Favicon" class="h-6 w-6 object-contain">
</template>
<span x-show="!platform?.favicon" class="text-gray-400 dark:text-gray-500 text-sm">Not configured</span>
</div>
</div>
</div>
</div>
<!-- Recent Pages -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Content Pages</h3>
<a :href="`/admin/content-pages?platform_code=${platformCode}`"
class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300">
View All →
</a>
</div>
<div x-show="recentPages.length > 0">
<table class="w-full">
<thead class="bg-gray-50 dark:bg-gray-700/50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Title</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Slug</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Updated</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<template x-for="page in recentPages" :key="page.id">
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
<td class="px-6 py-4">
<a :href="`/admin/content-pages/${page.id}/edit`"
class="text-gray-900 dark:text-white hover:text-purple-600 dark:hover:text-purple-400 font-medium"
x-text="page.title"></a>
</td>
<td class="px-6 py-4">
<code class="text-sm bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded" x-text="page.slug"></code>
</td>
<td class="px-6 py-4">
<span class="px-2 py-1 text-xs font-semibold rounded-full"
:class="getPageTypeBadgeClass(page)"
x-text="getPageTypeLabel(page)"></span>
</td>
<td class="px-6 py-4">
<span x-show="page.is_published" class="px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">Published</span>
<span x-show="!page.is_published" class="px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">Draft</span>
</td>
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400" x-text="formatDate(page.updated_at)"></td>
</tr>
</template>
</tbody>
</table>
</div>
<div x-show="recentPages.length === 0" class="p-6 text-center text-gray-500 dark:text-gray-400">
<span x-html="$icon('document-text', 'w-12 h-12 mx-auto mb-2 opacity-50')"></span>
<p>No content pages yet.</p>
<a :href="`/admin/content-pages/create?platform_code=${platformCode}`"
class="inline-block mt-2 text-purple-600 hover:text-purple-700 dark:text-purple-400">
Create your first page →
</a>
</div>
</div>
<!-- Timestamps -->
<div class="text-sm text-gray-500 dark:text-gray-400">
<p>Created: <span x-text="formatDate(platform?.created_at)"></span></p>
<p>Last Updated: <span x-text="formatDate(platform?.updated_at)"></span></p>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="/static/admin/js/platform-detail.js"></script>
{% endblock %}

View File

@@ -0,0 +1,324 @@
{# app/templates/admin/platform-edit.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import loading_state %}
{% from 'shared/macros/headers.html' import edit_page_header %}
{% block title %}Edit Platform{% endblock %}
{% block alpine_data %}platformEdit(){% endblock %}
{% block content %}
{% call edit_page_header('Edit Platform', '/admin/platforms', subtitle_show='platform', back_label='Back to Platforms') %}
<span x-text="platform?.name"></span>
<span class="text-gray-400"></span>
<code class="text-sm bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded" x-text="platform?.code"></code>
{% endcall %}
{{ loading_state('Loading platform...', show_condition='loading') }}
<!-- Error State -->
<div x-show="error && !loading" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg dark:bg-red-900/50 dark:border-red-600 dark:text-red-200">
<div class="flex items-center">
<span x-html="$icon('exclamation-circle', 'w-5 h-5 mr-2')"></span>
<span x-text="error"></span>
</div>
</div>
<!-- Success State -->
<div x-show="success" x-transition class="mb-6 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg dark:bg-green-900/50 dark:border-green-600 dark:text-green-200">
<div class="flex items-center">
<span x-html="$icon('check-circle', 'w-5 h-5 mr-2')"></span>
<span x-text="success"></span>
</div>
</div>
<!-- Edit Form -->
<div x-show="!loading && platform">
<!-- Quick Actions Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Quick Actions
</h3>
<div class="flex flex-wrap items-center gap-3">
<button
@click="toggleActive()"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
:class="formData.is_active ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700'">
<span x-html="$icon(formData.is_active ? 'pause' : 'play', 'w-4 h-4 mr-2')"></span>
<span x-text="formData.is_active ? 'Deactivate' : 'Activate'"></span>
</button>
<button
@click="togglePublic()"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
:class="formData.is_public ? 'bg-orange-600 hover:bg-orange-700' : 'bg-blue-600 hover:bg-blue-700'">
<span x-html="$icon(formData.is_public ? 'eye-off' : 'eye', 'w-4 h-4 mr-2')"></span>
<span x-text="formData.is_public ? 'Make Private' : 'Make Public'"></span>
</button>
<!-- Status Badges -->
<div class="ml-auto flex items-center gap-2">
<span
x-show="formData.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
<span x-html="$icon('check-circle', 'w-3 h-3 mr-1')"></span>
Active
</span>
<span
x-show="!formData.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-red-700 bg-red-100 rounded-full dark:bg-red-700 dark:text-red-100">
<span x-html="$icon('x-circle', 'w-3 h-3 mr-1')"></span>
Inactive
</span>
<span
x-show="formData.is_public"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-blue-700 bg-blue-100 rounded-full dark:bg-blue-700 dark:text-blue-100">
<span x-html="$icon('globe-alt', 'w-3 h-3 mr-1')"></span>
Public
</span>
<span
x-show="!formData.is_public"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-gray-700 bg-gray-100 rounded-full dark:bg-gray-700 dark:text-gray-100">
<span x-html="$icon('lock-closed', 'w-3 h-3 mr-1')"></span>
Private
</span>
</div>
</div>
</div>
<!-- Form Card -->
<form @submit.prevent="handleSubmit" class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Left Column: Basic Info -->
<div>
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Basic Information
</h3>
<!-- Platform Code (readonly) -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Platform Code
</span>
<input
type="text"
:value="platform?.code || ''"
disabled
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Cannot be changed after creation
</span>
</label>
<!-- Name -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Platform Name <span class="text-red-600">*</span>
</span>
<input
type="text"
x-model="formData.name"
required
maxlength="100"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.name }"
>
<span x-show="errors.name" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.name"></span>
</label>
<!-- Description -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Description
</span>
<textarea
x-model="formData.description"
rows="3"
maxlength="500"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
></textarea>
</label>
<!-- Default Language -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Default Language
</span>
<select
x-model="formData.default_language"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-select"
>
<template x-for="lang in availableLanguages" :key="lang.code">
<option :value="lang.code" x-text="lang.name"></option>
</template>
</select>
</label>
<!-- Supported Languages -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Supported Languages
</span>
<div class="mt-2 flex flex-wrap gap-2">
<template x-for="lang in availableLanguages" :key="lang.code">
<button
type="button"
@click="toggleLanguage(lang.code)"
:disabled="saving"
class="px-3 py-1 text-sm rounded-full transition-colors"
:class="isLanguageSupported(lang.code)
? 'bg-purple-600 text-white'
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'"
>
<span x-text="lang.name"></span>
</button>
</template>
</div>
</label>
</div>
<!-- Right Column: Routing & Branding -->
<div>
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Routing & Branding
</h3>
<!-- Domain -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Production Domain
</span>
<input
type="text"
x-model="formData.domain"
placeholder="e.g., oms.lu"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Domain used in production for this platform
</span>
</label>
<!-- Path Prefix -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Development Path Prefix
</span>
<input
type="text"
x-model="formData.path_prefix"
placeholder="e.g., oms"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Used for /platforms/{prefix}/ routing in development
</span>
</label>
<!-- Logo URL -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Logo URL (Light Mode)
</span>
<input
type="url"
x-model="formData.logo"
placeholder="https://..."
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
<!-- Logo Dark URL -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Logo URL (Dark Mode)
</span>
<input
type="url"
x-model="formData.logo_dark"
placeholder="https://..."
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
<!-- Favicon URL -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Favicon URL
</span>
<input
type="url"
x-model="formData.favicon"
placeholder="https://..."
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
</div>
</div>
<!-- Platform Stats (Read Only) -->
<div class="mb-8 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Platform Statistics
</h3>
<div class="grid grid-cols-3 gap-4 text-center">
<div>
<p class="text-2xl font-bold text-purple-600 dark:text-purple-400" x-text="platform?.vendor_count || 0"></p>
<p class="text-sm text-gray-500 dark:text-gray-400">Vendors</p>
</div>
<div>
<p class="text-2xl font-bold text-blue-600 dark:text-blue-400" x-text="platform?.platform_pages_count || 0"></p>
<p class="text-sm text-gray-500 dark:text-gray-400">Marketing Pages</p>
</div>
<div>
<p class="text-2xl font-bold text-teal-600 dark:text-teal-400" x-text="platform?.vendor_defaults_count || 0"></p>
<p class="text-sm text-gray-500 dark:text-gray-400">Vendor Defaults</p>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="flex justify-end gap-3">
<a
href="/admin/platforms"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
>
Cancel
</a>
<button
type="submit"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
>
<span x-show="saving" x-html="$icon('refresh', 'w-4 h-4 mr-2 animate-spin')"></span>
<span x-text="saving ? 'Saving...' : 'Save Changes'"></span>
</button>
</div>
</form>
<!-- Timestamps -->
<div class="text-sm text-gray-500 dark:text-gray-400">
<p>
Created: <span x-text="formatDate(platform?.created_at)"></span>
</p>
<p>
Last Updated: <span x-text="formatDate(platform?.updated_at)"></span>
</p>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="/static/admin/js/platform-edit.js"></script>
{% endblock %}

View File

@@ -0,0 +1,200 @@
{# app/templates/admin/platform-menu-config.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
{% from 'shared/macros/headers.html' import page_header %}
{% block title %}Menu Configuration{% endblock %}
{% block alpine_data %}adminPlatformMenuConfig('{{ platform_code }}'){% endblock %}
{% block content %}
{{ page_header('Menu Configuration', back_url='/admin/platforms/' + platform_code) }}
{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }}
{{ error_state('Error', show_condition='error') }}
<!-- Platform Info -->
<div class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="platform?.name || 'Loading...'"></h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
Configure which menu items are visible for admins and vendors on this platform.
</p>
</div>
<span class="px-3 py-1 text-sm font-medium rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200" x-text="platform?.code?.toUpperCase()"></span>
</div>
</div>
<!-- Frontend Type Tabs -->
<div class="mb-6">
<div class="flex space-x-1 bg-gray-100 dark:bg-gray-700 rounded-lg p-1 w-fit">
<button
@click="frontendType = 'admin'; loadPlatformMenuConfig()"
:class="{
'bg-white dark:bg-gray-800 shadow': frontendType === 'admin',
'text-gray-600 dark:text-gray-400': frontendType !== 'admin'
}"
class="px-4 py-2 text-sm font-medium rounded-md transition-all"
>
<span x-html="$icon('shield', 'w-4 h-4 inline mr-2')"></span>
Admin Frontend
</button>
<button
@click="frontendType = 'vendor'; loadPlatformMenuConfig()"
:class="{
'bg-white dark:bg-gray-800 shadow': frontendType === 'vendor',
'text-gray-600 dark:text-gray-400': frontendType !== 'vendor'
}"
class="px-4 py-2 text-sm font-medium rounded-md transition-all"
>
<span x-html="$icon('shopping-bag', 'w-4 h-4 inline mr-2')"></span>
Vendor Frontend
</button>
</div>
</div>
<!-- Stats Cards -->
<div class="grid gap-4 mb-6 md:grid-cols-3">
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('view-grid', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Total Items</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="menuConfig?.total_items || 0"></p>
</div>
</div>
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
<span x-html="$icon('eye', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Visible</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="menuConfig?.visible_items || 0"></p>
</div>
</div>
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-gray-500 bg-gray-100 rounded-full dark:text-gray-100 dark:bg-gray-600">
<span x-html="$icon('eye-off', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Hidden</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="menuConfig?.hidden_items || 0"></p>
</div>
</div>
</div>
<!-- Actions -->
<div class="mb-4 flex items-center justify-between">
<p class="text-sm text-gray-500 dark:text-gray-400">
Toggle visibility for menu items. Mandatory items cannot be hidden.
</p>
<div class="flex gap-2">
<button
@click="showAll()"
:disabled="saving"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
>
<span x-html="$icon('eye', 'w-4 h-4 mr-2')"></span>
Show All
</button>
<button
@click="resetToDefaults()"
:disabled="saving"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
>
<span x-html="$icon('eye-off', 'w-4 h-4 mr-2')"></span>
Hide All
</button>
</div>
</div>
<!-- Loading State -->
<div x-show="loading" class="flex items-center justify-center py-12 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<span x-html="$icon('refresh', 'w-8 h-8 animate-spin text-purple-600')"></span>
<span class="ml-3 text-gray-500 dark:text-gray-400">Loading menu configuration...</span>
</div>
<!-- Menu Items by Section -->
<div x-show="!loading" class="space-y-6">
<template x-for="section in groupedItems" :key="section.id">
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
<!-- Section Header -->
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div class="flex items-center">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="section.label || 'Main'"></h3>
<span
x-show="section.isSuperAdminOnly"
class="ml-2 px-2 py-0.5 text-xs font-medium rounded bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"
>
Super Admin Only
</span>
</div>
<span class="text-xs text-gray-500 dark:text-gray-400" x-text="`${section.visibleCount}/${section.items.length} visible`"></span>
</div>
</div>
<!-- Section Items -->
<div class="divide-y divide-gray-100 dark:divide-gray-700">
<template x-for="item in section.items" :key="item.id">
<div class="flex items-center justify-between px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/30">
<div class="flex items-center">
<span x-html="$icon(item.icon, 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-200" x-text="item.label"></p>
<p class="text-xs text-gray-400 dark:text-gray-500" x-text="item.url"></p>
</div>
</div>
<div class="flex items-center gap-3">
<!-- Mandatory Badge -->
<span
x-show="item.is_mandatory"
class="px-2 py-0.5 text-xs font-medium rounded bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
>
Mandatory
</span>
<!-- Toggle Switch -->
<button
@click="toggleVisibility(item)"
:disabled="item.is_mandatory || saving"
:class="{
'bg-purple-600': item.is_visible,
'bg-gray-200 dark:bg-gray-600': !item.is_visible,
'opacity-50 cursor-not-allowed': item.is_mandatory
}"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
role="switch"
:aria-checked="item.is_visible"
>
<span
:class="{
'translate-x-5': item.is_visible,
'translate-x-0': !item.is_visible
}"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
></span>
</button>
</div>
</div>
</template>
</div>
</div>
</template>
<!-- Empty State -->
<div x-show="groupedItems.length === 0" class="bg-white rounded-lg shadow-xs dark:bg-gray-800 p-8 text-center">
<span x-html="$icon('view-grid', 'w-12 h-12 mx-auto text-gray-400')"></span>
<p class="mt-4 text-gray-500 dark:text-gray-400">No menu items configured for this frontend type.</p>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/platform-menu-config.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,282 @@
{# app/templates/admin/platform-modules.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state %}
{% from 'shared/macros/headers.html' import page_header %}
{% block title %}Module Configuration{% endblock %}
{% block alpine_data %}adminPlatformModules('{{ platform_code }}'){% endblock %}
{% block content %}
{{ page_header('Module Configuration', back_url='/admin/platforms/' + platform_code) }}
{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }}
{{ error_state('Error', show_condition='error') }}
<!-- Platform Info -->
<div class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="platform?.name || 'Loading...'"></h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
Enable or disable feature modules for this platform. Core modules cannot be disabled.
</p>
</div>
<span class="px-3 py-1 text-sm font-medium rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200" x-text="platform?.code?.toUpperCase()"></span>
</div>
</div>
<!-- Stats Cards -->
<div class="grid gap-4 mb-6 md:grid-cols-4">
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('puzzle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Total Modules</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="moduleConfig?.total || 0"></p>
</div>
</div>
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Enabled</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="moduleConfig?.enabled || 0"></p>
</div>
</div>
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-gray-500 bg-gray-100 rounded-full dark:text-gray-100 dark:bg-gray-600">
<span x-html="$icon('x-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Disabled</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="moduleConfig?.disabled || 0"></p>
</div>
</div>
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('shield', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-1 text-sm font-medium text-gray-600 dark:text-gray-400">Core Modules</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="coreModulesCount"></p>
</div>
</div>
</div>
<!-- Actions -->
<div class="mb-4 flex items-center justify-between">
<p class="text-sm text-gray-500 dark:text-gray-400">
Toggle modules on/off. Dependencies are resolved automatically.
</p>
<div class="flex gap-2">
<button
@click="enableAll()"
:disabled="saving"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
>
<span x-html="$icon('check-circle', 'w-4 h-4 mr-2')"></span>
Enable All
</button>
<button
@click="disableOptional()"
:disabled="saving"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
>
<span x-html="$icon('x-circle', 'w-4 h-4 mr-2')"></span>
Core Only
</button>
</div>
</div>
<!-- Loading State -->
<div x-show="loading" class="flex items-center justify-center py-12 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<span x-html="$icon('refresh', 'w-8 h-8 animate-spin text-purple-600')"></span>
<span class="ml-3 text-gray-500 dark:text-gray-400">Loading module configuration...</span>
</div>
<!-- Module Groups -->
<div x-show="!loading" class="space-y-6">
<!-- Core Modules -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
<div class="px-4 py-3 bg-purple-50 dark:bg-purple-900/20 border-b border-purple-200 dark:border-purple-800">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span x-html="$icon('shield', 'w-5 h-5 text-purple-600 dark:text-purple-400 mr-2')"></span>
<h3 class="text-sm font-semibold text-purple-800 dark:text-purple-200">Core Modules</h3>
<span class="ml-2 px-2 py-0.5 text-xs font-medium rounded bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
Always Enabled
</span>
</div>
<span class="text-xs text-purple-600 dark:text-purple-400" x-text="`${coreModulesCount} modules`"></span>
</div>
</div>
<div class="divide-y divide-gray-100 dark:divide-gray-700">
<template x-for="module in coreModules" :key="module.code">
<div class="flex items-center justify-between px-4 py-4 hover:bg-gray-50 dark:hover:bg-gray-700/30">
<div class="flex items-center flex-1">
<div class="p-2 mr-3 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
<span x-html="$icon(getModuleIcon(module.code), 'w-5 h-5 text-purple-600 dark:text-purple-400')"></span>
</div>
<div class="flex-1">
<p class="text-sm font-medium text-gray-700 dark:text-gray-200" x-text="module.name"></p>
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="module.description"></p>
<!-- Features -->
<div x-show="module.features?.length > 0" class="mt-1 flex flex-wrap gap-1">
<template x-for="feature in module.features.slice(0, 3)" :key="feature">
<span class="px-1.5 py-0.5 text-xs rounded bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400" x-text="feature"></span>
</template>
<span x-show="module.features?.length > 3" class="text-xs text-gray-400" x-text="`+${module.features.length - 3} more`"></span>
</div>
</div>
</div>
<div class="flex items-center gap-3 ml-4">
<!-- View Details Button -->
<a :href="`/admin/platforms/${platformCode}/modules/${module.code}`"
class="p-1.5 rounded-lg text-gray-500 hover:text-purple-600 hover:bg-purple-50 dark:text-gray-400 dark:hover:text-purple-400 dark:hover:bg-purple-900/20 transition-colors"
title="View Details">
<span x-html="$icon('eye', 'w-4 h-4')"></span>
</a>
<!-- Configure Button (if has config) -->
<a x-show="hasConfig(module.code)"
:href="`/admin/platforms/${platformCode}/modules/${module.code}/config`"
class="p-1.5 rounded-lg text-gray-500 hover:text-blue-600 hover:bg-blue-50 dark:text-gray-400 dark:hover:text-blue-400 dark:hover:bg-blue-900/20 transition-colors"
title="Configure">
<span x-html="$icon('cog', 'w-4 h-4')"></span>
</a>
<span class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Enabled
</span>
</div>
</div>
</template>
</div>
</div>
<!-- Optional Modules -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 overflow-hidden">
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span x-html="$icon('puzzle', 'w-5 h-5 text-gray-600 dark:text-gray-400 mr-2')"></span>
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200">Optional Modules</h3>
</div>
<span class="text-xs text-gray-500 dark:text-gray-400" x-text="`${enabledOptionalCount}/${optionalModules.length} enabled`"></span>
</div>
</div>
<div class="divide-y divide-gray-100 dark:divide-gray-700">
<template x-for="module in optionalModules" :key="module.code">
<div class="flex items-center justify-between px-4 py-4 hover:bg-gray-50 dark:hover:bg-gray-700/30">
<div class="flex items-center flex-1">
<div class="p-2 mr-3 rounded-lg"
:class="module.is_enabled ? 'bg-green-100 dark:bg-green-900/30' : 'bg-gray-100 dark:bg-gray-700'">
<span x-html="$icon(getModuleIcon(module.code), 'w-5 h-5')"
:class="module.is_enabled ? 'text-green-600 dark:text-green-400' : 'text-gray-400'"></span>
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<p class="text-sm font-medium text-gray-700 dark:text-gray-200" x-text="module.name"></p>
<!-- Dependencies Badge -->
<template x-if="module.requires?.length > 0">
<span class="px-1.5 py-0.5 text-xs rounded bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
:title="`Requires: ${module.requires.join(', ')}`">
<span x-html="$icon('link', 'w-3 h-3 inline')"></span>
<span x-text="module.requires.length"></span>
</span>
</template>
<!-- Dependents Badge -->
<template x-if="module.dependent_modules?.length > 0">
<span class="px-1.5 py-0.5 text-xs rounded bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
:title="`Required by: ${module.dependent_modules.join(', ')}`">
<span x-html="$icon('users', 'w-3 h-3 inline')"></span>
<span x-text="module.dependent_modules.length"></span>
</span>
</template>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="module.description"></p>
<!-- Dependencies Info -->
<div x-show="module.requires?.length > 0" class="mt-1">
<span class="text-xs text-amber-600 dark:text-amber-400">
Requires: <span x-text="module.requires.join(', ')"></span>
</span>
</div>
<!-- Features -->
<div x-show="module.features?.length > 0" class="mt-1 flex flex-wrap gap-1">
<template x-for="feature in module.features.slice(0, 3)" :key="feature">
<span class="px-1.5 py-0.5 text-xs rounded bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400" x-text="feature"></span>
</template>
<span x-show="module.features?.length > 3" class="text-xs text-gray-400" x-text="`+${module.features.length - 3} more`"></span>
</div>
</div>
</div>
<div class="flex items-center gap-3 ml-4">
<!-- View Details Button -->
<a :href="`/admin/platforms/${platformCode}/modules/${module.code}`"
class="p-1.5 rounded-lg text-gray-500 hover:text-purple-600 hover:bg-purple-50 dark:text-gray-400 dark:hover:text-purple-400 dark:hover:bg-purple-900/20 transition-colors"
title="View Details">
<span x-html="$icon('eye', 'w-4 h-4')"></span>
</a>
<!-- Configure Button (if has config) -->
<a x-show="hasConfig(module.code)"
:href="`/admin/platforms/${platformCode}/modules/${module.code}/config`"
class="p-1.5 rounded-lg text-gray-500 hover:text-blue-600 hover:bg-blue-50 dark:text-gray-400 dark:hover:text-blue-400 dark:hover:bg-blue-900/20 transition-colors"
title="Configure">
<span x-html="$icon('cog', 'w-4 h-4')"></span>
</a>
<!-- Status Badge -->
<span x-show="module.is_enabled"
class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Enabled
</span>
<span x-show="!module.is_enabled"
class="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
Disabled
</span>
<!-- Toggle Switch -->
<button
@click="toggleModule(module)"
:disabled="saving"
:class="{
'bg-purple-600': module.is_enabled,
'bg-gray-200 dark:bg-gray-600': !module.is_enabled
}"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
role="switch"
:aria-checked="module.is_enabled"
>
<span
:class="{
'translate-x-5': module.is_enabled,
'translate-x-0': !module.is_enabled
}"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
></span>
</button>
</div>
</div>
</template>
</div>
</div>
<!-- Empty State -->
<div x-show="moduleConfig?.modules?.length === 0" class="bg-white rounded-lg shadow-xs dark:bg-gray-800 p-8 text-center">
<span x-html="$icon('puzzle', 'w-12 h-12 mx-auto text-gray-400')"></span>
<p class="mt-4 text-gray-500 dark:text-gray-400">No modules available.</p>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/platform-modules.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,162 @@
{# app/templates/admin/platforms.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/headers.html' import page_header %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% block title %}Platforms{% endblock %}
{% block alpine_data %}platformsManager(){% endblock %}
{% block content %}
{{ page_header('Platforms', subtitle='Manage platform configurations for OMS, Loyalty, and other business offerings') }}
{{ loading_state('Loading platforms...') }}
{{ error_state('Error loading platforms') }}
<!-- Platforms Grid -->
<div x-show="!loading && platforms.length > 0" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-3">
<template x-for="platform in platforms" :key="platform.id">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
<!-- Platform Header -->
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div class="flex items-center">
<!-- Platform Icon -->
<div class="flex items-center justify-center w-12 h-12 rounded-lg bg-purple-100 dark:bg-purple-900/50">
<span x-html="$icon(getPlatformIcon(platform.code), 'w-6 h-6 text-purple-600 dark:text-purple-400')"></span>
</div>
<div class="ml-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white" x-text="platform.name"></h3>
<code class="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded" x-text="platform.code"></code>
</div>
</div>
<!-- Status Badge -->
<span
class="px-3 py-1 text-xs font-semibold rounded-full"
:class="platform.is_active ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'"
x-text="platform.is_active ? 'Active' : 'Inactive'"
></span>
</div>
<p class="mt-3 text-sm text-gray-600 dark:text-gray-400" x-text="platform.description || 'No description'"></p>
</div>
<!-- Platform Stats -->
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-800/50">
<div class="grid grid-cols-3 gap-4 text-center">
<div>
<p class="text-2xl font-bold text-purple-600 dark:text-purple-400" x-text="platform.vendor_count"></p>
<p class="text-xs text-gray-500 dark:text-gray-400">Vendors</p>
</div>
<div>
<p class="text-2xl font-bold text-blue-600 dark:text-blue-400" x-text="platform.platform_pages_count"></p>
<p class="text-xs text-gray-500 dark:text-gray-400">Marketing Pages</p>
</div>
<div>
<p class="text-2xl font-bold text-teal-600 dark:text-teal-400" x-text="platform.vendor_defaults_count"></p>
<p class="text-xs text-gray-500 dark:text-gray-400">Vendor Defaults</p>
</div>
</div>
</div>
<!-- Platform Info -->
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
<div class="space-y-2 text-sm">
<div class="flex justify-between" x-show="platform.domain">
<span class="text-gray-500 dark:text-gray-400">Domain:</span>
<span class="text-gray-900 dark:text-white" x-text="platform.domain"></span>
</div>
<div class="flex justify-between" x-show="platform.path_prefix">
<span class="text-gray-500 dark:text-gray-400">Path Prefix:</span>
<code class="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded" x-text="platform.path_prefix"></code>
</div>
<div class="flex justify-between">
<span class="text-gray-500 dark:text-gray-400">Language:</span>
<span class="text-gray-900 dark:text-white" x-text="platform.default_language.toUpperCase()"></span>
</div>
</div>
</div>
<!-- Platform Actions -->
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<div class="flex justify-between items-center">
<a
:href="`/admin/platforms/${platform.code}`"
class="inline-flex items-center text-sm font-medium text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300"
>
<span x-html="$icon('eye', 'w-4 h-4 mr-1')"></span>
View Details
</a>
<div class="flex space-x-2">
<a
:href="`/admin/content-pages?platform_code=${platform.code}&slug=home`"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
title="Edit platform homepage"
>
<span x-html="$icon('home', 'w-4 h-4 mr-1')"></span>
Homepage
</a>
<a
:href="`/admin/content-pages?platform_code=${platform.code}`"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
title="View all content pages for this platform"
>
<span x-html="$icon('document-text', 'w-4 h-4 mr-1')"></span>
Pages
</a>
<a
:href="`/admin/platforms/${platform.code}/edit`"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
title="Edit platform settings"
>
<span x-html="$icon('cog', 'w-4 h-4 mr-1')"></span>
Settings
</a>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- Empty State -->
<div x-show="!loading && platforms.length === 0" class="text-center py-12 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<span x-html="$icon('globe-alt', 'inline w-16 h-16 text-gray-400 mb-4')"></span>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">No platforms found</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4">
No platforms have been configured yet.
</p>
</div>
<!-- Page Tier Legend -->
<div x-show="!loading && platforms.length > 0" class="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-4">Content Page Tiers</h4>
<div class="grid md:grid-cols-3 gap-6">
<div class="flex items-start">
<span class="inline-block w-3 h-3 rounded-full bg-blue-500 mt-1.5 mr-3"></span>
<div>
<p class="font-medium text-gray-900 dark:text-white">Platform Marketing Pages</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Public pages like homepage, pricing, features. Not inherited by vendors.</p>
</div>
</div>
<div class="flex items-start">
<span class="inline-block w-3 h-3 rounded-full bg-teal-500 mt-1.5 mr-3"></span>
<div>
<p class="font-medium text-gray-900 dark:text-white">Vendor Defaults</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Default pages inherited by all vendors (about, terms, privacy).</p>
</div>
</div>
<div class="flex items-start">
<span class="inline-block w-3 h-3 rounded-full bg-purple-500 mt-1.5 mr-3"></span>
<div>
<p class="font-medium text-gray-900 dark:text-white">Vendor Overrides</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Custom pages created by individual vendors.</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/platforms.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,121 @@
{# app/templates/admin/select-platform.html #}
{# standalone - This template does not extend base.html because it's shown before platform selection #}
<!DOCTYPE html>
<html :class="{ 'dark': dark }" x-data="selectPlatform()" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Select Platform - Admin Panel</title>
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="{{ url_for('static', path='admin/css/tailwind.output.css') }}" />
<style>[x-cloak] { display: none !important; }</style>
</head>
<body x-cloak>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900">
<div class="flex-1 h-full max-w-xl mx-auto overflow-hidden bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="flex flex-col overflow-y-auto">
<div class="flex items-center justify-center p-6 sm:p-12">
<div class="w-full">
<!-- Header -->
<div class="text-center mb-8">
<h1 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
Select Platform
</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
Choose a platform to manage
</p>
</div>
<!-- Loading State -->
<div x-show="loading" class="flex justify-center py-8">
<span x-html="$icon('spinner', 'h-8 w-8 text-purple-600')"></span>
</div>
<!-- Error State -->
<div x-show="error" x-cloak class="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p class="text-red-700 dark:text-red-400" x-text="error"></p>
</div>
<!-- Super Admin Notice -->
<div x-show="isSuperAdmin && !loading" x-cloak class="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p class="text-blue-700 dark:text-blue-400">
You are a Super Admin with access to all platforms. Redirecting to dashboard...
</p>
</div>
<!-- Platform List -->
<div x-show="!loading && !isSuperAdmin && platforms.length > 0" x-cloak class="space-y-3">
<template x-for="platform in platforms" :key="platform.id">
<button
@click="selectPlatform(platform)"
:disabled="selecting"
class="w-full flex items-center p-4 bg-gray-50 dark:bg-gray-700 rounded-lg border-2 border-transparent hover:border-purple-500 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<!-- Platform Icon/Logo -->
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center mr-4">
<template x-if="platform.logo">
<img :src="platform.logo" :alt="platform.name" class="w-8 h-8 object-contain">
</template>
<template x-if="!platform.logo">
<span class="text-xl font-bold text-purple-600 dark:text-purple-400" x-text="platform.code.charAt(0).toUpperCase()"></span>
</template>
</div>
<!-- Platform Info -->
<div class="flex-1 text-left">
<h3 class="text-lg font-medium text-gray-800 dark:text-gray-200" x-text="platform.name"></h3>
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="platform.code"></p>
</div>
<!-- Arrow -->
<div class="flex-shrink-0 ml-4">
<span x-html="$icon('chevron-right', 'w-5 h-5 text-gray-400')"></span>
</div>
</button>
</template>
</div>
<!-- No Platforms -->
<div x-show="!loading && !isSuperAdmin && platforms.length === 0" x-cloak class="text-center py-8">
<span x-html="$icon('exclamation', 'mx-auto h-12 w-12 text-gray-400')"></span>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-200">No platforms assigned</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Contact your administrator to get platform access.</p>
</div>
<!-- Logout Link -->
<div class="mt-8 text-center">
<button
@click="logout()"
class="text-sm text-gray-600 dark:text-gray-400 hover:text-purple-600 dark:hover:text-purple-400 underline"
>
Sign out and use a different account
</button>
</div>
<!-- Theme Toggle -->
<div class="mt-4 flex justify-center">
<button
@click="toggleDarkMode()"
class="p-2 text-gray-500 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
aria-label="Toggle dark mode"
>
<span x-show="!dark" x-html="$icon('moon', 'w-5 h-5')"></span>
<span x-show="dark" x-html="$icon('sun', 'w-5 h-5')"></span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>
<script src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
<script src="{{ url_for('static', path='shared/js/icons.js') }}"></script>
<script src="{{ url_for('tenancy_static', path='admin/js/select-platform.js') }}"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,193 @@
{# app/templates/admin/user-create.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/headers.html' import page_header %}
{% block title %}Create Admin User{% endblock %}
{% block alpine_data %}adminUserCreate(){% endblock %}
{% block content %}
{{ page_header('Create Admin User', subtitle='Add a new admin user to manage platforms', back_url='/admin/admin-users', back_label='Back to Admin Users') }}
<!-- Create Form -->
<form @submit.prevent="handleSubmit" class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Left Column: Account Info -->
<div>
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Account Information
</h3>
<!-- Username -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Username <span class="text-red-600">*</span>
</span>
<input
type="text"
x-model="formData.username"
required
maxlength="50"
:disabled="saving"
placeholder="johndoe"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.username }"
>
<span x-show="errors.username" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.username"></span>
<span x-show="!errors.username" class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Letters, numbers, and underscores only
</span>
</label>
<!-- Email -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Email <span class="text-red-600">*</span>
</span>
<input
type="email"
x-model="formData.email"
required
:disabled="saving"
placeholder="john@example.com"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.email }"
>
<span x-show="errors.email" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.email"></span>
</label>
<!-- Password -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Password <span class="text-red-600">*</span>
</span>
<input
type="password"
x-model="formData.password"
required
minlength="6"
:disabled="saving"
placeholder="Minimum 6 characters"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.password }"
>
<span x-show="errors.password" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.password"></span>
</label>
<!-- Admin Settings -->
<div class="mt-4 p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
<h4 class="text-sm font-medium text-purple-800 dark:text-purple-300 mb-3">Admin Settings</h4>
<!-- Super Admin Toggle -->
<label class="flex items-center mb-4">
<input
type="checkbox"
x-model="formData.is_super_admin"
:disabled="saving"
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700"
>
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">
Super Admin
</span>
</label>
<p class="text-xs text-gray-600 dark:text-gray-400 mb-4 -mt-2 ml-6">
Super admins have access to all platforms and can manage other admins.
</p>
<!-- Platform Assignment (only if not super admin) -->
<template x-if="!formData.is_super_admin">
<div>
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">
Assigned Platforms <span class="text-red-600">*</span>
</span>
<div class="mt-2 space-y-2 max-h-48 overflow-y-auto">
<template x-for="platform in platforms" :key="platform.id">
<label class="flex items-center p-2 rounded hover:bg-purple-100 dark:hover:bg-purple-900/30 cursor-pointer">
<input
type="checkbox"
:value="platform.id"
x-model="formData.platform_ids"
:disabled="saving"
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700"
>
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300" x-text="platform.name"></span>
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400" x-text="'(' + platform.code + ')'"></span>
</label>
</template>
</div>
<p x-show="platforms.length === 0" class="text-sm text-gray-500 dark:text-gray-400 mt-2">
No platforms available. Create a platform first.
</p>
<span x-show="errors.platform_ids" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.platform_ids"></span>
</label>
</div>
</template>
</div>
</div>
<!-- Right Column: Personal Info -->
<div>
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Personal Information
</h3>
<!-- First Name -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
First Name
</span>
<input
type="text"
x-model="formData.first_name"
maxlength="100"
:disabled="saving"
placeholder="John"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
<!-- Last Name -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Last Name
</span>
<input
type="text"
x-model="formData.last_name"
maxlength="100"
:disabled="saving"
placeholder="Doe"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
</div>
</div>
<!-- Submit Button -->
<div class="flex items-center justify-end gap-3 pt-6 border-t dark:border-gray-700">
<a
href="/admin/admin-users"
class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none">
Cancel
</a>
<button
type="submit"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!saving">
<span x-html="$icon('user-plus', 'w-4 h-4 mr-2 inline')"></span>
Create Admin User
</span>
<span x-show="saving" class="flex items-center">
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
Creating...
</span>
</button>
</div>
</form>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/user-create.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,283 @@
{# app/templates/admin/vendor-create.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/headers.html' import page_header %}
{% from 'shared/macros/alerts.html' import error_state %}
{% block title %}Create Vendor{% endblock %}
{% block alpine_data %}adminVendorCreate(){% endblock %}
{% block content %}
{{ page_header('Create New Vendor', subtitle='Create a vendor (storefront/brand) under an existing company', back_url='/admin/vendors', back_label='Back to Vendors') }}
{# noqa: FE-003 - Custom success message with nested template #}
<!-- Success Message -->
<div x-show="successMessage" x-cloak class="mb-6 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg">
<div class="flex items-start">
<span x-html="$icon('check-circle', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
<div class="flex-1">
<p class="font-semibold">Vendor Created Successfully!</p>
<template x-if="createdVendor">
<div class="mt-2 p-3 bg-white rounded border border-green-300">
<p class="text-sm font-semibold mb-2">Vendor Details:</p>
<div class="space-y-1 text-sm">
<div><span class="font-bold">Vendor Code:</span> <span x-text="createdVendor.vendor_code"></span></div>
<div><span class="font-bold">Name:</span> <span x-text="createdVendor.name"></span></div>
<div><span class="font-bold">Subdomain:</span> <span x-text="createdVendor.subdomain"></span></div>
<div><span class="font-bold">Company:</span> <span x-text="createdVendor.company_name"></span></div>
</div>
</div>
</template>
</div>
</div>
</div>
{{ error_state('Error Creating Vendor', error_var='errorMessage', show_condition='errorMessage') }}
<!-- Loading Companies -->
<div x-show="loadingCompanies" class="mb-6 p-4 bg-blue-50 border border-blue-200 text-blue-700 rounded-lg">
<div class="flex items-center">
<span x-html="$icon('spinner', 'w-5 h-5 mr-3 animate-spin')"></span>
<span>Loading companies...</span>
</div>
</div>
<!-- Create Vendor Form -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<form @submit.prevent="createVendor">
<!-- Parent Company Selection -->
<div class="mb-6">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Parent Company</h3>
<div class="p-4 mb-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
<p class="text-sm text-blue-800 dark:text-blue-300">
<span x-html="$icon('information-circle', 'w-4 h-4 inline mr-1')"></span>
Vendors are storefronts/brands under a company. Select the parent company for this vendor.
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Company <span class="text-red-500">*</span>
</label>
<select
x-model="formData.company_id"
required
:disabled="loadingCompanies"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
>
<option value="">Select a company...</option>
<template x-for="company in companies" :key="company.id">
<option :value="company.id" x-text="`${company.name} (ID: ${company.id})`"></option>
</template>
</select>
<p class="mt-1 text-xs text-gray-500">The company this vendor belongs to</p>
</div>
</div>
<!-- Vendor Information Section -->
<div class="mb-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Vendor Information</h3>
<div class="grid gap-6 mb-4 md:grid-cols-2">
<!-- Vendor Code -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Vendor Code <span class="text-red-500">*</span>
</label>
<input
type="text"
x-model="formData.vendor_code"
required
minlength="2"
maxlength="50"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600 uppercase"
placeholder="TECHSTORE"
@input="formData.vendor_code = $event.target.value.toUpperCase()"
/>
<p class="mt-1 text-xs text-gray-500">Unique identifier (uppercase, 2-50 chars)</p>
</div>
<!-- Subdomain -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Subdomain <span class="text-red-500">*</span>
</label>
<div class="flex items-center">
<input
type="text"
x-model="formData.subdomain"
required
minlength="2"
maxlength="100"
pattern="[a-z0-9][a-z0-9-]*[a-z0-9]"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="techstore"
@input="formData.subdomain = $event.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '')"
/>
<span class="ml-2 text-sm text-gray-500">.example.com</span>
</div>
<p class="mt-1 text-xs text-gray-500">Lowercase letters, numbers, and hyphens only</p>
</div>
</div>
<div class="grid gap-6 mb-4 md:grid-cols-2">
<!-- Vendor Name -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Display Name <span class="text-red-500">*</span>
</label>
<input
type="text"
x-model="formData.name"
required
minlength="2"
maxlength="255"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="Tech Store Luxembourg"
/>
</div>
<!-- Auto-generate subdomain from name -->
<div class="flex items-end">
<button
type="button"
@click="autoGenerateSubdomain()"
class="px-4 py-2 text-sm font-medium text-purple-600 border border-purple-600 rounded-lg hover:bg-purple-50 dark:hover:bg-gray-700"
>
<span class="flex items-center">
<span x-html="$icon('sparkles', 'w-4 h-4 mr-2')"></span>
Auto-generate Subdomain
</span>
</button>
</div>
</div>
<!-- Description -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
Description
</label>
<textarea
x-model="formData.description"
rows="3"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="Brief description of the vendor/brand..."
></textarea>
</div>
</div>
<!-- Platform Selection Section -->
<div class="mb-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Platform Access</h3>
<div class="p-4 mb-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
<p class="text-sm text-blue-800 dark:text-blue-300">
<span x-html="$icon('information-circle', 'w-4 h-4 inline mr-1')"></span>
Select which platforms this vendor should have access to. Each platform can have different settings and features.
</p>
</div>
<div class="space-y-2 max-h-48 overflow-y-auto">
<template x-for="platform in platforms" :key="platform.id">
<label class="flex items-center p-3 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-purple-50 dark:hover:bg-purple-900/20 cursor-pointer transition-colors">
<input
type="checkbox"
:value="platform.id"
x-model="formData.platform_ids"
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700"
>
<div class="ml-3 flex-1">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="platform.name"></span>
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400" x-text="'(' + platform.code + ')'"></span>
</div>
<template x-if="platform.description">
<span class="text-xs text-gray-500 dark:text-gray-400" x-text="platform.description"></span>
</template>
</label>
</template>
</div>
<p x-show="platforms.length === 0" class="text-sm text-gray-500 dark:text-gray-400 mt-2">
No platforms available. Create a platform first.
</p>
<p x-show="formData.platform_ids.length === 0 && platforms.length > 0" class="text-xs text-amber-600 dark:text-amber-400 mt-2">
<span x-html="$icon('exclamation-triangle', 'w-4 h-4 inline mr-1')"></span>
Select at least one platform for the vendor to be accessible.
</p>
</div>
<!-- Marketplace URLs Section (Optional) -->
<div class="mb-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Marketplace URLs (Optional)</h3>
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
CSV feed URLs for product import from Letzshop marketplace
</p>
<div class="grid gap-6 md:grid-cols-1">
<!-- French CSV URL -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
French CSV URL
</label>
<input
type="url"
x-model="formData.letzshop_csv_url_fr"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="https://letzshop.lu/feeds/vendor-fr.csv"
/>
</div>
<!-- English CSV URL -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
English CSV URL
</label>
<input
type="url"
x-model="formData.letzshop_csv_url_en"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="https://letzshop.lu/feeds/vendor-en.csv"
/>
</div>
<!-- German CSV URL -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">
German CSV URL
</label>
<input
type="url"
x-model="formData.letzshop_csv_url_de"
class="block w-full px-3 py-2 text-sm border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-600"
placeholder="https://letzshop.lu/feeds/vendor-de.csv"
/>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="flex items-center justify-between pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
@click="window.location.href='/admin/vendors'"
class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-500 focus:outline-none focus:shadow-outline-gray"
>
Cancel
</button>
<button
type="submit"
:disabled="loading || loadingCompanies || !formData.company_id"
class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!loading">Create Vendor</span>
<span x-show="loading" class="flex items-center">
<span x-html="$icon('spinner', 'w-4 h-4 mr-2 animate-spin')"></span>
Creating...
</span>
</button>
</div>
</form>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="/static/admin/js/vendor-create.js"></script>
{% endblock %}

View File

@@ -0,0 +1,420 @@
{# app/templates/admin/vendor-detail.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/headers.html' import detail_page_header %}
{% block title %}Vendor Details{% endblock %}
{% block alpine_data %}adminVendorDetail(){% endblock %}
{% block content %}
{% call detail_page_header("vendor?.name || 'Vendor Details'", '/admin/vendors', subtitle_show='vendor') %}
<span x-text="vendorCode"></span>
<span class="text-gray-400 mx-2"></span>
<span x-text="vendor?.subdomain"></span>
{% endcall %}
{{ loading_state('Loading vendor details...') }}
{{ error_state('Error loading vendor') }}
<!-- Vendor Details -->
<div x-show="!loading && vendor">
<!-- Quick Actions Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Quick Actions
</h3>
<div class="flex flex-wrap items-center gap-3">
<a
:href="`/admin/vendors/${vendorCode}/edit`"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
<span x-html="$icon('edit', 'w-4 h-4 mr-2')"></span>
Edit Vendor
</a>
<button
@click="deleteVendor()"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:shadow-outline-red">
<span x-html="$icon('delete', 'w-4 h-4 mr-2')"></span>
Delete Vendor
</button>
</div>
</div>
<!-- Status Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-4">
<!-- Verification Status -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 rounded-full"
:class="vendor?.is_verified ? 'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500' : 'text-orange-500 bg-orange-100 dark:text-orange-100 dark:bg-orange-500'">
<span x-html="$icon(vendor?.is_verified ? 'badge-check' : 'clock', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Verification
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="vendor?.is_verified ? 'Verified' : 'Pending'">
-
</p>
</div>
</div>
<!-- Active Status -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 rounded-full"
:class="vendor?.is_active ? 'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500' : 'text-red-500 bg-red-100 dark:text-red-100 dark:bg-red-500'">
<span x-html="$icon(vendor?.is_active ? 'check-circle' : 'x-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Status
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="vendor?.is_active ? 'Active' : 'Inactive'">
-
</p>
</div>
</div>
<!-- Created Date -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('calendar', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Created
</p>
<p class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="formatDate(vendor?.created_at)">
-
</p>
</div>
</div>
<!-- Updated Date -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('refresh', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Last Updated
</p>
<p class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="formatDate(vendor?.updated_at)">
-
</p>
</div>
</div>
</div>
<!-- Subscription Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="subscription">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
Subscription
</h3>
<button
@click="showSubscriptionModal = true"
class="flex items-center px-3 py-1.5 text-sm font-medium text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300">
<span x-html="$icon('edit', 'w-4 h-4 mr-1')"></span>
Edit
</button>
</div>
<!-- Tier and Status -->
<div class="flex flex-wrap items-center gap-4 mb-4">
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600 dark:text-gray-400">Tier:</span>
<span class="px-2.5 py-0.5 text-sm font-medium rounded-full"
:class="{
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300': subscription?.tier === 'essential',
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': subscription?.tier === 'professional',
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300': subscription?.tier === 'business',
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300': subscription?.tier === 'enterprise'
}"
x-text="subscription?.tier ? subscription.tier.charAt(0).toUpperCase() + subscription.tier.slice(1) : '-'">
</span>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600 dark:text-gray-400">Status:</span>
<span class="px-2.5 py-0.5 text-sm font-medium rounded-full"
:class="{
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300': subscription?.status === 'active',
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': subscription?.status === 'trial',
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300': subscription?.status === 'past_due',
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300': subscription?.status === 'cancelled' || subscription?.status === 'expired'
}"
x-text="subscription?.status ? subscription.status.replace('_', ' ').charAt(0).toUpperCase() + subscription.status.slice(1) : '-'">
</span>
</div>
<template x-if="subscription?.is_annual">
<span class="px-2.5 py-0.5 text-xs font-medium text-purple-800 bg-purple-100 rounded-full dark:bg-purple-900 dark:text-purple-300">
Annual
</span>
</template>
</div>
<!-- Period Info -->
<div class="flex flex-wrap gap-4 mb-4 text-sm">
<div>
<span class="text-gray-600 dark:text-gray-400">Period:</span>
<span class="ml-1 text-gray-700 dark:text-gray-300" x-text="formatDate(subscription?.period_start)"></span>
<span class="text-gray-400"></span>
<span class="text-gray-700 dark:text-gray-300" x-text="formatDate(subscription?.period_end)"></span>
</div>
<template x-if="subscription?.trial_ends_at">
<div>
<span class="text-gray-600 dark:text-gray-400">Trial ends:</span>
<span class="ml-1 text-gray-700 dark:text-gray-300" x-text="formatDate(subscription?.trial_ends_at)"></span>
</div>
</template>
</div>
<!-- Usage Meters -->
<div class="grid gap-4 md:grid-cols-3">
<!-- Orders Usage -->
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700">
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">Orders This Period</span>
</div>
<div class="flex items-baseline gap-1">
<span class="text-xl font-bold text-gray-700 dark:text-gray-200" x-text="subscription?.orders_this_period || 0"></span>
<span class="text-sm text-gray-500 dark:text-gray-400">
/ <span x-text="subscription?.orders_limit || '∞'"></span>
</span>
</div>
<div class="mt-2 h-1.5 bg-gray-200 rounded-full dark:bg-gray-600" x-show="subscription?.orders_limit">
<div class="h-1.5 rounded-full transition-all"
:class="getUsageBarColor(subscription?.orders_this_period, subscription?.orders_limit)"
:style="`width: ${Math.min(100, (subscription?.orders_this_period / subscription?.orders_limit) * 100)}%`">
</div>
</div>
</div>
<!-- Products Usage -->
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700">
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">Products</span>
</div>
<div class="flex items-baseline gap-1">
<span class="text-xl font-bold text-gray-700 dark:text-gray-200" x-text="subscription?.products_count || 0"></span>
<span class="text-sm text-gray-500 dark:text-gray-400">
/ <span x-text="subscription?.products_limit || '∞'"></span>
</span>
</div>
<div class="mt-2 h-1.5 bg-gray-200 rounded-full dark:bg-gray-600" x-show="subscription?.products_limit">
<div class="h-1.5 rounded-full transition-all"
:class="getUsageBarColor(subscription?.products_count, subscription?.products_limit)"
:style="`width: ${Math.min(100, (subscription?.products_count / subscription?.products_limit) * 100)}%`">
</div>
</div>
</div>
<!-- Team Members Usage -->
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700">
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">Team Members</span>
</div>
<div class="flex items-baseline gap-1">
<span class="text-xl font-bold text-gray-700 dark:text-gray-200" x-text="subscription?.team_count || 0"></span>
<span class="text-sm text-gray-500 dark:text-gray-400">
/ <span x-text="subscription?.team_members_limit || '∞'"></span>
</span>
</div>
<div class="mt-2 h-1.5 bg-gray-200 rounded-full dark:bg-gray-600" x-show="subscription?.team_members_limit">
<div class="h-1.5 rounded-full transition-all"
:class="getUsageBarColor(subscription?.team_count, subscription?.team_members_limit)"
:style="`width: ${Math.min(100, (subscription?.team_count / subscription?.team_members_limit) * 100)}%`">
</div>
</div>
</div>
</div>
</div>
<!-- No Subscription Notice -->
<div class="px-4 py-3 mb-6 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-900/20 dark:border-yellow-800" x-show="!subscription && !loading">
<div class="flex items-center gap-3">
<span x-html="$icon('exclamation', 'w-5 h-5 text-yellow-600 dark:text-yellow-400')"></span>
<div>
<p class="text-sm font-medium text-yellow-800 dark:text-yellow-200">No Subscription Found</p>
<p class="text-sm text-yellow-700 dark:text-yellow-300">This vendor doesn't have a subscription yet.</p>
</div>
<button
@click="createSubscription()"
class="ml-auto px-3 py-1.5 text-sm font-medium text-white bg-yellow-600 rounded-lg hover:bg-yellow-700">
Create Subscription
</button>
</div>
</div>
<!-- Main Info Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Basic Information -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Basic Information
</h3>
<div class="space-y-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Vendor Code</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="vendor?.vendor_code || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Name</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="vendor?.name || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Subdomain</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="vendor?.subdomain || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Description</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="vendor?.description || 'No description provided'">-</p>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Contact Information
</h3>
<div class="space-y-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Owner Email</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="vendor?.owner_email || '-'">-</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Owner's authentication email</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Contact Email</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="vendor?.contact_email || '-'">-</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Public business contact</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Phone</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="vendor?.contact_phone || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Website</p>
<a
x-show="vendor?.website"
:href="vendor?.website"
target="_blank"
class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400"
x-text="vendor?.website">
</a>
<span x-show="!vendor?.website" class="text-sm text-gray-700 dark:text-gray-300">-</span>
</div>
</div>
</div>
</div>
<!-- Business Details -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Business Details
</h3>
<div class="grid gap-6 md:grid-cols-2">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Business Address</p>
<p class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-line" x-text="vendor?.business_address || 'No address provided'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Tax Number</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="vendor?.tax_number || 'Not provided'">-</p>
</div>
</div>
</div>
<!-- Owner Information -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Owner Information
</h3>
<div class="grid gap-6 md:grid-cols-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Owner User ID</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="vendor?.owner_user_id || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Owner Username</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="vendor?.owner_username || '-'">-</p>
</div>
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2">Owner Email</p>
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="vendor?.owner_email || '-'">-</p>
</div>
</div>
</div>
<!-- Marketplace URLs -->
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="vendor?.letzshop_csv_url_fr || vendor?.letzshop_csv_url_en || vendor?.letzshop_csv_url_de">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Marketplace CSV URLs
</h3>
<div class="space-y-3">
<div x-show="vendor?.letzshop_csv_url_fr">
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-1">French (FR)</p>
<a
:href="vendor?.letzshop_csv_url_fr"
target="_blank"
class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 break-all"
x-text="vendor?.letzshop_csv_url_fr">
</a>
</div>
<div x-show="vendor?.letzshop_csv_url_en">
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-1">English (EN)</p>
<a
:href="vendor?.letzshop_csv_url_en"
target="_blank"
class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 break-all"
x-text="vendor?.letzshop_csv_url_en">
</a>
</div>
<div x-show="vendor?.letzshop_csv_url_de">
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-1">German (DE)</p>
<a
:href="vendor?.letzshop_csv_url_de"
target="_blank"
class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 break-all"
x-text="vendor?.letzshop_csv_url_de">
</a>
</div>
</div>
</div>
<!-- More Actions -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
More Actions
</h3>
<div class="flex flex-wrap gap-3">
<!-- View Parent Company -->
<a
:href="'/admin/companies/' + vendor?.company_id + '/edit'"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-150 bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 focus:outline-none focus:shadow-outline-blue"
>
<span x-html="$icon('office-building', 'w-4 h-4 mr-2')"></span>
View Parent Company
</a>
<!-- Customize Theme -->
<a
:href="`/admin/vendors/${vendorCode}/theme`"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple"
>
<span x-html="$icon('color-swatch', 'w-4 h-4 mr-2')"></span>
Customize Theme
</a>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
<span x-html="$icon('information-circle', 'w-4 h-4 inline mr-1')"></span>
This vendor belongs to company: <strong x-text="vendor?.company_name"></strong>.
Contact info and ownership are managed at the company level.
</p>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/vendor-detail.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,432 @@
{# app/templates/admin/vendor-edit.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import loading_state %}
{% from 'shared/macros/headers.html' import edit_page_header %}
{% block title %}Edit Vendor{% endblock %}
{% block alpine_data %}adminVendorEdit(){% endblock %}
{% block content %}
{% call edit_page_header('Edit Vendor', '/admin/vendors', subtitle_show='vendor', back_label='Back to Vendors') %}
<span x-text="vendor?.name"></span>
<span class="text-gray-400"></span>
<span x-text="vendor?.vendor_code"></span>
{% endcall %}
{{ loading_state('Loading vendor...', show_condition='loadingVendor') }}
<!-- Edit Form -->
<div x-show="!loadingVendor && vendor">
<!-- Quick Actions Card -->
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Quick Actions
</h3>
<div class="flex flex-wrap items-center gap-3">
<button
@click="toggleVerification()"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
:class="vendor?.is_verified ? 'bg-orange-600 hover:bg-orange-700' : 'bg-green-600 hover:bg-green-700'">
<span x-html="$icon(vendor?.is_verified ? 'x-circle' : 'badge-check', 'w-4 h-4 mr-2')"></span>
<span x-text="vendor?.is_verified ? 'Unverify Vendor' : 'Verify Vendor'"></span>
</button>
<button
@click="toggleActive()"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
:class="vendor?.is_active ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700'">
<span x-html="$icon(vendor?.is_active ? 'lock-closed' : 'lock-open', 'w-4 h-4 mr-2')"></span>
<span x-text="vendor?.is_active ? 'Deactivate' : 'Activate'"></span>
</button>
<!-- Status Badges -->
<div class="ml-auto flex items-center gap-2">
<span
x-show="vendor?.is_verified"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
<span x-html="$icon('badge-check', 'w-3 h-3 mr-1')"></span>
Verified
</span>
<span
x-show="!vendor?.is_verified"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-orange-700 bg-orange-100 rounded-full dark:bg-orange-700 dark:text-orange-100">
<span x-html="$icon('clock', 'w-3 h-3 mr-1')"></span>
Pending
</span>
<span
x-show="vendor?.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
Active
</span>
<span
x-show="!vendor?.is_active"
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-red-700 bg-red-100 rounded-full dark:bg-red-700 dark:text-red-100">
Inactive
</span>
</div>
</div>
</div>
<!-- Form Card -->
<form @submit.prevent="handleSubmit" class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="grid gap-6 mb-8 md:grid-cols-2">
<!-- Left Column: Basic Info -->
<div>
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Basic Information
</h3>
<!-- Vendor Code (readonly) -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Vendor Code
</span>
<input
type="text"
:value="vendor?.vendor_code || ''"
disabled
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Cannot be changed after creation
</span>
</label>
<!-- Name -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Vendor Name <span class="text-red-600">*</span>
</span>
<input
type="text"
x-model="formData.name"
required
maxlength="255"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.name }"
>
<span x-show="errors.name" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.name"></span>
</label>
<!-- Subdomain -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Subdomain <span class="text-red-600">*</span>
</span>
<input
type="text"
x-model="formData.subdomain"
@input="formatSubdomain()"
required
maxlength="100"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.subdomain }"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Lowercase letters, numbers, and hyphens only
</span>
<span x-show="errors.subdomain" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.subdomain"></span>
</label>
<!-- Description -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Description
</span>
<textarea
x-model="formData.description"
rows="3"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
></textarea>
</label>
</div>
<!-- Right Column: Contact Info -->
<div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
Contact Information
</h3>
<button
type="button"
@click="resetAllContactToCompany()"
:disabled="saving || !hasAnyContactOverride()"
class="text-xs px-2 py-1 text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-300 disabled:opacity-50 disabled:cursor-not-allowed"
title="Reset all contact fields to inherit from company">
<span x-html="$icon('refresh', 'w-3 h-3 inline mr-1')"></span>
Reset All to Company
</button>
</div>
<!-- Owner Email (readonly) -->
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Owner Email
</span>
<input
type="email"
:value="vendor?.owner_email || ''"
disabled
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Owner's authentication email
</span>
</label>
<!-- Contact Email -->
<label class="block mb-4 text-sm">
<div class="flex items-center justify-between">
<span class="text-gray-700 dark:text-gray-400">
Contact Email
<span x-show="!formData.contact_email"
class="ml-1 text-xs text-purple-500 dark:text-purple-400"
title="Inherited from company">
(from company)
</span>
</span>
<button
type="button"
x-show="formData.contact_email"
@click="resetFieldToCompany('contact_email')"
:disabled="saving"
class="text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400">
Reset
</button>
</div>
<input
type="email"
x-model="formData.contact_email"
:placeholder="vendor?.company_contact_email || 'contact@company.com'"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.contact_email }"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
<span x-show="!formData.contact_email">Using company value. Enter a value to override.</span>
<span x-show="formData.contact_email">Custom value (clear to inherit from company)</span>
</span>
<span x-show="errors.contact_email" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.contact_email"></span>
</label>
<!-- Phone -->
<label class="block mb-4 text-sm">
<div class="flex items-center justify-between">
<span class="text-gray-700 dark:text-gray-400">
Phone
<span x-show="!formData.contact_phone"
class="ml-1 text-xs text-purple-500 dark:text-purple-400">
(from company)
</span>
</span>
<button
type="button"
x-show="formData.contact_phone"
@click="resetFieldToCompany('contact_phone')"
:disabled="saving"
class="text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400">
Reset
</button>
</div>
<input
type="tel"
x-model="formData.contact_phone"
:placeholder="vendor?.company_contact_phone || '+352 XXX XXX'"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
<!-- Website -->
<label class="block mb-4 text-sm">
<div class="flex items-center justify-between">
<span class="text-gray-700 dark:text-gray-400">
Website
<span x-show="!formData.website"
class="ml-1 text-xs text-purple-500 dark:text-purple-400">
(from company)
</span>
</span>
<button
type="button"
x-show="formData.website"
@click="resetFieldToCompany('website')"
:disabled="saving"
class="text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400">
Reset
</button>
</div>
<input
type="url"
x-model="formData.website"
:placeholder="vendor?.company_website || 'https://company.com'"
:disabled="saving"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
</div>
</div>
<!-- Business Details -->
<div class="mb-8">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Business Details
</h3>
<div class="grid gap-6 md:grid-cols-2">
<!-- Business Address -->
<label class="block text-sm">
<div class="flex items-center justify-between">
<span class="text-gray-700 dark:text-gray-400">
Business Address
<span x-show="!formData.business_address"
class="ml-1 text-xs text-purple-500 dark:text-purple-400">
(from company)
</span>
</span>
<button
type="button"
x-show="formData.business_address"
@click="resetFieldToCompany('business_address')"
:disabled="saving"
class="text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400">
Reset
</button>
</div>
<textarea
x-model="formData.business_address"
rows="3"
:disabled="saving"
:placeholder="vendor?.company_business_address || 'No company address'"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
></textarea>
</label>
<!-- Tax Number -->
<label class="block text-sm">
<div class="flex items-center justify-between">
<span class="text-gray-700 dark:text-gray-400">
Tax Number
<span x-show="!formData.tax_number"
class="ml-1 text-xs text-purple-500 dark:text-purple-400">
(from company)
</span>
</span>
<button
type="button"
x-show="formData.tax_number"
@click="resetFieldToCompany('tax_number')"
:disabled="saving"
class="text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400">
Reset
</button>
</div>
<input
type="text"
x-model="formData.tax_number"
:disabled="saving"
:placeholder="vendor?.company_tax_number || 'No company tax number'"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
</label>
</div>
</div>
<!-- Marketplace Integration -->
<div class="mb-8">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('shopping-bag', 'inline w-5 h-5 mr-2')"></span>
Letzshop Marketplace URLs
</h3>
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Configure CSV feed URLs for automatic product imports from Letzshop marketplace
</p>
<div class="space-y-4">
<!-- French CSV URL -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">
French CSV URL
</span>
<input
type="url"
x-model="formData.letzshop_csv_url_fr"
:disabled="saving"
placeholder="https://letzshop.lu/feed/fr/products.csv"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
URL for French language product feed
</span>
</label>
<!-- English CSV URL -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">
English CSV URL
</span>
<input
type="url"
x-model="formData.letzshop_csv_url_en"
:disabled="saving"
placeholder="https://letzshop.lu/feed/en/products.csv"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
URL for English language product feed
</span>
</label>
<!-- German CSV URL -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">
German CSV URL
</span>
<input
type="url"
x-model="formData.letzshop_csv_url_de"
:disabled="saving"
placeholder="https://letzshop.lu/feed/de/products.csv"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
<span class="text-xs text-gray-600 dark:text-gray-400 mt-1">
URL for German language product feed
</span>
</label>
</div>
</div>
<!-- Save Button -->
<div class="flex items-center justify-end gap-3 pt-6 border-t dark:border-gray-700">
<a
href="/admin/vendors"
class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800 hover:border-gray-400 focus:outline-none">
Cancel
</a>
<button
type="submit"
:disabled="saving"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!saving">
<span x-html="$icon('check', 'w-4 h-4 mr-2 inline')"></span>
Save Changes
</span>
<span x-show="saving" class="flex items-center">
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
Saving...
</span>
</button>
</div>
</form>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/vendor-edit.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,450 @@
{# app/templates/admin/vendor-theme.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/headers.html' import page_header_flex %}
{% block title %}Theme Editor - {{ vendor_code }}{% endblock %}
{# ✅ CRITICAL: Binds to adminVendorTheme() function in vendor-theme.js #}
{% block alpine_data %}adminVendorTheme(){% endblock %}
{% block content %}
{% call page_header_flex(title='Theme Editor', subtitle_var="'Customize appearance for ' + (vendor?.name || '...')") %}
<a :href="`/admin/vendors/${vendorCode}`"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:shadow-outline-gray">
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
Back to Vendor
</a>
{% endcall %}
{{ loading_state('Loading theme...') }}
{{ error_state('Error', show_condition='error && !loading') }}
<!-- Main Content -->
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-3">
<!-- Theme Configuration Form (2 columns) -->
<div class="md:col-span-2 space-y-6">
<!-- Theme Presets -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('palette', 'inline w-5 h-5 mr-2')"></span>
Choose a Preset
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Start with a pre-designed theme, then customize it to match your brand.
</p>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<!-- Default Preset -->
<button @click="applyPreset('default')"
:disabled="saving"
:class="themeData.theme_name === 'default' ? 'ring-2 ring-purple-500' : ''"
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-purple-300 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
<div class="flex items-center justify-center space-x-2 mb-2">
<div class="w-4 h-4 rounded bg-indigo-500"></div>
<div class="w-4 h-4 rounded bg-purple-500"></div>
<div class="w-4 h-4 rounded bg-pink-500"></div>
</div>
<p class="text-gray-700 dark:text-gray-300">Default</p>
</button>
<!-- Modern Preset -->
<button @click="applyPreset('modern')"
:disabled="saving"
:class="themeData.theme_name === 'modern' ? 'ring-2 ring-purple-500' : ''"
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-purple-300 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
<div class="flex items-center justify-center space-x-2 mb-2">
<div class="w-4 h-4 rounded bg-indigo-500"></div>
<div class="w-4 h-4 rounded bg-purple-600"></div>
<div class="w-4 h-4 rounded bg-pink-500"></div>
</div>
<p class="text-gray-700 dark:text-gray-300">Modern</p>
</button>
<!-- Classic Preset -->
<button @click="applyPreset('classic')"
:disabled="saving"
:class="themeData.theme_name === 'classic' ? 'ring-2 ring-purple-500' : ''"
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-blue-300 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
<div class="flex items-center justify-center space-x-2 mb-2">
<div class="w-4 h-4 rounded bg-blue-800"></div>
<div class="w-4 h-4 rounded bg-purple-700"></div>
<div class="w-4 h-4 rounded bg-red-600"></div>
</div>
<p class="text-gray-700 dark:text-gray-300">Classic</p>
</button>
<!-- Minimal Preset -->
<button @click="applyPreset('minimal')"
:disabled="saving"
:class="themeData.theme_name === 'minimal' ? 'ring-2 ring-purple-500' : ''"
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-gray-400 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
<div class="flex items-center justify-center space-x-2 mb-2">
<div class="w-4 h-4 rounded bg-black"></div>
<div class="w-4 h-4 rounded bg-gray-600"></div>
<div class="w-4 h-4 rounded bg-gray-400"></div>
</div>
<p class="text-gray-700 dark:text-gray-300">Minimal</p>
</button>
<!-- Vibrant Preset -->
<button @click="applyPreset('vibrant')"
:disabled="saving"
:class="themeData.theme_name === 'vibrant' ? 'ring-2 ring-purple-500' : ''"
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-orange-300 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
<div class="flex items-center justify-center space-x-2 mb-2">
<div class="w-4 h-4 rounded bg-orange-500"></div>
<div class="w-4 h-4 rounded bg-red-500"></div>
<div class="w-4 h-4 rounded bg-purple-600"></div>
</div>
<p class="text-gray-700 dark:text-gray-300">Vibrant</p>
</button>
<!-- Elegant Preset -->
<button @click="applyPreset('elegant')"
:disabled="saving"
:class="themeData.theme_name === 'elegant' ? 'ring-2 ring-purple-500' : ''"
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-gray-400 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
<div class="flex items-center justify-center space-x-2 mb-2">
<div class="w-4 h-4 rounded bg-gray-500"></div>
<div class="w-4 h-4 rounded bg-gray-700"></div>
<div class="w-4 h-4 rounded bg-amber-600"></div>
</div>
<p class="text-gray-700 dark:text-gray-300">Elegant</p>
</button>
<!-- Nature Preset -->
<button @click="applyPreset('nature')"
:disabled="saving"
:class="themeData.theme_name === 'nature' ? 'ring-2 ring-purple-500' : ''"
class="p-3 text-sm font-medium bg-white border-2 border-gray-200 rounded-lg hover:border-green-300 transition-all disabled:opacity-50 dark:bg-gray-900 dark:border-gray-700">
<div class="flex items-center justify-center space-x-2 mb-2">
<div class="w-4 h-4 rounded bg-green-600"></div>
<div class="w-4 h-4 rounded bg-emerald-500"></div>
<div class="w-4 h-4 rounded bg-amber-500"></div>
</div>
<p class="text-gray-700 dark:text-gray-300">Nature</p>
</button>
</div>
</div>
<!-- Colors Section -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('color-swatch', 'inline w-5 h-5 mr-2')"></span>
Colors
</h3>
<div class="grid gap-4 md:grid-cols-2">
<!-- Primary Color -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400 font-medium">Primary Color</span>
<p class="text-xs text-gray-500 mb-2">Main brand color for buttons and links</p>
<div class="flex items-center mt-1 space-x-2">
<input type="color"
x-model="themeData.colors.primary"
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
<input type="text"
x-model="themeData.colors.primary"
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
</div>
</label>
<!-- Secondary Color -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400 font-medium">Secondary Color</span>
<p class="text-xs text-gray-500 mb-2">Supporting color for accents</p>
<div class="flex items-center mt-1 space-x-2">
<input type="color"
x-model="themeData.colors.secondary"
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
<input type="text"
x-model="themeData.colors.secondary"
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
</div>
</label>
<!-- Accent Color -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400 font-medium">Accent Color</span>
<p class="text-xs text-gray-500 mb-2">Call-to-action and highlights</p>
<div class="flex items-center mt-1 space-x-2">
<input type="color"
x-model="themeData.colors.accent"
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
<input type="text"
x-model="themeData.colors.accent"
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
</div>
</label>
<!-- Background Color -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400 font-medium">Background Color</span>
<p class="text-xs text-gray-500 mb-2">Page background</p>
<div class="flex items-center mt-1 space-x-2">
<input type="color"
x-model="themeData.colors.background"
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
<input type="text"
x-model="themeData.colors.background"
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
</div>
</label>
<!-- Text Color -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400 font-medium">Text Color</span>
<p class="text-xs text-gray-500 mb-2">Primary text color</p>
<div class="flex items-center mt-1 space-x-2">
<input type="color"
x-model="themeData.colors.text"
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
<input type="text"
x-model="themeData.colors.text"
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
</div>
</label>
<!-- Border Color -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400 font-medium">Border Color</span>
<p class="text-xs text-gray-500 mb-2">Borders and dividers</p>
<div class="flex items-center mt-1 space-x-2">
<input type="color"
x-model="themeData.colors.border"
class="h-10 w-20 border border-gray-300 rounded cursor-pointer dark:border-gray-600">
<input type="text"
x-model="themeData.colors.border"
class="block w-full text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input rounded">
</div>
</label>
</div>
</div>
<!-- Typography Section -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('document', 'inline w-5 h-5 mr-2')"></span>
Typography
</h3>
<div class="grid gap-4 md:grid-cols-2">
<!-- Heading Font -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400 font-medium">Heading Font</span>
<p class="text-xs text-gray-500 mb-2">For titles and headings</p>
<select x-model="themeData.fonts.heading"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select rounded">
<option value="Inter, sans-serif">Inter (Modern)</option>
<option value="Roboto, sans-serif">Roboto (Clean)</option>
<option value="Poppins, sans-serif">Poppins (Friendly)</option>
<option value="Playfair Display, serif">Playfair Display (Elegant)</option>
<option value="Merriweather, serif">Merriweather (Classic)</option>
<option value="Georgia, serif">Georgia (Traditional)</option>
<option value="Helvetica, sans-serif">Helvetica (Minimal)</option>
<option value="Montserrat, sans-serif">Montserrat (Bold)</option>
</select>
</label>
<!-- Body Font -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400 font-medium">Body Font</span>
<p class="text-xs text-gray-500 mb-2">For body text and content</p>
<select x-model="themeData.fonts.body"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select rounded">
<option value="Inter, sans-serif">Inter (Modern)</option>
<option value="Roboto, sans-serif">Roboto (Clean)</option>
<option value="Open Sans, sans-serif">Open Sans (Readable)</option>
<option value="Lato, sans-serif">Lato (Friendly)</option>
<option value="Arial, sans-serif">Arial (Universal)</option>
<option value="Georgia, serif">Georgia (Traditional)</option>
<option value="Helvetica, sans-serif">Helvetica (Minimal)</option>
</select>
</label>
</div>
</div>
<!-- Layout Section -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('view-grid', 'inline w-5 h-5 mr-2')"></span>
Layout
</h3>
<div class="grid gap-4 md:grid-cols-3">
<!-- Product Layout Style -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400 font-medium">Product Layout</span>
<select x-model="themeData.layout.style"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select rounded">
<option value="grid">Grid</option>
<option value="list">List</option>
<option value="masonry">Masonry</option>
</select>
</label>
<!-- Header Style -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400 font-medium">Header Style</span>
<select x-model="themeData.layout.header"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select rounded">
<option value="fixed">Fixed</option>
<option value="static">Static</option>
<option value="transparent">Transparent</option>
</select>
</label>
<!-- Product Card Style -->
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400 font-medium">Product Card</span>
<select x-model="themeData.layout.product_card"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select rounded">
<option value="modern">Modern</option>
<option value="classic">Classic</option>
<option value="minimal">Minimal</option>
</select>
</label>
</div>
</div>
<!-- Custom CSS Section -->
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('code', 'inline w-5 h-5 mr-2')"></span>
Advanced: Custom CSS
</h3>
<label class="block text-sm">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
Add custom CSS rules for advanced styling (use with caution)
</p>
<textarea x-model="themeData.custom_css"
rows="6"
placeholder=".my-custom-class {
color: red;
font-weight: bold;
}"
class="block w-full mt-1 text-sm font-mono dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-textarea rounded"></textarea>
</label>
</div>
<!-- Action Buttons -->
<div class="flex justify-between items-center">
<button @click="resetToDefault()"
:disabled="saving"
class="px-4 py-2 text-sm font-medium leading-5 text-red-700 transition-colors duration-150 bg-white border border-red-300 rounded-lg hover:bg-red-50 focus:outline-none disabled:opacity-50 dark:bg-gray-800 dark:text-red-400 dark:border-red-600">
<span x-html="$icon('refresh', 'inline w-4 h-4 mr-2')"></span>
Reset to Default
</button>
<button @click="saveTheme()"
:disabled="saving"
class="flex items-center px-6 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none disabled:opacity-50">
<span x-show="!saving">
<span x-html="$icon('save', 'inline w-4 h-4 mr-2')"></span>
Save Theme
</span>
<span x-show="saving" class="flex items-center">
<span x-html="$icon('spinner', 'inline w-4 h-4 mr-2 animate-spin')"></span>
Saving...
</span>
</button>
</div>
</div>
<!-- Preview Panel (1 column) -->
<div class="md:col-span-1">
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 sticky top-6">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-html="$icon('eye', 'inline w-5 h-5 mr-2')"></span>
Preview
</h3>
<!-- Theme Preview -->
<div class="space-y-4">
<!-- Current Theme Name -->
<div class="p-3 bg-purple-50 dark:bg-purple-900 dark:bg-opacity-20 rounded-lg">
<p class="text-xs font-semibold text-purple-800 dark:text-purple-200 mb-1">ACTIVE THEME</p>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 capitalize" x-text="themeData.theme_name"></p>
</div>
<!-- Colors Preview -->
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2">COLORS</p>
<div class="grid grid-cols-3 gap-2">
<div class="text-center">
<div class="h-12 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"
:style="`background-color: ${themeData.colors.primary}`"></div>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">Primary</p>
</div>
<div class="text-center">
<div class="h-12 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"
:style="`background-color: ${themeData.colors.secondary}`"></div>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">Secondary</p>
</div>
<div class="text-center">
<div class="h-12 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"
:style="`background-color: ${themeData.colors.accent}`"></div>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">Accent</p>
</div>
</div>
</div>
<!-- Typography Preview -->
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2">TYPOGRAPHY</p>
<div class="space-y-2 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<p class="text-lg font-bold" :style="`font-family: ${themeData.fonts.heading}`">
Heading Font
</p>
<p class="text-sm" :style="`font-family: ${themeData.fonts.body}`">
This is body text font example. It will be used for paragraphs and descriptions.
</p>
</div>
</div>
<!-- Button Preview -->
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2">BUTTONS</p>
<button class="px-4 py-2 text-sm font-medium text-white rounded-lg w-full shadow-sm"
:style="`background-color: ${themeData.colors.primary}`">
Primary Button
</button>
</div>
<!-- Layout Preview -->
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2">LAYOUT</p>
<div class="text-xs space-y-1 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<p class="text-gray-700 dark:text-gray-300">
<span class="font-semibold">Product Layout:</span>
<span class="capitalize" x-text="themeData.layout.style"></span>
</p>
<p class="text-gray-700 dark:text-gray-300">
<span class="font-semibold">Header:</span>
<span class="capitalize" x-text="themeData.layout.header"></span>
</p>
<p class="text-gray-700 dark:text-gray-300">
<span class="font-semibold">Product Card:</span>
<span class="capitalize" x-text="themeData.layout.product_card"></span>
</p>
</div>
</div>
<!-- Preview Link -->
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
<a :href="`http://${vendor?.subdomain}.localhost:8000`"
target="_blank"
class="flex items-center justify-center px-4 py-2 text-sm font-medium text-purple-700 bg-purple-50 border border-purple-300 rounded-lg hover:bg-purple-100 dark:bg-purple-900 dark:bg-opacity-20 dark:text-purple-300 dark:border-purple-700 transition-colors">
<span x-html="$icon('external-link', 'w-4 h-4 mr-2')"></span>
View Live Shop
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/vendor-theme.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,129 @@
{# app/templates/admin/vendor-themes.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/headers.html' import page_header %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% block title %}Vendor Themes{% endblock %}
{% block extra_head %}
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.css" rel="stylesheet">
<style>
.ts-wrapper { width: 100%; }
.ts-control {
background-color: rgb(249 250 251) !important;
border-color: rgb(209 213 219) !important;
border-radius: 0.5rem !important;
padding: 0.5rem 0.75rem !important;
}
.dark .ts-control {
background-color: rgb(55 65 81) !important;
border-color: rgb(75 85 99) !important;
color: rgb(229 231 235) !important;
}
.ts-dropdown {
border-radius: 0.5rem !important;
border-color: rgb(209 213 219) !important;
}
.dark .ts-dropdown {
background-color: rgb(55 65 81) !important;
border-color: rgb(75 85 99) !important;
}
.dark .ts-dropdown .option {
color: rgb(229 231 235) !important;
}
.dark .ts-dropdown .option.active {
background-color: rgb(75 85 99) !important;
}
</style>
{% endblock %}
{% block alpine_data %}adminVendorThemes(){% endblock %}
{% block content %}
{{ page_header('Vendor Themes', subtitle='Customize vendor theme colors and branding') }}
<!-- Selected Vendor Display (when filtered) -->
<div x-show="selectedVendor" x-cloak class="mb-6">
<div class="px-4 py-3 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span x-html="$icon('color-swatch', 'w-6 h-6 text-purple-600')"></span>
<div>
<p class="text-sm font-medium text-purple-700 dark:text-purple-300">Filtered by Vendor</p>
<p class="text-lg font-semibold text-purple-900 dark:text-purple-100" x-text="selectedVendor?.name"></p>
</div>
</div>
<button
@click="clearVendorFilter()"
class="flex items-center gap-1 px-3 py-1.5 text-sm text-purple-700 dark:text-purple-300 bg-purple-100 dark:bg-purple-800 rounded-md hover:bg-purple-200 dark:hover:bg-purple-700 transition-colors"
>
<span x-html="$icon('x-mark', 'w-4 h-4')"></span>
Clear Filter
</button>
</div>
</div>
</div>
<!-- Vendor Search/Filter -->
<div class="px-4 py-6 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Search Vendor
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Search for a vendor to customize their theme
</p>
<div class="max-w-md">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Vendor
</label>
<select x-ref="vendorSelect" placeholder="Search vendor by name or code..."></select>
</div>
</div>
{{ loading_state('Loading vendors...') }}
{{ error_state('Error loading vendors') }}
<!-- Vendors List -->
<div x-show="!loading && filteredVendors.length > 0">
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
<span x-text="selectedVendor ? 'Selected Vendor' : 'All Vendors'"></span>
<span class="text-sm font-normal text-gray-500 dark:text-gray-400" x-text="`(${filteredVendors.length})`"></span>
</h3>
</div>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<template x-for="vendor in filteredVendors" :key="vendor.vendor_code">
<a
:href="`/admin/vendors/${vendor.vendor_code}/theme`"
class="block p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-purple-500 dark:hover:border-purple-500 hover:shadow-md transition-all"
>
<div class="flex items-center justify-between mb-2">
<h4 class="font-semibold text-gray-700 dark:text-gray-200" x-text="vendor.name"></h4>
<span x-html="$icon('color-swatch', 'w-5 h-5 text-purple-600')"></span>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400" x-text="vendor.vendor_code"></p>
<div class="mt-3 flex items-center text-xs text-purple-600 dark:text-purple-400">
<span>Customize theme</span>
<span x-html="$icon('chevron-right', 'w-4 h-4 ml-1')"></span>
</div>
</a>
</template>
</div>
</div>
</div>
<!-- Empty State -->
<div x-show="!loading && filteredVendors.length === 0" class="text-center py-12">
<span x-html="$icon('shopping-bag', 'inline w-12 h-12 text-gray-400 mb-4')"></span>
<p class="text-gray-600 dark:text-gray-400">No vendors found</p>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script>
<script src="{{ url_for('tenancy_static', path='admin/js/vendor-themes.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,232 @@
{# app/templates/admin/vendors.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/pagination.html' import pagination %}
{% from 'shared/macros/headers.html' import page_header %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
{% block title %}Vendors{% endblock %}
{% block alpine_data %}adminVendors(){% endblock %}
{% block content %}
{{ page_header('Vendor Management', action_label='Create Vendor', action_url='/admin/vendors/create') }}
{{ loading_state('Loading vendors...') }}
{{ error_state('Error loading vendors') }}
<!-- Stats Cards -->
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
<!-- Card: Total Vendors -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
<span x-html="$icon('user-group', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Total Vendors
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total || 0">
0
</p>
</div>
</div>
<!-- Card: Verified Vendors -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
<span x-html="$icon('badge-check', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Verified Vendors
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.verified || 0">
0
</p>
</div>
</div>
<!-- Card: Pending Verification -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
<span x-html="$icon('clock', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Pending
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.pending || 0">
0
</p>
</div>
</div>
<!-- Card: Inactive Vendors -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-teal-500 bg-teal-100 rounded-full dark:text-teal-100 dark:bg-teal-500">
<span x-html="$icon('x-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Inactive
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.inactive || 0">
0
</p>
</div>
</div>
</div>
<!-- Search and Filters Bar -->
<div x-show="!loading" class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<!-- Search Input -->
<div class="flex-1 max-w-md">
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
</span>
<input
type="text"
x-model="filters.search"
@input="debouncedSearch()"
placeholder="Search by name or vendor code..."
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
>
</div>
</div>
<!-- Filters -->
<div class="flex flex-wrap gap-3">
<!-- Status Filter -->
<select
x-model="filters.is_active"
@change="pagination.page = 1; loadVendors()"
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
>
<option value="">All Status</option>
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
<!-- Verification Filter -->
<select
x-model="filters.is_verified"
@change="pagination.page = 1; loadVendors()"
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
>
<option value="">All Verification</option>
<option value="true">Verified</option>
<option value="false">Pending</option>
</select>
<!-- Refresh Button -->
<button
@click="refresh()"
class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="Refresh vendors"
>
<span x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
Refresh
</button>
</div>
</div>
</div>
<!-- Vendors Table with Pagination -->
<div x-show="!loading">
{% call table_wrapper() %}
{{ table_header(['Vendor', 'Subdomain', 'Status', 'Created', 'Actions']) }}
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<!-- Empty State -->
<template x-if="paginatedVendors.length === 0">
<tr>
<td colspan="5" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
<div class="flex flex-col items-center">
<span x-html="$icon('user-group', 'w-12 h-12 mb-2 text-gray-300')"></span>
<p class="font-medium">No vendors found</p>
<p class="text-xs mt-1" x-text="filters.search || filters.is_active || filters.is_verified ? 'Try adjusting your search or filters' : 'Create your first vendor to get started'"></p>
</div>
</td>
</tr>
</template>
<!-- Vendor Rows -->
<template x-for="vendor in paginatedVendors" :key="vendor.id || vendor.vendor_code">
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<!-- Vendor Info with Avatar -->
<td class="px-4 py-3">
<div class="flex items-center text-sm">
<div class="relative hidden w-8 h-8 mr-3 rounded-full md:block">
<div class="absolute inset-0 rounded-full bg-purple-100 dark:bg-purple-600 flex items-center justify-center">
<span class="text-xs font-semibold text-purple-600 dark:text-purple-100"
x-text="vendor.name?.charAt(0).toUpperCase() || '?'"></span>
</div>
</div>
<div>
<p class="font-semibold" x-text="vendor.name"></p>
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="vendor.vendor_code"></p>
</div>
</div>
</td>
<!-- Subdomain -->
<td class="px-4 py-3 text-sm" x-text="vendor.subdomain"></td>
<!-- Status Badge -->
<td class="px-4 py-3 text-xs">
<span class="inline-flex items-center px-2 py-1 font-semibold leading-tight rounded-full"
:class="vendor.is_verified ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-orange-700 bg-orange-100 dark:text-white dark:bg-orange-600'">
<span x-show="vendor.is_verified" x-html="$icon('badge-check', 'w-3 h-3 mr-1')"></span>
<span x-text="vendor.is_verified ? 'Verified' : 'Pending'"></span>
</span>
</td>
<!-- Created Date -->
<td class="px-4 py-3 text-sm" x-text="formatDate(vendor.created_at)"></td>
<!-- Actions -->
<td class="px-4 py-3">
<div class="flex items-center space-x-2 text-sm">
<!-- View Button -->
<button
@click="viewVendor(vendor.vendor_code)"
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="View details"
>
<span x-html="$icon('eye', 'w-5 h-5')"></span>
</button>
<!-- Edit Button -->
<button
@click="editVendor(vendor.vendor_code)"
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="Edit vendor"
>
<span x-html="$icon('edit', 'w-5 h-5')"></span>
</button>
<!-- Delete Button -->
<button
@click="deleteVendor(vendor)"
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="Delete vendor"
>
<span x-html="$icon('delete', 'w-5 h-5')"></span>
</button>
</div>
</td>
</tr>
</template>
</tbody>
{% endcall %}
{{ pagination() }}
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='admin/js/vendors.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,160 @@
{# app/templates/vendor/login.html #}
<!DOCTYPE html>
<html :class="{ 'dark': dark }" x-data="vendorLogin()" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vendor Login - Multi-Tenant Platform</title>
<!-- Fonts: Local fallback + Google Fonts -->
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="{{ url_for('static', path='vendor/css/tailwind.output.css') }}" />
<style>
[x-cloak] { display: none !important; }
</style>
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
<div class="flex-1 h-full max-w-4xl mx-auto overflow-hidden bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div class="flex flex-col overflow-y-auto md:flex-row">
<div class="h-32 md:h-auto md:w-1/2">
<img aria-hidden="true" class="object-cover w-full h-full dark:hidden"
src="{{ url_for('static', path='vendor/img/login-office.jpeg') }}" alt="Office" />
<img aria-hidden="true" class="hidden object-cover w-full h-full dark:block"
src="{{ url_for('static', path='vendor/img/login-office-dark.jpeg') }}" alt="Office" />
</div>
<div class="flex items-center justify-center p-6 sm:p-12 md:w-1/2">
<div class="w-full">
<!-- Vendor Info -->
<template x-if="vendor">
<div class="mb-6 text-center">
<div class="inline-flex items-center justify-center w-16 h-16 mb-4 rounded-full bg-purple-100 dark:bg-purple-600">
<span class="text-2xl font-bold text-purple-600 dark:text-purple-100"
x-text="vendor.name?.charAt(0).toUpperCase() || '🏪'"></span>
</div>
<h2 class="text-xl font-semibold text-gray-700 dark:text-gray-200" x-text="vendor.name"></h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
<strong x-text="vendor.vendor_code"></strong>
</p>
</div>
</template>
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
Vendor Portal Login
</h1>
<!-- Alert Messages -->
<div x-show="error" x-text="error"
class="px-4 py-3 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800"
x-transition></div>
<div x-show="success" x-text="success"
class="px-4 py-3 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800"
x-transition></div>
<!-- Login Form (only show if vendor found) -->
<template x-if="vendor">
<form @submit.prevent="handleLogin">
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Username</span>
<input x-model="credentials.username"
:disabled="loading"
@input="clearErrors"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600': errors.username }"
placeholder="Enter your username"
autocomplete="username"
required />
<span x-show="errors.username" x-text="errors.username"
class="text-xs text-red-600 dark:text-red-400"></span>
</label>
<label class="block mt-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">Password</span>
<input x-model="credentials.password"
:disabled="loading"
@input="clearErrors"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
:class="{ 'border-red-600': errors.password }"
placeholder="***************"
type="password"
autocomplete="current-password"
required />
<span x-show="errors.password" x-text="errors.password"
class="text-xs text-red-600 dark:text-red-400"></span>
</label>
<button type="submit" :disabled="loading"
class="block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!loading">Sign in</span>
<span x-show="loading" class="flex items-center justify-center">
<span class="inline w-4 h-4 mr-2" x-html="$icon('spinner', 'w-4 h-4 animate-spin')"></span>
Signing in...
</span>
</button>
</form>
</template>
<!-- Vendor Not Found -->
<template x-if="!vendor && !loading && checked">
<div class="text-center py-8">
<div class="text-6xl mb-4">🏪</div>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">
Vendor Not Found
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
The vendor you're trying to access doesn't exist or is inactive.
</p>
<a href="/" class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
Go to Platform Home
</a>
</div>
</template>
<!-- Loading State -->
<div x-show="loading && !vendor" class="text-center py-8">
<span class="inline-block w-8 h-8 text-purple-600" x-html="$icon('spinner', 'w-8 h-8 animate-spin')"></span>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Loading vendor information...</p>
</div>
<hr class="my-8" />
<p class="mt-4">
<a class="text-sm font-medium text-purple-600 dark:text-purple-400 hover:underline"
href="#">
Forgot your password?
</a>
</p>
<p class="mt-2">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="/">
← Back to Platform
</a>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Scripts - ORDER MATTERS! -->
<!-- 1. Log Configuration -->
<script src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>
<!-- 2. Icons -->
<script src="{{ url_for('static', path='shared/js/icons.js') }}"></script>
<!-- 3. Utils -->
<script src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
<!-- 4. API Client -->
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
<!-- 5. Alpine.js v3 -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
<!-- 6. Login Logic -->
<script src="{{ url_for('tenancy_static', path='vendor/js/login.js') }}"></script>
</body>
</html>

View File

@@ -0,0 +1,206 @@
{# app/templates/vendor/profile.html #}
{% extends "vendor/base.html" %}
{% from 'shared/macros/headers.html' import page_header_flex %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% block title %}Profile{% endblock %}
{% block alpine_data %}vendorProfile(){% endblock %}
{% block content %}
<!-- Page Header -->
{% call page_header_flex(title='Profile', subtitle='Manage your business information') %}
<div class="flex items-center gap-4">
<button
x-show="hasChanges"
@click="resetForm()"
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
>
Reset
</button>
<button
@click="saveProfile()"
:disabled="saving || !hasChanges"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
>
<span x-html="$icon('check', 'w-4 h-4 mr-2')"></span>
<span x-show="!saving">Save Changes</span>
<span x-show="saving">Saving...</span>
</button>
</div>
{% endcall %}
{{ loading_state('Loading profile...') }}
{{ error_state('Error loading profile') }}
<!-- Profile Form -->
<div x-show="!loading && !error" class="w-full mb-8">
<!-- Business Information -->
<div class="mb-8 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Business Information</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Basic information about your business</p>
</div>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Business Name -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Business Name *
</label>
<input
type="text"
x-model="form.name"
@input="markChanged()"
:class="{'border-red-500': errors.name}"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
/>
<p x-show="errors.name" class="mt-1 text-xs text-red-500" x-text="errors.name"></p>
</div>
<!-- Tax Number -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Tax Number / VAT ID
</label>
<input
type="text"
x-model="form.tax_number"
@input="markChanged()"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="LU12345678"
/>
</div>
<!-- Business Address -->
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Business Address
</label>
<textarea
x-model="form.business_address"
@input="markChanged()"
rows="3"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="123 Business Street&#10;City, Postal Code&#10;Country"
></textarea>
</div>
<!-- Description -->
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Business Description
</label>
<textarea
x-model="form.description"
@input="markChanged()"
rows="4"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="Tell customers about your business..."
></textarea>
</div>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="mb-8 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Contact Information</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">How customers can reach you</p>
</div>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Contact Email -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Contact Email
</label>
<input
type="email"
x-model="form.contact_email"
@input="markChanged()"
:class="{'border-red-500': errors.contact_email}"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="contact@yourbusiness.com"
/>
<p x-show="errors.contact_email" class="mt-1 text-xs text-red-500" x-text="errors.contact_email"></p>
</div>
<!-- Contact Phone -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Contact Phone
</label>
<input
type="tel"
x-model="form.contact_phone"
@input="markChanged()"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="+352 123 456 789"
/>
</div>
<!-- Website -->
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Website
</label>
<input
type="url"
x-model="form.website"
@input="markChanged()"
:class="{'border-red-500': errors.website}"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="https://www.yourbusiness.com"
/>
<p x-show="errors.website" class="mt-1 text-xs text-red-500" x-text="errors.website"></p>
</div>
</div>
</div>
</div>
<!-- Vendor Info (Read Only) -->
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Account Information</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Your vendor account details (read-only)</p>
</div>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Vendor Code -->
<div>
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Vendor Code</label>
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="profile?.vendor_code"></p>
</div>
<!-- Subdomain -->
<div>
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Subdomain</label>
<p class="text-sm font-mono text-gray-700 dark:text-gray-300" x-text="profile?.subdomain || '-'"></p>
</div>
<!-- Status -->
<div>
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Status</label>
<span
:class="profile?.is_active
? 'px-2 py-1 text-xs font-semibold text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100'
: 'px-2 py-1 text-xs font-semibold text-gray-700 bg-gray-100 rounded-full dark:bg-gray-700 dark:text-gray-100'"
x-text="profile?.is_active ? 'Active' : 'Inactive'"
></span>
<span
x-show="profile?.is_verified"
class="ml-2 px-2 py-1 text-xs font-semibold text-blue-700 bg-blue-100 rounded-full dark:bg-blue-700 dark:text-blue-100"
>Verified</span>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='vendor/js/profile.js') }}"></script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,293 @@
{# app/templates/vendor/team.html #}
{% extends "vendor/base.html" %}
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/modals.html' import modal_simple %}
{% from 'shared/macros/tables.html' import table_wrapper %}
{% block title %}Team{% endblock %}
{% block alpine_data %}vendorTeam(){% endblock %}
{% block content %}
<!-- Page Header -->
{% call page_header_flex(title='Team', subtitle='Manage your team members and roles') %}
<div class="flex items-center gap-4">
{{ refresh_button(loading_var='loading', onclick='loadMembers()', variant='secondary') }}
<button
@click="openInviteModal()"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple"
>
<span x-html="$icon('user-plus', 'w-4 h-4 mr-2')"></span>
Invite Member
</button>
</div>
{% endcall %}
{{ loading_state('Loading team...') }}
{{ error_state('Error loading team') }}
<!-- Stats Cards -->
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-3">
<!-- Total Members -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('users', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Members</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total">0</p>
</div>
</div>
<!-- Active Members -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
<span x-html="$icon('check-circle', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Active</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.active_count">0</p>
</div>
</div>
<!-- Pending Invitations -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-yellow-500 bg-yellow-100 rounded-full dark:text-yellow-100 dark:bg-yellow-500">
<span x-html="$icon('clock', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Pending Invitations</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.pending_invitations">0</p>
</div>
</div>
</div>
<!-- Team Members Table -->
<div x-show="!loading && !error" class="mb-8">
{% call table_wrapper() %}
<thead>
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
<th class="px-4 py-3">Member</th>
<th class="px-4 py-3">Role</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Joined</th>
<th class="px-4 py-3">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<template x-for="member in members" :key="member.user_id">
<tr class="text-gray-700 dark:text-gray-400">
<!-- Member Info -->
<td class="px-4 py-3">
<div class="flex items-center text-sm">
<div class="relative w-10 h-10 mr-3 rounded-full overflow-hidden bg-purple-100 dark:bg-purple-900 flex items-center justify-center">
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300" x-text="getInitials(member)"></span>
</div>
<div>
<p class="font-semibold" x-text="`${member.first_name || ''} ${member.last_name || ''}`.trim() || member.email"></p>
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="member.email"></p>
</div>
</div>
</td>
<!-- Role -->
<td class="px-4 py-3 text-sm">
<span
class="px-2 py-1 font-semibold leading-tight text-purple-700 bg-purple-100 rounded-full dark:bg-purple-700 dark:text-purple-100"
x-text="getRoleName(member)"
></span>
</td>
<!-- Status -->
<td class="px-4 py-3 text-xs">
<span
:class="{
'px-2 py-1 font-semibold leading-tight rounded-full': true,
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': member.is_active && !member.invitation_pending,
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': member.invitation_pending,
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100': !member.is_active
}"
x-text="member.invitation_pending ? 'Pending' : (member.is_active ? 'Active' : 'Inactive')"
></span>
</td>
<!-- Joined -->
<td class="px-4 py-3 text-sm" x-text="formatDate(member.joined_at)"></td>
<!-- Actions -->
<td class="px-4 py-3">
<div class="flex items-center space-x-2 text-sm">
<!-- Edit button - not for owners -->
<button
@click="openEditModal(member)"
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
title="Edit"
x-show="!member.is_owner"
>
<span x-html="$icon('pencil', 'w-5 h-5')"></span>
</button>
<!-- Owner badge -->
<span x-show="member.is_owner" class="text-xs text-gray-400 dark:text-gray-500 italic">Owner</span>
<!-- Remove button - not for owners -->
<button
@click="confirmRemove(member)"
class="p-1 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400"
title="Remove"
x-show="!member.is_owner"
>
<span x-html="$icon('trash', 'w-5 h-5')"></span>
</button>
</div>
</td>
</tr>
</template>
<!-- Empty State -->
<tr x-show="members.length === 0">
<td colspan="5" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<div class="flex flex-col items-center">
<span x-html="$icon('users', 'w-12 h-12 text-gray-300 dark:text-gray-600 mb-4')"></span>
<p class="text-lg font-medium">No team members yet</p>
<p class="text-sm">Invite your first team member to get started</p>
<button
@click="openInviteModal()"
class="mt-4 px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
>
Invite Member
</button>
</div>
</td>
</tr>
</tbody>
{% endcall %}
</div>
<!-- Invite Modal -->
<div x-show="showInviteModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50">
<div class="w-full max-w-md bg-white rounded-lg shadow-xl dark:bg-gray-800" @click.away="showInviteModal = false">
<div class="flex items-center justify-between p-4 border-b dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Invite Team Member</h3>
<button @click="showInviteModal = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
<span x-html="$icon('x', 'w-5 h-5')"></span>
</button>
</div>
<div class="p-4">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Email *</label>
<input
type="email"
x-model="inviteForm.email"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
placeholder="colleague@example.com"
/>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">First Name</label>
<input
type="text"
x-model="inviteForm.first_name"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Last Name</label>
<input
type="text"
x-model="inviteForm.last_name"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
/>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Role</label>
<select
x-model="inviteForm.role_name"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
>
<template x-for="role in roleOptions" :key="role.value">
<option :value="role.value" x-text="role.label"></option>
</template>
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" x-text="roleOptions.find(r => r.value === inviteForm.role_name)?.description"></p>
</div>
</div>
<div class="flex justify-end gap-2 p-4 border-t dark:border-gray-700">
<button @click="showInviteModal = false" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
Cancel
</button>
<button
@click="sendInvitation()"
:disabled="saving || !inviteForm.email"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
<span x-show="!saving">Send Invitation</span>
<span x-show="saving">Sending...</span>
</button>
</div>
</div>
</div>
<!-- Edit Modal -->
{% call modal_simple('editTeamMemberModal', 'Edit Team Member', show_var='showEditModal', size='sm') %}
<div class="space-y-4">
<template x-if="selectedMember">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Edit <span class="font-semibold" x-text="selectedMember.email"></span>
</p>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Role</label>
<select
x-model="editForm.role_id"
class="w-full px-4 py-2 text-sm text-gray-700 bg-gray-50 border border-gray-200 rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-1 focus:ring-purple-400"
>
<template x-for="role in roles" :key="role.id">
<option :value="role.id" x-text="role.name"></option>
</template>
</select>
</div>
<div class="flex items-center">
<input type="checkbox" x-model="editForm.is_active" id="is_active" class="w-4 h-4 text-purple-600 rounded focus:ring-purple-500" />
<label for="is_active" class="ml-2 text-sm text-gray-700 dark:text-gray-300">Active</label>
</div>
</div>
</template>
<div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button
@click="showEditModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>Cancel</button>
<button
@click="updateMember()"
:disabled="saving"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
>Save</button>
</div>
</div>
{% endcall %}
<!-- Remove Confirmation Modal -->
{% call modal_simple('removeTeamMemberModal', 'Remove Team Member', show_var='showRemoveModal', size='sm') %}
<div class="space-y-4">
<template x-if="selectedMember">
<p class="text-sm text-gray-600 dark:text-gray-400">
Are you sure you want to remove <span class="font-semibold" x-text="selectedMember.email"></span> from the team?
They will lose access to this vendor.
</p>
</template>
<div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button
@click="showRemoveModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>Cancel</button>
<button
@click="removeMember()"
:disabled="saving"
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
>Remove</button>
</div>
</div>
{% endcall %}
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('tenancy_static', path='vendor/js/team.js') }}"></script>
{% endblock %}