Generate-M365LicenseAssignmentReport.ps1
<#PSScriptInfo .VERSION 2.4 .GUID d23c77c4-d7c2-4b50-8518-9c9ee7718d43 .AUTHOR Roy Klooster .COMPANYNAME RK Solutions .COPYRIGHT .TAGS "RK Solutions" Microsoft365 "Microsoft Entra ID" "Microsoft Graph" .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES Initial release - Comprehensive Microsoft 365 license assignment report with HTML output 2.1 - Added function Test-MgGraphConnection to check for existing connections with the proper permissions .PRIVATEDATA #> <# .DESCRIPTION This script generates an advanced M365 license assignment report #> Param() function New-HTMLReport { param( [Parameter(Mandatory = $true)] [string]$Organization, [Parameter(Mandatory = $true)] [array]$Report, [Parameter(Mandatory = $true)] [array]$SubscriptionOverview, [Parameter(Mandatory = $false)] [string]$ExportPath = "$env:PUBLIC\Documents\$Organization-M365LicensingReport.html" ) # Calculate license counts for dashboard statistics $directLicenses = ($Report | Where-Object { $_.AssignmentType -eq "Direct" }).Count $inheritedLicenses = ($Report | Where-Object { $_.AssignmentType -eq "Inherited" }).Count $bothLicenses = ($Report | Where-Object { $_.AssignmentType -eq "Both" }).Count $inactiveUsersWithLicenses = ($Report | Where-Object { $_.AccountEnabled -eq "No" }).Count # Create HTML Template with DataTables $htmlTemplate = @' <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>$Organization M365 Licensing Report</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.1/css/buttons.bootstrap5.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <script src="https://code.jquery.com/jquery-3.7.0.js"></script> <script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.1/js/dataTables.buttons.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.bootstrap5.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/pdfmake.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.53/vfs_fonts.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.html5.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.print.min.js"></script> <script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.colVis.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <style> :root { /* Light mode variables (default) */ --primary-color: #0078d4; --secondary-color: #2b88d8; --direct-color: #0078d4; --inherited-color: #107c10; --both-color: #5c2d91; --inactive-color: #d83b01; --bg-color: #f8f9fa; --card-bg: #ffffff; --text-color: #333333; --table-header-bg: #f5f5f5; --table-header-color: #333333; --table-stripe-bg: rgba(0,0,0,0.02); --table-hover-bg: rgba(0,0,0,0.04); --table-border-color: #dee2e6; --filter-tag-bg: #e9ecef; --filter-tag-color: #495057; --filter-bg: white; --btn-outline-color: #6c757d; --border-color: #dee2e6; --toggle-bg: #ccc; --button-bg: #f8f9fa; --button-color: #333; --button-border: #ddd; --button-hover-bg: #e9ecef; --footer-text: white; --input-bg: #fff; --input-color: #333; --input-border: #ced4da; --input-focus-border: #86b7fe; --input-focus-shadow: rgba(13, 110, 253, 0.25); --datatable-even-row-bg: #fff; --datatable-odd-row-bg: rgba(0,0,0,0.02); } [data-theme="dark"] { /* Dark mode variables */ --primary-color: #0078d4; --secondary-color: #2b88d8; --direct-color: #0078d4; --inherited-color: #107c10; --both-color: #5c2d91; --inactive-color: #d83b01; --bg-color: #121212; --card-bg: #1e1e1e; --text-color: #e0e0e0; --table-header-bg: #333333; --table-header-color: #e0e0e0; --table-stripe-bg: rgba(255,255,255,0.03); --table-hover-bg: rgba(255,255,255,0.05); --table-border-color: #444444; --filter-tag-bg: #2d2d2d; --filter-tag-color: #d0d0d0; --filter-bg: #252525; --btn-outline-color: #adb5bd; --border-color: #444444; --toggle-bg: #555555; --button-bg: #2a2a2a; --button-color: #e0e0e0; --button-border: #444; --button-hover-bg: #3a3a3a; --footer-text: white; --input-bg: #2a2a2a; --input-color: #e0e0e0; --input-border: #444444; --input-focus-border: #0078d4; --input-focus-shadow: rgba(0, 120, 212, 0.25); --datatable-even-row-bg: #1e1e1e; --datatable-odd-row-bg: #252525; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; background-color: var(--bg-color); color: var(--text-color); min-height: 100vh; display: flex; flex-direction: column; transition: background-color 0.3s ease, color 0.3s ease; } .container-fluid { max-width: 1600px; padding: 20px; flex: 1; } .dashboard-header { padding: 20px 0; margin-bottom: 30px; border-bottom: 1px solid rgba(128,128,128,0.2); display: flex; align-items: center; justify-content: space-between; } .dashboard-title { display: flex; align-items: center; gap: 15px; } .dashboard-title h1 { margin: 0; font-size: 1.8rem; font-weight: 600; color: var(--primary-color); } .logo { height: 45px; width: 45px; } .report-date { font-size: 0.9rem; color: var(--text-color); opacity: 0.8; } .card { border: none; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); margin-bottom: 25px; transition: transform 0.2s, box-shadow 0.2s, background-color 0.3s ease; overflow: hidden; background-color: var(--card-bg); } .card:hover { transform: translateY(-5px); box-shadow: 0 8px 16px rgba(0,0,0,0.1); } .card-header { background-color: var(--primary-color); color: white; font-weight: 600; padding: 15px 20px; border-bottom: none; display: flex; align-items: center; justify-content: space-between; gap: 10px; } .card-header i { font-size: 1.2rem; } .card-body { padding: 20px; } .stats-card { height: 100%; text-align: center; padding: 25px 15px; border-radius: 10px; color: white; position: relative; overflow: hidden; min-height: 160px; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; transition: all 0.3s; } .stats-card.active { box-shadow: 0 0 0 4px rgba(255,255,255,0.6), 0 8px 16px rgba(0,0,0,0.2); transform: scale(1.05); } .stats-card::before { content: ''; position: absolute; top: -20px; right: -20px; width: 100px; height: 100px; border-radius: 50%; background-color: rgba(255,255,255,0.1); z-index: 0; } .stats-card i { font-size: 2.5rem; margin-bottom: 15px; position: relative; z-index: 1; } .stats-card h3 { font-size: 1rem; font-weight: 500; margin-bottom: 10px; position: relative; z-index: 1; } .stats-card .number { font-size: 2.2rem; font-weight: 700; position: relative; z-index: 1; } .direct-bg { background: linear-gradient(135deg, var(--direct-color), #2b88d8); } .inherited-bg { background: linear-gradient(135deg, var(--inherited-color), #2a9d2a); } .both-bg { background: linear-gradient(135deg, var(--both-color), #7b4db2); } .inactive-bg { background: linear-gradient(135deg, var(--inactive-color), #f25c05); } /* DataTables Dark Mode Overrides */ table.dataTable { border-collapse: collapse !important; width: 100% !important; color: var(--text-color) !important; border-color: var(--table-border-color) !important; } .table { color: var(--text-color) !important; border-color: var(--table-border-color) !important; } .table-striped>tbody>tr:nth-of-type(odd) { background-color: var(--datatable-odd-row-bg) !important; } .table-striped>tbody>tr:nth-of-type(even) { background-color: var(--datatable-even-row-bg) !important; } .table thead th { background-color: var(--table-header-bg) !important; color: var(--table-header-color) !important; font-weight: 600; border-top: none; padding: 12px; border-color: var(--table-border-color) !important; } .table tbody td { padding: 12px; vertical-align: middle; border-color: var(--table-border-color) !important; color: var(--text-color) !important; } .table.table-bordered { border-color: var(--table-border-color) !important; } .table-bordered td, .table-bordered th { border-color: var(--table-border-color) !important; } .table-hover tbody tr:hover { background-color: var(--table-hover-bg) !important; } .badge { padding: 6px 10px; font-weight: 500; border-radius: 6px; } .badge-direct { background-color: var(--direct-color); color: white; } .badge-inherited { background-color: var(--inherited-color); color: white; } .badge-both { background-color: var(--both-color); color: white; } .badge-inactive { background-color: var(--inactive-color); color: white; } .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_processing, .dataTables_wrapper .dataTables_paginate { color: var(--text-color) !important; } .dataTables_wrapper .dataTables_paginate .paginate_button { padding: 0.3em 0.8em; border-radius: 4px; margin: 0 3px; color: var(--text-color) !important; border: 1px solid var(--border-color) !important; background-color: var(--button-bg) !important; } .dataTables_wrapper .dataTables_paginate .paginate_button.current { background: var(--primary-color) !important; border-color: var(--primary-color) !important; color: white !important; } .dataTables_wrapper .dataTables_paginate .paginate_button:hover { background: var(--button-hover-bg) !important; border-color: var(--border-color) !important; color: var(--text-color) !important; } .dataTables_wrapper .dataTables_length select, .dataTables_wrapper .dataTables_filter input { border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--input-color); border-radius: 4px; padding: 5px 10px; } .dataTables_wrapper .dataTables_filter input:focus { border-color: var(--input-focus-border); box-shadow: 0 0 0 0.25rem var(--input-focus-shadow); } .dataTables_info { padding-top: 10px; color: var(--text-color); } footer { background-color: var(--primary-color); color: var(--footer-text); text-align: center; padding: 15px 0; margin-top: auto; } footer p { margin: 0; font-weight: 500; } .filter-buttons { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; } .filter-button { padding: 8px 16px; border-radius: 20px; border: none; cursor: pointer; font-weight: 500; transition: all 0.2s; display: flex; align-items: center; gap: 8px; } .filter-button:hover { opacity: 0.9; } .filter-button.active { box-shadow: 0 0 0 2px rgba(128,128,128,0.2); } .filter-button-direct { background-color: var(--direct-color); color: white; } .filter-button-inherited { background-color: var(--inherited-color); color: white; } .filter-button-both { background-color: var(--both-color); color: white; } .filter-button-inactive { background-color: var(--inactive-color); color: white; } .filter-button-all { background-color: var(--btn-outline-color); color: white; } .toggle-container { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; } .toggle-switch { position: relative; display: inline-block; width: 60px; height: 30px; } .toggle-switch input { opacity: 0; width: 0; height: 0; } .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--toggle-bg); transition: .4s; border-radius: 34px; } .toggle-slider:before { position: absolute; content: ""; height: 22px; width: 22px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .toggle-slider { background-color: var(--primary-color); } input:checked + .toggle-slider:before { transform: translateX(30px); } .license-filter-container { margin: 15px 0; display: flex; gap: 10px; flex-wrap: wrap; } .license-badge { padding: 6px 12px; border-radius: 20px; background-color: var(--filter-tag-bg); color: var(--filter-tag-color); cursor: pointer; transition: all 0.2s; font-size: 0.85rem; } .license-badge.active { background-color: var(--primary-color); color: white; } @media (max-width: 768px) { .dashboard-header { flex-direction: column; align-items: flex-start; gap: 10px; } .stats-card { min-height: 140px; } .filter-buttons { flex-direction: column; } } /* Export buttons styling */ div.dt-buttons { margin-bottom: 1rem; } button.dt-button { background-color: var(--button-bg) !important; border-color: var(--button-border) !important; color: var(--button-color) !important; font-weight: 500 !important; padding: 8px 16px !important; border-radius: 4px !important; margin-right: 8px !important; } button.dt-button:hover { background-color: var(--button-hover-bg) !important; border-color: var(--border-color) !important; } button.dt-button.active { background-color: var(--primary-color) !important; border-color: var(--primary-color) !important; color: white !important; } .filter-section { background-color: var(--filter-bg); padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin-bottom: 20px; transition: background-color 0.3s ease; } .filter-section h5 { color: var(--primary-color); margin-bottom: 12px; font-weight: 600; } .filter-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; } .filter-tag { background-color: var(--filter-tag-bg); padding: 4px 12px; border-radius: 16px; font-size: 0.85rem; color: var(--filter-tag-color); display: flex; align-items: center; gap: 6px; transition: background-color 0.3s ease, color 0.3s ease; } .filter-tag i { cursor: pointer; color: var(--filter-tag-color); } .filter-tag i:hover { color: var(--inactive-color); } .custom-search { display: flex; gap: 10px; margin-bottom: 15px; } .custom-search input { flex: 1; padding: 8px 12px; border: 1px solid var(--border-color); background-color: var(--bg-color); color: var(--text-color); border-radius: 4px; } .custom-search button { background-color: var(--primary-color); color: white; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; } .active-filters-container { margin-bottom: 15px; } /* Show all entries toggle */ .show-all-container { display: flex; align-items: center; gap: 12px; background-color: transparent; padding: 0; border: none; margin-left: 15px; } .show-all-text { font-weight: 500; margin: 0; color: white; font-size: 0.85rem; } /* Custom DataTable controls wrapper */ .datatable-header { display: flex; align-items: center; flex-wrap: wrap; margin-bottom: 1rem; } .datatable-controls { display: flex; align-items: center; gap: 15px; flex-wrap: wrap; } /* Theme toggle styles */ .theme-toggle { position: fixed; top: 20px; right: 20px; z-index: 1000; display: flex; align-items: center; gap: 10px; background-color: var(--card-bg); padding: 8px 12px; border-radius: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: background-color 0.3s ease; } .theme-toggle-switch { position: relative; display: inline-block; width: 50px; height: 26px; } .theme-toggle-switch input { opacity: 0; width: 0; height: 0; } .theme-toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--toggle-bg); transition: .4s; border-radius: 34px; } .theme-toggle-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .theme-toggle-slider { background-color: var(--primary-color); } input:checked + .theme-toggle-slider:before { transform: translateX(24px); } .theme-icon { display: flex; align-items: center; justify-content: center; font-size: 16px; color: var(--text-color); } /* Form elements for dark mode */ .form-select, .form-control { background-color: var(--input-bg) !important; color: var(--input-color) !important; border-color: var(--input-border) !important; } .form-select:focus, .form-control:focus { border-color: var(--input-focus-border) !important; box-shadow: 0 0 0 0.25rem var(--input-focus-shadow) !important; } .form-label { color: var(--text-color); } .btn-outline-secondary { color: var(--text-color); border-color: var(--border-color); background-color: transparent; } .btn-outline-secondary:hover { background-color: var(--filter-tag-bg); color: var(--text-color); } /* Override for dropdown menus and selects */ .form-select option { background-color: var(--input-bg); color: var(--input-color); } /* Fix DataTables odd/even row striping */ table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd { background-color: var(--datatable-odd-row-bg) !important; } table.dataTable.stripe tbody tr.even, table.dataTable.display tbody tr.even { background-color: var(--datatable-even-row-bg) !important; } /* Fix DataTables background color for hovered rows */ table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover { background-color: var(--table-hover-bg) !important; } /* Fix DataTables border colors */ table.dataTable.border-bottom, table.dataTable.border-top, table.dataTable thead th, table.dataTable tfoot th, table.dataTable thead td, table.dataTable tfoot td { border-color: var(--table-border-color) !important; } /* Bootstrap 5 DataTables specific overrides */ .table-striped>tbody>tr:nth-of-type(odd)>* { --bs-table-accent-bg: var(--datatable-odd-row-bg) !important; color: var(--text-color) !important; } .table>:not(caption)>*>* { background-color: var(--card-bg) !important; color: var(--text-color) !important; } .table-striped>tbody>tr { background-color: var(--datatable-even-row-bg) !important; } /* Direct cell background colors */ .table tbody tr td { background-color: transparent !important; } /* Force Bootstrap Tables to use the correct colors */ .table-striped>tbody>tr:nth-of-type(odd) { --bs-table-accent-bg: var(--datatable-odd-row-bg) !important; } </style> </head> <body> <!-- Dark Mode Toggle --> <div class="theme-toggle"> <div class="theme-icon"> <i class="fas fa-sun"></i> </div> <label class="theme-toggle-switch"> <input type="checkbox" id="themeToggle"> <span class="theme-toggle-slider"></span> </label> <div class="theme-icon"> <i class="fas fa-moon"></i> </div> </div> <div class="container-fluid"> <div class="dashboard-header"> <div class="dashboard-title"> <svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"> <path fill="#ff5722" d="M6 6H22V22H6z" transform="rotate(-180 14 14)"/> <path fill="#4caf50" d="M26 6H42V22H26z" transform="rotate(-180 34 14)"/> <path fill="#ffc107" d="M6 26H22V42H6z" transform="rotate(-180 14 34)"/> <path fill="#03a9f4" d="M26 26H42V42H26z" transform="rotate(-180 34 34)"/> </svg> <h1>$Organization M365 Licensing Report</h1> </div> <div class="report-date"> <i class="fas fa-calendar-alt me-2"></i> Report generated on: $ReportDate </div> </div> <div class="row mb-4"> <div class="col-md-3 mb-3"> <div class="stats-card direct-bg" id="directFilter"> <i class="fas fa-user-tag"></i> <h3>Direct Licenses</h3> <div class="number">$directLicenses</div> </div> </div> <div class="col-md-3 mb-3"> <div class="stats-card inherited-bg" id="inheritedFilter"> <i class="fas fa-users-cog"></i> <h3>Inherited Licenses</h3> <div class="number">$inheritedLicenses</div> </div> </div> <div class="col-md-3 mb-3"> <div class="stats-card both-bg" id="bothFilter"> <i class="fas fa-user-shield"></i> <h3>Both (Direct + Inherited)</h3> <div class="number">$bothLicenses</div> </div> </div> <div class="col-md-3 mb-3"> <div class="stats-card inactive-bg" id="inactiveFilter"> <i class="fas fa-user-slash"></i> <h3>Inactive Users with Licenses</h3> <div class="number">$inactiveUsersWithLicenses</div> </div> </div> </div> <div class="filter-section"> <h5><i class="fas fa-filter me-2"></i>Filter Options</h5> <div class="row"> <div class="col-md-6"> <div class="mb-3"> <label for="accountStatusFilter" class="form-label">Account Status</label> <select id="accountStatusFilter" class="form-select"> <option value="">All Accounts</option> <option value="Active">Active Accounts</option> <option value="Inactive">Inactive Accounts</option> </select> </div> </div> <div class="col-md-6"> <div class="mb-3"> <label for="assignmentTypeFilter" class="form-label">Assignment Type</label> <select id="assignmentTypeFilter" class="form-select"> <option value="">All Types</option> <option value="Direct">Direct</option> <option value="Inherited">Inherited</option> <option value="Both">Both</option> </select> </div> </div> </div> <div class="mb-3"> <label for="licenseNameFilter" class="form-label">License Name</label> <input type="text" id="licenseNameFilter" class="form-control" placeholder="Search for license names..."> </div> <div class="mb-3"> <label class="form-label">Quick Filters</label> <div class="filter-buttons"> <button class="filter-button filter-button-all" data-filter="all"><i class="fas fa-globe"></i> Show All</button> <button class="filter-button filter-button-direct" data-filter="direct"><i class="fas fa-user-tag"></i> Direct Only</button> <button class="filter-button filter-button-inherited" data-filter="inherited"><i class="fas fa-users-cog"></i> Inherited Only</button> <button class="filter-button filter-button-both" data-filter="both"><i class="fas fa-user-shield"></i> Both</button> <button class="filter-button filter-button-inactive" data-filter="inactive"><i class="fas fa-user-slash"></i> Inactive Users</button> </div> </div> <div class="active-filters-container"> <div class="d-flex justify-content-between align-items-center"> <label class="form-label mb-0">Active Filters:</label> <button id="clearAllFilters" class="btn btn-sm btn-outline-secondary">Clear All</button> </div> <div class="filter-tags" id="activeFilters"> <!-- Active filters will be displayed here --> </div> </div> </div> <div class="card"> <div class="card-header"> <div> <i class="fas fa-id-card"></i> License Assignment </div> <div class="show-all-container"> <label class="toggle-switch"> <input type="checkbox" id="licensesShowAllToggle"> <span class="toggle-slider"></span> </label> <p class="show-all-text">Show all entries</p> </div> </div> <div class="card-body"> <div class="table-responsive"> <table id="licensesTable" class="table table-striped table-bordered" style="width:100%"> <thead> <tr> <th>Display Name</th> <th>User Principal Name</th> <th>Account Status</th> <th>License</th> <th>Assignment Type</th> <th>Inheritance Details</th> </tr> </thead> <tbody> {{TABLE_DATA}} </tbody> </table> </div> </div> </div> <div class="card"> <div class="card-header"> <div> <i class="fas fa-project-diagram"></i> Subscription Overview </div> <div class="show-all-container"> <label class="toggle-switch"> <input type="checkbox" id="subscriptionShowAllToggle"> <span class="toggle-slider"></span> </label> <p class="show-all-text">Show all entries</p> </div> </div> <div class="card-body"> <div class="table-responsive"> <table id="subscriptionTable" class="table table-striped table-bordered" style="width:100%"> <thead> <tr> <th>Subscription</th> <th>Created Date</th> <th>End Date</th> <th>License Status</th> <th>Consumed Units</th> <th>Total Licenses</th> <th>Available Licenses</th> </tr> </thead> <tbody> {{SUBSCRIPTION_DATA}} </tbody> </table> </div> </div> </div> </div> <footer> <p>Generated by Roy Klooster - RK Solutions</p> </footer> <script> // Initialize DataTables $(document).ready(function() { // Theme toggling functionality const themeToggle = document.getElementById('themeToggle'); const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); // Function to update table colors for dark mode function updateTableColors() { // Force all table cells to have the correct background if (document.documentElement.getAttribute('data-theme') === 'dark') { // Dark mode $('table.dataTable tbody tr').css('background-color', 'var(--datatable-even-row-bg)'); $('table.dataTable tbody tr:nth-child(odd)').css('background-color', 'var(--datatable-odd-row-bg)'); $('table.dataTable tbody td').css('color', 'var(--text-color)'); $('table.dataTable thead th').css({ 'background-color': 'var(--table-header-bg)', 'color': 'var(--table-header-color)' }); } else { // Light mode $('table.dataTable tbody tr').css('background-color', 'var(--datatable-even-row-bg)'); $('table.dataTable tbody tr:nth-child(odd)').css('background-color', 'var(--datatable-odd-row-bg)'); $('table.dataTable tbody td').css('color', 'var(--text-color)'); $('table.dataTable thead th').css({ 'background-color': 'var(--table-header-bg)', 'color': 'var(--table-header-color)' }); } } // Check for saved user preference, or use system preference const savedTheme = localStorage.getItem('theme'); if (savedTheme === 'dark' || (!savedTheme && prefersDarkScheme.matches)) { document.documentElement.setAttribute('data-theme', 'dark'); themeToggle.checked = true; } // Add event listener for theme toggle themeToggle.addEventListener('change', function() { if (this.checked) { document.documentElement.setAttribute('data-theme', 'dark'); localStorage.setItem('theme', 'dark'); } else { document.documentElement.setAttribute('data-theme', 'light'); localStorage.setItem('theme', 'light'); } // Apply the table color changes after theme switch setTimeout(updateTableColors, 50); }); // Initialize DataTable for licenses const licensesTable = $('#licensesTable').DataTable({ dom: 'Bfrtip', buttons: [ { extend: 'collection', text: '<i class="fas fa-download"></i> Export', buttons: [ { extend: 'excel', text: '<i class="fas fa-file-excel"></i> Excel', exportOptions: { columns: ':visible' } }, { extend: 'csv', text: '<i class="fas fa-file-csv"></i> CSV', exportOptions: { columns: ':visible' } }, { extend: 'pdf', text: '<i class="fas fa-file-pdf"></i> PDF', exportOptions: { columns: ':visible' } }, { extend: 'print', text: '<i class="fas fa-print"></i> Print', exportOptions: { columns: ':visible' } } ] }, { extend: 'colvis', text: '<i class="fas fa-columns"></i> Columns' } ], paging: true, searching: true, ordering: true, info: true, responsive: true, lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]], order: [[0, 'asc']], language: { search: "<i class='fas fa-search'></i> _INPUT_", searchPlaceholder: "Search records...", lengthMenu: "Show _MENU_ entries", info: "Showing _START_ to _END_ of _TOTAL_ entries", paginate: { first: "<i class='fas fa-angle-double-left'></i>", last: "<i class='fas fa-angle-double-right'></i>", next: "<i class='fas fa-angle-right'></i>", previous: "<i class='fas fa-angle-left'></i>" } }, drawCallback: function() { // Enforce the correct colors after DataTables redraws updateTableColors(); } }); // Initialize DataTable for subscriptions const subscriptionTable = $('#subscriptionTable').DataTable({ dom: 'Bfrtip', buttons: [ { extend: 'collection', text: '<i class="fas fa-download"></i> Export', buttons: [ { extend: 'excel', text: '<i class="fas fa-file-excel"></i> Excel', exportOptions: { columns: ':visible' } }, { extend: 'csv', text: '<i class="fas fa-file-csv"></i> CSV', exportOptions: { columns: ':visible' } }, { extend: 'pdf', text: '<i class="fas fa-file-pdf"></i> PDF', exportOptions: { columns: ':visible' } }, { extend: 'print', text: '<i class="fas fa-print"></i> Print', exportOptions: { columns: ':visible' } } ] }, { extend: 'colvis', text: '<i class="fas fa-columns"></i> Columns' } ], paging: true, searching: true, ordering: true, info: true, responsive: true, lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "All"]], language: { search: "<i class='fas fa-search'></i> _INPUT_", searchPlaceholder: "Search records...", lengthMenu: "Show _MENU_ entries", info: "Showing _START_ to _END_ of _TOTAL_ entries", paginate: { first: "<i class='fas fa-angle-double-left'></i>", last: "<i class='fas fa-angle-double-right'></i>", next: "<i class='fas fa-angle-right'></i>", previous: "<i class='fas fa-angle-left'></i>" } }, drawCallback: function() { // Enforce the correct colors after DataTables redraws updateTableColors(); } }); // Apply initial table colors setTimeout(updateTableColors, 100); // Show all toggle functionality for licenses table $('#licensesShowAllToggle').on('change', function() { if ($(this).is(':checked')) { licensesTable.page.len(-1).draw(); } else { licensesTable.page.len(10).draw(); } }); // Show all toggle functionality for subscription table $('#subscriptionShowAllToggle').on('change', function() { if ($(this).is(':checked')) { subscriptionTable.page.len(-1).draw(); } else { subscriptionTable.page.len(10).draw(); } }); // Custom filtering function $.fn.dataTable.ext.search.push( function(settings, data, dataIndex) { // Only apply to licenses table if (settings.nTable.id !== 'licensesTable') { return true; } // Get filter values const accountStatus = $('#accountStatusFilter').val(); const assignmentType = $('#assignmentTypeFilter').val(); const licenseNameFilter = $('#licenseNameFilter').val().toLowerCase(); // Get row data const rowAccountStatus = data[2]; // Account Status column const rowAssignmentType = data[4]; // Assignment Type column const rowLicenseName = data[3].toLowerCase(); // License Name column // Filter by account status if (accountStatus && accountStatus === 'Active' && !rowAccountStatus.includes('Active')) { return false; } if (accountStatus && accountStatus === 'Inactive' && !rowAccountStatus.includes('Inactive')) { return false; } // Filter by assignment type if (assignmentType && !rowAssignmentType.includes(assignmentType)) { return false; } // Filter by license name if (licenseNameFilter && !rowLicenseName.includes(licenseNameFilter)) { return false; } return true; } ); // Stats card filtering $('#directFilter').on('click', function() { $('#assignmentTypeFilter').val('Direct'); updateActiveFilters('Assignment Type', 'Direct'); applyFilters(); toggleStatsCardActive('directFilter'); }); $('#inheritedFilter').on('click', function() { $('#assignmentTypeFilter').val('Inherited'); updateActiveFilters('Assignment Type', 'Inherited'); applyFilters(); toggleStatsCardActive('inheritedFilter'); }); $('#bothFilter').on('click', function() { $('#assignmentTypeFilter').val('Both'); updateActiveFilters('Assignment Type', 'Both'); applyFilters(); toggleStatsCardActive('bothFilter'); }); $('#inactiveFilter').on('click', function() { $('#accountStatusFilter').val('Inactive'); updateActiveFilters('Account Status', 'Inactive'); applyFilters(); toggleStatsCardActive('inactiveFilter'); }); // Button filtering $('.filter-button').on('click', function() { const filterType = $(this).data('filter'); // Clear all filter button active states $('.filter-button').removeClass('active'); $(this).addClass('active'); // Reset filters $('#accountStatusFilter').val(''); $('#assignmentTypeFilter').val(''); $('#licenseNameFilter').val(''); clearActiveFilters(); // Apply selected filter switch(filterType) { case 'direct': $('#assignmentTypeFilter').val('Direct'); updateActiveFilters('Assignment Type', 'Direct'); toggleStatsCardActive('directFilter'); break; case 'inherited': $('#assignmentTypeFilter').val('Inherited'); updateActiveFilters('Assignment Type', 'Inherited'); toggleStatsCardActive('inheritedFilter'); break; case 'both': $('#assignmentTypeFilter').val('Both'); updateActiveFilters('Assignment Type', 'Both'); toggleStatsCardActive('bothFilter'); break; case 'inactive': $('#accountStatusFilter').val('Inactive'); updateActiveFilters('Account Status', 'Inactive'); toggleStatsCardActive('inactiveFilter'); break; case 'all': default: // Reset all filters $('.stats-card').removeClass('active'); break; } applyFilters(); }); // Apply filters when select boxes change $('#accountStatusFilter, #assignmentTypeFilter').on('change', function() { const filterType = $(this).attr('id'); const filterValue = $(this).val(); if (filterValue) { if (filterType === 'accountStatusFilter') { updateActiveFilters('Account Status', filterValue); } else if (filterType === 'assignmentTypeFilter') { updateActiveFilters('Assignment Type', filterValue); } } else { if (filterType === 'accountStatusFilter') { removeActiveFilter('Account Status'); } else if (filterType === 'assignmentTypeFilter') { removeActiveFilter('Assignment Type'); } } applyFilters(); }); // Apply filter when license name input changes $('#licenseNameFilter').on('input', function() { const filterValue = $(this).val(); if (filterValue) { updateActiveFilters('License Name', filterValue); } else { removeActiveFilter('License Name'); } applyFilters(); }); // Clear all filters button $('#clearAllFilters').on('click', function() { $('#accountStatusFilter').val(''); $('#assignmentTypeFilter').val(''); $('#licenseNameFilter').val(''); $('.filter-button').removeClass('active'); $('.stats-card').removeClass('active'); clearActiveFilters(); applyFilters(); }); // Function to apply all filters function applyFilters() { licensesTable.draw(); } // Function to toggle stats card active state function toggleStatsCardActive(cardId) { $('.stats-card').removeClass('active'); $('#' + cardId).addClass('active'); } // Function to update active filters function updateActiveFilters(filterType, filterValue) { // Remove existing filter of the same type removeActiveFilter(filterType); // Add new filter tag const filterTag = ` <div class="filter-tag" data-filter-type="${filterType}"> <span>${filterType}: ${filterValue}</span> <i class="fas fa-times-circle remove-filter" data-filter-type="${filterType}"></i> </div> `; $('#activeFilters').append(filterTag); // Add click handler to remove filter $('.remove-filter').off('click').on('click', function() { const filterTypeToRemove = $(this).data('filter-type'); if (filterTypeToRemove === 'Account Status') { $('#accountStatusFilter').val(''); } else if (filterTypeToRemove === 'Assignment Type') { $('#assignmentTypeFilter').val(''); } else if (filterTypeToRemove === 'License Name') { $('#licenseNameFilter').val(''); } $(this).closest('.filter-tag').remove(); // Remove active state from stat cards and filter buttons if (filterTypeToRemove === 'Account Status' && $('#inactiveFilter').hasClass('active')) { $('#inactiveFilter').removeClass('active'); } else if (filterTypeToRemove === 'Assignment Type') { $('#directFilter, #inheritedFilter, #bothFilter').removeClass('active'); } $('.filter-button').removeClass('active'); applyFilters(); }); } // Function to remove active filter by type function removeActiveFilter(filterType) { $('.filter-tag[data-filter-type="' + filterType + '"]').remove(); } // Function to clear all active filters function clearActiveFilters() { $('#activeFilters').empty(); } // Force dark mode to take effect on page elements $(window).on('load', function() { setTimeout(updateTableColors, 200); }); // Re-apply styles after DataTables operations licensesTable.on('draw.dt', function() { setTimeout(updateTableColors, 50); }); subscriptionTable.on('draw.dt', function() { setTimeout(updateTableColors, 50); }); }); </script> </body> </html> '@ # Generate table rows for user licenses $tableRows = "" foreach ($item in $Report) { $accountStatusClass = if ($item.AccountEnabled -eq "No") { 'class="table-danger"' } else { '' } $assignmentTypeBadge = switch ($item.AssignmentType) { "Direct" { '<span class="badge badge-direct">Direct</span>' } "Inherited" { '<span class="badge badge-inherited">Inherited</span>' } "Both" { '<span class="badge badge-both">Both</span>' } default { '<span class="badge bg-secondary">Unknown</span>' } } $accountStatus = if ($item.AccountEnabled -eq "Yes") { '<span class="badge bg-success">Active</span>' } else { '<span class="badge badge-inactive">Inactive</span>' } $tableRows += @" <tr $accountStatusClass> <td>$($item.DisplayName)</td> <td>$($item.UserPrincipalName)</td> <td>$accountStatus</td> <td>$($item.AssignedLicensesFriendlyName)</td> <td>$assignmentTypeBadge</td> <td>$($item.Inheritence)</td> </tr> "@ } # Generate table rows for subscription overview $subscriptionRows = "" foreach ($item in $SubscriptionOverview) { $availabilityPercentage = if ($item.TotalLicenses -ne 0) { [Math]::Round(($item.AvailableLicenses / $item.TotalLicenses) * 100) } else { 0 } $availabilityBadge = if ($availabilityPercentage -lt 10) { '<span class="badge bg-danger">' + $item.AvailableLicenses + ' (' + $availabilityPercentage + '%)</span>' } elseif ($availabilityPercentage -lt 20) { '<span class="badge bg-warning text-dark">' + $item.AvailableLicenses + ' (' + $availabilityPercentage + '%)</span>' } else { '<span class="badge bg-success">' + $item.AvailableLicenses + ' (' + $availabilityPercentage + '%)</span>' } $licenseStatusBadge = if ($item.LicenseStatus -eq "Active") { '<span class="badge bg-success">Active</span>' } else { '<span class="badge bg-danger">Inactive</span>' } $subscriptionRows += @" <tr> <td>$($item.FriendlyName)</td> <td>$($item.CreatedDate)</td> <td>$($item.EndDate)</td> <td>$licenseStatusBadge</td> <td>$($item.ConsumedUnits)</td> <td>$($item.TotalLicenses)</td> <td>$availabilityBadge</td> </tr> "@ } # Get current date for report $currentDate = Get-Date -Format "dd-MM-yyyy HH:mm" # Replace placeholders in template with actual values $htmlContent = $htmlTemplate.Replace('$Organization', $Organization) $htmlContent = $htmlContent.Replace('$ReportDate', $currentDate) $htmlContent = $htmlContent.Replace('$directLicenses', $directLicenses) $htmlContent = $htmlContent.Replace('$inheritedLicenses', $inheritedLicenses) $htmlContent = $htmlContent.Replace('$bothLicenses', $bothLicenses) $htmlContent = $htmlContent.Replace('$inactiveUsersWithLicenses', $inactiveUsersWithLicenses) $htmlContent = $htmlContent.Replace('{{TABLE_DATA}}', $tableRows) $htmlContent = $htmlContent.Replace('{{SUBSCRIPTION_DATA}}', $subscriptionRows) # Add additional CSS for dark mode pagination $darkModePaginationCss = @" <style> /* Dark mode pagination buttons */ [data-theme="dark"] .page-link { background-color: var(--button-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } [data-theme="dark"] .page-item.active .page-link { background-color: var(--primary-color) !important; color: white !important; border-color: var(--primary-color) !important; } [data-theme="dark"] .page-item.disabled .page-link { background-color: var(--card-bg) !important; color: #6c757d !important; border-color: var(--border-color) !important; } [data-theme="dark"] .dataTables_paginate .paginate_button { background-color: var(--button-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } [data-theme="dark"] .dataTables_paginate .paginate_button.current, [data-theme="dark"] .dataTables_paginate .paginate_button.current:hover { background: var(--primary-color) !important; color: white !important; border-color: var(--primary-color) !important; } [data-theme="dark"] .dataTables_paginate .paginate_button:hover { background: var(--button-hover-bg) !important; color: var(--text-color) !important; border-color: var(--button-border) !important; } [data-theme="dark"] .dataTables_paginate .paginate_button.disabled, [data-theme="dark"] .dataTables_paginate .paginate_button.disabled:hover { background-color: var(--card-bg) !important; color: #6c757d !important; border-color: var(--border-color) !important; opacity: 0.6; } </style> "@ # Insert the dark mode pagination CSS before the </head> tag $htmlContent = $htmlContent.Replace('</head>', "$darkModePaginationCss`n</head>") # Export to HTML file $htmlContent | Out-File -FilePath $ExportPath -Encoding utf8 Write-Host "All actions completed successfully." -ForegroundColor Cyan Write-Host "Report saved to: $ExportPath" -ForegroundColor Cyan # Open the HTML file Invoke-Item $ExportPath } function Test-PSVersion { [CmdletBinding()] param() $minimumVersion = [Version]"7.0.0" $currentVersion = $PSVersionTable.PSVersion if ($currentVersion -lt $minimumVersion) { Write-Host "Error: This script requires PowerShell 7.0 or higher." -ForegroundColor Red Write-Host "Current PowerShell version: $($currentVersion)" -ForegroundColor Red Write-Host "Please install PowerShell 7 from https://github.com/PowerShell/PowerShell/releases" -ForegroundColor Yellow # Check if pwsh is installed but script was run in older version if (Get-Command pwsh -ErrorAction SilentlyContinue) { Write-Host "`nPowerShell 7 appears to be installed. You can run this script with:" -ForegroundColor Cyan Write-Host "pwsh -File `"$($MyInvocation.PSCommandPath)`"" -ForegroundColor Cyan # Ask if they want to relaunch with PowerShell 7 do { $response = Read-Host "Would you like to run this script using PowerShell 7 now? (Y/N)" } while ($response -notmatch '^(Y|y|N|n)$') if ($response -match '^(Y|y)$') { Start-Process -FilePath "pwsh" -ArgumentList "-File `"$($MyInvocation.PSCommandPath)`"" -NoNewWindow } else { Write-Host "Script execution stopped." -ForegroundColor Yellow } } # Stop script execution exit } Write-Host "PowerShell version check passed: Running PowerShell $currentVersion" -ForegroundColor Green } Function Install-Requirements { # Check if the required modules are installed $requiredModules = @( "Microsoft.Graph.Authentication", "Microsoft.Graph.Beta.Identity.DirectoryManagement", "Microsoft.Graph.Beta.Users", "Microsoft.Graph.Beta.Groups" ) foreach ($module in $requiredModules) { if (-not (Get-Module -ListAvailable -Name $module)) { Write-Host "Installing module: $module" -ForegroundColor Cyan Install-Module -Name $module -Scope CurrentUser -Force -RequiredVersion 2.25.0 } else { Write-Host "Module $module is already installed." -ForegroundColor Green } } # Import the required modules foreach ($module in $requiredModules) { $ImportedModules = Get-Module -Name Microsoft.Graph* if (-not ($ImportedModules | Where-Object Name -EQ $module)) { try { Import-Module -Name $module -Force -RequiredVersion 2.25.0 -ErrorAction Stop Write-Host "Module $module imported successfully." -ForegroundColor Green } catch { Write-Host "Failed to import module $module $_" -ForegroundColor Red throw } } else { Write-Host "Module $module was already imported successfully." -ForegroundColor Green } } } function Test-MgGraphConnection { Clear-Host # Authentication $AuthenticationScope = @( "User.Read.All", "AuditLog.Read.All", "Organization.Read.All", "RoleManagement.Read.Directory" ) Connect-MgGraph -Scopes $AuthenticationScope -NoWelcome # Check if already connected $Contextinfo = Get-MgContext -ErrorAction SilentlyContinue if (-not $Contextinfo) { # If not connected, connect now Connect-MgGraph -Scopes $AuthenticationScope -NoWelcome } else { Write-Host "Already connected to Microsoft Graph as $($Contextinfo.Account)." -ForegroundColor Green } Start-Sleep -Seconds 2 Clear-Host } # Check PowerShell version Test-PSVersion # Install required modules Install-Requirements # Connect to Microsoft Graph Test-MgGraphConnection $Organization = (Get-MgBetaOrganization).displayName # Import the Product names and service plan identifiers for licensing CSV file [array]$Identifiers = Invoke-RestMethod -Method Get -Uri "https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv" | ConvertFrom-Csv # Select all SKUs with friendly display name [array]$SKU_friendly = $Identifiers | Select-Object GUID, String_Id, Product_Display_Name -Unique # Get products used in tenant [Array]$Skus = Get-MgBetaSubscribedSku # Get subscriptions with their end dates [Array]$Subscriptions = Get-MgBetaDirectorySubscription -Property * # Create an overview of subscriptions with their end date $SubscriptionOverview = @() foreach ($subscription in $Subscriptions) { $sku = $Skus | Where-Object { $_.SkuId -eq $subscription.SkuId } $friendlyName = $SKU_friendly | Where-Object { $_.GUID -eq $sku.SkuId } | Select-Object -ExpandProperty Product_Display_Name $endDate = if ($subscription.NextLifecycleDateTime -eq $null) { "No end date found" } else { $subscription.NextLifecycleDateTime } # Format dates if they're not strings $formattedCreatedDate = if ($subscription.CreatedDateTime -is [DateTime]) { Get-Date $subscription.CreatedDateTime -Format "dd-MM-yyyy HH:mm" } else { $subscription.CreatedDateTime } $formattedEndDate = if ($endDate -is [DateTime]) { Get-Date $endDate -Format "dd-MM-yyyy HH:mm" } else { $endDate } # Determine license status based on end date $licenseStatus = if ($endDate -eq "No end date found") { "Active" } elseif ($endDate -is [DateTime] -and $endDate -gt (Get-Date)) { "Active" } else { "Inactive" } $SubscriptionOverview += [PSCustomObject]@{ SubscriptionId = $subscription.Id FriendlyName = $friendlyName CreatedDate = $formattedCreatedDate EndDate = $formattedEndDate LicenseStatus = $licenseStatus ConsumedUnits = $sku.ConsumedUnits TotalLicenses = $subscription.TotalLicenses AvailableLicenses = $subscription.TotalLicenses - $sku.ConsumedUnits } } # Output the overview Write-Host "INFO: Generating subscription overview..." -ForegroundColor Cyan # Generate CSV of all SKUs $Report = @() # Get all users with licenses $users = Get-MgBetaUser -All -Select UserPrincipalName, AssignedLicenses, LicenseAssignmentStates, DisplayName, AccountEnabled | Select-Object UserPrincipalName, DisplayName, AssignedLicenses, LicenseAssignmentStates, AccountEnabled # Get all groups and licenses $groups = Get-MgBetaGroup -All $groupsWithLicenses = @() # Loop through each group and check if it has any licenses assigned Write-Host "INFO: Checking groups for licenses..." -ForegroundColor Cyan foreach ($group in $groups) { $licenses = Get-MgBetaGroup -GroupId $group.Id -Property "AssignedLicenses, Id, DisplayName" | Select-Object AssignedLicenses, DisplayName, Id if ($licenses.AssignedLicenses) { $groupData = [PSCustomObject]@{ ObjectId = $group.Id DisplayName = $group.DisplayName Licenses = $licenses.AssignedLicenses } $groupsWithLicenses += $groupData } } $totalUsers = $users.Count $currentIndex = 0 foreach ($user in $users) { $currentIndex++ Write-Progress -Activity "Processing users" -Status "Processing $currentIndex of $totalUsers" -PercentComplete (($currentIndex / $totalUsers) * 100) $AssignmentDetails = $user | Select-Object -ExpandProperty licenseAssignmentStates $GroupedAssignmentDetails = $AssignmentDetails | Group-Object -Property SkuId foreach ($GroupedAssignmentDetail in $GroupedAssignmentDetails) { $SkuId = $GroupedAssignmentDetail.Name $AssignedByGroup = $GroupedAssignmentDetail.Group | Where-Object { $_.AssignedByGroup -ne $null } $DirectAssignment = $GroupedAssignmentDetail.Group | Where-Object { $_.AssignedByGroup -eq $null } $FriendlyName = ($SKU_friendly | Where-Object { $_.GUID -eq $SkuId }).Product_Display_Name $isDirect = if ($DirectAssignment) { $true } else { $false } $isInherited = if ($AssignedByGroup) { $true } else { $false } $assignmentType = if ($isDirect -and $isInherited) { "Both" } elseif ($isDirect) { "Direct" } elseif ($isInherited) { "Inherited" } else { "Unknown" } $AssignedGroups = @() if ($AssignedByGroup) { foreach ($Group in $AssignedByGroup) { $AssignedGroups += ($groupsWithLicenses | Where-Object { $_.ObjectId -eq $Group.AssignedByGroup }).DisplayName } $AssignedGroups = $AssignedGroups -join ", " } if ($DirectAssignment -and -not $AssignedGroups) { $inheritence = "Direct" } elseif (-not $DirectAssignment -and $AssignedGroups) { $inheritence = "$AssignedGroups" } elseif ($DirectAssignment -and $AssignedGroups) { $inheritence = "Direct, $AssignedGroups" } $licenseData = [PSCustomObject]@{ UserPrincipalName = $user.UserPrincipalName DisplayName = $user.DisplayName AccountEnabled = if ($user.AccountEnabled) { "Yes" } else { "No" } AssignedLicenses = $SkuId AssignedLicensesFriendlyName = $FriendlyName Inheritence = $inheritence AssignmentType = $assignmentType IsDirect = $isDirect IsInherited = $isInherited } $Report += $licenseData } } # Calculate metrics for summary boxes $directLicenses = ($Report | Where-Object { $_.IsDirect -eq $true }).Count $inheritedLicenses = ($Report | Where-Object { $_.IsInherited -eq $true -and $_.IsDirect -eq $false }).Count $bothLicenses = ($Report | Where-Object { $_.IsDirect -eq $true -and $_.IsInherited -eq $true }).Count $inactiveUsersWithLicenses = ($Report | Where-Object { $_.AccountEnabled -eq "No" } | Select-Object UserPrincipalName -Unique).Count # Output the report Write-Host "INFO: Generating report..." -ForegroundColor Cyan # Generate HTML report New-HTMLReport -Organization $Organization -Report $Report -SubscriptionOverview $SubscriptionOverview # Disconnect from Microsoft Graph Disconnect-MgGraph | Out-Null Write-Host "Disconnected from Microsoft Graph." -ForegroundColor Green |