Files
service-finder/frontend/admin/pages/moderation-map.vue
2026-03-23 21:43:40 +00:00

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>