Plugin Directory

Changeset 3692384


Ignore:
Timestamp:
09/12/2026 06:50:48 AM (6 weeks ago)
Author:
carlosmmairena
Message:

tagging version 1.4.0

Location:
ldap-staff-directory
Files:
18 edited
1 copied

Legend:

Unmodified
Added
Removed
  • ldap-staff-directory/tags/1.4.0/admin/views/settings-page.php

    r3634465 r3692384  
    145145                                printf( '</div>' );
    146146                                $this->row( __( 'Department order', 'ldap-staff-directory' ), 'ldap_ed_department_order', array( $this, 'render_field_department_order' ) );
     147                                $this->row( __( 'Employee order', 'ldap-staff-directory' ), 'ldap_ed_employee_order', array( $this, 'render_field_employee_order' ) );
    147148                                ?>
    148149                        </div>
  • ldap-staff-directory/tags/1.4.0/includes/class-admin.php

    r3634465 r3692384  
    147147                                ? $raw_department_order
    148148                                : 'alpha';
     149
     150                        $allowed_employee_orders = array( 'name_asc', 'name_desc', 'title_asc', 'title_desc' );
     151                        $raw_employee_order      = sanitize_text_field( $input['employee_order'] ?? 'name_asc' );
     152                        $clean['employee_order'] = in_array( $raw_employee_order, $allowed_employee_orders, true )
     153                                ? $raw_employee_order
     154                                : 'name_asc';
    149155
    150156                        $clean['cache_ttl'] = absint( $input['cache_ttl'] ?? 60 );
    … …  
    588594        }
    589595
     596        public function render_field_employee_order() {
     597                $saved   = $this->get_option( 'employee_order', 'name_asc' );
     598                $options = array(
     599                        'name_asc'   => __( 'Name (A–Z)', 'ldap-staff-directory' ),
     600                        'name_desc'  => __( 'Name (Z–A)', 'ldap-staff-directory' ),
     601                        'title_asc'  => __( 'Title (A–Z)', 'ldap-staff-directory' ),
     602                        'title_desc' => __( 'Title (Z–A)', 'ldap-staff-directory' ),
     603                );
     604
     605                printf(
     606                        '<select id="ldap_ed_employee_order" name="%1$s[employee_order]">',
     607                        esc_attr( LDAP_ED_OPTION_KEY )
     608                );
     609                foreach ( $options as $ldap_ed_order_value => $ldap_ed_order_label ) {
     610                        printf(
     611                                '<option value="%1$s" %2$s>%3$s</option>',
     612                                esc_attr( $ldap_ed_order_value ),
     613                                selected( $saved, $ldap_ed_order_value, false ),
     614                                esc_html( $ldap_ed_order_label )
     615                        );
     616                }
     617                printf(
     618                        '</select><p class="description">%s</p>',
     619                        esc_html__( 'Order of the employee cards within a department on the public directory. Ties on title are broken by name (A–Z).', 'ldap-staff-directory' )
     620                );
     621        }
     622
    590623        public function render_field_enable_search() {
    591624                printf(
  • ldap-staff-directory/tags/1.4.0/includes/class-ldap-connector.php

    r3633538 r3692384  
    239239         * Search the LDAP directory and return an array of user data.
    240240         *
     241         * The returned array's order is NOT guaranteed — it reflects whatever order
     242         * the LDAP server returned entries in. Callers that need a specific display
     243         * order (e.g. the shortcode's employee_order setting) must sort explicitly.
     244         *
    241245         * @return array|\WP_Error
    242246         */
    … …  
    277281                }
    278282
    279                 // Sort alphabetically by name.
    280                 usort( $users, function ( $a, $b ) {
    281                         return strcmp( $a['name'], $b['name'] );
    282                 } );
    283 
    284283                $this->disconnect();
    285284                return $users;
  • ldap-staff-directory/tags/1.4.0/includes/class-shortcode.php

    r3633538 r3692384  
    1212class LDAP_ED_Shortcode {
    1313
     14        /**
     15         * Deterministic avatar background colors, shared between employee and
     16         * department avatars. Color is picked via crc32( $name ) % count( palette ).
     17         */
     18        private const AVATAR_PALETTE = array(
     19                '#4f7df3', '#7c5cbf', '#0e9b8a',
     20                '#2e9e4f', '#c0392b', '#d35400',
     21                '#1a7bbf', '#8e44ad',
     22        );
     23
     24        /**
     25         * Inline SVG icon paths (Lucide), keyed by icon name. Static, trusted
     26         * markup — never built from user input.
     27         */
     28        private const ICONS = array(
     29                'briefcase'     => '<path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/><rect width="20" height="14" x="2" y="6" rx="2"/>',
     30                'mail'          => '<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>',
     31                'phone'         => '<path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/>',
     32                'building'      => '<rect width="16" height="20" x="4" y="2" rx="2"/><path d="M9 22v-4h6v4"/><path d="M8 6h.01"/><path d="M16 6h.01"/><path d="M12 6h.01"/><path d="M12 10h.01"/><path d="M12 14h.01"/><path d="M16 10h.01"/><path d="M16 14h.01"/><path d="M8 10h.01"/><path d="M8 14h.01"/>',
     33                'chevron-right' => '<path d="m9 18 6-6-6-6"/>',
     34                'arrow-left'    => '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>',
     35        );
     36
    1437        public function __construct() {
    1538                add_shortcode( 'ldap_directory', array( $this, 'render' ) );
    … …  
    5679        /**
    5780         * Render the directory HTML.
     81         *
     82         * Renders one of two views depending on whether a department is selected:
     83         * - No department (ldap_dept empty): department menu (department-menu.php).
     84         * - Department present: department detail — filtered/paginated employee
     85         *   grid (directory.php), unchanged from the pre-existing behavior.
    5886         *
    5987         * @param array $atts Shortcode attributes.
    … …  
    99127                }
    100128
    101                 // Extract department list from the full (unfiltered) set for correct chip counts.
     129                // Extract department list (with counts) from the full, unfiltered set.
    102130                $ldap_ed_department_order = $settings['department_order'] ?? 'alpha';
    103131                $ldap_ed_departments      = $this->extract_departments( $all_users, $ldap_ed_department_order );
    104132                $ldap_ed_all_count        = count( $all_users );
    105133
     134                // No department selected: render the department menu. No filtering or
     135                // pagination runs for this state — it's a flat list of all departments.
     136                if ( '' === $ldap_ed_current_dept ) {
     137                        ob_start();
     138                        include LDAP_ED_DIR . 'public/views/department-menu.php';
     139                        return ob_get_clean();
     140                }
     141
    106142                // Apply department and search filters in PHP.
    107143                $filtered_users = $this->filter_users( $all_users, $ldap_ed_search_query, $ldap_ed_current_dept );
     144
     145                // Sort the filtered result before paginating, so order is consistent across pages.
     146                $ldap_ed_employee_order = $settings['employee_order'] ?? 'name_asc';
     147                $filtered_users         = $this->sort_users( $filtered_users, $ldap_ed_employee_order );
    108148
    109149                // Paginate the filtered result.
    … …  
    121161                include LDAP_ED_DIR . 'public/views/directory.php';
    122162                return ob_get_clean();
     163        }
     164
     165        // -------------------------------------------------------------------------
     166        // Shared display helpers (avatars, icons) — used by both views' templates.
     167
     168        /**
     169         * Computes deterministic initials and an avatar background color for a
     170         * name (employee or department). Same color formula for both: crc32( $name )
     171         * modulo the shared 8-color palette.
     172         *
     173         * @param string $name            Employee or department name.
     174         * @param bool   $force_two_chars When true, a single-word name still yields
     175         *                                2-letter initials (used for department
     176         *                                abbreviations). Employee avatars keep the
     177         *                                pre-existing 1-letter behavior by default.
     178         * @return array{initials:string,color:string}
     179         */
     180        public function get_initials_and_color( string $name, bool $force_two_chars = false ): array {
     181                $parts    = preg_split( '/\s+/', trim( $name ), 2 );
     182                $initials = strtoupper( substr( $parts[0] ?? '', 0, 1 ) );
     183
     184                if ( ! empty( $parts[1] ) ) {
     185                        $initials .= strtoupper( substr( $parts[1], 0, 1 ) );
     186                } elseif ( $force_two_chars ) {
     187                        $initials .= strtoupper( substr( $parts[0] ?? '', 1, 1 ) );
     188                }
     189
     190                $color = self::AVATAR_PALETTE[ abs( crc32( $name ) ) % count( self::AVATAR_PALETTE ) ];
     191
     192                return array(
     193                        'initials' => $initials,
     194                        'color'    => $color,
     195                );
     196        }
     197
     198        /**
     199         * Returns inline SVG markup for a static, trusted icon (Lucide-style).
     200         * Never built from user input — safe to echo without escaping.
     201         *
     202         * @param string $name Icon key, see self::ICONS.
     203         * @return string SVG markup, or '' when the icon name is unknown.
     204         */
     205        public function get_icon_svg( string $name ): string {
     206                if ( ! isset( self::ICONS[ $name ] ) ) {
     207                        return '';
     208                }
     209
     210                return sprintf(
     211                        '<svg class="ldap-icon ldap-icon-%1$s" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">%2$s</svg>',
     212                        esc_attr( $name ),
     213                        self::ICONS[ $name ]
     214                );
    123215        }
    124216
    … …  
    207299
    208300        /**
     301         * Sort a user array for display, per the employee_order setting.
     302         *
     303         * Ties on title (including empty title) are always broken by name ascending,
     304         * regardless of the primary order's direction — this is intentional, not a bug.
     305         * Falls back to 'name_asc' for any value outside the known whitelist.
     306         *
     307         * @param array  $users Filtered user array.
     308         * @param string $order 'name_asc' (default), 'name_desc', 'title_asc', or 'title_desc'.
     309         * @return array Sorted array (re-indexed).
     310         */
     311        private function sort_users( array $users, string $order = 'name_asc' ): array {
     312                $allowed_orders = array( 'name_asc', 'name_desc', 'title_asc', 'title_desc' );
     313                if ( ! in_array( $order, $allowed_orders, true ) ) {
     314                        $order = 'name_asc';
     315                }
     316
     317                usort(
     318                        $users,
     319                        static function ( $a, $b ) use ( $order ) {
     320                                if ( 'name_desc' === $order ) {
     321                                        return strcmp( $b['name'] ?? '', $a['name'] ?? '' );
     322                                }
     323
     324                                if ( 'title_asc' === $order || 'title_desc' === $order ) {
     325                                        $result = strcmp( $a['title'] ?? '', $b['title'] ?? '' );
     326                                        if ( 'title_desc' === $order ) {
     327                                                $result = -$result;
     328                                        }
     329                                        // Tie on title: always break by name ascending, regardless of direction.
     330                                        return 0 !== $result ? $result : strcmp( $a['name'] ?? '', $b['name'] ?? '' );
     331                                }
     332
     333                                // name_asc (default).
     334                                return strcmp( $a['name'] ?? '', $b['name'] ?? '' );
     335                        }
     336                );
     337
     338                return array_values( $users );
     339        }
     340
     341        /**
    209342         * Slice a filtered user array for the requested page.
    210343         *
  • ldap-staff-directory/tags/1.4.0/ldap-staff-directory.php

    r3634465 r3692384  
    44 * Plugin URI:  https://wordpress.org/plugins/ldap-staff-directory/
    55 * Description: Connects to LDAP or LDAPS to display an employee directory from an OU. Supports Elementor, Beaver Builder and a native shortcode.
    6  * Version:     1.2.1
     6 * Version:     1.4.0
    77 * Requires at least: 5.8
    88 * Requires PHP: 7.4
    … …  
    1919
    2020// Plugin constants.
    21 define( 'LDAP_ED_VERSION',     '1.2.1' );
     21define( 'LDAP_ED_VERSION',     '1.4.0' );
    2222define( 'LDAP_ED_FILE',        __FILE__ );
    2323define( 'LDAP_ED_DIR',         plugin_dir_path( __FILE__ ) );
  • ldap-staff-directory/tags/1.4.0/public/css/directory.css

    r3606783 r3692384  
    2525        --ldap-dept-badge-bg:    rgba( 0, 115, 170, .08 );
    2626        --ldap-dept-badge-color: #005a87;
     27
     28        /* Inline field icons */
     29        --ldap-icon-color:       #8c8f94;
    2730}
    2831
    … …  
    3336        font-size: var( --ldap-font-size );
    3437        color: var( --ldap-text-color );
    35 }
    36 
    37 /* ── Department filter bar ──────────────────────────────────────────────── */
    38 
    39 .ldap-dept-filters {
    40         display: flex;
    41         flex-wrap: nowrap;
    42         gap: 8px;
    43         overflow-x: auto;
    44         white-space: nowrap;
    45         padding-bottom: 4px;
    46         margin-bottom: 24px;
    47         /* Hide scrollbar visually while keeping it functional */
    48         -ms-overflow-style: none;
    49         scrollbar-width: none;
    50 }
    51 
    52 .ldap-dept-filters::-webkit-scrollbar {
    53         display: none;
    54 }
    55 
    56 .ldap-dept-chip {
    57         display: inline-flex;
    58         align-items: center;
    59         gap: 5px;
    60         padding: 5px 14px;
    61         font-size: .82em;
    62         font-family: inherit;
    63         font-weight: 500;
    64         color: var( --ldap-text-color );
    65         background: transparent;
    66         border: 1px solid var( --ldap-card-border );
    67         border-radius: 20px;
    68         text-decoration: none;
    69         cursor: pointer;
    70         white-space: nowrap;
    71         transition: border-color .15s ease, background-color .15s ease, color .15s ease;
    72         flex-shrink: 0;
    73 }
    74 
    75 a.ldap-dept-chip:hover {
    76         border-color: var( --ldap-primary-color );
    77         color: var( --ldap-primary-color );
    78 }
    79 
    80 .ldap-dept-chip.is-active {
    81         background: var( --ldap-primary-color );
    82         border-color: var( --ldap-primary-color );
    83         color: #fff;
    84         cursor: default;
    85 }
    86 
    87 .ldap-dept-chip-clear {
    88         display: inline-flex;
    89         align-items: center;
    90         justify-content: center;
    91         margin-left: 2px;
    92         font-size: 1.1em;
    93         font-weight: 700;
    94         color: inherit;
    95         text-decoration: none;
    96         opacity: .75;
    97         cursor: pointer;
    98         transition: opacity .15s ease;
    99 }
    100 
    101 .ldap-dept-chip-clear:hover {
    102         opacity: 1;
    10338}
    10439
    … …  
    236171}
    237172
     173.ldap-title,
     174.ldap-email,
     175.ldap-phone,
     176.ldap-extension {
     177        display: flex;
     178        align-items: center;
     179        gap: 6px;
     180}
     181
    238182.ldap-email {
    239183        margin-top: 10px;
    … …  
    265209
    266210.ldap-extension {
    267         display: block;
    268211        margin-top: 4px;
    269212        font-size: .88em;
    270213        color: var( --ldap-muted-color );
     214}
     215
     216/* ── Inline field icons (employee card, breadcrumb, department rows) ───── */
     217
     218.ldap-icon {
     219        width: 12px;
     220        height: 12px;
     221        flex-shrink: 0;
     222        color: var( --ldap-icon-color );
    271223}
    272224
    … …  
    341293        font-variant-numeric: tabular-nums;
    342294}
     295
     296/* ── Department menu (root view) ───────────────────────────────────────── */
     297
     298.ldap-menu-header {
     299        margin-bottom: 20px;
     300}
     301
     302.ldap-menu-title {
     303        margin: 0 0 4px;
     304        font-size: 1.4em;
     305        font-weight: 700;
     306        color: var( --ldap-text-color );
     307}
     308
     309.ldap-menu-sub {
     310        margin: 0;
     311        font-size: .85em;
     312        color: var( --ldap-muted-color );
     313}
     314
     315.ldap-dept-search-wrap {
     316        max-width: 320px;
     317}
     318
     319.ldap-dept-search {
     320        max-width: none;
     321}
     322
     323.ldap-menu-eyebrow {
     324        margin: 0 0 12px;
     325        font-size: .72em;
     326        font-weight: 700;
     327        letter-spacing: .08em;
     328        text-transform: uppercase;
     329        color: var( --ldap-muted-color );
     330}
     331
     332/* Two columns on desktop (matches the prototype); pure CSS, no JS balancing. */
     333.ldap-dept-menu-list {
     334        columns: 2;
     335        column-gap: 10px;
     336}
     337
     338.ldap-dept-row {
     339        display: flex;
     340        align-items: center;
     341        gap: 10px;
     342        break-inside: avoid;
     343        margin-bottom: 8px;
     344        padding: 10px 14px;
     345        background: var( --ldap-card-bg );
     346        border: 1px solid var( --ldap-card-border );
     347        border-radius: var( --ldap-card-radius );
     348        color: inherit;
     349        text-decoration: none;
     350        transition: box-shadow .15s ease, border-color .15s ease;
     351}
     352
     353.ldap-dept-row:hover {
     354        box-shadow: 0 4px 14px rgba( 0, 0, 0, .1 );
     355        border-color: #c3c4c7;
     356}
     357
     358.ldap-dept-row[hidden] {
     359        display: none;
     360}
     361
     362.ldap-dept-row-avatar {
     363        flex-shrink: 0;
     364        width: 36px;
     365        height: 36px;
     366        border-radius: 50%;
     367        background-color: var( --ldap-avatar-bg );
     368        color: #fff;
     369        display: flex;
     370        align-items: center;
     371        justify-content: center;
     372        font-size: 12px;
     373        font-weight: 700;
     374        user-select: none;
     375}
     376
     377.ldap-dept-row-info {
     378        flex: 1;
     379        min-width: 0;
     380        display: flex;
     381        flex-direction: column;
     382        gap: 2px;
     383}
     384
     385.ldap-dept-row-name {
     386        font-size: .93em;
     387        font-weight: 600;
     388        color: var( --ldap-text-color );
     389}
     390
     391.ldap-dept-row-count {
     392        font-size: .8em;
     393        color: var( --ldap-muted-color );
     394}
     395
     396.ldap-dept-row .ldap-icon {
     397        width: 14px;
     398        height: 14px;
     399        color: var( --ldap-search-border );
     400}
     401
     402@media ( max-width: 540px ) {
     403        .ldap-dept-menu-list {
     404                columns: 1;
     405        }
     406}
     407
     408/* ── Breadcrumb + department header (detail view) ──────────────────────── */
     409
     410.ldap-breadcrumb {
     411        display: inline-flex;
     412        align-items: center;
     413        gap: 6px;
     414        margin-bottom: 16px;
     415        font-size: .93em;
     416        font-weight: 500;
     417        color: var( --ldap-primary-color );
     418        text-decoration: none;
     419}
     420
     421.ldap-breadcrumb:hover {
     422        text-decoration: underline;
     423}
     424
     425.ldap-breadcrumb .ldap-icon {
     426        width: 13px;
     427        height: 13px;
     428        color: var( --ldap-primary-color );
     429}
     430
     431.ldap-dept-header {
     432        display: flex;
     433        align-items: center;
     434        justify-content: space-between;
     435        gap: 16px;
     436        margin-bottom: 20px;
     437}
     438
     439.ldap-dept-header-name {
     440        margin: 0 0 4px;
     441        font-size: 1.25em;
     442        font-weight: 700;
     443        color: var( --ldap-text-color );
     444}
     445
     446.ldap-dept-header-sub {
     447        margin: 0;
     448        font-size: .82em;
     449        color: var( --ldap-muted-color );
     450}
     451
     452.ldap-dept-header-badge {
     453        flex-shrink: 0;
     454        width: 44px;
     455        height: 44px;
     456        border-radius: 50%;
     457        background-color: var( --ldap-avatar-bg );
     458        color: #fff;
     459        display: flex;
     460        align-items: center;
     461        justify-content: center;
     462        font-size: 14px;
     463        font-weight: 700;
     464        user-select: none;
     465}
     466
     467@media ( max-width: 540px ) {
     468        .ldap-dept-header {
     469                flex-direction: column;
     470                align-items: flex-start;
     471        }
     472
     473        .ldap-dept-header-badge {
     474                width: 36px;
     475                height: 36px;
     476                font-size: 12px;
     477        }
     478}
  • ldap-staff-directory/tags/1.4.0/public/js/directory.js

    r3548101 r3692384  
    11/**
    22 * LDAP Staff Directory — public JS
    3  * Filtering and pagination are now server-side. This file strips empty
    4  * ldap_search from the form submission to keep URLs clean.
     3 *
     4 * Two independent, small pieces of behavior:
     5 * 1. Employee search (department detail view) is server-side — this only
     6 *    strips an empty ldap_search from the form submission to keep URLs clean.
     7 * 2. Department search (department menu view) is client-side — it filters
     8 *    the already-rendered .ldap-dept-row elements by substring match, with
     9 *    no network request. Scoped to at most the total number of departments;
     10 *    it never touches employee cards or pagination.
    511 */
    612( function () {
    … …  
    1622                        } );
    1723                } );
     24
     25                var deptSearchInput = document.getElementById( 'ldap-dept-search-input' );
     26                if ( ! deptSearchInput ) {
     27                        return;
     28                }
     29
     30                var wrap = deptSearchInput.closest( '.ldap-directory-wrap' );
     31                if ( ! wrap ) {
     32                        return;
     33                }
     34
     35                var deptRows      = Array.prototype.slice.call( wrap.querySelectorAll( '.ldap-dept-row' ) );
     36                var deptNoResults = wrap.querySelector( '.ldap-dept-no-results' );
     37
     38                deptSearchInput.addEventListener( 'input', function () {
     39                        var query   = deptSearchInput.value.trim().toLowerCase();
     40                        var visible = 0;
     41
     42                        deptRows.forEach( function ( row ) {
     43                                var matches = '' === query || row.dataset.name.toLowerCase().indexOf( query ) !== -1;
     44                                row.hidden  = ! matches;
     45                                if ( matches ) {
     46                                        visible++;
     47                                }
     48                        } );
     49
     50                        if ( deptNoResults ) {
     51                                deptNoResults.hidden = visible > 0;
     52                        }
     53                } );
    1854        } );
    1955} () );
  • ldap-staff-directory/tags/1.4.0/public/views/directory.php

    r3606783 r3692384  
    11<?php
    22/**
    3  * Public directory template.
     3 * Department detail template — rendered when a department is selected
     4 * (ldap_dept present). Shows a breadcrumb back to the department menu, the
     5 * active department's header, and the existing filtered/paginated employee
     6 * grid.
    47 *
    58 * Variables available from class-shortcode.php:
    … …  
    710 *   $ldap_ed_fields        — array of field keys to display
    811 *   $ldap_ed_per_page      — int, items per page
    9  *   $ldap_ed_enable_search — bool, show search input and filter bar
    10  *   $ldap_ed_departments   — array [ 'Department' => count ] sorted alphabetically
    11  *   $ldap_ed_current_dept  — string, active department filter ('' = none)
     12 *   $ldap_ed_enable_search — bool, show the employee search input
     13 *   $ldap_ed_current_dept  — string, active department (always non-empty here)
    1214 *   $ldap_ed_search_query  — string, active search term
    1315 *   $ldap_ed_current_page  — int, current page number
    1416 *   $ldap_ed_total_pages   — int, total page count
    15  *   $ldap_ed_total_count   — int, total filtered employee count
    16  *   $ldap_ed_all_count     — int, total unfiltered employee count
     17 *   $ldap_ed_total_count   — int, total filtered employee count (this department)
    1718 *   $ldap_ed_prev_url      — string|null, URL for previous page (null = disabled)
    1819 *   $ldap_ed_next_url      — string|null, URL for next page (null = disabled)
    … …  
    2526}
    2627
    27 // Palette for avatar backgrounds — one color is deterministically assigned per employee.
    28 $ldap_avatar_palette = array(
    29         '#4f7df3', '#7c5cbf', '#0e9b8a',
    30         '#2e9e4f', '#c0392b', '#d35400',
    31         '#1a7bbf', '#8e44ad',
    32 );
     28$ldap_ed_dept_avatar = $this->get_initials_and_color( $ldap_ed_current_dept, true );
    3329?>
    3430<div class="ldap-directory-wrap">
     31
     32        <a
     33                href="<?php echo esc_url( remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ); ?>"
     34                class="ldap-breadcrumb"
     35        >
     36                <?php echo $this->get_icon_svg( 'arrow-left' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?>
     37                <?php esc_html_e( 'Departments', 'ldap-staff-directory' ); ?>
     38        </a>
     39
     40        <div class="ldap-dept-header">
     41                <div class="ldap-dept-header-info">
     42                        <h2 class="ldap-dept-header-name"><?php echo esc_html( $ldap_ed_current_dept ); ?></h2>
     43                        <p class="ldap-dept-header-sub">
     44                        <?php
     45                        echo esc_html(
     46                                sprintf(
     47                                        /* translators: %s: number of employees in this department */
     48                                        _n( '%s employee in this department', '%s employees in this department', $ldap_ed_total_count, 'ldap-staff-directory' ),
     49                                        absint( $ldap_ed_total_count )
     50                                )
     51                        );
     52                        ?>
     53                        </p>
     54                </div>
     55                <span
     56                        class="ldap-dept-header-badge"
     57                        aria-hidden="true"
     58                        style="--ldap-avatar-bg:<?php echo esc_attr( $ldap_ed_dept_avatar['color'] ); ?>"
     59                ><?php echo esc_html( $ldap_ed_dept_avatar['initials'] ); ?></span>
     60        </div>
    3561
    3662        <?php if ( $ldap_ed_enable_search ) : ?>
    … …  
    4571                                <?php esc_html_e( 'Search employees', 'ldap-staff-directory' ); ?>
    4672                        </label>
    47                         <?php if ( '' !== $ldap_ed_current_dept ) : ?>
    4873                        <input type="hidden" name="ldap_dept" value="<?php echo esc_attr( $ldap_ed_current_dept ); ?>">
    49                         <?php endif; ?>
    5074                        <input
    5175                                type="search"
    … …  
    5377                                name="ldap_search"
    5478                                class="ldap-search"
    55                                 placeholder="<?php esc_attr_e( 'Search employee…', 'ldap-staff-directory' ); ?>"
     79                                placeholder="<?php
     80                                echo esc_attr(
     81                                        sprintf(
     82                                                /* translators: %s: department name */
     83                                                __( 'Search in %s…', 'ldap-staff-directory' ),
     84                                                $ldap_ed_current_dept
     85                                        )
     86                                );
     87                                ?>"
    5688                                aria-label="<?php esc_attr_e( 'Search employee', 'ldap-staff-directory' ); ?>"
    5789                                value="<?php echo esc_attr( $ldap_ed_search_query ); ?>"
    … …  
    5991                </form>
    6092        </div>
    61 
    62         <?php if ( count( $ldap_ed_departments ) >= 2 ) : ?>
    63         <div class="ldap-dept-filters" role="list" aria-label="<?php esc_attr_e( 'Filter by department', 'ldap-staff-directory' ); ?>">
    64 
    65                 <?php // "All" chip — active when no department filter is set. ?>
    66                 <?php $ldap_ed_all_url = esc_url( add_query_arg( 'ldap_page', 1, remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ) ); ?>
    67                 <?php if ( '' === $ldap_ed_current_dept ) : ?>
    68                 <span class="ldap-dept-chip is-active" role="listitem" aria-pressed="true">
    69                         <?php
    70                         echo esc_html(
    71                                 sprintf(
    72                                         /* translators: %s: total employee count */
    73                                         __( 'All (%s)', 'ldap-staff-directory' ),
    74                                         absint( $ldap_ed_all_count )
    75                                 )
    76                         );
    77                         ?>
    78                 </span>
    79                 <?php else : ?>
    80                 <a
    81                         href="<?php echo $ldap_ed_all_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"
    82                         class="ldap-dept-chip"
    83                         role="listitem"
    84                         aria-pressed="false"
    85                 >
    86                         <?php
    87                         echo esc_html(
    88                                 sprintf(
    89                                         /* translators: %s: total employee count */
    90                                         __( 'All (%s)', 'ldap-staff-directory' ),
    91                                         absint( $ldap_ed_all_count )
    92                                 )
    93                         );
    94                         ?>
    95                 </a>
    96                 <?php endif; ?>
    97 
    98                 <?php foreach ( $ldap_ed_departments as $ldap_ed_dept_name => $ldap_ed_dept_count ) : ?>
    99                         <?php
    100                         $ldap_ed_is_active  = ( 0 === strcasecmp( $ldap_ed_dept_name, $ldap_ed_current_dept ) );
    101                         $ldap_ed_chip_url   = esc_url( add_query_arg( array( 'ldap_dept' => $ldap_ed_dept_name, 'ldap_page' => 1 ) ) );
    102                         $ldap_ed_clear_url  = esc_url( add_query_arg( 'ldap_page', 1, remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ) );
    103                         $ldap_ed_chip_label = esc_html( $ldap_ed_dept_name ) . ' (' . absint( $ldap_ed_dept_count ) . ')';
    104                         ?>
    105                         <?php if ( $ldap_ed_is_active ) : ?>
    106                         <span class="ldap-dept-chip is-active" role="listitem" aria-pressed="true">
    107                                 <?php echo $ldap_ed_chip_label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_html() + absint() applied above ?>
    108                                 <a
    109                                         href="<?php echo $ldap_ed_clear_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"
    110                                         class="ldap-dept-chip-clear"
    111                                         aria-label="<?php esc_attr_e( 'Clear department filter', 'ldap-staff-directory' ); ?>"
    112                                 >×</a>
    113                         </span>
    114                         <?php else : ?>
    115                         <a
    116                                 href="<?php echo $ldap_ed_chip_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"
    117                                 class="ldap-dept-chip"
    118                                 role="listitem"
    119                                 aria-pressed="false"
    120                         >
    121                                 <?php echo $ldap_ed_chip_label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_html() + absint() applied above ?>
    122                         </a>
    123                         <?php endif; ?>
    124                 <?php endforeach; ?>
    125 
    126         </div>
    127         <?php endif; ?>
    12893        <?php endif; ?>
    12994
    … …  
    13499                <?php else : ?>
    135100                        <?php foreach ( $ldap_ed_users as $ldap_ed_user ) :
    136                                 // Compute initials (up to 2 characters) from the display name.
    137                                 $ldap_name     = $ldap_ed_user['name'] ?? '';
    138                                 $ldap_parts    = preg_split( '/\s+/', trim( $ldap_name ), 2 );
    139                                 $ldap_initials = strtoupper( substr( $ldap_parts[0] ?? '', 0, 1 ) );
    140                                 if ( ! empty( $ldap_parts[1] ) ) {
    141                                         $ldap_initials .= strtoupper( substr( $ldap_parts[1], 0, 1 ) );
    142                                 }
     101                                $ldap_ed_name   = $ldap_ed_user['name'] ?? '';
     102                                $ldap_ed_avatar = $this->get_initials_and_color( $ldap_ed_name );
     103                                $ldap_initials  = $ldap_ed_avatar['initials'];
     104
    143105                                // Fall back to first letter of email when name is absent.
    144106                                if ( '' === $ldap_initials && ! empty( $ldap_ed_user['email'] ) ) {
    … …  
    146108                                }
    147109
    148                                 // Deterministic background color derived from the name.
    149                                 $ldap_color_idx = abs( crc32( $ldap_name ) ) % count( $ldap_avatar_palette );
    150                                 $ldap_avatar_bg = $ldap_avatar_palette[ $ldap_color_idx ];
     110                                $ldap_avatar_bg = $ldap_ed_avatar['color'];
    151111                        ?>
    152112                        <article class="ldap-staff-card">
    … …  
    162122
    163123                                <?php if ( in_array( 'title', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['title'] ) ) : ?>
    164                                 <p class="ldap-title"><?php echo esc_html( $ldap_ed_user['title'] ); ?></p>
     124                                <p class="ldap-title">
     125                                        <?php echo $this->get_icon_svg( 'briefcase' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?>
     126                                        <?php echo esc_html( $ldap_ed_user['title'] ); ?>
     127                                </p>
    165128                                <?php endif; ?>
    166129
    … …  
    173136                                <?php if ( in_array( 'email', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['email'] ) ) : ?>
    174137                                <a class="ldap-email" href="mailto:<?php echo esc_attr( $ldap_ed_user['email'] ); ?>">
     138                                        <?php echo $this->get_icon_svg( 'mail' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?>
    175139                                        <?php echo esc_html( $ldap_ed_user['email'] ); ?>
    176140                                </a>
    … …  
    179143                                <?php if ( in_array( 'phone', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['phone'] ) ) : ?>
    180144                                <a class="ldap-phone" href="tel:<?php echo esc_attr( $ldap_ed_user['phone'] ); ?>">
     145                                        <?php echo $this->get_icon_svg( 'phone' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?>
    181146                                        <?php echo esc_html( $ldap_ed_user['phone'] ); ?>
    182147                                </a>
    … …  
    184149
    185150                                <?php if ( in_array( 'extension', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['extension'] ) ) : ?>
    186                                 <span class="ldap-extension"><?php echo esc_html( $ldap_ed_user['extension'] ); ?></span>
     151                                <span class="ldap-extension">
     152                                        <?php echo $this->get_icon_svg( 'building' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?>
     153                                        <?php echo esc_html( $ldap_ed_user['extension'] ); ?>
     154                                </span>
    187155                                <?php endif; ?>
    188156                        </article>
    … …  
    209177                        $ldap_ed_from = absint( ( $ldap_ed_current_page - 1 ) * $ldap_ed_per_page + 1 );
    210178                        $ldap_ed_to   = absint( min( $ldap_ed_current_page * $ldap_ed_per_page, $ldap_ed_total_count ) );
    211                         if ( '' !== $ldap_ed_current_dept ) :
    212                                 echo esc_html(
    213                                         sprintf(
    214                                                 /* translators: 1: first record number on page, 2: last record number on page, 3: total filtered records, 4: department name */
    215                                                 __( 'Showing %1$s–%2$s of %3$s in %4$s', 'ldap-staff-directory' ),
    216                                                 absint( $ldap_ed_from ),
    217                                                 absint( $ldap_ed_to ),
    218                                                 absint( $ldap_ed_total_count ),
    219                                                 $ldap_ed_current_dept
    220                                         )
    221                                 );
    222                         else :
    223                                 echo esc_html(
    224                                         sprintf(
    225                                                 /* translators: 1: first record number on page, 2: last record number on page, 3: total records */
    226                                                 __( 'Showing %1$s–%2$s of %3$s', 'ldap-staff-directory' ),
    227                                                 absint( $ldap_ed_from ),
    228                                                 absint( $ldap_ed_to ),
    229                                                 absint( $ldap_ed_total_count )
    230                                         )
    231                                 );
    232                         endif;
     179                        echo esc_html(
     180                                sprintf(
     181                                        /* translators: 1: first record number on page, 2: last record number on page, 3: total filtered records, 4: department name */
     182                                        __( 'Showing %1$s–%2$s of %3$s in %4$s', 'ldap-staff-directory' ),
     183                                        absint( $ldap_ed_from ),
     184                                        absint( $ldap_ed_to ),
     185                                        absint( $ldap_ed_total_count ),
     186                                        $ldap_ed_current_dept
     187                                )
     188                        );
    233189                endif; ?>
    234190                </span>
  • ldap-staff-directory/tags/1.4.0/readme.txt

    r3634465 r3692384  
    33Tags:              ldap, directory, wpbeaverbuilder, staff, elementor
    44Requires at least: 5.8
    5 Tested up to:      7.0
     5Tested up to:      7.1
    66Requires PHP:      7.4
    7 Stable tag:        1.2.1
     7Stable tag:        1.4.0
    88License:           GPLv2 or later
    99License URI:       https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2323* Native Elementor widget with full style controls
    2424* Native Beaver Builder module with General and Style tabs
    25 * Department filter bar with configurable chip order (alphabetical or by contact count)
     25* Public directory browses department-first: a menu of departments (with employee counts) leads to each department's contact list, in configurable order (alphabetical or by contact count)
     26* Employee cards within a department can be ordered by name or job title, ascending or descending, from the admin — no code changes needed
    2627* Exclude specific departments — or employees with no department assigned — from the public directory, enforced at the LDAP query level so those employees are never fetched
    2728* Exclude disabled Active Directory accounts
    … …  
    7778By default 60 minutes. Change the TTL under **Settings → LDAP Directory → Fields → Advanced settings**, or flush immediately with the **Clear Cache** button on that same tab.
    7879
     80= Why do I now see a list of departments instead of all employees? =
     81
     82Starting in 1.3.0, the public directory browses department-first: visitors see a menu of departments with employee counts, and click into one to see its contacts. This replaces the previous single-page grid with a department filter bar. There is no setting to restore the old flat grid.
     83
    7984== Screenshots ==
    8085
    … …  
    8590
    8691== Changelog ==
     92
     93= 1.4.0 =
     94* Feature: New **Employee order** setting under **Settings → LDAP Directory → Fields** lets you choose how employee cards are ordered within a department: by name or job title, ascending or descending. Ties on job title (including employees with no title) are always broken by name (A–Z).
     95
     96= 1.3.0 =
     97* Feature: The public directory now browses department-first. The root view shows a menu of departments (name, employee count, color-coded avatar) instead of the full employee grid; clicking a department opens its filtered, paginated contact list with a breadcrumb back to the menu.
     98* Feature: Employee cards in the department view show a small icon next to job title, email, phone, and extension for easier scanning.
     99* Breaking change: The department filter chip bar has been removed and replaced by the department menu described above. There is no setting to restore the previous single-page grid.
     100* Chore: "Tested up to" updated to WordPress 7.1.
    87101
    88102= 1.2.1 =
  • ldap-staff-directory/trunk/admin/views/settings-page.php

    r3634465 r3692384  
    145145                                printf( '</div>' );
    146146                                $this->row( __( 'Department order', 'ldap-staff-directory' ), 'ldap_ed_department_order', array( $this, 'render_field_department_order' ) );
     147                                $this->row( __( 'Employee order', 'ldap-staff-directory' ), 'ldap_ed_employee_order', array( $this, 'render_field_employee_order' ) );
    147148                                ?>
    148149                        </div>
  • ldap-staff-directory/trunk/includes/class-admin.php

    r3634465 r3692384  
    147147                                ? $raw_department_order
    148148                                : 'alpha';
     149
     150                        $allowed_employee_orders = array( 'name_asc', 'name_desc', 'title_asc', 'title_desc' );
     151                        $raw_employee_order      = sanitize_text_field( $input['employee_order'] ?? 'name_asc' );
     152                        $clean['employee_order'] = in_array( $raw_employee_order, $allowed_employee_orders, true )
     153                                ? $raw_employee_order
     154                                : 'name_asc';
    149155
    150156                        $clean['cache_ttl'] = absint( $input['cache_ttl'] ?? 60 );
    … …  
    588594        }
    589595
     596        public function render_field_employee_order() {
     597                $saved   = $this->get_option( 'employee_order', 'name_asc' );
     598                $options = array(
     599                        'name_asc'   => __( 'Name (A–Z)', 'ldap-staff-directory' ),
     600                        'name_desc'  => __( 'Name (Z–A)', 'ldap-staff-directory' ),
     601                        'title_asc'  => __( 'Title (A–Z)', 'ldap-staff-directory' ),
     602                        'title_desc' => __( 'Title (Z–A)', 'ldap-staff-directory' ),
     603                );
     604
     605                printf(
     606                        '<select id="ldap_ed_employee_order" name="%1$s[employee_order]">',
     607                        esc_attr( LDAP_ED_OPTION_KEY )
     608                );
     609                foreach ( $options as $ldap_ed_order_value => $ldap_ed_order_label ) {
     610                        printf(
     611                                '<option value="%1$s" %2$s>%3$s</option>',
     612                                esc_attr( $ldap_ed_order_value ),
     613                                selected( $saved, $ldap_ed_order_value, false ),
     614                                esc_html( $ldap_ed_order_label )
     615                        );
     616                }
     617                printf(
     618                        '</select><p class="description">%s</p>',
     619                        esc_html__( 'Order of the employee cards within a department on the public directory. Ties on title are broken by name (A–Z).', 'ldap-staff-directory' )
     620                );
     621        }
     622
    590623        public function render_field_enable_search() {
    591624                printf(
  • ldap-staff-directory/trunk/includes/class-ldap-connector.php

    r3633538 r3692384  
    239239         * Search the LDAP directory and return an array of user data.
    240240         *
     241         * The returned array's order is NOT guaranteed — it reflects whatever order
     242         * the LDAP server returned entries in. Callers that need a specific display
     243         * order (e.g. the shortcode's employee_order setting) must sort explicitly.
     244         *
    241245         * @return array|\WP_Error
    242246         */
    … …  
    277281                }
    278282
    279                 // Sort alphabetically by name.
    280                 usort( $users, function ( $a, $b ) {
    281                         return strcmp( $a['name'], $b['name'] );
    282                 } );
    283 
    284283                $this->disconnect();
    285284                return $users;
  • ldap-staff-directory/trunk/includes/class-shortcode.php

    r3633538 r3692384  
    1212class LDAP_ED_Shortcode {
    1313
     14        /**
     15         * Deterministic avatar background colors, shared between employee and
     16         * department avatars. Color is picked via crc32( $name ) % count( palette ).
     17         */
     18        private const AVATAR_PALETTE = array(
     19                '#4f7df3', '#7c5cbf', '#0e9b8a',
     20                '#2e9e4f', '#c0392b', '#d35400',
     21                '#1a7bbf', '#8e44ad',
     22        );
     23
     24        /**
     25         * Inline SVG icon paths (Lucide), keyed by icon name. Static, trusted
     26         * markup — never built from user input.
     27         */
     28        private const ICONS = array(
     29                'briefcase'     => '<path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/><rect width="20" height="14" x="2" y="6" rx="2"/>',
     30                'mail'          => '<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>',
     31                'phone'         => '<path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/>',
     32                'building'      => '<rect width="16" height="20" x="4" y="2" rx="2"/><path d="M9 22v-4h6v4"/><path d="M8 6h.01"/><path d="M16 6h.01"/><path d="M12 6h.01"/><path d="M12 10h.01"/><path d="M12 14h.01"/><path d="M16 10h.01"/><path d="M16 14h.01"/><path d="M8 10h.01"/><path d="M8 14h.01"/>',
     33                'chevron-right' => '<path d="m9 18 6-6-6-6"/>',
     34                'arrow-left'    => '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>',
     35        );
     36
    1437        public function __construct() {
    1538                add_shortcode( 'ldap_directory', array( $this, 'render' ) );
    … …  
    5679        /**
    5780         * Render the directory HTML.
     81         *
     82         * Renders one of two views depending on whether a department is selected:
     83         * - No department (ldap_dept empty): department menu (department-menu.php).
     84         * - Department present: department detail — filtered/paginated employee
     85         *   grid (directory.php), unchanged from the pre-existing behavior.
    5886         *
    5987         * @param array $atts Shortcode attributes.
    … …  
    99127                }
    100128
    101                 // Extract department list from the full (unfiltered) set for correct chip counts.
     129                // Extract department list (with counts) from the full, unfiltered set.
    102130                $ldap_ed_department_order = $settings['department_order'] ?? 'alpha';
    103131                $ldap_ed_departments      = $this->extract_departments( $all_users, $ldap_ed_department_order );
    104132                $ldap_ed_all_count        = count( $all_users );
    105133
     134                // No department selected: render the department menu. No filtering or
     135                // pagination runs for this state — it's a flat list of all departments.
     136                if ( '' === $ldap_ed_current_dept ) {
     137                        ob_start();
     138                        include LDAP_ED_DIR . 'public/views/department-menu.php';
     139                        return ob_get_clean();
     140                }
     141
    106142                // Apply department and search filters in PHP.
    107143                $filtered_users = $this->filter_users( $all_users, $ldap_ed_search_query, $ldap_ed_current_dept );
     144
     145                // Sort the filtered result before paginating, so order is consistent across pages.
     146                $ldap_ed_employee_order = $settings['employee_order'] ?? 'name_asc';
     147                $filtered_users         = $this->sort_users( $filtered_users, $ldap_ed_employee_order );
    108148
    109149                // Paginate the filtered result.
    … …  
    121161                include LDAP_ED_DIR . 'public/views/directory.php';
    122162                return ob_get_clean();
     163        }
     164
     165        // -------------------------------------------------------------------------
     166        // Shared display helpers (avatars, icons) — used by both views' templates.
     167
     168        /**
     169         * Computes deterministic initials and an avatar background color for a
     170         * name (employee or department). Same color formula for both: crc32( $name )
     171         * modulo the shared 8-color palette.
     172         *
     173         * @param string $name            Employee or department name.
     174         * @param bool   $force_two_chars When true, a single-word name still yields
     175         *                                2-letter initials (used for department
     176         *                                abbreviations). Employee avatars keep the
     177         *                                pre-existing 1-letter behavior by default.
     178         * @return array{initials:string,color:string}
     179         */
     180        public function get_initials_and_color( string $name, bool $force_two_chars = false ): array {
     181                $parts    = preg_split( '/\s+/', trim( $name ), 2 );
     182                $initials = strtoupper( substr( $parts[0] ?? '', 0, 1 ) );
     183
     184                if ( ! empty( $parts[1] ) ) {
     185                        $initials .= strtoupper( substr( $parts[1], 0, 1 ) );
     186                } elseif ( $force_two_chars ) {
     187                        $initials .= strtoupper( substr( $parts[0] ?? '', 1, 1 ) );
     188                }
     189
     190                $color = self::AVATAR_PALETTE[ abs( crc32( $name ) ) % count( self::AVATAR_PALETTE ) ];
     191
     192                return array(
     193                        'initials' => $initials,
     194                        'color'    => $color,
     195                );
     196        }
     197
     198        /**
     199         * Returns inline SVG markup for a static, trusted icon (Lucide-style).
     200         * Never built from user input — safe to echo without escaping.
     201         *
     202         * @param string $name Icon key, see self::ICONS.
     203         * @return string SVG markup, or '' when the icon name is unknown.
     204         */
     205        public function get_icon_svg( string $name ): string {
     206                if ( ! isset( self::ICONS[ $name ] ) ) {
     207                        return '';
     208                }
     209
     210                return sprintf(
     211                        '<svg class="ldap-icon ldap-icon-%1$s" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">%2$s</svg>',
     212                        esc_attr( $name ),
     213                        self::ICONS[ $name ]
     214                );
    123215        }
    124216
    … …  
    207299
    208300        /**
     301         * Sort a user array for display, per the employee_order setting.
     302         *
     303         * Ties on title (including empty title) are always broken by name ascending,
     304         * regardless of the primary order's direction — this is intentional, not a bug.
     305         * Falls back to 'name_asc' for any value outside the known whitelist.
     306         *
     307         * @param array  $users Filtered user array.
     308         * @param string $order 'name_asc' (default), 'name_desc', 'title_asc', or 'title_desc'.
     309         * @return array Sorted array (re-indexed).
     310         */
     311        private function sort_users( array $users, string $order = 'name_asc' ): array {
     312                $allowed_orders = array( 'name_asc', 'name_desc', 'title_asc', 'title_desc' );
     313                if ( ! in_array( $order, $allowed_orders, true ) ) {
     314                        $order = 'name_asc';
     315                }
     316
     317                usort(
     318                        $users,
     319                        static function ( $a, $b ) use ( $order ) {
     320                                if ( 'name_desc' === $order ) {
     321                                        return strcmp( $b['name'] ?? '', $a['name'] ?? '' );
     322                                }
     323
     324                                if ( 'title_asc' === $order || 'title_desc' === $order ) {
     325                                        $result = strcmp( $a['title'] ?? '', $b['title'] ?? '' );
     326                                        if ( 'title_desc' === $order ) {
     327                                                $result = -$result;
     328                                        }
     329                                        // Tie on title: always break by name ascending, regardless of direction.
     330                                        return 0 !== $result ? $result : strcmp( $a['name'] ?? '', $b['name'] ?? '' );
     331                                }
     332
     333                                // name_asc (default).
     334                                return strcmp( $a['name'] ?? '', $b['name'] ?? '' );
     335                        }
     336                );
     337
     338                return array_values( $users );
     339        }
     340
     341        /**
    209342         * Slice a filtered user array for the requested page.
    210343         *
  • ldap-staff-directory/trunk/ldap-staff-directory.php

    r3634465 r3692384  
    44 * Plugin URI:  https://wordpress.org/plugins/ldap-staff-directory/
    55 * Description: Connects to LDAP or LDAPS to display an employee directory from an OU. Supports Elementor, Beaver Builder and a native shortcode.
    6  * Version:     1.2.1
     6 * Version:     1.4.0
    77 * Requires at least: 5.8
    88 * Requires PHP: 7.4
    … …  
    1919
    2020// Plugin constants.
    21 define( 'LDAP_ED_VERSION',     '1.2.1' );
     21define( 'LDAP_ED_VERSION',     '1.4.0' );
    2222define( 'LDAP_ED_FILE',        __FILE__ );
    2323define( 'LDAP_ED_DIR',         plugin_dir_path( __FILE__ ) );
  • ldap-staff-directory/trunk/public/css/directory.css

    r3606783 r3692384  
    2525        --ldap-dept-badge-bg:    rgba( 0, 115, 170, .08 );
    2626        --ldap-dept-badge-color: #005a87;
     27
     28        /* Inline field icons */
     29        --ldap-icon-color:       #8c8f94;
    2730}
    2831
    … …  
    3336        font-size: var( --ldap-font-size );
    3437        color: var( --ldap-text-color );
    35 }
    36 
    37 /* ── Department filter bar ──────────────────────────────────────────────── */
    38 
    39 .ldap-dept-filters {
    40         display: flex;
    41         flex-wrap: nowrap;
    42         gap: 8px;
    43         overflow-x: auto;
    44         white-space: nowrap;
    45         padding-bottom: 4px;
    46         margin-bottom: 24px;
    47         /* Hide scrollbar visually while keeping it functional */
    48         -ms-overflow-style: none;
    49         scrollbar-width: none;
    50 }
    51 
    52 .ldap-dept-filters::-webkit-scrollbar {
    53         display: none;
    54 }
    55 
    56 .ldap-dept-chip {
    57         display: inline-flex;
    58         align-items: center;
    59         gap: 5px;
    60         padding: 5px 14px;
    61         font-size: .82em;
    62         font-family: inherit;
    63         font-weight: 500;
    64         color: var( --ldap-text-color );
    65         background: transparent;
    66         border: 1px solid var( --ldap-card-border );
    67         border-radius: 20px;
    68         text-decoration: none;
    69         cursor: pointer;
    70         white-space: nowrap;
    71         transition: border-color .15s ease, background-color .15s ease, color .15s ease;
    72         flex-shrink: 0;
    73 }
    74 
    75 a.ldap-dept-chip:hover {
    76         border-color: var( --ldap-primary-color );
    77         color: var( --ldap-primary-color );
    78 }
    79 
    80 .ldap-dept-chip.is-active {
    81         background: var( --ldap-primary-color );
    82         border-color: var( --ldap-primary-color );
    83         color: #fff;
    84         cursor: default;
    85 }
    86 
    87 .ldap-dept-chip-clear {
    88         display: inline-flex;
    89         align-items: center;
    90         justify-content: center;
    91         margin-left: 2px;
    92         font-size: 1.1em;
    93         font-weight: 700;
    94         color: inherit;
    95         text-decoration: none;
    96         opacity: .75;
    97         cursor: pointer;
    98         transition: opacity .15s ease;
    99 }
    100 
    101 .ldap-dept-chip-clear:hover {
    102         opacity: 1;
    10338}
    10439
    … …  
    236171}
    237172
     173.ldap-title,
     174.ldap-email,
     175.ldap-phone,
     176.ldap-extension {
     177        display: flex;
     178        align-items: center;
     179        gap: 6px;
     180}
     181
    238182.ldap-email {
    239183        margin-top: 10px;
    … …  
    265209
    266210.ldap-extension {
    267         display: block;
    268211        margin-top: 4px;
    269212        font-size: .88em;
    270213        color: var( --ldap-muted-color );
     214}
     215
     216/* ── Inline field icons (employee card, breadcrumb, department rows) ───── */
     217
     218.ldap-icon {
     219        width: 12px;
     220        height: 12px;
     221        flex-shrink: 0;
     222        color: var( --ldap-icon-color );
    271223}
    272224
    … …  
    341293        font-variant-numeric: tabular-nums;
    342294}
     295
     296/* ── Department menu (root view) ───────────────────────────────────────── */
     297
     298.ldap-menu-header {
     299        margin-bottom: 20px;
     300}
     301
     302.ldap-menu-title {
     303        margin: 0 0 4px;
     304        font-size: 1.4em;
     305        font-weight: 700;
     306        color: var( --ldap-text-color );
     307}
     308
     309.ldap-menu-sub {
     310        margin: 0;
     311        font-size: .85em;
     312        color: var( --ldap-muted-color );
     313}
     314
     315.ldap-dept-search-wrap {
     316        max-width: 320px;
     317}
     318
     319.ldap-dept-search {
     320        max-width: none;
     321}
     322
     323.ldap-menu-eyebrow {
     324        margin: 0 0 12px;
     325        font-size: .72em;
     326        font-weight: 700;
     327        letter-spacing: .08em;
     328        text-transform: uppercase;
     329        color: var( --ldap-muted-color );
     330}
     331
     332/* Two columns on desktop (matches the prototype); pure CSS, no JS balancing. */
     333.ldap-dept-menu-list {
     334        columns: 2;
     335        column-gap: 10px;
     336}
     337
     338.ldap-dept-row {
     339        display: flex;
     340        align-items: center;
     341        gap: 10px;
     342        break-inside: avoid;
     343        margin-bottom: 8px;
     344        padding: 10px 14px;
     345        background: var( --ldap-card-bg );
     346        border: 1px solid var( --ldap-card-border );
     347        border-radius: var( --ldap-card-radius );
     348        color: inherit;
     349        text-decoration: none;
     350        transition: box-shadow .15s ease, border-color .15s ease;
     351}
     352
     353.ldap-dept-row:hover {
     354        box-shadow: 0 4px 14px rgba( 0, 0, 0, .1 );
     355        border-color: #c3c4c7;
     356}
     357
     358.ldap-dept-row[hidden] {
     359        display: none;
     360}
     361
     362.ldap-dept-row-avatar {
     363        flex-shrink: 0;
     364        width: 36px;
     365        height: 36px;
     366        border-radius: 50%;
     367        background-color: var( --ldap-avatar-bg );
     368        color: #fff;
     369        display: flex;
     370        align-items: center;
     371        justify-content: center;
     372        font-size: 12px;
     373        font-weight: 700;
     374        user-select: none;
     375}
     376
     377.ldap-dept-row-info {
     378        flex: 1;
     379        min-width: 0;
     380        display: flex;
     381        flex-direction: column;
     382        gap: 2px;
     383}
     384
     385.ldap-dept-row-name {
     386        font-size: .93em;
     387        font-weight: 600;
     388        color: var( --ldap-text-color );
     389}
     390
     391.ldap-dept-row-count {
     392        font-size: .8em;
     393        color: var( --ldap-muted-color );
     394}
     395
     396.ldap-dept-row .ldap-icon {
     397        width: 14px;
     398        height: 14px;
     399        color: var( --ldap-search-border );
     400}
     401
     402@media ( max-width: 540px ) {
     403        .ldap-dept-menu-list {
     404                columns: 1;
     405        }
     406}
     407
     408/* ── Breadcrumb + department header (detail view) ──────────────────────── */
     409
     410.ldap-breadcrumb {
     411        display: inline-flex;
     412        align-items: center;
     413        gap: 6px;
     414        margin-bottom: 16px;
     415        font-size: .93em;
     416        font-weight: 500;
     417        color: var( --ldap-primary-color );
     418        text-decoration: none;
     419}
     420
     421.ldap-breadcrumb:hover {
     422        text-decoration: underline;
     423}
     424
     425.ldap-breadcrumb .ldap-icon {
     426        width: 13px;
     427        height: 13px;
     428        color: var( --ldap-primary-color );
     429}
     430
     431.ldap-dept-header {
     432        display: flex;
     433        align-items: center;
     434        justify-content: space-between;
     435        gap: 16px;
     436        margin-bottom: 20px;
     437}
     438
     439.ldap-dept-header-name {
     440        margin: 0 0 4px;
     441        font-size: 1.25em;
     442        font-weight: 700;
     443        color: var( --ldap-text-color );
     444}
     445
     446.ldap-dept-header-sub {
     447        margin: 0;
     448        font-size: .82em;
     449        color: var( --ldap-muted-color );
     450}
     451
     452.ldap-dept-header-badge {
     453        flex-shrink: 0;
     454        width: 44px;
     455        height: 44px;
     456        border-radius: 50%;
     457        background-color: var( --ldap-avatar-bg );
     458        color: #fff;
     459        display: flex;
     460        align-items: center;
     461        justify-content: center;
     462        font-size: 14px;
     463        font-weight: 700;
     464        user-select: none;
     465}
     466
     467@media ( max-width: 540px ) {
     468        .ldap-dept-header {
     469                flex-direction: column;
     470                align-items: flex-start;
     471        }
     472
     473        .ldap-dept-header-badge {
     474                width: 36px;
     475                height: 36px;
     476                font-size: 12px;
     477        }
     478}
  • ldap-staff-directory/trunk/public/js/directory.js

    r3548101 r3692384  
    11/**
    22 * LDAP Staff Directory — public JS
    3  * Filtering and pagination are now server-side. This file strips empty
    4  * ldap_search from the form submission to keep URLs clean.
     3 *
     4 * Two independent, small pieces of behavior:
     5 * 1. Employee search (department detail view) is server-side — this only
     6 *    strips an empty ldap_search from the form submission to keep URLs clean.
     7 * 2. Department search (department menu view) is client-side — it filters
     8 *    the already-rendered .ldap-dept-row elements by substring match, with
     9 *    no network request. Scoped to at most the total number of departments;
     10 *    it never touches employee cards or pagination.
    511 */
    612( function () {
    … …  
    1622                        } );
    1723                } );
     24
     25                var deptSearchInput = document.getElementById( 'ldap-dept-search-input' );
     26                if ( ! deptSearchInput ) {
     27                        return;
     28                }
     29
     30                var wrap = deptSearchInput.closest( '.ldap-directory-wrap' );
     31                if ( ! wrap ) {
     32                        return;
     33                }
     34
     35                var deptRows      = Array.prototype.slice.call( wrap.querySelectorAll( '.ldap-dept-row' ) );
     36                var deptNoResults = wrap.querySelector( '.ldap-dept-no-results' );
     37
     38                deptSearchInput.addEventListener( 'input', function () {
     39                        var query   = deptSearchInput.value.trim().toLowerCase();
     40                        var visible = 0;
     41
     42                        deptRows.forEach( function ( row ) {
     43                                var matches = '' === query || row.dataset.name.toLowerCase().indexOf( query ) !== -1;
     44                                row.hidden  = ! matches;
     45                                if ( matches ) {
     46                                        visible++;
     47                                }
     48                        } );
     49
     50                        if ( deptNoResults ) {
     51                                deptNoResults.hidden = visible > 0;
     52                        }
     53                } );
    1854        } );
    1955} () );
  • ldap-staff-directory/trunk/public/views/directory.php

    r3606783 r3692384  
    11<?php
    22/**
    3  * Public directory template.
     3 * Department detail template — rendered when a department is selected
     4 * (ldap_dept present). Shows a breadcrumb back to the department menu, the
     5 * active department's header, and the existing filtered/paginated employee
     6 * grid.
    47 *
    58 * Variables available from class-shortcode.php:
    … …  
    710 *   $ldap_ed_fields        — array of field keys to display
    811 *   $ldap_ed_per_page      — int, items per page
    9  *   $ldap_ed_enable_search — bool, show search input and filter bar
    10  *   $ldap_ed_departments   — array [ 'Department' => count ] sorted alphabetically
    11  *   $ldap_ed_current_dept  — string, active department filter ('' = none)
     12 *   $ldap_ed_enable_search — bool, show the employee search input
     13 *   $ldap_ed_current_dept  — string, active department (always non-empty here)
    1214 *   $ldap_ed_search_query  — string, active search term
    1315 *   $ldap_ed_current_page  — int, current page number
    1416 *   $ldap_ed_total_pages   — int, total page count
    15  *   $ldap_ed_total_count   — int, total filtered employee count
    16  *   $ldap_ed_all_count     — int, total unfiltered employee count
     17 *   $ldap_ed_total_count   — int, total filtered employee count (this department)
    1718 *   $ldap_ed_prev_url      — string|null, URL for previous page (null = disabled)
    1819 *   $ldap_ed_next_url      — string|null, URL for next page (null = disabled)
    … …  
    2526}
    2627
    27 // Palette for avatar backgrounds — one color is deterministically assigned per employee.
    28 $ldap_avatar_palette = array(
    29         '#4f7df3', '#7c5cbf', '#0e9b8a',
    30         '#2e9e4f', '#c0392b', '#d35400',
    31         '#1a7bbf', '#8e44ad',
    32 );
     28$ldap_ed_dept_avatar = $this->get_initials_and_color( $ldap_ed_current_dept, true );
    3329?>
    3430<div class="ldap-directory-wrap">
     31
     32        <a
     33                href="<?php echo esc_url( remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ); ?>"
     34                class="ldap-breadcrumb"
     35        >
     36                <?php echo $this->get_icon_svg( 'arrow-left' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?>
     37                <?php esc_html_e( 'Departments', 'ldap-staff-directory' ); ?>
     38        </a>
     39
     40        <div class="ldap-dept-header">
     41                <div class="ldap-dept-header-info">
     42                        <h2 class="ldap-dept-header-name"><?php echo esc_html( $ldap_ed_current_dept ); ?></h2>
     43                        <p class="ldap-dept-header-sub">
     44                        <?php
     45                        echo esc_html(
     46                                sprintf(
     47                                        /* translators: %s: number of employees in this department */
     48                                        _n( '%s employee in this department', '%s employees in this department', $ldap_ed_total_count, 'ldap-staff-directory' ),
     49                                        absint( $ldap_ed_total_count )
     50                                )
     51                        );
     52                        ?>
     53                        </p>
     54                </div>
     55                <span
     56                        class="ldap-dept-header-badge"
     57                        aria-hidden="true"
     58                        style="--ldap-avatar-bg:<?php echo esc_attr( $ldap_ed_dept_avatar['color'] ); ?>"
     59                ><?php echo esc_html( $ldap_ed_dept_avatar['initials'] ); ?></span>
     60        </div>
    3561
    3662        <?php if ( $ldap_ed_enable_search ) : ?>
    … …  
    4571                                <?php esc_html_e( 'Search employees', 'ldap-staff-directory' ); ?>
    4672                        </label>
    47                         <?php if ( '' !== $ldap_ed_current_dept ) : ?>
    4873                        <input type="hidden" name="ldap_dept" value="<?php echo esc_attr( $ldap_ed_current_dept ); ?>">
    49                         <?php endif; ?>
    5074                        <input
    5175                                type="search"
    … …  
    5377                                name="ldap_search"
    5478                                class="ldap-search"
    55                                 placeholder="<?php esc_attr_e( 'Search employee…', 'ldap-staff-directory' ); ?>"
     79                                placeholder="<?php
     80                                echo esc_attr(
     81                                        sprintf(
     82                                                /* translators: %s: department name */
     83                                                __( 'Search in %s…', 'ldap-staff-directory' ),
     84                                                $ldap_ed_current_dept
     85                                        )
     86                                );
     87                                ?>"
    5688                                aria-label="<?php esc_attr_e( 'Search employee', 'ldap-staff-directory' ); ?>"
    5789                                value="<?php echo esc_attr( $ldap_ed_search_query ); ?>"
    … …  
    5991                </form>
    6092        </div>
    61 
    62         <?php if ( count( $ldap_ed_departments ) >= 2 ) : ?>
    63         <div class="ldap-dept-filters" role="list" aria-label="<?php esc_attr_e( 'Filter by department', 'ldap-staff-directory' ); ?>">
    64 
    65                 <?php // "All" chip — active when no department filter is set. ?>
    66                 <?php $ldap_ed_all_url = esc_url( add_query_arg( 'ldap_page', 1, remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ) ); ?>
    67                 <?php if ( '' === $ldap_ed_current_dept ) : ?>
    68                 <span class="ldap-dept-chip is-active" role="listitem" aria-pressed="true">
    69                         <?php
    70                         echo esc_html(
    71                                 sprintf(
    72                                         /* translators: %s: total employee count */
    73                                         __( 'All (%s)', 'ldap-staff-directory' ),
    74                                         absint( $ldap_ed_all_count )
    75                                 )
    76                         );
    77                         ?>
    78                 </span>
    79                 <?php else : ?>
    80                 <a
    81                         href="<?php echo $ldap_ed_all_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"
    82                         class="ldap-dept-chip"
    83                         role="listitem"
    84                         aria-pressed="false"
    85                 >
    86                         <?php
    87                         echo esc_html(
    88                                 sprintf(
    89                                         /* translators: %s: total employee count */
    90                                         __( 'All (%s)', 'ldap-staff-directory' ),
    91                                         absint( $ldap_ed_all_count )
    92                                 )
    93                         );
    94                         ?>
    95                 </a>
    96                 <?php endif; ?>
    97 
    98                 <?php foreach ( $ldap_ed_departments as $ldap_ed_dept_name => $ldap_ed_dept_count ) : ?>
    99                         <?php
    100                         $ldap_ed_is_active  = ( 0 === strcasecmp( $ldap_ed_dept_name, $ldap_ed_current_dept ) );
    101                         $ldap_ed_chip_url   = esc_url( add_query_arg( array( 'ldap_dept' => $ldap_ed_dept_name, 'ldap_page' => 1 ) ) );
    102                         $ldap_ed_clear_url  = esc_url( add_query_arg( 'ldap_page', 1, remove_query_arg( array( 'ldap_dept', 'ldap_page' ) ) ) );
    103                         $ldap_ed_chip_label = esc_html( $ldap_ed_dept_name ) . ' (' . absint( $ldap_ed_dept_count ) . ')';
    104                         ?>
    105                         <?php if ( $ldap_ed_is_active ) : ?>
    106                         <span class="ldap-dept-chip is-active" role="listitem" aria-pressed="true">
    107                                 <?php echo $ldap_ed_chip_label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_html() + absint() applied above ?>
    108                                 <a
    109                                         href="<?php echo $ldap_ed_clear_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"
    110                                         class="ldap-dept-chip-clear"
    111                                         aria-label="<?php esc_attr_e( 'Clear department filter', 'ldap-staff-directory' ); ?>"
    112                                 >×</a>
    113                         </span>
    114                         <?php else : ?>
    115                         <a
    116                                 href="<?php echo $ldap_ed_chip_url; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already passed through esc_url() above ?>"
    117                                 class="ldap-dept-chip"
    118                                 role="listitem"
    119                                 aria-pressed="false"
    120                         >
    121                                 <?php echo $ldap_ed_chip_label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_html() + absint() applied above ?>
    122                         </a>
    123                         <?php endif; ?>
    124                 <?php endforeach; ?>
    125 
    126         </div>
    127         <?php endif; ?>
    12893        <?php endif; ?>
    12994
    … …  
    13499                <?php else : ?>
    135100                        <?php foreach ( $ldap_ed_users as $ldap_ed_user ) :
    136                                 // Compute initials (up to 2 characters) from the display name.
    137                                 $ldap_name     = $ldap_ed_user['name'] ?? '';
    138                                 $ldap_parts    = preg_split( '/\s+/', trim( $ldap_name ), 2 );
    139                                 $ldap_initials = strtoupper( substr( $ldap_parts[0] ?? '', 0, 1 ) );
    140                                 if ( ! empty( $ldap_parts[1] ) ) {
    141                                         $ldap_initials .= strtoupper( substr( $ldap_parts[1], 0, 1 ) );
    142                                 }
     101                                $ldap_ed_name   = $ldap_ed_user['name'] ?? '';
     102                                $ldap_ed_avatar = $this->get_initials_and_color( $ldap_ed_name );
     103                                $ldap_initials  = $ldap_ed_avatar['initials'];
     104
    143105                                // Fall back to first letter of email when name is absent.
    144106                                if ( '' === $ldap_initials && ! empty( $ldap_ed_user['email'] ) ) {
    … …  
    146108                                }
    147109
    148                                 // Deterministic background color derived from the name.
    149                                 $ldap_color_idx = abs( crc32( $ldap_name ) ) % count( $ldap_avatar_palette );
    150                                 $ldap_avatar_bg = $ldap_avatar_palette[ $ldap_color_idx ];
     110                                $ldap_avatar_bg = $ldap_ed_avatar['color'];
    151111                        ?>
    152112                        <article class="ldap-staff-card">
    … …  
    162122
    163123                                <?php if ( in_array( 'title', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['title'] ) ) : ?>
    164                                 <p class="ldap-title"><?php echo esc_html( $ldap_ed_user['title'] ); ?></p>
     124                                <p class="ldap-title">
     125                                        <?php echo $this->get_icon_svg( 'briefcase' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?>
     126                                        <?php echo esc_html( $ldap_ed_user['title'] ); ?>
     127                                </p>
    165128                                <?php endif; ?>
    166129
    … …  
    173136                                <?php if ( in_array( 'email', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['email'] ) ) : ?>
    174137                                <a class="ldap-email" href="mailto:<?php echo esc_attr( $ldap_ed_user['email'] ); ?>">
     138                                        <?php echo $this->get_icon_svg( 'mail' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?>
    175139                                        <?php echo esc_html( $ldap_ed_user['email'] ); ?>
    176140                                </a>
    … …  
    179143                                <?php if ( in_array( 'phone', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['phone'] ) ) : ?>
    180144                                <a class="ldap-phone" href="tel:<?php echo esc_attr( $ldap_ed_user['phone'] ); ?>">
     145                                        <?php echo $this->get_icon_svg( 'phone' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?>
    181146                                        <?php echo esc_html( $ldap_ed_user['phone'] ); ?>
    182147                                </a>
    … …  
    184149
    185150                                <?php if ( in_array( 'extension', $ldap_ed_fields, true ) && ! empty( $ldap_ed_user['extension'] ) ) : ?>
    186                                 <span class="ldap-extension"><?php echo esc_html( $ldap_ed_user['extension'] ); ?></span>
     151                                <span class="ldap-extension">
     152                                        <?php echo $this->get_icon_svg( 'building' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static trusted SVG markup, no user input ?>
     153                                        <?php echo esc_html( $ldap_ed_user['extension'] ); ?>
     154                                </span>
    187155                                <?php endif; ?>
    188156                        </article>
    … …  
    209177                        $ldap_ed_from = absint( ( $ldap_ed_current_page - 1 ) * $ldap_ed_per_page + 1 );
    210178                        $ldap_ed_to   = absint( min( $ldap_ed_current_page * $ldap_ed_per_page, $ldap_ed_total_count ) );
    211                         if ( '' !== $ldap_ed_current_dept ) :
    212                                 echo esc_html(
    213                                         sprintf(
    214                                                 /* translators: 1: first record number on page, 2: last record number on page, 3: total filtered records, 4: department name */
    215                                                 __( 'Showing %1$s–%2$s of %3$s in %4$s', 'ldap-staff-directory' ),
    216                                                 absint( $ldap_ed_from ),
    217                                                 absint( $ldap_ed_to ),
    218                                                 absint( $ldap_ed_total_count ),
    219                                                 $ldap_ed_current_dept
    220                                         )
    221                                 );
    222                         else :
    223                                 echo esc_html(
    224                                         sprintf(
    225                                                 /* translators: 1: first record number on page, 2: last record number on page, 3: total records */
    226                                                 __( 'Showing %1$s–%2$s of %3$s', 'ldap-staff-directory' ),
    227                                                 absint( $ldap_ed_from ),
    228                                                 absint( $ldap_ed_to ),
    229                                                 absint( $ldap_ed_total_count )
    230                                         )
    231                                 );
    232                         endif;
     179                        echo esc_html(
     180                                sprintf(
     181                                        /* translators: 1: first record number on page, 2: last record number on page, 3: total filtered records, 4: department name */
     182                                        __( 'Showing %1$s–%2$s of %3$s in %4$s', 'ldap-staff-directory' ),
     183                                        absint( $ldap_ed_from ),
     184                                        absint( $ldap_ed_to ),
     185                                        absint( $ldap_ed_total_count ),
     186                                        $ldap_ed_current_dept
     187                                )
     188                        );
    233189                endif; ?>
    234190                </span>
  • ldap-staff-directory/trunk/readme.txt

    r3634465 r3692384  
    33Tags:              ldap, directory, wpbeaverbuilder, staff, elementor
    44Requires at least: 5.8
    5 Tested up to:      7.0
     5Tested up to:      7.1
    66Requires PHP:      7.4
    7 Stable tag:        1.2.1
     7Stable tag:        1.4.0
    88License:           GPLv2 or later
    99License URI:       https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2323* Native Elementor widget with full style controls
    2424* Native Beaver Builder module with General and Style tabs
    25 * Department filter bar with configurable chip order (alphabetical or by contact count)
     25* Public directory browses department-first: a menu of departments (with employee counts) leads to each department's contact list, in configurable order (alphabetical or by contact count)
     26* Employee cards within a department can be ordered by name or job title, ascending or descending, from the admin — no code changes needed
    2627* Exclude specific departments — or employees with no department assigned — from the public directory, enforced at the LDAP query level so those employees are never fetched
    2728* Exclude disabled Active Directory accounts
    … …  
    7778By default 60 minutes. Change the TTL under **Settings → LDAP Directory → Fields → Advanced settings**, or flush immediately with the **Clear Cache** button on that same tab.
    7879
     80= Why do I now see a list of departments instead of all employees? =
     81
     82Starting in 1.3.0, the public directory browses department-first: visitors see a menu of departments with employee counts, and click into one to see its contacts. This replaces the previous single-page grid with a department filter bar. There is no setting to restore the old flat grid.
     83
    7984== Screenshots ==
    8085
    … …  
    8590
    8691== Changelog ==
     92
     93= 1.4.0 =
     94* Feature: New **Employee order** setting under **Settings → LDAP Directory → Fields** lets you choose how employee cards are ordered within a department: by name or job title, ascending or descending. Ties on job title (including employees with no title) are always broken by name (A–Z).
     95
     96= 1.3.0 =
     97* Feature: The public directory now browses department-first. The root view shows a menu of departments (name, employee count, color-coded avatar) instead of the full employee grid; clicking a department opens its filtered, paginated contact list with a breadcrumb back to the menu.
     98* Feature: Employee cards in the department view show a small icon next to job title, email, phone, and extension for easier scanning.
     99* Breaking change: The department filter chip bar has been removed and replaced by the department menu described above. There is no setting to restore the previous single-page grid.
     100* Chore: "Tested up to" updated to WordPress 7.1.
    87101
    88102= 1.2.1 =
Note: See TracChangeset for help on using the changeset viewer.