06/09 Allow customer to reply admin answer
This commit is contained in:
+54
-1
@@ -182,7 +182,7 @@ def my_tickets():
|
|||||||
|
|
||||||
# ── Customer: ticket detail ───────────────────────────────────────────────────
|
# ── Customer: ticket detail ───────────────────────────────────────────────────
|
||||||
|
|
||||||
@bp.route('/my-tickets/<int:ticket_id>')
|
@bp.route('/my-tickets/<int:ticket_id>', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def my_ticket_detail(ticket_id):
|
def my_ticket_detail(ticket_id):
|
||||||
if current_user.role != 'customer':
|
if current_user.role != 'customer':
|
||||||
@@ -192,6 +192,36 @@ def my_ticket_detail(ticket_id):
|
|||||||
if ticket is None or ticket.customer_id != current_user.id:
|
if ticket is None or ticket.customer_id != current_user.id:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
if ticket.status == 'closed':
|
||||||
|
flash('This ticket is closed and cannot receive new replies.', 'warning')
|
||||||
|
return redirect(url_for('support.my_ticket_detail', ticket_id=ticket_id))
|
||||||
|
|
||||||
|
body = request.form.get('body', '').strip()
|
||||||
|
if not body:
|
||||||
|
flash('Reply cannot be empty.', 'warning')
|
||||||
|
return redirect(url_for('support.my_ticket_detail', ticket_id=ticket_id))
|
||||||
|
|
||||||
|
reply = SupportTicketReply(
|
||||||
|
ticket_id = ticket.id,
|
||||||
|
user_id = current_user.id,
|
||||||
|
body = body,
|
||||||
|
created_at = now_eastern(),
|
||||||
|
)
|
||||||
|
db.session.add(reply)
|
||||||
|
# Reopen if it was answered so admin sees there's a follow-up
|
||||||
|
if ticket.status == 'answered':
|
||||||
|
ticket.status = 'open'
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
log_action(ACTION_CREATE, 'SupportTicketReply', reply.id,
|
||||||
|
f'ticket #{ticket.id}',
|
||||||
|
f'customer reply by {current_user.username}')
|
||||||
|
|
||||||
|
_notify_admins_customer_reply(ticket, reply)
|
||||||
|
flash('Your reply has been sent.', 'success')
|
||||||
|
return redirect(url_for('support.my_ticket_detail', ticket_id=ticket_id))
|
||||||
|
|
||||||
replies = ticket.replies.order_by(SupportTicketReply.created_at.asc()).all()
|
replies = ticket.replies.order_by(SupportTicketReply.created_at.asc()).all()
|
||||||
return render_template('support/my_ticket_detail.html',
|
return render_template('support/my_ticket_detail.html',
|
||||||
ticket=ticket, replies=replies)
|
ticket=ticket, replies=replies)
|
||||||
@@ -316,3 +346,26 @@ def _notify_customer_reply(ticket, reply):
|
|||||||
send_email = True,
|
send_email = True,
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _notify_admins_customer_reply(ticket, reply):
|
||||||
|
"""Notify admins when a customer adds a follow-up reply to their ticket."""
|
||||||
|
admins = User.query.filter_by(role='admin', active=True).all()
|
||||||
|
if not admins:
|
||||||
|
return
|
||||||
|
|
||||||
|
customer_label = ticket.customer.display_name if ticket.customer else 'Unknown'
|
||||||
|
link = url_for('support.admin_ticket_detail', ticket_id=ticket.id)
|
||||||
|
title = f'Customer reply on ticket #{ticket.id} from {customer_label}'
|
||||||
|
body = (f'Re: {ticket.subject}\n\n'
|
||||||
|
f'{reply.body[:400]}{"…" if len(reply.body) > 400 else ""}')
|
||||||
|
|
||||||
|
for admin in admins:
|
||||||
|
notify(
|
||||||
|
recipient = admin,
|
||||||
|
title = title,
|
||||||
|
body = body,
|
||||||
|
link = link,
|
||||||
|
send_email = True,
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|||||||
@@ -60,7 +60,30 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="text-center mt-2">
|
{% if ticket.status != 'closed' %}
|
||||||
|
<div class="card shadow-sm mt-3">
|
||||||
|
<div class="card-header"><i class="bi bi-reply me-2"></i>Add a Follow-up</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<textarea name="body" class="form-control" rows="4" required
|
||||||
|
placeholder="Type your follow-up message…"></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">
|
||||||
|
<i class="bi bi-send me-1"></i>Send Reply
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-secondary mt-3">
|
||||||
|
<i class="bi bi-lock me-2"></i>This request is closed.
|
||||||
|
<a href="{{ url_for('support.chat') }}">Start a new chat</a> if you need further help.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
<a href="{{ url_for('support.chat') }}" class="btn btn-outline-primary btn-sm">
|
<a href="{{ url_for('support.chat') }}" class="btn btn-outline-primary btn-sm">
|
||||||
<i class="bi bi-chat-dots me-1"></i>Ask another question
|
<i class="bi bi-chat-dots me-1"></i>Ask another question
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user