- Introduced comprehensive documentation for the Travel Reimbursement Workflow, detailing form components, process definitions, and business rules. - Added new custom scripts for the Manager Approval Form and Travel Reimbursement Form to enhance dynamic behavior and validation. - Updated the ComponentPreview component to include a new prop for field states, improving state management during previews. - Created JSON files for the Manager Approval Form and Travel Reimbursement Form, defining their structure and validation rules. - Implemented a new process definition for the travel workflow, outlining the steps and decision points for claim processing. - Established global variables for managing workflow data, ensuring consistency and accessibility across the process.
295 lines
8.3 KiB
JavaScript
295 lines
8.3 KiB
JavaScript
// Travel Reimbursement Form Custom Script Engine
|
|
// This script provides dynamic form behavior using the FormScriptEngine context
|
|
|
|
// Initialize form on load
|
|
console.log('Travel Reimbursement Form Script Loaded');
|
|
|
|
// Auto-calculate total cost when any cost field changes
|
|
const calculateTotalCost = () => {
|
|
const transportCost = parseFloat(getField('transport_cost')) || 0;
|
|
const accommodationCost = parseFloat(getField('accommodation_cost')) || 0;
|
|
const mealsCost = parseFloat(getField('meals_cost')) || 0;
|
|
const otherCost = parseFloat(getField('other_cost')) || 0;
|
|
|
|
const totalCost = transportCost + accommodationCost + mealsCost + otherCost;
|
|
|
|
// Update the total cost display field
|
|
const totalDisplay = document.querySelector('[data-name="total_cost_display"] .formkit-inner');
|
|
if (totalDisplay) {
|
|
totalDisplay.innerHTML = `<strong>RM ${totalCost.toFixed(2)}</strong>`;
|
|
}
|
|
|
|
// Show budget warnings if total is high
|
|
if (totalCost > 5000) {
|
|
showInfo('High amount claim - may require manager approval');
|
|
}
|
|
|
|
return totalCost;
|
|
};
|
|
|
|
// Validate email format and domain
|
|
const validateEmployeeEmail = (email) => {
|
|
if (!email) return false;
|
|
|
|
// Basic email validation
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(email)) {
|
|
showError('Please enter a valid email address');
|
|
return false;
|
|
}
|
|
|
|
// Company domain validation (example)
|
|
const allowedDomains = ['company.com', 'gmail.com', 'outlook.com'];
|
|
const domain = email.split('@')[1];
|
|
if (!allowedDomains.includes(domain)) {
|
|
showInfo('Please use your company email if available');
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// Validate trip dates
|
|
const validateTripDates = () => {
|
|
const departureDate = getField('departure_date');
|
|
const returnDate = getField('return_date');
|
|
|
|
if (!departureDate || !returnDate) return true; // Skip if dates not set
|
|
|
|
const departure = new Date(departureDate);
|
|
const returnDateObj = new Date(returnDate);
|
|
const today = new Date();
|
|
|
|
// Check if return date is after departure date
|
|
if (returnDateObj <= departure) {
|
|
showError('Return date must be after departure date');
|
|
return false;
|
|
}
|
|
|
|
// Check if trip is too far in the past (more than 3 months)
|
|
const threeMonthsAgo = new Date();
|
|
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
|
|
|
|
if (departure < threeMonthsAgo) {
|
|
showError('Cannot claim for trips older than 3 months');
|
|
return false;
|
|
}
|
|
|
|
// Check if trip is too far in the future (more than 1 year)
|
|
const oneYearFromNow = new Date();
|
|
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);
|
|
|
|
if (departure > oneYearFromNow) {
|
|
showError('Cannot claim for trips more than 1 year in advance');
|
|
return false;
|
|
}
|
|
|
|
// Calculate and show trip duration
|
|
const diffTime = Math.abs(returnDateObj - departure);
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
|
|
|
if (diffDays > 30) {
|
|
showInfo(`Long trip duration: ${diffDays} days - may require additional documentation`);
|
|
} else {
|
|
showSuccess(`Trip duration: ${diffDays} days`);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// Show/hide fields based on travel type
|
|
const handleTravelTypeChange = (travelType) => {
|
|
console.log('Travel type changed to:', travelType);
|
|
|
|
// Show different guidance based on travel type
|
|
switch(travelType) {
|
|
case 'flight':
|
|
showInfo('Flight travel: Keep boarding passes and receipts. Maximum transport limit: RM 2,000');
|
|
break;
|
|
case 'train':
|
|
showInfo('Train travel: Keep tickets and receipts. Maximum transport limit: RM 500');
|
|
break;
|
|
case 'car':
|
|
showInfo('Car travel: Keep fuel receipts and toll receipts. Maximum transport limit: RM 800');
|
|
break;
|
|
case 'bus':
|
|
showInfo('Bus travel: Keep tickets and receipts. Maximum transport limit: RM 300');
|
|
break;
|
|
}
|
|
};
|
|
|
|
// Validate cost fields for reasonable amounts
|
|
const validateCostField = (fieldName, value) => {
|
|
const cost = parseFloat(value) || 0;
|
|
|
|
if (cost < 0) {
|
|
showError(`${fieldName} cannot be negative`);
|
|
return false;
|
|
}
|
|
|
|
// Set reasonable limits per field
|
|
const limits = {
|
|
'transport_cost': 5000,
|
|
'accommodation_cost': 10000,
|
|
'meals_cost': 2000,
|
|
'other_cost': 1000
|
|
};
|
|
|
|
if (cost > limits[fieldName]) {
|
|
showError(`${fieldName.replace('_', ' ')} seems unusually high (RM ${cost}). Please verify the amount.`);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// Form validation before submission
|
|
const validateForm = () => {
|
|
let isValid = true;
|
|
const errors = [];
|
|
|
|
// Check required fields
|
|
const requiredFields = {
|
|
'employee_name': 'Employee Name',
|
|
'employee_email': 'Employee Email',
|
|
'department': 'Department',
|
|
'trip_purpose': 'Trip Purpose',
|
|
'destination': 'Destination',
|
|
'departure_date': 'Departure Date',
|
|
'return_date': 'Return Date',
|
|
'travel_type': 'Travel Type'
|
|
};
|
|
|
|
Object.entries(requiredFields).forEach(([fieldName, displayName]) => {
|
|
const value = getField(fieldName);
|
|
if (!value || value.toString().trim() === '') {
|
|
errors.push(`${displayName} is required`);
|
|
isValid = false;
|
|
}
|
|
});
|
|
|
|
// Validate email
|
|
const email = getField('employee_email');
|
|
if (email && !validateEmployeeEmail(email)) {
|
|
isValid = false;
|
|
}
|
|
|
|
// Validate dates
|
|
if (!validateTripDates()) {
|
|
isValid = false;
|
|
}
|
|
|
|
// Check if at least one cost field has a value > 0
|
|
const totalCost = calculateTotalCost();
|
|
if (totalCost <= 0) {
|
|
errors.push('At least one expense amount must be greater than 0');
|
|
isValid = false;
|
|
}
|
|
|
|
// Check receipts confirmation
|
|
const hasReceipts = getField('has_receipts');
|
|
if (!hasReceipts || hasReceipts.length === 0) {
|
|
errors.push('Please confirm you have all supporting receipts');
|
|
isValid = false;
|
|
}
|
|
|
|
// Show validation results
|
|
if (errors.length > 0) {
|
|
showError(`Please fix the following errors:\n• ${errors.join('\n• ')}`);
|
|
} else {
|
|
showSuccess('Form validation passed! Ready to submit.');
|
|
}
|
|
|
|
return isValid;
|
|
};
|
|
|
|
// Set up field change handlers
|
|
onFieldChange('transport_cost', (newValue) => {
|
|
if (validateCostField('transport_cost', newValue)) {
|
|
calculateTotalCost();
|
|
}
|
|
});
|
|
|
|
onFieldChange('accommodation_cost', (newValue) => {
|
|
if (validateCostField('accommodation_cost', newValue)) {
|
|
calculateTotalCost();
|
|
}
|
|
});
|
|
|
|
onFieldChange('meals_cost', (newValue) => {
|
|
if (validateCostField('meals_cost', newValue)) {
|
|
calculateTotalCost();
|
|
}
|
|
});
|
|
|
|
onFieldChange('other_cost', (newValue) => {
|
|
if (validateCostField('other_cost', newValue)) {
|
|
calculateTotalCost();
|
|
}
|
|
});
|
|
|
|
onFieldChange('employee_email', (newValue) => {
|
|
if (newValue && newValue.trim() !== '') {
|
|
validateEmployeeEmail(newValue);
|
|
}
|
|
});
|
|
|
|
onFieldChange('departure_date', () => {
|
|
setTimeout(validateTripDates, 100); // Small delay to ensure both dates are updated
|
|
});
|
|
|
|
onFieldChange('return_date', () => {
|
|
setTimeout(validateTripDates, 100);
|
|
});
|
|
|
|
onFieldChange('travel_type', (newValue) => {
|
|
if (newValue) {
|
|
handleTravelTypeChange(newValue);
|
|
}
|
|
});
|
|
|
|
// Auto-format employee name (title case)
|
|
onFieldChange('employee_name', (newValue) => {
|
|
if (newValue && typeof newValue === 'string') {
|
|
const formatted = newValue
|
|
.toLowerCase()
|
|
.split(' ')
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ');
|
|
|
|
if (formatted !== newValue) {
|
|
setTimeout(() => {
|
|
setField('employee_name', formatted);
|
|
}, 100);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Department suggestions
|
|
onFieldChange('department', (newValue) => {
|
|
if (newValue && newValue.length >= 2) {
|
|
const commonDepartments = [
|
|
'Finance', 'IT', 'Marketing', 'Sales', 'HR', 'Operations',
|
|
'Engineering', 'Customer Service', 'Legal', 'Procurement'
|
|
];
|
|
|
|
const suggestions = commonDepartments.filter(dept =>
|
|
dept.toLowerCase().includes(newValue.toLowerCase())
|
|
);
|
|
|
|
if (suggestions.length > 0 && !suggestions.includes(newValue)) {
|
|
showInfo(`Department suggestions: ${suggestions.join(', ')}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Initialize total cost display on load
|
|
setTimeout(() => {
|
|
calculateTotalCost();
|
|
}, 1000);
|
|
|
|
// Add form submission validation
|
|
// Note: This would typically be connected to the form's submit event
|
|
// For now, we'll expose it as a global function for manual validation
|
|
window.validateTravelForm = validateForm;
|
|
|
|
console.log('Travel Reimbursement Form Script initialized successfully'); |