Files
BarangaySystem/resources/js/Pages/UltimateConsole.vue
2026-06-06 18:43:00 +08:00

1628 lines
77 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="ultimate-console min-vh-100 bg-white text-dark pb-5">
<!-- Premium Header -->
<header class="header-ultimate text-white py-1 shadow-2xl border-bottom border-primary border-4 position-relative overflow-hidden sticky-top" style="z-index: 1020;">
<!-- Decorative Background Elements -->
<div class="header-mesh-radial position-absolute top-0 start-0 w-100 h-100 opacity-50"></div>
<div class="container py-2 position-relative" style="z-index: 2;">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-4">
<div class="d-flex align-items-center gap-4 animate-fade-in">
<div class="display-container position-relative">
<div class="icon-glow position-absolute top-50 start-50 translate-middle"></div>
<i class="fas fa-user-shield fa-4x text-primary-gradient position-relative" style="z-index: 3;"></i>
<div class="pulse-ring-premium position-absolute top-50 start-50 translate-middle"></div>
</div>
<div>
<h1 class="display-5 fw-black text-white mb-0 ls-tight">Ultimate <span class="text-primary-gradient">Console</span></h1>
<p class="text-white-50 fw-medium small text-uppercase ls-wide mt-1 letter-spacing-2">
System Administration & Database Power Tools
</p>
</div>
</div>
<div class="stats-pills d-flex flex-wrap ">
<div v-for="(val, label) in quickStats" :key="label" class="badge-pill-premium d-flex m-1 align-items-center gap-3">
<div class="pill-icon-box">
<i v-if="label === 'Users'" class="fas fa-users-cog"></i>
<i v-else-if="label === 'Stores'" class="fas fa-store"></i>
<i v-else-if="label === 'Transactions'" class="fas fa-exchange-alt"></i>
<i v-else-if="label === 'POS Sessions'" class="fas fa-cash-register"></i>
<i v-else-if="label === 'Sys Logs'" class="fas fa-file-invoice"></i>
<i v-else-if="label === 'Balance'" class="fas fa-wallet"></i>
<i v-else class="fas fa-chart-bar"></i>
</div>
<div>
<div class="small fw-bold text-white-50 text-uppercase ls-wide" style="font-size: 0.65rem;">{{ label }}</div>
<div v-if="statsLoading" class="spinner-border spinner-border-sm text-primary"></div>
<div v-else class="fw-black h5 mb-0 text-white counter-animation">{{ val }}</div>
</div>
</div>
</div>
</div>
</div>
</header>
<div class="container px-3">
<div class="row g-4 justify-content-center">
<!-- Sidebar Navigation -->
<div class="col-lg-3">
<div class="card border-0 shadow-lg rounded-4 p-3 bg-white hover-up overflow-hidden sticky-top" style="top: 18rem; z-index: 10;">
<div class="d-flex flex-column gap-2">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
class="btn rounded-4 border-0 text-start px-3 py-2 fw-bold d-flex align-items-center gap-3 transition-all"
:class="activeTab === tab.id ? 'btn-primary-gradient shadow-primary-lg text-white' : 'btn-light text-muted hover-bg-light'"
>
<i :class="tab.icon"></i>
{{ tab.label }}
<i v-if="activeTab === tab.id" class="fas fa-chevron-right ms-auto"></i>
</button>
<hr class="my-3 opacity-10">
<div class="text-center p-3 rounded-4 bg-light small mb-2">
<div class="fw-bold mb-1">System Health</div>
<div class="d-flex align-items-center justify-content-center gap-2">
<span class="status-dot pulse" :class="redisHealthy ? 'bg-success' : 'bg-danger'"></span>
<span class="fw-bold" :class="redisHealthy ? 'text-success' : 'text-danger'">
{{ redisHealthy ? 'OPERATIONAL' : 'REDIS DOWN' }}
</span>
</div>
<div v-if="stats?.redis" class="mt-2 d-flex flex-column gap-1">
<div class="d-flex justify-content-between">
<span class="text-muted">Redis</span>
<span class="fw-bold" :class="redisHealthy ? 'text-success' : 'text-danger'">
{{ redisHealthy ? 'Connected' : 'Disconnected' }}
</span>
</div>
<div v-if="redisHealthy" class="d-flex justify-content-between">
<span class="text-muted">Ping</span>
<span class="fw-bold">{{ stats.redis.ping_ms }} ms</span>
</div>
<div v-if="redisHealthy && stats.redis.used_memory_human" class="d-flex justify-content-between">
<span class="text-muted">Memory</span>
<span class="fw-bold">{{ stats.redis.used_memory_human }}</span>
</div>
<div v-if="redisHealthy && stats.redis.version" class="d-flex justify-content-between">
<span class="text-muted">Version</span>
<span class="fw-bold">{{ stats.redis.version }}</span>
</div>
<div v-if="!redisHealthy && stats.redis.error" class="text-danger small text-truncate" :title="stats.redis.error">
{{ stats.redis.error }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Main Content Area -->
<div class="col-lg-9 col-xl-8">
<div class="content-container animate-slide-up">
<!-- Dashboard Tab -->
<div v-if="activeTab === 'dashboard'" class="tab-content">
<div class="row g-4 mb-4">
<div v-for="scard in statCards" :key="scard.label" class="col-md-4">
<div class="card border-0 shadow-sm rounded-4 p-4 text-center h-100 hover-up bg-white">
<div class="icon-shape rounded-circle mx-auto mb-3" :class="scard.colorClass">
<i :class="scard.icon"></i>
</div>
<div class="h3 fw-black mb-1 counter-animation">{{ scard.value }}</div>
<div class="small fw-bold text-muted text-uppercase ls-wide">{{ scard.label }}</div>
</div>
</div>
</div>
<!-- Maintenance Mode Card -->
<div class="card border-0 shadow-lg rounded-5 overflow-hidden bg-white mb-4">
<div class="card-body p-4">
<div class="row align-items-center">
<div class="col-lg-7">
<h4 class="fw-black mb-3 d-flex align-items-center gap-3">
<i class="fas fa-tools text-primary"></i>
System Maintenance
</h4>
<p class="text-muted pe-lg-5 mb-0">
When maintenance mode is enabled, non-ultimate accounts will see an "Application Down for Maintenance" screen. All API requests from unauthorized accounts will be blocked.
</p>
</div>
<div class="col-lg-5 text-lg-end mt-4 mt-lg-0">
<button
@click="maintenanceToggle"
class="btn btn-lg rounded-pill px-5 fw-black shadow-lg shadow-hover"
:class="stats?.maintenance_mode ? 'btn-danger' : 'btn-outline-success border-2'"
>
<i :class="stats?.maintenance_mode ? 'fas fa-power-off' : 'fas fa-play-circle'" class="me-2 fa-lg"></i>
{{ stats?.maintenance_mode ? 'Disable Maintenance' : 'Enable Maintenance' }}
</button>
</div>
</div>
</div>
</div>
<!-- Global Message Card -->
<div class="card border-0 shadow-lg rounded-5 bg-white overflow-hidden mb-4">
<div class="card-body p-4">
<h4 class="fw-black mb-4 d-flex align-items-center gap-3">
<i class="fas fa-bullhorn text-warning"></i>
Global System Broadcast
</h4>
<div class="d-flex align-items-center gap-2 bg-light rounded-pill p-2 mb-3 shadow-sm border border-2 border-white focus-within-shadow">
<div class="ps-4">
<i class="fas fa-pen-nib text-muted"></i>
</div>
<input
v-model="msgData.text"
type="text"
class="form-control bg-transparent border-0 border-0-focus shadow-none flex-grow-1"
placeholder="Type a global message for everyone..."
>
<button @click="broadcastMessage" class="btn btn-warning rounded-pill px-4 fw-black d-flex align-items-center gap-2">
<i class="fas fa-paper-plane fa-lg"></i>
Broadcast
</button>
</div>
<div class="d-flex flex-wrap gap-2 justify-content-center">
<button
v-for="t in ['info', 'success', 'warning', 'danger']"
:key="t"
@click="msgData.type = t"
class="btn btn-sm rounded-pill px-4 fw-bold text-uppercase border-2"
:class="msgData.type === t ? 'btn-' + t + ' shadow-sm' : 'btn-outline-' + t"
>
{{ t }}
</button>
</div>
</div>
</div>
</div>
<!-- Command Lab Tab -->
<div v-else-if="activeTab === 'lab'" class="tab-content">
<div class="card border-0 shadow-lg rounded-5 bg-white mb-4 overflow-hidden">
<div class="card-header bg-dark text-white border-0 py-3 px-4">
<h4 class="fw-black mb-0 d-flex align-items-center gap-3">
<i class="fas fa-terminal text-primary-gradient"></i>
Dev Commander
</h4>
</div>
<div class="card-body p-4">
<div class="alert alert-info border-0 rounded-4 mb-4 d-flex align-items-center gap-3">
<i class="fas fa-info-circle text-info"></i>
<span class="small fw-medium text-dark">Only specific administrative Artisan commands are allowed for safety.</span>
</div>
<div class="input-group rounded-4 overflow-hidden mb-4 border border-2 shadow-sm">
<span class="input-group-text bg-light border-0 ps-4">
<span class="text-primary-gradient fw-black">ARTISAN:</span>
</span>
<input
v-model="artisanCommand"
type="text"
class="form-control border-0 small font-monospace"
placeholder="e.g., config:clear"
>
<button @click="executeArtisan" class="btn btn-primary-gradient px-4 fw-black d-flex align-items-center gap-2">
<i class="fas fa-terminal fa-lg text-white"></i>
<span class="text-white">EXEC</span>
</button>
</div>
<!-- Quick Commands -->
<div class="d-flex flex-wrap gap-2 mb-4 animate-fade-in" style="animation-delay: 0.1s;">
<button
v-for="cmd in [
{ label: 'Migrate', cmd: 'migrate', icon: 'fas fa-database', color: 'btn-outline-primary' },
{ label: 'DB Seed', cmd: 'db:seed', icon: 'fas fa-seedling', color: 'btn-outline-success' },
{ label: 'Reset App (All Users)', cmd: 'reset-app all users', icon: 'fas fa-users-slash', color: 'btn-outline-danger' },
{ label: 'Clear Cache', cmd: 'cache:clear', icon: 'fas fa-broom', color: 'btn-outline-warning' },
{ label: 'Optimize', cmd: 'optimize', icon: 'fas fa-bolt', color: 'btn-outline-info' },
]"
:key="cmd.cmd"
@click="artisanCommand = cmd.cmd; executeArtisan();"
class="btn btn-sm rounded-pill px-3 fw-bold d-flex align-items-center gap-2 transition-all hover-up shadow-sm"
:class="cmd.color"
>
<i :class="cmd.icon"></i>
{{ cmd.label }}
</button>
</div>
<div v-if="commandOutput" class="terminal-view p-4 bg-dark text-white rounded-4 border-0 shadow-lg font-monospace small position-relative group mt-4">
<button @click="commandOutput = ''" class="btn btn-sm btn-outline-light rounded-pill position-absolute top-0 end-0 m-3 opacity-0 group-hover-opacity-100 transition-all">
Clear
</button>
<div class="text-light-50 mb-2 border-bottom border-white-10 pb-2">Command Output:</div>
<pre class="mb-0 text-success glow-text font-monospace">{{ commandOutput }}</pre>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-4">
<div class="card border-0 shadow-lg rounded-4 bg-white p-3 text-center h-100 shadow-hover border-hover-primary border-4-hover transition-all">
<div class="icon-shape bg-soft-primary rounded-circle mx-auto mb-4">
<i class="fas fa-database fa-2x"></i>
</div>
<h5 class="fw-black mb-3">Raw Query Lab</h5>
<p class="text-muted small px-3">Directly execute SQL queries against the database with real-time results table.</p>
<ButtonPrimary @click="showQueryModal = true" btnClass="btn btn-primary rounded-3 px-4 fw-semibold mt-auto shadow-sm d-flex align-items-center justify-content-center gap-2">
<i class="fas fa-database"></i>
Open Query Lab
</ButtonPrimary>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-lg rounded-4 bg-white p-3 text-center h-100 shadow-hover border-hover-info border-4-hover transition-all">
<div class="icon-shape bg-soft-info rounded-circle mx-auto mb-4">
<i class="fas fa-cloud-download-alt fa-2x"></i>
</div>
<h5 class="fw-black mb-3">Database Backup</h5>
<p class="text-muted small px-3">Generate and download a full SQL dump. System will enter maintenance mode briefly.</p>
<BaseButton @click="initiateBackup" btnClass="btn btn-info rounded-3 text-white shadow-sm d-flex align-items-center justify-content-center gap-2 px-4 fw-semibold mt-auto">
<i class="fas fa-cloud-download-alt"></i>
Backup & Download
</BaseButton>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-lg rounded-4 bg-white p-3 text-center h-100 shadow-hover border-hover-warning border-4-hover transition-all">
<div class="icon-shape bg-soft-warning rounded-circle mx-auto mb-4">
<i class="fas fa-tasks fa-2x text-warning"></i>
</div>
<h5 class="fw-black mb-3">Unit Testing</h5>
<p class="text-muted small px-3">Run system health checks, benchmark Redis connections, and verify SSE stability.</p>
<ButtonWarning btnClass="btn btn-warning rounded-3 px-4 fw-semibold mt-auto text-white shadow-sm d-flex align-items-center justify-content-center gap-2">
<i class="fas fa-tasks"></i>
Run Benchmarks
</ButtonWarning>
</div>
</div>
</div>
</div>
<!-- Batch Lab Tab -->
<div v-else-if="activeTab === 'batch'" class="tab-content animate-slide-up">
<div class="card border-0 shadow-lg rounded-5 bg-white p-4">
<h4 class="fw-black mb-4 d-flex align-items-center gap-3">
<i class="fas fa-layer-group text-primary"></i>
Bulk Operations Center
</h4>
<div class="row g-4">
<div v-for="b in batchOperations" :key="b.label" class="col-md-6">
<div class="p-4 rounded-5 border-2 border-dashed border-light bg-light-hover transition-all h-100">
<div class="d-flex align-items-center gap-3 mb-3">
<div class="icon-shape sm" :class="b.bg">
<i :class="b.icon"></i>
</div>
<div class="fw-black text-dark">{{ b.label }}</div>
</div>
<p class="small text-muted mb-4">{{ b.desc }}</p>
<button class="btn btn-white border rounded-pill px-4 btn-sm fw-bold hover-bg-primary hover-text-white w-100 shadow-sm">
Configure Operation
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Management Lab Tab -->
<div v-else-if="activeTab === 'manage'" class="tab-content animate-slide-up">
<div class="card border-0 shadow-lg rounded-5 bg-white p-4">
<h4 class="fw-black mb-4 d-flex align-items-center gap-3 text-danger">
<i class="fas fa-radiation text-danger"></i>
Destructive Zone
</h4>
<div class="p-4 rounded-4 bg-danger-5 border border-danger-10 mb-4">
<div class="d-flex align-items-start gap-3">
<i class="fas fa-exclamation-triangle mt-1 text-danger"></i>
<div class="small fw-bold text-danger">CRITICAL WARNING: The following operations cannot be undone. Always verify your backup status before performing data flushes.</div>
</div>
</div>
<div class="list-group list-group-flush rounded-4 overflow-hidden border">
<div v-for="flush in flushOptions" :key="flush.id" class="list-group-item p-4 d-flex align-items-center justify-content-between">
<div>
<div class="fw-black d-flex align-items-center gap-2">
{{ flush.label }}
<span class="badge bg-light text-dark fw-bold border small py-1 px-3 rounded-pill">{{ flush.rows }} records</span>
</div>
<div class="small text-muted">{{ flush.desc }}</div>
</div>
<button @click="doFlush(flush.id)" class="btn btn-outline-danger btn-sm rounded-pill px-4 fw-bold border-2">
Flush Now
</button>
</div>
</div>
</div>
</div>
<!-- Backups Lab Tab -->
<div v-if="activeTab === 'backups'" class="tab-content animate-slide-up">
<div class="card border-0 shadow-lg rounded-5 bg-white p-0 overflow-hidden">
<div class="card-header bg-primary text-white p-3 px-4 d-flex align-items-center justify-content-between">
<h4 class="fw-black mb-0 d-flex align-items-center gap-3">
<i class="fas fa-history text-white"></i>
Backup Repository
</h4>
<button @click="fetchBackups" class="btn btn-sm btn-glass-premium rounded-pill px-3 py-1 fw-bold">
<i class="fas fa-sync-alt me-2" :class="{'fa-spin': loadingBackups}"></i>
Refresh
</button>
</div>
<div class="card-body p-0">
<div class="table-responsive" style="min-height: 400px;">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-5">FILENAME</th>
<th>SIZE</th>
<th>CREATED BY</th>
<th>DATE</th>
<th class="text-end pe-5">ACTION</th>
</tr>
</thead>
<tbody>
<tr v-if="loadingBackups" class="text-center py-5">
<td colspan="5" class="py-5">
<div class="spinner-border text-primary mb-3"></div>
<div class="fw-bold text-muted opacity-50">RETRIEVING BACKUPS...</div>
</td>
</tr>
<tr v-else-if="backupData.length === 0" class="text-center py-5">
<td colspan="5" class="py-5">
<i class="fas fa-folder-open fa-3x text-muted opacity-25 mb-3"></i>
<div class="fw-bold text-muted opacity-50">NO BACKUPS FOUND IN DATABASE</div>
</td>
</tr>
<template v-else>
<tr v-for="backup in backupData" :key="backup.hashkey">
<td class="ps-5">
<div class="d-flex align-items-center gap-3">
<div class="icon-shape sm bg-soft-info rounded-3">
<i class="fas fa-file-archive text-info"></i>
</div>
<div>
<div class="d-flex align-items-center gap-2">
<div v-if="editingBackup === backup.hashkey" class="input-group input-group-sm">
<input v-model="backup.name" type="text" class="form-control form-control-sm rounded-start-pill" @keyup.enter="saveRename(backup)">
<button class="btn btn-primary btn-sm rounded-end-pill" @click="saveRename(backup)"><i class="fas fa-check"></i></button>
</div>
<div v-else class="fw-bold text-dark cursor-pointer hover-text-primary" @click="editingBackup = backup.hashkey">
{{ backup.name || backup.filename }}
<i class="fas fa-edit small ms-1 opacity-25"></i>
</div>
</div>
<div class="small text-muted font-monospace">{{ backup.hashkey.substring(0, 16) }}...</div>
</div>
</div>
</td>
<td>
<span class="badge bg-light text-dark border rounded-pill">{{ formatBytes(backup.size_in_bytes) }}</span>
</td>
<td>
<div v-if="backup.creator" class="d-flex align-items-center gap-2">
<img :src="`https://ui-avatars.com/api/?name=${backup.creator.username}&background=random`" class="rounded-circle" width="24" height="24">
<span class="small fw-bold">{{ backup.creator.username }}</span>
</div>
<span v-else class="text-muted small">System</span>
</td>
<td class="small text-muted">
{{ formatTime(backup.created_at) }}
</td>
<td class="text-end pe-5">
<div class="btn-group shadow-sm rounded-pill overflow-hidden">
<button @click="downloadBackupByHash(backup.hashkey)" class="btn btn-soft-primary btn-sm px-3 fw-bold" title="Download">
<i class="fas fa-download fa-lg"></i>
</button>
<button @click="doDeleteBackup(backup.hashkey)" class="btn btn-soft-danger btn-sm px-3 fw-bold" title="Delete">
<i class="fas fa-trash-alt fa-lg"></i>
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- System Settings Tab -->
<div v-else-if="activeTab === 'settings'" class="tab-content animate-slide-up">
<div class="card border-0 shadow-lg rounded-5 bg-white p-4">
<div class="d-flex align-items-center gap-4 mb-4">
<div class="icon-shape bg-soft-primary text-primary rounded-circle p-3 shadow-primary-sm">
<i class="fas fa-cog fa-2x"></i>
</div>
<div>
<h4 class="fw-black mb-1">Global System Configuration</h4>
<p class="text-muted mb-0">Manage application branding, maintenance mode, and advanced system flags.</p>
</div>
</div>
<div class="bg-light rounded-5 p-5 text-center mb-4 border border-2 border-dashed border-primary-subtle shadow-sm">
<h5 class="fw-black mb-3">Settings Hub Integrated</h5>
<p class="text-muted small px-lg-5 mb-4 fw-medium">
The platform configuration management has been centralized into a dedicated interface for enhanced security and branding control.
Changes take effect across the entire ecosystem in real-time.
</p>
<a href="/system-settings" class="btn btn-primary-gradient rounded-pill px-5 py-3 fw-black shadow-primary-lg animate-bounce-soft">
<i class="fas fa-external-link-alt me-2"></i> Enter Settings Hub
</a>
</div>
<div class="row g-4">
<div v-for="feat in [
{ title: 'Branding & Identity', desc: 'Customize application name, tagline and business logo.', icon: 'fas fa-paint-brush' },
{ title: 'Global Controls', desc: 'Toggle maintenance mode and restrict guest access system-wide.', icon: 'fas fa-shield-alt' },
{ title: 'Operational Flags', desc: 'Fine-tune feature availability and regional settings.', icon: 'fas fa-sliders-h' }
]" :key="feat.title" class="col-md-4">
<div class="p-3 rounded-4 border border-light-subtle h-100 bg-white shadow-sm hover-up">
<div class="d-flex align-items-center gap-3 mb-2">
<i :class="feat.icon" class="text-primary opacity-75"></i>
<div class="fw-bold small text-uppercase ls-wide">{{ feat.title }}</div>
</div>
<div class="text-muted smaller fw-medium">{{ feat.desc }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modules Tab -->
<div v-else-if="activeTab === 'modules'" class="tab-content animate-slide-up">
<div class="card border-0 shadow-lg rounded-5 bg-white p-4">
<div class="d-flex align-items-center justify-content-between mb-4 flex-wrap gap-3">
<div class="d-flex align-items-center gap-4">
<div class="icon-shape bg-soft-primary text-primary rounded-circle p-3 shadow-primary-sm">
<i class="fas fa-puzzle-piece fa-2x"></i>
</div>
<div>
<h4 class="fw-black mb-1">Module Manager</h4>
<p class="text-muted mb-0">Toggle features system-wide. These overrides take precedence over <code>.env</code> module flags.</p>
</div>
</div>
<div v-if="savingModules" class="badge bg-soft-primary text-primary rounded-pill px-4 py-2">
<i class="fas fa-sync-alt fa-spin me-2"></i> SAVING...
</div>
</div>
<div v-if="!modulesSystemEnabled" class="alert alert-warning border-0 rounded-4 d-flex align-items-center gap-3 mb-4">
<i class="fas fa-exclamation-triangle"></i>
<div class="small fw-medium">
<strong>Module system is disabled</strong> via <code>MODULES_SYSTEM_ENABLED=false</code>. All modules are treated as enabled regardless of these toggles until that env flag is removed.
</div>
</div>
<div v-if="loadingModules" class="text-center py-5">
<div class="spinner-border text-primary mb-3"></div>
<div class="fw-bold text-muted opacity-50">LOADING MODULES...</div>
</div>
<div v-else class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="border-0 rounded-start ps-4 fw-bold text-muted text-uppercase small">Module</th>
<th class="border-0 fw-bold text-muted text-uppercase small">Key</th>
<th class="border-0 fw-bold text-muted text-uppercase small text-center">Env Default</th>
<th class="border-0 fw-bold text-muted text-uppercase small text-center">Effective</th>
<th class="border-0 rounded-end pe-4 fw-bold text-muted text-uppercase small text-end">Override</th>
</tr>
</thead>
<tbody>
<tr v-for="(mod, key) in modules" :key="key">
<td class="ps-4">
<div class="fw-bold">{{ mod.label }}</div>
<div class="small text-muted">{{ mod.description }}</div>
</td>
<td>
<code class="bg-light rounded px-2 py-1 small text-muted">{{ key }}</code>
</td>
<td class="text-center">
<span
class="badge rounded-pill px-3 py-2 fw-bold"
:class="mod.config_default ? 'bg-success-subtle text-success' : 'bg-danger-subtle text-danger'"
>
{{ mod.config_default ? 'ON' : 'OFF' }}
</span>
</td>
<td class="text-center">
<span
class="badge rounded-pill px-3 py-2 fw-bold"
:class="mod.enabled ? 'bg-success-subtle text-success' : 'bg-danger-subtle text-danger'"
>
<i :class="mod.enabled ? 'fas fa-check-circle me-1' : 'fas fa-times-circle me-1'"></i>
{{ mod.enabled ? 'Enabled' : 'Disabled' }}
</span>
</td>
<td class="pe-4 text-end">
<div class="btn-group btn-group-sm shadow-sm rounded-pill overflow-hidden" role="group">
<button
type="button"
@click="setModuleOverride(key, true)"
class="btn fw-bold px-3"
:class="mod.override === true ? 'btn-success text-white' : 'btn-outline-success'"
title="Force enabled"
>
<i class="fas fa-check"></i>
</button>
<button
type="button"
@click="setModuleOverride(key, null)"
class="btn fw-bold px-3"
:class="mod.override === null ? 'btn-secondary text-white' : 'btn-outline-secondary'"
title="Use env default"
>
ENV
</button>
<button
type="button"
@click="setModuleOverride(key, false)"
class="btn fw-bold px-3"
:class="mod.override === false ? 'btn-danger text-white' : 'btn-outline-danger'"
title="Force disabled"
>
<i class="fas fa-times"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Page Controls Tab -->
<div v-else-if="activeTab === 'pages'" class="tab-content animate-slide-up">
<div class="card border-0 shadow-lg rounded-5 bg-white p-4">
<div class="d-flex align-items-center justify-content-between mb-4">
<div class="d-flex align-items-center gap-4">
<div class="icon-shape bg-soft-primary text-primary rounded-circle p-3 shadow-primary-sm">
<i class="fas fa-map-signs fa-2x"></i>
</div>
<div>
<h4 class="fw-black mb-1">Page Access Controls</h4>
<p class="text-muted mb-0">System-wide page disabling. Users will be redirected if they try to access a disabled page.</p>
</div>
</div>
<div v-if="savingPages" class="badge bg-soft-primary text-primary rounded-pill px-4 py-2">
<i class="fas fa-sync-alt fa-spin me-2"></i> SAVING...
</div>
</div>
<!-- Search Bar -->
<div class="mb-4">
<div class="position-relative">
<i class="fas fa-search position-absolute top-50 start-0 translate-middle-y ms-3 text-muted"></i>
<input
v-model="pageSearchQuery"
type="text"
class="form-control form-control-lg rounded-pill ps-5 border-2"
placeholder="Search pages..."
style="border-color: #e2e8f0;"
>
</div>
</div>
<!-- Table View -->
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="border-0 rounded-start ps-4 fw-bold text-muted text-uppercase small" style="width: 50px;">#</th>
<th class="border-0 fw-bold text-muted text-uppercase small">Page</th>
<th class="border-0 fw-bold text-muted text-uppercase small">Route ID</th>
<th class="border-0 fw-bold text-muted text-uppercase small text-center">Status</th>
<th class="border-0 fw-bold text-muted text-uppercase small text-center">Go to Page</th>
<th class="border-0 rounded-end pe-4 fw-bold text-muted text-uppercase small text-end">Toggle</th>
</tr>
</thead>
<tbody>
<tr
v-for="(page, index) in filteredPages"
:key="page.id"
:class="disabledPages.includes(page.id) ? 'opacity-50' : ''"
class="page-row"
>
<td class="ps-4 text-muted">{{ index + 1 }}</td>
<td>
<div class="d-flex align-items-center gap-3">
<div
class="d-flex align-items-center justify-content-center rounded-3"
:class="disabledPages.includes(page.id) ? 'bg-danger-subtle text-danger' : 'bg-primary-subtle text-primary'"
style="width: 40px; height: 40px;"
>
<i :class="page.icon"></i>
</div>
<div>
<div class="fw-bold" :class="disabledPages.includes(page.id) ? 'text-decoration-line-through text-muted' : ''">{{ page.label }}</div>
</div>
</div>
</td>
<td>
<code class="bg-light rounded px-2 py-1 small text-muted">{{ page.id }}</code>
</td>
<td class="text-center">
<span
v-if="disabledPages.includes(page.id)"
class="badge bg-danger-subtle text-danger rounded-pill px-3 py-2 fw-bold"
>
<i class="fas fa-times-circle me-1"></i> Disabled
</span>
<span
v-else
class="badge bg-success-subtle text-success rounded-pill px-3 py-2 fw-bold"
>
<i class="fas fa-check-circle me-1"></i> Enabled
</span>
</td>
<td class="text-center">
<button
@click="navigate({ page: page.id })"
class="btn btn-sm btn-outline-primary rounded-pill px-3 fw-bold"
:disabled="disabledPages.includes(page.id)"
:title="disabledPages.includes(page.id) ? 'Page is disabled' : 'Go to ' + page.label"
>
<i class="fas fa-external-link-alt me-1"></i> Go
</button>
</td>
<td class="pe-4 text-end">
<button
@click="togglePage(page.id)"
class="btn btn-sm rounded-pill px-3 fw-bold"
:class="disabledPages.includes(page.id) ? 'btn-success' : 'btn-outline-danger'"
>
<i :class="disabledPages.includes(page.id) ? 'fas fa-power-off me-1' : 'fas fa-ban me-1'"></i>
{{ disabledPages.includes(page.id) ? 'Enable' : 'Disable' }}
</button>
</td>
</tr>
<tr v-if="filteredPages.length === 0">
<td colspan="6" class="text-center py-5 text-muted">
<i class="fas fa-search fa-2x mb-3 d-block opacity-25"></i>
No pages found matching "{{ pageSearchQuery }}"
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Accounting Themes Tab -->
<div v-else-if="activeTab === 'accounting_themes'" class="tab-content animate-slide-up">
<div class="card border-0 shadow-lg rounded-5 bg-white p-4">
<div class="d-flex align-items-center gap-4 mb-4">
<div class="icon-shape bg-soft-primary text-primary rounded-circle p-3 shadow-primary-sm">
<i class="fas fa-palette fa-2x"></i>
</div>
<div>
<h4 class="fw-black mb-1">Accounting Themes</h4>
<p class="text-muted mb-0">Pick a starter Chart of Accounts preset for this deployment, then re-apply to seed any missing accounts.</p>
</div>
</div>
<div v-if="loadingThemes" class="text-center py-5">
<div class="spinner-border text-primary mb-3"></div>
<div class="fw-bold text-muted opacity-50">LOADING THEMES...</div>
</div>
<div v-else-if="themeInfo">
<div class="row g-3 mb-4">
<div
v-for="opt in themeInfo.options"
:key="opt.key"
class="col-md-6 col-lg-4"
>
<div
class="p-3 rounded-4 border h-100 hover-up cursor-pointer position-relative"
:class="opt.key === themeInfo.current ? 'border-primary border-2 bg-soft-primary' : 'border-light-subtle bg-white'"
@click="selectThemeKey = opt.key"
style="cursor: pointer;"
>
<div class="d-flex align-items-start gap-2 mb-2">
<i class="fas fa-palette text-primary mt-1"></i>
<div class="flex-grow-1">
<div class="fw-bold">{{ opt.label }}</div>
<div class="small text-muted">v{{ opt.version }}</div>
</div>
<span
v-if="opt.key === themeInfo.current"
class="badge bg-primary"
>Active</span>
<i
v-else-if="opt.key === selectThemeKey"
class="fas fa-check-circle text-primary"
></i>
</div>
<div class="small text-muted">{{ opt.description || 'No description.' }}</div>
</div>
</div>
</div>
<div class="d-flex flex-wrap gap-3 align-items-center mb-4 p-3 bg-light rounded-4">
<div class="small">
<span class="text-muted">Active:</span>
<strong class="ms-1">{{ themeInfo.definition.label }}</strong>
</div>
<div class="small">
<span class="text-muted">Theme accounts:</span>
<strong class="ms-1">{{ themeInfo.counts.theme_tagged }}</strong>
</div>
<div class="small">
<span class="text-muted">User-added:</span>
<strong class="ms-1">{{ themeInfo.counts.user_added }}</strong>
</div>
<div class="small">
<span class="text-muted">Archived:</span>
<strong class="ms-1">{{ themeInfo.counts.archived }}</strong>
</div>
<div v-if="themeDrift" class="small">
<span class="text-muted">Missing:</span>
<strong class="ms-1" :class="themeDrift.totals.missing > 0 ? 'text-warning' : ''">{{ themeDrift.totals.missing }}</strong>
</div>
<div v-if="themeDrift && themeDrift.totals.renamed > 0" class="small">
<span class="text-muted">Renamed:</span>
<strong class="ms-1">{{ themeDrift.totals.renamed }}</strong>
</div>
<div class="ms-auto d-flex gap-2">
<button
class="btn btn-outline-primary btn-sm"
:disabled="!selectThemeKey || selectThemeKey === themeInfo.current || switchingTheme"
@click="switchTheme"
>
<i class="fas fa-shuffle me-1"></i>
{{ switchingTheme ? 'Switching…' : 'Switch to selected' }}
</button>
<button
class="btn btn-primary btn-sm"
:disabled="applyingTheme"
@click="reapplyTheme"
>
<i class="fas fa-sync me-1" :class="{ 'fa-spin': applyingTheme }"></i>
{{ applyingTheme ? 'Applying…' : 'Re-apply active theme' }}
</button>
</div>
</div>
<div class="small text-muted">
<i class="fas fa-info-circle me-1"></i>
Switching only changes which accounts are tracked as "theme accounts" — it does not seed anything.
Re-applying the active theme is additive only: it never deletes or renames existing accounts.
</div>
</div>
</div>
</div>
<!-- API Tokens Tab -->
<div v-else-if="activeTab === 'api_tokens'" class="tab-content animate-slide-up">
<div class="card border-0 shadow-lg rounded-5 bg-white overflow-hidden">
<div class="card-body p-4">
<ApiTokensPanel />
</div>
</div>
</div>
<!-- Logs Lab Tab -->
<div v-else-if="activeTab === 'logs'" class="tab-content animate-slide-up">
<div class="card border-0 shadow-lg rounded-5 bg-white overflow-hidden">
<div class="card-header bg-dark text-white p-3 px-4 d-flex align-items-center justify-content-between">
<h4 class="fw-black mb-0 d-flex align-items-center gap-3">
<i class="fas fa-clipboard-list text-primary"></i>
System Intelligence Logs
</h4>
<div class="d-flex gap-2">
<button
v-for="ltype in [{id: 'database', label: 'Sys Logs', icon: 'fas fa-database'}, {id: 'audit', label: 'Audit', icon: 'fas fa-fingerprint'}, {id: 'file', label: 'Raw Files', icon: 'fas fa-file-code'}]"
:key="ltype.id"
@click="fetchLogs(ltype.id)"
class="btn btn-sm rounded-pill px-3 fw-bold border-0 d-flex align-items-center gap-2"
:class="activeLogType === ltype.id ? 'btn-primary shadow-primary-sm' : 'btn-outline-light opacity-50'"
>
<i :class="ltype.icon" class="small opacity-75"></i>
{{ ltype.label }}
</button>
</div>
</div>
<div class="card-body p-4">
<div v-if="loadingLogs" class="text-center py-5">
<div class="spinner-border text-primary mb-3"></div>
<div class="fw-bold text-muted opacity-50">FETCHING SYSTEM ANALYTICS...</div>
</div>
<div v-else-if="!logData || logData.length === 0" class="text-center py-5">
<i class="fas fa-inbox fa-3x text-muted opacity-25 mb-3"></i>
<div class="fw-bold text-muted opacity-50 mb-2">NO LOG ENTRIES FOUND</div>
<div class="small text-muted">No system activity has been recorded yet</div>
</div>
<div v-else-if="typeof logData === 'string'" class="terminal-view bg-dark text-light p-4 rounded-4 font-monospace overflow-auto" style="max-height: 500px; font-size: 0.8rem;">
<pre class="mb-0 text-success">{{ logData }}</pre>
</div>
<div v-else>
<div class="log-list">
<template v-if="activeLogType === 'database'">
<div v-for="log in logData" :key="log.uid" class="log-entry d-flex align-items-start gap-3 p-3 rounded-3 mb-2 bg-light-hover transition-all">
<div class="log-time text-muted small" style="min-width: 120px;">
{{ formatTime(log.log_time) }}
</div>
<div class="log-content flex-grow-1">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-soft-primary text-primary rounded-pill px-2" style="font-size: 10px;">{{ log.log_type }}</span>
<span class="badge bg-soft-info text-info rounded-pill px-2" style="font-size: 10px;">{{ log.log_category }}</span>
</div>
<div class="text-dark small">{{ log.description }}</div>
<div class="text-muted small mt-1" v-if="log.useruid">User: {{ log.useruid }}</div>
</div>
</div>
</template>
<template v-else-if="activeLogType === 'audit'">
<div v-for="log in logData" :key="log.id" class="log-entry d-flex align-items-start gap-3 p-3 rounded-3 mb-2 bg-light-hover transition-all">
<div class="log-time text-muted small" style="min-width: 120px;">
{{ formatTime(log.created_at) }}
</div>
<div class="log-content flex-grow-1">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-info text-white rounded-pill px-2" style="font-size: 10px;">{{ log.table_name }}</span>
<span class="badge bg-warning text-dark rounded-pill px-2" style="font-size: 10px;">ID #{{ log.target_id }}</span>
</div>
<div class="text-dark small">Audit Record Modified</div>
</div>
</div>
</template>
<template v-else>
<div v-for="log in logData" :key="log.path" class="log-entry d-flex align-items-start gap-3 p-3 rounded-3 mb-2 bg-light-hover transition-all">
<div class="log-icon text-muted">
<i class="fas fa-file-code"></i>
</div>
<div class="log-content flex-grow-1">
<div class="text-dark small font-monospace">{{ log.name }}</div>
<div class="d-flex gap-3 mt-1">
<span class="text-muted small">{{ log.size_human }}</span>
<span class="text-muted small">{{ formatTime(log.last_modified) }}</span>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Reusable Components -->
<UltimateQueryModal
:show="showQueryModal"
@close="showQueryModal = false"
/>
<!-- Toasts / Notifications -->
<div class="position-fixed bottom-0 end-0 p-4 mb-5" style="z-index: 9999">
<div v-for="t in toasts" :key="t.id" class="toast show animate-slide-in mb-3 border-0 shadow-lg rounded-4 overflow-hidden" :class="'bg-' + t.type">
<div class="d-flex p-3 align-items-center gap-3 text-white">
<i :class="t.icon"></i>
<span class="fw-bold">{{ t.msg }}</span>
<button @click="removeToast(t.id)" class="ms-auto btn-close btn-close-white small"></button>
</div>
</div>
</div>
<!-- Confirmation Modals -->
<ConfirmModal
v-model="showMaintenanceConfirm"
title="System Maintenance"
:message="pendingMaintenanceVal ? 'Enable Maintenance Mode? All non-ultimate users will be locked out.' : 'Disable Maintenance Mode? All users will regain access.'"
:confirmText="pendingMaintenanceVal ? 'Enable Mode' : 'Disable Mode'"
:variant="pendingMaintenanceVal ? 'danger' : 'info'"
@confirm="confirmMaintenanceToggle"
/>
<ConfirmModal
v-model="showBackupConfirm"
title="System Backup"
message="Initiate system-wide backup? The application will enter maintenance mode momentarily while the SQL dump is generated."
confirmText="Start Backup"
variant="warning"
@confirm="confirmInitiateBackup"
/>
<ConfirmModal
v-model="showFlushConfirm"
title="Confirm Flush Operation"
:message="'ARE YOU SURE? You are about to FLUSH ALL ' + (pendingFlushId?.toUpperCase().replace('_', ' ') || '') + '. This operation is critical and cannot be undone.'"
confirmText="Flush Now"
variant="danger"
@confirm="confirmFlush"
/>
<ConfirmModal
v-model="showDeleteBackupConfirm"
title="Delete Backup"
message="Permanently delete this backup from the database? This cannot be undone."
confirmText="Delete Permanently"
variant="danger"
@confirm="confirmDeleteBackup"
/>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import axios from 'axios';
import { useUltimate } from '@/composables/useUltimate';
import { useNavigate } from '@/composables/Core/useNavigate';
import UltimateQueryModal from '@/Components/Ultimate/UltimateQueryModal.vue';
import ApiTokensPanel from '@/Components/Ultimate/ApiTokensPanel.vue';
import ButtonPrimary from '@/Components/Core/Layouts/Buttons/ButtonPrimary.vue';
import ButtonWarning from '@/Components/Core/Layouts/Buttons/ButtonWarning.vue';
import BaseButton from '@/Components/Core/Layouts/Buttons/BaseButton.vue';
import ConfirmModal from '@/Components/Core/ConfirmModal.vue';
import { useUIStore } from '@/stores/ui';
const { navigate } = useNavigate();
const {
getStats, stats, loading: statsLoading,
toggleMaintenance, sendGlobalMessage,
flushData, runCommand, commandOutput,
getLogs, downloadBackup, getBackups, downloadBackupByHash, renameBackup, deleteBackup
} = useUltimate();
const uiStore = useUIStore();
const activeTab = ref('dashboard');
const showQueryModal = ref(false);
const artisanCommand = ref('');
const toasts = ref([]);
// Modal States
const showMaintenanceConfirm = ref(false);
const showBackupConfirm = ref(false);
const showFlushConfirm = ref(false);
const showDeleteBackupConfirm = ref(false);
const pendingFlushId = ref(null);
const pendingDeleteHash = ref(null);
const pendingMaintenanceVal = ref(null);
// Logs specific states
const activeLogType = ref('database');
const logData = ref([]);
const loadingLogs = ref(false);
const loadingBackups = ref(false);
const backupData = ref([]);
const editingBackup = ref(null);
const msgData = ref({
text: '',
type: 'info'
});
const tabs = [
{ id: 'dashboard', label: 'Monitor', icon: 'fas fa-chart-line fa-lg' },
{ id: 'settings', label: 'Global Settings', icon: 'fas fa-cog fa-lg' },
{ id: 'modules', label: 'Modules', icon: 'fas fa-puzzle-piece fa-lg' },
{ id: 'lab', label: 'Command Lab', icon: 'fas fa-flask fa-lg' },
{ id: 'batch', label: 'Batch Tools', icon: 'fas fa-boxes fa-lg' },
{ id: 'manage', label: 'Dangerous Zone', icon: 'fas fa-skull-crossbones fa-lg' },
{ id: 'backups', label: 'Backups', icon: 'fas fa-history fa-lg' },
{ id: 'pages', label: 'Page Controls', icon: 'fas fa-map-signs fa-lg' },
{ id: 'accounting_themes', label: 'Accounting Themes', icon: 'fas fa-palette fa-lg' },
{ id: 'api_tokens', label: 'API Tokens', icon: 'fas fa-key fa-lg' },
{ id: 'logs', label: 'System Logs', icon: 'fas fa-briefcase-medical fa-lg' },
];
const allPages = [
{ id: 'UserList', label: 'User List', icon: 'fas fa-users' },
{ id: 'ManageStoresAdmin', label: 'Manage Stores', icon: 'fas fa-store' },
{ id: 'ManageProductAdmin', label: 'Manage Products', icon: 'fas fa-box' },
{ id: 'ManageGlobalTransactions', label: 'Global Transactions', icon: 'fas fa-hand-holding-usd' },
{ id: 'UltimateConsole', label: 'Ultimate Console', icon: 'fas fa-user-shield text-danger' },
{ id: 'VerificationDashboard', label: 'Verification Dashboard', icon: 'fas fa-check-circle' },
{ id: 'ShipmentList', label: 'Shipment List', icon: 'fas fa-truck' },
{ id: 'CooperativeList', label: 'Cooperative List', icon: 'fas fa-people-carry' },
{ id: 'AddTransaction', label: 'Add Transaction', icon: 'fas fa-plus-circle' },
{ id: 'TransferMyCredit', label: 'Transfer Credit', icon: 'fas fa-exchange-alt' },
{ id: 'StoreProfileEdit', label: 'Create/Edit Store', icon: 'fas fa-store-alt' },
{ id: 'PosMain', label: 'POS System', icon: 'fas fa-cash-register' },
{ id: 'PosHistory', label: 'POS History', icon: 'fas fa-history' },
{ id: 'ListProductsMarket', label: 'Market Products', icon: 'fas fa-shopping-basket' },
{ id: 'ListStores', label: 'Market Stores', icon: 'fas fa-shopping-cart' },
{ id: 'ListReports', label: 'System Reports', icon: 'fas fa-file-invoice-dollar' },
{ id: 'ManageAnnouncements', label: 'Manage Announcements', icon: 'fas fa-bullhorn' },
{ id: 'SystemSettings', label: 'System Settings', icon: 'fas fa-cogs' },
{ id: 'LandingPageEditor', label: 'Landing Page Editor', icon: 'fas fa-palette' },
{ id: 'AccountingDashboard', label: 'Accounting Dashboard', icon: 'fas fa-file-invoice-dollar' },
];
const disabledPages = ref([]);
const savingPages = ref(false);
const pageSearchQuery = ref('');
// Module manager state
const modules = ref({});
const modulesSystemEnabled = ref(true);
const loadingModules = ref(false);
const savingModules = ref(false);
// Accounting themes state
const themeInfo = ref(null);
const themeDrift = ref(null);
const loadingThemes = ref(false);
const selectThemeKey = ref(null);
const switchingTheme = ref(false);
const applyingTheme = ref(false);
const loadAccountingThemes = async () => {
loadingThemes.value = true;
try {
const [infoRes, driftRes] = await Promise.all([
axios.post('/admin/accounting/theme', {}),
axios.post('/admin/accounting/theme/drift', {}),
]);
themeInfo.value = infoRes.data ?? null;
themeDrift.value = driftRes.data?.data ?? null;
if (themeInfo.value?.current && !selectThemeKey.value) {
selectThemeKey.value = themeInfo.value.current;
}
} catch (err) {
addToast('Failed to load accounting themes', 'danger', 'fas fa-exclamation-circle');
} finally {
loadingThemes.value = false;
}
};
const switchTheme = async () => {
if (!selectThemeKey.value || selectThemeKey.value === themeInfo.value?.current) return;
switchingTheme.value = true;
try {
const res = await axios.post('/admin/accounting/theme/set', { key: selectThemeKey.value });
if (res.data?.success) {
addToast('Active theme switched', 'success', 'fas fa-check-circle');
await loadAccountingThemes();
} else {
addToast(res.data?.message || 'Could not switch theme', 'danger', 'fas fa-exclamation-circle');
}
} catch (err) {
addToast(err.response?.data?.message || 'Could not switch theme', 'danger', 'fas fa-exclamation-circle');
} finally {
switchingTheme.value = false;
}
};
const reapplyTheme = async () => {
applyingTheme.value = true;
try {
const res = await axios.post('/admin/accounting/theme/apply', {});
if (res.data?.success) {
addToast(res.data.message || 'Theme applied', 'success', 'fas fa-check-circle');
await loadAccountingThemes();
} else {
addToast(res.data?.message || 'Apply failed', 'danger', 'fas fa-exclamation-circle');
}
} catch (err) {
addToast(err.response?.data?.message || 'Apply failed', 'danger', 'fas fa-exclamation-circle');
} finally {
applyingTheme.value = false;
}
};
watch(activeTab, (val) => {
if (val === 'accounting_themes' && !themeInfo.value) {
loadAccountingThemes();
}
});
const filteredPages = computed(() => {
if (!pageSearchQuery.value.trim()) return allPages;
const query = pageSearchQuery.value.toLowerCase();
return allPages.filter(page =>
page.label.toLowerCase().includes(query) ||
page.id.toLowerCase().includes(query)
);
});
const loadDisabledPages = async () => {
try {
const response = await axios.post('/admin/ultimate/system-settings', { keys: ['disabled_pages'] });
if (response.data && response.data.success) {
const raw = response.data.data.find(s => s.key === 'disabled_pages')?.value || '[]';
disabledPages.value = JSON.parse(raw);
}
} catch (error) {
console.error('Failed to load disabled pages:', error);
}
};
const togglePage = (pageId) => {
if (disabledPages.value.includes(pageId)) {
disabledPages.value = disabledPages.value.filter(id => id !== pageId);
} else {
disabledPages.value.push(pageId);
}
saveDisabledPages();
};
const saveDisabledPages = async () => {
savingPages.value = true;
try {
const response = await axios.post('/admin/ultimate/system-settings/update', {
settings: {
disabled_pages: JSON.stringify(disabledPages.value)
}
});
if (response.data && response.data.success) {
// Success
uiStore.setDisabledPages(disabledPages.value);
uiStore.lastSynced = new Date().toISOString();
}
} catch (error) {
console.error('Failed to save disabled pages:', error);
} finally {
savingPages.value = false;
}
};
// Merged with main onMounted below
const batchOperations = [
{ label: 'Activate All Farmers', icon: 'fas fa-tractor fa-2x', bg: 'bg-soft-success', desc: 'Batch verify all unverified farmer profiles in one click.' },
{ label: 'Send Weekly Rewards', icon: 'fas fa-gift fa-2x', bg: 'bg-soft-primary', desc: 'Transfer balance to all active users based on transaction volume.' },
{ label: 'Deactivate Inactive Stores', icon: 'fas fa-store-slash fa-2x', bg: 'bg-soft-warning', desc: 'Hide stores with no active sessions or products.' },
{ label: 'Bulk Price Update', icon: 'fas fa-tags fa-2x', bg: 'bg-soft-info', desc: 'Apply percentage-based price adjustments across categories.' },
];
const flushOptions = computed(() => [
{ id: 'transactions', label: 'Transaction History', desc: 'Purge all global transaction records.', rows: stats.value?.transactions || 0 },
{ id: 'pos_sessions', label: 'POS Sessions', desc: 'Clear all pending and completed checkout sessions.', rows: stats.value?.pos_sessions_count || 0 },
{ id: 'stores', label: 'All Stores', desc: 'Wipe every store along with its managers and product assignments (prd_str, store_managers, str).', rows: stats.value?.stores || 0 },
{ id: 'products', label: 'All Products', desc: 'Wipe the global product catalog and all per-store product links (prd_items, prd_str).', rows: stats.value?.products || 0 },
{ id: 'cooperatives', label: 'All Cooperatives', desc: 'Delete every COOPERATIVE organization and its members, votes, documents, and resolutions.', rows: stats.value?.cooperatives_count || 0 },
{ id: 'carts', label: 'All Carts', desc: 'Empty every shopping cart and its items (cart_items, carts).', rows: stats.value?.carts_count || 0 },
{ id: 'farmer_profiles', label: 'Farmer Profiles', desc: 'Wipe all farmer profile records (farmer_profiles).', rows: stats.value?.farmer_profiles_count || 0 },
{ id: 'cache', label: 'System Cache', desc: 'Flush Redis database and clear all application cache keys.', rows: '-' },
]);
const quickStats = computed(() => {
if (!stats.value) return {};
return {
'Users': stats.value.users,
'Stores': stats.value.active_stores,
'Transactions': stats.value.transactions,
'POS Sessions': stats.value.pos_sessions_count || 0,
'Sys Logs': stats.value.logs_count || 0,
'Balance': '₱' + parseFloat(stats.value.total_balance || 0).toLocaleString()
};
});
const statCards = computed(() => [
{ label: 'Products Live', value: stats.value?.products || 0, icon: 'fas fa-shopping-basket fa-2x', colorClass: 'bg-soft-primary text-primary' },
{ label: 'PHP Engine', value: stats.value?.php_version || '8.2', icon: 'fas fa-code fa-2x', colorClass: 'bg-soft-info text-info' },
{
label: stats.value?.redis?.connected ? `Redis · ${stats.value?.redis?.ping_ms ?? ''}ms` : 'Redis Offline',
value: stats.value?.redis?.connected
? (stats.value?.redis?.used_memory_human || 'OK')
: 'DOWN',
icon: 'fas fa-database fa-2x',
colorClass: stats.value?.redis?.connected ? 'bg-soft-success text-success' : 'bg-danger-5 text-danger',
},
]);
const redisHealthy = computed(() => !!stats.value?.redis?.connected);
onMounted(async () => {
loadDisabledPages();
await getStats();
if (stats.value?.global_message) {
msgData.value.text = stats.value.global_message.text;
msgData.value.type = stats.value.global_message.type;
}
});
const broadcastMessage = async () => {
if (!msgData.value.text) {
addToast('Message cleared', 'info', 'fas fa-check-circle');
}
await sendGlobalMessage(msgData.value.text, msgData.value.type);
addToast('Global broadcast sent!', 'success', 'fas fa-paper-plane');
};
const maintenanceToggle = () => {
pendingMaintenanceVal.value = !stats.value.maintenance_mode;
showMaintenanceConfirm.value = true;
};
const confirmMaintenanceToggle = async () => {
const newVal = pendingMaintenanceVal.value;
await toggleMaintenance(newVal);
addToast(`Maintenance ${newVal ? 'ENABLED' : 'DISABLED'}`, newVal ? 'danger' : 'success', 'fas fa-power-off');
};
const initiateBackup = () => {
showBackupConfirm.value = true;
};
const confirmInitiateBackup = () => {
addToast('Backup started... Downloading shortly.', 'info', 'fas fa-cloud-download');
downloadBackup();
};
const fetchLogs = async (type = 'database') => {
activeLogType.value = type;
loadingLogs.value = true;
try {
const res = await getLogs(type);
if (res.success) {
logData.value = res.data;
}
} catch (err) {
addToast('Failed to fetch logs', 'danger', 'fas fa-exclamation-circle');
} finally {
loadingLogs.value = false;
}
};
const formatTime = (time) => {
if (!time) return 'N/A';
return new Date(time).toLocaleString();
};
const doFlush = (id) => {
pendingFlushId.value = id;
showFlushConfirm.value = true;
};
const confirmFlush = async () => {
const id = pendingFlushId.value;
const res = await flushData(id);
const affected = res.data?.affected ?? 0;
addToast(`${id} successfully flushed (${affected} records removed)`, 'success', 'fas fa-trash-alt');
await getStats();
};
const executeArtisan = async () => {
if (!artisanCommand.value) return;
try {
const res = await runCommand(artisanCommand.value);
if (res.success) {
addToast('Command executed successfully', 'success', 'fas fa-terminal');
} else {
addToast(res.message, 'danger', 'fas fa-exclamation-triangle');
}
} catch (err) {
addToast(err.response?.data?.message || 'Execution failed', 'danger', 'fas fa-bomb');
}
};
const addToast = (msg, type, icon) => {
const id = Date.now();
toasts.value.push({ id, msg, type, icon });
setTimeout(() => removeToast(id), 5000);
};
const removeToast = (id) => {
toasts.value = toasts.value.filter(t => t.id !== id);
};
const fetchBackups = async () => {
loadingBackups.value = true;
try {
const res = await getBackups();
if (res.success) {
backupData.value = res.data.map(b => ({ ...b, name: b.name || b.filename }));
}
} catch (err) {
addToast('Failed to fetch backups', 'danger', 'fas fa-exclamation-circle');
} finally {
loadingBackups.value = false;
}
}
const saveRename = async (backup) => {
try {
const res = await renameBackup(backup.hashkey, backup.name);
if (res.data.success) {
addToast('Backup renamed', 'success', 'fas fa-check-circle');
editingBackup.value = null;
}
} catch (err) {
addToast('Rename failed', 'danger', 'fas fa-bomb');
}
};
const doDeleteBackup = (hash) => {
pendingDeleteHash.value = hash;
showDeleteBackupConfirm.value = true;
};
const confirmDeleteBackup = async () => {
const hash = pendingDeleteHash.value;
try {
const res = await deleteBackup(hash);
if (res.data.success) {
addToast('Backup deleted', 'success', 'fas fa-trash-alt');
backupData.value = backupData.value.filter(b => b.hashkey !== hash);
}
} catch (err) {
addToast('Delete failed', 'danger', 'fas fa-bomb');
}
};
const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
const fetchModules = async () => {
loadingModules.value = true;
try {
const res = await axios.get('/admin/ultimate/modules');
if (res.data && res.data.success) {
modules.value = res.data.data.modules || {};
modulesSystemEnabled.value = !!res.data.data.system_enabled;
}
} catch (err) {
addToast('Failed to load modules', 'danger', 'fas fa-exclamation-circle');
} finally {
loadingModules.value = false;
}
};
const setModuleOverride = async (key, value) => {
savingModules.value = true;
try {
const res = await axios.post('/admin/ultimate/modules/update', {
overrides: { [key]: value },
});
if (res.data && res.data.success) {
modules.value = res.data.data.modules || modules.value;
const label = modules.value[key]?.label || key;
const msg = value === null
? `${label}: using env default`
: `${label} ${value ? 'enabled' : 'disabled'}`;
addToast(msg, value === false ? 'warning' : 'success', 'fas fa-puzzle-piece');
}
} catch (err) {
addToast(err.response?.data?.message || 'Failed to update module', 'danger', 'fas fa-bomb');
} finally {
savingModules.value = false;
}
};
watch(activeTab, (newTab) => {
if (newTab === 'backups' && backupData.value.length === 0) {
fetchBackups();
}
if (newTab === 'modules' && Object.keys(modules.value).length === 0) {
fetchModules();
}
});
// Initial logs fetch if logs tab is active
computed(() => {
if (activeTab.value === 'logs' && logData.value.length === 0) {
fetchLogs('database');
}
});
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;900&display=swap');
.ultimate-console {
font-family: 'Outfit', sans-serif;
}
.fw-black { font-weight: 900; }
.text-primary-gradient {
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.btn-primary-gradient {
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
}
.shadow-primary-lg { box-shadow: 0 10px 30px rgba(59, 130, 246, 0.3); }
.shadow-info-lg { box-shadow: 0 10px 30px rgba(13, 202, 240, 0.3); }
.bg-soft-primary { background-color: rgba(13, 110, 253, 0.08); }
.bg-soft-success { background-color: rgba(25, 135, 84, 0.08); }
.bg-soft-info { background-color: rgba(13, 202, 240, 0.08); }
.bg-soft-warning { background-color: rgba(255, 193, 7, 0.08); }
.bg-danger-5 { background-color: rgba(220, 53, 69, 0.05); }
.white-5 { background-color: rgba(255, 255, 255, 0.05); }
.ls-tight { letter-spacing: -0.02em; }
.ls-wide { letter-spacing: 0.1em; }
.badge-pill {
background: rgba(255,255,255,0.05);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 99px;
padding: 0.5rem 1.5rem;
}
.icon-shape {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-shape.sm {
width: 48px;
height: 48px;
}
.hover-up {
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.hover-up:hover {
transform: translateY(-8px);
}
.bg-darker { background-color: #121212; }
.border-white-10 { border-color: rgba(255,255,255,0.1) !important; }
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.pulse {
animation: status-pulse 2s infinite;
}
@keyframes status-pulse {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(25, 135, 84, 0.7); }
70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(25, 135, 84, 0); }
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(25, 135, 84, 0); }
}
.counter-animation {
animation: counter-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes counter-pop {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.animate-fade-in { animation: fadeIn 0.8s ease-out; }
.animate-slide-up { animation: slideUp 0.6s ease-out; }
.border-0-focus:focus { border: none !important; box-shadow: none !important; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.glow-text {
text-shadow: 0 0 10px rgba(25, 135, 84, 0.5);
}
.transition-all { transition: all 0.3s ease; }
.btn-close-white { filter: invert(1) grayscale(100%) brightness(200%); }
.btn-glass-premium {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
transition: all 0.3s ease;
}
.btn-glass-premium:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
color: white;
}
.focus-ring:focus {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.15);
}
.focus-within-shadow:focus-within {
box-shadow: 0 10px 30px rgba(0,0,0,0.1) !important;
border-color: rgba(13, 110, 253, 0.2) !important;
}
.header-ultimate {
background: #020617; /* Deepest Indigo/Slate */
border-bottom: 4px solid #6366f1 !important;
}
.header-mesh-radial {
background-image:
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.25) 0, transparent 50%),
radial-gradient(at 50% 0%, rgba(139, 92, 246, 0.15) 0, transparent 50%),
radial-gradient(at 100% 0%, rgba(217, 70, 239, 0.15) 0, transparent 50%);
filter: blur(40px);
}
.icon-glow {
width: 60px;
height: 60px;
background: rgba(99, 102, 241, 0.3);
border-radius: 50%;
filter: blur(20px);
}
.pulse-ring-premium {
width: 80px;
height: 80px;
border: 2px solid rgba(99, 102, 241, 0.2);
border-radius: 50%;
animation: pulse-premium 3s infinite ease-out;
}
@keyframes pulse-premium {
0% { transform: translate(-50%, -50%) scale(0.8); opacity: 0; }
50% { opacity: 0.5; }
100% { transform: translate(-50%, -50%) scale(1.5); opacity: 0; }
}
.badge-pill-premium {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
padding: 0.75rem 1.25rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.badge-pill-premium:hover {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(99, 102, 241, 0.3);
transform: translateY(-4px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
}
.pill-icon-box {
width: 32px;
height: 32px;
background: rgba(99, 102, 241, 0.15);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #818cf8;
font-size: 0.85rem;
}
.letter-spacing-2 { letter-spacing: 0.2em; }
.shadow-2xl {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.text-white-50 { color: rgba(255, 255, 255, 0.5) !important; }
/* Page Controls Table */
.ultimate-console .page-row {
transition: background-color 0.2s ease;
}
.ultimate-console .page-row:hover {
background-color: rgba(13, 110, 253, 0.04) !important;
}
</style>