diff --git a/frontend/admin/admin.css b/frontend/admin/admin.css
deleted file mode 100644
index f178ab8..0000000
--- a/frontend/admin/admin.css
+++ /dev/null
@@ -1,1078 +0,0 @@
-/* ===== Reset & Base ===== */
-*,
-*::before,
-*::after {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-:root {
- /* Deep dark premium background */
- --bg-primary: #0a0a0f;
- --bg-sidebar: rgba(255, 255, 255, 0.02);
- --bg-card: rgba(255, 255, 255, 0.03);
- --bg-card-border: rgba(255, 255, 255, 0.05);
- --bg-input: rgba(255, 255, 255, 0.04);
- --bg-input-focus: rgba(255, 255, 255, 0.08);
- --bg-hover: rgba(255, 255, 255, 0.06);
-
- /* Typography */
- --text-primary: #f8fafc;
- --text-secondary: #94a3b8;
- --text-placeholder: #475569;
-
- /* Vibrant Accents */
- --accent: #8b5cf6;
- --accent-hover: #a78bfa;
- --accent-glow: rgba(139, 92, 246, 0.4);
- --accent-secondary: #ec4899;
-
- /* Status Colors */
- --error: #ef4444;
- --success: #10b981;
- --warning: #f59e0b;
-
- /* Spatial */
- --radius-sm: 10px;
- --radius-md: 16px;
- --transition: 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
-}
-
-/* ===== Light Theme ===== */
-[data-theme="light"] {
- --bg-primary: #f8fafc;
- --bg-sidebar: rgba(255, 255, 255, 0.7);
- --bg-card: rgba(255, 255, 255, 0.7);
- --bg-card-border: rgba(0, 0, 0, 0.08);
- --bg-input: rgba(0, 0, 0, 0.03);
- --bg-input-focus: rgba(0, 0, 0, 0.06);
- --bg-hover: rgba(0, 0, 0, 0.05);
- --text-primary: #0f172a;
- --text-secondary: #475569;
- --text-placeholder: #94a3b8;
- --accent: #6366f1;
- --accent-hover: #4f46e5;
- --accent-glow: rgba(99, 102, 241, 0.3);
- --accent-secondary: #d946ef;
- --error: #ef4444;
- --success: #10b981;
- --warning: #f59e0b;
-}
-
-[data-theme="light"] .form-group select option,
-[data-theme="light"] .filter-row select option {
- background: #fff;
- color: #1a1a2e;
-}
-
-[data-theme="light"] .nav-item.active {
- background: rgba(99, 102, 241, 0.18);
-}
-
-[data-theme="light"] .custom-multi-select .dropdown-menu {
- background: rgba(255, 255, 255, 0.98);
-}
-
-[data-theme="light"] .form-group input,
-[data-theme="light"] .form-group select,
-[data-theme="light"] .filter-row select {
- border-color: rgba(0, 0, 0, 0.15);
-}
-
-[data-theme="light"] tbody td {
- border-bottom-color: rgba(0, 0, 0, 0.08);
-}
-
-html {
- font-size: 16px;
- -webkit-font-smoothing: antialiased;
-}
-
-body {
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
- background: var(--bg-primary);
- color: var(--text-primary);
- min-height: 100vh;
- display: flex;
- transition: background 0.4s ease, color 0.4s ease;
-}
-
-/* ===== Sidebar ===== */
-.sidebar {
- width: 260px;
- min-height: 100vh;
- background: var(--bg-sidebar);
- backdrop-filter: blur(20px);
- -webkit-backdrop-filter: blur(20px);
- border-right: 1px solid var(--bg-card-border);
- display: flex;
- flex-direction: column;
- position: fixed;
- left: 0;
- top: 0;
- bottom: 0;
- z-index: 10;
- transition: background 0.4s ease, border-color 0.4s ease, transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
-}
-
-.sidebar-header {
- padding: 1.25rem;
- border-bottom: 1px solid var(--bg-card-border);
-}
-
-.logo {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- font-size: 1.15rem;
- font-weight: 700;
- letter-spacing: -0.02em;
-}
-
-.sidebar-nav {
- flex: 1;
- padding: 0.75rem;
-}
-
-.nav-item {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 0.75rem 1rem;
- margin-bottom: 0.25rem;
- border-radius: var(--radius-sm);
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.95rem;
- font-weight: 500;
- transition: all var(--transition);
- position: relative;
- overflow: hidden;
-}
-
-.nav-item::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 3px;
- background: var(--accent);
- border-radius: 0 4px 4px 0;
- transform: scaleY(0);
- transition: transform var(--transition);
- opacity: 0;
-}
-
-.nav-item:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- transform: translateX(4px);
-}
-
-.nav-item.active {
- background: rgba(139, 92, 246, 0.12);
- color: var(--accent-hover);
-}
-
-.nav-item.active::before {
- transform: scaleY(1);
- opacity: 1;
-}
-
-.nav-item svg {
- transition: transform var(--transition);
-}
-
-.nav-item:hover svg,
-.nav-item.active svg {
- transform: scale(1.15) rotate(-5deg);
-}
-
-/* Checkbox list styling */
-.checkbox-group {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- padding: 8px 0;
-}
-
-.checkbox-item {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 0.9rem;
- color: var(--text-base);
- cursor: pointer;
-}
-
-.checkbox-item input[type="checkbox"] {
- cursor: pointer;
- width: 16px;
- height: 16px;
- accent-color: var(--primary-color);
-}
-
-/* Classroom Status Badges */
-.badge-available {
- background-color: var(--success-bg);
- color: var(--success-color);
-}
-
-.badge-unavailable {
- background-color: var(--error-bg);
- color: var(--error-color);
-}
-
-.status-cell {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.btn-icon-toggle {
- background: var(--bg-body);
- border: 1px solid var(--border-color);
- color: var(--text-muted);
- width: 28px;
- height: 28px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- padding: 0;
-}
-
-.btn-icon-toggle:hover {
- background: var(--card-bg);
- border-color: var(--primary-color);
- color: var(--primary-color);
- transform: rotate(45deg);
- box-shadow: 0 0 10px rgba(99, 102, 241, 0.2);
-}
-
-.btn-icon-toggle svg {
- display: block;
-}
-
-.nav-item.active {
- background: rgba(99, 102, 241, 0.12);
- color: var(--accent-hover);
-}
-
-.sidebar-footer {
- padding: 0.75rem;
- border-top: 1px solid var(--bg-card-border);
-}
-
-.btn-logout {
- width: 100%;
- display: flex;
- align-items: center;
- gap: 0.6rem;
- padding: 0.65rem 0.8rem;
- border: none;
- border-radius: var(--radius-sm);
- background: none;
- color: var(--text-secondary);
- font-family: inherit;
- font-size: 0.9rem;
- cursor: pointer;
- transition: background var(--transition), color var(--transition);
-}
-
-.btn-logout:hover {
- background: rgba(248, 113, 113, 0.1);
- color: var(--error);
-}
-
-/* ===== Main ===== */
-.main {
- flex: 1;
- margin-left: 260px;
- min-height: 100vh;
-}
-
-.topbar {
- padding: 1.5rem 2rem;
- border-bottom: 1px solid var(--bg-card-border);
- transition: border-color 0.4s ease;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 1rem;
-}
-
-.topbar h1 {
- font-size: 1.3rem;
- font-weight: 700;
- letter-spacing: -0.02em;
- flex: 1;
-}
-
-.content {
- padding: 1.5rem 2rem;
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
-}
-
-/* ===== Cards ===== */
-@keyframes slideUpCard {
- from {
- opacity: 0;
- transform: translateY(15px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.card {
- background: var(--bg-card);
- backdrop-filter: blur(20px);
- -webkit-backdrop-filter: blur(20px);
- border: 1px solid var(--bg-card-border);
- border-radius: var(--radius-md);
- padding: 1.75rem;
- position: relative;
- overflow: visible;
- transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
- animation: slideUpCard 0.5s cubic-bezier(0.25, 0.8, 0.25, 1) both;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
-}
-
-.card::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 1px;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
- opacity: 0;
- transition: opacity var(--transition);
-}
-
-.card:hover {
- transform: translateY(-4px);
- box-shadow: 0 12px 30px rgba(0, 0, 0, 0.1);
- border-color: rgba(255, 255, 255, 0.12);
-}
-
-.card:hover::before {
- opacity: 1;
-}
-
-/* Staggered cards */
-.card:nth-child(1) {
- animation-delay: 0.1s;
-}
-
-.card:nth-child(2) {
- animation-delay: 0.2s;
-}
-
-.card:nth-child(3) {
- animation-delay: 0.3s;
-}
-
-/* Specific Cards */
-.create-card {
- z-index: 10;
-}
-
-.card h2 {
- font-size: 0.8rem;
- font-weight: 600;
- margin-bottom: 1rem;
- color: var(--text-secondary);
- text-transform: uppercase;
- letter-spacing: 0.04em;
-}
-
-/* ===== Create Form ===== */
-.form-row {
- display: flex;
- gap: 1rem;
- align-items: flex-end;
- flex-wrap: wrap;
-}
-
-.form-row .form-group {
- flex: 1;
- min-width: 160px;
-}
-
-.form-group label {
- display: block;
- font-size: 0.78rem;
- font-weight: 500;
- color: var(--text-secondary);
- margin-bottom: 0.4rem;
- text-transform: uppercase;
- letter-spacing: 0.04em;
-}
-
-.form-group input,
-.form-group select {
- width: 100%;
- padding: 0.75rem 1rem;
- background: var(--bg-input);
- border: 1px solid var(--bg-card-border);
- border-radius: var(--radius-sm);
- color: var(--text-primary);
- font-family: inherit;
- font-size: 0.95rem;
- outline: none;
- transition: all var(--transition);
-}
-
-.form-group input::placeholder {
- color: var(--text-placeholder);
- transition: opacity var(--transition);
-}
-
-.form-group input:focus,
-.form-group select:focus {
- background: var(--bg-input-focus);
- border-color: var(--accent);
- box-shadow: 0 0 0 4px var(--accent-glow);
- transform: translateY(-1px);
-}
-
-.form-group input:focus::placeholder {
- opacity: 0.5;
-}
-
-/* Hide Number Arrows */
-input[type="number"]::-webkit-outer-spin-button,
-input[type="number"]::-webkit-inner-spin-button {
- -webkit-appearance: none;
- margin: 0;
-}
-
-input[type="number"] {
- -moz-appearance: textfield;
-}
-
-.form-group select {
- cursor: pointer;
- appearance: none;
- background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: right 0.75rem center;
- padding-right: 2.25rem;
-}
-
-.form-group select option {
- background: #1a1a2e;
- color: var(--text-primary);
-}
-
-.btn-create {
- position: relative;
- overflow: hidden;
- padding: 0.75rem 1.75rem;
- background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
- border: none;
- border-radius: var(--radius-sm);
- color: #fff;
- font-family: inherit;
- font-size: 0.95rem;
- font-weight: 600;
- letter-spacing: 0.02em;
- cursor: pointer;
- white-space: nowrap;
- transition: all var(--transition);
- box-shadow: 0 4px 15px var(--accent-glow);
-}
-
-.btn-create::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: linear-gradient(rgba(255, 255, 255, 0.2), transparent);
- border-radius: inherit;
- opacity: 0;
- transition: opacity var(--transition);
-}
-
-.btn-create:hover {
- transform: translateY(-2px);
- box-shadow: 0 8px 25px var(--accent-glow);
-}
-
-.btn-create:hover::before {
- opacity: 1;
-}
-
-.btn-create:active {
- transform: translateY(1px);
- box-shadow: 0 2px 10px var(--accent-glow);
-}
-
-@keyframes slideDownAlert {
- from {
- opacity: 0;
- transform: translateY(-10px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.form-alert {
- display: none;
- padding: 0.6rem 1rem;
- border-radius: var(--radius-sm);
- font-size: 0.85rem;
- margin-top: 0.75rem;
-}
-
-.form-alert.error {
- display: block;
- background: rgba(248, 113, 113, 0.1);
- border: 1px solid rgba(248, 113, 113, 0.2);
- color: var(--error);
- animation: slideDownAlert 0.3s ease-out both;
-}
-
-.form-alert.success {
- display: block;
- background: rgba(52, 211, 153, 0.1);
- border: 1px solid rgba(52, 211, 153, 0.2);
- color: var(--success);
- animation: slideDownAlert 0.3s ease-out both;
-}
-
-/* ===== Table ===== */
-.table-wrap {
- overflow-x: visible;
-}
-
-table {
- width: 100%;
- border-collapse: collapse;
-}
-
-thead th {
- text-align: left;
- font-size: 0.78rem;
- font-weight: 600;
- color: var(--text-secondary);
- text-transform: uppercase;
- letter-spacing: 0.04em;
- padding: 0.6rem 0.8rem;
- border-bottom: 1px solid var(--bg-card-border);
-}
-
-tbody td {
- padding: 0.85rem 1rem;
- font-size: 0.95rem;
- border-bottom: 1px solid var(--bg-card-border);
- transition: background var(--transition);
-}
-
-@keyframes slideInRow {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes slideInRow {
- from {
- opacity: 0;
- transform: translateX(-10px);
- }
-
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
-
-tbody tr {
- transition: background var(--transition);
- animation: slideInRow 0.3s ease-out both;
-}
-
-tbody tr:nth-child(1) {
- animation-delay: 0.05s;
-}
-
-tbody tr:nth-child(2) {
- animation-delay: 0.1s;
-}
-
-tbody tr:nth-child(3) {
- animation-delay: 0.15s;
-}
-
-tbody tr:nth-child(4) {
- animation-delay: 0.2s;
-}
-
-tbody tr:nth-child(5) {
- animation-delay: 0.25s;
-}
-
-tbody tr:nth-child(n+6) {
- animation-delay: 0.3s;
-}
-
-tbody tr:hover {
- background: var(--bg-hover);
-}
-
-.loading-row {
- text-align: center;
- color: var(--text-secondary);
- padding: 2rem !important;
-}
-
-/* ===== Role Badges ===== */
-.badge {
- display: inline-block;
- padding: 0.2rem 0.6rem;
- border-radius: 999px;
- font-size: 0.75rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.03em;
-}
-
-.badge-admin {
- background: rgba(248, 113, 113, 0.15);
- color: var(--error);
-}
-
-.badge-teacher {
- background: rgba(251, 191, 36, 0.15);
- color: var(--warning);
-}
-
-.badge-student {
- background: rgba(52, 211, 153, 0.15);
- color: var(--success);
-}
-
-/* ===== Education Form Badge ===== */
-.badge-ef {
- background: rgba(99, 102, 241, 0.15);
- color: var(--accent-hover);
-}
-
-/* ===== Card Header Row ===== */
-.card-header-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 0.75rem;
- margin-bottom: 1rem;
-}
-
-.card-header-row h2 {
- margin-bottom: 0;
-}
-
-.filter-row {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.filter-row label {
- font-size: 0.78rem;
- font-weight: 500;
- color: var(--text-secondary);
- text-transform: uppercase;
- letter-spacing: 0.04em;
- white-space: nowrap;
-}
-
-.filter-row select {
- padding: 0.45rem 2rem 0.45rem 0.7rem;
- background: var(--bg-input);
- border: 1px solid transparent;
- border-radius: var(--radius-sm);
- color: var(--text-primary);
- font-family: inherit;
- font-size: 0.85rem;
- outline: none;
- cursor: pointer;
- appearance: none;
- background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: right 0.6rem center;
- transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
-}
-
-.filter-row select:focus {
- background-color: var(--bg-input-focus);
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-glow);
-}
-
-.filter-row select option {
- background: #1a1a2e;
- color: var(--text-primary);
-}
-
-/* ===== Tab Content ===== */
-.tab-content {
- animation: fadeIn 0.2s ease;
-}
-
-@keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(4px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-/* ===== Delete Button ===== */
-.btn-delete {
- padding: 0.35rem 0.7rem;
- background: rgba(248, 113, 113, 0.1);
- border: 1px solid rgba(248, 113, 113, 0.2);
- border-radius: var(--radius-sm);
- color: var(--error);
- font-family: inherit;
- font-size: 0.8rem;
- cursor: pointer;
- transition: background var(--transition), transform var(--transition);
-}
-
-.btn-delete:hover {
- background: rgba(248, 113, 113, 0.2);
- transform: scale(1.05);
-}
-
-/* ===== Ripple Effect ===== */
-.ripple {
- position: absolute;
- border-radius: 50%;
- transform: scale(0);
- animation: admin-ripple 0.6s linear;
- background-color: rgba(255, 255, 255, 0.3);
- pointer-events: none;
-}
-
-@keyframes admin-ripple {
- to {
- transform: scale(4);
- opacity: 0;
- }
-}
-
-/* ===== Mobile Menu Toggle ===== */
-.menu-toggle {
- display: none;
- padding: 0.4rem;
- background: none;
- border: none;
- color: var(--text-primary);
- cursor: pointer;
- border-radius: var(--radius-sm);
- transition: background var(--transition);
-}
-
-.menu-toggle:hover {
- background: var(--bg-hover);
-}
-
-/* ===== Sidebar Overlay ===== */
-.sidebar-overlay {
- display: none;
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.5);
- z-index: 9;
- backdrop-filter: blur(2px);
-}
-
-/* ===== Theme Toggle Button ===== */
-.theme-toggle {
- width: 40px;
- height: 40px;
- border: none;
- border-radius: 50%;
- background: var(--bg-card);
- border: 1px solid var(--bg-card-border);
- color: var(--text-primary);
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- transition: all var(--transition);
- z-index: 100;
- flex-shrink: 0;
-}
-
-.theme-toggle svg {
- width: 20px;
- height: 20px;
- transition: transform 0.4s ease;
-}
-
-.theme-toggle:hover {
- transform: scale(1.1);
- box-shadow: 0 4px 16px var(--accent-glow);
-}
-
-.theme-toggle:active {
- transform: scale(0.95);
-}
-
-/* ===== Custom Multi Select ===== */
-.custom-multi-select {
- position: relative;
- width: 100%;
-}
-
-.custom-multi-select .select-box {
- width: 100%;
- padding: 0.75rem 1rem;
- background: var(--bg-input);
- border: 1px solid var(--bg-card-border);
- border-radius: var(--radius-sm);
- color: var(--text-primary);
- font-family: inherit;
- font-size: 0.95rem;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: space-between;
- transition: all var(--transition);
-}
-
-.custom-multi-select .select-box.active {
- background: var(--bg-input-focus);
- border-color: var(--accent);
- box-shadow: 0 0 0 4px var(--accent-glow);
-}
-
-.custom-multi-select .dropdown-icon {
- transition: transform var(--transition);
-}
-
-.custom-multi-select .select-box.active .dropdown-icon {
- transform: rotate(180deg);
-}
-
-.custom-multi-select .dropdown-menu {
- position: absolute;
- top: calc(100% + 5px);
- left: 0;
- width: 100%;
- background: rgba(15, 15, 26, 0.98);
- backdrop-filter: blur(24px);
- -webkit-backdrop-filter: blur(24px);
- border: 1px solid var(--bg-card-border);
- border-radius: var(--radius-md);
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
- padding: 0.75rem;
- z-index: 9999;
- opacity: 0;
- visibility: hidden;
- transform: translateY(-10px);
- transition: all var(--transition);
- max-height: 250px;
- overflow-y: auto;
-}
-
-.custom-multi-select .dropdown-menu.open {
- opacity: 1;
- visibility: visible;
- transform: translateY(0);
-}
-
-.checkbox-group-vertical {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.checkbox-group-vertical .checkbox-item {
- padding: 6px 8px;
- border-radius: 6px;
- transition: background var(--transition);
- display: flex;
- align-items: center;
- gap: 8px;
- cursor: pointer;
-}
-
-.checkbox-group-vertical .checkbox-item:hover {
- background: var(--bg-hover);
-}
-
-/* ===== Modals ===== */
-.modal-overlay {
- display: none;
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.5);
- z-index: 1000;
- backdrop-filter: blur(2px);
- align-items: center;
- justify-content: center;
- padding: 1rem;
-}
-
-.modal-overlay.open {
- display: flex;
- animation: fadeIn 0.2s ease;
-}
-
-.modal-content {
- background: var(--bg-primary);
- border: 1px solid var(--bg-card-border);
- border-radius: var(--radius-md);
- padding: 2rem;
- width: 100%;
- max-width: 600px;
- position: relative;
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
-}
-
-.modal-content h2 {
- font-size: 1.2rem;
- font-weight: 600;
- margin-bottom: 0.5rem;
- color: var(--text-primary);
- border-bottom: 1px solid var(--bg-card-border);
- padding-bottom: 1rem;
-}
-
-.modal-close {
- position: absolute;
- top: 1.5rem;
- right: 1.5rem;
- background: none;
- border: none;
- color: var(--text-secondary);
- font-size: 1.5rem;
- cursor: pointer;
- line-height: 1;
- transition: color var(--transition);
-}
-
-.modal-close:hover {
- color: var(--error);
-}
-
-/* ===== Responsive ===== */
-@media (max-width: 768px) {
- .sidebar {
- width: 240px;
- transform: translateX(-100%);
- transition: transform 0.3s ease;
- z-index: 20;
- }
-
- .sidebar.open {
- transform: translateX(0);
- }
-
- .sidebar-overlay.open {
- display: block;
- }
-
- .menu-toggle {
- display: flex;
- }
-
- .main {
- margin-left: 0;
- }
-
- .topbar {
- padding: 1rem;
- display: flex;
- align-items: center;
- gap: 0.75rem;
- }
-
- .topbar h1 {
- font-size: 1.1rem;
- }
-
- .content {
- padding: 1rem;
- }
-
- .card {
- padding: 1rem;
- }
-
- .form-row {
- flex-direction: column;
- align-items: stretch;
- }
-
- .form-row .form-group {
- min-width: 0;
- }
-
- .btn-create {
- width: 100%;
- text-align: center;
- }
-
- tbody td {
- padding: 0.5rem 0.6rem;
- font-size: 0.82rem;
- }
-
- thead th {
- padding: 0.5rem 0.6rem;
- font-size: 0.72rem;
- }
-
- .table-wrap {
- margin: 0 -1rem;
- padding: 0 1rem;
- }
-}
-
-@media (max-width: 480px) {
- .topbar h1 {
- font-size: 0.95rem;
- }
-
- .badge {
- font-size: 0.65rem;
- padding: 0.15rem 0.45rem;
- }
-
- .btn-delete {
- padding: 0.25rem 0.5rem;
- font-size: 0.72rem;
- }
-}
\ No newline at end of file
diff --git a/frontend/admin/admin.js b/frontend/admin/admin.js
deleted file mode 100644
index 9d0c01b..0000000
--- a/frontend/admin/admin.js
+++ /dev/null
@@ -1,911 +0,0 @@
-(() => {
- 'use strict';
-
- const token = localStorage.getItem('token');
- const role = localStorage.getItem('role');
-
- if (!token || role !== 'ADMIN') {
- window.location.href = '/';
- return;
- }
-
- // ---- DOM refs ----
- const pageTitle = document.getElementById('page-title');
- const btnLogout = document.getElementById('btn-logout');
- const menuToggle = document.getElementById('menu-toggle');
- const sidebar = document.querySelector('.sidebar');
- const sidebarOverlay = document.getElementById('sidebar-overlay');
-
- // Global Ripple Effect
- document.addEventListener('click', function (e) {
- const btn = e.target.closest('.btn-create, .btn-delete, .btn-logout');
- if (!btn) return;
-
- const rect = btn.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
-
- const ripple = document.createElement('span');
- ripple.classList.add('ripple');
- ripple.style.left = `${x}px`;
- ripple.style.top = `${y}px`;
-
- if (getComputedStyle(btn).position === 'static') {
- btn.style.position = 'relative';
- }
- btn.style.overflow = 'hidden';
-
- btn.appendChild(ripple);
-
- setTimeout(() => ripple.remove(), 600);
- });
-
- // Users
- const usersTbody = document.getElementById('users-tbody');
- const createForm = document.getElementById('create-form');
- const createAlert = document.getElementById('create-alert');
-
- // Groups
- const groupsTbody = document.getElementById('groups-tbody');
- const createGroupForm = document.getElementById('create-group-form');
- const createGroupAlert = document.getElementById('create-group-alert');
- const newGroupEfSelect = document.getElementById('new-group-ef');
- const filterEfSelect = document.getElementById('filter-ef');
-
- // Education Forms
- const efTbody = document.getElementById('ef-tbody');
- const createEfForm = document.getElementById('create-ef-form');
- const createEfAlert = document.getElementById('create-ef-alert');
-
- // Classrooms
- const classroomsTbody = document.getElementById('classrooms-tbody');
- const createClassroomForm = document.getElementById('create-classroom-form');
- const createClassroomAlert = document.getElementById('create-classroom-alert');
- const modalEditClassroom = document.getElementById('modal-edit-classroom');
- const modalEditClassroomClose = document.getElementById('modal-edit-classroom-close');
- const editClassroomForm = document.getElementById('edit-classroom-form');
- const editClassroomAlert = document.getElementById('edit-classroom-alert');
- const editEquipmentCheckboxes = document.getElementById('edit-equipment-checkboxes');
-
- // Equipments
- const equipmentsTbody = document.getElementById('equipments-tbody');
- const createEquipmentForm = document.getElementById('create-equipment-form');
- const createEquipmentAlert = document.getElementById('create-equipment-alert');
- const equipmentCheckboxes = document.getElementById('equipment-checkboxes');
-
- // Subjects
- const subjectsTbody = document.getElementById('subjects-tbody');
- const createSubjectForm = document.getElementById('create-subject-form');
- const createSubjectAlert = document.getElementById('create-subject-alert');
- const assignTeacherForm = document.getElementById('assign-teacher-form');
- const assignTeacherAlert = document.getElementById('assign-teacher-alert');
- const assignTeacherSelect = document.getElementById('assign-teacher-select');
- const assignSubjectSelect = document.getElementById('assign-subject-select');
- const teacherSubjectsTbody = document.getElementById('teacher-subjects-tbody');
-
- // --- Multi-select logic ---
- function updateSelectText(containerId, textId) {
- const container = document.getElementById(containerId);
- const textEl = document.getElementById(textId);
- if (!container || !textEl) return;
- const checked = Array.from(container.querySelectorAll('input:checked'));
- if (checked.length === 0) {
- textEl.textContent = 'Выберите оборудование...';
- } else if (checked.length === 1) {
- textEl.textContent = checked[0].parentElement.textContent.trim();
- } else {
- textEl.textContent = `Выбрано: ${checked.length}`;
- }
- }
-
- function initMultiSelect(boxId, menuId, textId, checkboxContainerId) {
- const box = document.getElementById(boxId);
- const menu = document.getElementById(menuId);
- const container = document.getElementById(checkboxContainerId);
- if (!box || !menu || !container) return;
-
- box.addEventListener('click', (e) => {
- e.stopPropagation();
- const isOpen = menu.classList.contains('open');
- document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('open'));
- document.querySelectorAll('.select-box').forEach(b => b.classList.remove('active'));
- if (!isOpen) {
- menu.classList.add('open');
- box.classList.add('active');
- }
- });
-
- menu.addEventListener('click', (e) => {
- e.stopPropagation();
- });
-
- container.addEventListener('change', () => {
- updateSelectText(checkboxContainerId, textId);
- });
- }
-
- initMultiSelect('equipment-select-box', 'equipment-dropdown-menu', 'equipment-select-text', 'equipment-checkboxes');
- initMultiSelect('edit-equipment-select-box', 'edit-equipment-dropdown-menu', 'edit-equipment-select-text', 'edit-equipment-checkboxes');
-
- document.addEventListener('click', () => {
- document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('open'));
- document.querySelectorAll('.select-box').forEach(b => b.classList.remove('active'));
- });
- // --------------------------
-
- const navItems = document.querySelectorAll('.nav-item[data-tab]');
- const tabContents = document.querySelectorAll('.tab-content');
-
- // ---- State ----
- let allGroups = [];
- let allEducationForms = [];
- let allEquipments = [];
- let allSubjects = [];
- let allTeachers = [];
-
- // ---- Tab Switching ----
- const TAB_TITLES = {
- users: 'Управление пользователями',
- groups: 'Управление группами',
- 'edu-forms': 'Формы обучения',
- equipments: 'Оборудование',
- classrooms: 'Аудитории',
- subjects: 'Дисциплины и преподаватели'
- };
-
- navItems.forEach(item => {
- item.addEventListener('click', (e) => {
- e.preventDefault();
- switchTab(item.dataset.tab);
- });
- });
-
- function switchTab(tab) {
- navItems.forEach(n => n.classList.remove('active'));
- document.querySelector(`.nav-item[data-tab="${tab}"]`)?.classList.add('active');
-
- tabContents.forEach(tc => tc.style.display = 'none');
- const target = document.getElementById('tab-' + tab);
- if (target) target.style.display = '';
-
- pageTitle.textContent = TAB_TITLES[tab] || '';
-
- if (tab === 'users') loadUsers();
- if (tab === 'groups') { loadEducationForms().then(() => loadGroups()); }
- if (tab === 'edu-forms') loadEducationForms();
- if (tab === 'equipments') loadEquipments();
- if (tab === 'classrooms') { loadEquipments().then(() => loadClassrooms()); }
- if (tab === 'subjects') { Promise.all([loadSubjects(), loadTeachers()]).then(() => loadTeacherSubjects()); }
-
- sidebar.classList.remove('open');
- sidebarOverlay.classList.remove('open');
- }
-
- // ---- Mobile Menu ----
- menuToggle.addEventListener('click', () => {
- sidebar.classList.toggle('open');
- sidebarOverlay.classList.toggle('open');
- });
- sidebarOverlay.addEventListener('click', () => {
- sidebar.classList.remove('open');
- sidebarOverlay.classList.remove('open');
- });
-
- // ---- Helpers ----
- const ROLE_LABELS = { ADMIN: 'Администратор', TEACHER: 'Преподаватель', STUDENT: 'Студент' };
- const ROLE_BADGE = { ADMIN: 'badge-admin', TEACHER: 'badge-teacher', STUDENT: 'badge-student' };
-
- function escapeHtml(str) {
- const div = document.createElement('div');
- div.textContent = str;
- return div.innerHTML;
- }
-
- function showAlert(el, msg, type) {
- el.className = 'form-alert ' + type;
- el.textContent = msg;
- }
-
- function hideAlert(el) {
- el.className = 'form-alert';
- el.textContent = '';
- }
-
- // ============================================================
- // USERS
- // ============================================================
-
- async function loadUsers() {
- try {
- const res = await fetch('/api/users', {
- headers: { 'Authorization': 'Bearer ' + token },
- });
- const users = await res.json();
- renderUsers(users);
- } catch (e) {
- usersTbody.innerHTML = '
| Ошибка загрузки |
';
- }
- }
-
- function renderUsers(users) {
- if (!users.length) {
- usersTbody.innerHTML = '| Нет пользователей |
';
- return;
- }
- usersTbody.innerHTML = users.map(u => `
-
- | ${u.id} |
- ${escapeHtml(u.username)} |
- ${ROLE_LABELS[u.role] || u.role} |
- |
- |
-
`).join('');
- }
-
- createForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- hideAlert(createAlert);
- const username = document.getElementById('new-username').value.trim();
- const password = document.getElementById('new-password').value;
- const role = document.getElementById('new-role').value;
- if (!username || !password) { showAlert(createAlert, 'Заполните все поля', 'error'); return; }
-
- try {
- const res = await fetch('/api/users', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- body: JSON.stringify({ username, password, role }),
- });
- const data = await res.json();
- if (res.ok) {
- showAlert(createAlert, `Пользователь "${data.username}" создан`, 'success');
- createForm.reset();
- loadUsers();
- } else {
- showAlert(createAlert, data.message || 'Ошибка создания', 'error');
- }
- } catch (e) { showAlert(createAlert, 'Ошибка соединения', 'error'); }
- });
-
- usersTbody.addEventListener('click', async (e) => {
- const btn = e.target.closest('.btn-delete');
- if (!btn) return;
- if (!confirm('Удалить пользователя?')) return;
- try {
- const res = await fetch('/api/users/' + btn.dataset.id, {
- method: 'DELETE',
- headers: { 'Authorization': 'Bearer ' + token },
- });
- if (res.ok) loadUsers();
- else alert('Ошибка удаления');
- } catch (e) { alert('Ошибка соединения'); }
- });
-
- // ============================================================
- // EDUCATION FORMS
- // ============================================================
-
- async function loadEducationForms() {
- try {
- const res = await fetch('/api/education-forms', {
- headers: { 'Authorization': 'Bearer ' + token },
- });
- allEducationForms = await res.json();
- renderEfTable(allEducationForms);
- populateEfSelects(allEducationForms);
- } catch (e) {
- efTbody.innerHTML = '| Ошибка загрузки |
';
- }
- }
-
- function renderEfTable(forms) {
- if (!forms.length) {
- efTbody.innerHTML = '| Нет форм обучения |
';
- return;
- }
- efTbody.innerHTML = forms.map(ef => `
-
- | ${ef.id} |
- ${escapeHtml(ef.name)} |
- |
-
`).join('');
- }
-
- function populateEfSelects(forms) {
- // Group creation select
- const currentVal = newGroupEfSelect.value;
- newGroupEfSelect.innerHTML = forms.map(ef =>
- ``
- ).join('');
- if (currentVal && forms.find(f => f.id == currentVal)) {
- newGroupEfSelect.value = currentVal;
- }
-
- // Filter select
- const currentFilter = filterEfSelect.value;
- filterEfSelect.innerHTML = '' +
- forms.map(ef =>
- ``
- ).join('');
- if (currentFilter) filterEfSelect.value = currentFilter;
- }
-
- createEfForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- hideAlert(createEfAlert);
- const name = document.getElementById('new-ef-name').value.trim();
- if (!name) { showAlert(createEfAlert, 'Введите название', 'error'); return; }
-
- try {
- const res = await fetch('/api/education-forms', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- body: JSON.stringify({ name }),
- });
- const data = await res.json();
- if (res.ok) {
- showAlert(createEfAlert, `Форма "${data.name}" создана`, 'success');
- createEfForm.reset();
- loadEducationForms();
- } else {
- showAlert(createEfAlert, data.message || 'Ошибка создания', 'error');
- }
- } catch (e) { showAlert(createEfAlert, 'Ошибка соединения', 'error'); }
- });
-
- efTbody.addEventListener('click', async (e) => {
- const btn = e.target.closest('.btn-delete');
- if (!btn) return;
- if (!confirm('Удалить форму обучения?')) return;
- try {
- const res = await fetch('/api/education-forms/' + btn.dataset.id, {
- method: 'DELETE',
- headers: { 'Authorization': 'Bearer ' + token },
- });
- if (res.ok) {
- loadEducationForms();
- } else {
- const data = await res.json();
- alert(data.message || 'Ошибка удаления');
- }
- } catch (e) { alert('Ошибка соединения'); }
- });
-
- // ============================================================
- // GROUPS
- // ============================================================
-
- async function loadGroups() {
- try {
- const res = await fetch('/api/groups', {
- headers: { 'Authorization': 'Bearer ' + token },
- });
- allGroups = await res.json();
- applyGroupFilter();
- } catch (e) {
- groupsTbody.innerHTML = '| Ошибка загрузки |
';
- }
- }
-
- function applyGroupFilter() {
- const filterId = filterEfSelect.value;
- const filtered = filterId
- ? allGroups.filter(g => g.educationFormId == filterId)
- : allGroups;
- renderGroups(filtered);
- }
-
- filterEfSelect.addEventListener('change', applyGroupFilter);
-
- function renderGroups(groups) {
- if (!groups.length) {
- groupsTbody.innerHTML = '| Нет групп |
';
- return;
- }
- groupsTbody.innerHTML = groups.map(g => `
-
- | ${g.id} |
- ${escapeHtml(g.name)} |
- ${escapeHtml(g.educationFormName)} |
- |
-
`).join('');
- }
-
- createGroupForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- hideAlert(createGroupAlert);
- const name = document.getElementById('new-group-name').value.trim();
- const educationFormId = newGroupEfSelect.value;
- if (!name) { showAlert(createGroupAlert, 'Введите название группы', 'error'); return; }
- if (!educationFormId) { showAlert(createGroupAlert, 'Выберите форму обучения', 'error'); return; }
-
- try {
- const res = await fetch('/api/groups', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- body: JSON.stringify({ name, educationFormId: Number(educationFormId) }),
- });
- const data = await res.json();
- if (res.ok) {
- showAlert(createGroupAlert, `Группа "${data.name}" создана`, 'success');
- createGroupForm.reset();
- loadGroups();
- } else {
- showAlert(createGroupAlert, data.message || 'Ошибка создания', 'error');
- }
- } catch (e) { showAlert(createGroupAlert, 'Ошибка соединения', 'error'); }
- });
-
- groupsTbody.addEventListener('click', async (e) => {
- const btn = e.target.closest('.btn-delete');
- if (!btn) return;
- if (!confirm('Удалить группу?')) return;
- try {
- const res = await fetch('/api/groups/' + btn.dataset.id, {
- method: 'DELETE',
- headers: { 'Authorization': 'Bearer ' + token },
- });
- if (res.ok) loadGroups();
- else alert('Ошибка удаления');
- } catch (e) { alert('Ошибка соединения'); }
- });
-
- // ============================================================
- // EQUIPMENTS
- // ============================================================
-
- async function loadEquipments() {
- try {
- const res = await fetch('/api/equipments', {
- headers: { 'Authorization': 'Bearer ' + token },
- });
- allEquipments = await res.json();
- renderEquipments(allEquipments);
- renderEquipmentCheckboxes(allEquipments);
- } catch (e) {
- if (equipmentsTbody) equipmentsTbody.innerHTML = '| Ошибка загрузки |
';
- if (equipmentCheckboxes) equipmentCheckboxes.innerHTML = 'Ошибка загрузки
';
- }
- }
-
- function renderEquipments(equipments) {
- if (!equipments.length) {
- equipmentsTbody.innerHTML = '| Нет оборудования |
';
- return;
- }
- equipmentsTbody.innerHTML = equipments.map(eq => `
-
- | ${eq.id} |
- ${escapeHtml(eq.name)} |
- |
-
`).join('');
- }
-
- function renderEquipmentCheckboxes(equipments) {
- if (!equipments.length) {
- equipmentCheckboxes.innerHTML = 'Нет доступного оборудования
';
- return;
- }
- equipmentCheckboxes.innerHTML = equipments.map(eq => `
-
- `).join('');
- updateSelectText('equipment-checkboxes', 'equipment-select-text');
- }
-
- createEquipmentForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- hideAlert(createEquipmentAlert);
- const name = document.getElementById('new-equipment-name').value.trim();
- if (!name) { showAlert(createEquipmentAlert, 'Введите название', 'error'); return; }
-
- try {
- const res = await fetch('/api/equipments', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- body: JSON.stringify({ name }),
- });
- const data = await res.json();
- if (res.ok) {
- showAlert(createEquipmentAlert, `Оборудование "${data.name}" добавлено`, 'success');
- createEquipmentForm.reset();
- loadEquipments();
- } else {
- showAlert(createEquipmentAlert, data.message || 'Ошибка создания', 'error');
- }
- } catch (e) { showAlert(createEquipmentAlert, 'Ошибка соединения', 'error'); }
- });
-
- equipmentsTbody.addEventListener('click', async (e) => {
- const btn = e.target.closest('.btn-delete');
- if (!btn) return;
- if (!confirm('Удалить оборудование?')) return;
- try {
- const res = await fetch('/api/equipments/' + btn.dataset.id, {
- method: 'DELETE',
- headers: { 'Authorization': 'Bearer ' + token },
- });
- if (res.ok) {
- loadEquipments();
- } else {
- const data = await res.json();
- alert(data.message || 'Ошибка удаления');
- }
- } catch (e) { alert('Ошибка соединения'); }
- });
-
- // ============================================================
- // CLASSROOMS
- // ============================================================
-
- async function loadClassrooms() {
- try {
- const res = await fetch('/api/classrooms', {
- headers: { 'Authorization': 'Bearer ' + token },
- });
- const classrooms = await res.json();
- renderClassrooms(classrooms);
- } catch (e) {
- classroomsTbody.innerHTML = '| Ошибка загрузки |
';
- }
- }
-
- function renderClassrooms(classrooms) {
- if (!classrooms.length) {
- classroomsTbody.innerHTML = '| Нет аудиторий |
';
- return;
- }
- classroomsTbody.innerHTML = classrooms.map(c => {
- const equipHtml = c.equipments && c.equipments.length
- ? c.equipments.map(eq => escapeHtml(eq.name)).join(', ')
- : '—';
-
- return `
-
- | ${c.id} |
- ${escapeHtml(c.name)} |
- ${c.capacity} чел. |
- ${equipHtml} |
-
-
-
- ${c.isAvailable ? 'Доступна' : 'Не доступна'}
-
-
-
- |
-
-
-
- |
-
`;
- }).join('');
- }
-
- createClassroomForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- hideAlert(createClassroomAlert);
- const name = document.getElementById('new-classroom-name').value.trim();
- const capacity = parseInt(document.getElementById('new-classroom-capacity').value, 10);
-
- const checkedBoxes = Array.from(equipmentCheckboxes.querySelectorAll('input:checked'));
- const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10));
-
- if (!name || isNaN(capacity)) { showAlert(createClassroomAlert, 'Заполните обязательные поля', 'error'); return; }
-
- try {
- const res = await fetch('/api/classrooms', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- body: JSON.stringify({ name, capacity, equipmentIds, isAvailable: true }),
- });
- const data = await res.json();
- if (res.ok) {
- showAlert(createClassroomAlert, `Аудитория "${data.name}" добавлена`, 'success');
- createClassroomForm.reset();
- updateSelectText('equipment-checkboxes', 'equipment-select-text');
- loadClassrooms();
- } else {
- showAlert(createClassroomAlert, data.message || 'Ошибка создания', 'error');
- }
- } catch (e) { showAlert(createClassroomAlert, 'Ошибка соединения', 'error'); }
- });
-
- classroomsTbody.addEventListener('click', async (e) => {
- const btnDelete = e.target.closest('.btn-delete');
- const btnToggleStatus = e.target.closest('.btn-icon-toggle');
- const btnEdit = e.target.closest('.btn-edit-classroom');
-
- if (btnDelete) {
- if (!confirm('Удалить аудиторию?')) return;
- try {
- const res = await fetch('/api/classrooms/' + btnDelete.dataset.id, {
- method: 'DELETE',
- headers: { 'Authorization': 'Bearer ' + token },
- });
- if (res.ok) loadClassrooms();
- else alert('Ошибка удаления');
- } catch (err) { alert('Ошибка соединения'); }
- }
-
- if (btnToggleStatus) {
- const id = btnToggleStatus.dataset.id;
- const currentStatus = btnToggleStatus.dataset.currentStatus === 'true';
- try {
- const res = await fetch('/api/classrooms/' + id, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- body: JSON.stringify({ isAvailable: !currentStatus }),
- });
- if (res.ok) loadClassrooms();
- else alert('Ошибка изменения статуса');
- } catch (err) { alert('Ошибка соединения'); }
- }
-
- if (btnEdit) {
- const id = btnEdit.dataset.id;
- openEditClassroomModal(id);
- }
- });
-
- let editingClassroomData = null;
-
- async function openEditClassroomModal(id) {
- try {
- const res = await fetch('/api/classrooms', { headers: { 'Authorization': 'Bearer ' + token } });
- const classrooms = await res.json();
- editingClassroomData = classrooms.find(c => c.id == id);
-
- if (!editingClassroomData) return;
-
- document.getElementById('edit-classroom-id').value = editingClassroomData.id;
- document.getElementById('edit-classroom-name').value = editingClassroomData.name;
- document.getElementById('edit-classroom-capacity').value = editingClassroomData.capacity;
-
- if (allEquipments.length) {
- editEquipmentCheckboxes.innerHTML = allEquipments.map(eq => {
- const isChecked = editingClassroomData.equipments.some(e => e.id === eq.id) ? 'checked' : '';
- return `
-
- `;
- }).join('');
- } else {
- editEquipmentCheckboxes.innerHTML = 'Нет доступного оборудования
';
- }
- updateSelectText('edit-equipment-checkboxes', 'edit-equipment-select-text');
-
- hideAlert(editClassroomAlert);
- modalEditClassroom.classList.add('open');
- } catch (e) {
- alert('Ошибка загрузки данных аудитории');
- }
- }
-
- modalEditClassroomClose.addEventListener('click', () => {
- modalEditClassroom.classList.remove('open');
- });
-
- modalEditClassroom.addEventListener('click', (e) => {
- if (e.target === modalEditClassroom) {
- modalEditClassroom.classList.remove('open');
- }
- });
-
- editClassroomForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- hideAlert(editClassroomAlert);
- const id = document.getElementById('edit-classroom-id').value;
- const name = document.getElementById('edit-classroom-name').value.trim();
- const capacity = parseInt(document.getElementById('edit-classroom-capacity').value, 10);
-
- const checkedBoxes = Array.from(editEquipmentCheckboxes.querySelectorAll('input:checked'));
- const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10));
-
- if (!name || isNaN(capacity)) { showAlert(editClassroomAlert, 'Заполните обязательные поля', 'error'); return; }
-
- try {
- const res = await fetch('/api/classrooms/' + id, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- body: JSON.stringify({ name, capacity, equipmentIds, isAvailable: editingClassroomData.isAvailable }),
- });
- const data = await res.json();
- if (res.ok) {
- modalEditClassroom.classList.remove('open');
- showAlert(createClassroomAlert, `Аудитория "${data.name}" обновлена`, 'success');
- loadClassrooms();
- } else {
- showAlert(editClassroomAlert, data.message || 'Ошибка обновления', 'error');
- }
- } catch (e) { showAlert(editClassroomAlert, 'Ошибка соединения', 'error'); }
- });
-
- // ============================================================
- // SUBJECTS
- // ============================================================
-
- async function loadSubjects() {
- try {
- const res = await fetch('/api/subjects', {
- headers: { 'Authorization': 'Bearer ' + token },
- });
- allSubjects = await res.json();
- renderSubjects(allSubjects);
- populateSubjectSelect(allSubjects);
- } catch (e) {
- if (subjectsTbody) subjectsTbody.innerHTML = '| Ошибка загрузки |
';
- }
- }
-
- function renderSubjects(subjects) {
- if (!subjects.length) {
- subjectsTbody.innerHTML = '| Нет дисциплин |
';
- return;
- }
- subjectsTbody.innerHTML = subjects.map(s => `
-
- | ${s.id} |
- ${escapeHtml(s.name)} |
- |
-
`).join('');
- }
-
- function populateSubjectSelect(subjects) {
- if (!assignSubjectSelect) return;
- const currentVal = assignSubjectSelect.value;
- assignSubjectSelect.innerHTML = '' +
- subjects.map(s => ``).join('');
- if (currentVal && subjects.find(s => s.id == currentVal)) {
- assignSubjectSelect.value = currentVal;
- }
- }
-
- async function loadTeachers() {
- try {
- const res = await fetch('/api/users/teachers', {
- headers: { 'Authorization': 'Bearer ' + token },
- });
- allTeachers = await res.json();
- populateTeacherSelect(allTeachers);
- } catch (e) {
- if (assignTeacherSelect) assignTeacherSelect.innerHTML = '';
- }
- }
-
- function populateTeacherSelect(teachers) {
- if (!assignTeacherSelect) return;
- const currentVal = assignTeacherSelect.value;
- if (!teachers.length) {
- assignTeacherSelect.innerHTML = '';
- return;
- }
- assignTeacherSelect.innerHTML = '' +
- teachers.map(t => ``).join('');
- if (currentVal && teachers.find(t => t.id == currentVal)) {
- assignTeacherSelect.value = currentVal;
- }
- }
-
- async function loadTeacherSubjects() {
- try {
- const res = await fetch('/api/teacher-subjects', {
- headers: { 'Authorization': 'Bearer ' + token },
- });
- const tsData = await res.json();
- renderTeacherSubjects(tsData);
- } catch (e) {
- if (teacherSubjectsTbody) teacherSubjectsTbody.innerHTML = '| Ошибка загрузки |
';
- }
- }
-
- function renderTeacherSubjects(tsArray) {
- if (!tsArray.length) {
- teacherSubjectsTbody.innerHTML = '| Нет привязок |
';
- return;
- }
- teacherSubjectsTbody.innerHTML = tsArray.map(ts => `
-
- | ${escapeHtml(ts.username)} |
- ${escapeHtml(ts.subjectName)} |
- |
-
`).join('');
- }
-
- createSubjectForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- hideAlert(createSubjectAlert);
- const name = document.getElementById('new-subject-name').value.trim();
- if (!name) { showAlert(createSubjectAlert, 'Введите название', 'error'); return; }
-
- try {
- const res = await fetch('/api/subjects', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- body: JSON.stringify({ name }),
- });
- const data = await res.json();
- if (res.ok) {
- showAlert(createSubjectAlert, `Дисциплина "${data.name}" добавлена`, 'success');
- createSubjectForm.reset();
- loadSubjects();
- } else {
- showAlert(createSubjectAlert, data.message || 'Ошибка создания', 'error');
- }
- } catch (e) { showAlert(createSubjectAlert, 'Ошибка соединения', 'error'); }
- });
-
- subjectsTbody.addEventListener('click', async (e) => {
- const btn = e.target.closest('.btn-delete');
- if (!btn) return;
- if (!confirm('Удалить дисциплину?')) return;
- try {
- const res = await fetch('/api/subjects/' + btn.dataset.id, {
- method: 'DELETE',
- headers: { 'Authorization': 'Bearer ' + token },
- });
- if (res.ok) {
- loadSubjects();
- loadTeacherSubjects();
- } else {
- const data = await res.json();
- alert(data.message || 'Ошибка удаления');
- }
- } catch (e) { alert('Ошибка соединения'); }
- });
-
- assignTeacherForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- hideAlert(assignTeacherAlert);
- const userId = assignTeacherSelect.value;
- const subjectId = assignSubjectSelect.value;
- if (!userId) { showAlert(assignTeacherAlert, 'Выберите преподавателя', 'error'); return; }
- if (!subjectId) { showAlert(assignTeacherAlert, 'Выберите дисциплину', 'error'); return; }
-
- try {
- const res = await fetch('/api/teacher-subjects', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- body: JSON.stringify({ userId: Number(userId), subjectId: Number(subjectId) }),
- });
- const data = await res.json();
- if (res.ok) {
- showAlert(assignTeacherAlert, 'Привязка создана', 'success');
- loadTeacherSubjects();
- } else {
- showAlert(assignTeacherAlert, data.message || 'Ошибка привязки', 'error');
- }
- } catch (e) { showAlert(assignTeacherAlert, 'Ошибка соединения', 'error'); }
- });
-
- teacherSubjectsTbody.addEventListener('click', async (e) => {
- const btn = e.target.closest('.btn-delete');
- if (!btn) return;
- if (!confirm('Удалить привязку?')) return;
- try {
- const res = await fetch('/api/teacher-subjects', {
- method: 'DELETE',
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- body: JSON.stringify({ userId: Number(btn.dataset.userId), subjectId: Number(btn.dataset.subjectId) }),
- });
- if (res.ok) loadTeacherSubjects();
- else alert('Ошибка удаления');
- } catch (e) { alert('Ошибка соединения'); }
- });
-
- // ============================================================
- // LOGOUT & INIT
- // ============================================================
-
- btnLogout.addEventListener('click', () => {
- localStorage.removeItem('token');
- localStorage.removeItem('role');
- window.location.href = '/';
- });
-
- loadUsers();
-})();
diff --git a/frontend/admin/css/components.css b/frontend/admin/css/components.css
new file mode 100644
index 0000000..d03c9d2
--- /dev/null
+++ b/frontend/admin/css/components.css
@@ -0,0 +1,586 @@
+/* ===== Cards ===== */
+.card {
+ background: var(--bg-card);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid var(--bg-card-border);
+ border-radius: var(--radius-md);
+ padding: 1.75rem;
+ position: relative;
+ overflow: visible;
+ transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
+ animation: slideUpCard 0.5s cubic-bezier(0.25, 0.8, 0.25, 1) both;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
+}
+
+.card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
+ opacity: 0;
+ transition: opacity var(--transition);
+}
+
+.card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.1);
+ border-color: rgba(255, 255, 255, 0.12);
+}
+
+.card:hover::before {
+ opacity: 1;
+}
+
+.create-card {
+ z-index: 10;
+}
+
+.card h2 {
+ font-size: 0.8rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+/* ===== Form Structure ===== */
+.form-row {
+ display: flex;
+ gap: 1rem;
+ align-items: flex-end;
+ flex-wrap: wrap;
+}
+
+.form-group {
+ flex: 1;
+ min-width: 160px;
+}
+
+.form-group label {
+ display: block;
+ font-size: 0.78rem;
+ font-weight: 500;
+ color: var(--text-secondary);
+ margin-bottom: 0.4rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.form-group input,
+.form-group select {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ background: var(--bg-input);
+ border: 1px solid var(--bg-card-border);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-family: inherit;
+ font-size: 0.95rem;
+ outline: none;
+ transition: all var(--transition);
+}
+
+.form-group input::placeholder {
+ color: var(--text-placeholder);
+ transition: opacity var(--transition);
+}
+
+.form-group input:focus,
+.form-group select:focus {
+ background: var(--bg-input-focus);
+ border-color: var(--accent);
+ box-shadow: 0 0 0 4px var(--accent-glow);
+ transform: translateY(-1px);
+}
+
+.form-group input:focus::placeholder {
+ opacity: 0.5;
+}
+
+/* Hide Number Arrows */
+input[type="number"]::-webkit-outer-spin-button,
+input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+input[type="number"] {
+ -moz-appearance: textfield;
+}
+
+/* Select Base Style */
+.form-group select,
+.filter-row select {
+ cursor: pointer;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ padding-right: 2.25rem;
+}
+
+.form-group select option,
+.filter-row select option {
+ background: #1a1a2e;
+ color: var(--text-primary);
+}
+
+/* Light theme selects */
+[data-theme="light"] .form-group input,
+[data-theme="light"] .form-group select,
+[data-theme="light"] .filter-row select {
+ border-color: rgba(0, 0, 0, 0.15);
+}
+
+[data-theme="light"] .form-group select option,
+[data-theme="light"] .filter-row select option {
+ background: #fff;
+ color: #1a1a2e;
+}
+
+/* Filter Row */
+.card-header-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+}
+
+.card-header-row h2 {
+ margin-bottom: 0;
+}
+
+.filter-row {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.filter-row label {
+ font-size: 0.78rem;
+ font-weight: 500;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ white-space: nowrap;
+}
+
+.filter-row select {
+ padding: 0.45rem 2rem 0.45rem 0.7rem;
+ background: var(--bg-input);
+ border: 1px solid transparent;
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-size: 0.85rem;
+ transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
+}
+
+.filter-row select:focus {
+ background-color: var(--bg-input-focus);
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-glow);
+}
+
+
+/* ===== Custom Multi-Select ===== */
+.custom-multi-select {
+ position: relative;
+ user-select: none;
+ width: 100%;
+}
+
+.select-box {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 0.75rem 1rem;
+ background: var(--bg-input);
+ border: 1px solid var(--bg-card-border);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-size: 0.95rem;
+ cursor: pointer;
+ transition: all var(--transition);
+}
+
+.select-box:hover {
+ background: var(--bg-hover);
+}
+
+.select-box.active {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 4px var(--accent-glow);
+}
+
+.dropdown-icon {
+ transition: transform var(--transition);
+}
+
+.select-box.active .dropdown-icon {
+ transform: rotate(180deg);
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ width: 100%;
+ margin-top: 0.5rem;
+ background: rgba(15, 23, 42, 0.95);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border: 1px solid var(--bg-card-border);
+ border-radius: var(--radius-sm);
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
+ padding: 1rem;
+ z-index: 100;
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(-10px);
+ transition: all var(--transition);
+}
+
+[data-theme="light"] .custom-multi-select .dropdown-menu {
+ background: rgba(255, 255, 255, 0.98);
+}
+
+.dropdown-menu.open {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+
+.checkbox-group-vertical {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ max-height: 200px;
+ overflow-y: auto;
+}
+
+.checkbox-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ cursor: pointer;
+ font-size: 0.9rem;
+ color: var(--text-primary);
+ padding: 0.25rem 0;
+}
+
+.checkbox-item input[type="checkbox"] {
+ cursor: pointer;
+ width: 1.1rem;
+ height: 1.1rem;
+ accent-color: var(--accent);
+}
+
+/* ===== Buttons ===== */
+.btn-primary {
+ position: relative;
+ overflow: hidden;
+ padding: 0.75rem 1.75rem;
+ background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
+ border: none;
+ border-radius: var(--radius-sm);
+ color: #fff;
+ font-family: inherit;
+ font-size: 0.95rem;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: all var(--transition);
+ box-shadow: 0 4px 15px var(--accent-glow);
+}
+
+.btn-primary::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(rgba(255, 255, 255, 0.2), transparent);
+ border-radius: inherit;
+ opacity: 0;
+ transition: opacity var(--transition);
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px var(--accent-glow);
+}
+
+.btn-primary:hover::before {
+ opacity: 1;
+}
+
+.btn-primary:active {
+ transform: translateY(1px);
+ box-shadow: 0 2px 10px var(--accent-glow);
+}
+
+.btn-delete {
+ padding: 0.35rem 0.7rem;
+ background: rgba(248, 113, 113, 0.1);
+ border: 1px solid rgba(248, 113, 113, 0.2);
+ border-radius: var(--radius-sm);
+ color: var(--error);
+ font-family: inherit;
+ font-size: 0.8rem;
+ cursor: pointer;
+ transition: background var(--transition), transform var(--transition);
+ position: relative;
+ overflow: hidden;
+}
+
+.btn-delete:hover {
+ background: rgba(248, 113, 113, 0.2);
+ transform: scale(1.05);
+}
+
+.btn-icon-toggle {
+ background: transparent;
+ border: 1px solid var(--bg-card-border);
+ color: var(--text-secondary);
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ padding: 0;
+}
+
+.btn-icon-toggle:hover {
+ background: var(--bg-card);
+ border-color: var(--accent);
+ color: var(--accent);
+ transform: rotate(45deg);
+ box-shadow: 0 0 10px var(--accent-glow);
+}
+
+.btn-edit-classroom {
+ padding: 0.35rem 0.7rem;
+ background: rgba(99, 102, 241, 0.1);
+ border: 1px solid rgba(99, 102, 241, 0.2);
+ border-radius: var(--radius-sm);
+ color: var(--accent-hover);
+ cursor: pointer;
+ transition: all var(--transition);
+}
+
+.btn-edit-classroom:hover {
+ background: rgba(99, 102, 241, 0.2);
+ transform: translateY(-1px);
+}
+
+/* ===== Alerts ===== */
+.form-alert {
+ display: none;
+ padding: 0.6rem 1rem;
+ border-radius: var(--radius-sm);
+ font-size: 0.85rem;
+ margin-top: 0.75rem;
+}
+
+.form-alert.error {
+ display: block;
+ background: rgba(248, 113, 113, 0.1);
+ border: 1px solid rgba(248, 113, 113, 0.2);
+ color: var(--error);
+ animation: slideDownAlert 0.3s ease-out both;
+}
+
+.form-alert.success {
+ display: block;
+ background: rgba(52, 211, 153, 0.1);
+ border: 1px solid rgba(52, 211, 153, 0.2);
+ color: var(--success);
+ animation: slideDownAlert 0.3s ease-out both;
+}
+
+/* ===== Table ===== */
+.table-wrap {
+ overflow-x: auto;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+thead th {
+ text-align: left;
+ font-size: 0.78rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ padding: 0.6rem 0.8rem;
+ border-bottom: 1px solid var(--bg-card-border);
+}
+
+tbody td {
+ padding: 0.85rem 1rem;
+ font-size: 0.95rem;
+ border-bottom: 1px solid var(--bg-card-border);
+ transition: background var(--transition);
+}
+
+[data-theme="light"] tbody td {
+ border-bottom-color: rgba(0, 0, 0, 0.08);
+}
+
+tbody tr {
+ transition: background var(--transition);
+ animation: slideInRow 0.3s ease-out both;
+}
+
+/* Animation delays */
+tbody tr:nth-child(1) {
+ animation-delay: 0.05s;
+}
+
+tbody tr:nth-child(2) {
+ animation-delay: 0.1s;
+}
+
+tbody tr:nth-child(3) {
+ animation-delay: 0.15s;
+}
+
+tbody tr:nth-child(4) {
+ animation-delay: 0.2s;
+}
+
+tbody tr:nth-child(5) {
+ animation-delay: 0.25s;
+}
+
+tbody tr:nth-child(n+6) {
+ animation-delay: 0.3s;
+}
+
+tbody tr:hover {
+ background: var(--bg-hover);
+}
+
+.loading-row {
+ text-align: center;
+ color: var(--text-secondary);
+ padding: 2rem !important;
+}
+
+/* ===== Badges ===== */
+.badge {
+ display: inline-block;
+ padding: 0.2rem 0.6rem;
+ border-radius: 999px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+}
+
+.badge-admin {
+ background: rgba(248, 113, 113, 0.15);
+ color: var(--error);
+}
+
+.badge-teacher {
+ background: rgba(251, 191, 36, 0.15);
+ color: var(--warning);
+}
+
+.badge-student {
+ background: rgba(52, 211, 153, 0.15);
+ color: var(--success);
+}
+
+.badge-ef {
+ background: rgba(99, 102, 241, 0.15);
+ color: var(--accent-hover);
+}
+
+/* Classroom Status */
+.badge-available {
+ background: rgba(16, 185, 129, 0.15);
+ color: var(--success);
+}
+
+.badge-unavailable {
+ background: rgba(248, 113, 113, 0.15);
+ color: var(--error);
+}
+
+.status-cell {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+/* ===== Modal ===== */
+.modal-overlay {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(4px);
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transition: opacity var(--transition);
+}
+
+.modal-overlay.open {
+ display: flex;
+ opacity: 1;
+}
+
+.modal-content {
+ background: var(--bg-primary);
+ border: 1px solid var(--bg-card-border);
+ border-radius: var(--radius-md);
+ padding: 2rem;
+ width: 90%;
+ max-width: 500px;
+ position: relative;
+ transform: scale(0.95);
+ transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
+}
+
+.modal-overlay.open .modal-content {
+ transform: scale(1);
+}
+
+.modal-close {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: color var(--transition);
+}
+
+.modal-close:hover {
+ color: var(--error);
+}
\ No newline at end of file
diff --git a/frontend/admin/css/layout.css b/frontend/admin/css/layout.css
new file mode 100644
index 0000000..d6386ac
--- /dev/null
+++ b/frontend/admin/css/layout.css
@@ -0,0 +1,216 @@
+/* ===== Sidebar ===== */
+.sidebar {
+ width: 260px;
+ min-height: 100vh;
+ background: var(--bg-sidebar);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border-right: 1px solid var(--bg-card-border);
+ display: flex;
+ flex-direction: column;
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ z-index: 10;
+ transition: background 0.4s ease, border-color 0.4s ease, transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
+}
+
+.sidebar-header {
+ padding: 1.25rem;
+ border-bottom: 1px solid var(--bg-card-border);
+}
+
+.logo {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ font-size: 1.15rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+}
+
+.sidebar-nav {
+ flex: 1;
+ padding: 0.75rem;
+}
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ margin-bottom: 0.25rem;
+ border-radius: var(--radius-sm);
+ color: var(--text-secondary);
+ text-decoration: none;
+ font-size: 0.95rem;
+ font-weight: 500;
+ transition: all var(--transition);
+ position: relative;
+ overflow: hidden;
+}
+
+.nav-item::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background: var(--accent);
+ border-radius: 0 4px 4px 0;
+ transform: scaleY(0);
+ transition: transform var(--transition);
+ opacity: 0;
+}
+
+.nav-item:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ transform: translateX(4px);
+}
+
+.nav-item.active {
+ background: rgba(139, 92, 246, 0.12);
+ color: var(--accent-hover);
+}
+
+[data-theme="light"] .nav-item.active {
+ background: rgba(99, 102, 241, 0.18);
+}
+
+.nav-item.active::before {
+ transform: scaleY(1);
+ opacity: 1;
+}
+
+.nav-item svg {
+ transition: transform var(--transition);
+}
+
+.nav-item:hover svg,
+.nav-item.active svg {
+ transform: scale(1.15) rotate(-5deg);
+}
+
+.sidebar-footer {
+ padding: 0.75rem;
+ border-top: 1px solid var(--bg-card-border);
+}
+
+.btn-logout {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ padding: 0.65rem 0.8rem;
+ border: none;
+ border-radius: var(--radius-sm);
+ background: none;
+ color: var(--text-secondary);
+ font-family: inherit;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: background var(--transition), color var(--transition);
+ position: relative;
+}
+
+.btn-logout:hover {
+ background: rgba(248, 113, 113, 0.1);
+ color: var(--error);
+}
+
+/* ===== Main ===== */
+.main {
+ flex: 1;
+ margin-left: 260px;
+ min-height: 100vh;
+}
+
+.topbar {
+ padding: 1.5rem 2rem;
+ border-bottom: 1px solid var(--bg-card-border);
+ transition: border-color 0.4s ease;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.topbar h1 {
+ font-size: 1.3rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ flex: 1;
+}
+
+.content {
+ padding: 1.5rem 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ animation: fadeIn 0.2s ease;
+}
+
+/* ===== Mobile Menu Toggle ===== */
+.menu-toggle {
+ display: none;
+ padding: 0.4rem;
+ background: none;
+ border: none;
+ color: var(--text-primary);
+ cursor: pointer;
+ border-radius: var(--radius-sm);
+ transition: background var(--transition);
+}
+
+.menu-toggle:hover {
+ background: var(--bg-hover);
+}
+
+.sidebar-overlay {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(2px);
+ z-index: 9;
+ opacity: 0;
+ transition: opacity var(--transition);
+}
+
+/* ===== Responsive Mobile ===== */
+@media (max-width: 768px) {
+ .sidebar {
+ transform: translateX(-100%);
+ }
+
+ .sidebar.open {
+ transform: translateX(0);
+ }
+
+ .main {
+ margin-left: 0;
+ }
+
+ .topbar {
+ padding: 1rem 1.25rem;
+ }
+
+ .content {
+ padding: 1.25rem;
+ }
+
+ .menu-toggle,
+ .sidebar-overlay {
+ display: block;
+ }
+
+ .sidebar-overlay.open {
+ opacity: 1;
+ }
+}
\ No newline at end of file
diff --git a/frontend/admin/css/main.css b/frontend/admin/css/main.css
new file mode 100644
index 0000000..a03d06e
--- /dev/null
+++ b/frontend/admin/css/main.css
@@ -0,0 +1,110 @@
+/* ===== Reset & Base ===== */
+*,
+*::before,
+*::after {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ /* Deep dark premium background */
+ --bg-primary: #0a0a0f;
+ --bg-sidebar: rgba(255, 255, 255, 0.02);
+ --bg-card: rgba(255, 255, 255, 0.03);
+ --bg-card-border: rgba(255, 255, 255, 0.05);
+ --bg-input: rgba(255, 255, 255, 0.04);
+ --bg-input-focus: rgba(255, 255, 255, 0.08);
+ --bg-hover: rgba(255, 255, 255, 0.06);
+
+ /* Typography */
+ --text-primary: #f8fafc;
+ --text-secondary: #94a3b8;
+ --text-placeholder: #475569;
+
+ /* Vibrant Accents */
+ --accent: #8b5cf6;
+ --accent-hover: #a78bfa;
+ --accent-glow: rgba(139, 92, 246, 0.4);
+ --accent-secondary: #ec4899;
+
+ /* Status Colors */
+ --error: #ef4444;
+ --success: #10b981;
+ --warning: #f59e0b;
+
+ /* Spatial */
+ --radius-sm: 10px;
+ --radius-md: 16px;
+ --transition: 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+}
+
+/* ===== Light Theme ===== */
+[data-theme="light"] {
+ --bg-primary: #f8fafc;
+ --bg-sidebar: rgba(255, 255, 255, 0.7);
+ --bg-card: rgba(255, 255, 255, 0.75);
+ --bg-card-border: rgba(0, 0, 0, 0.08);
+ --bg-input: rgba(0, 0, 0, 0.03);
+ --bg-input-focus: rgba(0, 0, 0, 0.06);
+ --bg-hover: rgba(0, 0, 0, 0.05);
+ --text-primary: #0f172a;
+ --text-secondary: #475569;
+ --text-placeholder: #94a3b8;
+ --accent: #6366f1;
+ --accent-hover: #4f46e5;
+ --accent-glow: rgba(99, 102, 241, 0.3);
+ --accent-secondary: #d946ef;
+
+ --error: #ef4444;
+ --success: #10b981;
+ --warning: #f59e0b;
+}
+
+html {
+ font-size: 16px;
+ -webkit-font-smoothing: antialiased;
+}
+
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ min-height: 100vh;
+ display: flex;
+ transition: background 0.4s ease, color 0.4s ease;
+}
+
+/* ===== Animations ===== */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(4px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes slideUpCard {
+ from { opacity: 0; transform: translateY(15px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes slideDownAlert {
+ from { opacity: 0; transform: translateY(-10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes slideInRow {
+ from { opacity: 0; transform: translateX(-10px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+
+@keyframes admin-ripple {
+ to { transform: scale(4); opacity: 0; }
+}
+
+.ripple {
+ position: absolute;
+ border-radius: 50%;
+ transform: scale(0);
+ animation: admin-ripple 0.6s linear;
+ background-color: rgba(255, 255, 255, 0.3);
+ pointer-events: none;
+}
diff --git a/frontend/admin/index.html b/frontend/admin/index.html
index c8fa44a..ad7ac2f 100644
--- a/frontend/admin/index.html
+++ b/frontend/admin/index.html
@@ -8,7 +8,11 @@
-
+
+
+
+
+
@@ -31,7 +35,7 @@