feat(acquisition): add category grouping for acquisition methods
Add category field to acquisition_methods table with 'General' default Group acquisition methods by category in item and recipe detail views Enable category management in admin configuration
This commit is contained in:
@@ -1169,6 +1169,7 @@ CREATE TABLE IF NOT EXISTS pokemon_favorite_things (
|
||||
CREATE TABLE IF NOT EXISTS acquisition_methods (
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name text NOT NULL UNIQUE,
|
||||
category text NOT NULL DEFAULT 'General',
|
||||
sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0),
|
||||
created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
|
||||
updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
|
||||
@@ -1565,6 +1566,9 @@ ALTER TABLE environments
|
||||
ALTER TABLE favorite_things
|
||||
ADD COLUMN IF NOT EXISTS opposite_favorite_thing_id integer;
|
||||
|
||||
ALTER TABLE acquisition_methods
|
||||
ADD COLUMN IF NOT EXISTS category text NOT NULL DEFAULT 'General';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
|
||||
@@ -155,6 +155,7 @@ type ConfigType =
|
||||
type ConfigDefinition = {
|
||||
table: string;
|
||||
entityType: EntityType;
|
||||
hasCategory?: boolean;
|
||||
hasItemDrop?: boolean;
|
||||
hasTrading?: boolean;
|
||||
hasChangeLog?: boolean;
|
||||
@@ -663,6 +664,7 @@ type DailyChecklistChangeSource = {
|
||||
} & TranslationChangeSource;
|
||||
type ConfigChangeSource = {
|
||||
name: string;
|
||||
category?: string;
|
||||
description?: string;
|
||||
opposite?: { name: string } | null;
|
||||
hasItemDrop?: boolean;
|
||||
@@ -734,7 +736,7 @@ const configDefinitions: Record<ConfigType, ConfigDefinition> = {
|
||||
skills: { table: 'skills', entityType: 'skills', hasItemDrop: true, hasTrading: true },
|
||||
environments: { table: 'environments', entityType: 'environments', hasDescription: true, oppositeColumn: 'opposite_environment_id' },
|
||||
'favorite-things': { table: 'favorite_things', entityType: 'favorite-things', oppositeColumn: 'opposite_favorite_thing_id' },
|
||||
'acquisition-methods': { table: 'acquisition_methods', entityType: 'acquisition-methods' },
|
||||
'acquisition-methods': { table: 'acquisition_methods', entityType: 'acquisition-methods', hasCategory: true },
|
||||
maps: { table: 'maps', entityType: 'maps' },
|
||||
'game-versions': { table: 'game_versions', entityType: 'game-versions', hasChangeLog: true },
|
||||
'dish-flavors': { table: 'dish_flavors', entityType: 'dish-flavors' }
|
||||
@@ -1042,10 +1044,12 @@ async function deleteEntityTranslations(client: DbClient, entityType: EntityType
|
||||
function optionSelect(
|
||||
tableName: string,
|
||||
entityType: EntityType,
|
||||
locale: string
|
||||
): Promise<Array<{ id: number; name: string }>> {
|
||||
locale: string,
|
||||
includeCategory = false
|
||||
): Promise<Array<{ id: number; name: string; category?: string }>> {
|
||||
const name = localizedName(entityType, 'o', locale);
|
||||
return query(`SELECT o.id, ${name} AS name FROM ${tableName} o ORDER BY ${orderByEntity('o')}`);
|
||||
const columns = includeCategory ? `o.id, ${name} AS name, o.category` : `o.id, ${name} AS name`;
|
||||
return query(`SELECT ${columns} FROM ${tableName} o ORDER BY ${orderByEntity('o')}`);
|
||||
}
|
||||
|
||||
function systemListLabel(option: SystemListOption, locale: string): string {
|
||||
@@ -1178,6 +1182,9 @@ function configSelect(definition: ConfigDefinition, locale: string): string {
|
||||
if (definition.hasDescription) {
|
||||
columns.push(`c.description`);
|
||||
}
|
||||
if (definition.hasCategory) {
|
||||
columns.push(`c.category`);
|
||||
}
|
||||
if (definition.oppositeColumn) {
|
||||
columns.push(
|
||||
`
|
||||
@@ -1247,6 +1254,11 @@ function cleanOptionalText(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function cleanConfigCategory(value: unknown): string {
|
||||
const category = cleanOptionalText(value);
|
||||
return category || 'General';
|
||||
}
|
||||
|
||||
function isStaticImageFileName(fileName: string): boolean {
|
||||
return Boolean(fileName) && !fileName.includes('/') && !fileName.includes('\\') && !fileName.includes('..') && /^[A-Za-z0-9._()-]+$/.test(fileName);
|
||||
}
|
||||
@@ -2745,6 +2757,7 @@ function configEditChanges(
|
||||
before: ConfigChangeSource,
|
||||
after: {
|
||||
name: string;
|
||||
category?: string;
|
||||
description: string;
|
||||
translations: TranslationInput;
|
||||
oppositeName: string;
|
||||
@@ -2756,6 +2769,9 @@ function configEditChanges(
|
||||
const changes: EditChange[] = [];
|
||||
pushChange(changes, 'Name', before.name, after.name);
|
||||
pushTranslationChanges(changes, before.translations, after.translations, ['name']);
|
||||
if (definition.hasCategory) {
|
||||
pushChange(changes, 'Category', before.category, after.category);
|
||||
}
|
||||
if (definition.hasDescription) {
|
||||
pushChange(changes, 'Description', before.description, after.description);
|
||||
}
|
||||
@@ -2883,7 +2899,7 @@ export async function getOptions(locale = defaultLocale) {
|
||||
skillOptions(locale),
|
||||
optionSelect('environments', 'environments', locale),
|
||||
optionSelect('favorite_things', 'favorite-things', locale),
|
||||
optionSelect('acquisition_methods', 'acquisition-methods', locale),
|
||||
optionSelect('acquisition_methods', 'acquisition-methods', locale, true),
|
||||
optionSelect('maps', 'maps', locale),
|
||||
gameVersionOptions(locale),
|
||||
optionSelect('dish_flavors', 'dish-flavors', locale),
|
||||
@@ -5581,6 +5597,7 @@ export async function createConfig(type: ConfigType, payload: Record<string, unk
|
||||
const definition = configDefinitions[type];
|
||||
const name = cleanName(payload.name);
|
||||
const translations = cleanTranslations(payload.translations, ['name']);
|
||||
const category = definition.hasCategory ? cleanConfigCategory(payload.category) : '';
|
||||
const description = definition.hasDescription ? cleanOptionalText(payload.description) : '';
|
||||
const oppositeId = definition.oppositeColumn ? cleanOptionalPositiveInteger(payload.oppositeId) : null;
|
||||
const hasItemDrop = definition.hasItemDrop ? Boolean(payload.hasItemDrop) : false;
|
||||
@@ -5595,6 +5612,10 @@ export async function createConfig(type: ConfigType, payload: Record<string, unk
|
||||
columns.push('description');
|
||||
values.push(description);
|
||||
}
|
||||
if (definition.hasCategory) {
|
||||
columns.push('category');
|
||||
values.push(category);
|
||||
}
|
||||
if (definition.hasItemDrop) {
|
||||
columns.push('has_item_drop');
|
||||
values.push(hasItemDrop);
|
||||
@@ -5653,6 +5674,7 @@ export async function updateConfig(
|
||||
const definition = configDefinitions[type];
|
||||
const name = cleanName(payload.name);
|
||||
const translations = cleanTranslations(payload.translations, ['name']);
|
||||
const category = definition.hasCategory ? cleanConfigCategory(payload.category) : '';
|
||||
const description = definition.hasDescription ? cleanOptionalText(payload.description) : '';
|
||||
const oppositeId = definition.oppositeColumn ? cleanOptionalPositiveInteger(payload.oppositeId) : null;
|
||||
const before = await getConfigById(type, id, defaultLocale);
|
||||
@@ -5675,6 +5697,10 @@ export async function updateConfig(
|
||||
values.push(description);
|
||||
assignments.push(`description = $${values.length}`);
|
||||
}
|
||||
if (definition.hasCategory) {
|
||||
values.push(category);
|
||||
assignments.push(`category = $${values.length}`);
|
||||
}
|
||||
if (definition.hasItemDrop) {
|
||||
values.push(hasItemDrop);
|
||||
assignments.push(`has_item_drop = $${values.length}`);
|
||||
@@ -5735,6 +5761,7 @@ export async function updateConfig(
|
||||
const changes = before
|
||||
? configEditChanges(definition, before as ConfigChangeSource, {
|
||||
name,
|
||||
category,
|
||||
description,
|
||||
translations,
|
||||
oppositeName: oppositeId ? oppositeNames.get(oppositeId) ?? '' : '',
|
||||
@@ -6813,7 +6840,7 @@ export async function getItem(id: number, locale = defaultLocale) {
|
||||
] = await Promise.all([
|
||||
query(
|
||||
`
|
||||
SELECT am.id, ${acquisitionMethodName} AS name
|
||||
SELECT am.id, ${acquisitionMethodName} AS name, am.category
|
||||
FROM item_acquisition_methods iam
|
||||
JOIN acquisition_methods am ON am.id = iam.acquisition_method_id
|
||||
WHERE iam.item_id = $1
|
||||
@@ -6828,7 +6855,7 @@ export async function getItem(id: number, locale = defaultLocale) {
|
||||
${resultItemName} AS name,
|
||||
${auditSelect('r', 'recipe_created_user', 'recipe_updated_user')},
|
||||
COALESCE((
|
||||
SELECT json_agg(json_build_object('id', am.id, 'name', ${acquisitionMethodName}) ORDER BY ${orderByEntity('am')})
|
||||
SELECT json_agg(json_build_object('id', am.id, 'name', ${acquisitionMethodName}, 'category', am.category) ORDER BY ${orderByEntity('am')})
|
||||
FROM recipe_acquisition_methods ram
|
||||
JOIN acquisition_methods am ON am.id = ram.acquisition_method_id
|
||||
WHERE ram.recipe_id = r.id
|
||||
@@ -7496,7 +7523,7 @@ export async function getRecipe(id: number, locale = defaultLocale) {
|
||||
${resultItemName} AS name,
|
||||
${auditSelect('r', 'recipe_created_user', 'recipe_updated_user')},
|
||||
COALESCE((
|
||||
SELECT json_agg(json_build_object('id', am.id, 'name', ${acquisitionMethodName}) ORDER BY ${orderByEntity('am')})
|
||||
SELECT json_agg(json_build_object('id', am.id, 'name', ${acquisitionMethodName}, 'category', am.category) ORDER BY ${orderByEntity('am')})
|
||||
FROM recipe_acquisition_methods ram
|
||||
JOIN acquisition_methods am ON am.id = ram.acquisition_method_id
|
||||
WHERE ram.recipe_id = r.id
|
||||
|
||||
Reference in New Issue
Block a user