774 lines
25 KiB
Svelte
774 lines
25 KiB
Svelte
<script lang="ts">
|
|
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 = $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 },
|
|
{ key: 'surname', label: 'Last Name', required: true },
|
|
{ key: 'nationality', label: 'Nationality', required: true },
|
|
{ key: 'birthday', label: 'Birthday', required: true },
|
|
{ key: 'pictureUrl', label: 'Photo URL', required: true },
|
|
{ key: 'alreadyPrinted', label: 'Already Printed', required: false }
|
|
];
|
|
|
|
// 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');
|
|
}
|
|
});
|
|
|
|
// 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 {
|
|
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);
|
|
error = 'Failed to load sheet names. Please try again.';
|
|
} finally {
|
|
isLoadingSheets = false;
|
|
}
|
|
}
|
|
|
|
function handleSheetSelect(sheetName: string) {
|
|
console.log('Sheet selected:', sheetName);
|
|
selectedSheetName = sheetName;
|
|
|
|
// Clear any previous data when selecting a new sheet
|
|
rawSheetData.set([]);
|
|
sheetHeaders = [];
|
|
previewData = [];
|
|
mappedIndices = {
|
|
name: -1,
|
|
surname: -1,
|
|
nationality: -1,
|
|
birthday: -1,
|
|
pictureUrl: -1,
|
|
alreadyPrinted: -1
|
|
};
|
|
mappingComplete = false;
|
|
hasSavedMapping = false;
|
|
showMappingEditor = true;
|
|
|
|
// Load sheet data
|
|
if (sheetName) {
|
|
loadSheetData(sheetName);
|
|
}
|
|
}
|
|
|
|
async function loadSheetData(sheetName: string) {
|
|
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.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
|
|
// We don't need to set all the raw data here
|
|
|
|
// Try to auto-map columns
|
|
autoMapColumns();
|
|
|
|
// Check if we have saved column mapping for this sheet
|
|
loadSavedColumnMapping();
|
|
} else {
|
|
error = 'The selected sheet appears to be empty.';
|
|
console.warn('Sheet is empty');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error loading sheet data:', err);
|
|
error = 'Failed to load sheet data. Please try again.';
|
|
} finally {
|
|
isLoadingData = false;
|
|
}
|
|
}
|
|
|
|
function autoMapColumns() {
|
|
// Reset mappings
|
|
mappedIndices = {
|
|
name: -1,
|
|
surname: -1,
|
|
nationality: -1,
|
|
birthday: -1,
|
|
pictureUrl: -1,
|
|
alreadyPrinted: -1
|
|
};
|
|
|
|
// Auto-mapping 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,
|
|
birthday: /birth|date[\s_-]*of[\s_-]*birth|birthday|dob/i,
|
|
pictureUrl: /photo|picture|image|url|avatar/i,
|
|
alreadyPrinted: /already[\s_-]*printed|printed|status/i
|
|
};
|
|
|
|
sheetHeaders.forEach((header, index) => {
|
|
for (const [field, pattern] of Object.entries(patterns)) {
|
|
if (pattern.test(header) && mappedIndices[field] === -1) {
|
|
mappedIndices[field] = index;
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// If "Already Printed" column wasn't found, try to find the first empty column
|
|
if (mappedIndices.alreadyPrinted === -1 && previewData.length > 0) {
|
|
// Check up to 26 columns (A-Z) or the number of headers, whichever is larger
|
|
const maxColumns = Math.max(sheetHeaders.length, 26);
|
|
|
|
for (let colIndex = 0; colIndex < maxColumns; colIndex++) {
|
|
// Check if this column is empty (all preview rows are empty for this column)
|
|
const isEmpty = previewData.every(
|
|
(row) => !row[colIndex] || String(row[colIndex]).trim() === ''
|
|
);
|
|
|
|
// Also check if this column isn't already mapped to another field
|
|
const isAlreadyMapped = Object.entries(mappedIndices).some(
|
|
([field, index]) => field !== 'alreadyPrinted' && index === colIndex
|
|
);
|
|
|
|
if (isEmpty && !isAlreadyMapped) {
|
|
mappedIndices.alreadyPrinted = colIndex;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Auto-mapped columns:', mappedIndices);
|
|
updateMappingStatus();
|
|
}
|
|
|
|
function loadSavedColumnMapping() {
|
|
if (!$selectedSheet || !selectedSheetName) {
|
|
console.log('Cannot load saved column mapping: missing selectedSheet or selectedSheetName');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const recentSheetsKey = 'esn-recent-sheets';
|
|
const existingData = localStorage.getItem(recentSheetsKey);
|
|
|
|
if (existingData) {
|
|
const recentSheets = JSON.parse(existingData);
|
|
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,
|
|
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;
|
|
savedSheetInfo = savedSheet;
|
|
updateMappingStatus();
|
|
} else {
|
|
console.log('No saved column mapping found for the current sheet');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load saved column mapping:', err);
|
|
}
|
|
}
|
|
|
|
function handleColumnMapping(field: keyof ColumnMappingType, index: number) {
|
|
mappedIndices[field] = index;
|
|
updateMappingStatus();
|
|
}
|
|
|
|
function updateMappingStatus() {
|
|
// Only check required fields for completion
|
|
const requiredIndices = {
|
|
name: mappedIndices.name,
|
|
surname: mappedIndices.surname,
|
|
nationality: mappedIndices.nationality,
|
|
birthday: mappedIndices.birthday,
|
|
pictureUrl: mappedIndices.pictureUrl
|
|
};
|
|
|
|
mappingComplete = Object.values(requiredIndices).every((index) => index !== -1);
|
|
console.log('Mapping complete:', mappingComplete);
|
|
|
|
// Update the column mapping store
|
|
columnMapping.set({
|
|
name: mappedIndices.name,
|
|
surname: mappedIndices.surname,
|
|
nationality: mappedIndices.nationality,
|
|
birthday: mappedIndices.birthday,
|
|
pictureUrl: mappedIndices.pictureUrl,
|
|
alreadyPrinted: mappedIndices.alreadyPrinted
|
|
});
|
|
}
|
|
|
|
function handleContinue() {
|
|
if (!mappingComplete || !$selectedSheet || !selectedSheetName) return;
|
|
|
|
// Save column mapping to localStorage for the selected sheet
|
|
try {
|
|
const recentSheetsKey = 'esn-recent-sheets';
|
|
const existingData = localStorage.getItem(recentSheetsKey);
|
|
let recentSheets = existingData ? JSON.parse(existingData) : [];
|
|
|
|
// Find the current sheet in recent sheets and update its column mapping
|
|
const sheetIndex = recentSheets.findIndex(
|
|
(sheet: SheetInfoType) =>
|
|
(sheet.id === $selectedSheet.spreadsheetId ||
|
|
sheet.spreadsheetId === $selectedSheet.spreadsheetId) &&
|
|
(sheet.sheetName === selectedSheetName || sheet.sheetMapping === selectedSheetName)
|
|
);
|
|
|
|
const columnMappingData = {
|
|
name: mappedIndices.name,
|
|
surname: mappedIndices.surname,
|
|
nationality: mappedIndices.nationality,
|
|
birthday: mappedIndices.birthday,
|
|
pictureUrl: mappedIndices.pictureUrl,
|
|
alreadyPrinted: mappedIndices.alreadyPrinted
|
|
};
|
|
|
|
if (sheetIndex !== -1) {
|
|
// 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 = {
|
|
spreadsheetId: $selectedSheet.spreadsheetId,
|
|
name: $selectedSheet.name,
|
|
sheetMapping: selectedSheetName,
|
|
columnMapping: columnMappingData,
|
|
lastUsed: new Date().toISOString()
|
|
};
|
|
|
|
recentSheets.unshift(newEntry);
|
|
|
|
// Keep only the 3 most recent
|
|
if (recentSheets.length > 3) {
|
|
recentSheets = recentSheets.slice(0, 3);
|
|
}
|
|
}
|
|
|
|
localStorage.setItem(recentSheetsKey, JSON.stringify(recentSheets));
|
|
} catch (err) {
|
|
console.error('Failed to save column mapping to localStorage:', err);
|
|
}
|
|
|
|
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">
|
|
<div class="mb-6">
|
|
<h2 class="mb-2 text-xl font-semibold text-gray-900">Select Sheet and Map Columns</h2>
|
|
|
|
<p class="mb-4 text-sm text-gray-700">
|
|
First, select which sheet contains your member data, then map the columns to the required
|
|
fields.
|
|
</p>
|
|
</div>
|
|
|
|
{#if hasSavedMapping && !showMappingEditor}
|
|
<!-- Simplified view when we have saved mapping -->
|
|
<div class="mb-6 rounded-lg border border-green-200 bg-green-50 p-6">
|
|
<div class="text-center">
|
|
<svg class="mx-auto mb-4 h-16 w-16 text-green-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>
|
|
<h3 class="mb-3 text-xl font-semibold text-green-800">Configuration Complete</h3>
|
|
<p class="mb-2 text-green-700">
|
|
<span class="font-medium">Spreadsheet:</span>
|
|
{savedSheetInfo?.name}
|
|
</p>
|
|
<p class="mb-2 text-green-700">
|
|
<span class="font-medium">Sheet:</span>
|
|
{selectedSheetName}
|
|
</p>
|
|
<p class="mb-6 text-green-700">
|
|
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 rounded-lg border border-green-300 px-4 py-2 text-sm font-medium text-green-700 transition-colors hover:bg-green-100 hover:text-green-900"
|
|
>
|
|
<svg class="mr-2 h-4 w-4" 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>
|
|
</div>
|
|
{:else}
|
|
<!-- Sheet Selection -->
|
|
<div class="mb-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
|
<h3 class="mb-3 text-sm font-medium text-gray-700">Step 1: Select Sheet</h3>
|
|
|
|
{#if isLoadingSheets}
|
|
<div class="flex items-center">
|
|
<div
|
|
class="mr-3 h-5 w-5 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"
|
|
></div>
|
|
<span class="text-sm text-gray-600">Loading sheets...</span>
|
|
</div>
|
|
{:else if error}
|
|
<div class="mb-3 rounded-lg border border-red-300 bg-red-50 p-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="rounded border border-gray-200 bg-white py-6 text-center">
|
|
<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>
|
|
<p class="mb-3 block text-sm font-medium text-gray-700">Choose sheet:</p>
|
|
|
|
<div class="space-y-2">
|
|
{#each availableSheets as sheetName}
|
|
<div
|
|
role="button"
|
|
tabindex="0"
|
|
class="cursor-pointer rounded-lg border p-3 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">
|
|
<div
|
|
class="flex h-4 w-4 items-center justify-center rounded-full border-2
|
|
{selectedSheetName === sheetName
|
|
? 'border-blue-500 bg-blue-500'
|
|
: 'border-gray-300'}"
|
|
>
|
|
{#if selectedSheetName === sheetName}
|
|
<div class="h-2 w-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 class="flex-shrink-0">
|
|
<svg class="h-5 w-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="mb-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
|
<h3 class="mb-3 text-sm font-medium text-gray-700">Step 2: Map Columns</h3>
|
|
|
|
{#if isLoadingData}
|
|
<div class="flex items-center">
|
|
<div
|
|
class="mr-3 h-5 w-5 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"
|
|
></div>
|
|
<span class="text-sm text-gray-600">Loading sheet data...</span>
|
|
</div>
|
|
{:else if sheetHeaders.length === 0}
|
|
<div class="py-8 text-center text-gray-500">
|
|
<p class="text-sm">Select a sheet above to map columns</p>
|
|
</div>
|
|
{:else}
|
|
<div class="space-y-4">
|
|
<p class="mb-4 text-sm text-gray-600">
|
|
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 rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
>
|
|
<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="mb-3 text-sm font-medium text-gray-700">Data Preview:</h4>
|
|
<div class="overflow-x-auto">
|
|
<table
|
|
class="min-w-full divide-y divide-gray-200 rounded-lg border border-gray-200"
|
|
>
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
{#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 tracking-wider text-gray-500 uppercase
|
|
{Object.values(mappedIndices).includes(index)
|
|
? 'bg-blue-100'
|
|
: ''}"
|
|
>
|
|
{sheetHeaders[index] || `Column ${String.fromCharCode(65 + index)}`}
|
|
{#if Object.values(mappedIndices).includes(index)}
|
|
<div class="mt-1 text-xs text-blue-600">
|
|
{requiredFields.find((f) => mappedIndices[f.key] === index)?.label}
|
|
</div>
|
|
{/if}
|
|
</th>
|
|
{/each}
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-200 bg-white">
|
|
{#each previewData as row}
|
|
<tr>
|
|
{#each Array.from({ length: Math.min(Math.max(sheetHeaders.length, row.length), 26) }, (_, i) => i) as index}
|
|
<td
|
|
class="max-w-xs truncate px-3 py-2 text-sm text-gray-500
|
|
{Object.values(mappedIndices).includes(index)
|
|
? 'bg-blue-50'
|
|
: ''}"
|
|
>
|
|
{row[index] || ''}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Mapping status -->
|
|
{#if mappingComplete}
|
|
<div class="rounded border border-green-200 bg-green-50 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="rounded border border-yellow-200 bg-yellow-50 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-between">
|
|
<button
|
|
onclick={() => currentStep.set(2)}
|
|
class="rounded-lg bg-gray-200 px-4 py-2 font-medium text-gray-700 hover:bg-gray-300"
|
|
>
|
|
← Back to Sheet Selection
|
|
</button>
|
|
|
|
<button
|
|
onclick={handleContinue}
|
|
disabled={!mappingComplete}
|
|
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400"
|
|
>
|
|
{mappingComplete ? 'Continue →' : 'Select a column mapping'}
|
|
</button>
|
|
</div>
|
|
</div>
|