login as:
~/abapcraft.dev — code, crafted in SAP
florin@abapcraft:~/abap/posts/fiori-to-do-in-synology-docker/webapp/controller/Main.controller.js $ cat Main.controller.js
fiori-to-do-in-synology-docker / webapp / controller / Main.controller.js
JAVASCRIPT 133 lines
sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/ui/model/Filter",
  "sap/ui/model/FilterOperator",
  "sap/m/MessageToast",
  "sap/ui/core/format/DateFormat"
], function (Controller, Filter, FilterOperator, MessageToast, DateFormat) {
  "use strict";

  var API = "/todos";

  return Controller.extend("todo.controller.Main", {

    // ── Formatter ────────────────────────────────────────────────────────────

    formatDate: function (sValue) {
      if (!sValue) return "";
      var oParser    = DateFormat.getDateInstance({ pattern: "yyyy-MM-dd" });
      var oFormatter = DateFormat.getDateInstance({ style: "medium" });
      var oDate = oParser.parse(sValue);
      return oDate ? oFormatter.format(oDate) : "";
    },

    // ── Helpers ──────────────────────────────────────────────────────────────

    _updateActiveCount: function () {
      var oModel = this.getView().getModel();
      oModel.setProperty("/activeCount",
        oModel.getProperty("/todos").filter(function (t) { return !t.done; }).length
      );
    },

    _applyFilter: function () {
      var sKey = this.byId("filterBtn").getSelectedKey();
      var oBinding = this.byId("todoList").getBinding("items");
      var aFilters = [];
      if (sKey === "active") aFilters = [new Filter("done", FilterOperator.EQ, false)];
      if (sKey === "done")   aFilters = [new Filter("done", FilterOperator.EQ, true)];
      oBinding.filter(aFilters);
    },

    // ── Event handlers ────────────────────────────────────────────────────────

    onAddTodo: function () {
      var oModel = this.getView().getModel();
      var sTitle = oModel.getProperty("/newTodo").trim();
      if (!sTitle) { MessageToast.show("Please enter a task first."); return; }

      var oNewTodo = {
        title: sTitle,
        done: false,
        dueDate: oModel.getProperty("/newDueDate")
      };

      // POST → wait for the server's response to get the auto-assigned id
      fetch(API, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(oNewTodo)
      })
      .then(function (res) { return res.json(); })
      .then(function (oCreated) {
        var aTodos = oModel.getProperty("/todos");
        aTodos.push(oCreated);
        oModel.setProperty("/todos", aTodos);
        oModel.setProperty("/newTodo", "");
        oModel.setProperty("/newDueDate", "");
        this._updateActiveCount();
        this._applyFilter();
      }.bind(this))
      .catch(function () {
        MessageToast.show("Could not save — is the API server running?");
      });
    },

    // Two-way binding already updated the model before this fires.
    // Just PATCH the changed field and refresh the count.
    onToggleDone: function (oEvent) {
      var oCtx  = oEvent.getSource().getBindingContext();
      var iId   = oCtx.getProperty("id");
      var bDone = oCtx.getProperty("done");

      fetch(API + "/" + iId, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ done: bDone })
      });

      this._updateActiveCount();
    },

    onDeleteTodo: function (oEvent) {
      var oCtx   = oEvent.getSource().getBindingContext();
      var iId    = oCtx.getProperty("id");
      var iIndex = parseInt(oCtx.getPath().split("/").pop(), 10);

      // Optimistic update: remove from model immediately, then DELETE on the server
      fetch(API + "/" + iId, { method: "DELETE" });

      var oModel = this.getView().getModel();
      var aTodos = oModel.getProperty("/todos");
      aTodos.splice(iIndex, 1);
      oModel.setProperty("/todos", aTodos);
      this._updateActiveCount();
      this._applyFilter();
    },

    onClearCompleted: function () {
      var oModel = this.getView().getModel();
      var aTodos = oModel.getProperty("/todos");

      // Fire a DELETE for each completed todo
      aTodos.filter(function (t) { return t.done; }).forEach(function (t) {
        fetch(API + "/" + t.id, { method: "DELETE" });
      });

      oModel.setProperty("/todos", aTodos.filter(function (t) { return !t.done; }));
      this._updateActiveCount();
      this._applyFilter();
    },

    onFilterChange: function () {
      this._applyFilter();
    },

    onItemPress: function (oEvent) {
      var iId = oEvent.getParameter("listItem").getBindingContext().getProperty("id");
      this.getOwnerComponent().getRouter().navTo("detail", { id: iId });
    }

  });
});