bemade_sports_clinic: enforce DOB range validation and improve form errors
- Add DOB validation (not in future, not >120y) in controllers:\n - controllers/team_management_portal.py: portal_create_player_full, portal_add_player_submit\n - controllers/player_management_portal.py: create_player_submit, edit_player_submit\n- Preserve form inputs and display clear error messages on invalid DOB\n- Add dateutil.relativedelta import for date range checks\n- Minor template/context tweaks for consistent error rendering
This commit is contained in:
parent
6872e5fac2
commit
a47e829935
5 changed files with 1335 additions and 297 deletions
|
|
@ -4,6 +4,7 @@ from odoo.exceptions import UserError, ValidationError
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
from odoo.addons.portal.controllers.portal import CustomerPortal, pager
|
from odoo.addons.portal.controllers.portal import CustomerPortal, pager
|
||||||
from .access_control_mixin import AccessControlMixin
|
from .access_control_mixin import AccessControlMixin
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -13,6 +14,343 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
|
|
||||||
# Access control methods now inherited from AccessControlMixin
|
# Access control methods now inherited from AccessControlMixin
|
||||||
|
|
||||||
|
@http.route(['/my/player/create'], type='http', auth='user', website=True)
|
||||||
|
def create_player_form(self, **get):
|
||||||
|
"""Standalone create player with search-first workflow.
|
||||||
|
Shows a search UI first; only renders the full create form when a search
|
||||||
|
is attempted and yields no matching players.
|
||||||
|
"""
|
||||||
|
user = request.env.user
|
||||||
|
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||||
|
|
||||||
|
# Gather query params for search-first flow
|
||||||
|
first_name = (get.get('first_name') or '').strip()
|
||||||
|
last_name = (get.get('last_name') or '').strip()
|
||||||
|
dob = (get.get('date_of_birth') or '').strip()
|
||||||
|
attempted = any(k in get for k in ('first_name', 'last_name', 'date_of_birth'))
|
||||||
|
searched = bool(first_name or last_name or dob)
|
||||||
|
|
||||||
|
# Perform search similar to team add/link, but without team context
|
||||||
|
Patient = request.env['sports.patient']
|
||||||
|
active_rs = Patient.browse([])
|
||||||
|
archived_rs = Patient.browse([])
|
||||||
|
if searched and (first_name or last_name):
|
||||||
|
like_first = f"%{first_name}%" if first_name else "%"
|
||||||
|
like_last = f"%{last_name}%" if last_name else "%"
|
||||||
|
domain = [
|
||||||
|
('first_name', 'ilike', like_first),
|
||||||
|
('last_name', 'ilike', like_last),
|
||||||
|
]
|
||||||
|
if dob:
|
||||||
|
domain.append(('date_of_birth', '=', dob))
|
||||||
|
active_rs = Patient.search(domain + [('active', '=', True)], limit=20)
|
||||||
|
# Only TPs/admins can see archived
|
||||||
|
if is_treatment_prof or request.env.user.has_group('base.group_system'):
|
||||||
|
archived_rs = Patient.with_context(active_test=False).search(domain + [('active', '=', False)], limit=20)
|
||||||
|
|
||||||
|
def _to_dict(p):
|
||||||
|
return {
|
||||||
|
'id': p.id,
|
||||||
|
'name': p.name,
|
||||||
|
'first_name': p.first_name,
|
||||||
|
'last_name': p.last_name,
|
||||||
|
'date_of_birth': p.date_of_birth or '',
|
||||||
|
'active': bool(p.active),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Staff-only teams for multi-select
|
||||||
|
all_teams = request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name')
|
||||||
|
|
||||||
|
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||||
|
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||||
|
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||||
|
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
values.update({
|
||||||
|
'page_name': 'create_player',
|
||||||
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
'all_teams': all_teams,
|
||||||
|
'states': states,
|
||||||
|
'countries': countries,
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
# search-first context
|
||||||
|
'first_name': first_name,
|
||||||
|
'last_name': last_name,
|
||||||
|
'date_of_birth': dob,
|
||||||
|
'attempted': attempted,
|
||||||
|
'searched': searched,
|
||||||
|
'active_results': [_to_dict(p) for p in active_rs],
|
||||||
|
'archived_results': [_to_dict(p) for p in archived_rs],
|
||||||
|
})
|
||||||
|
if attempted and not searched:
|
||||||
|
values['error'] = _('Provide at least a first or last name to search')
|
||||||
|
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||||
|
|
||||||
|
@http.route(['/my/player/create/save'], type='http', auth='user', website=True, methods=['POST'])
|
||||||
|
def create_player_submit(self, **post):
|
||||||
|
"""Handle standalone player creation submit."""
|
||||||
|
user = request.env.user
|
||||||
|
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||||
|
|
||||||
|
first_name = (post.get('first_name') or '').strip()
|
||||||
|
last_name = (post.get('last_name') or '').strip()
|
||||||
|
dob = (post.get('date_of_birth') or '').strip()
|
||||||
|
|
||||||
|
if not first_name or not last_name:
|
||||||
|
request.session['notification'] = {
|
||||||
|
'type': 'danger',
|
||||||
|
'message': _('First name and last name are required')
|
||||||
|
}
|
||||||
|
return request.redirect('/my/player/create')
|
||||||
|
|
||||||
|
# Team selection (optional). Restrict to staff teams to avoid tampering
|
||||||
|
try:
|
||||||
|
selected_team_ids = [int(tid) for tid in request.httprequest.form.getlist('team_ids')]
|
||||||
|
except Exception:
|
||||||
|
selected_team_ids = []
|
||||||
|
allowed_team_ids = request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids
|
||||||
|
selected_team_ids = [tid for tid in selected_team_ids if tid in allowed_team_ids]
|
||||||
|
|
||||||
|
# Require at least one team for treatment professionals to ensure access control
|
||||||
|
if is_treatment_prof and not selected_team_ids:
|
||||||
|
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||||
|
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||||
|
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||||
|
all_teams = request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name')
|
||||||
|
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
values.update({
|
||||||
|
'page_name': 'create_player',
|
||||||
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
'all_teams': all_teams,
|
||||||
|
'states': states,
|
||||||
|
'countries': countries,
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
# keep the original search context so the create form shows
|
||||||
|
'first_name': first_name,
|
||||||
|
'last_name': last_name,
|
||||||
|
'date_of_birth': dob,
|
||||||
|
'attempted': True,
|
||||||
|
'searched': True,
|
||||||
|
'active_results': [],
|
||||||
|
'archived_results': [],
|
||||||
|
# provide posted values to prefill the form
|
||||||
|
'form_data': dict(post),
|
||||||
|
'error': _('Please select at least one team to assign to this player.'),
|
||||||
|
})
|
||||||
|
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||||
|
|
||||||
|
# Validate DOB format and range if provided
|
||||||
|
if dob:
|
||||||
|
try:
|
||||||
|
# Will raise if the date is not a valid string for a Date field
|
||||||
|
dob_date = fields.Date.to_date(dob)
|
||||||
|
except Exception:
|
||||||
|
_logger.info('Invalid date_of_birth provided on create: %s', dob)
|
||||||
|
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||||
|
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||||
|
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||||
|
all_teams = request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name')
|
||||||
|
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
values.update({
|
||||||
|
'page_name': 'create_player',
|
||||||
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
'all_teams': all_teams,
|
||||||
|
'states': states,
|
||||||
|
'countries': countries,
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
# keep the original search context so the create form shows
|
||||||
|
'first_name': first_name,
|
||||||
|
'last_name': last_name,
|
||||||
|
'date_of_birth': dob,
|
||||||
|
'attempted': True,
|
||||||
|
'searched': True,
|
||||||
|
'active_results': [],
|
||||||
|
'archived_results': [],
|
||||||
|
# provide posted values to prefill the form
|
||||||
|
'form_data': dict(post),
|
||||||
|
'error': _('Please enter a valid Date of Birth (YYYY-MM-DD).'),
|
||||||
|
})
|
||||||
|
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||||
|
|
||||||
|
# Range checks: not in the future, not older than 120 years
|
||||||
|
today = fields.Date.context_today(request.env.user) or fields.Date.today()
|
||||||
|
min_dob = today - relativedelta(years=120)
|
||||||
|
if dob_date > today or dob_date < min_dob:
|
||||||
|
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||||
|
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||||
|
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||||
|
all_teams = request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name')
|
||||||
|
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
values.update({
|
||||||
|
'page_name': 'create_player',
|
||||||
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
'all_teams': all_teams,
|
||||||
|
'states': states,
|
||||||
|
'countries': countries,
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
# keep the original search context so the create form shows
|
||||||
|
'first_name': first_name,
|
||||||
|
'last_name': last_name,
|
||||||
|
'date_of_birth': dob,
|
||||||
|
'attempted': True,
|
||||||
|
'searched': True,
|
||||||
|
'active_results': [],
|
||||||
|
'archived_results': [],
|
||||||
|
# provide posted values to prefill the form
|
||||||
|
'form_data': dict(post),
|
||||||
|
'error': _('Date of Birth must not be in the future and not be more than 120 years ago.'),
|
||||||
|
})
|
||||||
|
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||||
|
|
||||||
|
vals = {
|
||||||
|
'first_name': first_name,
|
||||||
|
'last_name': last_name,
|
||||||
|
}
|
||||||
|
if dob:
|
||||||
|
vals['date_of_birth'] = dob
|
||||||
|
# Optional contact and address fields
|
||||||
|
for key in ['email', 'phone', 'street', 'street2', 'city', 'zip']:
|
||||||
|
if key in post:
|
||||||
|
vals[key] = post.get(key) or False
|
||||||
|
if post.get('state_id'):
|
||||||
|
try:
|
||||||
|
vals['state_id'] = int(post.get('state_id'))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if post.get('country_id'):
|
||||||
|
try:
|
||||||
|
vals['country_id'] = int(post.get('country_id'))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Additional fields for treatment professionals
|
||||||
|
if is_treatment_prof:
|
||||||
|
if selected_team_ids:
|
||||||
|
vals['team_ids'] = [(6, 0, list(set(selected_team_ids)))]
|
||||||
|
if 'allergies' in post:
|
||||||
|
_all = (post.get('allergies') or '').strip()
|
||||||
|
vals['allergies'] = _all if _all else False
|
||||||
|
if 'team_info_notes' in post:
|
||||||
|
_notes = (post.get('team_info_notes') or '').strip()
|
||||||
|
vals['team_info_notes'] = _notes if _notes else False
|
||||||
|
if post.get('match_status'):
|
||||||
|
vals['match_status'] = post.get('match_status')
|
||||||
|
if post.get('practice_status'):
|
||||||
|
vals['practice_status'] = post.get('practice_status')
|
||||||
|
|
||||||
|
# Use public create method with permission checks
|
||||||
|
try:
|
||||||
|
patient = request.env['sports.patient'].create_portal_patient(vals)
|
||||||
|
except ValidationError as ve:
|
||||||
|
# Re-render the create form (search-first view) with preserved input and a friendly message
|
||||||
|
_logger.info('ValidationError during player create: %s', ve)
|
||||||
|
# Rebuild the context needed by the search-first page
|
||||||
|
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||||
|
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||||
|
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||||
|
all_teams = request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name')
|
||||||
|
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
values.update({
|
||||||
|
'page_name': 'create_player',
|
||||||
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
'all_teams': all_teams,
|
||||||
|
'states': states,
|
||||||
|
'countries': countries,
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
# keep the original search context so the create form shows
|
||||||
|
'first_name': first_name,
|
||||||
|
'last_name': last_name,
|
||||||
|
'date_of_birth': dob,
|
||||||
|
'attempted': True,
|
||||||
|
'searched': True,
|
||||||
|
'active_results': [],
|
||||||
|
'archived_results': [],
|
||||||
|
# provide posted values to prefill the form
|
||||||
|
'form_data': dict(post),
|
||||||
|
'error': str(ve) or _('Invalid combination of match and practice status.'),
|
||||||
|
})
|
||||||
|
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||||
|
except Exception as e:
|
||||||
|
# Render the create form with a generic error and preserve inputs
|
||||||
|
_logger.exception('Error creating patient from portal create')
|
||||||
|
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||||
|
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||||
|
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||||
|
all_teams = request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name')
|
||||||
|
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
values.update({
|
||||||
|
'page_name': 'create_player',
|
||||||
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
'all_teams': all_teams,
|
||||||
|
'states': states,
|
||||||
|
'countries': countries,
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
# keep the original search context so the create form shows
|
||||||
|
'first_name': first_name,
|
||||||
|
'last_name': last_name,
|
||||||
|
'date_of_birth': dob,
|
||||||
|
'attempted': True,
|
||||||
|
'searched': True,
|
||||||
|
'active_results': [],
|
||||||
|
'archived_results': [],
|
||||||
|
# provide posted values to prefill the form
|
||||||
|
'form_data': dict(post),
|
||||||
|
'error': str(e) or _('There was an error creating the player. Please review your inputs and try again.'),
|
||||||
|
})
|
||||||
|
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||||
|
|
||||||
|
# Optionally create a primary emergency contact if provided (TPs only)
|
||||||
|
if is_treatment_prof:
|
||||||
|
ec_name = (post.get('ec_name') or '').strip()
|
||||||
|
ec_type = (post.get('ec_contact_type') or '').strip()
|
||||||
|
if ec_name and ec_type:
|
||||||
|
contact_vals = {
|
||||||
|
'patient_id': patient.id,
|
||||||
|
'name': ec_name,
|
||||||
|
'contact_type': ec_type,
|
||||||
|
}
|
||||||
|
if post.get('ec_mobile'):
|
||||||
|
contact_vals['mobile'] = post.get('ec_mobile')
|
||||||
|
if post.get('ec_email'):
|
||||||
|
contact_vals['email'] = post.get('ec_email')
|
||||||
|
try:
|
||||||
|
request.env['sports.patient.contact'].sudo().create(contact_vals)
|
||||||
|
except Exception:
|
||||||
|
_logger.exception('Failed to create primary emergency contact during player create')
|
||||||
|
|
||||||
|
return request.redirect(f"/my/player?player_id={patient.id}")
|
||||||
|
|
||||||
@http.route(['/my/player/edit'], type='http', auth='user', website=True)
|
@http.route(['/my/player/edit'], type='http', auth='user', website=True)
|
||||||
def edit_player_form(self, patient_id, **post):
|
def edit_player_form(self, patient_id, **post):
|
||||||
"""Show form to edit player information"""
|
"""Show form to edit player information"""
|
||||||
|
|
@ -28,6 +366,12 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||||
|
|
||||||
teams = patient.team_ids
|
teams = patient.team_ids
|
||||||
|
# All teams for multi-select limited to current user's staff teams
|
||||||
|
all_teams = request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name')
|
||||||
|
|
||||||
# Create a dictionary with patient info for protected fields
|
# Create a dictionary with patient info for protected fields
|
||||||
patient_info = {}
|
patient_info = {}
|
||||||
|
|
@ -61,13 +405,24 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||||
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||||
countries = canada if canada else request.env['res.country'].search([], order='name')
|
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||||
|
# Determine primary emergency contact (lowest sequence)
|
||||||
|
primary_contact = request.env['sports.patient.contact'].search([
|
||||||
|
('patient_id', '=', patient.id)
|
||||||
|
], order='sequence,id', limit=1)
|
||||||
|
|
||||||
values = {
|
values = {
|
||||||
'patient': patient, # Keep original patient
|
'patient': patient, # Keep original patient
|
||||||
'patient_info': patient_info, # Add patient_info for protected fields
|
'patient_info': patient_info, # Add patient_info for protected fields
|
||||||
'teams': teams,
|
'teams': teams,
|
||||||
|
'all_teams': all_teams,
|
||||||
'states': states,
|
'states': states,
|
||||||
'countries': countries,
|
'countries': countries,
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
# Primary emergency contact defaults for prefill
|
||||||
|
'ec_name': primary_contact.name if primary_contact else '',
|
||||||
|
'ec_contact_type': primary_contact.contact_type if primary_contact else '',
|
||||||
|
'ec_mobile': primary_contact.mobile if primary_contact else '',
|
||||||
|
'ec_email': primary_contact.email if primary_contact else '',
|
||||||
'return_url': return_url,
|
'return_url': return_url,
|
||||||
'page_name': 'edit_player',
|
'page_name': 'edit_player',
|
||||||
'is_treatment_prof': is_treatment_prof,
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
|
@ -91,6 +446,69 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
# Check if user is a treatment professional
|
# Check if user is a treatment professional
|
||||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||||
|
|
||||||
|
# If DOB is being changed/provided, validate format upfront to avoid unhelpful errors
|
||||||
|
dob_str = (post.get('date_of_birth') or '').strip()
|
||||||
|
if dob_str:
|
||||||
|
try:
|
||||||
|
dob_date = fields.Date.to_date(dob_str)
|
||||||
|
except Exception:
|
||||||
|
# Rebuild dropdowns and re-render with preserved inputs
|
||||||
|
user = request.env.user
|
||||||
|
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||||
|
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||||
|
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||||
|
all_teams = request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name')
|
||||||
|
|
||||||
|
values = {
|
||||||
|
'patient': patient,
|
||||||
|
'patient_info': {},
|
||||||
|
'teams': patient.team_ids,
|
||||||
|
'all_teams': all_teams,
|
||||||
|
'states': states,
|
||||||
|
'countries': countries,
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||||
|
'page_name': 'edit_player',
|
||||||
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
'error': _('Please enter a valid Date of Birth (YYYY-MM-DD).'),
|
||||||
|
'form_data': dict(post),
|
||||||
|
}
|
||||||
|
return request.render('bemade_sports_clinic.portal_edit_player', values)
|
||||||
|
|
||||||
|
# Range checks: not in the future, not older than 120 years
|
||||||
|
today = fields.Date.context_today(request.env.user) or fields.Date.today()
|
||||||
|
min_dob = today - relativedelta(years=120)
|
||||||
|
if dob_date > today or dob_date < min_dob:
|
||||||
|
user = request.env.user
|
||||||
|
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||||
|
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||||
|
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||||
|
all_teams = request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name')
|
||||||
|
|
||||||
|
values = {
|
||||||
|
'patient': patient,
|
||||||
|
'patient_info': {},
|
||||||
|
'teams': patient.team_ids,
|
||||||
|
'all_teams': all_teams,
|
||||||
|
'states': states,
|
||||||
|
'countries': countries,
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||||
|
'page_name': 'edit_player',
|
||||||
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
'error': _('Date of Birth must not be in the future and not be more than 120 years ago.'),
|
||||||
|
'form_data': dict(post),
|
||||||
|
}
|
||||||
|
return request.render('bemade_sports_clinic.portal_edit_player', values)
|
||||||
|
|
||||||
# Prepare values for patient update
|
# Prepare values for patient update
|
||||||
vals = {}
|
vals = {}
|
||||||
|
|
||||||
|
|
@ -135,6 +553,18 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
|
|
||||||
# Additional fields that only treatment professionals can update
|
# Additional fields that only treatment professionals can update
|
||||||
if is_treatment_prof:
|
if is_treatment_prof:
|
||||||
|
# Team assignments via multi-select
|
||||||
|
try:
|
||||||
|
team_id_list = [int(tid) for tid in request.httprequest.form.getlist('team_ids')]
|
||||||
|
except Exception:
|
||||||
|
team_id_list = []
|
||||||
|
if team_id_list:
|
||||||
|
# Defense-in-depth: restrict to teams where current user is staff
|
||||||
|
allowed_team_ids = request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', request.env.user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids
|
||||||
|
filtered_team_ids = [tid for tid in team_id_list if tid in allowed_team_ids]
|
||||||
|
vals['team_ids'] = [(6, 0, list(set(filtered_team_ids)))]
|
||||||
if post.get('date_of_birth'):
|
if post.get('date_of_birth'):
|
||||||
vals.update({
|
vals.update({
|
||||||
'date_of_birth': post.get('date_of_birth'),
|
'date_of_birth': post.get('date_of_birth'),
|
||||||
|
|
@ -142,13 +572,15 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
|
|
||||||
# Medical information
|
# Medical information
|
||||||
if 'allergies' in post:
|
if 'allergies' in post:
|
||||||
|
_all = (post.get('allergies') or '').strip()
|
||||||
vals.update({
|
vals.update({
|
||||||
'allergies': post.get('allergies') or False,
|
'allergies': _all if _all else False,
|
||||||
})
|
})
|
||||||
|
|
||||||
if 'team_info_notes' in post:
|
if 'team_info_notes' in post:
|
||||||
|
_notes = (post.get('team_info_notes') or '').strip()
|
||||||
vals.update({
|
vals.update({
|
||||||
'team_info_notes': post.get('team_info_notes') or False,
|
'team_info_notes': _notes if _notes else False,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Status fields
|
# Status fields
|
||||||
|
|
@ -164,7 +596,113 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
|
|
||||||
# Update the patient - no sudo needed as field-level security is in place
|
# Update the patient - no sudo needed as field-level security is in place
|
||||||
if vals:
|
if vals:
|
||||||
patient.write(vals)
|
try:
|
||||||
|
patient.write(vals)
|
||||||
|
except ValidationError as ve:
|
||||||
|
# Re-render edit form with preserved input and error
|
||||||
|
user = request.env.user
|
||||||
|
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||||
|
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||||
|
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||||
|
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||||
|
# All teams for multi-select limited to current user's staff teams
|
||||||
|
all_teams = request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name')
|
||||||
|
|
||||||
|
values = {
|
||||||
|
'patient': patient,
|
||||||
|
'patient_info': {},
|
||||||
|
'teams': patient.team_ids,
|
||||||
|
'all_teams': all_teams,
|
||||||
|
'states': states,
|
||||||
|
'countries': countries,
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||||
|
'page_name': 'edit_player',
|
||||||
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
'error': str(ve) or _('Invalid combination of match and practice status.'),
|
||||||
|
'form_data': dict(post),
|
||||||
|
}
|
||||||
|
return request.render('bemade_sports_clinic.portal_edit_player', values)
|
||||||
|
except Exception as e:
|
||||||
|
# Generic exception: re-render with generic error and preserved inputs
|
||||||
|
user = request.env.user
|
||||||
|
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||||
|
canada = request.env['res.country'].search([('code', '=', 'CA')], limit=1)
|
||||||
|
states = request.env['res.country.state'].search([('country_id', '=', canada.id)], order='name') if canada else request.env['res.country.state']
|
||||||
|
countries = canada if canada else request.env['res.country'].search([], order='name')
|
||||||
|
all_teams = request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name')
|
||||||
|
|
||||||
|
values = {
|
||||||
|
'patient': patient,
|
||||||
|
'patient_info': {},
|
||||||
|
'teams': patient.team_ids,
|
||||||
|
'all_teams': all_teams,
|
||||||
|
'states': states,
|
||||||
|
'countries': countries,
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||||
|
'page_name': 'edit_player',
|
||||||
|
'is_treatment_prof': is_treatment_prof,
|
||||||
|
'error': str(e) or _('An unexpected error occurred.'),
|
||||||
|
'form_data': dict(post),
|
||||||
|
}
|
||||||
|
return request.render('bemade_sports_clinic.portal_edit_player', values)
|
||||||
|
|
||||||
|
# Update or create primary emergency contact (TPs only), regardless of patient field changes
|
||||||
|
if is_treatment_prof:
|
||||||
|
ec_name = (post.get('ec_name') or '').strip()
|
||||||
|
ec_type = (post.get('ec_contact_type') or '').strip()
|
||||||
|
ec_mobile = (post.get('ec_mobile') or '').strip()
|
||||||
|
ec_email = (post.get('ec_email') or '').strip()
|
||||||
|
|
||||||
|
# If any EC field provided, attempt update/create
|
||||||
|
if any([ec_name, ec_type, ec_mobile, ec_email]):
|
||||||
|
primary_contact = request.env['sports.patient.contact'].search([
|
||||||
|
('patient_id', '=', patient.id)
|
||||||
|
], order='sequence,id', limit=1)
|
||||||
|
|
||||||
|
if primary_contact:
|
||||||
|
update_vals = {}
|
||||||
|
# Only update name/type if provided (avoid clearing required selection)
|
||||||
|
if ec_name:
|
||||||
|
update_vals['name'] = ec_name
|
||||||
|
if ec_type:
|
||||||
|
update_vals['contact_type'] = ec_type
|
||||||
|
# For mobile/email, explicitly set to value or False to clear
|
||||||
|
if 'ec_mobile' in post:
|
||||||
|
update_vals['mobile'] = ec_mobile or False
|
||||||
|
if 'ec_email' in post:
|
||||||
|
update_vals['email'] = ec_email or False
|
||||||
|
if update_vals:
|
||||||
|
try:
|
||||||
|
primary_contact.sudo().write(update_vals)
|
||||||
|
except Exception:
|
||||||
|
_logger.exception('Failed to update primary emergency contact for patient %s', patient.id)
|
||||||
|
else:
|
||||||
|
# Create only if we have minimally required fields
|
||||||
|
if ec_name and ec_type:
|
||||||
|
create_vals = {
|
||||||
|
'patient_id': patient.id,
|
||||||
|
'name': ec_name,
|
||||||
|
'contact_type': ec_type,
|
||||||
|
'sequence': 0,
|
||||||
|
}
|
||||||
|
if 'ec_mobile' in post:
|
||||||
|
create_vals['mobile'] = ec_mobile or False
|
||||||
|
if 'ec_email' in post:
|
||||||
|
create_vals['email'] = ec_email or False
|
||||||
|
try:
|
||||||
|
request.env['sports.patient.contact'].sudo().create(create_vals)
|
||||||
|
except Exception:
|
||||||
|
_logger.exception('Failed to create primary emergency contact for patient %s', patient.id)
|
||||||
|
|
||||||
return request.redirect(f'/my/player?player_id={patient_id}')
|
return request.redirect(f'/my/player?player_id={patient_id}')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
from odoo import http, _
|
from odoo import http, fields, _
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||||
from odoo.exceptions import AccessError, MissingError, UserError, ValidationError
|
from odoo.exceptions import AccessError, MissingError, UserError, ValidationError
|
||||||
from .access_control_mixin import AccessControlMixin
|
from .access_control_mixin import AccessControlMixin
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import date
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -310,6 +312,17 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
'searched': searched,
|
'searched': searched,
|
||||||
'active_results': [_to_dict(p) for p in active_rs],
|
'active_results': [_to_dict(p) for p in active_rs],
|
||||||
'archived_results': [_to_dict(p) for p in archived_rs],
|
'archived_results': [_to_dict(p) for p in archived_rs],
|
||||||
|
# Extra context to support inline full create form
|
||||||
|
'is_treatment_prof': request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system'),
|
||||||
|
'states': request.env['res.country.state'].search([('country_id.code', '=', 'CA')], order='name'),
|
||||||
|
'countries': request.env['res.country'].search([('code', '=', 'CA')], limit=1) or request.env['res.country'].search([], order='name'),
|
||||||
|
'all_teams': request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', request.env.user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name'),
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
'prefill_team_id': team.id,
|
||||||
})
|
})
|
||||||
if attempted and not searched:
|
if attempted and not searched:
|
||||||
values['error'] = _('Provide at least a first or last name to search')
|
values['error'] = _('Provide at least a first or last name to search')
|
||||||
|
|
@ -326,6 +339,233 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
}
|
}
|
||||||
return request.redirect(f"/my/team/{team_id}")
|
return request.redirect(f"/my/team/{team_id}")
|
||||||
|
|
||||||
|
@http.route(['/my/team/<int:team_id>/player/create_full'], type='http', auth='user', website=True, methods=['POST'], csrf=True)
|
||||||
|
def portal_create_player_full(self, team_id, **post):
|
||||||
|
"""Create a brand new player with full details (inline edit form) and link to team.
|
||||||
|
This is used when the search yields no results and we render the full form inline.
|
||||||
|
Only treatment professionals/admins may create directly.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
team = self._check_team_access(team_id)
|
||||||
|
|
||||||
|
# Permission: only therapists/admins can create directly
|
||||||
|
is_tp = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||||
|
request.env.user.has_group('base.group_system')
|
||||||
|
if not is_tp:
|
||||||
|
raise AccessError(_("You don't have permission to create players."))
|
||||||
|
|
||||||
|
# Required basics
|
||||||
|
first_name = (post.get('first_name') or '').strip()
|
||||||
|
last_name = (post.get('last_name') or '').strip()
|
||||||
|
dob = (post.get('date_of_birth') or '').strip()
|
||||||
|
|
||||||
|
if not first_name or not last_name:
|
||||||
|
raise UserError(_('First name and last name are required'))
|
||||||
|
if not dob:
|
||||||
|
raise UserError(_('Date of birth is required'))
|
||||||
|
|
||||||
|
# Validate DOB format explicitly to avoid ValueError down in ORM
|
||||||
|
try:
|
||||||
|
dob_date = fields.Date.to_date(dob)
|
||||||
|
except Exception:
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
values.update({
|
||||||
|
'page_name': 'add_link_player',
|
||||||
|
'team': team,
|
||||||
|
'first_name': post.get('first_name') or '',
|
||||||
|
'last_name': post.get('last_name') or '',
|
||||||
|
'date_of_birth': dob,
|
||||||
|
'searched': True,
|
||||||
|
'active_results': [],
|
||||||
|
'archived_results': [],
|
||||||
|
'is_treatment_prof': request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system'),
|
||||||
|
'states': request.env['res.country.state'].search([('country_id.code', '=', 'CA')], order='name'),
|
||||||
|
'countries': request.env['res.country'].search([('code', '=', 'CA')], limit=1) or request.env['res.country'].search([], order='name'),
|
||||||
|
'all_teams': request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', request.env.user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name'),
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
'form_data': dict(post),
|
||||||
|
'error': _('Please enter a valid Date of Birth (YYYY-MM-DD).'),
|
||||||
|
'prefill_team_id': team.id,
|
||||||
|
})
|
||||||
|
return request.render('bemade_sports_clinic.portal_add_link_player_page', values)
|
||||||
|
|
||||||
|
# Enforce DOB not in future and not older than 120 years
|
||||||
|
today = fields.Date.context_today(request.env.user) or fields.Date.today()
|
||||||
|
min_dob = today - relativedelta(years=120)
|
||||||
|
if dob_date > today or dob_date < min_dob:
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
values.update({
|
||||||
|
'page_name': 'add_link_player',
|
||||||
|
'team': team,
|
||||||
|
'first_name': post.get('first_name') or '',
|
||||||
|
'last_name': post.get('last_name') or '',
|
||||||
|
'date_of_birth': dob,
|
||||||
|
'searched': True,
|
||||||
|
'active_results': [],
|
||||||
|
'archived_results': [],
|
||||||
|
'is_treatment_prof': request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system'),
|
||||||
|
'states': request.env['res.country.state'].search([('country_id.code', '=', 'CA')], order='name'),
|
||||||
|
'countries': request.env['res.country'].search([('code', '=', 'CA')], limit=1) or request.env['res.country'].search([], order='name'),
|
||||||
|
'all_teams': request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', request.env.user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name'),
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
'form_data': dict(post),
|
||||||
|
'error': _('Date of Birth must not be in the future and not be more than 120 years ago.'),
|
||||||
|
'prefill_team_id': team.id,
|
||||||
|
})
|
||||||
|
return request.render('bemade_sports_clinic.portal_add_link_player_page', values)
|
||||||
|
|
||||||
|
# Build values similar to edit form, honoring permissions
|
||||||
|
# Collect team_ids from multi-select; ensure current team is included by default
|
||||||
|
try:
|
||||||
|
selected_team_ids = [int(tid) for tid in request.httprequest.form.getlist('team_ids')]
|
||||||
|
except Exception:
|
||||||
|
selected_team_ids = []
|
||||||
|
if not selected_team_ids:
|
||||||
|
selected_team_ids = [team.id]
|
||||||
|
if team.id not in selected_team_ids:
|
||||||
|
selected_team_ids.append(team.id)
|
||||||
|
# Restrict to teams where current user is staff (defense-in-depth against tampering)
|
||||||
|
allowed_team_ids = request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', request.env.user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids
|
||||||
|
selected_team_ids = [tid for tid in selected_team_ids if tid in allowed_team_ids]
|
||||||
|
vals = {
|
||||||
|
'first_name': first_name,
|
||||||
|
'last_name': last_name,
|
||||||
|
'date_of_birth': dob,
|
||||||
|
'team_ids': [(6, 0, list(set(selected_team_ids)))],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Contact info
|
||||||
|
email = (post.get('email') or '').strip()
|
||||||
|
phone = (post.get('phone') or '').strip()
|
||||||
|
if email:
|
||||||
|
vals['email'] = email
|
||||||
|
if phone:
|
||||||
|
vals['phone'] = phone
|
||||||
|
|
||||||
|
# Address info
|
||||||
|
for f in ['street', 'street2', 'city', 'zip']:
|
||||||
|
if f in post:
|
||||||
|
vals[f] = post.get(f) or False
|
||||||
|
# State/Country
|
||||||
|
if post.get('state_id'):
|
||||||
|
try:
|
||||||
|
vals['state_id'] = int(post.get('state_id'))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if post.get('country_id'):
|
||||||
|
try:
|
||||||
|
vals['country_id'] = int(post.get('country_id'))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Extra medical/status fields (only for TP/admin)
|
||||||
|
if is_tp:
|
||||||
|
if 'allergies' in post:
|
||||||
|
vals['allergies'] = post.get('allergies') or False
|
||||||
|
if 'team_info_notes' in post:
|
||||||
|
vals['team_info_notes'] = post.get('team_info_notes') or False
|
||||||
|
if post.get('match_status'):
|
||||||
|
vals['match_status'] = post.get('match_status')
|
||||||
|
if post.get('practice_status'):
|
||||||
|
vals['practice_status'] = post.get('practice_status')
|
||||||
|
|
||||||
|
# Create through secured helper
|
||||||
|
patient = request.env['sports.patient']._create_portal_patient(vals)
|
||||||
|
|
||||||
|
# Optionally create a primary emergency contact if provided (TP/admin only)
|
||||||
|
if is_tp:
|
||||||
|
ec_name = (post.get('ec_name') or '').strip()
|
||||||
|
ec_type = (post.get('ec_contact_type') or '').strip()
|
||||||
|
if ec_name and ec_type:
|
||||||
|
contact_vals = {
|
||||||
|
'patient_id': patient.id,
|
||||||
|
'name': ec_name,
|
||||||
|
'contact_type': ec_type,
|
||||||
|
}
|
||||||
|
if post.get('ec_mobile'):
|
||||||
|
contact_vals['mobile'] = post.get('ec_mobile')
|
||||||
|
if post.get('ec_email'):
|
||||||
|
contact_vals['email'] = post.get('ec_email')
|
||||||
|
try:
|
||||||
|
request.env['sports.patient.contact'].sudo().create(contact_vals)
|
||||||
|
except Exception:
|
||||||
|
_logger.exception('Failed to create primary emergency contact during inline player create')
|
||||||
|
|
||||||
|
request.session['notification'] = {
|
||||||
|
'type': 'success',
|
||||||
|
'title': _('Player Created'),
|
||||||
|
'message': _('%s has been created and added to the team.') % patient.name,
|
||||||
|
'sticky': False,
|
||||||
|
}
|
||||||
|
# After one-step create, return to team page
|
||||||
|
return request.redirect(f"/my/team/{team.id}")
|
||||||
|
|
||||||
|
except (AccessError, UserError, ValidationError) as e:
|
||||||
|
# Re-render the add/link page with preserved form values and error
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
team = self._check_team_access(team_id)
|
||||||
|
values.update({
|
||||||
|
'page_name': 'add_link_player',
|
||||||
|
'team': team,
|
||||||
|
'first_name': post.get('first_name') or '',
|
||||||
|
'last_name': post.get('last_name') or '',
|
||||||
|
'date_of_birth': post.get('date_of_birth') or '',
|
||||||
|
'searched': True,
|
||||||
|
'active_results': [],
|
||||||
|
'archived_results': [],
|
||||||
|
'is_treatment_prof': request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system'),
|
||||||
|
'states': request.env['res.country.state'].search([('country_id.code', '=', 'CA')], order='name'),
|
||||||
|
'countries': request.env['res.country'].search([('code', '=', 'CA')], limit=1) or request.env['res.country'].search([], order='name'),
|
||||||
|
'all_teams': request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', request.env.user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name'),
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
'form_data': dict(post),
|
||||||
|
'error': str(e),
|
||||||
|
'prefill_team_id': team.id,
|
||||||
|
})
|
||||||
|
return request.render('bemade_sports_clinic.portal_add_link_player_page', values)
|
||||||
|
except Exception:
|
||||||
|
_logger.exception('Error creating player from inline full form')
|
||||||
|
# Render with generic error and preserved inputs
|
||||||
|
values = self._prepare_portal_layout_values()
|
||||||
|
team = self._check_team_access(team_id)
|
||||||
|
values.update({
|
||||||
|
'page_name': 'add_link_player',
|
||||||
|
'team': team,
|
||||||
|
'first_name': post.get('first_name') or '',
|
||||||
|
'last_name': post.get('last_name') or '',
|
||||||
|
'date_of_birth': post.get('date_of_birth') or '',
|
||||||
|
'searched': True,
|
||||||
|
'active_results': [],
|
||||||
|
'archived_results': [],
|
||||||
|
'is_treatment_prof': request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system'),
|
||||||
|
'states': request.env['res.country.state'].search([('country_id.code', '=', 'CA')], order='name'),
|
||||||
|
'countries': request.env['res.country'].search([('code', '=', 'CA')], limit=1) or request.env['res.country'].search([], order='name'),
|
||||||
|
'all_teams': request.env['sports.team'].search([
|
||||||
|
('id', 'in', request.env['sports.team.staff'].search([
|
||||||
|
('partner_id', '=', request.env.user.partner_id.id)
|
||||||
|
]).mapped('team_id').ids)
|
||||||
|
], order='name'),
|
||||||
|
'relationship_types': request.env['sports.patient.contact']._fields['contact_type'].selection,
|
||||||
|
'form_data': dict(post),
|
||||||
|
'error': _('An unexpected error occurred.'),
|
||||||
|
'prefill_team_id': team.id,
|
||||||
|
})
|
||||||
|
return request.render('bemade_sports_clinic.portal_add_link_player_page', values)
|
||||||
|
|
||||||
@http.route(['/my/team/<int:team_id>/add_player/submit'],
|
@http.route(['/my/team/<int:team_id>/add_player/submit'],
|
||||||
type='http', auth="user", website=True, methods=['POST'], csrf=True)
|
type='http', auth="user", website=True, methods=['POST'], csrf=True)
|
||||||
def portal_add_player_submit(self, team_id, **post):
|
def portal_add_player_submit(self, team_id, **post):
|
||||||
|
|
@ -346,6 +586,34 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
if not dob:
|
if not dob:
|
||||||
raise UserError(_("Date of birth is required"))
|
raise UserError(_("Date of birth is required"))
|
||||||
|
|
||||||
|
# Validate DOB format explicitly
|
||||||
|
try:
|
||||||
|
dob_date = fields.Date.to_date(dob)
|
||||||
|
except Exception:
|
||||||
|
values = {
|
||||||
|
'error': _('Please enter a valid Date of Birth (YYYY-MM-DD).'),
|
||||||
|
'team': team,
|
||||||
|
'page_name': 'add_player',
|
||||||
|
}
|
||||||
|
values.update(post)
|
||||||
|
return request.render("bemade_sports_clinic.portal_add_player", values)
|
||||||
|
|
||||||
|
# Enforce DOB not in future and not older than 120 years
|
||||||
|
today = fields.Date.context_today(request.env.user) or fields.Date.today()
|
||||||
|
min_dob = today - relativedelta(years=120)
|
||||||
|
if dob_date > today or dob_date < min_dob:
|
||||||
|
values = {
|
||||||
|
'error': _('Date of Birth must not be in the future and not be more than 120 years ago.'),
|
||||||
|
'team': team,
|
||||||
|
'page_name': 'add_player',
|
||||||
|
}
|
||||||
|
values.update(post)
|
||||||
|
return request.render("bemade_sports_clinic.portal_add_player", values)
|
||||||
|
|
||||||
|
# Determine if current user is allowed to set medical/status fields
|
||||||
|
is_tp_or_admin = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or \
|
||||||
|
request.env.user.has_group('base.group_system')
|
||||||
|
|
||||||
# Check for existing player
|
# Check for existing player
|
||||||
existing_patient = self._find_existing_patient(
|
existing_patient = self._find_existing_patient(
|
||||||
first_name, last_name, email, phone
|
first_name, last_name, email, phone
|
||||||
|
|
@ -395,6 +663,12 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
'phone': phone or False,
|
'phone': phone or False,
|
||||||
'date_of_birth': dob,
|
'date_of_birth': dob,
|
||||||
}
|
}
|
||||||
|
# Pass through status fields if permitted
|
||||||
|
if is_tp_or_admin:
|
||||||
|
if post.get('match_status'):
|
||||||
|
patient_vals['match_status'] = post.get('match_status')
|
||||||
|
if post.get('practice_status'):
|
||||||
|
patient_vals['practice_status'] = post.get('practice_status')
|
||||||
|
|
||||||
# Create patient through the private _create_portal_patient method which has proper access controls
|
# Create patient through the private _create_portal_patient method which has proper access controls
|
||||||
patient = request.env['sports.patient']._create_portal_patient(patient_vals)
|
patient = request.env['sports.patient']._create_portal_patient(patient_vals)
|
||||||
|
|
@ -556,6 +830,11 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
}
|
}
|
||||||
if dob:
|
if dob:
|
||||||
vals['date_of_birth'] = dob
|
vals['date_of_birth'] = dob
|
||||||
|
# Pass through status fields (route already restricts to TP/admin)
|
||||||
|
if post.get('match_status'):
|
||||||
|
vals['match_status'] = post.get('match_status')
|
||||||
|
if post.get('practice_status'):
|
||||||
|
vals['practice_status'] = post.get('practice_status')
|
||||||
|
|
||||||
patient = request.env['sports.patient']._create_portal_patient(vals)
|
patient = request.env['sports.patient']._create_portal_patient(vals)
|
||||||
|
|
||||||
|
|
@ -622,6 +901,11 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
||||||
vals['email'] = email
|
vals['email'] = email
|
||||||
if phone:
|
if phone:
|
||||||
vals['phone'] = phone
|
vals['phone'] = phone
|
||||||
|
# Pass through status fields
|
||||||
|
if post.get('match_status'):
|
||||||
|
vals['match_status'] = post.get('match_status')
|
||||||
|
if post.get('practice_status'):
|
||||||
|
vals['practice_status'] = post.get('practice_status')
|
||||||
|
|
||||||
patient = request.env['sports.patient']._create_portal_patient(vals)
|
patient = request.env['sports.patient']._create_portal_patient(vals)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -839,6 +839,15 @@ class Patient(models.Model):
|
||||||
'phone': vals.get('phone', False),
|
'phone': vals.get('phone', False),
|
||||||
'type': 'contact',
|
'type': 'contact',
|
||||||
}
|
}
|
||||||
|
# Optional address fields coming from portal form
|
||||||
|
# These are res.partner fields, so capture them here
|
||||||
|
for key in ['street', 'street2', 'city', 'zip']:
|
||||||
|
if key in vals:
|
||||||
|
partner_vals[key] = vals.get(key) or False
|
||||||
|
if vals.get('state_id'):
|
||||||
|
partner_vals['state_id'] = vals.get('state_id')
|
||||||
|
if vals.get('country_id'):
|
||||||
|
partner_vals['country_id'] = vals.get('country_id')
|
||||||
partner = (
|
partner = (
|
||||||
self.env['res.partner']
|
self.env['res.partner']
|
||||||
.sudo()
|
.sudo()
|
||||||
|
|
@ -858,6 +867,16 @@ class Patient(models.Model):
|
||||||
patient_vals['team_ids'] = vals['team_ids']
|
patient_vals['team_ids'] = vals['team_ids']
|
||||||
if 'date_of_birth' in vals and vals['date_of_birth']:
|
if 'date_of_birth' in vals and vals['date_of_birth']:
|
||||||
patient_vals['date_of_birth'] = vals['date_of_birth']
|
patient_vals['date_of_birth'] = vals['date_of_birth']
|
||||||
|
# Status fields from portal (treatment professionals only)
|
||||||
|
if vals.get('match_status'):
|
||||||
|
patient_vals['match_status'] = vals.get('match_status')
|
||||||
|
if vals.get('practice_status'):
|
||||||
|
patient_vals['practice_status'] = vals.get('practice_status')
|
||||||
|
# Other optional patient fields
|
||||||
|
if 'allergies' in vals:
|
||||||
|
patient_vals['allergies'] = vals.get('allergies') or False
|
||||||
|
if 'team_info_notes' in vals:
|
||||||
|
patient_vals['team_info_notes'] = vals.get('team_info_notes') or False
|
||||||
|
|
||||||
# Create patient with tracking disabled to avoid triggering mail/report side-effects
|
# Create patient with tracking disabled to avoid triggering mail/report side-effects
|
||||||
# Also disable auto-subscriptions on creation
|
# Also disable auto-subscriptions on creation
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,266 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
|
<!-- Reusable partial: Player Create/Edit Form Body (used by standalone and team create views) -->
|
||||||
|
<template id="portal_player_form_body" name="Player Form Body">
|
||||||
|
<!-- Player Details Card -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">Player Details</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>First Name <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" name="first_name" class="form-control" required="required"
|
||||||
|
t-att-value="(form_data and form_data.get('first_name')) or (patient and patient.first_name) or first_name or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Last Name <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" name="last_name" class="form-control" required="required"
|
||||||
|
t-att-value="(form_data and form_data.get('last_name')) or (patient and patient.last_name) or last_name or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Email</label>
|
||||||
|
<input type="email" name="email" class="form-control"
|
||||||
|
t-att-value="(form_data and form_data.get('email')) or (patient and patient.email) or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Phone</label>
|
||||||
|
<input type="tel" name="phone" class="form-control"
|
||||||
|
t-att-value="(form_data and form_data.get('phone')) or (patient and patient.phone) or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Date of Birth <span class="text-danger">*</span></label>
|
||||||
|
<input type="date" name="date_of_birth" class="form-control" required="required"
|
||||||
|
t-att-value="(form_data and form_data.get('date_of_birth')) or (patient_info and patient_info.get('date_of_birth')) or (patient and patient.date_of_birth) or date_of_birth or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TP-only additional info placed within Player Details -->
|
||||||
|
<t t-if="is_treatment_prof">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="allergies">Allergies</label>
|
||||||
|
<textarea name="allergies" id="allergies" class="form-control" rows="2" placeholder="Enter allergies information here"><t t-esc="(form_data and form_data.get('allergies')) or (patient_info and patient_info.get('allergies')) or ''"/></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="team_info_notes">Internal Notes</label>
|
||||||
|
<textarea name="team_info_notes" id="team_info_notes" class="form-control" rows="3" placeholder="Enter internal notes here"><t t-esc="(form_data and form_data.get('team_info_notes')) or (patient_info and patient_info.get('team_info_notes')) or ''"/></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Card (TP only) -->
|
||||||
|
<t t-if="is_treatment_prof">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">Status</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Match Status</label>
|
||||||
|
<select name="match_status" class="form-control" id="create_match_status">
|
||||||
|
<option value="yes" t-att-selected="'selected' if ((form_data and form_data.get('match_status') == 'yes') or (not form_data and patient_info and patient_info.get('match_status') == 'yes')) else None">Yes</option>
|
||||||
|
<option value="no" t-att-selected="'selected' if ((form_data and form_data.get('match_status') == 'no') or (not form_data and patient_info and patient_info.get('match_status') == 'no')) else None">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Practice Status</label>
|
||||||
|
<select name="practice_status" class="form-control" id="create_practice_status">
|
||||||
|
<option value="yes" t-att-selected="'selected' if ((form_data and form_data.get('practice_status') == 'yes') or (not form_data and patient_info and patient_info.get('practice_status') == 'yes')) else None">Yes</option>
|
||||||
|
<option value="no_contact" t-att-selected="'selected' if ((form_data and form_data.get('practice_status') == 'no_contact') or (not form_data and patient_info and patient_info.get('practice_status') == 'no_contact')) else None">Yes, no contact</option>
|
||||||
|
<option value="no" t-att-selected="'selected' if ((form_data and form_data.get('practice_status') == 'no') or (not form_data and patient_info and patient_info.get('practice_status') == 'no')) else None">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Address Card -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">Address Information</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Street Address</label>
|
||||||
|
<input type="text" name="street" class="form-control"
|
||||||
|
t-att-value="(form_data and form_data.get('street')) or (patient and patient.street) or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Street Address 2</label>
|
||||||
|
<input type="text" name="street2" class="form-control"
|
||||||
|
t-att-value="(form_data and form_data.get('street2')) or (patient and patient.street2) or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>City</label>
|
||||||
|
<input type="text" name="city" class="form-control"
|
||||||
|
t-att-value="(form_data and form_data.get('city')) or (patient and patient.city) or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>ZIP/Postal Code</label>
|
||||||
|
<input type="text" name="zip" class="form-control"
|
||||||
|
t-att-value="(form_data and form_data.get('zip')) or (patient and patient.zip) or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Province/Territory</label>
|
||||||
|
<select name="state_id" class="form-control">
|
||||||
|
<option value="">Select Province/Territory...</option>
|
||||||
|
<t t-foreach="states" t-as="state">
|
||||||
|
<option t-att-value="state.id"
|
||||||
|
t-att-selected="'selected' if (form_data and form_data.get('state_id') and int(form_data.get('state_id')) == state.id) or (not form_data and patient and patient.state_id and patient.state_id.id == state.id) else None"
|
||||||
|
t-esc="state.name"/>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Country</label>
|
||||||
|
<select name="country_id" class="form-control">
|
||||||
|
<t t-if="countries">
|
||||||
|
<option t-att-value="countries.id"
|
||||||
|
t-att-selected="'selected' if (form_data and form_data.get('country_id') and int(form_data.get('country_id')) == countries.id) or (not form_data and patient and patient.country_id and patient.country_id.id == countries.id) else None"
|
||||||
|
t-esc="countries.name"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<option value="">No country available</option>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Primary Emergency Contact (TP only) -->
|
||||||
|
<t t-if="is_treatment_prof">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">Primary Emergency Contact</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Contact Name</label>
|
||||||
|
<input type="text" name="ec_name" class="form-control" placeholder="e.g., Jane Doe"
|
||||||
|
t-att-value="(form_data and form_data.get('ec_name')) or ec_name or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Relationship</label>
|
||||||
|
<select name="ec_contact_type" class="form-select">
|
||||||
|
<option value="">Select...</option>
|
||||||
|
<t t-foreach="relationship_types" t-as="rel">
|
||||||
|
<option t-att-value="rel[0]"
|
||||||
|
t-att-selected="'selected' if ((form_data and form_data.get('ec_contact_type') == rel[0]) or (not form_data and ec_contact_type and ec_contact_type == rel[0])) else None"
|
||||||
|
t-esc="rel[1]"/>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Mobile</label>
|
||||||
|
<input type="tel" name="ec_mobile" class="form-control" placeholder="e.g., +1 555-555-1234"
|
||||||
|
t-att-value="(form_data and form_data.get('ec_mobile')) or ec_mobile or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Email</label>
|
||||||
|
<input type="email" name="ec_email" class="form-control" placeholder="name@example.com"
|
||||||
|
t-att-value="(form_data and form_data.get('ec_email')) or ec_email or ''"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Assign Teams (TP only) -->
|
||||||
|
<t t-if="is_treatment_prof">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">Assign Teams</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Teams</label>
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 g-2">
|
||||||
|
<t t-foreach="all_teams" t-as="t">
|
||||||
|
<div class="col">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox"
|
||||||
|
t-att-id="'team_ids_' + str(t.id)"
|
||||||
|
name="team_ids" t-att-value="t.id"
|
||||||
|
t-att-checked="'checked' if ((form_data and form_data.get('team_ids') and (str(t.id) in (form_data.get('team_ids') if isinstance(form_data.get('team_ids'), list) else [form_data.get('team_ids')]))) or (not form_data and patient and (t.id in patient.team_ids.ids)) or (prefill_team_id and t.id == prefill_team_id)) else None"/>
|
||||||
|
<label class="form-check-label" t-att-for="'team_ids_' + str(t.id)">
|
||||||
|
<t t-esc="t.name"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
<!-- Template for editing player information -->
|
<!-- Template for editing player information -->
|
||||||
<template id="portal_edit_player" name="Edit Player">
|
<template id="portal_edit_player" name="Edit Player">
|
||||||
<t t-call="portal.portal_layout">
|
<t t-call="portal.portal_layout">
|
||||||
|
|
@ -8,9 +269,12 @@
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h2>Edit Player Information</h2>
|
<h2>Edit Player Information</h2>
|
||||||
|
|
||||||
<t t-if="error" t-out="error" class="alert alert-danger"/>
|
<t t-if="error">
|
||||||
|
<div class="alert alert-danger" t-esc="error"/>
|
||||||
|
</t>
|
||||||
|
<div id="edit-player-client-error" class="alert alert-danger d-none"></div>
|
||||||
|
|
||||||
<form action="/my/player/save" method="post">
|
<form action="/my/player/save" method="post" id="edit-player-form">
|
||||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
<input type="hidden" name="patient_id" t-att-value="patient.id"/>
|
<input type="hidden" name="patient_id" t-att-value="patient.id"/>
|
||||||
<input type="hidden" name="return_url" t-att-value="return_url"/>
|
<input type="hidden" name="return_url" t-att-value="return_url"/>
|
||||||
|
|
@ -28,265 +292,54 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Team membership actions: Remove/Request Removal -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5>Team Membership</h5>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm align-middle">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Team</th>
|
|
||||||
<th class="text-end">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<t t-foreach="teams" t-as="team">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a t-attf-href="/my/team?team_id={{ team.id }}">
|
|
||||||
<t t-esc="team.name"/>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="text-end">
|
|
||||||
<t t-set="is_treatment_prof" t-value="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional') or request.env.user.has_group('base.group_system')"/>
|
|
||||||
<t t-set="is_team_staff" t-value="request.env.user.partner_id in team.staff_ids.user_ids.partner_id"/>
|
|
||||||
<!-- Direct remove for treatment professionals -->
|
|
||||||
<t t-if="is_treatment_prof">
|
|
||||||
<button type="submit"
|
|
||||||
class="btn btn-danger btn-sm"
|
|
||||||
t-att-formaction="'/my/team/' + str(team.id) + '/player/' + str(patient.id) + '/remove'"
|
|
||||||
formmethod="post"
|
|
||||||
formnovalidate="formnovalidate"
|
|
||||||
onclick="return confirm('Are you sure you want to remove this player from the team?');">
|
|
||||||
<i class="fa fa-user-times"/> Remove
|
|
||||||
</button>
|
|
||||||
</t>
|
|
||||||
<!-- Request removal for team staff (e.g., coaches) -->
|
|
||||||
<t t-elif="is_team_staff">
|
|
||||||
<button type="button"
|
|
||||||
class="btn bg-o-color-2 text-white btn-sm"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
t-att-data-bs-target="'#requestRemovalModalEdit' + str(team.id)">
|
|
||||||
<i class="fa fa-flag"/> Request Removal
|
|
||||||
</button>
|
|
||||||
<!-- Request Removal Modal -->
|
|
||||||
<div t-att-id="'requestRemovalModalEdit' + str(team.id)" class="modal fade" tabindex="-1" t-attf-aria-labelledby="requestRemovalModalEditLabel#{team.id}" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" t-attf-id="requestRemovalModalEditLabel#{team.id}">Request Player Removal</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<form t-attf-action="/my/team/{{ team.id }}/player/{{ patient.id }}/request_removal" method="post">
|
|
||||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Please provide a reason for removing this player from the team. This will create a task for the head therapist to review.</p>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Reason</label>
|
|
||||||
<textarea name="reason" class="form-control" rows="3" required="required" placeholder="Please explain why this player should be removed from the team"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn bg-o-color-2 text-white" data-bs-dismiss="modal">Cancel</button>
|
|
||||||
<button type="submit" class="btn bg-o-color-1 text-white">Submit Request</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</t>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
<!-- Unified form content via shared partial -->
|
||||||
<div class="col-md-6">
|
<t t-call="bemade_sports_clinic.portal_player_form_body"/>
|
||||||
<div class="form-group">
|
|
||||||
<label for="first_name">First Name <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" name="first_name" id="first_name" class="form-control"
|
|
||||||
required="required" t-att-value="patient.first_name"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="last_name">Last Name <span class="text-danger">*</span></label>
|
|
||||||
<input type="text" name="last_name" id="last_name" class="form-control"
|
|
||||||
required="required" t-att-value="patient.last_name"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
<!-- Client-side validation for status fields -->
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">Email</label>
|
|
||||||
<input type="email" name="email" id="email" class="form-control"
|
|
||||||
t-att-value="patient.email"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="phone">Phone</label>
|
|
||||||
<input type="tel" name="phone" id="phone" class="form-control"
|
|
||||||
t-att-value="patient.phone"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Address Information -->
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5>Address Information</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="street">Street Address</label>
|
|
||||||
<input type="text" name="street" id="street" class="form-control"
|
|
||||||
t-att-value="patient.street"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="street2">Street Address 2</label>
|
|
||||||
<input type="text" name="street2" id="street2" class="form-control"
|
|
||||||
t-att-value="patient.street2"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="city">City</label>
|
|
||||||
<input type="text" name="city" id="city" class="form-control"
|
|
||||||
t-att-value="patient.city"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="zip">ZIP/Postal Code</label>
|
|
||||||
<input type="text" name="zip" id="zip" class="form-control"
|
|
||||||
t-att-value="patient.zip"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="state_id">Province/Territory</label>
|
|
||||||
<select name="state_id" id="state_id" class="form-control">
|
|
||||||
<option value="">Select Province/Territory...</option>
|
|
||||||
<t t-foreach="states" t-as="state">
|
|
||||||
<option t-att-value="state.id"
|
|
||||||
t-att-selected="'selected' if patient.state_id and patient.state_id.id == state.id else None"
|
|
||||||
t-esc="state.name"/>
|
|
||||||
</t>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="country_id">Country</label>
|
|
||||||
<select name="country_id" id="country_id" class="form-control">
|
|
||||||
<t t-if="countries">
|
|
||||||
<option t-att-value="countries.id" selected="selected" t-esc="countries.name"/>
|
|
||||||
</t>
|
|
||||||
<t t-else="">
|
|
||||||
<option value="">No country available</option>
|
|
||||||
</t>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional fields accessible only to treatment professionals -->
|
|
||||||
<t t-if="is_treatment_prof">
|
<t t-if="is_treatment_prof">
|
||||||
<div class="row mb-3">
|
<script type="text/javascript">
|
||||||
<div class="col-md-6">
|
//<![CDATA[
|
||||||
<div class="form-group">
|
(function() {
|
||||||
<label for="date_of_birth">Date of Birth</label>
|
function showError(msg){
|
||||||
<input type="date" name="date_of_birth" id="date_of_birth" class="form-control"
|
var el = document.getElementById('edit-player-client-error');
|
||||||
t-att-value="patient_info.get('date_of_birth')"/>
|
if(!el) return;
|
||||||
</div>
|
el.textContent = msg;
|
||||||
</div>
|
el.classList.remove('d-none');
|
||||||
<div class="col-md-6">
|
}
|
||||||
<div class="form-group">
|
function clearError(){
|
||||||
<label for="age">Age</label>
|
var el = document.getElementById('edit-player-client-error');
|
||||||
<input type="text" name="age" id="age" class="form-control" readonly="readonly"
|
if(!el) return;
|
||||||
t-att-value="patient_info.get('age')"/>
|
el.textContent = '';
|
||||||
</div>
|
el.classList.add('d-none');
|
||||||
</div>
|
}
|
||||||
</div>
|
function isValidCombo(matchVal, practiceVal){
|
||||||
|
var valid = {
|
||||||
<div class="row mb-3">
|
'yes': {'yes': true},
|
||||||
<div class="col-12">
|
'no': {'yes': true, 'no_contact': true, 'no': true}
|
||||||
<div class="form-group">
|
};
|
||||||
<label for="allergies">Allergies</label>
|
return !!(valid[matchVal] && valid[matchVal][practiceVal]);
|
||||||
<textarea name="allergies" id="allergies" class="form-control"
|
}
|
||||||
rows="2" placeholder="Enter allergies information here"
|
var form = document.getElementById('edit-player-form');
|
||||||
t-esc="patient_info.get('allergies') or ''"/>
|
if(form){
|
||||||
</div>
|
form.addEventListener('submit', function(e){
|
||||||
</div>
|
var mEl = document.getElementById('match_status') || document.getElementById('create_match_status');
|
||||||
</div>
|
var pEl = document.getElementById('practice_status') || document.getElementById('create_practice_status');
|
||||||
|
var m = (mEl||{}).value;
|
||||||
<div class="row mb-3">
|
var p = (pEl||{}).value;
|
||||||
<div class="col-12">
|
clearError();
|
||||||
<div class="form-group">
|
if(m && p && !isValidCombo(m, p)){
|
||||||
<label for="team_info_notes">Team Notes</label>
|
e.preventDefault();
|
||||||
<textarea name="team_info_notes" id="team_info_notes" class="form-control"
|
showError('Invalid combination of match and practice status.');
|
||||||
rows="4" placeholder="Enter team notes here"
|
}
|
||||||
t-esc="patient_info.get('team_info_notes') or ''"/>
|
});
|
||||||
</div>
|
}
|
||||||
</div>
|
})();
|
||||||
</div>
|
//]]>
|
||||||
|
</script>
|
||||||
<!-- Status fields -->
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="match_status">Match Status</label>
|
|
||||||
<select name="match_status" id="match_status" class="form-control">
|
|
||||||
<option value="yes" t-att-selected="patient_info.get('match_status') == 'yes'">Yes</option>
|
|
||||||
<option value="no" t-att-selected="patient_info.get('match_status') == 'no'">No</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="practice_status">Practice Status</label>
|
|
||||||
<select name="practice_status" id="practice_status" class="form-control">
|
|
||||||
<option value="yes" t-att-selected="patient_info.get('practice_status') == 'yes'">Yes</option>
|
|
||||||
<option value="no_contact" t-att-selected="patient_info.get('practice_status') == 'no_contact'">Yes, no contact</option>
|
|
||||||
<option value="no" t-att-selected="patient_info.get('practice_status') == 'no'">No</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Injury information -->
|
|
||||||
<div class="row mb-3" t-if="patient_info.get('injured_since')">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Injured Since</label>
|
|
||||||
<p class="form-control-static" t-esc="patient_info.get('injured_since')"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<a t-att-href="return_url" class="btn bg-o-color-2 text-white">Cancel</a>
|
<a t-att-href="return_url" class="btn bg-o-color-2 text-white">Cancel</a>
|
||||||
|
|
@ -482,6 +535,13 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Error alert (e.g., invalid DOB on create_full) -->
|
||||||
|
<div t-if="error" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="fa fa-exclamation-triangle me-2"/>
|
||||||
|
<span t-esc="error"/>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form t-attf-action="/my/team/{{ team.id }}/player/add_link" method="get" class="row g-3">
|
<form t-attf-action="/my/team/{{ team.id }}/player/add_link" method="get" class="row g-3">
|
||||||
|
|
@ -574,30 +634,16 @@
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-header">Create New Player</div>
|
<div class="card-header">Create New Player</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form t-attf-action="/my/team/{{ team.id }}/player/create" method="post" class="row g-3">
|
<form t-attf-action="/my/team/{{ team.id }}/player/create_full" method="post">
|
||||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">First Name <span class="text-danger">*</span></label>
|
<!-- Reusable form body -->
|
||||||
<input type="text" name="first_name" class="form-control" required="required" t-att-value="first_name"/>
|
<t t-set="prefill_team_id" t-value="team.id"/>
|
||||||
</div>
|
<t t-call="bemade_sports_clinic.portal_player_form_body"/>
|
||||||
<div class="col-md-3">
|
<div class="row mt-3">
|
||||||
<label class="form-label">Last Name <span class="text-danger">*</span></label>
|
<div class="col-12 d-flex justify-content-end">
|
||||||
<input type="text" name="last_name" class="form-control" required="required" t-att-value="last_name"/>
|
<button type="submit" class="btn bg-o-color-1 text-white"><i class="fa fa-save me-1"/> Create Player</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Date of Birth <span class="text-danger">*</span></label>
|
|
||||||
<input type="date" name="date_of_birth" class="form-control" t-att-value="date_of_birth" required="required"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Email</label>
|
|
||||||
<input type="email" name="email" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Phone</label>
|
|
||||||
<input type="tel" name="phone" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 d-flex justify-content-end">
|
|
||||||
<button type="submit" class="btn bg-o-color-1 text-white"><i class="fa fa-save me-1"/> Create Player</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -607,4 +653,123 @@
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template id="portal_create_player" name="Portal Create Player">
|
||||||
|
<t t-call="portal.portal_layout">
|
||||||
|
<t t-set="title" t-value="'Create Player'"/>
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-10 col-lg-9">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h3 class="mb-0" t-esc="title"/>
|
||||||
|
<a href="/my/players" class="btn bg-o-color-2 text-white"><i class="fa fa-arrow-left me-1"/> Back to Players</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t t-if="error">
|
||||||
|
<div class="alert alert-danger" t-esc="error"/>
|
||||||
|
</t>
|
||||||
|
<div id="create-player-client-error" class="alert alert-danger d-none"></div>
|
||||||
|
|
||||||
|
<!-- Search Form -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="/my/player/create" method="get" class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">First Name</label>
|
||||||
|
<input type="text" name="first_name" class="form-control" t-att-value="first_name"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Last Name</label>
|
||||||
|
<input type="text" name="last_name" class="form-control" t-att-value="last_name"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Date of Birth</label>
|
||||||
|
<input type="date" name="date_of_birth" class="form-control" t-att-value="date_of_birth"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 d-flex justify-content-end">
|
||||||
|
<button type="submit" class="btn bg-o-color-1 text-white"><i class="fa fa-search me-1"/> Search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Results -->
|
||||||
|
<t t-if="searched">
|
||||||
|
<t t-set="has_active" t-value="len(active_results) > 0"/>
|
||||||
|
<t t-set="has_archived" t-value="len(archived_results) > 0"/>
|
||||||
|
|
||||||
|
<t t-if="has_active or has_archived">
|
||||||
|
<div class="row g-3">
|
||||||
|
<t t-if="has_active">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-success text-white">Active Matches</div>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<t t-foreach="active_results" t-as="p">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold" t-esc="p['name']"/>
|
||||||
|
<small class="text-muted">DOB: <t t-esc="p['date_of_birth'] or '-'"/></small>
|
||||||
|
</div>
|
||||||
|
<a t-attf-href="/my/player?player_id={{ p['id'] }}" class="btn btn-outline-primary btn-sm">View</a>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-if="has_archived">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-secondary text-white">Archived Matches</div>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<t t-foreach="archived_results" t-as="p">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold" t-esc="p['name']"/>
|
||||||
|
<small class="text-muted">DOB: <t t-esc="p['date_of_birth'] or '-'"/></small>
|
||||||
|
</div>
|
||||||
|
<a t-attf-href="/my/player?player_id={{ p['id'] }}" class="btn btn-outline-primary btn-sm">View</a>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Only show create when no matches -->
|
||||||
|
<t t-if="not has_active and not has_archived">
|
||||||
|
<div class="alert alert-info">No matching players found. You can create a new one below.</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Create Form: shown only when searched and there are no matches -->
|
||||||
|
<t t-if="searched and not (len(active_results) or len(archived_results))">
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">New Player Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form t-attf-action="/my/player/create/save" method="post" id="create-player-form">
|
||||||
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
|
|
||||||
|
<!-- Reusable form body -->
|
||||||
|
<t t-set="prefill_team_id" t-value="None"/>
|
||||||
|
<t t-call="bemade_sports_clinic.portal_player_form_body"/>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 d-flex justify-content-end">
|
||||||
|
<button type="submit" class="btn bg-o-color-1 text-white"><i class="fa fa-save me-1"/> Create Player</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,13 @@
|
||||||
<t t-call="portal.portal_layout">
|
<t t-call="portal.portal_layout">
|
||||||
<h1>Your Players</h1>
|
<h1>Your Players</h1>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end mb-3">
|
||||||
|
<a t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')"
|
||||||
|
href="/my/player/create" class="btn bg-o-color-1 text-white">
|
||||||
|
<i class="fa fa-plus"/> Create Player
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="container-fluid mb-3">
|
<div class="container-fluid mb-3">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
@ -635,7 +642,7 @@
|
||||||
<div class="col-md-3" t-if="player.stage">
|
<div class="col-md-3" t-if="player.stage">
|
||||||
<h6 class="mb-1">Status</h6>
|
<h6 class="mb-1">Status</h6>
|
||||||
<span t-if="player.stage == 'healthy'" class="text-success font-weight-bold h5">
|
<span t-if="player.stage == 'healthy'" class="text-success font-weight-bold h5">
|
||||||
<i class="fa fa-heart"></i> Play OK
|
<i class="fa fa-thumbs-up"></i> Play OK
|
||||||
</span>
|
</span>
|
||||||
<span t-elif="player.stage == 'practice_ok'" class="text-warning font-weight-bold h5">
|
<span t-elif="player.stage == 'practice_ok'" class="text-warning font-weight-bold h5">
|
||||||
<i class="fa fa-running"></i> Practice OK
|
<i class="fa fa-running"></i> Practice OK
|
||||||
|
|
@ -647,7 +654,7 @@
|
||||||
<i class="fa fa-question-circle"></i> <span t-esc="player.stage"/>
|
<i class="fa fa-question-circle"></i> <span t-esc="player.stage"/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3" t-if="player.allergies">
|
<div class="col-md-3" t-if="(player.allergies or '').replace('\xa0','').strip()">
|
||||||
<h6 class="mb-1">Allergies</h6>
|
<h6 class="mb-1">Allergies</h6>
|
||||||
<div class="alert alert-warning py-2 mb-0">
|
<div class="alert alert-warning py-2 mb-0">
|
||||||
<i class="fa fa-exclamation-triangle"></i>
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
|
|
@ -819,8 +826,8 @@
|
||||||
<td><strong>Last Consultation:</strong></td>
|
<td><strong>Last Consultation:</strong></td>
|
||||||
<td t-esc="player.last_consultation_date"/>
|
<td t-esc="player.last_consultation_date"/>
|
||||||
</tr>
|
</tr>
|
||||||
<tr t-if="is_treatment_prof and player.allergies">
|
<tr t-if="is_treatment_prof and (player.allergies or '').replace('\xa0','').strip()">
|
||||||
<td><strong>Allergies:</strong></td>
|
<td><strong>Allergies:</strong></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="alert alert-warning mb-0 p-2">
|
<div class="alert alert-warning mb-0 p-2">
|
||||||
<i class="fa fa-exclamation-triangle"></i>
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
|
|
@ -937,7 +944,7 @@
|
||||||
<tr t-if="player.stage">
|
<tr t-if="player.stage">
|
||||||
<td><strong>Status:</strong></td>
|
<td><strong>Status:</strong></td>
|
||||||
<td>
|
<td>
|
||||||
<span t-if="player.stage == 'healthy'" class="text-success font-weight-bold"><i class="fa fa-heart"></i> Play OK</span>
|
<span t-if="player.stage == 'healthy'" class="text-success font-weight-bold"><i class="fa fa-thumbs-up"></i> Play OK</span>
|
||||||
<span t-elif="player.stage == 'practice_ok'" class="text-warning font-weight-bold"><i class="fa fa-running"></i> Practice OK</span>
|
<span t-elif="player.stage == 'practice_ok'" class="text-warning font-weight-bold"><i class="fa fa-running"></i> Practice OK</span>
|
||||||
<span t-elif="player.stage == 'no_play'" class="text-danger font-weight-bold"><i class="fa fa-ban"></i> Injured</span>
|
<span t-elif="player.stage == 'no_play'" class="text-danger font-weight-bold"><i class="fa fa-ban"></i> Injured</span>
|
||||||
<span t-else="" class="text-muted" t-esc="player.stage"/>
|
<span t-else="" class="text-muted" t-esc="player.stage"/>
|
||||||
|
|
@ -1049,7 +1056,7 @@
|
||||||
<!-- Medical Information Tab (Treatment Professionals Only) -->
|
<!-- Medical Information Tab (Treatment Professionals Only) -->
|
||||||
<div class="tab-pane fade" id="medical-info" role="tabpanel" aria-labelledby="medical-info-tab" t-if="is_treatment_prof">
|
<div class="tab-pane fade" id="medical-info" role="tabpanel" aria-labelledby="medical-info-tab" t-if="is_treatment_prof">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6" t-if="patient_info.get('allergies')">
|
<div class="col-md-6" t-if="(patient_info.get('allergies') or '').replace('\xa0','').strip()">
|
||||||
<div class="card border-warning">
|
<div class="card border-warning">
|
||||||
<div class="card-header bg-warning text-dark">
|
<div class="card-header bg-warning text-dark">
|
||||||
<i class="fa fa-exclamation-triangle"></i> <strong>Allergies</strong>
|
<i class="fa fa-exclamation-triangle"></i> <strong>Allergies</strong>
|
||||||
|
|
@ -1069,7 +1076,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12" t-if="not patient_info.get('allergies') and not patient_info.get('team_info_notes')">
|
<div class="col-12" t-if="not (patient_info.get('allergies') or '').replace('\xa0','').strip() and not (patient_info.get('team_info_notes') or '').replace('\xa0','').strip()">
|
||||||
<div class="text-center text-muted py-4">
|
<div class="text-center text-muted py-4">
|
||||||
<i class="fa fa-notes-medical fa-3x mb-3"></i>
|
<i class="fa fa-notes-medical fa-3x mb-3"></i>
|
||||||
<p>No medical information available</p>
|
<p>No medical information available</p>
|
||||||
|
|
@ -1183,18 +1190,22 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- Success/Error Messages -->
|
<!-- Success/Error Messages -->
|
||||||
<t t-if="error" class="alert alert-danger" role="alert">
|
<div t-if="error" class="alert alert-danger" role="alert">
|
||||||
<i class="fa fa-exclamation-triangle me-2"/>
|
<i class="fa fa-exclamation-triangle me-2"/>
|
||||||
<span t-esc="error"/>
|
<span t-esc="error"/>
|
||||||
</t>
|
</div>
|
||||||
<t t-if="success" class="alert alert-success" role="alert">
|
<div t-if="success" class="alert alert-success" role="alert">
|
||||||
<i class="fa fa-check-circle me-2"/>
|
<i class="fa fa-check-circle me-2"/>
|
||||||
<span t-esc="success"/>
|
<span t-esc="success"/>
|
||||||
</t>
|
</div>
|
||||||
|
|
||||||
<form t-attf-action="/my/team/{{ team.id }}/add_player/submit" method="post" class="mt-3">
|
<form t-attf-action="/my/team/{{ team.id }}/add_player/submit" method="post" class="mt-3">
|
||||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
<input type="hidden" name="team_id" t-att-value="team.id"/>
|
<input type="hidden" name="team_id" t-att-value="team.id"/>
|
||||||
|
<!-- Player Details -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<h5 class="mb-0">Player Details</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="first_name" class="form-label">First Name *</label>
|
<label for="first_name" class="form-label">First Name *</label>
|
||||||
|
|
@ -1226,6 +1237,27 @@
|
||||||
t-att-value="date_of_birth or ''" required="required"/>
|
t-att-value="date_of_birth or ''" required="required"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Status fields (Treatment Professionals only) -->
|
||||||
|
<t t-if="user_has_group('bemade_sports_clinic.group_portal_treatment_professional') or user_has_group('base.group_system')">
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="match_status" class="form-label">Match Status</label>
|
||||||
|
<select class="form-select" id="match_status" name="match_status">
|
||||||
|
<option value="yes" t-att-selected="(match_status or '') == 'yes' and 'selected' or None">Yes</option>
|
||||||
|
<option value="no" t-att-selected="(match_status or '') == 'no' and 'selected' or None">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="practice_status" class="form-label">Practice Status</label>
|
||||||
|
<select class="form-select" id="practice_status" name="practice_status">
|
||||||
|
<option value="yes" t-att-selected="(practice_status or '') == 'yes' and 'selected' or None">Yes</option>
|
||||||
|
<option value="no_contact" t-att-selected="(practice_status or '') == 'no_contact' and 'selected' or None">Yes, no contact</option>
|
||||||
|
<option value="no" t-att-selected="(practice_status or '') == 'no' and 'selected' or None">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<a t-attf-href="/my/team?team_id={{ team.id }}" class="btn bg-o-color-2 text-white">
|
<a t-attf-href="/my/team?team_id={{ team.id }}" class="btn bg-o-color-2 text-white">
|
||||||
<i class="fa fa-times me-1"/> Cancel
|
<i class="fa fa-times me-1"/> Cancel
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue