Subversion Repositories SmartDukaan

Rev

Rev 36368 | Blame | Compare with Previous | Last modification | View Log | RSS feed

<link href="https://cdn.datatables.net/fixedcolumns/3.3.0/css/fixedColumns.bootstrap.css" rel="stylesheet"/>
<style>
        .timeline-table {
                width: 100%;
                border-collapse: collapse;
                font-size: 12px;
        }

        .timeline-table thead th {
                background: #2c3e50;
                color: #fff;
                padding: 6px 5px;
                text-align: center;
                font-weight: 600;
                font-size: 11px;
                border: 1px solid #34495e;
                white-space: nowrap;
        }

        .timeline-table tbody td {
                padding: 5px 4px;
                text-align: center;
                border: 1px solid #e0e0e0;
                vertical-align: middle;
                white-space: nowrap;
        }

        .timeline-table tbody tr:nth-child(even) {
                background-color: #f8f9fa;
        }

        .timeline-table tbody tr:hover {
                background-color: #eaf2ff;
        }

        .status-badge {
                display: inline-block;
                padding: 2px 6px;
                border-radius: 10px;
                font-size: 10px;
                font-weight: 600;
                text-transform: uppercase;
        }

        .status-open {
                background: #d4edda;
                color: #155724;
        }

        .status-pending {
                background: #fff3cd;
                color: #856404;
        }

        .status-completed {
                background: #cce5ff;
                color: #004085;
        }

        .status-rejected {
                background: #e2e3e5;
                color: #383d41;
        }

        .tl-cell {
                display: inline-flex;
                align-items: center;
                gap: 5px;
        }

        .tl-dot {
                height: 8px;
                width: 8px;
                border-radius: 50%;
                display: inline-block;
                flex-shrink: 0;
        }

        .tl-dot.completed {
                background-color: #28a745;
        }

        .tl-dot.delay {
                background-color: #dc3545;
        }

        .tl-dot.wip {
                background-color: #ffc107;
        }

        .tl-dot.not-started {
                background-color: #adb5bd;
        }

        .tl-date {
                font-size: 10px;
                color: #555;
        }

        .legend {
                display: inline-flex;
                gap: 14px;
                flex-wrap: wrap;
                margin: 0;
        }

        .legend-item {
                display: flex;
                align-items: center;
                gap: 5px;
                font-size: 11px;
                color: #555;
        }

        .dataTables_wrapper .top-controls {
                display: flex;
                align-items: center;
                justify-content: space-between;
                flex-wrap: nowrap;
                padding: 8px 0;
        }

        .dataTables_wrapper .top-controls .dataTables_length {
                flex-shrink: 0;
        }

        .dataTables_wrapper .top-controls .timeline-legend-row {
                flex: 1;
                display: flex;
                justify-content: center;
        }

        .dataTables_wrapper .top-controls .dataTables_filter {
                flex-shrink: 0;
}

        .section-header {
                font-size: 11px;
                color: #95a5a6;
                text-transform: uppercase;
                letter-spacing: 0.5px;
                padding: 2px 0;
        }

        .timeline-table tbody tr {
                cursor: pointer;
        }

        .timeline-table tbody tr.row-selected,
        .DTFC_LeftBodyWrapper table tbody tr.row-selected {
                background-color: #d6eaf8 !important;
        }

        .ob-stepper {
                display: flex;
                align-items: center;
                justify-content: center;
                margin: 0 0 20px;
                padding: 0;
        }

        .ob-step {
                display: flex;
                align-items: center;
        }

        .ob-step-circle {
                width: 28px;
                height: 28px;
                border-radius: 50%;
                display: flex;
                align-items: center;
                justify-content: center;
                font-size: 11px;
                font-weight: 700;
                color: #fff;
                flex-shrink: 0;
        }

        .ob-step-circle.completed {
                background: #28a745;
        }

        .ob-step-circle.inProgress {
                background: #007bff;
        }

        .ob-step-circle.notStarted {
                background: #adb5bd;
        }

        .ob-step-line {
                width: 50px;
                height: 3px;
        }

        .ob-step-line.completed {
                background: #28a745;
        }

        .ob-step-line.inProgress {
                background: #007bff;
        }

        .ob-step-line.notStarted {
                background: #dee2e6;
        }

        .ob-step-label {
                font-size: 10px;
                text-align: center;
                margin-top: 4px;
                color: #555;
        }

        .ob-step-wrap {
                display: flex;
                flex-direction: column;
                align-items: center;
        }

        .ob-kv .row {
                padding: 4px 0;
                border-bottom: 1px solid #f0f0f0;
        }

        .ob-kv .text-muted {
                font-size: 12px;
        }

        .ob-kv .col-sm-8 {
                font-size: 12px;
        }

        .ob-badge-ok {
                display: inline-block;
                padding: 2px 8px;
                border-radius: 3px;
                background: #d4edda;
                color: #155724;
                font-size: 11px;
        }

        .ob-badge-fail {
                display: inline-block;
                padding: 2px 8px;
                border-radius: 3px;
                background: #f8d7da;
                color: #721c24;
                font-size: 11px;
        }

        .ob-badge-pending {
                display: inline-block;
                padding: 2px 8px;
                border-radius: 3px;
                background: #fff3cd;
                color: #856404;
                font-size: 11px;
        }

        .ob-badge-na {
                display: inline-block;
                padding: 2px 8px;
                border-radius: 3px;
                background: #e2e3e5;
                color: #6c757d;
                font-size: 11px;
        }

        a.ob-step-circle {
                text-decoration: none;
                color: #fff;
                cursor: pointer;
                transition: transform 0.15s, box-shadow 0.15s;
        }

        a.ob-step-circle:hover {
                transform: scale(1.2);
                box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.3);
                color: #fff;
        }

        .ob-action-bar {
                padding: 8px 10px;
                margin-bottom: 12px;
                background: #f8f9fa;
                border: 1px solid #e9ecef;
                border-radius: 4px;
                display: flex;
                flex-wrap: wrap;
                gap: 8px;
                align-items: center;
        }

        .ob-action-bar .btn-warning {
                font-size: 11px;
                font-weight: 600;
        }

        .ob-action-bar .ob-badge-ok {
                font-size: 11px;
        }

        .loi-header {
                border-bottom: 2px solid #9b59b6;
        }

        .onb-header {
                border-bottom: 2px solid #2980b9;
        }

        .store-header {
                border-bottom: 2px solid #27ae60;
        }

        /* Per-row "Delay" summary column on the matrix view.
           Surfaces the worst-overdue event for each partner so users can spot the
           biggest blockers without scanning across all 19 stage cells. */
        .tl-delay-cell {
                text-align: center;
                min-width: 110px;
                line-height: 1.3;
        }

        .tl-delay-chip {
                display: inline-block;
                padding: 2px 7px;
                border-radius: 10px;
                font-size: 10px;
                font-weight: 700;
                color: #fff;
        }

        .tl-delay-chip.high {
                background: #d9534f;
        }

        .tl-delay-chip.med {
                background: #f0ad4e;
        }

        .tl-delay-chip.low {
                background: #f7dc6f;
                color: #333;
        }

        .tl-delay-team {
                display: block;
                font-size: 10px;
                color: #555;
                margin-top: 2px;
                max-width: 110px;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
        }

        .tl-delay-none {
                color: #ccc;
        }

        /* "Show delayed only" filter toggle styling — sits next to the legend in
           the DataTables top-controls flex row. */
        .tl-delay-filter {
                display: inline-flex;
                align-items: center;
                gap: 6px;
                font-size: 11px;
                font-weight: 600;
                color: #555;
                margin-left: 16px;
                cursor: pointer;
                user-select: none;
        }

        .tl-delay-filter input[type="checkbox"] {
                margin: 0;
                cursor: pointer;
        }

        .tl-delay-filter.active {
                color: #d9534f;
        }

        /* Tab strip — sits between the breadcrumb and the matrix table.
           Three preset views over the same data: All / Delayed Only / By Team. */
        .tl-tab-strip {
                display: flex;
                align-items: center;
                gap: 4px;
                padding: 8px 0 12px;
                border-bottom: 1px solid #e0e0e0;
                margin-bottom: 10px;
        }

        .tl-tab {
                background: #fff;
                border: 1px solid #d0d7de;
                border-bottom: none;
                border-radius: 4px 4px 0 0;
                padding: 8px 16px;
                font-size: 12px;
                font-weight: 600;
                color: #555;
                cursor: pointer;
                position: relative;
                bottom: -1px;
                transition: background 0.15s, color 0.15s;
        }

        .tl-tab:hover {
                background: #f5f7fa;
                color: #2c3e50;
        }

        .tl-tab.active {
                background: #2c3e50;
                color: #fff;
                border-color: #2c3e50;
        }

        .tl-tab i {
                margin-right: 5px;
        }

        .tl-tab-count {
                display: inline-block;
                margin-left: 6px;
                padding: 1px 7px;
                background: #d9534f;
                color: #fff;
                border-radius: 10px;
                font-size: 10px;
                font-weight: 700;
        }

        .tl-tab.active .tl-tab-count {
                background: #fff;
                color: #d9534f;
        }

        .tl-team-picker {
                margin-left: auto;
                display: flex;
                align-items: center;
                gap: 8px;
        }

        .tl-team-picker select {
                height: 30px;
                font-size: 12px;
                min-width: 200px;
        }
</style>

<section class="wrapper">
        <div class="row">
                <div class="col-lg-12">
                        <h3 class="page-header" style="color:#2c3e50;">
                                <i class="icon_document_alt"></i> Partner Onboarding Timeline
                        </h3>
                        <ol class="breadcrumb">
                                <li><i class="fa fa-home"></i><a href="${rc.contextPath}/dashboard">Home</a></li>
                                <li><i class="icon_document_alt"></i>Onboarding Timeline</li>
                        </ol>
                </div>
        </div>

        ## Tab strip — three preset views over the same matrix.
        ## "all" = no filter, "delayed" = only rows with active delays, "team" = delayed + team filter dropdown.
        ## Each tab is a button that flips JS filter state and re-draws the DataTable.
        <div class="col-lg-12 tl-tab-strip">
                <button type="button" class="tl-tab active" data-tab="all">
                        <i class="fa fa-list"></i> All Partners
                </button>
                <button type="button" class="tl-tab" data-tab="delayed">
                        <i class="fa fa-exclamation-triangle"></i> Delayed Only
                        #if($worstDelayMap && $worstDelayMap.size() > 0)
                                <span class="tl-tab-count">$worstDelayMap.size()</span>
                        #end
                </button>
                <button type="button" class="tl-tab" data-tab="team">
                        <i class="fa fa-users"></i> By Team
                </button>
                <div class="tl-team-picker" id="tlTeamPicker" style="display:none;">
                        <select id="tlTeamFilter" class="form-control input-sm">
                                <option value="">All teams</option>
                        </select>
                </div>
                <button type="button" class="btn btn-sm btn-info" id="btnShowSummary" style="margin-left:auto;">
                        <i class="fa fa-bar-chart"></i> Show Summary
                </button>
        </div>

        <div class="col-lg-12" style="padding:8px 0 4px;">
                <div class="legend" style="justify-content:flex-start;">
                        <div class="legend-item"><span class="tl-dot completed"></span> Done</div>
                        <div class="legend-item"><span class="tl-dot completed" style="outline:3px solid #ffe0e0;"></span><span
                                        style="background:#fff0f0;padding:1px 5px;border-radius:3px;margin-left:2px;">Done (crossed TAT)</span>
                        </div>
                        <div class="legend-item"><span class="tl-dot delay"></span> Crossed TAT</div>
                        <div class="legend-item"><span class="tl-dot wip"></span> On Time</div>
                        <div class="legend-item"><span class="tl-dot not-started"></span> Not Started</div>
                </div>
        </div>

        <div class="col-lg-12" style="overflow-x:auto;">
                <table class="timeline-table" id="storeTimeline">
                        <thead>
                        <tr>
                                <th rowspan="2">ID</th>
                                <th rowspan="2">Partner</th>
                                <th rowspan="2">Code</th>
                                <th rowspan="2">Status</th>
                                <th rowspan="2" style="background:#c0392b;">Delay</th>
                                <th colspan="4" style="background:#8e44ad;"><span class="section-header">LOI Process</span></th>
                                <th colspan="4" style="background:#2471a3;"><span class="section-header">Onboarding</span></th>
                                <th colspan="8" style="background:#1e8449;"><span class="section-header">Store Setup</span></th>
                                <th colspan="3" style="background:#d35400;"><span class="section-header">Launch</span></th>
                        </tr>
                        <tr>
                                <!-- LOI Process -->
                                <th style="background:#9b59b6;">LOI Form</th>
                                <th style="background:#9b59b6;">BM Approval</th>
                                <th style="background:#9b59b6;">Doc Approval</th>
                                <th style="background:#9b59b6;">Payment Approval</th>
                                <!-- Onboarding -->
                                <th style="background:#2980b9;">Onboarding</th>
                                <th style="background:#2980b9;">Verification</th>
                                <th style="background:#2980b9;">Store Code</th>
                                <th style="background:#2980b9;">Welcome Call</th>
                                <!-- Store Setup -->
                                <th style="background:#27ae60;">Recce</th>
                                <th style="background:#27ae60;">WOD</th>
                                <th style="background:#27ae60;">Fin Code</th>
                                <th style="background:#27ae60;">All Brand DMS</th>
                                <th style="background:#27ae60;">Branding</th>
                                <th style="background:#27ae60;">Full Stock</th>
                                <th style="background:#27ae60;">PO Creation</th>
                                <th style="background:#27ae60;">PO Approval</th>
                                <!-- Launch -->
                                <th style="background:#e67e22;">Billing</th>
                                <th style="background:#e67e22;">Inauguration</th>
                                <th style="background:#e67e22;">Training</th>
                        </tr>
                        </thead>
                        <tbody>
                                #foreach($st in $storeTimelines)
                                        ## Lookup per-row delay metadata once. partnerDelayMap maps eventName -> DelayReportItem
                                        ## (used for hover tooltips on DELAY cells). worstDelay is the single worst-overdue
                                        ## item for this partner (drives the new "Delay" column and the row-level filter).
                                        #set($partnerDelayMap = $delayIndex.get($st.getOnboardingId()))
                                        #set($worstDelay = $worstDelayMap.get($st.getOnboardingId()))
                                        #if($worstDelay)
                                        <tr data-has-delay="true" data-delay-team="$!{worstDelay.getResponsibleTeam()}">
                                        #else
                                        <tr data-has-delay="false" data-delay-team="">
                                        #end
                                        <td><strong>$st.getOnboardingId()</strong></td>
                                        <td style="text-align:left;max-width:120px;overflow:hidden;text-overflow:ellipsis;"
                                                title="$st.getOutletName() ($st.getCity())">$st.getOutletName() ($st.getCity())
                                        </td>
                                        #if($st.getCode())
                                                <td>$st.getCode()</td>
                                        #else
                                                <td style="color:#ccc;">-</td>
                                        #end
                                        <td>
                                                #set($statusLower = $st.getStatus().toLowerCase())
                                                #if($statusLower == "open")
                                                        <span class="status-badge status-open">$st.getStatus()</span>
                                                #elseif($statusLower == "pending")
                                                        <span class="status-badge status-pending">$st.getStatus()</span>
                                                #elseif($statusLower == "completed")
                                                        <span class="status-badge status-completed">$st.getStatus()</span>
                                                #else
                                                        <span class="status-badge status-rejected">$st.getStatus()</span>
                                                #end
                                        </td>
                                        ## --- New "Delay" summary column ---
                                        ## Renders the worst-overdue event with a severity-coloured chip + responsible team.
                                        ## Sort key (data-order) is daysOverdue so DataTables sorts numerically, not lexically.
                                        #if($worstDelay)
                                                #if($worstDelay.getDaysOverdue() > 7)
                                                        #set($delaySeverity = "high")
                                                #elseif($worstDelay.getDaysOverdue() > 3)
                                                        #set($delaySeverity = "med")
                                                #else
                                                        #set($delaySeverity = "low")
                                                #end
                                                <td class="tl-delay-cell" data-order="$worstDelay.getDaysOverdue()"
                                                        title="$worstDelay.getEventName() - planned $worstDelay.getPlannedDate().format($dateFormatter), overdue $worstDelay.getDaysOverdue() day(s), team: $worstDelay.getResponsibleTeam()">
                                                        <span class="tl-delay-chip $delaySeverity">$worstDelay.getDaysOverdue()d overdue</span>
                                                        <span class="tl-delay-team">$worstDelay.getResponsibleTeam()</span>
                                                </td>
                                        #else
                                                <td class="tl-delay-cell tl-delay-none" data-order="-1">-</td>
                                        #end
                                        ## Lookup late-completion metadata for this partner once.
                                        ## latePartnerMap maps eventName -> DelayReportItem for events completed
                                        ## after their TAT deadline (daysOverdue = how many days late).
                                        #set($latePartnerMap = false)
                                        #if($lateIndex)
                                                #set($latePartnerMap = $lateIndex.get($st.getOnboardingId()))
                                        #end
                                        #if($st.getObtm() && $st.getObtm().size() > 0)
                                                #foreach($obtm in $st.getObtm())
                                                        #if($obtm.getStatus().toString() == "COMPLETED")
                                                                ## Check if this event was completed late (after TAT deadline)
                                                                #set($cellLate = false)
                                                                #if($latePartnerMap)
                                                                        #set($cellLate = $latePartnerMap.get($obtm.getTitle().name()))
                                                                #end
                                                                #if($cellLate)
                                                                        <td style="background:#fff0f0;"
                                                                                title="Completed $cellLate.getDaysOverdue() day(s) late (planned: $cellLate.getPlannedDate().format($dateFormatter), actual: $obtm.getCompletedTimestamp().format($dateFormatter))"><span
                                                                                        class="tl-cell"><span
                                                                                        class="tl-dot completed"></span>#if($obtm.getCompletedTimestamp())<span
                                                                                        class="tl-date">$obtm.getCompletedTimestamp().format($dateFormatter)</span>#end</span>
                                                                        </td>
                                                                #else
                                                                        <td><span class="tl-cell"><span
                                                                                        class="tl-dot completed"></span>#if($obtm.getCompletedTimestamp())<span
                                                                                        class="tl-date">$obtm.getCompletedTimestamp().format($dateFormatter)</span>#end</span>
                                                                        </td>
                                                                #end
                                                        #elseif($obtm.getStatus().toString() == "DELAY")
                                                                ## Look up the matching DelayReportItem to enrich this red cell with
                                                                ## planned date, days overdue, and responsible team in the title tooltip.
                                                                ## $obtm.getTitle() is a StoreTimeline enum; .name() gives the string key.
                                                                #set($cellDelay = false)
                                                                #if($partnerDelayMap)
                                                                        #set($cellDelay = $partnerDelayMap.get($obtm.getTitle().name()))
                                                                #end
                                                                #if($cellDelay)
                                                                        <td style="background:#fff0f0;"
                                                                                title="Planned: $cellDelay.getPlannedDate().format($dateFormatter) | Overdue: $cellDelay.getDaysOverdue() day(s) | Team: $cellDelay.getResponsibleTeam()"><span
                                                                                        class="tl-cell"><span
                                                                                        class="tl-dot delay"></span>#if($obtm.getCompletedTimestamp())<span
                                                                                        class="tl-date">$obtm.getCompletedTimestamp().format($dateFormatter)</span>#end</span>
                                                                        </td>
                                                                #else
                                                                        <td style="background:#fff0f0;"
                                                                                title="Delayed (planned $!{obtm.getCompletedTimestamp().format($dateFormatter)})"><span
                                                                                        class="tl-cell"><span
                                                                                        class="tl-dot delay"></span>#if($obtm.getCompletedTimestamp())<span
                                                                                        class="tl-date">$obtm.getCompletedTimestamp().format($dateFormatter)</span>#end</span>
                                                                        </td>
                                                                #end
                                                        #elseif($obtm.getStatus().toString() == "WIP")
                                                                <td style="background:#fffde7;"><span class="tl-cell"><span
                                                                                class="tl-dot wip"></span>#if($obtm.getCompletedTimestamp())<span
                                                                                class="tl-date">$obtm.getCompletedTimestamp().format($dateFormatter)</span>#end</span>
                                                                </td>
                                                        #elseif($obtm.getStatus().toString() == "NOT_STARTED")
                                                                <td><span class="tl-cell"><span
                                                                                class="tl-dot not-started"></span>#if($obtm.getCompletedTimestamp())<span
                                                                                class="tl-date">$obtm.getCompletedTimestamp().format($dateFormatter)</span>#end</span>
                                                                </td>
                                                        #else
                                                                <td style="color:#ccc;">-</td>
                                                        #end
                                                #end
                                        #else
                                                #foreach($i in [1..19])
                                                        <td style="color:#ccc;">-</td>
                                                #end
                                        #end
                                </tr>
                                #end
                        </tbody>
                </table>
        </div>

        <div id="timeline-detail-container" style="display:none; margin-top:15px;">
                <div class="panel panel-default">
                        <div class="panel-heading">
                                <button type="button" class="close" id="closeDetailPanel">&times;</button>
                                <h4 class="panel-title">
                                        <span id="detail-partner-name"></span>
                                        <small id="detail-partner-code" class="text-muted" style="margin-left:10px;"></small>
                                </h4>
                        </div>
                        <div class="panel-body" id="timeline-detail-body">
                                <div class="text-center"><i class="fa fa-spinner fa-spin"></i> Loading...</div>
                        </div>
                </div>
        </div>
</section>

<script type="text/javascript">
        $(document).ready(function () {
                // Custom DataTables search filter — applies the active tab's filter rules.
                // Tab state lives on window so it survives the AJAX re-render of this panel:
                //   storeTimelineActiveTab: 'all' | 'delayed' | 'team'
                //   storeTimelineTeamFilter: string (only meaningful when tab === 'team')
                // Filter is registered once on window to stay idempotent across re-loads.
                window.storeTimelineActiveTab = window.storeTimelineActiveTab || 'all';
                window.storeTimelineTeamFilter = window.storeTimelineTeamFilter || '';
                if (!window.storeTimelineDelayFilterRegistered) {
                        window.storeTimelineDelayFilterRegistered = true;
                        $.fn.dataTable.ext.search.push(function (settings, data, dataIndex) {
                                if (settings.nTable.id !== 'storeTimeline') return true;
                                var tab = window.storeTimelineActiveTab;
                                if (tab === 'all') return true;
                                var rowNode = settings.aoData[dataIndex].nTr;
                                // Both 'delayed' and 'team' tabs require an active delay
                                if ($(rowNode).attr('data-has-delay') !== 'true') return false;
                                if (tab === 'team' && window.storeTimelineTeamFilter) {
                                        return $(rowNode).attr('data-delay-team') === window.storeTimelineTeamFilter;
                                }
                                return true;
                        });
                }

                var dtable = $('#storeTimeline').DataTable({
                        scrollX: true,
                        scrollY: '75vh',
                        scrollCollapse: true,
                        orderCellsTop: true,
                        order: [[0, "desc"]],
                        paging: false,
                        fixedColumns: {
                                leftColumns: 5
                        },
                        dom: '<"top-controls"f>rt<"bottom"i>',
                        language: {
                                search: "Search Partners:"
                        }
                });

                // Legend is now rendered as static HTML above the table (not inside DataTables controls).

                // Populate the team dropdown from the unique data-delay-team values on rows.
                // Only delayed rows have a team set, so this naturally excludes empty values.
                var teamSet = {};
                $('#storeTimeline tbody tr[data-has-delay="true"]').each(function () {
                        var team = $(this).attr('data-delay-team');
                        if (team) teamSet[team] = true;
                });
                var teamSelect = $('#tlTeamFilter');
                Object.keys(teamSet).sort().forEach(function (team) {
                        teamSelect.append($('<option></option>').val(team).text(team));
                });

                // applyTab — single source of truth for switching tabs.
                // Updates window state, button highlighting, dropdown visibility,
                // sort order, and triggers a DataTable redraw.
                function applyTab(tab) {
                        window.storeTimelineActiveTab = tab;
                        $('.tl-tab').removeClass('active');
                        $('.tl-tab[data-tab="' + tab + '"]').addClass('active');
                        $('#tlTeamPicker').toggle(tab === 'team');
                        if (tab !== 'team') {
                                window.storeTimelineTeamFilter = '';
                                teamSelect.val('');
                        }
                        dtable.order([0, 'desc']).draw();
                }

                // Tab click → apply the corresponding filter view
                $('.tl-tab').on('click', function () {
                        applyTab($(this).data('tab'));
                });

                // Team dropdown change → re-apply the team filter (only meaningful on the By Team tab)
                teamSelect.on('change', function () {
                        window.storeTimelineTeamFilter = $(this).val();
                        dtable.draw();
                });

                // Honor an optional defaultTab model attribute set by the controller.
                // Used by /partnerDelayPanel (which now delegates here) to land users
                // on the Delayed Only tab without an extra click.
                var initialTab = '$!{defaultTab}';
                if (initialTab === 'delayed' || initialTab === 'team') {
                        applyTab(initialTab);
                }

                // Row click -> load detail panel
                $('#storeTimeline tbody').on('click', 'tr', function () {
                        var $row = $(this);
                        var onboardingId = $.trim($row.find('td:first').text());
                        if (!onboardingId || isNaN(onboardingId)) return;

                        // Highlight selected row (both main table and fixed-column clone)
                        $('#storeTimeline tbody tr, .DTFC_LeftBodyWrapper table tbody tr').removeClass('row-selected');
                        var rowIndex = dtable.row($row).index();
                        $('#storeTimeline tbody tr').eq(rowIndex).addClass('row-selected');
                        $('.DTFC_LeftBodyWrapper table tbody tr').eq(rowIndex).addClass('row-selected');

                        var partnerName = $.trim($row.find('td:eq(1)').text());
                        var partnerCode = $.trim($row.find('td:eq(2)').text());

                        $('#detail-partner-name').text(partnerName);
                        $('#detail-partner-code').text(partnerCode !== '-' ? partnerCode : '');
                        $('#timeline-detail-body').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
                        $('#storeTimeline_wrapper').hide();
                        $('#timeline-detail-container').show();

                        $.ajax({
                                url: '${rc.contextPath}/partnerTimelineDetail',
                                data: {onboardingId: onboardingId},
                                success: function (html) {
                                        $('#timeline-detail-body').html(html);
                                },
                                error: function () {
                                        $('#timeline-detail-body').html('<div class="alert alert-danger">Failed to load details.</div>');
                                }
                        });

                        $('html, body').animate({scrollTop: $('#timeline-detail-container').offset().top - 60}, 300);
                });

                // Close detail panel
                $(document).on('click', '#closeDetailPanel', function () {
                        $('#timeline-detail-container').hide();
                        $('#storeTimeline_wrapper').show();
                        dtable.columns.adjust();
                        $('#storeTimeline tbody tr, .DTFC_LeftBodyWrapper table tbody tr').removeClass('row-selected');
                });

                // Show Summary modal — renders Chart.js charts on first open
                var chartsRendered = false;
                $('#btnShowSummary').on('click', function () {
                        $('#summaryModal').modal('show');
                        if (chartsRendered) return;
                        chartsRendered = true;

                        // --- Chart 1: Delays by Stage (horizontal bar) ---
                        var stageLabels = [#foreach($e in $delaysByStage.entrySet())'$e.getKey()'#if($foreach.hasNext),#end#end];
                        var stageCounts = [#foreach($e in $delaysByStage.entrySet())$e.getValue()#if($foreach.hasNext),#end#end];
                        new Chart(document.getElementById('chartDelaysByStage'), {
                                type: 'bar',
                                data: {
                                        labels: stageLabels,
                                        datasets: [{
                                                label: 'Delayed Partners',
                                                data: stageCounts,
                                                backgroundColor: '#e74c3c'
                                        }]
                                },
                                options: {
                                        indexAxis: 'y',
                                        responsive: true,
                                        maintainAspectRatio: false,
                                        plugins: {
                                                legend: {display: false},
                                                title: {display: true, text: 'Active Delays by Stage ($totalDelays total)', font: {size: 14}}
                                        },
                                        scales: {x: {beginAtZero: true, ticks: {stepSize: 1}}}
                                }
                        });

                        // --- Chart 2: Delays by Team (donut) ---
                        var teamLabels = [#foreach($e in $delaysByTeam.entrySet())'$e.getKey()'#if($foreach.hasNext),#end#end];
                        var teamCounts = [#foreach($e in $delaysByTeam.entrySet())$e.getValue()#if($foreach.hasNext),#end#end];
                        var teamColors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e67e22', '#34495e'];
                        new Chart(document.getElementById('chartDelaysByTeam'), {
                                type: 'doughnut',
                                data: {
                                        labels: teamLabels,
                                        datasets: [{data: teamCounts, backgroundColor: teamColors.slice(0, teamLabels.length)}]
                                },
                                options: {
                                        responsive: true,
                                        maintainAspectRatio: false,
                                        plugins: {
                                                title: {display: true, text: 'Delays by Responsible Team', font: {size: 14}},
                                                legend: {position: 'right'}
                                        }
                                }
                        });

                        // --- Chart 3: Delay Severity (donut) ---
                        new Chart(document.getElementById('chartDelaySeverity'), {
                                type: 'doughnut',
                                data: {
                                        labels: ['Critical (>7 days)', 'Medium (3-7 days)', 'Low (\u22643 days)'],
                                        datasets: [{
                                                data: [$severityCritical, $severityMedium, $severityLow],
                                                backgroundColor: ['#d9534f', '#f0ad4e', '#f7dc6f']
                                        }]
                                },
                                options: {
                                        responsive: true,
                                        maintainAspectRatio: false,
                                        plugins: {
                                                title: {display: true, text: 'Delay Severity Breakdown', font: {size: 14}},
                                                legend: {position: 'right'}
                                        }
                                }
                        });

                        // --- Chart 4: Late Completions by Stage (horizontal bar) ---
                        var lateLabels = [#foreach($e in $lateByStage.entrySet())'$e.getKey()'#if($foreach.hasNext),#end#end];
                        var lateCounts = [#foreach($e in $lateByStage.entrySet())$e.getValue()#if($foreach.hasNext),#end#end];
                        new Chart(document.getElementById('chartLateByStage'), {
                                type: 'bar',
                                data: {
                                        labels: lateLabels,
                                        datasets: [{
                                                label: 'Late Completions',
                                                data: lateCounts,
                                                backgroundColor: '#e67e22'
                                        }]
                                },
                                options: {
                                        indexAxis: 'y',
                                        responsive: true,
                                        maintainAspectRatio: false,
                                        plugins: {
                                                legend: {display: false},
                                                title: {display: true, text: 'Late Completions by Stage ($totalLate total)', font: {size: 14}}
                                        },
                                        scales: {x: {beginAtZero: true, ticks: {stepSize: 1}}}
                                }
                        });
                });
        });
</script>

<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>

## Summary Modal
<div class="modal fade" id="summaryModal" tabindex="-1" role="dialog">
        <div class="modal-dialog" style="width:90%;max-width:1100px;">
                <div class="modal-content">
                        <div class="modal-header" style="background:#2c3e50;color:#fff;">
                                <button type="button" class="close" data-dismiss="modal" style="color:#fff;opacity:0.8;">&times;
                                </button>
                                <h4 class="modal-title"><i class="fa fa-bar-chart"></i> Onboarding Timeline Summary</h4>
                        </div>
                        <div class="modal-body" style="padding:20px;">
                                <div class="row">
                                        <div class="col-md-6" style="height:320px;margin-bottom:20px;">
                                                <canvas id="chartDelaysByStage"></canvas>
                                        </div>
                                        <div class="col-md-6" style="height:320px;margin-bottom:20px;">
                                                <canvas id="chartDelaysByTeam"></canvas>
                                        </div>
                                </div>
                                <div class="row">
                                        <div class="col-md-6" style="height:320px;margin-bottom:20px;">
                                                <canvas id="chartDelaySeverity"></canvas>
                                        </div>
                                        <div class="col-md-6" style="height:320px;margin-bottom:20px;">
                                                <canvas id="chartLateByStage"></canvas>
                                        </div>
                                </div>
                        </div>
                        <div class="modal-footer">
                                <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                        </div>
                </div>
        </div>
</div>