Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | 12x 12x 502x 12x 2x 10x 10x 10x 50x 10x 1x 9x 30x 15x 15x 15x 1x 14x 15x 15x 12x 12x 12x 12x 684x 684x 351x 12x 9x 9x 290x 9x | /**
* Utility functions for parsing and converting participant CSV data
* for batch import feature
*/
import Papa from 'papaparse';
import type {
SourceParticipant,
BatchRegistrationRequest,
BatchRegistrationItem,
} from '../types/participantImport.types';
/**
* Parse participant CSV file
*
* @param fileContent - Raw CSV file content as string
* @returns Array of parsed participants
* @throws Error if CSV structure is invalid or parsing fails
*/
export function parseParticipantCsv(fileContent: string): SourceParticipant[] {
// Remove BOM character if present (common in UTF-8 files from Excel)
const cleanContent = fileContent.replace(/^\uFEFF/, '');
const { data, errors } = Papa.parse<SourceParticipant>(cleanContent, {
header: true,
skipEmptyLines: true,
transformHeader: (header) => header.trim(),
});
if (errors.length > 0) {
throw new Error(`CSV parsing errors: ${errors.map((e) => e.message).join(', ')}`);
}
// Validate structure (should have at least the required metadata columns)
Eif (data.length > 0) {
const columns = Object.keys(data[0]);
const requiredColumns = ['Name', 'FirstName', 'LastName', 'BestMail', 'companyKey'];
const missingColumns = requiredColumns.filter((col) => !columns.includes(col));
if (missingColumns.length > 0) {
throw new Error(`Missing required columns: ${missingColumns.join(', ')}`);
}
}
return data;
}
/**
* Normalize name for username generation
*
* Handles German characters (ä→ae, ö→oe, ü→ue, ß→ss)
* Removes all non-alphanumeric characters
*
* @param str - Name string to normalize
* @returns Normalized lowercase string
*/
function normalizeName(str: string): string {
return str
.toLowerCase()
.replace(/ä/g, 'ae')
.replace(/ö/g, 'oe')
.replace(/ü/g, 'ue')
.replace(/ß/g, 'ss')
.replace(/[^a-z0-9]/g, '');
}
/**
* Construct username from first name and last name
*
* Format: firstname.lastname (e.g., "john.doe")
* Handles German characters (ä→ae, ö→oe, ü→ue, ß→ss)
*
* @param firstName - Participant's first name
* @param lastName - Participant's last name
* @returns Constructed username
* @throws Error if both firstName and lastName are empty or normalize to empty strings
*/
export function constructUsername(firstName: string, lastName: string): string {
const normalizedFirst = normalizeName(firstName || '');
const normalizedLast = normalizeName(lastName || '');
// Validate: at least one name component must be non-empty
if (!normalizedFirst && !normalizedLast) {
throw new Error('Cannot construct username: both first name and last name are empty');
}
// Use "unknown" as fallback if one name is empty
const userFirst = normalizedFirst || 'unknown';
const userLast = normalizedLast || 'unknown';
return `${userFirst}.${userLast}`;
}
/**
* Generate synthetic email for participants without email
*
* Format: firstname.lastname@batbern.ch
* Handles German characters (ä→ae, ö→oe, ü→ue, ß→ss)
*
* @param firstName - Participant's first name
* @param lastName - Participant's last name
* @returns Synthetic email address
* @throws Error if both firstName and lastName are empty or normalize to empty strings
*/
export function generateSyntheticEmail(firstName: string, lastName: string): string {
const username = constructUsername(firstName, lastName);
return `${username}@batbern.ch`;
}
/**
* Extract event participation from CSV row
*
* Returns array of event codes where participant attended
* Columns 1-57 represent events: "1" = attended, "" = not attended
*
* @param participant - Source participant from CSV
* @returns Array of event codes (e.g., ["BATbern2", "BATbern25"])
*/
export function extractEventParticipation(participant: SourceParticipant): string[] {
const eventCodes: string[] = [];
for (let i = 1; i <= 57; i++) {
const columnValue = participant[i.toString()];
if (columnValue === '1') {
eventCodes.push(`BATbern${i}`);
}
}
return eventCodes;
}
/**
* Convert source participant to batch registration request
*
* Creates API request payload from parsed CSV participant
* Generates synthetic email if BestMail is missing
*
* @param participant - Source participant from CSV
* @returns Batch registration request for API
*/
export function convertParticipantToRegistrationRequest(
participant: SourceParticipant
): BatchRegistrationRequest {
// Use provided email or generate synthetic one
const email =
participant.BestMail?.trim() ||
generateSyntheticEmail(participant.FirstName, participant.LastName);
const eventCodes = extractEventParticipation(participant);
const registrations: BatchRegistrationItem[] = eventCodes.map((eventCode) => ({
eventCode,
status: 'ATTENDED',
}));
return {
participantEmail: email,
firstName: participant.FirstName,
lastName: participant.LastName,
companyId: participant.companyKey?.trim() || undefined, // Story 3.2: Include company from CSV
registrations,
};
}
|