BCards/src/BCards.Web/Views/Admin/Dashboard.cshtml
2025-08-17 20:50:40 -03:00

600 lines
30 KiB
Plaintext

@model BCards.Web.ViewModels.DashboardViewModel
@{
ViewData["Title"] = "Dashboard - BCards";
Layout = "_Layout";
}
<div class="container py-4">
<div class="row">
<div class="col-md-8">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1>Olá, @Model.CurrentUser.Name!</h1>
<p class="text-muted mb-0">Gerencie suas páginas profissionais</p>
</div>
<div>
@if (!string.IsNullOrEmpty(Model.CurrentUser.ProfileImage))
{
<img src="@Model.CurrentUser.ProfileImage" alt="@Model.CurrentUser.Name"
class="rounded-circle" style="width: 60px; height: 60px; object-fit: cover;">
}
</div>
</div>
<!-- Lista de Páginas -->
<div class="row">
@foreach (var pageItem in Model.UserPages)
{
<div class="col-md-6 col-lg-6 mb-4">
<div class="card h-100 @(pageItem.Status == BCards.Web.ViewModels.PageStatus.Active ? "" : "border-warning")">
<div class="card-body">
<h6 class="card-title">
@(pageItem.DisplayName)
<form method="post" action="/Admin/DeletePage/@(pageItem.Id)" style="display: inline;" onsubmit="return confirm('Tem certeza que deseja excluir esta página?')">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-link text-danger p-0" title="Excluir página"
style="font-size: 12px; text-decoration: none;">
<i class="fas fa-trash"></i>
</button>
</form>
<input type="hidden" id="displayName_@pageItem.Id" value="@(pageItem.DisplayName)" />
</h6>
<p class="text-muted small mb-2">@(pageItem.Category)/@(pageItem.Slug)</p>
<div class="mb-2">
@{
var pageStatus = pageItem.Status;
if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Inactive)
{
if (pageItem.LastModerationStatus.HasValue)
{
pageStatus = pageItem.LastModerationStatus.Value;
}
}
}
@switch (pageStatus)
{
case BCards.Web.ViewModels.PageStatus.Active:
<span class="badge bg-success">Ativa</span>
break;
case BCards.Web.ViewModels.PageStatus.Expired:
<span class="badge bg-danger">Expirada</span>
break;
case BCards.Web.ViewModels.PageStatus.PendingPayment:
<span class="badge bg-warning">Pagamento Pendente</span>
break;
case BCards.Web.ViewModels.PageStatus.Inactive:
<span class="badge bg-secondary">Inativa</span>
break;
case BCards.Web.ViewModels.PageStatus.PendingModeration:
<span class="badge bg-warning">Aguardando</span>
break;
case BCards.Web.ViewModels.PageStatus.Rejected:
<span class="badge bg-danger">Rejeitada</span>
break;
case BCards.Web.ViewModels.PageStatus.Creating:
<span class="badge bg-info">
<i class="fas fa-edit me-1"></i>Criando
</span>
break;
}
</div>
@if (Model.CurrentPlan.AllowsAnalytics)
{
<div class="row text-center small mb-3">
<div class="col-6">
<div class="text-primary fw-bold">@(pageItem.TotalViews)</div>
<div class="text-muted">Visualizações</div>
</div>
<div class="col-6">
<div class="text-success fw-bold">@(pageItem.TotalClicks)</div>
<div class="text-muted">Cliques</div>
</div>
</div>
}
</div>
<!-- Cards com Hover Effect -->
<div class="card-footer" data-page-id="@pageItem.Id" data-status="@pageItem.Status">
<div class="d-flex gap-2">
@if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Active)
{
<a href="/page/@pageItem.Category/@pageItem.Slug" target="_blank"
class="btn btn-success flex-fill">
<i class="fas fa-eye me-1"></i>Ver Página
</a>
}
else if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Creating ||
pageItem.Status == BCards.Web.ViewModels.PageStatus.Rejected ||
pageItem.Status == BCards.Web.ViewModels.PageStatus.PendingModeration)
{
<button type="button"
class="btn btn-outline-info flex-fill"
onclick="openPreview('@pageItem.Id')"
data-page-category="@pageItem.Category"
data-page-slug="@pageItem.Slug">
<i class="fas fa-vial me-1"></i>Testar Página
</button>
}
else
{
<button class="btn btn-secondary flex-fill" disabled>
<i class="fas fa-ban me-1"></i>Indisponível
</button>
}
<div class="btn-group">
<button class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton@(pageItem.Id)"
data-bs-toggle="dropdown"
aria-expanded="false"
title="Mais opções">
<i class="fas fa-cog"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuButton@(pageItem.Id)">
<!-- Editar - sempre presente -->
@if (pageItem.Status == BCards.Web.ViewModels.PageStatus.PendingModeration)
{
<li>
<span class="dropdown-item disabled">
<i class="fas fa-edit me-2"></i>Editar
</span>
</li>
}
else
{
<li>
<a href="@Url.Action("ManagePage", new { id = pageItem.Id })"
class="dropdown-item">
<i class="fas fa-edit me-2"></i>Editar
</a>
</li>
}
@if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Creating ||
pageItem.Status == BCards.Web.ViewModels.PageStatus.Rejected)
{
<li><hr class="dropdown-divider"></li>
<li>
<button type="button"
class="dropdown-item"
onclick="submitForModeration('@pageItem.Id')"
data-page-name="@pageItem.DisplayName">
<i class="fas fa-paper-plane me-2"></i>Enviar para Moderação
</button>
</li>
}
else if (pageItem.Status == BCards.Web.ViewModels.PageStatus.PendingModeration)
{
<li><hr class="dropdown-divider"></li>
<li>
<span class="dropdown-item disabled">
<i class="fas fa-hourglass-half me-2"></i>Aguardando Moderação
</span>
</li>
}
</ul>
</div>
</div>
<!-- Informações da página movidas para baixo dos botões -->
<div class="px-3 pt-2 pb-1">
<small class="text-muted">Criada em @(pageItem.CreatedAt.ToString("dd/MM/yyyy"))</small>
@if ((pageItem.LastModerationStatus ?? pageItem.Status) == BCards.Web.ViewModels.PageStatus.Rejected && !string.IsNullOrEmpty(pageItem.Motive))
{
<div class="alert alert-danger alert-dismissible fade show mt-2 mb-0" role="alert">
<div class="d-flex align-items-start">
<i class="fas fa-exclamation-triangle me-2 mt-1"></i>
<div class="flex-grow-1">
<strong>Motivo da rejeição:</strong><br>
<small>@(pageItem.Motive)</small>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
else if (pageItem.LastModerationStatus == BCards.Web.ViewModels.PageStatus.Active && !string.IsNullOrEmpty(pageItem.Motive))
{
<div class="alert alert-info alert-dismissible fade show mt-2 mb-0" role="alert">
<div class="d-flex align-items-start">
<i class="fas fa-exclamation-triangle me-2 mt-1"></i>
<div class="flex-grow-1">
<strong>Motivo:</strong><br>
<small>@(pageItem.Motive)</small>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
</div>
</div>
</div>
</div>
@if ((pageItem.LastModerationStatus ?? pageItem.Status) == BCards.Web.ViewModels.PageStatus.Creating)
{
<div class="col-12">
<div class="alert alert-secondary d-flex align-items-center alert-dismissible alert-permanent fade show">
<i class="fas fa-exclamation-triangle me-3"></i>
<div>
<strong>Página em criação!</strong>
Você pode editar e testar quantas vezes quiser. <br />
Ao terminar, clique em <i class="fas fa-ellipsis-v"></i> para enviar a página <b><span id="pageNameDisplay"></span></b> para moderação!
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
<script>
var pageNameDisplay = document.getElementById('pageNameDisplay');
var displayName = document.getElementById('displayName_@pageItem.Id');
pageNameDisplay.innerHTML = displayName.value;
</script>
}
}
<!-- Card para Criar Nova Página -->
@if (Model.CanCreateNewPage)
{
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 border-dashed text-center" style="border: 2px dashed #dee2e6;">
<div class="card-body d-flex align-items-center justify-content-center">
<div>
<i class="fas fa-plus fa-2x text-muted mb-3"></i>
<h6 class="text-muted">Criar Nova Página</h6>
<a href="@Url.Action("ManagePage", new { id = "new" })"
class="btn btn-primary">Começar</a>
</div>
</div>
</div>
</div>
}
else if (!Model.UserPages.Any())
{
<!-- Primeira Página -->
<div class="col-12">
<div class="card border-primary">
<div class="card-body text-center p-5">
<div class="mb-4">
<i class="display-1 text-primary">🚀</i>
</div>
<h3>Crie sua primeira página!</h3>
<p class="text-muted mb-4">
Comece criando sua página profissional personalizada com seus links organizados.
</p>
<a href="@Url.Action("ManagePage", new { id = "new" })" class="btn btn-primary btn-lg">
Criar Minha Página
</a>
</div>
</div>
</div>
}
else
{
<!-- Limite atingido -->
<div class="col-12">
<div class="alert alert-warning d-flex align-items-center alert-permanent">
<i class="fas fa-exclamation-triangle me-3"></i>
<div>
<strong>Limite atingido!</strong>
Você já criou o máximo de @Model.CurrentPlan.MaxPages página(s) para seu plano atual.
<a href="@Url.Action("Pricing", "Home")" class="alert-link ms-2">Fazer upgrade</a>
</div>
</div>
</div>
}
</div>
</div>
<div class="col-md-4">
<!-- Plano Atual -->
<div class="card mb-4 @(Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial ? "border-warning" : "")">
<div class="card-header @(Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial ? "bg-warning" : "bg-primary") text-white">
<h6 class="mb-0">
<i class="fas fa-crown me-2"></i>
Plano Atual
</h6>
</div>
<div class="card-body">
<h5 class="text-capitalize mb-1">@Model.CurrentPlan.Name</h5>
@if (Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial)
{
<p class="text-warning mb-2">
<i class="fas fa-clock me-1"></i>
@Model.DaysRemaining dia(s) restante(s)
</p>
}
else
{
<p class="text-muted small mb-2">R$ @Model.CurrentPlan.Price.ToString("F2")/mês</p>
}
<div class="mb-3">
<div class="d-flex justify-content-between small mb-1">
<span>Páginas</span>
<span>@Model.UserPages.Count/@Model.CurrentPlan.MaxPages</span>
</div>
<div class="progress" style="height: 6px;">
@{
var pagesPercentage = Model.CurrentPlan.MaxPages > 0 ?
(double)Model.UserPages.Count / Model.CurrentPlan.MaxPages * 100 : 0;
}
<div class="progress-bar @(pagesPercentage >= 80 ? "bg-warning" : "bg-primary")"
style="width: @pagesPercentage%"></div>
</div>
</div>
<div class="small mb-2">
<i class="fas fa-link me-2"></i>
Links por página: @(Model.CurrentPlan.MaxLinksPerPage == int.MaxValue ? "Ilimitado" : Model.CurrentPlan.MaxLinksPerPage.ToString())
</div>
<div class="small mb-2">
<i class="fas fa-chart-bar me-2"></i>
Analytics: @(Model.CurrentPlan.AllowsAnalytics ? "✅" : "❌")
</div>
<div class="small mb-3">
<i class="fas fa-palette me-2"></i>
Temas premium: @(Model.CurrentPlan.AllowsCustomThemes ? "✅" : "❌")
</div>
@if (Model.CurrentPlan.Type == BCards.Web.Models.PlanType.Trial)
{
<a href="@Url.Action("Pricing", "Home")" class="btn btn-warning w-100">
<i class="fas fa-rocket me-2"></i>
Fazer Upgrade
</a>
}
else
{
<a href="@Url.Action("ManageSubscription", "Payment")" class="btn btn-outline-secondary w-100">
<i class="fas fa-cog me-2"></i>
Gerenciar Assinatura
</a>
}
</div>
</div>
<!-- Estatísticas Rápidas -->
@if (Model.CurrentPlan.AllowsAnalytics && Model.UserPages.Any())
{
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-chart-line me-2"></i>
Estatísticas Gerais
</h6>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-6">
<div class="h4 text-primary mb-0">@Model.UserPages.Sum(p => p.TotalViews)</div>
<small class="text-muted">Total de Visualizações</small>
</div>
<div class="col-6">
<div class="h4 text-success mb-0">@Model.UserPages.Sum(p => p.TotalClicks)</div>
<small class="text-muted">Total de Cliques</small>
</div>
</div>
@if (Model.UserPages.Sum(p => p.TotalViews) > 0)
{
<hr class="my-3">
<div class="text-center">
<div class="h5 text-info mb-0">
@((Model.UserPages.Sum(p => p.TotalClicks) * 100.0 / Model.UserPages.Sum(p => p.TotalViews)).ToString("F1"))%
</div>
<small class="text-muted">Taxa de Cliques</small>
</div>
}
</div>
</div>
}
<!-- Dicas -->
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-lightbulb me-2"></i>
💡 Dicas
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled small mb-0">
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
Use uma bio clara e objetiva
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
Organize seus links por importância
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
Escolha URLs fáceis de lembrar
</li>
<li class="mb-2">
<i class="fas fa-check text-success me-2"></i>
Atualize regularmente seus links
</li>
<li class="mb-0">
<i class="fas fa-check text-success me-2"></i>
Monitore suas estatísticas
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
// Função para abrir preview com token fresh
async function openPreview(pageId) {
const button = event.target.closest('button');
const category = button.dataset.pageCategory;
const slug = button.dataset.pageSlug;
// Desabilitar botão temporariamente
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Carregando...';
try {
// Gerar novo token
const response = await fetch(`/Admin/GeneratePreviewToken/${pageId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
}
});
const result = await response.json();
if (result.success) {
// Abrir preview em nova aba com token novo
const previewUrl = `${window.location.origin}/page/${category}/${slug}?preview=${result.previewToken}`;
window.open(previewUrl, '_blank');
} else {
showToast(result.message || 'Erro ao gerar preview', 'error');
}
} catch (error) {
console.error('Erro ao gerar preview:', error);
showToast('Erro ao gerar preview. Tente novamente.', 'error');
} finally {
// Reabilitar botão
button.disabled = false;
button.innerHTML = originalText;
}
}
async function submitForModeration(pageId) {
const pageName = event.target.dataset.pageName || 'esta página';
if (!confirm(`Enviar "${pageName}" para moderação?\n\nApós enviar, você não poderá mais editá-la até receber o resultado da análise.`)) {
return;
}
// Desabilitar botão durante envio
const button = event.target;
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Enviando...';
try {
const response = await fetch(`/Admin/SubmitForModeration/${pageId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
}
});
const result = await response.json();
if (result.success) {
// Mostrar toast de sucesso
showToast(result.message, 'success');
// Recarregar página após 2 segundos
setTimeout(() => {
location.reload();
}, 2000);
} else {
showToast(result.message || 'Erro ao enviar página', 'error');
// Reabilitar botão
button.disabled = false;
button.innerHTML = originalText;
}
} catch (error) {
console.error('Erro:', error);
showToast('Erro ao enviar página para moderação', 'error');
// Reabilitar botão
button.disabled = false;
button.innerHTML = originalText;
}
}
function showToast(message, type) {
const toastContainer = getOrCreateToastContainer();
const bgClass = type === 'success' ? 'bg-success' : type === 'error' ? 'bg-danger' : 'bg-info';
const icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-triangle' : 'fa-info-circle';
const toastHtml = `
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="5000">
<div class="toast-header ${bgClass} text-white">
<i class="fas ${icon} me-2"></i>
<strong class="me-auto">${type === 'success' ? 'Sucesso' : type === 'error' ? 'Erro' : 'Informação'}</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">${message}</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const newToast = toastContainer.lastElementChild;
const toast = new bootstrap.Toast(newToast);
toast.show();
// Remover toast após ser fechado
newToast.addEventListener('hidden.bs.toast', function() {
newToast.remove();
});
}
function getOrCreateToastContainer() {
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container position-fixed top-0 end-0 p-3';
container.style.zIndex = '1055';
document.body.appendChild(container);
}
return container;
}
</script>
}
@if (TempData["Success"] != null)
{
<div class="toast-container position-fixed top-0 end-0 p-3">
<div class="toast show" role="alert">
<div class="toast-header">
<i class="fas fa-check-circle text-success me-2"></i>
<strong class="me-auto">Sucesso</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
@TempData["Success"]
</div>
</div>
</div>
}
@if (TempData["Error"] != null)
{
<div class="toast-container position-fixed top-0 end-0 p-3">
<div class="toast show" role="alert">
<div class="toast-header">
<i class="fas fa-exclamation-triangle text-warning me-2"></i>
<strong class="me-auto">Atenção</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
@TempData["Error"]
</div>
</div>
</div>
}