281 lines
8.5 KiB
Vue
281 lines
8.5 KiB
Vue
<template>
|
|
<div class="card bg-base-100 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title">
|
|
<i class="mdi mdi-shield-account text-2xl text-warning"></i>
|
|
Admin Panel
|
|
</h2>
|
|
|
|
<!-- Stats -->
|
|
<div class="stats stats-vertical lg:stats-horizontal shadow mb-6">
|
|
<div class="stat">
|
|
<div class="stat-title">Total Submissions</div>
|
|
<div class="stat-value text-primary">{{ stats.total_submissions }}</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-title">Total Responses</div>
|
|
<div class="stat-value text-secondary">{{ stats.total_responses }}</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-title">Need Validation</div>
|
|
<div class="stat-value text-warning">{{ stats.needs_validation }}</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-title">Validation Rate</div>
|
|
<div class="stat-value text-success">{{ Math.round(stats.validation_rate * 100) }}%</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Responses Needing Validation -->
|
|
<div v-if="responsesNeedingValidation.length > 0">
|
|
<h3 class="text-lg font-bold mb-4">Responses Needing Validation</h3>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra">
|
|
<thead>
|
|
<tr>
|
|
<th>Puzzle</th>
|
|
<th>OCR Data</th>
|
|
<th>Confidence</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="response in responsesNeedingValidation" :key="response.id">
|
|
<td>
|
|
<div class="font-bold">{{ response.puzzle_name }}</div>
|
|
<div class="text-sm opacity-50">ID: {{ response.id }}</div>
|
|
</td>
|
|
<td>
|
|
<div class="text-sm space-y-1">
|
|
<div>Cost: {{ response.cost || '-' }}</div>
|
|
<div>Cycles: {{ response.cycles || '-' }}</div>
|
|
<div>Area: {{ response.area || '-' }}</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="badge badge-warning badge-sm">
|
|
{{ response.ocr_confidence_score ? Math.round(response.ocr_confidence_score * 100) + '%' : 'Low' }}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<button
|
|
@click="openValidationModal(response)"
|
|
class="btn btn-sm btn-primary"
|
|
>
|
|
<i class="mdi mdi-check-circle mr-1"></i>
|
|
Validate
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="text-center py-8">
|
|
<i class="mdi mdi-check-all text-6xl text-success opacity-50"></i>
|
|
<p class="text-lg font-medium mt-2">All responses validated!</p>
|
|
<p class="text-sm opacity-70">No responses currently need manual validation.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Validation Modal -->
|
|
<div v-if="validationModal.show" class="modal modal-open">
|
|
<div class="modal-box">
|
|
<h3 class="font-bold text-lg mb-4">Validate Response</h3>
|
|
|
|
<div v-if="validationModal.response" class="space-y-4">
|
|
<div class="alert alert-info">
|
|
<i class="mdi mdi-information-outline"></i>
|
|
<div>
|
|
<div class="font-bold">{{ validationModal.response.puzzle_name }}</div>
|
|
<div class="text-sm">Review and correct the OCR data below</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Cost</span>
|
|
</label>
|
|
<input
|
|
v-model="validationModal.data.validated_cost"
|
|
type="text"
|
|
class="input input-bordered input-sm"
|
|
:placeholder="validationModal.response.cost || 'Enter cost'"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Cycles</span>
|
|
</label>
|
|
<input
|
|
v-model="validationModal.data.validated_cycles"
|
|
type="text"
|
|
class="input input-bordered input-sm"
|
|
:placeholder="validationModal.response.cycles || 'Enter cycles'"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Area</span>
|
|
</label>
|
|
<input
|
|
v-model="validationModal.data.validated_area"
|
|
type="text"
|
|
class="input input-bordered input-sm"
|
|
:placeholder="validationModal.response.area || 'Enter area'"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button @click="closeValidationModal" class="btn btn-ghost">Cancel</button>
|
|
<button
|
|
@click="submitValidation"
|
|
class="btn btn-primary"
|
|
:disabled="isValidating"
|
|
>
|
|
<span v-if="isValidating" class="loading loading-spinner loading-sm"></span>
|
|
{{ isValidating ? 'Validating...' : 'Validate' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-backdrop" @click="closeValidationModal"></div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { apiService } from '../services/apiService'
|
|
import type { PuzzleResponse } from '../types'
|
|
|
|
// Reactive data
|
|
const stats = ref({
|
|
total_submissions: 0,
|
|
total_responses: 0,
|
|
needs_validation: 0,
|
|
validated_submissions: 0,
|
|
validation_rate: 0
|
|
})
|
|
|
|
const responsesNeedingValidation = ref<PuzzleResponse[]>([])
|
|
const isLoading = ref(false)
|
|
const isValidating = ref(false)
|
|
|
|
const validationModal = ref({
|
|
show: false,
|
|
response: null as PuzzleResponse | null,
|
|
data: {
|
|
validated_cost: '',
|
|
validated_cycles: '',
|
|
validated_area: ''
|
|
}
|
|
})
|
|
|
|
// Methods
|
|
const loadData = async () => {
|
|
try {
|
|
isLoading.value = true
|
|
|
|
// Load stats (skip if endpoint doesn't exist)
|
|
try {
|
|
const statsResponse = await apiService.getStats()
|
|
if (statsResponse.data) {
|
|
stats.value = statsResponse.data
|
|
}
|
|
} catch (error) {
|
|
console.warn('Stats endpoint not available:', error)
|
|
// Set default stats
|
|
stats.value = {
|
|
total_submissions: 0,
|
|
total_responses: 0,
|
|
needs_validation: 0,
|
|
validated_submissions: 0,
|
|
validation_rate: 0
|
|
}
|
|
}
|
|
|
|
// Load responses needing validation
|
|
const responsesResponse = await apiService.getResponsesNeedingValidation()
|
|
if (responsesResponse.data) {
|
|
responsesNeedingValidation.value = responsesResponse.data
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load admin data:', error)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const openValidationModal = (response: PuzzleResponse) => {
|
|
validationModal.value.response = response
|
|
validationModal.value.data = {
|
|
validated_cost: response.cost || '',
|
|
validated_cycles: response.cycles || '',
|
|
validated_area: response.area || ''
|
|
}
|
|
validationModal.value.show = true
|
|
}
|
|
|
|
const closeValidationModal = () => {
|
|
validationModal.value.show = false
|
|
validationModal.value.response = null
|
|
validationModal.value.data = {
|
|
validated_cost: '',
|
|
validated_cycles: '',
|
|
validated_area: ''
|
|
}
|
|
}
|
|
|
|
const submitValidation = async () => {
|
|
if (!validationModal.value.response?.id) return
|
|
|
|
try {
|
|
isValidating.value = true
|
|
|
|
const response = await apiService.validateResponse(
|
|
validationModal.value.response.id,
|
|
validationModal.value.data
|
|
)
|
|
|
|
if (response.error) {
|
|
alert(`Validation failed: ${response.error}`)
|
|
return
|
|
}
|
|
|
|
// Remove from validation list
|
|
responsesNeedingValidation.value = responsesNeedingValidation.value.filter(
|
|
r => r.id !== validationModal.value.response?.id
|
|
)
|
|
|
|
// Update stats
|
|
stats.value.needs_validation = Math.max(0, stats.value.needs_validation - 1)
|
|
|
|
closeValidationModal()
|
|
|
|
} catch (error) {
|
|
console.error('Validation error:', error)
|
|
alert('Validation failed')
|
|
} finally {
|
|
isValidating.value = false
|
|
}
|
|
}
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
loadData()
|
|
})
|
|
|
|
// Expose refresh method
|
|
defineExpose({
|
|
refresh: loadData
|
|
})
|
|
</script>
|