// A list of schools. Some columns and mass operations need access to a list of schools.
const SCHOOLS = {
<% @schools_list.each do |s| %>
    "<%= s[:id] %>": {
        dn: "<%= s[:dn] %>",
        name: "<%= s[:name] %>",
    },
<% end %>
};

// Localized user role names
const USER_ROLES = {
    owner: "<%= t('organisation_owner_role') %>",
    schooladmin: "<%= t('school_admin_role') %>",
    admin: "<%= t('puavoEduPersonAffiliation_admin') %>",
    teacher: "<%= t('puavoEduPersonAffiliation_teacher') %>",
    student: "<%= t('puavoEduPersonAffiliation_student') %>",
    staff: "<%= t('puavoEduPersonAffiliation_staff') %>",
    testuser: "<%= t('puavoEduPersonAffiliation_testuser') %>",
    parent: "<%= t('puavoEduPersonAffiliation_parent') %>",
    visitor: "<%= t('puavoEduPersonAffiliation_visitor') %>",
    unknown: "<%= t('unknown_role') %>",
};

// Localized group types
const GROUP_TYPES = {
    "teaching group": "<%= t('group_type.teaching group') %>",
    "course group": "<%= t('group_type.course group') %>",
    "year class": "<%= t('group_type.year class') %>",
    "administrative group": "<%= t('group_type.administrative group') %>",
    "archive users": "<%= t('group_type.archive users') %>",
    "other groups": "<%= t('group_type.other groups') %>",
};

// All available columns, their names and types and other parameters
const COLUMN_DEFINITIONS = {
    id: {
        key: "id",
        title: "<%= t('columns.puavoid') %>",
        type: ST.ColumnType.NUMERIC,
        alias: ["puavoid"],
    },

    first: {
        key: "first",
        title: "<%= t('columns.users.first_names') %>",
        type: ST.ColumnType.STRING,
        alias: ["first_name", "fn"],
    },

    last: {
        key: "last",
        title: "<%= t('columns.users.last_name') %>",
        type: ST.ColumnType.STRING,
        alias: ["last_name", "ln"],
    },

    name: {
        key: "name",
        title: "<%= t('columns.users.full_name') %>",
        type: ST.ColumnType.STRING,
        transform: entry => {
            let s = "";

            if (entry.rrt)
                s += `<span class="stUserMarkedForDeletion">`;

            s += `<a href="${entry.link}">${ST.escapeHTML(entry.name)}</a>`;

            if (entry.locked)
                s += `<i class="icon-lock"></i>`;

            if (entry._has_expired)
                s += `<i class="icon-clock" title="<%= t('users.index.expired_account') %>"></i>`;

            if (entry.rrt)
                s += `</span>`;

            return [
                s,
                entry.name
            ];
        }
    },

    uid: {
        key: "uid",
        title: "<%= t('columns.users.uid') %>",
        type: ST.ColumnType.STRING,
        alias: ["username"],
    },

    eid: {
        key: "eid",
        title: "<%= t('columns.users.eid') %>",
        type: ST.ColumnType.STRING,
        flags: ST.ColumnFlag.F_NULLABLE,
        alias: ["external_id"]
    },

    learner_id: {
        key: "learner_id",
        title: "<%= t('columns.users.learner_id') %>",
        type: ST.ColumnType.STRING,
        flags: ST.ColumnFlag.F_NULLABLE,
        alias: ["lid"],
    },

    role: {
        key: "role",
        title: "<%= t('columns.users.role') %>",
        type: ST.ColumnType.STRING,
        transform: entry => {
            let values = [];

            // Translate user roles
            for (let i = 0; i < entry.role.length; i++) {
                const r = entry.role[i];

                if (r in USER_ROLES) {
                    if (r == "owner" || r == "schooladmin")
                        values.push(`<span class="adminOrOwner">${USER_ROLES[r]}</span>`);
                    else values.push(USER_ROLES[r]);
                } else values.push(USER_ROLES['unknown']);
            }

            return [
                values.join("<br>"),
                entry.role.join()
            ];
        },
    },

    phone: {
        key: "phone",
        title: "<%= t('columns.users.phone') %>",
        type: ST.ColumnType.STRING,
        flags: ST.ColumnFlag.ARRAY | ST.ColumnFlag.F_NULLABLE,
        alias: ["phone_number"],
    },

    email: {
        key: "email",
        title: "<%= t('columns.users.email') %>",
        type: ST.ColumnType.STRING,
        flags: ST.ColumnFlag.ARRAY | ST.ColumnFlag.F_NULLABLE,
    },

    v_email: {
        key: "v_email",
        title: "<%= t('columns.users.v_email') %>",
        type: ST.ColumnType.STRING,
        flags: ST.ColumnFlag.ARRAY | ST.ColumnFlag.F_NULLABLE,
    },

    p_email: {
        key: "p_email",
        title: "<%= t('columns.users.p_email') %>",
        type: ST.ColumnType.STRING,
        flags: ST.ColumnFlag.F_NULLABLE,
    },

    pnumber: {
        key: "pnumber",
        title: "<%= t('columns.users.pnumber') %>",
        type: ST.ColumnType.STRING,
    },

    rrt: {
        key: "rrt",
        title: "<%= t('columns.users.rrt') %>",
        type: ST.ColumnType.UNIXTIME,
        flags: ST.ColumnFlag.F_NULLABLE,
        transform: entry => {
            if (entry.rrt === undefined || entry.rrt === null || entry.rrt === 0)
                return ["", 0];

            const [valid, t] = ST.convertTimestamp(entry.rrt);

            if (!valid)
                return ["?", 0];

            return [t, entry.rrt];
        },
        alias: ["removal_request_time"],
    },

    dnd: {
        key: "dnd",
        title: "<%= t('columns.users.dnd') %>",
        type: ST.ColumnType.BOOL,
        flags: ST.ColumnFlag.F_NULLABLE,
        alias: ["do_not_delete"]
    },

    locked: {
        key: "locked",
        title: "<%= t('columns.users.is_locked') %>",
        type: ST.ColumnType.BOOL,
        flags: ST.ColumnFlag.F_NULLABLE,
    },

    created: {
        key: "created",
        title: "<%= t('columns.created') %>",
        type: ST.ColumnType.UNIXTIME,
        flags: ST.ColumnFlag.DESCENDING_DEFAULT,
    },

    modified: {
        key: "modified",
        title: "<%= t('columns.modified') %>",
        type: ST.ColumnType.UNIXTIME,
        flags: ST.ColumnFlag.DESCENDING_DEFAULT,
    },

    last_kerberos_auth_date: {
        key: "last_kerberos_auth_date",
        title: "<%= t('columns.last_kerberos_auth_date') %>",
        type: ST.ColumnType.UNIXTIME,
        flags: ST.ColumnFlag.DESCENDING_DEFAULT | ST.ColumnFlag.F_DATEONLY,
    },

    last_ldap_auth_date: {
        key: "last_ldap_auth_date",
        title: "<%= t('columns.last_ldap_auth_date') %>",
        type: ST.ColumnType.UNIXTIME,
        flags: ST.ColumnFlag.DESCENDING_DEFAULT | ST.ColumnFlag.F_DATEONLY,
    },

<% if @is_organisation %>
    school: {
        key: "school",
        title: "<%= t('columns.primary_school') %>",
        type: ST.ColumnType.STRING,
        transform: entry => {
            // Makes filtering by school name work as expected, but the exported CSV
            // will contain display names. Can't have everything.
            return [entry.school[1], entry.school[1]];
        },
    },
<% end %>

    schools: {
        key: "schools",
        title: "<%= t('columns.users.other_schools') %>",
        type: ST.ColumnType.STRING,
        transform: entry => {
            let out = [];

            if (Array.isArray(entry.schools))
                for (const id of entry.schools)
                    out.push((id in SCHOOLS) ? SCHOOLS[id].name : "?");

            return [out.join("<br>"), out.join(","), out.join(",")];
        }
    },

    licenses: {
        key: "licenses",
        title: "<%= t('columns.users.licenses') %>",
        type: ST.ColumnType.STRING,
        flags: ST.ColumnFlag.ARRAY,
    },

    uuid: {
        key: "uuid",
        title: "UUID",
        type: ST.ColumnType.STRING,
        customCSS: ["font-monospace"],      // this makes these easier to read
    },

    mfa: {
        key: "mfa",
        title: "MFA?",
        type: ST.ColumnType.BOOL,
    },

    notes: {
        key: "notes",
        title: "<%= t('columns.notes') %>",
        type: ST.ColumnType.STRING,
        transform: entry => [ST.escapeHTML(entry.notes).replaceAll("\r", "").replaceAll("\n", "<br>"), entry.notes],
    },

    devices: {
        key: "devices",
        title: "<%= t('columns.users.devices') %>",
        type: ST.ColumnType.STRING,
        flags: ST.ColumnFlag.ARRAY,
        transform: entry => {
            let display = [],
                raw = [];

            if (entry.devices) {
                for (const d of entry.devices) {
                    let link = `<a href="${d[1]}">${d[0]}</a>`,
                        plain = d[0];

                    if (d[2] in SCHOOLS) {
                        const name = SCHOOLS[d[2]].name;

                        link += ` (${name})`;
                        plain += `,${name}`;
                    }

                    display.push(link);
                    raw.push(plain);
                }
            }

            // Use the raw output for both sorting and filtering. Now you can filter by both
            // device hostnames and device schools.
            raw = raw.join(",");

            return [display.join("<br>"), raw, raw];
        }
    },

    groups: {
        key: "groups",
        title: "<%= t('columns.users.groups') %>",
        type: ST.ColumnType.STRING,
        flags: ST.ColumnFlag.ARRAY,

        transform: entry => {
            let html = [],
                value = [];

            // If the user has groups, list them in clickable form.
            // See the expandUserData() generator function elsewhere
            // in this file.
            if (entry.groups) {
                for (const gid of entry.groups) {
                    const group = GROUPS[gid];
                    let h = "";

                    // The group is not linked to if the current user cannot access
                    // the school where the group is. We'll still display its name.
                    if (group.link !== null)
                        h += `<a href="${group.link}">`;

                    h += group.name;

                    if (group.link !== null)
                        h += `</a>`;

                    if (group.type in GROUP_TYPES)
                        h += ` <small>(${GROUP_TYPES[group.type]})</small>`;

                    html.push(h);
                    value.push(group.name);
                }
            }

            html = html.join("<br>");
            value = value.join(",");

            return [html, value, value];
        },
    },

    expiration: {
        key: "expiration",
        title: "<%= t('columns.users.expiration') %>",
        type: ST.ColumnType.UNIXTIME,
        flags: ST.ColumnFlag.DESCENDING_DEFAULT | ST.ColumnFlag.F_NULLABLE,

        transform: entry => {
            const [valid, ts] = ST.convertTimestamp(entry.expiration)
            let html = "";

            if (valid && entry._has_expired)
                html = `<span class="invalidData">${ts}</span>`;
            else html = ts;

            return [html, entry.expiration, entry.expiration];
        }
    },
};

// The default order for columns
const COLUMN_ORDER = [
<% if @is_organisation %>
    "school",
<% end %>
    "schools",
    "id",
    "uuid",
    "name",
    "first",
    "last",
    "uid",
    "role",
    "eid",
    "learner_id",
    "phone",
    "email",
    "v_email",
    "p_email",
    "pnumber",
    "expiration",
    "rrt",
    "dnd",
    "locked",
    "created",
    "modified",
    "last_kerberos_auth_date",
    "last_ldap_auth_date",
    "licenses",
    "devices",
    "groups",
    "mfa",
    "notes",
];

<% if @permit_mass_user_deletion %>
// Mass operation: delete users
class MassUserDeletion extends ST.MassOperation {
    constructor(parent, container)
    {
        super(parent, container);
    }

    canProceed()
    {
        // Show an extra warning if there are synchronised deletions
        if (Object.keys(SYNC_DELETIONS).length == 0)
            return true;

        return window.confirm(`<%= t('destroy_generic_sync_warning') %>`);
    }

    prepareItem(item)
    {
        // Filter do-not-delete users, and those who have not been marked for deletion for
        // at least one week. These same checks are also done on the server, the purpose
        // of these is to reduce network traffic (no need to send a network request, when
        // nothing will be done to these users).
        const dnd = item.dnd[ST.INDEX_EXISTS] === true && item.dnd[ST.INDEX_FILTERABLE] !== undefined,
              marked = item.rrt[ST.INDEX_EXISTS] === true && item.rrt[ST.INDEX_FILTERABLE] !== undefined,
              locked = item.locked[ST.INDEX_EXISTS] === true && item.locked[ST.INDEX_FILTERABLE] === true;

        if (dnd)
            return { state: "error", message: "<%= t('users.index.mass_operations.delete.deletion_prevented') %>" };

        if (!marked)
            return { state: "error", message: "<%= t('users.index.mass_operations.delete.not_marked_for_deletion') %>" };

        const now = Math.floor(Date.now() / 1000);
        const next = item.rrt[ST.INDEX_FILTERABLE] + (60 * 60 * 24 * 7);   // 7 days in seconds

        if (next > now)
            return { state: "error", message: "<%= t('users.index.mass_operations.delete.marked_too_recently') %>" };

        // Don't delete yourself
        if (item.id[ST.INDEX_FILTERABLE] == <%= @current_user_id %>)
            return { state: "error", message: "<%= t('users.index.mass_operations.delete.cant_delete_yourself') %>" };

        // Don't delete organisation owners
        if (item.role[ST.INDEX_FILTERABLE].includes("owner"))
            return { state: "error", message: "<%= t('users.index.mass_operations.delete.cant_delete_owners') %>" };

        return { state: "ready" };
    }
};
<% end %>

// Mass operation: lock/unlock users
class MassUserLocking extends ST.MassOperation {
    constructor(parent, container)
    {
        super(parent, container);
        this.lock = true;
    }

    buildInterface()
    {
        this.container.innerHTML =
`<table class="settingsChild">
<tr>
    <th><%= t('users.index.mass_operations.lock.action') %></th>
    <td>
        <div class="flex-rows">
            <label><input type="radio" name="action" id="lock" checked><%= t('users.index.mass_operations.lock.lock') %></label>
            <label><input type="radio" name="action" id="unlock"><%= t('users.index.mass_operations.lock.unlock') %></label>
        </div>
    </td>
</tr>
</table>`;

        this.container.querySelector("input#lock").addEventListener("click", e => this.lock = true);
        this.container.querySelector("input#unlock").addEventListener("click", e => this.lock = false);
    }
    start()
    {
        for (const i of this.container.querySelectorAll("input"))
            i.disabled = true;
    }

    finish()
    {
        for (const i of this.container.querySelectorAll("input"))
            i.disabled = false;
    }

    getOperationParameters()
    {
        return {
            lock: this.lock,
        };
    }

    prepareItem(item)
    {
        // Don't lock yourself
        if (item.id[ST.INDEX_FILTERABLE] == <%= @current_user_id %>)
            return { state: "error", message: "<%= t('users.index.mass_operations.lock.cant_lock_yourself') %>" };

        // Don't lock organisation owners
        if (item.role[ST.INDEX_FILTERABLE].includes("owner"))
            return { state: "error", message: "<%= t('users.index.mass_operations.lock.cant_lock_owners') %>" };

        // Do nothing if the user is already in the desired locking state
        if (item.locked[ST.INDEX_EXISTS] === true && item.locked[ST.INDEX_FILTERABLE] === this.lock)
            return { state: "skip" };

        return { state: "ready" };
    }
};

// Mass operation: mark users for deletion (or remove the marking)
class MassUserMarkForDeletion extends ST.MassOperation {
    constructor(parent, container)
    {
        super(parent, container);

        // The default operation (mark for deletion if not marked yet)
        this.action = "mark";
    }

    buildInterface()
    {
        this.container.innerHTML =
`<table class="settingsChild">
<tr>
    <th><%= t('users.index.mass_operations.mark.action') %></th>
    <td>
        <div class="flex-rows">
            <label><input type="radio" name="action" id="mark" checked><%= t('users.index.mass_operations.mark.mark') %></label>
            <label><input type="radio" name="action" id="mark_force"><%= t('users.index.mass_operations.mark.mark_force') %></label>
            <label><input type="radio" name="action" id="unmark"><%= t('users.index.mass_operations.mark.unmark') %></label>
        </div>
    </td>
</tr>
</table>`;

        this.container.querySelector("input#mark").addEventListener("click", e => this.action = "mark");
        this.container.querySelector("input#mark_force").addEventListener("click", e => this.action = "mark_force");
        this.container.querySelector("input#unmark").addEventListener("click", e => this.action = "unmark");
    }

    start()
    {
        for (const i of this.container.querySelectorAll("input"))
            i.disabled = true;
    }

    finish()
    {
        for (const i of this.container.querySelectorAll("input"))
            i.disabled = false;
    }

    getOperationParameters()
    {
        return this.action;
    }

    prepareItem(item)
    {
        // Pre-filter users, avoid useless network requests. All of these checks
        // are also done on the server end.
        const dnd = item.dnd[ST.INDEX_EXISTS] === true && item.dnd[ST.INDEX_FILTERABLE] !== undefined,
              marked = item.rrt[ST.INDEX_EXISTS] === true && item.rrt[ST.INDEX_FILTERABLE] !== undefined;

        // Don't mark yourself for deletion
        if (item.id[ST.INDEX_FILTERABLE] == <%= @current_user_id %>)
            return { state: "error", message: "<%= t('users.index.mass_operations.mark.cant_mark_yourself') %>" };

        // Don't mark organisation owners for deletion
        if (item.role[ST.INDEX_FILTERABLE].includes("owner"))
            return { state: "error", message: "<%= t('users.index.mass_operations.mark.cant_mark_owners') %>" };

        if (dnd) {
            // Deletion prevented, can't mark for deletion
            return { state: "error", message: "<%= t('users.index.mass_operations.delete.deletion_prevented') %>" };
        }

        if (marked && this.action == "mark") {
            // Already marked for deletion and not in forced mode
            return { state: "skip" };
        }

        if (!marked && this.action == "unmark") {
            // Not marked for deletion, can't unmark
            return { state: "skip" };
        }

        return { state: "ready" };
    }
};

<% if @permit_mass_column_clear %>
// Mass operation: clear column (ie. set a specific database attribute to null for all selected users)
class MassUserClearColumn extends ST.MassOperation {
    constructor(parent, container)
    {
        super(parent, container);
        this.column = "email";
    }

    buildInterface()
    {
        this.container.innerHTML =
`<table class="settingsChild">
<tr>
    <th><%= t('users.index.mass_operations.clear_column.column_title') %></th>
    <td>
        <p class="genericError"><%= t('users.index.mass_operations.clear_column.warning') %></p>
        <div class="flex-rows">
<% unless @automatic_email_addresses %>
            <label><input type="radio" name="clear_column" value="email" checked><%= t('columns.users.email') %></label>
<% end %>
            <label><input type="radio" name="clear_column" value="telephone"><%= t('columns.users.phone') %></label>
            <label><input type="radio" name="clear_column" value="eid"><%= t('columns.users.eid') %></label>
            <label><input type="radio" name="clear_column" value="pnumber"><%= t('columns.users.pnumber') %></label>
            <label><input type="radio" name="clear_column" value="notes"><%= t('columns.users.notes') %></label>
        </div>
    </td>
</tr>
</table>`;

        for (const i of this.container.querySelectorAll("input"))
            i.addEventListener("click", e => this.column = e.target.value);
    }

    start()
    {
        for (const i of this.container.querySelectorAll("input"))
            i.disabled = true;
    }

    finish()
    {
        for (const i of this.container.querySelectorAll("input"))
            i.disabled = false;
    }

    getOperationParameters()
    {
        return this.column;
    }

    prepareItem(item)
    {
        return { state: "ready" };
    }
};
<% end %>

<% unless @is_organisation %>
// Create a new username list from selection
class MassUsernameList extends ST.MassOperation {
    constructor(parent, container)
    {
        super(parent, container);
        this.description = "";
    }

    buildInterface()
    {
        this.container.innerHTML =
`<label for="description"><%= t('users.index.mass_operations.username_list.description') %>:</label>
<input type="text" maxlength="100" size="50" id="description">`;

        this.container.querySelector("input#description").addEventListener("change", e => this.description = e.target.value.trim());
    }

    getOperationParameters()
    {
        return {
            creator: "<%= current_user.givenName %> <%= current_user.sn %> (<%= current_user.uid %>)",
            description: this.description,
        };
    }

    prepareItem(item)
    {
        return { state: "ready" };
    }
};
<% end %>

// Mass operation: change school
class MassUserChangeSchool extends ST.MassOperation {
    constructor(parent, container)
    {
        super(parent, container);

        // Same defaults as on the form below
        this.action = "move";
        this.keep = false;
        this.removePrev = true;
        this.schoolID = null;
    }

    buildInterface()
    {
        let html =
<% if @allowed_destination_schools.empty? %>
        `<p class="genericWarning"><%= t('users.index.mass_operations.change_school.no_other_schools') %></p>`;

        this.container.innerHTML = html;
<% else %>
`<p class="genericWarning"><%= t('users.index.mass_operations.change_school.automation_warning') %></p>
<table class="settingsChild">
<tr>
    <th><%= t('users.index.mass_operations.change_school.operation') %></label></th>
    <td>
        <div class="flex-rows gap-5px">
            <label><input type="radio" name="operation" id="move" value="move" checked><%= t('users.index.mass_operations.change_school.operation_move') %></label>
            <label class="margin-left-20px">                  <input type="checkbox" id="keep"><%= t('users.index.mass_operations.change_school.operation_move_keep') %></label>
            <label class="margin-left-20px margin-bottom-5px"><input type="checkbox" id="remove_prev" checked><%= t('users.index.mass_operations.change_school.operation_remove_prev_groups') %></label>
            <label><input type="radio" name="operation" id="add" value="add"><%= t('users.index.mass_operations.change_school.operation_add') %></label>
            <label><input type="radio" name="operation" id="remove" value="remove"><%= t('users.index.mass_operations.change_school.operation_remove') %></label>
        </div>
    </td>
</tr>
<tr>
    <th><label for="destination_school"><%= t('users.index.mass_operations.change_school.target_school') %></label></th>
    <td>
        <select id="destination_school">

        <% @allowed_destination_schools.sort { |a, b| a[:name].downcase <=> b[:name].downcase }.each do |s| %>
            <option value="<%= s[:id] %>"><%= s[:name] %></option>
        <% end %>

        </select></td></tr></table>`;

        this.container.innerHTML = html;

        this.container.querySelector("input#move").addEventListener("click", e => this.action = "move");
        this.container.querySelector("input#add").addEventListener("click", e => this.action = "add");
        this.container.querySelector("input#remove").addEventListener("click", e => this.action = "remove");
        this.container.querySelector("input#keep").addEventListener("click", e => this.keep = e.target.checked);
        this.container.querySelector("input#remove_prev").addEventListener("click", e => this.removePrev = e.target.checked);

        this.container.querySelector("select#destination_school").addEventListener("change", e => {
            console.log(e.target.value);
            this.schoolID = e.target.value
        });

        this.schoolID = this.container.querySelector("select#destination_school").value;
<% end %>
    }

    canProceed()
    {
<% if @allowed_destination_schools.empty? %>
        return false;
<% else %>
        if (this.container.querySelector("select#destination_school").selectedIndex == -1) {
            window.alert("<%= t('users.index.mass_operations.change_school.no_available_schools').html_safe %>");
            return false;
        }

        return true;
<% end %>
    }

    start()
    {
        for (const i of this.container.querySelectorAll("input, select"))
            i.disabled = true;
    }

    finish()
    {
        for (const i of this.container.querySelectorAll("input, select"))
            i.disabled = false;
    }

    getOperationParameters()
    {
        return {
            action: this.action,
            keep: this.keep,
            remove_prev: this.removePrev,
            school_id: this.schoolID,
            school_dn: SCHOOLS[this.schoolID].dn,
        };
    }

    prepareItem(item)
    {
        // Unfortunately there's nothing to optimize yet. The school list does not show
        // the current school, so every school change can be assumed to be real.
        return { state: "ready" };
    }
};

<% if @permit_user_mass_edit_expiration_times %>
const pad = (n, count=2) => n.toString().padStart(count, "0")

// Mass operation: set or clear expiration times
class MassSetExpirationTimes extends ST.MassOperation {
    constructor(parent, container)
    {
        super(parent, container);
        this.expirationTime = null;
    }

    buildInterface()
    {
        this.container.innerHTML =
`<table class="settingsChild">
<tr>
    <th><%= t('users.index.mass_operations.set_expiration_times.set_to') %></th>
    <td>
        <div class="flex-cols gap-5px">
            <input type="datetime-local" id="time" autocomplete="off" step="1">

            <button type="button" id="clear"><%= t('expiration_times.clear') %></button>

            <select id="preset" autocomplete="off">
            <option value="" disabled selected hidden><%= t('expiration_times.presets') %></option>
            <%= expiration_time_presets %>
            </select>
        </div>
    </td>
</tr>`;

        this.container.querySelector("input#time").addEventListener("change", e => this.onChangeTime(e));
        this.container.querySelector("button#clear").addEventListener("click", () => this.onClear());
        this.container.querySelector("select#preset").addEventListener("change", e => this.onSelectPreset(e));
    }

    onChangeTime(e)
    {
        const d = e.target.value;

        if (d === null) {
            this.expirationTime = null;
            return;
        }

        // Local time -> UTC time
        const offset = new Date().getTimezoneOffset(),
              offsetString = `${offset < 0 ? "+" : "-"}${pad(Math.abs(offset) / 60, 2)}:${pad(Math.abs(offset) % 60, 2)}`;

        this.expirationTime = new Date(e.target.value + offsetString);
        console.log(this.expirationTime);
    }

    onClear()
    {
        this.container.querySelector("input#time").value = null;
        this.expirationTime = null;
    }

    onSelectPreset(e)
    {
        this.expirationTime = new Date(new Date().getTime() + (parseInt(e.target.value, 10) * 1000));
        this.setTime(this.expirationTime);
        e.target.value = "";
    }

    setTime(date)
    {
        this.container.querySelector("input#time").value =
            pad(date.getFullYear(), 4) + "-" +
            pad(date.getMonth() + 1) + "-" +
            pad(date.getDate()) + "T" +
            pad(date.getHours()) + ":" +
            pad(date.getMinutes()) + ":" +
            pad(date.getSeconds());
    }

    canProceed()
    {
        return true;
    }

    start()
    {
        for (const i of this.container.querySelectorAll("input, button, select"))
            i.disabled = true;
    }

    finish()
    {
        for (const i of this.container.querySelectorAll("input, button, select"))
            i.disabled = false;
    }

    getOperationParameters()
    {
        return {
            action: 'set_expiration_time',
            time: (this.expirationTime === null) ? null : Math.floor(this.expirationTime.getTime() / 1000)
        };
    }

    prepareItem(item)
    {
        // Refuse to lock non-deletable users
        if (item.dnd[ST.INDEX_EXISTS] === true && item.dnd[ST.INDEX_FILTERABLE] === true)
            return { state: "error", message: "<%= t('users.index.mass_operations.set_expiration_times.dnd') %>" };

        return { state: "ready" };
    }
};
<% end %>

const MASS_OPERATIONS = [
<% if @permit_mass_user_deletion %>
    {
        operation: "delete",
        title: "<%= t('users.index.mass_operations.delete.title') %>",
        cls: MassUserDeletion,
    },
<% end %>

    {
        operation: "lock",
        title: "<%= t('users.index.mass_operations.lock.title') %>",
        haveSettings: true,
        cls: MassUserLocking
    },

    {
        operation: "mark_for_deletion",
        title: "<%= t('users.index.mass_operations.mark.title') %>",
        haveSettings: true,
        cls: MassUserMarkForDeletion
    },

<% if @permit_mass_column_clear %>
    {
        operation: "clear_column",
        title: "<%= t('users.index.mass_operations.clear_column.title') %>",
        haveSettings: true,
        cls: MassUserClearColumn
    },
<% end %>

<% unless @is_organisation %>
    {
        operation: "create_username_list",
        title: "<%= t('users.index.mass_operations.username_list.title') %>",
        haveSettings: true,
        singleShot: true,
        cls: MassUsernameList
    },
<% end %>

    {
        operation: "change_school",
        title: "<%= t('users.index.mass_operations.change_school.title') %>",
        haveSettings: true,
        cls: MassUserChangeSchool
    },

<% if @permit_user_mass_edit_expiration_times %>
    {
        operation: "set_expiration_time",
        title: "<%= t('users.index.mass_operations.set_expiration_times.title') %>",
        haveSettings: true,
        cls: MassSetExpirationTimes
    },
<% end %>
];

const FILTER_PRESETS = [
    // Traditional
    {
        students_only: {
            title: "<%= t('users.index.filters.students_only') %>",
            filters: [
                [1,"role","=","student"],
                [1,"role","!=","teacher|staff|test|admin|visitor"]
            ]
        },

        teachers_and_staff_only: {
            title: "<%= t('users.index.filters.teachers_and_staff_only') %>",
            filters: [
                [1,"role","=","teacher|staff"],
                [1,"role","!=","test|admin"]
            ]
        },

        marked_for_3months: {
            title: "<%= t('users.index.filters.marked_3months') %>",
            filters: [
                [1,"rrt","<","-90d"],
            ],
        },

        marked_for_6months: {
            title: "<%= t('users.index.filters.marked_6months') %>",
            filters: [
                [1,"rrt","<","-180d"],
            ],
        },

        marked_for_9months: {
            title: "<%= t('users.index.filters.marked_9months') %>",
            filters: [
                [1,"rrt","<","-270d"],
            ],
        },

        marked_for_12months: {
            title: "<%= t('users.index.filters.marked_12months') %>",
            filters: [
                [1,"rrt","<","-365d"],
            ],
        },

        smoketest_users: {
            title: "<%= t('users.index.filters.smoketest_users') %>",
            filters: [
                [1,"uid","=","^test.user.[0-9a-fA-F]+$"]
            ],
        },

        locked: {
            title: "<%= t('users.index.filters.locked_users') %>",
            filters: [
                [1,"locked","=","1"]
            ],
        },

        marked_for_deletion: {
            title: "<%= t('users.index.filters.marked_for_deletion') %>",
            filters: [
                [1,"rrt",">=","2010-01-01 00:00:00"]
            ],
        },

        unlocked_deleted: {
            title: "<%= t('users.index.filters.marked_for_deletion_not_locked') %>",
            filters: [
                [1,"locked","!=","1"],
                [1,"rrt",">=","2010-01-01 00:00:00"],
            ],
        },

        short_uids: {
            title: "<%= t('users.index.filters.short_uids') %>",
            filters: [
                [1,"uid","=","^[a-z0-9-.]{1,5}$"]
            ],
        },

        expired_but_not_locked: {
            title: "<%= t('users.index.filters.expired_but_not_locked') %>",
            filters: [
                [1,"expiration","<",0],
                [1,"locked","=",0]
            ],
        },
    },

    // Advanced
    {
        marked_for_3months: {
            title: "<%= t('users.index.filters.marked_3months') %>",
            filter: `rrt !! t && rrt < -90d`,
        },

        marked_for_6months: {
            title: "<%= t('users.index.filters.marked_6months') %>",
            filter: `rrt !! t && rrt < -180d`,
        },

        marked_for_9months: {
            title: "<%= t('users.index.filters.marked_9months') %>",
            filter: `rrt !! t && rrt < -270d`,
        },

        marked_for_12months: {
            title: "<%= t('users.index.filters.marked_12months') %>",
            filter: `rrt !! t && rrt < -365d`,
        },

        smoketest_users: {
            title: "<%= t('users.index.filters.smoketest_users') %>",
            filter: `uid = /^test.user.[0-9a-fA-F]+$/`,
        },

        locked: {
            title: "<%= t('users.index.filters.locked_users') %>",
            filter: `locked !! t && locked = t`,
        },

        marked_for_deletion_not_locked: {
            title: "<%= t('users.index.filters.marked_for_deletion_not_locked') %>",
            filter: `rrt !! t && rrt > 2010 && locked == f`,
        },

        short_uids: {
            title: "<%= t('users.index.filters.short_uids') %>",
            filter: `uid = /^[a-z0-9-.]{1,5}$/`,
        }
    }
];

const SYNC_DELETIONS = <%= raw JSON.pretty_generate(@synchronised_deletions) %>;

function userActions(item)
{
    let html = "";

    const link = `/users/${item.school_id}/users/${item.id[ST.INDEX_FILTERABLE]}`;
    const dnd = item.dnd[ST.INDEX_EXISTS] === true && item.dnd[ST.INDEX_FILTERABLE] !== undefined;

    html += `<a href="${link}/edit" class="btn"><%= t('link.edit') %></a> `;

    if (!dnd) {
<% if @permit_user_deletion %>
        let isAdmin = false,
            message = null;

        if (item.id[ST.INDEX_FILTERABLE] == <%= @current_user_id %>) {
            // This is the current user, don't display the deletion button. The controller
            // does not let users delete themselves, but this hides even the button.
            return html;
        }

        // Extra warning when deleting owners/admins
        for (let t of item.role[1].split(",")) {
            if (t == "owner" || t == "admin") {
                isAdmin = true;
                break;
            }
        }

        if (isAdmin)
            message = "<%= t('destroy_confirm_admin') %>";
        else message = "<%= t('general_confirm') %>";

        const school_id = item.school_id;

        if (school_id in SYNC_DELETIONS && SYNC_DELETIONS[school_id].length > 0) {
            // Extra warning about deletion synchronisations. "SYSTEM_NAMES_HERE" is the only
            // placeholder I could came up with to stop JavaScript's string interpolation from
            // throwing up because it couldn't find the placeholder. I tried ${name}, $(name)
            // and a bunch of others. Of course it attempted to process them all. I need to
            // use `` for the string because it's the only way to have multiline strings in
            // JavaScript.
            message += "\n\n";
            message += `<%= t('destroy_exact_sync_warning') %>`
                       .replace("SYSTEM_NAMES_HERE", SYNC_DELETIONS[school_id].join(", "));
        }

        html += `<a href="${link}" data-method="delete" data-confirm="${message}" rel="nofollow" class="btn btn-danger"><%= t('link.destroy') %></a>`
<% end %>
    }

    return html;
}

function openCallback(item)
{
    return `/users/${item.school_id}/users/${item.id[ST.INDEX_DISPLAYABLE]}`;
}

const MASS_SELECTS = [
    ["uid", "<%= t('columns.users.uid') %>"],
    ["name", "<%= t('columns.users.full_name') %>"],
    ["eid", "<%= t('columns.users.eid') %>"],
    ["id", "<%= t('columns.puavoid') %>"],
];

let GROUPS = {},
    GROUP_MEMBERS = {};

// User groups lists are not generated server-side due to their enormous size.
// Instead the server simply generates a list of groups and their members (DNs).
// This generator function is used to process that "raw" list into something we
// can use when formatting user tables.
function* expandUserData(raw)
{
    const NOW = new Date();

    GROUPS = raw.groups;
    GROUP_MEMBERS = {};

    // First, generate a reverse lookup table. Instead of having a list groups and
    // members in those groups, build a list of users and their groups.
    for (const [groupID, members] of Object.entries(raw.group_members)) {
        for (const dn of members) {
            // Extract the user's PuavoID from the DN. This is faster than regexp.
            const puavoId = parseInt(dn.split(",")[0].split("=")[1], 10);

            if (!(puavoId in GROUP_MEMBERS))
                GROUP_MEMBERS[puavoId] = [];

            GROUP_MEMBERS[puavoId].push(groupID);
        }
    }

    // Then for each user, insert the list of their groups into the user data
    for (const [_, s] of Object.entries(raw.users)) {
        let s2 = { ...s };

        if (s2.id in GROUP_MEMBERS)
            s2.groups = GROUP_MEMBERS[s2.id];

        // Inject an account expiration flag into the data, so we don't have to recalculate
        // it in several places.
        s2._has_expired = false;

        if (s2.expiration) {
            const [valid, ts] = ST.convertTimestamp(s2.expiration)

            if (valid && NOW >= new Date(s2.expiration * 1000))
                s2._has_expired = true;
        }

        yield s2;
    }
}
