diff options
Diffstat (limited to 'frontend/src/routes/(admin)/admin/+page.svelte')
| -rw-r--r-- | frontend/src/routes/(admin)/admin/+page.svelte | 309 |
1 files changed, 286 insertions, 23 deletions
diff --git a/frontend/src/routes/(admin)/admin/+page.svelte b/frontend/src/routes/(admin)/admin/+page.svelte index 24b56db..5be9154 100644 --- a/frontend/src/routes/(admin)/admin/+page.svelte +++ b/frontend/src/routes/(admin)/admin/+page.svelte @@ -13,6 +13,7 @@ token = data.token; services = data.services ?? []; initialized = true; + fetchMaintenances(); } }); let editingSvc: any = $state(null); @@ -21,10 +22,25 @@ let testing = $state<Record<number, boolean>>({}); let toggling = $state<Record<number, boolean>>({}); + // maintenance state + let maintenances = $state<any[]>([]); + let showMtnForm = $state(false); + let editingMtn: any = $state(null); + let mtnSaving = $state(false); + let mtnFormError = $state(''); + function apiHeaders() { return { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }; } + async function fetchMaintenances() { + try { + const res = await fetch(`${API}/maintenances`, { headers: { Authorization: `Bearer ${token}` } }); + if (await handleUnauthorized(res)) return; + if (res.ok) maintenances = await res.json(); + } catch {} + } + async function handleUnauthorized(res: Response) { if (res.status === 401) { window.location.href = '/login'; @@ -144,6 +160,75 @@ toggling = { ...toggling, [id]: false }; } + function openMtnCreate() { + editingMtn = null; + mtnFormError = ''; + showMtnForm = true; + } + + function openMtnEdit(m: any) { + editingMtn = { ...m }; + mtnFormError = ''; + showMtnForm = true; + } + + function closeMtnForm() { + showMtnForm = false; + editingMtn = null; + mtnFormError = ''; + } + + async function handleMtnSubmit(e: Event) { + e.preventDefault(); + mtnSaving = true; + mtnFormError = ''; + const form = e.target as HTMLFormElement; + const fd = new FormData(form); + const body: Record<string, any> = { + title: fd.get('title'), + start_time: fd.get('start_time'), + end_time: fd.get('end_time'), + is_active: fd.get('is_active') === 'on', + }; + try { + const id = fd.get('id'); + if (id) { + const res = await fetch(`${API}/maintenances/${id}`, { + method: 'PUT', + headers: apiHeaders(), + body: JSON.stringify(body), + }); + if (await handleUnauthorized(res)) return; + if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao salvar'); + } else { + const res = await fetch(`${API}/maintenances`, { + method: 'POST', + headers: apiHeaders(), + body: JSON.stringify({ title: body.title, start_time: body.start_time, end_time: body.end_time }), + }); + if (await handleUnauthorized(res)) return; + if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao salvar'); + } + await fetchMaintenances(); + closeMtnForm(); + } catch (e: any) { + mtnFormError = e.message; + } finally { + mtnSaving = false; + } + } + + async function handleMtnDelete(id: number) { + try { + const res = await fetch(`${API}/maintenances/${id}`, { + method: 'DELETE', + headers: apiHeaders() + }); + if (await handleUnauthorized(res)) return; + if (res.ok) await fetchMaintenances(); + } catch {} + } + async function handleTest(id: number) { testing = { ...testing, [id]: true }; try { @@ -274,7 +359,78 @@ {/each} {/if} -<!-- Modal create / edit --> +<div class="mb-8 mt-12 border-t border-[var(--border-color)] pt-8"> + <div class="mb-6 flex items-center justify-between"> + <div> + <h2 class="text-sm font-semibold uppercase tracking-wider text-[var(--text-secondary)]">Manutenções Programadas</h2> + <p class="mt-0.5 text-xs text-[var(--text-muted)]">Janelas de manutenção exibem um aviso no topo do site</p> + </div> + <button + onclick={openMtnCreate} + class="flex items-center gap-2 rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] px-4 py-2 text-sm font-medium text-[var(--text-primary)] transition-all hover:border-[var(--green)]/50 hover:text-[var(--green)]" + > + <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> + <path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" /> + </svg> + Nova Manutenção + </button> + </div> + + {#if maintenances.length === 0} + <div class="rounded-xl border border-dashed border-[var(--border-color)] p-8 text-center"> + <p class="text-xs text-[var(--text-muted)]">Nenhuma manutenção agendada</p> + </div> + {:else} + <div class="overflow-hidden rounded-xl border border-[var(--border-color)]"> + <table class="w-full text-left text-sm"> + <thead> + <tr class="border-b border-[var(--border-color)] bg-[var(--bg-secondary)]/30"> + <th class="px-4 py-3 text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)]">Título</th> + <th class="px-4 py-3 text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)]">Início</th> + <th class="px-4 py-3 text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)]">Fim</th> + <th class="px-4 py-3 text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)]">Ativa</th> + <th class="px-4 py-3 text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)]">Ações</th> + </tr> + </thead> + <tbody> + {#each maintenances as mtn (mtn.id)} + <tr class="border-b border-[var(--border-color)] transition-colors hover:bg-[var(--bg-secondary)]/20"> + <td class="px-4 py-3 font-medium text-white">{mtn.title}</td> + <td class="px-4 py-3 font-mono text-xs text-[var(--text-muted)]">{new Date(mtn.start_time).toLocaleString('pt-BR')}</td> + <td class="px-4 py-3 font-mono text-xs text-[var(--text-muted)]">{new Date(mtn.end_time).toLocaleString('pt-BR')}</td> + <td class="px-4 py-3"> + <span + class="inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[10px] font-semibold" + style="background-color: {mtn.is_active ? 'rgba(34,240,106,0.1)' : 'rgba(255,64,96,0.1)'}; color: {mtn.is_active ? 'var(--green)' : 'var(--red)'}" + > + {mtn.is_active ? 'Sim' : 'Não'} + </span> + </td> + <td class="px-4 py-3"> + <div class="flex gap-1.5"> + <button + onclick={() => openMtnEdit(mtn)} + class="rounded-lg px-2.5 py-1 text-[10px] font-medium text-[var(--text-secondary)] transition-colors hover:bg-[var(--border-color)] hover:text-white" + > + Editar + </button> + <button + onclick={() => handleMtnDelete(mtn.id)} + class="rounded-lg px-2.5 py-1 text-[10px] font-medium text-[var(--red)] transition-colors hover:bg-[var(--red)]/10" + > + Excluir + </button> + </div> + </td> + </tr> + {/each} + </tbody> + </table> + </div> + {/if} +</div> + +<!-- Modal create / edit service --> {#if showForm} <!-- svelte-ignore a11y_click_events_have_key_events --> <div @@ -323,30 +479,30 @@ <div> <label for="admin-url" class="mb-1.5 block text-xs font-medium text-[var(--text-secondary)]">URL</label> - <input - id="admin-url" - name="url" - type="url" - value={editingSvc?.url ?? ''} - required - placeholder="https://exemplo.com" - class="w-full rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] px-3.5 py-2.5 text-sm text-white outline-none transition-all placeholder:text-[var(--text-muted)] focus:border-[var(--green)]/50 focus:ring-1 focus:ring-[var(--green)]/20" - /> -</div> + <input + id="admin-url" + name="url" + type="url" + value={editingSvc?.url ?? ''} + required + placeholder="https://exemplo.com" + class="w-full rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] px-3.5 py-2.5 text-sm text-white outline-none transition-all placeholder:text-[var(--text-muted)] focus:border-[var(--green)]/50 focus:ring-1 focus:ring-[var(--green)]/20" + /> + </div> -<div> - <label for="admin-group" class="mb-1.5 block text-xs font-medium text-[var(--text-secondary)]">Grupo / Categoria</label> - <input - id="admin-group" - name="group_name" - type="text" - value={editingSvc?.group_name ?? 'Geral'} - placeholder="APIs, Websites, Servidores..." - class="w-full rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] px-3.5 py-2.5 text-sm text-white outline-none transition-all placeholder:text-[var(--text-muted)] focus:border-[var(--green)]/50 focus:ring-1 focus:ring-[var(--green)]/20" - /> -</div> + <div> + <label for="admin-group" class="mb-1.5 block text-xs font-medium text-[var(--text-secondary)]">Grupo / Categoria</label> + <input + id="admin-group" + name="group_name" + type="text" + value={editingSvc?.group_name ?? 'Geral'} + placeholder="APIs, Websites, Servidores..." + class="w-full rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] px-3.5 py-2.5 text-sm text-white outline-none transition-all placeholder:text-[var(--text-muted)] focus:border-[var(--green)]/50 focus:ring-1 focus:ring-[var(--green)]/20" + /> + </div> -<div> + <div> <label for="admin-interval" class="mb-1.5 block text-xs font-medium text-[var(--text-secondary)]">Intervalo (segundos)</label> <input id="admin-interval" @@ -424,6 +580,113 @@ </div> {/if} +<!-- Modal create / edit maintenance --> +{#if showMtnForm} + <!-- svelte-ignore a11y_click_events_have_key_events --> + <div + role="button" + tabindex="-1" + aria-label="Fechar" + class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" + onclick={closeMtnForm} + > + <div + class="w-full max-w-md animate-[modalIn_0.2s_ease-out] rounded-2xl border border-[var(--border-color)] bg-[var(--bg-secondary)] p-6 shadow-2xl" + onclick={(e) => e.stopPropagation()} + > + <div class="mb-5 flex items-center justify-between"> + <h2 class="text-sm font-semibold uppercase tracking-wider text-[var(--text-secondary)]"> + {editingMtn ? 'Editar Manutenção' : 'Nova Manutenção'} + </h2> + <button + onclick={closeMtnForm} + class="rounded-lg p-1.5 text-[var(--text-muted)] transition-colors hover:bg-[var(--border-color)] hover:text-white" + aria-label="Fechar" + > + <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + + <form onsubmit={handleMtnSubmit} class="space-y-4"> + {#if editingMtn} + <input type="hidden" name="id" value={editingMtn.id} /> + {/if} + + <div> + <label for="mtn-title" class="mb-1.5 block text-xs font-medium text-[var(--text-secondary)]">Título</label> + <input + id="mtn-title" + name="title" + type="text" + value={editingMtn?.title ?? ''} + required + placeholder="Manutenção nos servidores" + class="w-full rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] px-3.5 py-2.5 text-sm text-white outline-none transition-all placeholder:text-[var(--text-muted)] focus:border-[var(--green)]/50 focus:ring-1 focus:ring-[var(--green)]/20" + /> + </div> + + <div> + <label for="mtn-start" class="mb-1.5 block text-xs font-medium text-[var(--text-secondary)]">Início</label> + <input + id="mtn-start" + name="start_time" + type="datetime-local" + value={editingMtn ? editingMtn.start_time.slice(0, 16) : ''} + required + class="w-full rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] px-3.5 py-2.5 text-sm text-white outline-none transition-all focus:border-[var(--green)]/50 focus:ring-1 focus:ring-[var(--green)]/20" + /> + </div> + + <div> + <label for="mtn-end" class="mb-1.5 block text-xs font-medium text-[var(--text-secondary)]">Fim</label> + <input + id="mtn-end" + name="end_time" + type="datetime-local" + value={editingMtn ? editingMtn.end_time.slice(0, 16) : ''} + required + class="w-full rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] px-3.5 py-2.5 text-sm text-white outline-none transition-all focus:border-[var(--green)]/50 focus:ring-1 focus:ring-[var(--green)]/20" + /> + </div> + + <div> + <label class="flex items-center gap-3"> + <input + type="checkbox" + name="is_active" + checked={editingMtn?.is_active ?? true} + class="h-4 w-4 rounded border-[var(--border-color)] bg-[var(--bg-card)] text-[var(--green)] focus:ring-[var(--green)]/30" + /> + <span class="text-xs font-medium text-[var(--text-secondary)]">Ativa</span> + </label> + </div> + + {#if mtnFormError} + <p class="rounded-lg bg-[var(--red)]/5 px-3 py-2 text-xs text-[var(--red)]">{mtnFormError}</p> + {/if} + + <div class="flex gap-3 pt-1"> + <button + type="button" + onclick={closeMtnForm} + class="flex-1 rounded-xl border border-[var(--border-color)] px-4 py-2.5 text-sm font-medium text-[var(--text-muted)] transition-colors hover:border-[var(--text-muted)]/30 hover:text-white" + > + Cancelar + </button> + <button + type="submit" + disabled={mtnSaving} + class="flex-1 rounded-xl border border-[var(--green)] bg-[var(--green)]/10 px-4 py-2.5 text-sm font-medium text-[var(--green)] transition-all hover:bg-[var(--green)]/20 disabled:cursor-not-allowed disabled:opacity-40" + > + {mtnSaving ? 'Salvando…' : editingMtn ? 'Salvar' : 'Adicionar'} + </button> + </div> + </form> + </div> + </div> +{/if} <style> @keyframes modalIn { from { opacity: 0; transform: scale(0.95) translateY(-8px); } |