|
|
|
|
@@ -1,251 +1,118 @@
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import {
|
|
|
|
|
selectedSheet,
|
|
|
|
|
sheetData,
|
|
|
|
|
columnMapping,
|
|
|
|
|
rawSheetData,
|
|
|
|
|
filteredSheetData,
|
|
|
|
|
currentStep
|
|
|
|
|
selectedSheet,
|
|
|
|
|
currentStep,
|
|
|
|
|
} from '$lib/stores';
|
|
|
|
|
import type { RowData } from '$lib/stores';
|
|
|
|
|
import { getSheetData } from '$lib/google';
|
|
|
|
|
import { v4 as uuid } from 'uuid';
|
|
|
|
|
import Navigator from './subcomponents/Navigator.svelte';
|
|
|
|
|
import { onMount } from 'svelte';
|
|
|
|
|
import { getSheetNames, getSheetData } from '$lib/google';
|
|
|
|
|
|
|
|
|
|
let searchTerm = '';
|
|
|
|
|
let sortColumn = '';
|
|
|
|
|
let sortDirection: 'asc' | 'desc' = 'asc';
|
|
|
|
|
let selectedRows = new Set<number>();
|
|
|
|
|
let selectAll = false;
|
|
|
|
|
let processedData = $state<any[]>([]);
|
|
|
|
|
let headers: string[] = [];
|
|
|
|
|
let isLoading = $state(false);
|
|
|
|
|
let showAlreadyPrinted = $state(false);
|
|
|
|
|
let isLoading = $state(true);
|
|
|
|
|
let error = $state<string | null>(null);
|
|
|
|
|
let rows = $state<RowData[]>([]);
|
|
|
|
|
|
|
|
|
|
// Use $state for displayData instead of $derived to avoid TypeScript errors
|
|
|
|
|
let displayData = $state<any[]>([]);
|
|
|
|
|
|
|
|
|
|
// Update displayData whenever relevant values change
|
|
|
|
|
$effect(() => {
|
|
|
|
|
// Debug log at the start
|
|
|
|
|
console.log('Updating displayData from processedData:', processedData);
|
|
|
|
|
|
|
|
|
|
// If processedData is empty, return empty array
|
|
|
|
|
if (!processedData || !processedData.length) {
|
|
|
|
|
displayData = [];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clone array to avoid mutations
|
|
|
|
|
let data = [...processedData];
|
|
|
|
|
console.log('Initial data length:', data.length);
|
|
|
|
|
let sortColumn = $state<keyof RowData | null>(null);
|
|
|
|
|
let sortDirection = $state<'asc' | 'desc'>('asc');
|
|
|
|
|
|
|
|
|
|
// 1. Filter by search term
|
|
|
|
|
if (searchTerm.trim()) {
|
|
|
|
|
data = data.filter((row) =>
|
|
|
|
|
Object.values(row).some((value) =>
|
|
|
|
|
String(value)
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.includes(searchTerm.toLowerCase())
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
console.log('After search term filter:', data.length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Filter out already printed rows if collapsed
|
|
|
|
|
if (!showAlreadyPrinted) {
|
|
|
|
|
data = data.filter((row) => !isRowAlreadyPrinted(row));
|
|
|
|
|
console.log('After already printed filter:', data.length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Sort the data
|
|
|
|
|
if (sortColumn) {
|
|
|
|
|
data = [...data].sort((a, b) => {
|
|
|
|
|
const aVal = String(a[sortColumn] || '').toLowerCase();
|
|
|
|
|
const bVal = String(b[sortColumn] || '').toLowerCase();
|
|
|
|
|
|
|
|
|
|
if (sortDirection === 'asc') {
|
|
|
|
|
return aVal.localeCompare(bVal);
|
|
|
|
|
} else {
|
|
|
|
|
return bVal.localeCompare(aVal);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('Final filtered data:', data);
|
|
|
|
|
displayData = data;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
console.log('StepRowFilter mounted');
|
|
|
|
|
processSheetData();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Fetch raw sheet data from Google Sheets if not already loaded
|
|
|
|
|
async function fetchRawSheetData() {
|
|
|
|
|
console.log('Fetching raw sheet data...');
|
|
|
|
|
// Fetch and process data from the Google Sheet
|
|
|
|
|
async function fetchAndProcessData() {
|
|
|
|
|
isLoading = true;
|
|
|
|
|
error = null;
|
|
|
|
|
try {
|
|
|
|
|
if (!$selectedSheet) {
|
|
|
|
|
console.error('No sheet selected');
|
|
|
|
|
const sheet = $selectedSheet;
|
|
|
|
|
const mapping = $columnMapping;
|
|
|
|
|
|
|
|
|
|
if (!sheet || !mapping || !mapping.sheetName) {
|
|
|
|
|
error = 'Sheet information or column mapping is missing.';
|
|
|
|
|
isLoading = false;
|
|
|
|
|
rows = [];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sheetNames = await getSheetNames($selectedSheet.spreadsheetId);
|
|
|
|
|
if (sheetNames.length === 0) return;
|
|
|
|
|
const sheetName = sheetNames[0];
|
|
|
|
|
const range = `${sheetName}!A:Z`;
|
|
|
|
|
const data = await getSheetData($selectedSheet.spreadsheetId, range);
|
|
|
|
|
console.log('Fetched data:', data);
|
|
|
|
|
rawSheetData.set(data);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Error fetching raw sheet data:', e);
|
|
|
|
|
// Re-throw the error to be caught by the caller
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function processSheetData() {
|
|
|
|
|
isLoading = true;
|
|
|
|
|
try {
|
|
|
|
|
// Get headers from the mapping
|
|
|
|
|
headers = Object.keys($columnMapping);
|
|
|
|
|
const range = `${mapping.sheetName}!A:Z`;
|
|
|
|
|
const rawData = await getSheetData(sheet.id, range);
|
|
|
|
|
|
|
|
|
|
await fetchRawSheetData();
|
|
|
|
|
if (!rawData || rawData.length < 2) {
|
|
|
|
|
// Handle case with no data or only headers
|
|
|
|
|
rows = [];
|
|
|
|
|
isLoading = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Debug logging
|
|
|
|
|
console.log('Raw sheet data:', $rawSheetData);
|
|
|
|
|
console.log('Column mapping:', $columnMapping);
|
|
|
|
|
const dataRows = rawData.slice(1);
|
|
|
|
|
|
|
|
|
|
// Clear existing state before processing new data
|
|
|
|
|
selectedRows = new Set();
|
|
|
|
|
|
|
|
|
|
// Process the data starting from row 2 (skip header row)
|
|
|
|
|
processedData = $rawSheetData.slice(1).map((row, index) => {
|
|
|
|
|
const processedRow: any = {
|
|
|
|
|
_rowIndex: index + 1, // Store original row index
|
|
|
|
|
_isValid: true
|
|
|
|
|
};
|
|
|
|
|
const processedData = dataRows
|
|
|
|
|
.map((row, index): RowData | null => {
|
|
|
|
|
const name = mapping.name !== -1 ? row[mapping.name] || '' : '';
|
|
|
|
|
const surname = mapping.surname !== -1 ? row[mapping.surname] || '' : '';
|
|
|
|
|
const pictureUrl = mapping.pictureUrl !== -1 ? row[mapping.pictureUrl] || '' : '';
|
|
|
|
|
|
|
|
|
|
// Map each column according to the column mapping
|
|
|
|
|
for (const [field, columnIndex] of Object.entries($columnMapping)) {
|
|
|
|
|
if (columnIndex !== -1 && columnIndex !== undefined && columnIndex < row.length) {
|
|
|
|
|
processedRow[field] = row[columnIndex] || '';
|
|
|
|
|
} else {
|
|
|
|
|
processedRow[field] = '';
|
|
|
|
|
// Only mark as invalid if it's a required field
|
|
|
|
|
if (field !== 'alreadyPrinted') {
|
|
|
|
|
processedRow._isValid = false;
|
|
|
|
|
}
|
|
|
|
|
if (!name && !surname && !pictureUrl) {
|
|
|
|
|
return null; // Skip entirely empty rows
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if all required fields have values (excluding alreadyPrinted)
|
|
|
|
|
const requiredFields = ['name', 'surname', 'nationality', 'birthday', 'pictureUrl'];
|
|
|
|
|
const hasAllRequiredFields = requiredFields.every(
|
|
|
|
|
(field) => processedRow[field] && String(processedRow[field]).trim() !== ''
|
|
|
|
|
);
|
|
|
|
|
const alreadyPrinted =
|
|
|
|
|
mapping.alreadyPrinted !== -1
|
|
|
|
|
? (row[mapping.alreadyPrinted] || '').toLowerCase() === 'true'
|
|
|
|
|
: false;
|
|
|
|
|
|
|
|
|
|
if (!hasAllRequiredFields) {
|
|
|
|
|
processedRow._isValid = false;
|
|
|
|
|
}
|
|
|
|
|
const isValid = !!(name && surname && pictureUrl);
|
|
|
|
|
|
|
|
|
|
return processedRow;
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
id: uuid(),
|
|
|
|
|
name,
|
|
|
|
|
surname,
|
|
|
|
|
nationality: mapping.nationality !== -1 ? row[mapping.nationality] || '' : '',
|
|
|
|
|
birthday: mapping.birthday !== -1 ? row[mapping.birthday] || '' : '',
|
|
|
|
|
pictureUrl,
|
|
|
|
|
alreadyPrinted,
|
|
|
|
|
_rowIndex: index + 2, // Sheet rows are 1-based, plus header
|
|
|
|
|
_valid: isValid,
|
|
|
|
|
_checked: isValid && !alreadyPrinted
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.filter((row): row is RowData => row !== null);
|
|
|
|
|
|
|
|
|
|
// Initially select rows based on validity and "Already Printed" status
|
|
|
|
|
const rowsToConsider = processedData.filter((row) => {
|
|
|
|
|
if (!row._isValid) return false;
|
|
|
|
|
const alreadyPrinted = row.alreadyPrinted;
|
|
|
|
|
if (alreadyPrinted) {
|
|
|
|
|
const value = String(alreadyPrinted).toLowerCase().trim();
|
|
|
|
|
return !(value === 'true' || value === 'yes' || value === '1' || value === 'x');
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const initialSelection = rowsToConsider.map((row) => row._rowIndex);
|
|
|
|
|
// Make sure we create a completely new Set for reactivity
|
|
|
|
|
selectedRows = new Set([...initialSelection]);
|
|
|
|
|
|
|
|
|
|
// Update UI state
|
|
|
|
|
updateSelectAllState();
|
|
|
|
|
updateSelectedValidCount();
|
|
|
|
|
|
|
|
|
|
// Debug logging
|
|
|
|
|
console.log('Processed data:', processedData);
|
|
|
|
|
console.log('Display data:', displayData);
|
|
|
|
|
console.log('Selected rows:', selectedRows);
|
|
|
|
|
console.log('Selected valid count after initialization:', selectedValidCount);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Error processing sheet data:', e);
|
|
|
|
|
rows = processedData;
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
error = e.message || 'An unknown error occurred while fetching data.';
|
|
|
|
|
console.error(e);
|
|
|
|
|
rows = [];
|
|
|
|
|
} finally {
|
|
|
|
|
isLoading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} // Run on component mount
|
|
|
|
|
onMount(() => {
|
|
|
|
|
fetchAndProcessData();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function toggleRowSelection(rowIndex: number) {
|
|
|
|
|
if (selectedRows.has(rowIndex)) {
|
|
|
|
|
selectedRows.delete(rowIndex);
|
|
|
|
|
} else {
|
|
|
|
|
selectedRows.add(rowIndex);
|
|
|
|
|
// Function to toggle a single row's checked state
|
|
|
|
|
function toggleRow(id: string) {
|
|
|
|
|
const row = rows.find((r) => r.id === id);
|
|
|
|
|
if (row && row._valid) {
|
|
|
|
|
row._checked = !row._checked;
|
|
|
|
|
}
|
|
|
|
|
// Force reactivity with new Set
|
|
|
|
|
selectedRows = new Set([...selectedRows]);
|
|
|
|
|
console.log('Toggled row selection, new selectedRows size:', selectedRows.size);
|
|
|
|
|
updateSelectAllState();
|
|
|
|
|
updateSelectedValidCount();
|
|
|
|
|
console.log('After toggle - canProceed:', canProceed);
|
|
|
|
|
console.log('After toggle - selectedValidCount > 0:', selectedValidCount > 0);
|
|
|
|
|
console.log('After toggle - selectedValidCount:', selectedValidCount);
|
|
|
|
|
console.log("toggleRow", id, row?._checked);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleSelectAll() {
|
|
|
|
|
// Toggle selectAll state first
|
|
|
|
|
selectAll = !selectAll;
|
|
|
|
|
console.log('Toggle select all clicked, new state:', selectAll);
|
|
|
|
|
|
|
|
|
|
if (!selectAll) {
|
|
|
|
|
// If now unchecked, deselect all visible valid rows
|
|
|
|
|
displayData.forEach((row) => {
|
|
|
|
|
if (row._isValid) {
|
|
|
|
|
selectedRows.delete(row._rowIndex);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// If now checked, select all visible valid rows
|
|
|
|
|
displayData.forEach((row) => {
|
|
|
|
|
if (row._isValid) {
|
|
|
|
|
selectedRows.add(row._rowIndex);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Force reactivity with new Set
|
|
|
|
|
selectedRows = new Set([...selectedRows]);
|
|
|
|
|
console.log('Toggled select all, new selectedRows size:', selectedRows.size);
|
|
|
|
|
updateSelectedValidCount();
|
|
|
|
|
console.log('Selected valid count after toggleSelectAll:', selectedValidCount);
|
|
|
|
|
// Function to toggle all valid rows
|
|
|
|
|
function toggleSelectAll(event: Event) {
|
|
|
|
|
const target = event.target as HTMLInputElement;
|
|
|
|
|
const shouldCheck = target.checked;
|
|
|
|
|
|
|
|
|
|
rows.forEach((row) => {
|
|
|
|
|
if (row._valid && !row.alreadyPrinted) {
|
|
|
|
|
row._checked = shouldCheck;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateSelectAllState() {
|
|
|
|
|
// Find all valid rows that are currently visible
|
|
|
|
|
const visibleValidRows = displayData.filter(row => row._isValid);
|
|
|
|
|
|
|
|
|
|
if (visibleValidRows.length === 0) {
|
|
|
|
|
// No valid rows to select
|
|
|
|
|
selectAll = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if all visible valid rows are selected
|
|
|
|
|
const allSelected = visibleValidRows.every(row => selectedRows.has(row._rowIndex));
|
|
|
|
|
|
|
|
|
|
// Update selectAll state
|
|
|
|
|
selectAll = allSelected;
|
|
|
|
|
console.log('updateSelectAllState: selectAll =', selectAll,
|
|
|
|
|
', visibleValidRows =', visibleValidRows.length,
|
|
|
|
|
', selectedRows size =', selectedRows.size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSort(column: string) {
|
|
|
|
|
// Function to handle sorting
|
|
|
|
|
function sortBy(column: keyof RowData) {
|
|
|
|
|
if (sortColumn === column) {
|
|
|
|
|
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
|
|
|
|
} else {
|
|
|
|
|
@@ -254,374 +121,251 @@
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getFieldLabel(field: string): string {
|
|
|
|
|
const labels: { [key: string]: string } = {
|
|
|
|
|
name: 'First Name',
|
|
|
|
|
surname: 'Last Name',
|
|
|
|
|
nationality: 'Nationality',
|
|
|
|
|
birthday: 'Birthday',
|
|
|
|
|
pictureUrl: 'Photo URL',
|
|
|
|
|
alreadyPrinted: 'Already Printed'
|
|
|
|
|
};
|
|
|
|
|
return labels[field] || field;
|
|
|
|
|
}
|
|
|
|
|
// Derived state for sorted data to be displayed
|
|
|
|
|
const displayData = $derived.by(() => {
|
|
|
|
|
if (!sortColumn) return rows;
|
|
|
|
|
|
|
|
|
|
function isRowAlreadyPrinted(row: any): boolean {
|
|
|
|
|
const alreadyPrinted = row.alreadyPrinted;
|
|
|
|
|
if (!alreadyPrinted) return false;
|
|
|
|
|
return [...rows].sort((a, b) => {
|
|
|
|
|
const aValue = a[sortColumn];
|
|
|
|
|
const bValue = b[sortColumn];
|
|
|
|
|
|
|
|
|
|
const value = String(alreadyPrinted).toLowerCase().trim();
|
|
|
|
|
return value === 'true' || value === 'yes' || value === '1' || value === 'x';
|
|
|
|
|
}
|
|
|
|
|
if (aValue === bValue) return 0;
|
|
|
|
|
|
|
|
|
|
let comparison = 0;
|
|
|
|
|
if (aValue > bValue) {
|
|
|
|
|
comparison = 1;
|
|
|
|
|
} else {
|
|
|
|
|
comparison = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sortDirection === 'asc' ? comparison : -comparison;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Derived state to determine if the "Select All" checkbox should be checked
|
|
|
|
|
const allValidRowsSelected = $derived.by(() => {
|
|
|
|
|
const validRows = rows.filter((row) => row._valid && !row.alreadyPrinted);
|
|
|
|
|
if (validRows.length === 0) return false;
|
|
|
|
|
return validRows.every((row) => row._checked);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const selectedCount = $derived(rows.filter((row) => row._checked).length);
|
|
|
|
|
|
|
|
|
|
function handleContinue() {
|
|
|
|
|
// Filter the data to only include selected rows
|
|
|
|
|
const selectedData = processedData.filter(
|
|
|
|
|
(row) => selectedRows.has(row._rowIndex) && row._isValid
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Store the filtered data
|
|
|
|
|
filteredSheetData.set(selectedData);
|
|
|
|
|
$sheetData = rows.filter((row) => row._checked);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use $state for selectedValidCount
|
|
|
|
|
let selectedValidCount = $state(0);
|
|
|
|
|
|
|
|
|
|
// Create a dedicated function to recalculate selectedValidCount
|
|
|
|
|
function updateSelectedValidCount() {
|
|
|
|
|
// Get array of row indices
|
|
|
|
|
const rowIndices = Array.from(selectedRows);
|
|
|
|
|
console.log('Selected row indices:', rowIndices);
|
|
|
|
|
|
|
|
|
|
// Count valid rows
|
|
|
|
|
let count = 0;
|
|
|
|
|
for (const rowIndex of rowIndices) {
|
|
|
|
|
// Find the row in processedData
|
|
|
|
|
const row = processedData.find(r => r._rowIndex === rowIndex);
|
|
|
|
|
if (row && row._isValid) {
|
|
|
|
|
count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('Setting selectedValidCount to:', count, 'from', selectedValidCount);
|
|
|
|
|
selectedValidCount = count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update count when selectedRows or processedData changes
|
|
|
|
|
$effect(() => {
|
|
|
|
|
// Track dependencies explicitly
|
|
|
|
|
const rowsSize = selectedRows.size;
|
|
|
|
|
const dataSize = processedData.length;
|
|
|
|
|
|
|
|
|
|
console.log('Effect triggered - selectedRows size:', rowsSize, 'processedData size:', dataSize);
|
|
|
|
|
updateSelectedValidCount();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Allow proceeding only if at least one valid row is selected
|
|
|
|
|
let canProceed = $derived(selectedValidCount > 0);
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<div class="p-6">
|
|
|
|
|
<div class="mb-6">
|
|
|
|
|
<h2 class="mb-2 text-xl font-semibold text-gray-900">Filter and Select Rows</h2>
|
|
|
|
|
|
|
|
|
|
<p class="mb-4 text-sm text-gray-700">
|
|
|
|
|
Review your data and select which rows you want to include in the card generation. Only rows
|
|
|
|
|
with all required fields will be available for selection.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Search and Filter Controls -->
|
|
|
|
|
<div class="mb-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
|
|
|
|
<div class="flex flex-col gap-4 sm:flex-row">
|
|
|
|
|
<!-- Search -->
|
|
|
|
|
<div class="flex-grow">
|
|
|
|
|
<label for="search" class="mb-2 block text-sm font-medium text-gray-700">
|
|
|
|
|
Search rows
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
id="search"
|
|
|
|
|
type="text"
|
|
|
|
|
bind:value={searchTerm}
|
|
|
|
|
placeholder="Search in any field..."
|
|
|
|
|
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"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Sort -->
|
|
|
|
|
<div class="sm:w-48">
|
|
|
|
|
<label for="sort" class="mb-2 block text-sm font-medium text-gray-700"> Sort by </label>
|
|
|
|
|
<select
|
|
|
|
|
id="sort"
|
|
|
|
|
bind:value={sortColumn}
|
|
|
|
|
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="">No sorting</option>
|
|
|
|
|
{#each headers as header}
|
|
|
|
|
<option value={header}>{getFieldLabel(header)}</option>
|
|
|
|
|
{/each}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="container max-w-none p-6">
|
|
|
|
|
<div class="mb-4 flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="mb-2 text-xl font-semibold text-gray-900">Filter and Select Rows</h2>
|
|
|
|
|
<p class="text-sm text-gray-700">
|
|
|
|
|
Review your data and select which rows to include. Invalid or already printed rows are
|
|
|
|
|
disabled.
|
|
|
|
|
</p>
|
|
|
|
|
{#if $selectedSheet?.id}
|
|
|
|
|
<p class="mt-1 text-sm text-gray-500">
|
|
|
|
|
Need to make changes?
|
|
|
|
|
<a
|
|
|
|
|
href={`https://docs.google.com/spreadsheets/d/${$selectedSheet.id}/edit`}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
class="text-blue-600 underline hover:text-blue-800"
|
|
|
|
|
>
|
|
|
|
|
Open Google Sheet
|
|
|
|
|
</a>
|
|
|
|
|
</p>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Stats -->
|
|
|
|
|
<div class="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-600">
|
|
|
|
|
<span>Total rows: {processedData.length}</span>
|
|
|
|
|
<span>Valid rows: {processedData.filter((row) => row._isValid).length}</span>
|
|
|
|
|
<span class="text-orange-600"
|
|
|
|
|
>Printed: {processedData.filter((row) => isRowAlreadyPrinted(row)).length}</span
|
|
|
|
|
>
|
|
|
|
|
<span>Filtered rows: {displayData.length}</span>
|
|
|
|
|
<span class="font-medium text-blue-600">Selected: {selectedValidCount}</span>
|
|
|
|
|
|
|
|
|
|
<div class="ml-auto flex items-center gap-4">
|
|
|
|
|
<button
|
|
|
|
|
class="text-sm text-gray-600 hover:text-gray-900"
|
|
|
|
|
onclick={() => (showAlreadyPrinted = !showAlreadyPrinted)}
|
|
|
|
|
>
|
|
|
|
|
{showAlreadyPrinted ? 'Hide' : 'Show'} Printed ({processedData.filter((row) =>
|
|
|
|
|
isRowAlreadyPrinted(row)
|
|
|
|
|
).length})
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onclick={processSheetData}
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
class="inline-flex items-center rounded-md bg-blue-600 px-3 py-1 text-sm font-medium text-white hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-wait disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
{#if isLoading}
|
|
|
|
|
<svg
|
|
|
|
|
class="mr-2 h-4 w-4 animate-spin"
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
fill="none"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
>
|
|
|
|
|
<circle
|
|
|
|
|
class="opacity-25"
|
|
|
|
|
cx="12"
|
|
|
|
|
cy="12"
|
|
|
|
|
r="10"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="4"
|
|
|
|
|
/>
|
|
|
|
|
<path
|
|
|
|
|
class="opacity-75"
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
Refreshing...
|
|
|
|
|
{:else}
|
|
|
|
|
Refresh Data
|
|
|
|
|
{/if}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Data Table -->
|
|
|
|
|
<div class="relative mb-6 overflow-hidden rounded-lg border border-gray-200 bg-white">
|
|
|
|
|
{#if displayData.length === 0 && !isLoading}
|
|
|
|
|
<div class="py-12 text-center">
|
|
|
|
|
<button
|
|
|
|
|
onclick={fetchAndProcessData}
|
|
|
|
|
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-wait disabled:opacity-50"
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
>
|
|
|
|
|
{#if isLoading}
|
|
|
|
|
<svg
|
|
|
|
|
class="mx-auto h-12 w-12 text-gray-400"
|
|
|
|
|
class="-ml-1 mr-2 h-5 w-5 animate-spin text-gray-500"
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
fill="none"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
>
|
|
|
|
|
<circle
|
|
|
|
|
class="opacity-25"
|
|
|
|
|
cx="12"
|
|
|
|
|
cy="12"
|
|
|
|
|
r="10"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
stroke-width="4"
|
|
|
|
|
></circle>
|
|
|
|
|
<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"
|
|
|
|
|
/>
|
|
|
|
|
class="opacity-75"
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
|
|
|
></path>
|
|
|
|
|
</svg>
|
|
|
|
|
<h3 class="mt-2 text-sm font-medium text-gray-900">No data found</h3>
|
|
|
|
|
<p class="mt-1 text-sm text-gray-500">
|
|
|
|
|
{searchTerm ? 'No rows match your search criteria.' : 'No data available to display.'}
|
|
|
|
|
</p>
|
|
|
|
|
Refreshing...
|
|
|
|
|
{:else}
|
|
|
|
|
Refresh Data
|
|
|
|
|
{/if}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if isLoading}
|
|
|
|
|
<div class="py-12 text-center">
|
|
|
|
|
<p class="text-lg">Loading data from Google Sheet...</p>
|
|
|
|
|
<p class="text-gray-500">Please wait a moment.</p>
|
|
|
|
|
</div>
|
|
|
|
|
{:else if error}
|
|
|
|
|
<div
|
|
|
|
|
class="rounded-md border border-red-400 bg-red-50 p-4"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex">
|
|
|
|
|
<div class="flex-shrink-0">
|
|
|
|
|
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
|
|
|
<path
|
|
|
|
|
fill-rule="evenodd"
|
|
|
|
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
|
|
|
|
|
clip-rule="evenodd"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ml-3">
|
|
|
|
|
<h3 class="text-sm font-medium text-red-800">An Error Occurred</h3>
|
|
|
|
|
<div class="mt-2 text-sm text-red-700">
|
|
|
|
|
<p>{error}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
</div>
|
|
|
|
|
{:else if rows.length === 0}
|
|
|
|
|
<div class="py-12 text-center">
|
|
|
|
|
<h3 class="text-lg font-medium text-gray-900">No Data Found</h3>
|
|
|
|
|
<p class="mt-1 text-sm text-gray-500">
|
|
|
|
|
The selected sheet appears to be empty or could not be read.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="overflow-hidden rounded-lg border border-gray-200">
|
|
|
|
|
<div class="overflow-x-auto">
|
|
|
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
|
|
|
<thead class="bg-gray-50">
|
|
|
|
|
<tr>
|
|
|
|
|
<!-- Select All Checkbox -->
|
|
|
|
|
<th class="px-3 py-3 text-left">
|
|
|
|
|
<th class="px-4 py-3 text-left">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={selectAll}
|
|
|
|
|
onclick={(e) => {
|
|
|
|
|
// Use event.preventDefault() to avoid default checkbox behavior
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
toggleSelectAll();
|
|
|
|
|
}}
|
|
|
|
|
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
onchange={toggleSelectAll}
|
|
|
|
|
checked={allValidRowsSelected}
|
|
|
|
|
/>
|
|
|
|
|
</th>
|
|
|
|
|
|
|
|
|
|
<!-- Column Headers -->
|
|
|
|
|
{#each headers.filter((h) => h !== 'alreadyPrinted') as header}
|
|
|
|
|
<th
|
|
|
|
|
class="cursor-pointer px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase hover:bg-gray-100"
|
|
|
|
|
onclick={() => !isLoading && handleSort(header)}
|
|
|
|
|
>
|
|
|
|
|
<div class="flex items-center space-x-1">
|
|
|
|
|
<span>{getFieldLabel(header)}</span>
|
|
|
|
|
{#if sortColumn === header}
|
|
|
|
|
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
|
|
|
{#if sortDirection === 'asc'}
|
|
|
|
|
<path
|
|
|
|
|
fill-rule="evenodd"
|
|
|
|
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
|
|
|
clip-rule="evenodd"
|
|
|
|
|
/>
|
|
|
|
|
{:else}
|
|
|
|
|
<path
|
|
|
|
|
fill-rule="evenodd"
|
|
|
|
|
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z"
|
|
|
|
|
clip-rule="evenodd"
|
|
|
|
|
/>
|
|
|
|
|
{/if}
|
|
|
|
|
</svg>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</th>
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
|
|
<!-- Status Column -->
|
|
|
|
|
<th
|
|
|
|
|
class="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
|
|
|
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
|
|
|
|
onclick={() => sortBy('_rowIndex')}>#</th
|
|
|
|
|
>
|
|
|
|
|
<th
|
|
|
|
|
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
|
|
|
|
onclick={() => sortBy('name')}>Name</th
|
|
|
|
|
>
|
|
|
|
|
<th
|
|
|
|
|
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
|
|
|
|
onclick={() => sortBy('surname')}>Surname</th
|
|
|
|
|
>
|
|
|
|
|
<th
|
|
|
|
|
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
|
|
|
|
onclick={() => sortBy('nationality')}>Nationality</th
|
|
|
|
|
>
|
|
|
|
|
<th
|
|
|
|
|
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
|
|
|
|
onclick={() => sortBy('birthday')}>Birthday</th
|
|
|
|
|
>
|
|
|
|
|
<th
|
|
|
|
|
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
|
|
|
|
onclick={() => sortBy('pictureUrl')}>Picture URL</th
|
|
|
|
|
>
|
|
|
|
|
<th
|
|
|
|
|
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
|
|
|
|
onclick={() => sortBy('alreadyPrinted')}>Printed</th
|
|
|
|
|
>
|
|
|
|
|
<th
|
|
|
|
|
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
|
|
|
|
onclick={() => sortBy('_valid')}>Status</th
|
|
|
|
|
>
|
|
|
|
|
Status
|
|
|
|
|
</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody class="divide-y divide-gray-200 bg-white">
|
|
|
|
|
{#if isLoading}
|
|
|
|
|
<!-- Loading skeleton rows -->
|
|
|
|
|
{#each Array(5) as _, index}
|
|
|
|
|
<tr class="hover:bg-gray-50">
|
|
|
|
|
<!-- Selection Checkbox Skeleton -->
|
|
|
|
|
<td class="px-3 py-4">
|
|
|
|
|
<div class="h-4 w-4 animate-pulse rounded bg-gray-200"></div>
|
|
|
|
|
</td>
|
|
|
|
|
|
|
|
|
|
<!-- Data Columns Skeletons -->
|
|
|
|
|
{#each headers.filter((h) => h !== 'alreadyPrinted') as header}
|
|
|
|
|
<td class="px-3 py-4">
|
|
|
|
|
<div
|
|
|
|
|
class="h-4 animate-pulse rounded bg-gray-200"
|
|
|
|
|
style="width: {Math.random() * 40 + 60}%"
|
|
|
|
|
></div>
|
|
|
|
|
</td>
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
|
|
<!-- Status Column Skeleton -->
|
|
|
|
|
<td class="px-3 py-4">
|
|
|
|
|
<div class="flex flex-col space-y-1">
|
|
|
|
|
<div class="h-6 w-16 animate-pulse rounded-full bg-gray-200"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{/each}
|
|
|
|
|
{:else}
|
|
|
|
|
<!-- Actual data rows -->
|
|
|
|
|
{#each displayData as row}
|
|
|
|
|
<tr
|
|
|
|
|
class="hover:bg-gray-50 {!row._isValid ? 'opacity-50' : ''} {isRowAlreadyPrinted(
|
|
|
|
|
row
|
|
|
|
|
)
|
|
|
|
|
? 'bg-orange-50'
|
|
|
|
|
: ''}"
|
|
|
|
|
>
|
|
|
|
|
<!-- Selection Checkbox -->
|
|
|
|
|
<td class="px-3 py-4">
|
|
|
|
|
{#if row._isValid}
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={selectedRows.has(row._rowIndex)}
|
|
|
|
|
onclick={(e) => {
|
|
|
|
|
// Use event.preventDefault() to avoid default checkbox behavior
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
toggleRowSelection(row._rowIndex);
|
|
|
|
|
}}
|
|
|
|
|
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
|
|
|
/>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="h-4 w-4 rounded bg-gray-200"></div>
|
|
|
|
|
{/if}
|
|
|
|
|
</td>
|
|
|
|
|
|
|
|
|
|
<!-- Data Columns -->
|
|
|
|
|
{#each headers.filter((h) => h !== 'alreadyPrinted') as header}
|
|
|
|
|
<td class="max-w-xs truncate px-3 py-4 text-sm text-gray-900">
|
|
|
|
|
{row[header] || ''}
|
|
|
|
|
</td>
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
|
|
<!-- Status Column -->
|
|
|
|
|
<td class="px-3 py-4 text-sm">
|
|
|
|
|
<div class="flex flex-col space-y-1">
|
|
|
|
|
{#if row._isValid}
|
|
|
|
|
<span
|
|
|
|
|
class="inline-flex rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800"
|
|
|
|
|
>
|
|
|
|
|
Valid
|
|
|
|
|
</span>
|
|
|
|
|
{:else}
|
|
|
|
|
<span
|
|
|
|
|
class="inline-flex rounded-full bg-red-100 px-2 py-1 text-xs font-medium text-red-800"
|
|
|
|
|
>
|
|
|
|
|
Missing data
|
|
|
|
|
</span>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if isRowAlreadyPrinted(row)}
|
|
|
|
|
<span
|
|
|
|
|
class="inline-flex rounded-full bg-orange-100 px-2 py-1 text-xs font-medium text-orange-800"
|
|
|
|
|
>
|
|
|
|
|
Already Printed
|
|
|
|
|
</span>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{/each}
|
|
|
|
|
{/if}
|
|
|
|
|
{#each displayData as row (row.id)}
|
|
|
|
|
<tr
|
|
|
|
|
class="hover:bg-gray-50"
|
|
|
|
|
class:bg-gray-100={!row._valid}
|
|
|
|
|
class:text-gray-400={!row._valid || row.alreadyPrinted}
|
|
|
|
|
class:bg-orange-50={row.alreadyPrinted}
|
|
|
|
|
>
|
|
|
|
|
<td class="px-4 py-3">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-200"
|
|
|
|
|
checked={row._checked}
|
|
|
|
|
disabled={!row._valid}
|
|
|
|
|
onchange={() => toggleRow(row.id)}
|
|
|
|
|
/>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm">{row._rowIndex}</td>
|
|
|
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm">{row.name}</td>
|
|
|
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm">{row.surname}</td>
|
|
|
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm">{row.nationality}</td>
|
|
|
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm">{row.birthday}</td>
|
|
|
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm">
|
|
|
|
|
<a
|
|
|
|
|
href={row.pictureUrl}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
class="text-blue-600 hover:underline"
|
|
|
|
|
title={row.pictureUrl}>link</a
|
|
|
|
|
>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm">
|
|
|
|
|
{#if row.alreadyPrinted}
|
|
|
|
|
<span
|
|
|
|
|
class="inline-flex rounded-full bg-orange-100 px-2 text-xs font-semibold leading-5 text-orange-800"
|
|
|
|
|
>Yes</span
|
|
|
|
|
>
|
|
|
|
|
{:else}
|
|
|
|
|
<span
|
|
|
|
|
class="inline-flex rounded-full bg-gray-100 px-2 text-xs font-semibold leading-5 text-gray-800"
|
|
|
|
|
>No</span
|
|
|
|
|
>
|
|
|
|
|
{/if}
|
|
|
|
|
</td>
|
|
|
|
|
<td class="whitespace-nowrap px-4 py-3 text-sm">
|
|
|
|
|
{#if row._valid}
|
|
|
|
|
<span
|
|
|
|
|
class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800"
|
|
|
|
|
>Valid</span
|
|
|
|
|
>
|
|
|
|
|
{:else}
|
|
|
|
|
<span
|
|
|
|
|
class="inline-flex rounded-full bg-red-100 px-2 text-xs font-semibold leading-5 text-red-800"
|
|
|
|
|
>Invalid</span
|
|
|
|
|
>
|
|
|
|
|
{/if}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{/each}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Selection Summary -->
|
|
|
|
|
{#if selectedValidCount > 0}
|
|
|
|
|
<div class="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4">
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
<svg class="mr-2 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>
|
|
|
|
|
<span class="text-sm text-blue-800">
|
|
|
|
|
<strong>{selectedValidCount}</strong>
|
|
|
|
|
{selectedValidCount === 1 ? 'row' : 'rows'} selected for card generation
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<!-- Navigation -->
|
|
|
|
|
<Navigator
|
|
|
|
|
canProceed={canProceed}
|
|
|
|
|
currentStep={currentStep}
|
|
|
|
|
textBack="Back to Colum Selection"
|
|
|
|
|
textForwardDisabled="Select rows to continue"
|
|
|
|
|
textForwardEnabled={`Continue with ${selectedValidCount} ${selectedValidCount === 1 ? 'row' : 'rows'} →`}
|
|
|
|
|
onForward={handleContinue}
|
|
|
|
|
/>
|
|
|
|
|
<div class="mt-6">
|
|
|
|
|
<Navigator
|
|
|
|
|
canProceed={selectedCount > 0}
|
|
|
|
|
currentStep={currentStep}
|
|
|
|
|
onForward={handleContinue}
|
|
|
|
|
textBack="Back to Column Mapping"
|
|
|
|
|
textForwardEnabled="Continue ({selectedCount} selected)"
|
|
|
|
|
textForwardDisabled="Select at least one valid row"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|