Sync src/Index.html
This commit is contained in:
+912
@@ -0,0 +1,912 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<base target="_top" />
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Roboto:wght@300;400;500&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/framework7@8/framework7-bundle.min.css"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
|
<?!= HtmlService.createHtmlOutputFromFile('styles').getContent(); ?>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="text-slate-900 font-['Roboto'] antialiased">
|
||||||
|
<div x-data="appLauncher()" x-init="init()" x-cloak id="app-wrapper">
|
||||||
|
|
||||||
|
<!-- Login Overlay -->
|
||||||
|
<template x-if="!authenticated">
|
||||||
|
<div class="fixed inset-0 z-[2000] bg-slate-50 flex items-center justify-center p-6">
|
||||||
|
<div class="max-w-sm w-full bg-white p-8 rounded-3xl shadow-xl border border-slate-100 text-center">
|
||||||
|
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-primary/10 text-primary mb-6">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-['Poppins'] font-bold text-slate-900 mb-2">Welcome Back</h2>
|
||||||
|
<p class="text-slate-500 mb-8 text-sm">Silakan masuk untuk mengakses dashboard sekolah</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Google Login -->
|
||||||
|
<button @click="doGoogleLogin()" :disabled="loginLoading" class="w-full py-3 px-4 bg-white border border-slate-200 rounded-xl flex items-center justify-center gap-3 font-medium text-slate-700 hover:bg-slate-50 transition-all disabled:opacity-50">
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24">
|
||||||
|
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||||
|
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||||
|
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"/>
|
||||||
|
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||||
|
</svg>
|
||||||
|
Lanjutkan dengan Google
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="relative py-2">
|
||||||
|
<div class="absolute inset-0 flex items-center"><span class="w-full border-t border-slate-200"></span></div>
|
||||||
|
<div class="relative flex justify-center text-xs uppercase"><span class="bg-white px-2 text-slate-400 font-semibold">Atau</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WA OTP Login -->
|
||||||
|
<div x-show="loginMethod === 'google'">
|
||||||
|
<button @click="loginMethod = 'wa'" class="w-full py-2 text-sm text-primary font-medium hover:underline">Masuk dengan WhatsApp OTP</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="loginMethod === 'wa'" class="space-y-3">
|
||||||
|
<div class="relative">
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm">+62</span>
|
||||||
|
<input type="tel" x-model="authPhone" placeholder="8123456789" class="w-full pl-11 pr-3 py-3 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary text-sm">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="!otpSent">
|
||||||
|
<button @click="sendOTP()" :disabled="loginLoading" class="w-full py-3 bg-green-500 text-white rounded-xl font-semibold hover:bg-green-600 transition-all disabled:opacity-50">
|
||||||
|
Kirim Kode OTP
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="otpSent">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<input type="text" x-model="authCode" placeholder="Masukkan 6 Digit Kode" maxlength="6" class="w-full px-3 py-3 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary text-center text-lg font-bold tracking-widest">
|
||||||
|
<button @click="verifyOTP()" :disabled="loginLoading" class="w-full py-3 bg-primary text-white rounded-xl font-semibold hover:bg-blue-600 transition-all disabled:opacity-50">
|
||||||
|
Verifikasi & Masuk
|
||||||
|
</button>
|
||||||
|
<button @click="otpSent = false" class="text-xs text-slate-400 hover:underline">Ganti Nomor</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div x-show="authenticated" x-cloak id="app" class="view view-main h-full">
|
||||||
|
=======
|
||||||
|
<div class="page h-full flex flex-col">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<div class="navbar">
|
||||||
|
<div class="navbar-bg"></div>
|
||||||
|
<div class="navbar-inner px-6 flex items-center justify-between">
|
||||||
|
<div class="left flex flex-col">
|
||||||
|
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest leading-none mb-1">
|
||||||
|
SchoolHub
|
||||||
|
</span>
|
||||||
|
<h1
|
||||||
|
class="font-['Poppins'] text-xl font-bold text-slate-900 leading-none"
|
||||||
|
x-text="view === 'home' ? 'Dashboard' : (view === 'users' ? 'Community' : 'System Apps')"
|
||||||
|
></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right flex items-center gap-3">
|
||||||
|
<template x-if="user">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="exitApp()"
|
||||||
|
class="h-10 w-10 rounded-2xl bg-white border border-slate-100 flex items-center justify-center text-red-500 shadow-sm hover:bg-red-50 transition-colors"
|
||||||
|
aria-label="Keluar dari aplikasi"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Floating Navigation -->
|
||||||
|
<template x-if="isAdmin && !loading && !isSystemEmpty">
|
||||||
|
<nav class="floating-nav" aria-label="Admin navigation">
|
||||||
|
<button type="button" @click="view = 'home'" class="nav-item" :class="view === 'home' ? 'active' : ''" aria-label="Dashboard">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="view = 'manage'" class="nav-item" :class="view === 'manage' ? 'active' : ''" aria-label="Kelola aplikasi">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="view = 'users'" class="nav-item" :class="view === 'users' ? 'active' : ''" aria-label="Kelola pengguna">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Page Content -->
|
||||||
|
<div class="page-content bg-slate-50 px-4">
|
||||||
|
<div class="pb-10">
|
||||||
|
<!-- Loading -->
|
||||||
|
<template x-if="loading">
|
||||||
|
<div class="flex flex-col items-center justify-center py-20">
|
||||||
|
<div class="progress-ring-container">
|
||||||
|
<svg class="progress-ring" viewBox="0 0 120 120" aria-hidden="true">
|
||||||
|
<circle class="progress-ring-bg" cx="60" cy="60" r="54" fill="none" stroke="#e2e8f0" stroke-width="8"></circle>
|
||||||
|
<circle
|
||||||
|
class="progress-ring-circle"
|
||||||
|
cx="60"
|
||||||
|
cy="60"
|
||||||
|
r="54"
|
||||||
|
fill="none"
|
||||||
|
stroke="#2481cc"
|
||||||
|
stroke-width="8"
|
||||||
|
stroke-linecap="round"
|
||||||
|
:style="{ strokeDashoffset: 339.292 - (339.292 * loadingProgress / 100) }"
|
||||||
|
></circle>
|
||||||
|
</svg>
|
||||||
|
<div class="progress-ring-text">
|
||||||
|
<span class="text-2xl font-bold text-slate-900" x-text="loadingProgress + '%'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<p class="text-sm font-medium text-slate-600" x-text="loadingMessage"></p>
|
||||||
|
<div class="mt-3 flex gap-1 justify-center">
|
||||||
|
<template x-for="n in 6" :key="n">
|
||||||
|
<div class="w-2 h-2 rounded-full transition-all" :class="loadingStage >= n ? 'bg-primary' : 'bg-slate-200'"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Initial Setup -->
|
||||||
|
<template x-if="!loading && isSystemEmpty">
|
||||||
|
<div class="max-w-md mx-auto bg-white p-8 rounded-2xl shadow-sm border border-slate-200">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-10V4a1 1 0 011-1h2a1 1 0 011 1v3M12 15h.01M12 19h.01M12 7h.01M12 11h.01" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-['Poppins'] font-semibold">Welcome to SchoolHub</h2>
|
||||||
|
<p class="text-slate-500 mt-2">No users found. Register yourself as the first Administrator.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="registerFirstAdmin()">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-1">Your Email</label>
|
||||||
|
<input type="text" :value="userEmail" disabled class="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-slate-500 outline-none" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-1">Your Full Name</label>
|
||||||
|
<input type="text" x-model="setupName" required class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none" placeholder="Enter your name" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" :disabled="saving" class="w-full py-3 bg-primary text-white rounded-lg font-semibold hover:bg-blue-600 transition-colors disabled:opacity-50">
|
||||||
|
<span x-show="!saving">Initialize System</span>
|
||||||
|
<span x-show="saving">Setting up...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Needs Auth -->
|
||||||
|
<template x-if="!loading && needsAuth">
|
||||||
|
<div class="max-w-md mx-auto bg-white p-8 rounded-2xl shadow-sm border border-slate-200 text-center">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-50 text-blue-600 mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold mb-2">Identitas Tidak Terdeteksi</h2>
|
||||||
|
<p class="text-slate-600 mb-6 text-sm">Google tidak memberikan informasi akun Anda. Pastikan Anda telah login dan memberikan izin akses aplikasi.</p>
|
||||||
|
<div class="bg-amber-50 border border-amber-100 rounded-lg p-4 mb-6 text-left">
|
||||||
|
<p class="text-xs text-amber-800 font-medium mb-1">Tips Pengujian:</p>
|
||||||
|
<ul class="text-[10px] text-amber-700 list-disc ml-4 space-y-1">
|
||||||
|
<li>Gunakan <b>Incognito Window</b>.</li>
|
||||||
|
<li>Pastikan hanya 1 akun Google yang aktif di browser.</li>
|
||||||
|
<li>Klik tombol di bawah untuk muat ulang.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="window.top.location.reload()" class="w-full py-3 bg-primary text-white rounded-lg font-semibold hover:bg-blue-600 transition-colors">
|
||||||
|
Muat Ulang Aplikasi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<template x-if="!loading && error && !needsAuth">
|
||||||
|
<div class="max-w-md mx-auto bg-white p-8 rounded-2xl shadow-sm border border-red-100 text-center">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-50 text-red-600 mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold mb-2">Access Restricted</h2>
|
||||||
|
<p class="text-slate-600 mb-6" x-text="error"></p>
|
||||||
|
<button type="button" @click="window.top.location.reload()" class="w-full py-2 px-4 bg-slate-100 hover:bg-slate-200 rounded-lg transition-colors font-medium">
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Home -->
|
||||||
|
<template x-if="!loading && !error && view === 'home'">
|
||||||
|
<div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-2xl font-['Poppins'] font-medium">Your Applications</h2>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1 bg-slate-100 p-1 rounded-lg" aria-label="Layout mode">
|
||||||
|
<button type="button" @click="layoutMode='grid'" :class="{'bg-white shadow-sm text-primary': layoutMode==='grid'}" class="p-2 rounded-md text-slate-400 hover:text-primary transition-colors" title="Grid View">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="layoutMode='bento'" :class="{'bg-white shadow-sm text-primary': layoutMode==='bento'}" class="p-2 rounded-md text-slate-400 hover:text-primary transition-colors" title="Bento View">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 13a1 1 0 011-1h4a1 1 0 011 1v6a1 1 0 01-1 1h-4a1 1 0 01-1-1v-6z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="layoutMode === 'grid'">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<template x-for="app in apps" :key="app.appId">
|
||||||
|
<div @click="openApp(app.appUrl)" class="group bg-white p-6 rounded-2xl border border-slate-200 shadow-sm hover:shadow-md hover:border-primary/30 transition-all cursor-pointer" :title="'Buka ' + app.appName">
|
||||||
|
<div class="flex items-start justify-between mb-4">
|
||||||
|
<div class="h-12 w-12 rounded-xl bg-primary/5 text-primary flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-300 group-hover:text-primary transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold text-lg" x-text="app.appName"></h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="layoutMode === 'bento'">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<template x-if="featuredApps.length > 0">
|
||||||
|
<div @click="openApp(featuredApps[0].appUrl)" class="group col-span-2 bg-gradient-to-br from-primary to-blue-600 p-6 rounded-2xl text-white shadow-lg hover:shadow-xl transition-all cursor-pointer relative overflow-hidden" :title="'Buka ' + featuredApps[0].appName">
|
||||||
|
<div class="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -mr-16 -mt-16"></div>
|
||||||
|
<div class="absolute bottom-0 left-0 w-24 h-24 bg-white/5 rounded-full -ml-12 -mb-12"></div>
|
||||||
|
<div class="relative z-10">
|
||||||
|
<div class="flex items-start justify-between mb-4">
|
||||||
|
<div class="h-14 w-14 rounded-xl bg-white/20 backdrop-blur-sm text-white flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="px-2 py-1 bg-white/20 rounded-lg text-[10px] font-bold uppercase tracking-wider">Featured</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-['Poppins'] font-bold text-2xl mb-1" x-text="featuredApps[0].appName"></h3>
|
||||||
|
<p class="text-white/70 text-sm">Tap to open</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-for="app in featuredApps.slice(1)" :key="app.appId">
|
||||||
|
<div @click="openApp(app.appUrl)" class="group bg-white p-5 rounded-2xl border border-slate-200 shadow-sm hover:shadow-md hover:border-primary/30 transition-all cursor-pointer" :title="'Buka ' + app.appName">
|
||||||
|
<div class="flex items-start justify-between mb-3">
|
||||||
|
<div class="h-10 w-10 rounded-lg bg-primary/5 text-primary flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold text-base" x-text="app.appName"></h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-for="app in otherApps" :key="app.appId">
|
||||||
|
<div @click="openApp(app.appUrl)" class="group bg-white p-4 rounded-xl border border-slate-200 shadow-sm hover:shadow-md hover:border-primary/30 transition-all cursor-pointer" :title="'Buka ' + app.appName">
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<div class="h-8 w-8 rounded-lg bg-primary/5 text-primary flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-medium text-sm" x-text="app.appName"></h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="apps.length === 0">
|
||||||
|
<div class="text-center py-20 bg-white rounded-2xl border border-dashed border-slate-300">
|
||||||
|
<p class="text-slate-500">No applications assigned to your account.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Users -->
|
||||||
|
<template x-if="!loading && view === 'users'">
|
||||||
|
<div x-init="fetchAdminUsers()">
|
||||||
|
<div class="category-bar">
|
||||||
|
<button type="button" @click="userCategory = 'all'" class="category-pill" :class="userCategory === 'all' ? 'active' : ''">
|
||||||
|
<span>Semua</span>
|
||||||
|
<span class="category-count" x-text="adminUsers.length"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="userCategory = 'admin'" class="category-pill" :class="userCategory === 'admin' ? 'active' : ''">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.64.304 1.25.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Admin</span>
|
||||||
|
<span class="category-count" x-text="adminUsers.filter(u => u.isAdmin === 'TRUE' || u.isAdmin === true).length"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="userCategory = 'active'" class="category-pill" :class="userCategory === 'active' ? 'active' : ''">
|
||||||
|
<span>Aktif</span>
|
||||||
|
<span class="category-count" x-text="adminUsers.filter(u => String(u.status || '').toLowerCase() === 'active').length"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="userCategory = 'inactive'" class="category-pill" :class="userCategory === 'inactive' ? 'active' : ''">
|
||||||
|
<span>Nonaktif</span>
|
||||||
|
<span class="category-count" x-text="adminUsers.filter(u => String(u.status || '').toLowerCase() !== 'active').length"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="loadingUsers">
|
||||||
|
<div class="flex flex-col items-center justify-center py-10 bg-white rounded-xl border border-slate-200 shadow-sm">
|
||||||
|
<div class="relative w-16 h-16">
|
||||||
|
<svg class="w-16 h-16" viewBox="0 0 36 36">
|
||||||
|
<path class="text-slate-200" fill="none" stroke-width="3" stroke="currentColor" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||||||
|
<path class="text-primary" fill="none" stroke-width="3" stroke-linecap="round" stroke="currentColor" :stroke-dasharray="loadingProgress + ', 100'" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||||||
|
</svg>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center text-[10px] font-bold text-primary" x-text="loadingProgress + '%'"></div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3 text-xs text-slate-400 font-medium">Loading community...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div x-show="!loadingUsers" class="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm divide-y divide-slate-100">
|
||||||
|
<template x-for="u in filteredUsers" :key="u.email">
|
||||||
|
<div class="user-item p-4 flex items-center justify-between hover:bg-slate-50 transition-colors" @click="openUserModal(u)">
|
||||||
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
|
<div
|
||||||
|
class="h-10 w-10 rounded-full flex-shrink-0 flex items-center justify-center text-white font-semibold text-sm"
|
||||||
|
:class="String(u.status || '').toLowerCase() === 'active' ? 'bg-primary' : 'bg-slate-300'"
|
||||||
|
x-text="(u.name || '?').charAt(0).toUpperCase()"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="font-semibold text-slate-900 truncate" x-text="u.name"></span>
|
||||||
|
<template x-if="u.isAdmin == 'TRUE' || u.isAdmin === true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.64.304 1.25.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="text-[11px] text-slate-400 truncate" x-text="u.unit || 'No Department'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="button" @click.stop="openAccessModal(u)" class="p-1.5 text-[10px] font-bold bg-blue-50 text-blue-600 rounded-md hover:bg-blue-100 transition-colors uppercase tracking-wider">
|
||||||
|
Akses
|
||||||
|
</button>
|
||||||
|
<span x-show="String(u.status || '').toLowerCase() !== 'active'" class="text-[10px] bg-red-50 text-red-500 px-1.5 py-0.5 rounded font-bold">
|
||||||
|
INACTIVE
|
||||||
|
</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="!loadingUsers && filteredUsers.length === 0">
|
||||||
|
<div class="text-center py-10 bg-white rounded-xl border border-dashed border-slate-300">
|
||||||
|
<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-slate-100 mb-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-500 text-sm font-medium">Tidak ada pengguna dalam kategori ini</p>
|
||||||
|
<p class="text-slate-400 text-xs mt-1">Coba pilih kategori lain atau tambahkan pengguna baru</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Manage Apps -->
|
||||||
|
<template x-if="!loading && view === 'manage'">
|
||||||
|
<div x-init="fetchAdminApps()">
|
||||||
|
<div class="mb-4">
|
||||||
|
<p class="text-slate-500 text-xs px-2 uppercase font-bold tracking-widest">System Applications</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template x-if="loadingApps">
|
||||||
|
<div class="flex flex-col items-center justify-center py-10 bg-white rounded-xl border border-slate-200 shadow-sm">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
<p class="mt-3 text-xs text-slate-400">Loading system apps...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div x-show="!loadingApps" class="admin-card-list">
|
||||||
|
<template x-for="app in adminApps" :key="app.appId">
|
||||||
|
<div class="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
|
||||||
|
<div class="flex-1 min-w-0 mr-4">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<span class="font-bold text-slate-900 truncate" x-text="app.appName"></span>
|
||||||
|
<span
|
||||||
|
:class="app.active == 'TRUE' || app.active === true ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'"
|
||||||
|
class="px-2 py-0.5 rounded text-[10px] font-bold shrink-0"
|
||||||
|
x-text="app.active == 'TRUE' || app.active === true ? 'AKTIF' : 'NONAKTIF'"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div class="text-[11px] text-slate-400 font-mono truncate" x-text="app.appId"></div>
|
||||||
|
<div class="text-[11px] text-primary truncate mt-1" x-text="app.appUrl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex shrink-0 items-center gap-1">
|
||||||
|
<button type="button" @click="openAppModal(app)" class="p-2 text-slate-400 bg-slate-50 rounded-lg hover:text-primary" aria-label="Edit aplikasi">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="confirmDelete(app)" class="p-2 text-slate-400 bg-slate-50 rounded-lg hover:text-red-500" aria-label="Hapus aplikasi">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="adminApps.length === 0">
|
||||||
|
<div class="text-center py-10 bg-white rounded-xl border border-dashed border-slate-300 italic text-slate-400 text-sm">
|
||||||
|
Belum ada aplikasi terdaftar
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FAB -->
|
||||||
|
<template x-if="isAdmin && (view === 'users' || view === 'manage')">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="view === 'users' ? openUserModal() : openAppModal()"
|
||||||
|
class="fab-button bg-primary text-white"
|
||||||
|
:aria-label="view === 'users' ? 'Tambah pengguna' : 'Tambah aplikasi'"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Notification -->
|
||||||
|
<div
|
||||||
|
x-show="toast.open"
|
||||||
|
x-cloak
|
||||||
|
x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0"
|
||||||
|
x-transition:leave="transition ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="fixed bottom-20 left-1/2 -translate-x-1/2 z-[700] w-[90%] max-w-sm"
|
||||||
|
>
|
||||||
|
<div :class="{
|
||||||
|
'bg-green-600': toast.type === 'success',
|
||||||
|
'bg-amber-500': toast.type === 'warning',
|
||||||
|
'bg-red-600': toast.type === 'danger'
|
||||||
|
}" class="px-4 py-3 rounded-2xl shadow-lg text-white flex items-center gap-3">
|
||||||
|
<div class="bg-white/20 p-1.5 rounded-full">
|
||||||
|
<svg x-show="toast.type === 'success'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<svg x-show="toast.type === 'danger' || toast.type === 'warning'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm font-bold leading-none" x-text="toast.title"></span>
|
||||||
|
<span class="text-xs opacity-90" x-text="toast.message"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Modal -->
|
||||||
|
<div
|
||||||
|
x-show="userModalOpen"
|
||||||
|
x-cloak
|
||||||
|
class="fixed inset-0 z-[1000] overflow-y-auto"
|
||||||
|
@keydown.escape.window="userModalOpen = false"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div
|
||||||
|
x-show="userModalOpen"
|
||||||
|
@click="userModalOpen = false"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
class="fixed inset-0 transition-opacity bg-slate-900/50 backdrop-blur-sm"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-show="userModalOpen"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="user-modal-title"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
class="relative z-10 inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle max-w-[400px] w-full mx-auto"
|
||||||
|
>
|
||||||
|
<form @submit.prevent="saveUser()">
|
||||||
|
<div class="bg-white px-6 pt-6 pb-4">
|
||||||
|
<h3 id="user-modal-title" class="text-lg font-semibold text-slate-900 mb-4" x-text="userForm.isNew ? 'Add New User' : 'Edit User'"></h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-1">Email Address</label>
|
||||||
|
<input type="email" x-model="userForm.email" :disabled="!userForm.isNew" class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none disabled:bg-slate-50" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-1">Full Name</label>
|
||||||
|
<input type="text" x-model="userForm.name" class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-1">Unit / Department</label>
|
||||||
|
<input type="text" x-model="userForm.unit" class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none" placeholder="e.g. Grade 10, Mathematics" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-1">Status</label>
|
||||||
|
<select x-model="userForm.status" class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none">
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="inactive">Inactive</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-1">Admin Access</label>
|
||||||
|
<select x-model="userForm.isAdmin" class="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none">
|
||||||
|
<option value="FALSE">No</option>
|
||||||
|
<option value="TRUE">Yes (Full Admin)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-slate-50 px-6 py-4 flex justify-between items-center mt-4">
|
||||||
|
<div>
|
||||||
|
<template x-if="!userForm.isNew">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="confirmDeleteUser(userForm)"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-lg transition-colors flex items-center gap-2"
|
||||||
|
:disabled="saving"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
Delete User
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button type="button" @click="userModalOpen = false" class="px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-200 rounded-lg transition-colors">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" :disabled="saving" class="px-4 py-2 bg-primary text-white text-sm font-medium rounded-lg hover:bg-blue-600 transition-colors disabled:opacity-50">
|
||||||
|
<span x-show="!saving">Save User</span>
|
||||||
|
<span x-show="saving">Saving...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alert Modal -->
|
||||||
|
<div
|
||||||
|
x-show="alertModalOpen"
|
||||||
|
x-cloak
|
||||||
|
class="fixed inset-0 z-[2000] overflow-y-auto"
|
||||||
|
@keydown.escape.window="alertModalOpen = false"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div
|
||||||
|
x-show="alertModalOpen"
|
||||||
|
@click="alertModalOpen = false"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
class="fixed inset-0 transition-opacity bg-slate-900/50 backdrop-blur-sm"
|
||||||
|
></div>
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
|
<div
|
||||||
|
x-show="alertModalOpen"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
class="relative z-10 inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle max-w-sm w-full mx-auto"
|
||||||
|
>
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-amber-100 text-amber-600 mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-slate-900 mb-2" x-text="alertData.title || 'Notification'"></h3>
|
||||||
|
<p class="text-slate-600 text-sm mb-6" x-text="alertData.message"></p>
|
||||||
|
<button
|
||||||
|
@click="alertModalOpen = false"
|
||||||
|
class="w-full py-2.5 bg-slate-900 text-white rounded-xl font-semibold hover:bg-slate-800 transition-colors"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Modal -->
|
||||||
|
<div
|
||||||
|
x-show="confirmModalOpen"
|
||||||
|
x-cloak
|
||||||
|
class="fixed inset-0 z-[2000] overflow-y-auto"
|
||||||
|
@keydown.escape.window="confirmModalOpen = false"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div
|
||||||
|
x-show="confirmModalOpen"
|
||||||
|
@click="confirmModalOpen = false"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
class="fixed inset-0 transition-opacity bg-slate-900/50 backdrop-blur-sm"
|
||||||
|
></div>
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
|
<div
|
||||||
|
x-show="confirmModalOpen"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
class="relative z-10 inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle max-w-sm w-full mx-auto"
|
||||||
|
>
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 text-red-600 mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-slate-900 mb-2" x-text="confirmData.title || 'Are you sure?'"></h3>
|
||||||
|
<p class="text-slate-600 text-sm mb-6" x-text="confirmData.message"></p>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button
|
||||||
|
@click="confirmModalOpen = false"
|
||||||
|
class="flex-1 py-2.5 bg-slate-100 text-slate-700 rounded-xl font-semibold hover:bg-slate-200 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="handleConfirm()"
|
||||||
|
class="flex-1 py-2.5 bg-red-600 text-white rounded-xl font-semibold hover:bg-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Access Modal -->
|
||||||
|
<div
|
||||||
|
x-show="accessModalOpen"
|
||||||
|
x-cloak
|
||||||
|
class="fixed inset-0 z-[1000] overflow-y-auto"
|
||||||
|
@keydown.escape.window="accessModalOpen = false"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div
|
||||||
|
x-show="accessModalOpen"
|
||||||
|
@click="accessModalOpen = false"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
class="fixed inset-0 transition-opacity bg-slate-900/50 backdrop-blur-sm"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-show="accessModalOpen"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
class="relative z-10 inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle max-w-[450px] w-full mx-auto"
|
||||||
|
>
|
||||||
|
<div class="bg-white px-6 pt-6 pb-4">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold text-slate-900" x-text="'Manage Access'"></h3>
|
||||||
|
<p class="text-xs text-slate-500" x-text="accessUser ? accessUser.name + ' (' + accessUser.email + ')' : ''"></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="accessModalOpen = false" class="text-slate-400 hover:text-slate-600">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
|
||||||
|
<template x-for="app in availableApps" :key="app.appId">
|
||||||
|
<div class="p-3 border border-slate-100 rounded-xl flex items-center justify-between gap-3 hover:bg-slate-50 transition-colors">
|
||||||
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="isAppAssigned(app.appId)"
|
||||||
|
@change="toggleAppAccess(app)"
|
||||||
|
class="w-4 h-4 text-primary rounded focus:ring-primary"
|
||||||
|
>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-sm font-semibold text-slate-900 truncate" x-text="app.appName"></p>
|
||||||
|
<p class="text-[10px] text-slate-400 font-mono" x-text="app.appId"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="isAppAssigned(app.appId)" class="flex items-center gap-2">
|
||||||
|
<select
|
||||||
|
@change="updateAppRole(app.appId, $event.target.value)"
|
||||||
|
class="text-xs border-slate-200 rounded-md bg-white py-1 px-2 outline-none focus:ring-1 focus:ring-primary"
|
||||||
|
>
|
||||||
|
<template x-for="role in ['viewer', 'editor', 'admin']">
|
||||||
|
<option :value="role" :selected="getAppRole(app.appId) === role" x-text="role.toUpperCase()"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-50 px-6 py-4 flex justify-end">
|
||||||
|
<button type="button" @click="accessModalOpen = false" class="px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-200 rounded-lg transition-colors">
|
||||||
|
Done
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App Modal -->
|
||||||
|
<div
|
||||||
|
x-show="modalOpen"
|
||||||
|
x-cloak
|
||||||
|
class="fixed inset-0 z-[1000] overflow-y-auto"
|
||||||
|
@keydown.escape.window="modalOpen = false"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div
|
||||||
|
x-show="modalOpen"
|
||||||
|
@click="modalOpen = false"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
class="fixed inset-0 transition-opacity bg-slate-900/50 backdrop-blur-sm"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-show="modalOpen"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="app-modal-title"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
class="relative z-10 inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle max-w-[400px] w-full mx-auto"
|
||||||
|
>
|
||||||
|
<form @submit.prevent="saveApp()">
|
||||||
|
<div class="bg-white px-8 pt-8 pb-6">
|
||||||
|
<h3 id="app-modal-title" class="text-xl font-bold text-slate-900 mb-8" x-text="appForm.isNew ? 'Add New Application' : 'Edit Application'"></h3>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">App ID (Unique Slug)</label>
|
||||||
|
<input type="text" x-model="appForm.appId" :disabled="!appForm.isNew" class="w-full px-3 py-2 bg-slate-100 border-none rounded-lg text-slate-600 outline-none disabled:cursor-not-allowed" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">App Name</label>
|
||||||
|
<input type="text" x-model="appForm.appName" class="w-full px-0 py-2 bg-transparent border-b border-slate-100 focus:border-primary outline-none transition-colors" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">URL</label>
|
||||||
|
<input type="url" x-model="appForm.appUrl" class="w-full px-0 py-2 bg-transparent border-b border-slate-100 focus:border-primary outline-none transition-colors" required placeholder="https://..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-8">
|
||||||
|
<div>
|
||||||
|
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">Sort Order</label>
|
||||||
|
<input type="number" x-model="appForm.sortOrder" class="w-full px-0 py-2 bg-transparent border-b border-slate-100 focus:border-primary outline-none transition-colors" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">Status</label>
|
||||||
|
<select x-model="appForm.active" class="w-full px-0 py-2 bg-transparent border-b border-slate-100 focus:border-primary outline-none transition-colors appearance-none">
|
||||||
|
<option value="TRUE">Active</option>
|
||||||
|
<option value="FALSE">Inactive</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white px-8 py-6 flex justify-between items-center border-t border-slate-50">
|
||||||
|
<button type="button" @click="modalOpen = false" class="px-4 py-2 text-sm font-semibold text-slate-500 hover:text-slate-800 transition-colors">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" :disabled="saving" class="px-8 py-3 bg-primary text-white text-sm font-bold rounded-full hover:bg-blue-600 transition-all shadow-md hover:shadow-lg disabled:opacity-50">
|
||||||
|
<span x-show="!saving">Save App</span>
|
||||||
|
<span x-show="saving">Saving...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?!= HtmlService.createHtmlOutputFromFile('scripts').getContent(); ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user