Compare commits

...

7 Commits

Author SHA1 Message Date
Roman Krček
6ed1f985e0 Fixed the rest
All checks were successful
Build Docker image / build (push) Successful in 2m29s
Build Docker image / deploy (push) Successful in 4s
Build Docker image / verify (push) Successful in 40s
2025-08-06 15:08:45 +02:00
Roman Krček
c6cc9c6658 Fixed sheet local storage 2025-08-06 14:35:12 +02:00
Roman Krček
7fb72c7d75 Fiexed column mapping storage 2025-08-06 14:34:52 +02:00
Roman Krček
ebb14e9e1a Fixed card details 2025-08-06 14:34:44 +02:00
Roman Krček
3af8c116a4 Fixed column mapping 2025-08-06 14:27:56 +02:00
Roman Krček
e9987009c7 Fixed sheet search 2025-08-06 13:47:37 +02:00
Roman Krček
d8b4eea3ef Updated stores 2025-08-06 13:45:03 +02:00
12 changed files with 528 additions and 882 deletions

View File

@@ -14,4 +14,5 @@
- Remain consistent in styling and code structure. - Remain consistent in styling and code structure.
- Avoid unncessary iterations. If problems is mostly solved, stop. - Avoid unncessary iterations. If problems is mostly solved, stop.
- Split big components into subcomponents. Always create smaller subcomponents for better context management later. - Split big components into subcomponents. Always create smaller subcomponents for better context management later.
- Do not do what you're not being asked. Stick to scope of my request. - Do not do what you're not being asked. Stick to scope of my request.
- Do not edit stores.ts ! Unless is explicitly allow you to.

7
package-lock.json generated
View File

@@ -15,7 +15,6 @@
"@types/gapi.client.drive-v3": "^0.0.5", "@types/gapi.client.drive-v3": "^0.0.5",
"@types/gapi.client.sheets-v4": "^0.0.4", "@types/gapi.client.sheets-v4": "^0.0.4",
"@types/google.accounts": "^0.0.17", "@types/google.accounts": "^0.0.17",
"@types/uuid": "^10.0.0",
"fontkit": "^2.0.4", "fontkit": "^2.0.4",
"heic-convert": "^2.1.0", "heic-convert": "^2.1.0",
"idb": "^8.0.3", "idb": "^8.0.3",
@@ -29,6 +28,7 @@
"@sveltejs/kit": "^2.22.0", "@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0", "@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@types/uuid": "^10.0.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
@@ -631,6 +631,9 @@
}, },
"node_modules/@types/uuid": { "node_modules/@types/uuid": {
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@webgpu/types": { "node_modules/@webgpu/types": {
@@ -2019,6 +2022,8 @@
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [ "funding": [
"https://github.com/sponsors/broofa", "https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan" "https://github.com/sponsors/ctavan"

View File

@@ -18,6 +18,7 @@
"@sveltejs/kit": "^2.22.0", "@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0", "@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"@types/uuid": "^10.0.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
@@ -36,7 +37,6 @@
"@types/gapi.client.drive-v3": "^0.0.5", "@types/gapi.client.drive-v3": "^0.0.5",
"@types/gapi.client.sheets-v4": "^0.0.4", "@types/gapi.client.sheets-v4": "^0.0.4",
"@types/google.accounts": "^0.0.17", "@types/google.accounts": "^0.0.17",
"@types/uuid": "^10.0.0",
"fontkit": "^2.0.4", "fontkit": "^2.0.4",
"heic-convert": "^2.1.0", "heic-convert": "^2.1.0",
"idb": "^8.0.3", "idb": "^8.0.3",

View File

@@ -23,9 +23,9 @@
'Select Sheet', 'Select Sheet',
'Map Columns', 'Map Columns',
'Filter Rows', 'Filter Rows',
'Card Details', 'Enter Card Details',
'Review Photos', 'Preview Gallery',
'Generate PDFs' 'Generate Cards'
]; ];
</script> </script>

View File

@@ -1,25 +1,33 @@
<script lang="ts"> <script lang="ts">
import { currentStep, cardDetails } from '$lib/stores'; import { currentStep, cardDetails } from '$lib/stores';
import Navigator from './subcomponents/Navigator.svelte'; import Navigator from './subcomponents/Navigator.svelte';
import { onMount } from 'svelte';
// Initialize from localStorage if available, otherwise from the store let homeSection = $state('');
let homeSection = $state( let validityStart = $state('');
(typeof window !== 'undefined' && window.localStorage.getItem('homeSection')) ||
$cardDetails.homeSection
);
let validityStart = $state($cardDetails.validityStart || new Date().toISOString().split('T')[0]);
// Persist homeSection to localStorage whenever it changes onMount(() => {
$effect(() => { validityStart = new Date().toISOString().split('T')[0];
if (typeof window !== 'undefined') {
localStorage.setItem('homeSection', homeSection); try {
const savedHomeSection = localStorage.getItem('homeSection');
if (savedHomeSection) {
homeSection = savedHomeSection;
}
} catch (error) {
console.error('Failed to access localStorage on mount:', error);
} }
}); });
let canProceed = $derived(homeSection.trim() !== '' && validityStart.trim() !== ''); let canProceed = $derived(homeSection.trim() !== '' && validityStart.trim() !== '');
function handleContinue() { function handleContinue() {
cardDetails.set({ homeSection, validityStart }); try {
localStorage.setItem('homeSection', homeSection);
} catch (error) {
console.error('Failed to save to localStorage:', error);
}
$cardDetails = { homeSection, validityStart };
} }
</script> </script>
@@ -33,7 +41,7 @@
<div class="space-y-6"> <div class="space-y-6">
<div> <div>
<label for="homeSection" class="block text-sm font-medium text-gray-700 mb-2"> <label for="homeSection" class="mb-2 block text-sm font-medium text-gray-700">
Home Section Home Section
</label> </label>
<input <input
@@ -46,7 +54,7 @@
</div> </div>
<div> <div>
<label for="validityStart" class="block text-sm font-medium text-gray-700 mb-2"> <label for="validityStart" class="mb-2 block text-sm font-medium text-gray-700">
Card Validity Start Date Card Validity Start Date
</label> </label>
<input <input
@@ -55,18 +63,20 @@
bind:value={validityStart} bind:value={validityStart}
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" 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"
/> />
<p class="mt-2 text-xs text-gray-500">Default date is today, but future date can be selected.</p> <p class="mt-2 text-xs text-gray-500">
Default date is today, but future date can be selected.
</p>
</div> </div>
</div> </div>
<div class="mt-10"> <div class="mt-10">
<Navigator <Navigator
canProceed={canProceed} {canProceed}
currentStep={currentStep} {currentStep}
onForward={handleContinue} onForward={handleContinue}
textBack="Back to Row Filtering" textBack="Back to Row Filtering"
textForwardEnabled="Continue to Photo Review" textForwardEnabled="Continue to Photo Review"
textForwardDisabled="Please fill out all fields" textForwardDisabled="Please fill out all fields"
/> />
</div> </div>
</div> </div>

View File

@@ -1,30 +1,10 @@
<script lang="ts"> <script lang="ts">
import { selectedSheet, columnMapping, rawSheetData, currentStep } from '$lib/stores'; import { selectedSheet, currentStep, columnMapping } from '$lib/stores';
import type { ColumnMappingType, SheetInfoType } from '$lib/stores';
import { getSheetNames, getSheetData } from '$lib/google'; import { getSheetNames, getSheetData } from '$lib/google';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Navigator from './subcomponents/Navigator.svelte'; import Navigator from './subcomponents/Navigator.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 isLoadingSheets = $state(false);
let isLoadingData = $state(false); let isLoadingData = $state(false);
let availableSheets = $state<string[]>([]); let availableSheets = $state<string[]>([]);
@@ -36,14 +16,14 @@
let hasSavedMapping = $state(false); let hasSavedMapping = $state(false);
let showMappingEditor = $state(false); let showMappingEditor = $state(false);
let savedSheetInfo = $state<SheetInfoType | null>(null); let savedSheetInfo = $state<SheetInfoType | null>(null);
let mappedIndices = $state<ColumnMappingType>({ let mappedIndices = $state<ColumnMappingType>({
name: -1, name: -1,
surname: -1, surname: -1,
nationality: -1, nationality: -1,
birthday: -1, birthday: -1,
pictureUrl: -1, pictureUrl: -1,
alreadyPrinted: -1 alreadyPrinted: -1,
sheetName: ''
}); });
const requiredFields = [ const requiredFields = [
@@ -69,36 +49,31 @@
if (recentSheets && recentSheets.length > 0) { if (recentSheets && recentSheets.length > 0) {
// Find a sheet that matches the current spreadsheet // Find a sheet that matches the current spreadsheet
const savedSheet = recentSheets.find( const savedSheet = recentSheets.find(
(sheet: SheetInfoType) => (sheet: SheetInfoType) => sheet.id === $selectedSheet.id
sheet.id === $selectedSheet.spreadsheetId ||
sheet.spreadsheetId === $selectedSheet.spreadsheetId
); );
if (savedSheet) { if (savedSheet) {
console.log('Found saved sheet configuration:', savedSheet); console.log('Found saved sheet configuration:', savedSheet);
// We have a saved sheet for this spreadsheet // We have a saved sheet for this spreadsheet
selectedSheetName = savedSheet.sheetName || savedSheet.sheetMapping || ''; selectedSheetName = savedSheet.columnMapping.sheetName;
savedSheetInfo = savedSheet; savedSheetInfo = savedSheet;
if (savedSheet.columnMapping) { if (savedSheet.columnMapping) {
// Set the mapped indices from saved data // Set the mapped indices from saved data
mappedIndices = { mappedIndices = {
name: savedSheet.columnMapping.name ?? -1, name: savedSheet.columnMapping.name,
surname: savedSheet.columnMapping.surname ?? -1, surname: savedSheet.columnMapping.surname,
nationality: savedSheet.columnMapping.nationality ?? -1, nationality: savedSheet.columnMapping.nationality,
birthday: savedSheet.columnMapping.birthday ?? -1, birthday: savedSheet.columnMapping.birthday,
pictureUrl: savedSheet.columnMapping.pictureUrl ?? -1, pictureUrl: savedSheet.columnMapping.pictureUrl,
alreadyPrinted: savedSheet.columnMapping.alreadyPrinted ?? -1 alreadyPrinted: savedSheet.columnMapping.alreadyPrinted,
sheetName: selectedSheetName
}; };
hasSavedMapping = true; hasSavedMapping = true;
updateMappingStatus(); updateMappingStatus();
columnMapping.set(mappedIndices);
// Don't load sheet data immediately for better performance return;
// We'll load it when needed (when editing or continuing)
return; // Skip loading available sheets since we're using saved data
} }
} }
} }
@@ -114,72 +89,20 @@
} }
}); });
// 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() { async function loadAvailableSheets() {
if (!$selectedSheet) { if (!$selectedSheet) {
console.error('Cannot load available sheets: no sheet selected'); console.error('Cannot load available sheets: no sheet selected');
return; return;
} }
console.log('Loading available sheets for spreadsheet:', $selectedSheet.spreadsheetId); console.log('Loading available sheets for spreadsheet:', $selectedSheet.id);
isLoadingSheets = true; isLoadingSheets = true;
error = ''; error = '';
try { try {
const sheetNames = await getSheetNames($selectedSheet.spreadsheetId); const sheetNames = await getSheetNames($selectedSheet.id);
console.log('Loaded sheet names:', sheetNames); console.log('Loaded sheet names:', sheetNames);
availableSheets = sheetNames; availableSheets = sheetNames;
// Don't auto-select any sheet - let user choose
} catch (err) { } catch (err) {
console.error('Error loading sheet names:', err); console.error('Error loading sheet names:', err);
error = 'Failed to load sheet names. Please try again.'; error = 'Failed to load sheet names. Please try again.';
@@ -193,7 +116,6 @@
selectedSheetName = sheetName; selectedSheetName = sheetName;
// Clear any previous data when selecting a new sheet // Clear any previous data when selecting a new sheet
rawSheetData.set([]);
sheetHeaders = []; sheetHeaders = [];
previewData = []; previewData = [];
mappedIndices = { mappedIndices = {
@@ -202,7 +124,8 @@
nationality: -1, nationality: -1,
birthday: -1, birthday: -1,
pictureUrl: -1, pictureUrl: -1,
alreadyPrinted: -1 alreadyPrinted: -1,
sheetName: sheetName
}; };
mappingComplete = false; mappingComplete = false;
hasSavedMapping = false; hasSavedMapping = false;
@@ -220,19 +143,14 @@
return; return;
} }
console.log( console.log('Loading sheet data for spreadsheet:', $selectedSheet.id, 'sheet:', sheetName);
'Loading sheet data for spreadsheet:',
$selectedSheet.spreadsheetId,
'sheet:',
sheetName
);
isLoadingData = true; isLoadingData = true;
error = ''; error = '';
try { try {
// Fetch first 10 rows for headers and preview // Fetch first 10 rows for headers and preview
const range = `${sheetName}!A1:Z10`; const range = `${sheetName}!A1:Z10`;
const data = await getSheetData($selectedSheet.spreadsheetId, range); const data = await getSheetData($selectedSheet.id, range);
if (data && data.length > 0) { if (data && data.length > 0) {
console.log('Loaded sheet data with', data.length, 'rows'); console.log('Loaded sheet data with', data.length, 'rows');
@@ -265,11 +183,12 @@
nationality: -1, nationality: -1,
birthday: -1, birthday: -1,
pictureUrl: -1, pictureUrl: -1,
alreadyPrinted: -1 alreadyPrinted: -1,
sheetName: selectedSheetName
}; };
// Auto-mapping patterns // Auto-mapping patterns
const patterns: Record<keyof ColumnMappingType, RegExp> = { const patterns: Record<keyof Omit<ColumnMappingType, 'sheetName'>, RegExp> = {
name: /first[\s_-]*name|name|given[\s_-]*name|vorname/i, name: /first[\s_-]*name|name|given[\s_-]*name|vorname/i,
surname: /last[\s_-]*name|surname|family[\s_-]*name|nachname/i, surname: /last[\s_-]*name|surname|family[\s_-]*name|nachname/i,
nationality: /nationality|country|nation/i, nationality: /nationality|country|nation/i,
@@ -280,8 +199,9 @@
sheetHeaders.forEach((header, index) => { sheetHeaders.forEach((header, index) => {
for (const [field, pattern] of Object.entries(patterns)) { for (const [field, pattern] of Object.entries(patterns)) {
if (pattern.test(header) && mappedIndices[field] === -1) { const key = field as keyof ColumnMappingType;
mappedIndices[field] = index; if (pattern.test(header) && mappedIndices[key] === -1) {
mappedIndices[key] = index;
break; break;
} }
} }
@@ -300,7 +220,11 @@
// Also check if this column isn't already mapped to another field // Also check if this column isn't already mapped to another field
const isAlreadyMapped = Object.entries(mappedIndices).some( const isAlreadyMapped = Object.entries(mappedIndices).some(
([field, index]) => field !== 'alreadyPrinted' && index === colIndex ([field, index]) =>
field !== 'alreadyPrinted' &&
index === colIndex &&
field !== 'sheetName' &&
index === colIndex
); );
if (isEmpty && !isAlreadyMapped) { if (isEmpty && !isAlreadyMapped) {
@@ -327,10 +251,7 @@
if (existingData) { if (existingData) {
const recentSheets = JSON.parse(existingData); const recentSheets = JSON.parse(existingData);
const savedSheet = recentSheets.find( const savedSheet = recentSheets.find(
(sheet: SheetInfoType) => (sheet: SheetInfoType) => sheet.id === $selectedSheet.id
(sheet.id === $selectedSheet.spreadsheetId ||
sheet.spreadsheetId === $selectedSheet.spreadsheetId) &&
(sheet.sheetName === selectedSheetName || sheet.sheetMapping === selectedSheetName)
); );
if (savedSheet && savedSheet.columnMapping) { if (savedSheet && savedSheet.columnMapping) {
@@ -343,7 +264,8 @@
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 alreadyPrinted: savedSheet.columnMapping.alreadyPrinted ?? -1,
sheetName: selectedSheetName
}; };
hasSavedMapping = true; hasSavedMapping = true;
@@ -359,18 +281,34 @@
} }
function handleColumnMapping(field: keyof ColumnMappingType, index: number) { function handleColumnMapping(field: keyof ColumnMappingType, index: number) {
if (!mappedIndices) {
mappedIndices = {
name: -1,
surname: -1,
nationality: -1,
birthday: -1,
pictureUrl: -1,
alreadyPrinted: -1,
sheetName: selectedSheetName
};
}
mappedIndices[field] = index; mappedIndices[field] = index;
updateMappingStatus(); updateMappingStatus();
} }
function updateMappingStatus() { function updateMappingStatus() {
if (!mappedIndices) {
mappingComplete = false;
return;
}
// Only check required fields for completion // Only check required fields for completion
const requiredIndices = { const requiredIndices = {
name: mappedIndices.name, name: mappedIndices.name,
surname: mappedIndices.surname, surname: mappedIndices.surname,
nationality: mappedIndices.nationality, nationality: mappedIndices.nationality,
birthday: mappedIndices.birthday, birthday: mappedIndices.birthday,
pictureUrl: mappedIndices.pictureUrl pictureUrl: mappedIndices.pictureUrl,
sheetName: selectedSheetName
}; };
mappingComplete = Object.values(requiredIndices).every((index) => index !== -1); mappingComplete = Object.values(requiredIndices).every((index) => index !== -1);
@@ -383,7 +321,8 @@
nationality: mappedIndices.nationality, nationality: mappedIndices.nationality,
birthday: mappedIndices.birthday, birthday: mappedIndices.birthday,
pictureUrl: mappedIndices.pictureUrl, pictureUrl: mappedIndices.pictureUrl,
alreadyPrinted: mappedIndices.alreadyPrinted alreadyPrinted: mappedIndices.alreadyPrinted,
sheetName: selectedSheetName
}); });
} }
@@ -398,10 +337,7 @@
// Find the current sheet in recent sheets and update its column mapping // Find the current sheet in recent sheets and update its column mapping
const sheetIndex = recentSheets.findIndex( const sheetIndex = recentSheets.findIndex(
(sheet: SheetInfoType) => (sheet: SheetInfoType) => sheet.id === $selectedSheet.id
(sheet.id === $selectedSheet.spreadsheetId ||
sheet.spreadsheetId === $selectedSheet.spreadsheetId) &&
(sheet.sheetName === selectedSheetName || sheet.sheetMapping === selectedSheetName)
); );
const columnMappingData = { const columnMappingData = {
@@ -410,7 +346,8 @@
nationality: mappedIndices.nationality, nationality: mappedIndices.nationality,
birthday: mappedIndices.birthday, birthday: mappedIndices.birthday,
pictureUrl: mappedIndices.pictureUrl, pictureUrl: mappedIndices.pictureUrl,
alreadyPrinted: mappedIndices.alreadyPrinted alreadyPrinted: mappedIndices.alreadyPrinted,
sheetName: selectedSheetName
}; };
if (sheetIndex !== -1) { if (sheetIndex !== -1) {
@@ -419,16 +356,12 @@
recentSheets[sheetIndex].lastUsed = new Date().toISOString(); recentSheets[sheetIndex].lastUsed = new Date().toISOString();
// Ensure we have consistent property names // Ensure we have consistent property names
recentSheets[sheetIndex].spreadsheetId = recentSheets[sheetIndex].id = recentSheets[sheetIndex].id || recentSheets[sheetIndex].id;
recentSheets[sheetIndex].spreadsheetId || recentSheets[sheetIndex].id;
recentSheets[sheetIndex].sheetMapping =
recentSheets[sheetIndex].sheetMapping || recentSheets[sheetIndex].sheetName;
} else { } else {
// Add new entry // Add new entry
const newEntry = { const newEntry = {
spreadsheetId: $selectedSheet.spreadsheetId, id: $selectedSheet.id,
name: $selectedSheet.name, name: $selectedSheet.name,
sheetMapping: selectedSheetName,
columnMapping: columnMappingData, columnMapping: columnMappingData,
lastUsed: new Date().toISOString() lastUsed: new Date().toISOString()
}; };
@@ -461,7 +394,7 @@
try { try {
isLoadingData = true; isLoadingData = true;
const range = `${selectedSheetName}!A1:Z10`; const range = `${selectedSheetName}!A1:Z10`;
const data = await getSheetData($selectedSheet.spreadsheetId, range); const data = await getSheetData($selectedSheet.id, range);
if (data && data.length > 0) { if (data && data.length > 0) {
sheetHeaders = data[0]; sheetHeaders = data[0];
@@ -510,15 +443,17 @@
<h3 class="text-sm font-medium text-blue-800">Saved Configuration Found</h3> <h3 class="text-sm font-medium text-blue-800">Saved Configuration Found</h3>
<div class="mt-2 text-sm text-blue-700"> <div class="mt-2 text-sm text-blue-700">
<p> <p>
Using saved mapping for sheet <span class="font-semibold">"{selectedSheetName}"</span> from Using saved mapping for sheet <span class="font-semibold"
spreadsheet <span class="font-semibold">"{savedSheetInfo?.name}"</span>. >"{selectedSheetName}"</span
>
from spreadsheet <span class="font-semibold">"{savedSheetInfo?.name}"</span>.
</p> </p>
</div> </div>
</div> </div>
<div class="mt-3 md:mt-0 md:ml-6"> <div class="mt-3 md:mt-0 md:ml-6">
<button <button
onclick={handleShowEditor} onclick={handleShowEditor}
class="whitespace-nowrap rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" class="rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
> >
Edit Mapping Edit Mapping
</button> </button>
@@ -780,7 +715,8 @@
</div> </div>
<div class="ml-3"> <div class="ml-3">
<p class="text-sm text-yellow-800"> <p class="text-sm text-yellow-800">
Please map all required fields (<span class="text-red-500">*</span>) to continue. Please map all required fields (<span class="text-red-500">*</span>) to
continue.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import { columnMapping, filteredSheetData, currentStep, pictures, cropRects } from '$lib/stores'; import { columnMapping, sheetData, currentStep, pictures, cropRects } from '$lib/stores';
import { downloadDriveImage, isGoogleDriveUrl, createImageObjectUrl } from '$lib/google'; import { downloadDriveImage, isGoogleDriveUrl, createImageObjectUrl } from '$lib/google';
import Navigator from './subcomponents/Navigator.svelte'; import Navigator from './subcomponents/Navigator.svelte';
import PhotoCard from './subcomponents/PhotoCard.svelte'; import PhotoCard from './subcomponents/PhotoCard.svelte';
@@ -61,8 +61,6 @@
} }
async function processPhotosInParallel() { async function processPhotosInParallel() {
if (isProcessing) return;
console.log('Starting processPhotos with queues...'); console.log('Starting processPhotos with queues...');
isProcessing = true; isProcessing = true;
processedCount = 0; processedCount = 0;
@@ -73,6 +71,7 @@
console.log('Cleared IndexedDB.'); console.log('Cleared IndexedDB.');
} catch (e) { } catch (e) {
console.error('Could not clear IndexedDB:', e); console.error('Could not clear IndexedDB:', e);
return;
} }
// Initialize queues with more conservative concurrency // Initialize queues with more conservative concurrency
@@ -95,7 +94,7 @@
} }
}); });
const validRows = $filteredSheetData.filter((row) => row._isValid); const validRows = $sheetData.filter((row) => row._valid);
const photoUrls = new Set<string>(); const photoUrls = new Set<string>();
const photoMap = new Map<string, any[]>(); const photoMap = new Map<string, any[]>();
@@ -130,16 +129,12 @@
// Initialize detector and process photos // Initialize detector and process photos
onMount(() => { onMount(() => {
console.log('StepGallery mounted');
initializeDetector(); // Start loading model initializeDetector(); // Start loading model
if ($filteredSheetData.length > 0 && $columnMapping.pictureUrl !== undefined) { if ($sheetData.length > 0 && $columnMapping.pictureUrl !== undefined) {
console.log('Processing photos for gallery step'); console.log('Processing photos for gallery step');
processPhotosInParallel(); processPhotosInParallel();
} else { } else {
console.log('No data to process:', { console.log('No data to process: !');
dataLength: $filteredSheetData.length,
pictureUrlMapping: $columnMapping.pictureUrl
});
} }
}); });
@@ -148,7 +143,6 @@
if (!isRetry) { if (!isRetry) {
photo.status = 'loading'; photo.status = 'loading';
// No need to reassign photos array with $state reactivity
} }
try { try {
@@ -285,14 +279,6 @@
imageTensor = tf.browser.fromPixels(img); imageTensor = tf.browser.fromPixels(img);
const predictions = await detector.estimateFaces(imageTensor, false); const predictions = await detector.estimateFaces(imageTensor, false);
// Log memory usage for debugging
const memInfo = tf.memory();
console.log(`TensorFlow.js memory after face detection for ${photo.name}:`, {
numTensors: memInfo.numTensors,
numDataBuffers: memInfo.numDataBuffers,
numBytes: memInfo.numBytes
});
if (predictions.length > 0) { if (predictions.length > 0) {
const getProbability = (p: number | tf.Tensor) => const getProbability = (p: number | tf.Tensor) =>
typeof p === 'number' ? p : p.dataSync()[0]; typeof p === 'number' ? p : p.dataSync()[0];
@@ -518,23 +504,6 @@
<div class="text-gray-600">Failed</div> <div class="text-gray-600">Failed</div>
</div> </div>
</div> </div>
{#if photos.filter((p) => p.status === 'error').length > 0}
<div class="mt-4 rounded border border-yellow-200 bg-yellow-50 p-3">
<p class="text-sm text-yellow-800">
<strong>Note:</strong> Cards will only be generated for photos that load successfully.
</p>
</div>
{/if}
{#if !canProceed() && photos.filter((p) => p.status === 'success').length > 0}
<div class="mt-4 rounded border border-blue-200 bg-blue-50 p-3">
<p class="text-sm text-blue-800">
<strong>Tip:</strong> All photos need to be cropped before proceeding. Face detection runs
automatically.
</p>
</div>
{/if}
</div> </div>
{/if} {/if}

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { filteredSheetData, currentStep, pictures, cropRects } from '$lib/stores'; import { sheetData, currentStep, pictures, cropRects } from '$lib/stores';
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'; import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import * as fontkit from 'fontkit'; import * as fontkit from 'fontkit';
import { clear } from 'idb-keyval'; import { clear } from 'idb-keyval';
@@ -30,11 +30,12 @@
url: string | null; url: string | null;
size: number | null; size: number | null;
error: string | null; error: string | null;
downloadName?: string;
}; };
const initialFiles: GeneratedFile[] = [ const initialFiles: GeneratedFile[] = [
{ {
name: 'people_data.pdf', name: 'esncards_text.pdf',
displayName: 'Text PDF', displayName: 'Text PDF',
state: 'idle', state: 'idle',
url: null, url: null,
@@ -42,7 +43,7 @@
error: null error: null
}, },
{ {
name: 'people_photos.pdf', name: 'esncards_photos.pdf',
displayName: 'Photos PDF', displayName: 'Photos PDF',
state: 'idle', state: 'idle',
url: null, url: null,
@@ -71,11 +72,11 @@
onMount(() => { onMount(() => {
// Add event listener for page unload // Add event listener for page unload
window.addEventListener('beforeunload', handleBeforeUnload); window.addEventListener('beforeunload', handleBeforeUnload);
// Start generation automatically when the component mounts // Start generation automatically when the component mounts
handleGenerate('people_data.pdf'); handleGenerate('esncards_text.pdf');
handleGenerate('people_photos.pdf'); handleGenerate('esncards_photos.pdf');
// Cleanup function when component unmounts // Cleanup function when component unmounts
return () => { return () => {
window.removeEventListener('beforeunload', handleBeforeUnload); window.removeEventListener('beforeunload', handleBeforeUnload);
@@ -97,6 +98,17 @@
} }
} }
// Create a formatted timestamp string
function getTimestamp(): string {
const d = new Date();
const year = d.getFullYear();
const month = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
const hours = d.getHours().toString().padStart(2, '0');
const minutes = d.getMinutes().toString().padStart(2, '0');
return `${year}-${month}-${day}-${hours}-${minutes}`;
}
// Crop image using canvas // Crop image using canvas
async function cropImage( async function cropImage(
imageBlob: Blob, imageBlob: Blob,
@@ -163,7 +175,7 @@
try { try {
const pdfBytes = const pdfBytes =
fileName === 'people_data.pdf' ? await generateTextPDF() : await generatePhotoPDF(); fileName === 'esncards_text.pdf' ? await generateTextPDF() : await generatePhotoPDF();
const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const blob = new Blob([pdfBytes], { type: 'application/pdf' });
@@ -176,6 +188,10 @@
fileToUpdate.size = pdfBytes.length; fileToUpdate.size = pdfBytes.length;
fileToUpdate.state = 'done'; fileToUpdate.state = 'done';
const timestamp = getTimestamp();
const baseName = fileName.replace('.pdf', '');
fileToUpdate.downloadName = `${baseName}_${timestamp}.pdf`;
// Check if both PDFs are done, then clear sensitive data // Check if both PDFs are done, then clear sensitive data
const allDone = files.every((f) => f.state === 'done' || f.state === 'error'); const allDone = files.every((f) => f.state === 'done' || f.state === 'error');
if (allDone) { if (allDone) {
@@ -214,7 +230,7 @@
let currentRow = 0; let currentRow = 0;
let currentCol = 0; let currentCol = 0;
const validRows = $filteredSheetData.filter((row) => row._isValid); const validRows = $sheetData.filter((row) => row._valid);
for (let i = 0; i < validRows.length; i++) { for (let i = 0; i < validRows.length; i++) {
const row = validRows[i]; const row = validRows[i];
@@ -224,11 +240,10 @@
const cellY_mm = PAGE_SETTINGS.margin + currentRow * gridLayout.cellHeight; const cellY_mm = PAGE_SETTINGS.margin + currentRow * gridLayout.cellHeight;
// Get field values // Get field values
const name = row.name || row.Name || ''; const name = row.name;
const surname = row.surname || row.Surname || row.lastname || row.LastName || ''; const surname = row.surname;
const nationality = row.nationality || row.Nationality || row.country || row.Country || ''; const nationality = row.nationality;
const birthday = const birthday = row.birthday;
row.birthday || row.Birthday || row.birthdate || row.Birthdate || row.birth_date || '';
// Draw name // Draw name
const namePos = getAbsolutePositionPt( const namePos = getAbsolutePositionPt(
@@ -319,7 +334,7 @@
let currentRow = 0; let currentRow = 0;
let currentCol = 0; let currentCol = 0;
const validRows = $filteredSheetData.filter((row) => row._isValid); const validRows = $sheetData.filter((row) => row._valid);
for (let i = 0; i < validRows.length; i++) { for (let i = 0; i < validRows.length; i++) {
const row = validRows[i]; const row = validRows[i];
@@ -336,7 +351,7 @@
PHOTO_FIELD_LAYOUT.photo PHOTO_FIELD_LAYOUT.photo
); );
const pictureUrl = row.pictureUrl || row.picture_url || row.Picture || row.PictureUrl; const pictureUrl = row.pictureUrl;
const pictureInfo = $pictures[pictureUrl]; const pictureInfo = $pictures[pictureUrl];
const cropData = $cropRects[pictureUrl]; const cropData = $cropRects[pictureUrl];
@@ -397,8 +412,8 @@
} }
// Draw name // Draw name
const name = row.name || row.Name || ''; const name = row.name;
const surname = row.surname || row.Surname || row.lastname || row.LastName || ''; const surname = row.surname;
const namePos = getAbsolutePositionPt( const namePos = getAbsolutePositionPt(
cellX_mm, cellX_mm,
cellY_mm, cellY_mm,
@@ -430,7 +445,7 @@
if (!file.url) return; if (!file.url) return;
const link = document.createElement('a'); const link = document.createElement('a');
link.href = file.url; link.href = file.url;
link.download = file.name; link.download = file.downloadName || file.name;
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
@@ -443,10 +458,10 @@
} }
}); });
files = JSON.parse(JSON.stringify(initialFiles)); files = JSON.parse(JSON.stringify(initialFiles));
// Clear sensitive data when starting over // Clear sensitive data when starting over
clearSensitiveData(); clearSensitiveData();
currentStep.set(0); currentStep.set(0);
} }
@@ -478,7 +493,7 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div class="text-center"> <div class="text-center">
<div class="text-2xl font-bold text-gray-900"> <div class="text-2xl font-bold text-gray-900">
{$filteredSheetData.filter((row) => row._isValid).length} {$sheetData.filter((row) => row._valid).length}
</div> </div>
<div class="text-gray-600">Records to Process</div> <div class="text-gray-600">Records to Process</div>
</div> </div>
@@ -590,7 +605,7 @@
<!-- Navigation --> <!-- Navigation -->
<div class="flex justify-between"> <div class="flex justify-between">
<button <button
onclick={() => currentStep.set(5)} onclick={() => currentStep.set(6)}
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300"
> >
← Back to Gallery ← Back to Gallery

View File

@@ -1,251 +1,118 @@
<script lang="ts"> <script lang="ts">
import { import {
selectedSheet, sheetData,
columnMapping, columnMapping,
rawSheetData, selectedSheet,
filteredSheetData, currentStep,
currentStep
} from '$lib/stores'; } 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 Navigator from './subcomponents/Navigator.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { getSheetNames, getSheetData } from '$lib/google';
let searchTerm = ''; let isLoading = $state(true);
let sortColumn = ''; let error = $state<string | null>(null);
let sortDirection: 'asc' | 'desc' = 'asc'; let rows = $state<RowData[]>([]);
let selectedRows = new Set<number>();
let selectAll = false;
let processedData = $state<any[]>([]);
let headers: string[] = [];
let isLoading = $state(false);
let showAlreadyPrinted = $state(false);
// Use $state for displayData instead of $derived to avoid TypeScript errors let sortColumn = $state<keyof RowData | null>(null);
let displayData = $state<any[]>([]); let sortDirection = $state<'asc' | 'desc'>('asc');
// 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);
// 1. Filter by search term // Fetch and process data from the Google Sheet
if (searchTerm.trim()) { async function fetchAndProcessData() {
data = data.filter((row) => isLoading = true;
Object.values(row).some((value) => error = null;
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...');
try { try {
if (!$selectedSheet) { const sheet = $selectedSheet;
console.error('No sheet selected'); const mapping = $columnMapping;
if (!sheet || !mapping || !mapping.sheetName) {
error = 'Sheet information or column mapping is missing.';
isLoading = false;
rows = [];
return; 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() { const range = `${mapping.sheetName}!A:Z`;
isLoading = true; const rawData = await getSheetData(sheet.id, range);
try {
// Get headers from the mapping
headers = Object.keys($columnMapping);
await fetchRawSheetData(); if (!rawData || rawData.length < 2) {
// Handle case with no data or only headers
rows = [];
isLoading = false;
return;
}
// Debug logging const dataRows = rawData.slice(1);
console.log('Raw sheet data:', $rawSheetData);
console.log('Column mapping:', $columnMapping);
// Clear existing state before processing new data const processedData = dataRows
selectedRows = new Set(); .map((row, index): RowData | null => {
const name = mapping.name !== -1 ? row[mapping.name] || '' : '';
// Process the data starting from row 2 (skip header row) const surname = mapping.surname !== -1 ? row[mapping.surname] || '' : '';
processedData = $rawSheetData.slice(1).map((row, index) => { const pictureUrl = mapping.pictureUrl !== -1 ? row[mapping.pictureUrl] || '' : '';
const processedRow: any = {
_rowIndex: index + 1, // Store original row index
_isValid: true
};
// Map each column according to the column mapping if (!name && !surname && !pictureUrl) {
for (const [field, columnIndex] of Object.entries($columnMapping)) { return null; // Skip entirely empty rows
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 alreadyPrinted =
const requiredFields = ['name', 'surname', 'nationality', 'birthday', 'pictureUrl']; mapping.alreadyPrinted !== -1
const hasAllRequiredFields = requiredFields.every( ? (row[mapping.alreadyPrinted] || '').toLowerCase() === 'true'
(field) => processedRow[field] && String(processedRow[field]).trim() !== '' : false;
);
if (!hasAllRequiredFields) { const isValid = !!(name && surname && pictureUrl);
processedRow._isValid = false;
}
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 rows = processedData;
const rowsToConsider = processedData.filter((row) => { } catch (e: any) {
if (!row._isValid) return false; error = e.message || 'An unknown error occurred while fetching data.';
const alreadyPrinted = row.alreadyPrinted; console.error(e);
if (alreadyPrinted) { rows = [];
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);
} finally { } finally {
isLoading = false; isLoading = false;
} }
} } // Run on component mount
onMount(() => {
fetchAndProcessData();
});
function toggleRowSelection(rowIndex: number) { // Function to toggle a single row's checked state
if (selectedRows.has(rowIndex)) { function toggleRow(id: string) {
selectedRows.delete(rowIndex); const row = rows.find((r) => r.id === id);
} else { if (row && row._valid) {
selectedRows.add(rowIndex); row._checked = !row._checked;
} }
// Force reactivity with new Set console.log("toggleRow", id, row?._checked);
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);
} }
function toggleSelectAll() { // Function to toggle all valid rows
// Toggle selectAll state first function toggleSelectAll(event: Event) {
selectAll = !selectAll; const target = event.target as HTMLInputElement;
console.log('Toggle select all clicked, new state:', selectAll); const shouldCheck = target.checked;
if (!selectAll) { rows.forEach((row) => {
// If now unchecked, deselect all visible valid rows if (row._valid && !row.alreadyPrinted) {
displayData.forEach((row) => { row._checked = shouldCheck;
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 updateSelectAllState() { // Function to handle sorting
// Find all valid rows that are currently visible function sortBy(column: keyof RowData) {
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) {
if (sortColumn === column) { if (sortColumn === column) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else { } else {
@@ -254,374 +121,251 @@
} }
} }
function getFieldLabel(field: string): string { // Derived state for sorted data to be displayed
const labels: { [key: string]: string } = { const displayData = $derived.by(() => {
name: 'First Name', if (!sortColumn) return rows;
surname: 'Last Name',
nationality: 'Nationality',
birthday: 'Birthday',
pictureUrl: 'Photo URL',
alreadyPrinted: 'Already Printed'
};
return labels[field] || field;
}
function isRowAlreadyPrinted(row: any): boolean { return [...rows].sort((a, b) => {
const alreadyPrinted = row.alreadyPrinted; const aValue = a[sortColumn];
if (!alreadyPrinted) return false; const bValue = b[sortColumn];
const value = String(alreadyPrinted).toLowerCase().trim(); if (aValue === bValue) return 0;
return value === 'true' || value === 'yes' || value === '1' || value === 'x';
} 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() { function handleContinue() {
// Filter the data to only include selected rows $sheetData = rows.filter((row) => row._checked);
const selectedData = processedData.filter(
(row) => selectedRows.has(row._rowIndex) && row._isValid
);
// Store the filtered data
filteredSheetData.set(selectedData);
} }
// 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> </script>
<div class="p-6"> <div class="container max-w-none p-6">
<div class="mb-6"> <div class="mb-4 flex items-center justify-between">
<h2 class="mb-2 text-xl font-semibold text-gray-900">Filter and Select Rows</h2> <div>
<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"> <p class="text-sm text-gray-700">
Review your data and select which rows you want to include in the card generation. Only rows Review your data and select which rows to include. Invalid or already printed rows are
with all required fields will be available for selection. disabled.
</p> </p>
</div> {#if $selectedSheet?.id}
<p class="mt-1 text-sm text-gray-500">
<!-- Search and Filter Controls --> Need to make changes?
<div class="mb-6 rounded-lg border border-gray-200 bg-gray-50 p-4"> <a
<div class="flex flex-col gap-4 sm:flex-row"> href={`https://docs.google.com/spreadsheets/d/${$selectedSheet.id}/edit`}
<!-- Search --> target="_blank"
<div class="flex-grow"> rel="noopener noreferrer"
<label for="search" class="mb-2 block text-sm font-medium text-gray-700"> class="text-blue-600 underline hover:text-blue-800"
Search rows >
</label> Open Google Sheet
<input </a>
id="search" </p>
type="text" {/if}
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> </div>
<button
<!-- Stats --> onclick={fetchAndProcessData}
<div class="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-600"> 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"
<span>Total rows: {processedData.length}</span> disabled={isLoading}
<span>Valid rows: {processedData.filter((row) => row._isValid).length}</span> >
<span class="text-orange-600" {#if isLoading}
>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">
<svg <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" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor"
> >
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path <path
stroke-linecap="round" class="opacity-75"
stroke-linejoin="round" fill="currentColor"
stroke-width="2" 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"
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" ></path>
/>
</svg> </svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No data found</h3> Refreshing...
<p class="mt-1 text-sm text-gray-500"> {:else}
{searchTerm ? 'No rows match your search criteria.' : 'No data available to display.'} Refresh Data
</p> {/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> </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"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<!-- Select All Checkbox --> <th class="px-4 py-3 text-left">
<th class="px-3 py-3 text-left">
<input <input
type="checkbox" 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" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
disabled={isLoading} onchange={toggleSelectAll}
checked={allValidRowsSelected}
/> />
</th> </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 <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> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white"> <tbody class="divide-y divide-gray-200 bg-white">
{#if isLoading} {#each displayData as row (row.id)}
<!-- Loading skeleton rows --> <tr
{#each Array(5) as _, index} class="hover:bg-gray-50"
<tr class="hover:bg-gray-50"> class:bg-gray-100={!row._valid}
<!-- Selection Checkbox Skeleton --> class:text-gray-400={!row._valid || row.alreadyPrinted}
<td class="px-3 py-4"> class:bg-orange-50={row.alreadyPrinted}
<div class="h-4 w-4 animate-pulse rounded bg-gray-200"></div> >
</td> <td class="px-4 py-3">
<input
<!-- Data Columns Skeletons --> type="checkbox"
{#each headers.filter((h) => h !== 'alreadyPrinted') as header} class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-200"
<td class="px-3 py-4"> checked={row._checked}
<div disabled={!row._valid}
class="h-4 animate-pulse rounded bg-gray-200" onchange={() => toggleRow(row.id)}
style="width: {Math.random() * 40 + 60}%" />
></div> </td>
</td> <td class="whitespace-nowrap px-4 py-3 text-sm">{row._rowIndex}</td>
{/each} <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>
<!-- Status Column Skeleton --> <td class="whitespace-nowrap px-4 py-3 text-sm">{row.nationality}</td>
<td class="px-3 py-4"> <td class="whitespace-nowrap px-4 py-3 text-sm">{row.birthday}</td>
<div class="flex flex-col space-y-1"> <td class="whitespace-nowrap px-4 py-3 text-sm">
<div class="h-6 w-16 animate-pulse rounded-full bg-gray-200"></div> <a
</div> href={row.pictureUrl}
</td> target="_blank"
</tr> rel="noopener noreferrer"
{/each} class="text-blue-600 hover:underline"
{:else} title={row.pictureUrl}>link</a
<!-- Actual data rows --> >
{#each displayData as row} </td>
<tr <td class="whitespace-nowrap px-4 py-3 text-sm">
class="hover:bg-gray-50 {!row._isValid ? 'opacity-50' : ''} {isRowAlreadyPrinted( {#if row.alreadyPrinted}
row <span
) class="inline-flex rounded-full bg-orange-100 px-2 text-xs font-semibold leading-5 text-orange-800"
? 'bg-orange-50' >Yes</span
: ''}" >
> {:else}
<!-- Selection Checkbox --> <span
<td class="px-3 py-4"> class="inline-flex rounded-full bg-gray-100 px-2 text-xs font-semibold leading-5 text-gray-800"
{#if row._isValid} >No</span
<input >
type="checkbox" {/if}
checked={selectedRows.has(row._rowIndex)} </td>
onclick={(e) => { <td class="whitespace-nowrap px-4 py-3 text-sm">
// Use event.preventDefault() to avoid default checkbox behavior {#if row._valid}
e.preventDefault(); <span
toggleRowSelection(row._rowIndex); class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800"
}} >Valid</span
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" >
/> {:else}
{:else} <span
<div class="h-4 w-4 rounded bg-gray-200"></div> class="inline-flex rounded-full bg-red-100 px-2 text-xs font-semibold leading-5 text-red-800"
{/if} >Invalid</span
</td> >
{/if}
<!-- Data Columns --> </td>
{#each headers.filter((h) => h !== 'alreadyPrinted') as header} </tr>
<td class="max-w-xs truncate px-3 py-4 text-sm text-gray-900"> {/each}
{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}
</tbody> </tbody>
</table> </table>
</div> </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> </div>
{/if} {/if}
<!-- Navigation --> <div class="mt-6">
<Navigator <Navigator
canProceed={canProceed} canProceed={selectedCount > 0}
currentStep={currentStep} currentStep={currentStep}
textBack="Back to Colum Selection" onForward={handleContinue}
textForwardDisabled="Select rows to continue" textBack="Back to Column Mapping"
textForwardEnabled={`Continue with ${selectedValidCount} ${selectedValidCount === 1 ? 'row' : 'rows'} →`} textForwardEnabled="Continue ({selectedCount} selected)"
onForward={handleContinue} textForwardDisabled="Select at least one valid row"
/> />
</div>
</div> </div>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { availableSheets, selectedSheet, currentStep } from '$lib/stores'; import { selectedSheet, currentStep } from '$lib/stores';
import type { SheetInfoType } from '$lib/stores';
import { searchSheets } from '$lib/google'; import { searchSheets } from '$lib/google';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Navigator from './subcomponents/Navigator.svelte'; import Navigator from './subcomponents/Navigator.svelte';
@@ -25,19 +26,11 @@
try { try {
searchResults = await searchSheets(searchQuery); searchResults = await searchSheets(searchQuery);
availableSheets.set(
searchResults.map((sheet) => ({
spreadsheetId: sheet.spreadsheetId || sheet.id,
name: sheet.name,
url: sheet.webViewLink
}))
);
hasSearched = true; hasSearched = true;
} catch (err) { } catch (err) {
console.error('Error searching sheets:', err); console.error('Error searching sheets:', err);
error = 'Failed to search sheets. Please check your connection and try again.'; error = 'Failed to search sheets. Please check your connection and try again.';
searchResults = []; searchResults = [];
availableSheets.set([]);
} finally { } finally {
isLoading = false; isLoading = false;
} }
@@ -58,15 +51,15 @@
} }
function handleSelectSheet(sheet) { function handleSelectSheet(sheet) {
const sheetData = { const sheetData: SheetInfoType = {
spreadsheetId: sheet.spreadsheetId || sheet.id, id: sheet.id,
name: sheet.name, name: sheet.name,
url: sheet.webViewLink || sheet.url webViewLink: sheet.webViewLink
}; };
selectedSheet.set(sheetData); selectedSheet.set(sheetData);
} }
let canProceed = $derived($selectedSheet !== null); let canProceed = $derived($selectedSheet.id !== '');
</script> </script>
<div class="p-6"> <div class="p-6">
@@ -131,8 +124,8 @@
<div class="space-y-3"> <div class="space-y-3">
{#each searchResults as sheet} {#each searchResults as sheet}
<div <div
class="cursor-pointer rounded-lg border p-4 transition-colors hover:bg-gray-50 {$selectedSheet?.spreadsheetId === class="cursor-pointer rounded-lg border p-4 transition-colors hover:bg-gray-50 {$selectedSheet?.id ===
(sheet.spreadsheetId || sheet.id) (sheet.id || sheet.id)
? 'border-blue-500 bg-blue-50' ? 'border-blue-500 bg-blue-50'
: 'border-gray-200'}" : 'border-gray-200'}"
onclick={() => handleSelectSheet(sheet)} onclick={() => handleSelectSheet(sheet)}
@@ -153,7 +146,7 @@
<img src={sheet.iconLink} alt="Sheet icon" class="my-2 mr-2 h-5 w-5" /> <img src={sheet.iconLink} alt="Sheet icon" class="my-2 mr-2 h-5 w-5" />
{/if} {/if}
{#if $selectedSheet?.spreadsheetId === (sheet.spreadsheetId || sheet.id)} {#if $selectedSheet?.id === (sheet.id || sheet.id)}
<svg class="h-5 w-5 text-blue-600 my-2" fill="currentColor" viewBox="0 0 20 20"> <svg class="h-5 w-5 text-blue-600 my-2" fill="currentColor" viewBox="0 0 20 20">
<path <path
fill-rule="evenodd" fill-rule="evenodd"
@@ -195,8 +188,8 @@
<div class="space-y-3"> <div class="space-y-3">
{#each recentSheets as sheet} {#each recentSheets as sheet}
<div <div
class="cursor-pointer rounded-lg border p-4 transition-colors hover:bg-gray-50 {$selectedSheet?.spreadsheetId === class="cursor-pointer rounded-lg border p-4 transition-colors hover:bg-gray-50 {$selectedSheet?.id ===
(sheet.spreadsheetId || sheet.id) (sheet.id || sheet.id)
? 'border-blue-500 bg-blue-50' ? 'border-blue-500 bg-blue-50'
: 'border-gray-200'}" : 'border-gray-200'}"
onclick={() => handleSelectSheet(sheet)} onclick={() => handleSelectSheet(sheet)}
@@ -217,7 +210,7 @@
<img src={sheet.iconLink} alt="Sheet icon" class="mr-2 h-5 w-5" /> <img src={sheet.iconLink} alt="Sheet icon" class="mr-2 h-5 w-5" />
{/if} {/if}
{#if $selectedSheet?.spreadsheetId === (sheet.spreadsheetId || sheet.id)} {#if $selectedSheet.id === sheet.id}
<svg class="h-5 w-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20"> <svg class="h-5 w-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path <path
fill-rule="evenodd" fill-rule="evenodd"

View File

@@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import { on } from 'svelte/events';
let { let {
canProceed, canProceed,
currentStep, currentStep,

View File

@@ -1,64 +1,98 @@
import { writable, derived } from 'svelte/store'; import { writable, derived } from 'svelte/store';
// This file is holy and shall not be edited by Copilot!
// User session and authentication // User session and authentication
export const session = writable<{ export const session = writable<{
token?: string; token?: string;
user?: { name: string; email: string }; user?: { name: string; email: string };
}>({}); }>({});
// Raw sheet data after import // Data structure column mapping
export const rawSheetData = writable<string[][]>([]); export interface ColumnMappingType {
name: number;
surname: number;
nationality: number;
birthday: number;
pictureUrl: number;
alreadyPrinted: number;
sheetName: string;
}
// Filtered sheet data after row selection // Data structure for a row in the sheet
export const filteredSheetData = writable<any[]>([]);
// Column mapping configuration
export const columnMapping = writable<{
name?: number;
surname?: number;
nationality?: number;
birthday?: number;
pictureUrl?: number;
alreadyPrinted?: number;
}>({});
// Processed row data after mapping and validation
export interface RowData { export interface RowData {
id: string; id: string; // Unique identifier
name: string; name: string;
surname: string; surname: string;
nationality: string; nationality: string;
birthday: string; birthday: string;
pictureUrl: string; pictureUrl: string;
valid: boolean; alreadyPrinted: boolean;
included: boolean; _rowIndex: number;
age?: number; _checked: boolean;
validationErrors: string[]; _valid: boolean;
} }
export const sheetData = writable<RowData[]>([]);
// Picture storage and metadata // Picture storage and metadata
export interface PictureBlobInfo { export interface PictureBlobInfoType {
id: string; id: string;
blob: Blob;
url: string; url: string;
downloaded: boolean; downloaded: boolean;
faceDetected: boolean; faceDetected: boolean;
faceCount: number; faceCount: number;
} }
export const pictures = writable<Record<string, PictureBlobInfo>>({}); // CropType rectangles for each photo
export interface CropType {
// Crop rectangles for each photo
export interface Crop {
x: number; x: number;
y: number; y: number;
width: number; width: number;
height: number; height: number;
} }
export const cropRects = writable<Record<string, Crop>>({}); // Google Sheets list for search
export interface SheetInfoType {
id: string;
name: string;
webViewLink: string;
}
// Card details type
export interface CardDetailsType {
homeSection: string;
validityStart: string;
}
// Column mapping configuration
export const columnMapping = writable<ColumnMappingType>(
{
name: -1,
surname: -1,
nationality: -1,
birthday: -1,
pictureUrl: -1,
alreadyPrinted: -1,
sheetName: ''
});
// Store to hold the processed sheet data
export const sheetData = writable<RowData[]>([]);
// Store and hold the processed picture data
export const pictures = writable<Record<string, PictureBlobInfoType>>({});
// Store and hold the crop rectangles from face detection
export const cropRects = writable<Record<string, CropType>>({});
// Store and hold the selected sheet
export const selectedSheet = writable<SheetInfoType>({ id: '', name: '', webViewLink: '' });
// Card details for generation
export const cardDetails = writable<CardDetailsType | null>(null);
// Wizard state management // Wizard state management
export const currentStep = writable<number>(0); export const currentStep = writable<number>(0);
@@ -94,62 +128,3 @@ export const progress = writable<ProgressState>({
total: 0, total: 0,
message: '' message: ''
}); });
// Google Sheets list for search
export interface SheetInfo {
spreadsheetId: string;
name: string;
url: string;
}
export const availableSheets = writable<SheetInfo[]>([]);
// Selected sheet
export const selectedSheet = writable<SheetInfo | null>(null);
// Validation derived stores
export const validRowCount = derived(
sheetData,
($sheetData) => $sheetData.filter(row => row.valid && row.included).length
);
export const invalidRowCount = derived(
sheetData,
($sheetData) => $sheetData.filter(row => !row.valid).length
);
export const totalRowCount = derived(
sheetData,
($sheetData) => $sheetData.length
);
// Face detection status
export const faceDetectionProgress = writable<{
completed: number;
total: number;
currentImage: string;
}>({
completed: 0,
total: 0,
currentImage: ''
});
// PDF generation status
export const pdfGenerationStatus = writable<{
generating: boolean;
stage: 'preparing' | 'text-pdf' | 'photo-pdf' | 'complete';
progress: number;
}>({
generating: false,
stage: 'preparing',
progress: 0
});
// Card details for generation
export const cardDetails = writable<{
homeSection: string;
validityStart: string;
}>({
homeSection: '',
validityStart: ''
});