311 lines
6.4 KiB
Vue
311 lines
6.4 KiB
Vue
<template>
|
|
<div class="moderation-map-page">
|
|
<div class="page-header">
|
|
<h1>Geographical Service Map</h1>
|
|
<p class="subtitle">Visualize and moderate services within your geographical scope</p>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<div class="scope-selector">
|
|
<label for="scope">Change Scope:</label>
|
|
<select id="scope" v-model="selectedScopeId" @change="onScopeChange">
|
|
<option v-for="scope in availableScopes" :key="scope.id" :value="scope.id">
|
|
{{ scope.label }}
|
|
</option>
|
|
</select>
|
|
<button @click="refreshData" class="btn-refresh">Refresh Data</button>
|
|
</div>
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<span class="stat-label">Total Services</span>
|
|
<span class="stat-value">{{ services.length }}</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-label">Pending</span>
|
|
<span class="stat-value pending">{{ pendingServices.length }}</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-label">Approved</span>
|
|
<span class="stat-value approved">{{ approvedServices.length }}</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-label">In Scope</span>
|
|
<span class="stat-value">{{ servicesInScope.length }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="map-container">
|
|
<ServiceMap
|
|
:services="servicesInScope"
|
|
:scope-label="scopeLabel"
|
|
/>
|
|
</div>
|
|
|
|
<div class="service-list">
|
|
<h2>Services in Scope</h2>
|
|
<table class="service-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Status</th>
|
|
<th>Address</th>
|
|
<th>Distance</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="service in servicesInScope" :key="service.id">
|
|
<td>{{ service.name }}</td>
|
|
<td>
|
|
<span :class="service.status" class="status-badge">
|
|
{{ service.status }}
|
|
</span>
|
|
</td>
|
|
<td>{{ service.address }}</td>
|
|
<td>{{ service.distance }} km</td>
|
|
<td>
|
|
<button
|
|
@click="approveService(service.id)"
|
|
:disabled="service.status === 'approved'"
|
|
class="btn-action"
|
|
>
|
|
{{ service.status === 'approved' ? 'Approved' : 'Approve' }}
|
|
</button>
|
|
<button @click="zoomToService(service)" class="btn-action secondary">
|
|
View on Map
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import ServiceMap from '~/components/map/ServiceMap.vue'
|
|
import { useServiceMap, type Service } from '~/composables/useServiceMap'
|
|
|
|
const {
|
|
services,
|
|
pendingServices,
|
|
approvedServices,
|
|
scopeLabel,
|
|
currentScope,
|
|
servicesInScope,
|
|
approveService: approveServiceComposable,
|
|
changeScope,
|
|
availableScopes
|
|
} = useServiceMap()
|
|
|
|
const selectedScopeId = ref(currentScope.value.id)
|
|
|
|
const onScopeChange = () => {
|
|
const scope = availableScopes.find(s => s.id === selectedScopeId.value)
|
|
if (scope) {
|
|
changeScope(scope)
|
|
}
|
|
}
|
|
|
|
const refreshData = () => {
|
|
// In a real app, this would fetch fresh data from API
|
|
console.log('Refreshing data...')
|
|
}
|
|
|
|
const zoomToService = (service: Service) => {
|
|
// This would zoom the map to the service location
|
|
console.log('Zooming to service:', service)
|
|
// In a real implementation, we would emit an event to the ServiceMap component
|
|
}
|
|
|
|
const approveService = (serviceId: number) => {
|
|
approveServiceComposable(serviceId)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.moderation-map-page {
|
|
padding: 20px;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.page-header h1 {
|
|
font-size: 2.5rem;
|
|
color: #333;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.subtitle {
|
|
color: #666;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
}
|
|
|
|
.scope-selector {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.scope-selector label {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.scope-selector select {
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
border: 1px solid #ccc;
|
|
background: white;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.btn-refresh {
|
|
background-color: #4a90e2;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.btn-refresh:hover {
|
|
background-color: #3a7bc8;
|
|
}
|
|
|
|
.stats {
|
|
display: flex;
|
|
gap: 15px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
padding: 15px 20px;
|
|
min-width: 120px;
|
|
text-align: center;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.stat-label {
|
|
display: block;
|
|
font-size: 0.9rem;
|
|
color: #666;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.stat-value {
|
|
display: block;
|
|
font-size: 1.8rem;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
.stat-value.pending {
|
|
color: #ffc107;
|
|
}
|
|
|
|
.stat-value.approved {
|
|
color: #28a745;
|
|
}
|
|
|
|
.map-container {
|
|
margin-bottom: 30px;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.service-list {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.service-list h2 {
|
|
margin-top: 0;
|
|
margin-bottom: 20px;
|
|
color: #333;
|
|
}
|
|
|
|
.service-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.service-table thead {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.service-table th {
|
|
padding: 12px 15px;
|
|
text-align: left;
|
|
font-weight: bold;
|
|
color: #495057;
|
|
border-bottom: 2px solid #dee2e6;
|
|
}
|
|
|
|
.service-table td {
|
|
padding: 12px 15px;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 4px 8px;
|
|
border-radius: 12px;
|
|
font-size: 0.85rem;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.status-badge.pending {
|
|
background-color: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
|
|
.status-badge.approved {
|
|
background-color: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.btn-action {
|
|
background-color: #28a745;
|
|
color: white;
|
|
border: none;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.btn-action:disabled {
|
|
background-color: #6c757d;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-action.secondary {
|
|
background-color: #6c757d;
|
|
}
|
|
|
|
.btn-action:hover:not(:disabled) {
|
|
opacity: 0.9;
|
|
}
|
|
</style> |