5

Ninox Scheduler powered by FullCalendar and Ninext

Hello everyone,

Since several members have expressed interest in this type of implementation over the past months, I am happy to share a timeline resource scheduler built on the FullCalendar library and the Ninext plugin.

The starting point is the standard Ninox Tasks template, which I modified slightly to add a dedicated "Scheduler" page. From the calendar view, you can:

  • Open an existing task directly
  • Add a new project
  • Select a time interval to create a new task for a specific project

Use the Refresh button to reflect any changes on the calendar. The implementation is quite efficient, as the calendar fetches only the tasks within the currently visible date range.

FullCalendar offers extensive customisation options — you can explore the full property reference at https://v7.fullcalendar.io. According to the documentation, the library can be used free of charge for evaluation purposes, with no time limit.

One limitation I have encountered in a multi-user database context: the calendar does not automatically refresh when table data changes are made by other users.    would there be any way to address this through Ninext? I would be very grateful for your thoughts.

All the best,
Sotirios

6 replies

null
    • Ninox developper
    • Jacques_TUR
    • 2 days ago
    • Reported - view

    Great work Sotirios! 🎉

    To have the calendar refresh automatically on every data change, here is the full reasoning and the steps to follow.


    Why doesn't the calendar refresh automatically?

    Ninox does re-evaluate the formula containing the calendar every time a referenced piece of data is modified. The problem is that the HTML content generated by the formula doesn't change — and that is precisely what Ninox uses to decide whether it needs to recreate the element on screen.

    One could force that change by making the HTML dynamic, but that would recreate the calendar entirely on every modification: the selected view (day / week / month), the displayed date, and all user settings would be reset each time. That makes for a poor user experience.

    What we want is to refresh only the data of the calendar, without recreating its interface. That is exactly what the Refresh button already in the toolbar does:

    calendar.refetchResources();
    calendar.refetchEvents();

    Step 1 — Make the calendar object accessible from outside

    These two instructions run from inside the <script> that creates the calendar. To be able to call them from outside — that is, from the code Ninox runs each time it re-evaluates the formula — we need to keep a reference to the calendar object.

    The best approach is to attach it directly to the HTML element that contains the calendar (calendarEl). We therefore add this line right after calendar.render():

    calendar.render();
    calendarEl.calendar = calendar;  // ← new

    The calendar object is now accessible from any code that can retrieve the calendarEl element from the DOM.


    Step 2 — Add the refresh code at the top of the formula

    The following JavaScript block is placed at the very beginning of the Ninox formula, using Ninext's #{ ... }# syntax. This code runs every time the formula is re-evaluated — that is, on every data change in the referenced tables (here Projectsand Tasks).

    It looks up the calendarEl element in the DOM, checks whether a calendar is attached to it, and if so, triggers the refresh:

    #{
        var calendarEl = document.getElementById('calendar');
        if (calendarEl?.calendar) {
            calendarEl.calendar.refetchResources();
            calendarEl.calendar.refetchEvents();
        }
    }#;

    If the calendar hasn't been created yet (first load), calendarEl?.calendar will be undefined and the block will do nothing — which is exactly the expected behaviour.


    Complete formula

    #{
        var calendarEl = document.getElementById('calendar');
        if (calendarEl?.calendar) {
            calendarEl.calendar.refetchResources();
            calendarEl.calendar.refetchEvents();
        }
    }#;
    
    function getProjects() do
        let _projects := (select Projects).{
                id: number(Id),
                title: Projects,
                groupId: text(Status)
            };
        _projects
    end;
    function getTasks(startView : number,endView : number) do
        let _tasks := (select Tasks where Start <= date(endView) and End >= date(startView)).{
                id: number(Id),
                title: Designation + "  (" + Assignee.Name + ")",
                resourceId: Project,
                start: format(Start, "YYYY-MM-DDTHH:mm"),
                end: format(End, "YYYY-MM-DDTHH:mm"),
                color: switch number(Status) do
                case 1:
                    "yellow"
                case 2:
                    "blue"
                case 3:
                    "green"
                case 4:
                    "orange"
                case 5:
                    "red"
                end
            };
        _tasks
    end;
    function addProject() do
        popupRecord(create Projects)
    end;
    function addTask(startTime : number,endTime : number,projectId : text) do
        let newTask := (create Tasks);
        newTask.(
            Start := startTime;
            End := endTime;
            Project := number(projectId)
        );
        popupRecord(newTask)
    end;
    function openTask(id : text) do
        popupRecord(record(Tasks,number(id)))
    end;
    let style1 := ---
     <style> { text(http("GET", "https://cdn.jsdelivr.net/npm/fullcalendar@7.0.0-beta.8/skeleton.css").result) } </style>
        ---;
    let style2 := ---
     <style> { text(http("GET", "https://cdn.jsdelivr.net/npm/fullcalendar@7.0.0-beta.8/themes/breezy/theme.css").result) } </style>
        ---;
    let style3 := ---
     <style> { text(http("GET", "https://cdn.jsdelivr.net/npm/fullcalendar@7.0.0-beta.8/themes/breezy/palettes/indigo.css").result) } </style>
        ---;
    let script1 := ---
     <script> { text(http("GET", "https://cdn.jsdelivr.net/npm/fullcalendar@7.0.0-beta.8/all.global.js").result) } </script>
        ---;
    let script2 := ---
     <script> { text(http("GET", "https://cdn.jsdelivr.net/npm/fullcalendar@7.0.0-beta.8/themes/breezy/global.js").result) } </script>
        ---;
    let script3 := ---
     <script> { text(http("GET", "https://cdn.jsdelivr.net/npm/fullcalendar-scheduler@7.0.0-beta.8/all.global.js").result) } </script>
        ---;
    html(style1 + style2 + style3 + script1 + script2 + script3 +
    "<body>
    <div id='calendar'></div>
    <script>
        var calendarEl = document.getElementById('calendar');      var calendar = new FullCalendar.Calendar(calendarEl, {
            schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives',
            timeZone: 'local',
            height: 'auto',
            nowIndicator: true,
            initialView: 'resourceTimelineDay',
            borderless: false,
            colorScheme: 'light',
            buttons: {
                refresh: {
                    text: 'Refresh',
                    click() {
                        calendar.refetchResources();
                        calendar.refetchEvents();
                    }
                },
                addProject: {
                    text: 'Add Project',
                    click() {
                        ninext.callNinoxFunction('addProject', 'calendar');
                    }
                }
            },
            headerToolbar: {
                left: 'refresh addProject prev,today,next',
                center: 'title',
                right: 'resourceTimelineDay,resourceTimelineWeek,resourceTimelineMonth',
            },
            editable: true,
            selectable: true,
            select: function (info) {
                ninext.callNinoxFunction('addTask', 'calendar', info.start.valueOf(), info.end.valueOf(), info.resource.id);
            },
            eventClick: function (info) {
                ninext.callNinoxFunction('openTask', 'calendar', info.event.id);
            },
            resourceColumnHeaderContent: 'Projects',
            resourceGroupField: 'groupId',
            resources: function (fetchInfo, successCallback, failureCallback) {
                ninext.callNinoxFunction('getProjects', 'calendar', function (error, result) {
                    if (error) {
                        failureCallback(error);
                    } else {
                        successCallback(result);
                    }
                });
            },
            events: function (fetchInfo, successCallback, failureCallback) {
                ninext.callNinoxFunction('getTasks', 'calendar', fetchInfo.start.valueOf(), fetchInfo.end.valueOf(), function (error, result) {
                    if (error) {
                        failureCallback(error);
                    } else {
                        successCallback(result);
                    }
                });
            }
        });
        calendar.render();
        calendarEl.calendar = calendar; // <- new
    </script>
    </body>
    ")
      • szormpas
      • 3 hrs ago
      • Reported - view

        Thanks for this elegant solution! The reasoning behind it is just as helpful as the implementation itself.

      I tried the solution, and it works exactly as described. I did notice some random refreshes now and then, but nothing that really gets in the way.

      Ninext takes Ninox to the next level. You can inject and run JavaScript in the formula evaluation cycle, which opens up possibilities that just wouldn't be possible in native Ninox. It takes a powerful no-code tool and turns it into a development environment that you can really extend.

      A visual scheduler is a handy tool for lots of Ninox applications, and I hope other users find it useful.

      Thanks again for explaining this so thoroughly!

    • Rafael Sanchis
    • Rafael_Sanchis
    • 2 days ago
    • Reported - view

     

    I've switched to week and month with a few examples, it works, but it could be improved,

    I'm using Ninex as the app 

      • szormpas
      • 2 days ago
      • Reported - view

        there are plenty of potential use cases for this kind of scheduler.

      You can explore the available themes and get a feel for the different views here: https://themes.fullcalendar.io

      • Rafael Sanchis
      • Rafael_Sanchis
      • 2 days ago
      • Reported - view

       

      I develop this with IA Claude and I can Open an existing Meetings, Email and Deals directly.

      • Rafael Sanchis
      • Rafael_Sanchis
      • yesterday
      • Reported - view

       

      Share a little DB with calendar

Content aside

  • 5 Likes
  • 3 hrs agoLast active
  • 6Replies
  • 64Views
  • 3 Following