Files
2025-10-12 09:53:00 +02:00

2313 lines
64 KiB
Vue
Raw Permalink 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.
<template>
<div class="admin">
<div class="admin-header">
<h1>🔧 Beheer Dashboard</h1>
<p>Beheer fiets service aanvragen, geblokkeerde data en instellingen</p>
<div class="admin-user-info">
<span>Ingelogd als: {{ user?.name }}</span>
<button @click="handleLogout" class="logout-btn">Uitloggen</button>
</div>
</div>
<div class="admin-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
:class="['tab-button', { active: activeTab === tab.id }]"
>
{{ tab.label }}
</button>
</div>
<!-- Service Requests Tab -->
<div v-if="activeTab === 'requests'" class="tab-content">
<div class="section-header">
<h2>Service Aanvragen</h2>
<button @click="refreshRequests" class="refresh-btn" :disabled="loading" :title="loading ? 'Verversen...' : 'Verversen'">
{{ loading ? '🔄' : '🔄' }}
</button>
</div>
<!-- Status Filter -->
<div class="status-filter">
<div class="filter-title">Filter op status:</div>
<div class="filter-checkboxes">
<label class="filter-checkbox">
<input type="checkbox" v-model="statusFilters.pending" />
<span class="checkbox-label">
<span :class="['status-badge', 'pending']">{{ getStatusText('pending') }}</span>
</span>
</label>
<label class="filter-checkbox">
<input type="checkbox" v-model="statusFilters.confirmed" />
<span class="checkbox-label">
<span :class="['status-badge', 'confirmed']">{{ getStatusText('confirmed') }}</span>
</span>
</label>
<label class="filter-checkbox">
<input type="checkbox" v-model="statusFilters.mail_sent" />
<span class="checkbox-label">
<span :class="['status-badge', 'mail_sent']">{{ getStatusText('mail_sent') }}</span>
</span>
</label>
<label class="filter-checkbox">
<input type="checkbox" v-model="statusFilters.completed" />
<span class="checkbox-label">
<span :class="['status-badge', 'completed']">{{ getStatusText('completed') }}</span>
</span>
</label>
<label class="filter-checkbox">
<input type="checkbox" v-model="statusFilters.cancelled" />
<span class="checkbox-label">
<span :class="['status-badge', 'cancelled']">{{ getStatusText('cancelled') }}</span>
</span>
</label>
</div>
</div>
<div class="requests-table">
<div class="table-header">
<div class="header-cell">ID</div>
<div class="header-cell">Naam</div>
<div class="header-cell">Datum/Tijd</div>
<div class="header-cell">Status</div>
<div class="header-cell">Acties</div>
</div>
<div v-if="loading" class="loading-row">
<div class="loading-text">Aanvragen laden...</div>
</div>
<div v-else-if="filteredRequests.length === 0" class="empty-row">
<div class="empty-text">Geen service aanvragen gevonden met de geselecteerde filters</div>
</div>
<div v-else class="table-row" v-for="request in filteredRequests" :key="request.id">
<div class="cell">{{ request.id }}</div>
<div class="cell">{{ request.name }}</div>
<div class="cell">{{ formatDateTime(request.requested_date, request.requested_time) }}</div>
<div class="cell">
<span :class="['status-badge', request.status]">{{ getStatusText(request.status) }}</span>
</div>
<div class="cell actions">
<button @click="viewRequest(request)" class="action-btn view" title="Bekijken">
👁️
</button>
<button @click="sendEmail(request)" class="action-btn email" :disabled="!request.email" title="E-mail versturen">
📧
</button>
<button @click="sendSms(request)" class="action-btn sms" :disabled="!request.phone" title="SMS versturen">
📱
</button>
<button @click="viewCommunications(request)" class="action-btn comm" title="Communicatie bekijken">
💬
</button>
</div>
</div>
</div>
</div>
<!-- Blocked Dates Tab -->
<div v-if="activeTab === 'blocked'" class="tab-content">
<div class="section-header">
<h2>Geblokkeerde Data</h2>
<button @click="showAddBlockedDate = true" class="add-btn" title="Geblokkeerde Datum Toevoegen">➕</button>
</div>
<div class="blocked-dates-list">
<div v-for="date in blockedDates" :key="date.id" class="blocked-date-item">
<div class="date-info">
<span class="date">{{ formatDate(date.date) }}</span>
<span v-if="date.reason" class="reason">{{ date.reason }}</span>
</div>
<div class="date-actions">
<button @click="editBlockedDate(date)" class="edit-btn" title="Bewerken">✏️</button>
<button @click="deleteBlockedDate(date.id)" class="delete-btn" title="Verwijderen">🗑️</button>
</div>
</div>
</div>
</div>
<!-- Pricelist Tab -->
<div v-if="activeTab === 'pricelist'" class="tab-content">
<div class="section-header">
<h2>Prijslijst Beheer</h2>
<button @click="showAddPricelistItem = true" class="add-btn" title="Prijslijst Item Toevoegen">➕</button>
</div>
<div class="pricelist-table">
<div class="table-header">
<div class="header-cell">Titel</div>
<div class="header-cell">Beschrijving</div>
<div class="header-cell">Prijs</div>
<div class="header-cell">Volgorde</div>
<div class="header-cell">Status</div>
<div class="header-cell">Acties</div>
</div>
<div v-if="pricelistLoading" class="loading-row">
<div class="loading-text">Prijslijst laden...</div>
</div>
<div v-else-if="pricelist.length === 0" class="empty-row">
<div class="empty-text">Geen prijslijst items gevonden</div>
</div>
<div v-else class="table-row" v-for="item in pricelist" :key="item.id">
<div class="cell">{{ item.title }}</div>
<div class="cell">{{ item.description }}</div>
<div class="cell">€{{ item.price }}</div>
<div class="cell">{{ item.sort_order }}</div>
<div class="cell">
<span :class="['status-badge', item.is_active ? 'active' : 'inactive']">
{{ item.is_active ? 'Actief' : 'Inactief' }}
</span>
</div>
<div class="cell actions">
<button @click="editPricelistItem(item)" class="action-btn edit" title="Bewerken">✏️</button>
<button @click="deletePricelistItem(item.id)" class="action-btn delete" title="Verwijderen">🗑️</button>
</div>
</div>
</div>
</div>
<!-- Page Content Tab -->
<div v-if="activeTab === 'content'" class="tab-content">
<div class="section-header">
<h2>Pagina Teksten Beheer</h2>
</div>
<div class="content-sections">
<!-- Home Page Hero -->
<div class="content-section">
<h3>Openingspagina - Hero Sectie</h3>
<div class="content-form">
<div class="form-group">
<label>Hoofdtekst (Header)</label>
<input type="text" :value="getContentValue('home_hero_title')" @input="updateContentValue('home_hero_title', $event.target.value)" @blur="savePageContent('home_hero_title')" />
</div>
<div class="form-group">
<label>Subtekst (Subheader)</label>
<input type="text" :value="getContentValue('home_hero_subtitle')" @input="updateContentValue('home_hero_subtitle', $event.target.value)" @blur="savePageContent('home_hero_subtitle')" />
</div>
</div>
</div>
<!-- Home Page Features -->
<div class="content-section">
<h3>Openingspagina - Panelen</h3>
<div class="content-form">
<div class="feature-group">
<h4>Paneel 1</h4>
<div class="form-group">
<label>Koptekst</label>
<input type="text" :value="getContentValue('home_feature_1_title')" @input="updateContentValue('home_feature_1_title', $event.target.value)" @blur="savePageContent('home_feature_1_title')" />
</div>
<div class="form-group">
<label>Onderschrift</label>
<textarea :value="getContentValue('home_feature_1_text')" @input="updateContentValue('home_feature_1_text', $event.target.value)" @blur="savePageContent('home_feature_1_text')" rows="2"></textarea>
</div>
</div>
<div class="feature-group">
<h4>Paneel 2</h4>
<div class="form-group">
<label>Koptekst</label>
<input type="text" :value="getContentValue('home_feature_2_title')" @input="updateContentValue('home_feature_2_title', $event.target.value)" @blur="savePageContent('home_feature_2_title')" />
</div>
<div class="form-group">
<label>Onderschrift</label>
<textarea :value="getContentValue('home_feature_2_text')" @input="updateContentValue('home_feature_2_text', $event.target.value)" @blur="savePageContent('home_feature_2_text')" rows="2"></textarea>
</div>
</div>
<div class="feature-group">
<h4>Paneel 3</h4>
<div class="form-group">
<label>Koptekst</label>
<input type="text" :value="getContentValue('home_feature_3_title')" @input="updateContentValue('home_feature_3_title', $event.target.value)" @blur="savePageContent('home_feature_3_title')" />
</div>
<div class="form-group">
<label>Onderschrift</label>
<textarea :value="getContentValue('home_feature_3_text')" @input="updateContentValue('home_feature_3_text', $event.target.value)" @blur="savePageContent('home_feature_3_text')" rows="2"></textarea>
</div>
</div>
<div class="feature-group">
<h4>Paneel 4</h4>
<div class="form-group">
<label>Koptekst</label>
<input type="text" :value="getContentValue('home_feature_4_title')" @input="updateContentValue('home_feature_4_title', $event.target.value)" @blur="savePageContent('home_feature_4_title')" />
</div>
<div class="form-group">
<label>Onderschrift</label>
<textarea :value="getContentValue('home_feature_4_text')" @input="updateContentValue('home_feature_4_text', $event.target.value)" @blur="savePageContent('home_feature_4_text')" rows="2"></textarea>
</div>
</div>
</div>
</div>
<!-- Home Page CTA -->
<div class="content-section">
<h3>Openingspagina - Knoppen Container</h3>
<div class="content-form">
<div class="form-group">
<label>Hoofdtekst</label>
<input type="text" :value="getContentValue('home_cta_title')" @input="updateContentValue('home_cta_title', $event.target.value)" @blur="savePageContent('home_cta_title')" />
</div>
<div class="form-group">
<label>Subtekst</label>
<textarea :value="getContentValue('home_cta_subtitle')" @input="updateContentValue('home_cta_subtitle', $event.target.value)" @blur="savePageContent('home_cta_subtitle')" rows="2"></textarea>
</div>
<div class="form-group">
<label>Knop 1 Tekst</label>
<input type="text" :value="getContentValue('home_cta_button_1_text')" @input="updateContentValue('home_cta_button_1_text', $event.target.value)" @blur="savePageContent('home_cta_button_1_text')" />
</div>
<div class="form-group">
<label>Knop 2 Tekst</label>
<input type="text" :value="getContentValue('home_cta_button_2_text')" @input="updateContentValue('home_cta_button_2_text', $event.target.value)" @blur="savePageContent('home_cta_button_2_text')" />
</div>
</div>
</div>
<!-- Booking Page -->
<div class="content-section">
<h3>Boekingspagina - Header</h3>
<div class="content-form">
<div class="form-group">
<label>Hoofdtekst (Header)</label>
<input type="text" :value="getContentValue('booking_header_title')" @input="updateContentValue('booking_header_title', $event.target.value)" @blur="savePageContent('booking_header_title')" />
</div>
<div class="form-group">
<label>Onderschrift</label>
<textarea :value="getContentValue('booking_header_subtitle')" @input="updateContentValue('booking_header_subtitle', $event.target.value)" @blur="savePageContent('booking_header_subtitle')" rows="2"></textarea>
</div>
</div>
</div>
<!-- Contact Section -->
<div class="content-section">
<h3>Openingspagina - Contact Sectie</h3>
<div class="content-form">
<div class="form-group">
<label>Hoofdtekst (Header)</label>
<input type="text" :value="getContentValue('home_contact_title')" @input="updateContentValue('home_contact_title', $event.target.value)" @blur="savePageContent('home_contact_title')" />
</div>
<div class="form-group">
<label>Onderschrift</label>
<textarea :value="getContentValue('home_contact_subtitle')" @input="updateContentValue('home_contact_subtitle', $event.target.value)" @blur="savePageContent('home_contact_subtitle')" rows="2"></textarea>
</div>
<div class="form-group">
<label>Telefoonnummer</label>
<input type="text" :value="getContentValue('home_contact_phone')" @input="updateContentValue('home_contact_phone', $event.target.value)" @blur="savePageContent('home_contact_phone')" placeholder="+31 6 12345678" />
</div>
<div class="form-group">
<label>E-mailadres</label>
<input type="email" :value="getContentValue('home_contact_email')" @input="updateContentValue('home_contact_email', $event.target.value)" @blur="savePageContent('home_contact_email')" placeholder="info@fiet-service.nl" />
</div>
<div class="form-group">
<label>Adres</label>
<input type="text" :value="getContentValue('home_contact_address')" @input="updateContentValue('home_contact_address', $event.target.value)" @blur="savePageContent('home_contact_address')" placeholder="Fietsstraat 123, 1234 AB Amsterdam" />
</div>
</div>
</div>
</div>
</div>
<!-- Settings Tab -->
<div v-if="activeTab === 'settings'" class="tab-content">
<div class="section-header">
<h2>Applicatie Instellingen</h2>
</div>
<div class="settings-form">
<div class="setting-group">
<label for="admin_email">Beheer Notificatie E-mail</label>
<input
type="email"
id="admin_email"
v-model="settings.admin_notification_email"
placeholder="admin@voorbeeld.com"
/>
<button @click="updateSetting('admin_notification_email', settings.admin_notification_email)" class="save-btn" title="Opslaan">
💾
</button>
</div>
<div class="setting-group">
<label for="business_name">Bedrijfsnaam</label>
<input
type="text"
id="business_name"
v-model="settings.business_name"
placeholder="Fiets Service Centrum"
/>
<button @click="updateSetting('business_name', settings.business_name)" class="save-btn" title="Opslaan">
💾
</button>
</div>
<div class="setting-group">
<label for="pickup_confirmation_email_text">Ophaalbevestiging E-mail Tekst</label>
<textarea
id="pickup_confirmation_email_text"
v-model="settings.pickup_confirmation_email_text"
rows="8"
placeholder="Standaard tekst voor ophaalbevestiging e-mail"
></textarea>
<button @click="updateSetting('pickup_confirmation_email_text', settings.pickup_confirmation_email_text)" class="save-btn" title="Opslaan">
💾
</button>
</div>
</div>
</div>
<!-- View Request Modal -->
<div class="modal-overlay" v-if="showRequestModal" @click="closeRequestModal">
<div class="modal-content large" @click.stop>
<div class="modal-header">
<h3>Service Aanvraag Details</h3>
<button @click="closeRequestModal" class="close-btn" title="Sluiten">×</button>
</div>
<div class="modal-body" v-if="selectedRequest">
<div v-if="requestModalLoading" class="loading-state">
<div class="loading-spinner"></div>
<p>Gegevens laden...</p>
</div>
<div v-else class="request-details">
<div class="detail-row">
<strong>Naam:</strong> {{ selectedRequest.name }}
</div>
<div class="detail-row">
<strong>Adres:</strong> {{ selectedRequest.address }}
</div>
<div class="detail-row">
<strong>Plaats:</strong> {{ selectedRequest.place }}
</div>
<div class="detail-row">
<strong>E-mail:</strong> {{ selectedRequest.email || 'Niet opgegeven' }}
</div>
<div class="detail-row">
<strong>Telefoon:</strong> {{ selectedRequest.phone || 'Niet opgegeven' }}
</div>
<div class="detail-row">
<strong>Reparatie Beschrijving:</strong> {{ selectedRequest.repair_description }}
</div>
<div class="detail-row">
<strong>Gewenste Datum/Tijd:</strong> {{ formatDateTime(selectedRequest.requested_date, selectedRequest.requested_time) }}
</div>
<div class="detail-row">
<strong>Status:</strong>
<select v-model="selectedRequest.status" @change="updateRequestStatus">
<option value="pending">In afwachting</option>
<option value="confirmed">Bevestigd</option>
<option value="mail_sent">Bevestigd mail verstuurd</option>
<option value="completed">Voltooid</option>
<option value="cancelled">Geannuleerd</option>
</select>
</div>
<div class="detail-row">
<strong>Werkelijke ophaalDatum:</strong>
<input type="date" v-model="selectedRequest.actual_repair_date" @change="updateRequestDate" />
</div>
<div class="detail-row">
<strong>Werkelijke ophaaltijd:</strong>
<input type="time" v-model="selectedRequest.actual_repair_time" @change="updateRequestTime" />
</div>
</div>
</div>
<div class="modal-footer">
<button
@click="sendPickupConfirmation"
class="btn-pickup-confirmation"
:disabled="!selectedRequest?.email || !selectedRequest?.actual_repair_date || !selectedRequest?.actual_repair_time || pickupConfirmationLoading"
title="Verstuur ophaalbevestiging e-mail naar klant"
>
{{ pickupConfirmationLoading ? '📧 Versturen...' : '📧 Ophaalbevestiging Versturen' }}
</button>
<button @click="closeRequestModal" class="btn-primary">Sluiten</button>
</div>
</div>
</div>
<!-- Add/Edit Blocked Date Modal -->
<div class="modal-overlay" v-if="showAddBlockedDate" @click="closeBlockedDateModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>{{ editingBlockedDate ? 'Bewerken' : 'Toevoegen' }} Geblokkeerde Datum</h3>
<button @click="closeBlockedDateModal" class="close-btn" title="Sluiten">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="blocked_date">Datum</label>
<input type="date" id="blocked_date" v-model="blockedDateForm.date" :min="minDate" />
</div>
<div class="form-group">
<label for="blocked_reason">Reden (Optioneel)</label>
<textarea id="blocked_reason" v-model="blockedDateForm.reason" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button @click="saveBlockedDate" class="btn-primary">Opslaan</button>
<button @click="closeBlockedDateModal" class="btn-secondary">Annuleren</button>
</div>
</div>
</div>
<!-- Email Modal -->
<div class="modal-overlay" v-if="showEmailModal" @click="closeEmailModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>E-mail Versturen naar {{ selectedRequest?.name }}</h3>
<button @click="closeEmailModal" class="close-btn" title="Sluiten">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="email_message">Bericht</label>
<textarea id="email_message" v-model="emailForm.message" rows="6" placeholder="Voer je bericht in..."></textarea>
</div>
</div>
<div class="modal-footer">
<button @click="submitEmail" class="btn-primary" :disabled="emailLoading">
{{ emailLoading ? 'Versturen...' : 'E-mail Versturen' }}
</button>
<button @click="closeEmailModal" class="btn-secondary">Annuleren</button>
</div>
</div>
</div>
<!-- SMS Modal -->
<div class="modal-overlay" v-if="showSmsModal" @click="closeSmsModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>SMS Versturen naar {{ selectedRequest?.name }}</h3>
<button @click="closeSmsModal" class="close-btn" title="Sluiten">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="sms_message">Bericht</label>
<textarea id="sms_message" v-model="smsForm.message" rows="6" placeholder="Voer je bericht in..."></textarea>
</div>
</div>
<div class="modal-footer">
<button @click="submitSms" class="btn-primary" :disabled="smsLoading">
{{ smsLoading ? 'Versturen...' : 'SMS Versturen' }}
</button>
<button @click="closeSmsModal" class="btn-secondary">Annuleren</button>
</div>
</div>
</div>
<!-- Communications Modal -->
<div class="modal-overlay" v-if="showCommunicationsModal" @click="closeCommunicationsModal">
<div class="modal-content large" @click.stop>
<div class="modal-header">
<h3>Communicatie Geschiedenis - {{ selectedRequest?.name }}</h3>
<button @click="closeCommunicationsModal" class="close-btn" title="Sluiten">×</button>
</div>
<div class="modal-body">
<div v-if="communications.length === 0" class="no-communications">
Geen communicatie gevonden voor deze aanvraag.
</div>
<div v-else class="communications-list">
<div v-for="comm in communications" :key="comm.id" class="communication-item">
<div class="comm-header">
<span class="comm-type">{{ getCommTypeText(comm.type) }}</span>
<span class="comm-direction">{{ getCommDirectionText(comm.direction) }}</span>
<span class="comm-date">{{ formatDateTime(comm.created_at) }}</span>
</div>
<div class="comm-message">{{ comm.message }}</div>
</div>
</div>
</div>
<div class="modal-footer">
<button @click="closeCommunicationsModal" class="btn-primary">Sluiten</button>
</div>
</div>
</div>
<!-- Pricelist Modal -->
<div class="modal-overlay" v-if="showAddPricelistItem" @click="closePricelistModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>{{ editingPricelistItem ? 'Bewerken' : 'Toevoegen' }} Prijslijst Item</h3>
<button @click="closePricelistModal" class="close-btn" title="Sluiten">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="pricelist_title">Titel</label>
<input type="text" id="pricelist_title" v-model="pricelistForm.title" placeholder="Bijv. Banden vervangen" />
</div>
<div class="form-group">
<label for="pricelist_description">Beschrijving</label>
<textarea id="pricelist_description" v-model="pricelistForm.description" rows="3" placeholder="Beschrijving van de service"></textarea>
</div>
<div class="form-group">
<label for="pricelist_price">Prijs (€)</label>
<input type="number" id="pricelist_price" v-model="pricelistForm.price" step="0.01" min="0" placeholder="25.00" />
</div>
<div class="form-group">
<label for="pricelist_sort_order">Volgorde</label>
<input type="number" id="pricelist_sort_order" v-model="pricelistForm.sort_order" min="0" placeholder="1" />
</div>
<div class="form-group">
<label>
<input type="checkbox" v-model="pricelistForm.is_active" />
Actief
</label>
</div>
</div>
<div class="modal-footer">
<button @click="savePricelistItem" class="btn-primary">Opslaan</button>
<button @click="closePricelistModal" class="btn-secondary">Annuleren</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
export default {
name: 'Admin',
setup() {
const router = useRouter()
const authStore = useAuthStore()
const activeTab = ref('requests')
const loading = ref(false)
const requests = ref([])
const blockedDates = ref([])
const settings = ref({})
const pricelist = ref([])
const pageContent = ref([])
// Status filter state
const statusFilters = ref({
pending: true,
confirmed: true,
mail_sent: true,
completed: true,
cancelled: true
})
// Modal states
const showRequestModal = ref(false)
const showAddBlockedDate = ref(false)
const showEmailModal = ref(false)
const showSmsModal = ref(false)
const showCommunicationsModal = ref(false)
const showAddPricelistItem = ref(false)
const requestModalLoading = ref(false)
const pricelistLoading = ref(false)
// Form data
const selectedRequest = ref(null)
const editingBlockedDate = ref(null)
const editingPricelistItem = ref(null)
const blockedDateForm = ref({ date: '', reason: '' })
const pricelistForm = ref({ title: '', description: '', price: '', sort_order: 0, is_active: true })
const emailForm = ref({ message: '' })
const smsForm = ref({ message: '' })
const communications = ref([])
// Loading states
const emailLoading = ref(false)
const smsLoading = ref(false)
const pickupConfirmationLoading = ref(false)
const tabs = [
{ id: 'requests', label: 'Service Aanvragen' },
{ id: 'blocked', label: 'Geblokkeerde Data' },
{ id: 'pricelist', label: 'Prijslijst' },
{ id: 'content', label: 'Pagina Teksten' },
{ id: 'settings', label: 'Instellingen' }
]
const minDate = computed(() => {
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
return tomorrow.toISOString().split('T')[0]
})
// Filtered requests based on status checkboxes
const filteredRequests = computed(() => {
return requests.value.filter(request => {
return statusFilters.value[request.status] === true
})
})
// Helper functions for Dutch text
const getStatusText = (status) => {
const statusMap = {
'pending': 'In afwachting',
'confirmed': 'Bevestigd',
'mail_sent': 'Bevestigd mail verstuurd',
'completed': 'Voltooid',
'cancelled': 'Geannuleerd'
}
return statusMap[status] || status
}
const getCommTypeText = (type) => {
const typeMap = {
'email': 'E-MAIL',
'sms': 'SMS'
}
return typeMap[type] || type.toUpperCase()
}
const getCommDirectionText = (direction) => {
const directionMap = {
'admin_to_customer': 'Naar Klant',
'customer_to_admin': 'Van Klant'
}
return directionMap[direction] || direction
}
// Logout function
const handleLogout = async () => {
try {
await fetch('/api/logout', {
method: 'POST',
headers: authStore.getAuthHeaders()
})
} catch (error) {
console.error('Logout error:', error)
} finally {
authStore.logout()
router.push('/login')
}
}
// Fetch data
const fetchRequests = async () => {
try {
const response = await fetch('/api/service-requests', {
headers: authStore.getAuthHeaders()
})
if (response.ok) {
requests.value = await response.json()
}
} catch (error) {
console.error('Failed to fetch requests:', error)
}
}
const fetchBlockedDates = async () => {
try {
const response = await fetch('/api/blocked-dates', {
headers: authStore.getAuthHeaders()
})
if (response.ok) {
blockedDates.value = await response.json()
}
} catch (error) {
console.error('Failed to fetch blocked dates:', error)
}
}
const fetchSettings = async () => {
try {
const response = await fetch('/api/settings', {
headers: authStore.getAuthHeaders()
})
if (response.ok) {
const settingsData = await response.json()
settingsData.forEach(setting => {
settings.value[setting.key] = setting.value
})
}
} catch (error) {
console.error('Failed to fetch settings:', error)
}
}
const fetchPricelist = async () => {
try {
const response = await fetch('/api/admin/pricelist', {
headers: authStore.getAuthHeaders()
})
if (response.ok) {
pricelist.value = await response.json()
}
} catch (error) {
console.error('Failed to fetch pricelist:', error)
}
}
const fetchPageContent = async () => {
try {
const response = await fetch('/api/page-content', {
headers: authStore.getAuthHeaders()
})
if (response.ok) {
pageContent.value = await response.json()
}
} catch (error) {
console.error('Failed to fetch page content:', error)
}
}
const refreshRequests = async () => {
loading.value = true
await fetchRequests()
loading.value = false
}
// Modal functions
const viewRequest = async (request) => {
requestModalLoading.value = true
showRequestModal.value = true
// Get the latest data from the database to ensure we have the most current information
try {
const response = await fetch(`/api/service-requests/${request.id}`, {
headers: authStore.getAuthHeaders()
})
if (response.ok) {
const latestRequest = await response.json()
// Format dates for HTML date inputs (YYYY-MM-DD)
if (latestRequest.actual_repair_date) {
// Extract just the date part if it's a datetime string
latestRequest.actual_repair_date = latestRequest.actual_repair_date.split('T')[0]
}
selectedRequest.value = { ...latestRequest }
} else {
// Fallback to local data if API call fails
const latestRequest = requests.value.find(r => r.id === request.id)
if (latestRequest && latestRequest.actual_repair_date) {
latestRequest.actual_repair_date = latestRequest.actual_repair_date.split('T')[0]
}
selectedRequest.value = latestRequest ? { ...latestRequest } : { ...request }
}
} catch (error) {
console.error('Failed to fetch latest request data:', error)
// Fallback to local data if API call fails
const latestRequest = requests.value.find(r => r.id === request.id)
if (latestRequest && latestRequest.actual_repair_date) {
latestRequest.actual_repair_date = latestRequest.actual_repair_date.split('T')[0]
}
selectedRequest.value = latestRequest ? { ...latestRequest } : { ...request }
} finally {
requestModalLoading.value = false
}
}
const closeRequestModal = () => {
showRequestModal.value = false
selectedRequest.value = null
}
const closeBlockedDateModal = () => {
showAddBlockedDate.value = false
editingBlockedDate.value = null
blockedDateForm.value = { date: '', reason: '' }
}
const closeEmailModal = () => {
showEmailModal.value = false
emailForm.value.message = ''
}
const closeSmsModal = () => {
showSmsModal.value = false
smsForm.value.message = ''
}
const closeCommunicationsModal = () => {
showCommunicationsModal.value = false
communications.value = []
}
const closePricelistModal = () => {
showAddPricelistItem.value = false
editingPricelistItem.value = null
pricelistForm.value = { title: '', description: '', price: '', sort_order: 0, is_active: true }
}
// Action functions
const sendEmail = (request) => {
selectedRequest.value = request
showEmailModal.value = true
}
const sendSms = (request) => {
selectedRequest.value = request
showSmsModal.value = true
}
const viewCommunications = async (request) => {
selectedRequest.value = request
try {
const response = await fetch(`/api/service-requests/${request.id}/communications`, {
headers: authStore.getAuthHeaders()
})
if (response.ok) {
communications.value = await response.json()
}
} catch (error) {
console.error('Failed to fetch communications:', error)
}
showCommunicationsModal.value = true
}
const submitEmail = async () => {
if (!emailForm.value.message.trim()) return
emailLoading.value = true
try {
const response = await fetch(`/api/service-requests/${selectedRequest.value.id}/send-email`, {
method: 'POST',
headers: authStore.getAuthHeaders(),
body: JSON.stringify({ message: emailForm.value.message })
})
if (response.ok) {
closeEmailModal()
await fetchRequests() // Refresh to show new communication
}
} catch (error) {
console.error('Failed to send email:', error)
} finally {
emailLoading.value = false
}
}
const submitSms = async () => {
if (!smsForm.value.message.trim()) return
smsLoading.value = true
try {
const response = await fetch(`/api/service-requests/${selectedRequest.value.id}/send-sms`, {
method: 'POST',
headers: authStore.getAuthHeaders(),
body: JSON.stringify({ message: smsForm.value.message })
})
if (response.ok) {
closeSmsModal()
await fetchRequests() // Refresh to show new communication
}
} catch (error) {
console.error('Failed to send SMS:', error)
} finally {
smsLoading.value = false
}
}
const sendPickupConfirmation = async () => {
if (!selectedRequest.value?.email) {
alert('Geen e-mailadres beschikbaar voor deze klant')
return
}
if (!selectedRequest.value?.actual_repair_date || !selectedRequest.value?.actual_repair_time) {
alert('Vul eerst de werkelijke ophaaldatum en tijd in')
return
}
if (!confirm('Weet u zeker dat u de ophaalbevestiging wilt versturen naar ' + selectedRequest.value.email + '?')) {
return
}
pickupConfirmationLoading.value = true
try {
const response = await fetch(`/api/service-requests/${selectedRequest.value.id}/send-pickup-confirmation`, {
method: 'POST',
headers: authStore.getAuthHeaders()
})
if (response.ok) {
const data = await response.json()
alert('Ophaalbevestiging succesvol verzonden!')
// Update the selected request with the new status
if (data.service_request) {
selectedRequest.value = data.service_request
}
// Refresh the requests list
await fetchRequests()
} else {
const error = await response.json()
alert('Fout bij versturen: ' + (error.message || 'Onbekende fout'))
}
} catch (error) {
console.error('Failed to send pickup confirmation:', error)
alert('Fout bij versturen van ophaalbevestiging')
} finally {
pickupConfirmationLoading.value = false
}
}
const editBlockedDate = (date) => {
editingBlockedDate.value = date
blockedDateForm.value = { date: date.date, reason: date.reason || '' }
showAddBlockedDate.value = true
}
const saveBlockedDate = async () => {
try {
const url = editingBlockedDate.value
? `/api/blocked-dates/${editingBlockedDate.value.id}`
: '/api/blocked-dates'
const method = editingBlockedDate.value ? 'PUT' : 'POST'
const response = await fetch(url, {
method,
headers: authStore.getAuthHeaders(),
body: JSON.stringify(blockedDateForm.value)
})
if (response.ok) {
closeBlockedDateModal()
await fetchBlockedDates()
}
} catch (error) {
console.error('Failed to save blocked date:', error)
}
}
const deleteBlockedDate = async (id) => {
if (!confirm('Weet je zeker dat je deze geblokkeerde datum wilt verwijderen?')) return
try {
const response = await fetch(`/api/blocked-dates/${id}`, {
method: 'DELETE',
headers: authStore.getAuthHeaders()
})
if (response.ok) {
await fetchBlockedDates()
}
} catch (error) {
console.error('Failed to delete blocked date:', error)
}
}
const updateSetting = async (key, value) => {
try {
const response = await fetch(`/api/settings/key/${key}`, {
method: 'PUT',
headers: authStore.getAuthHeaders(),
body: JSON.stringify({ value, description: '' })
})
if (response.ok) {
// Show success feedback
console.log(`Setting ${key} updated successfully`)
}
} catch (error) {
console.error('Failed to update setting:', error)
}
}
const updateRequestStatus = async () => {
try {
const response = await fetch(`/api/service-requests/${selectedRequest.value.id}`, {
method: 'PUT',
headers: authStore.getAuthHeaders(),
body: JSON.stringify({ status: selectedRequest.value.status })
})
if (response.ok) {
// Update the local request in the requests array
const index = requests.value.findIndex(r => r.id === selectedRequest.value.id)
if (index !== -1) {
requests.value[index].status = selectedRequest.value.status
}
await fetchRequests()
}
} catch (error) {
console.error('Failed to update request status:', error)
}
}
const updateRequestDate = async () => {
try {
const response = await fetch(`/api/service-requests/${selectedRequest.value.id}`, {
method: 'PUT',
headers: authStore.getAuthHeaders(),
body: JSON.stringify({ actual_repair_date: selectedRequest.value.actual_repair_date })
})
if (response.ok) {
// Update the local request in the requests array
const index = requests.value.findIndex(r => r.id === selectedRequest.value.id)
if (index !== -1) {
requests.value[index].actual_repair_date = selectedRequest.value.actual_repair_date
}
await fetchRequests()
}
} catch (error) {
console.error('Failed to update request date:', error)
}
}
const updateRequestTime = async () => {
try {
const response = await fetch(`/api/service-requests/${selectedRequest.value.id}`, {
method: 'PUT',
headers: authStore.getAuthHeaders(),
body: JSON.stringify({
actual_repair_date: selectedRequest.value.actual_repair_date,
actual_repair_time: selectedRequest.value.actual_repair_time
})
})
if (response.ok) {
// Update the local request in the requests array
const index = requests.value.findIndex(r => r.id === selectedRequest.value.id)
if (index !== -1) {
requests.value[index].actual_repair_date = selectedRequest.value.actual_repair_date
requests.value[index].actual_repair_time = selectedRequest.value.actual_repair_time
}
await fetchRequests()
}
} catch (error) {
console.error('Failed to update request time:', error)
}
}
// Pricelist functions
const editPricelistItem = (item) => {
editingPricelistItem.value = item
pricelistForm.value = { ...item }
showAddPricelistItem.value = true
}
const savePricelistItem = async () => {
try {
const url = editingPricelistItem.value
? `/api/pricelist/${editingPricelistItem.value.id}`
: '/api/pricelist'
const method = editingPricelistItem.value ? 'PUT' : 'POST'
const response = await fetch(url, {
method,
headers: authStore.getAuthHeaders(),
body: JSON.stringify(pricelistForm.value)
})
if (response.ok) {
closePricelistModal()
await fetchPricelist()
}
} catch (error) {
console.error('Failed to save pricelist item:', error)
}
}
const deletePricelistItem = async (id) => {
if (!confirm('Weet je zeker dat je dit prijslijst item wilt verwijderen?')) return
try {
const response = await fetch(`/api/pricelist/${id}`, {
method: 'DELETE',
headers: authStore.getAuthHeaders()
})
if (response.ok) {
await fetchPricelist()
}
} catch (error) {
console.error('Failed to delete pricelist item:', error)
}
}
// Page content functions
const getContentValue = (key) => {
const content = pageContent.value.find(c => c.key === key)
return content ? content.value : ''
}
const updateContentValue = (key, value) => {
const content = pageContent.value.find(c => c.key === key)
if (content) {
content.value = value
}
}
const savePageContent = async (key) => {
const content = pageContent.value.find(c => c.key === key)
if (!content) return
try {
const response = await fetch(`/api/page-content/key/${key}`, {
method: 'PUT',
headers: authStore.getAuthHeaders(),
body: JSON.stringify({
value: content.value,
description: content.description
})
})
if (response.ok) {
console.log(`Content ${key} updated successfully`)
}
} catch (error) {
console.error('Failed to update page content:', error)
}
}
// Utility functions
const formatDateTime = (date, time) => {
if (!date) return 'N/A'
const dateObj = new Date(date)
const formattedDate = dateObj.toLocaleDateString('nl-NL')
return time ? `${formattedDate} ${time}` : formattedDate
}
const formatDate = (date) => {
return new Date(date).toISOString().split('T')[0]
}
onMounted(() => {
// Check authentication
authStore.checkAuth()
fetchRequests()
fetchBlockedDates()
fetchPricelist()
fetchPageContent()
fetchSettings()
})
return {
activeTab,
tabs,
loading,
requests,
filteredRequests,
statusFilters,
blockedDates,
settings,
pricelist,
pageContent,
user: computed(() => authStore.user),
showRequestModal,
showAddBlockedDate,
showEmailModal,
showSmsModal,
showCommunicationsModal,
showAddPricelistItem,
requestModalLoading,
pricelistLoading,
selectedRequest,
editingBlockedDate,
editingPricelistItem,
blockedDateForm,
pricelistForm,
emailForm,
smsForm,
communications,
emailLoading,
smsLoading,
pickupConfirmationLoading,
minDate,
getStatusText,
getCommTypeText,
getCommDirectionText,
handleLogout,
refreshRequests,
viewRequest,
closeRequestModal,
closeBlockedDateModal,
closePricelistModal,
closeEmailModal,
closeSmsModal,
closeCommunicationsModal,
sendEmail,
sendSms,
viewCommunications,
submitEmail,
submitSms,
sendPickupConfirmation,
editBlockedDate,
saveBlockedDate,
deleteBlockedDate,
editPricelistItem,
savePricelistItem,
deletePricelistItem,
updateSetting,
updateRequestStatus,
updateRequestDate,
updateRequestTime,
getContentValue,
updateContentValue,
savePageContent,
formatDateTime,
formatDate
}
}
}
</script>
<style scoped>
.admin {
max-width: 1200px;
margin: 0 auto;
}
.admin-header {
text-align: center;
margin-bottom: 3rem;
}
.admin-header h1 {
color: var(--primary-color);
font-size: 2.5rem;
margin-bottom: 1rem;
}
.admin-header p {
color: var(--text-gray);
font-size: 1.2rem;
margin-bottom: 1rem;
}
.admin-user-info {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
background: #f8f9fa;
padding: 1rem;
border-radius: 8px;
border: 1px solid #e1e8ed;
}
.admin-user-info span {
color: #2c3e50;
font-weight: 500;
}
.logout-btn {
background: #dc3545;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.3s ease;
}
.logout-btn:hover {
background: #c82333;
}
.admin-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
border-bottom: 2px solid #e1e8ed;
}
.tab-button {
background: none;
border: none;
padding: 1rem 2rem;
font-size: 1rem;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
}
.tab-button.active {
border-bottom-color: var(--primary-color);
color: var(--primary-color);
font-weight: bold;
}
.tab-button:hover {
background: #f8f9fa;
}
.tab-content {
background: white;
border-radius: 15px;
padding: 2rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.section-header h2 {
color: var(--primary-color);
margin: 0;
}
/* Status Filter Styles */
.status-filter {
background: #f8f9fa;
padding: 1rem;
border-radius: 10px;
margin-bottom: 1.5rem;
border: 2px solid #e1e8ed;
}
.filter-title {
font-weight: bold;
color: #2c3e50;
margin-bottom: 0.75rem;
font-size: 0.95rem;
}
.filter-checkboxes {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 8px;
transition: background-color 0.3s ease;
}
.filter-checkbox:hover {
background: white;
}
.filter-checkbox input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--primary-color);
}
.checkbox-label {
display: flex;
align-items: center;
user-select: none;
}
.filter-checkbox .status-badge {
font-size: 0.85rem;
padding: 0.4rem 0.8rem;
cursor: pointer;
}
.refresh-btn, .add-btn {
background: var(--primary-color);
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 8px;
cursor: pointer;
font-size: 1.2rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.refresh-btn:hover:not(:disabled), .add-btn:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(44, 95, 93, 0.3);
}
.refresh-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
opacity: 0.6;
}
/* Tooltip styles for refresh and add buttons */
.refresh-btn[title]:hover::after,
.add-btn[title]:hover::after {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
white-space: nowrap;
z-index: 1000;
margin-bottom: 0.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.refresh-btn[title]:hover::before,
.add-btn[title]:hover::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #333;
margin-bottom: -0.5rem;
z-index: 1000;
}
/* Table Styles */
.requests-table {
border: 1px solid #e1e8ed;
border-radius: 8px;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 80px 1fr 200px 120px 200px;
background: #f8f9fa;
font-weight: bold;
color: #2c3e50;
}
.header-cell {
padding: 1rem;
border-right: 1px solid #e1e8ed;
}
.header-cell:last-child {
border-right: none;
}
.table-row {
display: grid;
grid-template-columns: 80px 1fr 200px 120px 200px;
border-bottom: 1px solid #e1e8ed;
transition: background-color 0.3s ease;
}
.table-row:hover {
background: #f8f9fa;
}
.table-row:last-child {
border-bottom: none;
}
.cell {
padding: 1rem;
border-right: 1px solid #e1e8ed;
display: flex;
align-items: center;
}
.cell:last-child {
border-right: none;
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: bold;
text-transform: capitalize;
}
.status-badge.pending {
background: #fff3cd;
color: #856404;
}
.status-badge.confirmed {
background: #d1ecf1;
color: #0c5460;
}
.status-badge.mail_sent {
background: #e7d4c5;
color: #6d5637;
font-weight: bold;
}
.status-badge.completed {
background: #d4edda;
color: #155724;
}
.status-badge.cancelled {
background: #f8d7da;
color: #721c24;
}
.actions {
gap: 0.5rem;
justify-content: center;
}
.action-btn {
background: #6c757d;
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 6px;
cursor: pointer;
font-size: 1.2rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.action-btn:hover:not(:disabled) {
background: #5a6268;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.action-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
opacity: 0.6;
}
.action-btn.view {
background: #17a2b8;
}
.action-btn.view:hover:not(:disabled) {
background: #138496;
}
.action-btn.email {
background: #28a745;
}
.action-btn.email:hover:not(:disabled) {
background: #218838;
}
.action-btn.sms {
background: #ffc107;
color: #212529;
}
.action-btn.sms:hover:not(:disabled) {
background: #e0a800;
}
.action-btn.comm {
background: #6f42c1;
}
.action-btn.comm:hover:not(:disabled) {
background: #5a32a3;
}
.action-btn.edit {
background: #17a2b8;
}
.action-btn.edit:hover:not(:disabled) {
background: #138496;
}
.action-btn.delete {
background: #dc3545;
}
.action-btn.delete:hover:not(:disabled) {
background: #c82333;
}
/* Pricelist table styles */
.pricelist-table {
border: 1px solid #e1e8ed;
border-radius: 8px;
overflow: hidden;
}
.pricelist-table .table-header {
display: grid;
grid-template-columns: 1fr 2fr 100px 80px 100px 120px;
background: #f8f9fa;
font-weight: bold;
color: #2c3e50;
}
.pricelist-table .table-row {
display: grid;
grid-template-columns: 1fr 2fr 100px 80px 100px 120px;
border-bottom: 1px solid #e1e8ed;
transition: background-color 0.3s ease;
}
.pricelist-table .table-row:hover {
background: #f8f9fa;
}
.pricelist-table .table-row:last-child {
border-bottom: none;
}
.status-badge.active {
background: #d4edda;
color: #155724;
}
.status-badge.inactive {
background: #f8d7da;
color: #721c24;
}
/* Tooltip styles */
.action-btn[title]:hover::after {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
white-space: nowrap;
z-index: 1000;
margin-bottom: 0.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.action-btn[title]:hover::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #333;
margin-bottom: -0.5rem;
z-index: 1000;
}
.loading-row, .empty-row {
grid-column: 1 / -1;
padding: 2rem;
text-align: center;
color: #666;
}
/* Blocked Dates Styles */
.blocked-dates-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.blocked-date-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border: 1px solid #e1e8ed;
border-radius: 8px;
background: #f8f9fa;
}
.date-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.date {
font-weight: bold;
color: #2c3e50;
}
.reason {
color: #666;
font-size: 0.875rem;
}
.date-actions {
display: flex;
gap: 0.5rem;
}
.edit-btn, .delete-btn {
border: none;
width: 40px;
height: 40px;
border-radius: 6px;
cursor: pointer;
font-size: 1.2rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.edit-btn {
background: #17a2b8;
color: white;
}
.edit-btn:hover {
background: #138496;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.delete-btn {
background: #dc3545;
color: white;
}
.delete-btn:hover {
background: #c82333;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
/* Tooltip styles for blocked date buttons */
.edit-btn[title]:hover::after,
.delete-btn[title]:hover::after {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
white-space: nowrap;
z-index: 1000;
margin-bottom: 0.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.edit-btn[title]:hover::before,
.delete-btn[title]:hover::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #333;
margin-bottom: -0.5rem;
z-index: 1000;
}
/* Settings Styles */
.settings-form {
display: flex;
flex-direction: column;
gap: 2rem;
}
.setting-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.setting-group label {
font-weight: bold;
color: #2c3e50;
}
.setting-group input,
.setting-group textarea {
padding: 0.75rem;
border: 2px solid #e1e8ed;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
}
.setting-group input:focus,
.setting-group textarea:focus {
outline: none;
border-color: var(--primary-color);
}
.setting-group textarea {
resize: vertical;
min-height: 120px;
}
.save-btn {
background: #28a745;
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 8px;
cursor: pointer;
align-self: flex-start;
font-size: 1.2rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.save-btn:hover {
background: #218838;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
/* Tooltip styles for save buttons */
.save-btn[title]:hover::after {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
white-space: nowrap;
z-index: 1000;
margin-bottom: 0.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.save-btn[title]:hover::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #333;
margin-bottom: -0.5rem;
z-index: 1000;
}
/* Modal Styles */
.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;
}
.modal-content {
background: white;
border-radius: 15px;
padding: 2rem;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.modal-content.large {
max-width: 800px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e1e8ed;
}
.modal-header h3 {
color: #2c3e50;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.3s ease;
position: relative;
}
.close-btn:hover {
color: #333;
background: #f8f9fa;
}
/* Tooltip styles for close buttons */
.close-btn[title]:hover::after {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
white-space: nowrap;
z-index: 1000;
margin-bottom: 0.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.close-btn[title]:hover::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #333;
margin-bottom: -0.5rem;
z-index: 1000;
}
.modal-body {
margin-bottom: 2rem;
}
.modal-footer {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.btn-primary, .btn-secondary {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--primary-dark);
box-shadow: 0 4px 10px rgba(44, 95, 93, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-pickup-confirmation {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
background: var(--secondary-color);
color: white;
font-weight: bold;
}
.btn-pickup-confirmation:hover:not(:disabled) {
background: var(--secondary-dark);
box-shadow: 0 4px 10px rgba(139, 111, 71, 0.3);
transform: translateY(-2px);
}
.btn-pickup-confirmation:disabled {
background: #bdc3c7;
cursor: not-allowed;
opacity: 0.6;
}
/* Form Styles */
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.form-group label {
font-weight: bold;
color: #2c3e50;
}
.form-group input,
.form-group textarea {
padding: 0.75rem;
border: 2px solid #e1e8ed;
border-radius: 8px;
font-size: 1rem;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #3498db;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
/* Request Details Styles */
.request-details {
display: flex;
flex-direction: column;
gap: 1rem;
}
.detail-row {
display: flex;
gap: 1rem;
align-items: center;
}
.detail-row strong {
min-width: 150px;
color: #2c3e50;
}
.detail-row input,
.detail-row select {
padding: 0.5rem;
border: 1px solid #e1e8ed;
border-radius: 4px;
flex: 1;
}
/* Communications Styles */
.no-communications {
text-align: center;
color: #666;
padding: 2rem;
}
.communications-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.communication-item {
border: 1px solid #e1e8ed;
border-radius: 8px;
padding: 1rem;
background: #f8f9fa;
}
.comm-header {
display: flex;
gap: 1rem;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.comm-type {
background: #3498db;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-weight: bold;
}
.comm-direction {
color: #666;
}
.comm-date {
color: #666;
margin-left: auto;
}
.comm-message {
color: #2c3e50;
line-height: 1.5;
}
/* Loading State Styles */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
color: #666;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.admin-tabs {
flex-direction: column;
}
.table-header,
.table-row {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.header-cell,
.cell {
border-right: none;
border-bottom: 1px solid #e1e8ed;
}
.actions {
flex-direction: row;
justify-content: center;
gap: 0.5rem;
}
.blocked-date-item {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.modal-content {
margin: 1rem;
width: calc(100% - 2rem);
}
.admin-user-info {
flex-direction: column;
gap: 0.5rem;
}
}
/* Page Content Styles */
.content-sections {
display: flex;
flex-direction: column;
gap: 2rem;
}
.content-section {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 10px;
border: 1px solid #e1e8ed;
}
.content-section h3 {
color: #2c3e50;
margin-bottom: 1rem;
font-size: 1.3rem;
}
.content-section h4 {
color: #2c3e50;
margin-bottom: 0.75rem;
font-size: 1.1rem;
}
.content-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.feature-group {
background: white;
padding: 1rem;
border-radius: 8px;
border: 1px solid #e1e8ed;
}
.feature-group:not(:last-child) {
margin-bottom: 1rem;
}
.content-form .form-group {
margin-bottom: 0.5rem;
}
.content-form .form-group label {
font-weight: 600;
color: #2c3e50;
margin-bottom: 0.5rem;
display: block;
}
.content-form .form-group input,
.content-form .form-group textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e1e8ed;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.content-form .form-group input:focus,
.content-form .form-group textarea:focus {
outline: none;
border-color: var(--primary-color);
background: #f8f9fa;
box-shadow: 0 0 0 3px rgba(44, 95, 93, 0.1);
}
.content-form .form-group textarea {
resize: vertical;
font-family: inherit;
}
</style>