x
This commit is contained in:
1 parent
27cc56abaf
commit
0c3faaba29
11 files changed
+2286
-482
No files matched your search
@@ -264,7 +264,7 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
|
||||
const limit = query.limit ? parseInt(query.limit) : 12;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [category, total] = await Promise.all([
|
||||
const [category, total, availableFilters] = await Promise.all([
|
||||
prisma.category.findUnique({
|
||||
where: { id, deletedAt: null },
|
||||
include: {
|
||||
@@ -278,6 +278,14 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
|
||||
translations: true,
|
||||
variants: { orderBy: { price: "asc" as const } },
|
||||
images: { orderBy: { order: "asc" as const } },
|
||||
attributes: {
|
||||
include: {
|
||||
attributeDefinition: {
|
||||
include: { translations: true },
|
||||
},
|
||||
translations: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
skip,
|
||||
@@ -289,10 +297,45 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
|
||||
},
|
||||
}),
|
||||
prisma.product.count({ where: { categoryId: id, isDeleted: false } }),
|
||||
prisma.attributeDefinition.findMany({
|
||||
where: {
|
||||
isFilterable: true,
|
||||
OR: [{ categoryId: id }, { categoryId: null }],
|
||||
},
|
||||
include: {
|
||||
translations: true,
|
||||
},
|
||||
orderBy: [
|
||||
{ sortOrder: 'asc' },
|
||||
{ code: 'asc' },
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!category) return status(404, { message: "Category not found" });
|
||||
|
||||
const formattedAvailableFilters = availableFilters.map((def: any) => {
|
||||
const transMap: Record<string, string> = {};
|
||||
def.translations?.forEach((t: any) => {
|
||||
transMap[t.langCode] = t.label;
|
||||
});
|
||||
const label = lang === 'all'
|
||||
? (Object.keys(transMap).length > 0 ? transMap : { th: def.label, en: def.label })
|
||||
: (transMap[lang] || def.label);
|
||||
|
||||
return {
|
||||
id: def.id,
|
||||
code: def.code,
|
||||
label,
|
||||
type: def.type,
|
||||
unit: def.unit,
|
||||
options: def.options,
|
||||
isFilterable: def.isFilterable,
|
||||
sortOrder: def.sortOrder,
|
||||
categoryId: def.categoryId,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
category: {
|
||||
id: category.id,
|
||||
@@ -303,8 +346,10 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
|
||||
featuredSeries: category.featuredSeries,
|
||||
uspBenefits: category.uspBenefits,
|
||||
filterConfig: category.filterConfig,
|
||||
availableFilters: formattedAvailableFilters,
|
||||
_count: category._count,
|
||||
},
|
||||
availableFilters: formattedAvailableFilters,
|
||||
data: category.products.map((p: any) => {
|
||||
if (lang === 'all') {
|
||||
const nameObj: any = {};
|
||||
|
||||
@@ -18,20 +18,94 @@ function parseId(value: string, label: string) {
|
||||
}
|
||||
|
||||
function buildDefinitionPayload(payload: any) {
|
||||
const baseLabel = typeof payload.label === 'string'
|
||||
? payload.label
|
||||
: (payload.label?.th || payload.label?.en || Object.values(payload.label || {})[0] || payload.translations?.th?.label || payload.translations?.en?.label || (typeof payload.translations?.th === 'string' ? payload.translations.th : '') || (typeof payload.translations?.en === 'string' ? payload.translations.en : '') || payload.code);
|
||||
|
||||
const baseDesc = typeof payload.description === 'string'
|
||||
? payload.description
|
||||
: (payload.description?.th || payload.description?.en || Object.values(payload.description || {})[0] || payload.translations?.th?.description || payload.translations?.en?.description || null);
|
||||
|
||||
return {
|
||||
code: payload.code,
|
||||
label: payload.label,
|
||||
description: payload.description ?? null,
|
||||
type: normalizeAttributeType(payload.type),
|
||||
unit: payload.unit ?? null,
|
||||
code: String(payload.code || '').trim(),
|
||||
label: String(baseLabel || payload.code || '').trim(),
|
||||
description: baseDesc ? String(baseDesc).trim() : null,
|
||||
type: normalizeAttributeType(payload.type || 'TEXT'),
|
||||
unit: payload.unit ? String(payload.unit).trim() : null,
|
||||
options: payload.options ?? null,
|
||||
isRequired: Boolean(payload.isRequired),
|
||||
isFilterable: Boolean(payload.isFilterable),
|
||||
sortOrder: typeof payload.sortOrder === 'number' ? payload.sortOrder : 0,
|
||||
categoryId: payload.categoryId ?? null,
|
||||
sortOrder: typeof payload.sortOrder === 'number' ? payload.sortOrder : (parseInt(payload.sortOrder) || 0),
|
||||
categoryId: payload.categoryId ? parseInt(payload.categoryId) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function extractTranslationsMap(payload: any): Record<string, { label: string; description?: string | null }> {
|
||||
const result: Record<string, { label: string; description?: string | null }> = {};
|
||||
|
||||
for (const lang of SUPPORTED_LANGS) {
|
||||
let label = '';
|
||||
let description: string | null = null;
|
||||
|
||||
if (payload.translations?.[lang]) {
|
||||
if (typeof payload.translations[lang] === 'string') {
|
||||
label = payload.translations[lang].trim();
|
||||
} else if (typeof payload.translations[lang] === 'object') {
|
||||
label = (payload.translations[lang].label || '').trim();
|
||||
if (payload.translations[lang].description) {
|
||||
description = String(payload.translations[lang].description).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!label && payload.label && typeof payload.label === 'object' && payload.label[lang]) {
|
||||
label = String(payload.label[lang]).trim();
|
||||
}
|
||||
|
||||
if (!description && payload.description && typeof payload.description === 'object' && payload.description[lang]) {
|
||||
description = String(payload.description[lang]).trim();
|
||||
}
|
||||
|
||||
if (label) {
|
||||
result[lang] = { label, description };
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback Thai label
|
||||
if (!result['th'] && typeof payload.label === 'string' && payload.label.trim()) {
|
||||
result['th'] = {
|
||||
label: payload.label.trim(),
|
||||
description: typeof payload.description === 'string' ? payload.description.trim() : null,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function syncAttributeTranslations(definitionId: number, translationsMap: Record<string, { label: string; description?: string | null }>) {
|
||||
for (const [langCode, val] of Object.entries(translationsMap)) {
|
||||
if (!val.label) continue;
|
||||
await prisma.attributeDefinitionTranslation.upsert({
|
||||
where: {
|
||||
attributeDefinitionId_langCode: {
|
||||
attributeDefinitionId: definitionId,
|
||||
langCode,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
label: val.label,
|
||||
description: val.description ?? null,
|
||||
},
|
||||
create: {
|
||||
attributeDefinitionId: definitionId,
|
||||
langCode,
|
||||
label: val.label,
|
||||
description: val.description ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function assertProductAndDefinitionMatch(productId: number, attributeDefinitionId: number) {
|
||||
const [product, definition] = await Promise.all([
|
||||
prisma.product.findUnique({ where: { id: productId } }),
|
||||
@@ -84,31 +158,106 @@ export const productAttributesRoute = new Elysia()
|
||||
return status(400, { message: err.message });
|
||||
}
|
||||
})
|
||||
.post("/product-attributes/definitions/bulk-import", async ({ body, status }) => {
|
||||
try {
|
||||
const payload = body as any;
|
||||
const list = Array.isArray(payload)
|
||||
? payload
|
||||
: (Array.isArray(payload?.definitions) ? payload.definitions : (Array.isArray(payload?.attributes) ? payload.attributes : null));
|
||||
|
||||
if (!list || !Array.isArray(list) || list.length === 0) {
|
||||
return status(400, { message: "Invalid payload: Expected a JSON array of attribute definition objects" });
|
||||
}
|
||||
|
||||
let createdCount = 0;
|
||||
let updatedCount = 0;
|
||||
const results: any[] = [];
|
||||
|
||||
for (const item of list) {
|
||||
if (!item || (!item.code && !item.name && !item.id)) continue;
|
||||
|
||||
const code = String(item.code || item.name || '').trim().toLowerCase().replace(/[\s-]+/g, '_');
|
||||
if (!code && !item.id) continue;
|
||||
|
||||
const defPayload = buildDefinitionPayload({
|
||||
...item,
|
||||
code: code || item.code,
|
||||
});
|
||||
|
||||
const translationsMap = extractTranslationsMap(item);
|
||||
|
||||
let definition = null;
|
||||
|
||||
// Check if exists by ID or by Code
|
||||
if (item.id && typeof item.id === 'number') {
|
||||
definition = await prisma.attributeDefinition.findUnique({ where: { id: item.id } });
|
||||
}
|
||||
if (!definition && code) {
|
||||
definition = await prisma.attributeDefinition.findUnique({ where: { code } });
|
||||
}
|
||||
|
||||
if (definition) {
|
||||
definition = await prisma.attributeDefinition.update({
|
||||
where: { id: definition.id },
|
||||
data: defPayload,
|
||||
include: { category: true, translations: true },
|
||||
});
|
||||
updatedCount++;
|
||||
} else {
|
||||
definition = await prisma.attributeDefinition.create({
|
||||
data: defPayload,
|
||||
include: { category: true, translations: true },
|
||||
});
|
||||
createdCount++;
|
||||
}
|
||||
|
||||
// Sync all translations
|
||||
await syncAttributeTranslations(definition.id, translationsMap);
|
||||
|
||||
const updated = await prisma.attributeDefinition.findUnique({
|
||||
where: { id: definition.id },
|
||||
include: { category: true, translations: true },
|
||||
});
|
||||
|
||||
results.push(localizeAttributeDefinition(updated, 'all'));
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
count: results.length,
|
||||
createdCount,
|
||||
updatedCount,
|
||||
definitions: results,
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error("Bulk import attribute definitions error:", err);
|
||||
return status(500, { message: err?.message || "Failed to bulk import attribute definitions" });
|
||||
}
|
||||
}, {
|
||||
body: t.Any(),
|
||||
})
|
||||
.post("/product-attributes/definitions", async ({ body, status }) => {
|
||||
try {
|
||||
const payload = body as any;
|
||||
const defPayload = buildDefinitionPayload(payload);
|
||||
const translationsMap = extractTranslationsMap(payload);
|
||||
|
||||
const definition = await prisma.attributeDefinition.create({
|
||||
data: {
|
||||
...buildDefinitionPayload(payload),
|
||||
translations: payload.translations && typeof payload.translations === 'object'
|
||||
? {
|
||||
create: Object.entries(payload.translations)
|
||||
.filter(([lang, value]) => SUPPORTED_LANGS.includes(lang as any) && value && typeof value === 'object' && (value as any).label)
|
||||
.map(([lang, value]) => ({
|
||||
langCode: lang,
|
||||
label: (value as any).label,
|
||||
description: (value as any).description ?? null,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
data: defPayload,
|
||||
include: {
|
||||
category: true,
|
||||
translations: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, definition };
|
||||
await syncAttributeTranslations(definition.id, translationsMap);
|
||||
|
||||
const refreshed = await prisma.attributeDefinition.findUnique({
|
||||
where: { id: definition.id },
|
||||
include: { category: true, translations: true },
|
||||
});
|
||||
|
||||
return { success: true, definition: refreshed };
|
||||
} catch (err: any) {
|
||||
if (err.code === 'P2002') return status(400, { message: "Attribute definition code already exists" });
|
||||
return status(500, { message: err?.message || "Failed to create attribute definition" });
|
||||
@@ -120,16 +269,26 @@ export const productAttributesRoute = new Elysia()
|
||||
try {
|
||||
const id = parseId(params.id, "attribute definition ID");
|
||||
const payload = body as any;
|
||||
const defPayload = buildDefinitionPayload(payload);
|
||||
const translationsMap = extractTranslationsMap(payload);
|
||||
|
||||
const definition = await prisma.attributeDefinition.update({
|
||||
where: { id },
|
||||
data: buildDefinitionPayload(payload),
|
||||
data: defPayload,
|
||||
include: {
|
||||
category: true,
|
||||
translations: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, definition };
|
||||
await syncAttributeTranslations(definition.id, translationsMap);
|
||||
|
||||
const refreshed = await prisma.attributeDefinition.findUnique({
|
||||
where: { id: definition.id },
|
||||
include: { category: true, translations: true },
|
||||
});
|
||||
|
||||
return { success: true, definition: refreshed };
|
||||
} catch (err: any) {
|
||||
if (err.code === 'P2025') return status(404, { message: "Attribute definition not found" });
|
||||
if (err.code === 'P2002') return status(400, { message: "Attribute definition code already exists" });
|
||||
|
||||
Reference in new issue
Block a user