Commit 2ce88d1a authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

feat: optimize stylist retrieval with StarRocks SQL classification and UI grouping

parent c1f361ca
This diff is collapsed.
...@@ -124,7 +124,8 @@ async function loadProduct(code) { ...@@ -124,7 +124,8 @@ async function loadProduct(code) {
const desc = metaJson.description || {}; const desc = metaJson.description || {};
const dd = desc.description_data || desc.description || {}; const dd = desc.description_data || desc.description || {};
aiMatches = fmJson.ok ? (fmJson.ai_matches || {}) : {}; window.aiMatches = fmJson.ok ? (fmJson.ai_matches || {}) : {};
window.aiClassifications = fmJson.ok ? (fmJson.classifications || {}) : {};
// Populate header // Populate header
document.getElementById('prodImage').src = desc.product_image_url || ''; document.getElementById('prodImage').src = desc.product_image_url || '';
...@@ -154,40 +155,104 @@ function switchDetailTab(tab, btn) { ...@@ -154,40 +155,104 @@ function switchDetailTab(tab, btn) {
document.getElementById('tabTest').style.display = tab === 'test' ? 'flex' : 'none'; document.getElementById('tabTest').style.display = tab === 'test' ? 'flex' : 'none';
} }
// ══ MATCHES TAB ═══════════════════════════════ // ══ MATCHES TAB & SQL GROUPING ═══════════════════════════════
let currentGroupBy = 'occ';
let activeGroupTab = null;
const COLOR_TABS = {
neutral: "<i data-lucide='circle' class='icon-sm'></i> Màu Neutral",
light: "<i data-lucide='sun-dim' class='icon-sm'></i> Màu Light",
dark: "<i data-lucide='moon' class='icon-sm'></i> Màu Dark"
};
const MATERIAL_TABS = {
summer: "<i data-lucide='sun' class='icon-sm'></i> Mùa hè",
winter: "<i data-lucide='snowflake' class='icon-sm'></i> Mùa đông"
};
function getVirtualMatches() {
if (currentGroupBy === 'occ') {
return window.aiMatches || {};
} else if (currentGroupBy === 'color' || currentGroupBy === 'material') {
return window.aiClassifications?.[currentGroupBy] || {};
}
return {};
}
function setGroupBy(type) {
currentGroupBy = type;
document.querySelectorAll('#groupTabs .btn').forEach(btn => {
btn.classList.remove('active');
btn.style.color = "var(--muted-fg)";
});
const activeBtn = document.getElementById(
type === 'occ' ? 'btnGroupOcc' : (type === 'color' ? 'btnGroupColor' : 'btnGroupMaterial')
);
if(activeBtn) {
activeBtn.classList.add('active');
activeBtn.style.color = "var(--primary)";
}
if (type === 'occ') activeGroupTab = Object.keys(OCC_LABELS)[0];
else if (type === 'color') activeGroupTab = 'neutral';
else if (type === 'material') activeGroupTab = 'summer';
renderMatchesTab();
}
function renderMatchesTab() { function renderMatchesTab() {
renderOccTabs(); renderGroupTabs();
if (!activeOcc || !aiMatches[activeOcc]) { const virtual = getVirtualMatches();
activeOcc = Object.keys(aiMatches)[0] || Object.keys(OCC_LABELS)[0]; if (!activeGroupTab || !virtual[activeGroupTab]) {
const keys = Object.keys(virtual);
activeGroupTab = keys.length ? keys[0] : activeGroupTab;
} }
renderMatchContent(); renderMatchContent();
} }
function renderOccTabs() { function renderGroupTabs() {
const container = document.getElementById('occTabs'); const container = document.getElementById('occTabs');
container.innerHTML = Object.entries(OCC_LABELS).map(([occ, label]) => { let LABELS = OCC_LABELS;
const count = Object.values(aiMatches[occ] || {}).reduce((s, a) => s + a.length, 0); if (currentGroupBy === 'color') LABELS = COLOR_TABS;
const active = occ === activeOcc ? 'active' : ''; else if (currentGroupBy === 'material') LABELS = MATERIAL_TABS;
return `<button class="occ-tab ${active}" onclick="switchOcc('${occ}')">
const virtual = getVirtualMatches();
container.innerHTML = Object.entries(LABELS).map(([key, label]) => {
let count = 0;
const grpF = virtual[key];
if (Array.isArray(grpF)) {
count = grpF.length;
} else if (grpF && typeof grpF === 'object') {
count = Object.values(grpF).reduce((s, a) => s + (Array.isArray(a) ? a.length : 0), 0);
}
const active = key === activeGroupTab ? 'active' : '';
return `<button class="occ-tab ${active}" onclick="switchGroupTab('${key}')">
${label} <span class="occ-count">${count}</span> ${label} <span class="occ-count">${count}</span>
</button>`; </button>`;
}).join(''); }).join('');
if (window.lucide) lucide.createIcons();
} }
function switchOcc(occ) { function switchGroupTab(key) {
activeOcc = occ; activeGroupTab = key;
renderOccTabs(); renderMatchesTab();
renderMatchContent();
} }
function renderMatchContent() { function renderMatchContent() {
const container = document.getElementById('matchContent'); const container = document.getElementById('matchContent');
const empty = document.getElementById('emptyState'); const empty = document.getElementById('emptyState');
const occData = aiMatches[activeOcc] || {}; const virtual = getVirtualMatches();
const hasAny = Object.values(occData).some(arr => arr.length > 0); const occData = virtual[activeGroupTab] || {};
let hasAny = false;
if (Array.isArray(occData)) {
hasAny = occData.length > 0;
} else {
hasAny = Object.values(occData).some(arr => (Array.isArray(arr) && arr.length > 0));
}
if (!hasAny && !Object.keys(aiMatches).length) { if (!hasAny && !Object.keys(virtual).length) {
container.style.display = 'none'; container.style.display = 'none';
empty.style.display = 'flex'; empty.style.display = 'flex';
return; return;
...@@ -195,23 +260,37 @@ function renderMatchContent() { ...@@ -195,23 +260,37 @@ function renderMatchContent() {
container.style.display = 'flex'; container.style.display = 'flex';
empty.style.display = 'none'; empty.style.display = 'none';
const roles = ['bottom', 'outerwear', 'accessory']; if (Array.isArray(occData)) {
container.innerHTML = roles.map(role => { // Flat Array Render for SQL Super Classifications
const items = occData[role] || []; const items = occData;
const roleInfo = ROLE_LABELS[role] || { label: role, emoji: '📦' }; const cards = items.map((item, idx) => renderMatchCard(item, activeGroupTab, 'bottom', idx)).join('');
const cards = items.map((item, idx) => renderMatchCard(item, activeOcc, role, idx)).join(''); container.innerHTML = `<div class="role-section">
const addBtn = `<div class="add-card" onclick="openAddModal('${activeOcc}','${role}')"> <div class="role-header">
<div class="add-card-icon">➕</div> <div class="role-title"><i data-lucide="database" class="icon-sm"></i> Raw SQL SQL Match Results <span class="badge badge-info">${items.length}</span></div>
<div class="add-card-label">Thêm SP</div> </div>
</div>`; <div class="role-body">${cards}</div>
return `<div class="role-section"> </div>`;
<div class="role-header"> } else {
<div class="role-title">${roleInfo.emoji} ${roleInfo.label} <span class="badge badge-info">${items.length}</span></div> // Nested Roles Render
<button class="btn btn-ghost btn-sm" onclick="openAddModal('${activeOcc}','${role}')">+ Thêm</button> const roles = ['bottom', 'outerwear', 'accessory'];
</div> container.innerHTML = roles.map(role => {
<div class="role-body">${cards}${addBtn}</div> const items = occData[role] || [];
</div>`; const roleInfo = ROLE_LABELS[role] || { label: role, emoji: '📦' };
}).join(''); const cards = items.map((item, idx) => renderMatchCard(item, activeGroupTab, role, idx)).join('');
const addBtn = `<div class="add-card" onclick="openAddModal('${activeGroupTab}','${role}')">
<div class="add-card-icon">➕</div>
<div class="add-card-label">Thêm SP</div>
</div>`;
return `<div class="role-section">
<div class="role-header">
<div class="role-title">${roleInfo.emoji} ${roleInfo.label} <span class="badge badge-info">${items.length}</span></div>
<button class="btn btn-ghost btn-sm" onclick="openAddModal('${activeGroupTab}','${role}')">+ Thêm</button>
</div>
<div class="role-body">${cards}${addBtn}</div>
</div>`;
}).join('');
}
if (window.lucide) lucide.createIcons();
} }
function renderMatchCard(item, occ, role, idx) { function renderMatchCard(item, occ, role, idx) {
...@@ -235,10 +314,11 @@ function renderMatchCard(item, occ, role, idx) { ...@@ -235,10 +314,11 @@ function renderMatchCard(item, occ, role, idx) {
} }
function removeItem(occ, role, idx) { function removeItem(occ, role, idx) {
if (!aiMatches[occ]?.[role]) return; const virtual = getVirtualMatches();
aiMatches[occ][role].splice(idx, 1); if (!virtual[occ]?.[role]) return;
if (!aiMatches[occ][role].length) delete aiMatches[occ][role]; virtual[occ][role].splice(idx, 1);
if (!Object.keys(aiMatches[occ] || {}).length) delete aiMatches[occ]; if (!virtual[occ][role].length) delete virtual[occ][role];
if (!Object.keys(virtual[occ] || {}).length) delete virtual[occ];
renderMatchesTab(); renderMatchesTab();
} }
...@@ -430,7 +510,7 @@ function addItemFromModal(code, name, image) { ...@@ -430,7 +510,7 @@ function addItemFromModal(code, name, image) {
} }
aiMatches[occ][role].push({ code, name, image, score: 70, reason: 'Thêm thủ công' }); aiMatches[occ][role].push({ code, name, image, score: 70, reason: 'Thêm thủ công' });
document.getElementById('addModal').style.display = 'none'; document.getElementById('addModal').style.display = 'none';
addingCtx = null; activeOcc = occ; addingCtx = null; activeGroupTab = occ;
renderMatchesTab(); renderMatchesTab();
showToast(`✅ Đã thêm ${name}`, 'success'); showToast(`✅ Đã thêm ${name}`, 'success');
} }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment