diff --git a/apps/note/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py
index 12530c6e..265870a8 100644
--- a/apps/note/templatetags/pretty_money.py
+++ b/apps/note/templatetags/pretty_money.py
@@ -11,7 +11,7 @@ def pretty_money(value):
abs(value) // 100,
)
else:
- return "{:s}{:d} € {:02d}".format(
+ return "{:s}{:d}.{:02d} €".format(
"- " if value < 0 else "",
abs(value) // 100,
abs(value) % 100,
diff --git a/apps/note/views.py b/apps/note/views.py
index 82f2f4aa..f950fd73 100644
--- a/apps/note/views.py
+++ b/apps/note/views.py
@@ -141,7 +141,7 @@ class ConsoView(LoginRequiredMixin, SingleTableView):
"""
context = super().get_context_data(**kwargs)
context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \
- .order_by('category').order_by('name')
+ .order_by('category__name', 'name')
context['title'] = _("Consumptions")
context['polymorphic_ctype'] = ContentType.objects.get_for_model(TemplateTransaction).pk
diff --git a/apps/scripts b/apps/scripts
deleted file mode 160000
index 123466cf..00000000
--- a/apps/scripts
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 123466cfa914422422cd372197e64adf65ef05f7
diff --git a/static/js/base.js b/static/js/base.js
new file mode 100644
index 00000000..a2ad87c9
--- /dev/null
+++ b/static/js/base.js
@@ -0,0 +1,54 @@
+// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+/**
+ * Perform a request on an URL and get result
+ * @param url The url where the request is performed
+ * @param success The function to call with the request
+ * @param data The data for the request (optional)
+ */
+function getJSONSync(url, success, data) {
+ $.ajax({
+ url: url,
+ dataType: 'json',
+ data: data,
+ async: false,
+ success: success
+ });
+}
+
+/**
+ * Convert balance in cents to a human readable amount
+ * @param value the balance, in cents
+ * @returns {string}
+ */
+function pretty_money(value) {
+ if (value % 100 === 0)
+ return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + " €";
+ else
+ return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + "." + (Math.abs(value) % 100) + " €";
+}
+
+/**
+ * Reload the balance of the user on the right top corner
+ */
+function refreshBalance() {
+ $("#user_balance").load("/ #user_balance");
+}
+
+/**
+ * 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.
+ * This function is synchronous.
+ */
+function getMatchedNotes(pattern, fun) {
+ getJSONSync("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club", function(aliases) {
+ aliases.results.forEach(function(alias) {
+ getJSONSync("/api/note/note/" + alias.note + "/?format=json", function (note) {
+ fun(note, alias);
+ console.log(alias.name);
+ });
+ });
+ });
+}
diff --git a/static/js/consos.js b/static/js/consos.js
new file mode 100644
index 00000000..d3075edd
--- /dev/null
+++ b/static/js/consos.js
@@ -0,0 +1,227 @@
+// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+/**
+ * Refresh the history table on the consumptions page.
+ */
+function refreshHistory() {
+ $("#history").load("/note/consos/ #history");
+}
+
+$(document).ready(function() {
+ // If hash of a category in the URL, then select this category
+ // else select the first one
+ if (location.hash) {
+ $("a[href='" + location.hash + "']").tab("show");
+ } else {
+ $("a[data-toggle='tab']").first().tab("show");
+ }
+
+ // When selecting a category, change URL
+ $(document.body).on("click", "a[data-toggle='tab']", function() {
+ location.hash = this.getAttribute("href");
+ });
+});
+
+let old_pattern = null;
+let notes = [];
+let notes_display = [];
+let buttons = [];
+
+// When the user clicks on the search field, it is immediately cleared
+let note_obj = $("#note");
+note_obj.click(function() {
+ note_obj.val("");
+});
+
+function li(id, text) {
+ return "
" + text + "\n";
+}
+
+/**
+ * Render note name and picture
+ * @param note The note to render
+ * @param alias The alias to be displayed
+ * @param user_note_field
+ * @param profile_pic_field
+ */
+function displayNote(note, alias, user_note_field=null, profile_pic_field=null) {
+ let img = note == null ? null : note.display_image;
+ if (img == null)
+ img = '/media/pic/default.png';
+ if (note !== null && alias !== note.name)
+ alias += " (aka. " + note.name + ")";
+ if (note !== null && user_note_field !== null)
+ $("#" + user_note_field).text(alias + " : " + pretty_money(note.balance));
+ if (profile_pic_field != null)
+ $("#" + profile_pic_field).attr('src', img);
+}
+
+function remove_conso(c, obj, note_prefix="note") {
+ return (function() {
+ let new_notes_display = [];
+ let html = "";
+ notes_display.forEach(function (disp) {
+ if (disp[3] > 1 || disp[1] !== c[1]) {
+ disp[3] -= disp[1] === c[1] ? 1 : 0;
+ new_notes_display = new_notes_display.concat([disp]);
+ html += li(note_prefix + "_" + disp[1], disp[0]
+ + "" + disp[3] + "");
+ }
+ });
+ $("#note_list").html(html);
+ notes_display.forEach(function (disp) {
+ obj = $("#" + note_prefix + "_" + disp[1]);
+ obj.click(remove_conso(disp, obj, note_prefix));
+ obj.hover(function() {
+ displayNote(disp[2], disp[0]);
+ });
+ });
+ notes_display = new_notes_display;
+ });
+}
+
+
+function autoCompleteNote(field_id, alias_matched_id, alias_prefix="alias", note_prefix="note",
+ user_note_field=null, profile_pic_field=null) {
+ let field = $("#" + field_id);
+ field.keyup(function() {
+ let pattern = field.val();
+ // If the pattern is not modified, or if the field is empty, we don't query the API
+ if (pattern === old_pattern || pattern === "")
+ return;
+
+ old_pattern = pattern;
+
+ notes = [];
+ let aliases_matched_obj = $("#" + alias_matched_id);
+ let aliases_matched_html = "";
+ getMatchedNotes(pattern, function(note, alias) {
+ aliases_matched_html += li("alias_" + alias.normalized_name, alias.name);
+ note.alias = alias;
+ notes = notes.concat([note]);
+ });
+
+ aliases_matched_obj.html(aliases_matched_html);
+
+ notes.forEach(function (note) {
+ let alias = note.alias;
+ let alias_obj = $("#" + alias_prefix + "_" + alias.normalized_name);
+ alias_obj.hover(function() {
+ displayNote(note, alias.name, user_note_field, profile_pic_field);
+ });
+
+ alias_obj.click(function() {
+ field.val("");
+ var disp = null;
+ notes_display.forEach(function(d) {
+ if (d[1] === note.id) {
+ d[3] += 1;
+ disp = d;
+ }
+ });
+ if (disp == null)
+ notes_display = notes_display.concat([[alias.name, note.id, note, 1]]);
+ let note_list = $("#note_list");
+ let html = "";
+ notes_display.forEach(function(disp) {
+ html += li("note_" + disp[1], disp[0]
+ + "" + disp[3] + "");
+ });
+
+ note_list.html(html);
+
+ notes_display.forEach(function(disp) {
+ let line_obj = $("#" + note_prefix + "_" + disp[1]);
+ line_obj.hover(function() {
+ displayNote(disp[2], disp[0], user_note_field, profile_pic_field);
+ });
+
+ line_obj.click(remove_conso(disp, note_prefix));
+ });
+ });
+ });
+ });
+}
+
+
+// When the user searches an alias, we update the auto-completion
+autoCompleteNote("note", "alias_matched", "alias", "note",
+ "user_note", "profile_pic");
+
+/**
+ * Add a transaction from a button.
+ * @param dest Where the money goes
+ * @param amount The price of the item
+ * @param type The type of the transaction (content type id for TemplateTransaction)
+ * @param category_id The category identifier
+ * @param category_name The category name
+ * @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;
+ buttons.forEach(function(b) {
+ if (b[5] === template_id) {
+ b[1] += 1;
+ button = b;
+ }
+ });
+ if (button == null)
+ buttons = buttons.concat([[dest, 1, amount, type, category_id, category_name, template_id, template_name]]);
+
+ // TODO Only in simple consumption mode
+ if (notes.length > 0)
+ consumeAll();
+}
+
+/**
+ * Apply all transactions: all notes in `notes` buy each item in `buttons`
+ */
+function consumeAll() {
+ notes.forEach(function(note) {
+ buttons.forEach(function(button) {
+ consume(note.id, button[0], button[1], button[2], button[7] + " (" + button[5] + ")",
+ button[3], button[4], button[6]);
+ });
+ });
+}
+
+/**
+ * Create a new transaction from a button through the API.
+ * @param source The note that paid the item (type: int)
+ * @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 TemplateTransaction)
+ * @param category The category id of the button (type: int)
+ * @param template The button id (type: int)
+ */
+function consume(source, dest, quantity, amount, reason, type, category, template) {
+ $.post("/api/note/transaction/transaction/",
+ {
+ "csrfmiddlewaretoken": CSRF_TOKEN,
+ "quantity": quantity,
+ "amount": amount,
+ "reason": reason,
+ "valid": true,
+ "polymorphic_ctype": type,
+ "resourcetype": "TemplateTransaction",
+ "source": source,
+ "destination": dest,
+ "category": category,
+ "template": template
+ }, function() {
+ notes_display = [];
+ notes = [];
+ buttons = [];
+ old_pattern = null;
+ $("#note_list").html("");
+ $("#alias_matched").html("");
+ displayNote(null, "");
+ refreshHistory();
+ refreshBalance();
+ });
+}
diff --git a/templates/base.html b/templates/base.html
index 43f1ae5f..2f2f4ab4 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -46,6 +46,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
crossorigin="anonymous">
+
{# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
{% if form.media %}
diff --git a/templates/note/conso_form.html b/templates/note/conso_form.html
index 64205108..9b096df3 100644
--- a/templates/note/conso_form.html
+++ b/templates/note/conso_form.html
@@ -25,7 +25,7 @@
@@ -125,184 +125,21 @@
min-width: 100%;
}
-
-
-
-
-
-
-
-
-
-
-
{% endblock %}
{% block extrajavascript %}
+
{% endblock %}