Created Create Event portal template for portal treatment professionals to be able to add events.
This commit is contained in:
parent
aea07cb5a5
commit
fd43de7443
6 changed files with 904 additions and 26 deletions
|
|
@ -88,6 +88,7 @@ This module provides a complete sports medicine clinic management solution with
|
|||
"views/portal_attachments_template.xml",
|
||||
"views/portal_event_detail_template.xml",
|
||||
"views/portal_event_edit_template.xml",
|
||||
"views/portal_event_create_template.xml",
|
||||
"views/treatment_note_views.xml",
|
||||
"views/res_partner_views.xml",
|
||||
"views/res_users_views.xml",
|
||||
|
|
|
|||
|
|
@ -447,3 +447,170 @@ class EventsPortal(CustomerPortal, AccessControlMixin):
|
|||
# Clean error message to prevent redirect issues with newlines
|
||||
error_msg = str(e).replace('\n', ' ').replace('\r', ' ')
|
||||
return http.request.redirect(f'/my/event/{event_id}/edit?error={error_msg}')
|
||||
|
||||
@http.route(['/my/event/create'], type='http', auth='user', website=True)
|
||||
def create_event_form(self, **kw):
|
||||
"""Display event creation form - only accessible to therapists"""
|
||||
# Access: only therapists (or system) can create
|
||||
user = http.request.env.user
|
||||
is_therapist = user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
if not (is_therapist or user.has_group('base.group_system')):
|
||||
raise AccessError(_("You don't have permission to create events."))
|
||||
|
||||
# Options for form
|
||||
teams = self._get_accessible_teams() if not user.has_group('base.group_system') else http.request.env['sports.team'].search([])
|
||||
treatment_professionals = self._get_treatment_professionals()
|
||||
venues = http.request.env['res.partner'].search([('is_venue', '=', True)])
|
||||
|
||||
values = {
|
||||
'teams': teams,
|
||||
'treatment_professionals': treatment_professionals,
|
||||
'venues': venues,
|
||||
'page_name': 'event_create',
|
||||
}
|
||||
return http.request.render('bemade_sports_clinic.portal_event_create', values)
|
||||
|
||||
@http.route(['/my/event/create/submit'], type='http', auth='user', website=True, methods=['POST'], csrf=False)
|
||||
def create_event_submit(self, **post):
|
||||
"""Handle event creation - only accessible to therapists"""
|
||||
# Access: only therapists (or system) can create
|
||||
user = http.request.env.user
|
||||
is_therapist = user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
if not (is_therapist or user.has_group('base.group_system')):
|
||||
raise AccessError(_("You don't have permission to create events."))
|
||||
|
||||
try:
|
||||
create_vals = {}
|
||||
|
||||
# Required fields
|
||||
if 'name' in post and post['name']:
|
||||
create_vals['name'] = post['name']
|
||||
else:
|
||||
raise UserError(_('Event name is required.'))
|
||||
|
||||
if 'team_id' in post and post['team_id']:
|
||||
create_vals['team_id'] = int(post['team_id'])
|
||||
else:
|
||||
raise UserError(_('Team is required.'))
|
||||
|
||||
# Optional simple fields
|
||||
if 'description' in post:
|
||||
create_vals['description'] = post['description']
|
||||
if 'venue_id' in post and post['venue_id']:
|
||||
create_vals['venue_id'] = int(post['venue_id'])
|
||||
if 'event_type' in post:
|
||||
create_vals['event_type'] = post['event_type']
|
||||
if 'state' in post:
|
||||
create_vals['state'] = post['state']
|
||||
|
||||
# Datetime fields
|
||||
def _parse_dt(val):
|
||||
if not val:
|
||||
return False
|
||||
try:
|
||||
if 'T' in val:
|
||||
return datetime.strptime(val, '%Y-%m-%dT%H:%M')
|
||||
return datetime.strptime(val, '%Y-%m-%d %H:%M')
|
||||
except ValueError:
|
||||
try:
|
||||
if 'T' in val:
|
||||
return datetime.strptime(val, '%Y-%m-%dT%H:%M:%S')
|
||||
return datetime.strptime(val, '%Y-%m-%d %H:%M:%S')
|
||||
except ValueError:
|
||||
return val
|
||||
|
||||
if 'date_start' in post and post['date_start']:
|
||||
create_vals['date_start'] = _parse_dt(post['date_start'])
|
||||
else:
|
||||
raise UserError(_('Event start time is required.'))
|
||||
|
||||
if 'date_end' in post and post['date_end']:
|
||||
create_vals['date_end'] = _parse_dt(post['date_end'])
|
||||
else:
|
||||
raise UserError(_('Event end time is required.'))
|
||||
|
||||
if 'therapist_start' in post and post['therapist_start']:
|
||||
create_vals['therapist_start'] = _parse_dt(post['therapist_start'])
|
||||
if 'therapist_end' in post and post['therapist_end']:
|
||||
create_vals['therapist_end'] = _parse_dt(post['therapist_end'])
|
||||
|
||||
# Assigned staff (same handling as save_event)
|
||||
staff_param = None
|
||||
if 'assigned_staff_ids' in post:
|
||||
staff_param = post['assigned_staff_ids']
|
||||
elif 'assigned_staff_ids[]' in post:
|
||||
staff_param = post['assigned_staff_ids[]']
|
||||
|
||||
staff_ids = []
|
||||
if staff_param is not None:
|
||||
_logger.info(f"Raw assigned_staff_ids from form: {staff_param} (type: {type(staff_param)})")
|
||||
if isinstance(staff_param, list):
|
||||
staff_ids = [int(x) for x in staff_param if x]
|
||||
elif staff_param:
|
||||
if ',' in str(staff_param):
|
||||
staff_ids = [int(x.strip()) for x in str(staff_param).split(',') if x.strip()]
|
||||
else:
|
||||
staff_ids = [int(staff_param)]
|
||||
create_vals['assigned_staff_ids'] = [(6, 0, staff_ids)]
|
||||
|
||||
# Task creation is handled by the model's create() default logic
|
||||
|
||||
event = http.request.env['sports.event'].create(create_vals)
|
||||
|
||||
# Surgical sudo: create management task as superuser to bypass project/analytic ACLs
|
||||
try:
|
||||
event.sudo().create_management_task()
|
||||
except Exception as task_err:
|
||||
_logger.warning(f"Portal task creation failed for event {event.id}: {task_err}")
|
||||
|
||||
return http.request.redirect(f'/my/event/{event.id}?created=1')
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e).replace('\n', ' ').replace('\r', ' ')
|
||||
return http.request.redirect(f"/my/event/create?error={error_msg}")
|
||||
|
||||
@http.route(['/my/venue/create'], type='json', auth='user', website=True, methods=['POST'], csrf=False)
|
||||
def create_venue_ajax(self, **post):
|
||||
"""Create a venue partner record via AJAX for portal users.
|
||||
Uses surgical sudo to avoid ACL/record rule issues, but restricts fields strictly.
|
||||
"""
|
||||
user = http.request.env.user
|
||||
is_therapist = user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||
user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
if not (is_therapist or user.has_group('base.group_system')):
|
||||
return {'success': False, 'error': _("You don't have permission to create venues.")}
|
||||
|
||||
name = (post.get('name') or '').strip()
|
||||
if not name:
|
||||
return {'success': False, 'error': _('Venue name is required.')}
|
||||
|
||||
try:
|
||||
vals = {
|
||||
'name': name,
|
||||
'is_company': True,
|
||||
'is_venue': True,
|
||||
'type': 'other',
|
||||
}
|
||||
# Optional address fields
|
||||
for key in ['street', 'street2', 'city', 'zip']:
|
||||
if post.get(key):
|
||||
vals[key] = post.get(key)
|
||||
|
||||
# Optional country/state by id
|
||||
if post.get('country_id'):
|
||||
try:
|
||||
vals['country_id'] = int(post.get('country_id'))
|
||||
except Exception:
|
||||
pass
|
||||
if post.get('state_id'):
|
||||
try:
|
||||
vals['state_id'] = int(post.get('state_id'))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
venue = http.request.env['res.partner'].sudo().create(vals)
|
||||
return {'success': True, 'id': venue.id, 'name': venue.name}
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ class SportsEvent(models.Model):
|
|||
tracking=True,
|
||||
index=True,
|
||||
groups=_portal_groups,
|
||||
default=fields.Datetime.now,
|
||||
help='When the event starts'
|
||||
)
|
||||
|
||||
|
|
@ -90,7 +91,7 @@ class SportsEvent(models.Model):
|
|||
('in_progress', 'In Progress'),
|
||||
('completed', 'Completed'),
|
||||
('cancelled', 'Cancelled')
|
||||
], string='Status', default='draft', tracking=True, groups=_portal_groups)
|
||||
], string='Status', default='confirmed', tracking=True, groups=_portal_groups)
|
||||
|
||||
# ========================================
|
||||
# RELATIONSHIPS (Portal Accessible)
|
||||
|
|
@ -245,18 +246,27 @@ class SportsEvent(models.Model):
|
|||
# ONCHANGE METHODS
|
||||
# ========================================
|
||||
|
||||
@api.onchange('date_start', 'date_end')
|
||||
def _onchange_event_dates(self):
|
||||
"""Set default therapist times when event dates change"""
|
||||
if self.date_start and self.date_end:
|
||||
# Only set therapist times if they are currently blank
|
||||
if not self.therapist_start:
|
||||
# Set therapist start to 30 minutes before event start
|
||||
from datetime import timedelta
|
||||
self.therapist_start = self.date_start - timedelta(minutes=30)
|
||||
|
||||
if not self.therapist_end:
|
||||
# Set therapist end to event end time
|
||||
@api.onchange('date_start')
|
||||
def _onchange_date_start(self):
|
||||
"""When event start changes:
|
||||
- therapist_start = 30 minutes prior
|
||||
- date_end = 2 hours after
|
||||
- therapist_end = date_end
|
||||
"""
|
||||
if self.date_start:
|
||||
from datetime import timedelta
|
||||
self.therapist_start = self.date_start - timedelta(minutes=30)
|
||||
self.date_end = self.date_start + timedelta(hours=2)
|
||||
self.therapist_end = self.date_end
|
||||
|
||||
@api.onchange('date_end')
|
||||
def _onchange_date_end(self):
|
||||
"""When event end changes:
|
||||
- therapist_end = max(therapist_end, date_end)
|
||||
"""
|
||||
if self.date_end:
|
||||
# If therapist_end not set or earlier than date_end, align to date_end
|
||||
if not self.therapist_end or self.therapist_end < self.date_end:
|
||||
self.therapist_end = self.date_end
|
||||
|
||||
# ========================================
|
||||
|
|
@ -365,9 +375,13 @@ class SportsEvent(models.Model):
|
|||
events = super().create(vals_list)
|
||||
|
||||
# Auto-create management tasks for events that need them
|
||||
for event, vals in zip(events, vals_list):
|
||||
if vals.get('auto_create_task', True):
|
||||
event.create_management_task()
|
||||
# IMPORTANT: Only internal users (base.group_user) may auto-create tasks here.
|
||||
# Portal users will have tasks created via a controller-side sudo() after access checks.
|
||||
is_internal = self.env.user.has_group('base.group_user')
|
||||
if is_internal:
|
||||
for event, vals in zip(events, vals_list):
|
||||
if vals.get('auto_create_task', True):
|
||||
event.create_management_task()
|
||||
|
||||
return events
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
</div>
|
||||
<!-- View Type Navigation -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="col-md-6">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<a t-attf-class="nav-link #{'active' if view_type == 'all' else ''}"
|
||||
|
|
@ -48,9 +48,14 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<a href="/my/events?view_type=all&no_default_dates=1" class="btn btn-outline-secondary btn-sm">
|
||||
<a href="/my/events?view_type=all&no_default_dates=1" class="btn btn-outline-secondary btn-sm me-2">
|
||||
<i class="fa fa-times"/> Clear Filters
|
||||
</a>
|
||||
<t t-if="can_edit">
|
||||
<a href="/my/event/create" class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-plus"/> Add Event
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
495
bemade_sports_clinic/views/portal_event_create_template.xml
Normal file
495
bemade_sports_clinic/views/portal_event_create_template.xml
Normal file
|
|
@ -0,0 +1,495 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Event Create Template -->
|
||||
<template id="portal_event_create" name="Portal Event Create">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="breadcrumbs_searchbar" t-value="True"/>
|
||||
|
||||
<div class="container-fluid">
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/my">Home</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/my/events">Events</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Create</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2>Create Event</h2>
|
||||
<p class="text-muted mb-0">
|
||||
<i class="fa fa-plus"/> New sports event
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/my/events" class="btn btn-outline-secondary">
|
||||
<i class="fa fa-arrow-left"/> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Messages -->
|
||||
<t t-if="request.params.get('error')">
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fa fa-exclamation-circle"/> Error: <span t-esc="request.params.get('error')"/>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Create Form -->
|
||||
<form action="/my/event/create/submit" method="post" class="needs-validation" novalidate="true">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa fa-info-circle"/> Event Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- Event Name -->
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Event Name *</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required="true"/>
|
||||
<div class="invalid-feedback">
|
||||
Please provide a valid event name.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Type -->
|
||||
<div class="mb-3">
|
||||
<label for="event_type" class="form-label">Event Type</label>
|
||||
<select class="form-select" id="event_type" name="event_type">
|
||||
<option value="game">Game</option>
|
||||
<option value="practice">Practice</option>
|
||||
<option value="training">Training</option>
|
||||
<option value="meeting">Team Meeting</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="mb-3">
|
||||
<label for="state" class="form-label">Status</label>
|
||||
<select class="form-select" id="state" name="state">
|
||||
<option value="draft">Draft</option>
|
||||
<option value="confirmed" selected="selected">Confirmed</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<!-- Team -->
|
||||
<div class="mb-3">
|
||||
<label for="team_id" class="form-label">Team *</label>
|
||||
<select class="form-select" id="team_id" name="team_id" required="true">
|
||||
<option value="">Select a team...</option>
|
||||
<t t-foreach="teams" t-as="team">
|
||||
<option t-att-value="team.id">
|
||||
<t t-esc="team.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<div class="invalid-feedback">
|
||||
Please select a team.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Venue -->
|
||||
<div class="mb-3">
|
||||
<label for="venue_id" class="form-label">Venue</label>
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<select class="form-select" id="venue_id" name="venue_id">
|
||||
<option value="">Select a venue...</option>
|
||||
<t t-foreach="venues" t-as="venue">
|
||||
<option t-att-value="venue.id">
|
||||
<t t-esc="venue.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addVenueModal">
|
||||
<i class="fa fa-plus"/> Add Venue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-3">
|
||||
<label for="description_editor" class="form-label">Description</label>
|
||||
<!-- Hidden field actually submitted -->
|
||||
<input type="hidden" id="description" name="description" value=""/>
|
||||
<!-- Simple rich-text editor -->
|
||||
<div id="description_editor"
|
||||
class="form-control"
|
||||
style="min-height: 150px; overflow:auto;"
|
||||
contenteditable="true">
|
||||
</div>
|
||||
<small class="form-text text-muted">You can paste formatted text. The formatted content will be saved.</small>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
const form = document.currentScript && document.currentScript.closest('form');
|
||||
if (!form) return;
|
||||
const editor = form.querySelector('#description_editor');
|
||||
const hidden = form.querySelector('#description');
|
||||
if (!editor || !hidden) return;
|
||||
form.addEventListener('submit', function() {
|
||||
hidden.value = editor.innerHTML;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timing Information -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa fa-clock-o"/> Timing Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- Event Start -->
|
||||
<div class="mb-3">
|
||||
<label for="date_start" class="form-label">Event Start *</label>
|
||||
<input type="datetime-local" class="form-control" id="date_start" name="date_start" required="true"/>
|
||||
<div class="invalid-feedback">
|
||||
Please provide an event start time.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Therapist Start -->
|
||||
<div class="mb-3">
|
||||
<label for="therapist_start" class="form-label">Therapist Start</label>
|
||||
<input type="datetime-local" class="form-control" id="therapist_start" name="therapist_start"/>
|
||||
<small class="form-text text-muted">Leave empty to use event start time</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<!-- Event End -->
|
||||
<div class="mb-3">
|
||||
<label for="date_end" class="form-label">Event End *</label>
|
||||
<input type="datetime-local" class="form-control" id="date_end" name="date_end" required="true"/>
|
||||
<div class="invalid-feedback">
|
||||
Please provide an event end time.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Therapist End -->
|
||||
<div class="mb-3">
|
||||
<label for="therapist_end" class="form-label">Therapist End</label>
|
||||
<input type="datetime-local" class="form-control" id="therapist_end" name="therapist_end"/>
|
||||
<small class="form-text text-muted">Leave empty to use event end time</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Assigned Staff -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fa fa-user-md"/> Assigned Staff
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Treatment Professionals</label>
|
||||
<t t-foreach="treatment_professionals" t-as="professional">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
t-att-id="'staff_%s' % professional.id"
|
||||
name="assigned_staff_ids[]"
|
||||
t-att-value="professional.id"/>
|
||||
<label class="form-check-label" t-att-for="'staff_%s' % professional.id">
|
||||
<span t-esc="professional.name"/>
|
||||
<small class="text-muted d-block" t-esc="professional.email"/>
|
||||
</label>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-end">
|
||||
<a href="/my/events" class="btn btn-outline-secondary me-2">
|
||||
<i class="fa fa-times"/> Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-save"/> Create Event
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Add Venue Modal -->
|
||||
<div class="modal fade" id="addVenueModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add Venue</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name *</label>
|
||||
<input type="text" class="form-control" id="venue_name" required="required"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Street</label>
|
||||
<input type="text" class="form-control" id="venue_street"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Street 2</label>
|
||||
<input type="text" class="form-control" id="venue_street2"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">City</label>
|
||||
<input type="text" class="form-control" id="venue_city"/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">ZIP</label>
|
||||
<input type="text" class="form-control" id="venue_zip"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-danger d-none" id="venue_error"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveVenueBtn">Save Venue</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Validation and Venue AJAX Script -->
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
window.addEventListener('load', function() {
|
||||
var forms = document.getElementsByClassName('needs-validation');
|
||||
var validation = Array.prototype.filter.call(forms, function(form) {
|
||||
form.addEventListener('submit', function(event) {
|
||||
if (form.checkValidity() === false) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
|
||||
// Gather selected staff into a single hidden field (same pattern as edit)
|
||||
var allStaffCheckboxes = form.querySelectorAll('input[name="assigned_staff_ids[]"]');
|
||||
var checkedStaff = form.querySelectorAll('input[name="assigned_staff_ids[]"]:checked');
|
||||
var staffValues = [];
|
||||
checkedStaff.forEach(function(checkbox) {
|
||||
staffValues.push(checkbox.value);
|
||||
});
|
||||
|
||||
// Remove checkbox names to avoid duplicate submission
|
||||
allStaffCheckboxes.forEach(function(cb) { cb.removeAttribute('name'); });
|
||||
|
||||
var hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = 'assigned_staff_ids';
|
||||
hiddenInput.value = staffValues.join(',');
|
||||
form.appendChild(hiddenInput);
|
||||
}, false);
|
||||
});
|
||||
}, false);
|
||||
|
||||
// Add Venue AJAX handler using JSON-RPC
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var saveBtn = document.getElementById('saveVenueBtn');
|
||||
if (!saveBtn) return;
|
||||
saveBtn.addEventListener('click', function() {
|
||||
var name = document.getElementById('venue_name').value.trim();
|
||||
var street = document.getElementById('venue_street').value.trim();
|
||||
var street2 = document.getElementById('venue_street2').value.trim();
|
||||
var city = document.getElementById('venue_city').value.trim();
|
||||
var zip = document.getElementById('venue_zip').value.trim();
|
||||
var errorBox = document.getElementById('venue_error');
|
||||
errorBox.classList.add('d-none');
|
||||
errorBox.textContent = '';
|
||||
|
||||
if (!name) {
|
||||
errorBox.textContent = 'Venue name is required.';
|
||||
errorBox.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/my/venue/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: { name: name, street: street, street2: street2, city: city, zip: zip } })
|
||||
})
|
||||
.then(function(resp) { return resp.json(); })
|
||||
.then(function(data) {
|
||||
var result = data && (data.result || data);
|
||||
if (!result || result.success === false) {
|
||||
errorBox.textContent = (result && result.error) || 'Failed to create venue.';
|
||||
errorBox.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
var select = document.getElementById('venue_id');
|
||||
if (select) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = result.id;
|
||||
opt.textContent = result.name;
|
||||
select.appendChild(opt);
|
||||
select.value = String(result.id);
|
||||
}
|
||||
// Close modal (robust: works with or without global bootstrap)
|
||||
var modalEl = document.getElementById('addVenueModal');
|
||||
if (modalEl) {
|
||||
try {
|
||||
if (window && window.bootstrap && window.bootstrap.Modal) {
|
||||
var modal = window.bootstrap.Modal.getInstance(modalEl) || new window.bootstrap.Modal(modalEl);
|
||||
modal.hide();
|
||||
} else {
|
||||
var closeBtn = modalEl.querySelector('[data-bs-dismiss="modal"]');
|
||||
if (closeBtn) {
|
||||
closeBtn.click();
|
||||
} else {
|
||||
modalEl.classList.remove('show');
|
||||
modalEl.setAttribute('aria-hidden', 'true');
|
||||
modalEl.style.display = 'none';
|
||||
var backdrop = document.querySelector('.modal-backdrop');
|
||||
if (backdrop) backdrop.parentNode.removeChild(backdrop);
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.removeProperty('overflow');
|
||||
document.body.style.removeProperty('padding-right');
|
||||
}
|
||||
}
|
||||
} catch (e) { /* no-op */ }
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
errorBox.textContent = 'Error: ' + err;
|
||||
errorBox.classList.remove('d-none');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Date defaults and onchange behavior for portal form
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var dateStart = document.getElementById('date_start');
|
||||
var therapistStart = document.getElementById('therapist_start');
|
||||
var dateEnd = document.getElementById('date_end');
|
||||
var therapistEnd = document.getElementById('therapist_end');
|
||||
|
||||
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
||||
function formatLocal(dt) {
|
||||
return dt.getFullYear() + '-' + pad(dt.getMonth()+1) + '-' + pad(dt.getDate()) + 'T' + pad(dt.getHours()) + ':' + pad(dt.getMinutes());
|
||||
}
|
||||
function addMinutes(dt, mins) {
|
||||
var d = new Date(dt.getTime());
|
||||
d.setMinutes(d.getMinutes() + mins);
|
||||
return d;
|
||||
}
|
||||
function parseLocal(value) {
|
||||
// value like 'YYYY-MM-DDTHH:MM'
|
||||
// Construct as local time
|
||||
var parts = value.split('T');
|
||||
if (!parts || parts.length !== 2) return null;
|
||||
var d = parts[0].split('-');
|
||||
var t = parts[1].split(':');
|
||||
if (d.length !== 3 || t.length < 2) return null;
|
||||
return new Date(
|
||||
parseInt(d[0],10),
|
||||
parseInt(d[1],10)-1,
|
||||
parseInt(d[2],10),
|
||||
parseInt(t[0],10),
|
||||
parseInt(t[1],10),
|
||||
0,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// Set default date_start to now if empty
|
||||
if (dateStart && !dateStart.value) {
|
||||
var now = new Date();
|
||||
dateStart.value = formatLocal(now);
|
||||
}
|
||||
|
||||
function onDateStartChanged() {
|
||||
if (!(dateStart && dateStart.value)) return;
|
||||
var startDt = parseLocal(dateStart.value);
|
||||
if (!startDt) return;
|
||||
if (therapistStart) {
|
||||
therapistStart.value = formatLocal(addMinutes(startDt, -30));
|
||||
}
|
||||
if (dateEnd) {
|
||||
var endDt = addMinutes(startDt, 120);
|
||||
dateEnd.value = formatLocal(endDt);
|
||||
}
|
||||
if (therapistEnd && dateEnd && dateEnd.value) {
|
||||
therapistEnd.value = dateEnd.value;
|
||||
}
|
||||
}
|
||||
|
||||
function onDateEndChanged() {
|
||||
if (!(dateEnd && dateEnd.value)) return;
|
||||
var endDt = parseLocal(dateEnd.value);
|
||||
if (!endDt) return;
|
||||
if (therapistEnd) {
|
||||
if (!therapistEnd.value) {
|
||||
therapistEnd.value = dateEnd.value;
|
||||
} else {
|
||||
var tEnd = parseLocal(therapistEnd.value);
|
||||
if (tEnd && tEnd.getTime() < endDt.getTime()) {
|
||||
therapistEnd.value = dateEnd.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dateStart) {
|
||||
dateStart.addEventListener('change', onDateStartChanged);
|
||||
// Also run once on load to initialize dependent fields
|
||||
onDateStartChanged();
|
||||
}
|
||||
if (dateEnd) {
|
||||
dateEnd.addEventListener('change', onDateEndChanged);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -124,14 +124,19 @@
|
|||
<!-- Venue -->
|
||||
<div class="mb-3">
|
||||
<label for="venue_id" class="form-label">Venue</label>
|
||||
<select class="form-select" id="venue_id" name="venue_id">
|
||||
<option value="">Select a venue...</option>
|
||||
<t t-foreach="venues" t-as="venue">
|
||||
<option t-att-value="venue.id" t-att-selected="event.venue_id.id == venue.id">
|
||||
<t t-esc="venue.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<select class="form-select" id="venue_id" name="venue_id">
|
||||
<option value="">Select a venue...</option>
|
||||
<t t-foreach="venues" t-as="venue">
|
||||
<option t-att-value="venue.id" t-att-selected="event.venue_id.id == venue.id">
|
||||
<t t-esc="venue.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addVenueModal">
|
||||
<i class="fa fa-plus"/> Add Venue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -266,6 +271,47 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Add Venue Modal -->
|
||||
<div class="modal fade" id="addVenueModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add Venue</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name *</label>
|
||||
<input type="text" class="form-control" id="venue_name" required="required"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Street</label>
|
||||
<input type="text" class="form-control" id="venue_street"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Street 2</label>
|
||||
<input type="text" class="form-control" id="venue_street2"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">City</label>
|
||||
<input type="text" class="form-control" id="venue_city"/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">ZIP</label>
|
||||
<input type="text" class="form-control" id="venue_zip"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-danger d-none" id="venue_error"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveVenueBtn">Save Venue</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Validation Script -->
|
||||
<script>
|
||||
(function() {
|
||||
|
|
@ -324,6 +370,156 @@
|
|||
}, false);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Venue AJAX Script -->
|
||||
<script>
|
||||
(function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var saveBtn = document.getElementById('saveVenueBtn');
|
||||
if (!saveBtn) return;
|
||||
saveBtn.addEventListener('click', function() {
|
||||
var name = document.getElementById('venue_name').value.trim();
|
||||
var street = document.getElementById('venue_street').value.trim();
|
||||
var street2 = document.getElementById('venue_street2').value.trim();
|
||||
var city = document.getElementById('venue_city').value.trim();
|
||||
var zip = document.getElementById('venue_zip').value.trim();
|
||||
var errorBox = document.getElementById('venue_error');
|
||||
errorBox.classList.add('d-none');
|
||||
errorBox.textContent = '';
|
||||
|
||||
if (!name) {
|
||||
errorBox.textContent = 'Venue name is required.';
|
||||
errorBox.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/my/venue/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: { name: name, street: street, street2: street2, city: city, zip: zip } })
|
||||
})
|
||||
.then(function(resp) { return resp.json(); })
|
||||
.then(function(data) {
|
||||
var result = data && (data.result || data);
|
||||
if (!result || result.success === false) {
|
||||
errorBox.textContent = (result && result.error) || 'Failed to create venue.';
|
||||
errorBox.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
var select = document.getElementById('venue_id');
|
||||
if (select) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = result.id;
|
||||
opt.textContent = result.name;
|
||||
select.appendChild(opt);
|
||||
select.value = String(result.id);
|
||||
}
|
||||
// Close modal (robust: works with or without global bootstrap)
|
||||
var modalEl = document.getElementById('addVenueModal');
|
||||
if (modalEl) {
|
||||
try {
|
||||
if (window && window.bootstrap && window.bootstrap.Modal) {
|
||||
var modal = window.bootstrap.Modal.getInstance(modalEl) || new window.bootstrap.Modal(modalEl);
|
||||
modal.hide();
|
||||
} else {
|
||||
var closeBtn = modalEl.querySelector('[data-bs-dismiss="modal"]');
|
||||
if (closeBtn) {
|
||||
closeBtn.click();
|
||||
} else {
|
||||
modalEl.classList.remove('show');
|
||||
modalEl.setAttribute('aria-hidden', 'true');
|
||||
modalEl.style.display = 'none';
|
||||
var backdrop = document.querySelector('.modal-backdrop');
|
||||
if (backdrop) backdrop.parentNode.removeChild(backdrop);
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.removeProperty('overflow');
|
||||
document.body.style.removeProperty('padding-right');
|
||||
}
|
||||
}
|
||||
} catch (e) { /* no-op */ }
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
errorBox.textContent = 'Error: ' + err;
|
||||
errorBox.classList.remove('d-none');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Date defaults and onchange behavior for portal edit form
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var dateStart = document.getElementById('date_start');
|
||||
var therapistStart = document.getElementById('therapist_start');
|
||||
var dateEnd = document.getElementById('date_end');
|
||||
var therapistEnd = document.getElementById('therapist_end');
|
||||
|
||||
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
||||
function formatLocal(dt) {
|
||||
return dt.getFullYear() + '-' + pad(dt.getMonth()+1) + '-' + pad(dt.getDate()) + 'T' + pad(dt.getHours()) + ':' + pad(dt.getMinutes());
|
||||
}
|
||||
function addMinutes(dt, mins) {
|
||||
var d = new Date(dt.getTime());
|
||||
d.setMinutes(d.getMinutes() + mins);
|
||||
return d;
|
||||
}
|
||||
function parseLocal(value) {
|
||||
var parts = value.split('T');
|
||||
if (!parts || parts.length !== 2) return null;
|
||||
var d = parts[0].split('-');
|
||||
var t = parts[1].split(':');
|
||||
if (d.length !== 3 || t.length < 2) return null;
|
||||
return new Date(
|
||||
parseInt(d[0],10),
|
||||
parseInt(d[1],10)-1,
|
||||
parseInt(d[2],10),
|
||||
parseInt(t[0],10),
|
||||
parseInt(t[1],10),
|
||||
0,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
function onDateStartChanged() {
|
||||
if (!(dateStart && dateStart.value)) return;
|
||||
var startDt = parseLocal(dateStart.value);
|
||||
if (!startDt) return;
|
||||
if (therapistStart) {
|
||||
therapistStart.value = formatLocal(addMinutes(startDt, -30));
|
||||
}
|
||||
if (dateEnd) {
|
||||
var endDt = addMinutes(startDt, 120);
|
||||
dateEnd.value = formatLocal(endDt);
|
||||
}
|
||||
if (therapistEnd && dateEnd && dateEnd.value) {
|
||||
therapistEnd.value = dateEnd.value;
|
||||
}
|
||||
}
|
||||
|
||||
function onDateEndChanged() {
|
||||
if (!(dateEnd && dateEnd.value)) return;
|
||||
var endDt = parseLocal(dateEnd.value);
|
||||
if (!endDt) return;
|
||||
if (therapistEnd) {
|
||||
if (!therapistEnd.value) {
|
||||
therapistEnd.value = dateEnd.value;
|
||||
} else {
|
||||
var tEnd = parseLocal(therapistEnd.value);
|
||||
if (tEnd && tEnd.getTime() < endDt.getTime()) {
|
||||
therapistEnd.value = dateEnd.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dateStart) {
|
||||
dateStart.addEventListener('change', onDateStartChanged);
|
||||
}
|
||||
if (dateEnd) {
|
||||
dateEnd.addEventListener('change', onDateEndChanged);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
|
|
|
|||
Loading…
Reference in a new issue