x
This commit is contained in:
1 parent
570810acdb
commit
64bcd6388a
5 files changed
+917
-30
No files matched your search
@@ -1,13 +1,78 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { API_BASE } from '@homaroma/types'
|
||||
import { Loader2, DatabaseZap, CheckCircle2, AlertCircle } from 'lucide-react'
|
||||
import {
|
||||
Loader2,
|
||||
DatabaseZap,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Trash2,
|
||||
AlertTriangle,
|
||||
RefreshCw,
|
||||
Package,
|
||||
FolderTree,
|
||||
Tag,
|
||||
Sparkles,
|
||||
ShieldAlert,
|
||||
Layers,
|
||||
X
|
||||
} from 'lucide-react'
|
||||
|
||||
type SeedResult = { success: boolean; message: string; results?: { slug: string; status: string }[] }
|
||||
|
||||
type CatalogStats = {
|
||||
products: number
|
||||
variants: number
|
||||
categories: number
|
||||
brands: number
|
||||
demoCategories: { id: number; name: string; productCount: number }[]
|
||||
demoCategoriesCount: number
|
||||
demoBrandsCount: number
|
||||
}
|
||||
|
||||
type ActionModal = {
|
||||
type: 'purge-demo' | 'purge-products' | 'purge-categories' | 'purge-catalog'
|
||||
title: string
|
||||
description: string
|
||||
confirmText: string
|
||||
dangerLevel: 'high' | 'critical'
|
||||
} | null
|
||||
|
||||
export default function SystemMaintenancePage() {
|
||||
// Page Seeding State
|
||||
const [seeding, setSeeding] = useState(false)
|
||||
const [seedResult, setSeedResult] = useState<SeedResult | null>(null)
|
||||
|
||||
// Catalog Stats State
|
||||
const [stats, setStats] = useState<CatalogStats | null>(null)
|
||||
const [loadingStats, setLoadingStats] = useState(false)
|
||||
|
||||
// Maintenance Action Modal & Loading
|
||||
const [actionModal, setActionModal] = useState<ActionModal>(null)
|
||||
const [actionLoading, setActionLoading] = useState(false)
|
||||
const [actionResult, setActionResult] = useState<{ success: boolean; message: string } | null>(null)
|
||||
const [cascadeCategories, setCascadeCategories] = useState(true)
|
||||
const [clearBrandsWithCatalog, setClearBrandsWithCatalog] = useState(false)
|
||||
const [confirmedByUser, setConfirmedByUser] = useState(false)
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
setLoadingStats(true)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/settings/system/catalog-stats`)
|
||||
const data = await res.json()
|
||||
if (res.ok && data.success) {
|
||||
setStats(data.stats)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load stats:', err)
|
||||
} finally {
|
||||
setLoadingStats(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats()
|
||||
}, [fetchStats])
|
||||
|
||||
const handleSeedPages = async () => {
|
||||
setSeeding(true)
|
||||
setSeedResult(null)
|
||||
@@ -26,16 +91,347 @@ export default function SystemMaintenancePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const openActionModal = (modal: ActionModal) => {
|
||||
setActionResult(null)
|
||||
setConfirmedByUser(false)
|
||||
setActionModal(modal)
|
||||
}
|
||||
|
||||
const closeActionModal = () => {
|
||||
if (actionLoading) return
|
||||
setActionModal(null)
|
||||
setConfirmedByUser(false)
|
||||
}
|
||||
|
||||
const executeAction = async () => {
|
||||
if (!actionModal) return
|
||||
setActionLoading(true)
|
||||
setActionResult(null)
|
||||
|
||||
try {
|
||||
let endpoint = ''
|
||||
let payload: any = {}
|
||||
|
||||
if (actionModal.type === 'purge-demo') {
|
||||
endpoint = `${API_BASE}/settings/system/purge-demo-homaroma`
|
||||
} else if (actionModal.type === 'purge-products') {
|
||||
endpoint = `${API_BASE}/settings/system/purge-products`
|
||||
} else if (actionModal.type === 'purge-categories') {
|
||||
endpoint = `${API_BASE}/settings/system/purge-categories`
|
||||
payload = { forceCascadeProducts: cascadeCategories }
|
||||
} else if (actionModal.type === 'purge-catalog') {
|
||||
endpoint = `${API_BASE}/settings/system/purge-catalog`
|
||||
payload = { clearProducts: true, clearCategories: true, clearBrands: clearBrandsWithCatalog }
|
||||
}
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: Object.keys(payload).length > 0 ? JSON.stringify(payload) : undefined,
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
if (res.ok && data.success) {
|
||||
setActionResult({ success: true, message: data.message || 'Operation completed successfully' })
|
||||
setActionModal(null)
|
||||
await fetchStats()
|
||||
} else {
|
||||
setActionResult({ success: false, message: data.error || data.message || 'Operation failed' })
|
||||
}
|
||||
} catch (err: any) {
|
||||
setActionResult({ success: false, message: err.message || 'Network error occurred' })
|
||||
} finally {
|
||||
setActionLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl space-y-8 animate-fade-in">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold">System Maintenance</h1>
|
||||
<p className="text-slate-400 text-sm">Utilities for initializing or repairing system data.</p>
|
||||
<div className="max-w-4xl space-y-8 animate-fade-in pb-16">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold text-white">System Maintenance</h1>
|
||||
<p className="text-slate-400 text-sm">
|
||||
เครื่องมือจัดการและกู้คืนระบบ ซ่อมแซม และล้างข้อมูลแคตตาล็อกที่ตกค้างจากการ Seed
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={fetchStats}
|
||||
disabled={loadingStats}
|
||||
className="self-start md:self-auto flex items-center gap-2 px-3.5 py-2 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 text-xs font-semibold text-slate-300 transition cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${loadingStats ? 'animate-spin text-cyan-400' : 'text-slate-400'}`} />
|
||||
<span>รีเฟรชสถิติ</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="admin-card p-8 space-y-6">
|
||||
{/* Global Status Banner if any action completed */}
|
||||
{actionResult && (
|
||||
<div
|
||||
className={`rounded-xl p-4 text-sm flex items-start gap-3 border shadow-lg ${
|
||||
actionResult.success
|
||||
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-200'
|
||||
: 'bg-red-500/10 border-red-500/30 text-red-200'
|
||||
}`}
|
||||
>
|
||||
{actionResult.success ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-400 shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<h4 className="font-bold text-xs uppercase tracking-wider mb-0.5">
|
||||
{actionResult.success ? 'ดำเนินการสำเร็จ' : 'เกิดข้อผิดพลาด'}
|
||||
</h4>
|
||||
<p className="text-xs leading-relaxed">{actionResult.message}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setActionResult(null)}
|
||||
className="text-slate-400 hover:text-white p-1 rounded-md transition"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Live Catalog Statistics Card */}
|
||||
<div className="admin-card p-6 bg-night-900 border border-white/10 rounded-2xl shadow-xl space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-white/10 pb-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Layers className="w-5 h-5 text-cyan-400" />
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-white">
|
||||
สถิติข้อมูลในฐานข้อมูล (Database Catalog Stats)
|
||||
</h2>
|
||||
</div>
|
||||
{stats && stats.demoCategoriesCount > 0 && (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-amber-500/10 text-amber-300 border border-amber-500/20">
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
พบข้อมูล Demo Hom Aroma ({stats.demoCategoriesCount} หมวดหมู่)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<div className="p-4 rounded-xl bg-night-950/60 border border-white/5 space-y-1">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<Package className="w-3.5 h-3.5 text-blue-400" />
|
||||
<span>สินค้า (Products)</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-2xl font-bold text-white">{stats ? stats.products.toLocaleString() : '...'}</span>
|
||||
<span className="text-[11px] text-slate-500">
|
||||
({stats ? stats.variants.toLocaleString() : '...'} variants)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-xl bg-night-950/60 border border-white/5 space-y-1">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<FolderTree className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>หมวดหมู่ (Categories)</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-2xl font-bold text-white">{stats ? stats.categories.toLocaleString() : '...'}</span>
|
||||
<span className="text-[11px] text-slate-500">หมวดหมู่</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-xl bg-night-950/60 border border-white/5 space-y-1">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<Tag className="w-3.5 h-3.5 text-purple-400" />
|
||||
<span>แบรนด์ (Brands)</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-2xl font-bold text-white">{stats ? stats.brands.toLocaleString() : '...'}</span>
|
||||
<span className="text-[11px] text-slate-500">แบรนด์</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-xl bg-night-950/60 border border-white/5 space-y-1">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-amber-400" />
|
||||
<span>Demo ติดมา</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className={`text-2xl font-bold ${stats && stats.demoCategoriesCount > 0 ? 'text-amber-400' : 'text-slate-400'}`}>
|
||||
{stats ? stats.demoCategoriesCount : '...'}
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-500">หมวดหมู่เดโม</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stats && stats.demoCategories.length > 0 && (
|
||||
<div className="pt-2 text-xs text-slate-400 flex flex-wrap items-center gap-2">
|
||||
<span className="text-slate-500 font-medium">หมวดหมู่ตัวอย่าง Hom Aroma:</span>
|
||||
{stats.demoCategories.map((c) => (
|
||||
<span
|
||||
key={c.id}
|
||||
className="px-2 py-0.5 rounded bg-white/5 border border-white/10 text-slate-300 font-mono text-[11px]"
|
||||
>
|
||||
{c.name} {c.productCount > 0 && <span className="text-amber-400">({c.productCount})</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Catalog Cleanup & Relational Reset Card (NEW) */}
|
||||
<div className="admin-card p-8 bg-night-900 border border-red-500/20 rounded-2xl shadow-xl space-y-6">
|
||||
<div className="flex items-center justify-between border-b border-white/10 pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 bg-red-500/10 border border-red-500/20 rounded-xl">
|
||||
<ShieldAlert className="w-6 h-6 text-red-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold uppercase tracking-wider text-white">
|
||||
Catalog & Data Cleanup (จัดการและล้างข้อมูลสินค้า / หมวดหมู่)
|
||||
</h2>
|
||||
<p className="text-xs text-slate-400 mt-0.5">
|
||||
ล้างข้อมูล Seed หรือสินค้า/หมวดหมู่ที่ติด Relation Constraint อย่างปลอดภัยและสะอาด 100%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{/* Option 1: ล้างเฉพาะข้อมูลตัวอย่าง Hom Aroma (Recommended) */}
|
||||
<div className="p-5 rounded-xl bg-night-950/80 border border-amber-500/30 hover:border-amber-500/50 transition flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-amber-400" />
|
||||
<h3 className="text-sm font-bold text-white">ล้างเฉพาะข้อมูลตัวอย่าง Hom Aroma</h3>
|
||||
</div>
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-amber-500/20 text-amber-300 border border-amber-500/30">
|
||||
แนะนำ
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 leading-relaxed">
|
||||
ลบเฉพาะ 5 หมวดหมู่ตัวอย่าง (Oil Ozone, Aroma Oil, Reed Diffuser, Room Spray, Car Diffuser), สินค้าที่ผูกอยู่, และแบรนด์กลิ่นอโรมาที่ไม่ถูกใช้งาน <strong>โดยไม่กระทบข้อมูลสินค้าหรือหมวดหมู่อื่นๆ ที่สร้างไว้</strong>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openActionModal({
|
||||
type: 'purge-demo',
|
||||
title: 'ยืนยันล้างข้อมูลตัวอย่าง Hom Aroma',
|
||||
description:
|
||||
'ระบบจะทำการลบ 5 หมวดหมู่ตัวอย่าง (Oil Ozone, Aroma Oil, Reed Diffuser, Room Spray, Car Diffuser) พร้อมสินค้าและ Variants ที่ผูกอยู่ และเคลียร์แบรนด์กลิ่นอโรมาที่ไม่ถูกใช้งาน',
|
||||
confirmText: 'ล้างข้อมูล Demo Hom Aroma',
|
||||
dangerLevel: 'high',
|
||||
})
|
||||
}
|
||||
className="w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-lg bg-amber-500/10 border border-amber-500/30 hover:bg-amber-500/20 text-amber-400 text-xs font-bold uppercase tracking-wider transition cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>ล้างข้อมูล Demo Hom Aroma</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Option 2: ลบสินค้าทั้งหมด (Purge All Products) */}
|
||||
<div className="p-5 rounded-xl bg-night-950/80 border border-red-500/20 hover:border-red-500/40 transition flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="w-4 h-4 text-red-400" />
|
||||
<h3 className="text-sm font-bold text-white">ลบสินค้าทั้งหมด (Purge All Products)</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 leading-relaxed">
|
||||
ลบสินค้าทุกรายการในระบบ ({stats?.products || 0} รายการ) พร้อมตัด Foreign Keys ลบ Variants, ภาพถ่าย, สเปก (Attributes), Detail Sections และรีวิวทั้งหมด <strong>ไม่ติดปัญหา Relation ใดๆ แน่นอน</strong>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openActionModal({
|
||||
type: 'purge-products',
|
||||
title: 'ยืนยันลบสินค้าทั้งหมดในระบบ',
|
||||
description: `คุณกำลังจะลบสินค้าทั้งหมดจำนวน ${stats?.products || 0} รายการ (${stats?.variants || 0} variants) ข้อมูลและภาพถ่ายทั้งหมดจะถูกลบออกจากฐานข้อมูลอย่างถาวร`,
|
||||
confirmText: 'ลบสินค้าทั้งหมดทันที',
|
||||
dangerLevel: 'critical',
|
||||
})
|
||||
}
|
||||
className="w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-lg bg-red-500/10 border border-red-500/30 hover:bg-red-500/20 text-red-400 text-xs font-bold uppercase tracking-wider transition cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>ลบสินค้าทั้งหมด ({stats?.products || 0})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Option 3: ลบหมวดหมู่ทั้งหมด (Purge All Categories) */}
|
||||
<div className="p-5 rounded-xl bg-night-950/80 border border-red-500/20 hover:border-red-500/40 transition flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderTree className="w-4 h-4 text-red-400" />
|
||||
<h3 className="text-sm font-bold text-white">ลบหมวดหมู่ทั้งหมด (Purge All Categories)</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 leading-relaxed">
|
||||
ลบหมวดหมู่ทั้งหมดในระบบ ({stats?.categories || 0} หมวดหมู่) รวมถึง Media, Section และคำแปลภาษาทั้งหมด สามารถเปิดตัวเลือกล้างสินค้าที่ผูกอยู่ไปด้วยกันได้
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openActionModal({
|
||||
type: 'purge-categories',
|
||||
title: 'ยืนยันลบหมวดหมู่ทั้งหมดในระบบ',
|
||||
description: `คุณกำลังจะลบหมวดหมู่สินค้าทั้งหมดจำนวน ${stats?.categories || 0} หมวดหมู่`,
|
||||
confirmText: 'ลบหมวดหมู่ทั้งหมดทันที',
|
||||
dangerLevel: 'critical',
|
||||
})
|
||||
}
|
||||
className="w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-lg bg-red-500/10 border border-red-500/30 hover:bg-red-500/20 text-red-400 text-xs font-bold uppercase tracking-wider transition cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>ลบหมวดหมู่ทั้งหมด ({stats?.categories || 0})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Option 4: ล้างแคตตาล็อกทั้งหมด (Reset Entire Catalog) */}
|
||||
<div className="p-5 rounded-xl bg-night-950/80 border border-red-600/30 hover:border-red-500/60 transition flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<DatabaseZap className="w-4 h-4 text-red-400" />
|
||||
<h3 className="text-sm font-bold text-white">ล้างแคตตาล็อกทั้งหมด (Full Catalog Reset)</h3>
|
||||
</div>
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-red-500/20 text-red-300 border border-red-500/30">
|
||||
Full Reset
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 leading-relaxed">
|
||||
ล้างทั้งสินค้า ({stats?.products || 0}) และหมวดหมู่ ({stats?.categories || 0}) ทั้งหมดในครั้งเดียว คืนพื้นที่ให้ฐานข้อมูลว่างพร้อมสำหรับการ <strong>Import สินค้าและหมวดหมู่ชุดใหม่</strong> ทันที
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openActionModal({
|
||||
type: 'purge-catalog',
|
||||
title: 'ยืนยันล้างแคตตาล็อกสินค้าและหมวดหมู่ทั้งหมด',
|
||||
description:
|
||||
'การดำเนินการนี้จะลบสินค้าและหมวดหมู่ทั้งหมดในระบบอย่างสมบูรณ์แบบ ข้อมูลร้านค้า โครงสร้างหน้าเว็บ (CMS) และบัญชีผู้ใช้จะยังคงอยู่',
|
||||
confirmText: 'ล้างแคตตาล็อกทั้งหมดทันที',
|
||||
dangerLevel: 'critical',
|
||||
})
|
||||
}
|
||||
className="w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-lg bg-red-600/20 border border-red-500/40 hover:bg-red-600/30 text-red-300 text-xs font-bold uppercase tracking-wider transition cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>ล้างแคตตาล็อกทั้งหมด</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Seed Default Pages Card (Original) */}
|
||||
<div className="admin-card p-8 bg-night-900 border border-white/10 rounded-2xl shadow-xl space-y-6">
|
||||
<div className="flex items-center gap-3 border-b border-white/10 pb-4">
|
||||
<div className="p-2 bg-amber-500/10 rounded-lg"><DatabaseZap className="w-5 h-5 text-amber-400" /></div>
|
||||
<div className="p-2 bg-amber-500/10 rounded-lg">
|
||||
<DatabaseZap className="w-5 h-5 text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest text-white">Seed Default Pages</h2>
|
||||
<p className="text-xs text-slate-400 mt-0.5">สร้าง CMS page layouts เริ่มต้นสำหรับทุกหน้าในระบบ</p>
|
||||
@@ -55,7 +451,7 @@ export default function SystemMaintenancePage() {
|
||||
type="button"
|
||||
onClick={handleSeedPages}
|
||||
disabled={seeding}
|
||||
className="shrink-0 flex items-center gap-2 rounded-lg bg-amber-500/10 border border-amber-500/30 px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-amber-400 transition hover:bg-amber-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="shrink-0 flex items-center gap-2 rounded-lg bg-amber-500/10 border border-amber-500/30 px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-amber-400 transition hover:bg-amber-500/20 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
{seeding ? <Loader2 className="w-4 h-4 animate-spin" /> : <DatabaseZap className="w-4 h-4" />}
|
||||
{seeding ? 'Running...' : 'Run Seed'}
|
||||
@@ -63,16 +459,24 @@ export default function SystemMaintenancePage() {
|
||||
</div>
|
||||
|
||||
{seedResult && (
|
||||
<div className={`rounded-lg p-4 text-sm space-y-3 ${seedResult.success ? 'bg-emerald-500/10 border border-emerald-500/20' : 'bg-red-500/10 border border-red-500/20'}`}>
|
||||
<div
|
||||
className={`rounded-lg p-4 text-sm space-y-3 ${
|
||||
seedResult.success
|
||||
? 'bg-emerald-500/10 border border-emerald-500/20'
|
||||
: 'bg-red-500/10 border border-red-500/20'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{seedResult.success
|
||||
? <CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||
: <AlertCircle className="w-4 h-4 text-red-400 shrink-0" />}
|
||||
{seedResult.success ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||
) : (
|
||||
<AlertCircle className="w-4 h-4 text-red-400 shrink-0" />
|
||||
)}
|
||||
<span className={seedResult.success ? 'text-emerald-300' : 'text-red-300'}>{seedResult.message}</span>
|
||||
</div>
|
||||
{seedResult.results && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{seedResult.results.map(r => (
|
||||
{seedResult.results.map((r) => (
|
||||
<div key={r.slug} className="flex items-center gap-1.5 text-xs">
|
||||
<span className={r.status === 'created' ? 'text-emerald-400' : 'text-slate-500'}>●</span>
|
||||
<span className={r.status === 'created' ? 'text-slate-200' : 'text-slate-500'}>{r.slug}</span>
|
||||
@@ -84,6 +488,93 @@ export default function SystemMaintenancePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
{actionModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-fade-in">
|
||||
<div className="w-full max-w-lg bg-night-900 border border-red-500/40 rounded-2xl p-6 shadow-2xl space-y-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-xl shrink-0">
|
||||
<AlertTriangle className="w-6 h-6 text-red-400" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<h3 className="text-lg font-bold text-white">{actionModal.title}</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">{actionModal.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sub-options for Categories / Catalog */}
|
||||
{actionModal.type === 'purge-categories' && (
|
||||
<div className="p-3.5 rounded-xl bg-night-950 border border-white/10 space-y-2">
|
||||
<label className="flex items-center gap-2.5 text-xs text-slate-300 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={cascadeCategories}
|
||||
onChange={(e) => setCascadeCategories(e.target.checked)}
|
||||
className="rounded border-white/20 bg-night-900 text-red-500 focus:ring-red-500/40"
|
||||
/>
|
||||
<span>
|
||||
ลบสินค้าทั้งหมดที่ผูกอยู่กับหมวดหมู่อัตโนมัติ (Cascade Delete Products)
|
||||
</span>
|
||||
</label>
|
||||
<p className="text-[11px] text-slate-500 pl-6">
|
||||
แนะนำให้เปิดไว้เพื่อป้องกัน Error ข้อจำกัด Foreign Key ระหว่างตาราง
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actionModal.type === 'purge-catalog' && (
|
||||
<div className="p-3.5 rounded-xl bg-night-950 border border-white/10 space-y-2">
|
||||
<label className="flex items-center gap-2.5 text-xs text-slate-300 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={clearBrandsWithCatalog}
|
||||
onChange={(e) => setClearBrandsWithCatalog(e.target.checked)}
|
||||
className="rounded border-white/20 bg-night-900 text-red-500 focus:ring-red-500/40"
|
||||
/>
|
||||
<span>
|
||||
ลบแบรนด์สินค้าทั้งหมดที่ไม่มีสินค้าผูกอยู่ด้วย (Clear Unused Brands)
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mandatory User Confirmation Checkbox */}
|
||||
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20">
|
||||
<label className="flex items-center gap-2.5 text-xs font-semibold text-red-300 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={confirmedByUser}
|
||||
onChange={(e) => setConfirmedByUser(e.target.checked)}
|
||||
className="rounded border-red-500/40 bg-night-900 text-red-500 focus:ring-red-500/40 w-4 h-4"
|
||||
/>
|
||||
<span>ฉันเข้าใจความเสี่ยง และต้องการยืนยันการลบข้อมูลนี้ถาวร</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end gap-3 pt-2 border-t border-white/10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeActionModal}
|
||||
disabled={actionLoading}
|
||||
className="px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 text-xs font-semibold text-slate-300 transition cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
ยกเลิก
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={executeAction}
|
||||
disabled={!confirmedByUser || actionLoading}
|
||||
className="flex items-center gap-2 px-5 py-2 rounded-lg bg-red-600 hover:bg-red-500 border border-red-400 text-xs font-bold text-white uppercase tracking-wider transition shadow-lg shadow-red-600/30 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
{actionLoading && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
<span>{actionLoading ? 'กำลังดำเนินการ...' : actionModal.confirmText}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user