portal(events): polish headers and detail view
- Stack action buttons onto a second line in list/detail views\n- Remove redundant team/venue under event title (retained in details card)\n- Minor styling/consistency tweaks
This commit is contained in:
parent
a47e829935
commit
06b9c6d201
19 changed files with 760 additions and 363 deletions
|
|
@ -101,7 +101,17 @@ This module provides a complete sports medicine clinic management solution with
|
|||
"application": True,
|
||||
"assets": {
|
||||
"web.assets_frontend": [
|
||||
# Ensure legacy jQuery helpers exist where some website widgets expect them
|
||||
"bemade_sports_clinic/static/src/js/jquery_scrolling_polyfill.js",
|
||||
# Defensive patch for website TOC snippet to avoid null textContent errors
|
||||
"bemade_sports_clinic/static/src/js/website_toc_safety_patch.js",
|
||||
"bemade_sports_clinic/static/src/scss/portal_badges.scss",
|
||||
],
|
||||
# Also load in lazy bundle since many website widgets initialize lazily
|
||||
"web.assets_frontend_lazy": [
|
||||
"bemade_sports_clinic/static/src/js/jquery_scrolling_polyfill.js",
|
||||
# Ensure patch is also present in lazy assets where snippet may initialize
|
||||
"bemade_sports_clinic/static/src/js/website_toc_safety_patch.js",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,6 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
'patient_id': patient.id,
|
||||
'diagnosis': post.get('diagnosis', ''),
|
||||
'external_notes': post.get('external_notes', ''),
|
||||
'stage': 'active',
|
||||
}
|
||||
|
||||
# Handle injury date and injury_date_na checkbox
|
||||
|
|
@ -153,7 +152,16 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
vals['internal_notes'] = post.get('internal_notes')
|
||||
|
||||
# Create the injury record - portal users now have create permission
|
||||
injury = request.env['sports.patient.injury'].create(vals)
|
||||
# Determine role flags to choose safe context
|
||||
is_internal_user = request.env.user.has_group('base.group_user')
|
||||
is_tp_internal = request.env.user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
is_tp_portal = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
suppress_notifications = not (is_internal_user or is_tp_internal or is_tp_portal)
|
||||
|
||||
env_injury = request.env['sports.patient.injury']
|
||||
if suppress_notifications:
|
||||
env_injury = env_injury.with_context(mail_notrack=True, mail_create_nolog=True, mail_create_nosubscribe=True)
|
||||
injury = env_injury.create(vals)
|
||||
|
||||
# Determine role for assignment behavior
|
||||
# Use request.env.user.has_group() directly to avoid security violations
|
||||
|
|
@ -174,7 +182,10 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
if selected_tp_ids:
|
||||
# Respect explicit selection only
|
||||
injury.write({'treatment_professional_ids': [(6, 0, selected_tp_ids)]})
|
||||
if suppress_notifications:
|
||||
injury.with_context(mail_notrack=True, mail_create_nolog=True, mail_create_nosubscribe=True).write({'treatment_professional_ids': [(6, 0, selected_tp_ids)]})
|
||||
else:
|
||||
injury.write({'treatment_professional_ids': [(6, 0, selected_tp_ids)]})
|
||||
else:
|
||||
# No explicit selection: perform team-based auto-assignment (if a single team context exists)
|
||||
if team_id:
|
||||
|
|
@ -204,7 +215,10 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
_logger.warning("No valid therapists found to assign to the injury for team %s", selected_team_id)
|
||||
|
||||
if team_tp_user_ids:
|
||||
injury.write({'treatment_professional_ids': [(6, 0, list(team_tp_user_ids))]})
|
||||
if suppress_notifications:
|
||||
injury.with_context(mail_notrack=True, mail_create_nolog=True, mail_create_nosubscribe=True).write({'treatment_professional_ids': [(6, 0, list(team_tp_user_ids))]})
|
||||
else:
|
||||
injury.write({'treatment_professional_ids': [(6, 0, list(team_tp_user_ids))]})
|
||||
else:
|
||||
_logger.info(
|
||||
"Skipping team-based therapist auto-assignment: patient %s has %s teams",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
"""
|
||||
user = request.env.user
|
||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
is_coach = request.env.user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
# Gather query params for search-first flow
|
||||
first_name = (get.get('first_name') or '').strip()
|
||||
|
|
@ -73,6 +74,7 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
values.update({
|
||||
'page_name': 'create_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'is_coach': is_coach,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
|
|
@ -95,6 +97,7 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
"""Handle standalone player creation submit."""
|
||||
user = request.env.user
|
||||
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
is_coach = user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
first_name = (post.get('first_name') or '').strip()
|
||||
last_name = (post.get('last_name') or '').strip()
|
||||
|
|
@ -132,6 +135,7 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
values.update({
|
||||
'page_name': 'create_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'is_coach': is_coach,
|
||||
'all_teams': all_teams,
|
||||
'states': states,
|
||||
'countries': countries,
|
||||
|
|
@ -330,8 +334,8 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
})
|
||||
return request.render('bemade_sports_clinic.portal_create_player', values)
|
||||
|
||||
# Optionally create a primary emergency contact if provided (TPs only)
|
||||
if is_treatment_prof:
|
||||
# Optionally create a primary emergency contact if provided (TPs and coaches)
|
||||
if is_treatment_prof or is_coach:
|
||||
ec_name = (post.get('ec_name') or '').strip()
|
||||
ec_type = (post.get('ec_contact_type') or '').strip()
|
||||
if ec_name and ec_type:
|
||||
|
|
@ -361,9 +365,10 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
return_url = post.get('return_url', f'/my/player?player_id={patient_id}')
|
||||
|
||||
# Check if user is a treatment professional
|
||||
# Check if user is a treatment professional or coach
|
||||
user = request.env.user
|
||||
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
is_coach = user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
teams = patient.team_ids
|
||||
# All teams for multi-select limited to current user's staff teams
|
||||
|
|
@ -373,6 +378,31 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
]).mapped('team_id').ids)
|
||||
], order='name')
|
||||
|
||||
# Determine team context from query or post and whether a coach can request removal
|
||||
# Only consider a team context if the current user is staff on that team and the player is a member
|
||||
raw_team_ctx = request.httprequest.args.get('team_id') or post.get('team_id')
|
||||
team_context_id = None
|
||||
if raw_team_ctx:
|
||||
try:
|
||||
cand_id = int(raw_team_ctx)
|
||||
# Validate access via shared mixin helper
|
||||
team_obj = self._check_team_access(cand_id, check_staff=True)
|
||||
if team_obj and team_obj in teams:
|
||||
team_context_id = cand_id
|
||||
except Exception:
|
||||
# Ignore invalid team context silently
|
||||
team_context_id = None
|
||||
|
||||
# Compute removal visibility/target for coaches
|
||||
player_team_count = len(teams)
|
||||
can_request_removal = bool(is_coach and ((team_context_id is not None) or (player_team_count == 1)))
|
||||
removal_team_id = None
|
||||
if can_request_removal:
|
||||
if team_context_id is not None:
|
||||
removal_team_id = team_context_id
|
||||
elif player_team_count == 1:
|
||||
removal_team_id = teams[0].id
|
||||
|
||||
# Create a dictionary with patient info for protected fields
|
||||
patient_info = {}
|
||||
|
||||
|
|
@ -426,6 +456,11 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
'return_url': return_url,
|
||||
'page_name': 'edit_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'is_coach': is_coach,
|
||||
# Coach removal request context
|
||||
'can_request_removal': can_request_removal,
|
||||
'removal_team_id': removal_team_id,
|
||||
'team_context_id': team_context_id,
|
||||
}
|
||||
|
||||
return request.render('bemade_sports_clinic.portal_edit_player', values)
|
||||
|
|
@ -443,8 +478,9 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
except UserError as e:
|
||||
return request.render('portal.403', {'error': str(e)})
|
||||
|
||||
# Check if user is a treatment professional
|
||||
# Check if user is a treatment professional or coach
|
||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
is_coach = request.env.user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
# If DOB is being changed/provided, validate format upfront to avoid unhelpful errors
|
||||
dob_str = (post.get('date_of_birth') or '').strip()
|
||||
|
|
@ -474,6 +510,7 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||
'page_name': 'edit_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'is_coach': is_coach,
|
||||
'error': _('Please enter a valid Date of Birth (YYYY-MM-DD).'),
|
||||
'form_data': dict(post),
|
||||
}
|
||||
|
|
@ -504,6 +541,7 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||
'page_name': 'edit_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'is_coach': is_coach,
|
||||
'error': _('Date of Birth must not be in the future and not be more than 120 years ago.'),
|
||||
'form_data': dict(post),
|
||||
}
|
||||
|
|
@ -623,6 +661,7 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||
'page_name': 'edit_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'is_coach': is_coach,
|
||||
'error': str(ve) or _('Invalid combination of match and practice status.'),
|
||||
'form_data': dict(post),
|
||||
}
|
||||
|
|
@ -651,13 +690,14 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
'return_url': post.get('return_url', f'/my/player?player_id={patient_id}'),
|
||||
'page_name': 'edit_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'is_coach': is_coach,
|
||||
'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:
|
||||
# Update or create primary emergency contact (TPs and coaches), regardless of patient field changes
|
||||
if is_treatment_prof or is_coach:
|
||||
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()
|
||||
|
|
@ -716,12 +756,13 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
return_url = post.get('return_url', f'/my/player?player_id={patient_id}')
|
||||
|
||||
# Check if user is a treatment professional
|
||||
# Check if user is a treatment professional or coach
|
||||
user = request.env.user
|
||||
is_treatment_prof = user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
is_coach = user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
# Regular coaches shouldn't be able to add emergency contacts
|
||||
if not is_treatment_prof:
|
||||
# Only TPs and coaches can add emergency contacts
|
||||
if not (is_treatment_prof or is_coach):
|
||||
return request.redirect(return_url)
|
||||
|
||||
values = {
|
||||
|
|
@ -746,11 +787,12 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
except UserError as e:
|
||||
return request.render('portal.403', {'error': str(e)})
|
||||
|
||||
# Check if user is a treatment professional
|
||||
# Check if user is a treatment professional or coach
|
||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
is_coach = request.env.user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
# Regular coaches shouldn't be able to add emergency contacts
|
||||
if not is_treatment_prof:
|
||||
# Only TPs and coaches can add emergency contacts
|
||||
if not (is_treatment_prof or is_coach):
|
||||
return request.redirect(f'/my/player?player_id={patient_id}')
|
||||
|
||||
# Required fields
|
||||
|
|
@ -798,11 +840,12 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
return_url = post.get('return_url', f'/my/player?player_id={patient.id}')
|
||||
|
||||
# Check if user is a treatment professional
|
||||
# Check if user is a treatment professional or coach
|
||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
is_coach = request.env.user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
# Regular coaches shouldn't be able to edit emergency contacts
|
||||
if not is_treatment_prof:
|
||||
# Only TPs and coaches can edit emergency contacts
|
||||
if not (is_treatment_prof or is_coach):
|
||||
return request.redirect(return_url)
|
||||
|
||||
values = {
|
||||
|
|
@ -835,9 +878,10 @@ class PlayerManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
# Check if user is a treatment professional
|
||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
is_coach = request.env.user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
|
||||
# Regular coaches shouldn't be able to edit emergency contacts
|
||||
if not is_treatment_prof:
|
||||
# Only TPs and coaches can edit emergency contacts
|
||||
if not (is_treatment_prof or is_coach):
|
||||
return request.redirect(f'/my/player?player_id={patient.id}')
|
||||
|
||||
# Required fields
|
||||
|
|
|
|||
|
|
@ -955,6 +955,14 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
if not first_name or not last_name:
|
||||
raise UserError(_('First name and last name are required'))
|
||||
# Require DOB and validate format for better triage by therapists
|
||||
if not dob:
|
||||
raise UserError(_('Date of birth is required'))
|
||||
try:
|
||||
# Validate date format (YYYY-MM-DD); will raise if invalid
|
||||
fields.Date.to_date(dob)
|
||||
except Exception:
|
||||
raise UserError(_('Please enter a valid Date of Birth (YYYY-MM-DD).'))
|
||||
|
||||
# Find head therapist (reuse model helper on team if available)
|
||||
# Fallback to admin if none
|
||||
|
|
@ -964,8 +972,14 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
if not head_user:
|
||||
head_user = request.env.ref('base.user_admin', raise_if_not_found=False)
|
||||
|
||||
# In Odoo 18, mail.activity requires res_model_id (m2o), not res_model (char)
|
||||
# Portal users typically cannot read ir.model; use sudo safely.
|
||||
model_rec = request.env['ir.model'].sudo().search([('model', '=', 'sports.team')], limit=1)
|
||||
if not model_rec:
|
||||
raise UserError(_('Internal error: target model not found. Please contact an administrator.'))
|
||||
model_id = model_rec.id
|
||||
activity_vals = {
|
||||
'res_model': 'sports.team',
|
||||
'res_model_id': model_id,
|
||||
'res_id': team.id,
|
||||
'summary': _('Coach requests player addition'),
|
||||
'note': _('Requested player: %s %s%s\nReason: %s') % (
|
||||
|
|
@ -978,6 +992,11 @@ class TeamManagementPortal(CustomerPortal, AccessControlMixin):
|
|||
if head_user:
|
||||
activity_vals['user_id'] = head_user.id
|
||||
|
||||
# Ensure activity is created as a To Do
|
||||
todo_type = request.env.ref('mail.mail_activity_data_todo', raise_if_not_found=False)
|
||||
if todo_type:
|
||||
activity_vals['activity_type_id'] = todo_type.id
|
||||
|
||||
request.env['mail.activity'].sudo().create(activity_vals)
|
||||
|
||||
request.session['notification'] = {
|
||||
|
|
|
|||
|
|
@ -286,6 +286,36 @@ class TeamStaffPortal(CustomerPortal):
|
|||
patient_info['allergies'] = player.allergies
|
||||
patient_info['team_info_notes'] = player.team_info_notes
|
||||
|
||||
# Compute removal request visibility for coaches on the player detail view
|
||||
# Conditions:
|
||||
# - user is a coach, and
|
||||
# - a valid team context is provided (user is staff on that team AND player belongs to that team), OR
|
||||
# - player belongs to exactly one team and user is staff on that team
|
||||
is_coach = user.has_group('bemade_sports_clinic.group_portal_team_coach')
|
||||
partner = user.partner_id
|
||||
staff_team_ids = set(partner.team_staff_rel_ids.mapped('team_id.id'))
|
||||
|
||||
player_team_ids = set(player.team_ids.ids)
|
||||
player_team_count = len(player_team_ids)
|
||||
|
||||
# Validate team context
|
||||
valid_team_context = False
|
||||
removal_team_id = None
|
||||
team_context_id = None
|
||||
if team:
|
||||
team_context_id = team.id
|
||||
if (team.id in staff_team_ids) and (team.id in player_team_ids):
|
||||
valid_team_context = True
|
||||
removal_team_id = team.id
|
||||
|
||||
# Fallback: single team membership case
|
||||
if not valid_team_context and player_team_count == 1:
|
||||
sole_team_id = next(iter(player_team_ids)) if player_team_ids else None
|
||||
if sole_team_id and sole_team_id in staff_team_ids:
|
||||
removal_team_id = sole_team_id
|
||||
|
||||
can_request_removal = bool(is_coach and removal_team_id)
|
||||
|
||||
return http.request.render(
|
||||
template='bemade_sports_clinic.portal_my_player_injuries',
|
||||
qcontext={
|
||||
|
|
@ -297,5 +327,9 @@ class TeamStaffPortal(CustomerPortal):
|
|||
'page_name': 'my_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
'patient_info': patient_info,
|
||||
# Removal request context for coaches
|
||||
'can_request_removal': can_request_removal,
|
||||
'removal_team_id': removal_team_id,
|
||||
'team_context_id': team_context_id,
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,4 +29,18 @@
|
|||
<field name="priority">100</field>
|
||||
</record>
|
||||
-->
|
||||
|
||||
<!-- Scheduled action to create verification activities for unverified injuries -->
|
||||
<record id="ir_cron_create_injury_verification_tasks" model="ir.cron">
|
||||
<field name="name">Create Injury Verification Activities</field>
|
||||
<field name="model_id" ref="model_sports_patient_injury"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_create_injury_verification_tasks()</field>
|
||||
<field name="interval_number">5</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="nextcall">2025-06-27 00:05:00</field>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="priority">80</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -621,18 +621,18 @@ class Patient(models.Model):
|
|||
if existing_activity:
|
||||
continue
|
||||
|
||||
# Find head therapist or fallback to any therapist
|
||||
head_therapist = player.team_ids[0].staff_ids.filtered(
|
||||
lambda s: s.role == 'therapist' and s.is_head_therapist
|
||||
)
|
||||
|
||||
if not head_therapist and len(player.team_ids[0].staff_ids) > 0:
|
||||
# Fallback to any therapist
|
||||
head_therapist = player.team_ids[0].staff_ids.filtered(
|
||||
lambda s: s.role == 'therapist'
|
||||
)
|
||||
|
||||
user_id = head_therapist.user_ids[0].id if head_therapist and head_therapist.user_ids else SUPERUSER_ID
|
||||
# Find head therapist (role == 'head_therapist') or fallback to any therapist
|
||||
team = player.team_ids[0]
|
||||
staff = team.staff_ids
|
||||
head_therapist = staff.filtered(lambda s: s.role == 'head_therapist' and s.user_ids)
|
||||
if not head_therapist:
|
||||
# Fallback to any therapist with a linked user
|
||||
head_therapist = staff.filtered(lambda s: s.role == 'therapist' and s.user_ids)
|
||||
|
||||
user_id = SUPERUSER_ID
|
||||
if head_therapist:
|
||||
# pick first linked user id
|
||||
user_id = head_therapist.user_ids[0].id
|
||||
|
||||
# Create the activity
|
||||
self.env['mail.activity'].create({
|
||||
|
|
|
|||
|
|
@ -197,6 +197,16 @@ class PatientInjury(models.Model):
|
|||
self.write({'stage': 'active'})
|
||||
message = _("Injury verified by %s") % self.env.user.name
|
||||
self.message_post(body=message)
|
||||
# Close any open verification activities for this injury
|
||||
model_rec = self.env['ir.model']._get('sports.patient.injury')
|
||||
verif_acts = self.env['mail.activity'].sudo().search([
|
||||
('res_model_id', '=', model_rec.id),
|
||||
('res_id', '=', self.id),
|
||||
('summary', '=', 'Verify injury'),
|
||||
('active', '=', True),
|
||||
])
|
||||
if verif_acts:
|
||||
verif_acts.action_done()
|
||||
return True
|
||||
|
||||
def action_resolve_injury(self):
|
||||
|
|
@ -238,10 +248,17 @@ class PatientInjury(models.Model):
|
|||
for rec in self:
|
||||
old_treatment_prof_ids[rec.id] = rec.treatment_professional_ids.ids
|
||||
|
||||
# Detect suppression context (portal coach flows)
|
||||
suppress_followers = bool(
|
||||
self.env.context.get('mail_create_nosubscribe')
|
||||
or self.env.context.get('mail_notrack')
|
||||
or self.env.context.get('suppress_portal_mail')
|
||||
)
|
||||
|
||||
res = super().write(vals)
|
||||
|
||||
# If treatment professionals changed, update subscriptions
|
||||
if 'treatment_professional_ids' in vals:
|
||||
# If treatment professionals changed, update subscriptions (unless suppressed)
|
||||
if not suppress_followers and 'treatment_professional_ids' in vals:
|
||||
for rec in self:
|
||||
# Only run subscription manager if treatment professionals actually changed
|
||||
if rec.id in old_treatment_prof_ids and set(old_treatment_prof_ids[rec.id]) != set(rec.treatment_professional_ids.ids):
|
||||
|
|
@ -263,8 +280,8 @@ class PatientInjury(models.Model):
|
|||
if msg:
|
||||
rec.message_post(body=msg)
|
||||
|
||||
# Also update subscriptions if internal_notes changes
|
||||
if 'internal_notes' in vals:
|
||||
# Also update subscriptions if internal_notes changes (unless suppressed)
|
||||
if not suppress_followers and 'internal_notes' in vals:
|
||||
for rec in self:
|
||||
rec._manage_treatment_professional_subscriptions()
|
||||
|
||||
|
|
@ -273,6 +290,13 @@ class PatientInjury(models.Model):
|
|||
def _manage_treatment_professional_subscriptions(self):
|
||||
"""Subscribe treatment professionals to both regular and internal note updates
|
||||
while ensuring non-treatment professionals only subscribe to external updates."""
|
||||
# Skip entirely when portal flows suppress mail operations to avoid mail.followers access
|
||||
if (
|
||||
self.env.context.get('mail_create_nosubscribe')
|
||||
or self.env.context.get('mail_notrack')
|
||||
or self.env.context.get('suppress_portal_mail')
|
||||
):
|
||||
return
|
||||
self.ensure_one()
|
||||
|
||||
# Get the message subtypes
|
||||
|
|
@ -371,27 +395,105 @@ class PatientInjury(models.Model):
|
|||
)
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _cron_create_injury_verification_tasks(self):
|
||||
"""Create verification activities for unverified injuries.
|
||||
Assign to head therapist(s) of the injury's team, falling back to therapists.
|
||||
Runs with sudo to avoid portal ACL constraints from coach-created injuries.
|
||||
Deduplicates by checking for existing open 'Verify injury' activities on the same injury/user.
|
||||
"""
|
||||
Activity = self.env['mail.activity'].sudo()
|
||||
Injury = self.sudo()
|
||||
# Find all unverified injuries
|
||||
injuries = Injury.search([('stage', '=', 'unverified')])
|
||||
if not injuries:
|
||||
return True
|
||||
|
||||
# Use generic To Do activity type
|
||||
todo_type = self.env.ref('mail.mail_activity_data_todo', raise_if_not_found=False)
|
||||
|
||||
for injury in injuries:
|
||||
# Determine target team
|
||||
team = injury.team_id
|
||||
if not team and injury.patient_id.team_ids:
|
||||
team = injury.patient_id.team_ids[:1]
|
||||
if not team:
|
||||
continue
|
||||
|
||||
# Find staff for this team prioritized by head_therapist then therapist
|
||||
staff = self.env['sports.team.staff'].sudo().search([
|
||||
('team_id', '=', team.id),
|
||||
('role', 'in', ['head_therapist', 'therapist'])
|
||||
])
|
||||
if not staff:
|
||||
continue
|
||||
|
||||
# Prefer head therapists
|
||||
# Resolve model id once
|
||||
model_rec = self.env['ir.model']._get('sports.patient.injury')
|
||||
existing = Activity.search([
|
||||
('res_model_id', '=', model_rec.id),
|
||||
('res_id', '=', injury.id),
|
||||
('summary', '=', 'Verify injury'),
|
||||
('active', '=', True),
|
||||
], limit=1)
|
||||
if existing:
|
||||
continue
|
||||
|
||||
# Choose a single assignee: prefer first head therapist user, else first therapist user
|
||||
head_users = staff.filtered(lambda s: s.role == 'head_therapist').mapped('user_ids')[:1]
|
||||
therapist_users = staff.filtered(lambda s: s.role == 'therapist').mapped('user_ids')[:1]
|
||||
assignee = head_users or therapist_users
|
||||
if not assignee:
|
||||
continue
|
||||
|
||||
vals = {
|
||||
'res_model_id': model_rec.id,
|
||||
'res_id': injury.id,
|
||||
'user_id': assignee.id,
|
||||
'activity_type_id': todo_type.id if todo_type else False,
|
||||
'summary': 'Verify injury',
|
||||
'note': _("Please verify this injury and set the appropriate status."),
|
||||
'date_deadline': fields.Date.context_today(self),
|
||||
'automated': True,
|
||||
}
|
||||
Activity.create(vals)
|
||||
return True
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
# Determine context before creating to avoid mail.followers writes when portal/coach creates
|
||||
is_treatment_professional = self.env.user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
is_admin = self.env.user.has_group('base.group_system')
|
||||
is_internal_user = self.env.user.has_group('base.group_user')
|
||||
suppress_notifications = not (is_treatment_professional or is_admin or is_internal_user)
|
||||
|
||||
if suppress_notifications:
|
||||
# Disable auto-track, auto-log and auto-subscribe during create
|
||||
res = super(PatientInjury, self.with_context(
|
||||
mail_notrack=True,
|
||||
mail_create_nolog=True,
|
||||
mail_create_nosubscribe=True
|
||||
)).create(vals_list)
|
||||
else:
|
||||
res = super().create(vals_list)
|
||||
|
||||
for record in res:
|
||||
# Check if the current user is a treatment professional or admin
|
||||
is_treatment_professional = self.env.user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
is_admin = self.env.user.has_group('base.group_system')
|
||||
|
||||
# Creator role checks (re-use computed flags)
|
||||
# is_treatment_professional, is_admin, is_internal_user, suppress_notifications defined above
|
||||
|
||||
# Set initial stage without chatter/autosubscribe for portal/coach creators
|
||||
if is_treatment_professional or is_admin:
|
||||
# If created by a treatment professional or admin, set to active
|
||||
record.write({'stage': 'active'})
|
||||
record.with_context(mail_notrack=True, mail_create_nolog=True, mail_create_nosubscribe=True).write({'stage': 'active'})
|
||||
else:
|
||||
# Otherwise, set to unverified
|
||||
record.write({'stage': 'unverified'})
|
||||
record.with_context(mail_notrack=True, mail_create_nolog=True, mail_create_nosubscribe=True).write({'stage': 'unverified'})
|
||||
|
||||
# Automatically assign therapist when creating an injury
|
||||
current_user = self.env.user
|
||||
|
||||
# If the injury creator is a treatment professional, assign them
|
||||
if current_user.has_group('bemade_sports_clinic.group_sports_clinic_treatment_professional'):
|
||||
record.treatment_professional_ids = [(4, current_user.id)]
|
||||
record.with_context(mail_notrack=True, mail_create_nolog=True, mail_create_nosubscribe=True).treatment_professional_ids = [(4, current_user.id)]
|
||||
# Otherwise, if there's a team_id, find and assign team therapists
|
||||
elif record.team_id:
|
||||
# Find all team staff users
|
||||
|
|
@ -401,8 +503,6 @@ class PatientInjury(models.Model):
|
|||
|
||||
# Filter to only staff users who are treatment professionals
|
||||
if team_staff:
|
||||
treatment_professional_group = self.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
# Get all users from staff and filter them by group
|
||||
# Use direct group membership check instead of has_group() to avoid security violations
|
||||
staff_users = team_staff.mapped('user_ids')
|
||||
treatment_prof_group = self.env.ref('bemade_sports_clinic.group_sports_clinic_treatment_professional')
|
||||
|
|
@ -411,8 +511,12 @@ class PatientInjury(models.Model):
|
|||
)
|
||||
|
||||
if therapist_users:
|
||||
record.treatment_professional_ids = [(6, 0, therapist_users.ids)]
|
||||
|
||||
record.patient_id.recompute_followers()
|
||||
record.with_context(mail_notrack=True, mail_create_nolog=True, mail_create_nosubscribe=True).treatment_professional_ids = [(6, 0, therapist_users.ids)]
|
||||
|
||||
if not suppress_notifications:
|
||||
# Only internal/therapist flows adjust followers; portal/coach would 403 on mail.followers
|
||||
record._manage_treatment_professional_subscriptions()
|
||||
# Some flows rely on recomputing followers on patient; keep for staff
|
||||
record.patient_id.recompute_followers()
|
||||
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ access_sports_event_portal_treatment_professional,Portal Treatment Professional
|
|||
access_sports_event_portal_team_coach,Portal Team Coach Access for Sports Events,model_sports_event,bemade_sports_clinic.group_portal_team_coach,1,0,0,0
|
||||
access_patient_contact_user,User Access for Patient Contacts,model_sports_patient_contact,group_sports_clinic_user,1,1,1,1
|
||||
access_patient_contact_portal_tp,Portal TP Access for Patient Contacts,model_sports_patient_contact,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
access_patient_contact_portal_coach,Portal Coach Access for Patient Contacts,model_sports_patient_contact,bemade_sports_clinic.group_portal_team_coach,1,1,1,0
|
||||
access_injury_treatment_pro,Treatment Professional Access for Injuries,model_sports_patient_injury,group_sports_clinic_treatment_professional,1,1,1,1
|
||||
access_injury_portal,Portal Access for Injuries,model_sports_patient_injury,base.group_portal,1,0,1,0
|
||||
access_injury_portal_tp,Portal Treatment Prof Access for Injuries,model_sports_patient_injury,bemade_sports_clinic.group_portal_treatment_professional,1,1,1,0
|
||||
|
|
@ -65,3 +66,4 @@ access_project_tags_portal_tp,Portal TP Access for Project Tags,project.model_pr
|
|||
access_project_milestone_portal_tp,Portal TP Access for Project Milestones,project.model_project_milestone,bemade_sports_clinic.group_portal_treatment_professional,1,0,0,0
|
||||
access_snailmail_letter_portal_tp,Portal TP Access for Snailmail Letters,snailmail.model_snailmail_letter,bemade_sports_clinic.group_portal_treatment_professional,1,0,0,0
|
||||
access_snailmail_letter_portal_coach,Portal Coach Access for Snailmail Letters,snailmail.model_snailmail_letter,bemade_sports_clinic.group_portal_team_coach,1,0,0,0
|
||||
|
||||
|
|
|
|||
|
|
|
@ -85,13 +85,9 @@
|
|||
<field name="model_id" ref="mail.model_mail_activity_type"/>
|
||||
<field name="domain_force">[
|
||||
'|', '|', '|',
|
||||
# Generic activity types (no specific model)
|
||||
('res_model', '=', False),
|
||||
# Activity types for patients
|
||||
('res_model', '=', 'sports.patient'),
|
||||
# Activity types for injuries
|
||||
('res_model', '=', 'sports.patient.injury'),
|
||||
# Activity types for teams
|
||||
('res_model', '=', 'sports.team')
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_treatment_professional'))]"/>
|
||||
|
|
@ -144,19 +140,16 @@
|
|||
<field name="model_id" ref="mail.model_mail_followers"/>
|
||||
<field name="domain_force">[
|
||||
'|', '|',
|
||||
# Followers on patients they have access to through teams
|
||||
'&',
|
||||
'&',
|
||||
('res_model', '=', 'sports.patient'),
|
||||
('res_id', '!=', False),
|
||||
('res_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.patient_ids.id') or [0]),
|
||||
# Followers on injuries they have access to through teams
|
||||
'&',
|
||||
'&',
|
||||
('res_model', '=', 'sports.patient.injury'),
|
||||
('res_id', '!=', False),
|
||||
('res_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.patient_ids.injury_ids.id') or [0]),
|
||||
# Followers on teams they have access to
|
||||
'&',
|
||||
'&',
|
||||
('res_model', '=', 'sports.team'),
|
||||
|
|
@ -175,7 +168,6 @@
|
|||
<field name="name">Portal Treatment Professional: Patient Access</field>
|
||||
<field name="model_id" ref="bemade_sports_clinic.model_sports_patient"/>
|
||||
<field name="domain_force">[
|
||||
# Patients they have access to through teams
|
||||
('id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.patient_ids.id') or [0])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_treatment_professional'))]"/>
|
||||
|
|
@ -190,7 +182,6 @@
|
|||
<field name="name">Portal Treatment Professional: Patient Injury Access</field>
|
||||
<field name="model_id" ref="bemade_sports_clinic.model_sports_patient_injury"/>
|
||||
<field name="domain_force">[
|
||||
# Injuries on patients they have access to through teams
|
||||
('patient_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.patient_ids.id') or [0])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_treatment_professional'))]"/>
|
||||
|
|
|
|||
|
|
@ -8,16 +8,11 @@
|
|||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
'&',
|
||||
# SECURITY: Only tasks from projects with portal visibility
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
'|', '|', '|',
|
||||
# Tasks assigned to the user
|
||||
('user_ids', 'in', [user.id]),
|
||||
# Tasks where user is a follower
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# Tasks from projects where user is a follower
|
||||
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# Tasks from projects where user's teams are partners
|
||||
('project_id.partner_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.id') or [0])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_treatment_professional'))]"/>
|
||||
|
|
@ -33,12 +28,9 @@
|
|||
<field name="model_id" ref="project.model_project_project"/>
|
||||
<field name="domain_force">[
|
||||
'&',
|
||||
# SECURITY: Only projects with portal visibility
|
||||
('privacy_visibility', '=', 'portal'),
|
||||
'|',
|
||||
# Projects where user is explicitly a follower
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# Projects where user's teams are partners
|
||||
('partner_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.id') or [0])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_treatment_professional'))]"/>
|
||||
|
|
@ -54,11 +46,8 @@
|
|||
<field name="model_id" ref="project.model_project_task_type"/>
|
||||
<field name="domain_force">[
|
||||
'|', '|',
|
||||
# Stages from projects where user is a follower
|
||||
('project_ids.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# Stages from projects where user's teams are partners
|
||||
('project_ids.partner_id', 'in', user.partner_id.team_staff_rel_ids.mapped('team_id.id') or [0]),
|
||||
# Global stages (no project restriction)
|
||||
('project_ids', '=', False)
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('bemade_sports_clinic.group_portal_treatment_professional'))]"/>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,18 @@
|
|||
[('patient_id.team_ids.staff_ids.user_ids', 'in', user.id)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="restrict_patient_contact_access_to_coach_users" model="ir.rule">
|
||||
<field name="name">Restrict Patient Contact Access to Team Coaches</field>
|
||||
<field name="model_id" ref="model_sports_patient_contact"/>
|
||||
<field name="groups" eval="[(6, 0, [ref('bemade_sports_clinic.group_portal_team_coach')])]"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="domain_force">
|
||||
[('patient_id.team_ids.staff_ids.user_ids', 'in', user.id)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="allow_team_access_to_admins" model="ir.rule">
|
||||
<field name="name">Allow Team Access to Administrators</field>
|
||||
<field name="model_id" ref="model_sports_team"/>
|
||||
|
|
@ -111,3 +123,4 @@
|
|||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
|
|
|
|||
36
bemade_sports_clinic/static/src/js/jquery_scrolling_polyfill.js
vendored
Normal file
36
bemade_sports_clinic/static/src/js/jquery_scrolling_polyfill.js
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getScrollingElement as getTopScrollingEl } from "@web/core/utils/scrolling";
|
||||
|
||||
(function () {
|
||||
const w = window;
|
||||
const $ = w.jQuery;
|
||||
if (!$) {
|
||||
return;
|
||||
}
|
||||
// Polyfill only if missing to avoid overriding Odoo's legacy patch when present
|
||||
if (!$.fn.getScrollingElement) {
|
||||
/**
|
||||
* Returns the top-level scrolling element of the current document as a jQuery collection.
|
||||
* Usage parity: $().getScrollingElement()[0]
|
||||
*/
|
||||
$.fn.getScrollingElement = function () {
|
||||
const doc = (this && this[0] && this[0].ownerDocument) || w.document;
|
||||
return $(getTopScrollingEl(doc));
|
||||
};
|
||||
}
|
||||
if (!$.fn.getScrollingTarget) {
|
||||
/**
|
||||
* Returns a jQuery collection to listen to scroll events for a given scrollable element.
|
||||
* Accepts a DOM element or a jQuery collection.
|
||||
* If the element is the document's scrollingElement, return $(window); otherwise return $(element).
|
||||
* Usage parity: $().getScrollingTarget(elem)[0]
|
||||
*/
|
||||
$.fn.getScrollingTarget = function (el) {
|
||||
const node = (el && el.jquery) ? el[0] : el;
|
||||
const doc = (node && node.ownerDocument) || w.document;
|
||||
const isDocScrollEl = node === doc.scrollingElement;
|
||||
return isDocScrollEl ? $(w) : $(node);
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
|
||||
// Safely guard against null elements in TOC snippet's _stripNavbarStyles
|
||||
if (publicWidget && publicWidget.registry && publicWidget.registry.snippetTableOfContent) {
|
||||
publicWidget.registry.snippetTableOfContent.include({
|
||||
_stripNavbarStyles() {
|
||||
// This matches Odoo's original intent while being defensive
|
||||
const root = this && this.el ? this.el : null;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
const nodes = root.querySelectorAll('.s_table_of_content_navbar .table_of_content_link');
|
||||
nodes.forEach((node) => {
|
||||
let el = node;
|
||||
try {
|
||||
const translationEl = el ? el.querySelector('span[data-oe-translation-state]') : null;
|
||||
if (translationEl instanceof Element) {
|
||||
el = translationEl;
|
||||
}
|
||||
if (el && typeof el.textContent !== 'undefined') {
|
||||
const text = el.textContent || '';
|
||||
el.textContent = text;
|
||||
}
|
||||
} catch (_e) {
|
||||
// Silently ignore to avoid breaking the page due to malformed DOM
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -40,3 +40,51 @@
|
|||
color: #E5E5E5 !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* Injury count badge (outline, inherit text color, no background) */
|
||||
.badge-injury-count,
|
||||
.badge.badge-injury-count {
|
||||
/* Neutralize theme/background overrides */
|
||||
--bs-badge-bg: transparent;
|
||||
--bs-badge-color: inherit;
|
||||
--background-color: transparent !important;
|
||||
--color: inherit !important;
|
||||
background: transparent !important;
|
||||
background-image: none !important;
|
||||
color: inherit !important;
|
||||
border: 1px solid currentColor !important;
|
||||
/* Pill shape */
|
||||
border-radius: var(--bs-border-radius-pill, 10rem) !important;
|
||||
}
|
||||
|
||||
/* Stronger specificity to override theme where needed */
|
||||
.portal-event-card .badge.badge-injury-count,
|
||||
.badge.badge.badge-injury-count {
|
||||
background: transparent !important;
|
||||
background-image: none !important;
|
||||
color: inherit !important;
|
||||
border: 1px solid currentColor !important;
|
||||
border-radius: var(--bs-border-radius-pill, 10rem) !important;
|
||||
}
|
||||
|
||||
/* Generic dark outlined pill (no background, dark text) */
|
||||
.badge-outline-dark-pill,
|
||||
.badge.badge-outline-dark-pill {
|
||||
/* Set explicit dark color and transparent background */
|
||||
--bs-badge-bg: transparent;
|
||||
--bs-badge-color: var(--bs-dark, #212529);
|
||||
--background-color: transparent !important;
|
||||
--color: var(--bs-dark, #212529) !important;
|
||||
background: transparent !important;
|
||||
background-image: none !important;
|
||||
color: var(--bs-dark, #212529) !important;
|
||||
border: 1px solid var(--bs-dark, #212529) !important;
|
||||
border-radius: var(--bs-border-radius-pill, 10rem) !important;
|
||||
}
|
||||
|
||||
/* Stronger specificity variant */
|
||||
.badge.badge.badge-outline-dark-pill {
|
||||
background: transparent !important;
|
||||
color: var(--bs-dark, #212529) !important;
|
||||
border: 1px solid var(--bs-dark, #212529) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- View Type Navigation -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="row mb-1">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<a t-attf-class="nav-link #{'active' if view_type == 'all' else ''}"
|
||||
|
|
@ -47,15 +47,19 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<a href="/my/events?view_type=all&no_default_dates=1" class="btn btn-outline-secondary btn-sm me-2">
|
||||
<i class="fa fa-times"/> Clear Filters
|
||||
</a>
|
||||
<t t-if="can_edit">
|
||||
<a href="/my/event/create" class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-plus"/> Add Event
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="/my/events?view_type=all&no_default_dates=1" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fa fa-times"/> Clear Filters
|
||||
</a>
|
||||
</t>
|
||||
<t t-if="can_edit">
|
||||
<a href="/my/event/create" class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-plus"/> Add Event
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -42,15 +42,18 @@
|
|||
</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 ''"/>
|
||||
<!-- Date of Birth is visible/editable to treatment professionals only to respect field ACLs -->
|
||||
<t t-if="is_treatment_prof">
|
||||
<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 ''"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- TP-only additional info placed within Player Details -->
|
||||
<t t-if="is_treatment_prof">
|
||||
|
|
@ -179,8 +182,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Emergency Contact (TP only) -->
|
||||
<t t-if="is_treatment_prof">
|
||||
<!-- Primary Emergency Contact (TP and Coach) -->
|
||||
<t t-if="is_treatment_prof or is_coach">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Primary Emergency Contact</h6>
|
||||
|
|
@ -286,9 +289,16 @@
|
|||
<a t-att-href="return_url" class="btn bg-o-color-2 text-white">
|
||||
<i class="fa fa-times"></i> Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn bg-o-color-1 text-white">
|
||||
<i class="fa fa-save"></i> Save Changes
|
||||
</button>
|
||||
<div>
|
||||
<t t-if="can_request_removal">
|
||||
<button type="button" class="btn btn-outline-danger me-2" data-bs-toggle="modal" data-bs-target="#requestRemovalModal">
|
||||
<i class="fa fa-user-times"/> Request Removal
|
||||
</button>
|
||||
</t>
|
||||
<button type="submit" class="btn bg-o-color-1 text-white">
|
||||
<i class="fa fa-save"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -338,6 +348,32 @@
|
|||
//]]>
|
||||
</script>
|
||||
</t>
|
||||
<!-- Coach-only: Request Removal Modal -->
|
||||
<t t-if="can_request_removal">
|
||||
<div class="modal fade" id="requestRemovalModal" tabindex="-1" aria-labelledby="requestRemovalModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="requestRemovalModalLabel">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/{{ removal_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">
|
||||
<div class="mb-3">
|
||||
<label for="removal_reason" class="form-label">Reason for removal <span class="text-danger">*</span></label>
|
||||
<textarea id="removal_reason" name="reason" class="form-control" rows="4" placeholder="Provide a clear reason for this request" required="required"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-danger"><i class="fa fa-paper-plane me-1"/> Submit Request</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
<div class="row mt-4">
|
||||
|
|
|
|||
|
|
@ -29,18 +29,10 @@
|
|||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2 t-esc="event.name"/>
|
||||
<p class="text-muted mb-0">
|
||||
<i class="fa fa-users"/> <span t-esc="event.team_id.name"/>
|
||||
<t t-if="event.venue_id">
|
||||
| <i class="fa fa-map-marker"/> <span t-esc="event.venue_id.name"/>
|
||||
</t>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/my/events" class="btn btn-outline-secondary me-2">
|
||||
<div>
|
||||
<h2 t-esc="event.name"/>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="/my/events" class="btn btn-outline-secondary">
|
||||
<i class="fa fa-arrow-left"/> Back to Events
|
||||
</a>
|
||||
<t t-if="can_edit">
|
||||
|
|
|
|||
|
|
@ -31,6 +31,147 @@
|
|||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
<!-- Reusable patient card snippet used across Players and Team views -->
|
||||
<template id="portal_patient_card">
|
||||
<t t-set="stage" t-value="player.stage"/>
|
||||
<t t-set="url"
|
||||
t-value="(team and ('/my/player?player_id=' + str(player.id) + '&team_id=' + str(team.id))) or ('/my/player?player_id=' + str(player.id))"/>
|
||||
<div t-attf-class="card position-relative h-100 shadow-sm {{ 'border-danger' if stage == 'no_play' else 'border-warning' if stage and stage != 'healthy' else '' }}">
|
||||
<!-- Header: Player name and active injuries badge -->
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<a t-attf-href="{{ url }}" class="text-decoration-none fw-bold">
|
||||
<span t-field="player.last_name"/>, <span t-field="player.first_name"/>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Desktop-only: Right-aligned badges next to injury count -->
|
||||
<div class="d-none d-md-flex align-items-center justify-content-end flex-wrap gap-2">
|
||||
<!-- Match badge: Yes (green check) / No (red ban) -->
|
||||
<t t-if="player.match_status == 'yes'">
|
||||
<span class="badge bg-success text-white" title="Match: Yes">
|
||||
<i class="fa fa-check me-1"/>
|
||||
Match
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="player.match_status == 'no'">
|
||||
<span class="badge bg-danger text-white" title="Match: No">
|
||||
<i class="fa fa-ban me-1"/>
|
||||
Match
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge bg-secondary text-white" title="Match: Unknown">Match</span>
|
||||
</t>
|
||||
|
||||
<!-- Practice badge: Yes (green check) / Yes - No Contact (yellow !) / No (red ban) -->
|
||||
<t t-if="player.practice_status == 'yes'">
|
||||
<span class="badge bg-success text-white" title="Practice: Yes">
|
||||
<i class="fa fa-check me-1"/>
|
||||
Practice
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="player.practice_status == 'no_contact'">
|
||||
<span class="badge bg-warning text-dark" title="Practice: Yes - No Contact">
|
||||
<i class="fa fa-exclamation-triangle me-1"/>
|
||||
Practice
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="player.practice_status == 'no'">
|
||||
<span class="badge bg-danger text-white" title="Practice: No">
|
||||
<i class="fa fa-ban me-1"/>
|
||||
Practice
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge bg-secondary text-white" title="Practice: Unknown">Practice</span>
|
||||
</t>
|
||||
|
||||
<!-- Active injuries count badge -->
|
||||
<t t-set="inj_count" t-value="player.active_injury_count or 0"/>
|
||||
<span class="badge badge-injury-count">
|
||||
<i class="fa fa-chain-broken me-1"/>
|
||||
<t t-esc="inj_count"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Body: Teams (optional) and key fields -->
|
||||
<div class="card-body">
|
||||
<!-- Make whole card clickable -->
|
||||
<a t-attf-href="{{ url }}" class="stretched-link" aria-label="Open player details"></a>
|
||||
<!-- First line: Match/Practice badges (mobile only; desktop version is in header) -->
|
||||
<div class="mb-2 d-flex d-md-none flex-wrap gap-2">
|
||||
<!-- Match badge: Yes (green check) / No (red x) -->
|
||||
<t t-if="player.match_status == 'yes'">
|
||||
<span class="badge bg-success text-white" title="Match: Yes">
|
||||
<i class="fa fa-check me-1"/>
|
||||
Match
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="player.match_status == 'no'">
|
||||
<span class="badge bg-danger text-white" title="Match: No">
|
||||
<i class="fa fa-ban me-1"/>
|
||||
Match
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge bg-secondary text-white" title="Match: Unknown">Match</span>
|
||||
</t>
|
||||
|
||||
<!-- Practice badge: Yes (green check) / Yes - No Contact (yellow !) / No (red x) -->
|
||||
<t t-if="player.practice_status == 'yes'">
|
||||
<span class="badge bg-success text-white" title="Practice: Yes">
|
||||
<i class="fa fa-check me-1"/>
|
||||
Practice
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="player.practice_status == 'no_contact'">
|
||||
<span class="badge bg-warning text-dark" title="Practice: Yes - No Contact">
|
||||
<i class="fa fa-exclamation-triangle me-1"/>
|
||||
Practice
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="player.practice_status == 'no'">
|
||||
<span class="badge bg-danger text-white" title="Practice: No">
|
||||
<i class="fa fa-ban me-1"/>
|
||||
Practice
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge bg-secondary text-white" title="Practice: Unknown">Practice</span>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="not is_team_view">
|
||||
<div class="mb-2">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<t t-foreach="player.sudo().team_ids" t-as="t">
|
||||
<a t-attf-href="/my/team?team_id={{ t.id }}" class="badge badge-outline-dark-pill text-decoration-none">
|
||||
<t t-esc="t.name"/>
|
||||
</a>
|
||||
</t>
|
||||
<span t-if="not player.team_ids" class="text-muted small">No teams</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="text-muted small d-flex align-items-center">
|
||||
<i class="fa fa-stethoscope" title="Last Consultation Date"></i>
|
||||
<span class="ms-1 d-none d-md-inline">Last Consultation:</span>
|
||||
<span class="ms-1" t-field="player.last_consultation_date" t-options="{'widget': 'date'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-muted small d-flex align-items-center">
|
||||
<i class="fa fa-clock-o" title="Estimated Return"></i>
|
||||
<span class="ms-1 d-none d-md-inline">Estimated Return:</span>
|
||||
<span class="ms-1" t-field="player.predicted_return_date" t-options="{'widget': 'date'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer removed per spec -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
|
|
@ -214,8 +355,8 @@
|
|||
</div>
|
||||
</t>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1 class="mb-0">
|
||||
<div class="mb-3">
|
||||
<h1 class="mb-2">
|
||||
<span t-field="team.name"/> Players
|
||||
<t t-if="any(p.pending_removal for p in players)" class="position-relative">
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning">
|
||||
|
|
@ -225,11 +366,11 @@
|
|||
</span>
|
||||
</t>
|
||||
</h1>
|
||||
<div>
|
||||
<a t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')" t-attf-href="/my/activities?model=sports.team&res_id={{ team.id }}" class="btn bg-o-color-2 text-white me-2">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')" t-attf-href="/my/activities?model=sports.team&res_id={{ team.id }}" class="btn bg-o-color-2 text-white">
|
||||
<i class="fa fa-tasks"></i> Activities
|
||||
</a>
|
||||
<a t-if="is_treatment_prof" t-attf-href="/my/activity/create?model=sports.team&res_id={{ team.id }}" class="btn bg-o-color-2 text-white me-2">
|
||||
<a t-if="is_treatment_prof" t-attf-href="/my/activity/create?model=sports.team&res_id={{ team.id }}" class="btn bg-o-color-2 text-white">
|
||||
<i class="fa fa-tasks"></i> Add Activity
|
||||
</a>
|
||||
<!-- Therapist: go to dedicated Add/Link Player page -->
|
||||
|
|
@ -269,7 +410,7 @@
|
|||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Date of Birth</label>
|
||||
<input type="date" name="date_of_birth" class="form-control"/>
|
||||
<input type="date" name="date_of_birth" class="form-control" required="required"/>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Reason</label>
|
||||
|
|
@ -302,125 +443,18 @@
|
|||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-call="portal.portal_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Last Name</th>
|
||||
<th>First Name</th>
|
||||
<th>Active Injuries</th>
|
||||
<th>Match Status</th>
|
||||
<th>Practice Status</th>
|
||||
<th>Estimated Return Date</th>
|
||||
<th>Last Consultation Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Cards grid replacing table -->
|
||||
<div class="container-fluid">
|
||||
<div class="row g-3 justify-content-center">
|
||||
<t t-foreach="players" t-as="player">
|
||||
<t t-set="stage" t-value="player.stage"/>
|
||||
<t t-set="url"
|
||||
t-value="'/my/player?player_id=' + str(player.id) + '&team_id=' + str(team.id)"/>
|
||||
<tr t-attf-class="{{ 'table-warning' if player.pending_removal else '' }} {{ 'text-danger' if stage == 'no_play' else 'text-warning' if stage != 'healthy' else '' }}"
|
||||
t-attf-id="{{ 'pending-removal-' + str(player.id) if player.pending_removal else '' }}">
|
||||
<td>
|
||||
<strong><a t-attf-href="{{ url }}" class="text-decoration-none"><span t-field="player.last_name"/></a></strong>
|
||||
</td>
|
||||
<td>
|
||||
<strong><a t-attf-href="{{ url }}" class="text-decoration-none"><span t-field="player.first_name"/></a></strong>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="player.active_injury_count" t-field="player.active_injury_count"/>
|
||||
<span t-else="">0</span>
|
||||
</td>
|
||||
<td><span t-field="player.match_status"/></td>
|
||||
<td><span t-field="player.practice_status"/></td>
|
||||
<td>
|
||||
<span t-field="player.predicted_return_date"
|
||||
t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td>
|
||||
<span t-field="player.last_consultation_date"
|
||||
t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group" role="group">
|
||||
<a t-attf-href="/my/patient/injury/new?patient_id={{ player.id }}&team_id={{ team.id }}"
|
||||
class="btn bg-o-color-1 text-white btn-sm"
|
||||
t-att-disabled="player.pending_removal">
|
||||
<i class="fa fa-plus"/> Injury
|
||||
</a>
|
||||
|
||||
<!-- Pending Removal Badge -->
|
||||
<t t-if="player.pending_removal" class="ms-2">
|
||||
<span class="badge bg-warning text-dark" title="Pending Removal">
|
||||
<i class="fa fa-hourglass-half me-1"/> Pending Removal
|
||||
</span>
|
||||
</t>
|
||||
<!-- Show remove button for treatment professionals/team staff -->
|
||||
<t t-set="is_treatment_prof" t-value="user_has_group('bemade_sports_clinic.group_portal_treatment_professional') or user_has_group('base.group_system')"/>
|
||||
<t t-set="is_team_staff" t-value="team.staff_ids.filtered(lambda s: user.partner_id in s.user_ids.partner_id)"/>
|
||||
|
||||
<!-- For treatment professionals - direct remove -->
|
||||
<t t-if="is_treatment_prof">
|
||||
<form t-att-action="'/my/team/' + str(team.id) + '/player/' + str(player.id) + '/remove'" method="post" style="display: inline-block;">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<button type="submit"
|
||||
class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('Are you sure you want to remove this player from the team?');">
|
||||
<i class="fa fa-user-times"/> Remove
|
||||
</button>
|
||||
</form>
|
||||
</t>
|
||||
<!-- For team staff (coaches) - request removal -->
|
||||
<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="'#requestRemovalModal' + str(player.id)"
|
||||
t-att-class="'btn-warning' if not player.pending_removal else 'btn-secondary'"
|
||||
t-att-disabled="player.pending_removal">
|
||||
<i class="fa" t-attf-class="{{ 'fa-flag' if not player.pending_removal else 'fa-hourglass' }}" />
|
||||
<t t-if="not player.pending_removal">Request Removal</t>
|
||||
<t t-else="">Removal Requested</t>
|
||||
</button>
|
||||
|
||||
<!-- Request Removal Modal -->
|
||||
<div t-att-id="'requestRemovalModal' + str(player.id)" class="modal fade" tabindex="-1" t-attf-aria-labelledby="requestRemovalModalLabel#{player.id}" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" t-attf-id="requestRemovalModalLabel#{player.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/{{ player.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="alert alert-info">
|
||||
<i class="fa fa-info-circle"/> The player will be marked as pending removal until a therapist approves the request.
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Reason for removal:</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-2 text-white">
|
||||
<i class="fa fa-paper-plane"/> Submit Request
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="col-12 col-lg-8">
|
||||
<t t-set="is_team_view" t-value="True"/>
|
||||
<t t-set="show_player_activities" t-value="False"/>
|
||||
<t t-call="bemade_sports_clinic.portal_patient_card"/>
|
||||
</div>
|
||||
</t>
|
||||
</tbody>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="portal_my_players">
|
||||
|
|
@ -514,151 +548,132 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<t t-call="portal.portal_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Last Name</th>
|
||||
<th>First Name</th>
|
||||
<th>Teams</th>
|
||||
<th>Active Injuries</th>
|
||||
<th>Match Status</th>
|
||||
<th>Practice Status</th>
|
||||
<th>Estimated Return Date</th>
|
||||
<th>Last Consultation Date</th>
|
||||
<th>Activities</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Cards grid replacing table -->
|
||||
<div class="container-fluid">
|
||||
<div class="row g-3 justify-content-center">
|
||||
<t t-foreach="players" t-as="player">
|
||||
<t t-set="stage" t-value="player.stage"/>
|
||||
<t t-set="url" t-value="'/my/player?player_id=' + str(player.id)"/>
|
||||
<tr t-attf-class="{{ ('text-danger' if stage == 'no_play'
|
||||
else 'text-warning') if stage != 'healthy'
|
||||
else '' }}">
|
||||
<td>
|
||||
<strong><a t-attf-href="{{ url }}" class="text-decoration-none"><span t-field="player.last_name"/></a></strong>
|
||||
</td>
|
||||
<td>
|
||||
<strong><a t-attf-href="{{ url }}" class="text-decoration-none"><span t-field="player.first_name"/></a></strong>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="list-unstyled">
|
||||
<t t-foreach="player.sudo().team_ids" t-as="team">
|
||||
<li>
|
||||
<a t-attf-href="/my/team?team_id={{ team.id }}"
|
||||
t-field="team.name"/>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="player.active_injury_count" t-field="player.active_injury_count"/>
|
||||
<span t-else="">0</span>
|
||||
</td>
|
||||
<td><span t-field="player.match_status"/></td>
|
||||
<td><span t-field="player.practice_status"/></td>
|
||||
<td>
|
||||
<span t-field="player.predicted_return_date"
|
||||
t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td>
|
||||
<span t-field="player.last_consultation_date"
|
||||
t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-set="player_activity_count" t-value="player.activity_count or 0"/>
|
||||
<span t-esc="player_activity_count"/>
|
||||
</td>
|
||||
<td>
|
||||
<a t-attf-href="/my/activities?model=sports.patient&res_id={{ player.id }}"
|
||||
class="btn bg-o-color-1 text-white btn-sm">
|
||||
<i class="fa fa-tasks"></i> Activities
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="col-12 col-lg-8">
|
||||
<t t-set="is_team_view" t-value="False"/>
|
||||
<t t-set="show_player_activities" t-value="True"/>
|
||||
<t t-call="bemade_sports_clinic.portal_patient_card"/>
|
||||
</div>
|
||||
</t>
|
||||
</tbody>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="portal_my_player_injuries">
|
||||
<t t-call="portal.portal_layout">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="mb-2">
|
||||
<h1 class="mb-0"><span t-field="player.name"/></h1>
|
||||
<div class="ms-auto">
|
||||
<a t-att-href="'/my/player/edit?patient_id=%s' % player.id" class="btn bg-o-color-1 text-white me-2">
|
||||
<i class="fa fa-edit"></i> Edit Player
|
||||
</a>
|
||||
<a t-att-href="'/my/patient/injury/new?patient_id=%s' % player.id" class="btn bg-o-color-1 text-white me-2">
|
||||
<i class="fa fa-plus"></i> <span t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">Add Injury</span><span t-else="">Report Injury</span>
|
||||
</a>
|
||||
<a t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')" t-attf-href="/my/activities?model=sports.patient&res_id={{ player.id }}" class="btn bg-o-color-2 text-white me-2">
|
||||
<i class="fa fa-tasks"></i> Activities
|
||||
</a>
|
||||
<a t-if="is_treatment_prof" t-att-href="'/my/activity/create?model=sports.patient&res_id=%s' % player.id" class="btn bg-o-color-2 text-white me-2">
|
||||
<i class="fa fa-tasks"></i> Add Activity
|
||||
</a>
|
||||
<a t-if="is_treatment_prof" t-att-href="'/my/injury/notes?patient_id=%s' % player.id" class="btn bg-o-color-2 text-white">
|
||||
<i class="fa fa-clipboard-list"></i> View Notes
|
||||
<span class="badge badge-pill badge-primary ms-1" t-if="player.treatment_note_count">
|
||||
<t t-esc="player.treatment_note_count"/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<a t-att-href="'/my/player/edit?patient_id=%s' % player.id" class="btn bg-o-color-1 text-white">
|
||||
<i class="fa fa-edit"></i> Edit Player
|
||||
</a>
|
||||
<a t-att-href="'/my/patient/injury/new?patient_id=%s' % player.id" class="btn bg-o-color-1 text-white">
|
||||
<i class="fa fa-plus"></i> <span t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">Add Injury</span><span t-else="">Report Injury</span>
|
||||
</a>
|
||||
<!-- Coach: Request Removal (visible only if can_request_removal True) -->
|
||||
<button t-if="can_request_removal" type="button" class="btn btn-outline-danger"
|
||||
data-bs-toggle="modal" t-attf-data-bs-target="#requestRemovalModal{{ player.id }}">
|
||||
<i class="fa fa-user-times"></i> Request Removal
|
||||
</button>
|
||||
<a t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')" t-attf-href="/my/activities?model=sports.patient&res_id={{ player.id }}" class="btn bg-o-color-2 text-white">
|
||||
<i class="fa fa-tasks"></i> Activities
|
||||
</a>
|
||||
<a t-if="is_treatment_prof" t-att-href="'/my/activity/create?model=sports.patient&res_id=%s' % player.id" class="btn bg-o-color-2 text-white">
|
||||
<i class="fa fa-tasks"></i> Add Activity
|
||||
</a>
|
||||
<a t-if="is_treatment_prof" t-att-href="'/my/injury/notes?patient_id=%s' % player.id" class="btn bg-o-color-2 text-white">
|
||||
<i class="fa fa-clipboard-list"></i> View Notes
|
||||
<span class="badge badge-pill badge-primary ms-1" t-if="player.treatment_note_count">
|
||||
<t t-esc="player.treatment_note_count"/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Request Removal Modal -->
|
||||
<div t-if="can_request_removal and removal_team_id" t-attf-id="requestRemovalModal{{ player.id }}" class="modal fade" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">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/{{ removal_team_id }}/player/{{ player.id }}/request_removal" method="post">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Reason</label>
|
||||
<textarea name="reason" class="form-control" rows="3" placeholder="Explain why this player should be removed"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fa fa-paper-plane"></i> Submit Request
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player Status Card -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-3">
|
||||
<h6 class="mb-1">Match</h6>
|
||||
<span t-if="player.match_status == 'yes'" class="text-success font-weight-bold h5">
|
||||
<i class="fa fa-check-circle"></i> Yes
|
||||
</span>
|
||||
<span t-elif="player.match_status == 'no'" class="text-danger font-weight-bold h5">
|
||||
<i class="fa fa-times-circle"></i> No
|
||||
</span>
|
||||
<span t-else="" class="text-muted font-weight-bold h5">
|
||||
<i class="fa fa-question-circle"></i> <span t-esc="player.match_status or 'Unknown'"/>
|
||||
</span>
|
||||
<div class="row g-3 align-items-center">
|
||||
<!-- Left column: Match & Practice badges with text -->
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<!-- Match badge -->
|
||||
<t t-if="player.match_status == 'yes'">
|
||||
<span class="badge bg-success text-white" title="Match: Yes">
|
||||
<i class="fa fa-check me-1"/>
|
||||
Match
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="player.match_status == 'no'">
|
||||
<span class="badge bg-danger text-white" title="Match: No">
|
||||
<i class="fa fa-ban me-1"/>
|
||||
Match
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge bg-secondary text-white" title="Match: Unknown">Match</span>
|
||||
</t>
|
||||
|
||||
<!-- Practice badge -->
|
||||
<t t-if="player.practice_status == 'yes'">
|
||||
<span class="badge bg-success text-white" title="Practice: Yes">
|
||||
<i class="fa fa-check me-1"/>
|
||||
Practice
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="player.practice_status == 'no_contact'">
|
||||
<span class="badge bg-warning text-dark" title="Practice: Yes - No Contact">
|
||||
<i class="fa fa-exclamation-triangle me-1"/>
|
||||
Practice
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="player.practice_status == 'no'">
|
||||
<span class="badge bg-danger text-white" title="Practice: No">
|
||||
<i class="fa fa-ban me-1"/>
|
||||
Practice
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge bg-secondary text-white" title="Practice: Unknown">Practice</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6 class="mb-1">Practice</h6>
|
||||
<span t-if="player.practice_status == 'yes'" class="text-success font-weight-bold h5">
|
||||
<i class="fa fa-check-circle"></i> Yes
|
||||
</span>
|
||||
<span t-elif="player.practice_status == 'no_contact'" class="text-warning font-weight-bold h5">
|
||||
<i class="fa fa-exclamation-triangle"></i> Yes, No Contact
|
||||
</span>
|
||||
<span t-elif="player.practice_status == 'no'" class="text-danger font-weight-bold h5">
|
||||
<i class="fa fa-times-circle"></i> No
|
||||
</span>
|
||||
<span t-else="" class="text-muted font-weight-bold h5">
|
||||
<i class="fa fa-question-circle"></i> <span t-esc="player.practice_status or 'Unknown'"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-3" t-if="player.stage">
|
||||
<h6 class="mb-1">Status</h6>
|
||||
<span t-if="player.stage == 'healthy'" class="text-success font-weight-bold h5">
|
||||
<i class="fa fa-thumbs-up"></i> Play OK
|
||||
</span>
|
||||
<span t-elif="player.stage == 'practice_ok'" class="text-warning font-weight-bold h5">
|
||||
<i class="fa fa-running"></i> Practice OK
|
||||
</span>
|
||||
<span t-elif="player.stage == 'no_play'" class="text-danger font-weight-bold h5">
|
||||
<i class="fa fa-ban"></i> Injured
|
||||
</span>
|
||||
<span t-else="" class="text-muted font-weight-bold h5">
|
||||
<i class="fa fa-question-circle"></i> <span t-esc="player.stage"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-3" t-if="(player.allergies or '').replace('\xa0','').strip()">
|
||||
<h6 class="mb-1">Allergies</h6>
|
||||
<div class="alert alert-warning py-2 mb-0">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<small t-esc="player.allergies"/>
|
||||
|
||||
<!-- Right column: Allergies (if present) -->
|
||||
<div class="col-12 col-md-6" t-if="(player.allergies or '').replace('\xa0','').strip()">
|
||||
<div class="alert alert-warning py-2 mb-0 text-start">
|
||||
<strong>Allergies:</strong>
|
||||
<span class="ms-1" t-esc="player.allergies"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -920,7 +935,7 @@
|
|||
<td><strong>Match:</strong></td>
|
||||
<td>
|
||||
<span t-if="player.match_status == 'yes'" class="text-success font-weight-bold"><i class="fa fa-check-circle"></i> Yes</span>
|
||||
<span t-elif="player.match_status == 'no'" class="text-danger font-weight-bold"><i class="fa fa-times-circle"></i> No</span>
|
||||
<span t-elif="player.match_status == 'no'" class="text-danger font-weight-bold"><i class="fa fa-ban"></i> No</span>
|
||||
<span t-else="" class="text-muted" t-esc="player.match_status"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -929,7 +944,7 @@
|
|||
<td>
|
||||
<span t-if="player.practice_status == 'yes'" class="text-success font-weight-bold"><i class="fa fa-check-circle"></i> Yes</span>
|
||||
<span t-elif="player.practice_status == 'no_contact'" class="text-warning font-weight-bold"><i class="fa fa-exclamation-triangle"></i> Yes, No Contact</span>
|
||||
<span t-elif="player.practice_status == 'no'" class="text-danger font-weight-bold"><i class="fa fa-times-circle"></i> No</span>
|
||||
<span t-elif="player.practice_status == 'no'" class="text-danger font-weight-bold"><i class="fa fa-ban"></i> No</span>
|
||||
<span t-else="" class="text-muted" t-esc="player.practice_status"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
Loading…
Reference in a new issue