Use all-time client purchase patterns
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m25s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m25s
This commit is contained in:
@@ -910,7 +910,7 @@ const getClientDetailsAnalytics = async (clientToken, range = {}) => {
|
|||||||
periodFilters.push(`data_pedido_date <= $${periodParams.length}::date`);
|
periodFilters.push(`data_pedido_date <= $${periodParams.length}::date`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [summaryResult, periodResult] = await Promise.all([
|
const [summaryResult, periodResult, patternResult] = await Promise.all([
|
||||||
pool.query(`
|
pool.query(`
|
||||||
${CUSTOMER_IDENTITY_CTE}
|
${CUSTOMER_IDENTITY_CTE}
|
||||||
SELECT
|
SELECT
|
||||||
@@ -944,7 +944,20 @@ const getClientDetailsAnalytics = async (clientToken, range = {}) => {
|
|||||||
FROM identity_orders
|
FROM identity_orders
|
||||||
WHERE ${periodFilters.join(' AND ')}
|
WHERE ${periodFilters.join(' AND ')}
|
||||||
ORDER BY data_pedido_date DESC, COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text) DESC;
|
ORDER BY data_pedido_date DESC, COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text) DESC;
|
||||||
`, periodParams)
|
`, periodParams),
|
||||||
|
pool.query(`
|
||||||
|
${CUSTOMER_IDENTITY_CTE}
|
||||||
|
SELECT
|
||||||
|
data_pedido,
|
||||||
|
data_pedido_date,
|
||||||
|
created_at,
|
||||||
|
pedido_id,
|
||||||
|
valor_pedido
|
||||||
|
FROM identity_orders
|
||||||
|
WHERE ${CUSTOMER_KEY_SQL} = $1
|
||||||
|
AND data_pedido_date IS NOT NULL
|
||||||
|
ORDER BY data_pedido_date DESC, COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text) DESC;
|
||||||
|
`, [resolvedCustomerKey])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const summary = summaryResult.rows[0] || {};
|
const summary = summaryResult.rows[0] || {};
|
||||||
@@ -962,14 +975,12 @@ const getClientDetailsAnalytics = async (clientToken, range = {}) => {
|
|||||||
let periodSpent = 0;
|
let periodSpent = 0;
|
||||||
let periodItems = 0;
|
let periodItems = 0;
|
||||||
|
|
||||||
periodResult.rows.forEach(row => {
|
patternResult.rows.forEach(row => {
|
||||||
const itemRevenue = toNumber(row.quantidade) * toNumber(row.valor_unitario);
|
|
||||||
const dateKey = getDateOnly(row.data_pedido_date) || getDateOnly(row.data_pedido) || '';
|
|
||||||
const dateLabel = row.data_pedido || dateKey;
|
|
||||||
const groupKey = getOrderGroupKey(row);
|
const groupKey = getOrderGroupKey(row);
|
||||||
|
|
||||||
if (!patternOrderKeys.has(groupKey)) {
|
if (patternOrderKeys.has(groupKey)) return;
|
||||||
patternOrderKeys.add(groupKey);
|
patternOrderKeys.add(groupKey);
|
||||||
|
|
||||||
const weekdayIndex = getWeekdayIndex(row.data_pedido_date) ?? getWeekdayIndex(row.data_pedido);
|
const weekdayIndex = getWeekdayIndex(row.data_pedido_date) ?? getWeekdayIndex(row.data_pedido);
|
||||||
if (weekdayIndex !== null) {
|
if (weekdayIndex !== null) {
|
||||||
weekdayCounts[weekdayIndex].value += 1;
|
weekdayCounts[weekdayIndex].value += 1;
|
||||||
@@ -979,7 +990,13 @@ const getClientDetailsAnalytics = async (clientToken, range = {}) => {
|
|||||||
if (orderHour !== null) {
|
if (orderHour !== null) {
|
||||||
hourCounts[orderHour].value += 1;
|
hourCounts[orderHour].value += 1;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
periodResult.rows.forEach(row => {
|
||||||
|
const itemRevenue = toNumber(row.quantidade) * toNumber(row.valor_unitario);
|
||||||
|
const dateKey = getDateOnly(row.data_pedido_date) || getDateOnly(row.data_pedido) || '';
|
||||||
|
const dateLabel = row.data_pedido || dateKey;
|
||||||
|
const groupKey = getOrderGroupKey(row);
|
||||||
|
|
||||||
periodSpent += itemRevenue;
|
periodSpent += itemRevenue;
|
||||||
periodItems += toNumber(row.quantidade);
|
periodItems += toNumber(row.quantidade);
|
||||||
|
|||||||
@@ -805,7 +805,7 @@ test('getClientDetailsAnalytics resolves legacy name tokens to the canonical pho
|
|||||||
try {
|
try {
|
||||||
const details = await getClientDetailsAnalytics(clientToken, { start: '2026-06-01', end: '2026-06-15' });
|
const details = await getClientDetailsAnalytics(clientToken, { start: '2026-06-01', end: '2026-06-15' });
|
||||||
|
|
||||||
assert.equal(calls.length, 4);
|
assert.equal(calls.length, 5);
|
||||||
assert.match(calls[0].sql, /FROM client_identity_tokens/);
|
assert.match(calls[0].sql, /FROM client_identity_tokens/);
|
||||||
assert.deepEqual(calls[0].params, [clientToken]);
|
assert.deepEqual(calls[0].params, [clientToken]);
|
||||||
assert.match(calls[1].sql, /SELECT customer_key/);
|
assert.match(calls[1].sql, /SELECT customer_key/);
|
||||||
@@ -816,6 +816,10 @@ test('getClientDetailsAnalytics resolves legacy name tokens to the canonical pho
|
|||||||
assert.match(calls[3].sql, /data_pedido_date >= \$2::date/);
|
assert.match(calls[3].sql, /data_pedido_date >= \$2::date/);
|
||||||
assert.match(calls[3].sql, /data_pedido_date <= \$3::date/);
|
assert.match(calls[3].sql, /data_pedido_date <= \$3::date/);
|
||||||
assert.deepEqual(calls[3].params, ['(16) 99999-9999', '2026-06-01', '2026-06-15']);
|
assert.deepEqual(calls[3].params, ['(16) 99999-9999', '2026-06-01', '2026-06-15']);
|
||||||
|
assert.match(calls[4].sql, /created_at/);
|
||||||
|
assert.doesNotMatch(calls[4].sql, /data_pedido_date >=/);
|
||||||
|
assert.doesNotMatch(calls[4].sql, /data_pedido_date <=/);
|
||||||
|
assert.deepEqual(calls[4].params, ['(16) 99999-9999']);
|
||||||
assert.equal(details.clientName, 'Cliente Sem Fone');
|
assert.equal(details.clientName, 'Cliente Sem Fone');
|
||||||
assert.equal(details.clientPhone, '(16) 99999-9999');
|
assert.equal(details.clientPhone, '(16) 99999-9999');
|
||||||
assert.equal(details.allTimeOrderCount, 3);
|
assert.equal(details.allTimeOrderCount, 3);
|
||||||
|
|||||||
@@ -260,14 +260,21 @@ export const buildClientDetailsMetrics = (ordersData: OrderData[], customerKey:
|
|||||||
value: 0
|
value: 0
|
||||||
}));
|
}));
|
||||||
|
|
||||||
groupedOrders.forEach(group => {
|
const patternOrders = new Map<string, OrderData>();
|
||||||
const firstItem = group.items[0];
|
clientOrders.forEach(order => {
|
||||||
const orderDate = parseOrderDate(group.date);
|
const key = order.ID_Pedido || `${order.Data_Pedido}_${order.Valor_Pedido}`;
|
||||||
|
if (!patternOrders.has(key)) {
|
||||||
|
patternOrders.set(key, order);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
patternOrders.forEach(order => {
|
||||||
|
const orderDate = parseOrderDate(order.Data_Pedido);
|
||||||
if (!Number.isNaN(orderDate.getTime())) {
|
if (!Number.isNaN(orderDate.getTime())) {
|
||||||
weekdayCounts[orderDate.getDay()].value += 1;
|
weekdayCounts[orderDate.getDay()].value += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderHour = firstItem ? getOrderHour(firstItem) : null;
|
const orderHour = getOrderHour(order);
|
||||||
if (orderHour !== null) {
|
if (orderHour !== null) {
|
||||||
hourCounts[orderHour].value += 1;
|
hourCounts[orderHour].value += 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,10 +291,15 @@ const ClientDetails = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-dark-text">Padrão de Compra</h3>
|
<h3 className="text-lg font-bold text-zinc-900 dark:text-dark-text">Padrão de Compra</h3>
|
||||||
<p className="mt-1 text-sm font-medium text-zinc-500 dark:text-dark-muted">Quando este cliente costuma comprar.</p>
|
<p className="mt-1 text-sm font-medium text-zinc-500 dark:text-dark-muted">Quando este cliente costuma comprar.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="w-fit rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-bold uppercase tracking-wide text-zinc-500 dark:border-dark-border dark:bg-dark-card dark:text-dark-muted">
|
||||||
|
Todo período
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
<div className="bg-white dark:bg-dark-card border border-zinc-200 dark:border-dark-border rounded-2xl p-6 shadow-sm">
|
<div className="bg-white dark:bg-dark-card border border-zinc-200 dark:border-dark-border rounded-2xl p-6 shadow-sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user