609 lines
22 KiB
TypeScript
609 lines
22 KiB
TypeScript
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<string, Store>();
|
|
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<AuthSession | null>(null);
|
|
const [authLoading, setAuthLoading] = useState(true);
|
|
const [recoverySession, setRecoverySession] = useState<RecoverySession | null>(null);
|
|
const [recoveryError, setRecoveryError] = useState('');
|
|
const [spreadsheetId, setSpreadsheetId] = useState(DEFAULT_SPREADSHEET_ID);
|
|
const [isSyncing, setIsSyncing] = useState(false);
|
|
|
|
const [dataResult, setDataResult] = useState<FetchResult | null>(null);
|
|
const [activeTab, setActiveTab] = useState<'overview' | 'step1' | 'step2' | 'step3' | 'step4' | 'table'>('overview');
|
|
|
|
const [selectedAuditItem, setSelectedAuditItem] = useState<MergedStoreAudit | null>(null);
|
|
const [isSheetModalOpen, setIsSheetModalOpen] = useState(false);
|
|
const [isTutorialOpen, setIsTutorialOpen] = useState(false);
|
|
|
|
// Filters State
|
|
const [filters, setFilters] = useState<FilterState>({
|
|
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 (
|
|
<div className="min-h-screen bg-slate-50 flex items-center justify-center">
|
|
<div className="flex flex-col items-center gap-3 text-sky-700">
|
|
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-sky-500 to-blue-600 flex items-center justify-center shadow-lg">
|
|
<Zap className="w-5 h-5 fill-amber-300 text-amber-300" />
|
|
</div>
|
|
<LoaderCircle className="w-5 h-5 animate-spin" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!authSession || recoverySession || recoveryError) {
|
|
return (
|
|
<AuthScreen
|
|
recoverySession={recoverySession}
|
|
recoveryError={recoveryError}
|
|
onAuthenticated={session => {
|
|
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 (
|
|
<div className="min-h-screen bg-slate-50 text-slate-800 font-sans flex flex-col">
|
|
|
|
{/* Header */}
|
|
<Header
|
|
lastSynced={dataResult?.lastSynced || 'Just now'}
|
|
isSyncing={isSyncing}
|
|
onRefresh={handleRefresh}
|
|
onOpenTutorial={() => setIsTutorialOpen(true)}
|
|
userEmail={authSession.user.email || 'Signed in'}
|
|
onLogout={handleLogout}
|
|
/>
|
|
|
|
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6">
|
|
|
|
{/* Error notification if sync fails */}
|
|
{dataResult?.error && (
|
|
<div className="bg-amber-50 border border-amber-200 text-amber-900 px-4 py-3 rounded-2xl text-xs flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<AlertCircle className="w-4 h-4 text-amber-600 shrink-0" />
|
|
<span>{dataResult.error}</span>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsSheetModalOpen(true)}
|
|
className="text-xs font-extrabold underline text-amber-800 hover:text-amber-950 cursor-pointer"
|
|
>
|
|
Configure Google Sheet
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Global Filter Bar */}
|
|
<FilterBar
|
|
filters={filters}
|
|
stores={allStores}
|
|
weekOptions={weekOptions}
|
|
onChange={setFilters}
|
|
onReset={handleResetFilters}
|
|
/>
|
|
|
|
{/* KPI Cards Grid */}
|
|
<KPICards kpis={kpis} />
|
|
|
|
{/* Audit Step Tabs Navigation */}
|
|
<div className="bg-white/95 backdrop-blur-md rounded-2xl border border-sky-100 p-1.5 flex flex-wrap gap-1.5 shadow-xs sticky top-[172px] sm:top-[175px] z-20">
|
|
|
|
<button
|
|
onClick={() => setActiveTab('overview')}
|
|
className={`flex-1 sm:flex-none inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer ${
|
|
activeTab === 'overview'
|
|
? 'bg-amber-100 text-amber-950 border border-amber-200/80 shadow-2xs font-figtree'
|
|
: 'text-slate-500 hover:text-slate-900 hover:bg-sky-50'
|
|
}`}
|
|
>
|
|
<LayoutDashboard className="w-3.5 h-3.5 text-amber-800" />
|
|
<span>Overview</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('step1')}
|
|
className={`flex-1 sm:flex-none inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer ${
|
|
activeTab === 'step1'
|
|
? 'bg-amber-100 text-amber-950 border border-amber-200/80 shadow-2xs font-figtree'
|
|
: 'text-slate-500 hover:text-slate-900 hover:bg-sky-50'
|
|
}`}
|
|
>
|
|
<Eye className="w-3.5 h-3.5 text-amber-800" />
|
|
<span>Step 1: Presence (OOS)</span>
|
|
{kpis.oosStores > 0 && (
|
|
<span className="bg-rose-600 text-white text-[10px] font-black px-2 py-0.5 rounded-full">
|
|
{kpis.oosStores}
|
|
</span>
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('step2')}
|
|
className={`flex-1 sm:flex-none inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer ${
|
|
activeTab === 'step2'
|
|
? 'bg-amber-100 text-amber-950 border border-amber-200/80 shadow-2xs font-figtree'
|
|
: 'text-slate-500 hover:text-slate-900 hover:bg-sky-50'
|
|
}`}
|
|
>
|
|
<MapPin className="w-3.5 h-3.5 text-amber-800" />
|
|
<span>Step 2: Location & POP</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('step3')}
|
|
className={`flex-1 sm:flex-none inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer ${
|
|
activeTab === 'step3'
|
|
? 'bg-amber-100 text-amber-950 border border-amber-200/80 shadow-2xs font-figtree'
|
|
: 'text-slate-500 hover:text-slate-900 hover:bg-sky-50'
|
|
}`}
|
|
>
|
|
<BarChart3 className="w-3.5 h-3.5 text-amber-800" />
|
|
<span>Step 3: Shelf & Share</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('step4')}
|
|
className={`flex-1 sm:flex-none inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer ${
|
|
activeTab === 'step4'
|
|
? 'bg-amber-100 text-amber-950 border border-amber-200/80 shadow-2xs font-figtree'
|
|
: 'text-slate-500 hover:text-slate-900 hover:bg-sky-50'
|
|
}`}
|
|
>
|
|
<PackageCheck className="w-3.5 h-3.5 text-amber-800" />
|
|
<span>Step 4: Inventory & Backroom</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('table')}
|
|
className={`flex-1 sm:flex-none inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-xs font-extrabold transition-all cursor-pointer ${
|
|
activeTab === 'table'
|
|
? 'bg-sky-100 text-sky-950 shadow-2xs'
|
|
: 'text-slate-500 hover:text-slate-900 hover:bg-sky-50'
|
|
}`}
|
|
>
|
|
<Table className="w-3.5 h-3.5" />
|
|
<span>Store Table</span>
|
|
</button>
|
|
|
|
</div>
|
|
|
|
{/* Tab Content Display */}
|
|
{activeTab === 'overview' && (
|
|
<div className="space-y-6">
|
|
<AuditStep1Presencia
|
|
mergedData={filteredMergedData}
|
|
onSelectStore={item => setSelectedAuditItem(item)}
|
|
/>
|
|
<StoreTable
|
|
mergedData={filteredMergedData}
|
|
onSelectStore={item => setSelectedAuditItem(item)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'step1' && (
|
|
<AuditStep1Presencia
|
|
mergedData={filteredMergedData}
|
|
onSelectStore={item => setSelectedAuditItem(item)}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'step2' && (
|
|
<AuditStep2Exhibicion
|
|
mergedData={filteredMergedData}
|
|
onSelectStore={item => setSelectedAuditItem(item)}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'step3' && (
|
|
<AuditStep3Estante
|
|
mergedData={filteredMergedData}
|
|
onSelectStore={item => setSelectedAuditItem(item)}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'step4' && (
|
|
<AuditStep4Inventario
|
|
mergedData={filteredMergedData}
|
|
onSelectStore={item => setSelectedAuditItem(item)}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'table' && (
|
|
<StoreTable
|
|
mergedData={filteredMergedData}
|
|
onSelectStore={item => setSelectedAuditItem(item)}
|
|
/>
|
|
)}
|
|
|
|
</main>
|
|
|
|
|
|
|
|
{/* Audit Detail Modal */}
|
|
<StoreAuditModal
|
|
item={selectedAuditItem}
|
|
onClose={() => setSelectedAuditItem(null)}
|
|
/>
|
|
|
|
{/* Google Sheets Config Modal */}
|
|
<SyncSheetModal
|
|
isOpen={isSheetModalOpen}
|
|
currentSpreadsheetId={spreadsheetId}
|
|
source={dataResult?.source || 'fallback_demo'}
|
|
error={dataResult?.error}
|
|
onClose={() => setIsSheetModalOpen(false)}
|
|
onUpdateSpreadsheetId={newId => {
|
|
setSpreadsheetId(newId);
|
|
loadData(newId);
|
|
}}
|
|
/>
|
|
|
|
{/* Client PPT Presentation / Tutorial Deck Modal */}
|
|
<ClientTutorialModal
|
|
isOpen={isTutorialOpen}
|
|
onClose={() => setIsTutorialOpen(false)}
|
|
/>
|
|
|
|
</div>
|
|
);
|
|
}
|