portal: enable player removal from Edit Player view; fix QWeb group checks
- Replace nested remove form with a submit button using HTML5 formaction to post directly to /my/team/{team_id}/player/{player_id}/remove
- Prevent outer save form from intercepting submission; preserves CSRF; confirmation retained; redirects to team page
- Replace user_has_group helper with request.env.user.has_group in QWeb to prevent NoneType callable errors
Verification:
- Edit Player page renders without 500 error
- Clicking Remove removes the player and returns 303 redirect to /my/team/{team_id}
- Success notification displayed on team page
Notes:
- Removal route already implemented in controller (TeamManagementPortal.portal_remove_player)
This commit is contained in:
parent
78589bbcf3
commit
3ce8d051bc
12 changed files with 586 additions and 103 deletions
|
|
@ -92,7 +92,7 @@ class EventsPortal(CustomerPortal, AccessControlMixin):
|
|||
|
||||
@http.route(['/my/events', '/my/events/page/<int:page>'], type='http', auth='user', website=True)
|
||||
def view_events(self, page=1, view_type='all', team_id=None, organization_id=None, assigned_user_id=None,
|
||||
date_from=None, date_to=None, sortby=None, search=None, **kw):
|
||||
date_from=None, date_to=None, sortby=None, search=None, no_default_dates=None, **kw):
|
||||
"""Main events view with filtering and pagination"""
|
||||
|
||||
# Check access
|
||||
|
|
@ -104,6 +104,17 @@ class EventsPortal(CustomerPortal, AccessControlMixin):
|
|||
if not (is_therapist or is_coach or user.has_group('base.group_system')):
|
||||
raise AccessError(_("You don't have access to events."))
|
||||
|
||||
# Default date filter: from yesterday, similar to internal "Upcoming" behavior
|
||||
# Only apply when user did not specify any date filters AND no explicit clear flag
|
||||
if not date_from and not date_to and not no_default_dates:
|
||||
try:
|
||||
# Use date (not datetime) input format 'YYYY-MM-DD' for the portal date picker
|
||||
yesterday = (fields.Date.today() - timedelta(days=1))
|
||||
date_from = fields.Date.to_string(yesterday)
|
||||
except Exception:
|
||||
# Fallback using datetime in rare cases
|
||||
date_from = (datetime.today() - timedelta(days=1)).strftime('%Y-%m-%d')
|
||||
|
||||
# Prepare base domain
|
||||
domain = self._prepare_events_domain(view_type)
|
||||
|
||||
|
|
@ -167,7 +178,7 @@ class EventsPortal(CustomerPortal, AccessControlMixin):
|
|||
url='/my/events',
|
||||
url_args={'view_type': view_type, 'team_id': team_id, 'organization_id': organization_id,
|
||||
'assigned_user_id': assigned_user_id, 'date_from': date_from,
|
||||
'date_to': date_to, 'sortby': sortby, 'search': search},
|
||||
'date_to': date_to, 'sortby': sortby, 'search': search, 'no_default_dates': no_default_dates},
|
||||
total=event_count,
|
||||
page=page,
|
||||
step=self._items_per_page,
|
||||
|
|
@ -215,6 +226,7 @@ class EventsPortal(CustomerPortal, AccessControlMixin):
|
|||
'organizations': organizations,
|
||||
'treatment_professionals': treatment_professionals,
|
||||
'can_edit': can_edit,
|
||||
'no_default_dates': no_default_dates,
|
||||
}
|
||||
|
||||
return http.request.render('bemade_sports_clinic.portal_events_list', values)
|
||||
|
|
|
|||
|
|
@ -556,9 +556,13 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
# Get user's role
|
||||
is_treatment_prof = request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')
|
||||
|
||||
# Document categories
|
||||
categories = [('medical', 'Medical'), ('xray', 'X-Ray'), ('mri', 'MRI'),
|
||||
('prescription', 'Prescription'), ('other', 'Other')]
|
||||
# Document categories (aligned with model)
|
||||
categories = [
|
||||
('medical', 'Medical'),
|
||||
('medical_imaging', 'Medical Imaging'),
|
||||
('prescription', 'Prescription'),
|
||||
('other', 'Other'),
|
||||
]
|
||||
|
||||
values = {
|
||||
'injury': injury,
|
||||
|
|
@ -608,6 +612,7 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
# Create the document
|
||||
document = request.env['sports.injury.document'].sudo().create({
|
||||
'injury_id': int(injury_id),
|
||||
'patient_id': injury.patient_id.id,
|
||||
'name': post.get('document_name', name),
|
||||
'description': post.get('description', ''),
|
||||
'category': post.get('category', 'other'),
|
||||
|
|
@ -632,11 +637,14 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
raise request.not_found()
|
||||
|
||||
try:
|
||||
# Check access to the injury this document belongs to
|
||||
injury = self._check_access_to_injury(document.injury_id.id)
|
||||
# Prefer checking access via injury; if no injury, check via patient
|
||||
if document.injury_id:
|
||||
self._check_access_to_injury(document.injury_id.id)
|
||||
else:
|
||||
self._check_access_to_patient(document.patient_id.id)
|
||||
except UserError:
|
||||
raise request.not_found()
|
||||
|
||||
|
||||
# Return the file for download
|
||||
return request.make_response(
|
||||
base64.b64decode(document.file_content),
|
||||
|
|
@ -645,6 +653,72 @@ class PatientInjuryPortal(CustomerPortal, AccessControlMixin):
|
|||
('Content-Disposition', f'attachment; filename="{document.file_name}"'),
|
||||
]
|
||||
)
|
||||
|
||||
@http.route(['/my/patient/document/download/<int:document_id>'], type='http', auth='user')
|
||||
def download_patient_document(self, document_id, **post):
|
||||
"""Download a document linked to a patient (injury optional)."""
|
||||
document = request.env['sports.injury.document'].sudo().browse(int(document_id))
|
||||
if not document.exists():
|
||||
raise request.not_found()
|
||||
try:
|
||||
# Access check based on patient (primary link)
|
||||
self._check_access_to_patient(document.patient_id.id)
|
||||
except UserError:
|
||||
raise request.not_found()
|
||||
return request.make_response(
|
||||
base64.b64decode(document.file_content),
|
||||
headers=[
|
||||
('Content-Type', 'application/octet-stream'),
|
||||
('Content-Disposition', f'attachment; filename="{document.file_name}"'),
|
||||
]
|
||||
)
|
||||
|
||||
@http.route(['/my/patient/document/upload'], type='http', auth='user', website=True, methods=['POST'])
|
||||
def upload_patient_document(self, **post):
|
||||
"""Upload a document directly to a patient (injury optional)."""
|
||||
patient_id = post.get('patient_id')
|
||||
if not patient_id:
|
||||
return request.redirect('/my/players')
|
||||
|
||||
try:
|
||||
patient = self._check_access_to_patient(patient_id)
|
||||
except UserError as e:
|
||||
return request.render('http_routing.http_error', {
|
||||
'status_code': 403,
|
||||
'status_message': 'Forbidden',
|
||||
'error_message': str(e)
|
||||
})
|
||||
|
||||
attachment = post.get('attachment')
|
||||
if not attachment:
|
||||
return request.redirect(f'/my/player?player_id={patient.id}&error=no_file')
|
||||
|
||||
try:
|
||||
name = attachment.filename
|
||||
file_content = attachment.read()
|
||||
file_size = len(file_content)
|
||||
|
||||
# 10MB limit
|
||||
if file_size > 10 * 1024 * 1024:
|
||||
return request.redirect(f'/my/player?player_id={patient.id}&error=file_too_large')
|
||||
|
||||
# Create patient-linked document (injury optional)
|
||||
request.env['sports.injury.document'].sudo().create({
|
||||
'patient_id': patient.id,
|
||||
'injury_id': int(post['injury_id']) if post.get('injury_id') else False,
|
||||
'name': post.get('document_name', name),
|
||||
'description': post.get('description', ''),
|
||||
'category': post.get('category', 'other'),
|
||||
'file_content': base64.b64encode(file_content),
|
||||
'file_name': name,
|
||||
'created_by_id': request.env.user.id,
|
||||
})
|
||||
|
||||
return request.redirect(f'/my/player?player_id={patient.id}&success=document_uploaded')
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"Error uploading patient document: {e}")
|
||||
return request.redirect(f'/my/player?player_id={patient.id}&error=upload_failed')
|
||||
|
||||
@http.route(['/my/injury/document/delete/<int:document_id>'], type='http', auth='user', website=True)
|
||||
def delete_injury_document(self, document_id, **post):
|
||||
|
|
|
|||
|
|
@ -159,13 +159,25 @@ class TeamStaffPortal(CustomerPortal):
|
|||
|
||||
|
||||
|
||||
|
||||
# Show all injuries to treatment professionals, but only active ones to coaches
|
||||
if is_treatment_prof:
|
||||
injuries = player.injury_ids
|
||||
else:
|
||||
injuries = player.injury_ids.filtered(lambda r: r.stage == 'active')
|
||||
|
||||
|
||||
# Patient documents for Documents tab (primary association now on patient)
|
||||
patient_documents = http.request.env['sports.injury.document'].search([
|
||||
('patient_id', '=', player.id)
|
||||
], order='create_date desc, id desc')
|
||||
|
||||
# Categories for patient document uploads
|
||||
categories = [
|
||||
('medical', 'Medical'),
|
||||
('medical_imaging', 'Medical Imaging'),
|
||||
('prescription', 'Prescription'),
|
||||
('other', 'Other'),
|
||||
]
|
||||
|
||||
# Create patient_info dictionary for protected fields (when user is a treatment professional)
|
||||
# No need for sudo() now that we have proper field-level access rights
|
||||
patient_info = {}
|
||||
|
|
@ -179,6 +191,8 @@ class TeamStaffPortal(CustomerPortal):
|
|||
qcontext={
|
||||
'player': player,
|
||||
'injuries': injuries,
|
||||
'patient_documents': patient_documents,
|
||||
'categories': categories,
|
||||
'team': team,
|
||||
'page_name': 'my_player',
|
||||
'is_treatment_prof': is_treatment_prof,
|
||||
|
|
|
|||
|
|
@ -9,22 +9,49 @@ class InjuryDocument(models.Model):
|
|||
_order = 'create_date desc, id desc'
|
||||
|
||||
name = fields.Char(string='Name', required=True)
|
||||
injury_id = fields.Many2one('sports.patient.injury', string='Injury', required=True, ondelete='cascade')
|
||||
patient_id = fields.Many2one('sports.patient', string='Patient', related='injury_id.patient_id', store=True)
|
||||
# Refactor: documents are tied to a patient with optional injury linkage
|
||||
patient_id = fields.Many2one('sports.patient', string='Patient', required=True, ondelete='cascade', index=True)
|
||||
injury_id = fields.Many2one('sports.patient.injury', string='Injury', required=False, ondelete='cascade', index=True)
|
||||
description = fields.Text(string='Description')
|
||||
file_content = fields.Binary(string='File Content', required=True, attachment=False)
|
||||
file_name = fields.Char(string='File Name')
|
||||
file_size = fields.Integer(string='File Size', compute='_compute_file_size', store=True)
|
||||
# Note: 'xray' and 'mri' kept for backward compatibility; displayed as 'Medical Imaging'
|
||||
category = fields.Selection([
|
||||
('medical', 'Medical Report'),
|
||||
('xray', 'X-Ray'),
|
||||
('mri', 'MRI'),
|
||||
('medical', 'Medical'),
|
||||
('medical_imaging', 'Medical Imaging'),
|
||||
('xray', 'Medical Imaging'), # deprecated
|
||||
('mri', 'Medical Imaging'), # deprecated
|
||||
('prescription', 'Prescription'),
|
||||
('other', 'Other'),
|
||||
], string='Category', default='other', required=True)
|
||||
created_by_id = fields.Many2one('res.users', string='Uploaded By', default=lambda self: self.env.user, required=True)
|
||||
create_date = fields.Datetime(string='Upload Date')
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""Ensure backward compatibility: if an injury is provided but patient is missing,
|
||||
set patient_id from the injury's patient."""
|
||||
for vals in vals_list:
|
||||
if not vals.get('patient_id') and vals.get('injury_id'):
|
||||
injury = self.env['sports.patient.injury'].browse(vals['injury_id'])
|
||||
vals['patient_id'] = injury.patient_id.id
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.onchange('injury_id')
|
||||
def _onchange_injury_id_sync_patient(self):
|
||||
"""When selecting an injury, auto-set patient to the injury's patient."""
|
||||
for rec in self:
|
||||
if rec.injury_id:
|
||||
rec.patient_id = rec.injury_id.patient_id
|
||||
|
||||
@api.constrains('injury_id', 'patient_id')
|
||||
def _check_injury_belongs_to_patient(self):
|
||||
"""Ensure the selected injury belongs to the chosen patient."""
|
||||
for rec in self:
|
||||
if rec.injury_id and rec.patient_id and rec.injury_id.patient_id != rec.patient_id:
|
||||
raise ValidationError(_('The injury must belong to the selected patient.'))
|
||||
|
||||
@api.depends('file_content')
|
||||
def _compute_file_size(self):
|
||||
"""Compute the file size in bytes"""
|
||||
|
|
|
|||
|
|
@ -138,6 +138,16 @@ class Patient(models.Model):
|
|||
last_consultation_date = fields.Date(tracking=True)
|
||||
active_injury_count = fields.Integer(compute="_compute_active_injury_count")
|
||||
activity_count = fields.Integer(compute="_compute_activity_count")
|
||||
# Documents linked to this patient (optionally to an injury)
|
||||
document_ids = fields.One2many(
|
||||
comodel_name="sports.injury.document",
|
||||
inverse_name="patient_id",
|
||||
string="Documents",
|
||||
)
|
||||
document_count = fields.Integer(
|
||||
compute="_compute_document_count",
|
||||
string="Document Count",
|
||||
)
|
||||
allergies = fields.Text(
|
||||
groups="bemade_sports_clinic.group_sports_clinic_treatment_professional,bemade_sports_clinic.group_portal_treatment_professional",
|
||||
)
|
||||
|
|
@ -287,6 +297,25 @@ class Patient(models.Model):
|
|||
('res_id', '=', rec.id)
|
||||
])
|
||||
|
||||
def _compute_document_count(self):
|
||||
for patient in self:
|
||||
patient.document_count = self.env['sports.injury.document'].search_count([
|
||||
('patient_id', '=', patient.id)
|
||||
])
|
||||
|
||||
def action_view_documents(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Documents'),
|
||||
'res_model': 'sports.injury.document',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('patient_id', '=', self.id)],
|
||||
'context': {
|
||||
'default_patient_id': self.id,
|
||||
},
|
||||
}
|
||||
|
||||
def action_view_patient_form(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -29,26 +29,26 @@
|
|||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<a t-attf-class="nav-link #{'active' if view_type == 'all' else ''}"
|
||||
t-attf-href="/my/events?view_type=all#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}">
|
||||
t-attf-href="/my/events?view_type=all#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}#{no_default_dates and '&no_default_dates=1' or ''}">
|
||||
All Events
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a t-attf-class="nav-link #{'active' if view_type == 'my' else ''}"
|
||||
t-attf-href="/my/events?view_type=my#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}">
|
||||
t-attf-href="/my/events?view_type=my#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}#{no_default_dates and '&no_default_dates=1' or ''}">
|
||||
My Events
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a t-attf-class="nav-link #{'active' if view_type == 'unassigned' else ''}"
|
||||
t-attf-href="/my/events?view_type=unassigned#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}">
|
||||
t-attf-href="/my/events?view_type=unassigned#{project_id and '&project_id=%s' % project_id or ''}#{assigned_user_id and '&assigned_user_id=%s' % assigned_user_id or ''}#{date_from and '&date_from=%s' % date_from or ''}#{date_to and '&date_to=%s' % date_to or ''}#{no_default_dates and '&no_default_dates=1' or ''}">
|
||||
Unassigned Events
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<a href="/my/events?view_type=all" class="btn btn-outline-secondary btn-sm">
|
||||
<a href="/my/events?view_type=all&no_default_dates=1" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fa fa-times"/> Clear Filters
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -101,18 +101,33 @@
|
|||
|
||||
<!-- Second Row: Date Filters -->
|
||||
<div class="row g-2">
|
||||
<input type="hidden" name="no_default_dates" t-att-value="no_default_dates or ''"/>
|
||||
<!-- Date From Filter -->
|
||||
<div class="col-md-6">
|
||||
<label for="date_from" class="form-label text-muted small">From Date</label>
|
||||
<input type="date" id="date_from" name="date_from" class="form-control"
|
||||
t-att-value="date_from" onchange="this.form.submit();"/>
|
||||
<div class="input-group">
|
||||
<input type="date" id="date_from" name="date_from" class="form-control"
|
||||
t-att-value="date_from" onchange="this.form.submit();"/>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
title="Clear From Date"
|
||||
onclick="document.getElementById('date_from').value=''; this.form.no_default_dates.value='1'; this.form.submit();">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date To Filter -->
|
||||
<div class="col-md-6">
|
||||
<label for="date_to" class="form-label text-muted small">To Date</label>
|
||||
<input type="date" id="date_to" name="date_to" class="form-control"
|
||||
t-att-value="date_to" onchange="this.form.submit();"/>
|
||||
<div class="input-group">
|
||||
<input type="date" id="date_to" name="date_to" class="form-control"
|
||||
t-att-value="date_to" onchange="this.form.submit();"/>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
title="Clear To Date"
|
||||
onclick="document.getElementById('date_to').value=''; this.form.no_default_dates.value='1'; this.form.submit();">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -199,7 +214,8 @@
|
|||
<td>
|
||||
<strong t-esc="event.name"/>
|
||||
<t t-if="event.description">
|
||||
<br/><small class="text-muted" t-esc="event.description[:100] + '...' if len(event.description) > 100 else event.description"/>
|
||||
<br/>
|
||||
<div t-field="event.description" class="text-muted oe_no_empty"/>
|
||||
</t>
|
||||
</td>
|
||||
|
||||
|
|
|
|||
|
|
@ -581,17 +581,16 @@
|
|||
<t t-foreach="documents" t-as="doc">
|
||||
<tr>
|
||||
<td><t t-esc="doc.name"/></td>
|
||||
<td>
|
||||
<span t-if="doc.category == 'medical'" class="badge badge-info">Medical</span>
|
||||
<span t-elif="doc.category == 'xray'" class="badge badge-primary">X-Ray</span>
|
||||
<span t-elif="doc.category == 'mri'" class="badge badge-warning">MRI</span>
|
||||
<span t-elif="doc.category == 'prescription'" class="badge badge-success">Prescription</span>
|
||||
<span t-else="" class="badge badge-secondary">Other</span>
|
||||
</td>
|
||||
<td><t t-esc="doc.description"/></td>
|
||||
<td><span t-field="doc.created_by_id"/></td>
|
||||
<td><span t-field="doc.create_date" t-options='{"widget": "date"}'/></td>
|
||||
<td>
|
||||
<td>
|
||||
<span t-if="doc.category == 'medical'" class="badge badge-info">Medical</span>
|
||||
<span t-elif="doc.category == 'medical_imaging' or doc.category == 'xray' or doc.category == 'mri'" class="badge badge-primary">Medical Imaging</span>
|
||||
<span t-elif="doc.category == 'prescription'" class="badge badge-success">Prescription</span>
|
||||
<span t-else="" class="badge badge-secondary">Other</span>
|
||||
</td>
|
||||
<td><t t-raw="doc.description or ''"/></td>
|
||||
<td><span t-field="doc.created_by_id"/></td>
|
||||
<td><span t-field="doc.create_date" t-options='{"widget": "date"}'/></td>
|
||||
<td>
|
||||
<a t-att-href="'/my/injury/document/download/%s' % doc.id" class="btn btn-sm bg-o-color-1 text-white">
|
||||
<i class="fa fa-download"></i> Download
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,82 @@
|
|||
</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">
|
||||
<div class="col-md-6">
|
||||
|
|
|
|||
|
|
@ -138,10 +138,30 @@
|
|||
|
||||
<!-- Description -->
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="4"
|
||||
t-esc="event.description"></textarea>
|
||||
<label for="description_editor" class="form-label">Description</label>
|
||||
<!-- Hidden field actually submitted -->
|
||||
<input type="hidden" id="description" name="description" t-att-value="event.description or ''"/>
|
||||
<!-- Simple rich-text editor -->
|
||||
<div id="description_editor"
|
||||
class="form-control"
|
||||
style="min-height: 150px; overflow:auto;"
|
||||
contenteditable="true">
|
||||
<div t-field="event.description" class="oe_no_empty"/>
|
||||
</div>
|
||||
<small class="form-text text-muted">You can paste formatted text. The formatted content will be saved.</small>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
const form = document.currentScript && document.currentScript.closest('form');
|
||||
if (!form) return;
|
||||
const editor = form.querySelector('#description_editor');
|
||||
const hidden = form.querySelector('#description');
|
||||
if (!editor || !hidden) return;
|
||||
form.addEventListener('submit', function() {
|
||||
hidden.value = editor.innerHTML;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,24 +7,26 @@
|
|||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Teams</t>
|
||||
<t t-set="url">/my/teams</t>
|
||||
<t t-set="show_count" t-value="True"/>
|
||||
<t t-set="placeholder_count">teams_count</t>
|
||||
</t>
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Players</t>
|
||||
<t t-set="url">/my/players</t>
|
||||
<t t-set="show_count" t-value="True"/>
|
||||
<t t-set="placeholder_count">players_count</t>
|
||||
</t>
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Activities</t>
|
||||
<t t-set="url">/my/activities</t>
|
||||
<t t-set="show_count" t-value="True"/>
|
||||
<t t-set="placeholder_count">activities_count</t>
|
||||
<t t-set="config_card" t-value="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')"/>
|
||||
</t>
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Events</t>
|
||||
<t t-set="url">/my/events</t>
|
||||
<t t-set="show_count" t-value="True"/>
|
||||
<t t-set="config_card" t-value="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')"/>
|
||||
<t t-if="request.env.user.has_group('bemade_sports_clinic.group_portal_treatment_professional')">
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Events</t>
|
||||
<t t-set="url">/my/events</t>
|
||||
<t t-set="placeholder_count">events_count</t>
|
||||
<t t-set="config_card" t-value="True"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</xpath>
|
||||
|
|
@ -92,46 +94,24 @@
|
|||
<script type="text/javascript">
|
||||
<![CDATA[
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Remove problematic placeholder_count attributes for coaches and treatment professionals
|
||||
if (document.body.classList.contains('o_portal_user_coach') ||
|
||||
document.body.classList.contains('o_portal_user_therapist')) {
|
||||
|
||||
console.log('Portal spinner fix: Detected coach/therapist user');
|
||||
|
||||
// Remove ALL placeholder_count attributes that cause spinner issues
|
||||
const allCounterElements = document.querySelectorAll('[data-placeholder_count]');
|
||||
console.log('Portal spinner fix: Found ' + allCounterElements.length + ' placeholder_count elements');
|
||||
|
||||
allCounterElements.forEach(function(el) {
|
||||
const counterType = el.getAttribute('data-placeholder_count');
|
||||
console.log('Portal spinner fix: Removing ' + counterType + ' counter');
|
||||
el.removeAttribute('data-placeholder_count');
|
||||
|
||||
// Hide the parent card
|
||||
const card = el.closest('.o_portal_index_card');
|
||||
if (card && !card.classList.contains('d-none')) {
|
||||
card.classList.add('d-none');
|
||||
console.log('Portal spinner fix: Hiding card for ' + counterType);
|
||||
}
|
||||
});
|
||||
|
||||
// Force remove spinner immediately
|
||||
// Scope any DOM tweaks to the portal home container and coaches only
|
||||
const home = document.querySelector('.o_portal_my_home');
|
||||
if (!home) return;
|
||||
const isCoach = home.classList.contains('o_portal_user_coach');
|
||||
|
||||
if (!isCoach) {
|
||||
// Do not alter placeholders or visibility for therapists or other users
|
||||
return;
|
||||
}
|
||||
|
||||
// Minimal safe tweak for coaches only: if Odoo leaves a long-running spinner, remove it after a grace period
|
||||
setTimeout(function() {
|
||||
const spinner = document.querySelector('.o_portal_doc_spinner');
|
||||
if (spinner) {
|
||||
spinner.remove();
|
||||
console.log('Portal spinner fix: Spinner removed');
|
||||
console.log('Portal spinner fix: Spinner removed for coach');
|
||||
}
|
||||
|
||||
// Also remove any knowledge cards for therapists (they should only see activities)
|
||||
const knowledgeCards = document.querySelectorAll('a[href*="/knowledge"]');
|
||||
knowledgeCards.forEach(function(card) {
|
||||
const parentCard = card.closest('.o_portal_index_card');
|
||||
if (parentCard) {
|
||||
parentCard.classList.add('d-none');
|
||||
console.log('Portal spinner fix: Hiding knowledge card');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 2500);
|
||||
});
|
||||
]]>
|
||||
</script>
|
||||
|
|
@ -561,6 +541,14 @@
|
|||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="documents-tab" data-bs-toggle="tab" data-bs-target="#documents" type="button" role="tab" aria-controls="documents" aria-selected="false">
|
||||
<i class="fa fa-file"></i> Documents
|
||||
<span class="badge badge-pill badge-secondary ms-1" t-if="patient_documents">
|
||||
<t t-esc="len(patient_documents)"/>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
|
@ -811,7 +799,7 @@
|
|||
<h5 class="mt-4">Team Notes</h5>
|
||||
<div class="card border-info">
|
||||
<div class="card-body">
|
||||
<p t-esc="player.team_info_notes" class="mb-0"/>
|
||||
<div t-raw="player.team_info_notes" class="mb-0 oe_no_empty"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
|
@ -819,6 +807,95 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Documents Tab -->
|
||||
<div class="tab-pane fade" id="documents" role="tabpanel" aria-labelledby="documents-tab">
|
||||
<t t-if="is_treatment_prof">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="fa fa-upload"></i> Upload Document
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/my/patient/document/upload" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="patient_id" t-att-value="player.id"/>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" name="document_name" class="form-control" placeholder="Document name"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Category</label>
|
||||
<select name="category" class="form-select">
|
||||
<t t-foreach="categories" t-as="cat">
|
||||
<option t-att-value="cat[0]" t-esc="cat[1]"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Link to Injury (optional)</label>
|
||||
<select name="injury_id" class="form-select">
|
||||
<option value="">-- None --</option>
|
||||
<t t-foreach="injuries" t-as="injury">
|
||||
<option t-att-value="injury.id" t-esc="injury.display_name"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-control" rows="2" placeholder="Optional description"></textarea>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">File</label>
|
||||
<input type="file" name="attachment" class="form-control" required="required"/>
|
||||
<small class="form-text text-muted">Max size 10MB</small>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end justify-content-end">
|
||||
<button type="submit" class="btn bg-o-color-1 text-white">
|
||||
<i class="fa fa-upload"></i> Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="patient_documents">
|
||||
<t t-call="portal.portal_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Category</th>
|
||||
<th>Description</th>
|
||||
<th>Created</th>
|
||||
<th>By</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="patient_documents" t-as="doc">
|
||||
<tr>
|
||||
<td><span t-esc="doc.name"/></td>
|
||||
<td><span t-esc="doc.category"/></td>
|
||||
<td><span t-raw="doc.description or ''"/></td>
|
||||
<td><span t-field="doc.create_date" t-options="{'widget': 'datetime'}"/></td>
|
||||
<td><span t-esc="doc.created_by_id.display_name"/></td>
|
||||
<td class="text-end">
|
||||
<a t-att-href="'/my/patient/document/download/%s' % doc.id" class="btn btn-sm bg-o-color-1 text-white">
|
||||
<i class="fa fa-download"></i> Download
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</t>
|
||||
</t>
|
||||
<div t-else="" class="text-center text-muted py-4">
|
||||
<i class="fa fa-file fa-3x mb-3"></i>
|
||||
<p>No documents uploaded</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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="row">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,72 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<!-- Injury Document: backend views -->
|
||||
<record id="view_injury_document_tree" model="ir.ui.view">
|
||||
<field name="name">sports.injury.document.list</field>
|
||||
<field name="model">sports.injury.document</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Injury Documents" create="true" delete="true">
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="injury_id"/>
|
||||
<field name="category"/>
|
||||
<field name="description"/>
|
||||
<field name="created_by_id"/>
|
||||
<field name="create_date"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_injury_document_form" model="ir.ui.view">
|
||||
<field name="name">sports.injury.document.form</field>
|
||||
<field name="model">sports.injury.document</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Injury Document">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="injury_id" domain="[('patient_id', '=', patient_id)]"/>
|
||||
<field name="category"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="file_content" filename="file_name"/>
|
||||
<field name="file_name"/>
|
||||
<field name="file_size" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_injury_document_search" model="ir.ui.view">
|
||||
<field name="name">sports.injury.document.search</field>
|
||||
<field name="model">sports.injury.document</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="injury_id"/>
|
||||
<field name="category"/>
|
||||
<field name="created_by_id"/>
|
||||
<filter string="My Uploads" name="my_uploads" domain="[('created_by_id', '=', uid)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sports_injury_documents" model="ir.actions.act_window">
|
||||
<field name="name">Injury Documents</field>
|
||||
<field name="res_model">sports.injury.document</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="context">{"search_default_injury_id": active_id, "default_injury_id": active_id}</field>
|
||||
<field name="domain">[("injury_id", "=", active_id)]</field>
|
||||
</record>
|
||||
<record id="sports_patient_injury_view_form" model="ir.ui.view">
|
||||
<field name="name">sports.patient.injury.view.form</field>
|
||||
<field name="model">sports.patient.injury</field>
|
||||
|
|
@ -7,6 +74,10 @@
|
|||
<form>
|
||||
<header>
|
||||
<field name="stage" widget="statusbar"/>
|
||||
<!-- Smart button: Documents -->
|
||||
<button class="oe_stat_button" type="action" name="%(action_sports_injury_documents)d" icon="fa-file">
|
||||
<field name="document_count" string="Documents" widget="statinfo"/>
|
||||
</button>
|
||||
<button name="action_verify_injury"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
|
|
@ -59,6 +130,38 @@
|
|||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Documents">
|
||||
<field name="document_ids">
|
||||
<list string="Documents" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="category"/>
|
||||
<field name="description"/>
|
||||
<field name="created_by_id" readonly="1"/>
|
||||
<field name="create_date" readonly="1"/>
|
||||
</list>
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="injury_id" domain="[('patient_id', '=', patient_id)]"/>
|
||||
<field name="category"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="file_content" filename="file_name"/>
|
||||
<field name="file_name"/>
|
||||
<field name="file_size" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter reload_on_post="True"/>
|
||||
|
|
@ -80,6 +183,7 @@
|
|||
<field name="treatment_professional_ids"
|
||||
widget="many2many_avatar_user"/>
|
||||
<field name="activity_ids" widget="list_activity"/>
|
||||
<field name="document_count"/>
|
||||
<field name="parental_consent"/>
|
||||
<button name="action_view_injury_form" type="object"
|
||||
title="Details" string="Details"
|
||||
|
|
|
|||
|
|
@ -113,15 +113,18 @@
|
|||
<header>
|
||||
<field name="stage" widget="statusbar" />
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_report_injury" type="object" class="oe_stat_button" icon="fa-medkit">
|
||||
<div class="o_stat_info">
|
||||
<span class="o_stat_text">Report</span>
|
||||
<span class="o_stat_text">Injury</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_report_injury" type="object" class="oe_stat_button" icon="fa-medkit">
|
||||
<div class="o_stat_info">
|
||||
<span class="o_stat_text">Report</span>
|
||||
<span class="o_stat_text">Injury</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="action_view_documents" type="object" class="oe_stat_button" icon="fa-file">
|
||||
<field name="document_count" widget="statinfo" string="Documents"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title"><h1>Patient Record</h1></div>
|
||||
<group>
|
||||
<group col="3">
|
||||
|
|
@ -148,11 +151,11 @@
|
|||
<field name="return_date"/>
|
||||
<field name="team_info_notes"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Injuries">
|
||||
<field name="injury_ids"/>
|
||||
</page>
|
||||
<page string="Treatment Notes">
|
||||
<notebook>
|
||||
<page string="Injuries">
|
||||
<field name="injury_ids"/>
|
||||
</page>
|
||||
<page string="Treatment Notes">
|
||||
<field name="treatment_note_ids">
|
||||
<list string="Treatment Notes" editable="bottom">
|
||||
<field name="date"/>
|
||||
|
|
@ -162,8 +165,8 @@
|
|||
<field name="user_id" readonly="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Contacts">
|
||||
</page>
|
||||
<page string="Contacts">
|
||||
<group>
|
||||
<group string="Patient Phone & Email">
|
||||
<field name="phone" widget="phone"/>
|
||||
|
|
@ -190,10 +193,42 @@
|
|||
</list>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</group>
|
||||
</sheet>
|
||||
</page>
|
||||
<page string="Documents">
|
||||
<field name="document_ids">
|
||||
<list string="Documents" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="injury_id" domain="[('patient_id', '=', parent.id)]"/>
|
||||
<field name="category"/>
|
||||
<field name="description"/>
|
||||
<field name="created_by_id" readonly="1"/>
|
||||
<field name="create_date" readonly="1"/>
|
||||
</list>
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="patient_id"/>
|
||||
<field name="injury_id" domain="[('patient_id', '=', patient_id)]"/>
|
||||
<field name="category"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="file_content" filename="file_name"/>
|
||||
<field name="file_name"/>
|
||||
<field name="file_size" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter reload_on_post="True"/>
|
||||
</form>
|
||||
</field>
|
||||
|
|
|
|||
Loading…
Reference in a new issue