martedì 13 dicembre 2011

FullCalendar - un calendario jQuery con agenda e drag & drop

Per un piccolo gestionale medico ultimamente mi è capitato sotto mano FullCalendar, un ottimo calendario jQuery "full-size" altamente configurabile. Già dalla home page di questo progetto emergono i punti forti del plugin: eventi di tutto il giorno o schedulati, drag & drop degli eventi, viste per mese, per settimana e vista giornaliera. Il progetto è molto ben documentato, quindi per un utilizzo base rimando ogni spiegazione al sito.
Voglio pubblicare però la configurazione che ho utilizzato, cercando di mostrarne le potenzialità:
$('#calendar_strutture').fullCalendar({
   header: {
    left: 'prev,next today',
    center: 'title',
    right: 'month' //,agendaWeek,agendaDay
   },
   firstHour: 6,
   firstDay: 1,
   allDaySlot : false,
   timeFormat: 'HH:mm',
   monthNames:['Gennaio','Febbraio','Marzo','Aprile','Maggio','Giugno','Luglio','Agosto','Settembre','Ottobre','Novembre','Dicembre'],
   monthNamesShort: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'],
   dayNames: ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'],
   dayNamesShort: ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'],
   buttonText: {
    prev:     ' &#9668 ',  // left triangle
    next:     ' &#9658 ',  // right triangle
    prevYear: ' &lt;&lt; ', // <<
    nextYear: ' &lt;&lt; ', // >>
    today:    'Oggi',
    month:    'Mese',
    week:     'Settimana',
    day:      'Giorno'
   },
   columnFormat: {
    month: 'ddd',    // Mon
    week: 'd/M ddd', // Mon 9/7
    day: 'dddd d/M'  // Monday 9/7
   },
   editable: true,
   
   events: {
    url: 'lista_calendario_strutture.php',
    cache: true,
    //color: 'purple',   // a non-ajax option
          textColor: 'white' // a non-ajax option
   },
   lazyFetching: false,
     
   eventClick: function(calEvent) {
    if(confirm("Elimino dal calendario la disponibilita' della struttura per il giorno selezionato?")) {
     $.ajax({
       url: "interventi.php?section=calendario_strutture&action=delete",
       type: 'POST',
       data: {
      struttura: calEvent.title,
      data: calEvent.start.getTime()
       },
       success: function(msg){
      //alert(msg); //debug
      $('#calendar_strutture').fullCalendar('removeEvents',calEvent.id); 
       },
       error: function() {
      revertFunc();
       }
     });
    }
   },
   
   eventMouseover: function(calEvent) {
    $(this).find('.fc-event-title').html('<img src="template/default/admin/assets/images/delete.png" /> '+calEvent.title);
   },
   
   eventMouseout: function(calEvent) {
    $(this).find('.fc-event-title').html(calEvent.title);  
   },
   
   eventDrop: function(calEvent,dayDelta,minuteDelta,allDay,revertFunc) {
    //Qui ajax per spostamento evento
    $.ajax({
      url: "interventi.php?section=calendario_strutture&action=move",
      type: 'POST',
      data: {
     struttura: calEvent.title,
     movimento: dayDelta,
     nuova_data: calEvent.start.getTime()
               },
      success: function(msg){
     //alert(msg);
      },
      error: function() {
     revertFunc();
      }
    });
   },
   droppable: true,
   drop: function(date, allDay) { // this function is called when something is dropped
   
    // retrieve the dropped element's stored Event Object
    var originalEventObject = $(this).data('eventObject');
    
    // we need to copy it, so that multiple events don't have a reference to the same object
    var copiedEventObject = $.extend({}, originalEventObject);
    
    var today = new Date();
    
    // assign it the date that was reported
    copiedEventObject.id = today.getTime();
    copiedEventObject.start = date;
    copiedEventObject.allDay = allDay;
    
    //alert(copiedEventObject.title + ' ' + copiedEventObject.start.getTime());
    
    //Qui ajax per inserimento disponibilita in calendario struttura
    $.ajax({
      url: "interventi.php?section=calendario_strutture&action=insert",
      type: 'POST',
      data: {
     struttura: copiedEventObject.title,
     data: copiedEventObject.start.getTime()
               },
      success: function(msg){
     //alert(msg); //debug
     // render the event on the calendar
     $('#calendar_strutture').fullCalendar('renderEvent', copiedEventObject, false);
      }
    });
    
   }
   
  });

$('#external-events div.external-event').each(function() {

 // create an Event Object (http://arshaw.com/fullcalendar/docs/event_data/Event_Object/)
 // it doesn't need to have a start or end
 var eventObject = {
  title: $.trim($(this).text()), // use the element's text as the event title
  backgroundColor: $(this).css('background-color')
 };
 
 // store the Event Object in the DOM element so we can get to it later
 $(this).data('eventObject', eventObject);
 
 // make the event draggable using jQuery UI
 $(this).draggable({
  zIndex: 999,
  revert: true,      // will cause the event to go back to its
  revertDuration: 0  //  original position after the drag
 });
 
});

Questa prima parte, che deve essere contenuta nella parte relativa a $(document).ready(...), specifica il comportamento del calendario, in particolare:
  1. deve poter gestire, tramite chiamate ajax, lo spostamento degli eventi (eventDrop), che in questo caso particolare, sono "disponibilità" delle sale operatorie in determinate strutture ospedaliere
  2. deve gestire l'eliminazione delle disponibilità, mediante click del mouse (eventClick) sull'evento stesso (notare che quando il mouse si trova sopra l'evento viene mostrata una icona "cestino")
  3. deve gestire la creazione delle disponibilità mediante il trascinamento dall'elenco strutture sulla sinistra al calendario (drop), date una occhiata allo screenshot per capire di cosa sto parlando
  4. La parte finale del codice jQuery è relativa al punto 3, fa capire al motore javascript che i
    sulla sinistra deve gestirli come "draggable" e creare un oggetto evento (Event Object) in modo da poterlo poi gestire in FullCalendar.
Di seguito il codice html utilizzato (elenco strutture + calendario):
<div id='wrap' style="width:100%;margin: 0 auto;"> 
                 <div id='external-events' style="float: left;width: 150px;padding: 0 10px;border: 1px solid #ccc;background: #eee;text-align: left;margin-right:20px;">
                  <h4>Elenco Strutture</h4>
                                    <div class='external-event' style="background-color:#0099FF">Struttura Ospedaliera 1</div>
                                    <div class='external-event' style="background-color:#996699">Struttura Ospedaliera 2</div>

                                    </div>
                 <div id='calendar_strutture' style="width: 900px;float:left;"></div>
                 <div style='clear:both'></div>
              </div>
            </div>
Il risultato finale!

17 commenti:

Anonimo ha detto...

Ciao, potresti postare un esempio completo di HTML? perchè non riesco a dividere gli eventi come fai tu.

Unknown ha detto...

Ciao, cosa intendi per dividere gli eventi? Intendi il diversificarli con colori e titoli (in pratica quello che mostro nell'esempio a sinistra) oppure qualcos'altro?

Anonimo ha detto...

In pratica ho utilizzato il tuo codice per un esempio banale, ma il drag & drop non funziona e dunque mi chiedevo se potessi pubblicare un esempio con il codice completo in modo da verificare il comportamento.

Unknown ha detto...

Ciao, ho fatto un copia e incolla su pastebin del mio codice:
http://pastebin.com/BVU1jzxC

Prova a vedere se così ti funziona. Ovviamente certi collegamenti sono relativi e li dovrai adattare (ad esempio le immagini o lo script fullcalendar.js ecc.)

Anonimo ha detto...

Purtroppo anche utilizzando il tuo esempio il drag & drop non mi funziona, quando droppo un evento questo non resta bloccato sul calendario.
Occorre un file ajax che salvi gli eventi? se si mi mostri un esempio?

Unknown ha detto...

Hai perfettamente ragione, mi era sfuggito che se la richiesta ajax non va a buon fine, viene annullato lo spostamento. Lo puoi verificare in queste linee di codice:

drop: function(date, allDay) { // this function is called when something is dropped

[ QUI ALTRO CODICE }

//Qui ajax per inserimento disponibilita in calendario struttura
$.ajax({
url: "interventi.php?section=calendario_strutture&action=insert",
type: 'POST',
data: {
struttura: copiedEventObject.title,
data: copiedEventObject.start.getTime()
},
success: function(msg){
//alert(msg); //debug
// render the event on the calendar
$('#calendar_strutture').fullCalendar('renderEvent', copiedEventObject, false);
}
});

Come vedi, solo in caso di successo della chiamata ajax (leggi il file interventi.php deve esistere, anche se vuoto) viene eseguito il drag & drop. Devi creare (nel mio esempio in interventi.php) una pagina che prenda dei campi post. Se vuoi che i risultati siano permanenti, come nel mio caso, la pagina farà operazioni su db. Ma per far funzionare semplicemente il FullCalendar basta che questo file a cui fare la richiesta ajax esista.

Se non vuoi fare la richiesta ajax, dovrebbe bastare questo codice da sostituire alla funzione drop (che è il codice originale senza la chiamata ajax):

drop: function(date, allDay) {
// retrieve the dropped element's stored Event Object
var originalEventObject = $(this).data('eventObject');

// we need to copy it, so that multiple events don't have a reference to the same object
var copiedEventObject = $.extend({}, originalEventObject);

var today = new Date();

// assign it the date that was reported
copiedEventObject.id = today.getTime();
copiedEventObject.start = date;
copiedEventObject.allDay = allDay;
$('#calendar_strutture').fullCalendar('renderEvent', copiedEventObject, false);
}

Anonimo ha detto...

Posso farti alcune altre domande?
Approfittando della tue gentilezza.

1) Vedo che le date vengono passate al file ajax con getTime quindi in millisecondi, eppure quando le converto con un comando PHP del tipo date("m/d/Y", '$_POST[data]') ottengo date sballate, come mai?

2) Quando uso il drag and drop allungando un appuntamento su più date, non vedo una funzione che salva data inizio e data fine o sbaglio? o viene salvata per più date? (sempre da ajax?)

3) lista_calendario_strutture.php in che formato deve restituire l'elenco degli appuntamenti? cioè il file cosa genera? e cosa leggera il jQuery

Unknown ha detto...

Tutte le domande che vuoi.
Ti rispondo una alla volta:
1)
Quello passato dovrebbe essere un UNIXTIME in millisecondi. Io lato ajax ho fatto eseguire la trasformazione direttamente al Mysql:
$query = $this->db->query("UPDATE tab_interventi SET intervento_data=FROM_UNIXTIME('".($timestamp/1000)."','%Y-%m-%d'), intervento_ordine=".$ordine." WHERE id_intervento='".(int)$id_intervento ."'");

Nota il /1000 che lo trasforma in secondi.

2)
ci guardo, nel mio progetto si tratta di disponibilità giornaliere di strutture mediche quindi non ho affrontato quella parte.

3)
Vuole il formato JSON. L'importante è mantenere il formato array con "id", "title", "start", "color". "AllDay" se è true dura tutto il giorno. Ti rimando per maggiori (e più precise) informazioni a http://arshaw.com/fullcalendar/docs/event_data/Event_Object/.

Io ad esempio ho fatto così:
$listaAperture = $calendario_strutture->getAll($request->get['start'],$request->get['end']);



$arrayAperture = array();

$id=1;

foreach($listaAperture as $apertura) {

array_push($arrayAperture, array(
'id' => $id,
'title' => $apertura['struttura_descrizione'],
'start' => $apertura['calendario_strutture_apertura'],
'color' => $apertura['struttura_colore'],
'allDay' => true
));
$id++;
}

echo json_encode($arrayAperture);

Anonimo ha detto...

Nel caso venisse spostato un evento già presente nel calendario... dove è gestita questa cosa a livello di database?!

Unknown ha detto...

Ciao ti va di va di aiutarmi ad inserirlo sul sito?

Unknown ha detto...

È un blog senza vita?

Unknown ha detto...

Salve,

interessante articolo, vorrei solo capire di preciso a cosa serve il codice dentro

drop: function(date, allDay) { // this function is called when something is dropped

ho provato a mettere una semplice alert all'uinterno della function ma allo spostamento non si attiva.

Di fondo vorrei capire la differenza tra queste due voci, drop ed eventdrop. la secondo si attiva in effetti allo spostamento dell'evento nel calendario...ma la prima no....

Unknown ha detto...

Ciao Gianmichele, scusa il ritardo.
Allora la funzione eventdrop funziona col drag&drop degli eventi, mentre la funzione drop viene eseguita solo in caso di drop di div esterni sul calendario.
Ad esempio, nell'immagine del post (lo so, è piccola) vedrai a sinistra un elenco di strutture mediche (mostrate come div colorati) e a destra il calendario. Sostanzialmente se trascini una di queste strutture sul calendario, viene richiamata la funzione drop.

Unknown ha detto...

@Mimmo
Perdonami ma sono diventato babbo e ho dovuto perdere di vista per un po' il blog. Fammi sapere se hai ancora bisogno.

Unknown ha detto...

Sinceramente ho risolto, se dovessi avere dubbi su php posso contattarti? Comunque auguri x il lieto evento

Anonimo ha detto...

Buongiorno stefano. Avrei bisogno di un piccolo aiutino.
Dovrei integrare il fullCalendar con i mio database in modo che quando clicco su una voce a lato del calendario, mi si apra il calendario riguardante quella voce (non so se mi sono spiegato bene). Avrebbe qualche idea? Grazie in anticipo.

Anonimo ha detto...

ciao, ho letto cosa avete scritto volevo chiederti c'è la possibilità di avere in allegato un script funzionante? grazie