2313 lines
64 KiB
Vue
2313 lines
64 KiB
Vue
<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>
|