| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600 |
- <template>
- <div v-if="authStore.isLoading" />
- <div v-else-if="!authStore.user || authStore.user.role !== 'admin'">
- <RouterLink to="/auth" /><!-- redirect handled in onMounted -->
- </div>
- <div v-else class="min-h-screen bg-background text-foreground">
- <Header />
- <main class="container mx-auto px-4 pt-32 pb-20">
- <!-- DEPLOY INDICATOR -->
- <div class="bg-rose-600 text-white text-[10px] py-1 text-center font-bold uppercase tracking-widest mb-4 rounded-lg">
- DEPLOY VERIFIED: 12:50 CEST
- </div>
- <!-- Admin Header -->
- <div class="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-12">
- <div>
- <span class="text-primary font-display text-sm tracking-widest uppercase mb-2 block">{{ t("admin.managementCenter") }}</span>
- <h1 class="font-display text-4xl font-bold">Admin <span class="text-gradient">{{ t("admin.dashboard") }}</span></h1>
- </div>
- <div class="flex bg-card/40 backdrop-blur-md border border-border/50 rounded-2xl p-1 gap-1">
- <button v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="[
- 'px-6 py-2.5 rounded-xl text-sm font-bold transition-all whitespace-nowrap',
- activeTab === tab.id ? 'bg-primary text-primary-foreground shadow-glow' : 'text-muted-foreground hover:bg-white/5 hover:text-foreground'
- ]">
- {{ t('admin.tabs.' + tab.id) }}
- </button>
- </div>
- </div>
- <!-- Global Search & Quick Actions -->
- <div class="flex flex-col gap-4 mb-8">
- <div class="flex flex-col sm:flex-row gap-4">
- <div class="relative flex-1">
- <Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
- <input type="text" v-model="searchQuery" :placeholder="t('admin.searchPlaceholder', { tab: activeTab })"
- class="w-full bg-card/40 border border-border/50 rounded-xl pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all text-sm" />
- </div>
- <Button v-if="activeTab !== 'orders' && activeTab !== 'audit'" variant="hero" class="gap-2 sm:px-8" @click="handleAddNew">
- <Plus class="w-4 h-4" />{{ t("admin.addNew") }}
- </Button>
- </div>
- </div>
- <!-- Content Loader -->
- <div v-if="isLoading" class="flex items-center justify-center py-20">
- <RefreshCw class="w-8 h-8 text-primary animate-spin" />
- </div>
- <!-- Modular Sections -->
- <div v-else>
- <OrdersSection
- v-if="activeTab === 'orders'"
- :orders="orders"
- :statusConfig="STATUS_CONFIG"
- :resourcesBaseUrl="RESOURCES_BASE_URL"
- :adminChatId="adminChatId"
- :notifyStatusMap="notifyStatusMap"
- :fiscalFormMap="fiscalFormMap"
- :searchQuery="searchQuery"
- @update-status="handleUpdateStatus"
- @delete-order="handleDeleteOrder"
- @attach-file="handleAttachFile"
- @upload-photo="handleUploadPhoto"
- @delete-file="handleDeleteFile"
- @delete-photo="handleDeletePhoto"
- @toggle-photo-public="handleTogglePhotoPublic"
- @approve-review="handleApproveReview"
- @open-chat="toggleAdminChat"
- @close-chat="adminChatId = null"
- @update-notify="(id, val) => notifyStatusMap[id] = val"
- @update-fiscal="handleUpdateFiscal"
- @edit-order="handleEditOrder"
- />
- <MaterialsSection
- v-if="activeTab === 'materials'"
- :materials="materials"
- :searchQuery="searchQuery"
- @edit="m => { editingMaterial = m; Object.assign(matForm, m); showAddModal = true; }"
- @delete="handleDeleteMaterial"
- @toggle-active="toggleMaterialActive"
- />
- <ServicesSection
- v-if="activeTab === 'services'"
- :services="services"
- :searchQuery="searchQuery"
- @edit="s => { editingService = s; Object.assign(svcForm, s); showAddModal = true; }"
- @delete="handleDeleteService"
- @toggle-active="toggleServiceActive"
- />
- <UsersSection
- v-if="activeTab === 'users'"
- :users="usersResult.users"
- :total="usersResult.total"
- :currentPage="userPage"
- v-model:searchQuery="userSearch"
- @toggle-chat="handleToggleUserChat"
- @toggle-active="handleToggleUserActive"
- @reset-password="handleResetPassword"
- @toggle-role="handleUpdateUserRole"
- @update-page="p => { userPage = p; fetchUsers(); }"
- />
- <PostsSection
- v-if="activeTab === 'posts'"
- :posts="posts"
- :searchQuery="searchQuery"
- @edit="p => { editingPost = p; Object.assign(postForm, p); showAddModal = true; }"
- @delete="handleDeletePost"
- @toggle-publish="togglePostActive"
- />
- <PortfolioSection
- v-if="activeTab === 'portfolio'"
- :portfolioItems="portfolioItems"
- :resourcesBaseUrl="RESOURCES_BASE_URL"
- @delete="handleDeletePhoto"
- />
- <AuditSection
- v-if="activeTab === 'audit'"
- :auditLogs="auditLogs"
- :total="auditTotal"
- :currentPage="auditPage"
- @update-page="p => { auditPage = p; fetchAuditLogs(); }"
- />
- <ReviewsSection
- v-if="activeTab === 'reviews'"
- />
- </div>
- </main>
- <!-- Global Modals -->
- <div v-if="showAddModal" class="fixed inset-0 z-[99999] flex items-center justify-center p-4">
- <div class="absolute inset-0 bg-background/80 backdrop-blur-sm" @click="closeModals" />
- <div class="relative w-full max-w-lg bg-card border border-border/50 rounded-3xl p-8 shadow-2xl overflow-y-auto max-h-[90vh]">
- <h3 class="text-xl font-bold font-display mb-6">{{ editingMaterial || editingService || editingPost || editingOrder ? t('admin.actions.edit') : t("admin.addNew") }}</h3>
-
- <!-- Order Edit Form -->
- <form v-if="editingOrder" @submit.prevent="handleSaveOrder" class="space-y-4">
- <!-- Form items... no changes there -->
- <div class="grid grid-cols-2 gap-4">
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">{{ t("auth.fields.firstName") }}</label>
- <input v-model="orderForm.first_name" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
- </div>
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">{{ t("auth.fields.lastName") }}</label>
- <input v-model="orderForm.last_name" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
- </div>
- </div>
- <div class="grid grid-cols-2 gap-4">
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">{{ t("auth.fields.email") }}</label>
- <input v-model="orderForm.email" type="email" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
- </div>
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.phone") }}</label>
- <input v-model="orderForm.phone" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
- </div>
- </div>
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.address") }}</label>
- <input v-model="orderForm.shipping_address" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
- </div>
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.projectNotes") }}</label>
- <textarea v-model="orderForm.notes" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm h-20" />
- </div>
- <div class="grid grid-cols-2 gap-4 pt-4 border-t border-border/10">
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.totalPrice") }} (EUR)</label>
- <input v-model.number="orderForm.total_price" type="number" step="0.01" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
- </div>
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.quantity") }}</label>
- <input v-model.number="orderForm.quantity" type="number" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
- </div>
- </div>
- <div class="grid grid-cols-2 gap-4">
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.material") }}</label>
- <input v-model="orderForm.material_name" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
- </div>
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.colors") }}</label>
- <input v-model="orderForm.color_name" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
- </div>
- </div>
- <!-- Review Edit fallback -->
- <div v-if="orderForm.review_text" class="pt-4 border-t border-border/10 space-y-4">
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">Review Text</label>
- <textarea v-model="orderForm.review_text" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm h-20" />
- </div>
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">Rating (1-5)</label>
- <input v-model.number="orderForm.rating" type="number" min="1" max="5" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" />
- </div>
- </div>
- <div class="flex gap-3 pt-6 border-t border-border/10">
- <Button type="button" variant="ghost" class="flex-1" @click="closeModals">{{ t("admin.actions.cancel") }}</Button>
- <Button type="submit" variant="hero" class="flex-1">{{ t("admin.actions.save") }}</Button>
- </div>
- </form>
-
- <!-- Material Modal Form -->
- <form v-if="activeTab === 'materials'" @submit.prevent="handleSaveMaterial" class="space-y-4">
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.nameEn") }}</label><input v-model="matForm.name_en" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.nameRu") }}</label><input v-model="matForm.name_ru" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.nameUa") }}</label><input v-model="matForm.name_ua" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.pricePerCm3") }}</label><input v-model.number="matForm.price_per_cm3" type="number" step="0.01" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
-
- <div class="space-y-1">
- <label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.colors") }}</label>
- <div class="flex gap-2">
- <input v-model="newColor" @keydown.enter.prevent="addColor" class="flex-1 bg-background border border-border/50 rounded-xl px-4 py-2 text-sm" placeholder="e.g. Red" />
- <Button type="button" variant="hero" @click="addColor">Add</Button>
- </div>
- <div class="flex flex-wrap gap-2 mt-3">
- <span v-for="(c, idx) in matForm.available_colors" :key="idx" class="px-2 py-1 bg-primary/10 text-primary rounded-lg text-xs font-bold border border-primary/20 flex items-center gap-2">
- {{ c }} <X class="w-3 h-3 cursor-pointer hover:text-rose-500" @click="removeColor(idx)" />
- </span>
- </div>
- </div>
- <div class="flex gap-3 pt-6 border-t border-border/10">
- <Button type="button" variant="ghost" class="flex-1" @click="closeModals">{{ t("admin.actions.cancel") }}</Button>
- <Button type="submit" variant="hero" class="flex-1">{{ t("admin.actions.save") }}</Button>
- </div>
- </form>
- <!-- Service Modal Form -->
- <form v-if="activeTab === 'services'" @submit.prevent="handleSaveService" class="space-y-4">
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.nameEn") }}</label><input v-model="svcForm.name_en" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.nameRu") }}</label><input v-model="svcForm.name_ru" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.techType") }}</label><input v-model="svcForm.tech_type" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
- <div class="flex gap-3 pt-6 border-t border-border/10">
- <Button type="button" variant="ghost" class="flex-1" @click="closeModals">{{ t("admin.actions.cancel") }}</Button>
- <Button type="submit" variant="hero" class="flex-1">{{ t("admin.actions.save") }}</Button>
- </div>
- </form>
- <!-- User Creation Form -->
- <form v-if="activeTab === 'users'" @submit.prevent="handleSaveUser" class="space-y-4">
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.email") }}</label><input v-model="userForm.email" type="email" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.password") }}</label><input v-model="userForm.password" type="password" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
- <div class="grid grid-cols-2 gap-4">
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.firstName") }}</label><input v-model="userForm.first_name" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.lastName") }}</label><input v-model="userForm.last_name" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
- </div>
- <div class="space-y-1"><label class="text-[10px] font-bold uppercase ml-1">{{ t("admin.fields.phone") }}</label><input v-model="userForm.phone" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm" /></div>
- <div class="flex gap-3 pt-6 border-t border-border/10">
- <Button type="button" variant="ghost" class="flex-1" @click="closeModals">{{ t("admin.actions.cancel") }}</Button>
- <Button type="submit" variant="hero" class="flex-1">{{ t("admin.actions.save") }}</Button>
- </div>
- </form>
- </div>
- </div>
- <Footer />
- </div>
- </template>
- <script setup lang="ts">
- import { ref, watch, reactive, onMounted, onUnmounted } from "vue";
- import { useRouter, useRoute, RouterLink } from "vue-router";
- import { useI18n } from "vue-i18n";
- import { loadAdminTranslations } from "@/i18n";
- import { toast } from "vue-sonner";
- // Icons
- import {
- Package, Clock, RefreshCw, Search, Layers, Plus, Database,
- Newspaper, History, X, Users, ImageIcon, Truck, CheckCircle2, XCircle
- } from "lucide-vue-next";
- // UI Components
- import Button from "@/components/ui/button.vue";
- import Header from "@/components/Header.vue";
- import Footer from "@/components/Footer.vue";
- // Admin Sections
- import OrdersSection from "@/components/admin/OrdersSection.vue";
- import MaterialsSection from "@/components/admin/MaterialsSection.vue";
- import ServicesSection from "@/components/admin/ServicesSection.vue";
- import UsersSection from "@/components/admin/UsersSection.vue";
- import PostsSection from "@/components/admin/PostsSection.vue";
- import PortfolioSection from "@/components/admin/PortfolioSection.vue";
- import AuditSection from "@/components/admin/AuditSection.vue";
- import ReviewsSection from "@/components/admin/ReviewsSection.vue";
- // API & Stores
- import { useAuthStore } from "@/stores/auth";
- import {
- adminGetOrders, adminUpdateOrder, adminGetMaterials, adminUpdateMaterial,
- adminDeleteMaterial, adminCreateMaterial, adminGetServices, adminUpdateService,
- adminDeleteService, adminCreateService, adminUploadOrderPhoto, adminUpdatePhotoStatus,
- adminDeletePhoto, adminGetAllPhotos, adminAttachFile, adminDeleteFile, adminDeleteOrder,
- getBlogPosts, adminUpdatePost, adminDeletePost, adminCreatePost,
- adminGetUsers, adminUpdateUser, adminCreateUser,
- adminGetAuditLogs, approveOrderReview, RESOURCES_BASE_URL
- } from "@/lib/api";
- const { t } = useI18n();
- const router = useRouter();
- const route = useRoute();
- const authStore = useAuthStore();
- // Status Configuration shared with OrdersSection
- const STATUS_CONFIG: Record<string, { color: string; icon: any }> = {
- pending: { color: "text-amber-500 bg-amber-500/10", icon: Clock },
- processing: { color: "text-blue-500 bg-blue-500/10", icon: RefreshCw },
- shipped: { color: "text-purple-500 bg-purple-500/10", icon: Truck },
- completed: { color: "text-emerald-500 bg-emerald-500/10", icon: CheckCircle2 },
- cancelled: { color: "text-rose-500 bg-rose-500/10", icon: XCircle },
- };
- // State
- const isLoading = ref(true);
- const searchQuery = ref("");
- const adminChatId = ref<any>(null);
- // Records
- const orders = ref<any[]>([]);
- const materials = ref<any[]>([]);
- const services = ref<any[]>([]);
- const posts = ref<any[]>([]);
- const portfolioItems = ref<any[]>([]);
- const auditLogs = ref<any[]>([]);
- const auditTotal = ref(0);
- const auditPage = ref(1);
- const usersResult = ref({ users: [] as any[], total: 0, page: 1, size: 50 });
- const userSearch = ref("");
- const userPage = ref(1);
- // Maps for reactive UI state
- const notifyStatusMap = ref<Record<number, boolean>>({});
- const fiscalFormMap = ref<Record<number, { fiscal_qr_url: string; ikof: string; jikr: string }>>({});
- const tabs: { id: Tab }[] = [
- { id: "orders" },
- { id: "materials" },
- { id: "services" },
- { id: "portfolio" },
- { id: "reviews" },
- { id: "users" },
- { id: "posts" },
- { id: "audit" },
- ];
- type Tab = "orders" | "materials" | "services" | "posts" | "users" | "portfolio" | "audit" | "reviews";
- function getValidTab(val: any): Tab {
- const t = val?.toString();
- return ["orders", "materials", "services", "posts", "users", "portfolio", "audit", "reviews"].includes(t) ? (t as Tab) : "orders";
- }
- const activeTab = ref<Tab>(getValidTab(route.query.tab));
- // Watchers
- watch(activeTab, (newTab) => {
- if (route.query.tab !== newTab) {
- router.replace({ query: { ...route.query, tab: newTab } });
- }
- fetchData();
- });
- watch([searchQuery], () => {
- if (activeTab.value === 'orders') debouncedFetchData();
- else fetchData();
- });
- // Modals State & Forms
- const showAddModal = ref(false);
- const editingMaterial = ref<any | null>(null);
- const editingService = ref<any | null>(null);
- const editingPost = ref<any | null>(null);
- const editingOrder = ref<any | null>(null);
- const matForm = reactive({ name_en: "", name_ru: "", name_ua: "", name_me: "", desc_en: "", desc_ru: "", desc_ua: "", desc_me: "", price_per_cm3: 0, available_colors: [] as string[], is_active: true });
- const svcForm = reactive({ name_en: "", name_ru: "", name_ua: "", name_me: "", desc_en: "", desc_ru: "", desc_ua: "", desc_me: "", tech_type: "", is_active: true });
- const postForm = reactive({ slug: "", title_en: "", title_me: "", title_ru: "", title_ua: "", category: "Technology", image_url: "", is_published: true });
- const userForm = reactive({ email: "", password: "", first_name: "", last_name: "", phone: "" });
- const orderForm = reactive({ total_price: 0, material_name: "", color_name: "", quantity: 1, first_name: "", last_name: "", email: "", phone: "", shipping_address: "", notes: "", review_text: "", rating: 0 });
- const handleEditOrder = (order: any) => {
- toast.info("Opening order edit for #" + order.id);
- console.log("CLICK DETECTED: Order #", order.id);
- editingOrder.value = order;
- Object.assign(orderForm, {
- total_price: order.invoice_amount || 0,
- material_name: order.material_name || "",
- color_name: order.color_name || "",
- quantity: order.quantity || 1,
- first_name: order.first_name || "",
- last_name: order.last_name || "",
- email: order.email || "",
- phone: order.phone || "",
- shipping_address: order.shipping_address || "",
- notes: order.notes || "",
- review_text: order.review_text || "",
- rating: order.rating || 0
- });
- showAddModal.value = true;
- };
- const newColor = ref("");
- // Fetching Logic
- let fetchTimeout: any = null;
- function debouncedFetchData() {
- clearTimeout(fetchTimeout);
- fetchTimeout = setTimeout(fetchData, 400);
- }
- async function fetchData() {
- isLoading.value = true;
- try {
- const tab = activeTab.value;
- if (tab === "orders") {
- orders.value = await adminGetOrders({ search: searchQuery.value });
- orders.value.forEach(o => {
- if (notifyStatusMap.value[o.id] === undefined) notifyStatusMap.value[o.id] = true;
- if (!fiscalFormMap.value[o.id]) fiscalFormMap.value[o.id] = { fiscal_qr_url: o.fiscal_qr_url || "", ikof: o.ikof || "", jikr: o.jikr || "" };
- });
- }
- else if (tab === "materials") materials.value = await adminGetMaterials();
- else if (tab === "services") services.value = await adminGetServices();
- else if (tab === "posts") posts.value = await getBlogPosts(false);
- else if (tab === "portfolio") portfolioItems.value = await adminGetAllPhotos();
- else if (tab === "users") await fetchUsers();
- else if (tab === "audit") await fetchAuditLogs();
- } catch (err: any) {
- toast.error(err.message || "Failed to load data");
- } finally {
- isLoading.value = false;
- }
- }
- async function fetchUsers() {
- usersResult.value = await adminGetUsers(userPage.value, 50, userSearch.value);
- }
- async function fetchAuditLogs() {
- const res = await adminGetAuditLogs(auditPage.value);
- auditLogs.value = res.logs; auditTotal.value = res.total;
- }
- // Global Handlers
- const toggleAdminChat = (id: number) => adminChatId.value = adminChatId.value === id ? null : id;
- const handleUpdateStatus = async (id: number, status: string) => {
- try {
- await adminUpdateOrder(id, { status, send_notification: notifyStatusMap.value[id] });
- toast.success("Status updated"); fetchData();
- } catch (err: any) { toast.error(err.message); }
- };
- const handleUpdateFiscal = async (id: number, data: any) => {
- try {
- await adminUpdateOrder(id, data); toast.success("Fiscal data saved"); fetchData();
- } catch (err: any) { toast.error(err.message); }
- };
- const handleApproveReview = async (id: number) => {
- try {
- await approveOrderReview(id); toast.success("Review approved"); fetchData();
- } catch (err: any) { toast.error(err.message); }
- };
- const handleDeleteOrder = async (id: number) => {
- if (confirm(`Delete Order #${id}?`)) {
- try { await adminDeleteOrder(id); toast.success("Order deleted"); fetchData(); }
- catch (err: any) { toast.error(err.message); }
- }
- };
- const handleAttachFile = async (id: number, file?: File) => {
- if (!file) return;
- try {
- const fd = new FormData(); fd.append("file", file);
- await adminAttachFile(id, fd); toast.success("File attached"); fetchData();
- } catch (err: any) { toast.error(err.message); }
- };
- const handleUploadPhoto = async (id: number, file?: File) => {
- if (!file) return;
- try {
- const fd = new FormData(); fd.append("file", file);
- await adminUploadOrderPhoto(id, fd); toast.success("Photo added"); fetchData();
- } catch (err: any) { toast.error(err.message); }
- };
- const handleDeleteFile = async (id: number, fid: number, fname: string) => {
- if (confirm(`Delete ${fname}?`)) {
- try { await adminDeleteFile(id, fid); fetchData(); }
- catch (err: any) { toast.error(err.message); }
- }
- };
- const handleDeletePhoto = async (id: number) => {
- if (confirm(`Delete photo?`)) {
- try { await adminDeletePhoto(id); fetchData(); }
- catch (err: any) { toast.error(err.message); }
- }
- };
- const handleTogglePhotoPublic = async (id: number, current: boolean) => {
- try { await adminUpdatePhotoStatus(id, { is_public: !current }); fetchData(); }
- catch (err: any) { toast.error(err.message); }
- };
- const toggleMaterialActive = async (m: any) => { try { await adminUpdateMaterial(m.id, { is_active: !m.is_active }); fetchData(); } catch (err: any) { toast.error(err.message); } };
- const toggleServiceActive = async (s: any) => { try { await adminUpdateService(s.id, { is_active: !s.is_active }); fetchData(); } catch (err: any) { toast.error(err.message); } };
- const togglePostActive = async (p: any) => { try { await adminUpdatePost(p.id, { ...p, is_published: !p.is_published }); fetchData(); } catch (err: any) { toast.error(err.message); } };
- const handleDeleteMaterial = async (id: number, name: string) => { if (confirm(`Delete ${name}?`)) { try { await adminDeleteMaterial(id); fetchData(); } catch (err: any) { toast.error(err.message); } } };
- const handleDeleteService = async (id: number, name: string) => { if (confirm(`Delete ${name}?`)) { try { await adminDeleteService(id); fetchData(); } catch (err: any) { toast.error(err.message); } } };
- const handleDeletePost = async (id: number, title: string) => { if (confirm(`Delete ${title}?`)) { try { await adminDeletePost(id); fetchData(); } catch (err: any) { toast.error(err.message); } } };
- const handleToggleUserChat = async (id: number, current: boolean) => { try { await adminUpdateUser(id, { can_chat: !current }); fetchUsers(); } catch (err: any) { toast.error(err.message); } };
- const handleToggleUserActive = async (id: number, current: boolean) => { if (confirm(`Toggle active?`)) { try { await adminUpdateUser(id, { is_active: !current }); fetchUsers(); } catch (err: any) { toast.error(err.message); } } };
- const handleUpdateUserRole = async (id: number, role: string) => { try { await adminUpdateUser(id, { role }); fetchUsers(); } catch (err: any) { toast.error(err.message); } };
- const handleResetPassword = async (id: number) => {
- const p = prompt("New password:"); if (p) { try { await adminUpdateUser(id, { password: p }); toast.success("Updated"); } catch (err: any) { toast.error(err.message); } }
- };
- // Modal Actions
- const handleAddNew = () => { closeModals(); showAddModal.value = true; };
- const closeModals = () => {
- showAddModal.value = false; editingMaterial.value = null; editingService.value = null; editingPost.value = null; editingOrder.value = null;
- Object.assign(matForm, { name_en: "", name_ru: "", name_me: "", name_ua: "", desc_en: "", desc_ru: "", desc_ua: "", desc_me: "", price_per_cm3: 0, available_colors: [], is_active: true });
- Object.assign(svcForm, { name_en: "", name_ru: "", name_me: "", name_ua: "", desc_en: "", desc_ru: "", desc_ua: "", desc_me: "", tech_type: "", is_active: true });
- Object.assign(userForm, { email: "", password: "", first_name: "", last_name: "", phone: "" });
- Object.assign(postForm, { slug: "", title_en: "", title_me: "", title_ru: "", title_ua: "", category: "Technology", image_url: "", is_published: true });
- Object.assign(orderForm, { total_price: 0, material_name: "", color_name: "", quantity: 1, first_name: "", last_name: "", email: "", phone: "", shipping_address: "", notes: "", review_text: "", rating: 0 });
- };
- function addColor() { if (newColor.value) { matForm.available_colors.push(newColor.value); newColor.value = ""; } }
- function removeColor(idx: number) { matForm.available_colors.splice(idx, 1); }
- async function handleSaveMaterial() {
- try {
- if (editingMaterial.value) await adminUpdateMaterial(editingMaterial.value.id, matForm);
- else await adminCreateMaterial(matForm);
- closeModals(); fetchData();
- } catch (err: any) { toast.error(err.message); }
- }
- async function handleSaveService() {
- try {
- if (editingService.value) await adminUpdateService(editingService.value.id, svcForm);
- else await adminCreateService(svcForm);
- closeModals(); fetchData();
- } catch (err: any) { toast.error(err.message); }
- }
- async function handleSavePost() {
- try {
- if (editingPost.value) await adminUpdatePost(editingPost.value.id, postForm);
- else await adminCreatePost(postForm);
- closeModals(); fetchData();
- } catch (err: any) { toast.error(err.message); }
- }
- async function handleSaveOrder() {
- if (!editingOrder.value) return;
- try {
- await adminUpdateOrder(editingOrder.value.id, orderForm);
- closeModals(); fetchData();
- toast.success("Order updated");
- } catch (err: any) { toast.error(err.message); }
- }
- async function handleSaveUser() { try { await adminCreateUser(userForm); closeModals(); fetchUsers(); } catch (err: any) { toast.error(err.message); } }
- // Lifecycle
- onMounted(async () => {
- if (!authStore.user || authStore.user.role !== "admin") { router.push("/auth"); return; }
- await loadAdminTranslations();
- fetchData();
- window.addEventListener('paste', handlePaste);
- });
- onUnmounted(() => {
- window.removeEventListener('paste', handlePaste);
- });
- async function handlePaste(event: ClipboardEvent) {
- const active = document.activeElement;
- if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) return;
- }
- </script>
|