Fix portal activity modal dismiss buttons and XML syntax errors

- Replace unreliable data-dismiss='modal' with direct JavaScript closeModal() calls
- Add robust closeModal() function with Bootstrap detection and DOM fallback
- Fix Complete, Reschedule, and Cancel modal dismiss button functionality
- Wrap JavaScript in CDATA section to resolve XML parsing errors with unescaped ampersands
- Add copyFeedbackToHidden() and copyDateToHidden() functions for proper form data transfer
- Ensure all modal action buttons work consistently across portal UI

All portal activity modal interactions now functional with console logging for debugging.
This commit is contained in:
Denis Durepos 2025-07-27 21:32:10 -04:00
parent fae0d98637
commit f5911f0767
7 changed files with 296 additions and 119 deletions

View file

@ -39,11 +39,20 @@ class TaskManagementPortal(CustomerPortal):
# User must be staff on at least one of the patient's teams
if not (user_teams & patient_teams):
raise UserError(_('You do not have access to this injury.'))
# For team records, check if user is staff on the team
elif model_name == 'sports.team':
# Check if user has access through team staff relationships
user_teams = user.partner_id.team_staff_rel_ids.mapped('team_id')
# User must be staff on this specific team
if record not in user_teams:
raise UserError(_('You do not have access to this team.'))
return record
@http.route(['/my/activities'], type='http', auth='user', website=True)
def view_activities(self, model=None, **kw):
def view_activities(self, model=None, res_id=None, **kw):
"""Display list of activities assigned to the current user"""
user = request.env.user
partner = user.partner_id
@ -54,8 +63,11 @@ class TaskManagementPortal(CustomerPortal):
# Apply model filtering if specified
if model:
domain.append(('res_model', '=', model))
# Apply res_id filtering if specified
if res_id:
domain.append(('res_id', '=', int(res_id)))
else:
domain.append(('res_model', 'in', ['sports.patient', 'sports.patient.injury']))
domain.append(('res_model', 'in', ['sports.patient', 'sports.patient.injury', 'sports.team']))
# Get activities assigned to this user
activities = request.env['mail.activity'].search(domain, order='date_deadline asc')
@ -63,6 +75,7 @@ class TaskManagementPortal(CustomerPortal):
# Group activities by model
patient_activities = activities.filtered(lambda a: a.res_model == 'sports.patient')
injury_activities = activities.filtered(lambda a: a.res_model == 'sports.patient.injury')
team_activities = activities.filtered(lambda a: a.res_model == 'sports.team')
# Get activity types for filtering
activity_types = request.env['mail.activity.type'].search([])
@ -73,6 +86,7 @@ class TaskManagementPortal(CustomerPortal):
'activities': activities,
'patient_activities': patient_activities,
'injury_activities': injury_activities,
'team_activities': team_activities,
'activity_types': activity_types,
'page_name': 'activities',
'today': date.today().strftime('%Y-%m-%d'),
@ -84,7 +98,7 @@ class TaskManagementPortal(CustomerPortal):
def create_activity_form(self, model=None, res_id=None, **kw):
"""Display form to create a new activity"""
# Validate model and res_id
valid_models = ['sports.patient', 'sports.patient.injury']
valid_models = ['sports.patient', 'sports.patient.injury', 'sports.team']
if model not in valid_models or not res_id:
return request.redirect('/my/activities')
@ -118,6 +132,8 @@ class TaskManagementPortal(CustomerPortal):
# Default return URL
if model == 'sports.patient':
return_url = f'/my/player?player_id={res_id}'
elif model == 'sports.team':
return_url = f'/my/team?team_id={res_id}'
else: # sports.patient.injury
return_url = f'/my/player?player_id={record.patient_id.id}'
@ -145,7 +161,7 @@ class TaskManagementPortal(CustomerPortal):
res_id = post.get('res_id')
# Validate model and res_id
valid_models = ['sports.patient', 'sports.patient.injury']
valid_models = ['sports.patient', 'sports.patient.injury', 'sports.team']
if model not in valid_models or not res_id:
return request.redirect('/my/activities')
@ -258,7 +274,7 @@ class TaskManagementPortal(CustomerPortal):
separator = '&' if '?' in return_url else '?'
return request.redirect(f'{return_url}{separator}success=activity_updated')
@http.route(['/my/activity/complete'], type='http', auth='user', website=True, methods=['POST'], csrf=False)
@http.route(['/my/activity/complete'], type='http', auth='user', website=True, methods=['POST'])
def complete_activity(self, **post):
"""Mark an activity as done"""
activity_id = post.get('activity_id')
@ -280,7 +296,7 @@ class TaskManagementPortal(CustomerPortal):
# Redirect to activities page
return request.redirect('/my/activities')
@http.route(['/my/activity/cancel'], type='http', auth='user', website=True, methods=['POST'], csrf=False)
@http.route(['/my/activity/cancel'], type='http', auth='user', website=True, methods=['POST'])
def cancel_activity(self, **post):
"""Cancel an activity"""
activity_id = post.get('activity_id')
@ -299,7 +315,7 @@ class TaskManagementPortal(CustomerPortal):
# Redirect to activities page
return request.redirect('/my/activities')
@http.route(['/my/activity/reschedule'], type='http', auth='user', website=True, methods=['POST'], csrf=False)
@http.route(['/my/activity/reschedule'], type='http', auth='user', website=True, methods=['POST'])
def reschedule_activity(self, **post):
"""Reschedule an activity to a new date"""
activity_id = post.get('activity_id')

View file

@ -8,9 +8,12 @@ class TeamStaffPortal(CustomerPortal):
rtn = super()._prepare_home_portal_values(counters)
teams_domain = self._prepare_teams_domain()
players_domain = self._prepare_players_domain(teams_domain)
activities_domain = self._prepare_activities_domain()
rtn['teams_count'] = http.request.env['sports.team'].search_count(teams_domain)
rtn['players_count'] = http.request.env['sports.patient'].search_count(
players_domain)
rtn['activities_count'] = http.request.env['mail.activity'].search_count(
activities_domain)
return rtn
@classmethod
@ -27,6 +30,13 @@ class TeamStaffPortal(CustomerPortal):
('team_ids', 'in', team_ids),
]
@classmethod
def _prepare_activities_domain(cls):
user = http.request.env.user
return [
('user_id', '=', user.id),
]
@http.route(route=['/my/teams', '/my/teams/page/<int:page>'], type='http', auth='user', website=True)
def view_teams(self, page=0, **kw):
""" Display the list of teams that a portal user has access to """

View file

@ -130,6 +130,7 @@ 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")
allergies = fields.Text(
groups="bemade_sports_clinic.group_sports_clinic_treatment_professional,bemade_sports_clinic.group_portal_treatment_professional",
)
@ -265,6 +266,13 @@ class Patient(models.Model):
[('patient_id', '=', patient.id)]
)
def _compute_activity_count(self):
for rec in self:
rec.activity_count = self.env['mail.activity'].search_count([
('res_model', '=', 'sports.patient'),
('res_id', '=', rec.id)
])
def action_view_patient_form(self):
self.ensure_one()
return {

View file

@ -141,6 +141,10 @@ class PatientInjury(models.Model):
string='Document Count',
compute='_compute_document_count'
)
activity_count = fields.Integer(
string='Activity Count',
compute='_compute_activity_count'
)
@api.depends('treatment_note_ids')
def _compute_treatment_note_count(self):
@ -152,6 +156,13 @@ class PatientInjury(models.Model):
for record in self:
record.document_count = len(record.document_ids)
def _compute_activity_count(self):
for rec in self:
rec.activity_count = self.env['mail.activity'].search_count([
('res_model', '=', 'sports.patient.injury'),
('res_id', '=', rec.id)
])
@api.constrains("injury_date_na", "injury_date")
def constrain_date_blank_only_if_na(self):
for rec in self:

View file

@ -19,6 +19,7 @@ class SportsTeam(models.Model):
player_count = fields.Integer(compute="_compute_player_counts")
injured_count = fields.Integer(compute="_compute_player_counts")
healthy_count = fields.Integer(compute="_compute_player_counts")
activity_count = fields.Integer(compute="_compute_activity_count")
parent_id = fields.Many2one(
comodel_name="res.partner",
string="Parent Organization",
@ -96,6 +97,13 @@ class SportsTeam(models.Model):
staff = rec.staff_ids.filtered(lambda r: r.role == "head_therapist")
rec.head_therapist_id = staff.partner_id if staff else False
def _compute_activity_count(self):
for rec in self:
rec.activity_count = self.env['mail.activity'].search_count([
('res_model', '=', 'sports.team'),
('res_id', '=', rec.id)
])
def _compute_allowed_user_ids(self):
for rec in self:
rec.allowed_user_ids = rec.staff_ids.user_ids
@ -214,16 +222,13 @@ class TeamStaff(models.Model):
def _action_revoke_portal_access(self):
"""Private method containing the actual sudo operations for revoking portal access."""
group_portal = self.env.ref("base.group_portal")
group_public = self.env.ref("base.group_public")
# Deactivate the user and remove from portal group
# Deactivate the user and set to public user type (standard Odoo approach)
if self.user_ids:
self.user_ids.write(
self.user_ids.sudo().write(
{
"groups_id": [
Command.unlink(group_portal.id),
Command.link(group_public.id),
],
"groups_id": [(6, 0, [group_public.id])], # Set to public user only
"active": False,
}
)

View file

@ -13,6 +13,11 @@
<t t-set="url">/my/players</t>
<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="placeholder_count">activities_count</t>
</t>
</xpath>
</template>
<template id="portal_my_teams">
@ -25,6 +30,8 @@
<th>Parent Organization</th>
<th>Total Players</th>
<th>Injured</th>
<th>Activities</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -39,6 +46,16 @@
<td><span t-field="team.parent_id"/></td>
<td><span t-field="team.player_count"/></td>
<td><span t-field="team.injured_count"/></td>
<td>
<t t-set="team_activity_count" t-value="team.activity_count or 0"/>
<span t-esc="team_activity_count"/>
</td>
<td>
<a t-attf-href="/my/activities?model=sports.team&amp;res_id={{ team.id }}"
class="btn bg-o-color-1 text-white btn-sm">
<i class="fa fa-tasks"></i> Activities
</a>
</td>
</tr>
</t>
</tbody>
@ -91,9 +108,17 @@
</span>
</t>
</h1>
<a t-attf-href="/my/team/{{ team.id }}/add_player" class="btn bg-o-color-1 text-white">
<i class="fa fa-plus me-1"/> Add Player
</a>
<div>
<a t-attf-href="/my/activities?model=sports.team&amp;res_id={{ team.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-attf-href="/my/activity/create?model=sports.team&amp;res_id={{ team.id }}" class="btn bg-o-color-2 text-white me-2">
<i class="fa fa-tasks"></i> Add Activity
</a>
<a t-attf-href="/my/team/{{ team.id }}/add_player" class="btn bg-o-color-1 text-white">
<i class="fa fa-plus me-1"/> Add Player
</a>
</div>
</div>
<!-- Pending Removals Alert -->
@ -243,6 +268,8 @@
<th>Practice Status</th>
<th>Estimated Return Date</th>
<th>Last Consultation Date</th>
<th>Activities</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -282,6 +309,16 @@
<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&amp;res_id={{ player.id }}"
class="btn bg-o-color-1 text-white btn-sm">
<i class="fa fa-tasks"></i> Activities
</a>
</td>
</tr>
</t>
</tbody>
@ -299,6 +336,9 @@
<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> Report Injury
</a>
<a t-attf-href="/my/activities?model=sports.patient&amp;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&amp;res_id=%s' % player.id" class="btn bg-o-color-2 text-white me-2">
<i class="fa fa-tasks"></i> Add Activity
</a>
@ -416,6 +456,7 @@
<t t-if="not is_treatment_prof">
<th>External Notes</th>
</t>
<th>Activities</th>
<t t-if="is_treatment_prof">
<th class="text-end">Actions</th>
</t>
@ -452,6 +493,16 @@
<div t-else="" class="text-muted">No external notes</div>
</td>
</t>
<td>
<div class="d-flex align-items-center">
<t t-set="injury_activity_count" t-value="injury.activity_count or 0"/>
<span t-esc="injury_activity_count" class="me-2"/>
<a t-attf-href="/my/activities?model=sports.patient.injury&amp;res_id={{ injury.id }}"
class="btn bg-o-color-1 text-white btn-sm">
<i class="fa fa-tasks"></i>
</a>
</div>
</td>
<t t-if="is_treatment_prof">
<td class="text-end">
<div class="btn-group">
@ -464,6 +515,9 @@
<a t-att-href="'/my/injury/documents?injury_id=%s' % injury.id" class="btn btn-sm bg-o-color-2 text-white">
<i class="fa fa-file-medical"></i> Docs
</a>
<a t-attf-href="/my/activity/create?model=sports.patient.injury&amp;res_id={{ injury.id }}" class="btn btn-sm bg-o-color-2 text-white">
<i class="fa fa-tasks"></i> Add Activity
</a>
</div>
</td>
</t>

View file

@ -120,106 +120,22 @@
</td>
<td>
<!-- Mark as done button -->
<button class="btn btn-sm bg-o-color-1 text-white mb-1" data-toggle="modal" t-attf-data-target="#completeActivityModal_#{activity.id}">
<button type="button" class="btn btn-sm bg-o-color-1 text-white mb-1"
t-attf-onclick="openActivityModal('completeModal', #{activity.id})">
<i class="fa fa-check"></i> Done
</button>
<!-- Reschedule button -->
<button class="btn btn-sm bg-o-color-2 text-white mb-1" data-toggle="modal" t-attf-data-target="#rescheduleActivityModal_#{activity.id}">
<button type="button" class="btn btn-sm bg-o-color-2 text-white mb-1"
t-attf-onclick="openActivityModal('rescheduleModal', #{activity.id})">
<i class="fa fa-calendar"></i> Reschedule
</button>
<!-- Cancel button -->
<button class="btn btn-sm btn-danger mb-1" data-toggle="modal" t-attf-data-target="#cancelActivityModal_#{activity.id}">
<button type="button" class="btn btn-sm btn-danger mb-1"
t-attf-onclick="openActivityModal('cancelModal', #{activity.id})">
<i class="fa fa-times"></i> Cancel
</button>
<!-- Complete Activity Modal -->
<div class="modal fade" t-attf-id="completeActivityModal_#{activity.id}" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form t-att-action="'/my/activity/complete'" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="activity_id" t-att-value="activity.id"/>
<div class="modal-header">
<h5 class="modal-title">Complete Activity</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&amp;times;</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to mark this activity as completed?</p>
<div class="form-group">
<label for="feedback">Feedback (optional)</label>
<textarea name="feedback" class="form-control" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn bg-o-color-2 text-white" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn bg-o-color-1 text-white">Complete Activity</button>
</div>
</form>
</div>
</div>
</div>
<!-- Reschedule Activity Modal -->
<div class="modal fade" t-attf-id="rescheduleActivityModal_#{activity.id}" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form t-att-action="'/my/activity/reschedule'" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="activity_id" t-att-value="activity.id"/>
<div class="modal-header">
<h5 class="modal-title">Reschedule Activity</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&amp;times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="new_deadline">New Due Date</label>
<input type="date" name="new_deadline" class="form-control" required="required"
t-att-min="today"
t-att-value="today"/>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn bg-o-color-2 text-white" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn bg-o-color-1 text-white">Reschedule</button>
</div>
</form>
</div>
</div>
</div>
<!-- Cancel Activity Modal -->
<div class="modal fade" t-attf-id="cancelActivityModal_#{activity.id}" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form t-att-action="'/my/activity/cancel'" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="activity_id" t-att-value="activity.id"/>
<div class="modal-header">
<h5 class="modal-title">Cancel Activity</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&amp;times;</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to cancel this activity? This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn bg-o-color-2 text-white" data-dismiss="modal">No</button>
<button type="submit" class="btn btn-danger">Yes, Cancel Activity</button>
</div>
</form>
</div>
</div>
</div>
</td>
</tr>
</t>
@ -229,6 +145,174 @@
</tbody>
</table>
</div>
<!-- Shared Activity Action Modals -->
<!-- Complete Activity Modal -->
<div class="modal fade" id="completeModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Complete Activity</h5>
<button type="button" class="close" onclick="closeModal('completeModal')">
<span>×</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to mark this activity as completed?</p>
<div class="form-group">
<label for="feedback">Feedback (optional)</label>
<textarea id="complete_feedback" class="form-control" rows="3" placeholder="Add any feedback or notes about completing this activity..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn bg-o-color-2 text-white" onclick="closeModal('completeModal')">Cancel</button>
<form method="post" action="/my/activity/complete" style="display: inline;">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="activity_id" id="completeActivityId"/>
<input type="hidden" name="feedback" id="complete_feedback_hidden"/>
<button type="submit" class="btn bg-o-color-1 text-white" onclick="copyFeedbackToHidden()">Complete Activity</button>
</form>
</div>
</div>
</div>
</div>
<!-- Reschedule Activity Modal -->
<div class="modal fade" id="rescheduleModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Reschedule Activity</h5>
<button type="button" class="close" onclick="closeModal('rescheduleModal')">
<span>×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="new_deadline">New Due Date</label>
<input type="date" id="reschedule_new_deadline" class="form-control" required="required"
t-att-min="datetime.date.today().strftime('%Y-%m-%d')"/>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn bg-o-color-2 text-white" onclick="closeModal('rescheduleModal')">Cancel</button>
<form method="post" action="/my/activity/reschedule" style="display: inline;">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="activity_id" id="rescheduleActivityId"/>
<input type="hidden" name="new_deadline" id="reschedule_deadline_hidden"/>
<button type="submit" class="btn bg-o-color-1 text-white" onclick="copyDateToHidden()">Reschedule</button>
</form>
</div>
</div>
</div>
</div>
<!-- Cancel Activity Modal -->
<div class="modal fade" id="cancelModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Cancel Activity</h5>
<button type="button" class="close" onclick="closeModal('cancelModal')">
<span>×</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to cancel this activity? This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn bg-o-color-2 text-white" onclick="closeModal('cancelModal')">No</button>
<form method="post" action="/my/activity/cancel" style="display: inline;">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="activity_id" id="cancelActivityId"/>
<button type="submit" class="btn btn-danger">Yes, Cancel Activity</button>
</form>
</div>
</div>
</div>
</div>
<!-- JavaScript for modal functionality -->
<script>
<![CDATA[
function openActivityModal(modalType, activityId) {
console.log('openActivityModal called:', modalType, activityId);
// Check if Bootstrap is loaded
if (typeof $ === 'undefined') {
console.error('jQuery not loaded');
return;
}
if (typeof $.fn.modal === 'undefined') {
console.error('Bootstrap modal not loaded');
return;
}
// Set the activity ID in the appropriate modal form
if (modalType === 'completeModal') {
document.getElementById('completeActivityId').value = activityId;
} else if (modalType === 'rescheduleModal') {
document.getElementById('rescheduleActivityId').value = activityId;
} else if (modalType === 'cancelModal') {
document.getElementById('cancelActivityId').value = activityId;
}
console.log('Activity ID set for', modalType, ':', activityId);
// Open the modal using jQuery
$('#' + modalType).modal('show');
console.log('Modal opened:', modalType);
}
// Test function to manually open modal
function testModal() {
console.log('Testing modal...');
$('#completeModal').modal('show');
}
// Function to copy date from visible input to hidden input
function copyDateToHidden() {
var visibleDate = document.getElementById('reschedule_new_deadline').value;
document.getElementById('reschedule_deadline_hidden').value = visibleDate;
console.log('Date copied to hidden field:', visibleDate);
}
// Function to copy feedback from visible textarea to hidden input
function copyFeedbackToHidden() {
var visibleFeedback = document.getElementById('complete_feedback').value;
document.getElementById('complete_feedback_hidden').value = visibleFeedback;
console.log('Feedback copied to hidden field:', visibleFeedback);
}
// Function to close modal
function closeModal(modalId) {
console.log('closeModal called for:', modalId);
// Check if Bootstrap is loaded
if (typeof $ !== 'undefined' && typeof $.fn.modal !== 'undefined') {
$('#' + modalId).modal('hide');
console.log('Modal closed via Bootstrap:', modalId);
} else {
// Fallback: manually hide modal
var modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'none';
modal.classList.remove('show');
// Remove backdrop
var backdrop = document.querySelector('.modal-backdrop');
if (backdrop) {
backdrop.remove();
}
// Remove modal-open class from body
document.body.classList.remove('modal-open');
console.log('Modal closed via fallback method:', modalId);
}
}
}
]]>
</script>
</template>
<!-- Template for creating a new activity -->
@ -432,18 +516,7 @@
<!-- Activity buttons now integrated directly into the portal_my_player_injuries template -->
<!-- Add activities section to the portal home page -->
<template id="portal_my_home_activities" name="Portal My Home : Activities" inherit_id="portal.portal_my_home">
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
<div t-if="activities_count" class="o_portal_docs list-group-item list-group-item-action">
<a href="/my/activities">
<span class="d-flex justify-content-between align-items-center">
<span>Activities</span>
<span class="badge badge-pill badge-primary" t-esc="activities_count"/>
</span>
</a>
</div>
</xpath>
</template>
<!-- Activities entry removed - now handled in main portal template for consistency -->
<!-- Add link to activities in the portal navigation -->
<template id="portal_breadcrumbs_activities" name="Portal Breadcrumbs : Activities" inherit_id="portal.portal_breadcrumbs">