Fixed first two steps
This commit is contained in:
4
.github/copilot-instructions.md
vendored
4
.github/copilot-instructions.md
vendored
@@ -3,8 +3,10 @@
|
||||
- This code is written in Svelte 5
|
||||
- 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
|
||||
- User $effect, $state, $derived
|
||||
- Use $effect, $state, $derived, eg. let { value } = $state(initialValue);
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -22,12 +22,6 @@
|
||||
'Filter Rows',
|
||||
'Review Photos'
|
||||
];
|
||||
|
||||
function goToPreviousStep() {
|
||||
if ($currentStep > 1) {
|
||||
currentStep.update(n => n - 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
@@ -55,17 +49,6 @@
|
||||
<!-- Step content -->
|
||||
<div class="bg-white rounded-lg shadow-sm">
|
||||
<svelte:component this={steps[$currentStep - 1]} />
|
||||
</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>
|
||||
|
||||
@@ -2,15 +2,48 @@
|
||||
import { selectedSheet, columnMapping, rawSheetData, currentStep } from '$lib/stores';
|
||||
import { getSheetNames, getSheetData } from '$lib/google';
|
||||
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 isLoadingData = false;
|
||||
let availableSheets: string[] = [];
|
||||
let selectedSheetName = '';
|
||||
let error = '';
|
||||
let sheetHeaders: string[] = [];
|
||||
let previewData: string[][] = [];
|
||||
let mappingComplete = false;
|
||||
let isLoadingSheets = $state(false);
|
||||
let isLoadingData = $state(false);
|
||||
let availableSheets = $state<string[]>([]);
|
||||
let selectedSheetName = $state('');
|
||||
let error = $state('');
|
||||
let sheetHeaders = $state<string[]>([]);
|
||||
let previewData = $state<string[][]>([]);
|
||||
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 = [
|
||||
{ key: 'name', label: 'First Name', required: true },
|
||||
@@ -21,30 +54,123 @@
|
||||
{ key: 'alreadyPrinted', label: 'Already Printed', required: false }
|
||||
];
|
||||
|
||||
let mappedIndices = {
|
||||
name: -1,
|
||||
surname: -1,
|
||||
nationality: -1,
|
||||
birthday: -1,
|
||||
pictureUrl: -1,
|
||||
alreadyPrinted: -1
|
||||
};
|
||||
|
||||
// Load available sheets when component mounts
|
||||
// Load available sheets when component mounts
|
||||
onMount(async () => {
|
||||
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();
|
||||
} else {
|
||||
console.error('No spreadsheet selected on mount');
|
||||
}
|
||||
});
|
||||
|
||||
async function loadAvailableSheets() {
|
||||
if (!$selectedSheet) return;
|
||||
// Load sheet data quietly (for previously saved sheets)
|
||||
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;
|
||||
error = '';
|
||||
|
||||
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
|
||||
} catch (err) {
|
||||
console.error('Error loading sheet names:', err);
|
||||
@@ -55,7 +181,9 @@
|
||||
}
|
||||
|
||||
function handleSheetSelect(sheetName: string) {
|
||||
console.log('Sheet selected:', sheetName);
|
||||
selectedSheetName = sheetName;
|
||||
|
||||
// Clear any previous data when selecting a new sheet
|
||||
rawSheetData.set([]);
|
||||
sheetHeaders = [];
|
||||
@@ -69,6 +197,8 @@
|
||||
alreadyPrinted: -1
|
||||
};
|
||||
mappingComplete = false;
|
||||
hasSavedMapping = false;
|
||||
showMappingEditor = true;
|
||||
|
||||
// Load sheet data
|
||||
if (sheetName) {
|
||||
@@ -77,20 +207,25 @@
|
||||
}
|
||||
|
||||
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;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
// Fetch first 10 rows for headers and preview
|
||||
const range = `${sheetName}!A1:Z10`;
|
||||
const data = await getSheetData($selectedSheet.id, range);
|
||||
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
|
||||
rawSheetData.set(data);
|
||||
// We don't need to set all the raw data here
|
||||
|
||||
// Try to auto-map columns
|
||||
autoMapColumns();
|
||||
@@ -99,6 +234,7 @@
|
||||
loadSavedColumnMapping();
|
||||
} else {
|
||||
error = 'The selected sheet appears to be empty.';
|
||||
console.warn('Sheet is empty');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading sheet data:', err);
|
||||
@@ -120,7 +256,7 @@
|
||||
};
|
||||
|
||||
// Auto-mapping patterns
|
||||
const patterns = {
|
||||
const patterns: Record<keyof ColumnMappingType, RegExp> = {
|
||||
name: /first[\s_-]*name|name|given[\s_-]*name|vorname/i,
|
||||
surname: /last[\s_-]*name|surname|family[\s_-]*name|nachname/i,
|
||||
nationality: /nationality|country|nation/i,
|
||||
@@ -159,11 +295,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Auto-mapped columns:', mappedIndices);
|
||||
updateMappingStatus();
|
||||
}
|
||||
|
||||
function loadSavedColumnMapping() {
|
||||
if (!$selectedSheet || !selectedSheetName) return;
|
||||
if (!$selectedSheet || !selectedSheetName) {
|
||||
console.log('Cannot load saved column mapping: missing selectedSheet or selectedSheetName');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const recentSheetsKey = 'esn-recent-sheets';
|
||||
@@ -171,11 +311,14 @@
|
||||
|
||||
if (existingData) {
|
||||
const recentSheets = JSON.parse(existingData);
|
||||
const savedSheet = recentSheets.find(sheet =>
|
||||
sheet.id === $selectedSheet.id && sheet.sheetName === selectedSheetName
|
||||
const savedSheet = recentSheets.find((sheet: SheetInfoType) =>
|
||||
(sheet.id === $selectedSheet.spreadsheetId || sheet.spreadsheetId === $selectedSheet.spreadsheetId) &&
|
||||
(sheet.sheetName === selectedSheetName || sheet.sheetMapping === selectedSheetName)
|
||||
);
|
||||
|
||||
if (savedSheet && savedSheet.columnMapping) {
|
||||
console.log('Found saved column mapping for current sheet:', savedSheet.columnMapping);
|
||||
|
||||
// Override auto-mapping with saved mapping
|
||||
mappedIndices = {
|
||||
name: savedSheet.columnMapping.name ?? -1,
|
||||
@@ -186,7 +329,11 @@
|
||||
alreadyPrinted: savedSheet.columnMapping.alreadyPrinted ?? -1
|
||||
};
|
||||
|
||||
hasSavedMapping = true;
|
||||
savedSheetInfo = savedSheet;
|
||||
updateMappingStatus();
|
||||
} else {
|
||||
console.log('No saved column mapping found for the current sheet');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -194,7 +341,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleColumnMapping(field: string, index: number) {
|
||||
function handleColumnMapping(field: keyof ColumnMappingType, index: number) {
|
||||
mappedIndices[field] = index;
|
||||
updateMappingStatus();
|
||||
}
|
||||
@@ -210,6 +357,7 @@
|
||||
};
|
||||
|
||||
mappingComplete = Object.values(requiredIndices).every(index => index !== -1);
|
||||
console.log('Mapping complete:', mappingComplete);
|
||||
|
||||
// Update the column mapping store
|
||||
columnMapping.set({
|
||||
@@ -232,8 +380,9 @@
|
||||
let recentSheets = existingData ? JSON.parse(existingData) : [];
|
||||
|
||||
// Find the current sheet in recent sheets and update its column mapping
|
||||
const sheetIndex = recentSheets.findIndex(sheet =>
|
||||
sheet.id === $selectedSheet.id && sheet.sheetName === selectedSheetName
|
||||
const sheetIndex = recentSheets.findIndex((sheet: SheetInfoType) =>
|
||||
(sheet.id === $selectedSheet.spreadsheetId || sheet.spreadsheetId === $selectedSheet.spreadsheetId) &&
|
||||
(sheet.sheetName === selectedSheetName || sheet.sheetMapping === selectedSheetName)
|
||||
);
|
||||
|
||||
const columnMappingData = {
|
||||
@@ -249,12 +398,16 @@
|
||||
// Update existing entry
|
||||
recentSheets[sheetIndex].columnMapping = columnMappingData;
|
||||
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 {
|
||||
// Add new entry
|
||||
const newEntry = {
|
||||
id: $selectedSheet.id,
|
||||
spreadsheetId: $selectedSheet.spreadsheetId,
|
||||
name: $selectedSheet.name,
|
||||
sheetName: selectedSheetName,
|
||||
sheetMapping: selectedSheetName,
|
||||
columnMapping: columnMappingData,
|
||||
lastUsed: new Date().toISOString()
|
||||
};
|
||||
@@ -274,6 +427,34 @@
|
||||
|
||||
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>
|
||||
|
||||
<div class="p-6">
|
||||
@@ -287,201 +468,236 @@
|
||||
First, select which sheet contains your member data, then map the columns to the required fields.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Sheet Selection -->
|
||||
<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 1: Select Sheet
|
||||
</h3>
|
||||
|
||||
{#if isLoadingSheets}
|
||||
<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 sheets...</span>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="bg-red-50 border border-red-300 rounded-lg p-3 mb-3">
|
||||
<p class="text-sm text-red-800">{error}</p>
|
||||
<button
|
||||
class="mt-2 text-sm text-blue-600 hover:text-blue-800"
|
||||
on:click={loadAvailableSheets}
|
||||
|
||||
{#if hasSavedMapping && !showMappingEditor}
|
||||
<!-- Simplified view when we have saved mapping -->
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-6 mb-6">
|
||||
<div class="text-center">
|
||||
<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"/>
|
||||
</svg>
|
||||
<h3 class="text-xl font-semibold text-green-800 mb-3">Configuration Complete</h3>
|
||||
<p class="text-green-700 mb-2">
|
||||
<span class="font-medium">Spreadsheet:</span> {savedSheetInfo?.name}
|
||||
</p>
|
||||
<p class="text-green-700 mb-2">
|
||||
<span class="font-medium">Sheet:</span> {selectedSheetName}
|
||||
</p>
|
||||
<p class="text-green-700 mb-6">
|
||||
Column mapping loaded from your previous session.<br>
|
||||
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>
|
||||
</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>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-3">
|
||||
Choose sheet:
|
||||
</label>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Sheet Selection -->
|
||||
<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 1: Select Sheet
|
||||
</h3>
|
||||
|
||||
{#if isLoadingSheets}
|
||||
<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 sheets...</span>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="bg-red-50 border border-red-300 rounded-lg p-3 mb-3">
|
||||
<p class="text-sm text-red-800">{error}</p>
|
||||
<button
|
||||
class="mt-2 text-sm text-blue-600 hover:text-blue-800"
|
||||
onclick={loadAvailableSheets}
|
||||
>
|
||||
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">
|
||||
{#each availableSheets as sheetName}
|
||||
<div
|
||||
class="border rounded-lg p-3 cursor-pointer transition-colors hover:bg-gray-50
|
||||
{selectedSheetName === sheetName ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}"
|
||||
on:click={() => handleSheetSelect(sheetName)}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-4 h-4 rounded-full border-2 flex items-center justify-center
|
||||
{selectedSheetName === sheetName ? 'border-blue-500 bg-blue-500' : 'border-gray-300'}">
|
||||
{#if selectedSheetName === sheetName}
|
||||
<div class="w-2 h-2 rounded-full bg-white"></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-3 flex-grow">
|
||||
<p class="text-sm font-medium text-gray-900">{sheetName}</p>
|
||||
</div>
|
||||
|
||||
{#if selectedSheetName === sheetName}
|
||||
<div>
|
||||
<p class="block text-sm font-medium text-gray-700 mb-3">
|
||||
Choose sheet:
|
||||
</p>
|
||||
|
||||
<div class="space-y-2">
|
||||
{#each availableSheets as sheetName}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="border rounded-lg p-3 cursor-pointer transition-colors hover:bg-gray-50
|
||||
{selectedSheetName === sheetName ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}"
|
||||
onclick={() => handleSheetSelect(sheetName)}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleSheetSelect(sheetName)}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<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 class="w-4 h-4 rounded-full border-2 flex items-center justify-center
|
||||
{selectedSheetName === sheetName ? 'border-blue-500 bg-blue-500' : 'border-gray-300'}">
|
||||
{#if selectedSheetName === sheetName}
|
||||
<div class="w-2 h-2 rounded-full bg-white"></div>
|
||||
{/if}
|
||||
</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>
|
||||
{/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 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}
|
||||
|
||||
<!-- 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, 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 Array.from({length: Math.min(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>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{#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>
|
||||
{/if}
|
||||
|
||||
<!-- Mapping status -->
|
||||
{#if mappingComplete}
|
||||
<div class="bg-green-50 border border-green-200 rounded p-3">
|
||||
<p class="text-sm text-green-800">
|
||||
✓ All required fields are mapped! You can continue to the next step.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded p-3">
|
||||
<p class="text-sm text-yellow-800">
|
||||
Please map all required fields to continue.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Mapping status -->
|
||||
{#if mappingComplete}
|
||||
<div class="bg-green-50 border border-green-200 rounded p-3">
|
||||
<p class="text-sm text-green-800">
|
||||
✓ All required fields are mapped! You can continue to the next step.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded p-3">
|
||||
<p class="text-sm text-yellow-800">
|
||||
Please map all required fields to continue.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={handleContinue}
|
||||
onclick={handleContinue}
|
||||
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"
|
||||
>
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import { searchSheets } from '$lib/google';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let searchQuery = '';
|
||||
let isLoading = false;
|
||||
let error = '';
|
||||
let searchResults: any[] = [];
|
||||
let hasSearched = false;
|
||||
let recentSheets: any[] = [];
|
||||
let searchQuery = $state('');
|
||||
let isLoading = $state(false);
|
||||
let error = $state('');
|
||||
let searchResults = $state<any[]>([]);
|
||||
let hasSearched = $state(false);
|
||||
let recentSheets = $state<any[]>([]);
|
||||
|
||||
const RECENT_SHEETS_KEY = 'esn-recent-sheets';
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
searchResults = await searchSheets(searchQuery);
|
||||
availableSheets.set(
|
||||
searchResults.map(sheet => ({
|
||||
id: sheet.id,
|
||||
spreadsheetId: sheet.spreadsheetId || sheet.id,
|
||||
name: sheet.name,
|
||||
url: sheet.webViewLink
|
||||
}))
|
||||
@@ -56,17 +56,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectSheet(sheet) {
|
||||
function handleSelectSheet(sheet) {
|
||||
const sheetData = {
|
||||
id: sheet.id,
|
||||
spreadsheetId: sheet.spreadsheetId || sheet.id,
|
||||
name: sheet.name,
|
||||
url: sheet.webViewLink || sheet.url
|
||||
};
|
||||
|
||||
selectedSheet.set(sheetData);
|
||||
}
|
||||
|
||||
let canProceed = $derived($selectedSheet !== null);
|
||||
|
||||
function handleContinue() {
|
||||
if (!canProceed) return;
|
||||
currentStep.set(3); // Move to the column mapping step
|
||||
}
|
||||
</script>
|
||||
@@ -96,10 +98,11 @@
|
||||
bind:value={searchQuery}
|
||||
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"
|
||||
onkeydown={e => { if (e.key === 'Enter') handleSearch(); }}
|
||||
/>
|
||||
|
||||
<button
|
||||
on:click={handleSearch}
|
||||
onclick={handleSearch}
|
||||
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"
|
||||
>
|
||||
@@ -131,9 +134,11 @@
|
||||
<div class="space-y-3">
|
||||
{#each searchResults as sheet}
|
||||
<div
|
||||
class="border rounded-lg p-4 cursor-pointer transition-colors hover:bg-gray-50
|
||||
{$selectedSheet?.id === sheet.id ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}"
|
||||
on:click={() => handleSelectSheet(sheet)}
|
||||
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'}"
|
||||
onclick={() => handleSelectSheet(sheet)}
|
||||
tabindex="0"
|
||||
role="button"
|
||||
onkeydown={e => { if (e.key === 'Enter' || e.key === ' ') handleSelectSheet(sheet); }}
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -146,7 +151,7 @@
|
||||
<img src={sheet.iconLink} alt="Sheet icon" class="w-5 h-5 mr-2" />
|
||||
{/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">
|
||||
<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>
|
||||
@@ -176,9 +181,11 @@
|
||||
<div class="space-y-3">
|
||||
{#each recentSheets as sheet}
|
||||
<div
|
||||
class="border rounded-lg p-4 cursor-pointer transition-colors hover:bg-gray-50
|
||||
{$selectedSheet?.id === sheet.id ? 'border-blue-500 bg-blue-50' : 'border-gray-200'}"
|
||||
on:click={() => handleSelectSheet(sheet)}
|
||||
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'}"
|
||||
onclick={() => handleSelectSheet(sheet)}
|
||||
tabindex="0"
|
||||
role="button"
|
||||
onkeydown={e => { if (e.key === 'Enter' || e.key === ' ') handleSelectSheet(sheet); }}
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -191,7 +198,7 @@
|
||||
<img src={sheet.iconLink} alt="Sheet icon" class="w-5 h-5 mr-2" />
|
||||
{/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">
|
||||
<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>
|
||||
@@ -221,16 +228,24 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Continue button -->
|
||||
{#if $selectedSheet}
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button
|
||||
on:click={handleContinue}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700"
|
||||
>
|
||||
Continue →
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Navigation -->
|
||||
<div class="flex justify-between">
|
||||
<button
|
||||
onclick={() => currentStep.set(1)}
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300"
|
||||
>
|
||||
← Back to Auth
|
||||
</button>
|
||||
|
||||
<button
|
||||
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>
|
||||
|
||||
@@ -97,7 +97,7 @@ export const progress = writable<ProgressState>({
|
||||
|
||||
// Google Sheets list for search
|
||||
export interface SheetInfo {
|
||||
id: string;
|
||||
spreadsheetId: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user