diff --git a/web-next/app/[locale]/admin/page.tsx b/web-next/app/[locale]/admin/page.tsx
index 393439b..9923b4b 100644
--- a/web-next/app/[locale]/admin/page.tsx
+++ b/web-next/app/[locale]/admin/page.tsx
@@ -22,6 +22,11 @@ export default async function AdminPage({ params }: { params: Promise<{ locale:
{t('manageNews')}
+
+
+ {t('managePrices')}
+
+
)
diff --git a/web-next/app/[locale]/admin/prices/page.tsx b/web-next/app/[locale]/admin/prices/page.tsx
new file mode 100644
index 0000000..2e2f494
--- /dev/null
+++ b/web-next/app/[locale]/admin/prices/page.tsx
@@ -0,0 +1,24 @@
+import { getTranslations, setRequestLocale } from 'next-intl/server'
+import { redirect } from '@/i18n/navigation'
+import { getSession } from '@/lib/session'
+import { isAdmin } from '@/lib/admin'
+import { getAllPrices } from '@/lib/admin-prices'
+import { AdminPricesManager } from '@/components/AdminPricesManager'
+
+export const dynamic = 'force-dynamic'
+
+export default async function AdminPricesPage({ params }: { params: Promise<{ locale: string }> }) {
+ const { locale } = await params
+ setRequestLocale(locale)
+ const t = await getTranslations('Admin')
+ const session = await getSession()
+ if (!session.bnetId) redirect({ href: '/login', locale })
+ if (!(await isAdmin(session))) redirect({ href: '/', locale })
+ const prices = await getAllPrices()
+ return (
+
+ {t('prices')}
+
+
+ )
+}
diff --git a/web-next/app/api/admin/prices/route.ts b/web-next/app/api/admin/prices/route.ts
new file mode 100644
index 0000000..5351c8d
--- /dev/null
+++ b/web-next/app/api/admin/prices/route.ts
@@ -0,0 +1,15 @@
+import { getSession } from '@/lib/session'
+import { isAdmin } from '@/lib/admin'
+import { setPrice } from '@/lib/admin-prices'
+
+export async function POST(request: Request) {
+ const session = await getSession()
+ if (!(await isAdmin(session))) return Response.json({ success: false, error: 'forbidden' }, { status: 403 })
+ let b: Record = {}
+ try { b = await request.json() } catch { return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 }) }
+ const service = String(b.service ?? '')
+ const value = Number(b.value)
+ if (!service || !(value >= 0)) return Response.json({ success: false, error: 'invalidRequest' })
+ const ok = await setPrice(service, value)
+ return Response.json({ success: ok })
+}
diff --git a/web-next/components/AdminPricesManager.tsx b/web-next/components/AdminPricesManager.tsx
new file mode 100644
index 0000000..27a117d
--- /dev/null
+++ b/web-next/components/AdminPricesManager.tsx
@@ -0,0 +1,62 @@
+'use client'
+
+import { useState } from 'react'
+import { useTranslations } from 'next-intl'
+import type { PriceRow } from '@/lib/admin-prices'
+
+function Row({ row }: { row: PriceRow }) {
+ const t = useTranslations('Admin')
+ const [value, setValue] = useState(String(row.price))
+ const [busy, setBusy] = useState(false)
+ const [saved, setSaved] = useState(false)
+
+ async function save() {
+ setBusy(true)
+ setSaved(false)
+ try {
+ const res = await fetch('/api/admin/prices', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'same-origin',
+ body: JSON.stringify({ service: row.service, value: Number(value) }),
+ })
+ const data: { success?: boolean } = await res.json()
+ if (data.success) {
+ setSaved(true)
+ setTimeout(() => setSaved(false), 2000)
+ }
+ } finally {
+ setBusy(false)
+ }
+ }
+
+ return (
+
+ {t(`svc_${row.service}`)}
+
+ setValue(e.target.value)}
+ className="w-24 rounded border border-amber-900/60 bg-[#2c1e14] px-2 py-1 text-right"
+ />
+
+ {saved && {t('saved')}}
+
+
+ )
+}
+
+export function AdminPricesManager({ prices }: { prices: PriceRow[] }) {
+ return (
+
+ {prices.map((row) => (
+
+ ))}
+
+ )
+}
diff --git a/web-next/lib/admin-prices.ts b/web-next/lib/admin-prices.ts
new file mode 100644
index 0000000..d67c49d
--- /dev/null
+++ b/web-next/lib/admin-prices.ts
@@ -0,0 +1,41 @@
+import type { RowDataPacket } from 'mysql2'
+import { db, DB } from './db'
+
+// Servicios con precio único (una fila) -> tabla django_wow.
+export const PRICE_TABLES: Record = {
+ rename: 'home_renameprice',
+ customize: 'home_customizeprice',
+ 'change-race': 'home_changeraceprice',
+ 'change-faction': 'home_changefactionprice',
+ 'level-up': 'home_levelupprice',
+ transfer: 'home_transferprice',
+}
+
+export interface PriceRow {
+ service: string
+ price: number
+}
+
+export async function getAllPrices(): Promise {
+ const out: PriceRow[] = []
+ for (const [service, table] of Object.entries(PRICE_TABLES)) {
+ let price = 0
+ try {
+ const [rows] = await db(DB.default).query(`SELECT price FROM ${table} LIMIT 1`)
+ price = rows[0] ? Number(rows[0].price) : 0
+ } catch {
+ /* ignore */
+ }
+ out.push({ service, price })
+ }
+ return out
+}
+
+/** Fija el precio (fila única) de un servicio. */
+export async function setPrice(service: string, value: number): Promise {
+ const table = PRICE_TABLES[service]
+ if (!table || !(value >= 0)) return false
+ await db(DB.default).query(`DELETE FROM ${table}`)
+ await db(DB.default).query(`INSERT INTO ${table} (price) VALUES (?)`, [value])
+ return true
+}
diff --git a/web-next/messages/en.json b/web-next/messages/en.json
index 73fd56e..ce17bb7 100644
--- a/web-next/messages/en.json
+++ b/web-next/messages/en.json
@@ -281,6 +281,17 @@
"noNews": "No news.",
"forbidden": "You do not have permission to access here.",
"emptyError": "Fill in the title and content.",
- "genericError": "Something went wrong."
+ "genericError": "Something went wrong.",
+ "managePrices": "Manage prices",
+ "prices": "Service prices",
+ "price": "Price (€)",
+ "save": "Save",
+ "saved": "Saved",
+ "svc_rename": "Rename character",
+ "svc_customize": "Customize",
+ "svc_change-race": "Change race",
+ "svc_change-faction": "Change faction",
+ "svc_level-up": "Level up to 80",
+ "svc_transfer": "Transfer character"
}
}
diff --git a/web-next/messages/es.json b/web-next/messages/es.json
index 9007def..e5d1042 100644
--- a/web-next/messages/es.json
+++ b/web-next/messages/es.json
@@ -281,6 +281,17 @@
"noNews": "No hay noticias.",
"forbidden": "No tienes permiso para acceder aquí.",
"emptyError": "Completa el título y el contenido.",
- "genericError": "Algo ha salido mal."
+ "genericError": "Algo ha salido mal.",
+ "managePrices": "Gestionar precios",
+ "prices": "Precios de servicios",
+ "price": "Precio (€)",
+ "save": "Guardar",
+ "saved": "Guardado",
+ "svc_rename": "Renombrar personaje",
+ "svc_customize": "Personalizar",
+ "svc_change-race": "Cambiar raza",
+ "svc_change-faction": "Cambiar facción",
+ "svc_level-up": "Subir a nivel 80",
+ "svc_transfer": "Transferir personaje"
}
}