Mojolicious-Plugin-Fondation-User-UI-Bootstrap

 view release on metacpan or  search on metacpan

share/public/js/DatatableUser.js  view on Meta::CPAN

var tableau;

$(function() {
    if (!$('#panel-user').length) return;
    initialiseTabUser();
    $('#menuAddUser').on('click', function(event) {
        addUser();
    });

    // ────────────────────────────────────────────────
    // Form validation using validators.js
    // ────────────────────────────────────────────────
async function validateUserForm() {
    const form = document.getElementById('user-form');
    if (!form) {
        console.error("Form #user-form not found");
        return { valid: false, errors: [{ field: null, message: l("Form not found") }], data: {} };
    }

    // Collect raw data
    const formDataObj = Object.fromEntries(new FormData(form).entries());

    // Cleanup and conversions (trim all strings)
    Object.keys(formDataObj).forEach(key => {
        if (typeof formDataObj[key] === 'string') {
            formDataObj[key] = formDataObj[key].trim();
        }
    });

    // IMPORTANT: DO NOT REMOVE password_confirm here, we need it for verification

    // Validate via FondationValidators
    if (typeof window.FondationValidators === 'undefined' || typeof FondationValidators.validate !== 'function') {
        console.error("FondationValidators not loaded or invalid");
        return { valid: false, errors: [{ field: null, message: l("Validator not available") }], data: {} };
    }

    // Determine schema based on mode
    const isUpdate = formDataObj.id && formDataObj.id.trim() !== '';
    const schemaName = isUpdate ? 'UserUpdate' : 'UserCreate';

    // In create mode, remove id to avoid sending an empty string to the server
    if (!isUpdate) {
        delete formDataObj.id;
    }
    const result = FondationValidators.validate(schemaName, formDataObj);

    // Additional validation for password / confirm (only if password is filled)
    if (formDataObj.password) {
        const pw = formDataObj.password.trim();
        const confirm = (formDataObj.password_confirm || '').trim();

        if (pw && !confirm) {
            result.valid = false;
            result.errors.push(l("Please confirm the password"));
        } else if (pw && confirm && pw !== confirm) {
            result.valid = false;
            result.errors.push(l("Passwords do not match"));
        }
    }

    // NOW remove password_confirm from the data sent to the server
    delete formDataObj.password_confirm;

    // In update mode, remove empty password (means "don't change password")
    // Must be stripped BEFORE sending to avoid server-side minLength rejection
    if (isUpdate && (!formDataObj.password || formDataObj.password.trim() === '')) {
        delete formDataObj.password;
    }

    // Collect group assignments (provided by Fondation::Group::UI::Bootstrap)
    if (typeof collectGroupAssignments === 'function') {
        formDataObj.groups = collectGroupAssignments();
    }

    return {
        valid: result.valid,
        errors: result.errors.map(msg => ({
            field: null,
            message: msg
        })),
        data: formDataObj
    };
}
    // Display errors (Bootstrap 5 style)
    function displayValidationErrors(errors) {
        $('.is-invalid').removeClass('is-invalid');
        $('.invalid-feedback').remove();

        errors.forEach(err => {
            let input = err.field ? document.getElementById(err.field) : null;

            if (!input) {
                const alert = $(`<div class="alert alert-danger mt-3">${err.message}</div>`);
                $('#user-form').prepend(alert);
                setTimeout(() => alert.fadeOut(3000), 8000);
                return;
            }

            input.classList.add('is-invalid');
            const feedback = document.createElement('div');
            feedback.className = 'invalid-feedback';
            feedback.textContent = err.message;
            input.parentNode.appendChild(feedback);
        });
    }

    // ────────────────────────────────────────────────
    // "Save" button
    // ────────────────────────────────────────────────
    $('#saveObject').on('click', async function() {
        const button = $(this);
        button.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> ' + l("Saving..."));

        try {
            const validation = await validateUserForm();
            if (!validation.valid) {
                displayValidationErrors(validation.errors);
                return;
            }

            const id = validation.data.id;
            const method = id ? 'PUT' : 'POST';
            const url = id ? `/api/user/${id}` : '/api/user';

            // id is in the URL path, not the request body
            delete validation.data.id;

            const response = await fetch(url, {
                method,
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(validation.data)
            });

            if (!response.ok) {
                const body = await response.json();
                // OpenAPI validation errors: {errors: [{message: "...", path: "..."}]}
                const msg = body?.errors?.map(e => e.message).join('; ')
                    || body?.message
                    || response.statusText;
                throw new Error(msg);
            }

            successBox(l("User saved successfully"));
            $('#user-form')[0].reset();
            $('#user-modal').modal('hide');
            $('#example').DataTable().ajax.reload(null, false);
        } catch (err) {
            console.error("Error saving:", err);
            alert(l("Error: ") + (err.message || l("Unknown error")));
        } finally {
            button.prop('disabled', false).text(l("Save"));
        }
    });
});

/**
 * Initialize the user table
 */
function initialiseTabUser() {
    $("#loading-icon").hide();
    $("#panel-user").show();

    tableau = $('#example').DataTable({
        "dom": "<'row'<'col-sm-4'l><'col-sm-4'B><'col-sm-4'f>>" + "<'row'<'col-sm-12'tr>>" + "<'row'<'col-sm-5'i><'col-sm-7'p>>",
        "buttons": ['copy', 'csv', 'excel'],
        "aLengthMenu": [[10,50,100,200,500,1000,-1], [10,50, 100, 200, 500, 1000, l("All")]],
        "iDisplayLength": 10,
        mark: true,
        fixedHeader: {
            header: true,
        },
        "search": {
            "regex": true,
        },
        "bPaginate": true,



( run in 1.607 second using v1.01-cache-2.11-cpan-f4a522933cf )