| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946 |
- <?xml version="1.0" encoding="utf-8"?>
- <odoo>
- <!-- Extend portal tickets page to add "New" button for collaborators -->
- <template id="portal_helpdesk_ticket_new_button" name="Portal Helpdesk Tickets New Button" inherit_id="helpdesk.portal_helpdesk_ticket" priority="20">
- <!-- Add button after the searchbar, similar to how helpdesk_sale_timesheet extends this template -->
- <xpath expr="//t[@t-call='portal.portal_searchbar']" position="after">
- <div t-if="collaborator_team_form_url or admin_teams" class="mb-3 text-end">
- <a t-if="collaborator_team_form_url" t-attf-href="#{collaborator_team_form_url}" class="btn btn-primary me-2">
- <i class="fa fa-plus me-2"/>
- Nuevo Ticket
- </a>
- <t t-if="admin_teams" t-foreach="admin_teams" t-as="admin_team">
- <a t-attf-href="/my/helpdesk/teams/#{admin_team.id}/collaborators" class="btn btn-primary me-2">
- <i class="fa fa-users me-2"/>
- Gestionar Colaboradores - <t t-esc="admin_team.name"/>
- </a>
- </t>
- </div>
- </xpath>
- </template>
-
- <!-- Extend ticket detail view to show new fields -->
- <template id="portal_helpdesk_ticket_detail_extras" name="Portal Helpdesk Ticket Detail Extras" inherit_id="helpdesk.tickets_followup" priority="20">
- <!-- Remove hard-coded fields, render everything dynamically based on workflow template -->
- <xpath expr="//div[@name='description']" position="replace">
- <!-- Render all workflow template fields in order, considering visibility -->
- <t t-if="ticket.team_id.workflow_template_id and ticket.team_id.workflow_template_id.field_ids">
- <t t-foreach="ticket.team_id.workflow_template_id.field_ids.sorted(lambda f: f.sequence)" t-as="template_field">
- <!-- Check visibility condition -->
- <t t-set="is_visible" t-value="True"/>
- <t t-if="template_field.visibility_dependency">
- <t t-set="dependency_field_name" t-value="template_field.visibility_dependency.name"/>
- <t t-set="field_value" t-value="ticket[dependency_field_name] if dependency_field_name in ticket._fields else False"/>
-
- <!-- Evaluate visibility based on comparator -->
- <t t-if="template_field.visibility_comparator == 'equal'">
- <t t-if="template_field.visibility_dependency.ttype == 'many2one'">
- <t t-set="is_visible" t-value="field_value.id == template_field.visibility_condition_m2o_id if field_value else False"/>
- </t>
- <t t-elif="template_field.visibility_dependency.ttype == 'selection'">
- <t t-set="is_visible" t-value="field_value == template_field.visibility_condition_selection"/>
- </t>
- <t t-else="">
- <t t-set="is_visible" t-value="str(field_value) == template_field.visibility_condition"/>
- </t>
- </t>
- <t t-elif="template_field.visibility_comparator == '!equal'">
- <t t-if="template_field.visibility_dependency.ttype == 'many2one'">
- <t t-set="is_visible" t-value="field_value.id != template_field.visibility_condition_m2o_id if field_value else True"/>
- </t>
- <t t-elif="template_field.visibility_dependency.ttype == 'selection'">
- <t t-set="is_visible" t-value="field_value != template_field.visibility_condition_selection"/>
- </t>
- <t t-else="">
- <t t-set="is_visible" t-value="str(field_value) != template_field.visibility_condition"/>
- </t>
- </t>
- <t t-elif="template_field.visibility_comparator == 'set'">
- <t t-set="is_visible" t-value="bool(field_value)"/>
- </t>
- <t t-elif="template_field.visibility_comparator == '!set'">
- <t t-set="is_visible" t-value="not bool(field_value)"/>
- </t>
- <t t-elif="template_field.visibility_comparator == 'contains'">
- <t t-set="is_visible" t-value="template_field.visibility_condition in str(field_value)"/>
- </t>
- <t t-elif="template_field.visibility_comparator == '!contains'">
- <t t-set="is_visible" t-value="template_field.visibility_condition not in str(field_value)"/>
- </t>
- </t>
-
- <!-- Display field if visible and has value (or is description which is special) -->
- <t t-set="field_name" t-value="template_field.field_name"/>
- <t t-set="field_val" t-value="ticket[field_name] if field_name in ticket._fields else False"/>
-
- <div t-if="is_visible and (field_val or field_name == 'description')" class="row mb-4" t-att-name="field_name">
- <strong class="col-lg-3" t-esc="template_field.label_custom or template_field.field_id.field_description"/>
- <div class="col-lg-9">
- <t t-if="template_field.field_type in ['char', 'text']">
- <span t-out="field_val"/>
- </t>
- <t t-elif="template_field.field_type == 'html'">
- <div t-out="field_val"/>
- </t>
- <t t-elif="template_field.field_type in ['integer', 'float', 'monetary']">
- <span t-out="field_val"/>
- </t>
- <t t-elif="template_field.field_type in ['date', 'datetime']">
- <span t-out="field_val"/>
- </t>
- <t t-elif="template_field.field_type == 'boolean'">
- <span t-if="field_val">Yes</span>
- <span t-else="">No</span>
- </t>
- <t t-elif="template_field.field_type == 'selection'">
- <span t-out="field_val"/>
- </t>
- <t t-elif="template_field.field_type == 'many2one'">
- <span t-out="field_val.display_name if field_val else ''"/>
- </t>
- <t t-elif="template_field.field_type in ['many2many', 'one2many']">
- <t t-foreach="field_val" t-as="item">
- <span class="badge bg-primary me-1" t-esc="item.display_name"/>
- </t>
- </t>
- <t t-else="">
- <span t-out="field_val"/>
- </t>
- </div>
- </div>
- </t>
- </t>
-
- <!-- Fallback: If no workflow template, show description at least -->
- <div t-else="" class="row mb-4" name="description">
- <strong class="col-lg-3">Description</strong>
- <div class="col-lg-9" t-field="ticket.description"/>
- </div>
-
- <!-- Customer Approval Section - ALWAYS AT THE END -->
- <div t-if="ticket.stage_id.requires_customer_approval" class="row mb-4">
- <div class="col-12">
- <div t-if="ticket.customer_approval_status == 'pending'" class="alert alert-warning mt-3" role="alert">
- <h5><i class="fa fa-exclamation-triangle"/> <span>Aprobación Requerida</span></h5>
- <p>Este ticket requiere su aprobación para continuar. Por favor revise y apruebe o rechace.</p>
-
- <div class="d-flex gap-2 mt-3">
- <!-- Approve Button (with confirmation modal) -->
- <button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#approveModal">
- <i class="fa fa-check"/> <span>Aprobar</span>
- </button>
-
- <!-- Reject Button (with modal) -->
- <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#rejectModal">
- <i class="fa fa-times"/> <span>Rechazar</span>
- </button>
- </div>
- </div>
-
- <!-- Approval status badge -->
- <div class="mt-2">
- <strong><span>Estado de Aprobación del Cliente</span>:</strong>
- <span t-if="ticket.customer_approval_status == 'approved'" class="badge bg-success ms-2">
- <i class="fa fa-check"/> <span>Aprobado</span>
- </span>
- <span t-elif="ticket.customer_approval_status == 'rejected'" class="badge bg-danger ms-2">
- <i class="fa fa-times"/> <span>Rechazado</span>
- </span>
- <span t-else="" class="badge bg-warning ms-2">
- <i class="fa fa-clock-o"/> <span>Pendiente de Aprobación</span>
- </span>
- </div>
-
- <!-- Show rejection reason if exists -->
- <div t-if="ticket.customer_rejection_reason" class="mt-2">
- <strong><span>Razón de Rechazo</span>:</strong>
- <p class="text-muted" t-esc="ticket.customer_rejection_reason"/>
- </div>
- </div>
- </div>
-
- <!-- Modal for Approve -->
- <div class="modal fade" id="approveModal" tabindex="-1" aria-hidden="true">
- <div class="modal-dialog">
- <div class="modal-content">
- <form method="POST" t-attf-action="/my/ticket/#{ticket.id}/approve?access_token=#{ticket.access_token}">
- <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
-
- <div class="modal-header bg-success text-white">
- <h5 class="modal-title">Aprobar Ticket</h5>
- <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"/>
- </div>
-
- <div class="modal-body">
- <p>¿Está seguro de que desea aprobar este ticket?</p>
- <div class="alert alert-info">
- <i class="fa fa-info-circle"/> <span>Esta acción marcará el ticket como aprobado y le permitirá continuar.</span>
- </div>
- </div>
-
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
- Cancelar
- </button>
- <button type="submit" class="btn btn-success">
- <i class="fa fa-check"/> <span>Confirmar Aprobación</span>
- </button>
- </div>
- </form>
- </div>
- </div>
- </div>
-
- <!-- Modal for Reject -->
- <div class="modal fade" id="rejectModal" tabindex="-1" aria-hidden="true">
- <div class="modal-dialog">
- <div class="modal-content">
- <form method="POST" t-attf-action="/my/ticket/#{ticket.id}/reject?access_token=#{ticket.access_token}">
- <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
-
- <div class="modal-header bg-danger text-white">
- <h5 class="modal-title">Rechazar Ticket</h5>
- <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"/>
- </div>
-
- <div class="modal-body">
- <p>Por favor proporcione una razón para rechazar este ticket:</p>
- <textarea name="reason"
- class="form-control"
- rows="3"
- placeholder="Ingrese su razón..."
- required=""/>
- </div>
-
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
- Cancelar
- </button>
- <button type="submit" class="btn btn-danger">
- <i class="fa fa-times"/> <span>Confirmar Rechazo</span>
- </button>
- </div>
- </form>
- </div>
- </div>
- </div>
-
- <!-- Success/Error messages -->
- <div t-if="message == 'approved'" class="alert alert-success mt-3" role="alert">
- <strong><span>¡Gracias!</span></strong><br/>
- <span>Ha aprobado este ticket.</span>
- </div>
- <div t-if="message == 'rejected'" class="alert alert-info mt-3" role="alert">
- <strong><span>Tomado en cuenta.</span></strong><br/>
- <span>Ha rechazado este ticket.</span>
- </div>
- </xpath>
- </template>
- <!-- Page to manage collaborators -->
- <template id="portal_team_collaborators" name="Team Collaborators Management">
- <t t-call="portal.portal_layout">
- <t t-set="title">Gestionar Colaboradores</t>
- <div class="container mt-3">
- <div class="row">
- <div class="col-12">
- <h2 class="mb-4">
- <i class="fa fa-users me-2"/>
- Gestionar Colaboradores - <t t-esc="team.name"/>
- </h2>
-
- <!-- Success/Error Messages -->
- <t t-if="request.session.get('success')">
- <div class="alert alert-success alert-dismissible fade show" role="alert">
- <t t-esc="request.session.pop('success')"/>
- <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
- </div>
- </t>
- <t t-if="request.session.get('error')">
- <div class="alert alert-danger alert-dismissible fade show" role="alert">
- <t t-esc="request.session.pop('error')"/>
- <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
- </div>
- </t>
-
- <!-- Add Collaborator Form -->
- <div class="card mb-3">
- <div class="card-header">
- <h5 class="mb-0">Agregar Colaborador</h5>
- </div>
- <div class="card-body">
- <form t-attf-action="/my/helpdesk/teams/#{team.id}/collaborators/add" method="post" id="add-collaborator-form">
- <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
- <div class="row">
- <div class="col-md-5">
- <div class="mb-3" style="position: relative;">
- <label class="form-label">Contacto</label>
- <div class="input-group" style="position: relative;">
- <input type="text"
- class="form-control partner-search"
- placeholder="Buscar contacto o crear nuevo..."
- autocomplete="off"
- id="partner-search-input"
- required="required"/>
- <input type="hidden" name="partner_id" id="partner-id-input"/>
- <a t-attf-href="/my/helpdesk/teams/#{team.id}/collaborators/new" class="btn btn-outline-secondary">
- <i class="fa fa-plus"></i> Nuevo
- </a>
- </div>
- <div class="partner-search-results" id="partner-search-results" style="display: none; position: absolute; z-index: 1000; background: white; border: 1px solid #ddd; max-height: 200px; overflow-y: auto; width: 100%; box-shadow: 0 2px 5px rgba(0,0,0,0.2); top: 100%; left: 0;"></div>
- </div>
- </div>
- <div class="col-md-4">
- <div class="mb-3">
- <label class="form-label">Rol</label>
- <select name="access_mode" class="form-select" required="required">
- <option value="user_own">User - Own Tickets</option>
- <option value="user_all">User - All Tickets</option>
- <option value="admin">Administrator</option>
- </select>
- </div>
- </div>
- <div class="col-md-3">
- <div class="mb-3">
- <label class="form-label"> </label>
- <button type="submit" class="btn btn-primary w-100">
- <i class="fa fa-plus me-2"/>
- Agregar
- </button>
- </div>
- </div>
- </div>
- <small class="text-muted">Solo puedes agregar contactos de tu misma red de contactos (misma empresa).</small>
- </form>
- </div>
- </div>
-
- <!-- Collaborators List -->
- <div class="card">
- <div class="card-header">
- <h5 class="mb-0">Colaboradores del Equipo</h5>
- </div>
- <div class="card-body">
- <t t-if="collaborators">
- <div class="table-responsive">
- <table class="table table-hover">
- <thead>
- <tr>
- <th>Colaborador</th>
- <th>Email</th>
- <th>Rol</th>
- <th class="text-end">Acciones</th>
- </tr>
- </thead>
- <tbody>
- <t t-foreach="collaborators" t-as="collaborator">
- <tr>
- <td>
- <t t-esc="collaborator.partner_id.name"/>
- </td>
- <td>
- <t t-esc="collaborator.partner_email or ''"/>
- </td>
- <td>
- <t t-if="collaborator.partner_id.id != current_partner_id">
- <form t-attf-action="/my/helpdesk/teams/#{team.id}/collaborators/#{collaborator.id}/update" method="post" style="display: inline;">
- <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
- <select name="access_mode" class="form-select form-select-sm" onchange="this.form.submit();">
- <option value="admin" t-att-selected="collaborator.access_mode == 'admin'">Administrator</option>
- <option value="user_all" t-att-selected="collaborator.access_mode == 'user_all'">User - All Tickets</option>
- <option value="user_own" t-att-selected="collaborator.access_mode == 'user_own'">User - Own Tickets</option>
- </select>
- </form>
- </t>
- <t t-else="">
- <select class="form-select form-select-sm" disabled="disabled">
- <option value="admin" t-att-selected="collaborator.access_mode == 'admin'">Administrator</option>
- <option value="user_all" t-att-selected="collaborator.access_mode == 'user_all'">User - All Tickets</option>
- <option value="user_own" t-att-selected="collaborator.access_mode == 'user_own'">User - Own Tickets</option>
- </select>
- <small class="text-muted d-block mt-1">No puedes cambiar tu propio rol</small>
- </t>
- </td>
- <td class="text-end">
- <t t-if="collaborator.partner_id.id != current_partner_id">
- <form t-attf-action="/my/helpdesk/teams/#{team.id}/collaborators/#{collaborator.id}/delete" method="post" style="display: inline;" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este colaborador?');">
- <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
- <button type="submit" class="btn btn-sm btn-outline-danger">
- <i class="fa fa-trash me-1"/>
- Eliminar
- </button>
- </form>
- </t>
- <t t-else="">
- <span class="text-muted">No puedes eliminarte a ti mismo</span>
- </t>
- </td>
- </tr>
- </t>
- </tbody>
- </table>
- </div>
- </t>
- <t t-else="">
- <p class="text-muted mb-0">No hay colaboradores en este equipo.</p>
- </t>
- </div>
- </div>
-
- <div class="mt-3">
- <a href="/my/tickets" class="btn btn-secondary">
- <i class="fa fa-arrow-left me-2"/>
- Volver a Tickets
- </a>
- </div>
- </div>
- </div>
- </div>
-
- <script type="text/javascript">
- <t t-set="teamId" t-value="team.id"/>
- <![CDATA[
- document.addEventListener('DOMContentLoaded', function() {
- 'use strict';
-
- var teamId = ]]><t t-esc="teamId"/><![CDATA[;
- var searchUrl = '/my/helpdesk/teams/' + teamId + '/collaborators/search-partners';
- var createUrl = '/my/helpdesk/teams/' + teamId + '/collaborators/create-partner';
-
- var searchInput = document.getElementById('partner-search-input');
- var partnerIdInput = document.getElementById('partner-id-input');
- var resultsDiv = document.getElementById('partner-search-results');
- var inputGroup = searchInput ? searchInput.closest('.input-group') : null;
- var parentDiv = searchInput ? searchInput.closest('.mb-3') : null;
-
- if (!searchInput || !resultsDiv || !partnerIdInput) {
- console.error('Required elements not found');
- return;
- }
-
- // Set width of results div to match input group
- if (inputGroup && parentDiv) {
- resultsDiv.style.width = inputGroup.offsetWidth + 'px';
- }
-
- var searchTimeout;
-
- // Function to perform search
- function performSearch(searchTerm) {
- clearTimeout(searchTimeout);
- var term = searchTerm ? searchTerm.trim() : '';
-
- console.log('🔍 Performing search with term:', term);
-
- // If term is empty or less than 2 chars, show all available contacts (empty search)
- // This allows users to see all related contacts even without typing
- searchTimeout = setTimeout(function() {
- console.log('📡 Fetching from:', searchUrl);
- fetch(searchUrl, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest'
- },
- body: JSON.stringify({search_term: term, limit: 20})
- })
- .then(function(response) {
- console.log('📥 Response status:', response.status, response.statusText);
- if (!response.ok) {
- throw new Error('Network response was not ok: ' + response.status);
- }
- return response.json();
- })
- .then(function(data) {
- console.log('📦 Raw response data:', data);
-
- // Odoo JSON-RPC wraps the response in {jsonrpc: "2.0", result: {...}}
- // Check if it's wrapped
- var responseData = data;
- if (data && data.jsonrpc && data.result !== undefined) {
- console.log('📦 Unwrapping JSON-RPC response');
- responseData = data.result;
- }
-
- console.log('📦 Processed response data:', responseData);
-
- if (responseData.error) {
- console.error('❌ Error in response:', responseData.error);
- resultsDiv.innerHTML = '<div class="p-2 text-danger">' + (responseData.error || 'Error desconocido') + '</div>';
- resultsDiv.style.display = 'block';
- return;
- }
-
- if (responseData.partners && responseData.partners.length > 0) {
- console.log('✅ Found', responseData.partners.length, 'partners');
- resultsDiv.innerHTML = responseData.partners.map(function(p) {
- var name = (p.name || '').replace(/"/g, '"').replace(/'/g, ''');
- var email = (p.email || 'Sin email').replace(/"/g, '"').replace(/'/g, ''');
- return '<div class="p-2 border-bottom partner-option" style="cursor: pointer;" data-id="' + p.id + '" data-name="' + name + '" data-email="' + email + '"><strong>' + (p.name || 'Sin nombre') + '</strong><br/><small class="text-muted">' + email + '</small></div>';
- }).join('');
-
- resultsDiv.querySelectorAll('.partner-option').forEach(function(option) {
- option.addEventListener('click', function() {
- searchInput.value = this.dataset.name || '';
- partnerIdInput.value = this.dataset.id || '';
- resultsDiv.style.display = 'none';
- });
- });
-
- resultsDiv.style.display = 'block';
- } else {
- console.log('⚠️ No partners found in response');
- resultsDiv.innerHTML = '<div class="p-2 text-muted">No se encontraron contactos</div>';
- resultsDiv.style.display = 'block';
- }
- })
- .catch(function(error) {
- console.error('❌ Search error:', error);
- resultsDiv.innerHTML = '<div class="p-2 text-danger">Error al buscar: ' + (error.message || error) + '</div>';
- resultsDiv.style.display = 'block';
- });
- }, 300);
- }
-
- // Search on input
- searchInput.addEventListener('input', function() {
- var term = this.value.trim();
- if (term.length === 0) {
- partnerIdInput.value = '';
- }
- performSearch(term);
- });
-
- // Show all contacts when focusing on the input (if empty)
- searchInput.addEventListener('focus', function() {
- if (this.value.trim().length === 0) {
- performSearch('');
- }
- });
-
- // Close results when clicking outside
- document.addEventListener('click', function(e) {
- if (parentDiv && !parentDiv.contains(e.target)) {
- resultsDiv.style.display = 'none';
- }
- });
- });
- ]]>
- </script>
- </t>
- </template>
- <!-- Page to create new contact and add as collaborator -->
- <template id="portal_new_collaborator" name="New Collaborator">
- <t t-call="portal.portal_layout">
- <t t-set="title">Crear Nuevo Contacto y Colaborador</t>
- <div class="container mt-3">
- <div class="row">
- <div class="col-12">
- <h2 class="mb-4">
- <i class="fa fa-user-plus me-2"/>
- Crear Nuevo Contacto y Colaborador - <t t-esc="team.name"/>
- </h2>
-
- <!-- Success/Error Messages -->
- <t t-if="request.session.get('success')">
- <div class="alert alert-success alert-dismissible fade show" role="alert">
- <t t-esc="request.session.pop('success')"/>
- <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
- </div>
- </t>
- <t t-if="request.session.get('error')">
- <div class="alert alert-danger alert-dismissible fade show" role="alert">
- <t t-esc="request.session.pop('error')"/>
- <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
- </div>
- </t>
-
- <!-- Create Contact Form -->
- <div class="card">
- <div class="card-header">
- <h5 class="mb-0">Información del Contacto</h5>
- </div>
- <div class="card-body">
- <form t-attf-action="/my/helpdesk/teams/#{team.id}/collaborators/create" method="post" id="create-collaborator-form">
- <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
-
- <div class="row">
- <div class="col-md-6">
- <div class="mb-3">
- <label for="name" class="form-label">Nombre <span class="text-danger">*</span></label>
- <input type="text"
- class="form-control"
- id="name"
- name="name"
- required="required"
- placeholder="Nombre completo del contacto"
- value=""/>
- <div class="invalid-feedback">El nombre es requerido.</div>
- </div>
- </div>
- <div class="col-md-6">
- <div class="mb-3">
- <label for="email" class="form-label">Email <span class="text-danger">*</span></label>
- <input type="email"
- class="form-control"
- id="email"
- name="email"
- required="required"
- placeholder="email@ejemplo.com"
- value=""/>
- <div class="invalid-feedback">El email es requerido y debe ser válido.</div>
- </div>
- </div>
- </div>
-
- <div class="row">
- <div class="col-md-6">
- <div class="mb-3">
- <label for="phone" class="form-label">Teléfono</label>
- <input type="tel"
- class="form-control"
- id="phone"
- name="phone"
- placeholder="+52 55 1234 5678"
- value=""/>
- <small class="form-text text-muted">Opcional.</small>
- </div>
- </div>
- <div class="col-md-6">
- <div class="mb-3">
- <label for="function" class="form-label">Cargo / Función</label>
- <input type="text"
- class="form-control"
- id="function"
- name="function"
- placeholder="Ej: Gerente de TI, Desarrollador, etc."
- value=""/>
- <small class="form-text text-muted">Opcional.</small>
- </div>
- </div>
- </div>
-
- <div class="row">
- <div class="col-md-6">
- <div class="mb-3">
- <label for="access_mode" class="form-label">Rol del Colaborador <span class="text-danger">*</span></label>
- <select class="form-select" id="access_mode" name="access_mode" required="required">
- <option value="user_own">User - Own Tickets</option>
- <option value="user_all">User - All Tickets</option>
- <option value="admin">Administrator</option>
- </select>
- <small class="form-text text-muted">
- <strong>Administrator:</strong> Puede ver todos los tickets y gestionar otros usuarios.<br/>
- <strong>User - All Tickets:</strong> Puede ver todos los tickets y crear sus propios tickets.<br/>
- <strong>User - Own Tickets:</strong> Solo puede crear y ver sus propios tickets.
- </small>
- </div>
- </div>
- </div>
-
- <div class="alert alert-info">
- <i class="fa fa-info-circle me-2"></i>
- <small>El contacto se creará en tu misma red de contactos y se agregará automáticamente como colaborador del equipo con el rol seleccionado.</small>
- </div>
-
- <div class="mt-4">
- <button type="submit" class="btn btn-primary">
- <i class="fa fa-save me-2"/>
- Crear Contacto y Agregar como Colaborador
- </button>
- <a t-attf-href="/my/helpdesk/teams/#{team.id}/collaborators" class="btn btn-secondary ms-2">
- <i class="fa fa-times me-2"/>
- Cancelar
- </a>
- </div>
- </form>
- </div>
- </div>
-
- <div class="mt-3">
- <a t-attf-href="/my/helpdesk/teams/#{team.id}/collaborators" class="btn btn-secondary">
- <i class="fa fa-arrow-left me-2"/>
- Volver a Colaboradores
- </a>
- </div>
- </div>
- </div>
- </div>
- </t>
- </template>
- <!-- Wizard to share team / manage collaborators -->
- <!-- Wizard to share team / manage collaborators -->
- <template id="portal_team_share_wizard" name="Team Share Wizard">
- <t t-call="portal.portal_layout">
- <t t-set="title">Compartir Equipo</t>
- <div class="container mt-3">
- <div class="row">
- <div class="col-12">
- <h2 class="mb-4">
- <i class="fa fa-share-alt me-2"/>
- Compartir Equipo - <t t-esc="team.name"/>
- </h2>
-
- <form t-attf-action="/my/helpdesk/teams/#{team.id}/collaborators/share" method="post" class="card">
- <div class="card-header">
- <h5 class="mb-0">Gestionar Colaboradores</h5>
- </div>
- <div class="card-body">
- <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
-
- <div class="mb-3">
- <label class="form-label">Enlace Público</label>
- <div class="input-group">
- <input type="text" class="form-control" t-att-value="wizard.share_link" readonly="readonly"/>
- <button type="button" class="btn btn-outline-secondary" onclick="navigator.clipboard.writeText(this.previousElementSibling.value); this.innerHTML='<i class="fa fa-check"></i> Copiado'; setTimeout(() => this.innerHTML='<i class="fa fa-copy"></i> Copiar', 2000);">
- <i class="fa fa-copy"/>
- Copiar
- </button>
- </div>
- </div>
-
- <div class="mb-3">
- <label class="form-label">Colaboradores</label>
- <div class="table-responsive">
- <table class="table table-sm">
- <thead>
- <tr>
- <th>Colaborador</th>
- <th>Rol</th>
- <th>Enviar Invitación</th>
- <th class="text-end">Acción</th>
- </tr>
- </thead>
- <tbody>
- <t t-foreach="wizard.collaborator_ids" t-as="collab_wiz">
- <tr>
- <td>
- <t t-esc="collab_wiz.partner_id.name"/>
- <br/>
- <small class="text-muted"><t t-esc="collab_wiz.partner_id.email or ''"/></small>
- </td>
- <td>
- <select name="access_mode" class="form-select form-select-sm">
- <option value="admin" t-att-selected="collab_wiz.access_mode == 'admin'">Administrator</option>
- <option value="user_all" t-att-selected="collab_wiz.access_mode == 'user_all'">User - All Tickets</option>
- <option value="user_own" t-att-selected="collab_wiz.access_mode == 'user_own'">User - Own Tickets</option>
- </select>
- </td>
- <td class="text-center">
- <input type="checkbox" name="send_invitation" t-att-checked="collab_wiz.send_invitation" class="form-check-input"/>
- </td>
- <td class="text-end">
- <button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('tr').remove();">
- <i class="fa fa-trash"/>
- </button>
- </td>
- </tr>
- </t>
- </tbody>
- </table>
- </div>
- <button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addCollaboratorRow(this);">
- <i class="fa fa-plus me-1"/>
- Agregar Colaborador
- </button>
- </div>
-
- <div class="alert alert-info">
- <strong>Roles disponibles:</strong>
- <ul class="mb-0">
- <li><strong>Administrator:</strong> Puede ver todos los tickets y gestionar otros usuarios.</li>
- <li><strong>User - All Tickets:</strong> Puede ver todos los tickets y crear sus propios tickets.</li>
- <li><strong>User - Own Tickets:</strong> Solo puede crear y ver sus propios tickets.</li>
- </ul>
- <p class="mb-0 mt-2"><strong>Nota:</strong> Solo puedes agregar contactos de tu misma red de contactos (misma empresa).</p>
- </div>
- </div>
- <div class="card-footer">
- <button type="submit" name="action_share" class="btn btn-primary">
- <i class="fa fa-share me-2"/>
- Guardar Cambios
- </button>
- <a t-attf-href="/my/helpdesk/teams/#{team.id}/collaborators" class="btn btn-secondary">
- Cancelar
- </a>
- </div>
- </form>
-
- <div class="mt-3">
- <a t-attf-href="/my/helpdesk/teams/#{team.id}/collaborators" class="btn btn-secondary">
- <i class="fa fa-arrow-left me-2"/>
- Volver a Colaboradores
- </a>
- </div>
- </div>
- </div>
- </div>
- <script type="text/javascript">
- <t t-set="teamId" t-value="team.id"/>
- <![CDATA[
- (function() {
- 'use strict';
-
- var teamId = ]]><t t-esc="teamId"/><![CDATA[;
- var searchUrl = '/my/helpdesk/teams/' + teamId + '/collaborators/search-partners';
- var createUrl = '/my/helpdesk/teams/' + teamId + '/collaborators/create-partner';
-
- window.addCollaboratorRow = function(button) {
- var tbody = button.closest('.card-body').querySelector('tbody');
- var row = document.createElement('tr');
- row.innerHTML = '<td><div class="input-group" style="position: relative;"><input type="text" class="form-control partner-search" placeholder="Buscar contacto o crear nuevo..." autocomplete="off" data-partner-id="" data-partner-name=""/><button type="button" class="btn btn-outline-secondary" onclick="createNewPartner(this);"><i class="fa fa-plus"></i> Nuevo</button></div><div class="partner-search-results" style="display: none; position: absolute; z-index: 1000; background: white; border: 1px solid #ddd; max-height: 200px; overflow-y: auto; width: 100%; box-shadow: 0 2px 5px rgba(0,0,0,0.2);"></div></td><td><select name="new_access_mode" class="form-select form-select-sm"><option value="user_own">User - Own Tickets</option><option value="user_all">User - All Tickets</option><option value="admin">Administrator</option></select></td><td class="text-center"><input type="checkbox" name="new_send_invitation" class="form-check-input" checked/></td><td class="text-end"><button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest(\'tr\').remove();"><i class="fa fa-trash"></i></button></td>';
- tbody.appendChild(row);
-
- var searchInput = row.querySelector('.partner-search');
- var resultsDiv = row.querySelector('.partner-search-results');
- var inputGroup = searchInput.closest('.input-group');
- inputGroup.style.position = 'relative';
- resultsDiv.style.position = 'absolute';
- resultsDiv.style.top = '100%';
- resultsDiv.style.left = '0';
- resultsDiv.style.width = inputGroup.offsetWidth + 'px';
-
- var searchTimeout;
-
- searchInput.addEventListener('input', function() {
- clearTimeout(searchTimeout);
- var term = this.value.trim();
-
- if (term.length < 2) {
- resultsDiv.style.display = 'none';
- return;
- }
-
- searchTimeout = setTimeout(function() {
- fetch(searchUrl, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({search_term: term, limit: 10})
- })
- .then(function(response) { return response.json(); })
- .then(function(data) {
- if (data.error) {
- resultsDiv.innerHTML = '<div class="p-2 text-danger">' + data.error + '</div>';
- resultsDiv.style.display = 'block';
- return;
- }
-
- if (data.partners && data.partners.length > 0) {
- resultsDiv.innerHTML = data.partners.map(function(p) {
- return '<div class="p-2 border-bottom partner-option" style="cursor: pointer;" data-id="' + p.id + '" data-name="' + p.name.replace(/"/g, '"') + '" data-email="' + (p.email || '') + '"><strong>' + p.name + '</strong><br/><small class="text-muted">' + (p.email || 'Sin email') + '</small></div>';
- }).join('');
-
- resultsDiv.querySelectorAll('.partner-option').forEach(function(option) {
- option.addEventListener('click', function() {
- searchInput.value = this.dataset.name;
- searchInput.dataset.partnerId = this.dataset.id;
- searchInput.dataset.partnerName = this.dataset.name;
- resultsDiv.style.display = 'none';
- });
- });
-
- resultsDiv.style.display = 'block';
- } else {
- resultsDiv.innerHTML = '<div class="p-2 text-muted">No se encontraron contactos</div>';
- resultsDiv.style.display = 'block';
- }
- })
- .catch(function(error) {
- resultsDiv.innerHTML = '<div class="p-2 text-danger">Error al buscar: ' + error + '</div>';
- resultsDiv.style.display = 'block';
- });
- }, 300);
- });
-
- document.addEventListener('click', function(e) {
- if (!row.contains(e.target)) {
- resultsDiv.style.display = 'none';
- }
- });
-
- searchInput.focus();
- };
-
- window.createNewPartner = function(button) {
- var row = button.closest('tr');
- var searchInput = row.querySelector('.partner-search');
- var name = searchInput.value.trim();
-
- if (!name) {
- alert('Por favor ingresa un nombre para el nuevo contacto.');
- return;
- }
-
- var email = prompt('Ingresa el email del nuevo contacto (opcional):', '');
- if (email === null) return;
-
- fetch(createUrl, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({name: name, email: email || ''})
- })
- .then(function(response) { return response.json(); })
- .then(function(data) {
- if (data.error) {
- alert('Error: ' + data.error);
- return;
- }
-
- if (data.partner) {
- searchInput.value = data.partner.name;
- searchInput.dataset.partnerId = data.partner.id;
- searchInput.dataset.partnerName = data.partner.name;
- alert('Contacto creado exitosamente.');
- }
- })
- .catch(function(error) {
- alert('Error al crear contacto: ' + error);
- });
- };
-
- document.addEventListener('DOMContentLoaded', function() {
- var form = document.querySelector('form[action*="/collaborators/share"]');
- if (form) {
- form.addEventListener('submit', function(e) {
- var rows = form.querySelectorAll('tbody tr');
- rows.forEach(function(row, index) {
- var searchInput = row.querySelector('.partner-search');
- if (searchInput && searchInput.dataset.partnerId) {
- var hiddenInput = document.createElement('input');
- hiddenInput.type = 'hidden';
- hiddenInput.name = 'new_collaborator_partner_id[]';
- hiddenInput.value = searchInput.dataset.partnerId;
- form.appendChild(hiddenInput);
-
- var accessModeSelect = row.querySelector('select[name="new_access_mode"]');
- if (accessModeSelect) {
- var accessModeInput = document.createElement('input');
- accessModeInput.type = 'hidden';
- accessModeInput.name = 'new_collaborator_access_mode[]';
- accessModeInput.value = accessModeSelect.value;
- form.appendChild(accessModeInput);
- }
-
- var sendInvitationCheckbox = row.querySelector('input[name="new_send_invitation"]');
- if (sendInvitationCheckbox) {
- var sendInvitationInput = document.createElement('input');
- sendInvitationInput.type = 'hidden';
- sendInvitationInput.name = 'new_collaborator_send_invitation[]';
- sendInvitationInput.value = sendInvitationCheckbox.checked ? '1' : '0';
- form.appendChild(sendInvitationInput);
- }
- }
- });
- });
- }
- });
- })();
- ]]>
- </script>
- </t>
- </template>
- </odoo>
|