fix(subscriptions): fix subscription UI and API after store→merchant migration
Store detail page now shows all platform subscriptions instead of always "No Subscription Found". Subscriptions listing page renamed from Store to Merchant throughout (template, JS, menu, i18n) with Platform column added. Tiers API supports platform_id filtering. Merchant detail page no longer hardcodes 'oms' platform — loads all platforms, shows subscription cards per platform with labels, and the Create Subscription modal includes a platform selector with platform-filtered tiers. Create button always accessible in Quick Actions. Edit modal on /admin/subscriptions loads tiers from API filtered by platform instead of hardcoded options, sends tier_code (not tier) to match PATCH schema, and shows platform context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -171,7 +171,7 @@ billing_module = ModuleDefinition(
|
|||||||
),
|
),
|
||||||
MenuItemDefinition(
|
MenuItemDefinition(
|
||||||
id="subscriptions",
|
id="subscriptions",
|
||||||
label_key="billing.menu.store_subscriptions",
|
label_key="billing.menu.merchant_subscriptions",
|
||||||
icon="credit-card",
|
icon="credit-card",
|
||||||
route="/admin/subscriptions",
|
route="/admin/subscriptions",
|
||||||
order=20,
|
order=20,
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
"billing_subscriptions": "Abrechnung & Abonnements",
|
"billing_subscriptions": "Abrechnung & Abonnements",
|
||||||
"subscription_tiers": "Abo-Stufen",
|
"subscription_tiers": "Abo-Stufen",
|
||||||
"store_subscriptions": "Shop-Abonnements",
|
"store_subscriptions": "Shop-Abonnements",
|
||||||
|
"merchant_subscriptions": "Händler-Abonnements",
|
||||||
"billing_history": "Abrechnungsverlauf",
|
"billing_history": "Abrechnungsverlauf",
|
||||||
"sales_orders": "Verkäufe & Bestellungen",
|
"sales_orders": "Verkäufe & Bestellungen",
|
||||||
"invoices": "Rechnungen",
|
"invoices": "Rechnungen",
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
"billing_subscriptions": "Billing & Subscriptions",
|
"billing_subscriptions": "Billing & Subscriptions",
|
||||||
"subscription_tiers": "Subscription Tiers",
|
"subscription_tiers": "Subscription Tiers",
|
||||||
"store_subscriptions": "Store Subscriptions",
|
"store_subscriptions": "Store Subscriptions",
|
||||||
|
"merchant_subscriptions": "Merchant Subscriptions",
|
||||||
"billing_history": "Billing History",
|
"billing_history": "Billing History",
|
||||||
"sales_orders": "Sales & Orders",
|
"sales_orders": "Sales & Orders",
|
||||||
"invoices": "Invoices",
|
"invoices": "Invoices",
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
"billing_subscriptions": "Facturation et Abonnements",
|
"billing_subscriptions": "Facturation et Abonnements",
|
||||||
"subscription_tiers": "Niveaux d'abonnement",
|
"subscription_tiers": "Niveaux d'abonnement",
|
||||||
"store_subscriptions": "Abonnements des magasins",
|
"store_subscriptions": "Abonnements des magasins",
|
||||||
|
"merchant_subscriptions": "Abonnements des marchands",
|
||||||
"billing_history": "Historique de facturation",
|
"billing_history": "Historique de facturation",
|
||||||
"sales_orders": "Ventes et Commandes",
|
"sales_orders": "Ventes et Commandes",
|
||||||
"invoices": "Factures",
|
"invoices": "Factures",
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
"billing_subscriptions": "Ofrechnung & Abonnementer",
|
"billing_subscriptions": "Ofrechnung & Abonnementer",
|
||||||
"subscription_tiers": "Abo-Stufen",
|
"subscription_tiers": "Abo-Stufen",
|
||||||
"store_subscriptions": "Buttek-Abonnementer",
|
"store_subscriptions": "Buttek-Abonnementer",
|
||||||
|
"merchant_subscriptions": "Händler-Abonnementer",
|
||||||
"billing_history": "Ofrechnungsverlaf",
|
"billing_history": "Ofrechnungsverlaf",
|
||||||
"sales_orders": "Verkaf & Bestellungen",
|
"sales_orders": "Verkaf & Bestellungen",
|
||||||
"invoices": "Rechnungen",
|
"invoices": "Rechnungen",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.api.deps import get_current_admin_api, require_module_access
|
from app.api.deps import get_current_admin_api, require_module_access
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
|
from app.exceptions import ResourceNotFoundException
|
||||||
from app.modules.billing.services import admin_subscription_service, subscription_service
|
from app.modules.billing.services import admin_subscription_service, subscription_service
|
||||||
from app.modules.enums import FrontendType
|
from app.modules.enums import FrontendType
|
||||||
from app.modules.billing.schemas import (
|
from app.modules.billing.schemas import (
|
||||||
@@ -51,11 +52,12 @@ admin_router = APIRouter(
|
|||||||
@admin_router.get("/tiers", response_model=SubscriptionTierListResponse)
|
@admin_router.get("/tiers", response_model=SubscriptionTierListResponse)
|
||||||
def list_subscription_tiers(
|
def list_subscription_tiers(
|
||||||
include_inactive: bool = Query(False, description="Include inactive tiers"),
|
include_inactive: bool = Query(False, description="Include inactive tiers"),
|
||||||
|
platform_id: int | None = Query(None, description="Filter tiers by platform"),
|
||||||
current_user: UserContext = Depends(get_current_admin_api),
|
current_user: UserContext = Depends(get_current_admin_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""List all subscription tiers."""
|
"""List all subscription tiers."""
|
||||||
tiers = admin_subscription_service.get_tiers(db, include_inactive=include_inactive)
|
tiers = admin_subscription_service.get_tiers(db, include_inactive=include_inactive, platform_id=platform_id)
|
||||||
|
|
||||||
return SubscriptionTierListResponse(
|
return SubscriptionTierListResponse(
|
||||||
tiers=[SubscriptionTierResponse.model_validate(t) for t in tiers],
|
tiers=[SubscriptionTierResponse.model_validate(t) for t in tiers],
|
||||||
@@ -133,15 +135,18 @@ def list_merchant_subscriptions(
|
|||||||
db, page=page, per_page=per_page, status=status, tier=tier, search=search
|
db, page=page, per_page=per_page, status=status, tier=tier, search=search
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from app.modules.tenancy.models import Platform
|
||||||
|
|
||||||
subscriptions = []
|
subscriptions = []
|
||||||
for sub, merchant in data["results"]:
|
for sub, merchant in data["results"]:
|
||||||
sub_resp = MerchantSubscriptionAdminResponse.model_validate(sub)
|
sub_resp = MerchantSubscriptionAdminResponse.model_validate(sub)
|
||||||
tier_name = sub.tier.name if sub.tier else None
|
tier_name = sub.tier.name if sub.tier else None
|
||||||
|
platform = db.query(Platform).filter(Platform.id == sub.platform_id).first()
|
||||||
subscriptions.append(
|
subscriptions.append(
|
||||||
MerchantSubscriptionWithMerchant(
|
MerchantSubscriptionWithMerchant(
|
||||||
**sub_resp.model_dump(),
|
**sub_resp.model_dump(),
|
||||||
merchant_name=merchant.name,
|
merchant_name=merchant.name,
|
||||||
platform_name="", # Platform name can be resolved if needed
|
platform_name=platform.name if platform else "",
|
||||||
tier_name=tier_name,
|
tier_name=tier_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -244,63 +249,64 @@ def get_subscription_for_store(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get subscription + feature usage for a store (resolves to merchant).
|
Get subscriptions + feature usage for a store (resolves to merchant).
|
||||||
|
|
||||||
Convenience endpoint for the admin store detail page. Resolves
|
Convenience endpoint for the admin store detail page. Resolves
|
||||||
store -> merchant -> subscription internally and returns subscription
|
store -> merchant -> all platform subscriptions and returns a list
|
||||||
info with feature usage metrics.
|
of subscription entries with feature usage metrics.
|
||||||
"""
|
"""
|
||||||
from app.modules.billing.services.feature_service import feature_service
|
from app.modules.billing.services.feature_service import feature_service
|
||||||
from app.modules.billing.schemas.subscription import FeatureSummaryResponse
|
from app.modules.tenancy.models import Platform
|
||||||
|
|
||||||
# Resolve store to merchant
|
# Resolve store to merchant + all platform IDs
|
||||||
merchant_id, platform_id = feature_service._get_merchant_for_store(db, store_id)
|
merchant_id, platform_ids = feature_service._get_merchant_and_platforms_for_store(db, store_id)
|
||||||
if merchant_id is None or platform_id is None:
|
if merchant_id is None or not platform_ids:
|
||||||
raise HTTPException(status_code=404, detail="Store not found or has no merchant association")
|
raise HTTPException(status_code=404, detail="Store not found or has no platform association")
|
||||||
|
|
||||||
# Get subscription
|
results = []
|
||||||
try:
|
for pid in platform_ids:
|
||||||
sub, merchant = admin_subscription_service.get_subscription(
|
try:
|
||||||
db, merchant_id, platform_id
|
sub, merchant = admin_subscription_service.get_subscription(db, merchant_id, pid)
|
||||||
)
|
except ResourceNotFoundException:
|
||||||
except Exception:
|
continue
|
||||||
return {
|
|
||||||
"subscription": None,
|
|
||||||
"tier": None,
|
|
||||||
"features": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get feature summary
|
# Get feature summary
|
||||||
features_summary = feature_service.get_merchant_features_summary(db, merchant_id, platform_id)
|
features_summary = feature_service.get_merchant_features_summary(db, merchant_id, pid)
|
||||||
|
|
||||||
# Build tier info
|
# Build tier info
|
||||||
tier_info = None
|
tier_info = None
|
||||||
if sub.tier:
|
if sub.tier:
|
||||||
tier_info = {
|
tier_info = {
|
||||||
"code": sub.tier.code,
|
"code": sub.tier.code,
|
||||||
"name": sub.tier.name,
|
"name": sub.tier.name,
|
||||||
"feature_codes": [fl.feature_code for fl in (sub.tier.feature_limits or [])],
|
"feature_codes": [fl.feature_code for fl in (sub.tier.feature_limits or [])],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build usage metrics (quantitative features only)
|
# Build usage metrics (quantitative features only)
|
||||||
usage_metrics = []
|
usage_metrics = []
|
||||||
for fs in features_summary:
|
for fs in features_summary:
|
||||||
if fs.feature_type == "quantitative" and fs.enabled:
|
if fs.feature_type == "quantitative" and fs.enabled:
|
||||||
usage_metrics.append({
|
usage_metrics.append({
|
||||||
"name": fs.name_key.replace("_", " ").title(),
|
"name": fs.name_key.replace("_", " ").title(),
|
||||||
"current": fs.current or 0,
|
"current": fs.current or 0,
|
||||||
"limit": fs.limit,
|
"limit": fs.limit,
|
||||||
"percentage": fs.percent_used or 0,
|
"percentage": fs.percent_used or 0,
|
||||||
"is_unlimited": fs.limit is None,
|
"is_unlimited": fs.limit is None,
|
||||||
"is_at_limit": fs.remaining == 0 if fs.remaining is not None else False,
|
"is_at_limit": fs.remaining == 0 if fs.remaining is not None else False,
|
||||||
"is_approaching_limit": (fs.percent_used or 0) >= 80,
|
"is_approaching_limit": (fs.percent_used or 0) >= 80,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
# Resolve platform name
|
||||||
"subscription": MerchantSubscriptionAdminResponse.model_validate(sub).model_dump(),
|
platform = db.query(Platform).filter(Platform.id == pid).first()
|
||||||
"tier": tier_info,
|
|
||||||
"features": usage_metrics,
|
results.append({
|
||||||
}
|
"subscription": MerchantSubscriptionAdminResponse.model_validate(sub).model_dump(),
|
||||||
|
"tier": tier_info,
|
||||||
|
"features": usage_metrics,
|
||||||
|
"platform_name": platform.name if platform else "",
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"subscriptions": results}
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -40,14 +40,20 @@ class AdminSubscriptionService:
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
def get_tiers(
|
def get_tiers(
|
||||||
self, db: Session, include_inactive: bool = False
|
self, db: Session, include_inactive: bool = False, platform_id: int | None = None
|
||||||
) -> list[SubscriptionTier]:
|
) -> list[SubscriptionTier]:
|
||||||
"""Get all subscription tiers."""
|
"""Get all subscription tiers, optionally filtered by platform."""
|
||||||
query = db.query(SubscriptionTier)
|
query = db.query(SubscriptionTier)
|
||||||
|
|
||||||
if not include_inactive:
|
if not include_inactive:
|
||||||
query = query.filter(SubscriptionTier.is_active == True) # noqa: E712
|
query = query.filter(SubscriptionTier.is_active == True) # noqa: E712
|
||||||
|
|
||||||
|
if platform_id is not None:
|
||||||
|
query = query.filter(
|
||||||
|
(SubscriptionTier.platform_id == platform_id)
|
||||||
|
| (SubscriptionTier.platform_id.is_(None))
|
||||||
|
)
|
||||||
|
|
||||||
return query.order_by(SubscriptionTier.display_order).all()
|
return query.order_by(SubscriptionTier.display_order).all()
|
||||||
|
|
||||||
def get_tier_by_code(self, db: Session, tier_code: str) -> SubscriptionTier:
|
def get_tier_by_code(self, db: Session, tier_code: str) -> SubscriptionTier:
|
||||||
|
|||||||
@@ -115,27 +115,48 @@ class FeatureService:
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (merchant_id, platform_id), either may be None
|
Tuple of (merchant_id, platform_id), either may be None
|
||||||
"""
|
"""
|
||||||
from app.modules.tenancy.models import Store
|
from app.modules.tenancy.models import Store, StorePlatform
|
||||||
|
|
||||||
store = db.query(Store).filter(Store.id == store_id).first()
|
store = db.query(Store).filter(Store.id == store_id).first()
|
||||||
if not store:
|
if not store:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
merchant_id = store.merchant_id
|
merchant_id = store.merchant_id
|
||||||
# Get platform_id from store's platform association
|
# Get primary platform_id from StorePlatform junction
|
||||||
platform_id = getattr(store, "platform_id", None)
|
sp = (
|
||||||
if platform_id is None:
|
db.query(StorePlatform.platform_id)
|
||||||
# Try StorePlatform junction
|
.filter(StorePlatform.store_id == store_id, StorePlatform.is_active == True) # noqa: E712
|
||||||
from app.modules.tenancy.models import StorePlatform
|
.order_by(StorePlatform.is_primary.desc())
|
||||||
sp = (
|
.first()
|
||||||
db.query(StorePlatform.platform_id)
|
)
|
||||||
.filter(StorePlatform.store_id == store_id)
|
platform_id = sp[0] if sp else None
|
||||||
.first()
|
|
||||||
)
|
|
||||||
platform_id = sp[0] if sp else None
|
|
||||||
|
|
||||||
return merchant_id, platform_id
|
return merchant_id, platform_id
|
||||||
|
|
||||||
|
def _get_merchant_and_platforms_for_store(
|
||||||
|
self, db: Session, store_id: int
|
||||||
|
) -> tuple[int | None, list[int]]:
|
||||||
|
"""
|
||||||
|
Resolve store_id to (merchant_id, [platform_ids]).
|
||||||
|
|
||||||
|
Returns all active platform IDs for the store's merchant,
|
||||||
|
ordered with the primary platform first.
|
||||||
|
"""
|
||||||
|
from app.modules.tenancy.models import Store, StorePlatform
|
||||||
|
|
||||||
|
store = db.query(Store).filter(Store.id == store_id).first()
|
||||||
|
if not store:
|
||||||
|
return None, []
|
||||||
|
|
||||||
|
platform_ids = [
|
||||||
|
sp[0]
|
||||||
|
for sp in db.query(StorePlatform.platform_id)
|
||||||
|
.filter(StorePlatform.store_id == store_id, StorePlatform.is_active == True) # noqa: E712
|
||||||
|
.order_by(StorePlatform.is_primary.desc())
|
||||||
|
.all()
|
||||||
|
]
|
||||||
|
return store.merchant_id, platform_ids
|
||||||
|
|
||||||
def _get_subscription(
|
def _get_subscription(
|
||||||
self, db: Session, merchant_id: int, platform_id: int
|
self, db: Session, merchant_id: int, platform_id: int
|
||||||
) -> MerchantSubscription | None:
|
) -> MerchantSubscription | None:
|
||||||
|
|||||||
@@ -39,17 +39,21 @@ function adminSubscriptions() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
sortBy: 'store_name',
|
sortBy: 'merchant_name',
|
||||||
sortOrder: 'asc',
|
sortOrder: 'asc',
|
||||||
|
|
||||||
// Modal state
|
// Modal state
|
||||||
showModal: false,
|
showModal: false,
|
||||||
editingSub: null,
|
editingSub: null,
|
||||||
formData: {
|
formData: {
|
||||||
tier: '',
|
tier_code: '',
|
||||||
status: ''
|
status: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Tiers for edit modal
|
||||||
|
editTiers: [],
|
||||||
|
loadingTiers: false,
|
||||||
|
|
||||||
// Feature overrides
|
// Feature overrides
|
||||||
featureOverrides: [],
|
featureOverrides: [],
|
||||||
quantitativeFeatures: [],
|
quantitativeFeatures: [],
|
||||||
@@ -208,15 +212,34 @@ function adminSubscriptions() {
|
|||||||
async openEditModal(sub) {
|
async openEditModal(sub) {
|
||||||
this.editingSub = sub;
|
this.editingSub = sub;
|
||||||
this.formData = {
|
this.formData = {
|
||||||
tier: sub.tier,
|
tier_code: sub.tier,
|
||||||
status: sub.status
|
status: sub.status
|
||||||
};
|
};
|
||||||
this.featureOverrides = [];
|
this.featureOverrides = [];
|
||||||
this.quantitativeFeatures = [];
|
this.quantitativeFeatures = [];
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
|
|
||||||
// Load feature catalog and merchant overrides
|
// Load tiers filtered by platform and feature overrides in parallel
|
||||||
await this.loadFeatureOverrides(sub.merchant_id);
|
await Promise.all([
|
||||||
|
this.loadEditTiers(sub.platform_id),
|
||||||
|
this.loadFeatureOverrides(sub.merchant_id),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadEditTiers(platformId) {
|
||||||
|
this.loadingTiers = true;
|
||||||
|
try {
|
||||||
|
const url = platformId
|
||||||
|
? `/admin/subscriptions/tiers?platform_id=${platformId}`
|
||||||
|
: '/admin/subscriptions/tiers';
|
||||||
|
const response = await apiClient.get(url);
|
||||||
|
this.editTiers = response.tiers || [];
|
||||||
|
subsLog.info('Loaded tiers for edit modal:', this.editTiers.length);
|
||||||
|
} catch (error) {
|
||||||
|
subsLog.error('Failed to load tiers:', error);
|
||||||
|
} finally {
|
||||||
|
this.loadingTiers = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
closeModal() {
|
closeModal() {
|
||||||
@@ -312,7 +335,7 @@ function adminSubscriptions() {
|
|||||||
// Save feature overrides
|
// Save feature overrides
|
||||||
await this.saveFeatureOverrides(this.editingSub.merchant_id);
|
await this.saveFeatureOverrides(this.editingSub.merchant_id);
|
||||||
|
|
||||||
this.successMessage = `Subscription for "${this.editingSub.store_name || this.editingSub.merchant_name}" updated`;
|
this.successMessage = `Subscription for "${this.editingSub.merchant_name}" updated`;
|
||||||
|
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
await this.loadSubscriptions();
|
await this.loadSubscriptions();
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header_custom, th_sortable %}
|
{% from 'shared/macros/tables.html' import table_wrapper, table_header_custom, th_sortable %}
|
||||||
{% from 'shared/macros/pagination.html' import pagination %}
|
{% from 'shared/macros/pagination.html' import pagination %}
|
||||||
|
|
||||||
{% block title %}Store Subscriptions{% endblock %}
|
{% block title %}Merchant Subscriptions{% endblock %}
|
||||||
|
|
||||||
{% block alpine_data %}adminSubscriptions(){% endblock %}
|
{% block alpine_data %}adminSubscriptions(){% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ page_header_refresh('Store Subscriptions') }}
|
{{ page_header_refresh('Merchant Subscriptions') }}
|
||||||
|
|
||||||
{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }}
|
{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
x-model="filters.search"
|
x-model="filters.search"
|
||||||
@input.debounce.300ms="loadSubscriptions()"
|
@input.debounce.300ms="loadSubscriptions()"
|
||||||
placeholder="Search store name..."
|
placeholder="Search merchant name..."
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +140,8 @@
|
|||||||
{% call table_wrapper() %}
|
{% call table_wrapper() %}
|
||||||
<table class="w-full whitespace-nowrap">
|
<table class="w-full whitespace-nowrap">
|
||||||
{% call table_header_custom() %}
|
{% call table_header_custom() %}
|
||||||
{{ th_sortable('store_name', 'Store', 'sortBy', 'sortOrder') }}
|
{{ th_sortable('merchant_name', 'Merchant', 'sortBy', 'sortOrder') }}
|
||||||
|
<th class="px-4 py-3">Platform</th>
|
||||||
{{ th_sortable('tier', 'Tier', 'sortBy', 'sortOrder') }}
|
{{ th_sortable('tier', 'Tier', 'sortBy', 'sortOrder') }}
|
||||||
{{ th_sortable('status', 'Status', 'sortBy', 'sortOrder') }}
|
{{ th_sortable('status', 'Status', 'sortBy', 'sortOrder') }}
|
||||||
<th class="px-4 py-3 text-center">Features</th>
|
<th class="px-4 py-3 text-center">Features</th>
|
||||||
@@ -150,7 +151,7 @@
|
|||||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||||
<template x-if="loading">
|
<template x-if="loading">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
<td colspan="7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||||
<span x-html="$icon('refresh', 'inline w-6 h-6 animate-spin mr-2')"></span>
|
<span x-html="$icon('refresh', 'inline w-6 h-6 animate-spin mr-2')"></span>
|
||||||
Loading subscriptions...
|
Loading subscriptions...
|
||||||
</td>
|
</td>
|
||||||
@@ -158,7 +159,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template x-if="!loading && subscriptions.length === 0">
|
<template x-if="!loading && subscriptions.length === 0">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
<td colspan="7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||||
No subscriptions found.
|
No subscriptions found.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -168,11 +169,13 @@
|
|||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold text-gray-900 dark:text-gray-100" x-text="sub.store_name"></p>
|
<p class="font-semibold text-gray-900 dark:text-gray-100" x-text="sub.merchant_name"></p>
|
||||||
<p class="text-xs text-gray-500" x-text="sub.store_code"></p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-400" x-text="sub.platform_name || '-'"></span>
|
||||||
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<span class="px-2 py-1 text-xs font-medium rounded-full"
|
<span class="px-2 py-1 text-xs font-medium rounded-full"
|
||||||
:class="{
|
:class="{
|
||||||
@@ -204,7 +207,7 @@
|
|||||||
<button @click="openEditModal(sub)" class="p-2 text-gray-500 hover:text-purple-600 dark:hover:text-purple-400" title="Edit">
|
<button @click="openEditModal(sub)" class="p-2 text-gray-500 hover:text-purple-600 dark:hover:text-purple-400" title="Edit">
|
||||||
<span x-html="$icon('pencil', 'w-4 h-4')"></span>
|
<span x-html="$icon('pencil', 'w-4 h-4')"></span>
|
||||||
</button>
|
</button>
|
||||||
<a :href="'/admin/stores/' + sub.store_code" class="p-2 text-gray-500 hover:text-blue-600 dark:hover:text-blue-400" title="View Store">
|
<a :href="'/admin/merchants/' + sub.merchant_id" class="p-2 text-gray-500 hover:text-blue-600 dark:hover:text-blue-400" title="View Merchant">
|
||||||
<span x-html="$icon('external-link', 'w-4 h-4')"></span>
|
<span x-html="$icon('external-link', 'w-4 h-4')"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,20 +227,21 @@
|
|||||||
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center overflow-auto bg-black bg-opacity-50">
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center overflow-auto bg-black bg-opacity-50">
|
||||||
<div class="relative w-full max-w-lg p-6 mx-4 bg-white rounded-lg shadow-xl dark:bg-gray-800" @click.away="closeModal()">
|
<div class="relative w-full max-w-lg p-6 mx-4 bg-white rounded-lg shadow-xl dark:bg-gray-800" @click.away="closeModal()">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Edit Subscription</h3>
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Edit Subscription</h3>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4" x-text="'Store: ' + (editingSub?.store_name || '')"></p>
|
<p class="text-sm text-gray-500 dark:text-gray-400 mb-1" x-text="'Merchant: ' + (editingSub?.merchant_name || '')"></p>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4" x-show="editingSub?.platform_name" x-text="'Platform: ' + (editingSub?.platform_name || '')"></p>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Tier -->
|
<!-- Tier -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Tier</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Tier</label>
|
||||||
<select
|
<select
|
||||||
x-model="formData.tier"
|
x-model="formData.tier_code"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||||
|
:disabled="loadingTiers"
|
||||||
>
|
>
|
||||||
<option value="essential">Essential</option>
|
<template x-for="t in editTiers" :key="t.code">
|
||||||
<option value="professional">Professional</option>
|
<option :value="t.code" x-text="t.name"></option>
|
||||||
<option value="business">Business</option>
|
</template>
|
||||||
<option value="enterprise">Enterprise</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,12 @@ function adminMerchantDetail() {
|
|||||||
merchantId: null,
|
merchantId: null,
|
||||||
|
|
||||||
// Subscription state
|
// Subscription state
|
||||||
subscription: null,
|
platforms: [],
|
||||||
subscriptionTier: null,
|
subscriptions: [],
|
||||||
usageMetrics: [],
|
|
||||||
tiers: [],
|
tiers: [],
|
||||||
platformId: null,
|
tiersForPlatformId: null,
|
||||||
showCreateSubscriptionModal: false,
|
showCreateSubscriptionModal: false,
|
||||||
createForm: { tier_code: 'essential', status: 'trial', trial_days: 14, is_annual: false },
|
createForm: { platform_id: null, tier_code: 'essential', status: 'trial', trial_days: 14, is_annual: false },
|
||||||
creatingSubscription: false,
|
creatingSubscription: false,
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
@@ -49,9 +48,7 @@ function adminMerchantDetail() {
|
|||||||
merchantDetailLog.info('Viewing merchant:', this.merchantId);
|
merchantDetailLog.info('Viewing merchant:', this.merchantId);
|
||||||
await this.loadMerchant();
|
await this.loadMerchant();
|
||||||
await this.loadPlatforms();
|
await this.loadPlatforms();
|
||||||
if (this.platformId) {
|
await this.loadSubscriptions();
|
||||||
await this.loadSubscription();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
merchantDetailLog.error('No merchant ID in URL');
|
merchantDetailLog.error('No merchant ID in URL');
|
||||||
this.error = 'Invalid merchant URL';
|
this.error = 'Invalid merchant URL';
|
||||||
@@ -98,64 +95,61 @@ function adminMerchantDetail() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Load platforms and find OMS platform ID
|
// Load all available platforms
|
||||||
async loadPlatforms() {
|
async loadPlatforms() {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/admin/platforms');
|
const response = await apiClient.get('/admin/platforms');
|
||||||
const platforms = response.platforms || [];
|
this.platforms = (response.platforms || []).map(p => ({ id: p.id, name: p.name, code: p.code }));
|
||||||
const oms = platforms.find(p => p.code === 'oms');
|
merchantDetailLog.info('Platforms loaded:', this.platforms.length);
|
||||||
if (oms) {
|
|
||||||
this.platformId = oms.id;
|
|
||||||
merchantDetailLog.info('OMS platform resolved:', this.platformId);
|
|
||||||
} else {
|
|
||||||
merchantDetailLog.warn('OMS platform not found');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
merchantDetailLog.warn('Failed to load platforms:', error.message);
|
merchantDetailLog.warn('Failed to load platforms:', error.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Load subscription for this merchant
|
// Load subscriptions for all platforms
|
||||||
async loadSubscription() {
|
async loadSubscriptions() {
|
||||||
if (!this.merchantId || !this.platformId) return;
|
if (!this.merchantId || this.platforms.length === 0) return;
|
||||||
|
|
||||||
merchantDetailLog.info('Loading subscription for merchant:', this.merchantId);
|
merchantDetailLog.info('Loading subscriptions for merchant:', this.merchantId);
|
||||||
|
this.subscriptions = [];
|
||||||
|
|
||||||
try {
|
for (const platform of this.platforms) {
|
||||||
const url = `/admin/subscriptions/merchants/${this.merchantId}/platforms/${this.platformId}`;
|
try {
|
||||||
window.LogConfig.logApiCall('GET', url, null, 'request');
|
const url = `/admin/subscriptions/merchants/${this.merchantId}/platforms/${platform.id}`;
|
||||||
|
const response = await apiClient.get(url);
|
||||||
|
const sub = response.subscription || response;
|
||||||
|
|
||||||
const response = await apiClient.get(url);
|
this.subscriptions.push({
|
||||||
window.LogConfig.logApiCall('GET', url, response, 'response');
|
subscription: sub,
|
||||||
|
tier: response.tier || null,
|
||||||
this.subscription = response.subscription || response;
|
features: response.features || [],
|
||||||
this.subscriptionTier = response.tier || null;
|
platform_id: platform.id,
|
||||||
this.usageMetrics = response.features || [];
|
platform_name: platform.name,
|
||||||
|
});
|
||||||
merchantDetailLog.info('Subscription loaded:', {
|
} catch (error) {
|
||||||
tier: this.subscription?.tier,
|
if (error.status !== 404) {
|
||||||
status: this.subscription?.status,
|
merchantDetailLog.warn(`Failed to load subscription for platform ${platform.name}:`, error.message);
|
||||||
features_count: this.usageMetrics.length
|
}
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (error.status === 404) {
|
|
||||||
merchantDetailLog.info('No subscription found for merchant');
|
|
||||||
this.subscription = null;
|
|
||||||
this.usageMetrics = [];
|
|
||||||
} else {
|
|
||||||
merchantDetailLog.warn('Failed to load subscription:', error.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
merchantDetailLog.info('Subscriptions loaded:', {
|
||||||
|
count: this.subscriptions.length,
|
||||||
|
platforms: this.subscriptions.map(e => e.platform_name)
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Load available subscription tiers
|
// Load available subscription tiers for a platform
|
||||||
async loadTiers() {
|
async loadTiers(platformId) {
|
||||||
if (this.tiers.length > 0) return;
|
if (this.tiers.length > 0 && this.tiersForPlatformId === platformId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/admin/subscriptions/tiers');
|
const url = platformId
|
||||||
|
? `/admin/subscriptions/tiers?platform_id=${platformId}`
|
||||||
|
: '/admin/subscriptions/tiers';
|
||||||
|
const response = await apiClient.get(url);
|
||||||
this.tiers = response.tiers || [];
|
this.tiers = response.tiers || [];
|
||||||
|
this.tiersForPlatformId = platformId;
|
||||||
merchantDetailLog.info('Loaded tiers:', this.tiers.length);
|
merchantDetailLog.info('Loaded tiers:', this.tiers.length);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
merchantDetailLog.warn('Failed to load tiers:', error.message);
|
merchantDetailLog.warn('Failed to load tiers:', error.message);
|
||||||
@@ -164,23 +158,32 @@ function adminMerchantDetail() {
|
|||||||
|
|
||||||
// Open create subscription modal
|
// Open create subscription modal
|
||||||
async openCreateSubscriptionModal() {
|
async openCreateSubscriptionModal() {
|
||||||
await this.loadTiers();
|
const firstPlatformId = this.platforms.length > 0 ? this.platforms[0].id : null;
|
||||||
this.createForm = { tier_code: 'essential', status: 'trial', trial_days: 14, is_annual: false };
|
this.createForm = { platform_id: firstPlatformId, tier_code: 'essential', status: 'trial', trial_days: 14, is_annual: false };
|
||||||
|
await this.loadTiers(firstPlatformId);
|
||||||
this.showCreateSubscriptionModal = true;
|
this.showCreateSubscriptionModal = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Reload tiers when platform changes in create modal
|
||||||
|
async onCreatePlatformChange() {
|
||||||
|
this.tiers = [];
|
||||||
|
this.tiersForPlatformId = null;
|
||||||
|
await this.loadTiers(this.createForm.platform_id);
|
||||||
|
},
|
||||||
|
|
||||||
// Create subscription for this merchant
|
// Create subscription for this merchant
|
||||||
async createSubscription() {
|
async createSubscription() {
|
||||||
if (!this.merchantId || !this.platformId) return;
|
if (!this.merchantId || !this.createForm.platform_id) return;
|
||||||
|
|
||||||
this.creatingSubscription = true;
|
this.creatingSubscription = true;
|
||||||
merchantDetailLog.info('Creating subscription for merchant:', this.merchantId);
|
const platformId = this.createForm.platform_id;
|
||||||
|
merchantDetailLog.info('Creating subscription for merchant:', this.merchantId, 'platform:', platformId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `/admin/subscriptions/merchants/${this.merchantId}/platforms/${this.platformId}`;
|
const url = `/admin/subscriptions/merchants/${this.merchantId}/platforms/${platformId}`;
|
||||||
const payload = {
|
const payload = {
|
||||||
merchant_id: parseInt(this.merchantId),
|
merchant_id: parseInt(this.merchantId),
|
||||||
platform_id: this.platformId,
|
platform_id: platformId,
|
||||||
tier_code: this.createForm.tier_code,
|
tier_code: this.createForm.tier_code,
|
||||||
status: this.createForm.status,
|
status: this.createForm.status,
|
||||||
trial_days: this.createForm.status === 'trial' ? parseInt(this.createForm.trial_days) : 0,
|
trial_days: this.createForm.status === 'trial' ? parseInt(this.createForm.trial_days) : 0,
|
||||||
@@ -195,7 +198,7 @@ function adminMerchantDetail() {
|
|||||||
Utils.showToast('Subscription created successfully', 'success');
|
Utils.showToast('Subscription created successfully', 'success');
|
||||||
merchantDetailLog.info('Subscription created');
|
merchantDetailLog.info('Subscription created');
|
||||||
|
|
||||||
await this.loadSubscription();
|
await this.loadSubscriptions();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.LogConfig.logError(error, 'Create Subscription');
|
window.LogConfig.logError(error, 'Create Subscription');
|
||||||
@@ -276,9 +279,7 @@ function adminMerchantDetail() {
|
|||||||
async refresh() {
|
async refresh() {
|
||||||
merchantDetailLog.info('=== MERCHANT REFRESH TRIGGERED ===');
|
merchantDetailLog.info('=== MERCHANT REFRESH TRIGGERED ===');
|
||||||
await this.loadMerchant();
|
await this.loadMerchant();
|
||||||
if (this.platformId) {
|
await this.loadSubscriptions();
|
||||||
await this.loadSubscription();
|
|
||||||
}
|
|
||||||
Utils.showToast(I18n.t('tenancy.messages.merchant_details_refreshed'), 'success');
|
Utils.showToast(I18n.t('tenancy.messages.merchant_details_refreshed'), 'success');
|
||||||
merchantDetailLog.info('=== MERCHANT REFRESH COMPLETE ===');
|
merchantDetailLog.info('=== MERCHANT REFRESH COMPLETE ===');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ function adminStoreDetail() {
|
|||||||
// Store detail page specific state
|
// Store detail page specific state
|
||||||
currentPage: 'store-detail',
|
currentPage: 'store-detail',
|
||||||
store: null,
|
store: null,
|
||||||
subscription: null,
|
subscriptions: [],
|
||||||
subscriptionTier: null,
|
|
||||||
usageMetrics: [],
|
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
storeCode: null,
|
storeCode: null,
|
||||||
@@ -44,7 +42,7 @@ function adminStoreDetail() {
|
|||||||
await this.loadStore();
|
await this.loadStore();
|
||||||
// Load subscription after store is loaded
|
// Load subscription after store is loaded
|
||||||
if (this.store?.id) {
|
if (this.store?.id) {
|
||||||
await this.loadSubscription();
|
await this.loadSubscriptions();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
detailLog.error('No store code in URL');
|
detailLog.error('No store code in URL');
|
||||||
@@ -102,14 +100,14 @@ function adminStoreDetail() {
|
|||||||
return formatted;
|
return formatted;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Load subscription data for this store via convenience endpoint
|
// Load subscriptions data for this store via convenience endpoint
|
||||||
async loadSubscription() {
|
async loadSubscriptions() {
|
||||||
if (!this.store?.id) {
|
if (!this.store?.id) {
|
||||||
detailLog.warn('Cannot load subscription: no store ID');
|
detailLog.warn('Cannot load subscriptions: no store ID');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
detailLog.info('Loading subscription for store:', this.store.id);
|
detailLog.info('Loading subscriptions for store:', this.store.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `/admin/subscriptions/store/${this.store.id}`;
|
const url = `/admin/subscriptions/store/${this.store.id}`;
|
||||||
@@ -118,24 +116,20 @@ function adminStoreDetail() {
|
|||||||
const response = await apiClient.get(url);
|
const response = await apiClient.get(url);
|
||||||
window.LogConfig.logApiCall('GET', url, response, 'response');
|
window.LogConfig.logApiCall('GET', url, response, 'response');
|
||||||
|
|
||||||
this.subscription = response.subscription;
|
this.subscriptions = response.subscriptions || [];
|
||||||
this.subscriptionTier = response.tier;
|
|
||||||
this.usageMetrics = response.features || [];
|
|
||||||
|
|
||||||
detailLog.info('Subscription loaded:', {
|
detailLog.info('Subscriptions loaded:', {
|
||||||
tier: this.subscription?.tier,
|
count: this.subscriptions.length,
|
||||||
status: this.subscription?.status,
|
platforms: this.subscriptions.map(e => e.platform_name)
|
||||||
features_count: this.usageMetrics.length
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 404 means no subscription exists - that's OK
|
// 404 means no subscription exists - that's OK
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
detailLog.info('No subscription found for store');
|
detailLog.info('No subscriptions found for store');
|
||||||
this.subscription = null;
|
this.subscriptions = [];
|
||||||
this.usageMetrics = [];
|
|
||||||
} else {
|
} else {
|
||||||
detailLog.warn('Failed to load subscription:', error.message);
|
detailLog.warn('Failed to load subscriptions:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,6 +32,13 @@
|
|||||||
<span x-html="$icon('edit', 'w-4 h-4 mr-2')"></span>
|
<span x-html="$icon('edit', 'w-4 h-4 mr-2')"></span>
|
||||||
Edit Merchant
|
Edit Merchant
|
||||||
</a>
|
</a>
|
||||||
|
<button
|
||||||
|
@click="openCreateSubscriptionModal()"
|
||||||
|
x-show="platforms.length > 0"
|
||||||
|
class="flex items-center px-4 py-2 text-sm font-medium leading-5 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 Subscription
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="deleteMerchant()"
|
@click="deleteMerchant()"
|
||||||
:disabled="merchant?.store_count > 0"
|
:disabled="merchant?.store_count > 0"
|
||||||
@@ -194,94 +201,99 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Subscription Card -->
|
<!-- Subscription Cards -->
|
||||||
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="subscription">
|
<template x-for="entry in subscriptions" :key="entry.subscription.id">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
<div class="flex items-center justify-between mb-4">
|
||||||
Subscription
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||||
</h3>
|
Subscription
|
||||||
</div>
|
<span x-show="entry.platform_name" class="text-sm font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
— <span x-text="entry.platform_name"></span>
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tier and Status -->
|
<!-- Tier and Status -->
|
||||||
<div class="flex flex-wrap items-center gap-4 mb-4">
|
<div class="flex flex-wrap items-center gap-4 mb-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-sm text-gray-600 dark:text-gray-400">Tier:</span>
|
<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"
|
<span class="px-2.5 py-0.5 text-sm font-medium rounded-full"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300': subscription?.tier === 'essential',
|
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300': entry.subscription?.tier === 'essential',
|
||||||
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': subscription?.tier === 'professional',
|
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': entry.subscription?.tier === 'professional',
|
||||||
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300': subscription?.tier === 'business',
|
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300': entry.subscription?.tier === 'business',
|
||||||
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300': subscription?.tier === 'enterprise'
|
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300': entry.subscription?.tier === 'enterprise'
|
||||||
}"
|
}"
|
||||||
x-text="subscription?.tier ? subscription.tier.charAt(0).toUpperCase() + subscription.tier.slice(1) : '-'">
|
x-text="entry.subscription?.tier ? entry.subscription.tier.charAt(0).toUpperCase() + entry.subscription.tier.slice(1) : '-'">
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</template>
|
<div class="flex items-center gap-2">
|
||||||
</div>
|
<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': entry.subscription?.status === 'active',
|
||||||
|
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': entry.subscription?.status === 'trial',
|
||||||
|
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300': entry.subscription?.status === 'past_due',
|
||||||
|
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300': entry.subscription?.status === 'cancelled' || entry.subscription?.status === 'expired'
|
||||||
|
}"
|
||||||
|
x-text="entry.subscription?.status ? entry.subscription.status.replace('_', ' ').charAt(0).toUpperCase() + entry.subscription.status.slice(1) : '-'">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<template x-if="entry.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>
|
||||||
|
|
||||||
<!-- Usage Meters -->
|
<!-- Period Info -->
|
||||||
<div class="grid gap-4 md:grid-cols-3">
|
<div class="flex flex-wrap gap-4 mb-4 text-sm">
|
||||||
<template x-for="metric in usageMetrics" :key="metric.name">
|
<div>
|
||||||
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700">
|
<span class="text-gray-600 dark:text-gray-400">Period:</span>
|
||||||
<div class="flex items-center justify-between mb-2">
|
<span class="ml-1 text-gray-700 dark:text-gray-300" x-text="formatDate(entry.subscription?.period_start)"></span>
|
||||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400 uppercase" x-text="metric.name"></span>
|
<span class="text-gray-400">→</span>
|
||||||
|
<span class="text-gray-700 dark:text-gray-300" x-text="formatDate(entry.subscription?.period_end)"></span>
|
||||||
|
</div>
|
||||||
|
<template x-if="entry.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(entry.subscription?.trial_ends_at)"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-baseline gap-1">
|
</template>
|
||||||
<span class="text-xl font-bold text-gray-700 dark:text-gray-200" x-text="metric.current"></span>
|
</div>
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
/ <span x-text="metric.is_unlimited ? '∞' : metric.limit"></span>
|
<!-- Usage Meters -->
|
||||||
</span>
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
</div>
|
<template x-for="metric in entry.features" :key="metric.name">
|
||||||
<div class="mt-2 h-1.5 bg-gray-200 rounded-full dark:bg-gray-600" x-show="!metric.is_unlimited">
|
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700">
|
||||||
<div class="h-1.5 rounded-full transition-all"
|
<div class="flex items-center justify-between mb-2">
|
||||||
:class="getUsageBarColor(metric.current, metric.limit)"
|
<span class="text-xs font-medium text-gray-600 dark:text-gray-400 uppercase" x-text="metric.name"></span>
|
||||||
:style="`width: ${Math.min(100, metric.percentage || 0)}%`">
|
</div>
|
||||||
|
<div class="flex items-baseline gap-1">
|
||||||
|
<span class="text-xl font-bold text-gray-700 dark:text-gray-200" x-text="metric.current"></span>
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
/ <span x-text="metric.is_unlimited ? '∞' : metric.limit"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 h-1.5 bg-gray-200 rounded-full dark:bg-gray-600" x-show="!metric.is_unlimited">
|
||||||
|
<div class="h-1.5 rounded-full transition-all"
|
||||||
|
:class="getUsageBarColor(metric.current, metric.limit)"
|
||||||
|
:style="`width: ${Math.min(100, metric.percentage || 0)}%`">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
<template x-if="entry.features.length === 0">
|
||||||
<template x-if="usageMetrics.length === 0">
|
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700 md:col-span-3">
|
||||||
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700 md:col-span-3">
|
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">No usage data available</p>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">No usage data available</p>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<!-- No Subscription Notice -->
|
<!-- 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 && platformId">
|
<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="subscriptions.length === 0 && !loading && platforms.length > 0">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span x-html="$icon('exclamation', 'w-5 h-5 text-yellow-600 dark:text-yellow-400')"></span>
|
<span x-html="$icon('exclamation', 'w-5 h-5 text-yellow-600 dark:text-yellow-400')"></span>
|
||||||
<div>
|
<div>
|
||||||
@@ -302,6 +314,18 @@
|
|||||||
<div class="relative z-10 w-full max-w-md p-6 bg-white rounded-lg shadow-xl dark:bg-gray-800">
|
<div class="relative z-10 w-full max-w-md p-6 bg-white rounded-lg shadow-xl dark:bg-gray-800">
|
||||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Create Subscription</h3>
|
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Create Subscription</h3>
|
||||||
|
|
||||||
|
<!-- Platform Selector -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Platform</label>
|
||||||
|
<select x-model="createForm.platform_id"
|
||||||
|
@change="onCreatePlatformChange()"
|
||||||
|
class="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-500">
|
||||||
|
<template x-for="p in platforms" :key="p.id">
|
||||||
|
<option :value="p.id" x-text="p.name"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tier Selector -->
|
<!-- Tier Selector -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Subscription Tier</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Subscription Tier</label>
|
||||||
|
|||||||
@@ -106,100 +106,105 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Subscription Card -->
|
<!-- Subscription Cards -->
|
||||||
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="subscription">
|
<template x-for="entry in subscriptions" :key="entry.subscription.id">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
<div class="flex items-center justify-between mb-4">
|
||||||
Subscription
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||||
</h3>
|
Subscription
|
||||||
<a
|
<span x-show="entry.platform_name" class="text-sm font-normal text-gray-500 dark:text-gray-400">
|
||||||
:href="'/admin/merchants/' + store?.merchant_id"
|
— <span x-text="entry.platform_name"></span>
|
||||||
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>
|
||||||
<span x-html="$icon('external-link', 'w-4 h-4 mr-1')"></span>
|
</h3>
|
||||||
Manage on Merchant Page
|
<a
|
||||||
</a>
|
:href="'/admin/merchants/' + store?.merchant_id"
|
||||||
</div>
|
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('external-link', 'w-4 h-4 mr-1')"></span>
|
||||||
|
Manage on Merchant Page
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tier and Status -->
|
<!-- Tier and Status -->
|
||||||
<div class="flex flex-wrap items-center gap-4 mb-4">
|
<div class="flex flex-wrap items-center gap-4 mb-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-sm text-gray-600 dark:text-gray-400">Tier:</span>
|
<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"
|
<span class="px-2.5 py-0.5 text-sm font-medium rounded-full"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300': subscription?.tier === 'essential',
|
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300': entry.subscription?.tier === 'essential',
|
||||||
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': subscription?.tier === 'professional',
|
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': entry.subscription?.tier === 'professional',
|
||||||
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300': subscription?.tier === 'business',
|
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300': entry.subscription?.tier === 'business',
|
||||||
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300': subscription?.tier === 'enterprise'
|
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300': entry.subscription?.tier === 'enterprise'
|
||||||
}"
|
}"
|
||||||
x-text="subscription?.tier ? subscription.tier.charAt(0).toUpperCase() + subscription.tier.slice(1) : '-'">
|
x-text="entry.subscription?.tier ? entry.subscription.tier.charAt(0).toUpperCase() + entry.subscription.tier.slice(1) : '-'">
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</template>
|
<div class="flex items-center gap-2">
|
||||||
</div>
|
<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': entry.subscription?.status === 'active',
|
||||||
|
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': entry.subscription?.status === 'trial',
|
||||||
|
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300': entry.subscription?.status === 'past_due',
|
||||||
|
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300': entry.subscription?.status === 'cancelled' || entry.subscription?.status === 'expired'
|
||||||
|
}"
|
||||||
|
x-text="entry.subscription?.status ? entry.subscription.status.replace('_', ' ').charAt(0).toUpperCase() + entry.subscription.status.slice(1) : '-'">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<template x-if="entry.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>
|
||||||
|
|
||||||
<!-- Usage Meters -->
|
<!-- Period Info -->
|
||||||
<div class="grid gap-4 md:grid-cols-3">
|
<div class="flex flex-wrap gap-4 mb-4 text-sm">
|
||||||
<template x-for="metric in usageMetrics" :key="metric.name">
|
<div>
|
||||||
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700">
|
<span class="text-gray-600 dark:text-gray-400">Period:</span>
|
||||||
<div class="flex items-center justify-between mb-2">
|
<span class="ml-1 text-gray-700 dark:text-gray-300" x-text="formatDate(entry.subscription?.period_start)"></span>
|
||||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400 uppercase" x-text="metric.name"></span>
|
<span class="text-gray-400">→</span>
|
||||||
|
<span class="text-gray-700 dark:text-gray-300" x-text="formatDate(entry.subscription?.period_end)"></span>
|
||||||
|
</div>
|
||||||
|
<template x-if="entry.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(entry.subscription?.trial_ends_at)"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-baseline gap-1">
|
</template>
|
||||||
<span class="text-xl font-bold text-gray-700 dark:text-gray-200" x-text="metric.current"></span>
|
</div>
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
/ <span x-text="metric.is_unlimited ? '∞' : metric.limit"></span>
|
<!-- Usage Meters -->
|
||||||
</span>
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
</div>
|
<template x-for="metric in entry.features" :key="metric.name">
|
||||||
<div class="mt-2 h-1.5 bg-gray-200 rounded-full dark:bg-gray-600" x-show="!metric.is_unlimited">
|
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700">
|
||||||
<div class="h-1.5 rounded-full transition-all"
|
<div class="flex items-center justify-between mb-2">
|
||||||
:class="getUsageBarColor(metric.current, metric.limit)"
|
<span class="text-xs font-medium text-gray-600 dark:text-gray-400 uppercase" x-text="metric.name"></span>
|
||||||
:style="`width: ${Math.min(100, metric.percentage || 0)}%`">
|
</div>
|
||||||
|
<div class="flex items-baseline gap-1">
|
||||||
|
<span class="text-xl font-bold text-gray-700 dark:text-gray-200" x-text="metric.current"></span>
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
/ <span x-text="metric.is_unlimited ? '∞' : metric.limit"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 h-1.5 bg-gray-200 rounded-full dark:bg-gray-600" x-show="!metric.is_unlimited">
|
||||||
|
<div class="h-1.5 rounded-full transition-all"
|
||||||
|
:class="getUsageBarColor(metric.current, metric.limit)"
|
||||||
|
:style="`width: ${Math.min(100, metric.percentage || 0)}%`">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
<template x-if="entry.features.length === 0">
|
||||||
<template x-if="usageMetrics.length === 0">
|
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700 md:col-span-3">
|
||||||
<div class="p-3 bg-gray-50 rounded-lg dark:bg-gray-700 md:col-span-3">
|
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">No usage data available</p>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">No usage data available</p>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<!-- No Subscription Notice -->
|
<!-- 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="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="subscriptions.length === 0 && !loading">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span x-html="$icon('exclamation', 'w-5 h-5 text-yellow-600 dark:text-yellow-400')"></span>
|
<span x-html="$icon('exclamation', 'w-5 h-5 text-yellow-600 dark:text-yellow-400')"></span>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user