helpdesk_portal_templates.xml 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <odoo>
  3. <!-- Extend portal tickets page to add "New" button for collaborators -->
  4. <template id="portal_helpdesk_ticket_new_button" name="Portal Helpdesk Tickets New Button" inherit_id="helpdesk.portal_helpdesk_ticket" priority="20">
  5. <!-- Add button after the searchbar, similar to how helpdesk_sale_timesheet extends this template -->
  6. <xpath expr="//t[@t-call='portal.portal_searchbar']" position="after">
  7. <div t-if="collaborator_team_form_url or admin_teams" class="mb-3 text-end">
  8. <a t-if="collaborator_team_form_url" t-attf-href="#{collaborator_team_form_url}" class="btn btn-primary me-2">
  9. <i class="fa fa-plus me-2"/>
  10. Nuevo Ticket
  11. </a>
  12. <t t-if="admin_teams" t-foreach="admin_teams" t-as="admin_team">
  13. <a t-attf-href="/my/helpdesk/teams/#{admin_team.id}/collaborators" class="btn btn-primary me-2">
  14. <i class="fa fa-users me-2"/>
  15. Gestionar Colaboradores - <t t-esc="admin_team.name"/>
  16. </a>
  17. </t>
  18. </div>
  19. </xpath>
  20. </template>
  21. <!-- Extend ticket detail view to show new fields -->
  22. <template id="portal_helpdesk_ticket_detail_extras" name="Portal Helpdesk Ticket Detail Extras" inherit_id="helpdesk.tickets_followup" priority="20">
  23. <!-- Add new fields after "Reported on" -->
  24. <xpath expr="//div[@name='description']" position="before">
  25. <div t-if="ticket.request_type_id" class="row mb-4">
  26. <strong class="col-lg-3">Tipo de Solicitud</strong>
  27. <span class="col-lg-9" t-field="ticket.request_type_id.name"/>
  28. </div>
  29. <div t-if="ticket.affected_module_id" class="row mb-4">
  30. <strong class="col-lg-3">Módulo Afectado</strong>
  31. <span class="col-lg-9" t-field="ticket.affected_module_id.name"/>
  32. </div>
  33. <div t-if="ticket.business_impact" class="row mb-4">
  34. <strong class="col-lg-3">Impacto de Negocio</strong>
  35. <span class="col-lg-9">
  36. <t t-if="ticket.business_impact == '0'">Crítico</t>
  37. <t t-elif="ticket.business_impact == '1'">Alto</t>
  38. <t t-elif="ticket.business_impact == '2'">Normal</t>
  39. </span>
  40. </div>
  41. <div t-if="ticket.reproduce_steps and ticket.request_type_code == 'incident'" class="row mb-4">
  42. <strong class="col-lg-3">Pasos para Reproducir</strong>
  43. <div class="col-lg-9" t-field="ticket.reproduce_steps"/>
  44. </div>
  45. <div t-if="ticket.business_goal and ticket.request_type_code == 'improvement'" class="row mb-4">
  46. <strong class="col-lg-3">Objetivo de Negocio</strong>
  47. <div class="col-lg-9" t-field="ticket.business_goal"/>
  48. </div>
  49. <div t-if="ticket.estimated_hours" class="row mb-4">
  50. <strong class="col-lg-3">Horas Estimadas</strong>
  51. <span class="col-lg-9" t-field="ticket.estimated_hours" t-options='{"widget": "float"}'/>
  52. </div>
  53. <div t-if="ticket.approval_status" class="row mb-4">
  54. <strong class="col-lg-3">Estado de Aprobación</strong>
  55. <span class="col-lg-9">
  56. <t t-if="ticket.approval_status == 'draft'">N/A</t>
  57. <t t-elif="ticket.approval_status == 'waiting'">Esperando Aprobación</t>
  58. <t t-elif="ticket.approval_status == 'approved'">Aprobado</t>
  59. <t t-elif="ticket.approval_status == 'rejected'">Rechazado</t>
  60. </span>
  61. </div>
  62. </xpath>
  63. </template>
  64. <!-- Page to manage collaborators -->
  65. <template id="portal_team_collaborators" name="Team Collaborators Management">
  66. <t t-call="portal.portal_layout">
  67. <t t-set="title">Gestionar Colaboradores</t>
  68. <div class="container mt-3">
  69. <div class="row">
  70. <div class="col-12">
  71. <h2 class="mb-4">
  72. <i class="fa fa-users me-2"/>
  73. Gestionar Colaboradores - <t t-esc="team.name"/>
  74. </h2>
  75. <!-- Success/Error Messages -->
  76. <t t-if="request.session.get('success')">
  77. <div class="alert alert-success alert-dismissible fade show" role="alert">
  78. <t t-esc="request.session.pop('success')"/>
  79. <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
  80. </div>
  81. </t>
  82. <t t-if="request.session.get('error')">
  83. <div class="alert alert-danger alert-dismissible fade show" role="alert">
  84. <t t-esc="request.session.pop('error')"/>
  85. <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
  86. </div>
  87. </t>
  88. <!-- Add Collaborator Form -->
  89. <div class="card mb-3">
  90. <div class="card-header">
  91. <h5 class="mb-0">Agregar Colaborador</h5>
  92. </div>
  93. <div class="card-body">
  94. <form t-attf-action="/my/helpdesk/teams/#{team.id}/collaborators/add" method="post" id="add-collaborator-form">
  95. <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
  96. <div class="row">
  97. <div class="col-md-5">
  98. <div class="mb-3" style="position: relative;">
  99. <label class="form-label">Contacto</label>
  100. <div class="input-group" style="position: relative;">
  101. <input type="text"
  102. class="form-control partner-search"
  103. placeholder="Buscar contacto o crear nuevo..."
  104. autocomplete="off"
  105. id="partner-search-input"
  106. required="required"/>
  107. <input type="hidden" name="partner_id" id="partner-id-input"/>
  108. <a t-attf-href="/my/helpdesk/teams/#{team.id}/collaborators/new" class="btn btn-outline-secondary">
  109. <i class="fa fa-plus"></i> Nuevo
  110. </a>
  111. </div>
  112. <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>
  113. </div>
  114. </div>
  115. <div class="col-md-4">
  116. <div class="mb-3">
  117. <label class="form-label">Rol</label>
  118. <select name="access_mode" class="form-select" required="required">
  119. <option value="user_own">User - Own Tickets</option>
  120. <option value="user_all">User - All Tickets</option>
  121. <option value="admin">Administrator</option>
  122. </select>
  123. </div>
  124. </div>
  125. <div class="col-md-3">
  126. <div class="mb-3">
  127. <label class="form-label">&#160;</label>
  128. <button type="submit" class="btn btn-primary w-100">
  129. <i class="fa fa-plus me-2"/>
  130. Agregar
  131. </button>
  132. </div>
  133. </div>
  134. </div>
  135. <small class="text-muted">Solo puedes agregar contactos de tu misma red de contactos (misma empresa).</small>
  136. </form>
  137. </div>
  138. </div>
  139. <!-- Collaborators List -->
  140. <div class="card">
  141. <div class="card-header">
  142. <h5 class="mb-0">Colaboradores del Equipo</h5>
  143. </div>
  144. <div class="card-body">
  145. <t t-if="collaborators">
  146. <div class="table-responsive">
  147. <table class="table table-hover">
  148. <thead>
  149. <tr>
  150. <th>Colaborador</th>
  151. <th>Email</th>
  152. <th>Rol</th>
  153. <th class="text-end">Acciones</th>
  154. </tr>
  155. </thead>
  156. <tbody>
  157. <t t-foreach="collaborators" t-as="collaborator">
  158. <tr>
  159. <td>
  160. <t t-esc="collaborator.partner_id.name"/>
  161. </td>
  162. <td>
  163. <t t-esc="collaborator.partner_email or ''"/>
  164. </td>
  165. <td>
  166. <t t-if="collaborator.partner_id.id != current_partner_id">
  167. <form t-attf-action="/my/helpdesk/teams/#{team.id}/collaborators/#{collaborator.id}/update" method="post" style="display: inline;">
  168. <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
  169. <select name="access_mode" class="form-select form-select-sm" onchange="this.form.submit();">
  170. <option value="admin" t-att-selected="collaborator.access_mode == 'admin'">Administrator</option>
  171. <option value="user_all" t-att-selected="collaborator.access_mode == 'user_all'">User - All Tickets</option>
  172. <option value="user_own" t-att-selected="collaborator.access_mode == 'user_own'">User - Own Tickets</option>
  173. </select>
  174. </form>
  175. </t>
  176. <t t-else="">
  177. <select class="form-select form-select-sm" disabled="disabled">
  178. <option value="admin" t-att-selected="collaborator.access_mode == 'admin'">Administrator</option>
  179. <option value="user_all" t-att-selected="collaborator.access_mode == 'user_all'">User - All Tickets</option>
  180. <option value="user_own" t-att-selected="collaborator.access_mode == 'user_own'">User - Own Tickets</option>
  181. </select>
  182. <small class="text-muted d-block mt-1">No puedes cambiar tu propio rol</small>
  183. </t>
  184. </td>
  185. <td class="text-end">
  186. <t t-if="collaborator.partner_id.id != current_partner_id">
  187. <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?');">
  188. <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
  189. <button type="submit" class="btn btn-sm btn-outline-danger">
  190. <i class="fa fa-trash me-1"/>
  191. Eliminar
  192. </button>
  193. </form>
  194. </t>
  195. <t t-else="">
  196. <span class="text-muted">No puedes eliminarte a ti mismo</span>
  197. </t>
  198. </td>
  199. </tr>
  200. </t>
  201. </tbody>
  202. </table>
  203. </div>
  204. </t>
  205. <t t-else="">
  206. <p class="text-muted mb-0">No hay colaboradores en este equipo.</p>
  207. </t>
  208. </div>
  209. </div>
  210. <div class="mt-3">
  211. <a href="/my/tickets" class="btn btn-secondary">
  212. <i class="fa fa-arrow-left me-2"/>
  213. Volver a Tickets
  214. </a>
  215. </div>
  216. </div>
  217. </div>
  218. </div>
  219. <script type="text/javascript">
  220. <t t-set="teamId" t-value="team.id"/>
  221. <![CDATA[
  222. document.addEventListener('DOMContentLoaded', function() {
  223. 'use strict';
  224. var teamId = ]]><t t-esc="teamId"/><![CDATA[;
  225. var searchUrl = '/my/helpdesk/teams/' + teamId + '/collaborators/search-partners';
  226. var createUrl = '/my/helpdesk/teams/' + teamId + '/collaborators/create-partner';
  227. var searchInput = document.getElementById('partner-search-input');
  228. var partnerIdInput = document.getElementById('partner-id-input');
  229. var resultsDiv = document.getElementById('partner-search-results');
  230. var inputGroup = searchInput ? searchInput.closest('.input-group') : null;
  231. var parentDiv = searchInput ? searchInput.closest('.mb-3') : null;
  232. if (!searchInput || !resultsDiv || !partnerIdInput) {
  233. console.error('Required elements not found');
  234. return;
  235. }
  236. // Set width of results div to match input group
  237. if (inputGroup && parentDiv) {
  238. resultsDiv.style.width = inputGroup.offsetWidth + 'px';
  239. }
  240. var searchTimeout;
  241. // Function to perform search
  242. function performSearch(searchTerm) {
  243. clearTimeout(searchTimeout);
  244. var term = searchTerm ? searchTerm.trim() : '';
  245. console.log('🔍 Performing search with term:', term);
  246. // If term is empty or less than 2 chars, show all available contacts (empty search)
  247. // This allows users to see all related contacts even without typing
  248. searchTimeout = setTimeout(function() {
  249. console.log('📡 Fetching from:', searchUrl);
  250. fetch(searchUrl, {
  251. method: 'POST',
  252. headers: {
  253. 'Content-Type': 'application/json',
  254. 'X-Requested-With': 'XMLHttpRequest'
  255. },
  256. body: JSON.stringify({search_term: term, limit: 20})
  257. })
  258. .then(function(response) {
  259. console.log('📥 Response status:', response.status, response.statusText);
  260. if (!response.ok) {
  261. throw new Error('Network response was not ok: ' + response.status);
  262. }
  263. return response.json();
  264. })
  265. .then(function(data) {
  266. console.log('📦 Raw response data:', data);
  267. // Odoo JSON-RPC wraps the response in {jsonrpc: "2.0", result: {...}}
  268. // Check if it's wrapped
  269. var responseData = data;
  270. if (data && data.jsonrpc && data.result !== undefined) {
  271. console.log('📦 Unwrapping JSON-RPC response');
  272. responseData = data.result;
  273. }
  274. console.log('📦 Processed response data:', responseData);
  275. if (responseData.error) {
  276. console.error('❌ Error in response:', responseData.error);
  277. resultsDiv.innerHTML = '<div class="p-2 text-danger">' + (responseData.error || 'Error desconocido') + '</div>';
  278. resultsDiv.style.display = 'block';
  279. return;
  280. }
  281. if (responseData.partners && responseData.partners.length > 0) {
  282. console.log('✅ Found', responseData.partners.length, 'partners');
  283. resultsDiv.innerHTML = responseData.partners.map(function(p) {
  284. var name = (p.name || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
  285. var email = (p.email || 'Sin email').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
  286. 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>';
  287. }).join('');
  288. resultsDiv.querySelectorAll('.partner-option').forEach(function(option) {
  289. option.addEventListener('click', function() {
  290. searchInput.value = this.dataset.name || '';
  291. partnerIdInput.value = this.dataset.id || '';
  292. resultsDiv.style.display = 'none';
  293. });
  294. });
  295. resultsDiv.style.display = 'block';
  296. } else {
  297. console.log('⚠️ No partners found in response');
  298. resultsDiv.innerHTML = '<div class="p-2 text-muted">No se encontraron contactos</div>';
  299. resultsDiv.style.display = 'block';
  300. }
  301. })
  302. .catch(function(error) {
  303. console.error('❌ Search error:', error);
  304. resultsDiv.innerHTML = '<div class="p-2 text-danger">Error al buscar: ' + (error.message || error) + '</div>';
  305. resultsDiv.style.display = 'block';
  306. });
  307. }, 300);
  308. }
  309. // Search on input
  310. searchInput.addEventListener('input', function() {
  311. var term = this.value.trim();
  312. if (term.length === 0) {
  313. partnerIdInput.value = '';
  314. }
  315. performSearch(term);
  316. });
  317. // Show all contacts when focusing on the input (if empty)
  318. searchInput.addEventListener('focus', function() {
  319. if (this.value.trim().length === 0) {
  320. performSearch('');
  321. }
  322. });
  323. // Close results when clicking outside
  324. document.addEventListener('click', function(e) {
  325. if (parentDiv && !parentDiv.contains(e.target)) {
  326. resultsDiv.style.display = 'none';
  327. }
  328. });
  329. });
  330. ]]>
  331. </script>
  332. </t>
  333. </template>
  334. <!-- Page to create new contact and add as collaborator -->
  335. <template id="portal_new_collaborator" name="New Collaborator">
  336. <t t-call="portal.portal_layout">
  337. <t t-set="title">Crear Nuevo Contacto y Colaborador</t>
  338. <div class="container mt-3">
  339. <div class="row">
  340. <div class="col-12">
  341. <h2 class="mb-4">
  342. <i class="fa fa-user-plus me-2"/>
  343. Crear Nuevo Contacto y Colaborador - <t t-esc="team.name"/>
  344. </h2>
  345. <!-- Success/Error Messages -->
  346. <t t-if="request.session.get('success')">
  347. <div class="alert alert-success alert-dismissible fade show" role="alert">
  348. <t t-esc="request.session.pop('success')"/>
  349. <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
  350. </div>
  351. </t>
  352. <t t-if="request.session.get('error')">
  353. <div class="alert alert-danger alert-dismissible fade show" role="alert">
  354. <t t-esc="request.session.pop('error')"/>
  355. <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
  356. </div>
  357. </t>
  358. <!-- Create Contact Form -->
  359. <div class="card">
  360. <div class="card-header">
  361. <h5 class="mb-0">Información del Contacto</h5>
  362. </div>
  363. <div class="card-body">
  364. <form t-attf-action="/my/helpdesk/teams/#{team.id}/collaborators/create" method="post" id="create-collaborator-form">
  365. <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
  366. <div class="row">
  367. <div class="col-md-6">
  368. <div class="mb-3">
  369. <label for="name" class="form-label">Nombre <span class="text-danger">*</span></label>
  370. <input type="text"
  371. class="form-control"
  372. id="name"
  373. name="name"
  374. required="required"
  375. placeholder="Nombre completo del contacto"
  376. value=""/>
  377. <div class="invalid-feedback">El nombre es requerido.</div>
  378. </div>
  379. </div>
  380. <div class="col-md-6">
  381. <div class="mb-3">
  382. <label for="email" class="form-label">Email <span class="text-danger">*</span></label>
  383. <input type="email"
  384. class="form-control"
  385. id="email"
  386. name="email"
  387. required="required"
  388. placeholder="email@ejemplo.com"
  389. value=""/>
  390. <div class="invalid-feedback">El email es requerido y debe ser válido.</div>
  391. </div>
  392. </div>
  393. </div>
  394. <div class="row">
  395. <div class="col-md-6">
  396. <div class="mb-3">
  397. <label for="phone" class="form-label">Teléfono</label>
  398. <input type="tel"
  399. class="form-control"
  400. id="phone"
  401. name="phone"
  402. placeholder="+52 55 1234 5678"
  403. value=""/>
  404. <small class="form-text text-muted">Opcional.</small>
  405. </div>
  406. </div>
  407. <div class="col-md-6">
  408. <div class="mb-3">
  409. <label for="function" class="form-label">Cargo / Función</label>
  410. <input type="text"
  411. class="form-control"
  412. id="function"
  413. name="function"
  414. placeholder="Ej: Gerente de TI, Desarrollador, etc."
  415. value=""/>
  416. <small class="form-text text-muted">Opcional.</small>
  417. </div>
  418. </div>
  419. </div>
  420. <div class="row">
  421. <div class="col-md-6">
  422. <div class="mb-3">
  423. <label for="access_mode" class="form-label">Rol del Colaborador <span class="text-danger">*</span></label>
  424. <select class="form-select" id="access_mode" name="access_mode" required="required">
  425. <option value="user_own">User - Own Tickets</option>
  426. <option value="user_all">User - All Tickets</option>
  427. <option value="admin">Administrator</option>
  428. </select>
  429. <small class="form-text text-muted">
  430. <strong>Administrator:</strong> Puede ver todos los tickets y gestionar otros usuarios.<br/>
  431. <strong>User - All Tickets:</strong> Puede ver todos los tickets y crear sus propios tickets.<br/>
  432. <strong>User - Own Tickets:</strong> Solo puede crear y ver sus propios tickets.
  433. </small>
  434. </div>
  435. </div>
  436. </div>
  437. <div class="alert alert-info">
  438. <i class="fa fa-info-circle me-2"></i>
  439. <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>
  440. </div>
  441. <div class="mt-4">
  442. <button type="submit" class="btn btn-primary">
  443. <i class="fa fa-save me-2"/>
  444. Crear Contacto y Agregar como Colaborador
  445. </button>
  446. <a t-attf-href="/my/helpdesk/teams/#{team.id}/collaborators" class="btn btn-secondary ms-2">
  447. <i class="fa fa-times me-2"/>
  448. Cancelar
  449. </a>
  450. </div>
  451. </form>
  452. </div>
  453. </div>
  454. <div class="mt-3">
  455. <a t-attf-href="/my/helpdesk/teams/#{team.id}/collaborators" class="btn btn-secondary">
  456. <i class="fa fa-arrow-left me-2"/>
  457. Volver a Colaboradores
  458. </a>
  459. </div>
  460. </div>
  461. </div>
  462. </div>
  463. </t>
  464. </template>
  465. <!-- Wizard to share team / manage collaborators -->
  466. <!-- Wizard to share team / manage collaborators -->
  467. <template id="portal_team_share_wizard" name="Team Share Wizard">
  468. <t t-call="portal.portal_layout">
  469. <t t-set="title">Compartir Equipo</t>
  470. <div class="container mt-3">
  471. <div class="row">
  472. <div class="col-12">
  473. <h2 class="mb-4">
  474. <i class="fa fa-share-alt me-2"/>
  475. Compartir Equipo - <t t-esc="team.name"/>
  476. </h2>
  477. <form t-attf-action="/my/helpdesk/teams/#{team.id}/collaborators/share" method="post" class="card">
  478. <div class="card-header">
  479. <h5 class="mb-0">Gestionar Colaboradores</h5>
  480. </div>
  481. <div class="card-body">
  482. <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
  483. <div class="mb-3">
  484. <label class="form-label">Enlace Público</label>
  485. <div class="input-group">
  486. <input type="text" class="form-control" t-att-value="wizard.share_link" readonly="readonly"/>
  487. <button type="button" class="btn btn-outline-secondary" onclick="navigator.clipboard.writeText(this.previousElementSibling.value); this.innerHTML='&lt;i class=&quot;fa fa-check&quot;&gt;&lt;/i&gt; Copiado'; setTimeout(() =&gt; this.innerHTML='&lt;i class=&quot;fa fa-copy&quot;&gt;&lt;/i&gt; Copiar', 2000);">
  488. <i class="fa fa-copy"/>
  489. Copiar
  490. </button>
  491. </div>
  492. </div>
  493. <div class="mb-3">
  494. <label class="form-label">Colaboradores</label>
  495. <div class="table-responsive">
  496. <table class="table table-sm">
  497. <thead>
  498. <tr>
  499. <th>Colaborador</th>
  500. <th>Rol</th>
  501. <th>Enviar Invitación</th>
  502. <th class="text-end">Acción</th>
  503. </tr>
  504. </thead>
  505. <tbody>
  506. <t t-foreach="wizard.collaborator_ids" t-as="collab_wiz">
  507. <tr>
  508. <td>
  509. <t t-esc="collab_wiz.partner_id.name"/>
  510. <br/>
  511. <small class="text-muted"><t t-esc="collab_wiz.partner_id.email or ''"/></small>
  512. </td>
  513. <td>
  514. <select name="access_mode" class="form-select form-select-sm">
  515. <option value="admin" t-att-selected="collab_wiz.access_mode == 'admin'">Administrator</option>
  516. <option value="user_all" t-att-selected="collab_wiz.access_mode == 'user_all'">User - All Tickets</option>
  517. <option value="user_own" t-att-selected="collab_wiz.access_mode == 'user_own'">User - Own Tickets</option>
  518. </select>
  519. </td>
  520. <td class="text-center">
  521. <input type="checkbox" name="send_invitation" t-att-checked="collab_wiz.send_invitation" class="form-check-input"/>
  522. </td>
  523. <td class="text-end">
  524. <button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('tr').remove();">
  525. <i class="fa fa-trash"/>
  526. </button>
  527. </td>
  528. </tr>
  529. </t>
  530. </tbody>
  531. </table>
  532. </div>
  533. <button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addCollaboratorRow(this);">
  534. <i class="fa fa-plus me-1"/>
  535. Agregar Colaborador
  536. </button>
  537. </div>
  538. <div class="alert alert-info">
  539. <strong>Roles disponibles:</strong>
  540. <ul class="mb-0">
  541. <li><strong>Administrator:</strong> Puede ver todos los tickets y gestionar otros usuarios.</li>
  542. <li><strong>User - All Tickets:</strong> Puede ver todos los tickets y crear sus propios tickets.</li>
  543. <li><strong>User - Own Tickets:</strong> Solo puede crear y ver sus propios tickets.</li>
  544. </ul>
  545. <p class="mb-0 mt-2"><strong>Nota:</strong> Solo puedes agregar contactos de tu misma red de contactos (misma empresa).</p>
  546. </div>
  547. </div>
  548. <div class="card-footer">
  549. <button type="submit" name="action_share" class="btn btn-primary">
  550. <i class="fa fa-share me-2"/>
  551. Guardar Cambios
  552. </button>
  553. <a t-attf-href="/my/helpdesk/teams/#{team.id}/collaborators" class="btn btn-secondary">
  554. Cancelar
  555. </a>
  556. </div>
  557. </form>
  558. <div class="mt-3">
  559. <a t-attf-href="/my/helpdesk/teams/#{team.id}/collaborators" class="btn btn-secondary">
  560. <i class="fa fa-arrow-left me-2"/>
  561. Volver a Colaboradores
  562. </a>
  563. </div>
  564. </div>
  565. </div>
  566. </div>
  567. <script type="text/javascript">
  568. <t t-set="teamId" t-value="team.id"/>
  569. <![CDATA[
  570. (function() {
  571. 'use strict';
  572. var teamId = ]]><t t-esc="teamId"/><![CDATA[;
  573. var searchUrl = '/my/helpdesk/teams/' + teamId + '/collaborators/search-partners';
  574. var createUrl = '/my/helpdesk/teams/' + teamId + '/collaborators/create-partner';
  575. window.addCollaboratorRow = function(button) {
  576. var tbody = button.closest('.card-body').querySelector('tbody');
  577. var row = document.createElement('tr');
  578. 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>';
  579. tbody.appendChild(row);
  580. var searchInput = row.querySelector('.partner-search');
  581. var resultsDiv = row.querySelector('.partner-search-results');
  582. var inputGroup = searchInput.closest('.input-group');
  583. inputGroup.style.position = 'relative';
  584. resultsDiv.style.position = 'absolute';
  585. resultsDiv.style.top = '100%';
  586. resultsDiv.style.left = '0';
  587. resultsDiv.style.width = inputGroup.offsetWidth + 'px';
  588. var searchTimeout;
  589. searchInput.addEventListener('input', function() {
  590. clearTimeout(searchTimeout);
  591. var term = this.value.trim();
  592. if (term.length < 2) {
  593. resultsDiv.style.display = 'none';
  594. return;
  595. }
  596. searchTimeout = setTimeout(function() {
  597. fetch(searchUrl, {
  598. method: 'POST',
  599. headers: {'Content-Type': 'application/json'},
  600. body: JSON.stringify({search_term: term, limit: 10})
  601. })
  602. .then(function(response) { return response.json(); })
  603. .then(function(data) {
  604. if (data.error) {
  605. resultsDiv.innerHTML = '<div class="p-2 text-danger">' + data.error + '</div>';
  606. resultsDiv.style.display = 'block';
  607. return;
  608. }
  609. if (data.partners && data.partners.length > 0) {
  610. resultsDiv.innerHTML = data.partners.map(function(p) {
  611. return '<div class="p-2 border-bottom partner-option" style="cursor: pointer;" data-id="' + p.id + '" data-name="' + p.name.replace(/"/g, '&quot;') + '" data-email="' + (p.email || '') + '"><strong>' + p.name + '</strong><br/><small class="text-muted">' + (p.email || 'Sin email') + '</small></div>';
  612. }).join('');
  613. resultsDiv.querySelectorAll('.partner-option').forEach(function(option) {
  614. option.addEventListener('click', function() {
  615. searchInput.value = this.dataset.name;
  616. searchInput.dataset.partnerId = this.dataset.id;
  617. searchInput.dataset.partnerName = this.dataset.name;
  618. resultsDiv.style.display = 'none';
  619. });
  620. });
  621. resultsDiv.style.display = 'block';
  622. } else {
  623. resultsDiv.innerHTML = '<div class="p-2 text-muted">No se encontraron contactos</div>';
  624. resultsDiv.style.display = 'block';
  625. }
  626. })
  627. .catch(function(error) {
  628. resultsDiv.innerHTML = '<div class="p-2 text-danger">Error al buscar: ' + error + '</div>';
  629. resultsDiv.style.display = 'block';
  630. });
  631. }, 300);
  632. });
  633. document.addEventListener('click', function(e) {
  634. if (!row.contains(e.target)) {
  635. resultsDiv.style.display = 'none';
  636. }
  637. });
  638. searchInput.focus();
  639. };
  640. window.createNewPartner = function(button) {
  641. var row = button.closest('tr');
  642. var searchInput = row.querySelector('.partner-search');
  643. var name = searchInput.value.trim();
  644. if (!name) {
  645. alert('Por favor ingresa un nombre para el nuevo contacto.');
  646. return;
  647. }
  648. var email = prompt('Ingresa el email del nuevo contacto (opcional):', '');
  649. if (email === null) return;
  650. fetch(createUrl, {
  651. method: 'POST',
  652. headers: {'Content-Type': 'application/json'},
  653. body: JSON.stringify({name: name, email: email || ''})
  654. })
  655. .then(function(response) { return response.json(); })
  656. .then(function(data) {
  657. if (data.error) {
  658. alert('Error: ' + data.error);
  659. return;
  660. }
  661. if (data.partner) {
  662. searchInput.value = data.partner.name;
  663. searchInput.dataset.partnerId = data.partner.id;
  664. searchInput.dataset.partnerName = data.partner.name;
  665. alert('Contacto creado exitosamente.');
  666. }
  667. })
  668. .catch(function(error) {
  669. alert('Error al crear contacto: ' + error);
  670. });
  671. };
  672. document.addEventListener('DOMContentLoaded', function() {
  673. var form = document.querySelector('form[action*="/collaborators/share"]');
  674. if (form) {
  675. form.addEventListener('submit', function(e) {
  676. var rows = form.querySelectorAll('tbody tr');
  677. rows.forEach(function(row, index) {
  678. var searchInput = row.querySelector('.partner-search');
  679. if (searchInput && searchInput.dataset.partnerId) {
  680. var hiddenInput = document.createElement('input');
  681. hiddenInput.type = 'hidden';
  682. hiddenInput.name = 'new_collaborator_partner_id[]';
  683. hiddenInput.value = searchInput.dataset.partnerId;
  684. form.appendChild(hiddenInput);
  685. var accessModeSelect = row.querySelector('select[name="new_access_mode"]');
  686. if (accessModeSelect) {
  687. var accessModeInput = document.createElement('input');
  688. accessModeInput.type = 'hidden';
  689. accessModeInput.name = 'new_collaborator_access_mode[]';
  690. accessModeInput.value = accessModeSelect.value;
  691. form.appendChild(accessModeInput);
  692. }
  693. var sendInvitationCheckbox = row.querySelector('input[name="new_send_invitation"]');
  694. if (sendInvitationCheckbox) {
  695. var sendInvitationInput = document.createElement('input');
  696. sendInvitationInput.type = 'hidden';
  697. sendInvitationInput.name = 'new_collaborator_send_invitation[]';
  698. sendInvitationInput.value = sendInvitationCheckbox.checked ? '1' : '0';
  699. form.appendChild(sendInvitationInput);
  700. }
  701. }
  702. });
  703. });
  704. }
  705. });
  706. })();
  707. ]]>
  708. </script>
  709. </t>
  710. </template>
  711. </odoo>