<template>

<lightning-card title="Expense Claim Form" icon-name="custom:custom14">

<div class="slds-var-m-around_medium">

<div class="slds-grid slds-wrap">

<div class="slds-col slds-size_1-of-2 slds-var-p-around_x-small">

<lightning-input type="text" label="Naam" value={expense.Naam__c} onchange={handleInputChange} data-field="Naam__c" required></lightning-input>

</div>

<div class="slds-col slds-size_1-of-2 slds-var-p-around_x-small">

<lightning-radio-group

label="Rol"

options={roleOptions}

value={expense.Role__c}

variant="label-hidden"

onchange={handleRoleChange}

data-field="Role__c">

</lightning-radio-group>

</div>

<div class="slds-col slds-size_1-of-2 slds-var-p-around_x-small">

<lightning-input

type="email"

label="E-mail"

value={expense.Email_Volunteer__c}

onchange={handleInputChange}

data-field="Email_Volunteer__c"

required>

</lightning-input>

</div>

<div class="slds-col slds-size_1-of-2 slds-var-p-around_x-small">

<lightning-input

type="text"

label="IBAN"

value={expense.IBAN__c}

onchange={handleInputChange}

data-field="IBAN__c"

required >

</lightning-input>

</div>

<template if:true={isCodeEenheidVisible}>

<div class="slds-col slds-size_1-of-2 slds-var-p-around_x-small">

<lightning-combobox

name="codeEenheid"

label="Code eenheid"

value={expense.Code_eenheid2__c}

options={codeEenheidOptions}

onchange={handleInputChange}

data-field="Code_eenheid2__c"

required>

</lightning-combobox>

</div>

</template>

<div class="slds-col slds-size_1-of-2 slds-var-p-around_x-small">

<lightning-input

type="text"

label="Woonplaats"

value={expense.Woonplaats__c}

onchange={handleInputChange}

data-field="Woonplaats__c"

required></lightning-input>

</div>

</div>


<table class="slds-table slds-table_cell-buffer slds-table_bordered slds-table_striped">

<thead>

<tr class="slds-line-height_reset">

<th scope="col" style="width: 15%;">

<div class="slds-truncate" title=" Selecteer de datum waarop de kosten zijn gemaakt. Je kunt ook zelf de datum intypen zoals 27-11-2024">Datum (i)</div>

</th>

<th scope="col" style="width: 25%;">

<div class="slds-truncate" title="Vul een korte omschrijving in. Bij km vergoeding kan dit bijvoorbeeld zijn: 'van huis naar beheerkantoor x' of ‘treinkaartje begin- en eindstation’.">Omschrijving (i)</div>

</th>

<th scope="col" style="width: 15%;">

<div class="slds-truncate" title="Maak een keuze tussen een kilometer- of kostenvergoeding">Declaratiecategorie (i)</div>

</th>

<th scope="col" style="width: 15%;">

<div class="slds-truncate" title="Bij de keuze voor kilometervergoeding kan er gekozen worden voor type vervoermiddel. Bij Keuze voor kostenvergoeding kan gekozen worden voor openbaar vervoer, veerboot/pont/tunnel en intermediaire kosten (een bedrag voorgeschoten voor NM)">Declaratietype (i)</div>

</th>

<th scope="col" style="width: 10%;">

<div class="slds-truncate" title="Als je reist van de ene naar de andere werklocatie van NM, dan is dit een dienstreis. Vink dan Dienstreis aan">Dienstreis?(i)</div>

</th>

<th scope="col" style="width: 10%;">

<div class="slds-truncate" title="Vul de gereden kilometers heen en terug in, max. 50 km">Kilometers (km)(i)</div>

</th>

<th scope="col" style="width: 10%;">

<div class="slds-truncate" title="Vul het bedrag van de gemaakte kosten in">Bedrag (€)(i)</div>

</th>

<th scope="col" style="width: 5%;">

<div class="slds-truncate" title="Als je de ingevulde regel wilt verwijderen, klik je op de prullebak ">Acties(i)</div>

</th>

</tr>

</thead>

<tbody>

<template for:each={expenseItems} for:item="item" for:index="index">

<tr key={item.id}>

<td style="width: 15%;">

<lightning-input type="date" label="Datum" variant="label-hidden" value={item.expenseDate} onchange={handleItemChange}

data-index={index} data-field="expenseDate" required></lightning-input>

</td>

<td style="width: 25%;">

<lightning-input type="text" label="Omschrijving" variant="label-hidden" value={item.description} onchange={handleItemChange}

data-index={index} data-field="description" required></lightning-input>

</td>

<td style="width: 15%;">

<lightning-combobox name="expenseCategory" label="Categorie" variant="label-hidden" value={item.expenseCategory}

options={expenseCategoryOptions} onchange={handleItemChange}

data-index={index} data-field="expenseCategory"

required></lightning-combobox>

</td>

<td style="width: 15%;">

<lightning-combobox name="expenseType" label="Type" variant="label-hidden" value={item.expenseType}

options={item.expenseTypeOptions} onchange={handleItemChange}

data-index={index} data-field="expenseType"

required></lightning-combobox>

</td>

<td style="width: 10%;">

<lightning-input type="checkbox" label="Dienstreis?" variant="label-hidden" checked={item.isBusinesstrip} onchange={handleItemChange} data-index={index} data-field="isBusinesstrip"></lightning-input>

</td>

<td style="width: 10%;">

<lightning-input type="number" label="Kilometers (km)" variant="label-hidden" value={item.mileage} readonly={item.isReimbursement} onchange={handleItemChange} data-index={index} data-field="mileage"></lightning-input>

</td>

<td style="width: 10%;">

<template if:true={item.isMileage}>

<lightning-formatted-number

value={item.calculatedAmount}

format-style="currency"

currency-code="EUR">

</lightning-formatted-number>

</template>

<template if:true={item.isReimbursement}>

<lightning-input

type="text" label="Bedrag (€)" variant="label-hidden" value={item.amount}

onchange={handleItemChange} onblur={handleBlur} data-index={index} data-field="amount" required></lightning-input>

</template>

</td>

<td style="width: 5%;">

<lightning-button variant="destructive" icon-name="utility:delete" size="small" title="Verwijderen" onclick={removeItem} data-index={index}></lightning-button>

</td>

</tr>

</template>

</tbody>

</table>

<div class="slds-var-m-top_medium slds-var-m-bottom_small">

<lightning-button variant="brand" label="Declaratieregel Toevoegen" title="Declaratieregel Toevoegen" icon-name="utility:add" onclick={addItem}></lightning-button>

</div>


<div class="slds-var-m-top_medium slds-var-m-bottom_small">

<lightning-formatted-number value={totalAmount} format-style="currency" currency-code="EUR"></lightning-formatted-number>

</div>


<lightning-button type="submit" label="Indienen" variant="brand" onclick={handleSubmit} disabled={isSubmitDisabled}></lightning-button>

</div>

</lightning-card>

</template>

import { LightningElement, track, wire } from 'lwc';

import { getPicklistValues, getObjectInfo } from 'lightning/uiObjectInfoApi';

import EXPENSE_ITEM_OBJECT from '@salesforce/schema/ExpenseItem__c';

import EXPENSE_TYPE_KM_FIELD from '@salesforce/schema/ExpenseItem__c.Expense_Type_KM__c';

import CODE_EENHEID_FIELD from '@salesforce/schema/Expense__c.Code_eenheid2__c';

import EXPENSE_OBJECT from '@salesforce/schema/Expense__c';

import createExpenseWithItems from '@salesforce/apex/ExpenseForm.createExpenseWithItems';

import doesEmailExist from '@salesforce/apex/ExpenseForm.doesEmailExist';

import ToastContainer from 'lightning/toastContainer';

import Toast from 'lightning/toast';


export default class ExpenseForm extends LightningElement {

@track expense = { Naam__c: '', Email_Volunteer__c: '', IBAN__c: '', Code_eenheid2__c: '', Woonplaats__c: '', Role__c: 'Vrijwilliger' };

@track expenseItems = [

{ id: Date.now(), expenseDate: '', description: '', expenseCategory: '', expenseType: '', isBusinesstrip: false, mileage: null, amount: null, calculatedAmount: null, isMileage: false, isReimbursement: false, expenseTypeOptions: [] }

];

@track totalAmount = 0;

@track codeEenheidOptions = []; // Store Code_eenheid options here

@track mileageTypeOptions = []; // Store fetched mileage type options

@track isCodeEenheidVisible = true; // Track visibility of codeEenheid field

isLoading = false;

isSubmitDisabled = false;

error;


expenseCategoryOptions = [

{ label: 'Kilometervergoeding', value: 'Mileage' },

{ label: 'Kostenvergoeding', value: 'Reimbursement' }

];


reimbursementTypeOptions = [

{ label: 'Openbaar vervoer', value: 'Openbaar vervoer' },

// { label: 'Overige kosten', value: 'Overige kosten' },

{ label: 'Intermediaire kosten', value: 'Intermediaire kosten' },

{ label: 'Veerboot/Pont/Tunnel', value: 'Veerboot/Pont/Tunnel' }

];


roleOptions = [

{ label: 'Vrijwilliger', value: 'Vrijwilliger' },

{ label: 'LC Lid', value: 'LC Lid' }

];


@wire(getObjectInfo, { objectApiName: EXPENSE_OBJECT })

objectInfo;


@wire(getPicklistValues, {

recordTypeId: '$objectInfo.data.defaultRecordTypeId',

fieldApiName: CODE_EENHEID_FIELD

})

getCodeEenheidPicklistValues({ error, data }) {

if (data) {

this.codeEenheidOptions = data.values.map(value => {

return { label: value.label, value: value.value };

});

this.error = undefined;

} else if (error) {

this.error = 'Error loading Code eenheid picklist values';

console.error('Error fetching Code eenheid picklist values:', error);

}

}


@wire(getObjectInfo, { objectApiName: EXPENSE_ITEM_OBJECT })

expenseItemObjectInfo;


@wire(getPicklistValues, {

recordTypeId: '$expenseItemObjectInfo.data.defaultRecordTypeId',

fieldApiName: EXPENSE_TYPE_KM_FIELD

})

getMileageTypeOptions({ error, data }) {

if (data) {

this.mileageTypeOptions = data.values.map(value => {

return { label: value.label, value: value.value };

});

this.error = undefined;

} else if (error) {

this.error = 'Error loading mileage type picklist values';

console.error('Error fetching mileage type picklist values:', error);

}

}


handleInputChange(event) {

const { field } = event.target.dataset;

let value = event.target.value;

if (field === 'Email_Volunteer__c') {

this.checkEmail(event.target.value);

}

if (field === 'IBAN__c') {

// Remove spaces and convert to uppercase

value = value.replace(/\s+/g, '').toUpperCase();

event.target.value = value; // Reflect changes in the UI

this.validateIBAN(event.target); // Validate IBAN

}

this.expense[field] = value; // Update the expense object

console.log('handleInputChange:', this.expense);

}

validateIBAN(inputField) {

const ibanPattern = /^NL\d{2}[A-Z0-9]{4}[A-Z0-9]{10}$/;

const value = inputField.value;

let errorMessage = '';

// Perform both validations

if (!ibanPattern.test(value) || !this.validateIBANMod97(value)) {

errorMessage = 'Voer een geldig Nederlands IBAN-nummer in.';

}

inputField.setCustomValidity(errorMessage);

inputField.reportValidity();

}

// Modulus 97 check for IBAN validation

validateIBANMod97(iban) {

try {

// Move the first four characters to the end

const rearrangedIban = iban.slice(4) + iban.slice(0, 4);

// Convert letters to numbers (A = 10, B = 11, ..., Z = 35)

const numericIban = rearrangedIban.replace(/[A-Z]/g, (char) => {

return char.charCodeAt(0) - 55;

});

// Perform modulus 97 operation

const remainder = BigInt(numericIban) % 97n;

// If remainder is 1, the IBAN is valid

return remainder === 1n;

} catch (error) {

console.error('IBAN Mod-97 validation failed:', error);

return false; // Return false if there's an issue

}

}

handleRoleChange(event) {

const { field } = event.target.dataset;

this.expense = { ...this.expense, [field]: event.target.value };

// Check if the role is 'LC Lid' and set Code eenheid to '21000 Bestuur en Vereniging 913'

if (this.expense.Role__c === 'LC Lid') {

this.expense.Code_eenheid2__c = '21000 Bestuur en Vereniging 913'; // Automatically set Code eenheid to '21000 Bestuur en Vereniging 913'

this.isCodeEenheidVisible = false; // Optionally hide the Code eenheid field

} else {

this.isCodeEenheidVisible = true; // Show the Code eenheid field for other roles

}

}


handleItemChange(event) {

const { index, field } = event.target.dataset;

const value = this.extractFieldValue(event, field);

// Process field-specific logic

switch (field) {

case 'expenseCategory':

this.updateExpenseCategory(index, value);

break;

case 'expenseType': // When expense type changes

this.updateGenericField(index, field, value);

this.updateCalculatedFields(index); // Recompute after type selection

break;

case 'isBusinesstrip':

this.updateBusinessTrip(index, field, value);

break;

case 'mileage':

this.updateMileage(index, field, value, event.target);

this.updateCalculatedFields(index); // Recompute after type selection

break;

case 'amount':

this.updateAmount(index, field, value, event.target);

break;

default:

this.updateGenericField(index, field, value);

break;

}

// Recompute calculated fields and total amount

this.updateCalculatedFields(index);

this.expenseItems = [...this.expenseItems]; // Trigger reactivity

this.calculateTotalAmount();

}


//Updates the expense category and adjusts related fields.

updateExpenseCategory(index, value) {

const item = this.expenseItems[index];

// Set flags based on the selected category

item.expenseCategory = value; // Assign the selected value

item.isMileage = value === 'Mileage';

item.isReimbursement = value === 'Reimbursement';

// Reset fields and options based on the selected category

if (value === 'Mileage') {

item.expenseTypeOptions = this.mileageTypeOptions;

item.mileage = null; // Ensure mileage starts fresh

item.calculatedAmount = 0; // Reset calculated amount

} else if (value === 'Reimbursement') {

item.expenseTypeOptions = this.reimbursementTypeOptions;

item.mileage = null; // Clear mileage if switching to reimbursement

item.calculatedAmount = null; // Hide calculated amount

} else {

item.expenseTypeOptions = []; // Reset options if the category doesn't match predefined values

}


this.expenseItems = [...this.expenseItems]; // Trigger reactivity


}

//Extracts the field value from the event based on its type.

extractFieldValue(event, field) {

return field === 'isBusinesstrip'

? event.target.checked

: field === 'amount'

? event.target.value.replace(',', '.')

: event.target.value;

}


updateGenericField(index, field, value) {

this.expenseItems[index][field] = value;

}

updateBusinessTrip(index, field, value) {

this.expenseItems[index][field] = value;

}

//Updates the mileage field and validates it.

updateMileage(index, field, value, target) {

const parsedValue = parseFloat(value);

this.expenseItems[index][field] = parsedValue;

if (parsedValue > 50) {

target.setCustomValidity('De maximale afstand per declaratie is 50 kilometer.');

} else {

target.setCustomValidity('');

}

target.reportValidity();

}

handleBlur(event) {

const index = event.target.dataset.index;

const field = event.target.dataset.field;

const rawInput = event.target.value.trim();

try {

// Parse the input to handle commas as decimal separators

const parsedValue = parseFloat(rawInput.replace(',', '.'));

if (isNaN(parsedValue) || parsedValue <= 0) {

// Show validation error if the input is invalid

event.target.setCustomValidity('Voer een geldig bedrag in (bijvoorbeeld 25,99).');

} else {

// Update internal model with the parsed value

this.expenseItems[index][field] = parsedValue;

// Format the value to Dutch style

const formattedValue = new Intl.NumberFormat('nl-NL', {

minimumFractionDigits: 2,

maximumFractionDigits: 2,

}).format(parsedValue);

// Reflect the formatted value in the UI

event.target.value = formattedValue;

event.target.setCustomValidity(''); // Clear previous errors

}

event.target.reportValidity();

} catch (error) {

console.error('Error in handleBlur:', error);

event.target.setCustomValidity('Er is een fout opgetreden.');

event.target.reportValidity();

}

}

updateAmount(index, field, value) {

// Raw input handling during typing (optional, for live validation)

const rawInput = value.trim();

// Handle commas and typing dynamically without formatting

if (!rawInput.endsWith(',')) {

const parsedValue = parseFloat(rawInput.replace(',', '.'));

if (!isNaN(parsedValue)) {

this.expenseItems[index][field] = parsedValue;

}

}

}

//Recomputes calculated fields such as the mileage amount.

updateCalculatedFields(index) {

const item = this.expenseItems[index];

if (item.isMileage && item.expenseType && item.mileage) {

item.calculatedAmount = this.calculateMileageAmount(item);


// Format the calculated amount for Dutch locale

const formattedAmount = this.formatNumberForDutchLocale(item.calculatedAmount);

console.log('Formatted Amount (Dutch):', formattedAmount); // Logs: €1.234,56

} else {

item.calculatedAmount = 0; // Reset if conditions are not met

}

}


formatCurrency(value) {

// Convert the input to a number with 2 decimal places

const number = parseFloat(value).toFixed(2);

if (isNaN(number)) {

return null;

}

return parseFloat(number);

}

formatNumberForDutchLocale(value) {

const formatter = new Intl.NumberFormat('nl-NL', {

style: 'decimal', //Pieter: of 'currency' ?

//currency: 'EUR',

minimumFractionDigits: 2,

maximumFractionDigits: 2

});

return formatter.format(value);

}


calculateMileageAmount(item) {

if (item.expenseCategory === 'Mileage' && item.expenseType) {

const rate = this.getMileageRate(item.expenseType); // Fetch rate

const amount = item.mileage ? item.mileage * rate : 0;

// Format the amount for the Dutch locale

const formatter = new Intl.NumberFormat('nl-NL', {

style: 'currency',

currency: 'EUR',

});

return parseFloat(amount.toFixed(2)); // Store number format internally

}

return 0; // Default to 0

}


getMileageRate(expenseType) {

// Fetch mileage rate based on expense type

const rates = {

'(e-)Fiets of lopen': 0.23,

'Motor': 0.28,

'Auto, benzine': 0.28,

'Auto, diesel': 0.28,

'Auto, (plug-in) hybride': 0.28,

'Auto, 100% elektrisch': 0.28,

'Auto, andere brandstoffen': 0.28,

'Motorfiets, benzine': 0.28,

'Motorfiets, elektrisch': 0.28,

'Bromfiets/scooter, benzine': 0.28,

'Bromfiets/scooter, elektrisch (inclusief speed-pedelec)': 0.28

};

return rates[expenseType] || 0; // Default to 0 if type is undefined

}


addItem() {

this.expenseItems = [

...this.expenseItems,

{ id: Date.now(), expenseDate: '', description: '', expenseCategory: '', expenseType: '', isBusinesstrip: false, mileage: null, amount: null, calculatedAmount: null, isMileage: false, isReimbursement: false, expenseTypeOptions: [] }

];

this.calculateTotalAmount();

}


removeItem(event) {

const index = event.target.dataset.index;

this.expenseItems.splice(index, 1);

this.expenseItems = [...this.expenseItems];

this.calculateTotalAmount();

}


calculateTotalAmount() {

this.totalAmount = this.expenseItems.reduce((total, item) => {

if (item.isMileage) {

return total + (item.calculatedAmount || 0);

}

return total + (item.amount || 0);

}, 0);

}


validateForm() {

const allValid = [...this.template.querySelectorAll('lightning-input, lightning-combobox')]

.reduce((validSoFar, inputCmp) => {

inputCmp.reportValidity();

return validSoFar && inputCmp.checkValidity();

}, true);

// Check if any expense item exceeds 999 euros

const validAmounts = this.expenseItems.every(item => item.amount <= 999);

if (!validAmounts) {

this.showErrorToast('Fout', 'Het maximale bedrag per declaratie is 999 euro.');

return false;

}

return allValid && validAmounts;

}


handleSubmit() {

if (!this.validateForm()) {

this.showErrorToast('Fout', 'Vul alle verplichte velden in.');

return;

}

this.isLoading = true;

const itemsToSubmit = this.expenseItems.map(item => ({

expenseDate: item.expenseDate,

description: item.description,

expenseCategory: item.expenseCategory,

expenseType: item.expenseType,

isBusinesstrip: item.isBusinesstrip,

mileage: item.mileage,

amount: item.isMileage ? this.calculateMileageAmount(item) : item.amount

}));


const payload = { expense: this.expense, items: itemsToSubmit };


console.log('Submitting data to Apex:', JSON.stringify(payload));


createExpenseWithItems({ payload: JSON.stringify(payload) })

.then(() => {

console.log('Expense submitted successfully');

this.showSuccessToast();

this.resetForm();

})

.catch(error => {

console.error('Error submitting expense:', error);

this.showErrorToast('Error', 'Error submitting expense');

})

.finally(() => {

this.isLoading = false;

});

}


checkEmail(email) {

doesEmailExist({ email })

.then(result => {

this.isSubmitDisabled = !result;

})

.catch(error => {

console.error('Error checking email:', error);

// Only log the error without showing a toast

});

}


showSuccessToast() {

Toast.show(

{

label: 'Succes',

message: 'uw declaratie is naar het door u opgegeven e-mailadres verzonden. Open uw e-mail om de declaratie verder af te maken',

mode: 'dismissible',

variant: 'success'

},

this

);

}


showErrorToast(title, message) {

Toast.show(

{

label: title,

message: message,

mode: 'dismissible',

variant: 'error'

},

this

);

}


resetForm() {

this.expense = { Naam__c: '', Email_Volunteer__c: '', IBAN__c: '', Code_eenheid2__c: '', Woonplaats__c: '', Role__c: 'Vrijwilliger' };

this.isCodeEenheidVisible = true; // Reset visibility of codeEenheid field

this.expenseItems = [

{ id: Date.now(), expenseDate: '', description: '', expenseCategory: '', expenseType: '', isBusinesstrip: false, mileage: null, amount: null, calculatedAmount: null, isMileage: false, isReimbursement: false, expenseTypeOptions: [] }

];

this.totalAmount = 0;

}

}