Compare commits
7 Commits
2f730fdbbb
...
6ed1f985e0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ed1f985e0 | ||
|
|
c6cc9c6658 | ||
|
|
7fb72c7d75 | ||
|
|
ebb14e9e1a | ||
|
|
3af8c116a4 | ||
|
|
e9987009c7 | ||
|
|
d8b4eea3ef |
3
.github/copilot-instructions.md
vendored
3
.github/copilot-instructions.md
vendored
@@ -14,4 +14,5 @@
|
||||
- Remain consistent in styling and code structure.
|
||||
- Avoid unncessary iterations. If problems is mostly solved, stop.
|
||||
- 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
7
package-lock.json
generated
@@ -15,7 +15,6 @@
|
||||
"@types/gapi.client.drive-v3": "^0.0.5",
|
||||
"@types/gapi.client.sheets-v4": "^0.0.4",
|
||||
"@types/google.accounts": "^0.0.17",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"fontkit": "^2.0.4",
|
||||
"heic-convert": "^2.1.0",
|
||||
"idb": "^8.0.3",
|
||||
@@ -29,6 +28,7 @@
|
||||
"@sveltejs/kit": "^2.22.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
@@ -631,6 +631,9 @@
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"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"
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
@@ -2019,6 +2022,8 @@
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"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": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@sveltejs/kit": "^2.22.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
@@ -36,7 +37,6 @@
|
||||
"@types/gapi.client.drive-v3": "^0.0.5",
|
||||
"@types/gapi.client.sheets-v4": "^0.0.4",
|
||||
"@types/google.accounts": "^0.0.17",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"fontkit": "^2.0.4",
|
||||
"heic-convert": "^2.1.0",
|
||||
"idb": "^8.0.3",
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
'Select Sheet',
|
||||
'Map Columns',
|
||||
'Filter Rows',
|
||||
'Card Details',
|
||||
'Review Photos',
|
||||
'Generate PDFs'
|
||||
'Enter Card Details',
|
||||
'Preview Gallery',
|
||||
'Generate Cards'
|
||||
];
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { currentStep, cardDetails } from '$lib/stores';
|
||||
import Navigator from './subcomponents/Navigator.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// Initialize from localStorage if available, otherwise from the store
|
||||
let homeSection = $state(
|
||||
(typeof window !== 'undefined' && window.localStorage.getItem('homeSection')) ||
|
||||
$cardDetails.homeSection
|
||||
);
|
||||
let validityStart = $state($cardDetails.validityStart || new Date().toISOString().split('T')[0]);
|
||||
let homeSection = $state('');
|
||||
let validityStart = $state('');
|
||||
|
||||
// Persist homeSection to localStorage whenever it changes
|
||||
$effect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('homeSection', homeSection);
|
||||
onMount(() => {
|
||||
validityStart = new Date().toISOString().split('T')[0];
|
||||
|
||||
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() !== '');
|
||||
|
||||
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>
|
||||
|
||||
@@ -33,7 +41,7 @@
|
||||
|
||||
<div class="space-y-6">
|
||||
<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
|
||||
</label>
|
||||
<input
|
||||
@@ -46,7 +54,7 @@
|
||||
</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
|
||||
</label>
|
||||
<input
|
||||
@@ -55,18 +63,20 @@
|
||||
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"
|
||||
/>
|
||||
<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 class="mt-10">
|
||||
<Navigator
|
||||
canProceed={canProceed}
|
||||
currentStep={currentStep}
|
||||
onForward={handleContinue}
|
||||
textBack="Back to Row Filtering"
|
||||
textForwardEnabled="Continue to Photo Review"
|
||||
textForwardDisabled="Please fill out all fields"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-10">
|
||||
<Navigator
|
||||
{canProceed}
|
||||
{currentStep}
|
||||
onForward={handleContinue}
|
||||
textBack="Back to Row Filtering"
|
||||
textForwardEnabled="Continue to Photo Review"
|
||||
textForwardDisabled="Please fill out all fields"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,10 @@
|
||||
<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 { onMount } from '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 isLoadingData = $state(false);
|
||||
let availableSheets = $state<string[]>([]);
|
||||
@@ -36,14 +16,14 @@
|
||||
let hasSavedMapping = $state(false);
|
||||
let showMappingEditor = $state(false);
|
||||
let savedSheetInfo = $state<SheetInfoType | null>(null);
|
||||
|
||||
let mappedIndices = $state<ColumnMappingType>({
|
||||
name: -1,
|
||||
surname: -1,
|
||||
nationality: -1,
|
||||
birthday: -1,
|
||||
pictureUrl: -1,
|
||||
alreadyPrinted: -1
|
||||
alreadyPrinted: -1,
|
||||
sheetName: ''
|
||||
});
|
||||
|
||||
const requiredFields = [
|
||||
@@ -69,36 +49,31 @@
|
||||
if (recentSheets && recentSheets.length > 0) {
|
||||
// Find a sheet that matches the current spreadsheet
|
||||
const savedSheet = recentSheets.find(
|
||||
(sheet: SheetInfoType) =>
|
||||
sheet.id === $selectedSheet.spreadsheetId ||
|
||||
sheet.spreadsheetId === $selectedSheet.spreadsheetId
|
||||
(sheet: SheetInfoType) => sheet.id === $selectedSheet.id
|
||||
);
|
||||
|
||||
if (savedSheet) {
|
||||
console.log('Found saved sheet configuration:', savedSheet);
|
||||
// We have a saved sheet for this spreadsheet
|
||||
selectedSheetName = savedSheet.sheetName || savedSheet.sheetMapping || '';
|
||||
selectedSheetName = savedSheet.columnMapping.sheetName;
|
||||
savedSheetInfo = savedSheet;
|
||||
|
||||
if (savedSheet.columnMapping) {
|
||||
// Set the mapped indices from saved data
|
||||
mappedIndices = {
|
||||
name: savedSheet.columnMapping.name ?? -1,
|
||||
surname: savedSheet.columnMapping.surname ?? -1,
|
||||
nationality: savedSheet.columnMapping.nationality ?? -1,
|
||||
birthday: savedSheet.columnMapping.birthday ?? -1,
|
||||
pictureUrl: savedSheet.columnMapping.pictureUrl ?? -1,
|
||||
alreadyPrinted: savedSheet.columnMapping.alreadyPrinted ?? -1
|
||||
name: savedSheet.columnMapping.name,
|
||||
surname: savedSheet.columnMapping.surname,
|
||||
nationality: savedSheet.columnMapping.nationality,
|
||||
birthday: savedSheet.columnMapping.birthday,
|
||||
pictureUrl: savedSheet.columnMapping.pictureUrl,
|
||||
alreadyPrinted: savedSheet.columnMapping.alreadyPrinted,
|
||||
sheetName: selectedSheetName
|
||||
};
|
||||
|
||||
hasSavedMapping = true;
|
||||
updateMappingStatus();
|
||||
columnMapping.set(mappedIndices);
|
||||
|
||||
// Don't load sheet data immediately for better performance
|
||||
// We'll load it when needed (when editing or continuing)
|
||||
|
||||
return; // Skip loading available sheets since we're using saved data
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
if (!$selectedSheet) {
|
||||
console.error('Cannot load available sheets: no sheet selected');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Loading available sheets for spreadsheet:', $selectedSheet.spreadsheetId);
|
||||
console.log('Loading available sheets for spreadsheet:', $selectedSheet.id);
|
||||
isLoadingSheets = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const sheetNames = await getSheetNames($selectedSheet.spreadsheetId);
|
||||
const sheetNames = await getSheetNames($selectedSheet.id);
|
||||
console.log('Loaded sheet names:', sheetNames);
|
||||
availableSheets = sheetNames;
|
||||
// Don't auto-select any sheet - let user choose
|
||||
} catch (err) {
|
||||
console.error('Error loading sheet names:', err);
|
||||
error = 'Failed to load sheet names. Please try again.';
|
||||
@@ -193,7 +116,6 @@
|
||||
selectedSheetName = sheetName;
|
||||
|
||||
// Clear any previous data when selecting a new sheet
|
||||
rawSheetData.set([]);
|
||||
sheetHeaders = [];
|
||||
previewData = [];
|
||||
mappedIndices = {
|
||||
@@ -202,7 +124,8 @@
|
||||
nationality: -1,
|
||||
birthday: -1,
|
||||
pictureUrl: -1,
|
||||
alreadyPrinted: -1
|
||||
alreadyPrinted: -1,
|
||||
sheetName: sheetName
|
||||
};
|
||||
mappingComplete = false;
|
||||
hasSavedMapping = false;
|
||||
@@ -220,19 +143,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'Loading sheet data for spreadsheet:',
|
||||
$selectedSheet.spreadsheetId,
|
||||
'sheet:',
|
||||
sheetName
|
||||
);
|
||||
console.log('Loading sheet data for spreadsheet:', $selectedSheet.id, 'sheet:', sheetName);
|
||||
isLoadingData = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
// Fetch first 10 rows for headers and preview
|
||||
const range = `${sheetName}!A1:Z10`;
|
||||
const data = await getSheetData($selectedSheet.spreadsheetId, range);
|
||||
const data = await getSheetData($selectedSheet.id, range);
|
||||
|
||||
if (data && data.length > 0) {
|
||||
console.log('Loaded sheet data with', data.length, 'rows');
|
||||
@@ -265,11 +183,12 @@
|
||||
nationality: -1,
|
||||
birthday: -1,
|
||||
pictureUrl: -1,
|
||||
alreadyPrinted: -1
|
||||
alreadyPrinted: -1,
|
||||
sheetName: selectedSheetName
|
||||
};
|
||||
|
||||
// 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,
|
||||
surname: /last[\s_-]*name|surname|family[\s_-]*name|nachname/i,
|
||||
nationality: /nationality|country|nation/i,
|
||||
@@ -280,8 +199,9 @@
|
||||
|
||||
sheetHeaders.forEach((header, index) => {
|
||||
for (const [field, pattern] of Object.entries(patterns)) {
|
||||
if (pattern.test(header) && mappedIndices[field] === -1) {
|
||||
mappedIndices[field] = index;
|
||||
const key = field as keyof ColumnMappingType;
|
||||
if (pattern.test(header) && mappedIndices[key] === -1) {
|
||||
mappedIndices[key] = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -300,7 +220,11 @@
|
||||
|
||||
// Also check if this column isn't already mapped to another field
|
||||
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) {
|
||||
@@ -327,10 +251,7 @@
|
||||
if (existingData) {
|
||||
const recentSheets = JSON.parse(existingData);
|
||||
const savedSheet = recentSheets.find(
|
||||
(sheet: SheetInfoType) =>
|
||||
(sheet.id === $selectedSheet.spreadsheetId ||
|
||||
sheet.spreadsheetId === $selectedSheet.spreadsheetId) &&
|
||||
(sheet.sheetName === selectedSheetName || sheet.sheetMapping === selectedSheetName)
|
||||
(sheet: SheetInfoType) => sheet.id === $selectedSheet.id
|
||||
);
|
||||
|
||||
if (savedSheet && savedSheet.columnMapping) {
|
||||
@@ -343,7 +264,8 @@
|
||||
nationality: savedSheet.columnMapping.nationality ?? -1,
|
||||
birthday: savedSheet.columnMapping.birthday ?? -1,
|
||||
pictureUrl: savedSheet.columnMapping.pictureUrl ?? -1,
|
||||
alreadyPrinted: savedSheet.columnMapping.alreadyPrinted ?? -1
|
||||
alreadyPrinted: savedSheet.columnMapping.alreadyPrinted ?? -1,
|
||||
sheetName: selectedSheetName
|
||||
};
|
||||
|
||||
hasSavedMapping = true;
|
||||
@@ -359,18 +281,34 @@
|
||||
}
|
||||
|
||||
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;
|
||||
updateMappingStatus();
|
||||
}
|
||||
|
||||
function updateMappingStatus() {
|
||||
if (!mappedIndices) {
|
||||
mappingComplete = false;
|
||||
return;
|
||||
}
|
||||
// Only check required fields for completion
|
||||
const requiredIndices = {
|
||||
name: mappedIndices.name,
|
||||
surname: mappedIndices.surname,
|
||||
nationality: mappedIndices.nationality,
|
||||
birthday: mappedIndices.birthday,
|
||||
pictureUrl: mappedIndices.pictureUrl
|
||||
pictureUrl: mappedIndices.pictureUrl,
|
||||
sheetName: selectedSheetName
|
||||
};
|
||||
|
||||
mappingComplete = Object.values(requiredIndices).every((index) => index !== -1);
|
||||
@@ -383,7 +321,8 @@
|
||||
nationality: mappedIndices.nationality,
|
||||
birthday: mappedIndices.birthday,
|
||||
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
|
||||
const sheetIndex = recentSheets.findIndex(
|
||||
(sheet: SheetInfoType) =>
|
||||
(sheet.id === $selectedSheet.spreadsheetId ||
|
||||
sheet.spreadsheetId === $selectedSheet.spreadsheetId) &&
|
||||
(sheet.sheetName === selectedSheetName || sheet.sheetMapping === selectedSheetName)
|
||||
(sheet: SheetInfoType) => sheet.id === $selectedSheet.id
|
||||
);
|
||||
|
||||
const columnMappingData = {
|
||||
@@ -410,7 +346,8 @@
|
||||
nationality: mappedIndices.nationality,
|
||||
birthday: mappedIndices.birthday,
|
||||
pictureUrl: mappedIndices.pictureUrl,
|
||||
alreadyPrinted: mappedIndices.alreadyPrinted
|
||||
alreadyPrinted: mappedIndices.alreadyPrinted,
|
||||
sheetName: selectedSheetName
|
||||
};
|
||||
|
||||
if (sheetIndex !== -1) {
|
||||
@@ -419,16 +356,12 @@
|
||||
recentSheets[sheetIndex].lastUsed = new Date().toISOString();
|
||||
|
||||
// Ensure we have consistent property names
|
||||
recentSheets[sheetIndex].spreadsheetId =
|
||||
recentSheets[sheetIndex].spreadsheetId || recentSheets[sheetIndex].id;
|
||||
recentSheets[sheetIndex].sheetMapping =
|
||||
recentSheets[sheetIndex].sheetMapping || recentSheets[sheetIndex].sheetName;
|
||||
recentSheets[sheetIndex].id = recentSheets[sheetIndex].id || recentSheets[sheetIndex].id;
|
||||
} else {
|
||||
// Add new entry
|
||||
const newEntry = {
|
||||
spreadsheetId: $selectedSheet.spreadsheetId,
|
||||
id: $selectedSheet.id,
|
||||
name: $selectedSheet.name,
|
||||
sheetMapping: selectedSheetName,
|
||||
columnMapping: columnMappingData,
|
||||
lastUsed: new Date().toISOString()
|
||||
};
|
||||
@@ -461,7 +394,7 @@
|
||||
try {
|
||||
isLoadingData = true;
|
||||
const range = `${selectedSheetName}!A1:Z10`;
|
||||
const data = await getSheetData($selectedSheet.spreadsheetId, range);
|
||||
const data = await getSheetData($selectedSheet.id, range);
|
||||
|
||||
if (data && data.length > 0) {
|
||||
sheetHeaders = data[0];
|
||||
@@ -510,15 +443,17 @@
|
||||
<h3 class="text-sm font-medium text-blue-800">Saved Configuration Found</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<p>
|
||||
Using saved mapping for sheet <span class="font-semibold">"{selectedSheetName}"</span> from
|
||||
spreadsheet <span class="font-semibold">"{savedSheetInfo?.name}"</span>.
|
||||
Using saved mapping for sheet <span class="font-semibold"
|
||||
>"{selectedSheetName}"</span
|
||||
>
|
||||
from spreadsheet <span class="font-semibold">"{savedSheetInfo?.name}"</span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 md:mt-0 md:ml-6">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -780,7 +715,8 @@
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
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 Navigator from './subcomponents/Navigator.svelte';
|
||||
import PhotoCard from './subcomponents/PhotoCard.svelte';
|
||||
@@ -61,8 +61,6 @@
|
||||
}
|
||||
|
||||
async function processPhotosInParallel() {
|
||||
if (isProcessing) return;
|
||||
|
||||
console.log('Starting processPhotos with queues...');
|
||||
isProcessing = true;
|
||||
processedCount = 0;
|
||||
@@ -73,6 +71,7 @@
|
||||
console.log('Cleared IndexedDB.');
|
||||
} catch (e) {
|
||||
console.error('Could not clear IndexedDB:', e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 photoMap = new Map<string, any[]>();
|
||||
|
||||
@@ -130,16 +129,12 @@
|
||||
|
||||
// Initialize detector and process photos
|
||||
onMount(() => {
|
||||
console.log('StepGallery mounted');
|
||||
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');
|
||||
processPhotosInParallel();
|
||||
} else {
|
||||
console.log('No data to process:', {
|
||||
dataLength: $filteredSheetData.length,
|
||||
pictureUrlMapping: $columnMapping.pictureUrl
|
||||
});
|
||||
console.log('No data to process: !');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -148,7 +143,6 @@
|
||||
|
||||
if (!isRetry) {
|
||||
photo.status = 'loading';
|
||||
// No need to reassign photos array with $state reactivity
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -285,14 +279,6 @@
|
||||
imageTensor = tf.browser.fromPixels(img);
|
||||
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) {
|
||||
const getProbability = (p: number | tf.Tensor) =>
|
||||
typeof p === 'number' ? p : p.dataSync()[0];
|
||||
@@ -518,23 +504,6 @@
|
||||
<div class="text-gray-600">Failed</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>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 * as fontkit from 'fontkit';
|
||||
import { clear } from 'idb-keyval';
|
||||
@@ -30,11 +30,12 @@
|
||||
url: string | null;
|
||||
size: number | null;
|
||||
error: string | null;
|
||||
downloadName?: string;
|
||||
};
|
||||
|
||||
const initialFiles: GeneratedFile[] = [
|
||||
{
|
||||
name: 'people_data.pdf',
|
||||
name: 'esncards_text.pdf',
|
||||
displayName: 'Text PDF',
|
||||
state: 'idle',
|
||||
url: null,
|
||||
@@ -42,7 +43,7 @@
|
||||
error: null
|
||||
},
|
||||
{
|
||||
name: 'people_photos.pdf',
|
||||
name: 'esncards_photos.pdf',
|
||||
displayName: 'Photos PDF',
|
||||
state: 'idle',
|
||||
url: null,
|
||||
@@ -71,11 +72,11 @@
|
||||
onMount(() => {
|
||||
// Add event listener for page unload
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
|
||||
// Start generation automatically when the component mounts
|
||||
handleGenerate('people_data.pdf');
|
||||
handleGenerate('people_photos.pdf');
|
||||
|
||||
handleGenerate('esncards_text.pdf');
|
||||
handleGenerate('esncards_photos.pdf');
|
||||
|
||||
// Cleanup function when component unmounts
|
||||
return () => {
|
||||
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
|
||||
async function cropImage(
|
||||
imageBlob: Blob,
|
||||
@@ -163,7 +175,7 @@
|
||||
|
||||
try {
|
||||
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' });
|
||||
|
||||
@@ -176,6 +188,10 @@
|
||||
fileToUpdate.size = pdfBytes.length;
|
||||
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
|
||||
const allDone = files.every((f) => f.state === 'done' || f.state === 'error');
|
||||
if (allDone) {
|
||||
@@ -214,7 +230,7 @@
|
||||
let currentRow = 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++) {
|
||||
const row = validRows[i];
|
||||
@@ -224,11 +240,10 @@
|
||||
const cellY_mm = PAGE_SETTINGS.margin + currentRow * gridLayout.cellHeight;
|
||||
|
||||
// Get field values
|
||||
const name = row.name || row.Name || '';
|
||||
const surname = row.surname || row.Surname || row.lastname || row.LastName || '';
|
||||
const nationality = row.nationality || row.Nationality || row.country || row.Country || '';
|
||||
const birthday =
|
||||
row.birthday || row.Birthday || row.birthdate || row.Birthdate || row.birth_date || '';
|
||||
const name = row.name;
|
||||
const surname = row.surname;
|
||||
const nationality = row.nationality;
|
||||
const birthday = row.birthday;
|
||||
|
||||
// Draw name
|
||||
const namePos = getAbsolutePositionPt(
|
||||
@@ -319,7 +334,7 @@
|
||||
let currentRow = 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++) {
|
||||
const row = validRows[i];
|
||||
@@ -336,7 +351,7 @@
|
||||
PHOTO_FIELD_LAYOUT.photo
|
||||
);
|
||||
|
||||
const pictureUrl = row.pictureUrl || row.picture_url || row.Picture || row.PictureUrl;
|
||||
const pictureUrl = row.pictureUrl;
|
||||
const pictureInfo = $pictures[pictureUrl];
|
||||
const cropData = $cropRects[pictureUrl];
|
||||
|
||||
@@ -397,8 +412,8 @@
|
||||
}
|
||||
|
||||
// Draw name
|
||||
const name = row.name || row.Name || '';
|
||||
const surname = row.surname || row.Surname || row.lastname || row.LastName || '';
|
||||
const name = row.name;
|
||||
const surname = row.surname;
|
||||
const namePos = getAbsolutePositionPt(
|
||||
cellX_mm,
|
||||
cellY_mm,
|
||||
@@ -430,7 +445,7 @@
|
||||
if (!file.url) return;
|
||||
const link = document.createElement('a');
|
||||
link.href = file.url;
|
||||
link.download = file.name;
|
||||
link.download = file.downloadName || file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
@@ -443,10 +458,10 @@
|
||||
}
|
||||
});
|
||||
files = JSON.parse(JSON.stringify(initialFiles));
|
||||
|
||||
|
||||
// Clear sensitive data when starting over
|
||||
clearSensitiveData();
|
||||
|
||||
|
||||
currentStep.set(0);
|
||||
}
|
||||
|
||||
@@ -478,7 +493,7 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900">
|
||||
{$filteredSheetData.filter((row) => row._isValid).length}
|
||||
{$sheetData.filter((row) => row._valid).length}
|
||||
</div>
|
||||
<div class="text-gray-600">Records to Process</div>
|
||||
</div>
|
||||
@@ -590,7 +605,7 @@
|
||||
<!-- Navigation -->
|
||||
<div class="flex justify-between">
|
||||
<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"
|
||||
>
|
||||
← Back to Gallery
|
||||
|
||||
@@ -1,251 +1,118 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
selectedSheet,
|
||||
sheetData,
|
||||
columnMapping,
|
||||
rawSheetData,
|
||||
filteredSheetData,
|
||||
currentStep
|
||||
selectedSheet,
|
||||
currentStep,
|
||||
} from '$lib/stores';
|
||||
import type { RowData } from '$lib/stores';
|
||||
import { getSheetData } from '$lib/google';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import Navigator from './subcomponents/Navigator.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getSheetNames, getSheetData } from '$lib/google';
|
||||
|
||||
let searchTerm = '';
|
||||
let sortColumn = '';
|
||||
let sortDirection: 'asc' | 'desc' = 'asc';
|
||||
let selectedRows = new Set<number>();
|
||||
let selectAll = false;
|
||||
let processedData = $state<any[]>([]);
|
||||
let headers: string[] = [];
|
||||
let isLoading = $state(false);
|
||||
let showAlreadyPrinted = $state(false);
|
||||
let isLoading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let rows = $state<RowData[]>([]);
|
||||
|
||||
// Use $state for displayData instead of $derived to avoid TypeScript errors
|
||||
let displayData = $state<any[]>([]);
|
||||
|
||||
// Update displayData whenever relevant values change
|
||||
$effect(() => {
|
||||
// Debug log at the start
|
||||
console.log('Updating displayData from processedData:', processedData);
|
||||
|
||||
// If processedData is empty, return empty array
|
||||
if (!processedData || !processedData.length) {
|
||||
displayData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Clone array to avoid mutations
|
||||
let data = [...processedData];
|
||||
console.log('Initial data length:', data.length);
|
||||
let sortColumn = $state<keyof RowData | null>(null);
|
||||
let sortDirection = $state<'asc' | 'desc'>('asc');
|
||||
|
||||
// 1. Filter by search term
|
||||
if (searchTerm.trim()) {
|
||||
data = data.filter((row) =>
|
||||
Object.values(row).some((value) =>
|
||||
String(value)
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase())
|
||||
)
|
||||
);
|
||||
console.log('After search term filter:', data.length);
|
||||
}
|
||||
|
||||
// 2. Filter out already printed rows if collapsed
|
||||
if (!showAlreadyPrinted) {
|
||||
data = data.filter((row) => !isRowAlreadyPrinted(row));
|
||||
console.log('After already printed filter:', data.length);
|
||||
}
|
||||
|
||||
// 3. Sort the data
|
||||
if (sortColumn) {
|
||||
data = [...data].sort((a, b) => {
|
||||
const aVal = String(a[sortColumn] || '').toLowerCase();
|
||||
const bVal = String(b[sortColumn] || '').toLowerCase();
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return aVal.localeCompare(bVal);
|
||||
} else {
|
||||
return bVal.localeCompare(aVal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Final filtered data:', data);
|
||||
displayData = data;
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
console.log('StepRowFilter mounted');
|
||||
processSheetData();
|
||||
});
|
||||
|
||||
// Fetch raw sheet data from Google Sheets if not already loaded
|
||||
async function fetchRawSheetData() {
|
||||
console.log('Fetching raw sheet data...');
|
||||
// Fetch and process data from the Google Sheet
|
||||
async function fetchAndProcessData() {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
try {
|
||||
if (!$selectedSheet) {
|
||||
console.error('No sheet selected');
|
||||
const sheet = $selectedSheet;
|
||||
const mapping = $columnMapping;
|
||||
|
||||
if (!sheet || !mapping || !mapping.sheetName) {
|
||||
error = 'Sheet information or column mapping is missing.';
|
||||
isLoading = false;
|
||||
rows = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const sheetNames = await getSheetNames($selectedSheet.spreadsheetId);
|
||||
if (sheetNames.length === 0) return;
|
||||
const sheetName = sheetNames[0];
|
||||
const range = `${sheetName}!A:Z`;
|
||||
const data = await getSheetData($selectedSheet.spreadsheetId, range);
|
||||
console.log('Fetched data:', data);
|
||||
rawSheetData.set(data);
|
||||
} catch (e) {
|
||||
console.error('Error fetching raw sheet data:', e);
|
||||
// Re-throw the error to be caught by the caller
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function processSheetData() {
|
||||
isLoading = true;
|
||||
try {
|
||||
// Get headers from the mapping
|
||||
headers = Object.keys($columnMapping);
|
||||
const range = `${mapping.sheetName}!A:Z`;
|
||||
const rawData = await getSheetData(sheet.id, range);
|
||||
|
||||
await fetchRawSheetData();
|
||||
if (!rawData || rawData.length < 2) {
|
||||
// Handle case with no data or only headers
|
||||
rows = [];
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
console.log('Raw sheet data:', $rawSheetData);
|
||||
console.log('Column mapping:', $columnMapping);
|
||||
const dataRows = rawData.slice(1);
|
||||
|
||||
// Clear existing state before processing new data
|
||||
selectedRows = new Set();
|
||||
|
||||
// Process the data starting from row 2 (skip header row)
|
||||
processedData = $rawSheetData.slice(1).map((row, index) => {
|
||||
const processedRow: any = {
|
||||
_rowIndex: index + 1, // Store original row index
|
||||
_isValid: true
|
||||
};
|
||||
const processedData = dataRows
|
||||
.map((row, index): RowData | null => {
|
||||
const name = mapping.name !== -1 ? row[mapping.name] || '' : '';
|
||||
const surname = mapping.surname !== -1 ? row[mapping.surname] || '' : '';
|
||||
const pictureUrl = mapping.pictureUrl !== -1 ? row[mapping.pictureUrl] || '' : '';
|
||||
|
||||
// Map each column according to the column mapping
|
||||
for (const [field, columnIndex] of Object.entries($columnMapping)) {
|
||||
if (columnIndex !== -1 && columnIndex !== undefined && columnIndex < row.length) {
|
||||
processedRow[field] = row[columnIndex] || '';
|
||||
} else {
|
||||
processedRow[field] = '';
|
||||
// Only mark as invalid if it's a required field
|
||||
if (field !== 'alreadyPrinted') {
|
||||
processedRow._isValid = false;
|
||||
}
|
||||
if (!name && !surname && !pictureUrl) {
|
||||
return null; // Skip entirely empty rows
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all required fields have values (excluding alreadyPrinted)
|
||||
const requiredFields = ['name', 'surname', 'nationality', 'birthday', 'pictureUrl'];
|
||||
const hasAllRequiredFields = requiredFields.every(
|
||||
(field) => processedRow[field] && String(processedRow[field]).trim() !== ''
|
||||
);
|
||||
const alreadyPrinted =
|
||||
mapping.alreadyPrinted !== -1
|
||||
? (row[mapping.alreadyPrinted] || '').toLowerCase() === 'true'
|
||||
: false;
|
||||
|
||||
if (!hasAllRequiredFields) {
|
||||
processedRow._isValid = false;
|
||||
}
|
||||
const isValid = !!(name && surname && pictureUrl);
|
||||
|
||||
return processedRow;
|
||||
});
|
||||
return {
|
||||
id: uuid(),
|
||||
name,
|
||||
surname,
|
||||
nationality: mapping.nationality !== -1 ? row[mapping.nationality] || '' : '',
|
||||
birthday: mapping.birthday !== -1 ? row[mapping.birthday] || '' : '',
|
||||
pictureUrl,
|
||||
alreadyPrinted,
|
||||
_rowIndex: index + 2, // Sheet rows are 1-based, plus header
|
||||
_valid: isValid,
|
||||
_checked: isValid && !alreadyPrinted
|
||||
};
|
||||
})
|
||||
.filter((row): row is RowData => row !== null);
|
||||
|
||||
// Initially select rows based on validity and "Already Printed" status
|
||||
const rowsToConsider = processedData.filter((row) => {
|
||||
if (!row._isValid) return false;
|
||||
const alreadyPrinted = row.alreadyPrinted;
|
||||
if (alreadyPrinted) {
|
||||
const value = String(alreadyPrinted).toLowerCase().trim();
|
||||
return !(value === 'true' || value === 'yes' || value === '1' || value === 'x');
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const initialSelection = rowsToConsider.map((row) => row._rowIndex);
|
||||
// Make sure we create a completely new Set for reactivity
|
||||
selectedRows = new Set([...initialSelection]);
|
||||
|
||||
// Update UI state
|
||||
updateSelectAllState();
|
||||
updateSelectedValidCount();
|
||||
|
||||
// Debug logging
|
||||
console.log('Processed data:', processedData);
|
||||
console.log('Display data:', displayData);
|
||||
console.log('Selected rows:', selectedRows);
|
||||
console.log('Selected valid count after initialization:', selectedValidCount);
|
||||
} catch (e) {
|
||||
console.error('Error processing sheet data:', e);
|
||||
rows = processedData;
|
||||
} catch (e: any) {
|
||||
error = e.message || 'An unknown error occurred while fetching data.';
|
||||
console.error(e);
|
||||
rows = [];
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
} // Run on component mount
|
||||
onMount(() => {
|
||||
fetchAndProcessData();
|
||||
});
|
||||
|
||||
function toggleRowSelection(rowIndex: number) {
|
||||
if (selectedRows.has(rowIndex)) {
|
||||
selectedRows.delete(rowIndex);
|
||||
} else {
|
||||
selectedRows.add(rowIndex);
|
||||
// Function to toggle a single row's checked state
|
||||
function toggleRow(id: string) {
|
||||
const row = rows.find((r) => r.id === id);
|
||||
if (row && row._valid) {
|
||||
row._checked = !row._checked;
|
||||
}
|
||||
// Force reactivity with new Set
|
||||
selectedRows = new Set([...selectedRows]);
|
||||
console.log('Toggled row selection, new selectedRows size:', selectedRows.size);
|
||||
updateSelectAllState();
|
||||
updateSelectedValidCount();
|
||||
console.log('After toggle - canProceed:', canProceed);
|
||||
console.log('After toggle - selectedValidCount > 0:', selectedValidCount > 0);
|
||||
console.log('After toggle - selectedValidCount:', selectedValidCount);
|
||||
console.log("toggleRow", id, row?._checked);
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
// Toggle selectAll state first
|
||||
selectAll = !selectAll;
|
||||
console.log('Toggle select all clicked, new state:', selectAll);
|
||||
|
||||
if (!selectAll) {
|
||||
// If now unchecked, deselect all visible valid rows
|
||||
displayData.forEach((row) => {
|
||||
if (row._isValid) {
|
||||
selectedRows.delete(row._rowIndex);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If now checked, select all visible valid rows
|
||||
displayData.forEach((row) => {
|
||||
if (row._isValid) {
|
||||
selectedRows.add(row._rowIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Force reactivity with new Set
|
||||
selectedRows = new Set([...selectedRows]);
|
||||
console.log('Toggled select all, new selectedRows size:', selectedRows.size);
|
||||
updateSelectedValidCount();
|
||||
console.log('Selected valid count after toggleSelectAll:', selectedValidCount);
|
||||
// Function to toggle all valid rows
|
||||
function toggleSelectAll(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const shouldCheck = target.checked;
|
||||
|
||||
rows.forEach((row) => {
|
||||
if (row._valid && !row.alreadyPrinted) {
|
||||
row._checked = shouldCheck;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateSelectAllState() {
|
||||
// Find all valid rows that are currently visible
|
||||
const visibleValidRows = displayData.filter(row => row._isValid);
|
||||
|
||||
if (visibleValidRows.length === 0) {
|
||||
// No valid rows to select
|
||||
selectAll = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if all visible valid rows are selected
|
||||
const allSelected = visibleValidRows.every(row => selectedRows.has(row._rowIndex));
|
||||
|
||||
// Update selectAll state
|
||||
selectAll = allSelected;
|
||||
console.log('updateSelectAllState: selectAll =', selectAll,
|
||||
', visibleValidRows =', visibleValidRows.length,
|
||||
', selectedRows size =', selectedRows.size);
|
||||
}
|
||||
|
||||
function handleSort(column: string) {
|
||||
// Function to handle sorting
|
||||
function sortBy(column: keyof RowData) {
|
||||
if (sortColumn === column) {
|
||||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
@@ -254,374 +121,251 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getFieldLabel(field: string): string {
|
||||
const labels: { [key: string]: string } = {
|
||||
name: 'First Name',
|
||||
surname: 'Last Name',
|
||||
nationality: 'Nationality',
|
||||
birthday: 'Birthday',
|
||||
pictureUrl: 'Photo URL',
|
||||
alreadyPrinted: 'Already Printed'
|
||||
};
|
||||
return labels[field] || field;
|
||||
}
|
||||
// Derived state for sorted data to be displayed
|
||||
const displayData = $derived.by(() => {
|
||||
if (!sortColumn) return rows;
|
||||
|
||||
function isRowAlreadyPrinted(row: any): boolean {
|
||||
const alreadyPrinted = row.alreadyPrinted;
|
||||
if (!alreadyPrinted) return false;
|
||||
return [...rows].sort((a, b) => {
|
||||
const aValue = a[sortColumn];
|
||||
const bValue = b[sortColumn];
|
||||
|
||||
const value = String(alreadyPrinted).toLowerCase().trim();
|
||||
return value === 'true' || value === 'yes' || value === '1' || value === 'x';
|
||||
}
|
||||
if (aValue === bValue) return 0;
|
||||
|
||||
let comparison = 0;
|
||||
if (aValue > bValue) {
|
||||
comparison = 1;
|
||||
} else {
|
||||
comparison = -1;
|
||||
}
|
||||
|
||||
return sortDirection === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
});
|
||||
|
||||
// Derived state to determine if the "Select All" checkbox should be checked
|
||||
const allValidRowsSelected = $derived.by(() => {
|
||||
const validRows = rows.filter((row) => row._valid && !row.alreadyPrinted);
|
||||
if (validRows.length === 0) return false;
|
||||
return validRows.every((row) => row._checked);
|
||||
});
|
||||
|
||||
const selectedCount = $derived(rows.filter((row) => row._checked).length);
|
||||
|
||||
function handleContinue() {
|
||||
// Filter the data to only include selected rows
|
||||
const selectedData = processedData.filter(
|
||||
(row) => selectedRows.has(row._rowIndex) && row._isValid
|
||||
);
|
||||
|
||||
// Store the filtered data
|
||||
filteredSheetData.set(selectedData);
|
||||
$sheetData = rows.filter((row) => row._checked);
|
||||
}
|
||||
|
||||
// Use $state for selectedValidCount
|
||||
let selectedValidCount = $state(0);
|
||||
|
||||
// Create a dedicated function to recalculate selectedValidCount
|
||||
function updateSelectedValidCount() {
|
||||
// Get array of row indices
|
||||
const rowIndices = Array.from(selectedRows);
|
||||
console.log('Selected row indices:', rowIndices);
|
||||
|
||||
// Count valid rows
|
||||
let count = 0;
|
||||
for (const rowIndex of rowIndices) {
|
||||
// Find the row in processedData
|
||||
const row = processedData.find(r => r._rowIndex === rowIndex);
|
||||
if (row && row._isValid) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Setting selectedValidCount to:', count, 'from', selectedValidCount);
|
||||
selectedValidCount = count;
|
||||
}
|
||||
|
||||
// Update count when selectedRows or processedData changes
|
||||
$effect(() => {
|
||||
// Track dependencies explicitly
|
||||
const rowsSize = selectedRows.size;
|
||||
const dataSize = processedData.length;
|
||||
|
||||
console.log('Effect triggered - selectedRows size:', rowsSize, 'processedData size:', dataSize);
|
||||
updateSelectedValidCount();
|
||||
});
|
||||
|
||||
// Allow proceeding only if at least one valid row is selected
|
||||
let canProceed = $derived(selectedValidCount > 0);
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="mb-6">
|
||||
<h2 class="mb-2 text-xl font-semibold text-gray-900">Filter and Select Rows</h2>
|
||||
|
||||
<p class="mb-4 text-sm text-gray-700">
|
||||
Review your data and select which rows you want to include in the card generation. Only rows
|
||||
with all required fields will be available for selection.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Controls -->
|
||||
<div class="mb-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<div class="flex flex-col gap-4 sm:flex-row">
|
||||
<!-- Search -->
|
||||
<div class="flex-grow">
|
||||
<label for="search" class="mb-2 block text-sm font-medium text-gray-700">
|
||||
Search rows
|
||||
</label>
|
||||
<input
|
||||
id="search"
|
||||
type="text"
|
||||
bind:value={searchTerm}
|
||||
placeholder="Search in any field..."
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Sort -->
|
||||
<div class="sm:w-48">
|
||||
<label for="sort" class="mb-2 block text-sm font-medium text-gray-700"> Sort by </label>
|
||||
<select
|
||||
id="sort"
|
||||
bind:value={sortColumn}
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
>
|
||||
<option value="">No sorting</option>
|
||||
{#each headers as header}
|
||||
<option value={header}>{getFieldLabel(header)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="container max-w-none p-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="mb-2 text-xl font-semibold text-gray-900">Filter and Select Rows</h2>
|
||||
<p class="text-sm text-gray-700">
|
||||
Review your data and select which rows to include. Invalid or already printed rows are
|
||||
disabled.
|
||||
</p>
|
||||
{#if $selectedSheet?.id}
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Need to make changes?
|
||||
<a
|
||||
href={`https://docs.google.com/spreadsheets/d/${$selectedSheet.id}/edit`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-blue-600 underline hover:text-blue-800"
|
||||
>
|
||||
Open Google Sheet
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="mt-4 flex flex-wrap items-center gap-4 text-sm text-gray-600">
|
||||
<span>Total rows: {processedData.length}</span>
|
||||
<span>Valid rows: {processedData.filter((row) => row._isValid).length}</span>
|
||||
<span class="text-orange-600"
|
||||
>Printed: {processedData.filter((row) => isRowAlreadyPrinted(row)).length}</span
|
||||
>
|
||||
<span>Filtered rows: {displayData.length}</span>
|
||||
<span class="font-medium text-blue-600">Selected: {selectedValidCount}</span>
|
||||
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<button
|
||||
class="text-sm text-gray-600 hover:text-gray-900"
|
||||
onclick={() => (showAlreadyPrinted = !showAlreadyPrinted)}
|
||||
>
|
||||
{showAlreadyPrinted ? 'Hide' : 'Show'} Printed ({processedData.filter((row) =>
|
||||
isRowAlreadyPrinted(row)
|
||||
).length})
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={processSheetData}
|
||||
disabled={isLoading}
|
||||
class="inline-flex items-center rounded-md bg-blue-600 px-3 py-1 text-sm font-medium text-white hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-wait disabled:opacity-50"
|
||||
>
|
||||
{#if isLoading}
|
||||
<svg
|
||||
class="mr-2 h-4 w-4 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
||||
/>
|
||||
</svg>
|
||||
Refreshing...
|
||||
{:else}
|
||||
Refresh Data
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="relative mb-6 overflow-hidden rounded-lg border border-gray-200 bg-white">
|
||||
{#if displayData.length === 0 && !isLoading}
|
||||
<div class="py-12 text-center">
|
||||
<button
|
||||
onclick={fetchAndProcessData}
|
||||
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-wait disabled:opacity-50"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{#if isLoading}
|
||||
<svg
|
||||
class="mx-auto h-12 w-12 text-gray-400"
|
||||
class="-ml-1 mr-2 h-5 w-5 animate-spin text-gray-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No data found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
{searchTerm ? 'No rows match your search criteria.' : 'No data available to display.'}
|
||||
</p>
|
||||
Refreshing...
|
||||
{:else}
|
||||
Refresh Data
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="py-12 text-center">
|
||||
<p class="text-lg">Loading data from Google Sheet...</p>
|
||||
<p class="text-gray-500">Please wait a moment.</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div
|
||||
class="rounded-md border border-red-400 bg-red-50 p-4"
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">An Error Occurred</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
</div>
|
||||
{:else if rows.length === 0}
|
||||
<div class="py-12 text-center">
|
||||
<h3 class="text-lg font-medium text-gray-900">No Data Found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
The selected sheet appears to be empty or could not be read.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-hidden rounded-lg border border-gray-200">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<!-- Select All Checkbox -->
|
||||
<th class="px-3 py-3 text-left">
|
||||
<th class="px-4 py-3 text-left">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectAll}
|
||||
onclick={(e) => {
|
||||
// Use event.preventDefault() to avoid default checkbox behavior
|
||||
e.preventDefault();
|
||||
toggleSelectAll();
|
||||
}}
|
||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
disabled={isLoading}
|
||||
onchange={toggleSelectAll}
|
||||
checked={allValidRowsSelected}
|
||||
/>
|
||||
</th>
|
||||
|
||||
<!-- Column Headers -->
|
||||
{#each headers.filter((h) => h !== 'alreadyPrinted') as header}
|
||||
<th
|
||||
class="cursor-pointer px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase hover:bg-gray-100"
|
||||
onclick={() => !isLoading && handleSort(header)}
|
||||
>
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{getFieldLabel(header)}</span>
|
||||
{#if sortColumn === header}
|
||||
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
{#if sortDirection === 'asc'}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
{:else}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
</th>
|
||||
{/each}
|
||||
|
||||
<!-- Status Column -->
|
||||
<th
|
||||
class="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
||||
onclick={() => sortBy('_rowIndex')}>#</th
|
||||
>
|
||||
<th
|
||||
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
||||
onclick={() => sortBy('name')}>Name</th
|
||||
>
|
||||
<th
|
||||
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
||||
onclick={() => sortBy('surname')}>Surname</th
|
||||
>
|
||||
<th
|
||||
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
||||
onclick={() => sortBy('nationality')}>Nationality</th
|
||||
>
|
||||
<th
|
||||
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
||||
onclick={() => sortBy('birthday')}>Birthday</th
|
||||
>
|
||||
<th
|
||||
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
||||
onclick={() => sortBy('pictureUrl')}>Picture URL</th
|
||||
>
|
||||
<th
|
||||
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
||||
onclick={() => sortBy('alreadyPrinted')}>Printed</th
|
||||
>
|
||||
<th
|
||||
class="cursor-pointer px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-600 hover:bg-gray-100"
|
||||
onclick={() => sortBy('_valid')}>Status</th
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#if isLoading}
|
||||
<!-- Loading skeleton rows -->
|
||||
{#each Array(5) as _, index}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<!-- Selection Checkbox Skeleton -->
|
||||
<td class="px-3 py-4">
|
||||
<div class="h-4 w-4 animate-pulse rounded bg-gray-200"></div>
|
||||
</td>
|
||||
|
||||
<!-- Data Columns Skeletons -->
|
||||
{#each headers.filter((h) => h !== 'alreadyPrinted') as header}
|
||||
<td class="px-3 py-4">
|
||||
<div
|
||||
class="h-4 animate-pulse rounded bg-gray-200"
|
||||
style="width: {Math.random() * 40 + 60}%"
|
||||
></div>
|
||||
</td>
|
||||
{/each}
|
||||
|
||||
<!-- Status Column Skeleton -->
|
||||
<td class="px-3 py-4">
|
||||
<div class="flex flex-col space-y-1">
|
||||
<div class="h-6 w-16 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
<!-- Actual data rows -->
|
||||
{#each displayData as row}
|
||||
<tr
|
||||
class="hover:bg-gray-50 {!row._isValid ? 'opacity-50' : ''} {isRowAlreadyPrinted(
|
||||
row
|
||||
)
|
||||
? 'bg-orange-50'
|
||||
: ''}"
|
||||
>
|
||||
<!-- Selection Checkbox -->
|
||||
<td class="px-3 py-4">
|
||||
{#if row._isValid}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRows.has(row._rowIndex)}
|
||||
onclick={(e) => {
|
||||
// Use event.preventDefault() to avoid default checkbox behavior
|
||||
e.preventDefault();
|
||||
toggleRowSelection(row._rowIndex);
|
||||
}}
|
||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{:else}
|
||||
<div class="h-4 w-4 rounded bg-gray-200"></div>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<!-- Data Columns -->
|
||||
{#each headers.filter((h) => h !== 'alreadyPrinted') as header}
|
||||
<td class="max-w-xs truncate px-3 py-4 text-sm text-gray-900">
|
||||
{row[header] || ''}
|
||||
</td>
|
||||
{/each}
|
||||
|
||||
<!-- Status Column -->
|
||||
<td class="px-3 py-4 text-sm">
|
||||
<div class="flex flex-col space-y-1">
|
||||
{#if row._isValid}
|
||||
<span
|
||||
class="inline-flex rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800"
|
||||
>
|
||||
Valid
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
class="inline-flex rounded-full bg-red-100 px-2 py-1 text-xs font-medium text-red-800"
|
||||
>
|
||||
Missing data
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if isRowAlreadyPrinted(row)}
|
||||
<span
|
||||
class="inline-flex rounded-full bg-orange-100 px-2 py-1 text-xs font-medium text-orange-800"
|
||||
>
|
||||
Already Printed
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
{#each displayData as row (row.id)}
|
||||
<tr
|
||||
class="hover:bg-gray-50"
|
||||
class:bg-gray-100={!row._valid}
|
||||
class:text-gray-400={!row._valid || row.alreadyPrinted}
|
||||
class:bg-orange-50={row.alreadyPrinted}
|
||||
>
|
||||
<td class="px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-200"
|
||||
checked={row._checked}
|
||||
disabled={!row._valid}
|
||||
onchange={() => toggleRow(row.id)}
|
||||
/>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm">{row._rowIndex}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm">{row.name}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm">{row.surname}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm">{row.nationality}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm">{row.birthday}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm">
|
||||
<a
|
||||
href={row.pictureUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-blue-600 hover:underline"
|
||||
title={row.pictureUrl}>link</a
|
||||
>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm">
|
||||
{#if row.alreadyPrinted}
|
||||
<span
|
||||
class="inline-flex rounded-full bg-orange-100 px-2 text-xs font-semibold leading-5 text-orange-800"
|
||||
>Yes</span
|
||||
>
|
||||
{:else}
|
||||
<span
|
||||
class="inline-flex rounded-full bg-gray-100 px-2 text-xs font-semibold leading-5 text-gray-800"
|
||||
>No</span
|
||||
>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm">
|
||||
{#if row._valid}
|
||||
<span
|
||||
class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800"
|
||||
>Valid</span
|
||||
>
|
||||
{:else}
|
||||
<span
|
||||
class="inline-flex rounded-full bg-red-100 px-2 text-xs font-semibold leading-5 text-red-800"
|
||||
>Invalid</span
|
||||
>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Selection Summary -->
|
||||
{#if selectedValidCount > 0}
|
||||
<div class="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<div class="flex items-center">
|
||||
<svg class="mr-2 h-5 w-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm text-blue-800">
|
||||
<strong>{selectedValidCount}</strong>
|
||||
{selectedValidCount === 1 ? 'row' : 'rows'} selected for card generation
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Navigation -->
|
||||
<Navigator
|
||||
canProceed={canProceed}
|
||||
currentStep={currentStep}
|
||||
textBack="Back to Colum Selection"
|
||||
textForwardDisabled="Select rows to continue"
|
||||
textForwardEnabled={`Continue with ${selectedValidCount} ${selectedValidCount === 1 ? 'row' : 'rows'} →`}
|
||||
onForward={handleContinue}
|
||||
/>
|
||||
<div class="mt-6">
|
||||
<Navigator
|
||||
canProceed={selectedCount > 0}
|
||||
currentStep={currentStep}
|
||||
onForward={handleContinue}
|
||||
textBack="Back to Column Mapping"
|
||||
textForwardEnabled="Continue ({selectedCount} selected)"
|
||||
textForwardDisabled="Select at least one valid row"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<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 { onMount } from 'svelte';
|
||||
import Navigator from './subcomponents/Navigator.svelte';
|
||||
@@ -25,19 +26,11 @@
|
||||
|
||||
try {
|
||||
searchResults = await searchSheets(searchQuery);
|
||||
availableSheets.set(
|
||||
searchResults.map((sheet) => ({
|
||||
spreadsheetId: sheet.spreadsheetId || sheet.id,
|
||||
name: sheet.name,
|
||||
url: sheet.webViewLink
|
||||
}))
|
||||
);
|
||||
hasSearched = true;
|
||||
} catch (err) {
|
||||
console.error('Error searching sheets:', err);
|
||||
error = 'Failed to search sheets. Please check your connection and try again.';
|
||||
searchResults = [];
|
||||
availableSheets.set([]);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -58,15 +51,15 @@
|
||||
}
|
||||
|
||||
function handleSelectSheet(sheet) {
|
||||
const sheetData = {
|
||||
spreadsheetId: sheet.spreadsheetId || sheet.id,
|
||||
const sheetData: SheetInfoType = {
|
||||
id: sheet.id,
|
||||
name: sheet.name,
|
||||
url: sheet.webViewLink || sheet.url
|
||||
webViewLink: sheet.webViewLink
|
||||
};
|
||||
selectedSheet.set(sheetData);
|
||||
}
|
||||
|
||||
let canProceed = $derived($selectedSheet !== null);
|
||||
let canProceed = $derived($selectedSheet.id !== '');
|
||||
</script>
|
||||
|
||||
<div class="p-6">
|
||||
@@ -131,8 +124,8 @@
|
||||
<div class="space-y-3">
|
||||
{#each searchResults as sheet}
|
||||
<div
|
||||
class="cursor-pointer rounded-lg border p-4 transition-colors hover:bg-gray-50 {$selectedSheet?.spreadsheetId ===
|
||||
(sheet.spreadsheetId || sheet.id)
|
||||
class="cursor-pointer rounded-lg border p-4 transition-colors hover:bg-gray-50 {$selectedSheet?.id ===
|
||||
(sheet.id || sheet.id)
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200'}"
|
||||
onclick={() => handleSelectSheet(sheet)}
|
||||
@@ -153,7 +146,7 @@
|
||||
<img src={sheet.iconLink} alt="Sheet icon" class="my-2 mr-2 h-5 w-5" />
|
||||
{/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">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
@@ -195,8 +188,8 @@
|
||||
<div class="space-y-3">
|
||||
{#each recentSheets as sheet}
|
||||
<div
|
||||
class="cursor-pointer rounded-lg border p-4 transition-colors hover:bg-gray-50 {$selectedSheet?.spreadsheetId ===
|
||||
(sheet.spreadsheetId || sheet.id)
|
||||
class="cursor-pointer rounded-lg border p-4 transition-colors hover:bg-gray-50 {$selectedSheet?.id ===
|
||||
(sheet.id || sheet.id)
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200'}"
|
||||
onclick={() => handleSelectSheet(sheet)}
|
||||
@@ -217,7 +210,7 @@
|
||||
<img src={sheet.iconLink} alt="Sheet icon" class="mr-2 h-5 w-5" />
|
||||
{/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">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { on } from 'svelte/events';
|
||||
|
||||
let {
|
||||
canProceed,
|
||||
currentStep,
|
||||
|
||||
@@ -1,64 +1,98 @@
|
||||
import { writable, derived } from 'svelte/store';
|
||||
|
||||
|
||||
// This file is holy and shall not be edited by Copilot!
|
||||
|
||||
|
||||
// User session and authentication
|
||||
export const session = writable<{
|
||||
token?: string;
|
||||
user?: { name: string; email: string };
|
||||
}>({});
|
||||
|
||||
// Raw sheet data after import
|
||||
export const rawSheetData = writable<string[][]>([]);
|
||||
// Data structure column mapping
|
||||
export interface ColumnMappingType {
|
||||
name: number;
|
||||
surname: number;
|
||||
nationality: number;
|
||||
birthday: number;
|
||||
pictureUrl: number;
|
||||
alreadyPrinted: number;
|
||||
sheetName: string;
|
||||
}
|
||||
|
||||
// Filtered sheet data after row selection
|
||||
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
|
||||
// Data structure for a row in the sheet
|
||||
export interface RowData {
|
||||
id: string;
|
||||
id: string; // Unique identifier
|
||||
name: string;
|
||||
surname: string;
|
||||
nationality: string;
|
||||
birthday: string;
|
||||
pictureUrl: string;
|
||||
valid: boolean;
|
||||
included: boolean;
|
||||
age?: number;
|
||||
validationErrors: string[];
|
||||
alreadyPrinted: boolean;
|
||||
_rowIndex: number;
|
||||
_checked: boolean;
|
||||
_valid: boolean;
|
||||
}
|
||||
|
||||
export const sheetData = writable<RowData[]>([]);
|
||||
|
||||
// Picture storage and metadata
|
||||
export interface PictureBlobInfo {
|
||||
export interface PictureBlobInfoType {
|
||||
id: string;
|
||||
blob: Blob;
|
||||
url: string;
|
||||
downloaded: boolean;
|
||||
faceDetected: boolean;
|
||||
faceCount: number;
|
||||
}
|
||||
|
||||
export const pictures = writable<Record<string, PictureBlobInfo>>({});
|
||||
|
||||
// Crop rectangles for each photo
|
||||
export interface Crop {
|
||||
// CropType rectangles for each photo
|
||||
export interface CropType {
|
||||
x: number;
|
||||
y: number;
|
||||
width: 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
|
||||
export const currentStep = writable<number>(0);
|
||||
@@ -94,62 +128,3 @@ export const progress = writable<ProgressState>({
|
||||
total: 0,
|
||||
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: ''
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user