export function tennisLessonsCalculator(form: FormTs) {
form.addRow(row => {
row.addTextPanel('header', {
computedValue: () => 'Tennis Lessons Calculator',
customStyles: { 'font-size': '1.5rem', 'font-weight': '600', 'color': '#1e293b' }
});
});
form.addSpacer({ height: 20 });
// Lesson Type Section
const lessonSection = form.addSubform('lesson', { title: '๐พ Lesson Type' });
lessonSection.addRow(row => {
row.addRadioButton('lessonType', {
label: 'Type of Lesson',
options: [
{ id: 'private', name: 'Private (1-on-1)' },
{ id: 'semi-private', name: 'Semi-Private (2 players)' },
{ id: 'small-group', name: 'Small Group (3-4 players)' },
{ id: 'group', name: 'Group Clinic (5-8 players)' },
{ id: 'camp', name: 'Tennis Camp' },
{ id: 'hitting', name: 'Hitting Session (ball machine)' }
],
defaultValue: 'private',
orientation: 'vertical',
isRequired: true
});
});
lessonSection.addRow(row => {
row.addDropdown('skillLevel', {
label: 'Player Skill Level',
options: [
{ id: 'beginner', name: 'Beginner (Never played)' },
{ id: 'novice', name: 'Novice (Basic strokes)' },
{ id: 'intermediate', name: 'Intermediate (Rally capable)' },
{ id: 'advanced', name: 'Advanced (USTA 3.5-4.0)' },
{ id: 'competitive', name: 'Competitive (USTA 4.0+)' },
{ id: 'junior-competitive', name: 'Junior Tournament Player' }
],
defaultValue: 'beginner',
isRequired: true
}, '1fr');
row.addDropdown('ageGroup', {
label: 'Age Group',
options: [
{ id: 'tots', name: 'Tots (4-6)' },
{ id: 'junior', name: 'Junior (7-12)' },
{ id: 'teen', name: 'Teen (13-17)' },
{ id: 'adult', name: 'Adult (18-54)' },
{ id: 'senior', name: 'Senior (55+)' }
],
defaultValue: 'adult'
}, '1fr');
});
// Session Details Section
const sessionSection = form.addSubform('session', { title: 'โฑ๏ธ Session Details' });
sessionSection.addRow(row => {
row.addDropdown('duration', {
label: 'Lesson Duration',
options: [
{ id: '30', name: '30 Minutes' },
{ id: '45', name: '45 Minutes' },
{ id: '60', name: '1 Hour' },
{ id: '90', name: '1.5 Hours' },
{ id: '120', name: '2 Hours' }
],
defaultValue: '60',
isRequired: true
}, '1fr');
row.addDropdown('coachLevel', {
label: 'Coach Level',
options: [
{ id: 'assistant', name: 'Assistant Pro' },
{ id: 'certified', name: 'USPTA/PTR Certified' },
{ id: 'senior', name: 'Senior Pro' },
{ id: 'director', name: 'Director of Tennis' },
{ id: 'touring', name: 'Former Touring Pro' }
],
defaultValue: 'certified'
}, '1fr');
});
sessionSection.addRow(row => {
row.addDropdown('venue', {
label: 'Venue Type',
options: [
{ id: 'outdoor', name: 'Outdoor Courts' },
{ id: 'indoor', name: 'Indoor Courts' },
{ id: 'club', name: 'Private Club' },
{ id: 'resort', name: 'Resort/Hotel' }
],
defaultValue: 'outdoor'
}, '1fr');
row.addDropdown('courtSurface', {
label: 'Court Surface',
options: [
{ id: 'hard', name: 'Hard Court' },
{ id: 'clay', name: 'Clay Court' },
{ id: 'grass', name: 'Grass Court' },
{ id: 'indoor-hard', name: 'Indoor Hard' },
{ id: 'carpet', name: 'Indoor Carpet' }
],
defaultValue: 'hard'
}, '1fr');
});
sessionSection.addRow(row => {
row.addInteger('players', {
label: 'Number of Players',
min: 2,
max: 8,
defaultValue: 2,
isVisible: () => ['semi-private', 'small-group', 'group'].includes(lessonSection.radioButton('lessonType')?.value() || '')
}, '1fr');
});
// Focus Areas Section
const focusSection = form.addSubform('focus', { title: '๐ฏ Focus Areas' });
focusSection.addRow(row => {
row.addDropdown('primaryFocus', {
label: 'Primary Focus',
options: [
{ id: 'fundamentals', name: 'Fundamentals/Basics' },
{ id: 'groundstrokes', name: 'Groundstrokes' },
{ id: 'serve', name: 'Serve & Return' },
{ id: 'net', name: 'Net Play/Volleys' },
{ id: 'strategy', name: 'Strategy & Tactics' },
{ id: 'footwork', name: 'Footwork & Movement' },
{ id: 'match-play', name: 'Match Play Practice' },
{ id: 'all-around', name: 'All-Around Development' }
],
defaultValue: 'fundamentals'
}, '1fr');
});
// Camp Options (conditional)
const campSection = form.addSubform('camp', {
title: '๐๏ธ Camp Details',
isVisible: () => lessonSection.radioButton('lessonType')?.value() === 'camp'
});
campSection.addRow(row => {
row.addDropdown('campType', {
label: 'Camp Type',
options: [
{ id: 'half-day', name: 'Half Day (3-4 hours)' },
{ id: 'full-day', name: 'Full Day (6-8 hours)' },
{ id: 'intensive', name: 'Intensive Training' },
{ id: 'tournament-prep', name: 'Tournament Prep' }
],
defaultValue: 'half-day'
}, '1fr');
row.addInteger('campDays', {
label: 'Number of Days',
min: 1,
max: 10,
defaultValue: 5
}, '1fr');
});
// Package Section
const packageSection = form.addSubform('package', { title: '๐ฆ Packages' });
packageSection.addRow(row => {
row.addDropdown('package', {
label: 'Lesson Package',
options: [
{ id: 'single', name: 'Single Lesson' },
{ id: '4-pack', name: '4 Lessons (5% off)' },
{ id: '8-pack', name: '8 Lessons (10% off)' },
{ id: '12-pack', name: '12 Lessons (15% off)' },
{ id: 'monthly', name: 'Monthly Program' }
],
defaultValue: 'single'
}, '1fr');
});
// Add-ons Section
const addonsSection = form.addSubform('addons', { title: 'โจ Add-ons' });
addonsSection.addRow(row => {
row.addCheckbox('racketRental', {
label: 'Racket Rental (+$5)',
defaultValue: false
}, '1fr');
row.addCheckbox('ballMachine', {
label: 'Ball Machine Time (+$20)',
defaultValue: false
}, '1fr');
});
addonsSection.addRow(row => {
row.addCheckbox('videoAnalysis', {
label: 'Video Analysis (+$30)',
defaultValue: false
}, '1fr');
row.addCheckbox('courtRental', {
label: 'Court Rental Included (+$25)',
defaultValue: false,
tooltip: 'If not already included'
}, '1fr');
});
addonsSection.addRow(row => {
row.addCheckbox('stringing', {
label: 'Racket Stringing (+$25)',
defaultValue: false
}, '1fr');
row.addCheckbox('fitnessTraining', {
label: 'Tennis Fitness Training (+$40)',
defaultValue: false
}, '1fr');
});
form.addSpacer({ height: 20, showLine: true, lineStyle: 'dashed' });
// Pricing Section
const pricingSection = form.addSubform('pricing', { title: '๐ฐ Pricing', isCollapsible: false });
const calculatePrice = () => {
const lessonType = lessonSection.radioButton('lessonType')?.value() || 'private';
const skillLevel = lessonSection.dropdown('skillLevel')?.value() || 'beginner';
const duration = parseInt(sessionSection.dropdown('duration')?.value() || '60');
const coachLevel = sessionSection.dropdown('coachLevel')?.value() || 'certified';
const venue = sessionSection.dropdown('venue')?.value() || 'outdoor';
const packageType = packageSection.dropdown('package')?.value() || 'single';
const players = sessionSection.integer('players')?.value() || 2;
// Base rates per hour by lesson type
const lessonRates: Record<string, number> = {
'private': 75,
'semi-private': 50,
'small-group': 35,
'group': 25,
'camp': 0,
'hitting': 30
};
let baseRate = lessonRates[lessonType] || 75;
// Coach level multiplier
const coachMult: Record<string, number> = {
'assistant': 0.8,
'certified': 1.0,
'senior': 1.3,
'director': 1.5,
'touring': 2.0
};
baseRate *= coachMult[coachLevel] || 1.0;
// Skill level adjustment (competitive/advanced costs more)
const skillMult: Record<string, number> = {
'beginner': 1.0,
'novice': 1.0,
'intermediate': 1.05,
'advanced': 1.15,
'competitive': 1.25,
'junior-competitive': 1.2
};
baseRate *= skillMult[skillLevel] || 1.0;
// Venue adjustment
const venueMult: Record<string, number> = {
'outdoor': 1.0,
'indoor': 1.25,
'club': 1.2,
'resort': 1.4
};
baseRate *= venueMult[venue] || 1.0;
// Duration calculation
let lessonPrice = baseRate * (duration / 60);
// Camp pricing
if (lessonType === 'camp') {
const campType = campSection.dropdown('campType')?.value() || 'half-day';
const campDays = campSection.integer('campDays')?.value() || 5;
const campRates: Record<string, number> = {
'half-day': 65,
'full-day': 110,
'intensive': 150,
'tournament-prep': 175
};
lessonPrice = (campRates[campType] ?? 0) * campDays;
}
// Add-ons
let addons = 0;
if (addonsSection.checkbox('racketRental')?.value()) addons += 5;
if (addonsSection.checkbox('ballMachine')?.value()) addons += 20;
if (addonsSection.checkbox('videoAnalysis')?.value()) addons += 30;
if (addonsSection.checkbox('courtRental')?.value()) addons += 25;
if (addonsSection.checkbox('stringing')?.value()) addons += 25;
if (addonsSection.checkbox('fitnessTraining')?.value()) addons += 40;
lessonPrice += addons;
// Package discounts
const packageDiscounts: Record<string, number> = {
'single': 0,
'4-pack': 0.05,
'8-pack': 0.10,
'12-pack': 0.15,
'monthly': 0.20
};
const discount = packageDiscounts[packageType] || 0;
const packageQuantities: Record<string, number> = {
'single': 1,
'4-pack': 4,
'8-pack': 8,
'12-pack': 12,
'monthly': 4
};
const sessions = packageQuantities[packageType] || 1;
const discountedPrice = lessonPrice * (1 - discount);
const packageTotal = discountedPrice * sessions;
const monthlyEstimate = discountedPrice * 4;
return {
perLesson: Math.round(lessonPrice),
discountedLesson: Math.round(discountedPrice),
packageTotal: Math.round(packageTotal),
monthlyEstimate: Math.round(monthlyEstimate),
discount: Math.round(discount * 100),
sessions,
addons: Math.round(addons),
duration
};
};
pricingSection.addRow(row => {
row.addPriceDisplay('perLesson', {
label: () => `Per Lesson (${calculatePrice().duration} min)`,
computedValue: () => calculatePrice().perLesson,
variant: 'default'
}, '1fr');
row.addPriceDisplay('discounted', {
label: () => `After ${calculatePrice().discount}% Discount`,
computedValue: () => calculatePrice().discountedLesson,
variant: 'success',
isVisible: () => calculatePrice().discount > 0
}, '1fr');
});
pricingSection.addRow(row => {
row.addPriceDisplay('addons', {
label: 'Add-ons Included',
computedValue: () => calculatePrice().addons,
variant: 'default',
isVisible: () => calculatePrice().addons > 0
});
});
// Summary Section
const summarySection = form.addSubform('summary', {
title: '๐งพ Summary',
isCollapsible: false,
sticky: 'bottom'
});
summarySection.addRow(row => {
row.addPriceDisplay('total', {
label: () => calculatePrice().sessions > 1 ? `Package Total (${calculatePrice().sessions} lessons)` : 'Lesson Total',
computedValue: () => calculatePrice().packageTotal,
variant: 'large'
}, '1fr');
row.addPriceDisplay('monthly', {
label: 'Monthly Estimate (4 lessons)',
computedValue: () => calculatePrice().monthlyEstimate,
variant: 'default',
isVisible: () => calculatePrice().sessions === 1
}, '1fr');
});
summarySection.addRow(row => {
row.addTextPanel('note', {
computedValue: () => 'Court fees may be additional. Equipment not included unless specified.',
customStyles: { 'font-size': '0.85rem', 'color': '#64748b', 'text-align': 'center' }
});
});
form.configureSubmitButton({
label: 'Book Tennis Lesson'
});
}