11/**
2- * Portal Organization Diagram - Frontend Widget (Production-Ready )
2+ * Portal Organization Diagram - Frontend Widget (Vanilla JavaScript - Odoo 18 Compatible )
33 *
44 * PURPOSE: Customer-facing portal organization chart visualization
55 * USE CASE: /my/organization route - shows company hierarchy to portal users
1515 * 3. vis.Network renders interactive diagram
1616 * 4. User interactions (search, export, layout) handled by widget
1717 *
18- * PERFORMANCE OPTIMIZATIONS (Grok 2025 ):
19- * - Batch DOM queries (cache elements, single pass updates)
20- * - Optimized search with node highlighting
21- * - Better animation easing for smooth UX
22- * - Accessibility logging for screen readers
18+ * CONVERSION NOTES (Odoo 18 ):
19+ * - Removed: odoo.define(), publicWidget dependency
20+ * - Replaced: jQuery with native DOM APIs
21+ * - Added: IIFE wrapper for module isolation
22+ * - Preserved: All features, vis-network integration
2323 *
2424 * FEATURES:
2525 * ✓ Interactive diagram with drag/zoom
3131 *
3232 * BROWSER SUPPORT: Modern browsers (ES6+), graceful fallback for older browsers
3333 */
34- odoo . define ( 'records_management.portal_organization_diagram' , [ 'web.public.widget' ] , function ( require ) {
34+
35+ ( function ( ) {
3536 'use strict' ;
3637
37- const publicWidget = require ( 'web.public.widget' ) ;
38+ class OrgDiagramPortal {
39+ constructor ( containerElement ) {
40+ this . container = containerElement ;
41+ this . diagramData = null ;
42+ this . network = null ;
43+ this . init ( ) ;
44+ }
3845
39- const OrgDiagramPortal = publicWidget . Widget . extend ( {
40- selector : '.o_portal_organization_diagram' ,
41- events : {
42- 'click #refresh-diagram' : '_onRefresh' ,
43- 'click #export-diagram' : '_onExport' ,
44- 'click #search-button' : '_onSearch' ,
45- 'keyup #search-query' : '_onSearchKey' ,
46- 'change #layout-select' : '_onLayoutChanged' ,
47- } ,
48- start ( ) {
49- const self = this ;
46+ init ( ) {
5047 this . _parseData ( ) ;
5148 this . _updateStats ( ) ;
49+ this . _setupEventHandlers ( ) ;
5250
5351 // Load vis-network library from CDN, then render diagram
54- this . _loadVisNetwork ( ) . then ( function ( ) {
55- self . _renderDiagram ( ) ;
56- } ) . catch ( function ( err ) {
57- console . error ( '[OrgDiagramPortal] Failed to load vis-network library' , err ) ;
58- self . _showFallbackMessage ( ) ;
59- } ) ;
60-
61- return this . _super . apply ( this , arguments ) ;
62- } ,
52+ this . _loadVisNetwork ( )
53+ . then ( ( ) => {
54+ this . _renderDiagram ( ) ;
55+ } )
56+ . catch ( ( err ) => {
57+ console . error ( '[OrgDiagramPortal] Failed to load vis-network library' , err ) ;
58+ this . _showFallbackMessage ( ) ;
59+ } ) ;
60+ }
61+
62+ _setupEventHandlers ( ) {
63+ // Refresh button
64+ const refreshBtn = this . container . querySelector ( '#refresh-diagram' ) ;
65+ if ( refreshBtn ) {
66+ refreshBtn . addEventListener ( 'click' , ( e ) => {
67+ e . preventDefault ( ) ;
68+ this . _onRefresh ( ) ;
69+ } ) ;
70+ }
71+
72+ // Export button
73+ const exportBtn = this . container . querySelector ( '#export-diagram' ) ;
74+ if ( exportBtn ) {
75+ exportBtn . addEventListener ( 'click' , ( e ) => {
76+ e . preventDefault ( ) ;
77+ this . _onExport ( ) ;
78+ } ) ;
79+ }
80+
81+ // Search button
82+ const searchBtn = this . container . querySelector ( '#search-button' ) ;
83+ if ( searchBtn ) {
84+ searchBtn . addEventListener ( 'click' , ( e ) => {
85+ e . preventDefault ( ) ;
86+ this . _applySearch ( ) ;
87+ } ) ;
88+ }
89+
90+ // Search input (enter key)
91+ const searchInput = this . container . querySelector ( '#search-query' ) ;
92+ if ( searchInput ) {
93+ searchInput . addEventListener ( 'keyup' , ( e ) => {
94+ if ( e . key === 'Enter' ) {
95+ this . _applySearch ( ) ;
96+ }
97+ } ) ;
98+ }
99+
100+ // Layout selector
101+ const layoutSelect = this . container . querySelector ( '#layout-select' ) ;
102+ if ( layoutSelect ) {
103+ layoutSelect . addEventListener ( 'change' , ( ) => {
104+ this . _onLayoutChanged ( ) ;
105+ } ) ;
106+ }
107+ }
108+
63109 _loadVisNetwork ( ) {
64110 // Check if already loaded
65111 if ( window . vis && window . vis . Network ) {
@@ -85,11 +131,12 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
85131 script . onerror = reject ;
86132 document . head . appendChild ( script ) ;
87133 } ) ;
88- } ,
134+ }
135+
89136 _showFallbackMessage ( ) {
90- const container = this . el . querySelector ( '#organization-diagram-container' ) ;
91- if ( container ) {
92- container . innerHTML = `
137+ const diagramContainer = this . container . querySelector ( '#organization-diagram-container' ) ;
138+ if ( diagramContainer ) {
139+ diagramContainer . innerHTML = `
93140 <div class="p-5 text-center">
94141 <i class="fa fa-exclamation-triangle fa-3x text-warning mb-3"></i>
95142 <h5>Diagram Visualization Unavailable</h5>
@@ -98,10 +145,11 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
98145 </div>
99146 ` ;
100147 }
101- } ,
148+ }
149+
102150 _parseData ( ) {
103151 try {
104- const jsonEl = this . el . querySelector ( '#diagram-data' ) ;
152+ const jsonEl = this . container . querySelector ( '#diagram-data' ) ;
105153 if ( jsonEl ) {
106154 this . diagramData = JSON . parse ( jsonEl . textContent . trim ( ) ) ;
107155 } else {
@@ -111,10 +159,11 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
111159 console . error ( '[OrgDiagramPortal] Failed to parse diagram JSON' , e ) ;
112160 this . diagramData = { nodes : [ ] , edges : [ ] , stats : { } , config : { } } ;
113161 }
114- } ,
162+ }
163+
115164 _renderDiagram ( ) {
116- const container = this . el . querySelector ( '#organization-diagram-container' ) ;
117- if ( ! container ) {
165+ const diagramContainer = this . container . querySelector ( '#organization-diagram-container' ) ;
166+ if ( ! diagramContainer ) {
118167 console . warn ( '[OrgDiagramPortal] Container #organization-diagram-container not found' ) ;
119168 return ;
120169 }
@@ -128,7 +177,7 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
128177
129178 // Check if we have data
130179 if ( ! this . diagramData . nodes || ! this . diagramData . nodes . length ) {
131- container . innerHTML = `
180+ diagramContainer . innerHTML = `
132181 <div class="p-5 text-center">
133182 <i class="fa fa-info-circle fa-3x text-info mb-3"></i>
134183 <h5>No Organization Data</h5>
@@ -140,7 +189,7 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
140189 }
141190
142191 // Remove loading overlay
143- const loadingOverlay = container . querySelector ( '.loading-overlay' ) ;
192+ const loadingOverlay = diagramContainer . querySelector ( '.loading-overlay' ) ;
144193 if ( loadingOverlay ) {
145194 loadingOverlay . remove ( ) ;
146195 }
@@ -150,21 +199,22 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
150199
151200 // Render the network
152201 try {
153- this . network = new vis . Network ( container , {
202+ this . network = new vis . Network ( diagramContainer , {
154203 nodes : new vis . DataSet ( this . diagramData . nodes ) ,
155204 edges : new vis . DataSet ( this . diagramData . edges ) ,
156205 } , options ) ;
157206
158207 console . log ( '[OrgDiagramPortal] Diagram rendered successfully with' , this . diagramData . nodes . length , 'nodes' ) ;
159208
160209 // Add event listeners
161- this . network . on ( 'click' , this . _onNodeClick . bind ( this ) ) ;
210+ this . network . on ( 'click' , ( params ) => this . _onNodeClick ( params ) ) ;
162211
163212 } catch ( e ) {
164213 console . error ( '[OrgDiagramPortal] vis.Network rendering failed' , e ) ;
165214 this . _showFallbackMessage ( ) ;
166215 }
167- } ,
216+ }
217+
168218 _buildVisOptions ( ) {
169219 const layoutType = ( this . diagramData . config && this . diagramData . config . layout_type ) || 'hierarchical' ;
170220 const hierarchical = layoutType === 'hierarchical' ;
@@ -222,7 +272,8 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
222272 } ;
223273
224274 return options ;
225- } ,
275+ }
276+
226277 _onNodeClick ( params ) {
227278 if ( params . nodes && params . nodes . length > 0 ) {
228279 const nodeId = params . nodes [ 0 ] ;
@@ -231,11 +282,12 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
231282 this . _showNodeDetails ( node ) ;
232283 }
233284 }
234- } ,
285+ }
286+
235287 _showNodeDetails ( node ) {
236- const modal = this . el . querySelector ( '#node-details-modal' ) ;
237- const title = this . el . querySelector ( '#node-modal-title' ) ;
238- const body = this . el . querySelector ( '#node-modal-body' ) ;
288+ const modal = this . container . querySelector ( '#node-details-modal' ) ;
289+ const title = this . container . querySelector ( '#node-modal-title' ) ;
290+ const body = this . container . querySelector ( '#node-modal-body' ) ;
239291
240292 if ( ! modal || ! title || ! body ) return ;
241293
@@ -267,9 +319,20 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
267319
268320 body . innerHTML = html ;
269321
270- // Show modal using Bootstrap
271- $ ( modal ) . modal ( 'show' ) ;
272- } ,
322+ // Show modal using Bootstrap 5
323+ if ( window . bootstrap && window . bootstrap . Modal ) {
324+ const modalInstance = new bootstrap . Modal ( modal ) ;
325+ modalInstance . show ( ) ;
326+ } else {
327+ // Fallback for older Bootstrap
328+ modal . classList . add ( 'show' ) ;
329+ modal . style . display = 'block' ;
330+ const backdrop = document . createElement ( 'div' ) ;
331+ backdrop . className = 'modal-backdrop fade show' ;
332+ document . body . appendChild ( backdrop ) ;
333+ }
334+ }
335+
273336 _updateStats ( ) {
274337 const stats = this . diagramData . stats || { } ;
275338 const mapping = {
@@ -278,61 +341,60 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
278341 users : '#users-count' ,
279342 connections : '#connections-count' ,
280343 } ;
344+
281345 // Batch DOM queries for performance
282346 const elements = { } ;
283347 for ( const key in mapping ) {
284- elements [ key ] = this . el . querySelector ( mapping [ key ] ) ;
348+ elements [ key ] = this . container . querySelector ( mapping [ key ] ) ;
285349 }
350+
286351 // Batch DOM updates
287352 for ( const key in elements ) {
288353 const el = elements [ key ] ;
289354 if ( el ) {
290355 el . textContent = stats [ key ] != null ? stats [ key ] : '-' ;
291356 }
292357 }
293- } ,
294- _onRefresh ( ev ) {
295- ev . preventDefault ( ) ;
358+ }
359+
360+ _onRefresh ( ) {
296361 this . _parseData ( ) ;
297362 this . _renderDiagram ( ) ;
298363 this . _updateStats ( ) ;
299- } ,
300- _onExport ( ev ) {
301- ev . preventDefault ( ) ;
364+ }
365+
366+ _onExport ( ) {
302367 const blob = new Blob ( [ JSON . stringify ( this . diagramData , null , 2 ) ] , { type : 'application/json' } ) ;
303368 const a = document . createElement ( 'a' ) ;
304369 a . href = URL . createObjectURL ( blob ) ;
305370 a . download = 'organization_diagram.json' ;
306371 a . click ( ) ;
307- } ,
308- _onSearch ( ev ) {
309- ev . preventDefault ( ) ;
310- this . _applySearch ( ) ;
311- } ,
312- _onSearchKey ( ev ) {
313- if ( ev . key === 'Enter' ) {
314- this . _applySearch ( ) ;
315- }
316- } ,
317- _onLayoutChanged ( ) {
318- if ( ! this . network ) { return ; }
319- this . diagramData . config = this . diagramData . config || { } ;
320- this . diagramData . config . layout_type = this . el . querySelector ( '#layout-select' ) ?. value ;
321- this . _renderDiagram ( ) ;
322- } ,
372+ }
373+
323374 _applySearch ( ) {
324375 if ( ! this . network ) { return ; }
325- const query = ( this . el . querySelector ( '#search-query' ) ?. value || '' ) . trim ( ) . toLowerCase ( ) ;
376+
377+ const searchInput = this . container . querySelector ( '#search-query' ) ;
378+ const query = ( searchInput ?. value || '' ) . trim ( ) . toLowerCase ( ) ;
379+
326380 if ( ! query ) {
327381 // Clear previous selection
328382 this . network . unselectAll ( ) ;
329383 return ;
330384 }
385+
331386 const matches = this . diagramData . nodes . filter ( n => ( n . label || '' ) . toLowerCase ( ) . includes ( query ) ) ;
387+
332388 if ( matches . length ) {
333389 const ids = matches . map ( m => m . id ) ;
334390 this . network . selectNodes ( ids , true ) ;
335- this . network . focus ( ids [ 0 ] , { scale : 1.2 , animation : { duration : 500 , easingFunction : 'easeInOutQuad' } } ) ;
391+ this . network . focus ( ids [ 0 ] , {
392+ scale : 1.2 ,
393+ animation : {
394+ duration : 500 ,
395+ easingFunction : 'easeInOutQuad'
396+ }
397+ } ) ;
336398
337399 // Accessibility: Announce results
338400 const resultMsg = matches . length === 1
@@ -342,9 +404,32 @@ odoo.define('records_management.portal_organization_diagram', ['web.public.widge
342404 } else {
343405 console . log ( '[OrgDiagramPortal] Search: No matches found for "' + query + '"' ) ;
344406 }
345- } ,
346- } ) ;
407+ }
408+
409+ _onLayoutChanged ( ) {
410+ if ( ! this . network ) { return ; }
411+
412+ const layoutSelect = this . container . querySelector ( '#layout-select' ) ;
413+ this . diagramData . config = this . diagramData . config || { } ;
414+ this . diagramData . config . layout_type = layoutSelect ?. value ;
415+ this . _renderDiagram ( ) ;
416+ }
417+ }
418+
419+ // Auto-initialize on page load
420+ if ( document . readyState === 'loading' ) {
421+ document . addEventListener ( 'DOMContentLoaded' , initOrgDiagram ) ;
422+ } else {
423+ initOrgDiagram ( ) ;
424+ }
425+
426+ function initOrgDiagram ( ) {
427+ const containers = document . querySelectorAll ( '.o_portal_organization_diagram' ) ;
428+ containers . forEach ( container => {
429+ new OrgDiagramPortal ( container ) ;
430+ } ) ;
431+ }
347432
348- publicWidget . registry . OrgDiagramPortal = OrgDiagramPortal ;
349- return OrgDiagramPortal ;
350- } ) ;
433+ // Expose globally for manual initialization if needed
434+ window . RecordsManagementOrgDiagram = OrgDiagramPortal ;
435+ } ) ( ) ;
0 commit comments