Week Month Planer

Just as I posted the prior notice for this article, a client asked me for a task summary of their current projects.
I was very pleased when I showed him my sample file for the weekly / monthly planner and he immediately said
“That’s exactly what we need”.

A few thoughts in advance:

Putting dates in filemaker into a temporal context is always a bit difficult, at least for me.

The actual format of an appointment is actually very simple:
There is a start and end date, a date title and text.
Often there are attached files or links to other records like projects, employees or contacts.

Mostly it ends up in the known portal column layouts that show weekdays.
This always looks very much like a plain display of a series of appointments and always feels a bit out of time:

You can only display a fixed number of rows within an appointment and such common handlings as drag and
drop to sort or move appointments is always a challenge and never really feels good solved natively under Filemaker.
Row height is not flexible, column width is fixed, sort columns is terrible, Webviews cannot be used in rows…I could go on forever.

One of my colleagues has at least made the display contemporary so it doesn’t look like a spreadsheet, but Filemaker
just has its limitations with portals.

In a webview with JQUERY on the other hand, and I may repeat myself for the umpteenth time, the possibilities are
almost unlimited, the presentation extremely flexible and almost every handling feature is possible there.

The gamechanger for me was, after the incredibly patient and capable Aaron Giard introduced me to the wonderful
world of JQUERY, a JQUERY calendar that probably every Fielmaker programmer has seen by now:
The Fullcalendar by Adam Shaw.

All the features you could wish for in a calendar were brought together here in a very nice and customizable
interface.

The idea of the Fullcalendar and the great flexible JQUERY portlets can be used very well to create an
overview tool for tasks.

We want to create a weekly or monthly planner that provides a quick overview of the tasks at hand.

The display should show at a glance the workload of a week or month and the overdue tasks. Furthermore,
it must be possible to easily redistribute the tasks.

Many of the techniques we have already discussed in other articles and I will always try to link to the
appropriate articles at the appropriate places.

Two notes before we really get into it:

1) I didn’t put the icons in this project as an exported file in the temporary directory, but include them
directly in the webview by a customized swap – much more elegant 🙂

2) In this tool we use a Days table instead of having to deal with calculations of calendar weeks or
which day is Monday of the week etc.. If you create all days from 01/01/2017 to 12/31/2035 like here,
it needs 6939 records – and believe me after working with the table instead of calculations, I can only
recommend this to everyone. After all, we have a database program and it is so much easier through
the individual records !

Enough talk – let’s code !

Basically the weekly / monthly planner is an extension of the portlet examples I described in the
articles “JQUERY Portlets“ and „Kanban“. Therefore I don’t go into all the details here, please just
have a look at the two articles if you are interested in the background.

First we take a look at the tables used:

As always we have a Parameter table with a single record containing all the JQUERY libraries or
icons and some settings for our display.

Bildschirmfoto 2022 08 30 um 16 06 17

The field ‘HTML_Portlet’ contains as text the basis for the presentation of the planner and the
functions of the portlets.
All entries with a preceding ‘__’ will later be replaced by other contents in a formula field.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>__jquery-ui.css</style>
<script type="text/javascript">__jquery-1.10.2.js</script>
<script type="text/javascript">__jquery-ui.js</script>
<style>
html, body{
margin: 0px;
padding: 1px;
font-family: 'arial';
font-size: 0.8em;
}
body {
}
p {
margin-top: 4px;
margin-bottom: 0px;
}

.parent {
white-space: nowrap;
}

.ui-tooltip {
padding: 6px 6px;
color: black;
background-color: #edeef2;
position: absolute;
border: 1px solid #767676;
max-width: 180px;
z-index: 9999;
box-shadow:
0 2.8px 2.2px rgba(0, 0, 0, 0.034),
0 6.7px 5.3px rgba(0, 0, 0, 0.048),
0 12.5px 10px rgba(0, 0, 0, 0.06),
0 22.3px 17.9px rgba(0, 0, 0, 0.072),
0 41.8px 33.4px rgba(0, 0, 0, 0.086),
0 100px 80px rgba(0, 0, 0, 0.12)
}

.ui-icon-comment {
cursor:pointer;
}
.ui-icon-document {
cursor:pointer;
}
.ui-icon-bullet {
cursor:pointer;
}
.ui-icon-check {
cursor:pointer;
}
.ui-icon-person {
cursor:pointer;
}
.columnHeader {
width: __columnHeaderWidthpx;
display: block;
margin: 0px 4px 4px 0px;
padding-top: 4px;
padding-bottom: 4px;
}
.columnHeaderDays {
width: __columnHeaderDaysWidthpx;
display: block;
margin: 0px 4px 4px 0px;
padding-top: 4px;
padding-bottom: 4px;
}
.column {
width: __columnWidthpx;
padding-bottom: 0px;
text-align: center;
display:inline-block;
vertical-align: top;
white-space: normal;
}
.columnWeek {
width: __columnWidthpx;
background-color: #F4F4F4;
padding-bottom: 0px;
text-align: center;
display:inline-block;
vertical-align: top;
white-space: normal;
}
.columnDays {
width: __columnDaysWidthpx;
padding-bottom: 4px;
text-align: center;
display:inline-block;
vertical-align: top;
white-space: normal;
__heightColumnDays
}
.portlet {
margin: 0 0.6em 0.1em 0;
padding: 0.3em;
}
.function-header {
padding: 0.3em 0.8em;
margin-bottom: 0.2em;
position: relative;
  text-align: center;
cursor:pointer;
  border-radius: 50%;
  -webkit-border-radius: 50%;
  -moz-border-radius: 50%;
}
.function-footer {
padding: 0.1em 0.8em;
margin-top: 0.2em;
margin-bottom: 0.2em;
position: relative;
  text-align: center;
cursor:pointer;
  border-radius: 50%;
  -webkit-border-radius: 50%;
  -moz-border-radius: 50%;
}
.portlet-header {
padding: 0.2em;
word-wrap: break-word;
margin-bottom: 0em;
position: relative;
text-align: left;
cursor:move;
}
.portlet-header-disabled {
padding: 0.2em;
word-wrap: break-word;
margin-bottom: 0em;
position: relative;
text-align: left;
}
.portlet-footer {
__display
word-wrap: break-word;
text-align:justify;
padding: 0.3em 0.3em;
position: relative;
cursor:pointer; <!-- falls Footer Click Aufruf Item ist -->
}
.portlet-toggle {
position: absolute;
top: 50%;
right: 0;
margin-top: -8px;
cursor:pointer;
}
.portlet-content {
__display
word-wrap: break-word;
padding: 0.4em;
text-align: left;
cursor:pointer;
}
.portlet-placeholder {
border: 1px dotted black;
margin: 0 1em 1em 0;
height: 50px;
}
.disabled {

}
.item {
width: __itemWidthpx;
}
a {
color: blue !important;
}
.ui-tooltip {
white-space: pre-line;
}
</style>
<script>
$(document).ready(function() {

$( function() {
// $(document ).tooltip({ position: "bottom left", opacity: 0.7});

$( ".columnDays").sortable({
connectWith: ".columnDays",
scroll: true,
scrollSensitivity: 80,
scrollSpeed: 3,
handle: ".portlet-header",
cancel: ".portlet-toggle",
placeholder: "portlet-placeholder ui-corner-all",
items: ".portlet:not(.disabled)",

update: function( event, ui ) {
var changedList = this.id;
var order = $(this).sortable('toArray');
var positions = order.join(';');
output = changedList + '[' + positions + ']'
},
stop: function( event, ui ) {
var id = ui.item.attr("id");
$( self.location = '__script&param=reorder||||' + output + id );
}
});

$( ".portlet" )
.addClass( "ui-widget ui-widget-content ui-helper-clearfix ui-corner-all" )
.removeClass("disabled")
.find( ".portlet-header-all" )
.addClass( "ui-widget-header ui-corner-all" )
.prepend( "<span class='ui-icon __ui-icon ui-icon portlet-toggle'></span>");

$( ".portlet-toggle" ).on( "click", function(e) {
var icon = $( this );
icon.toggleClass( "ui-icon-minus ui-icon-plus" );
icon.closest( ".portlet" ).find( ".portlet-content" ).toggle();
icon.closest( ".portlet" ).find( ".portlet-footer" ).toggle();
// prevent function portlet-header" ).on( "click", function to fire
e.preventDefault();
e.stopPropagation();
return false;
});

$( ".portlet-content" ).on( "click", function( e ) {
var id = $(this).closest('.portlet').attr('id');
$( self.location = '__script&param=selectItem||||' + id );
});

$( ".portlet-header" ).on( "click", function( e ) {
var id = $(this).closest('.portlet').attr('id');
$( self.location = '__script&param=selectItem||||' + id );
});

$( ".portlet-header-disabled" ).on( "click", function( e ) {
var id = $(this).closest('.portlet').attr('id');
$( self.location = '__script&param=selectItem||||' + id );
});

$( ".portlet-footer" ).on( "click", function( e ) {
var id = $(this).closest('.portlet').attr('id');
$( self.location = '__script&param=selectItem||||' + id );
});

$( ".function-header" ).on( "click", function( e ) {
var id = $(this).attr('id');
$( self.location = '__script&param=newItem||||' + id );
});

$( ".function-footer" ).on( "click", function( e ) {
var id = $(this).attr('id');
$( self.location = '__script&param=viewAll||||' + id );
});

} );

}); //Document Ready
</script>
</head>
<body>
<div class='parent'>
__item
</div>
</body>
</html>

Other default tables are:
Colors (table for color selection)
The Color Picker was developed back in 2014 by the smart folks at desktopservices.net and I still find
it to be one of the smartest solutions for a lean color picker in Filemaker.
The tool can be integrated with a single script and then calculates the HTML color code from the selected color.
We then simply place this color code in a text field and include it in the HTML via Substitute.

Bildschirmfoto 2022 08 30 um 16 07 24

Days (table with days of the week and other identifiers that make our life easier in our example)

Bildschirmfoto 2022 12 27 um 13 42 04

The ‘Days’ table contains all days up to the year 2036 and additional fields for the calendar week, month, year
and calendar week as a combination of numbers, the date of Monday and Sunday of each week.
Furthermore, I have a marker in the date records for the first day of a week and the first day of a month.
All these fields are just to make relationships easier – we save a lot of formulas and just use auto-enter calculations.

More interesting are the two portlet fields that each create the columns for our representation of a single week or
an entire month.

The calculation for the respective day – div_column_day – serves as one column each in the weekly display and
is quite complex since many parameters are processed here such as weekends or holidays for coloring.

This portlet contains a header with the date and a button area for creating new appointments on this day.

In this portlet all appointments from the portlet table for this date are collected and displayed.

We take advantage of the fact that a JQUERY portlet can consist of portlets, which makes the display very clear
and simple, because the portlets each carry the entire functionality.

The div_column_week portlet builds directly on this and actually only collects the respective div_column_day portlets
for a week. This way we have a very elegant structure and adjustments have an immediate effect on all displays.

div_column_day =

Let (
[
colorizeWeekend = Parameter::colorWeekend ; //e.g. "#F9DFDF"
colorDay = Case (
Get ( CurrentDate ) = Date ; Parameter::colorToday ;
weekday = 1 and colorizeWeekend ≠ "" ; colorizeWeekend ;
weekday = 7 and colorizeWeekend ≠ "" ; colorizeWeekend ;
isHoliday = 1 and colorizeWeekend ≠ "" ; colorizeWeekend ;
Parameter::colorDay
) ;
R = HexToRGB ( colorDay ; "R" ) ;
G = HexToRGB ( colorDay ; "G" ) ;
B = HexToRGB ( colorDay ; "B" )
] ;

"<div id = '" & Date & "' class='columnDays portlet disabled'><span id = '" & Date &
"' class='columnHeaderDays ui-widget-header ui-corner-all' style='background-color:" &
colorDay & If ( Month ≠ Team::V_month_select and Team::V_display ≠ "details" ; ";
opacity: " & Parameter::opacity & ";'" ; "'" ) & ">" & DayName_cf ( Date ) & " " &
Date & "</span>
<div id='" & Date & "' class='function-header disabled'" & If ( colorDay ≠ "" ;
" style='background-color:" & colorDay & If ( Month ≠ Team::V_month_select and
Team::V_display ≠ "details" ; "; opacity: " & Parameter::opacity & ";'" ; "'" ) ; "" ) &
">+ New appointment</div>
<div id = '" & Date & "' class=\"column\">¶" & If ( Team::V_display = "details" or
Team::V_viewAllDay = Date ; List ( Days~Portlets#date::div_portlet ) ;
LeftValues ( List ( Days~Portlets#date::div_portlet ) ; 3 ) ) & "</div>" &
If ( Team::V_display ≠ "details" and Count ( Days~Portlets#date::id ) > 3 ;
"<div id='" & Date & "' class='function-footer disabled'" & If ( colorDay ≠ "" ;
" style='background-color:" & "rgba(" & R & "," & G & "," & B & ",.5)" ; "" ) &
If ( Month ≠ Team::V_month_select and Team::V_display ≠ "details" ; "; opacity: " &
Parameter::opacity & ";'" ; "'" ) & ">" & If ( Team::V_display = "details" or
Team::V_viewAllDay = Date ; "- " ; "+ " & Count ( Days~Portlets#date::id ) - 3 ) &
" Appointment(s)</div>
" ; "" ) & "
</div>"
)

 

div_column_week =

If ( KWMarker = 1 ;
"<div id = '" & YearKW & "' class='columnWeek portlet disabled'><span id = '" & YearKW &
"' class='columnHeader ui-widget-header ui-corner-all' style='background-color:" &
Parameter::colorWeek & "'>Weeknumber " & KW & "</span>
<div id = '" & YearKW & "' class=\"column\">¶" &
List ( Days~Days#yearKW::div_column_day ) & "
</div>
</div>"
;
""
)

 

Links (table with which you can convert text links into HTML links – details can be found in the
article ‘Generate HTML link from text’)

Bildschirmfoto 2022 08 30 um 16 08 48

Portlets (here the portlets (cards for the appointments) are generated)

Bildschirmfoto 2022 08 30 um 16 15 42

The portlets table contains the appointment data and creates the appointment portlets from it.
The formula field looks quite complicated at first, but actually consists only of the portlet components
Header, Content and Footer.
The header shows the headline and defines if the appointment can be moved and shows itself with a
transparency if necessary.
Content contains the appointment text which is extended by another formula field – content_HTML – in its
functionality by links and text attributes.
Also here a transparency is used if necessary.
The footer shows a locked icon for appointments that cannot be moved from the included JQUERY UI graphics,
as well as a warning if an appointment is overdue.

div_portlet =

If ( activ ≠ 1 or hide = 1 ; "" ;
Let (
[
charOffset = If ( dueDate ≠ "" ; 0 ; 5 ) + If ( sort_disabled = 1 ; 0 ; 5 ) ;
headline = If ( Team::V_display ≠ "details" and Length ( headline ) > 20 + charOffset ;
Left ( headline ; 20 + charOffset ) & "..." ; headline ) ;
color = Case ( dueDate ≠ "" and dueDate < Get ( CurrentDate ) ; "#FF7D7D" ;
dueDate ≠ "" and dueDate < date ; "#FF7D7D" ; color ≠ "" ; color ; Parameter::colorDay ) ;
R = HexToRGB ( color ; "R" ) ;
G = HexToRGB ( color ; "G" ) ;
B = HexToRGB ( color ; "B" )
] ;

" <div title='" & tooltip & "' id='" & id & "' class='portlet item" & If ( sort_disabled = 1 ;
" disabled" ; "" ) & "'>
<div class='portlet-header-all " & If ( sort_disabled = 1 ; "portlet-header-disabled" ;
"portlet-header" ) & "'" & If ( color ≠ "" ; " style='background-color:" &
"rgba(" & R & "," & G & "," & B & ",.5)" ; "" ) & If ( Month ( date ) ≠ Team::V_month_select and
Team::V_display ≠ "details" ; "; opacity: " & Parameter::opacity & ";'" ; "'" ) & ">"

& "<b>" & If (headline = "" ; "&nbsp;" ; Substitute ( headline ; ¶ ; "<br/>" ) ) & "</span>"

& "</div>
<div class='portlet-content'" & If ( Month ( date ) ≠ Team::V_month_select and
Team::V_display ≠ "details" ; " style='opacity: " & Parameter::opacity & ";'" ; "" ) & ">" &
content_HTML & "
</div>
<div class='portlet-footer'" & If ( color ≠ "" ; " style='background-color:" &
"rgba(" & R & "," & G & "," & B & ",.2)" &
"'" ; "" ) & ">" &
If ( sort_disabled = 1 ; "<span class='ui-icon ui-icon-locked'></span>" ; "" ) &
Case (
dueDate ≤ Get ( CurrentDate ) and dueDate ≠ "" ;"<span
style='float:right'class='ui-state-error error-state-icon'><span id='" & id &
"' class='ui-icon ui-icon-alert'></span><span style='color:red'>" & dueDate &
"</span></span></br>" ;
dueDate ≠ "" ; "<span style='float:right'><span id='" & id &
"' class='ui-icon ui-icon-calendar'></span><span>" & dueDate &
"</span></span></br>" ;
""
) & "
</div>
</div>"

)

)
content_HTML =

If ( activ ≠ 1 ; "" ; Substitute ( masterCall ( GetAsCSS( FilterChar ( content ) ) ;
List ( Links::replace ) ; 1 ) ; ¶ ; "<br/>" ) )

 

Team (table that creates the actual user interface)

Bildschirmfoto 2022 08 30 um 16 13 18

The Teams table is used to provide each user with a separate data set in which to work.
This simply prevents blocked records or two users overwriting each other’s entries.

Most of the data is stored here as gloable fields for the parameters.
There is actually only one field that collects the columns of the weeks or days and connects them with the
HTML from the parameter table.

We exchange here the already mentioned ‘__’ placeholders against the real data. This gives us the possibility to
include formula results in the HTML.
Besides including the JQUERY libraries we also include the graphics for the ICONS JQUERY IU – and this we do
now no longer as files stored in the temporary directory, but directly from the container field.
I had to do some trial and error to find out that you have to use the Filemaker command Base64EncodeRFC
with the parameter 4648. Stackoverflow put me on the right track again 🙂

WebviewPortlet =

Let (
[
COLUMNS = If ( V_display = "details" ; List ( Team~Days#V_yearKW::div_column_day ) ;

List ( Team~Days#V_yearKW#KWMarker::div_column_week ) )
] ;

Substitute ( Parameter::HTML_Portlet ;
["__heightColumnDays" ; If ( V_display = "overview" ; " min-height: 140px;" ; "" )] ;
["__itemWidth" ; Parameter::ItemWidth] ;
["__columnWidth" ; Parameter::columnWidth] ;
["__columnHeaderDaysWidth" ; Parameter::columnHeaderDaysWidth] ;
["__columnHeaderWidth" ; Parameter::columnHeaderWidth] ;
["__columnDaysWidth" ; Parameter::columnDaysWidth] ;
["__display" ; If ( DefaultMinimize = 1 ; "display: none;"; "" )] ;
["__ui-icon ui-icon" ; If ( DefaultMinimize = 1 ; "ui-icon ui-icon-plus" ; "ui-icon ui-icon-minus" )] ;
["__jquery-ui.css" ; Parameter::jquery_ui_css_Portlet] ;
["__jquery-ui.js" ; Parameter::jquery_ui.js ] ;
["__jquery-1.10.2.js"; Parameter::jquery_1.10.2.js] ;
["__item"; COLUMNS ] ;

["__glyphiconspath_ui-icons_444444_256x240" ;

"data:image/png;base64," & Base64EncodeRFC ( 4648 ; Parameter::ui_icons_444444_256x240 )] ;
["__glyphiconspath_ui-icons_555555_256x240" ;

"data:image/png;base64," & Base64EncodeRFC ( 4648 ; Parameter::ui_icons_555555_256x240 )] ;
["__glyphiconspath_ui-icons_ffffff_256x240" ;

"data:image/png;base64," & Base64EncodeRFC ( 4648 ; Parameter::ui_icons_ffffff_256x240 )] ;
["__glyphiconspath_ui-icons_777620_256x240" ;

"data:image/png;base64," & Base64EncodeRFC ( 4648 ; Parameter::ui_icons_777620_256x240 )] ;
["__glyphiconspath_ui-icons_cc0000_256x240" ;

"data:image/png;base64," & Base64EncodeRFC ( 4648 ; Parameter::ui_icons_cc0000_256x240 )] ;
["__glyphiconspath_ui-icons_777777_256x240" ;

"data:image/png;base64," & Base64EncodeRFC ( 4648 ; Parameter::ui_icons_777777_256x240 )] ;

["__script"; Parameter::Script_Portlet]
)

)

Lastly, let’s look at the relationship diagram.

In this file (as in many others) I connect all main occurrences of the tables by field ‘A_1’ (A field with automatic value 1),
so that one can always access all other tables.

I have entered the connections of the individual files in the graphic.

Bildschirmfoto 2022 12 29 um 16 07 07

 

If you now create a layout with the WebviewPortlet you can use a global field ‘V_display’ = ‘details’ (week view) or
‘V_display’ = ‘overview’ (month view) to create the whole display with only one webview.

Bildschirmfoto 2023 01 14 um 15 54 59

We then have a compact display of the monthly overview and a more detailed display in a weekly view.

Bildschirmfoto 2023 01 14 um 15 50 08

Bildschirmfoto 2023 01 14 um 15 50 14

If you like to have an open version of the file, just send me an email.
You have any questions? Feel free to send me an email, I’ll try to answer as soon as possible.