Step RowFiltering done
This commit is contained in:
@@ -3,18 +3,21 @@
|
|||||||
import StepAuth from './wizard/StepAuth.svelte';
|
import StepAuth from './wizard/StepAuth.svelte';
|
||||||
import StepSheetSearch from './wizard/StepSheetSearch.svelte';
|
import StepSheetSearch from './wizard/StepSheetSearch.svelte';
|
||||||
import StepColumnMap from './wizard/StepColumnMap.svelte';
|
import StepColumnMap from './wizard/StepColumnMap.svelte';
|
||||||
|
import StepRowFilter from './wizard/StepRowFilter.svelte';
|
||||||
// Additional steps to be added as they are implemented
|
// Additional steps to be added as they are implemented
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
StepAuth,
|
StepAuth,
|
||||||
StepSheetSearch,
|
StepSheetSearch,
|
||||||
StepColumnMap
|
StepColumnMap,
|
||||||
|
StepRowFilter
|
||||||
];
|
];
|
||||||
|
|
||||||
const stepTitles = [
|
const stepTitles = [
|
||||||
'Authenticate',
|
'Authenticate',
|
||||||
'Select Sheet',
|
'Select Sheet',
|
||||||
'Map Columns'
|
'Map Columns',
|
||||||
|
'Filter Rows'
|
||||||
];
|
];
|
||||||
|
|
||||||
function goToPreviousStep() {
|
function goToPreviousStep() {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
{ key: 'surname', label: 'Last Name', required: true },
|
{ key: 'surname', label: 'Last Name', required: true },
|
||||||
{ key: 'nationality', label: 'Nationality', required: true },
|
{ key: 'nationality', label: 'Nationality', required: true },
|
||||||
{ key: 'birthday', label: 'Birthday', required: true },
|
{ key: 'birthday', label: 'Birthday', required: true },
|
||||||
{ key: 'pictureUrl', label: 'Photo URL', required: true }
|
{ key: 'pictureUrl', label: 'Photo URL', required: true },
|
||||||
|
{ key: 'alreadyPrinted', label: 'Already Printed', required: false }
|
||||||
];
|
];
|
||||||
|
|
||||||
let mappedIndices = {
|
let mappedIndices = {
|
||||||
@@ -25,7 +26,8 @@
|
|||||||
surname: -1,
|
surname: -1,
|
||||||
nationality: -1,
|
nationality: -1,
|
||||||
birthday: -1,
|
birthday: -1,
|
||||||
pictureUrl: -1
|
pictureUrl: -1,
|
||||||
|
alreadyPrinted: -1
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load available sheets when component mounts
|
// Load available sheets when component mounts
|
||||||
@@ -63,7 +65,8 @@
|
|||||||
surname: -1,
|
surname: -1,
|
||||||
nationality: -1,
|
nationality: -1,
|
||||||
birthday: -1,
|
birthday: -1,
|
||||||
pictureUrl: -1
|
pictureUrl: -1,
|
||||||
|
alreadyPrinted: -1
|
||||||
};
|
};
|
||||||
mappingComplete = false;
|
mappingComplete = false;
|
||||||
|
|
||||||
@@ -112,7 +115,8 @@
|
|||||||
surname: -1,
|
surname: -1,
|
||||||
nationality: -1,
|
nationality: -1,
|
||||||
birthday: -1,
|
birthday: -1,
|
||||||
pictureUrl: -1
|
pictureUrl: -1,
|
||||||
|
alreadyPrinted: -1
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-mapping patterns
|
// Auto-mapping patterns
|
||||||
@@ -121,7 +125,8 @@
|
|||||||
surname: /last[\s_-]*name|surname|family[\s_-]*name|nachname/i,
|
surname: /last[\s_-]*name|surname|family[\s_-]*name|nachname/i,
|
||||||
nationality: /nationality|country|nation/i,
|
nationality: /nationality|country|nation/i,
|
||||||
birthday: /birth|date[\s_-]*of[\s_-]*birth|birthday|dob/i,
|
birthday: /birth|date[\s_-]*of[\s_-]*birth|birthday|dob/i,
|
||||||
pictureUrl: /photo|picture|image|url|avatar/i
|
pictureUrl: /photo|picture|image|url|avatar/i,
|
||||||
|
alreadyPrinted: /already[\s_-]*printed|printed|status/i
|
||||||
};
|
};
|
||||||
|
|
||||||
sheetHeaders.forEach((header, index) => {
|
sheetHeaders.forEach((header, index) => {
|
||||||
@@ -133,6 +138,27 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateMappingStatus();
|
updateMappingStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +182,8 @@
|
|||||||
surname: savedSheet.columnMapping.surname ?? -1,
|
surname: savedSheet.columnMapping.surname ?? -1,
|
||||||
nationality: savedSheet.columnMapping.nationality ?? -1,
|
nationality: savedSheet.columnMapping.nationality ?? -1,
|
||||||
birthday: savedSheet.columnMapping.birthday ?? -1,
|
birthday: savedSheet.columnMapping.birthday ?? -1,
|
||||||
pictureUrl: savedSheet.columnMapping.pictureUrl ?? -1
|
pictureUrl: savedSheet.columnMapping.pictureUrl ?? -1,
|
||||||
|
alreadyPrinted: savedSheet.columnMapping.alreadyPrinted ?? -1
|
||||||
};
|
};
|
||||||
|
|
||||||
updateMappingStatus();
|
updateMappingStatus();
|
||||||
@@ -173,7 +200,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateMappingStatus() {
|
function updateMappingStatus() {
|
||||||
mappingComplete = Object.values(mappedIndices).every(index => index !== -1);
|
// 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);
|
||||||
|
|
||||||
// Update the column mapping store
|
// Update the column mapping store
|
||||||
columnMapping.set({
|
columnMapping.set({
|
||||||
@@ -181,7 +217,8 @@
|
|||||||
surname: mappedIndices.surname,
|
surname: mappedIndices.surname,
|
||||||
nationality: mappedIndices.nationality,
|
nationality: mappedIndices.nationality,
|
||||||
birthday: mappedIndices.birthday,
|
birthday: mappedIndices.birthday,
|
||||||
pictureUrl: mappedIndices.pictureUrl
|
pictureUrl: mappedIndices.pictureUrl,
|
||||||
|
alreadyPrinted: mappedIndices.alreadyPrinted
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +241,8 @@
|
|||||||
surname: mappedIndices.surname,
|
surname: mappedIndices.surname,
|
||||||
nationality: mappedIndices.nationality,
|
nationality: mappedIndices.nationality,
|
||||||
birthday: mappedIndices.birthday,
|
birthday: mappedIndices.birthday,
|
||||||
pictureUrl: mappedIndices.pictureUrl
|
pictureUrl: mappedIndices.pictureUrl,
|
||||||
|
alreadyPrinted: mappedIndices.alreadyPrinted
|
||||||
};
|
};
|
||||||
|
|
||||||
if (sheetIndex !== -1) {
|
if (sheetIndex !== -1) {
|
||||||
@@ -370,8 +408,13 @@
|
|||||||
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"
|
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>
|
<option value={-1}>-- Select column --</option>
|
||||||
{#each sheetHeaders as header, index}
|
{#each Array.from({length: Math.max(sheetHeaders.length, 26)}, (_, i) => i) as index}
|
||||||
<option value={index}>{header}</option>
|
<option value={index}>
|
||||||
|
{sheetHeaders[index] || `Column ${String.fromCharCode(65 + index)}`}
|
||||||
|
{#if !sheetHeaders[index]}
|
||||||
|
(empty)
|
||||||
|
{/if}
|
||||||
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,10 +430,10 @@
|
|||||||
<table class="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
|
<table class="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
{#each sheetHeaders as header, index}
|
{#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
|
<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' : ''}">
|
{Object.values(mappedIndices).includes(index) ? 'bg-blue-100' : ''}">
|
||||||
{header}
|
{sheetHeaders[index] || `Column ${String.fromCharCode(65 + index)}`}
|
||||||
{#if Object.values(mappedIndices).includes(index)}
|
{#if Object.values(mappedIndices).includes(index)}
|
||||||
<div class="text-blue-600 text-xs mt-1">
|
<div class="text-blue-600 text-xs mt-1">
|
||||||
{requiredFields.find(f => mappedIndices[f.key] === index)?.label}
|
{requiredFields.find(f => mappedIndices[f.key] === index)?.label}
|
||||||
@@ -403,10 +446,10 @@
|
|||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
{#each previewData as row}
|
{#each previewData as row}
|
||||||
<tr>
|
<tr>
|
||||||
{#each row as cell, index}
|
{#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
|
<td class="px-3 py-2 text-sm text-gray-500 max-w-xs truncate
|
||||||
{Object.values(mappedIndices).includes(index) ? 'bg-blue-50' : ''}">
|
{Object.values(mappedIndices).includes(index) ? 'bg-blue-50' : ''}">
|
||||||
{cell}
|
{row[index] || ''}
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,4 +1,401 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { selectedSheet, columnMapping, rawSheetData, filteredSheetData, currentStep } from '$lib/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
let searchTerm = '';
|
||||||
|
let sortColumn = '';
|
||||||
|
let sortDirection: 'asc' | 'desc' = 'asc';
|
||||||
|
let selectedRows = new Set<number>();
|
||||||
|
let selectAll = false;
|
||||||
|
let processedData: any[] = [];
|
||||||
|
let filteredData: any[] = [];
|
||||||
|
let headers: string[] = [];
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// Filter data based on search term
|
||||||
|
if (searchTerm.trim()) {
|
||||||
|
filteredData = processedData.filter(row =>
|
||||||
|
Object.values(row).some(value =>
|
||||||
|
String(value).toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
filteredData = processedData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// Sort data if sort column is selected
|
||||||
|
if (sortColumn && filteredData.length > 0) {
|
||||||
|
filteredData = [...filteredData].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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
processSheetData();
|
||||||
|
});
|
||||||
|
|
||||||
|
function processSheetData() {
|
||||||
|
if (!$rawSheetData || $rawSheetData.length === 0 || !$columnMapping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get headers from the mapping
|
||||||
|
headers = Object.keys($columnMapping);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() !== ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasAllRequiredFields) {
|
||||||
|
processedRow._isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedRow;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initially select rows based on validity and "Already Printed" status
|
||||||
|
selectedRows = new Set(
|
||||||
|
processedData
|
||||||
|
.filter(row => {
|
||||||
|
if (!row._isValid) return false;
|
||||||
|
|
||||||
|
// Check "Already Printed" column value
|
||||||
|
const alreadyPrinted = row.alreadyPrinted;
|
||||||
|
if (alreadyPrinted) {
|
||||||
|
const value = String(alreadyPrinted).toLowerCase().trim();
|
||||||
|
// If the value is "true", "yes", "1", or any truthy value, don't select
|
||||||
|
return !(value === 'true' || value === 'yes' || value === '1' || value === 'x');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If empty or falsy, select the row
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(row => row._rowIndex)
|
||||||
|
);
|
||||||
|
|
||||||
|
updateSelectAllState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRowSelection(rowIndex: number) {
|
||||||
|
if (selectedRows.has(rowIndex)) {
|
||||||
|
selectedRows.delete(rowIndex);
|
||||||
|
} else {
|
||||||
|
selectedRows.add(rowIndex);
|
||||||
|
}
|
||||||
|
selectedRows = new Set(selectedRows); // Trigger reactivity
|
||||||
|
updateSelectAllState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
if (selectAll) {
|
||||||
|
// Deselect all visible valid rows that aren't already printed
|
||||||
|
filteredData.forEach(row => {
|
||||||
|
if (row._isValid && !isRowAlreadyPrinted(row)) {
|
||||||
|
selectedRows.delete(row._rowIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Select all visible valid rows that aren't already printed
|
||||||
|
filteredData.forEach(row => {
|
||||||
|
if (row._isValid && !isRowAlreadyPrinted(row)) {
|
||||||
|
selectedRows.add(row._rowIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
selectedRows = new Set(selectedRows);
|
||||||
|
updateSelectAllState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectAllState() {
|
||||||
|
const visibleValidUnprintedRows = filteredData.filter(row => row._isValid && !isRowAlreadyPrinted(row));
|
||||||
|
const selectedVisibleValidUnprintedRows = visibleValidUnprintedRows.filter(row => selectedRows.has(row._rowIndex));
|
||||||
|
|
||||||
|
selectAll = visibleValidUnprintedRows.length > 0 && selectedVisibleValidUnprintedRows.length === visibleValidUnprintedRows.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSort(column: string) {
|
||||||
|
if (sortColumn === column) {
|
||||||
|
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
sortColumn = column;
|
||||||
|
sortDirection = 'asc';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldLabel(field: string): string {
|
||||||
|
const labels = {
|
||||||
|
name: 'First Name',
|
||||||
|
surname: 'Last Name',
|
||||||
|
nationality: 'Nationality',
|
||||||
|
birthday: 'Birthday',
|
||||||
|
pictureUrl: 'Photo URL',
|
||||||
|
alreadyPrinted: 'Already Printed'
|
||||||
|
};
|
||||||
|
return labels[field] || field;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRowAlreadyPrinted(row: any): boolean {
|
||||||
|
const alreadyPrinted = row.alreadyPrinted;
|
||||||
|
if (!alreadyPrinted) return false;
|
||||||
|
|
||||||
|
const value = String(alreadyPrinted).toLowerCase().trim();
|
||||||
|
return value === 'true' || value === 'yes' || value === '1' || value === 'x';
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Move to next step
|
||||||
|
currentStep.set(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: selectedValidCount = Array.from(selectedRows).filter(rowIndex => {
|
||||||
|
const row = processedData.find(r => r._rowIndex === rowIndex);
|
||||||
|
return row && row._isValid;
|
||||||
|
}).length;
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h2 class="text-xl font-semibold text-gray-900">Filter Rows</h2>
|
<div class="max-w-4xl mx-auto">
|
||||||
<p class="text-sm text-gray-700">Row filtering functionality will be implemented here.</p>
|
<div class="mb-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-2">
|
||||||
|
Filter and Select Rows
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-700 mb-4">
|
||||||
|
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="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="flex-grow">
|
||||||
|
<label for="search" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Search rows
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="search"
|
||||||
|
type="text"
|
||||||
|
bind:value={searchTerm}
|
||||||
|
placeholder="Search in any field..."
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sort -->
|
||||||
|
<div class="sm:w-48">
|
||||||
|
<label for="sort" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Sort by
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="sort"
|
||||||
|
bind:value={sortColumn}
|
||||||
|
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="">No sorting</option>
|
||||||
|
{#each headers as header}
|
||||||
|
<option value={header}>{getFieldLabel(header)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="mt-4 flex flex-wrap 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">Already printed: {processedData.filter(row => isRowAlreadyPrinted(row)).length}</span>
|
||||||
|
<span>Filtered rows: {filteredData.length}</span>
|
||||||
|
<span class="font-medium text-blue-600">Selected: {selectedValidCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Table -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden mb-6">
|
||||||
|
{#if filteredData.length === 0}
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<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 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>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<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">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={selectAll}
|
||||||
|
on:change={toggleSelectAll}
|
||||||
|
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<!-- Column Headers -->
|
||||||
|
{#each headers as header}
|
||||||
|
<th
|
||||||
|
class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||||
|
on:click={() => handleSort(header)}
|
||||||
|
>
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<span>{getFieldLabel(header)}</span>
|
||||||
|
{#if sortColumn === header}
|
||||||
|
<svg class="w-4 h-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 text-gray-500 uppercase tracking-wider">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
{#each filteredData 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)}
|
||||||
|
on:change={() => toggleRowSelection(row._rowIndex)}
|
||||||
|
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
|
disabled={isRowAlreadyPrinted(row)}
|
||||||
|
/>
|
||||||
|
{#if isRowAlreadyPrinted(row)}
|
||||||
|
<div class="text-xs text-orange-600 mt-1">Already printed</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="w-4 h-4 bg-gray-200 rounded"></div>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Data Columns -->
|
||||||
|
{#each headers as header}
|
||||||
|
<td class="px-3 py-4 text-sm text-gray-900 max-w-xs truncate">
|
||||||
|
{#if header === 'alreadyPrinted'}
|
||||||
|
{#if isRowAlreadyPrinted(row)}
|
||||||
|
<span class="inline-flex px-2 py-1 text-xs font-medium bg-orange-100 text-orange-800 rounded-full">
|
||||||
|
Already Printed
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="inline-flex px-2 py-1 text-xs font-medium bg-gray-100 text-gray-600 rounded-full">
|
||||||
|
Not Printed
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
{row[header] || ''}
|
||||||
|
{/if}
|
||||||
|
</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 px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded-full">
|
||||||
|
Valid
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="inline-flex px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded-full">
|
||||||
|
Missing data
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if isRowAlreadyPrinted(row)}
|
||||||
|
<span class="inline-flex px-2 py-1 text-xs font-medium bg-orange-100 text-orange-800 rounded-full">
|
||||||
|
Already Printed
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selection Summary -->
|
||||||
|
{#if selectedValidCount > 0}
|
||||||
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="w-5 h-5 text-blue-600 mr-2" 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 -->
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
on:click={handleContinue}
|
||||||
|
disabled={selectedValidCount === 0}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{selectedValidCount > 0 ? `Continue with ${selectedValidCount} ${selectedValidCount === 1 ? 'row' : 'rows'} →` : 'Select rows to continue'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export const session = writable<{
|
|||||||
// Raw sheet data after import
|
// Raw sheet data after import
|
||||||
export const rawSheetData = writable<string[][]>([]);
|
export const rawSheetData = writable<string[][]>([]);
|
||||||
|
|
||||||
|
// Filtered sheet data after row selection
|
||||||
|
export const filteredSheetData = writable<any[]>([]);
|
||||||
|
|
||||||
// Column mapping configuration
|
// Column mapping configuration
|
||||||
export const columnMapping = writable<{
|
export const columnMapping = writable<{
|
||||||
name?: number;
|
name?: number;
|
||||||
@@ -16,6 +19,7 @@ export const columnMapping = writable<{
|
|||||||
nationality?: number;
|
nationality?: number;
|
||||||
birthday?: number;
|
birthday?: number;
|
||||||
pictureUrl?: number;
|
pictureUrl?: number;
|
||||||
|
alreadyPrinted?: number;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
// Processed row data after mapping and validation
|
// Processed row data after mapping and validation
|
||||||
|
|||||||
Reference in New Issue
Block a user