import React, { useState, useEffect, useMemo } from 'react'; import { AuthSession, RecoverySession, clearAuthActionFromUrl, getRecoverySessionFromUrl, getStoredSession, refreshSession, signOut } from './services/supabaseAuth'; import { fetchGoogleSheetsData, DEFAULT_SPREADSHEET_ID, FetchResult, mergeAuditData } from './services/googleSheets'; import { FilterState, MergedStoreAudit, DashboardKPIs, Store } from './types'; import { extractWeekOptions, isDateInWeek, isItemInWeek } from './utils/dateUtils'; import { Header } from './components/Header'; import { FilterBar } from './components/FilterBar'; import { KPICards } from './components/KPICards'; import { AuditStep1Presencia } from './components/AuditStep1Presencia'; import { AuditStep2Exhibicion } from './components/AuditStep2Exhibicion'; import { AuditStep3Estante } from './components/AuditStep3Estante'; import { AuditStep4Inventario } from './components/AuditStep4Inventario'; import { StoreTable } from './components/StoreTable'; import { StoreAuditModal } from './components/StoreAuditModal'; import { SyncSheetModal } from './components/SyncSheetModal'; import { ClientTutorialModal } from './components/ClientTutorialModal'; import { AuthScreen } from './components/AuthScreen'; import { LayoutDashboard, Eye, MapPin, BarChart3, PackageCheck, Table, AlertCircle, LoaderCircle, Zap } from 'lucide-react'; // Helper to deduplicate stores strictly by Customer Name (Store Name) function getUniqueStores(stores: Store[]): Store[] { const map = new Map(); stores.forEach(s => { if (!s.customer || !s.customer.trim()) return; const key = s.customer.trim().toLowerCase(); if (!map.has(key)) { map.set(key, s); } else { const existing = map.get(key)!; if (!existing.assignedDate && s.assignedDate) { map.set(key, s); } } }); return Array.from(map.values()); } export default function App() { const [authSession, setAuthSession] = useState(null); const [authLoading, setAuthLoading] = useState(true); const [recoverySession, setRecoverySession] = useState(null); const [recoveryError, setRecoveryError] = useState(''); const [spreadsheetId, setSpreadsheetId] = useState(DEFAULT_SPREADSHEET_ID); const [isSyncing, setIsSyncing] = useState(false); const [dataResult, setDataResult] = useState(null); const [activeTab, setActiveTab] = useState<'overview' | 'step1' | 'step2' | 'step3' | 'step4' | 'table'>('overview'); const [selectedAuditItem, setSelectedAuditItem] = useState(null); const [isSheetModalOpen, setIsSheetModalOpen] = useState(false); const [isTutorialOpen, setIsTutorialOpen] = useState(false); // Filters State const [filters, setFilters] = useState({ zone: 'all', channel: 'all', customer: 'all', status: 'all', oosStatus: 'all', selectedWeek: 'all', search: '' }); // Supabase email/password session initialization useEffect(() => { let mounted = true; const initializeAuth = async () => { try { const recovery = await getRecoverySessionFromUrl(); if (recovery.session || recovery.error) { if (recovery.error) clearAuthActionFromUrl(); if (mounted) { setRecoverySession(recovery.session); setRecoveryError(recovery.error); setAuthSession(null); setAuthLoading(false); } return; } const storedSession = await getStoredSession(); if (mounted) { setAuthSession(storedSession); setAuthLoading(false); } } catch (error) { console.error('Unable to restore the authentication session:', error); if (mounted) { setAuthSession(null); setRecoverySession(null); setRecoveryError(''); setAuthLoading(false); } } }; initializeAuth(); return () => { mounted = false; }; }, []); const loadData = async (spId: string) => { setIsSyncing(true); try { const res = await fetchGoogleSheetsData(spId, null); setDataResult(res); } catch (e) { console.error('Error loading data:', e); } finally { setIsSyncing(false); } }; useEffect(() => { if (authSession && !recoverySession && !dataResult) { loadData(spreadsheetId); } }, [authSession, recoverySession]); useEffect(() => { if (!authSession) return; const refreshIn = Math.max(30_000, (authSession.expires_at * 1000) - Date.now() - 60_000); const timer = window.setTimeout(async () => { const refreshed = await refreshSession(authSession); if (refreshed) { setAuthSession(refreshed); } else { setAuthSession(null); setDataResult(null); } }, refreshIn); return () => window.clearTimeout(timer); }, [authSession]); const handleRefresh = () => { loadData(spreadsheetId); }; const handleResetFilters = () => { setFilters({ zone: 'all', channel: 'all', customer: 'all', status: 'all', oosStatus: 'all', selectedWeek: 'all', search: '' }); }; // Extract week options for Monday - Sunday selection const weekOptions = useMemo(() => { if (!dataResult) return []; return extractWeekOptions(dataResult.stores, dataResult.responses); }, [dataResult]); // Auto-select initial active week when weekOptions load useEffect(() => { if (weekOptions.length > 0 && filters.selectedWeek === 'all') { const target = weekOptions.find(w => w.key.includes('2026-07-20')) || weekOptions[0]; setFilters(prev => ({ ...prev, selectedWeek: target.key })); } }, [weekOptions]); // Compute Base Merged Items according to selectedWeek filter const baseMergedData = useMemo(() => { if (!dataResult) return []; const { stores, responses } = dataResult; const selectedWeek = filters.selectedWeek || 'all'; if (selectedWeek !== 'all') { // Filter stores by 'week' column or assignedDate falling in selectedWeek const weekStores = stores.filter(s => isItemInWeek(s.week, s.assignedDate, selectedWeek)); const uniqueWeekStores = getUniqueStores(weekStores); // Distinct count of stores from 'tiendas' sheet for this week const targetStores = uniqueWeekStores.length > 0 ? uniqueWeekStores : getUniqueStores(stores); // Filter responses by 'week' column or submissionDate falling in selectedWeek const weekResponses = responses.filter(r => isItemInWeek(r.week, r.submissionDate, selectedWeek)); return mergeAuditData(targetStores, weekResponses); } else { // All weeks selected: deduplicate stores strictly by Customer name const uniqueStores = getUniqueStores(stores); return mergeAuditData(uniqueStores, responses); } }, [dataResult, filters.selectedWeek]); // Filtered Merged Items const filteredMergedData = useMemo(() => { return baseMergedData.filter(item => { // Zone if (filters.zone !== 'all' && item.store.zone !== filters.zone) return false; // Channel if (filters.channel !== 'all' && item.store.customerChannel !== filters.channel) return false; // Customer if (filters.customer !== 'all' && item.store.customer !== filters.customer) return false; // Audit Status if (filters.status !== 'all' && item.status !== filters.status) return false; // OOS Status if (filters.oosStatus === 'Available' && item.response?.isAvailable !== 'Yes') return false; if (filters.oosStatus === 'OOS' && item.response?.isAvailable !== 'No') return false; // Search Query if (filters.search.trim()) { const q = filters.search.toLowerCase().trim(); const matchesCode = item.store.customerCode.toLowerCase().includes(q); const matchesName = item.store.customer.toLowerCase().includes(q); if (!matchesCode && !matchesName) return false; } return true; }); }, [baseMergedData, filters]); // Dashboard KPIs Calculation const kpis: DashboardKPIs = useMemo(() => { const totalStores = filteredMergedData.length; const visitedItems = filteredMergedData.filter(m => m.status === 'Visited'); const visitedStores = visitedItems.length; const pendingStores = totalStores - visitedStores; const complianceRate = totalStores > 0 ? (visitedStores / totalStores) * 100 : 0; // Total raw response submissions matching the filtered stores in the active week const activeResponses = (filters.selectedWeek && filters.selectedWeek !== 'all') ? (dataResult?.responses || []).filter(r => r.submissionDate && isDateInWeek(r.submissionDate, filters.selectedWeek)) : (dataResult?.responses || []); let totalResponses = visitedStores; if (activeResponses.length > 0) { const storeNamesSet = new Set(filteredMergedData.map(m => m.store.customer.toLowerCase().trim())); const matchingResponses = activeResponses.filter(r => r.customer && storeNamesSet.has(r.customer.toLowerCase().trim())); if (matchingResponses.length > 0) { totalResponses = matchingResponses.length; } } // Available and Out of Stock stores in filtered dataset const availableStores = visitedItems.filter(m => m.response?.isAvailable === 'Yes').length; const oosStores = visitedItems.filter(m => m.response?.isAvailable === 'No').length; // Denominator for rates based on visited stores const denominator = visitedStores > 0 ? visitedStores : totalStores; const oosRate = denominator > 0 ? (oosStores / denominator) * 100 : 0; const numericDistribution = denominator > 0 ? (availableStores / denominator) * 100 : 0; let totalPhysicalInventory = 0; let totalUnitsSoldIn = 0; let priceSum = 0; let priceCount = 0; let facingsSum = 0; let totalIceKickFacings = 0; let totalLucozadeFacings = 0; let totalCategoryFacings = 0; const visitedAvailableStores = filteredMergedData.filter(m => m.status === 'Visited' && m.response?.isAvailable === 'Yes'); filteredMergedData.forEach(item => { totalUnitsSoldIn += item.store.unitsSoldIn || 0; if (item.response) { totalPhysicalInventory += item.response.totalPhysicalInventory || 0; } }); visitedAvailableStores.forEach(item => { if (item.response) { if (item.response.retailPrice && item.response.retailPrice > 0) { priceSum += item.response.retailPrice; priceCount++; } facingsSum += item.response.facingsIceKick || 0; totalIceKickFacings += item.response.facingsIceKick || 0; totalLucozadeFacings += item.response.facingsLucozadeBrand || 0; totalCategoryFacings += item.response.facingsCategoryTotal || 0; } }); const avgRetailPrice = priceCount > 0 ? priceSum / priceCount : 0; const avgIceKickFacings = visitedStores > 0 ? facingsSum / visitedStores : 0; const avgBrandShelfShare = totalLucozadeFacings > 0 ? (totalIceKickFacings / totalLucozadeFacings) * 100 : 0; const avgCategoryShelfShare = totalCategoryFacings > 0 ? (totalLucozadeFacings / totalCategoryFacings) * 100 : 0; // Placement & Additional Exhibition Metrics (Secondary Display & Gondola End) let mainShelfStores = 0; let secondaryDisplayStores = 0; let gondolaEndStores = 0; let additionalExhibitionStores = 0; visitedItems.forEach(m => { const loc = (m.response?.placementLocation || '').toLowerCase(); const hasMain = loc.includes('main') || loc.includes('shelf') || loc.includes('estante'); const hasSecondary = loc.includes('secondary') || loc.includes('secundaria') || loc.includes('exhibici') || loc.includes('display'); const hasGondola = loc.includes('gondola') || loc.includes('góndola') || loc.includes('end') || loc.includes('cabecera') || loc.includes('endcap'); if (hasMain) mainShelfStores++; if (hasSecondary) secondaryDisplayStores++; if (hasGondola) gondolaEndStores++; if (hasSecondary || hasGondola) additionalExhibitionStores++; }); const mainShelfRate = denominator > 0 ? (mainShelfStores / denominator) * 100 : 0; const secondaryDisplayRate = denominator > 0 ? (secondaryDisplayStores / denominator) * 100 : 0; const gondolaEndRate = denominator > 0 ? (gondolaEndStores / denominator) * 100 : 0; const additionalExhibitionRate = denominator > 0 ? (additionalExhibitionStores / denominator) * 100 : 0; let totalSellOutUnits = 0; let totalSellOutValue = 0; visitedItems.forEach(item => { const inv = item.response?.totalPhysicalInventory || 0; const price = item.response?.retailPrice || 0; const sellIn = item.store.unitsSoldIn || 0; const sellOutUnits = Math.max(0, sellIn - inv); const valSellOut = sellOutUnits * price; totalSellOutUnits += sellOutUnits; totalSellOutValue += valSellOut; }); return { totalStores, totalResponses, visitedStores, pendingStores, complianceRate, oosStores, availableStores, oosRate, numericDistribution, totalPhysicalInventory, totalUnitsSoldIn, totalSellOutUnits, totalSellOutValue, avgRetailPrice, avgIceKickFacings, avgBrandShelfShare, avgCategoryShelfShare, mainShelfStores, secondaryDisplayStores, gondolaEndStores, additionalExhibitionStores, mainShelfRate, secondaryDisplayRate, gondolaEndRate, additionalExhibitionRate }; }, [filteredMergedData, dataResult]); if (authLoading) { return (
); } if (!authSession || recoverySession || recoveryError) { return ( { setAuthSession(session); setRecoverySession(null); setRecoveryError(''); setDataResult(null); }} onRecoveryComplete={() => { clearAuthActionFromUrl(); setRecoverySession(null); setRecoveryError(''); setAuthSession(null); setDataResult(null); }} /> ); } const allStores = dataResult?.stores || []; const handleLogout = async () => { await signOut(authSession); setAuthSession(null); setDataResult(null); setRecoverySession(null); setRecoveryError(''); }; return (
{/* Header */}
setIsTutorialOpen(true)} userEmail={authSession.user.email || 'Signed in'} onLogout={handleLogout} />
{/* Error notification if sync fails */} {dataResult?.error && (
{dataResult.error}
)} {/* Global Filter Bar */} {/* KPI Cards Grid */} {/* Audit Step Tabs Navigation */}