From f64138605d20654d993be7d63a2fc0bed4c3d8e8 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Fri, 18 Jul 2025 17:09:06 +0200 Subject: [PATCH] JS for manage page --- apps/family/static/family/js/achievements.js | 358 ++++++++++++++----- apps/family/templates/family/manage.html | 314 ++++++++-------- 2 files changed, 420 insertions(+), 252 deletions(-) diff --git a/apps/family/static/family/js/achievements.js b/apps/family/static/family/js/achievements.js index 5794bfe5..dba07f0d 100644 --- a/apps/family/static/family/js/achievements.js +++ b/apps/family/static/family/js/achievements.js @@ -8,8 +8,7 @@ var LOCK = false * Refresh the history table on the consumptions page. */ function refreshHistory () { - $('#history').load('/note/consos/ #history') - $('#most_used').load('/note/consos/ #most_used') + $('#history').load('/family/manage/ #history') } $(document).ready(function () { @@ -38,18 +37,14 @@ notes_display = [] buttons = [] // When the user searches an alias, we update the auto-completion -autoCompleteNote('note', 'note_list', notes, notes_display, - 'alias', 'note', 'user_note', 'profile_pic', function () { - if (buttons.length > 0 && $('#single_conso').is(':checked')) { - consumeAll() - return false - } +autoCompleteFamily('note', 'note_list', notes, notes_display, + 'note', 'user_note', 'profile_pic', function () { return true }) /** * Add a transaction from a button. - * @param dest Where the money goes + * @param fam Where the money goes * @param amount The price of the item * @param type The type of the transaction (content type id for RecurrentTransaction) * @param category_id The category identifier @@ -57,35 +52,32 @@ autoCompleteNote('note', 'note_list', notes, notes_display, * @param template_id The identifier of the button * @param template_name The name of the button */ -function addConso (dest, amount, type, category_id, category_name, template_id, template_name) { - var button = null +function addChallenge (id, name, amount) { + var challenge = null + /** Ajout de 1 à chaque clic d'un bouton déjà choisi */ buttons.forEach(function (b) { - if (b.id === template_id) { + if (b.id === id) { b.quantity += 1 - button = b + challenge = b } }) - if (button == null) { - button = { - id: template_id, - name: template_name, - dest: dest, + if (challenge == null) { + challenge = { + id: id, + name: name, quantity: 1, amount: amount, - type: type, - category_id: category_id, - category_name: category_name } - buttons.push(button) + buttons.push(challenge) } - const dc_obj = $('#double_conso') - if (dc_obj.is(':checked') || notes_display.length === 0) { - const list = dc_obj.is(':checked') ? 'consos_list' : 'note_list' + const dc_obj = true + + const list = 'consos_list' let html = '' - buttons.forEach(function (button) { - html += li('conso_button_' + button.id, button.name + - '' + button.quantity + '') + buttons.forEach(function (challenge) { + html += li('conso_button_' + challenge.id, challenge.name + + '' + challenge.quantity + '') }) document.getElementById(list).innerHTML = html @@ -95,13 +87,14 @@ function addConso (dest, amount, type, category_id, category_name, template_id, removeNote(button, 'conso_button', buttons, list)() }) }) - } else { consumeAll() } + } /** * Reset the page as its initial state. */ function reset () { + console.log("reset lancée") notes_display.length = 0 notes.length = 0 buttons.length = 0 @@ -113,7 +106,6 @@ function reset () { document.getElementById('profile_pic').src = '/static/member/img/default_picture.png' document.getElementById('profile_pic_link').href = '#' refreshHistory() - refreshBalance() LOCK = false } @@ -121,6 +113,7 @@ function reset () { * Apply all transactions: all notes in `notes` buy each item in `buttons` */ function consumeAll () { + console.log("consumeAll lancée") if (LOCK) { return } LOCK = true @@ -129,12 +122,12 @@ function consumeAll () { if (notes_display.length === 0) { document.getElementById('note').classList.add('is-invalid') - $('#note_list').html(li('', 'Ajoutez des émetteurs.', 'text-danger')) + $('#note_list').html(li('', 'Ajoutez des familles.', 'text-danger')) error = true } if (buttons.length === 0) { - $('#consos_list').html(li('', 'Ajoutez des consommations.', 'text-danger')) + $('#consos_list').html(li('', 'Ajoutez des défis.', 'text-danger')) error = true } @@ -143,79 +136,39 @@ function consumeAll () { return } - notes_display.forEach(function (note_display) { - buttons.forEach(function (button) { - consume(note_display.note, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount, - button.name + ' (' + button.category_name + ')', button.type, button.category_id, button.id) + notes_display.forEach(function (family) { + buttons.forEach(function (challenge) { + grantAchievement(family, challenge) }) }) } /** - * Create a new transaction from a button through the API. - * @param source The note that paid the item (type: note) - * @param source_alias The alias used for the source (type: str) - * @param dest The note that sold the item (type: int) - * @param quantity The quantity sold (type: int) - * @param amount The price of one item, in cents (type: int) - * @param reason The transaction details (type: str) - * @param type The type of the transaction (content type id for RecurrentTransaction) - * @param category The category id of the button (type: int) - * @param template The button id (type: int) + * Create a new achievement through the API. + * @param family The selected family + * @param challenge The selected challenge */ -function consume (source, source_alias, dest, quantity, amount, reason, type, category, template) { - $.post('/api/note/transaction/transaction/', +function grantAchievement (family, challenge) { + console.log("grant lancée",family,challenge) + $.post('/api/family/achievement/', { csrfmiddlewaretoken: CSRF_TOKEN, - quantity: quantity, - amount: amount, - reason: reason, - valid: true, - polymorphic_ctype: type, - resourcetype: 'RecurrentTransaction', - source: source.id, - source_alias: source_alias, - destination: dest, - template: template + family: family.id, + challenge: challenge.id, }) .done(function () { - if (!isNaN(source.balance)) { - const newBalance = source.balance - quantity * amount - if (newBalance <= -2000) { - addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' + - 'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000) - } else if (newBalance < 0) { - addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' + - 'but the emitter note %s is negative.'), [source_alias, source_alias]), 'warning', 30000) - } - if (source.membership && source.membership.date_end < new Date().toISOString()) { - addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source_alias]), - 'danger', 30000) - } - } reset() - }).fail(function (e) { - $.post('/api/note/transaction/transaction/', - { - csrfmiddlewaretoken: CSRF_TOKEN, - quantity: quantity, - amount: amount, - reason: reason, - valid: false, - invalidity_reason: 'Solde insuffisant', - polymorphic_ctype: type, - resourcetype: 'RecurrentTransaction', - source: source.id, - source_alias: source_alias, - destination: dest, - template: template - }).done(function () { - reset() - addMsg(gettext("The transaction couldn't be validated because of insufficient balance."), 'danger', 10000) - }).fail(function () { - reset() + addMsg("Défi validé pour la famille !", 'success', 5000) + }) + .fail(function (e) { + reset() + if (e.responseJSON) { errMsg(e.responseJSON) - }) + } else if (e.responseText) { + errMsg(e.responseText) + } else { + errMsg("Erreur inconnue lors de la création de l'achievement.") + } }) } @@ -261,3 +214,222 @@ function createshiny() { shiny_class.replace('btn-outline-dark', 'btn-outline-dark-shiny') } createshiny() + + + + + +/** + * Query the 20 first matched notes with a given pattern + * @param pattern The pattern that is queried + * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called. + */ +function getMatchedFamilies (pattern, fun) { + $.getJSON('/api/family/family/?format=json&alias=' + pattern + '&search=family', fun) +} + +/** + * Generate a
  • entry with a given id and text + */ +function li (id, text, extra_css) { + return '
  • ' + text + '
  • \n' +} + + +/** + * Génère un champ d'auto-complétion pour rechercher une famille par son nom (version simplifiée sans alias) + * @param field_id L'identifiant du champ texte où le nom est saisi + * @param family_list_id L'identifiant du bloc div où les familles sélectionnées sont affichées + * @param families Un tableau contenant les objets famille sélectionnés + * @param families_display Un tableau contenant les infos des familles sélectionnées : [nom, id, objet famille, quantité] + * @param family_prefix Le préfixe des
  • pour les familles sélectionnées + * @param user_family_field L'identifiant du champ qui affiche la famille survolée (optionnel) + * @param profile_pic_field L'identifiant du champ qui affiche la photo de la famille survolée (optionnel) + * @param family_click Fonction appelée lors du clic sur un nom. Si elle existe et ne retourne pas true, la famille n'est pas affichée. + */ +function autoCompleteFamily(field_id, family_list_id, families, families_display, family_prefix = 'family', user_family_field = null, profile_pic_field = null, family_click = null) { + const field = $('#' + field_id) + console.log("autoCompleteFamily commence") + // Configuration du tooltip + field.tooltip({ + html: true, + placement: 'bottom', + title: 'Chargement...', + trigger: 'manual', + container: field.parent(), + fallbackPlacement: 'clockwise' + }) + + // Masquer le tooltip lors d'un clic ailleurs + $(document).click(function (e) { + if (!e.target.id.startsWith(family_prefix)) { + field.tooltip('hide') + } + }) + + let old_pattern = null + + // Réinitialiser la recherche au clic + field.click(function () { + field.tooltip('hide') + field.removeClass('is-invalid') + field.val('') + old_pattern = '' + }) + + // Sur "Entrée", sélectionner la première famille + field.keypress(function (event) { + if (event.originalEvent.charCode === 13 && families.length > 0) { + const li_obj = field.parent().find('ul li').first() + displayFamily(families[0], families[0].name, user_family_field, profile_pic_field) + li_obj.trigger('click') + } + }) + + // Mise à jour des suggestions lors de la saisie + field.keyup(function (e) { + field.removeClass('is-invalid') + + if (e.originalEvent.charCode === 13) { return } + + const pattern = field.val() + + if (pattern === old_pattern) { return } + old_pattern = pattern + families.length = 0 + + if (pattern === '') { + field.tooltip('hide') + families.length = 0 + return + } + + // Appel à l'API pour récupérer les familles correspondantes + $.getJSON('/api/family/family/?format=json&search=' + pattern, + function (results) { + if (pattern !== $('#' + field_id).val()) { return } + + let matched_html = '' + + field.attr('data-original-title', matched_html).tooltip('show') + + results.results.forEach(function (family) { + const family_obj = $('#' + family_prefix + '_' + family.id) + family_obj.hover(function () { + displayFamily(family, family.name, user_family_field, profile_pic_field) + }) + family_obj.click(function () { + var disp = null + families_display.forEach(function (d) { + if (d.id === family.id) { + d.quantity += 1 + disp = d + } + }) + if (disp == null) { + disp = { + name: family.name, + id: family.id, + family: family, + quantity: 1 + } + families_display.push(disp) + } + + if (family_click && !family_click()) { return } + + const family_list = $('#' + family_list_id) + let html = '' + families_display.forEach(function (disp) { + html += li(family_prefix + '_' + disp.id, + disp.name + + '' + + disp.quantity + '', + '') + }) + + family_list.html(html) + field.tooltip('update') + + families_display.forEach(function (disp) { + const line_obj = $('#' + family_prefix + '_' + disp.id) + line_obj.hover(function () { + displayFamily(disp.family, disp.name, user_family_field, profile_pic_field) + }) + line_obj.click(removeFamily(disp, family_prefix, families_display, family_list_id, user_family_field, + profile_pic_field)) + }) + }) + }) + }) + }) +} + +/** + * Affiche le nom et la photo d'une famille + * @param family L'objet famille à afficher + * @param user_family_field L'identifiant du champ où afficher le nom (optionnel) + * @param profile_pic_field L'identifiant du champ où afficher la photo (optionnel) + */ +function displayFamily(family, user_family_field = null, profile_pic_field = null) { + if (!family.display_image) { + family.display_image = '/static/member/img/default_picture.png' + } + if (user_family_field !== null) { + $('#' + user_family_field).removeAttr('class') + $('#' + user_family_field).text(family.name) + if (profile_pic_field != null) { + $('#' + profile_pic_field).attr('src', family.display_image) + // Si tu veux un lien vers la page famille : + $('#' + profile_pic_field + '_link').attr('href', '/family/detail/' + family.id + '/') + } + } +} + + +/** + * Retire une famille de la liste sélectionnée. + * @param d La famille à retirer + * @param family_prefix Le préfixe des
  • + * @param families_display Le tableau des familles sélectionnées + * @param family_list_id L'id du bloc où sont affichées les familles + * @param user_family_field Champ d'affichage (optionnel) + * @param profile_pic_field Champ photo (optionnel) + * @returns une fonction compatible avec les événements jQuery + */ +function removeFamily(d, family_prefix, families_display, family_list_id, user_family_field = null, profile_pic_field = null) { + return function () { + const new_families_display = [] + let html = '' + families_display.forEach(function (disp) { + if (disp.quantity > 1 || disp.id !== d.id) { + disp.quantity -= disp.id === d.id ? 1 : 0 + new_families_display.push(disp) + html += li(family_prefix + '_' + disp.id, disp.name + + '' + disp.quantity + '') + } + }) + + families_display.length = 0 + new_families_display.forEach(function (disp) { + families_display.push(disp) + }) + + $('#' + family_list_id).html(html) + families_display.forEach(function (disp) { + const obj = $('#' + family_prefix + '_' + disp.id) + obj.click(removeFamily(disp, family_prefix, families_display, family_list_id, user_family_field, profile_pic_field)) + obj.hover(function () { + displayFamily(disp.family, user_family_field, profile_pic_field) + }) + }) + } +} \ No newline at end of file diff --git a/apps/family/templates/family/manage.html b/apps/family/templates/family/manage.html index 84506884..275337fa 100644 --- a/apps/family/templates/family/manage.html +++ b/apps/family/templates/family/manage.html @@ -9,180 +9,176 @@ SPDX-License-Identifier: GPL-3.0-or-later {% block content %}
    -
    -
    -
    - {# User details column #} -
    -
    - - - -
    - {% trans "Please select a family" %} -
    -
    -
    +
    +
    + {# Family details column #} +
    +
    + + + +
    + {% trans "Please select a family" %} +
    +
    +
    - {# Family selection column #} -
    + {# Family selection column #} +
    +
    +
    +

    + {% trans "Families" %} +

    +
    +
    +
      +
      + {# User search with autocompletion #} + +
      +
      + + {# Summary of challenges and validate button #} +
      +
      +
      +

      + {% trans "Challenges" %} +

      +
      +
      +
        +
        + +
        +
        +
        + + {# Create family/challenge buttons #}
        -
        -

        - {% trans "Families" %} -

        -
        -
        -
          -
          - {# User search with autocompletion #} - -
          -
          - - {# Summary of challenges and validate button #} -
          -
          -
          -

          - {% trans "Challenges" %} -

          -
          -
          -
            -
            - -
            -
            -
            - - {# Create family/challenge buttons #} -
            -

            - {% trans "Create a family or challenge" %} -

            -
            - {% if can_add_family %} - - {% trans "Add a family" %} - - {% endif %} - {% if can_add_challenge %} - - {% trans "Add a challenge" %} - - {% endif %} -
            -
            -
            - - {# Buttons column #} -
            -
            - {# Tabs for list and search #} - - - {# Tabs content #} -
            -
            -
            -
            - {% for challenge in all_challenges %} - - {% endfor %} -
            -
            - + {% endif %} +
            +
            +
            + + {# Buttons column #} +
            +
            + {# Tabs for list and search #} + + + {# Tabs content #} +
            +
            +
            +
            + {% for challenge in all_challenges %} + + {% endfor %} +
            +
            + +
            +
            + + {# Mode switch #} +
            -
            - - {# Mode switch #} -
            -
            {# transaction history #} -
            -
            -

            - {% trans "Recent achievements history" %} -

            -
            - {% render_table table %} -
            +
            +
            +

            + {% trans "Recent achievements history" %} +

            +
            + {% render_table table %} +
            {% endblock %} -{% block extrajavascript %} - - + + {% endfor %} + + {% for challenge in all_challenges %} + document.getElementById("search_challenge{{ challenge.id }}").addEventListener("click", function() { + addChallenge({{ challenge.id}}, "{{ challenge.name|escapejs }}", {{ challenge.points }}); + }); + {% endfor %} + {% endblock %} \ No newline at end of file