mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:45:24 +02:00
feat: integrate Glance Views (GV) from Traffic Weekly.xlsx
- types.ts: Added TrafficRecord interface and glanceViews field to CombinedKPIs - dataProcessor.ts: Added processTrafficExcel parser for Traffic Weekly.xlsx - dataProcessor.ts: Updated mergeSalesAndAdsData to accept and merge traffic data - App.tsx: Added trafficData state and handleTrafficUpload handler - FileUpload.tsx: Added Traffic file upload section for GV data - AdsPerformance.tsx: Added GV and CVR(GV) summary cards - AdsPerformance.tsx: Added GV column to product table
This commit is contained in:
@@ -5,8 +5,8 @@ import Dashboard from './components/Dashboard';
|
|||||||
import FilterBar from './components/FilterBar';
|
import FilterBar from './components/FilterBar';
|
||||||
import AIChat from './components/AIChat';
|
import AIChat from './components/AIChat';
|
||||||
import CrazeLogo from './components/CrazeLogo';
|
import CrazeLogo from './components/CrazeLogo';
|
||||||
import { SalesRecord, FilterState, AggregatedData, AdsRecord } from './types'; // Imported AdsRecord
|
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord } from './types';
|
||||||
import { processCSV, filterData, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, mergeSalesAndAdsData } from './services/dataProcessor';
|
import { processCSV, filterData, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, processTrafficExcel, mergeSalesAndAdsData } from './services/dataProcessor';
|
||||||
import { queryGemini } from './services/geminiService';
|
import { queryGemini } from './services/geminiService';
|
||||||
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
|
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
|
||||||
import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage';
|
import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage';
|
||||||
@@ -38,7 +38,8 @@ const PERMANENT_ADS_URL = "https://www.dropbox.com/scl/fi/wng8tep7awhvzd65amwad/
|
|||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [rawData, setRawData] = useState<SalesRecord[]>([]);
|
const [rawData, setRawData] = useState<SalesRecord[]>([]);
|
||||||
const [adsData, setAdsData] = useState<AdsRecord[]>([]); // New Ads State
|
const [adsData, setAdsData] = useState<AdsRecord[]>([]);
|
||||||
|
const [trafficData, setTrafficData] = useState<TrafficRecord[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [syncing, setSyncing] = useState(false);
|
const [syncing, setSyncing] = useState(false);
|
||||||
const [view, setView] = useState<'dashboard' | 'table' | 'weekly' | 'movers' | 'ads'>('dashboard'); // Added 'weekly' view
|
const [view, setView] = useState<'dashboard' | 'table' | 'weekly' | 'movers' | 'ads'>('dashboard'); // Added 'weekly' view
|
||||||
@@ -228,6 +229,22 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle uploaded Traffic file (Manual)
|
||||||
|
const handleTrafficUpload = async (file: File) => {
|
||||||
|
setSyncing(true);
|
||||||
|
try {
|
||||||
|
const data = await processTrafficExcel(file);
|
||||||
|
setTrafficData(data);
|
||||||
|
console.log("Traffic loaded:", data.length, "records from", file.name);
|
||||||
|
setIsDataModalOpen(false);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Failed to parse Traffic file", error);
|
||||||
|
alert(`Error parsing Traffic file: ${error.message || 'Unknown error'}. Please check the format.`);
|
||||||
|
} finally {
|
||||||
|
setSyncing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 2. Schedule Auto-Refresh (Background)
|
// 2. Schedule Auto-Refresh (Background)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAndRefresh = () => {
|
const checkAndRefresh = () => {
|
||||||
@@ -322,8 +339,8 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
// Combine Sales & Ads Data dynamically based on current filters
|
// Combine Sales & Ads Data dynamically based on current filters
|
||||||
const combinedAdsData = useMemo(() => {
|
const combinedAdsData = useMemo(() => {
|
||||||
return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata);
|
return mergeSalesAndAdsData(filteredData, filteredAdsData, globalAsinMetadata, trafficData);
|
||||||
}, [filteredData, filteredAdsData, globalAsinMetadata]);
|
}, [filteredData, filteredAdsData, globalAsinMetadata, trafficData]);
|
||||||
|
|
||||||
// Derive Context Data (Product Line Context when drilling down)
|
// Derive Context Data (Product Line Context when drilling down)
|
||||||
const contextAggregatedData = useMemo(() => {
|
const contextAggregatedData = useMemo(() => {
|
||||||
@@ -527,6 +544,7 @@ const App: React.FC = () => {
|
|||||||
<FileUpload
|
<FileUpload
|
||||||
onSalesUpload={handleSalesUpload}
|
onSalesUpload={handleSalesUpload}
|
||||||
onAdsUpload={handleAdsUpload}
|
onAdsUpload={handleAdsUpload}
|
||||||
|
onTrafficUpload={handleTrafficUpload}
|
||||||
onUrlSubmit={handleDataFetch}
|
onUrlSubmit={handleDataFetch}
|
||||||
isLoading={syncing}
|
isLoading={syncing}
|
||||||
activeUrl={activeUrl}
|
activeUrl={activeUrl}
|
||||||
|
|||||||
Binary file not shown.
@@ -77,6 +77,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
|||||||
target.impressions += item.impressions;
|
target.impressions += item.impressions;
|
||||||
target.conversions += item.conversions;
|
target.conversions += item.conversions;
|
||||||
target.salesOrganic += item.salesOrganic;
|
target.salesOrganic += item.salesOrganic;
|
||||||
|
target.glanceViews = (target.glanceViews || 0) + (item.glanceViews || 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -112,7 +113,8 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
|||||||
impressions: acc.impressions + curr.impressions,
|
impressions: acc.impressions + curr.impressions,
|
||||||
clicks: acc.clicks + curr.clicks,
|
clicks: acc.clicks + curr.clicks,
|
||||||
conversions: acc.conversions + curr.conversions,
|
conversions: acc.conversions + curr.conversions,
|
||||||
}), { salesTotal: 0, salesAds: 0, salesOrganic: 0, cost: 0, impressions: 0, clicks: 0, conversions: 0 });
|
glanceViews: acc.glanceViews + (curr.glanceViews || 0),
|
||||||
|
}), { salesTotal: 0, salesAds: 0, salesOrganic: 0, cost: 0, impressions: 0, clicks: 0, conversions: 0, glanceViews: 0 });
|
||||||
|
|
||||||
const currTotals = sum(aggregatedByAsin);
|
const currTotals = sum(aggregatedByAsin);
|
||||||
const prevTotals = comparisonConfig ? sum(aggregatedByAsin.map(a => a.prevData).filter(Boolean)) : null;
|
const prevTotals = comparisonConfig ? sum(aggregatedByAsin.map(a => a.prevData).filter(Boolean)) : null;
|
||||||
@@ -127,6 +129,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
|||||||
ctr: t.impressions > 0 ? (t.clicks / t.impressions) * 100 : 0,
|
ctr: t.impressions > 0 ? (t.clicks / t.impressions) * 100 : 0,
|
||||||
cpc: t.clicks > 0 ? t.cost / t.clicks : 0,
|
cpc: t.clicks > 0 ? t.cost / t.clicks : 0,
|
||||||
cvr: t.clicks > 0 ? (t.conversions / t.clicks) * 100 : 0,
|
cvr: t.clicks > 0 ? (t.conversions / t.clicks) * 100 : 0,
|
||||||
|
cvrGv: t.glanceViews > 0 ? (t.conversions / t.glanceViews) * 100 : 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const currMetrics = calculateDerived(totals.curr);
|
const currMetrics = calculateDerived(totals.curr);
|
||||||
@@ -196,6 +199,8 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
|||||||
<SummaryCard label="ACOS" value={currMetrics.acos} prevValue={prevMetrics?.acos} format="percent" inverse />
|
<SummaryCard label="ACOS" value={currMetrics.acos} prevValue={prevMetrics?.acos} format="percent" inverse />
|
||||||
<SummaryCard label="ROAS" value={currMetrics.roas} prevValue={prevMetrics?.roas} format="number" decimals={2} color="text-blue-300" />
|
<SummaryCard label="ROAS" value={currMetrics.roas} prevValue={prevMetrics?.roas} format="number" decimals={2} color="text-blue-300" />
|
||||||
<SummaryCard label="TACOS" value={currMetrics.tacos} prevValue={prevMetrics?.tacos} format="percent" inverse />
|
<SummaryCard label="TACOS" value={currMetrics.tacos} prevValue={prevMetrics?.tacos} format="percent" inverse />
|
||||||
|
<SummaryCard label="GV" value={totals.curr.glanceViews} prevValue={totals.prev?.glanceViews} format="number" color="text-teal-400" />
|
||||||
|
<SummaryCard label="CVR (GV)" value={currMetrics.cvrGv} prevValue={prevMetrics?.cvrGv} format="percent" color="text-teal-300" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table Section */}
|
{/* Table Section */}
|
||||||
@@ -236,6 +241,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
|||||||
<SortableHeader label="ACOS" sortKey="acos" activeSort={sortConfig} onSort={handleSort} />
|
<SortableHeader label="ACOS" sortKey="acos" activeSort={sortConfig} onSort={handleSort} />
|
||||||
<SortableHeader label="ROAS" sortKey="roas" activeSort={sortConfig} onSort={handleSort} />
|
<SortableHeader label="ROAS" sortKey="roas" activeSort={sortConfig} onSort={handleSort} />
|
||||||
<SortableHeader label="TACOS" sortKey="tacos" activeSort={sortConfig} onSort={handleSort} />
|
<SortableHeader label="TACOS" sortKey="tacos" activeSort={sortConfig} onSort={handleSort} />
|
||||||
|
<SortableHeader label="GV" sortKey="glanceViews" activeSort={sortConfig} onSort={handleSort} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-white/[0.03]">
|
<tbody className="divide-y divide-white/[0.03]">
|
||||||
@@ -261,6 +267,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters }) => {
|
|||||||
<TableCell value={row.acos} prevValue={row.prevData?.acos} format="percent" inverse />
|
<TableCell value={row.acos} prevValue={row.prevData?.acos} format="percent" inverse />
|
||||||
<TableCell value={row.roas} prevValue={row.prevData?.roas} format="number" highlight />
|
<TableCell value={row.roas} prevValue={row.prevData?.roas} format="number" highlight />
|
||||||
<TableCell value={row.tacos} prevValue={row.prevData?.tacos} format="percent" inverse />
|
<TableCell value={row.tacos} prevValue={row.prevData?.tacos} format="percent" inverse />
|
||||||
|
<TableCell value={row.glanceViews || 0} prevValue={row.prevData?.glanceViews} format="number" />
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { UploadIcon, MegaphoneIcon } from './Icons';
|
|||||||
interface FileUploadProps {
|
interface FileUploadProps {
|
||||||
onSalesUpload: (file: File) => void;
|
onSalesUpload: (file: File) => void;
|
||||||
onAdsUpload: (file: File) => void;
|
onAdsUpload: (file: File) => void;
|
||||||
onUrlSubmit: () => void; // Changed: no longer takes URL parameter
|
onTrafficUpload?: (file: File) => void;
|
||||||
|
onUrlSubmit: () => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
activeUrl?: string | null;
|
activeUrl?: string | null;
|
||||||
onDisconnect?: () => void;
|
onDisconnect?: () => void;
|
||||||
@@ -15,6 +16,7 @@ interface FileUploadProps {
|
|||||||
const FileUpload: React.FC<FileUploadProps> = ({
|
const FileUpload: React.FC<FileUploadProps> = ({
|
||||||
onSalesUpload,
|
onSalesUpload,
|
||||||
onAdsUpload,
|
onAdsUpload,
|
||||||
|
onTrafficUpload,
|
||||||
onUrlSubmit,
|
onUrlSubmit,
|
||||||
isLoading,
|
isLoading,
|
||||||
activeUrl,
|
activeUrl,
|
||||||
@@ -35,6 +37,12 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTrafficChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files.length > 0 && onTrafficUpload) {
|
||||||
|
onTrafficUpload(e.target.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUrlSubmit = (e: React.FormEvent) => {
|
const handleUrlSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (url.trim()) {
|
if (url.trim()) {
|
||||||
@@ -128,6 +136,31 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{onTrafficUpload && (
|
||||||
|
<div className="rounded-xl border border-dashed border-slate-700 bg-slate-900/50 hover:bg-slate-900 transition-colors group h-full col-span-2">
|
||||||
|
<label htmlFor="traffic-upload" className="cursor-pointer flex flex-col items-center justify-center gap-3 p-6 h-full">
|
||||||
|
<div className="p-3 bg-teal-500/10 rounded-full text-teal-400 group-hover:scale-110 transition-transform">
|
||||||
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="font-semibold text-slate-200">Upload Traffic Excel</h3>
|
||||||
|
<p className="text-[10px] text-slate-500 mt-1">Glance Views (GV) Data</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="traffic-upload"
|
||||||
|
type="file"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
onChange={handleTrafficChange}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SalesRecord, AdsRecord, CombinedKPIs, FilterState, AggregatedData, LineGrowthMetric, ItemGrowthMetric, SeasonalityPoint, YearlySplitData, PivotRow, YearlyData, TimeSeriesData, ComparisonTimeSeriesPoint } from '../types';
|
import { SalesRecord, AdsRecord, TrafficRecord, CombinedKPIs, FilterState, AggregatedData, LineGrowthMetric, ItemGrowthMetric, SeasonalityPoint, YearlySplitData, PivotRow, YearlyData, TimeSeriesData, ComparisonTimeSeriesPoint } from '../types';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import Papa from 'papaparse';
|
import Papa from 'papaparse';
|
||||||
|
|
||||||
@@ -439,17 +439,87 @@ export const processAdsExcel = async (fileOrBuffer: File | ArrayBuffer): Promise
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- TRAFFIC DATA PARSING ---
|
||||||
|
|
||||||
|
export const processTrafficExcel = async (fileOrBuffer: File | ArrayBuffer): Promise<TrafficRecord[]> => {
|
||||||
|
try {
|
||||||
|
const arrayBuffer = fileOrBuffer instanceof File
|
||||||
|
? await fileOrBuffer.arrayBuffer()
|
||||||
|
: fileOrBuffer;
|
||||||
|
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||||||
|
const allData: TrafficRecord[] = [];
|
||||||
|
|
||||||
|
// Process first sheet only (Traffic Weekly.xlsx typically has one sheet)
|
||||||
|
const sheetName = workbook.SheetNames[0];
|
||||||
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
const jsonData: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "" });
|
||||||
|
|
||||||
|
console.log(`Processing Traffic sheet "${sheetName}": ${jsonData.length} rows`);
|
||||||
|
|
||||||
|
// Column mapping for Traffic Weekly.xlsx:
|
||||||
|
// A (0): Year
|
||||||
|
// B (1): Week
|
||||||
|
// C (2): ASIN
|
||||||
|
// F (5): Country
|
||||||
|
// G (6): Glance Views (GV)
|
||||||
|
|
||||||
|
// Skip header row (index 0), process data rows
|
||||||
|
for (let i = 1; i < jsonData.length; i++) {
|
||||||
|
const row = jsonData[i];
|
||||||
|
if (!row || row.length < 7) continue;
|
||||||
|
|
||||||
|
const yearRaw = row[0];
|
||||||
|
const weekRaw = row[1];
|
||||||
|
const asin = row[2];
|
||||||
|
const countryRaw = row[5];
|
||||||
|
const gvRaw = row[6];
|
||||||
|
|
||||||
|
// Skip if missing essential data
|
||||||
|
if (!asin || yearRaw === undefined || weekRaw === undefined || !countryRaw) continue;
|
||||||
|
|
||||||
|
const year = parseInt(String(yearRaw));
|
||||||
|
const weekNum = parseInt(String(weekRaw));
|
||||||
|
if (isNaN(year) || year < 2020 || year > 2100) continue;
|
||||||
|
if (isNaN(weekNum) || weekNum < 1 || weekNum > 53) continue;
|
||||||
|
|
||||||
|
allData.push({
|
||||||
|
country: mapCountryToMarketplace(String(countryRaw)),
|
||||||
|
year,
|
||||||
|
week: weekNum,
|
||||||
|
asin: String(asin).trim().toUpperCase(),
|
||||||
|
glanceViews: parseUnits(String(gvRaw)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Total Traffic records loaded: ${allData.length}`);
|
||||||
|
return allData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing Traffic Excel:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- DATA MERGING ---
|
// --- DATA MERGING ---
|
||||||
|
|
||||||
export const mergeSalesAndAdsData = (
|
export const mergeSalesAndAdsData = (
|
||||||
salesData: SalesRecord[],
|
salesData: SalesRecord[],
|
||||||
adsData: AdsRecord[],
|
adsData: AdsRecord[],
|
||||||
asinMetadataMap?: Map<string, { sku: string; title: string; line: string }>
|
asinMetadataMap?: Map<string, { sku: string; title: string; line: string }>,
|
||||||
|
trafficData?: TrafficRecord[]
|
||||||
): CombinedKPIs[] => {
|
): CombinedKPIs[] => {
|
||||||
// Key for both sales and ads: ASIN|Customer|Year|Week
|
// Key for both sales and ads: ASIN|Customer|Year|Week
|
||||||
const createKey = (asin: string, customer: string, year: number, week: number) =>
|
const createKey = (asin: string, customer: string, year: number, week: number) =>
|
||||||
`${asin.trim().toUpperCase()}|${customer.trim().toUpperCase()}|${year}|${week}`;
|
`${asin.trim().toUpperCase()}|${customer.trim().toUpperCase()}|${year}|${week}`;
|
||||||
|
|
||||||
|
// Build traffic lookup map
|
||||||
|
const trafficMap = new Map<string, number>();
|
||||||
|
if (trafficData) {
|
||||||
|
trafficData.forEach(t => {
|
||||||
|
const key = createKey(t.asin, t.country, t.year, t.week);
|
||||||
|
trafficMap.set(key, (trafficMap.get(key) || 0) + t.glanceViews);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Initialize metadata lookup map with provided global map if available, otherwise build from current sales
|
// 1. Initialize metadata lookup map with provided global map if available, otherwise build from current sales
|
||||||
const asinMetadata = asinMetadataMap || new Map<string, { sku: string; title: string; line: string }>();
|
const asinMetadata = asinMetadataMap || new Map<string, { sku: string; title: string; line: string }>();
|
||||||
|
|
||||||
@@ -579,7 +649,8 @@ export const mergeSalesAndAdsData = (
|
|||||||
roas,
|
roas,
|
||||||
ctr,
|
ctr,
|
||||||
cpc,
|
cpc,
|
||||||
cvrUnits
|
cvrUnits,
|
||||||
|
glanceViews: trafficMap.get(key) || 0
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -617,7 +688,8 @@ export const mergeSalesAndAdsData = (
|
|||||||
roas: ad.cost > 0 ? ad.attributedSales30d / ad.cost : 0,
|
roas: ad.cost > 0 ? ad.attributedSales30d / ad.cost : 0,
|
||||||
ctr: ad.impressions > 0 ? (ad.clicks / ad.impressions) * 100 : 0,
|
ctr: ad.impressions > 0 ? (ad.clicks / ad.impressions) * 100 : 0,
|
||||||
cpc: ad.clicks > 0 ? ad.cost / ad.clicks : 0,
|
cpc: ad.clicks > 0 ? ad.cost / ad.clicks : 0,
|
||||||
cvrUnits: ad.clicks > 0 ? (ad.attributedUnits30d / ad.clicks) * 100 : 0
|
cvrUnits: ad.clicks > 0 ? (ad.attributedUnits30d / ad.clicks) * 100 : 0,
|
||||||
|
glanceViews: trafficMap.get(key) || 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -150,6 +150,14 @@ export interface AdsRecord {
|
|||||||
attributedSales30d: number;
|
attributedSales30d: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TrafficRecord {
|
||||||
|
country: string; // Marketplace (DE, UK, etc.)
|
||||||
|
year: number;
|
||||||
|
week: number;
|
||||||
|
asin: string;
|
||||||
|
glanceViews: number; // Page views
|
||||||
|
}
|
||||||
|
|
||||||
export interface CombinedKPIs {
|
export interface CombinedKPIs {
|
||||||
id: string;
|
id: string;
|
||||||
marketplace: string;
|
marketplace: string;
|
||||||
@@ -184,4 +192,5 @@ export interface CombinedKPIs {
|
|||||||
ctr: number;
|
ctr: number;
|
||||||
cpc: number;
|
cpc: number;
|
||||||
cvrUnits: number;
|
cvrUnits: number;
|
||||||
|
glanceViews: number; // Traffic / page views
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user