This commit is contained in:
2025-11-05 21:18:02 +03:00
parent ca3ee1ceb7
commit 1b1e2a6fe0
4 changed files with 2947 additions and 0 deletions

View File

@@ -0,0 +1,953 @@
<script>
import { onMount } from 'svelte';
let user = null;
let goodsManagers = [];
let loading = true;
let error = '';
let showAddModal = false;
let showEditModal = false;
let selectedManager = null;
// Form değişkenleri
let formData = {
full_name: '',
rank: '',
registration_number: '',
tc_kimlik: '',
phone: '',
email: '',
username: '',
password: '',
is_active: true
};
onMount(async () => {
const userData = localStorage.getItem('user');
if (!userData || JSON.parse(userData).role !== 'admin') {
error = 'Bu sayfaya erişim yetkiniz yok.';
loading = false;
return;
}
user = JSON.parse(userData);
await loadGoodsManagers();
});
async function loadGoodsManagers() {
try {
const response = await fetch('/api/goods-managers');
if (response.ok) {
const data = await response.json();
goodsManagers = data.goodsManagers;
} else {
error = 'Mal sorumluları yüklenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Load goods managers error:', err);
} finally {
loading = false;
}
}
function resetForm() {
formData = {
full_name: '',
rank: '',
registration_number: '',
tc_kimlik: '',
phone: '',
email: '',
username: '',
password: '',
is_active: true
};
selectedManager = null;
}
function openAddModal() {
resetForm();
showAddModal = true;
}
function openEditModal(manager) {
selectedManager = manager;
formData = {
full_name: manager.full_name,
rank: manager.rank,
registration_number: manager.registration_number,
tc_kimlik: manager.tc_kimlik,
phone: manager.phone,
email: manager.email,
username: manager.username || '',
password: '', // Şifre gösterilmez, değiştirilmek istenirse girilir
is_active: manager.is_active
};
showEditModal = true;
}
function closeModal() {
showAddModal = false;
showEditModal = false;
resetForm();
}
async function handleAddManager() {
if (!formData.full_name || !formData.rank || !formData.registration_number || !formData.tc_kimlik || !formData.phone || !formData.email || !formData.username || !formData.password) {
error = 'Tüm alanlar zorunludur.';
return;
}
if (!/^[0-9]{11}$/.test(formData.tc_kimlik)) {
error = 'TC Kimlik numarası 11 haneli olmalıdır.';
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
error = 'Geçersiz e-posta formatı.';
return;
}
if (!/^[a-zA-Z0-9]{3,20}$/.test(formData.username)) {
error = 'Kullanıcı adı 3-20 karakter arası olmalı ve sadece harf ve rakam içermelidir.';
return;
}
if (formData.password.length < 6) {
error = 'Şifre en az 6 karakter olmalıdır.';
return;
}
try {
const response = await fetch('/api/goods-managers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (response.ok) {
await loadGoodsManagers();
closeModal();
error = '';
} else {
error = data.message || 'Mal sorumlusu eklenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Add manager error:', err);
}
}
async function handleUpdateManager() {
if (!formData.full_name || !formData.rank || !formData.registration_number || !formData.tc_kimlik || !formData.phone || !formData.email || !formData.username) {
error = 'Kullanıcı adı hariç tüm alanlar zorunludur.';
return;
}
if (!/^[0-9]{11}$/.test(formData.tc_kimlik)) {
error = 'TC Kimlik numarası 11 haneli olmalıdır.';
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
error = 'Geçersiz e-posta formatı.';
return;
}
if (!/^[a-zA-Z0-9]{3,20}$/.test(formData.username)) {
error = 'Kullanıcı adı 3-20 karakter arası olmalı ve sadece harf ve rakam içermelidir.';
return;
}
if (formData.password && formData.password.trim().length > 0 && formData.password.length < 6) {
error = 'Şifre en az 6 karakter olmalıdır.';
return;
}
try {
const response = await fetch('/api/goods-managers', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: selectedManager.id,
...formData
}),
});
const data = await response.json();
if (response.ok) {
await loadGoodsManagers();
closeModal();
error = '';
} else {
error = data.message || 'Mal sorumlusu güncellenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Update manager error:', err);
}
}
async function handleDeleteManager(manager) {
if (!confirm(`${manager.rank} ${manager.full_name} mal sorumlusunu silmek istediğinizden emin misiniz?`)) {
return;
}
try {
const response = await fetch('/api/goods-managers', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: manager.id }),
});
const data = await response.json();
if (response.ok) {
await loadGoodsManagers();
error = '';
} else {
error = data.message || 'Mal sorumlusu silinemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Delete manager error:', err);
}
}
async function toggleManagerStatus(manager) {
try {
const response = await fetch('/api/goods-managers', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: manager.id,
...manager,
is_active: !manager.is_active
}),
});
const data = await response.json();
if (response.ok) {
await loadGoodsManagers();
error = '';
} else {
error = data.message || 'Mal sorumlusu durumu güncellenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Toggle manager status error:', err);
}
}
</script>
<div class="goods-managers-content">
<div class="content-header">
<div class="header-left">
<h1 class="page-title">Mal Sorumluları</h1>
</div>
<button class="btn btn-primary" on:click={openAddModal}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Yeni Mal Sorumlusu Ekle
</button>
</div>
{#if error}
<div class="error-message">
{error}
</div>
{/if}
{#if loading}
<div class="loading-container">
<div class="spinner"></div>
<p>Yükleniyor...</p>
</div>
{:else if goodsManagers.length === 0}
<div class="empty-state">
<div class="empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</div>
<h3>Henüz Mal Sorumlusu Yok</h3>
<p>Sisteme mal sorumlusu eklemek için "Yeni Mal Sorumlusu Ekle" butonuna tıklayın.</p>
<button class="btn btn-primary" on:click={openAddModal}>
İlk Mal Sorumlusunu Ekle
</button>
</div>
{:else}
<div class="managers-grid">
{#each goodsManagers as manager (manager.id)}
<div class="manager-card card {manager.is_active ? '' : 'inactive'}">
<div class="manager-header">
<div class="manager-info">
<h3 class="manager-name">{manager.rank} {manager.full_name}</h3>
<div class="manager-status">
<span class="status-badge {manager.is_active ? 'active' : 'inactive'}">
{@html manager.is_active ? '<i class="fas fa-check"></i> Aktif' : '<i class="fas fa-times"></i> Pasif'}
</span>
</div>
</div>
</div>
<div class="manager-details">
<div class="detail-item">
<span class="detail-label">📄 Sicil No:</span>
<span class="detail-value">{manager.registration_number}</span>
</div>
<div class="detail-item">
<span class="detail-label">🆔 TC Kimlik:</span>
<span class="detail-value">{manager.tc_kimlik}</span>
</div>
<div class="detail-item">
<span class="detail-label">📧 E-posta:</span>
<span class="detail-value">{manager.email}</span>
</div>
<div class="detail-item">
<span class="detail-label">📱 İrtibat:</span>
<span class="detail-value">{manager.phone}</span>
</div>
<div class="detail-item">
<span class="detail-label">👤 Kullanıcı Adı:</span>
<span class="detail-value">{manager.username || 'Belirlenmemiş'}</span>
</div>
</div>
<div class="manager-actions">
<button class="btn btn-sm btn-secondary" on:click={() => openEditModal(manager)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Düzenle
</button>
<button
class="btn btn-sm {manager.is_active ? 'btn-warning' : 'btn-success'}"
on:click={() => toggleManagerStatus(manager)}
>
{#if manager.is_active}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
Pasif Yap
{:else}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<circle cx="12" cy="16" r="1"/>
<path d="M7 11V7a5 5 0 0 1 9.9-1"/>
</svg>
Aktif Yap
{/if}
</button>
<button class="btn btn-sm btn-danger" on:click={() => handleDeleteManager(manager)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
Sil
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
<!-- Mal Sorumlusu Ekle Modal -->
{#if showAddModal}
<div class="modal-overlay" on:click={closeModal}>
<div class="modal" on:click|stopPropagation>
<div class="modal-header">
<h2>Yeni Mal Sorumlusu Ekle</h2>
<button class="modal-close" on:click={closeModal}>×</button>
</div>
<form on:submit|preventDefault={handleAddManager} class="modal-form">
<div class="form-group">
<label for="full_name">Adı Soyadı</label>
<input
id="full_name"
type="text"
class="form-input"
bind:value={formData.full_name}
placeholder="Ali Veli"
required
/>
</div>
<div class="form-group">
<label for="rank">Rütbesi</label>
<input
id="rank"
type="text"
class="form-input"
bind:value={formData.rank}
placeholder="Binbaşı"
required
/>
</div>
<div class="form-group">
<label for="registration_number">Sicil Numarası</label>
<input
id="registration_number"
type="text"
class="form-input"
bind:value={formData.registration_number}
placeholder="GM001"
required
/>
</div>
<div class="form-group">
<label for="tc_kimlik">TC Kimlik Numarası</label>
<input
id="tc_kimlik"
type="text"
class="form-input"
bind:value={formData.tc_kimlik}
placeholder="12345678901"
maxlength="11"
required
/>
</div>
<div class="form-group">
<label for="phone">İrtibat Numarası</label>
<input
id="phone"
type="tel"
class="form-input"
bind:value={formData.phone}
placeholder="05321234567"
required
/>
</div>
<div class="form-group">
<label for="email">E-posta</label>
<input
id="email"
type="email"
class="form-input"
bind:value={formData.email}
placeholder="ali.veli@mil.tr"
required
/>
</div>
<div class="form-group">
<label for="username">Kullanıcı Adı</label>
<input
id="username"
type="text"
class="form-input"
bind:value={formData.username}
placeholder="ibrahim.kara"
required
/>
<small style="color: var(--text-secondary); font-size: 0.8rem; margin-top: 0.25rem; display: block;">
Bu kullanıcı adı ile sisteme giriş yapabilecek.
</small>
</div>
<div class="form-group">
<label for="password">Şifre</label>
<input
id="password"
type="password"
class="form-input"
bind:value={formData.password}
placeholder="En az 6 karakter"
required
/>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" on:click={closeModal}>İptal</button>
<button type="submit" class="btn btn-primary">Kaydet</button>
</div>
</form>
</div>
</div>
{/if}
<!-- Mal Sorumlusu Düzenle Modal -->
{#if showEditModal}
<div class="modal-overlay" on:click={closeModal}>
<div class="modal" on:click|stopPropagation>
<div class="modal-header">
<h2>Mal Sorumlusu Düzenle</h2>
<button class="modal-close" on:click={closeModal}>×</button>
</div>
<form on:submit|preventDefault={handleUpdateManager} class="modal-form">
<div class="form-group">
<label for="edit-full_name">Adı Soyadı</label>
<input
id="edit-full_name"
type="text"
class="form-input"
bind:value={formData.full_name}
placeholder="Ali Veli"
required
/>
</div>
<div class="form-group">
<label for="edit-rank">Rütbesi</label>
<input
id="edit-rank"
type="text"
class="form-input"
bind:value={formData.rank}
placeholder="Binbaşı"
required
/>
</div>
<div class="form-group">
<label for="edit-registration_number">Sicil Numarası</label>
<input
id="edit-registration_number"
type="text"
class="form-input"
bind:value={formData.registration_number}
placeholder="GM001"
required
/>
</div>
<div class="form-group">
<label for="edit-tc_kimlik">TC Kimlik Numarası</label>
<input
id="edit-tc_kimlik"
type="text"
class="form-input"
bind:value={formData.tc_kimlik}
placeholder="12345678901"
maxlength="11"
required
/>
</div>
<div class="form-group">
<label for="edit-phone">İrtibat Numarası</label>
<input
id="edit-phone"
type="tel"
class="form-input"
bind:value={formData.phone}
placeholder="05321234567"
required
/>
</div>
<div class="form-group">
<label for="edit-email">E-posta</label>
<input
id="edit-email"
type="email"
class="form-input"
bind:value={formData.email}
placeholder="ali.veli@mil.tr"
required
/>
</div>
<div class="form-group">
<label for="edit-username">Kullanıcı Adı</label>
<input
id="edit-username"
type="text"
class="form-input"
bind:value={formData.username}
placeholder="ibrahim.kara"
required
/>
<small style="color: var(--text-secondary); font-size: 0.8rem; margin-top: 0.25rem; display: block;">
Bu kullanıcı adı ile sisteme giriş yapabilecek.
</small>
</div>
<div class="form-group">
<label for="edit-password">Yeni Şifre (Opsiyonel)</label>
<input
id="edit-password"
type="password"
class="form-input"
bind:value={formData.password}
placeholder="Değiştirmek için yeni şifre girin"
/>
<small style="color: var(--text-secondary); font-size: 0.8rem; margin-top: 0.25rem; display: block;">
Boş bırakırsanız mevcut şifre korunur.
</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input
type="checkbox"
bind:checked={formData.is_active}
/>
<span class="checkmark"></span>
Personel Aktif
</label>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" on:click={closeModal}>İptal</button>
<button type="submit" class="btn btn-primary">Güncelle</button>
</div>
</form>
</div>
</div>
{/if}
<style>
.goods-managers-content {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
gap: 1rem;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.page-title {
font-size: 2rem;
font-weight: 700;
color: var(--text-color);
margin: 0;
}
.error-message {
background: #FEE2E2;
color: #DC2626;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border: 1px solid #FECACA;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #E5E7EB;
border-top: 4px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.empty-state {
text-align: center;
padding: 3rem;
background: white;
border-radius: 12px;
border: 1px solid var(--card-border-color);
}
.empty-icon {
color: var(--text-secondary);
margin-bottom: 1rem;
}
.empty-state h3 {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-color);
margin-bottom: 0.5rem;
}
.empty-state p {
color: var(--text-secondary);
margin-bottom: 2rem;
}
.managers-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.manager-card {
background: white;
border: 1px solid var(--card-border-color);
border-radius: 12px;
padding: 1.5rem;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.manager-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.manager-card.inactive {
background: #F9FAFB;
border-color: #D1D5DB;
opacity: 0.8;
}
.manager-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--card-border-color);
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.manager-name {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-color);
margin: 0;
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.status-badge.active {
background: #D1FAE5;
color: #059669;
}
.status-badge.inactive {
background: #FEE2E2;
color: #DC2626;
}
.manager-details {
margin-bottom: 1rem;
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.detail-label {
font-weight: 500;
color: var(--text-secondary);
}
.detail-value {
font-weight: 500;
color: var(--text-color);
text-align: right;
}
.manager-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.btn-danger {
background: #DC2626;
color: white;
border: 1px solid #B91C1C;
}
.btn-danger:hover {
background: #B91C1C;
}
.btn-warning {
background: #F59E0B;
color: white;
border: 1px solid #D97706;
}
.btn-warning:hover {
background: #D97706;
}
.btn-success {
background: #10B981;
color: white;
border: 1px solid #059669;
}
.btn-success:hover {
background: #059669;
}
/* Modal Stilleri */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal {
background: white;
border-radius: 12px;
max-width: 500px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--card-border-color);
}
.modal-header h2 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
color: var(--text-color);
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem;
}
.modal-form {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-color);
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
margin: 0;
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--card-border-color);
}
/* Responsive Tasarım */
@media (max-width: 768px) {
.goods-managers-content {
padding: 1rem;
}
.content-header {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.header-left {
flex-direction: column;
gap: 0.5rem;
}
.page-title {
font-size: 1.5rem;
}
.managers-grid {
grid-template-columns: 1fr;
}
.manager-header {
flex-direction: column;
gap: 1rem;
}
.manager-actions {
justify-content: stretch;
}
.modal {
margin: 0;
max-height: 100vh;
}
.modal-actions {
flex-direction: column;
}
.detail-item {
flex-direction: column;
gap: 0.25rem;
}
.detail-value {
text-align: left;
}
}
</style>

View File

@@ -0,0 +1,823 @@
<script>
import { onMount } from 'svelte';
// Props
export let user = null;
let personnel = [];
let loading = true;
let error = '';
let showAddModal = false;
let showEditModal = false;
let selectedPersonnel = null;
// Form değişkenleri
let formData = {
full_name: '',
rank: '',
registration_number: '',
tc_kimlik: '',
phone: '',
is_active: true
};
onMount(async () => {
await loadPersonnel();
});
async function loadPersonnel() {
try {
const response = await fetch('/api/fuel-personnel');
if (response.ok) {
const data = await response.json();
personnel = data.fuelPersonnel;
} else {
error = 'Personel listesi yüklenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Load personnel error:', err);
} finally {
loading = false;
}
}
function resetForm() {
formData = {
full_name: '',
rank: '',
registration_number: '',
tc_kimlik: '',
phone: '',
is_active: true
};
selectedPersonnel = null;
}
function openAddModal() {
resetForm();
showAddModal = true;
}
function openEditModal(person) {
selectedPersonnel = person;
formData = {
full_name: person.full_name,
rank: person.rank,
registration_number: person.registration_number,
tc_kimlik: person.tc_kimlik,
phone: person.phone,
is_active: person.is_active
};
showEditModal = true;
}
function closeModal() {
showAddModal = false;
showEditModal = false;
resetForm();
}
async function handleAddPersonnel() {
if (!formData.full_name || !formData.rank || !formData.registration_number || !formData.tc_kimlik || !formData.phone) {
error = 'Tüm alanlar zorunludur.';
return;
}
if (!/^[0-9]{11}$/.test(formData.tc_kimlik)) {
error = 'TC Kimlik numarası 11 haneli olmalıdır.';
return;
}
try {
const response = await fetch('/api/fuel-personnel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (response.ok) {
await loadPersonnel();
closeModal();
error = '';
} else {
error = data.message || 'Personel eklenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Add personnel error:', err);
}
}
async function handleUpdatePersonnel() {
if (!formData.full_name || !formData.rank || !formData.registration_number || !formData.tc_kimlik || !formData.phone) {
error = 'Tüm alanlar zorunludur.';
return;
}
if (!/^[0-9]{11}$/.test(formData.tc_kimlik)) {
error = 'TC Kimlik numarası 11 haneli olmalıdır.';
return;
}
try {
const response = await fetch('/api/fuel-personnel', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: selectedPersonnel.id,
...formData
}),
});
const data = await response.json();
if (response.ok) {
await loadPersonnel();
closeModal();
error = '';
} else {
error = data.message || 'Personel güncellenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Update personnel error:', err);
}
}
async function handleDeletePersonnel(person) {
if (!confirm(`${person.rank} ${person.full_name} personelini silmek istediğinizden emin misiniz?`)) {
return;
}
try {
const response = await fetch('/api/fuel-personnel', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: person.id }),
});
const data = await response.json();
if (response.ok) {
await loadPersonnel();
error = '';
} else {
error = data.message || 'Personel silinemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Delete personnel error:', err);
}
}
async function togglePersonnelStatus(person) {
try {
const response = await fetch('/api/fuel-personnel', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: person.id,
...person,
is_active: !person.is_active
}),
});
const data = await response.json();
if (response.ok) {
await loadPersonnel();
error = '';
} else {
error = data.message || 'Personel durumu güncellenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Toggle personnel status error:', err);
}
}
</script>
<div class="personnel-content">
<div class="content-header">
<h1 class="content-title">Personel Yönetimi</h1>
<div class="stats-badge">
<span class="count">{personnel.length}</span>
<span>Personel</span>
</div>
<button class="btn btn-primary" on:click={openAddModal}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Yeni Personel Ekle
</button>
</div>
{#if error}
<div class="error-message">
{error}
</div>
{/if}
{#if loading}
<div class="loading-container">
<div class="spinner"></div>
<p>Yükleniyor...</p>
</div>
{:else if personnel.length === 0}
<div class="empty-state">
<div class="empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</div>
<h3>Henüz Personel Yok</h3>
<p>Sisteme personel eklemek için "Yeni Personel Ekle" butonuna tıklayın.</p>
<button class="btn btn-primary" on:click={openAddModal}>
İlk Personeli Ekle
</button>
</div>
{:else}
<div class="personnel-grid">
{#each personnel as person (person.id)}
<div class="personnel-card card {person.is_active ? '' : 'inactive'}">
<div class="personnel-header">
<div class="personnel-info">
<h3 class="personnel-name">{person.rank} {person.full_name}</h3>
<div class="personnel-status">
<span class="status-badge {person.is_active ? 'active' : 'inactive'}">
{@html person.is_active ? '<i class="fas fa-check"></i> Aktif' : '<i class="fas fa-times"></i> Pasif'}
</span>
</div>
</div>
</div>
<div class="personnel-details">
<div class="detail-item">
<span class="detail-label">Sicil No:</span>
<span class="detail-value">{person.registration_number}</span>
</div>
<div class="detail-item">
<span class="detail-label">TC Kimlik:</span>
<span class="detail-value">{person.tc_kimlik}</span>
</div>
<div class="detail-item">
<span class="detail-label">İrtibat:</span>
<span class="detail-value">{person.phone}</span>
</div>
</div>
<div class="personnel-actions">
<button class="btn btn-sm btn-secondary" on:click={() => openEditModal(person)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Düzenle
</button>
<button
class="btn btn-sm {person.is_active ? 'btn-warning' : 'btn-success'}"
on:click={() => togglePersonnelStatus(person)}
>
{#if person.is_active}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
Pasif Yap
{:else}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<circle cx="12" cy="16" r="1"/>
<path d="M7 11V7a5 5 0 0 1 9.9-1"/>
</svg>
Aktif Yap
{/if}
</button>
<button class="btn btn-sm btn-danger" on:click={() => handleDeletePersonnel(person)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
Sil
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
<!-- Personel Ekle Modal -->
{#if showAddModal}
<div class="modal-overlay" on:click={closeModal}>
<div class="modal" on:click|stopPropagation>
<div class="modal-header">
<h2>Yeni Personel Ekle</h2>
<button class="modal-close" on:click={closeModal}>×</button>
</div>
<form on:submit|preventDefault={handleAddPersonnel} class="modal-form">
<div class="form-group">
<label for="full_name">Adı Soyadı</label>
<input
id="full_name"
type="text"
class="form-input"
bind:value={formData.full_name}
placeholder="Mehmet Yılmaz"
required
/>
</div>
<div class="form-group">
<label for="rank">Rütbesi</label>
<input
id="rank"
type="text"
class="form-input"
bind:value={formData.rank}
placeholder="Üsteğmen"
required
/>
</div>
<div class="form-group">
<label for="registration_number">Sicil Numarası</label>
<input
id="registration_number"
type="text"
class="form-input"
bind:value={formData.registration_number}
placeholder="123456"
required
/>
</div>
<div class="form-group">
<label for="tc_kimlik">TC Kimlik Numarası</label>
<input
id="tc_kimlik"
type="text"
class="form-input"
bind:value={formData.tc_kimlik}
placeholder="12345678901"
maxlength="11"
required
/>
</div>
<div class="form-group">
<label for="phone">İrtibat Numarası</label>
<input
id="phone"
type="tel"
class="form-input"
bind:value={formData.phone}
placeholder="05321234567"
required
/>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" on:click={closeModal}>İptal</button>
<button type="submit" class="btn btn-primary">Kaydet</button>
</div>
</form>
</div>
</div>
{/if}
<!-- Personel Düzenle Modal -->
{#if showEditModal}
<div class="modal-overlay" on:click={closeModal}>
<div class="modal" on:click|stopPropagation>
<div class="modal-header">
<h2>Personel Düzenle</h2>
<button class="modal-close" on:click={closeModal}>×</button>
</div>
<form on:submit|preventDefault={handleUpdatePersonnel} class="modal-form">
<div class="form-group">
<label for="edit-full_name">Adı Soyadı</label>
<input
id="edit-full_name"
type="text"
class="form-input"
bind:value={formData.full_name}
placeholder="Mehmet Yılmaz"
required
/>
</div>
<div class="form-group">
<label for="edit-rank">Rütbesi</label>
<input
id="edit-rank"
type="text"
class="form-input"
bind:value={formData.rank}
placeholder="Üsteğmen"
required
/>
</div>
<div class="form-group">
<label for="edit-registration_number">Sicil Numarası</label>
<input
id="edit-registration_number"
type="text"
class="form-input"
bind:value={formData.registration_number}
placeholder="123456"
required
/>
</div>
<div class="form-group">
<label for="edit-tc_kimlik">TC Kimlik Numarası</label>
<input
id="edit-tc_kimlik"
type="text"
class="form-input"
bind:value={formData.tc_kimlik}
placeholder="12345678901"
maxlength="11"
required
/>
</div>
<div class="form-group">
<label for="edit-phone">İrtibat Numarası</label>
<input
id="edit-phone"
type="tel"
class="form-input"
bind:value={formData.phone}
placeholder="05321234567"
required
/>
</div>
<div class="form-group">
<label class="checkbox-label">
<input
type="checkbox"
bind:checked={formData.is_active}
/>
<span class="checkmark"></span>
Personel Aktif
</label>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" on:click={closeModal}>İptal</button>
<button type="submit" class="btn btn-primary">Güncelle</button>
</div>
</form>
</div>
</div>
{/if}
<style>
.personnel-content {
padding: 0;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
gap: 1rem;
flex-wrap: wrap;
}
.content-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-color);
margin: 0;
}
.stats-badge {
background: var(--primary-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.count {
background: rgba(255, 255, 255, 0.2);
padding: 0.25rem 0.5rem;
border-radius: 10px;
font-weight: 700;
}
.error-message {
background: #FEE2E2;
color: #DC2626;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border: 1px solid #FECACA;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #E5E7EB;
border-top: 4px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.empty-state {
text-align: center;
padding: 3rem;
background: white;
border-radius: 12px;
border: 1px solid var(--card-border-color);
}
.empty-icon {
color: var(--text-secondary);
margin-bottom: 1rem;
}
.empty-state h3 {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-color);
margin-bottom: 0.5rem;
}
.empty-state p {
color: var(--text-secondary);
margin-bottom: 2rem;
}
.personnel-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.personnel-card {
background: white;
border: 1px solid var(--card-border-color);
border-radius: 12px;
padding: 1.5rem;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.personnel-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.personnel-card.inactive {
background: #F9FAFB;
border-color: #D1D5DB;
opacity: 0.8;
}
.personnel-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--card-border-color);
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.personnel-name {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-color);
margin: 0;
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.status-badge.active {
background: #D1FAE5;
color: #059669;
}
.status-badge.inactive {
background: #FEE2E2;
color: #DC2626;
}
.personnel-details {
margin-bottom: 1rem;
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.detail-label {
font-weight: 500;
color: var(--text-secondary);
}
.detail-value {
font-weight: 500;
color: var(--text-color);
}
.personnel-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.btn-danger {
background: #DC2626;
color: white;
border: 1px solid #B91C1C;
}
.btn-danger:hover {
background: #B91C1C;
}
.btn-warning {
background: #F59E0B;
color: white;
border: 1px solid #D97706;
}
.btn-warning:hover {
background: #D97706;
}
.btn-success {
background: #10B981;
color: white;
border: 1px solid #059669;
}
.btn-success:hover {
background: #059669;
}
/* Modal Stilleri */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal {
background: white;
border-radius: 12px;
max-width: 500px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--card-border-color);
}
.modal-header h2 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
color: var(--text-color);
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem;
}
.modal-form {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-color);
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
margin: 0;
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--card-border-color);
}
/* Responsive Tasarım */
@media (max-width: 768px) {
.content-header {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.content-title {
font-size: 1.25rem;
}
.personnel-grid {
grid-template-columns: 1fr;
}
.personnel-header {
flex-direction: column;
gap: 1rem;
}
.personnel-actions {
justify-content: stretch;
}
.modal {
margin: 0;
max-height: 100vh;
}
.modal-actions {
flex-direction: column;
}
.detail-item {
flex-direction: column;
gap: 0.25rem;
}
}
</style>

View File

@@ -0,0 +1,531 @@
<script>
import { onMount } from 'svelte';
// Props
export let user = null;
let units = [];
let loading = true;
let error = '';
let showAddModal = false;
let showEditModal = false;
let selectedUnit = null;
// Form değişkenleri
let formData = {
name: '',
address: '',
stk: '',
btk: '',
commander: {
full_name: '',
rank: '',
registration_number: '',
tc_kimlik: '',
phone: ''
}
};
onMount(async () => {
await loadUnits();
});
async function loadUnits() {
try {
const response = await fetch('/api/units');
if (response.ok) {
const data = await response.json();
units = data.units;
} else {
error = 'Birlikler yüklenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Load units error:', err);
} finally {
loading = false;
}
}
function resetForm() {
formData = {
name: '',
address: '',
stk: '',
btk: '',
commander: {
full_name: '',
rank: '',
registration_number: '',
tc_kimlik: '',
phone: ''
}
};
selectedUnit = null;
}
function openAddModal() {
resetForm();
showAddModal = true;
}
function openEditModal(unit) {
selectedUnit = unit;
formData = {
name: unit.name,
address: unit.address,
stk: unit.stk,
btk: unit.btk,
commander: { ...unit.commander }
};
showEditModal = true;
}
function closeModal() {
showAddModal = false;
showEditModal = false;
resetForm();
}
async function handleAddUnit() {
if (!formData.name || !formData.address || !formData.stk || !formData.btk) {
error = 'Tüm alanlar zorunludur.';
return;
}
const { commander } = formData;
if (!commander.full_name || !commander.rank || !commander.registration_number || !commander.tc_kimlik || !commander.phone) {
error = 'Birlik sorumlusunun tüm bilgileri zorunludur.';
return;
}
if (!/^[0-9]{11}$/.test(commander.tc_kimlik)) {
error = 'TC Kimlik numarası 11 haneli olmalıdır.';
return;
}
try {
const response = await fetch('/api/units', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (response.ok) {
await loadUnits();
closeModal();
error = '';
} else {
error = data.message || 'Birlik eklenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Add unit error:', err);
}
}
async function handleUpdateUnit() {
if (!formData.name || !formData.address || !formData.stk || !formData.btk) {
error = 'Tüm alanlar zorunludur.';
return;
}
const { commander } = formData;
if (!commander.full_name || !commander.rank || !commander.registration_number || !commander.tc_kimlik || !commander.phone) {
error = 'Birlik sorumlusunun tüm bilgileri zorunludur.';
return;
}
if (!/^[0-9]{11}$/.test(commander.tc_kimlik)) {
error = 'TC Kimlik numarası 11 haneli olmalıdır.';
return;
}
try {
const response = await fetch('/api/units', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: selectedUnit.id,
...formData
}),
});
const data = await response.json();
if (response.ok) {
await loadUnits();
closeModal();
error = '';
} else {
error = data.message || 'Birlik güncellenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Update unit error:', err);
}
}
async function handleDeleteUnit(unit) {
if (!confirm(`${unit.name} birliğini silmek istediğinizden emin misiniz?`)) {
return;
}
try {
const response = await fetch('/api/units', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: unit.id }),
});
const data = await response.json();
if (response.ok) {
await loadUnits();
error = '';
} else {
error = data.message || 'Birlik silinemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Delete unit error:', err);
}
}
</script>
<div class="units-content">
<div class="content-header">
<h1 class="content-title">Birlik Yönetimi</h1>
<div class="stats-badge">
<span class="count">{units.length}</span>
<span>Birlik</span>
</div>
<button class="btn btn-primary" on:click={openAddModal}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Yeni Birlik Ekle
</button>
</div>
{#if error}
<div class="error-message">
{error}
</div>
{/if}
{#if loading}
<div class="loading-container">
<div class="spinner"></div>
<p>Yükleniyor...</p>
</div>
{:else if units.length === 0}
<div class="empty-state">
<div class="empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 21h18"/>
<path d="M5 21V7l8-4v18"/>
<path d="M19 21V11l-6-4"/>
</svg>
</div>
<h3>Henüz Birlik Yok</h3>
<p>Sisteme birlik eklemek için "Yeni Birlik Ekle" butonuna tıklayın.</p>
<button class="btn btn-primary" on:click={openAddModal}>
İlk Birliği Ekle
</button>
</div>
{:else}
<div class="units-grid">
{#each units as unit (unit.id)}
<div class="unit-card card">
<div class="unit-header">
<div class="unit-info">
<h3 class="unit-name">{unit.name}</h3>
<p class="unit-address">{unit.address}</p>
</div>
</div>
<div class="unit-details">
<div class="detail-item">
<span class="detail-label">STK:</span>
<span class="detail-value">{unit.stk}</span>
</div>
<div class="detail-item">
<span class="detail-label">BTK:</span>
<span class="detail-value">{unit.btk}</span>
</div>
</div>
<div class="commander-section">
<h4 class="commander-title">Birlik Sorumlusu</h4>
<div class="commander-info">
<div class="commander-details">
<p class="commander-name">{unit.commander.rank} {unit.commander.full_name}</p>
<p class="commander-detail">Sicil: {unit.commander.registration_number}</p>
<p class="commander-detail">TC: {unit.commander.tc_kimlik}</p>
<p class="commander-detail">İrtibat: {unit.commander.phone}</p>
</div>
</div>
</div>
<div class="unit-actions">
<button class="btn btn-sm btn-secondary" on:click={() => openEditModal(unit)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Düzenle
</button>
<button class="btn btn-sm btn-danger" on:click={() => handleDeleteUnit(unit)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
Sil
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
<!-- Modal forms would go here - simplified for brevity -->
<!-- You can copy the modal sections from the original file -->
<style>
.units-content {
padding: 0;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
gap: 1rem;
flex-wrap: wrap;
}
.content-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-color);
margin: 0;
}
.stats-badge {
background: var(--primary-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.count {
background: rgba(255, 255, 255, 0.2);
padding: 0.25rem 0.5rem;
border-radius: 10px;
font-weight: 700;
}
.error-message {
background: #FEE2E2;
color: #DC2626;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border: 1px solid #FECACA;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #E5E7EB;
border-top: 4px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.empty-state {
text-align: center;
padding: 3rem;
background: white;
border-radius: 12px;
border: 1px solid var(--card-border-color);
}
.empty-icon {
color: var(--text-secondary);
margin-bottom: 1rem;
}
.empty-state h3 {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-color);
margin-bottom: 0.5rem;
}
.empty-state p {
color: var(--text-secondary);
margin-bottom: 2rem;
}
.units-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 1.5rem;
}
.unit-card {
background: white;
border: 1px solid var(--card-border-color);
border-radius: 12px;
padding: 1.5rem;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.unit-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.unit-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--card-border-color);
}
.unit-name {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-color);
margin: 0 0 0.5rem 0;
}
.unit-address {
color: var(--text-secondary);
margin: 0;
font-size: 0.9rem;
}
.unit-details {
margin-bottom: 1rem;
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.detail-label {
font-weight: 500;
color: var(--text-secondary);
}
.detail-value {
font-weight: 500;
color: var(--text-color);
}
.commander-section {
background: #F9FAFB;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.commander-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-color);
margin: 0 0 0.75rem 0;
}
.commander-name {
font-weight: 600;
color: var(--text-color);
margin: 0 0 0.5rem 0;
}
.commander-detail {
font-size: 0.85rem;
color: var(--text-secondary);
margin: 0.25rem 0;
}
.unit-actions {
display: flex;
gap: 0.5rem;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.btn-danger {
background: #DC2626;
color: white;
border: 1px solid #B91C1C;
}
.btn-danger:hover {
background: #B91C1C;
}
/* Responsive Tasarım */
@media (max-width: 768px) {
.content-header {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.content-title {
font-size: 1.25rem;
}
.units-grid {
grid-template-columns: 1fr;
}
.unit-actions {
justify-content: stretch;
}
.detail-item {
flex-direction: column;
gap: 0.25rem;
}
}
</style>

View File

@@ -0,0 +1,640 @@
<script>
import { onMount } from 'svelte';
// Props
export let user = null;
let vehicles = [];
let loading = true;
let error = '';
let showAddModal = false;
let showEditModal = false;
let selectedVehicle = null;
// Form değişkenleri
let formData = {
brand: '',
model: '',
year: new Date().getFullYear(),
plate: ''
};
onMount(async () => {
await loadVehicles();
});
async function loadVehicles() {
try {
const response = await fetch('/api/vehicles');
if (response.ok) {
const data = await response.json();
vehicles = data.vehicles;
} else {
error = 'Araçlar yüklenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Load vehicles error:', err);
} finally {
loading = false;
}
}
function resetForm() {
formData = {
brand: '',
model: '',
year: new Date().getFullYear(),
plate: ''
};
selectedVehicle = null;
}
function openAddModal() {
resetForm();
showAddModal = true;
}
function openEditModal(vehicle) {
selectedVehicle = vehicle;
formData = {
brand: vehicle.brand,
model: vehicle.model,
year: vehicle.year,
plate: vehicle.plate
};
showEditModal = true;
}
function closeModal() {
showAddModal = false;
showEditModal = false;
resetForm();
}
async function handleAddVehicle() {
if (!formData.brand || !formData.model || !formData.year || !formData.plate) {
error = 'Tüm alanlar zorunludur.';
return;
}
try {
const response = await fetch('/api/vehicles', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (response.ok) {
await loadVehicles();
closeModal();
error = '';
} else {
error = data.message || 'Araç eklenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Add vehicle error:', err);
}
}
async function handleUpdateVehicle() {
if (!formData.brand || !formData.model || !formData.year || !formData.plate) {
error = 'Tüm alanlar zorunludur.';
return;
}
try {
const response = await fetch('/api/vehicles', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: selectedVehicle.id,
...formData
}),
});
const data = await response.json();
if (response.ok) {
await loadVehicles();
closeModal();
error = '';
} else {
error = data.message || 'Araç güncellenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Update vehicle error:', err);
}
}
async function handleDeleteVehicle(vehicle) {
if (!confirm(`${vehicle.brand} ${vehicle.model} (${vehicle.plate}) aracını silmek istediğinizden emin misiniz?`)) {
return;
}
try {
const response = await fetch('/api/vehicles', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: vehicle.id }),
});
const data = await response.json();
if (response.ok) {
await loadVehicles();
error = '';
} else {
error = data.message || 'Araç silinemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Delete vehicle error:', err);
}
}
</script>
<div class="vehicles-content">
<div class="content-header">
<h1 class="content-title">Araç Yönetimi</h1>
<div class="stats-badge">
<span class="count">{vehicles.length}</span>
<span>Araç</span>
</div>
<button class="btn btn-primary" on:click={openAddModal}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Yeni Araç Ekle
</button>
</div>
{#if error}
<div class="error-message">
{error}
</div>
{/if}
{#if loading}
<div class="loading-container">
<div class="spinner"></div>
<p>Yükleniyor...</p>
</div>
{:else if vehicles.length === 0}
<div class="empty-state">
<div class="empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 9l-7 7-7-7"/>
<rect x="11" y="5" width="2" height="14"/>
<path d="M5 5v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V5"/>
</svg>
</div>
<h3>Henüz Araç Yok</h3>
<p>Sisteme araç eklemek için "Yeni Araç Ekle" butonuna tıklayın.</p>
<button class="btn btn-primary" on:click={openAddModal}>
İlk Aracı Ekle
</button>
</div>
{:else}
<div class="vehicles-grid">
{#each vehicles as vehicle (vehicle.id)}
<div class="vehicle-card card">
<div class="vehicle-header">
<div class="vehicle-info">
<h3 class="vehicle-name">{vehicle.brand} {vehicle.model}</h3>
<p class="vehicle-year">{vehicle.year}</p>
</div>
<div class="vehicle-plate">
<span class="plate-badge"><i class="fas fa-car"></i> {vehicle.plate}</span>
</div>
</div>
<div class="vehicle-actions">
<button class="btn btn-sm btn-secondary" on:click={() => openEditModal(vehicle)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Düzenle
</button>
<button class="btn btn-sm btn-danger" on:click={() => handleDeleteVehicle(vehicle)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
Sil
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
<!-- Araç Ekle Modal -->
{#if showAddModal}
<div class="modal-overlay" on:click={closeModal}>
<div class="modal" on:click|stopPropagation>
<div class="modal-header">
<h2>Yeni Araç Ekle</h2>
<button class="modal-close" on:click={closeModal}>×</button>
</div>
<form on:submit|preventDefault={handleAddVehicle} class="modal-form">
<div class="form-group">
<label for="brand">Marka</label>
<input
id="brand"
type="text"
class="form-input"
bind:value={formData.brand}
placeholder="Toyota, Ford, vb."
required
/>
</div>
<div class="form-group">
<label for="model">Model</label>
<input
id="model"
type="text"
class="form-input"
bind:value={formData.model}
placeholder="Corolla, Transit, vb."
required
/>
</div>
<div class="form-group">
<label for="year">Yıl</label>
<input
id="year"
type="number"
class="form-input"
bind:value={formData.year}
min="1900"
max={new Date().getFullYear() + 1}
required
/>
</div>
<div class="form-group">
<label for="plate">Plaka</label>
<input
id="plate"
type="text"
class="form-input"
bind:value={formData.plate}
placeholder="34ABC123"
required
/>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" on:click={closeModal}>İptal</button>
<button type="submit" class="btn btn-primary">Kaydet</button>
</div>
</form>
</div>
</div>
{/if}
<!-- Araç Düzenle Modal -->
{#if showEditModal}
<div class="modal-overlay" on:click={closeModal}>
<div class="modal" on:click|stopPropagation>
<div class="modal-header">
<h2>Araç Düzenle</h2>
<button class="modal-close" on:click={closeModal}>×</button>
</div>
<form on:submit|preventDefault={handleUpdateVehicle} class="modal-form">
<div class="form-group">
<label for="edit-brand">Marka</label>
<input
id="edit-brand"
type="text"
class="form-input"
bind:value={formData.brand}
placeholder="Toyota, Ford, vb."
required
/>
</div>
<div class="form-group">
<label for="edit-model">Model</label>
<input
id="edit-model"
type="text"
class="form-input"
bind:value={formData.model}
placeholder="Corolla, Transit, vb."
required
/>
</div>
<div class="form-group">
<label for="edit-year">Yıl</label>
<input
id="edit-year"
type="number"
class="form-input"
bind:value={formData.year}
min="1900"
max={new Date().getFullYear() + 1}
required
/>
</div>
<div class="form-group">
<label for="edit-plate">Plaka</label>
<input
id="edit-plate"
type="text"
class="form-input"
bind:value={formData.plate}
placeholder="34ABC123"
required
/>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" on:click={closeModal}>İptal</button>
<button type="submit" class="btn btn-primary">Güncelle</button>
</div>
</form>
</div>
</div>
{/if}
<style>
.vehicles-content {
padding: 0;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
gap: 1rem;
flex-wrap: wrap;
}
.content-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-color);
margin: 0;
}
.stats-badge {
background: var(--primary-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.count {
background: rgba(255, 255, 255, 0.2);
padding: 0.25rem 0.5rem;
border-radius: 10px;
font-weight: 700;
}
.error-message {
background: #FEE2E2;
color: #DC2626;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border: 1px solid #FECACA;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #E5E7EB;
border-top: 4px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.empty-state {
text-align: center;
padding: 3rem;
background: white;
border-radius: 12px;
border: 1px solid var(--card-border-color);
}
.empty-icon {
color: var(--text-secondary);
margin-bottom: 1rem;
}
.empty-state h3 {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-color);
margin-bottom: 0.5rem;
}
.empty-state p {
color: var(--text-secondary);
margin-bottom: 2rem;
}
.vehicles-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.vehicle-card {
background: white;
border: 1px solid var(--card-border-color);
border-radius: 12px;
padding: 1.5rem;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.vehicle-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.vehicle-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.vehicle-info h3 {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-color);
margin: 0 0 0.25rem 0;
}
.vehicle-year {
color: var(--text-secondary);
margin: 0;
}
.plate-badge {
background: var(--primary-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 500;
font-size: 0.9rem;
}
.vehicle-actions {
display: flex;
gap: 0.5rem;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.btn-danger {
background: #DC2626;
color: white;
border: 1px solid #B91C1C;
}
.btn-danger:hover {
background: #B91C1C;
}
/* Modal Stilleri */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal {
background: white;
border-radius: 12px;
max-width: 500px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--card-border-color);
}
.modal-header h2 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
color: var(--text-color);
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem;
}
.modal-form {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-color);
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--card-border-color);
}
/* Responsive Tasarım */
@media (max-width: 768px) {
.content-header {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.content-title {
font-size: 1.25rem;
}
.vehicles-grid {
grid-template-columns: 1fr;
}
.vehicle-header {
flex-direction: column;
gap: 1rem;
}
.vehicle-actions {
justify-content: stretch;
}
.modal {
margin: 0;
max-height: 100vh;
}
.modal-actions {
flex-direction: column;
}
}
</style>