WEB-INF/lib/tcpdf/
nbproject/
upload/
-.vscode/
\ No newline at end of file
+.vscode/
// define('AUTH_DEBUG', false); // Note: enabling AUTH_DEBUG breaks redirects as debug output is printed before setting redirect header. Do not enable on production systems.
+// team manager can set monthly quota for years between these values:
+define('MONTHLY_QUOTA_YEARS_START', 2010); // if nothing is specified, it falls back to 2010
+define('MONTHLY_QUOTA_YEARS_END', 2031); // if nothing is specified it falls back to 2030
+
// note input height in time.php
-define('NOTE_INPUT_HEIGHT', '40');
\ No newline at end of file
+define('NOTE_INPUT_HEIGHT', '40');
static function update($team_id, $fields)
{
// We'll require team name to be always set.
- if (!isset($fields['name'])) return false;
+ if (!isset($fields['name']) || $fields['name'] == "") return false;
$mdb2 = getConnection();
$name_part = 'name = '.$mdb2->quote($fields['name']);
$record_type_part = '';
$plugins_part = '';
$lock_spec_part = '';
+ $working_hours_part = '';
if (isset($fields['address'])) $addr_part = ', address = '.$mdb2->quote($fields['address']);
if (isset($fields['currency'])) $currency_part = ', currency = '.$mdb2->quote($fields['currency']);
if (isset($fields['record_type'])) $record_type_part = ', record_type = '.intval($fields['record_type']);
if (isset($fields['plugins'])) $plugins_part = ', plugins = '.$mdb2->quote($fields['plugins']);
if (isset($fields['lock_spec'])) $lock_spec_part = ', lock_spec = '.$mdb2->quote($fields['lock_spec']);
+ if (isset($fields['working_hours'])) $working_hours_part = ', daily_working_hours = '.$mdb2->quote($fields['working_hours']);
$sql = "update tt_teams set $name_part $addr_part $currency_part $lang_part $decimal_mark_part
$date_format_part $time_format_part $week_start_part $tracking_mode_part $record_type_part
- $plugins_part $lock_spec_part where id = $team_id";
+ $plugins_part $lock_spec_part $working_hours_part where id = $team_id";
$affected = $mdb2->exec($sql);
if (is_a($affected, 'PEAR_Error')) return false;
return (int)@$time_a[1] + ((int)@$time_a[0]) * 60;
}
- // toDuration - calculates duration between start and finish times in 00:00 format.
- static function toDuration($start, $finish) {
- $duration_minutes = ttTimeHelper::toMinutes($finish) - ttTimeHelper::toMinutes($start);
- if ($duration_minutes <= 0) return false;
-
- $hours = (string)((int)($duration_minutes / 60));
- $mins = (string)($duration_minutes % 60);
+ // fromMinutes - converts a number of minutes to format 00:00
+ static function fromMinutes($minutes){
+ $hours = (string)((int)($minutes / 60));
+ $mins = (string)(abs($minutes % 60));
if (strlen($hours) == 1)
$hours = '0'.$hours;
if (strlen($mins) == 1)
return $hours.':'.$mins;
}
+ // toDuration - calculates duration between start and finish times in 00:00 format.
+ static function toDuration($start, $finish) {
+ $duration_minutes = ttTimeHelper::toMinutes($finish) - ttTimeHelper::toMinutes($start);
+ if ($duration_minutes <= 0) return false;
+
+ return ttTimeHelper::fromMinutes($duration_minutes);
+ }
+
// The to12HourFormat function converts a 24-hour time value (such as 15:23) to 12 hour format (03:23 PM).
static function to12HourFormat($value) {
if ('24:00' == $value) return '12:00 AM';
return 0;
}
+ // getTimeForMonth - gets total time for a user for a given month.
+ static function getTimeForMonth($user_id, $date){
+ import('Period');
+ $mdb2 = getConnection();
+
+ $period = new Period(INTERVAL_THIS_MONTH, $date);
+ $sql = "select sum(time_to_sec(duration)) as sm from tt_log where user_id = $user_id and date >= '".$period->getBeginDate(DB_DATEFORMAT)."' and date <= '".$period->getEndDate(DB_DATEFORMAT)."' and status = 1";
+ $res = $mdb2->query($sql);
+ if (!is_a($res, 'PEAR_Error')) {
+ $val = $res->fetchRow();
+ return sec_to_time_fmt_hm($val['sm']);
+ }
+ return 0;
+ }
+
// getUncompleted - retrieves an uncompleted record for user, if one exists.
static function getUncompleted($user_id) {
$mdb2 = getConnection();
'label.cost' => 'Kosten',
'label.week_total' => 'Summe (Woche)',
'label.day_total' => 'Summe (Tag)',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => 'Heute',
'label.total_hours' => 'Gesamtstunden',
'label.total_cost' => 'Totale Kosten',
// TODO: translate the following strings.
// 'label.cron_schedule' => 'Cron schedule',
// 'label.what_is_it' => 'What is it?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'Anmelden',
// 'title.add_notification' => 'Adding Notification',
// 'title.edit_notification' => 'Editing Notification',
// 'title.delete_notification' => 'Deleting Notification',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => 'Daten exportieren',
'title.import' => 'Daten importieren',
'title.options' => 'Optionen',
'label.cost' => 'Cost',
'label.week_total' => 'Week total',
'label.day_total' => 'Day total',
+'label.month_total' => 'Month total',
+'label.month_left' => 'Time until quota is met',
+'label.month_over' => 'Over monthly quota',
'label.today' => 'Today',
'label.total_hours' => 'Total hours',
'label.total_cost' => 'Total cost',
'label.fav_report' => 'Favorite report',
'label.cron_schedule' => 'Cron schedule',
'label.what_is_it' => 'What is it?',
+'label.year' => 'Year',
+'label.month' => 'Month',
+'label.quota' => 'Quota',
+'label.dailyWorkingHours' => 'Daily working hours',
+'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'Login',
'title.add_notification' => 'Adding Notification',
'title.edit_notification' => 'Editing Notification',
'title.delete_notification' => 'Deleting Notification',
+'title.monthly_quota' => 'Monthly quota',
'title.export' => 'Exporting Team Data',
'title.import' => 'Importing Team Data',
'title.options' => 'Options',
// 'label.cost' => 'Cost',
// 'label.week_total' => 'Week total',
// 'label.day_total' => 'Day total',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => 'Hoy',
'label.total_hours' => 'Horas totales',
// TODO: translate the following strings.
// TODO: translate the following strings.
// 'label.cron_schedule' => 'Cron schedule',
// 'label.what_is_it' => 'What is it?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'SesiĆ³n iniciada',
// 'title.add_notification' => 'Adding Notification',
// 'title.edit_notification' => 'Editing Notification',
// 'title.delete_notification' => 'Deleting Notification',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => 'Exportar datos',
'title.import' => 'Importar datos',
'title.options' => 'Opciones',
'label.cost' => 'ŁŲ²ŪŁŁ',
'label.week_total' => 'Ś©Ł ŁŁŲŖŁ',
'label.day_total' => 'Ś©Ł Ų±ŁŲ²',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => 'Ų§Ł
Ų±ŁŲ²',
'label.total_hours' => 'Ł
Ų¬Ł
ŁŲ¹ Ų³Ų§Ų¹ŲŖ',
'label.total_cost' => 'Ł
Ų¬Ł
ŁŲ¹ ŁŲ²ŪŁŁ ŁŲ§',
// TODO: translate the following strings.
// 'label.cron_schedule' => 'Cron schedule',
// 'label.what_is_it' => 'What is it?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'ŁŲ±ŁŲÆ',
// 'title.add_notification' => 'Adding Notification',
// 'title.edit_notification' => 'Editing Notification',
// 'title.delete_notification' => 'Deleting Notification',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => 'Ł¾Ų“ŲŖŪŲ§ŁŪ ŚÆŲ±ŁŲŖŁ Ų§Ų² Ų§Ų·ŁŲ§Ų¹Ų§ŲŖ ŲŖŪŁ
',
'title.import' => 'ŁŲ§Ų±ŲÆ Ś©Ų±ŲÆŁ Ų§Ų·ŁŲ§Ų¹Ų§ŲŖ ŲŖŪŁ
',
'title.options' => 'ŚÆŲ²ŪŁŁ ŁŲ§',
'label.cost' => 'Hinta',
'label.week_total' => 'Viikko yhteensƤ',
'label.day_total' => 'PƤivƤ yhteensƤ',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => 'TƤnƤƤn',
'label.total_hours' => 'Tunnit yhteensƤ',
'label.total_cost' => 'Hinta yhteensƤ',
'label.fav_report' => 'Raporttipohja',
'label.cron_schedule' => 'Cron-ajoitus',
'label.what_is_it' => 'MikƤ se on?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'Kirjautuminen',
'title.add_notification' => 'Ilmoituksen lisƤys',
'title.edit_notification' => 'Ilmoituksen muokkaus',
'title.delete_notification' => 'Ilmoituksen poisto',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => 'Tiimitietojen vienti',
'title.import' => 'Tiimitietojen tunti',
'title.options' => 'Optiot',
'label.week_total' => 'Total hebdomadaire',
// TODO: translate the following string.
// 'label.day_total' => 'Day total',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => 'Aujourd\\\'hui',
'label.total_hours' => 'Total d\\\'heures',
'label.total_cost' => 'Coƻt total',
// TODO: translate the following strings.
// 'label.cron_schedule' => 'Cron schedule',
// 'label.what_is_it' => 'What is it?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'Connexion',
// 'title.add_notification' => 'Adding Notification',
// 'title.edit_notification' => 'Editing Notification',
// 'title.delete_notification' => 'Deleting Notification',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => 'Exporter les donnƩes', // TODO: use a noun.
'title.import' => 'Importer les donnƩes', // TODO: use a noun.
'title.options' => 'Options',
'label.cost' => '×¢×××Ŗ',
'label.week_total' => '×”×××× ×©×××¢×',
'label.day_total' => '×”×××× ××××',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => '××××',
'label.total_hours' => '×”× ××× ×©×¢××Ŗ',
'label.total_cost' => '×”× ××× ×¢×××Ŗ',
// TODO: translate the following strings.
// 'label.cron_schedule' => 'Cron schedule',
// 'label.what_is_it' => 'What is it?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => '×× ××”×',
// 'title.add_notification' => 'Adding Notification',
// 'title.edit_notification' => 'Editing Notification',
// 'title.delete_notification' => 'Deleting Notification',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => '××צ×× × ×Ŗ×× × ×¦×××Ŗ',
'title.import' => '××××× × ×Ŗ×× × ×¦×××Ŗ',
'title.options' => '×פש×Ø××××Ŗ',
'label.cost' => 'Kosten',
'label.week_total' => 'Week totaal',
'label.day_total' => 'Dag totaal',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => 'Vandaag',
'label.total_hours' => 'Uren totaal',
'label.total_cost' => 'Totale kosten',
'label.fav_report' => 'Standaard rapport',
'label.cron_schedule' => 'Cron schema',
'label.what_is_it' => 'Wat betekent dit?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'Aanmelden',
'title.add_notification' => 'Notificatie toevoegen',
'title.edit_notification' => 'Notificatie bewerken',
'title.delete_notification' => 'Notificatie verwijderen',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => 'Exporteer teamgegevens',
'title.import' => 'Importeer teamgegevens',
'title.options' => 'Opties',
'label.cost' => 'Koszt',
'label.week_total' => 'W tym tygodniu',
'label.day_total' => 'DziÅ',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => 'DziÅ',
'label.total_hours' => 'CaÅkowita liczba godzin',
'label.total_cost' => 'Koszt caÅkowity',
'label.fav_report' => 'Ulubiony raport',
'label.cron_schedule' => 'Harmonogram crona',
'label.what_is_it' => 'Co to jest?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'Logowanie',
'title.add_notification' => 'Dodawanie powiadomienia',
'title.edit_notification' => 'Edytowanie powiadomienia',
'title.delete_notification' => 'Usuwanie powiadomienia',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => 'Eksport danych zespoÅu',
'title.import' => 'Import danych zespoÅu',
'title.options' => 'Opcje',
'label.cost' => 'Custo',
'label.week_total' => 'Total semanal',
'label.day_total' => 'Total diƔrio',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => 'Hoje',
'label.total_hours' => 'Total de horas',
'label.total_cost' => 'Custo total',
'label.fav_report' => 'RelatĆ³rio favorito',
'label.cron_schedule' => 'Agenda cron',
'label.what_is_it' => 'O que Ć©?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'Login',
'title.add_notification' => 'Adicionando notificaĆ§Ć£o',
'title.edit_notification' => 'Editando notificaĆ§Ć£o',
'title.delete_notification' => 'Apagando notificaĆ§Ć£o',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => 'Exportando dados de equipe',
'title.import' => 'Importando dados de equipe',
'title.options' => 'OpƧƵes',
'label.cost' => 'Š”ŃŠ¾ŠøŠ¼Š¾ŃŃŃ',
'label.week_total' => 'ŠŃŠ¾Š³ Š·Š° Š½ŠµŠ“ŠµŠ»Ń',
'label.day_total' => 'ŠŃŠ¾Š³ Š·Š° Š“ŠµŠ½Ń',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => 'Š”ŠµŠ³Š¾Š“Š½Ń',
'label.total_hours' => 'ŠŃŠ¾Š³Š¾ ŃŠ°ŃŠ¾Š²',
'label.total_cost' => 'ŠŃŠ¾Š³Š¾Š²Š°Ń ŃŃŠ¾ŠøŠ¼Š¾ŃŃŃ',
'label.fav_report' => 'Š”ŃŠ°Š½Š“Š°ŃŃŠ½ŃŠ¹ Š¾ŃŃŃŃ',
'label.cron_schedule' => 'Š Š°ŃŠæŠøŃŠ°Š½ŠøŠµ cron',
'label.what_is_it' => 'Š§ŃŠ¾ ŃŃŠ¾?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'ŠŃ
Š¾Š“ Š² ŃŠøŃŃŠµŠ¼Ń',
'title.add_notification' => 'ŠŠ¾Š±Š°Š²Š»ŠµŠ½ŠøŠµ ŃŠ²ŠµŠ“Š¾Š¼Š»ŠµŠ½ŠøŃ',
'title.edit_notification' => 'Š ŠµŠ“Š°ŠŗŃŠøŃŠ¾Š²Š°Š½ŠøŠµ ŃŠ²ŠµŠ“Š¾Š¼Š»ŠµŠ½ŠøŃ',
'title.delete_notification' => 'Š£Š“Š°Š»ŠµŠ½ŠøŠµ ŃŠ²ŠµŠ“Š¾Š¼Š»ŠµŠ½ŠøŃ',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => 'ŠŠŗŃŠæŠ¾ŃŃŠøŃŠ¾Š²Š°Š½ŠøŠµ Š“Š°Š½Š½ŃŃ
ŠŗŠ¾Š¼Š°Š½Š“Ń',
'title.import' => 'ŠŠ¼ŠæŠ¾ŃŃŠøŃŠ¾Š²Š°Š½ŠøŠµ Š“Š°Š½Š½ŃŃ
ŠŗŠ¾Š¼Š°Š½Š“Ń',
'title.options' => 'ŠŠæŃŠøŠø',
'label.week_total' => 'TĆ½Å¾deÅ celkom',
// TODO: translate the following string.
// 'label.day_total' => 'Day total',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => 'Dnes',
'label.total_hours' => 'HodĆn celkom',
'label.total_cost' => 'NƔklady celkom',
// TODO: translate the following strings.
// 'label.cron_schedule' => 'Cron schedule',
// 'label.what_is_it' => 'What is it?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'PrihlƔsenie',
// 'title.add_notification' => 'Adding Notification',
// 'title.edit_notification' => 'Editing Notification',
// 'title.delete_notification' => 'Deleting Notification',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => 'Exportovanie Ćŗdajov o tĆme',
'title.import' => 'Importovanie Ćŗdajov o tĆme',
'title.options' => 'Nastavenia',
'label.cost' => 'Cena',
'label.week_total' => 'Zbir Äasova nedeljno',
'label.day_total' => 'Zbir Äasova dnevno',
+// 'label.month_total' => 'Month total',
+// 'label.month_left' => 'Time until quota is met',
+// 'label.month_over' => 'Over monthly quota',
'label.today' => 'Danas',
'label.total_hours' => 'Ukupno Äasova',
'label.total_cost' => 'Ukupna cena',
'label.fav_report' => 'Omiljeni izveŔtaji',
'label.cron_schedule' => 'Sredi raspored',
'label.what_is_it' => 'Å ta je ovo?',
+// 'label.year' => 'Year',
+// 'label.month' => 'Month',
+// 'label.quota' => 'Quota',
+// 'label.dailyWorkingHours' => 'Daily working hours',
+// 'label.empty_values_explanation' => 'If values are empty, quotas are calculated automatically based on holidays in config',
// Form titles.
'title.login' => 'Prijava',
'title.add_notification' => 'Dodavanje napomene',
'title.edit_notification' => 'Izmena napomene',
'title.delete_notification' => 'Brisanje napomene',
+// 'title.monthly_quota' => 'Monthly quota',
'title.export' => 'Izvoz podataka tim-a',
'title.import' => 'Uvoz podataka tim-a',
'title.options' => 'Opcije',
--- /dev/null
+{$forms.monthlyQuotaForm.open}
+<div style="padding: 0 0 10 0">
+ <table border="0" class="divider">
+ <tr>
+ <td align="center">
+ <table>
+ <tr>
+ <td>{$i18n.label.dailyWorkingHours}</td>
+ <td>{$forms.monthlyQuotaForm.dailyWorkingHours.control}</td>
+ <td><input type="submit" name="dailyHours" value="{$i18n.button.save}"></td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+</div>
+<table>
+ <tr>
+ <td>{$i18n.label.year}:</td>
+ <td>{$forms.monthlyQuotaForm.years.control}</td>
+ </tr>
+ <tr>
+ <td colspan="2">
+ <table>
+ <tr>
+ <td class="tableHeader">{$i18n.label.month}</td>
+ <td class="tableHeader">{$i18n.label.quota}</td>
+ </tr>
+ {foreach $months as $month}
+ <tr>
+ <td>{$month}</td>
+ <td>{$forms.monthlyQuotaForm.$month.control}</td>
+ </tr>
+ {/foreach}
+ <tr>
+ <td colspan="2" style="text-align:center;">
+ <input type="submit" name="quotas" value="{$i18n.button.save}*">
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+</table>
+<div>* - {$i18n.label.empty_values_explanation}</div>
+{$forms.monthlyQuotaForm.close}
+<script>
+function yearChange(value){
+ var url = window.location.href;
+
+ if (url.indexOf('?') > 0){
+ var parameter = url.substring(url.indexOf('?') + 1, url.length);
+ url = url.replace(parameter, 'year=' + value);
+ } else {
+ url = '?year=' + value;
+ }
+
+ window.location = url;
+}
+</script>
\ No newline at end of file
<br>
<table cellspacing="0" cellpadding="4" width="100%" border="0">
<tr>
- <td align="center"> Anuko Time Tracker 1.9.25.3499 | Copyright © <a href="https://www.anuko.com/lp/tt_3.htm" target="_blank">Anuko</a> |
+ <td align="center"> Anuko Time Tracker 1.9.26.3503 | Copyright © <a href="https://www.anuko.com/lp/tt_3.htm" target="_blank">Anuko</a> |
<a href="https://www.anuko.com/lp/tt_4.htm" target="_blank">{$i18n.footer.credits}</a> |
<a href="https://www.anuko.com/lp/tt_5.htm" target="_blank">{$i18n.footer.license}</a> |
<a href="https://www.anuko.com/lp/tt_7.htm" target="_blank">{$i18n.footer.improve}</a>
} else {
configureLabel.style.visibility = "hidden";
}
+
+ var monthlyQuotaCheckBox = document.getElementById("monthly_quota");
+ configureLabel = document.getElementById("monthly_quota_config");
+ if (monthlyQuotaCheckBox.checked){
+ configureLabel.style.visibility = "visible";
+ } else {
+ configureLabel.style.visibility = "hidden";
+ }
+
}
</script>
</tr>
<tr>
<td align="right" nowrap>{$forms.profileForm.locking.control}</td>
- <td><label for="notifications">{$i18n.title.locking}</label> <span id="locking_config"><a href="locking.php">{$i18n.label.configure}</a></span></td>
+ <td><label for="locking">{$i18n.title.locking}</label> <span id="locking_config"><a href="locking.php">{$i18n.label.configure}</a></span></td>
+ </tr>
+ <tr>
+ <td align="right" nowrap>{$forms.profileForm.monthly_quota.control}</td>
+ <td><label for="monthly_quota">{$i18n.title.monthly_quota}</label> <span id="monthly_quota_config"><a href="cf_monthly_quota.php">{$i18n.label.configure}</a></span></td>
</tr>
{/if}
{$forms.reportForm.open}
<div style="padding: 0 0 10 0;">
- <table border="0" bgcolor="#efefef" width="720">
+ <table border="0" class="divider">
<tr>
<td>
<table cellspacing="1" cellpadding="3" border="0">
</table>
<div style="padding: 10 0 10 0;">
- <table border="0" bgcolor="#efefef" width="720">
+ <table border="0" class="divider">
<tr>
<td align="center">
<table cellspacing="1" cellpadding="3" border="0">
<td align="left">{$i18n.label.week_total}: {$week_total}</td>
<td align="right">{$i18n.label.day_total}: {$day_total}</td>
</tr>
+ {if $month_total}
+ <tr>
+ <td align="left">{$i18n.label.month_total}: {$month_total}</td>
+ {if $month_left|strpos:'-' === 0}
+ <td align="right">{$i18n.label.month_over}: <span style="color: green;">{$month_left|substr:1}</span></td>
+ {else}
+ <td align="right">{$i18n.label.month_left}: <span style="color: red;">{$month_left}</span></td>
+ {/if}
+ </tr>
+ {/if}
</table>
{/if}
{$forms.timeRecordForm.close}
--- /dev/null
+<?php
+
+require_once('initialize.php');
+require_once('plugins/MonthlyQuota.class.php');
+import('form.Form');
+import('WEB-INF/lib/ttTeamHelper');
+
+// Access check.
+if (!ttAccessCheck(right_manage_team)) {
+ header('Location: access_denied.php');
+ exit();
+}
+
+// fallback values
+$yearStart = 2010;
+$yearEnd = 2030;
+
+if (defined('MONTHLY_QUOTA_YEARS_START')){
+ $yearStart = (int)MONTHLY_QUOTA_YEARS_START;
+}
+if (defined('MONTHLY_QUOTA_YEARS_END')){
+ $yearEnd = (int)MONTHLY_QUOTA_YEARS_END;
+}
+
+// create values for dropdown
+$years = array();
+for ($i=$yearStart; $i < $yearEnd; $i++) {
+ array_push($years, array('id'=>$i, 'name'=>$i));
+}
+
+// get selected year from url parameters
+$selectedYear = $request->getParameter("year");
+if (!$selectedYear or !ttValidInteger($selectedYear)){
+ $selectedYear = date("Y");
+} else {
+ $selectedYear = intval($selectedYear);
+}
+
+// months are zero indexed
+$months = $i18n->monthNames;
+
+$quota = new MonthlyQuota();
+
+if ($request->isPost()){
+ $res = false;
+ // if user pressed save fpr monthly quotas
+ if ($_POST["quotas"]){
+ $postedYear = $request->getParameter("years");
+ $selectedYear = intval($postedYear);
+ for ($i=0; $i < count($months); $i++){
+ $res = $quota->update($postedYear, $i+1, $request->getParameter($months[$i]));
+ }
+ }
+ // if user saved required working hours for a day
+ if ($_POST["dailyHours"]){
+ $hours = $request->getParameter("dailyWorkingHours");
+ $teamDetails = ttTeamHelper::getTeamDetails($quota->usersTeamId);
+ $res = ttTeamHelper::update($quota->usersTeamId, array('name'=>$teamDetails['team_name'],
+ 'working_hours'=>$hours));
+ }
+ if ($res) {
+ header('Location: profile_edit.php');
+ exit();
+ } else {
+ $err->add($i18n->getKey('error.db'));
+ }
+}
+
+// returns months where January is month 1, not 0
+$monthsData = $quota->get($selectedYear);
+
+$form = new Form('monthlyQuotaForm');
+
+$form->addInput(array('type'=>'combobox', 'name'=>'years', 'data'=>$years, 'datakeys'=>array('id', 'name'), 'value'=>$selectedYear, 'onchange'=>'yearChange(this.value);'));
+for ($i=0; $i < count($months); $i++) {
+ $value = "";
+ if (array_key_exists($i+1, $monthsData)){
+ $value = $monthsData[$i+1];
+ }
+ $name = $months[$i];
+ $form->addInput(array('type'=>'text', 'name'=>$name, 'maxlength'=>3, 'value'=> $value, 'style'=>'width:50px'));
+}
+$form->addInput(array('type'=>'text', 'name'=>'dailyWorkingHours', 'value'=>$quota->getDailyWorkingHours(), 'style'=>'width:50px'));
+$smarty->assign('months', $months);
+$smarty->assign('forms', array($form->getName()=>$form->toArray()));
+$smarty->assign('title', $i18n->getKey('title.monthly_quota'));
+$smarty->assign('content_page_name', 'cf_monthly_quota.tpl');
+$smarty->display('index.tpl');
print "successful update<br>\n";
}
+if (!$request->isPost()) {
+ echo('<h2>Environment check</h2>');
+
+ // Check if WEB-INF/templates_c dir is writable.
+ if (is_writable(APP_DIR.'/WEB-INF/templates_c/')) {
+ echo('WEB-INF/templates_c/ directory is writable.<br>');
+ } else {
+ echo('<font color="red">Error: WEB-INF/templates_c/ directory is not writable.</font><br>');
+ }
+
+ // Require the configuration file with application settings.
+ if (file_exists(APP_DIR."/WEB-INF/config.php")) {
+ echo('WEB-INF/config.php file exists.<br>');
+ } else {
+ echo('<font color="red">Error: WEB-INF/config.php file does not exist.</font><br>');
+ }
+
+ // Check whether DSN is defined.
+ if (defined('DSN')) {
+ // echo('DSN is defined as '.DSN.'<br>');
+ echo('DSN is defined.<br>');
+ } else {
+ echo('<font color="red">Error: DSN value is not defined. Check your config.php file.</font><br>');
+ }
+
+ // Depending on DSN, require either mysqli or mysql extensions.
+ if (strrpos(DSN, 'mysqli://', -strlen(DSN)) !== FALSE) {
+ if (extension_loaded('mysqli')) {
+ echo('mysqli PHP extension is loaded.<br>');
+ } else {
+ echo('<font color="red">Error: mysqli PHP extension is required but is not loaded.</font><br>');
+ }
+ }
+ if (strrpos(DSN, 'mysql://', -strlen(DSN)) !== FALSE) {
+ if (extension_loaded('mysql')) {
+ echo('mysql PHP extension is loaded.<br>');
+ } else {
+ echo('<font color="red">Error: mysql PHP extension is required but is not loaded.</font><br>');
+ }
+ }
+
+ // Check mbstring extension.
+ if (extension_loaded('mbstring')) {
+ echo('mbstring PHP extension is loaded.<br>');
+ } else {
+ echo('<font color="red">Error: mbstring PHP extension is not loaded.</font><br>');
+ }
+
+ // Check gd extension.
+ if (extension_loaded('gd')) {
+ echo('gd PHP extension is loaded.<br>');
+ } else {
+ echo('<font color="red">Error: gd PHP extension is not loaded. It is required for charts plugin.</font><br>');
+ }
+
+ // Check ldap extension.
+ if (AUTH_MODULE == 'ldap') {
+ if (extension_loaded('ldap_')) {
+ echo('ldap PHP extension is loaded.<br>');
+ } else {
+ echo('<font color="red">Error: ldap PHP extension is not loaded. It is required for LDAP authentication.</font><br>');
+ }
+ }
+
+ // Check database access.
+ require_once('MDB2.php');
+ $conn = MDB2::connect(DSN);
+ if (!is_a($conn, 'MDB2_Error')) {
+ echo('Connection to database successful.<br>');
+ } else {
+ die('<font color="red">Error: connection to database failed. '.$conn->getMessage().'</font><br>');
+ }
+
+ $conn->setOption('debug', true);
+ $conn->setFetchMode(MDB2_FETCHMODE_ASSOC);
+
+ $sql = "show tables";
+ $res = $conn->query($sql);
+ if (is_a($res, 'MDB2_Error')) {
+ die('<font color="red">Error: show tables returned an error. '.$res->getMessage().'</font><br>');
+ }
+ $tblCnt = 0;
+ while ($val = $res->fetchRow()) {
+ $tblCnt++;
+ }
+ if ($tblCnt > 0) {
+ echo("There are $tblCnt tables in database.<br>");
+ } else {
+ echo('<font color="red">There are no tables in database. Execute step 1 - Create database structure.</font><br>');
+ }
+ $conn->disconnect();
+}
+
if ($_POST) {
print "Processing...<br>\n";
setChange("create unique index name_idx on tt_invoices(team_id, name, status)");
setChange("ALTER TABLE tt_teams ADD COLUMN lock_spec varchar(255) default NULL");
setChange("ALTER TABLE tt_teams DROP locktime");
+ setChange("CREATE TABLE `tt_monthly_quota` (`team_id` int(11) NOT NULL, `year` smallint(5) UNSIGNED NOT NULL, `month` tinyint(3) UNSIGNED NOT NULL, `quota` smallint(5) UNSIGNED NOT NULL, PRIMARY KEY (`year`,`month`,`team_id`))");
+ setChange("ALTER TABLE `tt_monthly_quota` ADD CONSTRAINT `FK_TT_TEAM_CONSTRAING` FOREIGN KEY (`team_id`) REFERENCES `tt_teams` (`id`) ON DELETE CASCADE ON UPDATE CASCADE");
+ setChange("ALTER TABLE `tt_teams` ADD `daily_working_hours` SMALLINT NULL DEFAULT '8' AFTER `lock_spec`");
+ setChange("UPDATE `tt_teams` SET `daily_working_hours` = 8");
}
-
+
// The update_clients function updates projects field in tt_clients table.
if ($_POST["update_clients"]) {
$mdb2 = getConnection();
setChange("OPTIMIZE TABLE tt_fav_reports");
setChange("OPTIMIZE TABLE tt_invoices");
setChange("OPTIMIZE TABLE tt_log");
+ setChange("OPTIMIZE TABLE tt_monthly_quota");
setChange("OPTIMIZE TABLE tt_project_task_binds");
setChange("OPTIMIZE TABLE tt_projects");
setChange("OPTIMIZE TABLE tt_tasks");
<h2>DB Install</h2>
<table width="80%" border="1" cellpadding="10" cellspacing="0">
<tr>
- <td width="80%"><b>Create database structure (v1.9)</b>
+ <td width="80%"><b>Create database structure (v1.9.30)</b>
<br>(applies only to new installations, do not execute when updating)</br></td><td><input type="submit" name="crstructure" value="Create"></td>
</tr>
</table>
border-bottom: 1px solid silver;
}
+.borderTop td {
+ border-top: 1px solid silver;
+}
+
.sectionHeaderNoBorder {
font-weight: bold;
}
color: #0000c0;
}
+.divider {
+ background-color: #efefef;
+}
+table.divider {
+ width: 720px;
+}
+
div#LoginAboutText { width:400px; }
`plugins` varchar(255) default NULL, # a list of enabled plugins for team
`lock_spec` varchar(255) default NULL, # Cron specification for record locking,
# for example: "0 10 * * 1" for "weekly on Mon at 10:00".
+ `daily_working_hours` smallint(6) DEFAULT '8', # number of working hours per days, a worker is suppose to work
`custom_logo` tinyint(4) default '0', # whether to use a custom logo or not
`status` tinyint(4) default '1', # team status
PRIMARY KEY (`id`)
create index client_idx on tt_expense_items(client_id);
create index project_idx on tt_expense_items(project_id);
create index invoice_idx on tt_expense_items(invoice_id);
+
+#
+# Structure for table tt_monthly_quota.
+# This table lists monthly quota per team.
+#
+
+CREATE TABLE `tt_monthly_quota` (
+ `team_id` int(11) NOT NULL, # team's id
+ `year` smallint(5) UNSIGNED NOT NULL, # year we'setting monthly quota for
+ `month` tinyint(3) UNSIGNED NOT NULL, # month we're settng monthly quota for
+ `quota` smallint(5) UNSIGNED NOT NULL, # the monthly quota
+ PRIMARY KEY (`year`,`month`,`team_id`)
+);
+
+ALTER TABLE `tt_monthly_quota`
+ ADD CONSTRAINT `FK_TT_TEAM_CONSTRAING` FOREIGN KEY (`team_id`) REFERENCES `tt_teams` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
--- /dev/null
+<?php
+
+class MonthlyQuota {
+
+ var $db;
+ var $holidays;
+ var $usersTeamId;
+ // Old style constructors are DEPRECATED in PHP 7.0, and will be removed in a future version. You should always use __construct() in new code.
+ function __construct() {
+ $this->db = getConnection();
+ $i18n = $GLOBALS['I18N'];
+ $this->holidays = $i18n->holidays;
+ global $user;
+ $this->usersTeamId = $user->team_id;
+ }
+
+ public function update($year, $month, $quota) {
+ $teamId = $this->usersTeamId;
+ $deleteSql = "DELETE FROM tt_monthly_quota WHERE year = $year AND month = $month AND team_id = $teamId";
+ $this->db->exec($deleteSql);
+ if ($quota){
+ $insertSql = "INSERT INTO tt_monthly_quota (team_id, year, month, quota) values ($teamId, $year, $month, $quota)";
+ $affected = $this->db->exec($insertSql);
+ return (!is_a($affected, 'PEAR_Error'));
+ }
+ return true;
+ }
+
+ public function get($year, $month) {
+ if (is_null($month)){
+ return $this->getMany($year);
+ }
+
+ return $this->getSingle($year, $month);
+ }
+
+ public function getDailyWorkingHours(){
+ $teamId = $this->usersTeamId;
+ $sql = "SELECT daily_working_hours FROM tt_teams where id = $teamId";
+ $reader = $this->db->query($sql);
+ if (is_a($reader, 'PEAR_Error')) {
+ return false;
+ }
+
+ $row = $reader->fetchRow();
+ return $row["daily_working_hours"];
+ }
+
+ private function getSingle($year, $month) {
+ $teamId = $this->usersTeamId;
+ $sql = "SELECT quota FROM tt_monthly_quota WHERE year = $year AND month = $month AND team_id = $teamId";
+ $reader = $this->db->query($sql);
+ if (is_a($reader, 'PEAR_Error')) {
+ return false;
+ }
+
+ $row = $reader->fetchRow();
+
+ // if we don't find a record, return calculated monthly quota
+ if (is_null($row)){
+
+ $holidaysWithYear = array();
+ foreach ($this->holidays as $day) {
+ $parts = explode("/", $day);
+ $holiday = "$year-$parts[0]-$parts[1]";
+ array_push($holidaysWithYear, $holiday);
+ }
+
+ $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
+ return $this->getWorkingDays("$year-$month-01", "$year-$month-$daysInMonth", $holidaysWithYear) * $this->getDailyWorkingHours();
+ }
+
+ return $row["quota"];
+ }
+
+ private function getMany($year){
+ $teamId = $this->usersTeamId;
+ $sql = "SELECT month, quota FROM tt_monthly_quota WHERE year = $year AND team_id = $teamId";
+ $result = array();
+ $res = $this->db->query($sql);
+ if (is_a($res, 'PEAR_Error')) {
+ return false;
+ }
+
+ while ($val = $res->fetchRow()) {
+ $result[$val["month"]] = $val["quota"];
+ // $result[] = $val;
+ }
+
+ return $result;
+ }
+
+ //The function returns the no. of business days between two dates and it skips the holidays
+ private function getWorkingDays($startDate, $endDate, $holidays) {
+ // do strtotime calculations just once
+ $endDate = strtotime($endDate);
+ $startDate = strtotime($startDate);
+
+ //The total number of days between the two dates. We compute the no. of seconds and divide it to 60*60*24
+ //We add one to inlude both dates in the interval.
+ $days = ($endDate - $startDate) / 86400 + 1;
+
+ $noOfFullWeeks = floor($days / 7);
+ $noOfRemainingDays = fmod($days, 7);
+
+ //It will return 1 if it's Monday,.. ,7 for Sunday
+ $firstDayofWeek = date("N", $startDate);
+ $lastDayofWeek = date("N", $endDate);
+
+ //---->The two can be equal in leap years when february has 29 days, the equal sign is added here
+ //In the first case the whole interval is within a week, in the second case the interval falls in two weeks.
+ if ($firstDayofWeek <= $lastDayofWeek) {
+ if ($firstDayofWeek <= 6 && 6 <= $lastDayofWeek) {
+ $noOfRemainingDays--;
+ }
+
+ if ($firstDayofWeek <= 7 && 7 <= $lastDayofWeek) {
+ $noOfRemainingDays--;
+ }
+ }
+ else {
+ // (edit by Tokes to fix an edge case where the start day was a Sunday
+ // and the end day was NOT a Saturday)
+
+ // the day of the week for start is later than the day of the week for end
+ if ($firstDayofWeek == 7) {
+ // if the start date is a Sunday, then we definitely subtract 1 day
+ $noOfRemainingDays--;
+
+ if ($lastDayofWeek == 6) {
+ // if the end date is a Saturday, then we subtract another day
+ $noOfRemainingDays--;
+ }
+ }
+ else {
+ // the start date was a Saturday (or earlier), and the end date was (Mon..Fri)
+ // so we skip an entire weekend and subtract 2 days
+ $noOfRemainingDays -= 2;
+ }
+ }
+
+ //T he no. of business days is: (number of weeks between the two dates) * (5 working days) + the remainder
+ // ---->february in none leap years gave a remainder of 0 but still calculated weekends between first and last day, this is one way to fix it
+ $workingDays = $noOfFullWeeks * 5;
+ if ($noOfRemainingDays > 0 ) {
+ $workingDays += $noOfRemainingDays;
+ }
+
+ // We subtract the holidays
+ foreach($holidays as $holiday){
+ $timeStamp = strtotime($holiday);
+ // If the holiday doesn't fall in weekend
+ // TODO: add handling for countries where they move non working day to first working day if holiday is on weekends
+ if ($startDate <= $timeStamp && $timeStamp <= $endDate && date("N", $timeStamp) != 6 && date("N", $timeStamp ) != 7)
+ $workingDays--;
+ }
+
+ return $workingDays;
+ }
+}
\ No newline at end of file
$cl_tax_expenses = $request->getParameter('tax_expenses');
$cl_notifications = $request->getParameter('notifications');
$cl_locking = $request->getParameter('locking');
+ $cl_monthly_quota = $request->getParameter('monthly_quota');
}
} else {
$cl_name = $user->name;
$cl_clients = in_array('cl', $plugins);
$cl_client_required = in_array('cm', $plugins);
$cl_invoices = in_array('iv', $plugins);
- $cl_custom_fields = in_array('cf', $plugins);
+ $cl_custom_fields = in_array('cf', $plugins);
$cl_expenses = in_array('ex', $plugins);
$cl_tax_expenses = in_array('et', $plugins);
$cl_notifications = in_array('no', $plugins);
$cl_locking = in_array('lk', $plugins);
+ $cl_monthly_quota = in_array('mq', $plugins);
}
}
$form->addInput(array('type'=>'checkbox','name'=>'tax_expenses','data'=>1,'value'=>$cl_tax_expenses));
$form->addInput(array('type'=>'checkbox','name'=>'notifications','data'=>1,'value'=>$cl_notifications,'onchange'=>'handlePluginCheckboxes()'));
$form->addInput(array('type'=>'checkbox','name'=>'locking','data'=>1,'value'=>$cl_locking,'onchange'=>'handlePluginCheckboxes()'));
+ $form->addInput(array('type'=>'checkbox','name'=>'monthly_quota','data'=>1,'value'=>$cl_monthly_quota,'onchange'=>'handlePluginCheckboxes()'));
}
$form->addInput(array('type'=>'submit','name'=>'btn_save','value'=>$i18n->getKey('button.save')));
$plugins .= ',no';
if ($cl_locking)
$plugins .= ',lk';
+ if ($cl_monthly_quota)
+ $plugins .= ',mq';
$plugins = trim($plugins, ',');
$update_result = ttTeamHelper::update($user->team_id, array(
$smarty->assign('custom_fields', $custom_fields);
}
+if ($user->isPluginEnabled('mq')){
+ require_once('plugins/MonthlyQuota.class.php');
+ $quota = new MonthlyQuota();
+ $monthlyQuota = $quota->get($selected_date->mYear, $selected_date->mMonth);
+ $month_total = ttTimeHelper::getTimeForMonth($user->getActiveUser(), $selected_date);
+ $minutesLeft = ttTimeHelper::toMinutes($monthlyQuota) - ttTimeHelper::toMinutes($month_total);
+
+ $smarty->assign('month_total', $month_total);
+ $smarty->assign('month_left', ttTimeHelper::fromMinutes($minutesLeft));
+}
+
// Initialize variables.
$cl_start = trim($request->getParameter('start'));
$cl_finish = trim($request->getParameter('finish'));