|
@@ -1,117 +1,105 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="space-y-6">
|
|
<div class="space-y-6">
|
|
|
- <div class="flex items-center justify-between">
|
|
|
|
|
- <h2 class="text-2xl font-display font-bold">{{ t('admin.reviews.title') }}</h2>
|
|
|
|
|
- <div class="text-sm text-muted-foreground">
|
|
|
|
|
- {{ total }} {{ t('admin.reviews.total') }}
|
|
|
|
|
|
|
+ <!-- Header / Summary Bar -->
|
|
|
|
|
+ <div class="flex items-center justify-between bg-card/40 p-4 rounded-2xl border border-border/50">
|
|
|
|
|
+ <h2 class="text-sm font-bold uppercase tracking-widest text-primary ml-2">{{ t('admin.reviews.title') }}</h2>
|
|
|
|
|
+ <div class="text-xs font-bold text-muted-foreground uppercase tracking-widest bg-muted/30 px-4 py-2 rounded-xl border border-border/50">
|
|
|
|
|
+ {{ t("admin.total") }}: {{ total }}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- Reviews List -->
|
|
|
|
|
- <div class="grid gap-4">
|
|
|
|
|
- <div v-if="loading" class="flex justify-center py-12">
|
|
|
|
|
- <Loader2 class="w-8 h-8 animate-spin text-primary" />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-else-if="reviews.length === 0" class="text-center py-12 bg-card/50 rounded-3xl border border-dashed border-border">
|
|
|
|
|
- <MessageSquare class="w-12 h-12 text-muted-foreground/30 mx-auto mb-4" />
|
|
|
|
|
- <p class="text-muted-foreground">{{ t('admin.reviews.noReviews') }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div
|
|
|
|
|
- v-for="review in reviews"
|
|
|
|
|
- :key="review.id"
|
|
|
|
|
- class="bg-card/50 backdrop-blur-xl border border-border/50 p-6 rounded-3xl hover:border-primary/30 transition-all group relative overflow-hidden"
|
|
|
|
|
- >
|
|
|
|
|
- <!-- Status Badge -->
|
|
|
|
|
- <div class="absolute top-6 right-6 flex items-center gap-2">
|
|
|
|
|
- <span
|
|
|
|
|
- :class="[
|
|
|
|
|
- 'px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider',
|
|
|
|
|
- review.review_approved ? 'bg-emerald-500/10 text-emerald-500' : 'bg-amber-500/10 text-amber-500'
|
|
|
|
|
- ]"
|
|
|
|
|
- >
|
|
|
|
|
- {{ review.review_approved ? t('admin.reviews.approved') : t('admin.reviews.pending') }}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="flex items-start gap-4">
|
|
|
|
|
- <div class="w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center text-primary font-bold text-lg">
|
|
|
|
|
- {{ review.first_name[0] }}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="flex-1 min-w-0">
|
|
|
|
|
- <div class="flex items-center gap-2 mb-1">
|
|
|
|
|
- <p class="font-bold">{{ review.first_name }} {{ review.last_name }}</p>
|
|
|
|
|
- <span class="text-muted-foreground text-xs">•</span>
|
|
|
|
|
- <p class="text-xs text-muted-foreground">{{ new Date(review.created_at).toLocaleDateString() }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <p class="text-xs text-muted-foreground mb-4 truncate">{{ review.email }}</p>
|
|
|
|
|
-
|
|
|
|
|
- <div class="flex gap-0.5 mb-4">
|
|
|
|
|
- <Star
|
|
|
|
|
- v-for="i in 5"
|
|
|
|
|
- :key="i"
|
|
|
|
|
- class="w-4 h-4"
|
|
|
|
|
- :class="i <= review.rating ? 'text-yellow-500 fill-yellow-500' : 'text-muted-foreground/20'"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <!-- Empty State -->
|
|
|
|
|
+ <div v-if="!loading && reviews.length === 0" class="flex flex-col items-center justify-center py-20 bg-card/40 border border-border/50 rounded-3xl opacity-50">
|
|
|
|
|
+ <MessageSquare class="w-12 h-12 text-muted-foreground/20 mb-4" />
|
|
|
|
|
+ <p class="text-sm text-muted-foreground">{{ t('admin.reviews.noReviews') }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <p class="text-foreground/80 italic bg-background/50 p-4 rounded-2xl border border-border/50 mb-6 font-serif">
|
|
|
|
|
- "{{ review.review_text }}"
|
|
|
|
|
- </p>
|
|
|
|
|
|
|
+ <!-- Loading State -->
|
|
|
|
|
+ <div v-if="loading" class="flex items-center justify-center py-20">
|
|
|
|
|
+ <Loader2 class="w-8 h-8 text-primary animate-spin" />
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div class="flex items-center gap-3">
|
|
|
|
|
- <Button
|
|
|
|
|
- v-if="!review.review_approved"
|
|
|
|
|
- variant="default"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- class="rounded-xl h-9 px-4"
|
|
|
|
|
- @click="handleApprove(review.id)"
|
|
|
|
|
- >
|
|
|
|
|
- <Check class="w-4 h-4 mr-2" />
|
|
|
|
|
- {{ t('admin.reviews.approve') }}
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- class="rounded-xl h-9 px-4 border-rose-500/20 text-rose-500 hover:bg-rose-500/10 hover:border-rose-500/30"
|
|
|
|
|
- @click="handleDelete(review.id)"
|
|
|
|
|
- >
|
|
|
|
|
- <Trash2 class="w-4 h-4 mr-2" />
|
|
|
|
|
- {{ t('admin.reviews.delete') }}
|
|
|
|
|
- </Button>
|
|
|
|
|
-
|
|
|
|
|
- <div class="flex-1" />
|
|
|
|
|
-
|
|
|
|
|
- <p class="text-[10px] text-muted-foreground uppercase tracking-widest font-medium">
|
|
|
|
|
- Order #{{ review.id }}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <!-- Table Layout -->
|
|
|
|
|
+ <div v-if="!loading && reviews.length > 0" class="bg-card/40 backdrop-blur-md border border-border/50 rounded-3xl overflow-hidden shadow-xl">
|
|
|
|
|
+ <div class="overflow-x-auto">
|
|
|
|
|
+ <table class="w-full text-left border-collapse">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr class="bg-muted/30 border-b border-border/50">
|
|
|
|
|
+ <th class="p-4 text-[10px] font-bold uppercase tracking-wider">{{ t("admin.labels.user") }}</th>
|
|
|
|
|
+ <th class="p-4 text-[10px] font-bold uppercase tracking-wider">{{ t("admin.reviews.rating") }}</th>
|
|
|
|
|
+ <th class="p-4 text-[10px] font-bold uppercase tracking-wider">{{ t("admin.reviews.content") }}</th>
|
|
|
|
|
+ <th class="p-4 text-[10px] font-bold uppercase tracking-wider">{{ t("admin.reviews.status") }}</th>
|
|
|
|
|
+ <th class="p-4 text-[10px] font-bold uppercase tracking-wider">{{ t("admin.labels.registered") }}</th>
|
|
|
|
|
+ <th class="p-4 text-[10px] font-bold uppercase tracking-wider text-right">{{ t("admin.labels.actions") }}</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody class="divide-y divide-border/30">
|
|
|
|
|
+ <tr v-for="review in reviews" :key="review.id" class="hover:bg-primary/5 transition-colors group/row">
|
|
|
|
|
+ <td class="p-4">
|
|
|
|
|
+ <div class="flex flex-col">
|
|
|
|
|
+ <span class="text-sm font-bold">{{ review.first_name }} {{ review.last_name }}</span>
|
|
|
|
|
+ <span class="text-[10px] text-muted-foreground opacity-50">{{ review.email }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="p-4">
|
|
|
|
|
+ <div class="flex items-center gap-1">
|
|
|
|
|
+ <span class="text-sm font-bold">{{ review.rating }}</span>
|
|
|
|
|
+ <span class="text-[10px] text-muted-foreground">/ 5</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="p-4">
|
|
|
|
|
+ <p class="text-xs text-foreground/80 max-w-md line-clamp-2 italic">
|
|
|
|
|
+ "{{ review.review_text }}"
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="p-4">
|
|
|
|
|
+ <span
|
|
|
|
|
+ :class="[
|
|
|
|
|
+ 'px-2 py-0.5 rounded-full text-[9px] font-bold uppercase border',
|
|
|
|
|
+ review.review_approved ? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' : 'bg-amber-500/10 text-amber-500 border-amber-500/20'
|
|
|
|
|
+ ]"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ review.review_approved ? t('admin.reviews.approved') : t('admin.reviews.pending') }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="p-4 text-[10px] text-muted-foreground font-bold uppercase">
|
|
|
|
|
+ {{ new Date(review.created_at).toLocaleDateString() }}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="p-4 text-right">
|
|
|
|
|
+ <div class="flex items-center justify-end gap-2">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ v-if="!review.review_approved"
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ class="h-7 px-3 text-[10px] font-bold uppercase rounded-lg border-emerald-500/30 text-emerald-500 hover:bg-emerald-500/10"
|
|
|
|
|
+ @click="handleApprove(review.id)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ t('admin.reviews.approve') }}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ class="h-7 px-3 text-[10px] font-bold uppercase rounded-lg border-rose-500/30 text-rose-500 hover:bg-rose-500/10"
|
|
|
|
|
+ @click="handleDelete(review.id)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ t('admin.reviews.delete') }}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <!-- Paging -->
|
|
|
|
|
- <div v-if="total > size" class="flex justify-center gap-2 pt-6">
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- :disabled="page === 1"
|
|
|
|
|
- @click="page--"
|
|
|
|
|
- class="rounded-xl"
|
|
|
|
|
- >
|
|
|
|
|
- <ChevronLeft class="w-4 h-4 mr-2" />
|
|
|
|
|
- {{ t('admin.common.prev') }}
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- :disabled="page * size >= total"
|
|
|
|
|
- @click="page++"
|
|
|
|
|
- class="rounded-xl"
|
|
|
|
|
- >
|
|
|
|
|
- {{ t('admin.common.next') }}
|
|
|
|
|
- <ChevronRight class="w-4 h-4 ml-2" />
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Pagination -->
|
|
|
|
|
+ <div v-if="total > size" class="flex items-center justify-center gap-2 py-4">
|
|
|
|
|
+ <Button v-for="p in Math.ceil(total / size)" :key="p"
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ @click="page = p"
|
|
|
|
|
+ :class="['w-8 h-8 rounded-lg font-bold text-xs transition-all p-0', page === p ? 'bg-primary text-white shadow-glow border-primary' : 'bg-card border border-border/50 text-muted-foreground hover:border-primary/50']">
|
|
|
|
|
+ {{ p }}
|
|
|
|
|
+ </Button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
@@ -119,10 +107,7 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, onMounted, watch } from 'vue';
|
|
import { ref, onMounted, watch } from 'vue';
|
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useI18n } from 'vue-i18n';
|
|
|
-import {
|
|
|
|
|
- MessageSquare, Star, Check, Trash2,
|
|
|
|
|
- Loader2, ChevronLeft, ChevronRight
|
|
|
|
|
-} from 'lucide-vue-next';
|
|
|
|
|
|
|
+import { MessageSquare, Loader2 } from 'lucide-vue-next';
|
|
|
import { adminGetReviews, approveOrderReview, adminUpdateOrder } from '@/lib/api';
|
|
import { adminGetReviews, approveOrderReview, adminUpdateOrder } from '@/lib/api';
|
|
|
import Button from '@/components/ui/button.vue';
|
|
import Button from '@/components/ui/button.vue';
|
|
|
import { toast } from 'vue-sonner';
|
|
import { toast } from 'vue-sonner';
|
|
@@ -161,7 +146,6 @@ const handleApprove = async (orderId: number) => {
|
|
|
const handleDelete = async (orderId: number) => {
|
|
const handleDelete = async (orderId: number) => {
|
|
|
if (!confirm(t('admin.reviews.confirmDelete'))) return;
|
|
if (!confirm(t('admin.reviews.confirmDelete'))) return;
|
|
|
try {
|
|
try {
|
|
|
- // We just clear the review fields for this order
|
|
|
|
|
await adminUpdateOrder(orderId, {
|
|
await adminUpdateOrder(orderId, {
|
|
|
review_text: "",
|
|
review_text: "",
|
|
|
rating: 0,
|
|
rating: 0,
|
|
@@ -175,6 +159,5 @@ const handleDelete = async (orderId: number) => {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
watch(page, fetchReviews);
|
|
watch(page, fetchReviews);
|
|
|
-
|
|
|
|
|
onMounted(fetchReviews);
|
|
onMounted(fetchReviews);
|
|
|
</script>
|
|
</script>
|