|
@@ -360,6 +360,32 @@
|
|
|
<button @click="handleDeleteMaterial(m.id, m.name_en)" class="p-2 hover:bg-rose-500/10 rounded-lg text-muted-foreground hover:text-rose-500 transition-colors"><Trash2 class="w-4 h-4" /></button>
|
|
<button @click="handleDeleteMaterial(m.id, m.name_en)" class="p-2 hover:bg-rose-500/10 rounded-lg text-muted-foreground hover:text-rose-500 transition-colors"><Trash2 class="w-4 h-4" /></button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Review Moderation Block -->
|
|
|
|
|
+ <div v-if="order.review_text" class="p-4 bg-amber-500/5 border-t border-border/50 flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
|
|
|
+ <div class="flex items-start gap-4 flex-1">
|
|
|
|
|
+ <div class="pt-1">
|
|
|
|
|
+ <div class="flex gap-0.5">
|
|
|
|
|
+ <Star v-for="i in 5" :key="i" class="w-3 h-3" :class="i <= order.rating ? 'text-yellow-500 fill-yellow-500' : 'text-muted-foreground/30'" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p class="text-[11px] font-bold text-foreground italic">"{{ order.review_text }}"</p>
|
|
|
|
|
+ <div class="flex items-center gap-2 mt-1">
|
|
|
|
|
+ <span class="text-[8px] font-black uppercase tracking-widest text-muted-foreground">Customer Feedback</span>
|
|
|
|
|
+ <span v-if="order.review_approved" class="flex items-center gap-1 text-[8px] font-black uppercase tracking-widest text-emerald-500">
|
|
|
|
|
+ <CheckCircle2 class="w-2.5 h-2.5" /> Approved
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span v-else class="flex items-center gap-1 text-[8px] font-black uppercase tracking-widest text-amber-500">
|
|
|
|
|
+ <Clock class="w-2.5 h-2.5" /> Pending Moderation
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button v-if="!order.review_approved" variant="hero" size="sm" class="h-8 px-4 text-[10px]" @click="handleApproveReview(order.id)">
|
|
|
|
|
+ Approve Feedback
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -915,7 +941,7 @@ import { RouterLink, useRouter, useRoute } from "vue-router";
|
|
|
import { useI18n } from "vue-i18n";
|
|
import { useI18n } from "vue-i18n";
|
|
|
import { loadAdminTranslations } from "@/i18n";
|
|
import { loadAdminTranslations } from "@/i18n";
|
|
|
import { toast } from "vue-sonner";
|
|
import { toast } from "vue-sonner";
|
|
|
-import { Package, Clock, CheckCircle2, Truck, XCircle, AlertCircle, FileText, ExternalLink, ShieldCheck, Eye, RefreshCw, Search, Filter, Layers, Settings, Plus, Edit2, Trash2, ToggleLeft, ToggleRight, Database, Image as ImageIcon, EyeOff, Hash, MessageCircle, FileBox, Download, Newspaper, History, X, Users, Save, Key } from "lucide-vue-next";
|
|
|
|
|
|
|
+import { Package, Clock, CheckCircle2, Truck, XCircle, AlertCircle, FileText, ExternalLink, ShieldCheck, Eye, RefreshCw, Search, Filter, Layers, Settings, Plus, Edit2, Trash2, ToggleLeft, ToggleRight, Database, Image as ImageIcon, EyeOff, Hash, MessageCircle, FileBox, Download, Newspaper, History, X, Users, Save, Key, Star } from "lucide-vue-next";
|
|
|
import Button from "@/components/ui/button.vue";
|
|
import Button from "@/components/ui/button.vue";
|
|
|
import Header from "@/components/Header.vue";
|
|
import Header from "@/components/Header.vue";
|
|
|
import Footer from "@/components/Footer.vue";
|
|
import Footer from "@/components/Footer.vue";
|
|
@@ -925,7 +951,7 @@ import {
|
|
|
adminGetOrders, adminUpdateOrder, adminGetMaterials, adminCreateMaterial, adminUpdateMaterial, adminDeleteMaterial,
|
|
adminGetOrders, adminUpdateOrder, adminGetMaterials, adminCreateMaterial, adminUpdateMaterial, adminDeleteMaterial,
|
|
|
adminGetServices, adminCreateService, adminUpdateService, adminDeleteService, adminUploadOrderPhoto,
|
|
adminGetServices, adminCreateService, adminUpdateService, adminDeleteService, adminUploadOrderPhoto,
|
|
|
adminUpdatePhotoStatus, adminDeletePhoto, adminGetAllPhotos, adminAttachFile, adminDeleteFile, adminDeleteOrder, getBlogPosts, adminCreatePost, adminUpdatePost, adminDeletePost, adminUpdateUser, adminGetUsers, adminCreateUser, adminGetAuditLogs,
|
|
adminUpdatePhotoStatus, adminDeletePhoto, adminGetAllPhotos, adminAttachFile, adminDeleteFile, adminDeleteOrder, getBlogPosts, adminCreatePost, adminUpdatePost, adminDeletePost, adminUpdateUser, adminGetUsers, adminCreateUser, adminGetAuditLogs,
|
|
|
- adminGetOrderItems, adminUpdateOrderItems, API_BASE_URL, RESOURCES_BASE_URL
|
|
|
|
|
|
|
+ adminGetOrderItems, adminUpdateOrderItems, approveOrderReview, API_BASE_URL, RESOURCES_BASE_URL
|
|
|
} from "@/lib/api";
|
|
} from "@/lib/api";
|
|
|
|
|
|
|
|
const { t, locale } = useI18n();
|
|
const { t, locale } = useI18n();
|
|
@@ -1122,6 +1148,16 @@ async function fetchData() {
|
|
|
finally { isLoading.value = false; }
|
|
finally { isLoading.value = false; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function handleApproveReview(orderId: number) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await approveOrderReview(orderId);
|
|
|
|
|
+ toast.success(t('admin.toasts.reviewApproved'));
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ } catch (e: any) {
|
|
|
|
|
+ toast.error(e.message || 'Failed to approve review');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function fetchUsers() {
|
|
async function fetchUsers() {
|
|
|
try {
|
|
try {
|
|
|
const res = await adminGetUsers(userPage.value, 50, userSearch.value);
|
|
const res = await adminGetUsers(userPage.value, 50, userSearch.value);
|