corrad-bp/docs/json/form/travel-reimbursement-customScript.js
Md Afiq Iskandar b1fc3d027a Add Travel Reimbursement Workflow Documentation and Enhance Component Functionality
- 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.
2025-07-25 12:02:13 +08:00

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');