diff --git a/dDashboardServer/routes/APIRoutes.cpp b/dDashboardServer/routes/APIRoutes.cpp index 9abf66b63..3387d1bee 100644 --- a/dDashboardServer/routes/APIRoutes.cpp +++ b/dDashboardServer/routes/APIRoutes.cpp @@ -12,6 +12,7 @@ #include "BehaviorXml.h" #include "CharacterTools.h" #include "ClientAssets.h" +#include "CharacterXml.h" #include "Scenery.h" #include "Workers.h" #include "LiveWorld.h" @@ -88,6 +89,30 @@ namespace { return json.dump(); } + /** + * Adds each pet's LOT and kind (its CDClient name) to pet_names rows. pet_names doesn't hold the LOT; the owner's + * save does (

), so each owner on the page is read once. Pets whose owner is unknown, or who have + * left the owner's save, get lot 0. + */ + std::string WithPetKinds(const std::string& raw) { + auto json = nlohmann::json::parse(raw, nullptr, false); + if (json.is_discarded() || !json.contains("data") || !json["data"].is_array()) return raw; + std::map> owners; + for (auto& row : json["data"]) { + const auto owner = GeneralUtils::TryParse(row.value("owner_id", std::string{})).value_or(0); + const auto pet = GeneralUtils::TryParse(row.value("id", std::string{})).value_or(0); + LOT lot = 0; + if (owner && pet) { + auto [it, added] = owners.try_emplace(owner); + if (added) for (const auto& held : CharacterXml::Pets(Database::Get()->GetCharacterXml(owner))) it->second[held.id] = held.lot; + if (const auto found = it->second.find(pet); found != it->second.end()) lot = found->second; + } + row["lot"] = lot; + row["kind"] = lot > 0 ? ClientAssets::ItemName(lot) : ""; + } + return json.dump(); + } + // Register a DataTables endpoint. The fetcher returns the DB layer's JSON string. Access: a GM level or a Perm. template void TableRoute(const std::string& path, const Access& access, const std::string& description, TableFetcher fetcher) { @@ -1290,12 +1315,14 @@ namespace { } void RegisterPetRoutes() { - TableRoute("/api/tables/pet_names", Perm("moderate_pet_names"), "Pet names (DataTables)", [](const DataTablesRequest& r, const nlohmann::json& body) { - return Database::Get()->GetPetNamesTable(r.start, r.length, r.search, r.orderColumn, r.orderAsc, body.value("pending", false)); + TableRoute("/api/tables/pet_names", Perm("moderate_pet_names"), "Pet names (DataTables); rows include the pet's lot and kind. Order columns: 0 id, 2 name, 3 status, 4 owner", [](const DataTablesRequest& r, const nlohmann::json& body) { + // The page shows the pet's kind (not sortable) as column 1; the database numbers its columns without it + const uint32_t column = r.orderColumn >= 2 ? r.orderColumn - 1 : 0; + return WithPetKinds(Database::Get()->GetPetNamesTable(r.start, r.length, r.search, column, r.orderAsc, body.value("pending", false))); }); - TableRoute("/api/tables/pending_pet_names", Perm("moderate_pet_names"), "Pet names awaiting review (DataTables)", [](const DataTablesRequest& r, const nlohmann::json&) { - return Database::Get()->GetPetNamesTable(r.start, r.length, r.search, r.orderColumn, r.orderAsc, true); + TableRoute("/api/tables/pending_pet_names", Perm("moderate_pet_names"), "Pet names awaiting review (DataTables); rows include the pet's lot and kind", [](const DataTablesRequest& r, const nlohmann::json&) { + return WithPetKinds(Database::Get()->GetPetNamesTable(r.start, r.length, r.search, r.orderColumn, r.orderAsc, true)); }); Route(eHTTPMethod::POST, "/api/pet_names/:id/approve", Perm("moderate_pet_names"), "Approve a pet name", diff --git a/dDashboardServer/static/js/common.js b/dDashboardServer/static/js/common.js index 29018a177..90d11dffc 100644 --- a/dDashboardServer/static/js/common.js +++ b/dDashboardServer/static/js/common.js @@ -135,6 +135,12 @@ if (!id || id === '0') return '-'; return name ? fmt.link('/characters/' + id, name) : 'Unknown (' + esc(id) + ')'; }, + // A pet's kind: its icon and CDClient name (rows from the pet name tables carry lot and kind) + pet: function (lot, kind) { + if (!lot) return 'Unknown'; + return '' + + esc(kind || ('LOT ' + lot)) + ' (' + esc(lot) + ')'; + }, zone: function (id, name) { return esc(name || ('Zone ' + id)) + ' (' + esc(id) + ')'; } diff --git a/dDashboardServer/templates/moderation.jinja2 b/dDashboardServer/templates/moderation.jinja2 index ac075523b..382f57e22 100644 --- a/dDashboardServer/templates/moderation.jinja2 +++ b/dDashboardServer/templates/moderation.jinja2 @@ -24,7 +24,7 @@

Pending Pet Names
- +
IDPet NameOwnerActions
IDPetPet NameOwnerActions
@@ -78,6 +78,7 @@ $(function () { if (DASH.can('moderate_pet_names')) { serverTable('#pendingPetsTable', '/api/tables/pending_pet_names', [ { data: 'id', orderable: false }, + { data: 'lot', orderable: false, render: function (d, t, row) { return fmt.pet(row.lot, row.kind); } }, { data: 'pet_name', orderable: false }, { data: 'owner_id', orderable: false, defaultContent: '', render: petOwner }, { data: 'id', orderable: false, render: function (d, t, row) { return actionButtons('pet', d, row.owner_id, row.pet_name); } } diff --git a/dDashboardServer/templates/pet_names.jinja2 b/dDashboardServer/templates/pet_names.jinja2 index 15be697d2..673f41d83 100644 --- a/dDashboardServer/templates/pet_names.jinja2 +++ b/dDashboardServer/templates/pet_names.jinja2 @@ -18,6 +18,7 @@ ID + Pet Pet Name Status Owner @@ -36,6 +37,7 @@ $(function () { var pending = document.getElementById('pendingOnly'); var table = serverTable('#petNamesTable', '/api/tables/pet_names', [ { data: 'id' }, + { data: 'lot', orderable: false, render: function (d, t, row) { return fmt.pet(row.lot, row.kind); } }, { data: 'pet_name' }, // 1 = pending moderation, 2 = approved (rejected names are deleted) { data: 'approved', render: function (d) { return d === 2 ? fmt.badge('Approved', 'success') : fmt.badge('Pending', 'warning'); } }, diff --git a/docs/Dashboard.md b/docs/Dashboard.md index a230d0e41..e464c3825 100644 --- a/docs/Dashboard.md +++ b/docs/Dashboard.md @@ -721,6 +721,9 @@ tells the player in game if they're online. Rejecting asks for an optional reaso account page next to what they asked for. The queues show only what the game itself would still review: rejected properties and names whose player must pick a new one no longer appear. +Pet names show what kind of pet each one is (its icon and name from the CDClient), read from the pet's entry in its +owner's save. A pet whose owner is unknown or no longer has it shows as Unknown. + ### Moderation history, warnings and bans Each account page has a **Moderation history** (GM 2+, `accounts_notes`): notes and warnings staff write, plus every