Files
ytp/client/src/components/AdminPanel.svelte
2025-11-03 22:54:10 +03:00

1448 lines
38 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
import { onMount } from 'svelte';
import {
listInventoryManagers,
createInventoryManager,
updateInventoryManager,
deleteInventoryManager,
listVehicles,
createVehicle,
updateVehicle,
deleteVehicle,
listUnits,
createUnit,
updateUnit,
deleteUnit,
listFuelPersonnel,
createFuelPersonnel,
updateFuelPersonnel,
deleteFuelPersonnel
} from '../api';
export let token;
const sections = [
{
id: 'inventory',
title: 'Mal sorumluları',
description: 'Depo süreçleri için yetkilendirilmiş kullanıcıları yönetin.',
icon: 'fa-solid fa-user-gear'
},
{
id: 'vehicles',
title: 'Araçlar',
description: 'Yakıt sevkiyatı yapan araçların kayıtlarını oluşturun.',
icon: 'fa-solid fa-truck-front'
},
{
id: 'units',
title: 'Birlikler',
description: 'Birlik bilgilerini ve sorumlu kişileri güncel tutun.',
icon: 'fa-solid fa-building'
},
{
id: 'personnel',
title: 'Yakıt personeli',
description: 'Yakıt veren personeli tanımlayın ve takip edin.',
icon: 'fa-solid fa-gas-pump'
}
];
let activeSection = 'inventory';
let lists = {
inventory: [],
vehicles: [],
units: [],
personnel: []
};
let loading = {
inventory: false,
vehicles: false,
units: false,
personnel: false
};
let submitting = {
inventory: false,
vehicles: false,
units: false,
personnel: false
};
let removing = {
inventory: false,
vehicles: false,
units: false,
personnel: false
};
let loaded = {
inventory: false,
vehicles: false,
units: false,
personnel: false
};
let feedback = {
inventory: null,
vehicles: null,
units: null,
personnel: null
};
let inventoryForm = {
username: '',
password: '',
displayName: ''
};
let vehicleForm = {
brand: '',
model: '',
year: '',
plate: ''
};
let unitForm = {
name: '',
address: '',
stk: '',
btk: '',
contactName: '',
contactRank: '',
contactRegistry: '',
contactIdentity: '',
contactPhone: ''
};
let personnelForm = {
fullName: '',
rank: '',
registryNumber: '',
identityNumber: '',
phone: ''
};
let editing = {
inventory: null,
vehicles: null,
units: null,
personnel: null
};
function resetInventoryForm() {
inventoryForm = {
username: '',
password: '',
displayName: ''
};
}
function resetVehicleForm() {
vehicleForm = {
brand: '',
model: '',
year: '',
plate: ''
};
}
function resetUnitForm() {
unitForm = {
name: '',
address: '',
stk: '',
btk: '',
contactName: '',
contactRank: '',
contactRegistry: '',
contactIdentity: '',
contactPhone: ''
};
}
function resetPersonnelForm() {
personnelForm = {
fullName: '',
rank: '',
registryNumber: '',
identityNumber: '',
phone: ''
};
}
onMount(() => {
loadSection('inventory', { force: true });
});
function setActiveSection(sectionId) {
activeSection = sectionId;
if (!loaded[sectionId]) {
loadSection(sectionId, { force: true });
}
}
function setFeedback(section, type = null, text = '') {
feedback = {
...feedback,
[section]: type ? { type, text } : null
};
}
async function loadSection(section, { force = false } = {}) {
if (!force && loaded[section]) {
return;
}
loading = { ...loading, [section]: true };
setFeedback(section);
try {
if (section === 'inventory') {
const response = await listInventoryManagers(token);
lists = { ...lists, inventory: response.managers };
} else if (section === 'vehicles') {
const response = await listVehicles(token);
lists = { ...lists, vehicles: response.vehicles };
} else if (section === 'units') {
const response = await listUnits(token);
lists = { ...lists, units: response.units };
} else if (section === 'personnel') {
const response = await listFuelPersonnel(token);
lists = { ...lists, personnel: response.personnel };
}
loaded = { ...loaded, [section]: true };
} catch (err) {
setFeedback(section, 'error', err.message);
} finally {
loading = { ...loading, [section]: false };
}
}
async function handleInventorySubmit(event) {
event.preventDefault();
setFeedback('inventory');
submitting = { ...submitting, inventory: true };
try {
if (editing.inventory) {
const displayName = inventoryForm.displayName.trim();
const password = inventoryForm.password ? inventoryForm.password.trim() : '';
if (!displayName && !password) {
throw new Error('En az bir alanı güncelleyin.');
}
const payload = {};
if (displayName) {
payload.displayName = displayName;
}
if (password) {
payload.password = password;
}
await updateInventoryManager(token, editing.inventory, payload);
setFeedback('inventory', 'success', 'Mal sorumlusu güncellendi.');
editing = { ...editing, inventory: null };
} else {
await createInventoryManager(token, {
username: inventoryForm.username.trim(),
displayName: inventoryForm.displayName.trim(),
password: inventoryForm.password
});
setFeedback('inventory', 'success', 'Yeni mal sorumlusu kaydedildi.');
}
resetInventoryForm();
await loadSection('inventory', { force: true });
} catch (err) {
setFeedback('inventory', 'error', err.message);
} finally {
submitting = { ...submitting, inventory: false };
}
}
async function handleVehicleSubmit(event) {
event.preventDefault();
setFeedback('vehicles');
submitting = { ...submitting, vehicles: true };
try {
const payload = {
brand: vehicleForm.brand.trim(),
model: vehicleForm.model.trim(),
year: vehicleForm.year ? Number(vehicleForm.year) : '',
plate: vehicleForm.plate.trim().toUpperCase()
};
if (!payload.brand || !payload.model || !payload.year || !payload.plate) {
throw new Error('Tüm alanları doldurun.');
}
if (editing.vehicles) {
await updateVehicle(token, editing.vehicles, payload);
setFeedback('vehicles', 'success', 'Araç kaydı güncellendi.');
editing = { ...editing, vehicles: null };
} else {
await createVehicle(token, payload);
setFeedback('vehicles', 'success', 'Araç kaydı oluşturuldu.');
}
resetVehicleForm();
await loadSection('vehicles', { force: true });
} catch (err) {
setFeedback('vehicles', 'error', err.message);
} finally {
submitting = { ...submitting, vehicles: false };
}
}
async function handleUnitSubmit(event) {
event.preventDefault();
setFeedback('units');
submitting = { ...submitting, units: true };
try {
const payload = {
name: unitForm.name.trim(),
address: unitForm.address.trim(),
stk: unitForm.stk.trim(),
btk: unitForm.btk.trim(),
contactName: unitForm.contactName.trim(),
contactRank: unitForm.contactRank.trim(),
contactRegistry: unitForm.contactRegistry.trim(),
contactIdentity: unitForm.contactIdentity.trim(),
contactPhone: unitForm.contactPhone.trim()
};
if (Object.values(payload).some((value) => !value)) {
throw new Error('Tüm alanları doldurun.');
}
if (editing.units) {
await updateUnit(token, editing.units, payload);
setFeedback('units', 'success', 'Birlik kaydı güncellendi.');
editing = { ...editing, units: null };
} else {
await createUnit(token, payload);
setFeedback('units', 'success', 'Birlik kaydı oluşturuldu.');
}
resetUnitForm();
await loadSection('units', { force: true });
} catch (err) {
setFeedback('units', 'error', err.message);
} finally {
submitting = { ...submitting, units: false };
}
}
async function handlePersonnelSubmit(event) {
event.preventDefault();
setFeedback('personnel');
submitting = { ...submitting, personnel: true };
try {
const payload = {
fullName: personnelForm.fullName.trim(),
rank: personnelForm.rank.trim(),
registryNumber: personnelForm.registryNumber.trim().toUpperCase(),
identityNumber: personnelForm.identityNumber.trim(),
phone: personnelForm.phone.trim()
};
if (Object.values(payload).some((value) => !value)) {
throw new Error('Tüm alanları doldurun.');
}
if (editing.personnel) {
await updateFuelPersonnel(token, editing.personnel, payload);
setFeedback('personnel', 'success', 'Personel kaydı güncellendi.');
editing = { ...editing, personnel: null };
} else {
await createFuelPersonnel(token, payload);
setFeedback('personnel', 'success', 'Personel kaydı oluşturuldu.');
}
resetPersonnelForm();
await loadSection('personnel', { force: true });
} catch (err) {
setFeedback('personnel', 'error', err.message);
} finally {
submitting = { ...submitting, personnel: false };
}
}
function startEdit(section, record) {
if (section === 'inventory') {
inventoryForm = {
username: record.username,
password: '',
displayName: record.displayName
};
} else if (section === 'vehicles') {
vehicleForm = {
brand: record.brand,
model: record.model,
year: String(record.year),
plate: record.plate
};
} else if (section === 'units') {
unitForm = {
name: record.name,
address: record.address,
stk: record.stk,
btk: record.btk,
contactName: record.contactName,
contactRank: record.contactRank,
contactRegistry: record.contactRegistry,
contactIdentity: record.contactIdentity,
contactPhone: record.contactPhone
};
} else if (section === 'personnel') {
personnelForm = {
fullName: record.fullName,
rank: record.rank,
registryNumber: record.registryNumber,
identityNumber: record.identityNumber,
phone: record.phone
};
}
editing = { ...editing, [section]: record.id };
setFeedback(section);
}
function cancelEdit(section) {
editing = { ...editing, [section]: null };
setFeedback(section);
if (section === 'inventory') {
resetInventoryForm();
} else if (section === 'vehicles') {
resetVehicleForm();
} else if (section === 'units') {
resetUnitForm();
} else if (section === 'personnel') {
resetPersonnelForm();
}
}
async function handleDelete(section, id) {
if (typeof window !== 'undefined') {
const confirmed = window.confirm('Kaydı silmek istediğinize emin misiniz?');
if (!confirmed) {
return;
}
}
removing = { ...removing, [section]: id };
setFeedback(section);
try {
if (section === 'inventory') {
await deleteInventoryManager(token, id);
} else if (section === 'vehicles') {
await deleteVehicle(token, id);
} else if (section === 'units') {
await deleteUnit(token, id);
} else if (section === 'personnel') {
await deleteFuelPersonnel(token, id);
}
setFeedback(section, 'success', 'Kayıt silindi.');
if (editing[section] != null && editing[section] === id) {
editing = { ...editing, [section]: null };
if (section === 'inventory') {
resetInventoryForm();
} else if (section === 'vehicles') {
resetVehicleForm();
} else if (section === 'units') {
resetUnitForm();
} else if (section === 'personnel') {
resetPersonnelForm();
}
}
await loadSection(section, { force: true });
} catch (err) {
setFeedback(section, 'error', err.message);
} finally {
removing = { ...removing, [section]: false };
}
}
function formatDate(value) {
if (!value) {
return '-';
}
return value.replace('T', ' ');
}
</script>
<div class="admin">
<nav class="section-nav">
{#each sections as section}
<button
type="button"
class:selected={section.id === activeSection}
on:click={() => setActiveSection(section.id)}
>
<div class="icon-badge">
<i class={section.icon} aria-hidden="true"></i>
</div>
<span>{section.title}</span>
<small>{section.description}</small>
</button>
{/each}
</nav>
<section class="panel-area">
{#if activeSection === 'inventory'}
<div class="section-layout">
<form class="panel-card form-card" on:submit|preventDefault={handleInventorySubmit}>
<div class="form-header">
<h3>{editing.inventory ? 'Mal sorumlusu düzenle' : 'Mal sorumlusu ekle'}</h3>
{#if editing.inventory}
<button type="button" class="ghost" on:click={() => cancelEdit('inventory')}>
İptal
</button>
{/if}
</div>
<p class="hint">
{editing.inventory
? 'Var olan kaydı güncelleyebilir, yeni şifre vermek için alanı doldurabilirsiniz.'
: 'Depo sürecini yönetecek kullanıcıyı tanımlayın.'}
</p>
{#if feedback.inventory}
<div class="message {feedback.inventory.type}">{feedback.inventory.text}</div>
{/if}
<div class="field-grid two">
<label>
<span>Kullanıcı adı</span>
<input
placeholder="ornek: malsorum2"
bind:value={inventoryForm.username}
autocomplete="off"
required
readonly={Boolean(editing.inventory)}
/>
</label>
<label>
<span>Görünen ad</span>
<input
placeholder="ornek: Mal Sorumlusu 2"
bind:value={inventoryForm.displayName}
autocomplete="off"
required
/>
</label>
</div>
<label>
<span>Geçici şifre</span>
<input
type="text"
placeholder={editing.inventory ? 'Boş bırakırsanız mevcut şifre korunur' : 'ornek: Mal@456'}
bind:value={inventoryForm.password}
autocomplete="off"
required={!editing.inventory}
/>
</label>
<button type="submit" class="primary" disabled={submitting.inventory}>
{#if submitting.inventory}
{editing.inventory ? 'Güncelleniyor...' : 'Kaydediliyor...'}
{:else}
{editing.inventory ? 'Güncelle' : 'Kaydet'}
{/if}
</button>
</form>
<div class="panel-card list-card">
<header class="list-header">
<div>
<h3>Kayıtlı mal sorumluları</h3>
<p>Oluşturma tarihi ve kullanıcı adı ile listelenir.</p>
</div>
<button
type="button"
on:click={() => loadSection('inventory', { force: true })}
disabled={loading.inventory}
>
Yenile
</button>
</header>
{#if loading.inventory}
<p class="info">Liste yükleniyor...</p>
{:else if lists.inventory.length === 0}
<p class="info">Henüz mal sorumlusu oluşturulmadı.</p>
{:else}
<table class="simple-table">
<thead>
<tr>
<th>Görünen ad</th>
<th>Kullanıcı adı</th>
<th>Oluşturma tarihi</th>
<th>İşlemler</th>
</tr>
</thead>
<tbody>
{#each lists.inventory as manager}
<tr>
<td>{manager.displayName}</td>
<td><code>{manager.username}</code></td>
<td>{formatDate(manager.createdAt)}</td>
<td class="actions">
<button type="button" class="ghost" on:click={() => startEdit('inventory', manager)}>
Düzenle
</button>
<button
type="button"
class="ghost danger"
on:click={() => handleDelete('inventory', manager.id)}
disabled={removing.inventory === manager.id}
>
{#if removing.inventory === manager.id}
Siliniyor...
{:else}
Sil
{/if}
</button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
</div>
{:else if activeSection === 'vehicles'}
<div class="section-layout">
<form class="panel-card form-card" on:submit|preventDefault={handleVehicleSubmit}>
<div class="form-header">
<h3>{editing.vehicles ? 'Araç kaydını düzenle' : 'Araç oluştur'}</h3>
{#if editing.vehicles}
<button type="button" class="ghost" on:click={() => cancelEdit('vehicles')}>
İptal
</button>
{/if}
</div>
<p class="hint">
{editing.vehicles
? 'Araç bilgilerini güncelleyebilir, plaka değişikliklerini kaydedebilirsiniz.'
: 'Yakıt transferinde kullanılacak araç bilgilerini girin.'}
</p>
{#if feedback.vehicles}
<div class="message {feedback.vehicles.type}">{feedback.vehicles.text}</div>
{/if}
<div class="field-grid two">
<label>
<span>Marka</span>
<input placeholder="Ford" bind:value={vehicleForm.brand} required />
</label>
<label>
<span>Model</span>
<input placeholder="Transit" bind:value={vehicleForm.model} required />
</label>
</div>
<div class="field-grid two">
<label>
<span>Model yılı</span>
<input
type="number"
min="1990"
max="2100"
placeholder="2024"
bind:value={vehicleForm.year}
required
/>
</label>
<label>
<span>Plaka</span>
<input placeholder="34 ABC 123" bind:value={vehicleForm.plate} required />
</label>
</div>
<button type="submit" class="primary" disabled={submitting.vehicles}>
{#if submitting.vehicles}
{editing.vehicles ? 'Güncelleniyor...' : 'Kaydediliyor...'}
{:else}
{editing.vehicles ? 'Güncelle' : 'Kaydet'}
{/if}
</button>
</form>
<div class="panel-card list-card">
<header class="list-header">
<div>
<h3>Araç listesi</h3>
<p>Plaka bazlı kayıtlarınızı buradan görüntüleyin.</p>
</div>
<button
type="button"
on:click={() => loadSection('vehicles', { force: true })}
disabled={loading.vehicles}
>
Yenile
</button>
</header>
{#if loading.vehicles}
<p class="info">Liste yükleniyor...</p>
{:else if lists.vehicles.length === 0}
<p class="info">Kayıtlı aracınız bulunmuyor.</p>
{:else}
<table class="simple-table">
<thead>
<tr>
<th>Araç</th>
<th>Model yılı</th>
<th>Plaka</th>
<th>Eklenme</th>
<th>İşlemler</th>
</tr>
</thead>
<tbody>
{#each lists.vehicles as vehicle}
<tr>
<td>{vehicle.brand} {vehicle.model}</td>
<td>{vehicle.year}</td>
<td><code>{vehicle.plate}</code></td>
<td>{formatDate(vehicle.createdAt)}</td>
<td class="actions">
<button type="button" class="ghost" on:click={() => startEdit('vehicles', vehicle)}>
Düzenle
</button>
<button
type="button"
class="ghost danger"
on:click={() => handleDelete('vehicles', vehicle.id)}
disabled={removing.vehicles === vehicle.id}
>
{#if removing.vehicles === vehicle.id}
Siliniyor...
{:else}
Sil
{/if}
</button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
</div>
{:else if activeSection === 'units'}
<div class="section-layout">
<form class="panel-card form-card" on:submit|preventDefault={handleUnitSubmit}>
<div class="form-header">
<h3>{editing.units ? 'Birlik kaydını düzenle' : 'Birlik ekle'}</h3>
{#if editing.units}
<button type="button" class="ghost" on:click={() => cancelEdit('units')}>
İptal
</button>
{/if}
</div>
<p class="hint">
{editing.units
? 'Birlik ve sorumlu bilgilerini güncelleyebilirsiniz.'
: 'Birlik ve sorumlularını ilişkilendirerek kayıt edin.'}
</p>
{#if feedback.units}
<div class="message {feedback.units.type}">{feedback.units.text}</div>
{/if}
<label>
<span>Birlik adı</span>
<input placeholder="Merkez Birlik" bind:value={unitForm.name} required />
</label>
<label>
<span>Adres</span>
<textarea
rows="2"
placeholder="Adres bilgisi"
bind:value={unitForm.address}
required
/>
</label>
<div class="field-grid two">
<label>
<span>STK</span>
<input placeholder="STK-1234" bind:value={unitForm.stk} required />
</label>
<label>
<span>BTK</span>
<input placeholder="BTK-5678" bind:value={unitForm.btk} required />
</label>
</div>
<div class="field-grid two">
<label>
<span>Sorumlu adı soyadı</span>
<input placeholder="Yzb. Murat Kaya" bind:value={unitForm.contactName} required />
</label>
<label>
<span>Rütbesi</span>
<input placeholder="Yüzbaşı" bind:value={unitForm.contactRank} required />
</label>
</div>
<div class="field-grid two">
<label>
<span>Sicil</span>
<input placeholder="MK4587" bind:value={unitForm.contactRegistry} required />
</label>
<label>
<span>TC Kimlik No</span>
<input
placeholder="25478963210"
bind:value={unitForm.contactIdentity}
required
/>
</label>
</div>
<label>
<span>İrtibat numarası</span>
<input placeholder="+90 5.." bind:value={unitForm.contactPhone} required />
</label>
<button type="submit" class="primary" disabled={submitting.units}>
{#if submitting.units}
{editing.units ? 'Güncelleniyor...' : 'Kaydediliyor...'}
{:else}
{editing.units ? 'Güncelle' : 'Kaydet'}
{/if}
</button>
</form>
<div class="panel-card list-card">
<header class="list-header">
<div>
<h3>Birlik listesi</h3>
<p>Sorumlu bilgileri ile birlikte görüntülenir.</p>
</div>
<button
type="button"
on:click={() => loadSection('units', { force: true })}
disabled={loading.units}
>
Yenile
</button>
</header>
{#if loading.units}
<p class="info">Liste yükleniyor...</p>
{:else if lists.units.length === 0}
<p class="info">Kayıtlı birlik bulunmuyor.</p>
{:else}
<div class="unit-grid">
{#each lists.units as unit}
<article class="unit-card">
<header>
<h4>{unit.name}</h4>
<span>{formatDate(unit.createdAt)}</span>
</header>
<p class="meta">{unit.address}</p>
<div class="unit-tags">
<span>STK: {unit.stk}</span>
<span>BTK: {unit.btk}</span>
</div>
<div class="unit-contact">
<strong>{unit.contactName}</strong>
<span>{unit.contactRank}{unit.contactRegistry}</span>
<span>{unit.contactPhone}</span>
<span>TC: {unit.contactIdentity}</span>
</div>
<div class="actions">
<button type="button" class="ghost" on:click={() => startEdit('units', unit)}>
Düzenle
</button>
<button
type="button"
class="ghost danger"
on:click={() => handleDelete('units', unit.id)}
disabled={removing.units === unit.id}
>
{#if removing.units === unit.id}
Siliniyor...
{:else}
Sil
{/if}
</button>
</div>
</article>
{/each}
</div>
{/if}
</div>
</div>
{:else if activeSection === 'personnel'}
<div class="section-layout">
<form class="panel-card form-card" on:submit|preventDefault={handlePersonnelSubmit}>
<div class="form-header">
<h3>{editing.personnel ? 'Personel kaydını düzenle' : 'Yakıt personeli ekle'}</h3>
{#if editing.personnel}
<button type="button" class="ghost" on:click={() => cancelEdit('personnel')}>
İptal
</button>
{/if}
</div>
<p class="hint">
{editing.personnel
? 'Personel kimlik ve irtibat bilgilerini güncelleyebilirsiniz.'
: 'Yakıt veren personelin kimlik ve iletişim bilgilerini kaydedin.'}
</p>
{#if feedback.personnel}
<div class="message {feedback.personnel.type}">{feedback.personnel.text}</div>
{/if}
<label>
<span>Adı soyadı</span>
<input placeholder="Astsb. Cahit Demir" bind:value={personnelForm.fullName} required />
</label>
<div class="field-grid two">
<label>
<span>Rütbesi</span>
<input placeholder="Astsubay" bind:value={personnelForm.rank} required />
</label>
<label>
<span>Sicili</span>
<input placeholder="CD5561" bind:value={personnelForm.registryNumber} required />
</label>
</div>
<div class="field-grid two">
<label>
<span>TC Kimlik No</span>
<input placeholder="14523698741" bind:value={personnelForm.identityNumber} required />
</label>
<label>
<span>İrtibat numarası</span>
<input placeholder="+90 5.." bind:value={personnelForm.phone} required />
</label>
</div>
<button type="submit" class="primary" disabled={submitting.personnel}>
{#if submitting.personnel}
{editing.personnel ? 'Güncelleniyor...' : 'Kaydediliyor...'}
{:else}
{editing.personnel ? 'Güncelle' : 'Kaydet'}
{/if}
</button>
</form>
<div class="panel-card list-card">
<header class="list-header">
<div>
<h3>Yakıt personeli listesi</h3>
<p>Sicil numarası ve iletişim bilgileriyle görüntülenir.</p>
</div>
<button
type="button"
on:click={() => loadSection('personnel', { force: true })}
disabled={loading.personnel}
>
Yenile
</button>
</header>
{#if loading.personnel}
<p class="info">Liste yükleniyor...</p>
{:else if lists.personnel.length === 0}
<p class="info">Kayıtlı yakıt personeli bulunmuyor.</p>
{:else}
<table class="simple-table">
<thead>
<tr>
<th>Adı soyadı</th>
<th>Rütbesi</th>
<th>Sicil</th>
<th>İrtibat</th>
<th>İşlemler</th>
</tr>
</thead>
<tbody>
{#each lists.personnel as person}
<tr>
<td>{person.fullName}</td>
<td>{person.rank}</td>
<td><code>{person.registryNumber}</code></td>
<td>
<div>{person.phone}</div>
<div class="personnel-meta">TC: {person.identityNumber}</div>
</td>
<td class="actions">
<button type="button" class="ghost" on:click={() => startEdit('personnel', person)}>
Düzenle
</button>
<button
type="button"
class="ghost danger"
on:click={() => handleDelete('personnel', person.id)}
disabled={removing.personnel === person.id}
>
{#if removing.personnel === person.id}
Siliniyor...
{:else}
Sil
{/if}
</button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
</div>
{/if}
</section>
</div>
<style>
.admin {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section-nav {
display: grid;
gap: 0.9rem;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.section-nav button {
position: relative;
text-align: left;
border: 1px solid #d4ddee;
background: linear-gradient(135deg, #ffffff, #f4f7ff);
border-radius: 16px;
padding: 1.1rem 1.15rem 1.05rem 1.15rem;
color: #27344d;
cursor: pointer;
display: grid;
gap: 0.45rem;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.section-nav button:hover {
transform: translateY(-2px);
box-shadow: 0 18px 32px rgba(88, 115, 173, 0.16);
}
.section-nav button.selected {
border-color: #4c6ef5;
box-shadow: 0 24px 46px rgba(76, 110, 245, 0.25);
background: linear-gradient(135deg, #4c6ef5, #6f8bff);
color: #ffffff;
}
.section-nav button span {
display: block;
font-weight: 700;
color: inherit;
font-size: 1rem;
}
.section-nav button small {
display: block;
margin-top: 0.15rem;
font-size: 0.85rem;
color: inherit;
line-height: 1.4;
opacity: 0.85;
}
.section-nav button.selected small {
color: rgba(255, 255, 255, 0.8);
}
.icon-badge {
width: 40px;
height: 40px;
border-radius: 12px;
background: rgba(76, 110, 245, 0.12);
display: grid;
place-items: center;
color: inherit;
}
.section-nav button.selected .icon-badge {
background: rgba(255, 255, 255, 0.22);
color: #ffffff;
}
.panel-area {
margin-top: 0.5rem;
}
.section-layout {
display: grid;
gap: 1.5rem;
grid-template-columns: minmax(0, 360px) minmax(0, 1fr);
align-items: start;
}
@media (max-width: 900px) {
.section-layout {
grid-template-columns: minmax(0, 1fr);
}
}
.panel-card {
border: 1px solid #dde3f1;
border-radius: 20px;
background: #ffffff;
box-shadow: 0 24px 48px rgba(88, 115, 173, 0.18);
padding: 1.9rem;
display: grid;
gap: 1.25rem;
color: #2a3856;
}
.form-card h3,
.list-card h3 {
margin: 0;
font-size: 1.3rem;
color: #1d2943;
}
.form-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.form-header .ghost {
padding: 0.3rem 0.75rem;
font-size: 0.8rem;
}
.hint {
margin: -0.25rem 0 0.75rem;
color: #5d6a83;
font-size: 0.95rem;
}
.field-grid {
display: grid;
gap: 1rem;
}
.field-grid.two {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
label {
display: grid;
gap: 0.35rem;
color: #26324d;
font-weight: 600;
}
input,
textarea {
border: 1px solid rgba(121, 139, 189, 0.45);
border-radius: 12px;
padding: 0.85rem 1rem;
font-size: 1rem;
font-family: inherit;
background: #f9fbff;
color: #1f2d44;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
textarea {
resize: vertical;
min-height: 70px;
}
input:focus,
textarea:focus {
outline: none;
border-color: #4c6ef5;
box-shadow: 0 0 0 3px rgba(76, 110, 245, 0.2);
background: #ffffff;
}
input:disabled {
background: #eef2fb;
color: #7a88a8;
cursor: not-allowed;
}
input[readonly] {
background: #f6f8ff;
color: #7a88a8;
}
.primary {
align-self: flex-start;
background: linear-gradient(135deg, #4c6ef5, #6f8bff);
color: #ffffff;
border: none;
border-radius: 12px;
padding: 0.85rem 1.4rem;
font-weight: 600;
cursor: pointer;
box-shadow: 0 18px 32px rgba(76, 110, 245, 0.28);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.primary[disabled] {
background: #b5c0e3;
cursor: wait;
box-shadow: none;
}
.primary:not([disabled]):hover {
transform: translateY(-1px);
box-shadow: 0 22px 40px rgba(76, 110, 245, 0.38);
}
.message {
border-radius: 12px;
padding: 0.8rem 1rem;
font-weight: 500;
font-size: 0.95rem;
}
.message.success {
background: rgba(76, 175, 80, 0.12);
color: #2e7d32;
border: 1px solid rgba(76, 175, 80, 0.3);
}
.message.error {
background: rgba(229, 57, 53, 0.12);
color: #c62828;
border: 1px solid rgba(229, 57, 53, 0.28);
}
.list-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.list-header h3 {
margin: 0;
}
.list-header p {
margin: 0.3rem 0 0;
color: #5d6a83;
font-size: 0.9rem;
}
.list-header button {
border: 1px solid #c9d6ed;
background: #edf2ff;
color: #3451a3;
padding: 0.45rem 0.9rem;
border-radius: 10px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.list-header button[disabled] {
color: #a6b4d5;
border-color: #d8e1f3;
cursor: wait;
}
.info {
margin: 0;
color: #5d6a83;
font-size: 0.95rem;
}
.simple-table {
width: 100%;
border-collapse: collapse;
border-radius: 16px;
overflow: hidden;
}
.simple-table th {
background: #f0f4ff;
color: #2a3856;
padding: 0.85rem 1rem;
font-size: 0.9rem;
text-align: left;
}
.simple-table td {
padding: 0.85rem 1rem;
border-bottom: 1px solid #e7ecf7;
color: #364564;
vertical-align: top;
}
.simple-table tr:last-child td {
border-bottom: none;
}
.actions {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
.ghost {
border: 1px solid #d3dcf0;
background: #f8faff;
color: #30426a;
border-radius: 10px;
padding: 0.35rem 0.85rem;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease;
}
.ghost:hover {
background: #e9f0ff;
border-color: #b8c6ea;
}
.ghost.danger {
border-color: #f5c7c5;
background: #fff0f0;
color: #c62828;
}
.ghost.danger:hover {
background: #ffe1df;
border-color: #f2a8a5;
}
code {
background: rgba(76, 110, 245, 0.1);
color: #2644a6;
padding: 0.25rem 0.5rem;
border-radius: 8px;
font-family: 'Roboto Mono', monospace;
font-size: 0.9rem;
}
.unit-grid {
display: grid;
gap: 1rem;
}
.unit-card {
border: 1px solid #dce4f5;
border-radius: 18px;
padding: 1.35rem;
background: #f6f9ff;
display: grid;
gap: 0.6rem;
}
.unit-card header {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
}
.unit-card header h4 {
margin: 0;
font-size: 1.05rem;
color: #1d2943;
}
.unit-card header span {
color: #7a88a8;
font-size: 0.85rem;
}
.unit-card .meta {
margin: 0;
color: #4e5d78;
line-height: 1.4;
}
.unit-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.unit-tags span {
background: rgba(76, 110, 245, 0.12);
color: #354fa8;
padding: 0.25rem 0.7rem;
border-radius: 999px;
font-size: 0.8rem;
}
.unit-contact {
display: grid;
gap: 0.25rem;
color: #4e5d78;
font-size: 0.9rem;
}
.unit-contact strong {
color: #1d2943;
}
.unit-card .actions {
margin-top: 0.75rem;
}
.personnel-meta {
margin-top: 0.25rem;
font-size: 0.85rem;
color: #7a88a8;
}
@media (max-width: 640px) {
.panel-card {
padding: 1.5rem;
}
.section-nav button {
padding: 0.85rem 1rem;
}
.simple-table th,
.simple-table td {
padding: 0.75rem 0.85rem;
}
}
</style>