Fixed first two steps

This commit is contained in:
Roman Krček
2025-07-17 20:41:09 +02:00
parent 735e13731c
commit ffa427d42c
5 changed files with 476 additions and 260 deletions

View File

@@ -3,8 +3,10 @@
- This code is written in Svelte 5 - This code is written in Svelte 5
- It's important to only use modern Svelte 5 syntax, runes, and features. - It's important to only use modern Svelte 5 syntax, runes, and features.
- Do not use $:, do not use eventDispatching as they are both deprecated - Do not use $:, do not use eventDispatching as they are both deprecated
- User $effect, $state, $derived - Use $effect, $state, $derived, eg. let { value } = $state(initialValue);
- Pass fucntions as props instead od dispatching events - Pass fucntions as props instead od dispatching events
- Mixing old (on:click) and new syntaxes for event handling is not allowed. Use only the onclick syntax
- when setting state entity, simply od variable = newValue, do not use setState or similar methods like $state.
- Use styling from ".github/styling.md" for any UI components. - Use styling from ".github/styling.md" for any UI components.
- Refer to the ".github/core-instructions.md" for the overall structure of the application. - Refer to the ".github/core-instructions.md" for the overall structure of the application.
- Generate ".github/done.md" file to see what is done and what is not. Check it when you start and finish a task. - Generate ".github/done.md" file to see what is done and what is not. Check it when you start and finish a task.

View File

@@ -22,12 +22,6 @@
'Filter Rows', 'Filter Rows',
'Review Photos' 'Review Photos'
]; ];
function goToPreviousStep() {
if ($currentStep > 1) {
currentStep.update(n => n - 1);
}
}
</script> </script>
<div class="min-h-screen bg-gray-50"> <div class="min-h-screen bg-gray-50">
@@ -55,17 +49,6 @@
<!-- Step content --> <!-- Step content -->
<div class="bg-white rounded-lg shadow-sm"> <div class="bg-white rounded-lg shadow-sm">
<svelte:component this={steps[$currentStep - 1]} /> <svelte:component this={steps[$currentStep - 1]} />
</div> </div>
<!-- Navigation -->
<div class="flex justify-between mt-6">
<button
on:click={goToPreviousStep}
disabled={$currentStep <= 1}
class="px-4 py-2 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
← Previous
</button>
</div>
</div> </div>
</div> </div>

View File

@@ -2,15 +2,48 @@
import { selectedSheet, columnMapping, rawSheetData, currentStep } from '$lib/stores'; import { selectedSheet, columnMapping, rawSheetData, currentStep } from '$lib/stores';
import { getSheetNames, getSheetData } from '$lib/google'; import { getSheetNames, getSheetData } from '$lib/google';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
// Type definitions for better TypeScript support
interface ColumnMappingType {
name: number;
surname: number;
nationality: number;
birthday: number;
pictureUrl: number;
alreadyPrinted: number;
[key: string]: number; // Index signature to allow string indexing
}
interface SheetInfoType {
id?: string;
spreadsheetId?: string;
name: string;
sheetName?: string;
sheetMapping?: string;
columnMapping?: ColumnMappingType;
lastUsed?: string;
}
let isLoadingSheets = false; let isLoadingSheets = $state(false);
let isLoadingData = false; let isLoadingData = $state(false);
let availableSheets: string[] = []; let availableSheets = $state<string[]>([]);
let selectedSheetName = ''; let selectedSheetName = $state('');
let error = ''; let error = $state('');
let sheetHeaders: string[] = []; let sheetHeaders = $state<string[]>([]);
let previewData: string[][] = []; let previewData = $state<string[][]>([]);
let mappingComplete = false; let mappingComplete = $state(false);
let hasSavedMapping = $state(false);
let showMappingEditor = $state(false);
let savedSheetInfo = $state<SheetInfoType | null>(null);
let mappedIndices = $state<ColumnMappingType>({
name: -1,
surname: -1,
nationality: -1,
birthday: -1,
pictureUrl: -1,
alreadyPrinted: -1
});
const requiredFields = [ const requiredFields = [
{ key: 'name', label: 'First Name', required: true }, { key: 'name', label: 'First Name', required: true },
@@ -21,30 +54,123 @@
{ key: 'alreadyPrinted', label: 'Already Printed', required: false } { key: 'alreadyPrinted', label: 'Already Printed', required: false }
]; ];
let mappedIndices = { // Load available sheets when component mounts
name: -1,
surname: -1,
nationality: -1,
birthday: -1,
pictureUrl: -1,
alreadyPrinted: -1
};
// Load available sheets when component mounts
onMount(async () => { onMount(async () => {
if ($selectedSheet) { if ($selectedSheet) {
console.log('Selected sheet on mount:', $selectedSheet);
// Check if we already have saved mapping data
const recentSheetsData = localStorage.getItem('esn-recent-sheets');
if (recentSheetsData) {
try {
const recentSheets = JSON.parse(recentSheetsData);
if (recentSheets && recentSheets.length > 0) {
// Find a sheet that matches the current spreadsheet
const savedSheet = recentSheets.find((sheet: SheetInfoType) =>
(sheet.id === $selectedSheet.spreadsheetId || sheet.spreadsheetId === $selectedSheet.spreadsheetId)
);
if (savedSheet) {
console.log('Found saved sheet configuration:', savedSheet);
// We have a saved sheet for this spreadsheet
selectedSheetName = savedSheet.sheetName || savedSheet.sheetMapping || '';
savedSheetInfo = savedSheet;
if (savedSheet.columnMapping) {
// Set the mapped indices from saved data
mappedIndices = {
name: savedSheet.columnMapping.name ?? -1,
surname: savedSheet.columnMapping.surname ?? -1,
nationality: savedSheet.columnMapping.nationality ?? -1,
birthday: savedSheet.columnMapping.birthday ?? -1,
pictureUrl: savedSheet.columnMapping.pictureUrl ?? -1,
alreadyPrinted: savedSheet.columnMapping.alreadyPrinted ?? -1
};
hasSavedMapping = true;
updateMappingStatus();
columnMapping.set(mappedIndices);
// Don't load sheet data immediately for better performance
// We'll load it when needed (when editing or continuing)
return; // Skip loading available sheets since we're using saved data
}
}
}
} catch (err) {
console.error('Error parsing saved sheets data:', err);
}
}
// If no saved data was found or it couldn't be used, load sheets as usual
await loadAvailableSheets(); await loadAvailableSheets();
} else {
console.error('No spreadsheet selected on mount');
} }
}); });
async function loadAvailableSheets() { // Load sheet data quietly (for previously saved sheets)
if (!$selectedSheet) return; async function loadSheetDataQuietly(sheetName: string) {
if (!$selectedSheet || !sheetName) {
console.error('Cannot load sheet data: missing selectedSheet or sheetName', {
selectedSheet: $selectedSheet,
sheetName: sheetName
});
return;
}
try {
console.log('Loading sheet data quietly for spreadsheet:', $selectedSheet.spreadsheetId, 'sheet:', sheetName);
// Make sure we verify the sheet exists before trying to load it
if (availableSheets.length === 0) {
// We need to load available sheets first
await loadAvailableSheets();
// If after loading sheets, we still don't have the sheet, show the editor
if (!availableSheets.includes(sheetName)) {
console.warn(`Sheet "${sheetName}" not found in spreadsheet, showing editor`);
showMappingEditor = true;
return;
}
}
// Fetch first 10 rows for headers only
const range = `${sheetName}!A1:Z10`;
const data = await getSheetData($selectedSheet.spreadsheetId, range);
if (data && data.length > 0) {
console.log('Loaded sheet data with', data.length, 'rows');
sheetHeaders = data[0];
previewData = data.slice(1, Math.min(4, data.length)); // Get up to 3 rows for preview
// Don't set the rawSheetData here as that will be loaded in the next step
} else {
console.warn(`No data returned for sheet "${sheetName}", showing editor`);
showMappingEditor = true;
}
} catch (err) {
console.error('Error loading sheet data quietly:', err, 'for sheet:', sheetName);
// If there's an error, show the full editor so the user can select a sheet
showMappingEditor = true;
}
}
async function loadAvailableSheets() {
if (!$selectedSheet) {
console.error('Cannot load available sheets: no sheet selected');
return;
}
console.log('Loading available sheets for spreadsheet:', $selectedSheet.spreadsheetId);
isLoadingSheets = true; isLoadingSheets = true;
error = ''; error = '';
try { try {
availableSheets = await getSheetNames($selectedSheet.id); const sheetNames = await getSheetNames($selectedSheet.spreadsheetId);
console.log('Loaded sheet names:', sheetNames);
availableSheets = sheetNames;
// Don't auto-select any sheet - let user choose // Don't auto-select any sheet - let user choose
} catch (err) { } catch (err) {
console.error('Error loading sheet names:', err); console.error('Error loading sheet names:', err);
@@ -55,7 +181,9 @@
} }
function handleSheetSelect(sheetName: string) { function handleSheetSelect(sheetName: string) {
console.log('Sheet selected:', sheetName);
selectedSheetName = sheetName; selectedSheetName = sheetName;
// Clear any previous data when selecting a new sheet // Clear any previous data when selecting a new sheet
rawSheetData.set([]); rawSheetData.set([]);
sheetHeaders = []; sheetHeaders = [];
@@ -69,6 +197,8 @@
alreadyPrinted: -1 alreadyPrinted: -1
}; };
mappingComplete = false; mappingComplete = false;
hasSavedMapping = false;
showMappingEditor = true;
// Load sheet data // Load sheet data
if (sheetName) { if (sheetName) {
@@ -77,20 +207,25 @@
} }
async function loadSheetData(sheetName: string) { async function loadSheetData(sheetName: string) {
if (!$selectedSheet) return; if (!$selectedSheet) {
console.error('Cannot load sheet data: no sheet selected');
return;
}
console.log('Loading sheet data for spreadsheet:', $selectedSheet.spreadsheetId, 'sheet:', sheetName);
isLoadingData = true; isLoadingData = true;
error = ''; error = '';
try { try {
// Fetch first 10 rows for headers and preview // Fetch first 10 rows for headers and preview
const range = `${sheetName}!A1:Z10`; const range = `${sheetName}!A1:Z10`;
const data = await getSheetData($selectedSheet.id, range); const data = await getSheetData($selectedSheet.spreadsheetId, range);
if (data && data.length > 0) { if (data && data.length > 0) {
console.log('Loaded sheet data with', data.length, 'rows');
sheetHeaders = data[0]; sheetHeaders = data[0];
previewData = data.slice(1, Math.min(4, data.length)); // Get up to 3 rows for preview previewData = data.slice(1, Math.min(4, data.length)); // Get up to 3 rows for preview
rawSheetData.set(data); // We don't need to set all the raw data here
// Try to auto-map columns // Try to auto-map columns
autoMapColumns(); autoMapColumns();
@@ -99,6 +234,7 @@
loadSavedColumnMapping(); loadSavedColumnMapping();
} else { } else {
error = 'The selected sheet appears to be empty.'; error = 'The selected sheet appears to be empty.';
console.warn('Sheet is empty');
} }
} catch (err) { } catch (err) {
console.error('Error loading sheet data:', err); console.error('Error loading sheet data:', err);
@@ -120,7 +256,7 @@
}; };
// Auto-mapping patterns // Auto-mapping patterns
const patterns = { const patterns: Record<keyof ColumnMappingType, RegExp> = {
name: /first[\s_-]*name|name|given[\s_-]*name|vorname/i, name: /first[\s_-]*name|name|given[\s_-]*name|vorname/i,
surname: /last[\s_-]*name|surname|family[\s_-]*name|nachname/i, surname: /last[\s_-]*name|surname|family[\s_-]*name|nachname/i,
nationality: /nationality|country|nation/i, nationality: /nationality|country|nation/i,
@@ -159,11 +295,15 @@
} }
} }
console.log('Auto-mapped columns:', mappedIndices);
updateMappingStatus(); updateMappingStatus();
} }
function loadSavedColumnMapping() { function loadSavedColumnMapping() {
if (!$selectedSheet || !selectedSheetName) return; if (!$selectedSheet || !selectedSheetName) {
console.log('Cannot load saved column mapping: missing selectedSheet or selectedSheetName');
return;
}
try { try {
const recentSheetsKey = 'esn-recent-sheets'; const recentSheetsKey = 'esn-recent-sheets';
@@ -171,11 +311,14 @@
if (existingData) { if (existingData) {
const recentSheets = JSON.parse(existingData); const recentSheets = JSON.parse(existingData);
const savedSheet = recentSheets.find(sheet => const savedSheet = recentSheets.find((sheet: SheetInfoType) =>
sheet.id === $selectedSheet.id && sheet.sheetName === selectedSheetName (sheet.id === $selectedSheet.spreadsheetId || sheet.spreadsheetId === $selectedSheet.spreadsheetId) &&
(sheet.sheetName === selectedSheetName || sheet.sheetMapping === selectedSheetName)
); );
if (savedSheet && savedSheet.columnMapping) { if (savedSheet && savedSheet.columnMapping) {
console.log('Found saved column mapping for current sheet:', savedSheet.columnMapping);
// Override auto-mapping with saved mapping // Override auto-mapping with saved mapping
mappedIndices = { mappedIndices = {
name: savedSheet.columnMapping.name ?? -1, name: savedSheet.columnMapping.name ?? -1,
@@ -186,7 +329,11 @@
alreadyPrinted: savedSheet.columnMapping.alreadyPrinted ?? -1 alreadyPrinted: savedSheet.columnMapping.alreadyPrinted ?? -1
}; };
hasSavedMapping = true;
savedSheetInfo = savedSheet;
updateMappingStatus(); updateMappingStatus();
} else {
console.log('No saved column mapping found for the current sheet');
} }
} }
} catch (err) { } catch (err) {
@@ -194,7 +341,7 @@
} }
} }
function handleColumnMapping(field: string, index: number) { function handleColumnMapping(field: keyof ColumnMappingType, index: number) {
mappedIndices[field] = index; mappedIndices[field] = index;
updateMappingStatus(); updateMappingStatus();
} }
@@ -210,6 +357,7 @@
}; };
mappingComplete = Object.values(requiredIndices).every(index => index !== -1); mappingComplete = Object.values(requiredIndices).every(index => index !== -1);
console.log('Mapping complete:', mappingComplete);
// Update the column mapping store // Update the column mapping store
columnMapping.set({ columnMapping.set({
@@ -232,8 +380,9 @@
let recentSheets = existingData ? JSON.parse(existingData) : []; let recentSheets = existingData ? JSON.parse(existingData) : [];
// Find the current sheet in recent sheets and update its column mapping // Find the current sheet in recent sheets and update its column mapping
const sheetIndex = recentSheets.findIndex(sheet => const sheetIndex = recentSheets.findIndex((sheet: SheetInfoType) =>
sheet.id === $selectedSheet.id && sheet.sheetName === selectedSheetName (sheet.id === $selectedSheet.spreadsheetId || sheet.spreadsheetId === $selectedSheet.spreadsheetId) &&
(sheet.sheetName === selectedSheetName || sheet.sheetMapping === selectedSheetName)
); );
const columnMappingData = { const columnMappingData = {
@@ -249,12 +398,16 @@
// Update existing entry // Update existing entry
recentSheets[sheetIndex].columnMapping = columnMappingData; recentSheets[sheetIndex].columnMapping = columnMappingData;
recentSheets[sheetIndex].lastUsed = new Date().toISOString(); recentSheets[sheetIndex].lastUsed = new Date().toISOString();
// Ensure we have consistent property names
recentSheets[sheetIndex].spreadsheetId = recentSheets[sheetIndex].spreadsheetId || recentSheets[sheetIndex].id;
recentSheets[sheetIndex].sheetMapping = recentSheets[sheetIndex].sheetMapping || recentSheets[sheetIndex].sheetName;
} else { } else {
// Add new entry // Add new entry
const newEntry = { const newEntry = {
id: $selectedSheet.id, spreadsheetId: $selectedSheet.spreadsheetId,
name: $selectedSheet.name, name: $selectedSheet.name,
sheetName: selectedSheetName, sheetMapping: selectedSheetName,
columnMapping: columnMappingData, columnMapping: columnMappingData,
lastUsed: new Date().toISOString() lastUsed: new Date().toISOString()
}; };
@@ -274,6 +427,34 @@
currentStep.set(4); // Move to next step currentStep.set(4); // Move to next step
} }
async function handleShowEditor() {
showMappingEditor = true;
// Load available sheets if they haven't been loaded yet
if (availableSheets.length === 0) {
await loadAvailableSheets();
}
// Ensure we have sheet data if a sheet is already selected
if (selectedSheetName && sheetHeaders.length === 0) {
// Load the sheet data but keep mappings intact
try {
isLoadingData = true;
const range = `${selectedSheetName}!A1:Z10`;
const data = await getSheetData($selectedSheet.spreadsheetId, range);
if (data && data.length > 0) {
sheetHeaders = data[0];
previewData = data.slice(1, Math.min(4, data.length));
}
} catch (err) {
console.error('Error loading sheet data for editor:', err);
} finally {
isLoadingData = false;
}
}
}
</script> </script>
<div class="p-6"> <div class="p-6">
@@ -287,201 +468,236 @@
First, select which sheet contains your member data, then map the columns to the required fields. First, select which sheet contains your member data, then map the columns to the required fields.
</p> </p>
</div> </div>
<!-- Sheet Selection --> {#if hasSavedMapping && !showMappingEditor}
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6"> <!-- Simplified view when we have saved mapping -->
<h3 class="text-sm font-medium text-gray-700 mb-3"> <div class="bg-green-50 border border-green-200 rounded-lg p-6 mb-6">
Step 1: Select Sheet <div class="text-center">
</h3> <svg class="mx-auto h-16 w-16 text-green-600 mb-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
{#if isLoadingSheets} </svg>
<div class="flex items-center"> <h3 class="text-xl font-semibold text-green-800 mb-3">Configuration Complete</h3>
<div class="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mr-3"></div> <p class="text-green-700 mb-2">
<span class="text-sm text-gray-600">Loading sheets...</span> <span class="font-medium">Spreadsheet:</span> {savedSheetInfo?.name}
</div> </p>
{:else if error} <p class="text-green-700 mb-2">
<div class="bg-red-50 border border-red-300 rounded-lg p-3 mb-3"> <span class="font-medium">Sheet:</span> {selectedSheetName}
<p class="text-sm text-red-800">{error}</p> </p>
<button <p class="text-green-700 mb-6">
class="mt-2 text-sm text-blue-600 hover:text-blue-800" Column mapping loaded from your previous session.<br>
on:click={loadAvailableSheets} Everything is ready to proceed to the next step.
</p>
<button
onclick={handleShowEditor}
class="inline-flex items-center px-4 py-2 text-sm text-green-700 hover:text-green-900 font-medium border border-green-300 rounded-lg hover:bg-green-100 transition-colors"
> >
Try again <svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
</svg>
Make changes if needed
</button> </button>
</div> </div>
{:else if availableSheets.length === 0} </div>
<div class="text-center py-6 bg-white rounded border border-gray-200"> {:else}
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <!-- Sheet Selection -->
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/> <div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
</svg> <h3 class="text-sm font-medium text-gray-700 mb-3">
<h3 class="mt-2 text-sm font-medium text-gray-900">No sheets found</h3> Step 1: Select Sheet
<p class="mt-1 text-sm text-gray-500"> </h3>
This spreadsheet doesn't appear to contain any sheets.
</p> {#if isLoadingSheets}
</div> <div class="flex items-center">
{:else} <div class="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mr-3"></div>
<div class="space-y-3"> <span class="text-sm text-gray-600">Loading sheets...</span>
<p class="text-sm text-gray-600"> </div>
Spreadsheet: <span class="font-medium">{$selectedSheet?.name}</span> {:else if error}
</p> <div class="bg-red-50 border border-red-300 rounded-lg p-3 mb-3">
<p class="text-sm text-red-800">{error}</p>
<div> <button
<label class="block text-sm font-medium text-gray-700 mb-3"> class="mt-2 text-sm text-blue-600 hover:text-blue-800"
Choose sheet: onclick={loadAvailableSheets}
</label> >
Try again
</button>
</div>
{:else if availableSheets.length === 0}
<div class="text-center py-6 bg-white rounded border border-gray-200">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No sheets found</h3>
<p class="mt-1 text-sm text-gray-500">
This spreadsheet doesn't appear to contain any sheets.
</p>
</div>
{:else}
<div class="space-y-3">
<p class="text-sm text-gray-600">
Spreadsheet: <span class="font-medium">{$selectedSheet?.name}</span>
</p>
<div class="space-y-2"> <div>
{#each availableSheets as sheetName} <p class="block text-sm font-medium text-gray-700 mb-3">
<div Choose sheet:
class="border rounded-lg p-3 cursor-pointer transition-colors hover:bg-gray-50 </p>
{selectedSheetName === sheetName ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}"
on:click={() => handleSheetSelect(sheetName)} <div class="space-y-2">
> {#each availableSheets as sheetName}
<div class="flex items-center"> <div
<div class="flex-shrink-0"> role="button"
<div class="w-4 h-4 rounded-full border-2 flex items-center justify-center tabindex="0"
{selectedSheetName === sheetName ? 'border-blue-500 bg-blue-500' : 'border-gray-300'}"> class="border rounded-lg p-3 cursor-pointer transition-colors hover:bg-gray-50
{#if selectedSheetName === sheetName} {selectedSheetName === sheetName ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}"
<div class="w-2 h-2 rounded-full bg-white"></div> onclick={() => handleSheetSelect(sheetName)}
{/if} onkeydown={(e) => e.key === 'Enter' && handleSheetSelect(sheetName)}
</div> >
</div> <div class="flex items-center">
<div class="ml-3 flex-grow">
<p class="text-sm font-medium text-gray-900">{sheetName}</p>
</div>
{#if selectedSheetName === sheetName}
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20"> <div class="w-4 h-4 rounded-full border-2 flex items-center justify-center
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> {selectedSheetName === sheetName ? 'border-blue-500 bg-blue-500' : 'border-gray-300'}">
</svg> {#if selectedSheetName === sheetName}
<div class="w-2 h-2 rounded-full bg-white"></div>
{/if}
</div>
</div> </div>
{/if}
<div class="ml-3 flex-grow">
<p class="text-sm font-medium text-gray-900">{sheetName}</p>
</div>
{#if selectedSheetName === sheetName}
<div class="flex-shrink-0">
<svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
</div>
{/if}
</div>
</div>
{/each}
</div>
</div>
</div>
{/if}
</div>
<!-- Column Mapping Section -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
<h3 class="text-sm font-medium text-gray-700 mb-3">
Step 2: Map Columns
</h3>
{#if isLoadingData}
<div class="flex items-center">
<div class="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mr-3"></div>
<span class="text-sm text-gray-600">Loading sheet data...</span>
</div>
{:else if sheetHeaders.length === 0}
<div class="text-center py-8 text-gray-500">
<p class="text-sm">Select a sheet above to map columns</p>
</div>
{:else}
<div class="space-y-4">
<p class="text-sm text-gray-600 mb-4">
Map the columns from your sheet to the required fields:
</p>
<!-- Column mapping dropdowns -->
<div class="grid gap-4">
{#each requiredFields as field}
<div class="flex items-center">
<div class="w-32 flex-shrink-0">
<label for={`field-${field.key}`} class="text-sm font-medium text-gray-700">
{field.label}
{#if field.required}
<span class="text-red-500">*</span>
{/if}
</label>
</div>
<div class="flex-grow">
<select
id={`field-${field.key}`}
bind:value={mappedIndices[field.key]}
onchange={() => handleColumnMapping(field.key as keyof ColumnMappingType, mappedIndices[field.key])}
class="w-full px-3 py-2 border border-gray-300 rounded-md bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value={-1}>-- Select column --</option>
{#each Array.from({length: Math.max(sheetHeaders.length, 26)}, (_, i) => i) as index}
<option value={index}>
{sheetHeaders[index] || `Column ${String.fromCharCode(65 + index)}`}
{#if !sheetHeaders[index]}
(empty)
{/if}
</option>
{/each}
</select>
</div> </div>
</div> </div>
{/each} {/each}
</div> </div>
</div>
</div> <!-- Data preview -->
{/if} {#if previewData.length > 0}
</div> <div class="mt-6">
<h4 class="text-sm font-medium text-gray-700 mb-3">Data Preview:</h4>
<!-- Column Mapping Section --> <div class="overflow-x-auto">
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6"> <table class="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
<h3 class="text-sm font-medium text-gray-700 mb-3"> <thead class="bg-gray-50">
Step 2: Map Columns
</h3>
{#if isLoadingData}
<div class="flex items-center">
<div class="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mr-3"></div>
<span class="text-sm text-gray-600">Loading sheet data...</span>
</div>
{:else if sheetHeaders.length === 0}
<div class="text-center py-8 text-gray-500">
<p class="text-sm">Select a sheet above to map columns</p>
</div>
{:else}
<div class="space-y-4">
<p class="text-sm text-gray-600 mb-4">
Map the columns from your sheet to the required fields:
</p>
<!-- Column mapping dropdowns -->
<div class="grid gap-4">
{#each requiredFields as field}
<div class="flex items-center">
<div class="w-32 flex-shrink-0">
<label class="text-sm font-medium text-gray-700">
{field.label}
{#if field.required}
<span class="text-red-500">*</span>
{/if}
</label>
</div>
<div class="flex-grow">
<select
bind:value={mappedIndices[field.key]}
on:change={() => handleColumnMapping(field.key, mappedIndices[field.key])}
class="w-full px-3 py-2 border border-gray-300 rounded-md bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value={-1}>-- Select column --</option>
{#each Array.from({length: Math.max(sheetHeaders.length, 26)}, (_, i) => i) as index}
<option value={index}>
{sheetHeaders[index] || `Column ${String.fromCharCode(65 + index)}`}
{#if !sheetHeaders[index]}
(empty)
{/if}
</option>
{/each}
</select>
</div>
</div>
{/each}
</div>
<!-- Data preview -->
{#if previewData.length > 0}
<div class="mt-6">
<h4 class="text-sm font-medium text-gray-700 mb-3">Data Preview:</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
<thead class="bg-gray-50">
<tr>
{#each Array.from({length: Math.max(sheetHeaders.length, previewData[0]?.length || 0, 26)}, (_, i) => i) as index}
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider
{Object.values(mappedIndices).includes(index) ? 'bg-blue-100' : ''}">
{sheetHeaders[index] || `Column ${String.fromCharCode(65 + index)}`}
{#if Object.values(mappedIndices).includes(index)}
<div class="text-blue-600 text-xs mt-1">
{requiredFields.find(f => mappedIndices[f.key] === index)?.label}
</div>
{/if}
</th>
{/each}
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each previewData as row}
<tr> <tr>
{#each Array.from({length: Math.max(sheetHeaders.length, row.length, 26)}, (_, i) => i) as index} {#each Array.from({length: Math.min(Math.max(sheetHeaders.length, previewData[0]?.length || 0), 26)}, (_, i) => i) as index}
<td class="px-3 py-2 text-sm text-gray-500 max-w-xs truncate <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider
{Object.values(mappedIndices).includes(index) ? 'bg-blue-50' : ''}"> {Object.values(mappedIndices).includes(index) ? 'bg-blue-100' : ''}">
{row[index] || ''} {sheetHeaders[index] || `Column ${String.fromCharCode(65 + index)}`}
</td> {#if Object.values(mappedIndices).includes(index)}
<div class="text-blue-600 text-xs mt-1">
{requiredFields.find(f => mappedIndices[f.key] === index)?.label}
</div>
{/if}
</th>
{/each} {/each}
</tr> </tr>
{/each} </thead>
</tbody> <tbody class="bg-white divide-y divide-gray-200">
</table> {#each previewData as row}
<tr>
{#each Array.from({length: Math.min(Math.max(sheetHeaders.length, row.length), 26)}, (_, i) => i) as index}
<td class="px-3 py-2 text-sm text-gray-500 max-w-xs truncate
{Object.values(mappedIndices).includes(index) ? 'bg-blue-50' : ''}">
{row[index] || ''}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
</div> </div>
</div> {/if}
{/if}
<!-- Mapping status -->
<!-- Mapping status --> {#if mappingComplete}
{#if mappingComplete} <div class="bg-green-50 border border-green-200 rounded p-3">
<div class="bg-green-50 border border-green-200 rounded p-3"> <p class="text-sm text-green-800">
<p class="text-sm text-green-800"> ✓ All required fields are mapped! You can continue to the next step.
✓ All required fields are mapped! You can continue to the next step. </p>
</p> </div>
</div> {:else}
{:else} <div class="bg-yellow-50 border border-yellow-200 rounded p-3">
<div class="bg-yellow-50 border border-yellow-200 rounded p-3"> <p class="text-sm text-yellow-800">
<p class="text-sm text-yellow-800"> Please map all required fields to continue.
Please map all required fields to continue. </p>
</p> </div>
</div> {/if}
{/if} </div>
</div> {/if}
{/if} </div>
</div> {/if}
<!-- Navigation --> <!-- Navigation -->
<div class="flex justify-end"> <div class="flex justify-end">
<button <button
on:click={handleContinue} onclick={handleContinue}
disabled={!mappingComplete} disabled={!mappingComplete}
class="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed" class="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
> >

View File

@@ -3,12 +3,12 @@
import { searchSheets } from '$lib/google'; import { searchSheets } from '$lib/google';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let searchQuery = ''; let searchQuery = $state('');
let isLoading = false; let isLoading = $state(false);
let error = ''; let error = $state('');
let searchResults: any[] = []; let searchResults = $state<any[]>([]);
let hasSearched = false; let hasSearched = $state(false);
let recentSheets: any[] = []; let recentSheets = $state<any[]>([]);
const RECENT_SHEETS_KEY = 'esn-recent-sheets'; const RECENT_SHEETS_KEY = 'esn-recent-sheets';
@@ -26,7 +26,7 @@
searchResults = await searchSheets(searchQuery); searchResults = await searchSheets(searchQuery);
availableSheets.set( availableSheets.set(
searchResults.map(sheet => ({ searchResults.map(sheet => ({
id: sheet.id, spreadsheetId: sheet.spreadsheetId || sheet.id,
name: sheet.name, name: sheet.name,
url: sheet.webViewLink url: sheet.webViewLink
})) }))
@@ -56,17 +56,19 @@
} }
} }
function handleSelectSheet(sheet) { function handleSelectSheet(sheet) {
const sheetData = { const sheetData = {
id: sheet.id, spreadsheetId: sheet.spreadsheetId || sheet.id,
name: sheet.name, name: sheet.name,
url: sheet.webViewLink || sheet.url url: sheet.webViewLink || sheet.url
}; };
selectedSheet.set(sheetData); selectedSheet.set(sheetData);
} }
let canProceed = $derived($selectedSheet !== null);
function handleContinue() { function handleContinue() {
if (!canProceed) return;
currentStep.set(3); // Move to the column mapping step currentStep.set(3); // Move to the column mapping step
} }
</script> </script>
@@ -96,10 +98,11 @@
bind:value={searchQuery} bind:value={searchQuery}
placeholder="Type sheet name..." placeholder="Type sheet name..."
class="flex-grow px-4 py-2 border border-gray-300 rounded-l-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent" class="flex-grow px-4 py-2 border border-gray-300 rounded-l-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent"
onkeydown={e => { if (e.key === 'Enter') handleSearch(); }}
/> />
<button <button
on:click={handleSearch} onclick={handleSearch}
disabled={isLoading || !searchQuery.trim()} disabled={isLoading || !searchQuery.trim()}
class="px-4 py-2 bg-blue-600 text-white rounded-r-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed" class="px-4 py-2 bg-blue-600 text-white rounded-r-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
> >
@@ -131,9 +134,11 @@
<div class="space-y-3"> <div class="space-y-3">
{#each searchResults as sheet} {#each searchResults as sheet}
<div <div
class="border rounded-lg p-4 cursor-pointer transition-colors hover:bg-gray-50 class="border rounded-lg p-4 cursor-pointer transition-colors hover:bg-gray-50 {$selectedSheet?.spreadsheetId === (sheet.spreadsheetId || sheet.id) ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}"
{$selectedSheet?.id === sheet.id ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}" onclick={() => handleSelectSheet(sheet)}
on:click={() => handleSelectSheet(sheet)} tabindex="0"
role="button"
onkeydown={e => { if (e.key === 'Enter' || e.key === ' ') handleSelectSheet(sheet); }}
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
@@ -146,7 +151,7 @@
<img src={sheet.iconLink} alt="Sheet icon" class="w-5 h-5 mr-2" /> <img src={sheet.iconLink} alt="Sheet icon" class="w-5 h-5 mr-2" />
{/if} {/if}
{#if $selectedSheet?.id === sheet.id} {#if $selectedSheet?.spreadsheetId === (sheet.spreadsheetId || sheet.id)}
<svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg> </svg>
@@ -176,9 +181,11 @@
<div class="space-y-3"> <div class="space-y-3">
{#each recentSheets as sheet} {#each recentSheets as sheet}
<div <div
class="border rounded-lg p-4 cursor-pointer transition-colors hover:bg-gray-50 class="border rounded-lg p-4 cursor-pointer transition-colors hover:bg-gray-50 {$selectedSheet?.spreadsheetId === (sheet.spreadsheetId || sheet.id) ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}"
{$selectedSheet?.id === sheet.id ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}" onclick={() => handleSelectSheet(sheet)}
on:click={() => handleSelectSheet(sheet)} tabindex="0"
role="button"
onkeydown={e => { if (e.key === 'Enter' || e.key === ' ') handleSelectSheet(sheet); }}
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
@@ -191,7 +198,7 @@
<img src={sheet.iconLink} alt="Sheet icon" class="w-5 h-5 mr-2" /> <img src={sheet.iconLink} alt="Sheet icon" class="w-5 h-5 mr-2" />
{/if} {/if}
{#if $selectedSheet?.id === sheet.id} {#if $selectedSheet?.spreadsheetId === (sheet.spreadsheetId || sheet.id)}
<svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg> </svg>
@@ -221,16 +228,24 @@
{/if} {/if}
{/if} {/if}
<!-- Continue button --> <!-- Navigation -->
{#if $selectedSheet} <div class="flex justify-between">
<div class="mt-6 flex justify-end"> <button
<button onclick={() => currentStep.set(1)}
on:click={handleContinue} class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300"
class="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700" >
> ← Back to Auth
Continue → </button>
</button>
</div> <button
{/if} onclick={handleContinue}
disabled={!canProceed}
class="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{canProceed
? 'Continue →'
: 'Select a sheet to continue'}
</button>
</div>
</div> </div>
</div> </div>

View File

@@ -97,7 +97,7 @@ export const progress = writable<ProgressState>({
// Google Sheets list for search // Google Sheets list for search
export interface SheetInfo { export interface SheetInfo {
id: string; spreadsheetId: string;
name: string; name: string;
url: string; url: string;
} }