1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8use Tiki\Package\VendorHelper;
9
10function wikiplugin_trackercalendar_info()
11{
12	global $prefs;
13
14	return [
15		'name' => tr('Tracker Calendar'),
16		'description' => tr('Create and display a calendar using tracker data'),
17		'prefs' => ['wikiplugin_trackercalendar', 'calendar_fullcalendar'],
18		'packages_required' => ['fullcalendar/fullcalendar-scheduler' => VendorHelper::getAvailableVendorPath('fullcalendarscheduler', 'fullcalendar/fullcalendar-scheduler/dist/scheduler.min.js')],
19		'format' => 'html',
20		'iconname' => 'calendar',
21		'introduced' => 10,
22		'params' => [
23			'trackerId' => [
24				'name' => tr('Tracker ID'),
25				'description' => tr('Tracker to search from'),
26				'since' => '10.0',
27				'required' => false,
28				'default' => 0,
29				'filter' => 'int',
30				'profile_reference' => 'tracker',
31			],
32			'begin' => [
33				'name' => tr('Begin Date Field'),
34				'description' => tr('Permanent name of the field to use for event beginning'),
35				'since' => '10.0',
36				'required' => true,
37				'filter' => 'word',
38			],
39			'end' => [
40				'name' => tr('End Date Field'),
41				'description' => tr('Permanent name of the field to use for event ending'),
42				'since' => '10.0',
43				'required' => true,
44				'filter' => 'word',
45			],
46			'resource' => [
47				'name' => tr('Resource Descriptor Field'),
48				'description' => tr('Permanent name of the field to use as the resource indicator'),
49				'since' => '10.0',
50				'required' => false,
51				'filter' => 'word',
52			],
53			'coloring' => [
54				'name' => tr('Coloring Discriminator Field'),
55				'description' => tr('Permanent name of the field to use to segment the information into color schemes.'),
56				'since' => '10.0',
57				'required' => false,
58				'filter' => 'word',
59			],
60			'external' => [
61				'required' => false,
62				'name' => tra('External Link'),
63				'description' => tra('Follow external link when event item is clicked. Useful for supporting links to
64					pretty tracker supported pages.'),
65				'since' => '12.4',
66				'filter' => 'alpha',
67				'default' => 'n',
68				'options' => [
69					['text' => '', 'value' => ''],
70					['text' => tra('Yes'), 'value' => 'y'],
71					['text' => tra('No'), 'value' => 'n']
72				]
73			],
74			'url' => [
75				'required' => false,
76				'name' => tra('URL'),
77				'description' => tra('Complete URL, internal or external.'),
78				'since' => '12.4',
79				'filter' => 'url',
80				'default' => '',
81				'parentparam' => ['name' => 'external', 'value' => 'y'],
82			],
83			'trkitemid' => [
84				'required' => false,
85				'name' => tra('Tracker Item Id'),
86				'description' => tr('If Yes (%0y%1) the item id will be passed as %0itemId%1, which can be used
87					by Tracker plugins. Will be passed as %0itemid%1 if No (%0n%1)', '<code>', '</code>'),
88				'since' => '12.4',
89				'filter' => 'alpha',
90				'default' => 'n',
91				'options' => [
92					['text' => '', 'value' => ''],
93					['text' => tra('Yes'), 'value' => 'y'],
94					['text' => tra('No'), 'value' => 'n']
95				],
96				'parentparam' => ['name' => 'external', 'value' => 'y'],
97			],
98			'addAllFields' => [
99				'required' => false,
100				'name' => tra('Add All Fields'),
101				'description' => tr('If Yes (%0y%1)  all fields in the tracker will be added to the URL, not just the
102					itemId', '<code>', '</code>'),
103				'since' => '12.4',
104				'filter' => 'alpha',
105				'default' => 'y',
106				'options' => [
107					['text' => '', 'value' => ''],
108					['text' => tra('Yes'), 'value' => 'y'],
109					['text' => tra('No'), 'value' => 'n']
110				],
111				'parentparam' => ['name' => 'external', 'value' => 'y'],
112			],
113			'useSessionStorage' => [
114				'required' => false,
115				'name' => tra('Use Session Storage'),
116				'description' => tr('If Yes (%0y%1) copy all the field values into window.sessionStorage so it can be
117					accessed via JavaScript.', '<code>', '</code>'),
118				'since' => '12.4',
119				'filter' => 'alpha',
120				'default' => 'y',
121				'options' => [
122					['text' => '', 'value' => ''],
123					['text' => tra('Yes'), 'value' => 'y'],
124					['text' => tra('No'), 'value' => 'n']
125				],
126				'parentparam' => ['name' => 'addAllFields', 'value' => 'y'],
127			],
128			'amonth' => [
129				'required' => false,
130				'name' => tra('Agenda by Months'),
131				'description' => tra('Display the option to change the view to agenda by months'),
132				'since' => '12.1',
133				'filter' => 'alpha',
134				'default' => 'y',
135				'options' => [
136					['text' => '', 'value' => ''],
137					['text' => tra('Yes'), 'value' => 'y'],
138					['text' => tra('No'), 'value' => 'n']
139				]
140			],
141			'aweek' => [
142				'required' => false,
143				'name' => tra('Agenda by Weeks'),
144				'description' => tra('Display the option to change the view to agenda by weeks'),
145				'since' => '12.1',
146				'filter' => 'alpha',
147				'default' => 'y',
148				'options' => [
149					['text' => '', 'value' => ''],
150					['text' => tra('Yes'), 'value' => 'y'],
151					['text' => tra('No'), 'value' => 'n']
152				]
153			],
154			'aday' => [
155				'required' => false,
156				'name' => tra('Agenda by Days'),
157				'description' => tra('Display the option to change the view to agenda by days'),
158				'since' => '12.1',
159				'filter' => 'alpha',
160				'default' => 'y',
161				'options' => [
162					['text' => '', 'value' => ''],
163					['text' => tra('Yes'), 'value' => 'y'],
164					['text' => tra('No'), 'value' => 'n']
165				]
166			],
167			'lyear' => [
168				'required' => false,
169				'name' => tra('List by Years'),
170				'description' => tra('Display the option to change the view to list by years'),
171				'since' => '20.1',
172				'filter' => 'alpha',
173				'default' => 'y',
174				'options' => [
175					['text' => '', 'value' => ''],
176					['text' => tra('Yes'), 'value' => 'y'],
177					['text' => tra('No'), 'value' => 'n']
178				]
179			],
180			'lmonth' => [
181				'required' => false,
182				'name' => tra('List by Months'),
183				'description' => tra('Display the option to change the view to list by months'),
184				'since' => '20.1',
185				'filter' => 'alpha',
186				'default' => 'y',
187				'options' => [
188					['text' => '', 'value' => ''],
189					['text' => tra('Yes'), 'value' => 'y'],
190					['text' => tra('No'), 'value' => 'n']
191				]
192			],
193			'lweek' => [
194				'required' => false,
195				'name' => tra('List by Weeks'),
196				'description' => tra('Display the option to change the view to list by weeks'),
197				'since' => '20.1',
198				'filter' => 'alpha',
199				'default' => 'y',
200				'options' => [
201					['text' => '', 'value' => ''],
202					['text' => tra('Yes'), 'value' => 'y'],
203					['text' => tra('No'), 'value' => 'n']
204				]
205			],
206			'lday' => [
207				'required' => false,
208				'name' => tra('List by Days'),
209				'description' => tra('Display the option to change the view to list by days'),
210				'since' => '20.1',
211				'filter' => 'alpha',
212				'default' => 'y',
213				'options' => [
214					['text' => '', 'value' => ''],
215					['text' => tra('Yes'), 'value' => 'y'],
216					['text' => tra('No'), 'value' => 'n']
217				]
218			],
219			'ryear' => [
220				'required' => false,
221				'name' => tra('Resources by Years'),
222				'description' => tra('Display the option to change the view to resources by years'),
223				'since' => '20.1',
224				'filter' => 'alpha',
225				'default' => 'y',
226				'options' => [
227					['text' => '', 'value' => ''],
228					['text' => tra('Yes'), 'value' => 'y'],
229					['text' => tra('No'), 'value' => 'n']
230				]
231			],
232			'rmonth' => [
233				'required' => false,
234				'name' => tra('Resources by Months'),
235				'description' => tra('Display the option to change the view to resources by months'),
236				'since' => '12.1',
237				'filter' => 'alpha',
238				'default' => 'y',
239				'options' => [
240					['text' => '', 'value' => ''],
241					['text' => tra('Yes'), 'value' => 'y'],
242					['text' => tra('No'), 'value' => 'n']
243				]
244			],
245			'rweek' => [
246				'required' => false,
247				'name' => tra('Resources by Weeks'),
248				'description' => tra('Display the option to change the view to resources by weeks'),
249				'since' => '12.1',
250				'filter' => 'alpha',
251				'default' => 'y',
252				'options' => [
253					['text' => '', 'value' => ''],
254					['text' => tra('Yes'), 'value' => 'y'],
255					['text' => tra('No'), 'value' => 'n']
256				]
257			],
258			'rday' => [
259				'required' => false,
260				'name' => tra('Resources by Days'),
261				'description' => tra('Display the option to change the view to resources by days'),
262				'since' => '12.1',
263				'filter' => 'alpha',
264				'default' => 'y',
265				'options' => [
266					['text' => '', 'value' => ''],
267					['text' => tra('Yes'), 'value' => 'y'],
268					['text' => tra('No'), 'value' => 'n']
269				]
270			],
271			'dView' => [
272				'required' => false,
273				'name' => tra('Default View'),
274				'description' => tra('Choose the default view for the Tracker Calendar'),
275				'since' => '12.1',
276				'filter' => 'alpha',
277				'default' => 'month',
278				'options' => [
279					['text' => '', 'value' => ''],
280					['text' => tra('Agenda by Months'), 'value' => 'month'],
281					['text' => tra('Agenda by Weeks'), 'value' => 'agendaWeek'],
282					['text' => tra('Agenda by Days'), 'value' => 'agendaDay'],
283					['text' => tra('List'), 'value' => 'list'],
284					['text' => tra('List by Months'), 'value' => 'listMonth'],
285					['text' => tra('List by Weeks'), 'value' => 'listWeek'],
286					['text' => tra('List by Days'), 'value' => 'listDay'],
287					['text' => tra('Resources by Years'), 'value' => 'timelineYear'],
288					['text' => tra('Resources by Months'), 'value' => 'timelineMonth'],
289					['text' => tra('Resources by Weeks'), 'value' => 'timelineWeek'],
290					['text' => tra('Resources by Days'), 'value' => 'timelineDay']
291				]
292			],
293			'dYear' => [
294				'required' => false,
295				'name' => tra('Default Year'),
296				'description' => tra('Choose the default year (yyyy) to use for the display'),
297				'since' => '12.1',
298				'default' => 0,
299				'filter' => 'int',
300			],
301			'dMonth' => [
302				'required' => false,
303				'name' => tra('Default Month'),
304				'description' => tra('Choose the default month (mm, as numeric value) to use for the display. Numeric
305					values here are 1-based, meaning January=1, February=2, etc'),
306				'since' => '12.1',
307				'default' => 0,
308				'filter' => 'int',
309			],
310			'dDay' => [
311				'required' => false,
312				'name' => tra('Default Day'),
313				'description' => tra('Choose the default day (dd) to use for the display'),
314				'since' => '12.1',
315				'default' => 0,
316				'filter' => 'int',
317			],
318			'colormap' => [
319				'required' => false,
320				'name' => tra('Colormap for coloring'),
321				'description' => tr('Colormap to be used when segmenting the information using the coloring field.
322					Each map is composed of value and color separated with a comma, use pipes to separate multiple colormaps: %0', '<code>1,#6cf|2,#6fc</code>'),
323				'since' => '18.0',
324				'filter' => 'text',
325			],
326			'fDayofWeek' => [
327				'required' => false,
328				'name' => tra('First day of the Week'),
329				'description' => tr('Choose the day that each week begins with, for the tracker calendar display.
330					The value must be a number that represents the day of the week: Sunday=0, Monday=1, Tuesday=2,
331					etc. Default: %0 (Sunday)', '<code>0</code>'),
332				'since' => '12.1',
333				'default' => 0,
334				'filter' => 'int',
335			],
336			'weekends' => [
337				'required' => false,
338				'name' => tra('Show Weekends'),
339				'description' => tra('Display Saturdays and Sundays (shown by default)'),
340				'filter' => 'alpha',
341				'default' => 'y',
342				'options' => [
343					['text' => '', 'value' => ''],
344					['text' => tra('Yes'), 'value' => 'y'],
345					['text' => tra('No'), 'value' => 'n']
346				]
347			],
348			'minHourOfDay' => [
349				'required' => false,
350				'name' => tra('Day Start'),
351				'description' => tr('First time slot that will be displayed for each day, e.g. %0', '07:00:00'),
352				'since' => '19.1',
353				'filter' => 'text',
354				'default' => '07:00:00',
355			],
356			'maxHourOfDay' => [
357				'required' => false,
358				'name' => tra('Day End'),
359				'description' => tr('Last time slot that will be displayed for each day, e.g. %0', '24:00:00'),
360				'since' => '19.1',
361				'filter' => 'text',
362				'default' => '24:00:00',
363			],
364			'slotDuration' => [
365				'required' => false,
366				'name' => tra('Slot Duration'),
367				'description' => tr('Frequency for displayting time slots, e.g. %0 (defaults to the calendar_timespan preference)', "00:{$prefs['calendar_timespan']}:00"),
368				'since' => '19.1',
369				'filter' => 'text',
370				'default' => "00:{$prefs['calendar_timespan']}:00",
371			],
372			'eventOverlap' => [
373				'required' => false,
374				'name' => tra('Overlapping allowed'),
375				'description' => tra('Allow resources to overlap in time.'),
376				'since' => '20.1',
377				'filter' => 'alpha',
378				'default' => 'y',
379				'options' => [
380					['text' => '', 'value' => ''],
381					['text' => tra('Yes'), 'value' => 'y'],
382					['text' => tra('No'), 'value' => 'n']
383				]
384			],
385		],
386	];
387}
388
389function wikiplugin_trackercalendar($data, $params)
390{
391	global $prefs;
392
393	static $id = 0;
394	$headerlib = TikiLib::lib('header');
395	$vendorPath = VendorHelper::getAvailableVendorPath('fullcalendarscheduler', 'fullcalendar/fullcalendar-scheduler/dist/scheduler.min.js', false);
396
397	if (! $vendorPath) {
398		return WikiParser_PluginOutput::userError(tr('To view Tracker Calendar Tiki needs the fullcalendar/fullcalendar-scheduler package. If you do not have permission to install this package, ask the site administrator.'));
399	}
400
401	$headerlib->add_cssfile($vendorPath . '/fullcalendar/fullcalendar/dist/fullcalendar.min.css');
402	// Disable fullcalendar's force events to be one-line tall
403	$headerlib->add_css('.fc-day-grid-event > .fc-content, .fc-timeline-event > .fc-content { white-space: normal; }');
404	$headerlib->add_cssfile($vendorPath . '/fullcalendar/fullcalendar-scheduler/dist/scheduler.min.css');
405	$headerlib->add_jsfile($vendorPath . '/moment/moment/min/moment.min.js', true);
406	$headerlib->add_jsfile($vendorPath . '/fullcalendar/fullcalendar/dist/fullcalendar.min.js', true);
407	$headerlib->add_jsfile($vendorPath . '/fullcalendar/fullcalendar-scheduler/dist/scheduler.min.js', true);
408
409	$jit = new JitFilter($params);
410	$definition = Tracker_Definition::get($jit->trackerId->int());
411	$itemObject = Tracker_Item::newItem($jit->trackerId->int());
412
413	if (! $definition) {
414		return WikiParser_PluginOutput::userError(tr('Tracker not found.'));
415	}
416
417	$beginField = $definition->getFieldFromPermName($jit->begin->word());
418	$endField = $definition->getFieldFromPermName($jit->end->word());
419
420	if (! $beginField || ! $endField) {
421		return WikiParser_PluginOutput::userError(tr('Fields not found.'));
422	}
423
424	$views = [];
425	if (! empty($params['amonth']) and $params['amonth'] != 'y') {
426		$amonth = 'n';
427	} else {
428		$amonth = 'y';
429		$views[] = 'month';
430	}
431	if (! empty($params['aweek']) and $params['aweek'] != 'y') {
432		$aweek = 'n';
433	} else {
434		$aweek = 'y';
435		$views[] = 'agendaWeek';
436	}
437	if (! empty($params['aday']) and $params['aday'] != 'y') {
438		$aday = 'n';
439	} else {
440		$aday = 'y';
441		$views[] = 'agendaDay';
442	}
443	if (! empty($params['lyear']) and $params['lyear'] != 'y') {
444		$lyear = 'n';
445	} else {
446		$lyear = 'y';
447		$views[] = 'listYear';
448	}
449	if (! empty($params['lmonth']) and $params['lmonth'] != 'y') {
450		$lmonth = 'n';
451	} else {
452		$lmonth = 'y';
453		$views[] = 'listMonth';
454	}
455	if (! empty($params['lweek']) and $params['lweek'] != 'y') {
456		$lweek = 'n';
457	} else {
458		$lweek = 'y';
459		$views[] = 'listWeek';
460	}
461	if (! empty($params['lday']) and $params['lday'] != 'y') {
462		$lday = 'n';
463	} else {
464		$lday = 'y';
465		$views[] = 'listDay';
466	}
467
468	$resources = [];
469	if ($resourceField = $jit->resource->word()) {
470		$field = $definition->getFieldFromPermName($resourceField);
471		$resources = wikiplugin_trackercalendar_get_resources($field);
472
473		if (! empty($params['ryear']) and $params['ryear'] != 'y') {
474			$ryear = 'n';
475		} else {
476			$ryear = 'y';
477			$views[] = 'timelineYear';
478		}
479		if (! empty($params['rmonth']) and $params['rmonth'] != 'y') {
480			$rmonth = 'n';
481		} else {
482			$rmonth = 'y';
483			$views[] = 'timelineMonth';
484		}
485		if (! empty($params['rweek']) and $params['rweek'] != 'y') {
486			$rweek = 'n';
487		} else {
488			$rweek = 'y';
489			$views[] = 'timelineWeek';
490		}
491		if (! empty($params['rday']) and $params['rday'] != 'y') {
492			$rday = 'n';
493		} else {
494			$rday = 'y';
495			$views[] = 'timelineDay';
496		}
497	}
498
499	// Define the default View (dView)
500	if (! empty($params['dView'])) {
501		$dView = $params['dView'] == 'resourceWeek' ? 'timelineWeek' : $params['dView'];
502	} else {
503		$dView = 'month';
504	}
505
506	// Define the default date (dYear, dMonth, dDay)
507	if (! empty($params['dYear'])) {
508		$dYear = $params['dYear'];
509	} else {
510		$dYear = (int) date('Y');
511	}
512	if (! empty($params['dMonth']) and $params['dMonth'] > 0 and $params['dMonth'] < 13) {
513		$dMonth = $params['dMonth'];
514	} else {
515		$dMonth = (int) date('n');
516	}
517	if (! empty($params['dDay']) and $params['dDay'] > 0 and $params['dDay'] < 32) {
518		$dDay = $params['dDay'];
519	} else {
520		$dDay = (int) date('j');
521	}
522	// day duration
523	if (! empty($params['minHourOfDay'])) {
524		$minHourOfDay = $params['minHourOfDay'];
525	} else {
526		$minHourOfDay = '07:00:00';
527	}
528	if (! empty($params['maxHourOfDay'])) {
529		$maxHourOfDay = $params['maxHourOfDay'];
530	} else {
531		$maxHourOfDay = '24:00:00';
532	}
533	if (! empty($params['slotDuration'])) {
534		$slotDuration = $params['slotDuration'];
535	} else {
536		$slotDuration = "00:{$prefs['calendar_timespan']}:00";
537	}
538	if (! empty($params['eventOverlap']) and $params['eventOverlap'] != 'y') {
539		$eventOverlap = false;
540	} else {
541		$eventOverlap = true;
542	}
543
544	// Format the default date as Y-m-d instead of Y-n-d, required by MomentJs
545	$dDate = (new DateTime($dYear . '-' . $dMonth . '-' . $dDay))->format('Y-m-d');
546
547	if (! empty($params['fDayofWeek']) and $params['fDayofWeek'] > -1 and $params['fDayofWeek'] < 7) {
548		$firstDayofWeek = $params['fDayofWeek'];
549	} elseif ($prefs['calendar_firstDayofWeek'] !== 'user') {
550		$firstDayofWeek = $prefs['calendar_firstDayofWeek'];
551	} else {
552		$firstDayofWeek = 0;
553	}
554
555	$params['addAllFields'] = empty($params['addAllFields']) ? 'y' : $params['addAllFields'];
556	$params['useSessionStorage'] = empty($params['useSessionStorage']) ? 'y' : $params['useSessionStorage'];
557	$params['weekends'] = empty($params['weekends']) ? 'y' : $params['weekends'];
558	$params['external'] = $params['external'] ?? 'n';
559
560	$matches = WikiParser_PluginMatcher::match($data);
561	$builder = new Search_Formatter_Builder;
562	$builder->apply($matches);
563	$formatter = $builder->getFormatter();
564	$filters = str_replace(['~np~', '~/np~'], '', $formatter->renderFilters());
565
566	$smarty = TikiLib::lib('smarty');
567	$smarty->assign(
568		'trackercalendar',
569		[
570			'id' => 'trackercalendar' . ++$id,
571			'trackerId' => $jit->trackerId->int(),
572			'colormap' => base64_encode($jit->colormap->none()),
573			'begin' => $jit->begin->word(),
574			'end' => $jit->end->word(),
575			'resource' => $resourceField,
576			'resourceList' => $resources,
577			'coloring' => $jit->coloring->word(),
578			'beginFieldName' => 'ins_' . $beginField['fieldId'],
579			'endFieldName' => 'ins_' . $endField['fieldId'],
580			'firstDayofWeek' => $firstDayofWeek,
581			'views' => implode(',', $views),
582			'viewyear' => $dYear,
583			'viewmonth' => $dMonth,
584			'viewday' => $dDay,
585			'dDate' => $dDate,
586			'minHourOfDay' => $minHourOfDay,
587			'maxHourOfDay' => $maxHourOfDay,
588			'slotDuration' => $slotDuration,
589			'addTitle' => tr('Insert'),
590			'canInsert' => $itemObject->canModify(),
591			'dView' => $dView,
592			'eventOverlap' => $eventOverlap,
593			'body' => $data,
594			'filterValues' => $_REQUEST,
595			'url' => $params['external'] === 'y' ? $params['url'] : '',
596			'trkitemid' => $params['external'] === 'y' ? $params['trkitemid'] : '',
597			'addAllFields' => $params['external'] === 'y' ? $params['addAllFields'] : '',
598			'useSessionStorage' => $params['external'] === 'y' ? $params['useSessionStorage'] : '',
599			'timeFormat' => $prefs['display_12hr_clock'] === 'y' ? 'h(:mm)TT' : 'HH:mm',
600			'weekends' => $params['weekends'] === 'y' ? 1 : 0,
601			'utcOffset' => TikiDate::tzServerOffset(TikiLib::lib('tiki')->get_display_timezone()) / 60, // In minutes
602		]
603	);
604	$smarty->assign('filters', $filters);
605	return $smarty->fetch('wiki-plugins/trackercalendar.tpl');
606}
607
608function wikiplugin_trackercalendar_get_resources($field)
609{
610	$db = TikiDb::get();
611
612	return $db->fetchAll('SELECT DISTINCT LOWER(value) as id, value as title FROM tiki_tracker_item_fields WHERE fieldId = ? ORDER BY  value', $field['fieldId']);
613}
614