Compare commits

...

760 Commits
1.14 ... main

Author SHA1 Message Date
Weblate (bot)
d456cd3ce2 Translated using Weblate (#5892)
Co-authored-by: Lasse Edsvik <lasse@lasseedsvik.se>
Co-authored-by: Milo Ivir <mail@milotype.de>
2026-04-13 21:23:49 +02:00
Kevin Papst
bad92d7215 Release 2.54 (#5896) 2026-04-13 21:22:06 +02:00
Kevin Papst
16703081cd Exporter/Invoice formula cleanup (#5899) 2026-04-12 09:27:05 +02:00
Kevin Papst
cbdf91f316 Team API docs (#5897)
* let view_team permission be handled by global ACLs
* code style and API docs
* improve permission check
2026-04-11 17:22:59 +02:00
Kevin Papst
999d820d4c Release 2.53 (#5878) 2026-04-10 18:09:27 +02:00
Weblate (bot)
fe4185ae45 Translated using Weblate (#5879)
Co-authored-by: C. H. <them4z@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Patryk <patryk230206@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: remo <remohexa@gmail.com>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
2026-04-08 23:15:13 +02:00
Kevin Papst
cad9f58703 Release 2.52 (#5874) 2026-03-16 17:44:12 +01:00
Weblate (bot)
70b4fbcae8 Translated using Weblate (#5848)
Co-authored-by: AlaxLima <thanhkhoidangngoc@gmail.com>
Co-authored-by: Christopher Picón <ntrpc.tech@users.noreply.hosted.weblate.org>
Co-authored-by: Francisco Serrador <fserrador@gmail.com>
Co-authored-by: Kamborio <Kamborio15@users.noreply.hosted.weblate.org>
Co-authored-by: PizzaPoot <pizzapoot@users.noreply.hosted.weblate.org>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: clearstripe <sakurasan000510@gmail.com>
2026-03-16 17:39:50 +01:00
Kevin Papst
a0601c8cb2 check customer permissions on invoice api access (#5849) 2026-03-01 16:53:49 +01:00
Kevin Papst
5b320bf2ea Release 2.51 (#5847)
* bump packages
2026-03-01 16:21:07 +01:00
Kevin Papst
15110f78d8 fix sticky calendar popup (#5846) 2026-02-28 19:25:06 +01:00
Kevin Papst
4154de6bd1 Release 2.50 (#5835)
* replace p-0 class with fullsize embed option
* bump parsedown package
* remove support for file:// urls
* fix missing macro in export print template
* fix weekly hours with breaks
2026-02-25 21:07:40 +01:00
Weblate (bot)
8094fcf5b5 Translated using Weblate (#5831)
Co-authored-by: Francisco Serrador <fserrador@gmail.com>
Co-authored-by: Marat Ismailov <klippygames@gmail.com>
Co-authored-by: Sean Young <assanges@users.noreply.hosted.weblate.org>
2026-02-25 21:06:48 +01:00
Kevin Papst
77afa207e0 Allow to customize statistic queries (#5827)
* use TimesheetStatisticsQUery for all repository calls
* send event to customize statistics query
2026-02-24 13:27:48 +01:00
Kevin Papst
d9ef6dfcad Update database requirements in README (#5825) 2026-02-15 21:42:25 +01:00
Kevin Papst
ff6918fcab Release 2.49 (#5820)
* bump packages
* add column summarization per customer (#5808)
* clarify database requirements
* added helper methods to fetch original expected time

Co-authored-by: GregorB54321 <34287148+GregorB54321@users.noreply.github.com>
2026-02-15 21:31:06 +01:00
Weblate (bot)
f376b5c8a1 Translated using Weblate (#5804)
Co-authored-by: AlaxLima <thanhkhoidangngoc@gmail.com>
Co-authored-by: Arif Budiman <arifpedia@gmail.com>
Co-authored-by: Artem <artemkozhin80@gmail.com>
Co-authored-by: Eleni Diamantopoulou <elenidiama00@gmail.com>
Co-authored-by: Florent Berthelot <florentius.b@gmail.com>
Co-authored-by: Kehribar <103407696+dpentx@users.noreply.github.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Lasse Edsvik <lasse@lasseedsvik.se>
Co-authored-by: Levente Déri <derilevi@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Paul <snide-slum-partake@duck.com>
Co-authored-by: Posemartonis <weblate.drainage895@passmail.net>
Co-authored-by: Romhányi-Kakucska Viktor <viktor@romhanyi.dev>
Co-authored-by: irooniline <mart.styff@protonmail.com>
Co-authored-by: leonidovmob <leonidovmob@gmail.com>
2026-02-15 21:28:53 +01:00
Kevin Papst
b00fbd2516 Delete .opensourcefinder-verify 2026-02-14 18:56:38 +01:00
Kevin Papst
d97abc3f57 Add .opensourcefinder-verify file with claims (#5824) 2026-02-14 08:19:51 +01:00
Kevin Papst
3f184e42c8 missing translations 2026-01-31 10:28:30 +01:00
Kevin Papst
4a31411d69 Release 2.48 (#5789) 2026-01-30 16:44:26 +01:00
Weblate (bot)
3925eacf9f Translated using Weblate (#5748)
Co-authored-by: AlaxLima <thanhkhoidangngoc@gmail.com>
Co-authored-by: Amir <amearb@duck.com>
Co-authored-by: Fabio Gomes de lima <fabiogomesdelima598@gmail.com>
Co-authored-by: Heeheon Ryu <heeheon.ryu001@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Kolappan N <kolappan@users.noreply.hosted.weblate.org>
Co-authored-by: Lasse Edsvik <lasse@lasseedsvik.se>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Mauro F. T. <maurofroeltani@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pose marto <weblate.drainage895@passmail.net>
Co-authored-by: Serhii Horichenko <serhii@horichenko.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: avv-dev <vildan.abdullin@gmail.com>
Co-authored-by: m45ked <m45ked@users.noreply.hosted.weblate.org>
2026-01-30 13:14:21 +01:00
Kevin Papst
394f377890 Update header (#5799) 2026-01-28 20:10:33 +01:00
Milo Ivir
e8b0dc4969 Add translation links to README (#5798)
Co-authored-by: Kevin Papst <kevinpapst@users.noreply.github.com>
2026-01-28 19:58:12 +01:00
Kevin Papst
d429c56687 Release 2.47 (#5784) 2026-01-25 09:51:22 +01:00
Kevin Papst
6a86afb5fd Release 2.46 (#5757) 2026-01-07 00:59:47 +01:00
Kevin Papst
9e87fc131b fade-out customer address (#5749) 2025-12-27 17:53:03 +01:00
Kevin Papst
5a0c783a76 added section name for meta-fields positioning (#5747) 2025-12-26 12:54:15 +01:00
Kevin Papst
26361da873 Support PHP 8.5 (#5746) 2025-12-25 16:44:11 +01:00
Weblate (bot)
26f8abfcc6 Translated using Weblate (#5745)
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Santiago Atienza Ferro <satienza@gmail.com>
Co-authored-by: World170 <dylanartigas10@gmail.com>
2025-12-25 14:10:09 +01:00
Kevin Papst
27c2b912f7 run CI also for php 8.5 2025-12-25 12:20:56 +01:00
Kevin Papst
7e72f144cb clarify api doc 2025-12-25 12:20:05 +01:00
Kevin Papst
3c3d6379a8 Invoice tax rates API (#5740) 2025-12-24 18:47:42 +01:00
Kevin Papst
806bb97e60 query modified_after in UTC (#5743) 2025-12-24 16:33:28 +01:00
Kevin Papst
3fcaf55d02 merge default currency settings (#5739) 2025-12-22 17:55:48 +01:00
Kevin Papst
f0e2337d67 fix dark theme was bright depending on OS settings 2025-12-22 14:24:20 +01:00
Weblate (bot)
00d801616a Translated using Weblate (#5705)
Co-authored-by: 4ipset <228gus228uu@gmail.com>
Co-authored-by: Anthony Cyndora <anthony270777@gmail.com>
Co-authored-by: C. H. <them4z@gmail.com>
Co-authored-by: Christopher Picón <ntrpc.tech@users.noreply.hosted.weblate.org>
Co-authored-by: Dane Lazov <dane.lazov@gmail.com>
Co-authored-by: Lasse Edsvik <lasse@lasseedsvik.se>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Mohammed “Medait” AIT ALI <medait.31@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Serhii Horichenko <serhii@horichenko.com>
Co-authored-by: Wilker Santana da Silva <wilker@posteo.com.br>
2025-12-21 17:11:42 +01:00
Kevin Papst
afb5a0bba4 Release 2.45 (#5721)
Co-authored-by: Henning Klein <info@henningklein.de>
2025-12-21 16:42:34 +01:00
Kevin Papst
8c1ed68817 improved dark mode, automatic theme switch (#5720) 2025-12-19 23:51:27 +01:00
Kevin Papst
b61420d633 prevent lock file changes 2025-12-18 23:07:00 +01:00
Kevin Papst
595b5f4b25 Configurable rate rounding (#5734)
* added invoice hydration of the issuer object
* added "rate calculator" mode
* new config to select rounding mode
* replace static calls with dependency injection
2025-12-16 16:47:46 +01:00
Weblate (bot)
07ac6c308f Translated using Weblate (#5704)
Co-authored-by: Serhii Horichenko <serhii@horichenko.com>
Co-authored-by: mostafa <m2-vision@users.noreply.hosted.weblate.org>
2025-11-19 15:21:30 +01:00
Kevin Papst
a6766373b4 fix modal reload 2025-11-19 15:17:24 +01:00
Kevin Papst
a15c1e56cb Release 2.44 (#5699) 2025-11-19 14:28:54 +01:00
Kevin Papst
8a42c9640f Release 2.43 (#5694) 2025-11-14 17:41:33 +01:00
Kevin Papst
8e6764b67a Release 2.42 (#5686) 2025-11-12 16:15:04 +01:00
Weblate (bot)
ea29799b89 Translated using Weblate (#5690)
Co-authored-by: Lasse Edsvik <lasse@lasseedsvik.se>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Santiago Atienza Ferro <satienza@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
2025-11-12 15:40:55 +01:00
Weblate (bot)
a3ef1fd1cd Translated using Weblate (#5649)
Co-authored-by: Adrián Gelmotto Ruiz <adriangelmotto@gmail.com>
Co-authored-by: Arif Budiman <arifpedia@gmail.com>
Co-authored-by: Bamowen <mathieu.monsauret@gmail.com>
Co-authored-by: Carlos Carreras <mytriponlinux@gmail.com>
Co-authored-by: Dao Duy Tin <duytin095@gmail.com>
Co-authored-by: GD <guillaume.debat65@gmail.com>
Co-authored-by: Henry Higgins <leserboka@outlook.com>
Co-authored-by: Jonas Tisell <jonas.tisell@live.no>
Co-authored-by: Lasse Edsvik <lasse@lasseedsvik.se>
Co-authored-by: Lenny Angst <lenny@familie-angst.ch>
Co-authored-by: Levente Déri <derilevi@gmail.com>
Co-authored-by: LordTenebrous <danielmorenoperez836@gmail.com>
Co-authored-by: Lourenço Martins <hlourencoam93@gmail.com>
Co-authored-by: Marco Moreno <hibarioath@proton.me>
Co-authored-by: Martin Maslyankov <m.maslyankov@me.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Michedepain <benoitravel63000@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nadie <ef.dal.1200@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Parms <shoppingpar+weblate@simplelogin.com>
Co-authored-by: Preben Rather Sørensen <preben@rather.dk>
Co-authored-by: Py- Droid <droidpy587@gmail.com>
Co-authored-by: Rafa Herzog <49111482+necronyxon@users.noreply.github.com>
Co-authored-by: RoboDoc <theonlyrobodoc@gmail.com>
Co-authored-by: Serhii Horichenko <serhii@horichenko.com>
Co-authored-by: Tuna <ahmettunadem@gmail.com>
Co-authored-by: Turkish Language Team 🇹🇷 <turkishmark@yandex.com>
Co-authored-by: Wolf <wolski.marex@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: michte <michteting@proton.me>
Co-authored-by: mostafa <m2-vision@users.noreply.hosted.weblate.org>
Co-authored-by: no <kotvpaltoof@yandex.com>
Co-authored-by: vanapro1 <law820314@gmail.com>
Co-authored-by: zhao rongkuan (kuank) <zhaorongkuan2011@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic4@gmail.com>
2025-11-09 22:48:25 +01:00
Kevin Papst
1a38c7d7a3 Release 2.41 (#5653) 2025-11-08 23:03:44 +01:00
Kevin Papst
636a51e721 group invoices by project and activity (#5675) 2025-11-02 12:34:41 +01:00
Kevin Papst
cc64acf0f8 Meta-fields for InvoiceTemplate, structured Customer address (#5519) 2025-11-02 11:24:17 +01:00
Kevin Papst
76821c24ed bump workflow version 2025-10-06 12:37:30 +02:00
Weblate (bot)
190b1dd4f3 Translated using Weblate (#5634)
Co-authored-by: Lasse Edsvik <lasse@lasseedsvik.se>
Co-authored-by: Lenny Angst <lenny@familie-angst.ch>
Co-authored-by: Py- Droid <droidpy587@gmail.com>
Co-authored-by: Ryohei Morimoto <caulked.thorax_3l@icloud.com>
Co-authored-by: WastedInside <arvidsloc@gmail.com>
Co-authored-by: oneshotkot <oneshotkot@gmail.com>
Co-authored-by: zhao rongkuan (kuank) <zhaorongkuan2011@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
2025-09-26 12:12:48 +02:00
Kevin Papst
7937fa281a Release 2.40.0 (#5621) 2025-09-26 10:49:06 +02:00
Kevin Papst
6d78c6ba36 Configurable PDF exports (#5641) 2025-09-23 18:32:31 +02:00
Weblate (bot)
5d3e46cce2 Translated using Weblate (#5623)
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Martin Maslyankov <m.maslyankov@me.com>
Co-authored-by: Wei-Cheng Yeh (IID) <iid@ccns.ncku.edu.tw>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-08-31 12:56:12 +02:00
Kevin Papst
1c3aac78f6 remove debug 2025-08-30 18:27:21 +02:00
Kevin Papst
b1fb300a71 missing assets 2025-08-30 17:08:03 +02:00
Kevin Papst
a4d658b821 Release 2.39 (#5604)
* prepare audit via annotation
* default calendar slot label distance of 1h
+ replace freestyle config with dropdown
* added missing return definition in callbacks
* refactor view name handling
* dispatch calendar view changes and push them into the URL to be able to reload the poage
* bump packages
* fix timezone issue in calendar sum calculation
* fixes #5618 resetRates()
* show expected daily hours in working-contract screen
2025-08-30 11:41:17 +02:00
Weblate (bot)
4920ea5075 Translated using Weblate (#5611)
Co-authored-by: Frank JIn <chinafrank0129@outlook.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Kolappan N <kolappan@users.noreply.hosted.weblate.org>
Co-authored-by: Marc Sánchez Díaz <msanchez@aporta.biz>
Co-authored-by: Martin Maslyankov <m.maslyankov@me.com>
Co-authored-by: Wolf <wolski.marex@gmail.com>
Co-authored-by: asefeee <3470154407@qq.com>
Co-authored-by: lolly76 <thelolly76@gmail.com>
2025-08-29 14:57:25 +02:00
Kevin Papst
03dbbdc5ba added bulgarian locale (#5619) 2025-08-28 20:10:49 +02:00
Weblate (bot)
023048e7c0 Translated using Weblate (#5605)
Co-authored-by: Martin Maslyankov <m.maslyankov@me.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Mathias Peene <mathiaspeene@proton.me>
Co-authored-by: Sofia <sofimanci2112@gmail.com>
Co-authored-by: vanapro1 <law820314@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim5@gmail.com>
2025-08-15 13:44:53 +02:00
Kevin Papst
24778d3ffb replace PHPUnit annotations with attributes (#5608) 2025-08-11 18:57:42 +02:00
Kevin Papst
04331420ae Release 2.38.0 (#5563) 2025-08-08 23:25:42 +02:00
Weblate (bot)
f3890691ce Translated using Weblate (#5565)
Co-authored-by: Alioc <hit.177411245@gmail.com>
Co-authored-by: Fanny Van Prade <SmawyTranslate@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Lenny Angst <lenny@familie-angst.ch>
Co-authored-by: LordTenebrous <danielmorenoperez836@gmail.com>
Co-authored-by: Luke Gilmore <muntz18788@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Michedepain <benoitravel63000@gmail.com>
Co-authored-by: Nguyễn Quang Minh (NQM) <nguyenquangminh391@gmail.com>
Co-authored-by: Preben Rather Sørensen <preben@rather.dk>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Sasi Ba <sasiba8328@fuasha.com>
Co-authored-by: Sofia <sofimanci2112@gmail.com>
Co-authored-by: Vallo Rähn <rahn.vallo@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Yurt Page <yurtpage@gmail.com>
Co-authored-by: michte <michteting@proton.me>
Co-authored-by: qwertdery <qwertdery4@gmail.com>
Co-authored-by: tygyh <jonis9898@hotmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim5@gmail.com>
2025-08-08 17:00:46 +02:00
Kevin Papst
dfec807166 Release 2.37 (#5546) 2025-07-04 16:43:38 +02:00
Weblate (bot)
06b3060fe1 Translated using Weblate (#5543)
Co-authored-by: Hazret <tncytrk@live.com>
Co-authored-by: Péter Rezsuta <rezsutapeter@gmail.com>
Co-authored-by: RoboDoc <theonlyrobodoc@gmail.com>
Co-authored-by: Silvan <silvans2005@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-07-04 16:12:40 +02:00
Kevin Papst
b6a8ae783f added test 2025-07-04 11:59:26 +02:00
Weblate (bot)
3d8fa3af44 Translated using Weblate (#5535)
Co-authored-by: Lourenço Martins <hlourencoam93@gmail.com>
Co-authored-by: Álvaro Alonso Ramírez <aalonsoramirez@gmail.com>
2025-06-16 18:25:33 +02:00
Kevin Papst
59dc82351e Release 2.36.1 (#5540)
* fix weekly hours for new entries with non-global activities
* bump packages
* prevent creating invoices with 0 entries
2025-06-16 17:53:27 +02:00
Weblate (bot)
da10099248 Translated using Weblate (#5524)
Co-authored-by: Izackalaf <izackalaf@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Monika Bikki <bikki.monika@euroone.hu>
Co-authored-by: Rafa Herzog <49111482+necronyxon@users.noreply.github.com>
Co-authored-by: RoboDoc <theonlyrobodoc@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic4@gmail.com>
2025-06-11 14:26:18 +02:00
Kevin Papst
05aaa1950a configurable csv/xlsx export templates (#5531) 2025-06-11 13:06:33 +02:00
Kevin Papst
46129c7ab9 improved LDAP logging (#5517) 2025-06-07 11:48:09 +02:00
Kevin Papst
c8b4e4eabb Improve weekly hours form (#5528) 2025-06-06 15:20:00 +02:00
Kevin Papst
2f2ebd6293 Release 2.36.0 (#5514) 2025-06-02 16:07:48 +02:00
Weblate (bot)
594385f741 Translated using Weblate (#5510)
Co-authored-by: Robert Drygała <robert.drygala@gmail.com>
Co-authored-by: Tuna <ahmettunadem@gmail.com>
2025-06-02 14:35:55 +02:00
Kevin Papst
12190141d7 Release 2.35.1 (#5509)
do not trigger changes on empty time fields or empty duration
2025-05-31 15:29:43 +02:00
Kevin Papst
0420eb27c7 random improvements (#5506)
* fix order of destroying form elements
* API documentation - fixes #1949
* make command path independent
* use PhpSubprocess to execute migrations
* allow to configure optional SAML attributes
* fix deprecation
* unify wording of exported state - fixes #5392
2025-05-28 13:34:01 +02:00
Kevin Papst
81107377f4 Relax time input format requirements (#5504)
* remove locale support from time-input, only allow 12 and 24 hour format
* added blur listeners to parse all kinds of time inputs
* fix duration inputs like ":9" for 9 minutes
2025-05-28 09:36:29 +02:00
Kevin Papst
e9c172daea improve API endpoint names (#5502) 2025-05-27 13:23:08 +02:00
Kevin Papst
f42d42f512 fix searchterm parsing (#5501) 2025-05-27 13:20:27 +02:00
Weblate (bot)
b38c6fbb0e Translated using Weblate (#5498)
Co-authored-by: Maksim_220 Кабанов <law820314@gmail.com>
2025-05-27 13:15:55 +02:00
Kevin Papst
e2a1146670 Use arrow keys to change duration (#5495) 2025-05-27 13:07:29 +02:00
Kevin Papst
e69ac6968b Improved Locales (#5497)
- Switch Chinese time format to 24-hours - fixes #5496
- Use always 4 chars for year
- Use one `HH` instead of `H`, so we don't have to convert during runtime
- Update all locales
- Allow seeing time and date format in help UI
- Fix RTL in UI
- Show stats about found formats in Locales command
2025-05-26 20:59:39 +02:00
Weblate (bot)
87168de4f1 Translated using Weblate (#5484)
Co-authored-by: Piotr Laszczkowski <swistach@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
2025-05-24 22:59:07 +02:00
Kevin Papst
7b158d57b5 API improvements (#5494)
* hide logo in docs UI
* always return a user in api base controller
* added example for duration fields
2025-05-24 22:09:48 +02:00
Kevin Papst
1e0fbf0b73 Release 2.35 (#5470)
* open up API for plugins by removing internal
* use constants in entity column definition
* simplified entity management API
* bump packages
* allow installing assets and run database migrations independently
* bump to apidoc-bundle 5
* do not duplicate http method in API operationId
* new security entries in Open API definition
* changed API UI provider for Swagger to Stoplight, improved endpoint titles, hide internal endpoints
* deactivate swagger json endpoint
2025-05-24 14:28:39 +02:00
Weblate (bot)
6e26a37c4d Translated using Weblate (#5471)
Co-authored-by: LordTenebrous <lordtenebrous@users.noreply.hosted.weblate.org>
Co-authored-by: Mandeep <mandeeps708@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Piotr Laszczkowski <swistach@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic4@gmail.com>
2025-05-18 14:57:19 +02:00
Weblate (bot)
fac5a910d8 Translated using Weblate (#5462)
Co-authored-by: Harshit Sethi <hstsethi@outlook.com>
Co-authored-by: Henry Higgins <leserboka@outlook.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
2025-05-09 14:30:29 +02:00
Kevin Papst
dfd97fd6f3 Release 2.34 (#5465)
* fix timing issue in timesheet edit form with deactivated rounding
* bump packages
* only show update messages for newer plugin versions
* replace deprecated method
* remove internal from API
* remove technical terms from translation
* prevent calls to internal symfony methods
* helper method to flag entry as modified
* support meta-fields in weekly-hourse view
* fix running timesheets were deleted in weekly-hourse
2025-05-09 14:22:47 +02:00
Kevin Papst
452a8d9390 improved search with negation (#5453) 2025-05-05 18:18:00 +02:00
Weblate (bot)
2a9de506a1 Translated using Weblate (#5451)
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: John Titor <utkin2007@gmail.com>
Co-authored-by: Lucas <merlin.lucas99@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Sotski Eugene <jekakmail@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
2025-05-03 11:40:11 +02:00
Kevin Papst
e663cc2b02 Release 2.33.0 (#5438) 2025-04-22 20:43:22 +02:00
Weblate (bot)
a70c803158 Translated using Weblate (#5440)
Co-authored-by: C. H <them4z@gmail.com>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Kuwush <79j0hancarl@gmail.com>
Co-authored-by: Raphael-11 <raef.laffi@medtech.tn>
Co-authored-by: fran secs <fransecs@gmail.com>
2025-04-22 10:36:44 +02:00
Weblate (bot)
9ca69e1114 Translated using Weblate (#5414)
Co-authored-by: Fajar Shiddiq <justgamers0102@gmail.com>
Co-authored-by: Hien <hienly@yandex.com>
Co-authored-by: Marc Sánchez Díaz <msanchez@aporta.biz>
Co-authored-by: Marc-Daniel DALEBA <marcodxv10@gmail.com>
Co-authored-by: Tachibana Saza <tachibanasaza@proton.me>
Co-authored-by: gacarel <gacarel657@bariswc.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-04-06 11:36:12 +02:00
Kevin Papst
2e6b700b43 Release 2.32 (#5411)
* bump packages
* dynamic invoice options
* make sure that invoice previews can be detected
* support for mpdf associated files
* do not include any future times in work contract calculation
* re-add username column in Excel spreadsheet
* deactivate internal rate editing
* show if plugin update exists
* shorten name to Kimai only, without Time-Tracking
* remove check for existing id in work contract
* fix metafield already defined in search
* helper methods to unlock months
* new translation
* send event on unlock month
2025-04-06 09:53:48 +02:00
Kevin Papst
2a75cd6230 random improvements (#5382)
* remove permission check, as own timesheets should always be visible
* new methods to create datetime
* allow access to user roles in javascript
* support class for dropdown actions
* allow to edit internal rate
* support human readable duration in export via user configuration
* allow to order timesheet listing by user, exported and billable field
* bump codecov action
2025-03-13 18:00:50 +01:00
Weblate (bot)
934fdeb107 Translated using Weblate (#5383)
Co-authored-by: Dawid Kiełkowski <dawid55a@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Xyruz <alphxyruz@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: hugompd <hugompd@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
2025-03-12 18:26:08 +01:00
Kevin Papst
14cdcd3f63 Release 2.31 (#5372)
* simplify translation
* bump version
* deprecate translations
* pass date-range as argument to export and timesheet filter URL from monthly overview report
* added class for use in responsive screens
* show technical role name
* simplify multi-update title
* more statistic models
* bump composer packages
2025-02-27 17:41:25 +01:00
Weblate (bot)
674bf3a6b5 Translated using Weblate (#5371)
Co-authored-by: Vág Csaba <vagcsaba@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-02-27 17:22:17 +01:00
Kevin Papst
cce49cc409 Translation cleanup (#5370)
* use correct translation key
* de-duplicate translation
2025-02-21 20:50:49 +01:00
Weblate (bot)
04cb403ea9 Translated using Weblate (#5369)
Co-authored-by: Jussi Juven <jussi.juven@disec.fi>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Kristoffer Grundström <swedishsailfishosuser@tutanota.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
2025-02-21 20:50:23 +01:00
Kevin Papst
afaa845a9a fix export dates are not localized (#5368) 2025-02-20 15:07:28 +01:00
Weblate (bot)
928b5208e9 Translated using Weblate (#5362)
Co-authored-by: Kristoffer Grundström <swedishsailfishosuser@tutanota.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
2025-02-20 15:04:53 +01:00
Kevin Papst
8a8d0503f7 prepare break time field (#5366) 2025-02-20 14:46:20 +01:00
Kevin Papst
b341358d0a Release 2.30 (#5345)
- added missing InvoiceTemplate company, title) field validator
- graceful fallback for missing working-contract mode
- improve email test command (use configured MAIL_FROM)
- additional form types for simple usage in SystemConfiguration and UserPreferences
- allow to extend the working time query via event
2025-02-17 08:32:22 +01:00
Weblate (bot)
d30166e691 Translated using Weblate (#5344)
Co-authored-by: 3limssmile <33elimssmile@gmail.com>
Co-authored-by: Alfredo Sola <alfredo@sola.es>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Serhii Horichenko <serhii@horichenko.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: vaykly <vaykly@icloud.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-02-17 08:31:54 +01:00
Kevin Papst
b42c77a2a1 Release 2.29 (#5325)
* bump composer packages
* fixes #5329 quotes for ANSI_MODE
* improve year selection
* improve year selection via dropdown
* added range selector in month-picker
* fix week number if week starts with sunday
* fix first day of month in URL
* predefined options for week chooser
* z-index issue with sticky table header
* replace duplicated translations
* add logout button to allow user switch without having to re-login in "remember me" login
* new flag to detect if invoice entry is a fixed rate
* improve export column lengths
2025-02-09 00:16:03 +01:00
Weblate (bot)
8444928ae4 Translated using Weblate (#5342)
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Serhii Horichenko <serhii@horichenko.com>
2025-02-08 19:29:33 +01:00
Weblate (bot)
b250edc2a1 Translated using Weblate (#5318)
Co-authored-by: Antonín JUDYTKA <judytka@judytka.cz>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Lukáš Granatier <lukas.granatier@compacer.cz>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: on the road simson <d-youtube@kopaszhegy.hu>
Co-authored-by: polarwood <polarwood@users.noreply.hosted.weblate.org>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
2025-02-08 19:27:49 +01:00
Kevin Papst
4a8c2a79c9 added last months and last quarters to daterange-picker selections (#5317) 2025-01-24 15:21:54 +01:00
Weblate (bot)
bff72b30cc Translations update from Hosted Weblate (#5306)
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: TaaviLepik <info@brunex.ee>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-01-24 13:37:03 +01:00
Kevin Papst
9d7b418d99 fix date column in export (#5308) 2025-01-20 16:09:29 +01:00
Kevin Papst
f2fb338539 Release 2.28 (#5253)
* fix year in dashboard
* make batch actions accessible via javascript
* bump packages
* remove BOM from CSV
* rebuild assets
* fix duplicated automated-email warning
2025-01-18 01:49:16 +01:00
Weblate (bot)
1b3ff212d2 Translated using Weblate (#5293)
Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com>
Co-authored-by: John Titor <utkin2007@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-01-18 01:48:48 +01:00
Maarten Becker
c07979e3a9 fix usage of activity api globals parameter (#5284) 2025-01-10 10:02:14 +01:00
Weblate (bot)
23e8dcf495 Translated using Weblate (added Tamil) (#5254)
Co-authored-by: Biscuittttt <biscuitwithtea.tall310@passinbox.com>
Co-authored-by: Carlos Carreras <mytriponlinux@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: abdelbasset jabrane <ribago9317@cubene.com>
Co-authored-by: yblis <yblisss@yahoo.fr>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-01-09 12:16:45 +01:00
Kevin Papst
4332ef95a2 Release 2.27 (#5212) 2024-12-22 22:50:42 +01:00
Weblate (bot)
4fdfb6f478 Translations update from Hosted Weblate (#5211)
Co-authored-by: Dawid <dawidgorski.m@gmail.com>
Co-authored-by: Florent Berthelot <florentius.b@gmail.com>
Co-authored-by: Giorgos Skafidas <giorgos@skafidas.online>
Co-authored-by: Kamborio <Kamborio15@users.noreply.hosted.weblate.org>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Serhii Horichenko <serhii@horichenko.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
Co-authored-by: BouenMarsh <mveretsuk@yandex.ru>
2024-12-22 19:15:32 +01:00
Kevin Papst
136104d4b0 faster spreadsheet exporter based on opensout and other export improvements (#5238) 2024-12-22 18:36:47 +01:00
Kevin Papst
c7f0508707 Upgrade tests to PhpUnit 10 (#5252) 2024-12-22 01:25:30 +01:00
buti1021
9bd37fb695 update php versions (#5246) 2024-12-20 14:56:06 +01:00
Kevin Papst
21c031f2c8 export filtered timesheets without additional search form (#5234) 2024-12-15 18:38:30 +01:00
Kevin Papst
17a815e5a9 updated frontend builds (#5210)
* do not rely on node_modules path
* bump eslint to v9, run eslint via npm task, remove from build task
* loosen dependencies and update all packages
* rebuild assets with latest frontend packages
* bump webpack encore and dependencies
* bump to latest stable yarn
* explicitly mention dependencies
2024-12-06 14:31:04 +01:00
Kevin Papst
648686c001 added open collective 2024-12-06 14:27:06 +01:00
Kevin Papst
82a3b99a31 Release 2.26 (#5189)
* bring back deprecated methods
* bump packages
* fix SAML redirect
* config flag for break times
* use class constant instead of string in attributes
* throw if all tags were not found - fixes #4792
2024-12-05 10:42:07 +01:00
Weblate (bot)
70741eebfd Translations update from Hosted Weblate (#5192)
Co-authored-by: Lassi Määttä <lassi.maetta@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
2024-12-05 09:42:37 +01:00
Weblate (bot)
eb933e7021 Translations update from Hosted Weblate (#5185)
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
2024-11-30 14:26:07 +01:00
Weblate (bot)
372f3c8507 Translations update from Hosted Weblate (#5179)
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Serhii Horichenko <m@sgg.im>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Doctorredits_here <alkaf.alkaf2018@tutamail.com>
2024-11-27 15:35:26 +01:00
Kevin Papst
e030ff08db API endpoints to delete customer/project/activity (#5181)
* added service methods with events to delete customer, project, activity
* added API endpoints to delete customer, project, activity
* added tests for new API endpoints
2024-11-27 15:25:13 +01:00
Kevin Papst
f13b81ede7 Query hints & persistent cache for latest approvals (#5176) 2024-11-25 21:04:53 +01:00
Weblate (bot)
46c4449504 Translations update from Hosted Weblate (#5172)
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Serhii Horichenko <m@sgg.im>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
2024-11-25 21:04:22 +01:00
Kevin Papst
8b55cd3693 trigger form change upon copy-data event 2024-11-25 20:46:34 +01:00
Kevin Papst
406ac7b9cb allow PHP 8.4 (#5173) 2024-11-21 23:00:00 +01:00
Kevin Papst
0c26a2678e Release 2.25 (#5109) 2024-11-21 22:44:49 +01:00
Weblate (bot)
49eb7068c9 Translated using Weblate (#5130)
Co-authored-by: Adrien N <adriennathaniel1999@gmail.com>
Co-authored-by: Igor Coimbra Carvalheira <igorccarvalheira111@gmail.com>
Co-authored-by: John Titor <utkin2007@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Peter Dave Hello <hsu@peterdavehello.org>
Co-authored-by: Serhii Horichenko <m@sgg.im>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
2024-11-18 13:52:50 +01:00
Kevin Papst
dcc52f1a95 API begin and end fields for Admins (#5134) 2024-10-25 10:47:58 +02:00
Weblate (bot)
31bae44f3c Translated using Weblate (#5120)
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Peter Dave Hello <hsu@peterdavehello.org>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
2024-10-20 22:30:35 +02:00
Kevin Papst
84a0ca5ce6 removed duplicate installation call 2024-10-14 23:37:45 +02:00
Kevin Papst
9e736b26f6 Improve docker installation (#5115)
* ignore certain connection errors on startup
* remove invalid code that checks for the existence of the migration table
* fetch kimai code via tar archive instead of git clone, to respect .gitattributes
2024-10-14 23:09:14 +02:00
Weblate (bot)
32a1306394 Translated using Weblate (#5110)
Co-authored-by: Dklfajsjfi49wefklsf32 <nlincus@users.noreply.hosted.weblate.org>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
2024-10-14 21:46:28 +02:00
Kevin Papst
96043afd6a better support for installing plugins via composer (#5112)
* merge installation and update commands
* generate metadata from array
* new command to list available packages
* added a management script to simplify updates
* added directory for dev files
* helper functions for installation and listing of packages
* run plugin database installers
2024-10-14 21:44:42 +02:00
Kevin Papst
255c7d77d6 Ignore files from showing up in a release (#5111)
* moved email css files to template directory
* prevent developer files from showing up in a release
2024-10-12 23:45:13 +02:00
Weblate (bot)
e6d5a720bf Translations update from Hosted Weblate (#5098)
Co-authored-by: Lukáš Kaňka <lukas.kanka@outlook.cz>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-10-11 22:33:57 +02:00
Kevin Papst
ff9bf163ee Release 2.24 (#5097) 2024-10-11 21:56:54 +02:00
Kevin Papst
5fac6a642c Docker improvements (#5096)
* install missing unzip
* run update and install in one step
* remove unused TRUSTED_HOSTS env
2024-10-11 19:36:40 +02:00
Kevin Papst
b1b81ee8a6 remove dev dependencies from prod image 2024-10-04 17:35:25 +02:00
Kevin Papst
1ea64bf18e allow to manually run workflow 2024-10-04 17:03:31 +02:00
Kevin Papst
0c4fdda00a allow to manually run workflow 2024-10-04 17:01:25 +02:00
Kevin Papst
215bd6a01a allow to manually run workflow 2024-10-04 16:57:48 +02:00
Kevin Papst
63b8164926 install missing buildx 2024-10-04 16:45:51 +02:00
Weblate (bot)
8c26ff9aed Translated using Weblate (#5092)
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
2024-10-04 11:24:50 +02:00
Kevin Papst
921a703ea2 Merge Docker images (#5094) 2024-10-04 11:14:10 +02:00
Kevin Papst
855a546683 Docker improvements (#5093) 2024-10-04 10:25:46 +02:00
Kevin Papst
52b949daa3 cleanup docker environment variables (#5074) 2024-10-03 14:27:33 +02:00
Weblate (bot)
ee040d29b6 Translations update from Hosted Weblate (#5079)
Co-authored-by: Doctorredits_here <alkaf.alkaf2018@tutamail.com>
Co-authored-by: Hien <hienly@yandex.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Roman Ondráček <mail@romanondracek.cz>
Co-authored-by: Tito Tapiola <ikirouta013@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Kevin Papst <kevinpapst@users.noreply.github.com>
2024-10-03 10:38:48 +02:00
Kevin Papst
fb9a0dc499 Release 2.23.0 (#5075) 2024-10-03 10:34:20 +02:00
Kevin Papst
40154be8f9 Upgrade Tabler frontend to 1.0.21 (#5066) 2024-09-23 18:31:33 +02:00
Kevin Papst
13649e146c Improve pagination support in API (#5073) 2024-09-22 17:26:02 +02:00
Kevin Papst
4076e1c3d3 Support for changeable work contract types (#5069) 2024-09-22 16:18:21 +02:00
Kevin Papst
8de54e1fa7 Added API endpoints to fetch invoices (#5070)
* added serializer attributes for API usage
* new setters to fill invoice data from fixture
* re-usable fixture helper methods
* added API endpoints to fetch invoices
* adjust tests
2024-09-22 16:17:45 +02:00
Weblate (bot)
1965c35b43 Translated using Weblate (#5045)
Co-authored-by: Doctorredits_here <alkaf.alkaf2018@tutamail.com>
Co-authored-by: Hien <hienly@yandex.com>
Co-authored-by: Jose Delvani <jsdelvani@users.noreply.hosted.weblate.org>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: albanobattistella <albano_battistella@hotmail.com>
Co-authored-by: enly sure <enlysure@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-09-22 15:38:51 +02:00
Kevin Papst
3fd770047e allow Authorization header in CORS request 2024-09-20 23:26:29 +02:00
Kevin Papst
036b297a76 fix hydration issues in favorites (#5068) 2024-09-20 22:43:16 +02:00
Kevin Papst
537c120ad9 Release 2.22.0 (#5043) 2024-09-20 14:30:11 +02:00
Weblate (bot)
3e0dadc0c8 Translated using Weblate (#5037)
Co-authored-by: Jose Delvani <jsdelvani@users.noreply.hosted.weblate.org>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
2024-09-01 14:58:47 +02:00
Kevin Papst
004ac5a6b9 re-phrase 2024-09-01 14:57:07 +02:00
Kevin Papst
3204dcb03e bump PHPSpreadsheet major version (#5040) 2024-08-31 23:42:41 +02:00
Kevin Papst
b1903ba183 Release 2.21.0 (#5014) 2024-08-28 17:16:37 +02:00
Weblate (bot)
75fa7a20b8 Translated using Weblate (#5012)
Co-authored-by: BeckeBauer <berko@gmx.de>
Co-authored-by: Biscuittttt <biscuitwithtea.tall310@passinbox.com>
Co-authored-by: Héctor Borrás Aleixandre <hborrasaleixandre@gmail.com>
Co-authored-by: John Titor <utkin2007@gmail.com>
Co-authored-by: Léane GRASSER <leane.grasser@proton.me>
Co-authored-by: Prefill add-on <noreply-addon-prefill@weblate.org>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Uporabnik Alen <obutimacek556@gmail.com>
Co-authored-by: XblateX <blate@users.noreply.hosted.weblate.org>
Co-authored-by: tygyh <jonis9898@hotmail.com>
2024-08-28 17:15:57 +02:00
Kevin Papst
a19426147a added event to send email to user (#5034) 2024-08-28 14:04:36 +02:00
Kevin Papst
9d933f62c0 refactored repositories and DB queries (#5026)
* removed unused teams from export order
* added new paginator for query instead of querybuilder
* added field hydrate enums
* hide PARTIAL deprecation
* never log deprecations in production
* replaced InvoiceLoader with native Doctrine feature
* prevent excessive permission queries
* support loading customers of team
* improved findByIds
* internalized API
* fix null string deprecations
2024-08-27 10:11:19 +02:00
Kevin Papst
9e3d243b4b use enabled_locales logic to handle locales (#5017)
* allow to skip locales (here: catalan)
* use enabled_locales and replace app_locales with kimai_locales
* added test to call all reports once for super_admin
2024-08-11 17:43:20 +02:00
Kevin Papst
62047c7c01 link security guidelines on the website 2024-08-05 18:44:38 +02:00
Kevin Papst
0bcd45babe clarify test exclusions 2024-08-05 13:23:53 +02:00
Kevin Papst
2a72815481 remove invalid label 2024-08-04 19:38:47 +02:00
Kevin Papst
ff6a3e8262 Release 2.20.0 (#4987) 2024-08-04 18:22:27 +02:00
Kevin Papst
0947b88d36 added OCI docker labels (#4996) 2024-08-04 16:43:46 +02:00
Kevin Papst
5de5b4fef5 BC: Cleanup Docker tags (#4995) 2024-08-04 16:37:55 +02:00
robjuz
cde856a9c1 BC: Docker support for encoded characters in DATABASE_URL (#5000) 2024-08-04 16:02:08 +02:00
Kevin Papst
6f8877a740 new export of project overview list (#5009) 2024-08-03 00:43:05 +02:00
Kevin Papst
e7d71bff64 remove comments from XLIFF files 2024-08-02 22:43:36 +02:00
Weblate (bot)
cca0657291 Update translation files (#5010) 2024-08-02 21:49:13 +02:00
Weblate (bot)
a4904d3335 Update translation files (#5007)
Co-authored-by: Héctor Borrás <hborras@cyberclick.net>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Naoko Takano <naokomc@gmail.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: stuntworks <stuntworks@saber.games>
2024-08-02 21:43:29 +02:00
Kevin Papst
b9c1036b21 Revert Docker credentials urldecode (#4998) 2024-07-28 18:04:18 +02:00
Weblate (bot)
818c080d28 Translated using Weblate (#4990)
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: Wouter Meeusen <wouter.meeusen@outlook.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-07-27 17:37:10 +02:00
Weblate (bot)
a5a6ae065b Translated using Weblate (#4984)
Co-authored-by: Adrien N <adriennathaniel1999@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
2024-07-23 10:03:56 +02:00
Kevin Papst
8b06ca9d10 unify CSRF token usage in comment sections (#4986) 2024-07-23 08:33:02 +02:00
Kevin Papst
7e1025d61d fix financial year issue, fix test, improve DateTimeFactory (#4985) 2024-07-23 07:20:11 +02:00
Kevin Papst
8788311faf Release 2.19 (#4922) 2024-07-22 17:51:03 +02:00
Weblate (bot)
ce22520d7f Translated using Weblate (#4946)
Co-authored-by: Ahmad Dakhlallah <ahmad@linuxarabia.co>
Co-authored-by: Bent Haar <BentHaar@users.noreply.hosted.weblate.org>
Co-authored-by: Carlos Carreras <mytriponlinux@gmail.com>
Co-authored-by: David Bauer <dbs23.dbs23@gmail.com>
Co-authored-by: Jose Delvani <delvani.eletricista@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: Mads R. Andersen <madsdk_789@hotmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Mikkel Ricky <rimi@aarhus.dk>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Quiwy <github@quiwy.ninja>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: geekom13 <geekom13@proton.me>
Co-authored-by: lilosti <lilosti@aarhus.dk>
Co-authored-by: ssantos <ssantos@web.de>
2024-07-20 17:36:02 +02:00
robjuz
f5d76cfd9a fix database URLs with special characters for docker (#4976) 2024-07-20 17:20:23 +02:00
Chinmay Purav
27f7f14539 fixed case of AS keyword in Dockerfile (#4943) 2024-06-29 21:45:14 +02:00
Kevin Papst
f7c32e633a bump docker action versions to fix GH deprecations 2024-06-17 11:05:49 +02:00
Kevin Papst
6d9055826f bump docker action versions to fix GH deprecations 2024-06-17 10:07:27 +02:00
Toby Batch
4627a9047e feat: stop multiarch images being lost on re tagging docker images (#4856) 2024-06-17 10:03:11 +02:00
Kevin Papst
987b46bf8f Release 2.18 (#4878) 2024-06-16 13:15:49 +02:00
Weblate (bot)
8792a1df09 Translated using Weblate (#4876)
Co-authored-by: Chan Young Park <designbycy@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Oh Wow! <kasra.hashemi1999@gmail.com>
Co-authored-by: René Bischoff <rene.bischoff@gmail.com>
Co-authored-by: Wei-Cheng Yeh (IID) <iid@ccns.ncku.edu.tw>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: wkwkjddndjdusiq <wkwkjddndjdusiq@onionmail.com>
2024-06-16 13:02:27 +02:00
Kevin Papst
a8678892d6 yarn upgrade 2024-06-15 11:40:35 +02:00
Kevin Papst
72350b76c0 improve equality detection of same user 2024-06-15 11:40:28 +02:00
Kevin Papst
8578bbe033 improve project end handling 2024-06-13 22:31:33 +02:00
Kevin Papst
93ca983e0b invoice: do not use activity name as fallback for description (#4884)
* added replacement field description_safe
2024-06-03 19:01:40 +02:00
Kevin Papst
e29ef25581 link domains for donations 2024-05-22 14:19:11 +02:00
Kevin Papst
a617eba535 link about pages for donations 2024-05-22 14:15:42 +02:00
Kevin Papst
0c445d1bc4 Release 2.17 (#4836)
see https://github.com/kimai/kimai/pull/4836
2024-05-19 17:42:03 +02:00
Weblate (bot)
5f7114e008 Translated using Weblate (#4862)
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Wei-Cheng Yeh (IID) <iid@ccns.ncku.edu.tw>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
2024-05-19 17:03:48 +02:00
Habibhaidari1
e2c511cf46 Update RateCalculatorInterface (#4851) 2024-05-17 22:55:08 +02:00
Weblate (bot)
e042ef6bfa Translated using Weblate (#4850)
Co-authored-by: Barna László <barna.laszlo@lscomputer.hu>
Co-authored-by: Wei-Cheng Yeh (IID) <iid@ccns.ncku.edu.tw>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
2024-05-12 12:41:06 +02:00
Daniel Langemann
c059e51b3c Fix typo in markdown link (#4845) 2024-05-07 09:30:33 +02:00
Kevin Papst
661bb16342 Fix hidden API token (#4828) 2024-05-03 11:38:33 +02:00
Weblate (bot)
94d6b43dcc Cleanup translation files (#4827) 2024-05-03 11:06:35 +02:00
Kevin Papst
99c296a751 Release 2.16 (#4780) 2024-05-01 14:24:24 +02:00
Weblate (bot)
8f8d228fb3 Translated using Weblate (#4826)
Co-authored-by: Bogumił Kraszewski <gogulcio@gmail.com>
Co-authored-by: Colgrave <hanqixu.blogs@simplelogin.co>
Co-authored-by: Freek <info@f3k.tech>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Nicoara Alex <alex.nicoara@yahoo.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Wei-Cheng Yeh (IID) <iid@ccns.ncku.edu.tw>
Co-authored-by: qeepoo <pfrade1996@protonmail.com>
2024-05-01 13:43:31 +02:00
Weblate (bot)
1214e508fe Translated using Weblate (#4814)
Co-authored-by: Bogumił Kraszewski <gogulcio@gmail.com>
Co-authored-by: Jose Delvani <delvani.eletricista@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: streaming s <fsrmllll1111@gmail.com>
2024-04-24 06:36:55 +02:00
Andreas Brain
7d394fbe56 added ldap-libs to for TLS certificate verification (#4802) 2024-04-23 10:33:51 +02:00
Weblate (bot)
fd4dfc6be4 Translated using Weblate (#4812)
Co-authored-by: Andre Costa <andrecaeu@gmail.com>
Co-authored-by: Balanda Nazarii <balaraz@tuta.io>
Co-authored-by: Joaquim Homrighausen <joho@boojam.se>
Co-authored-by: Jose Delvani <delvani.eletricista@gmail.com>
Co-authored-by: Julia <juliadeo@proton.me>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nicoara Alex <alex.nicoara@yahoo.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Petar Babić <petar@jesteibice.com>
Co-authored-by: SGpro <mark520fay35@gmail.com>
Co-authored-by: Tito Tapiola <ikirouta013@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: حاج حسین نجفی <hajhsynnjfy@gmail.com>
2024-04-22 18:44:21 +02:00
Kevin Papst
9dc9c71a46 added "api_access" permission for limiting API access (#4779) 2024-04-13 17:27:51 +02:00
Kevin Papst
345bb6601e fix test 2024-04-13 17:00:13 +02:00
Kevin Papst
5c8c21d609 fix menu name 2024-04-13 16:52:44 +02:00
Kevin Papst
7219b3f421 Release 2.15 (#4749) 2024-04-12 19:03:09 +02:00
Weblate (bot)
b76a5d5c35 Translations update from Hosted Weblate (#4765)
Co-authored-by: Andre Costa <andrecaeu@gmail.com>
Co-authored-by: Balanda Nazarii <balaraz@tuta.io>
Co-authored-by: Joaquim Homrighausen <joho@boojam.se>
Co-authored-by: Jose Delvani <delvani.eletricista@gmail.com>
Co-authored-by: Julia <juliadeo@proton.me>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Petar Babić <petar@jesteibice.com>
Co-authored-by: Tito Tapiola <ikirouta013@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Kevin Papst <kevinpapst@users.noreply.github.com>
2024-04-12 16:22:17 +02:00
Kevin Papst
40a9027d34 try v5 without discussion 2024-04-06 11:09:02 +02:00
Kevin Papst
553bd20e06 internal: bump versions due to node16 deprecation 2024-04-06 10:33:53 +02:00
Kevin Papst
3ea5803e08 revert to @v4 2024-04-06 10:29:15 +02:00
Kevin Papst
4949afb098 try different token 2024-04-06 10:28:27 +02:00
Kevin Papst
d328badca1 enable debug 2024-04-06 10:26:47 +02:00
Kevin Papst
175d371ef9 change name 2024-04-06 10:17:12 +02:00
Kevin Papst
6bb7b38252 bump version 2024-04-06 10:15:56 +02:00
Kevin Papst
afe0656502 added API tokens, deprecate API passwords (#4637) 2024-04-05 23:51:16 +02:00
Kevin Papst
dd51c8dfba utilize UserService for SAML (#4748) 2024-04-05 19:22:13 +02:00
Kevin Papst
b6c98f871d Release 2.14 (#4710)
- show "link has expired message" in password reset screen
- added date objects as hydrator variables - for custom date formats in invoice templates
- show meta-fields with null values (e.g. booleans with `false` where hidden)
- fix permission check: allow to remove `view_own_timesheet` but still record times
- prevent error 500 if customer country is empty
- fix API 500 error if project does not exist when creating new timesheet
- fix tags are not created in remote-search mode
- do not check "export items" by default
- fix daterange query, if user an request locale are different
- added logging for invalid SAML responses (see various discussions)
2024-04-05 12:38:21 +02:00
Weblate (bot)
19b2d47591 Translated using Weblate (#4746)
Co-authored-by: Joaquim Homrighausen <joho@boojam.se>
Co-authored-by: Jose Delvani <delvani.eletricista@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
2024-04-04 17:50:23 +02:00
Kevin Papst
a636683dee Configurable activity and project number (#4729)
* added configurable activity number
* added configurable project number
* fix deprecations
* added some tests for entity exporter
* better configuration of dropdown pattern for customer, project and activity
2024-04-04 17:43:22 +02:00
Kevin Papst
c1f5d3def4 do not export items by default 2024-04-04 17:35:43 +02:00
Kevin Papst
3e88a007d0 allow to change locale in DateRangeType 2024-04-04 17:35:16 +02:00
Kevin Papst
1fdd11e94c link to wall of love 2024-04-04 00:20:31 +02:00
Toby Batch
2ac783a300 split docker to use base image for faster builds (#4586) 2024-04-02 17:50:24 +02:00
Weblate (bot)
3a358b1434 Translated using Weblate (#4722)
Co-authored-by: Jean-Michel <arsene_lupin_57@hotmail.fr>
Co-authored-by: Joaquim Homrighausen <joho@boojam.se>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Thanh <pancakes21f@gmail.com>
2024-04-02 01:06:57 +02:00
Yannik Beaulieu
216da19cc9 Add support for negative durations in All Users reports (#4717) 2024-03-27 23:57:52 +01:00
Kevin Papst
dee90bb15e Release 2.13 (#4659) 2024-03-10 15:35:59 +01:00
Weblate (bot)
a78e8ed8c4 Translated using Weblate (#4693)
Co-authored-by: Andre Costa <andrecaeu@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2024-03-10 15:29:27 +01:00
Weblate (bot)
e293d62aa1 Translated using Weblate (#4658)
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: DOHEX <xuyaohui617@outlook.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
2024-02-22 18:00:51 +01:00
Kevin Papst
f016221e8e Fixes for 1.12 (#4655)
* fix missing locales
* bump composer packages
* changed translations
* assert timezone on customer
* include user accountNumber in excel export
* improve next customer number calculation
* fix z-index of contextmenu / action dropdown
2024-02-22 13:03:09 +01:00
Kevin Papst
36c578452e show top nav links as buttons (#4633) 2024-02-12 17:50:22 +01:00
Kevin Papst
6e643ee99c revert to classical bootstrap form layout (#4632) 2024-02-12 17:35:30 +01:00
Kevin Papst
58cbf1776f Weekly hours improvements (#4631)
* fix table padding
* reduced minimum quick-entry recent activity row amount
* use activity favorites in weekly hours
2024-02-12 14:20:17 +01:00
Kevin Papst
85a16a9363 Release 2.12 (#4609) 2024-02-07 23:47:25 +01:00
Kevin Papst
7abe787778 added project filter in user-list reports (#4615) 2024-02-07 18:00:29 +01:00
Weblate (bot)
e98732f44a Translated using Weblate (#4614)
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
2024-02-06 16:06:40 +01:00
Kevin Papst
c2afebbb5e Fix Doctrine deprecations (#4608) 2024-02-02 15:48:11 +01:00
Kevin Papst
49e69d1ae3 Release 2.11 (#4580) 2024-02-02 13:52:24 +01:00
Andreas Brain
a9a32c83ac Add library to re-enable TLS certificate verification for LDAP connections (#4598) 2024-02-02 13:46:40 +01:00
Weblate (bot)
64fa67f15d Translated using Weblate (#4587)
Co-authored-by: Alex Terlan <terlan2012@gmail.com>
Co-authored-by: Bruno Paré-Simard <bruno.p@brunops.com>
Co-authored-by: Bálint László <blaszlobors@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: abdelbasset jabrane <ribago9317@cubene.com>
Co-authored-by: delvani <inavleb@users.noreply.hosted.weblate.org>
2024-02-02 13:44:25 +01:00
Kevin Papst
df3ca9d5a9 Split user language (UI translation) from locale (formatted values) (#4595) 2024-01-30 00:09:53 +01:00
Kevin Papst
12ef19df28 support default time on date-time-picker (#4579) 2024-01-21 01:59:30 +01:00
Kevin Papst
a5b4a92086 new phpstan version 2024-01-21 00:28:03 +01:00
Kevin Papst
3aaaf9ffc0 bump packages 2024-01-20 02:12:26 +01:00
Weblate (bot)
3f73f2c570 Translated using Weblate (#4576)
Co-authored-by: Bálint László <blaszlobors@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Saveliy <jitropolit@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2024-01-20 01:27:25 +01:00
Kevin Papst
46739795ff Release 2.10 (#4549) 2024-01-19 12:05:07 +01:00
Kevin Papst
afd60a67f6 merged and removed small translation files (#4572)
* merged about with messages
* merged invoice-numbergenerator with messages
2024-01-16 23:39:13 +01:00
Weblate (bot)
bb325888a1 Translated using Weblate (#4551)
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Miloš Havlíček <milos.havlicek@cothema.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: albanobattistella <albano_battistella@hotmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: сэр Аноним <mcptminei@gmail.com>
2024-01-16 23:16:46 +01:00
Kevin Papst
fdda9841fa allow to edit username (#4559) 2024-01-14 13:02:44 +01:00
Ronald Derksen
5acd33dd64 Make 'pagebreak' optional in pdf layout exporter (#4561) 2024-01-13 14:44:31 +01:00
Kevin Papst
6531e7fe52 Release 2.9.0 (#4526)
* added fix to work around bc break in new phpword version
* fix phpoffice deprecations
* mark unused option as deprecated
* support for DateTimeInterface and DateTimeImmutable where possible
* use TRUSTED_PROXIES setting - fixes #4533
* re-enable Kimai test in docker test build script (#4541)
* bump dependencies
2024-01-10 12:43:07 +01:00
Weblate (bot)
c6a651456e Translated using Weblate (#4531)
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
2024-01-09 23:53:56 +01:00
Kevin Papst
8eeb9c120a Release 2.8.0 (#4508) 2024-01-03 00:28:35 +01:00
Janne Heß
d8f89d9f3d release-drafter: adapt supported PHP versions (#4512) 2024-01-03 00:25:24 +01:00
Simon Schaufelberger
26a89725eb fix wrong default protocol for customer homepage (#4514) 2024-01-02 15:20:24 +01:00
Weblate (bot)
9f0165ef4d Translated using Weblate (#4517)
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
2024-01-01 21:05:29 +01:00
Kevin Papst
3d0a35f0cd bump checkout action version 2023-12-27 02:10:46 +01:00
Kevin Papst
c5dcf9e0fd replace deprecated set-output with environment usage, deactivate broken test-lite 2023-12-26 17:18:08 +01:00
Kevin Papst
ad645f5b58 Release 2.7.0 (#4506)
* added api URL for simpler integration
* allow to request password change upon next login
* make ModifiedAt timesheet independent
* improved plugin api
* replace kernel calls with AutoconfigureTag and TaggedIterator attributes
* new translation keys (e.g. for days)
* support setting min and max date on date and daterange pickers
2023-12-26 14:49:11 +01:00
Weblate (bot)
f86eca05ae Translated using Weblate (#4507)
Co-authored-by: Alan Aparecido da coneicao <alanaparecidodaconeicao@gmail.com>
Co-authored-by: Balanda Nazarii <balaraz@tuta.io>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nikolay Soloshin <nigiriits@users.noreply.hosted.weblate.org>
Co-authored-by: Petal <Petalzu@outlook.com>
Co-authored-by: Thomas <jzh20180312@163.com>
Co-authored-by: Vipul Hangadiya <hangadiyavipul@gmail.com>
Co-authored-by: Wei-Cheng Yeh (IID) <iid@ccns.ncku.edu.tw>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2023-12-26 01:32:18 +01:00
Kevin Papst
ac2a6ba2c8 use a full mysql database URL as default (#4504) 2023-12-25 23:41:13 +01:00
Christian Schupfner
e23d0474fd Upgrade Docker to PHP 8.2 (#4476)
- base image changed to PHP 8.2
- base images changed to bookworm
- libicu63 upgraded to libicu72
2023-12-17 17:05:38 +01:00
Kevin Papst
2edcbcca50 sort invoice items by date before passing to template (#4495) 2023-12-17 16:50:47 +01:00
Kevin Papst
5f4b6d3cfa Release 2.6.0 (#4472)
- Added: calendar entry title combination for customer, project and activity
- Added: show not_invoiced and not_exported data in detail screens
- Added: force logout if user is disabled
- Added: reduced amount of database queries on several screens
- Fixed: open-close status on work-contract screen for users without configuration
- Fixed: failsafe order/orderBy in query if manually manipulated to be null
- Fixed: unify statistic calculation (not_invoiced and not_exported) across screens
- Tech: bump packages
- Tech: Symfony 6.4
2023-12-17 16:19:50 +01:00
Kevin Papst
af82fd040d removed deprecated test script (#4490) 2023-12-14 12:12:17 +01:00
Kevin Papst
e0769bc3dc allow PHP 8.3 (#4471)
Fix use of range() and auto-width columns for Excel exports
2023-12-01 22:40:02 +01:00
Weblate (bot)
8fd2e7029e Translated using Weblate (#4466)
Co-authored-by: EngageIndo <engageindo@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Shimpei <sfutami0821@gmail.com>
Co-authored-by: albanobattistella <albano_battistella@hotmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: nigiriits <i79046290200@gmail.com>
Co-authored-by: nigiriits <nigiriits@users.noreply.hosted.weblate.org>
Co-authored-by: xorly <patrikbachan@gmail.com>
2023-12-01 22:38:32 +01:00
Kevin Papst
f570b5506a added missing translations 2023-12-01 18:33:09 +01:00
Kevin Papst
0e1bd818bf bump packages 2023-12-01 16:42:51 +01:00
Kevin Papst
02bcd45116 Release 2.5.0 (#4454)
- added command to list users
- corrected wrong german translation
- work contract validations
- prevent error with missing params
- use collapsible, minor UI improvement
- added classes to target menu buttons in custom rules
- disable webpack notifier, incompatible with mac arm
- use explicit menu service to generate menu
- bump to fontawesome 6 and replace restart icon
- change repeat icon for recent activities
- moved user bookmarks (favorites) to top nav
- fix totp seconds window (leeway)
- new migration to fix remaining user preferences with dots in name
- remove duplicate named column in user screen
- unify and added translations
- added missing filter and tags to InvoiceSecurity
2023-12-01 11:38:17 +01:00
Kevin Papst
16d4a691f8 Fix LDAP with internal users (#4453)
* added logging, in case Laminas libs are not available
* make sure that internal users can still login if LDAP is activated
2023-11-20 21:33:36 +01:00
Kevin Papst
20164295f8 Release 2.4.0 (#4427)
* button to duplicate old timesheets, even those from lockdown period
* bump composer packages
* fixes tooltip remains in view #4426
* fix js error if all widgets were removed
* allow to open timesheet edit dialog from export listing
* css classes for timesheet context menu, so they can be hidden
* added flag to force a user to set password upon login
* enable lazy-ghost-objects to fix deprecation
* change user, username, internal_rate export column labels
* helper method to find user by displayname
* log improvements and format changes
* deactivate broken schema validation
2023-11-19 16:05:43 +01:00
Weblate (bot)
9d8e5ecd7a Translated using Weblate (#4447)
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matti O <matt1@users.noreply.hosted.weblate.org>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Patrick Kartschewski <weblate@patrick-kartschewski.de>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Victor Hitriy <pvnhome@yandex.ru>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: albanobattistella <albano_battistella@hotmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2023-11-19 16:01:35 +01:00
Kevin Papst
8a1059fdd1 added docker build and publish workflow (#4446) 2023-11-17 14:37:32 +01:00
Kevin Papst
92c6922afb fix LDAP issues and allow to migrate from local to LDAP account (#4445) 2023-11-17 14:37:00 +01:00
Kevin Papst
2fb9ea9239 bump packages 2023-11-17 13:22:50 +01:00
Toby Batch
350875be2a moved dockerfile to main repo (#4393) 2023-11-17 13:07:51 +01:00
Kevin Papst
6a828cc9bd Release 2.3.0 (#4412)
- move user-preferences to edit submenu
- unify user preferences with other forms
- use light fieldset for all user-edit-forms
- support offcanvas elements
- allow to dynamically inject toolbar buttons (via plugins)
- use interface for formatting dates
- improve docker issue template
- bump version and composer packages
2023-11-08 22:30:22 +01:00
Weblate (bot)
7034066de9 Translated using Weblate (#4346)
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Benjamin Alan Jamie <benjamin@weblate.org>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Den SE <dense800@gmail.com>
Co-authored-by: EngageIndo <engageindo@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Liam Azofra <liamazofra@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Prefill add-on <noreply-addon-prefill@weblate.org>
Co-authored-by: Serhii Horichenko <m@sgg.im>
Co-authored-by: Singh <spamio@protonmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: albanobattistella <albano_battistella@hotmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: مهدی امیری راد <mahdiamererad@gmail.com>
2023-11-07 12:41:57 +01:00
Kevin Papst
2a9bdb7982 fix locale definitions starting with AM/PM (#4408)
* adapt locale script to fix locales starting with AM/PM
* fix korean locales that start with AM / PM
* added missing uk and zh_Hant locales
2023-11-07 12:27:11 +01:00
Kevin Papst
a1d9874c13 added role cards to permission screen (#4401)
* simplify creating new roles by auto-replacing input
2023-11-04 20:17:05 +01:00
Kevin Papst
0b1625175b remove link to external docker repo 2023-11-03 00:06:19 +01:00
Kevin Papst
0f6a6e7561 Update docker.yml (#4395) 2023-11-03 00:05:21 +01:00
Kevin Papst
7bbd497ef1 change URL to support landing page 2023-11-02 23:55:42 +01:00
Kevin Papst
0b3f645bda added docker bug report template 2023-11-02 23:54:29 +01:00
Kevin Papst
c840fee911 Release 2.2.1 (#4389)
* added missing functions to invoice security policy
2023-11-01 01:16:17 +01:00
Simon Schaufelberger
e06acc95c5 add parameter types and fix a phpstan issue (#4384) 2023-10-31 17:03:26 +01:00
Kevin Papst
95f15e6c88 Release 2.2.0 (#4359)
* deactivate deprecation logging in prod for now
* fix several deprecations
* enable CSRF for logout
* allow more twig methods and functions in InvoiceSecurity policy
2023-10-31 16:41:09 +01:00
Kevin Papst
114617a052 clarify EOL of Kimai 1 2023-10-20 16:30:20 +02:00
Kevin Papst
38e37f1c2e Release 2.1.0 (#4321)
* fix deprecations
* remove unused config
* replace invalid annotation type with attribute
* use AsDoctrineListener to fix deprecation
* new ModifiedSubscriber to support custom logic and fix deprecation
* removed inheritdoc comment
* new ModifiedSubscriber to support custom logic and fix deprecation
* cleanup event dispatcher interface
* re-order annotation params
* one more doctrine based deprecation
* fix query to count active timesheets
* link to "all times" to identify active timesheets
* link icon instead of text
* fix "skin" translation in wizard
* use duration filter to show duration
* added login link command and controller
* bump tabler theme to 1.0
* added wizard to force password reset by user
* allow to configure that new accounts need to reset their password
* prevent uploading twig templates by default
* bump composer packages
* enable sandbox and basic security measures for custom twig templates for invoice and export
* bump to symfony 6.3.5
* allow to export single user reports to excel
* removed broken method to reload twig cache
* added api parameter to fetch user collection fully serialized
* allow to replace or append description via timesheet batch update
* show api username above form
2023-10-19 11:21:50 +02:00
Kevin Papst
7a5b12762a Release 2.0.35 (#4302) 2023-09-23 18:15:22 +02:00
Kevin Papst
de419350b2 Release 2.0.34 (#4281) 2023-09-17 22:32:01 +02:00
Weblate (bot)
a6b237936e Translated using Weblate (#4282)
Co-authored-by: Kieran W <alarmcuddly@outlook.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Mykola Skira <mykola.skira@icloud.com>
Co-authored-by: No Pasaran <damiengourdin@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Łukasz Kosiński <lukikosin@gmail.com>
2023-09-11 23:18:07 +02:00
Kevin Papst
6e1207578e Release 2.0.33 (#4273)
* added function to check if meta-field is defined by a plugin
* hide un-defined meta-fields in details view
* fix default charset = utf8mb4
* remove broken DATABASE_VERSION fallback
* split off upgrade infos for version 1.x
* fix page out of range #4279
2023-09-07 22:24:08 +02:00
Kevin Papst
31348da4c0 Release 2.0.32 (#4256) 2023-08-31 15:33:53 +02:00
Bruno Paré-Simard
452de88e18 change isWeekend() handling to use work-hour configuration (#4261) 2023-08-31 13:38:58 +02:00
Weblate (bot)
18d953e79b Translated using Weblate (#4267)
Co-authored-by: Ali Evcil <evcil@proton.me>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
2023-08-31 12:51:37 +02:00
Kevin Papst
636422e342 show global form errors - fixes #4233 2023-08-21 20:38:32 +02:00
Weblate (bot)
20149ae765 Translated using Weblate (#4252)
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
2023-08-21 20:31:38 +02:00
Kevin Papst
a392f61d61 Release 2.0.31 (#4245)
* replace strings by class references
* allow 64 chars for username
* fix time can be optional and null
* fix timezone is not respected
* bump version
* fix br in attribute
* support different visible duration from booked duration
* fix image URL
2023-08-21 20:24:24 +02:00
Kevin Papst
3e61e0dce6 added supervisor setting for user (#4251) 2023-08-20 13:18:35 +02:00
Kevin Papst
9baa477bed fix payload 2023-08-17 22:58:28 +02:00
Kevin Papst
4d182ad1a7 validate version 2023-08-17 22:54:35 +02:00
Kevin Papst
e77056c4b7 fix payload and validate version 2023-08-17 22:22:53 +02:00
Johannes Przymusinski
d7f1df2987 update website version automatically (#4248) 2023-08-17 21:16:36 +02:00
Kevin Papst
8c1a793b0a added workflow to update website version 2023-08-16 22:33:25 +02:00
Kevin Papst
adc0779912 Release 2.0.30 (#4225) 2023-08-16 18:20:14 +02:00
Weblate (bot)
c7bc8fae73 Translated using Weblate (#4210)
Co-authored-by: Evgeniy Khramov <thejenjagamertjg@gmail.com>
Co-authored-by: Joaquim Homrighausen <joho@boojam.se>
Co-authored-by: John Aleksander Jazbec <johnny@xhorizont.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthew Durajka <logrcz@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: PatoGordo <icariusv@gmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Serhii Horichenko <m@sgg.im>
Co-authored-by: Tobs Hart <tobias@2bproduction.net>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Yaroslaw <bruce.levitangens@gmail.com>
Co-authored-by: beq <beqbdean@gmail.com>
Co-authored-by: full name <iaeqgz09gu6d@opayq.com>
Co-authored-by: jhtm <jhtm@users.noreply.hosted.weblate.org>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2023-08-16 15:08:22 +02:00
Kevin Papst
0a0ffb4ac9 remove text from tag choice after selection - fixes #4076 2023-08-04 13:03:11 +02:00
Kevin Papst
b831532323 Release 2.0.29 (#4178)
- show button title if delete is used in page actions
- fix invoice due date depends on invoice date, replace DateTime with DateTimeI… 
- lowercase all font names in PDFs, otherwise they fail loading
- hide empty fieldset (work-contract page)
- activate contract_other_profile by default for admin and super-admin
- deactivate rule to check "maximum duration of entries" by default
- allow to deactivate presets in DateRange Picker (for Devs)
2023-07-26 15:16:59 +02:00
Kevin Papst
385753fbc3 Release 2.0.28 (#4172)
See https://github.com/kimai/kimai/pull/4172
2023-07-09 15:27:27 +02:00
Kevin Papst
651f812da8 Release 2.0.27 (#4107) 2023-07-04 17:01:29 +02:00
Weblate (bot)
6a99ad41ca Translated using Weblate (#4109)
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthew Durajka <logrcz@gmail.com>
Co-authored-by: Matúš Bulla <matbull12345@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Sergii Horichenko <m@sgg.im>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Yonggen <vizualizer@gmail.com>
Co-authored-by: bigvictorio <bigvictorio12345@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2023-07-04 00:17:38 +02:00
Kevin Papst
dd89363c72 Improve UX for invoice template management (#4121)
change translations and to explain that calculator actually group items by fields
2023-06-21 08:17:54 +02:00
Weblate (bot)
983bf2c88b Translated using Weblate (#4099)
Co-authored-by: Arjan Houben <arjan@houben.es>
Co-authored-by: Jiri Nakola <github@nakola.fi>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Sebastiaan <djsebas@home.nl>
Co-authored-by: Tomas Koutek <spam@hug0.cz>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: akawshi <zenranoakawshi@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2023-06-13 14:17:37 +02:00
Kevin Papst
ce7fe67fcc use modern flat form styles with more icons (#4098) 2023-06-09 19:16:36 +02:00
Kevin Papst
053a1d9017 use collapsible element instead of collapsible card (#4096) 2023-06-09 18:17:32 +02:00
Kevin Papst
2e2bf986a4 Support visibility for tags (#4086) 2023-06-09 17:07:49 +02:00
Kevin Papst
6e781b59e2 Release 2.0.26 (#4087)
- setting "rounding days" not required
- developer: added user pref for public holiday group (to be used by plugins)
- developer: added WorkingTimeYearEvent (to be used by plugins)
- user pref: cleanup work contract form and support more fields (to be used by plugins)
- code cleanup
- bump theme and composer packages
2023-06-09 16:29:19 +02:00
Kevin Papst
0663995d06 Release 2.0.25 (#4066)
* added support for hourly rate column in detail table
* allow to register icon in extension
* allow to show QR code secret
* new translations
* bump theme and packages
* fix validation for invoice-document-filenames with uppercase character
* max upload size 1MB
* fix last month in daterange-picker in certain situations, more years in quick-select
* remove unused package-versions-deprecated
* link preferences from contract warning message
* added page_setup page layout
* prevent DROP TABLE in addSQL() and replace drop table with schema call
* added azuyalabs/yasumi
2023-06-06 23:05:20 +02:00
Kevin Papst
2ba9cff281 bump theme to get navigation id specific classes (#4065) 2023-05-26 22:39:07 +02:00
Kevin Papst
9c1150c50e added JS api to access current user in frontend (#4064) 2023-05-26 18:40:54 +02:00
Kevin Papst
b4a4d81ca8 Fix nullable parameter in work-contract API (#4063) 2023-05-26 18:39:44 +02:00
Kevin Papst
2681b81990 calendar: update popover content when shown (#4045) 2023-05-22 18:41:30 +02:00
Kevin Papst
97092acae9 fix single dropdown became two lines (#4041) 2023-05-21 17:49:44 +02:00
Kevin Papst
fdb6cf5cf8 allow to remove date from non-required field (#4040) 2023-05-21 17:13:41 +02:00
Kevin Papst
7ab0a8cea1 dynamic favicon that indicates if an entry is running (#4038) 2023-05-21 15:40:45 +02:00
Kevin Papst
2664d6117f show available github release only if newer version exists (#4037) 2023-05-20 13:49:31 +02:00
Kevin Papst
6bdf27fc9b change parameter name 2023-05-20 12:46:16 +02:00
Weblate (bot)
f0298335ee Translated using Weblate (#4036)
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sabri Ünal <libreajans@gmail.com>
Co-authored-by: Sebastiaan <djsebas@home.nl>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
2023-05-20 12:08:36 +02:00
Kevin Papst
a58a33fc7d Release 2.0.22 (#4032)
* fix version string in migration
* disable search in order and order_by
* reduce divider spacing for context menu and select with optgroup
2023-05-20 12:08:01 +02:00
Kevin Papst
9bd5965ecd Release 2.0.21 (#4030)
* increase padding in wizard
* fix theme chooser in wizard
* improve dynamic export columns in PDFs
* added label for total column
* bump theme bundle
* improve avatar list spacing
* be more flexible parsing times - fixes #4031
2023-05-17 15:02:10 +02:00
Kevin Papst
29624354a2 Release 2.0.20 (#4028)
* max duration per entry increased from 8 to 10 hours
* fixes #3981 - clickable area in dropdown too small
* fixes #4008 - duplicate activities in project-details report
* headers and summary styling in project-details report
* show billable stats in project-details report
* added new invoice variable for entry.duration_format
2023-05-15 22:30:37 +02:00
Kevin Papst
04422f3530 frontend update to tabler beta 19 (#4029) 2023-05-15 19:10:00 +02:00
Kevin Papst
43bd7bf1ab added API for handling work contract times (#4016) 2023-05-13 01:24:15 +02:00
Kevin Papst
4b2a3669e6 Release 2.0.19 (#4022)
* prevent invoices with exceeding filename
* fixed invalid LDAP log level
* support locale switching in action events
2023-05-12 14:45:51 +02:00
Kevin Papst
1099e76244 Release 2.0.18 (#4003)
* prevent too long values for user preferences
* add chart value 0 to y-axis if last value > 0, to always display the zero line (project details)
* add user-preferences to invoice hydrator
* DateTime vs DateTimeInterface
* simplify permission config
* fix query for teamleads limiting to only selected teams not possible
2023-05-08 17:11:55 +02:00
Weblate (bot)
d8f72b8b88 Translated using Weblate (#4010)
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Hama Czech <hamaczech13@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mæve Rey <martin.rey@mailbox.org>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sanji Finsmok <khldeilamil@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2023-05-08 13:07:59 +02:00
Weblate (bot)
b2314de315 Translated using Weblate (#4002)
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
2023-05-02 00:44:49 +02:00
Kevin Papst
66413d789c Release 2.0.17 (#3992)
* prevent empty title for select options
* removed css for unused sweetalert
* fix isWeekend() check with DateTimeInterface
* reset timesheet rates on "create copy"
* wrap long names in multi-selects - fix #4001
* rename profile menus
* show all possible links in user profile dropdown
* check if plugin is compatible before displaying the buy link
2023-05-02 00:40:43 +02:00
Kevin Papst
7112e932a2 Allow 2FA for SAML and LDAP users (#4000)
* inline totp image as data uri to prevent caching issues
* allow 2fa for ldap and saml users
* allow 2FA access for super admin to all profiles
2023-05-01 08:28:35 +02:00
Kevin Papst
01e26dca9e Release 2.0.16 (#3990)
https://github.com/kimai/kimai/pull/3990
2023-04-27 18:48:31 +02:00
Weblate (bot)
844062b90e Translated using Weblate (#3991)
Co-authored-by: Anders Johansson <johansson@aljmedia.se>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Petr Daniel Ambrož <paces@paces.cz>
2023-04-27 18:22:01 +02:00
Kevin Papst
f60065d089 split timesheet validator to simplify imports (#3974) 2023-04-27 13:56:07 +02:00
Kevin Papst
046ed313c9 Release 2.0.15 (#3970) 2023-04-16 00:44:00 +02:00
Weblate (bot)
ecd8ee85f3 Translated using Weblate (#3968)
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Kyotaro Iijima <kyotaro.eyes@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gnu-ewm <gnu.ewm@protonmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: tachyglossues <tachyglossues@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2023-04-06 22:03:33 +02:00
Kevin Papst
0c397e5080 Release 2.0.14 (#3963)
- show customer totals in single-user reports
- support custom fonts in export / invoice templates
- require daterange for export and invoice searches
- improve help label for user locale
- show decimal formats in help controller
- cache tag amount per request
- composer updates
- improved code styles
- added default label to date picker
- new macros & use own macro
2023-04-05 19:14:49 +02:00
Kevin Papst
01226a1243 Release 2.0.13 (#3955)
- added missing escape to prevent HTML injection
- added missing color attribute
- upgrade theme
  - use dropdown submenu if title is set, otherwise dropdown tends to get too long
  - allow to use card-table instead of card-body
  - added `required` attribute to username and password field
- fix pagination back to page 1
- prevent tag name too long
- re-add missing user preferences link
2023-03-30 01:08:15 +02:00
Weblate (bot)
7164ec126f Translated using Weblate (#3945)
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gnu-ewm <gnu.ewm@protonmail.com>
Co-authored-by: tachyglossues <tachyglossues@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2023-03-30 01:06:24 +02:00
Kevin Papst
04b5eb4c38 Release 2.0.12 (#3947)
- added submenus in action drodowns, to shorten them
- show all user-edit-screens in action dropdown
- fix cascade delete teams through customer
- fix cascade delete customer/project/activity through teams
- fix responsive classes for "internal rate" column
- clarify error message if invoice number generator or calculator is missing
2023-03-24 00:47:36 +01:00
Kevin Papst
3a5d7a62de Release 2.0.11 (#3932)
- added "today" as selector in date-range dropdown
- added feature to prevent auto-select of dropdowns with only one entry
- added hint that no changes were detected in batch update
- added negative invoice sums are possible (e.g. for credit notes)
- fix project list is expanded after submission
- fix invalid date parsing causes 500
- fix: prevent auto-select of activities in export and invoice form (in case only one global activity exists)
- fix team assignments for customer and project were not saved (using API now)
- fix form fieldset with legend styling (e.g. team project assignment)
- fix required meta-field were forced to have a value in batch update
- fix tomselect meta-field was not disabled in batch update
- fix unset internal rate is shown as 0
- fix one minute rounding problem in duration-only mode  with "now" being default time
- fix column width and label for duration-only mode
- tech debt: cleanup invoice template (remove invoice layout)
- tech debt: reorder for simpler comparison with invoice form
- possible BC for devs: remove unused methods from form trait
- bump composer packages (includes new translations for auth screens)
2023-03-21 12:42:18 +01:00
Weblate (bot)
252652082d Translated using Weblate (#3937)
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Blueberry <igory.ygr200@gmail.com>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Felipe Nogueira <contato.fnog@gmail.com>
Co-authored-by: Jeffrey <kjw5608kjw@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Lars Vogdt <lars.vogdt@suse.com>
Co-authored-by: Ondřej Mach <machondra2003@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Tang Yin <bingyuanshiye@126.com>
Co-authored-by: Viktor Kryvda <ivic4u@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: tygyh <jonis9898@hotmail.com>
2023-03-18 22:06:36 +01:00
Kevin Papst
f007697a73 Release 2.0.10 (#3927)
* allow API calls via GET
* allow to stop timesheet via GET
* improve form handling and validation
* improve stop button handling
* bump version
2023-03-15 23:02:28 +01:00
Kevin Papst
1a8c78944a Release 2.0.9 (#3922)
* fix api definition
* include customer number in validation message
* fix doctrine deprecation, prepare for DBAL 4
* added DataSubscriberInterface to identify certain doctrine subscribers
2023-03-14 23:51:49 +01:00
Kevin Papst
8449eafcb6 Release 2.0.8 (#3914)
* support parsing negative durations in JS
* bump luxon dependency
* make sure that 2FA is not required for session based API calls
* show name of items to delete
* fix permission issue for recent activity items
2023-03-13 01:48:53 +01:00
Kevin Papst
1310285133 bump version 2023-03-08 23:30:45 +01:00
Weblate (bot)
588e85d48f Translated using Weblate (#3905) 2023-03-08 21:05:19 +01:00
Kevin Papst
aebd57e311 added translation domain for dashboard widgets (#3909) 2023-03-08 19:01:49 +01:00
Kevin Papst
9d503300f6 Release 2.0.6 (#3900)
* cleanup invoice templates
* cleanup invoice translations
* fix invoice datetime for now
* support invoice archive order by number and status
* bump version
* allow meta-field sub-classing
* removed unused translation task.delete
* prevent null in str_replace
2023-03-08 00:38:10 +01:00
Weblate (bot)
80f8f70695 Translated using Weblate (#3902)
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Felipe Nogueira <contato.fnog@gmail.com>
Co-authored-by: Jeffrey <kjw5608kjw@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Ondřej Mach <machondra2003@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2023-03-07 21:39:42 +01:00
Weblate (bot)
c6fc06222f Unify language files (#3899)
Co-authored-by: Felipe Nogueira <contato.fnog@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2023-03-05 15:23:18 +01:00
Weblate (bot)
8926f7677e Update translation files (#3898) 2023-03-05 04:40:00 +01:00
Weblate (bot)
517e7eae89 Update translation files (#3897) 2023-03-05 04:38:12 +01:00
Weblate (bot)
8b53980ce7 Update translation files (#3896) 2023-03-05 04:36:34 +01:00
Weblate (bot)
d13083fc6f Cleanup translation files (#3895) 2023-03-05 04:34:54 +01:00
Weblate (bot)
34a79b1e3a Translated using Weblate (#3894)
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
2023-03-05 03:52:28 +01:00
Weblate (bot)
ea8bf6d1fa Cleanup translation files (#3893) 2023-03-05 03:48:04 +01:00
Weblate (bot)
cc39f25d37 Update translation files (#3887)
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: Felipe Nogueira <contato.fnog@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Ondřej Mach <machondra2003@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: i3sey <deniskedrovsky@mail.ru>
Co-authored-by: tygyh <jonis9898@hotmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2023-03-05 03:44:28 +01:00
Kevin Papst
0cbf0053d2 Release 2.0.5 (#3888)
- Fixed: mandatory fields / form validation for invoice template
- Added: Duration as calendar title replacer (open: running records)
- Fixed: HTML injection in Calendar
- Fixed: Doctrine Proxies are not initialized (leads to empty customer/project)
- Fixed: tag creation
- Fixed: validation errors do not need to be logged in prod
- Removed: Customer VCard download, as used library is outdated and not maintained
- Added: supporting translations domains in many new places (menu, help_text, page_setup, report, exporter, calendar)
2023-03-05 03:08:43 +01:00
Kevin Papst
3b93afabc8 Revert "bump packages"
This reverts commit d03fbf0ef9.
2023-03-02 15:15:01 +01:00
Kevin Papst
d03fbf0ef9 bump packages 2023-03-02 15:10:23 +01:00
Weblate (bot)
5c473ad39b Translated using Weblate (#3882)
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Felipe Nogueira <contato.fnog@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Ondřej Mach <machondra2003@gmail.com>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: tygyh <jonis9898@hotmail.com>
2023-03-02 14:22:19 +01:00
Kevin Papst
3f09a2674b Release 2.0.4 (#3883)
* fix column data truncated
* calculate internal rate from user
* show internal rate in timesheet listing
* Fixed: responsivenss and size of report start page icons
* fix: name display in dropdowns (and added tests)
* translate reload button
* fix invoice date might be in the past
* fail safe customer name handling
* translate invoice_date and invoice_date help
* prevent URLs like start=null
* prevent to reload select twice
2023-03-02 14:04:06 +01:00
Kevin Papst
a8b972f8a5 added permission caching (#3877) 2023-02-25 20:57:48 +01:00
Weblate (bot)
3c28288cac Translated using Weblate (#3867)
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Noemi <mikrometer-gearbeitet-0v@icloud.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sergii Horichenko <m@sgg.im>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2023-02-25 20:49:01 +01:00
Kevin Papst
2cb29bfef5 Release 2.0.3 (#3879) 2023-02-25 20:37:52 +01:00
Kevin Papst
068e9425eb added action to download invoice document (#3878) 2023-02-25 19:23:45 +01:00
Kevin Papst
a9849fc6cf added action to reload twig invoice template (#3876) 2023-02-25 19:10:29 +01:00
Kevin Papst
266c4bad25 Cleanup after 2.0 (#3868)
* removed duration_only mode
* cleanup database
* remove deleted user preference keys
2023-02-22 15:06:54 +01:00
Weblate (bot)
f04e0d92aa Translations update from Hosted Weblate (#3863)
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sergii Horichenko <m@sgg.im>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
2023-02-22 12:47:13 +01:00
Kevin Papst
cca3fa6b8a Improve inline stats (#3865)
* deactivate timesheet stats if no filter was chosen (otherwise a full table scan would be triggered for each listing display)
* hide duration stat if duration = 0 (timesheet, invoice, export)
2023-02-22 11:46:58 +01:00
Kevin Papst
1bc1fadc56 Move recent activities to modal (#3864)
* automatically remove deleted timesheets from bookmarks
2023-02-22 01:45:40 +01:00
Weblate (bot)
e7eb89dfd9 Translated using Weblate (#3830)
Co-authored-by: Anders Johansson <johansson@aljmedia.se>
Co-authored-by: Lars Vogdt <lars.vogdt@suse.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: jhtm <jhtm@users.noreply.hosted.weblate.org>
Co-authored-by: sandronidi <sandro.niederhauser@gmail.com>
2023-02-21 19:36:46 +01:00
Kevin Papst
25469113fd Release 2.0.2 (#3856)
* allow to overwrite global spreadsheet styles
* bump version
* fix deprecations in vcard download
* bump composer packages
* added help page for all registered locales
* fix menu id's, cleanup times route, fix configurable homepage redirect
* format duration without leading zero in hours (unify javascript with php behavior)
* fix active records in all screen sizes
* improved responsiveness in XS
* fixed invoice number for customer null fields
* fix javascript respects multiple recent-activity dropdowns
* show recent activities on small screens
* fix user-profile layout column in XS
* fix search is always marked as active
2023-02-21 19:21:49 +01:00
Kevin Papst
e474087257 Version 2.0.1 (#3853)
* configure email validation mode to fix deprecation message
* allow to use non brand icon in saml provider
* new method getCalculatedDuration()
* getRawData() by id
* only stop entries if new one is running
* fix validator tampering with timesheet duration
* fix allow setting null as customer
* bump packages
2023-02-18 14:19:16 +01:00
Kevin Papst
75aea7c51b added incompatible-plugins sentence 2023-02-15 00:35:38 +01:00
Kevin Papst
a64b44a74b simplify docs and cleanup links 2023-02-15 00:34:58 +01:00
Kevin Papst
d481f68266 remove user registration settings (#3832)
* can be re-activated through kimai.yaml
2023-02-07 19:36:10 +01:00
Kevin Papst
8095ae298f 2.0 RC 2 (#3825)
* fix menu highlight (invoice sub-pages)
* move calendar drag & drop boxes to right
* bump composer packages and install rate-limiter
* added login throttling
2023-02-07 01:01:15 +01:00
Weblate (bot)
99ad182358 Translated using Weblate (#3820)
Co-authored-by: Anders Johansson <johansson@aljmedia.se>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Pierre QUILLERY <pierre@quillery.fr>
Co-authored-by: ssantos <ssantos@web.de>
2023-02-06 18:48:18 +01:00
Weblate (bot)
f32448b9f5 Translated using Weblate (#3819)
Co-authored-by: Anders Johansson <johansson@aljmedia.se>
2023-02-05 20:14:28 +01:00
Weblate (bot)
f3b4078c6a Translated using Weblate (#3803)
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Roya K <royakhajavi@gmail.com>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: gnu-ewm <gnu.ewm@protonmail.com>
2023-02-05 20:07:12 +01:00
Kevin Papst
ca846b5fbf 2.0 RC 1 - new ConfigurationService (#3810)
* use html5 email validation
* remove static cache seed, for compatibility with non default (file based) caches
* added caching ConfigurationService, removed use of doctrine result cache
* simplify configuration API
2023-02-05 19:51:08 +01:00
Kevin Papst
63a3ae1147 2.0 release candidate (#3808)
* use SAML interface instead of implementation
* moved implementation of SAML configs to system-configuration base class
* bump composer packages
* simplify configuration for different env
2023-02-02 00:45:50 +01:00
Weblate (bot)
5711c15eac Translated using Weblate (#3800)
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
2023-01-27 23:44:43 +01:00
Kevin Papst
a705703996 cleanup translation keys 2023-01-27 00:58:07 +01:00
Kevin Papst
326dc64f65 2.0 beta 3 (#3786)
* added id for custom js options
* readme
* composer update
* fix migration filename
2023-01-27 00:50:04 +01:00
Kevin Papst
974e1b3b5a cleanup translation keys 2023-01-27 00:44:26 +01:00
Kevin Papst
f7f1765c2e cleanup translation keys 2023-01-27 00:14:17 +01:00
Kevin Papst
aae31c567c remove empty translation files 2023-01-26 23:28:27 +01:00
Hosted Weblate
49f307d605 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (345 of 345 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 51.3% (177 of 345 strings)

Translated using Weblate (Greek)

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Faroese)

Currently translated at 71.3% (246 of 345 strings)

Translated using Weblate (Finnish)

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Portuguese)

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (344 of 345 strings)

Translated using Weblate (Swedish)

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Slovak)

Currently translated at 74.4% (257 of 345 strings)

Translated using Weblate (Russian)

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Romanian)

Currently translated at 82.0% (283 of 345 strings)

Translated using Weblate (Polish)

Currently translated at 83.7% (289 of 345 strings)

Translated using Weblate (Dutch)

Currently translated at 77.3% (267 of 345 strings)

Translated using Weblate (Japanese)

Currently translated at 55.3% (191 of 345 strings)

Translated using Weblate (Hungarian)

Currently translated at 83.7% (289 of 345 strings)

Translated using Weblate (Basque)

Currently translated at 68.9% (238 of 345 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (345 of 345 strings)

Translated using Weblate (Esperanto)

Currently translated at 81.7% (282 of 345 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Danish)

Currently translated at 64.0% (221 of 345 strings)

Translated using Weblate (Czech)

Currently translated at 82.0% (283 of 345 strings)

Translated using Weblate (English)

Currently translated at 100.0% (345 of 345 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (345 of 345 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 51.0% (176 of 345 strings)

Translated using Weblate (Greek)

Currently translated at 81.7% (282 of 345 strings)

Translated using Weblate (Faroese)

Currently translated at 70.7% (244 of 345 strings)

Translated using Weblate (Finnish)

Currently translated at 81.7% (282 of 345 strings)

Translated using Weblate (Portuguese)

Currently translated at 82.0% (283 of 345 strings)

Translated using Weblate (Turkish)

Currently translated at 99.4% (343 of 345 strings)

Translated using Weblate (Swedish)

Currently translated at 82.0% (283 of 345 strings)

Translated using Weblate (Slovak)

Currently translated at 74.2% (256 of 345 strings)

Translated using Weblate (Russian)

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Polish)

Currently translated at 83.1% (287 of 345 strings)

Translated using Weblate (Japanese)

Currently translated at 54.7% (189 of 345 strings)

Translated using Weblate (Italian)

Currently translated at 81.7% (282 of 345 strings)

Translated using Weblate (Hungarian)

Currently translated at 83.7% (289 of 345 strings)

Translated using Weblate (Basque)

Currently translated at 68.6% (237 of 345 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (345 of 345 strings)

Translated using Weblate (Esperanto)

Currently translated at 81.1% (280 of 345 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 81.7% (282 of 345 strings)

Translated using Weblate (Danish)

Currently translated at 63.7% (220 of 345 strings)

Translated using Weblate (Czech)

Currently translated at 81.7% (282 of 345 strings)

Translated using Weblate (English)

Currently translated at 100.0% (345 of 345 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 3.5% (1 of 28 strings)

Translated using Weblate (Faroese)

Currently translated at 46.4% (13 of 28 strings)

Translated using Weblate (Portuguese)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Greek)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (28 of 28 strings)

Translated using Weblate (Swedish)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Slovak)

Currently translated at 82.1% (23 of 28 strings)

Translated using Weblate (Romanian)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (28 of 28 strings)

Translated using Weblate (Italian)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Hungarian)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Basque)

Currently translated at 82.1% (23 of 28 strings)

Translated using Weblate (Esperanto)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Czech)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Finnish)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 0.0% (0 of 28 strings)

Translated using Weblate (Faroese)

Currently translated at 42.8% (12 of 28 strings)

Translated using Weblate (Portuguese)

Currently translated at 85.7% (24 of 28 strings)

Translated using Weblate (Greek)

Currently translated at 85.7% (24 of 28 strings)

Translated using Weblate (Finnish)

Currently translated at 85.7% (24 of 28 strings)

Translated using Weblate (Turkish)

Currently translated at 96.4% (27 of 28 strings)

Translated using Weblate (Swedish)

Currently translated at 85.7% (24 of 28 strings)

Translated using Weblate (Slovak)

Currently translated at 78.5% (22 of 28 strings)

Translated using Weblate (Romanian)

Currently translated at 85.7% (24 of 28 strings)

Translated using Weblate (Polish)

Currently translated at 96.4% (27 of 28 strings)

Translated using Weblate (Hungarian)

Currently translated at 85.7% (24 of 28 strings)

Translated using Weblate (Basque)

Currently translated at 78.5% (22 of 28 strings)

Translated using Weblate (Esperanto)

Currently translated at 85.7% (24 of 28 strings)

Translated using Weblate (Czech)

Currently translated at 85.7% (24 of 28 strings)

Translated using Weblate (Hungarian)

Currently translated at 83.7% (289 of 345 strings)

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (German (Switzerland))

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (Slovak)

Added translation using Weblate (Korean)

Added translation using Weblate (Faroese)

Added translation using Weblate (Finnish)

Added translation using Weblate (Swedish)

Added translation using Weblate (Dutch)

Added translation using Weblate (Arabic)

Added translation using Weblate (Hungarian)

Added translation using Weblate (Vietnamese)

Added translation using Weblate (Portuguese)

Added translation using Weblate (Czech)

Added translation using Weblate (Japanese)

Added translation using Weblate (Romanian)

Added translation using Weblate (Russian)

Added translation using Weblate (Basque)

Added translation using Weblate (Esperanto)

Added translation using Weblate (Greek)

Added translation using Weblate (Danish)

Added translation using Weblate (Chinese (Traditional))

Translated using Weblate (Chinese (Traditional))

Currently translated at 51.3% (177 of 345 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 0.0% (0 of 28 strings)

Translated using Weblate (Faroese)

Currently translated at 42.8% (12 of 28 strings)

Translated using Weblate (Portuguese)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Greek)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Greek)

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Faroese)

Currently translated at 71.3% (246 of 345 strings)

Translated using Weblate (Finnish)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Finnish)

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Portuguese)

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (28 of 28 strings)

Translated using Weblate (Swedish)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Slovak)

Currently translated at 82.1% (23 of 28 strings)

Translated using Weblate (Romanian)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (28 of 28 strings)

Translated using Weblate (Dutch)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Hungarian)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Basque)

Currently translated at 82.1% (23 of 28 strings)

Translated using Weblate (Esperanto)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Danish)

Currently translated at 67.8% (19 of 28 strings)

Translated using Weblate (Czech)

Currently translated at 89.2% (25 of 28 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (345 of 345 strings)

Translated using Weblate (Swedish)

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Slovak)

Currently translated at 74.7% (258 of 345 strings)

Translated using Weblate (Romanian)

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Polish)

Currently translated at 83.7% (289 of 345 strings)

Translated using Weblate (Dutch)

Currently translated at 77.6% (268 of 345 strings)

Translated using Weblate (Japanese)

Currently translated at 55.3% (191 of 345 strings)

Translated using Weblate (Hungarian)

Currently translated at 79.7% (275 of 345 strings)

Translated using Weblate (Basque)

Currently translated at 68.9% (238 of 345 strings)

Translated using Weblate (Esperanto)

Currently translated at 81.7% (282 of 345 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 82.3% (284 of 345 strings)

Translated using Weblate (Danish)

Currently translated at 64.0% (221 of 345 strings)

Translated using Weblate (Czech)

Currently translated at 82.3% (284 of 345 strings)

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (Chinese (Traditional))

Added translation using Weblate (Chinese (Traditional))

Translated using Weblate (Arabic)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Arabic)

Currently translated at 88.4% (23 of 26 strings)

Translated using Weblate (Arabic)

Currently translated at 94.2% (325 of 345 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: ButterflyOfFire <ButterflyOfFire@protonmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Kis Momesz <kismomesz@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/cs/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/da/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/de_CH/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/el/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/eo/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/eu/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/fi/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/fo/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/hu/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/it/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/nl/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/pl/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/pt/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/ro/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/sk/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/sv/
Translate-URL: https://hosted.weblate.org/projects/kimai/actions/tr/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/ar/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/cs/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/da/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/de_CH/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/el/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/en/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/eo/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/es/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/eu/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/fi/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/fo/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/hu/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/it/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/ja/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/nl/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/pl/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/pt/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/ro/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/ru/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/sk/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/sv/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/tr/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/uk/
Translate-URL: https://hosted.weblate.org/projects/kimai/messages/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/kimai/teams/ar/
Translate-URL: https://hosted.weblate.org/projects/kimai/validations/ar/
Translation: Kimai - Time tracking/Actions (Dropdowns and Toolbars)
Translation: Kimai - Time tracking/Main application
Translation: Kimai - Time tracking/Teams
Translation: Kimai - Time tracking/Validations
2023-01-26 23:20:18 +01:00
Weblate (bot)
f93d081a88 Translated using Weblate (#3784)
Co-authored-by: ButterflyOfFire <ButterflyOfFire@protonmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2023-01-25 17:59:54 +01:00
Kevin Papst
a230be77dd beta 3 (#3780)
* merge master - allow to upload twig invoice templates via UI
* support adding existing teams with same name
* permissions cannot be set right after role was created - fixes #3777
* allow to deactivate unique customer number validation - fixes #3762 
* invalid message when trying to edit locked or exported timesheets in calendar - fixes #3766
* updated icons and manifest - fixes #3761
2023-01-21 14:49:55 +01:00
Weblate (bot)
b62253e1f3 Translated using Weblate (#3760)
Co-authored-by: Nathan <bonnemainsnathan@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gnu-ewm <gnu.ewm@protonmail.com>
2023-01-18 14:59:45 +01:00
Kevin Papst
0e91dd886e release 2.0 beta 2 (#3757)
* do not traverse into invoice template subdirectories (#3735)
* fix security open api definition
* fix currency can be null, removed fluent interface
* merged release 1.30.3
* allow to pre-fill timesheet metafields via URL
* fix api description
* added test accounts with simpler names and password
* upgrade to Symfony 6.2
* removed FrameworkExtraBundle (by Sensio) and replaced with new native SF annotations
* fixed symfony 6.2 deprecations
* fixed #3768
2023-01-18 14:47:48 +01:00
Kevin Papst
6e0500972e fix bug report template 2023-01-12 21:32:44 +01:00
Kevin Papst
8069e332fe release 2.0 beta (#3722)
* remove twitter link
* remove WIP file
* adjust release draft message
* reset code coverage threshold back to 0.5
* changed wordings
* re-activate wizard for fixture accounts
* fix repo url and license
* license identifier
* bump version
* moved Kimai 1 import command from core to plugin
* do not traverse into invoice template subdirectories (#3735)
* fix branch alias
* composer update
* switch language on wizard select
* new twig function to create qr code
* fix daily stats in timesheet listing
* improved html invoice templates
2023-01-12 12:10:11 +01:00
Weblate (bot)
cbd65f1f1d Translated using Weblate (#3723)
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Magdalena Turek <magdalena.2rek@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sergii Horichenko <m@sgg.im>
Co-authored-by: Sima Teimourianmotlagh <simateimourian@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gnu-ewm <gnu.ewm@protonmail.com>
Co-authored-by: h <tachyglossues@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2023-01-12 01:43:58 +01:00
Kevin Papst
90a0fd8a22 Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902) 2022-12-31 21:19:55 +01:00
Weblate (bot)
95e06746bd Translated using Weblate (#3705)
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2022-12-28 15:47:57 +01:00
Kevin Papst
1e492624cc fixed owner name 2022-12-26 17:27:25 +01:00
Kevin Papst
6a524f9f7c changed repository url (#3708) 2022-12-26 17:09:07 +01:00
Weblate (bot)
84cc65b008 Translated using Weblate (#3655)
Co-authored-by: Jonas Tisell <jonas.tisell@live.no>
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2022-12-17 01:43:45 +01:00
Kevin Papst
b8b3a3c8ab fix find activity for project (#3681) 2022-12-07 22:42:02 +01:00
Kevin Papst
db0e555eae weekly timesheet uses automatic billable mode, auto responsiveness (#3679) 2022-12-07 18:44:17 +01:00
Kevin Papst
58a9facc6d Release 1.30 (#3651)
* exclude all 403 and 404 from logs
* bump version
* cleanup tag handling
* prevent mandatory user preferences turning null
2022-12-04 15:02:36 +01:00
Kevin Papst
11dac19c8c added mastodon link 2022-11-25 20:45:29 +01:00
Weblate (bot)
2d82f0400d Translated using Weblate (#3649)
Co-authored-by: Teng You Jyun <youjyun0728@gmail.com>
2022-11-24 14:37:52 +01:00
Kevin Papst
94a6b99c57 fix php 8.1 deprecations (#3648)
* show session maxlifetime in doctor
* rephrase compatibility in release notes
* bump version
2022-11-24 13:12:18 +01:00
Pieter Frenssen
2aa5eccc7c Add accessibility attributes to sortable table headers. (#3647) 2022-11-23 11:36:57 +01:00
Weblate (bot)
52f5e5640e Translated using Weblate (#3628)
Co-authored-by: BeckeBauer <berko@gmx.de>
Co-authored-by: Sergii Horichenko <m@sgg.im>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2022-11-21 21:17:02 +01:00
Kevin Papst
d6c15bb1cb respect project start and end dates in quick-entry form (#3642) 2022-11-21 19:07:33 +01:00
Kevin Papst
4f698f73dc added username parameter to export command (#3638) 2022-11-19 18:38:13 +01:00
Kevin Papst
e413325337 added project + customer setting for calendar entry titles (#3636) 2022-11-16 23:02:26 +01:00
Kevin Papst
608a45408a Release 1.29 (#3622) 2022-11-06 20:44:30 +01:00
Weblate (bot)
323baa2d65 Translations update from Hosted Weblate (#3607)
Co-authored-by: Laurens Wuyts <laurens@mapix.be>
Co-authored-by: Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
Co-authored-by: MohammadReza Baeedi (M.R.B) <mohammadreza.baeedi@gmail.com>
Co-authored-by: Sergii Horichenko <m@sgg.im>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
2022-11-06 19:54:22 +01:00
Kevin Papst
6235d18dab improve user unique check (#3621) 2022-11-06 14:15:21 +01:00
Kevin Papst
3d0f480d9e added missing permission check on project rates in monthly report (#3627) 2022-11-05 11:39:18 +01:00
Kevin Papst
642d31432a bump dependencies and improved export command (#3606) 2022-10-30 11:34:17 +01:00
Weblate (bot)
421be6835e Translated using Weblate (#3586)
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Bogi Napoleon Wennerstrøm <bogi.wennerstrom@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: MohammadReza Baeedi (M.R.B) <mohammadreza.baeedi@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sergii Horichenko <m@sgg.im>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gnu-ewm <gnu.ewm@protonmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Мистер Перевод <sergei@racingrebel.com>
2022-10-29 18:23:28 +02:00
Kevin Papst
d58720252e create exports via command (#3605) 2022-10-29 18:20:19 +02:00
Kevin Papst
0461539f4b new stateless firewall to prevent new sessions for API calls (#3602) 2022-10-28 15:13:40 +02:00
Kevin Papst
78749ad3b4 copy teams from logged-in user for new projects (#3599) 2022-10-25 16:31:34 +02:00
Kevin Papst
8b4828f236 allow 4 decimals for rounded rates (#3596) 2022-10-24 16:21:57 +02:00
Kevin Papst
e5614b92a0 fix test 2022-10-24 11:59:05 +02:00
Kevin Papst
f5b725e32b added first and last fields for invoices (#3594) 2022-10-24 11:46:13 +02:00
Kevin Papst
006de60f66 Release 1.28 (#3587)
* bump version
* extract toolbar cleanup logic to helper class
* allow 3 digits in invoice template tax rate
2022-10-24 11:42:56 +02:00
Kevin Papst
14f4530de0 added team filter in timesheet search forms (#3590)
* added team filter in export and invoice search form
* added teams select to timesheet filter
* added team select to report filter
* hide user and team filter if count < 2
2022-10-21 16:14:39 +02:00
Kevin Papst
2022fa54d3 added customer name as pattern option in project dropdown (#3589) 2022-10-21 15:26:53 +02:00
Kevin Papst
73ef090b9e fix several rendering issues in markdown (#3588)
- fix double escaping of html entities: &quot; instead of "
- fix table is missing css class "table"
- fix quotes are not rendered
2022-10-19 21:17:49 +02:00
Kevin Papst
6d7054c18c Release 1.27.0 (#3571)
* bump version
* ignore .disabled for plugins
* simplify gitignore
* prevent duration field from showing up in the API
* change release template wording
* prepare method for later usage
* add 8.2 to issue template
2022-10-19 08:55:42 +02:00
Weblate (bot)
4549d826dd Translations update from Hosted Weblate (#3570)
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Sergii Horichenko <m@sgg.im>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gnu-ewm <gnu.ewm@protonmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2022-10-19 08:44:42 +02:00
Kevin Papst
7eacb1dc3c improved issue templates 2022-10-19 01:00:22 +02:00
Kevin Papst
5264dd111d fix markdown 2022-10-19 00:13:32 +02:00
Kevin Papst
a4f0b6a97e fix markdown 2022-10-19 00:12:48 +02:00
Kevin Papst
46af5e8ebf fix markdown 2022-10-19 00:12:19 +02:00
Kevin Papst
35fb15fe9f fix filename 2022-10-19 00:08:56 +02:00
Kevin Papst
662259468d improve issue templates 2022-10-18 23:42:16 +02:00
Kevin Papst
1e1b9a6919 allow to order by hourlyRate (#3582) 2022-10-16 12:01:57 +02:00
Kevin Papst
5fa10dfdf1 fix one-minute rounding bug in quick-entries (#3580)
* fix one-minute rounding bug for quick entries form
* allow to delete and create/update timesheets in one go
* prevent issues with negative durations
2022-10-16 01:05:38 +02:00
Kevin Papst
a5556a0c63 do not show potential invoices with a negative total (#3579) 2022-10-13 12:29:11 +02:00
Kevin Papst
0cefcf0e16 allow negative duration via internal API (#3573) 2022-10-10 17:47:11 +02:00
Kevin Papst
1959d4d8df added filter for globalActivities in project collection (#3565) 2022-10-01 23:25:33 +02:00
Kevin Papst
1d445d9eb6 Project API and globalActivities flag (#3564)
* expose globalActivities setting in project entity
* fix globalActivities being true by default if created via API
2022-10-01 18:14:22 +02:00
Weblate (bot)
6cfcbccabc Translated using Weblate (#3549)
Co-authored-by: Daniel <danchao@freedom.net.tw>
Co-authored-by: Eric <hamburger1024@mailbox.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Nguyễn Hoàng Minh <mingu03@yandex.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: SHINJI.K <shinkuroshi@gmail.com>
Co-authored-by: adam Petro <grimnoobster@gmail.com>
Co-authored-by: office256 <ohofman@seznam.cz>
Co-authored-by: power meshal <powermeshal@gmail.com>
Co-authored-by: Мистер Перевод <sergei@racingrebel.com>
Co-authored-by: 이정희 <daemul72@gmail.com>
2022-10-01 17:40:42 +02:00
Kevin Papst
de39623378 prepare release 1.26.0 (#3554)
* bump version
* suppress deprecation
* composer update to fix twig security issue
* make sure to use the same timezone in all test runs
2022-10-01 17:23:15 +02:00
Kevin Papst
96d0d4943b update composer packages (#3502)
drop full database schema in test
2022-09-25 17:06:49 +02:00
Kevin Papst
bfab6f42d0 use saml config interface instead of generic system configuration (#3551) 2022-09-23 13:29:46 +02:00
Weblate (bot)
b38fc96623 Translated using Weblate (#3542)
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Maamon AlHaqli <mamon321@hotmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nguyễn Hoàng Minh <mingu03@yandex.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: skotperez <skotperez@voragine.net>
2022-09-20 15:53:32 +02:00
Kevin Papst
b7bade20ea make project and customer available in summaries (#3543) 2022-09-16 13:57:03 +02:00
Kevin Papst
283ab80fc7 configurable calendar drag and drop behavior (#3537) 2022-09-14 23:29:26 +02:00
Kevin Papst
d705287510 prepare release 1.25.0 (#3531)
* remove unused variable
* allow to set field name in form helper
* fix edge case permission check in menu
2022-09-14 15:16:06 +02:00
Weblate (bot)
47d17af593 Translated using Weblate (#3532)
Co-authored-by: Nguyễn Hoàng Minh <mingu03@yandex.com>
Co-authored-by: phande <phanwiki@gmail.com>
Co-authored-by: skotperez <skotperez@voragine.net>
2022-09-14 14:43:35 +02:00
Kevin Papst
496a74f7a3 added keyboard shortcuts (#3536) 2022-09-14 14:30:15 +02:00
Kevin Papst
7eded6e126 fix resname 2022-09-08 18:08:42 +02:00
Weblate (bot)
4c9d3222d7 Translated using Weblate (#3523)
Co-authored-by: Daniel <danchao@freedom.net.tw>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jordy Heutinck <jordyheutinck@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
2022-09-08 18:02:10 +02:00
Kevin Papst
5a2b539003 version 1.24.0 (#3515) 2022-09-04 23:55:52 +02:00
Weblate (bot)
a9252701c5 Translated using Weblate (Faroese) (#3496) 2022-09-04 23:18:18 +02:00
Alexander Pankow
2fbbbe7260 added inline disposition for PDF previews (#3486) 2022-09-04 12:15:47 +02:00
Alexander Pankow
1d1f5835da allow to hide tax rows in invoice templates if vat rate is zero (#3484) 2022-09-04 11:43:38 +02:00
Alexander Pankow
a04b72e866 use custom fonts in mpdf via twig template (#3509) 2022-09-04 00:58:54 +02:00
Kevin Papst
3f4f63dc33 fix datetime modify to now (#3511) 2022-09-03 01:11:48 +02:00
Jelle Sebreghts
1763a2efff fix grace period in quick-entry form (#3504) 2022-09-03 00:49:16 +02:00
Kevin Papst
00906851b5 bump version to 1.23.1 (#3499) 2022-09-01 13:15:17 +02:00
Kevin Papst
844a805de6 fix unit test failing on month borders (#3494) 2022-08-31 15:12:18 +02:00
Kevin Papst
718e0b5c1e prevent accidental invoice template updates (#3493) 2022-08-31 13:18:21 +02:00
Weblate (bot)
ad20fd1c64 Translated using Weblate (#3482)
Co-authored-by: Cường Bá <cuongba956@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: SDGArmenia <webadmin@armstat.am>
Co-authored-by: Tom Sawyer <weblate@grymkoll.se>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2022-08-30 18:02:44 +02:00
Kevin Papst
0eb5c0a08a fixed multiple budget validation checks (#3489)
* fix budget check for monthly budgets for records with a changed month
* fix API update of begin and end
* fix update of project and activity
2022-08-29 01:03:44 +02:00
Kevin Papst
7c3a08057d Order quick entries by project name (#3488) 2022-08-27 17:11:48 +02:00
Weblate (bot)
c3a43fe677 Translated using Weblate (#3462)
Co-authored-by: Ahmed Saleh <sniperasa8@gmail.com>
Co-authored-by: Alireza Mousavi <alrzmsv@gmail.com>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Jiri Nakola <github@nakola.fi>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2022-08-18 14:28:35 +02:00
Kevin Papst
1c613d0a52 do not require user_preference permission when creating new user (#3474) 2022-08-16 21:13:26 +02:00
Kevin Papst
303ec5be11 bump composer packages for version 1.22.1 (#3455) 2022-08-01 18:56:48 +02:00
Kevin Papst
0e33da7760 fix invoice preview opening in current tab (#3454) 2022-08-01 18:09:41 +02:00
Kevin Papst
64c752bea3 disable create-invoice links after click to prevent double invoices (#3452) 2022-08-01 14:45:22 +02:00
Weblate (bot)
cb2938b5c2 Translations update from Hosted Weblate (#3436)
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jan Čejka <posta@jancejka.cz>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nubovik <babushkin.nikita.2008@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: andt <andywondering@protonmail.com>
2022-08-01 14:44:52 +02:00
Kevin Papst
1b8a26d36b rebuild assets for croatian (#3451) 2022-08-01 14:14:17 +02:00
Milo Ivir
afdcc88342 added javascript translations for croatian (#3447) 2022-08-01 13:57:54 +02:00
Kevin Papst
7238f404f7 technical debts (#3438)
* reformat release notes
* deactivate transactions in migrations by default
* fix migration base class
* fix default value
* composer update
2022-07-27 00:54:43 +02:00
Kevin Papst
1b8948a086 show total hourly rate in detail pages (#3441) 2022-07-27 00:52:10 +02:00
Kevin Papst
95ab9bda2d saml: allow to keep existing roles on login (#3440) 2022-07-26 23:18:04 +02:00
Kevin Papst
ed7f89cfe1 allow to restrict usage of global activities for projects (#3437) 2022-07-23 01:17:24 +02:00
Kevin Papst
8d695d03df link customer, project and activity in invoice listing (#3428) 2022-07-22 13:26:17 +02:00
Kevin Papst
ad880b5b97 bump version 1.22 (#3429)
* adjusted release note template
* stabilize test
* deactivate audit listener, more pre-validation for kimai 1 importer
2022-07-22 12:41:49 +02:00
Kevin Papst
c0f3f31095 total sums for duration and rate in invoice and export preview (#3431) 2022-07-22 12:39:50 +02:00
Kevin Papst
e2ac6a0b17 Project date-range report: allow budget-type independent project-listing (#3430) 2022-07-22 12:27:53 +02:00
Kevin Papst
0dba83d8e9 Budget graph in project details (#3406)
* do not copy description if restarting via calendar
* added support to display negative durations
* fix charts for larger numbers
2022-07-13 17:21:36 +02:00
Weblate (bot)
245dd47082 Translated using Weblate (Polish) (#3399)
Co-authored-by: Alejandro Malo <malo@pinion.education>
Co-authored-by: Anupam Malhotra <anpm.malhotra@gmail.com>
Co-authored-by: DNSE <kty5663@gmail.com>
Co-authored-by: Jiri Nakola <github@nakola.fi>
Co-authored-by: Jozef Cibík <jozef.cibik@fatchillimedia.com>
Co-authored-by: KonradMazur <konrad@mazur.legal>
2022-07-13 17:15:00 +02:00
Kevin Papst
dc40a05ebd allow to filter for canceled invoices (#3415) 2022-07-13 12:21:57 +02:00
Kevin Papst
0616506b31 re-style overlapping border (#3400) 2022-07-05 20:23:10 +02:00
Kevin Papst
196ca4f1cb new command to delete empty translations (#3392)
* composer update and phpstan issues
* bump version
2022-06-30 14:19:56 +02:00
Weblate (bot)
2d9677eefa Translations update (#3356)
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nathan <bonnemainsnathan@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: haoyubox <3306221756@qq.com>
Co-authored-by: phlostically <phlostically@mailinator.com>
Co-authored-by: whisley.wang <whisley.wang@amsrbt.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2022-06-30 14:05:18 +02:00
Kevin Papst
cd297150ea suppress deprecation notice, convert route param (#3391) 2022-06-30 12:14:01 +02:00
Kevin Papst
665f0d0ca7 require a language for invoice templates (#3387) 2022-06-27 22:57:15 +02:00
Kevin Papst
bd1097f7b5 fix en_GB format definition (#3383) 2022-06-27 22:37:31 +02:00
Kevin Papst
f30bb89597 remove currency symbol if unknown (#3381) 2022-06-25 01:23:16 +02:00
Kevin Papst
9f1f7b989c allow to set api token when creating user via API (#3380) 2022-06-25 00:26:39 +02:00
Kevin Papst
aa8549b633 reoad permissions page after role was created (#3377) 2022-06-24 16:57:59 +02:00
Kevin Papst
222056f973 removed invalid destroy calls to daterangepicker (#3371) 2022-06-23 01:59:09 +02:00
Kevin Papst
e431a1ad9e Export invoice metafields (#3366) 2022-06-21 14:06:23 +02:00
Kevin Papst
452cc46d67 fix original filenames for translations (#3363) 2022-06-19 18:01:08 +02:00
Kevin Papst
c956d075d9 added release drafter workflow (#3362) 2022-06-19 17:42:09 +02:00
Kevin Papst
cdf5a545eb fix calendar drag and drop for regular user (#3358) 2022-06-14 13:07:57 +02:00
Weblate (bot)
a60df65062 Translations update from Weblate (#3339)
* Translated using Weblate (Croatian)
Co-authored-by: ABDULLAH MOHAMMED ALMONTSHRI <acc302@gmail.com>
Co-authored-by: Bogi Napoleon Wennerstrøm <bogi.wennerstrom@gmail.com>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jiri Nakola <github@nakola.fi>
Co-authored-by: Luiz Carlos Lucasv <csversut@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: majorwave <majorwavebeats@gmail.com>
2022-06-11 13:53:10 +02:00
Kevin Papst
f8cb8900d6 Release 1.20.3 (#3340)
* fix edit timesheet link
* fix drag & drop creates record for wrong user
* fix search with multiple bindings
* hide user switcher in calendar if there is only one user to choose
* make quick entry responsive for mobile-only users
* mark invoices as exported by default
* fix serialization deprecation warning
* fix duration calculation for fixed rate entries
* updated composer packages
* fix checkbox for horizontal forms
* support pdfContext for PDF invoice templates
2022-06-11 13:25:50 +02:00
Kevin Papst
b46fdcefad added new permission to separate time and money budget (#3352) 2022-06-11 12:23:02 +02:00
Kevin Papst
3f827b3104 code cleanup (#3338) 2022-05-31 18:08:41 +02:00
Kevin Papst
88bf534573 added invoice text field to project and activity (#3335) 2022-05-31 17:49:23 +02:00
Weblate (bot)
a93e78bd34 Translated using Weblate (Norwegian Bokmål) (#3329)
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
2022-05-31 17:41:30 +02:00
Kevin Papst
4eb1fffeb2 added meta-field names to listing pages as css classes (#3336) 2022-05-31 17:38:53 +02:00
Kevin Papst
889e3c0d4a pre-select an option if it is the only available one (#3337) 2022-05-31 17:38:21 +02:00
Andreas Pollak
184026f5f1 added css class for custom fields (#3328) 2022-05-26 17:11:02 +02:00
Weblate (bot)
3c14fee36c Translated using Weblate (#3321)
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
2022-05-26 14:08:09 +02:00
Kevin Papst
5fb326db4d Release 1.20.2 (#3320) 2022-05-24 16:28:14 +02:00
Kevin Papst
d5f8655827 Release 1.20.1 (#3317)
- improved timesheet calculator with changesets and priority
- calculate and include exported stats (e.g. available in export templates)
- added hourly rate column to timesheet listing
2022-05-21 13:13:56 +02:00
Kevin Papst
3e6100f368 bump version to 1.20 (#3315) 2022-05-19 01:36:12 +02:00
Kevin Papst
3c71640547 allow switching user displayed in calendar (#3314) 2022-05-18 21:30:58 +02:00
Kevin Papst
913839727c permission check for mark-as-exported buttons (#3313) 2022-05-18 12:54:59 +02:00
Kevin Papst
9e2a295182 Release 1.19.8 (#3296)
* unify extension check
* fix updating one config section redirects to all sections
* weekly times: configure number of weeks for recent activities
* weekly times: configure minimum number of rows
* added explicit formats for en_GB and pt
2022-05-17 11:52:45 +02:00
Lukas Steinmann
27c60d9fc4 Prevent bookings with same start / stop time (#3304) 2022-05-17 11:12:08 +02:00
Weblate (bot)
f409ce5364 Translated using Weblate (#3310)
Co-authored-by: SHINJI.K <shinkuroshi@gmail.com>
Co-authored-by: poog <poogchamp@gmail.com>
2022-05-17 10:36:20 +02:00
Weblate (bot)
68cd879d1e Translated using Weblate (Portuguese) (#3276)
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
2022-05-08 22:27:48 +02:00
Kevin Papst
c8098b2e00 Release 1.19.7 (#3286)
* fix isWeekend() test for sunday fdow
* re-use the pattern for optgroup title
* prevent method on null
* composer update
* pre-select an option if it is the only available one
* added command to stop all active timesheets
2022-05-07 23:47:32 +02:00
Kevin Papst
100f8a5165 Release 1.19.6 (#3277) 2022-04-29 18:14:12 +02:00
Kevin Papst
76899d2fdb Release 1.19.5 (#3265) 2022-04-26 18:48:43 +02:00
j0hannesr0th
41e8e196ec fix: billable not respected in api post request (#3275) 2022-04-26 17:46:44 +02:00
Kevin Papst
ef5977bcfe allow changing calendar entry title (#3272) 2022-04-25 18:18:48 +02:00
Weblate (bot)
e94d47bf0f Translations update from Hosted Weblate (#3267)
Co-authored-by: SHINJI.K <shinkuroshi@gmail.com>
Co-authored-by: Tamás Papp <tamas.papp.it@gmail.com>
Co-authored-by: xiexieqing <xqmayone@outlook.com>
2022-04-25 16:59:55 +02:00
Weblate (bot)
f8bbb3065c Translated using Weblate (#3263)
Co-authored-by: Nathan <bonnemainsnathan@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: adam Petro <grimnoobster@gmail.com>
Co-authored-by: dkstiler <dkstiler@gmail.com>
2022-04-19 13:12:44 +02:00
Kevin Papst
bc908b4534 Release 1.19.4 (#3255)
* login redirects to homepage if already being logged-in
* fix budget check for entries that were moved to another moth
* invoice: fix amount should be decimal if decimal template is used
* added new month grouped by project/activity/user report
2022-04-18 12:41:45 +02:00
Kevin Papst
a179719c3e version 1.19.3 (#3250) 2022-04-05 18:10:09 +02:00
Kevin Papst
2ca3a3f7db interface for saml config (#3249) 2022-04-05 17:49:41 +02:00
Kevin Papst
f2a8cf0930 Release 1.19.2 (#3244) 2022-04-05 00:59:13 +02:00
Weblate (bot)
966701f94e Translated using Weblate (#3235)
Co-authored-by: ANIGO R. (AR) <franigoyt@gmail.com>
Co-authored-by: leuedaniel <leuenberger.daniel@bluewin.ch>
2022-04-04 18:20:55 +02:00
Kevin Papst
1a183a4825 allow arbitrary string length for system configurations (#3243) 2022-04-04 17:38:50 +02:00
Kevin Papst
3a7e3b2168 bump version 1.19.1 (#3229) 2022-03-30 22:23:12 +02:00
Kevin Papst
eb2664ac79 support more complex metafield queries (#3228) 2022-03-30 19:01:22 +02:00
Kevin Papst
31743cf962 fix billable calculation on timesheet restart (#3225) 2022-03-29 23:24:22 +02:00
Kevin Papst
2c2ba2e32c show user account number in report export (#3224) 2022-03-29 12:48:42 +02:00
Weblate (bot)
4ff2153e15 Translated using Weblate (#3221)
Co-authored-by: ButterflyOfFire <ButterflyOfFire@protonmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mzw <466271552@qq.com>
2022-03-29 12:31:35 +02:00
Kevin Papst
3c6b5c6c1e fix relative times in budget calculation in export (#3216) 2022-03-22 21:02:29 +01:00
Kevin Papst
6c1d79fe5b release 1.19 (#3215) 2022-03-21 23:42:56 +01:00
Weblate (bot)
e5f1df8c70 Translated using Weblate (#3208)
Co-authored-by: Gil <almontegil@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Taplou <ploutarchosgram@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: 이정희 <daemul72@gmail.com>
2022-03-21 21:32:46 +01:00
Kevin Papst
d6b59f6799 defensive javascript (#3210) 2022-03-20 21:08:59 +01:00
Kevin Papst
4b069edd33 added event to extend detail pages (#3209) 2022-03-19 23:54:40 +01:00
Kevin Papst
a8ac6b2eda Release 1.18.3 (#3204)
* fix truncated comments for customer, project and activity
* fix export button label for non-translated renderer
* fixed avatar size if image or SVG is used
* filter timesheets by billable state in API
2022-03-18 23:45:41 +01:00
Weblate (bot)
5c925f0ca5 Translated using Weblate (#3194)
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Taplou <ploutarchosgram@gmail.com>
2022-03-18 23:25:22 +01:00
Kevin Papst
30c7782d6e automatic billable calculation (#3200) 2022-03-18 22:31:50 +01:00
Weblate (bot)
f7480902b4 Translated using Weblate (#3181)
Co-authored-by: ButterflyOfFire <ButterflyOfFire@protonmail.com>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Tiger <tigercrl@icloud.com>
2022-03-11 23:50:37 +01:00
Kevin Papst
00b5a65859 1.18.2 (#3190)
* improve console version output
* fix empty string issue in csv export (DDE protection) - fixes #3189
* fix twig sort deprecation in php 8
* sort user by displayName in users report
* fix missing custom translations in edit modal
* fix font-family in invoice.css
* invoice template "freelancer pdf" - added customer number, moved some fields around
* invoice template "default" - relocate customer number and order number in
2022-03-11 23:25:23 +01:00
Kevin Papst
317ac59f47 fix select2 and dropdown width for quick-entry form (#3188) 2022-03-07 14:22:16 +01:00
Kevin Papst
b08c156552 1.18.1 - fix description pattern in title (#3179)
* bump version
* add comment to customer/project/activity entity and collections api 
* improve title pattern usage
2022-02-28 16:32:41 +01:00
Weblate (bot)
2ad7629c2a Translations update from Hosted Weblate (#3177)
* fix broken files containing keys from messages namespace
* pre-fill missing translations with english
* new command to pre-fill empty translations
* fix locale in filename
* activate locales fa and nb_NO

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2022-02-28 16:02:40 +01:00
Kevin Papst
42963ead5c switch to excel cell formats for report exports (#3178) 2022-02-27 13:36:52 +01:00
Kevin Papst
c0168ecc5b 1.18 (#3158)
* bump version
* fix translation ids
* added css classes to modify form with custom css
* improve export pdf file names
* respect financial year in new report
* added new InvoiceCalculator: price
* upgrade packages and node-sass to v7
* title pattern for customer, project and activity via API
* support negative money without currency
* fix sub-locale in print export template
* fix overbooking validation for monthly budget
* fix copying entities with different set of custom-fields compared to the current configuration
2022-02-25 20:41:56 +01:00
Weblate (bot)
34649d809e Translated using Weblate (Croatian) (#3174)
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
2022-02-25 20:40:16 +01:00
Weblate (bot)
ee0f6b0ccf Translations update from Hosted Weblate (#3171)
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
2022-02-25 15:30:37 +01:00
Kevin Papst
c8b47d1033 added explicit classes for widgets (#3170) 2022-02-22 21:15:19 +01:00
Weblate (bot)
3d3ae97af4 Translations update from Hosted Weblate (#3160)
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
2022-02-22 13:29:48 +01:00
fredyb
d7066a2bbc support subtotal in filtered spreadsheet (#3166) 2022-02-22 13:29:03 +01:00
Weblate (bot)
7f8545f1dc Translations update from Hosted Weblate (#3105)
Co-authored-by: Miovio <mgrundemar@gmail.com>
Co-authored-by: ButterflyOfFire <ButterflyOfFire@protonmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2022-02-16 12:57:47 +01:00
Kevin Papst
85a63c4b78 new user-year reporting and data-type chooser (#3155) 2022-02-16 12:18:04 +01:00
Kevin Papst
1852d71974 export user-list reports in excel (#3154) 2022-02-14 17:55:10 +01:00
Kevin Papst
7da7468965 configure display of customer, project and activity in dropdown lists (#3151) 2022-02-14 17:43:05 +01:00
Kevin Papst
83a894823f release 1.17.2 (#3135)
* allow to input password interactively in console
* added block to simplify overwriting export template parts
* prevent installation in PHP 8.1
* composer update
* phpunit version to 9
* support negative amounts in excel export
* added system configuration actions for calendar, weekly timesheet, users
* added method to render text with full markdown support
2022-02-14 17:33:52 +01:00
Kevin Papst
4cb0ac37a6 unify access to custom fields (#3106) 2022-01-28 00:04:58 +01:00
Weblate (bot)
7a6fab8624 Translations update from Hosted Weblate (#3093)
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Bogi Napoleon Wennerstrøm <bogi.wennerstrom@gmail.com>
Co-authored-by: Oliver Darvishi <nimawm81@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
2022-01-25 14:07:30 +01:00
Kevin Papst
6416b67c63 fix admin access for customer in invoice module (#3095) 2022-01-25 14:06:09 +01:00
Kevin Papst
1e269a6f82 improve team member handling (#3097) 2022-01-25 13:44:49 +01:00
Kevin Papst
8694a3bfe4 added invoice delete event (#3096) 2022-01-24 21:33:13 +01:00
Kevin Papst
9e320674c1 phpstan improvements (#3092) 2022-01-23 15:49:38 +01:00
Kevin Papst
9047e9e785 optimize prod error messages (#3091) 2022-01-23 15:29:41 +01:00
Kevin Papst
b8d8f0c16d release 1.17 (#3090) 2022-01-23 01:33:00 +01:00
Kevin Papst
8b8e9b001e bump dependencies (#3089) 2022-01-22 17:11:30 +01:00
Weblate (bot)
9c40bd98aa Translations update from Hosted Weblate (#3048)
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: 黄煜祺 <QQBB18646265284@163.com>
Co-authored-by: Hibiki Kimura <h.kimura787@gmail.com>
Co-authored-by: 좀비때려잡아 <mickey9301@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2022-01-22 13:38:45 +01:00
Kevin Papst
1a0608f49e new export template (#3082) 2022-01-22 13:20:22 +01:00
Kevin Papst
3543e0a1b5 code improvements (#3088) 2022-01-22 02:46:02 +01:00
Kevin Papst
97c23f75dd improve permission handling for quick entry controller (#3081) 2022-01-20 22:24:19 +01:00
Kevin Papst
49bb9980b8 added invoice model to invoice created event (#3079)
* added model to InvoiceCreatedEvent
* allow to switch formatter locale
2022-01-20 18:41:55 +01:00
Kevin Papst
fd4cbb43c1 bugfixes (#3078)
* fix phpdoc
* fix invoice export field order
* prevent empty migration warning
* do not trigger export validation on new timesheets
* update license year
* remove trailing comma in function call for php compatibility
* add new composer plugin config
* bump phpunit schema version
2022-01-20 17:10:56 +01:00
Kevin Papst
40242b7dcc support custom fields for invoices (#3077) 2022-01-20 16:59:26 +01:00
Kevin Papst
9731023014 1.16.10 (#3047) 2022-01-01 18:12:57 +01:00
Weblate (bot)
0abbba7a56 Translations update from Hosted Weblate (#3032)
Co-authored-by: easyjoh <johannes@easyplusplus.net>
Co-authored-by: Dmitry <dmitrydmitry761@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: 拉姆徐 <xulong_715138082@qq.com>
Co-authored-by: Michal Gajewski <gajewmic@gmail.com>
Co-authored-by: DJScias <djscias@gmail.com>
2021-12-30 22:53:01 +01:00
Kevin Papst
cc809183ae added comment field to invoice (#3045) 2021-12-30 21:49:30 +01:00
Kevin Papst
f7b3f4ed76 improve export permission checks (#3027) 2021-12-17 01:10:49 +01:00
Kevin Papst
04fc954769 allow to configure the amount of recent activity rows in an empty week (#3026) 2021-12-16 16:58:10 +01:00
Kevin Papst
3621b8c27c fix invoice budget calculation (#3024) 2021-12-15 17:56:18 +01:00
Kevin Papst
358959522d release 1.16.9 (#3018)
* fix filter action display
* allow to set system configuration during runtime
* bump version
* use deepl pro free to translate missing keys
* replacing broken Github action
2021-12-14 02:29:06 +01:00
Weblate (bot)
19cd5962aa Translations update from Hosted Weblate (#3013)
Co-authored-by: Trần Công Minh <minhhp198x@gmail.com>
2021-12-14 00:10:26 +01:00
Kevin Papst
961336f35c fix project are not filtered after submit (#3016) 2021-12-11 15:51:23 +01:00
Kevin Papst
573fa9f7f3 fix create invoice token issue (#3007) 2021-12-09 01:36:58 +01:00
Kevin Papst
22ce6b047a new invoice template variables for budgets (#3005) 2021-12-08 22:15:07 +01:00
Kevin Papst
82525f382d fix invoice preview (#3002) 2021-12-08 14:21:15 +01:00
Kevin Papst
ec9dd08970 cleanup duplicate translation ids (#3001) 2021-12-08 01:49:44 +01:00
Kevin Papst
9ec0280334 improve translation test and fix error (#2998)
Co-authored-by: Kristoffer Grundström <swedishsailfishosuser@tutanota.com>
2021-12-07 16:56:08 +01:00
Kevin Papst
0861dbd75d new command to work with translation files (#2993) 2021-12-05 14:54:02 +01:00
Kevin Papst
f5949b6b9a use new trustedAuthors flag in CI action (#2992) 2021-12-04 22:46:39 +01:00
Weblate (bot)
99905497e1 Translations update from Hosted Weblate (#2951)
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: BeckeBauer <berko@gmx.de>
Co-authored-by: Bogi Napoleon Wennerstrøm <bogi.wennerstrom@gmail.com>
Co-authored-by: Miovio <mgrundemar@gmail.com>
Co-authored-by: Roman Ondráček <ondracek.roman@centrum.cz>
Co-authored-by: Dronrs <Dutythinking@protonmail.com>
Co-authored-by: Michal Matúšov <kubof.hromoslav@gmail.com>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-12-04 02:37:04 +01:00
Kevin Papst
4ae18b73d1 bump version (#2991) 2021-12-04 02:27:32 +01:00
Kevin Papst
1da26e041d fix invoice create and search (#2990) 2021-12-04 01:18:49 +01:00
Kevin Papst
4e42911f3d more csrf protection for invoice and search (#2984) 2021-12-02 18:01:51 +01:00
Kevin Papst
b0045a910c prevent that lock files will be committed in PRs (#2983) 2021-12-02 13:06:23 +01:00
Kevin Papst
9491f07ade fix deleting invoice documents (#2980) 2021-11-29 15:57:39 +01:00
Kevin Papst
d994339de5 fix two opening form tags (#2972) 2021-11-29 15:49:54 +01:00
Kevin Papst
0464054a2b bump version 2021-11-24 12:55:38 +01:00
Kevin Papst
b062164277 allow to delete invoice documents (#2968) 2021-11-24 10:30:49 +01:00
Kevin Papst
ff9acab0fc improve permissison handling in invoice screen (#2965) 2021-11-21 16:41:03 +01:00
Kevin Papst
76e09447c8 make sure that markdown uses safe mode (#2961) 2021-11-19 23:07:27 +01:00
Kevin Papst
9470699798 escape data in calendar popover (#2960) 2021-11-19 22:57:03 +01:00
Kevin Papst
89bfa82c61 escape customer, project and activity name in javascript (#2959) 2021-11-19 19:45:37 +01:00
Kevin Papst
859131308a improve export filename (#2958) 2021-11-19 18:53:43 +01:00
tdozbun-reno
56d02673ed Update CSRF Token IDs for Issue kevinpapst/kimai2#2947 (#2948)
* bump version
* removed not needed token, as it is already contained in the form
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-11-18 18:32:00 +01:00
Kevin Papst
b28e9c120c version 1.16.2 (#2942)
* bump version
* include calendar week in week chooser
* table names in SQL
* show save flash message
* prevent migration warning
* drop default value to prevent error when server version is not set
* csrf token for duplicate actions
* updated translations
2021-11-18 12:33:13 +01:00
Kevin Papst
c858edf5c9 release 1.16.1 (#2938)
* optional csrf token name
* fix regression when selecting all projects as super user
* prevent invalid sql
2021-11-16 21:48:50 +01:00
Kevin Papst
99ebd1d503 deactivate codecov patch percentage check (#2937) 2021-11-16 12:09:41 +01:00
Kevin Papst
f63a2c8ef6 Release 1.16 (#2929)
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
2021-11-16 10:51:01 +01:00
Kevin Papst
95796ab256 improve csrf handling (#2936) 2021-11-16 10:17:26 +01:00
Kevin Papst
a1992494d3 fetch user preferences via API (#2905) 2021-11-15 21:14:45 +01:00
Kevin Papst
5896ae26c6 improve error handling during invoice generation (#2932)
* prevent that items will be marked as exported if invoice can not be generated
* check for existing invoice number to prevent that invalid invoices will be generated
* allow to add a unique id to invoice preview files on batch export
* hide delete invoice action with deactivated permission
* try to automatically fix duplicate invoice id
2021-11-15 19:21:22 +01:00
Weblate (bot)
8978c181ee Translations update from Weblate (#2915)
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Kevin Papst <kevin@kevinpapst.de>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Anna Gonzalez Mellado <annagm615@gmail.com>
Co-authored-by: Gil <almontegil@gmail.com>
2021-11-15 00:57:59 +01:00
Kevin Papst
ae975d07db submit invoice search after changing the template (#2931) 2021-11-15 00:20:43 +01:00
Kevin Papst
6b49535b52 prevent csrf to flush logs (#2930) 2021-11-14 20:39:22 +01:00
Kevin Papst
8b0962e192 configure am/pm time-format as user preference (#2789) 2021-11-14 18:18:26 +01:00
pkaltenboeck
dce0578a2b new ProjectConstraint to add dynamic project validation (#2747) 2021-11-14 16:56:52 +01:00
Kevin Papst
a483184372 PDF memory optimizations (#2736) 2021-11-14 02:51:52 +01:00
Kevin Papst
6e6c60b29a added new invoice status: canceled (#2922) 2021-11-10 12:37:36 +01:00
Kevin Papst
21dbfdb4f9 do not reset password for LDAP and SAML users or rehash on API calls (#2916) 2021-11-10 11:34:50 +01:00
Kevin Papst
73ab26c7b7 added resname for tool compatibility (#2912) 2021-11-05 14:40:31 +01:00
Weblate (bot)
3e61b60c4e Translations update from Weblate (#2850)
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Anders Johansson <johansson@aljmedia.se>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Roman Shabanov <romash2605@gmail.com>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Artem <Localizer_in_Russian@protonmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: KanI <twinklingnerd@gmail.com>
Co-authored-by: ButterflyOfFire <ButterflyOfFire@protonmail.com>
2021-11-05 14:18:37 +01:00
Kevin Papst
bd2fe32d5a optimizations (#2904)
* calc once, then re-use
* prevent invalid theme switch
* allow to turn off weekly-quick-entries by permissions
* remove 00:00 from date-times that likely do not need a time
* fix datepicker out of window
2021-11-05 10:28:48 +01:00
Kevin Papst
c8854b0775 change data filter on project month report (#2911) 2021-11-05 09:45:12 +01:00
iusgit
042c119058 extend project orderNumber to 50 characters (#2828) 2021-11-03 16:48:35 +01:00
Kevin Papst
b92488e32f include roles and teams in user create form (#2849) 2021-11-03 16:24:22 +01:00
Kevin Papst
2bc76e7adc prevent empty migration warning (#2901) 2021-11-03 15:48:42 +01:00
Kevin Papst
d4278996ae composer upgrade, prevent dbal upgrade, new phpstan version (#2900) 2021-11-03 14:58:10 +01:00
Kevin Papst
eb63dae405 added invoice replacer for currently logged-in user (#2899) 2021-11-03 12:00:52 +01:00
Kevin Papst
1b35356f81 activate bleeding edge phpstan rules (#2898) 2021-11-02 13:33:21 +01:00
Kevin Papst
36c08b0dea fix weekly view day format (#2893)
* make duplicate project action available on details page
* move "weekly hours" form to main menu
* changed label of week chooser
* hide quick entries menu for punch mode users
2021-11-01 20:23:17 +01:00
Kevin Papst
1d32e4ecee use token in invoice delete route (#2889) 2021-10-30 15:17:06 +02:00
Kevin Papst
40061e43bb simplify building theme independent plugins (#2888) 2021-10-30 14:48:05 +02:00
Kevin Papst
7ed1998d04 fixes for new quick-entry week form (#2887) 2021-10-29 21:35:48 +02:00
Kevin Papst
fd2490c135 added workflow to trigger new docker images on release (#2882) 2021-10-28 10:42:53 +02:00
Kevin Papst
9ab098af86 weekly quick-entry form (#2793) 2021-10-18 11:12:46 +02:00
Kevin Papst
64893e0a95 release 1.15.6 (#2863) 2021-10-18 00:30:35 +02:00
Kevin Papst
6fdc304313 improve sunday FDOW handling (#2862) 2021-10-17 19:17:47 +02:00
Kevin Papst
fb265ed732 make sure that minute_increment is not zero (#2860) 2021-10-16 13:38:28 +02:00
Kevin Papst
e2be5b401c improve summary rows in reports (#2861) 2021-10-16 13:38:08 +02:00
Kevin Papst
8539938b8a use new codecov action (#2854) 2021-10-14 17:23:45 +02:00
Kevin Papst
bbf25823b8 release 1.15.5 (#2848) 2021-10-13 18:31:28 +02:00
Kevin Papst
6b066046c6 default value for billable flag and support in batch update (#2851) 2021-10-13 15:49:20 +02:00
Weblate (bot)
9226daa891 Translations update from Weblate (#2823)
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Péter Gyetvai <gyetpet@mailbox.org>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Frank.wu <me@wuzhiping.top>
Co-authored-by: web chigusa <chigusaweb66@gmail.com>
Co-authored-by: KasukeLp <kasukelp23@yahoo.com>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-10-13 14:09:15 +02:00
Kevin Papst
843cd8fb67 refactor system configuration title and wording (#2847) 2021-10-13 13:37:44 +02:00
Kevin Papst
6d7d441d42 fix missing meta fields in duplicate timesheet action (#2845) 2021-10-12 23:41:27 +02:00
Kevin Papst
1c4da30b61 fix version number 2021-10-07 16:42:17 +02:00
Kevin Papst
84e25851d3 set security options on cookies (#2825) 2021-10-07 12:40:06 +02:00
Kevin Papst
3a52daedab fix open budget calculation (#2821) 2021-10-07 11:46:48 +02:00
Kevin Papst
b7cf9cd776 added croatian language (#2817) 2021-10-06 13:41:05 +02:00
Kevin Papst
ccb184b17f Release 1.15.3 (#2813) 2021-10-05 15:30:29 +02:00
Weblate (bot)
162ff07bd5 Translations update from Weblate (#2791)
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Daniel' Ishmaev <dan.samara@gmail.com>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-10-05 15:22:45 +02:00
Kevin Papst
7326a452c3 re-added lock threads app 2021-10-04 17:09:59 +02:00
Kevin Papst
9453947309 export budget, timeBudget and budgetType (#2812) 2021-10-04 16:57:45 +02:00
Kevin Papst
d25653b6ed removed export and system-config subtitles (#2811) 2021-10-04 14:36:17 +02:00
Kevin Papst
ea415e33f5 do not apply rate factor multiple times (#2807) 2021-10-03 17:40:36 +02:00
Kevin Papst
b6430d5622 allow to customize dashboard permissions for latest row (#2806) 2021-10-03 17:17:01 +02:00
Kevin Papst
6fcd50ecdf hide not started projects in widget (#2805) 2021-10-03 13:53:52 +02:00
Kevin Papst
6857d92c4f respect customer visibility in project-overview report (#2804) 2021-10-03 13:41:14 +02:00
Weblate (bot)
4bb9edb2b5 Translations update from Weblate (#2781)
Co-authored-by: frooog in raain <sadsatan211@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Mokhtar Garmehi <Mokhtar_garmeh@yahoo.com>
Co-authored-by: Pham Mina <phamthinhalinh93@gmail.com>
Co-authored-by: Ana Carla Vasconcelos <anacarlavgs@gmail.com>
Co-authored-by: SC <lalocas@protonmail.com>
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: Kevin Papst <kevinpapst@users.noreply.github.com>
2021-09-24 15:16:36 +02:00
Input-BDF
dd881bb856 prevent whitespace on line endings in created docx invoices (#2756) 2021-09-24 15:02:01 +02:00
Kevin Papst
204e0bc179 1.15.2 (#2782)
* fix division by zero
* cleanup required directories, check configured data directory, do not check for freetype support
* bump version number
2021-09-24 14:47:40 +02:00
Guillaume-Duc-95
d026460106 copy the billable status when restarting timesheet (#2778) 2021-09-20 16:23:23 +02:00
Kevin Papst
934a7855b9 remove duplicate statistic events (#2776) 2021-09-18 13:15:23 +02:00
Kevin Papst
54d7c4490f added customer number in invoice export (#2775) 2021-09-18 02:49:47 +02:00
Kevin Papst
fa6af3d148 fix api doc (teams) (#2773) 2021-09-17 14:31:37 +02:00
Kevin Papst
063753b569 improve upgrade docs (for LDAP users in 1.15) (#2772) 2021-09-17 13:42:55 +02:00
Kevin Papst
baff2d78d9 prepare release 1.15 (#2707)
* bump version
* fix invisible class on labels
* fail safe removal of foreign key
* check if optional form field exists before accessing it
* silently ignore stopped timesheets
* prevent colliding parameter names
* make sure decimal duration is always rendered with two decimals
* simplify translation
* fix #2751 ANSI_QUOTES
* rename composer task
* fix billable statistic rates
* added missing translation for export
2021-09-17 00:58:25 +02:00
Weblate (bot)
8f832856a5 Translations update from Weblate (#2739)
Co-authored-by: Sérgio Morais <lalocas@protonmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: Yan Kazachkov <yankazachkov@gmail.com>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: pisquan8 <cimurro@outlook.de>
Co-authored-by: Maamon AlHaqli <mamon321@hotmail.com>
Co-authored-by: Adolfo Jayme Barrientos <fitojb@ubuntu.com>
Co-authored-by: Krishna Chand <krishna_chand67@naver.com>
Co-authored-by: Anders Johansson <johansson@aljmedia.se>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-09-16 17:16:29 +02:00
Kevin Papst
f2eec4f315 API fixes (#2766) 2021-09-16 17:06:32 +02:00
Weblate (bot)
2536105ad0 Translations update from Weblate (#2717)
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Ville Kovacs <regpile@mailbox.org>
Co-authored-by: Simona Iacob <s@zp1.net>
Co-authored-by: Andrea Martini <andrea.martini.7002@gmail.com>
Co-authored-by: Лолло Нуб <lollo.samp@mail.ru>
Co-authored-by: Peter Valo <pvalo@abcom.sk>
Co-authored-by: A M <augim3@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Ana Carla Vasconcelos <anacarlavgs@gmail.com>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-08-26 02:08:50 +02:00
Kevin Papst
98a3fc99a2 store search in session (#2735)
* allow to detect used query filters
* allow to set label in action buttons
* allow to reset last search from session
* migrate invoice archive to new search system
* display number of used search filters
2021-08-26 01:37:40 +02:00
Kevin Papst
56524a9773 data-table sorting icons and alignment (#2734) 2021-08-26 00:56:36 +02:00
Kevin Papst
fae525c82f fix duplicating timesheet if allow overlapping is deactivated (#2732) 2021-08-25 19:31:48 +02:00
Kevin Papst
5023fa6189 prevent email or username from being non-unique (#2730) 2021-08-25 15:18:46 +02:00
Kevin Papst
f743503705 added search modal for timesheet export (#2728) 2021-08-24 19:01:48 +02:00
Kevin Papst
f6a82ba119 fix team edit with deactivated users (#2716) 2021-08-11 01:23:55 +02:00
Weblate (bot)
305d3ba6fe Translations update from Weblate (#2672)
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Victor Cleaner <victor.cleaner@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: edo-2313 <edo2313@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Jack-Benny Persson <jack-benny@cyberinfo.se>
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: Nathan <bonnemainsnathan@gmail.com>
Co-authored-by: dottypottylotty <muzzbuzzer@outlook.com>
Co-authored-by: Adam K <uz@zu.uz>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-08-10 15:14:30 +02:00
Kevin Papst
7873bd812e fix statistics in user profile for first year not starting in january (#2712) 2021-08-10 14:39:52 +02:00
Kevin Papst
b31ded22b9 minor restyling of login screen (#2711) 2021-08-10 14:25:36 +02:00
Toby Batch
f80fe0e4d0 allow to prefill timesheet description by get param (#2580) 2021-08-08 14:29:12 +02:00
Kevin Papst
c063aed867 fixes for 1.15 (#2704) 2021-08-08 12:38:19 +02:00
Kevin Papst
b2d3272151 support multiple teamleads in each team (#2702)
* fix jumping avatars
* fix line-break after color dot for long names
2021-08-07 18:05:41 +02:00
Kevin Papst
e9986c92d6 code cleanup (#2700)
* fix doctrine definition
* do not count api calls as login
* remove unknown form options
* fix annotations
* cleanup deprecations in tests
2021-08-07 01:36:59 +02:00
Kevin Papst
3a7dba437c move supported languages logic to service (#2701) 2021-08-07 01:11:42 +02:00
Kevin Papst
cefd747e91 monthly budget, monthly report, unified report calculation (#2684) 2021-08-06 18:38:41 +02:00
Jelle Sebreghts
21f785638c added calendar configuration event (#2698) 2021-08-06 18:35:37 +02:00
Kevin Papst
df62c8a428 improved play and stop button (#2692)
* improved play and stop button if only one record is allowed
* stop button first in action menu
* fix skin colors
* autofocus term field in search form
* fix responsive column layout in invoice listing
* improve responsive header for small screens
2021-07-30 19:57:57 +02:00
Kevin Papst
37e02365c7 importer supports merge of multiple Kimai 1 installations (#2691) 2021-07-28 14:16:49 +02:00
Kevin Papst
7352c9a394 allow 65535 character in meta fields (#2690) 2021-07-27 19:41:12 +02:00
Kevin Papst
5b8510be56 fix ldap issues due to new security components (#2689) 2021-07-27 19:31:49 +02:00
Kevin Papst
6d5244aa5d LDAP: fixing issues for re-authenticating users (#2681) 2021-07-23 16:12:38 +02:00
Kevin Papst
f1f60042d8 bugfixes (#2669)
* allow storage of multiple .env without git
* clarify requirements
* fail safe statistic queries
* remove unused translations
* reduce customer statistic calls by 50%
2021-07-23 15:15:54 +02:00
Weblate (bot)
6558bcae1d Translations update from Weblate (#2665)
Co-authored-by: Viktor Żakowski <pocomito250@gmail.com>
Co-authored-by: phlostically <phlostically@mailinator.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Tur <tur+translate@simplelogin.fr>
Co-authored-by: Adaś <adam.prosniak@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Eugenia Russell <eugenia.russell2019@gmail.com>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-07-19 01:02:11 +02:00
Kevin Papst
828b9c8b4a added account number field to user (#2671) 2021-07-19 00:13:39 +02:00
Kevin Papst
7d052eba00 make sure that timezone is properly validated (#2663) 2021-07-13 23:23:18 +02:00
Weblate (bot)
43a31efeac updated french (#2661)
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
2021-07-13 16:16:20 +02:00
Kevin Papst
f12f1ae60d bugfixes and improvements (#2660) 2021-07-13 15:51:27 +02:00
Weblate (bot)
9dc94084c1 Translations update from Weblate (#2613)
Co-authored-by: phlostically <phlostically@mailinator.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: Yan Kazachkov <yankazachkov@gmail.com>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-07-13 14:49:46 +02:00
Kevin Papst
03d5d3c70c better readability for drag and drop boxes (#2659) 2021-07-13 12:12:10 +02:00
Kevin Papst
27cea996c0 configurable print export (#2657) 2021-07-13 10:11:20 +02:00
Kevin Papst
7661d43327 improve permission checks for report access (#2658) 2021-07-13 06:21:31 +02:00
Kevin Papst
a1f90ffaba limit amount of items in calendar drag and drop boxes (#2291) 2021-07-11 15:03:18 +02:00
Kevin Papst
2583fdf20e small bugfixes (#2655)
* improve report visibility
* fix timesheet duration, which breaks validation
* added contributing section in README
* do not allow to change project for existing activities - fixes #2576
* bump theme version to fix FOSUserBundle dependency
* use entity color instead of inherited one - fixes #2200
2021-07-10 00:37:03 +02:00
Kevin Papst
9ddb87a6fd added project detail report (#2651)
- avatars via css only
- random colors for entities
- improves print view
- added colors for teams
- added color to tag
2021-07-06 22:01:20 +02:00
Kevin Papst
cc6031bfde invoice number pattern for customer-name and number (#2640) 2021-06-30 17:23:20 +02:00
Kevin Papst
6d196879b4 refactor configurations (#2626)
* composition instead of inheritance
* refactored some twig extensions to runtime extensions
2021-06-26 13:03:54 +02:00
Kevin Papst
3d1fba650b improve color chooser and name validation (#2622) 2021-06-25 16:35:16 +02:00
Kevin Papst
0d5e0b3e80 fix url under certain environments (#2621) 2021-06-25 09:48:38 +02:00
Kevin Papst
7460647d58 added setting to limit the maximum length of a timesheet record (#2612) 2021-06-12 01:05:33 +02:00
Weblate (bot)
53e01177d9 Translations update from Weblate (#2577)
Co-authored-by: Maamon AlHaqli <mamon321@hotmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Tymofij Lytvynenko <till.svit@gmail.com>
Co-authored-by: Adolfo Jayme Barrientos <fitojb@ubuntu.com>
Co-authored-by: Nerdiin <dznissei10@gmail.com>
Co-authored-by: Wellington Terumi Uemura <wellingtonuemura@gmail.com>
Co-authored-by: Richard Mangahas <richard.mangahas.aeom@gmail.com>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: phlostically <phlostically@mailinator.com>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-06-12 00:56:24 +02:00
Kevin Papst
6709ef4c4c removed soft_limit setting (#2611) 2021-06-10 16:01:09 +02:00
Kevin Papst
0b519e9d08 fix datepicker appears out of screen (#2610) 2021-06-10 15:51:26 +02:00
Kevin Papst
7f20cb045c Refactor authentication system (#2602)
Make auth configuration available via UI, remove FOSUserBundle and SAML-Bundle dependency
2021-06-10 15:34:13 +02:00
Kevin Papst
286b63e2c8 allow to configure timezone for the lockdown period (#2593) 2021-06-02 00:05:44 +02:00
Kevin Papst
c1eb5ba90a lint container, workflows and composer update (#2591) 2021-06-01 19:20:12 +02:00
Kevin Papst
4859ac8ecb Support for PHP 8 (#2158)
painful 66 commits later: required PHP version bumped to 7.3, plugin updates using migrations necessary!
2021-05-31 14:53:22 +02:00
Kevin Papst
df6bd30d03 fix calendar totals for empty weeks (#2581) 2021-05-21 15:27:10 +02:00
Kevin Papst
70fd0ae269 show revenues in dashboard to admins only, not teamleads (#2573) 2021-05-16 20:36:43 +02:00
Kevin Papst
dd83872339 new permission to hide user choice in report (#2572) 2021-05-16 00:52:42 +02:00
Kevin Papst
f7aa3c1e13 financial year setting + new users working time per year report (#2547) 2021-05-14 18:40:48 +02:00
dkstiler
a9da5f8476 added greek language (#2568) 2021-05-14 18:04:46 +02:00
Weblate (bot)
06f3a1ecd4 Translations update from Weblate (#2546)
Co-authored-by: Guilherme Dimas <guilhermedimasrosa@gmail.com>
Co-authored-by: dkstiler <dkstiler@gmail.com>
Co-authored-by: P-A Lindkvist <pa.lindkvist@gmail.com>
Co-authored-by: Henrik Dankvardt <dankvardt@gmail.com>
Co-authored-by: 김주열 <quick3377@gmail.com>
Co-authored-by: Nerdiin <dznissei10@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: whenwesober <naomi16i_1298q@cikuh.com>
Co-authored-by: Ido Ido <kobid88923@mxcdd.com>
Co-authored-by: Kevin Papst <kpapst@gmx.net>
2021-05-14 17:06:37 +02:00
Kevin Papst
bed3ade221 Improve progressbar values in report (#2569) 2021-05-14 11:45:03 +02:00
Kevin Papst
f2b32b211b use dialog to duplicate timesheet (#2567)
* fix: display time budget in customer listing if set
* updated api methods with annotations
* do not require checkboxes for system configurations
* open dialog for duplicated timesheets
2021-05-12 22:32:08 +02:00
Kevin Papst
f81f9ba492 added API route to fetch infos about installed plugins (#2561) 2021-05-12 14:03:46 +02:00
Kevin Papst
f10aad927f fix timesheet collection fetch via API with end datetime set (#2552) 2021-05-05 13:02:09 +02:00
Kevin Papst
4fcd97b142 do not include running entries in invoices (#2551) 2021-05-04 12:29:20 +02:00
Kevin Papst
dad1b8b772 version 1.14.1 (#2532)
* no back links in modal pages
* remove unused service links to bountysource and gitter
* add validation for budget and time-budget fields
* display time budget if set
* remove console log
* sanitize DDE payloads
* do not show status and name in version string
2021-04-29 18:29:03 +02:00
Weblate (bot)
22af82cb15 Translations update from Weblate (#2528)
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: phlostically <phlostically@mailinator.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
2021-04-29 12:00:27 +02:00
Kevin Papst
e4b176a4d5 use events for budget calculation (#2544) 2021-04-29 11:50:48 +02:00
3215 changed files with 231074 additions and 121271 deletions

View File

@@ -1,33 +1,30 @@
codecov:
notify:
require_ci_to_pass: yes
notify:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "80...100"
status:
project:
default:
threshold: 0.5%
patch:
default:
threshold: 50%
changes: no
precision: 2
round: down
range: "80...100"
status:
project:
default:
threshold: 0.5%
patch: off
changes: no
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "diff, flags, files"
behavior: default
require_changes: yes
require_base: no
require_head: yes
branches: null
layout: "diff, flags, files"
behavior: default
require_changes: yes
require_base: no
require_head: yes
branches: null

20
.docker/000-default.conf Normal file
View File

@@ -0,0 +1,20 @@
<VirtualHost *:8001>
ServerAdmin webmaster@localhost
DocumentRoot /opt/kimai/public
PassEnv MAILER_FROM
PassEnv APP_ENV
PassEnv APP_SECRET
PassEnv DATABASE_URL
PassEnv MAILER_URL
PassEnv TRUSTED_PROXIES
<Directory "/opt/kimai/public">
Require all granted
DirectoryIndex index.php
AllowOverride All
</Directory>
</VirtualHost>
ServerName localhost

51
.docker/dbtest.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
$DATABASE_HOST = urldecode($argv[1]);
$DATABASE_BASE = urldecode($argv[2]);
$DATABASE_PORT = $argv[3];
$DATABASE_USER = urldecode($argv[4]);
$DATABASE_PASS = urldecode($argv[5]);
echo "Testing DB:";
try {
$pdo = new \PDO("mysql:host=$DATABASE_HOST;dbname=$DATABASE_BASE;port=$DATABASE_PORT", "$DATABASE_USER", "$DATABASE_PASS", [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
]);
} catch(\Exception $ex) {
switch ($ex->getCode()) {
case 1045:
// we can immediately stop here and show the error message
echo 'Access denied (1045)';
die(1);
case 1049:
// error "Unknown database (1049)" can be ignored, the database will be created by Kimai
return;
// a lot of errors share the same meaningless error code zero
case 0:
// this error includes the database name, so we can only search for the static part of the error message
if (stripos($ex->getMessage(), 'SQLSTATE[HY000] [1049] Unknown database') !== false) {
// error "Unknown database (1049)" can be ignored, the database will be created by Kimai
return;
}
switch ($ex->getMessage()) {
// eg. no response (fw) - the startup script should retry it a couple of times
case 'SQLSTATE[HY000] [2002] Operation timed out':
echo 'Operation timed out (0-2002)';
die(4);
// special case "localhost" with a stopped db server (should not happen in docker compose setup)
case 'SQLSTATE[HY000] [2002] No such file or directory':
echo 'Connection could not be established (0-2002)';
die(5);
// using IP with stopped db server - the startup script should retry it a couple of times
case 'SQLSTATE[HY000] [2002] Connection refused':
echo 'Connection refused (0-2002)';
die(5);
}
echo $ex->getMessage() . " (0)";
die(7);
default:
// unknown error
echo $ex->getMessage() . " (?)";
die(10);
}
}

102
.docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,102 @@
#!/bin/bash -x
KIMAI=$(cat /opt/kimai/version.txt)
echo $KIMAI
function waitForDB() {
# Parse sql connection data
DATABASE_USER=$(awk -F '[/:@]' '{print $4}' <<< "$DATABASE_URL")
DATABASE_PASS=$(awk -F '[/:@]' '{print $5}' <<< "$DATABASE_URL")
DATABASE_HOST=$(awk -F '[/:@]' '{print $6}' <<< "$DATABASE_URL")
DATABASE_PORT=$(awk -F '[/:@]' '{print $7}' <<< "$DATABASE_URL")
DATABASE_BASE=$(awk -F '[/?]' '{print $4}' <<< "$DATABASE_URL")
re='^[0-9]+$'
if ! [[ $DATABASE_PORT =~ $re ]] ; then
DATABASE_PORT=3306
fi
echo "Wait for database connection ..."
until php /dbtest.php "$DATABASE_HOST" "$DATABASE_BASE" "$DATABASE_PORT" "$DATABASE_USER" "$DATABASE_PASS"; do
echo Checking DB: $?
sleep 3
done
echo "Connection established"
}
function handleStartup() {
# set mem limits and copy in custom logger config
if [ -z "$memory_limit" ]; then
memory_limit=512M
fi
sed -i "s/memory_limit.*/memory_limit=$memory_limit/g" /usr/local/etc/php/php.ini
cp /assets/monolog.yaml /opt/kimai/config/packages/monolog.yaml
if [ -z "$USER_ID" ]; then
USER_ID=$(id -u www-data)
fi
if [ -z "$GROUP_ID" ]; then
GROUP_ID=$(id -g www-data)
fi
# if group doesn't exist
if grep -w "$GROUP_ID" /etc/group &>/dev/null; then
echo Group already exists
else
echo www-kimai:x:"$GROUP_ID": >> /etc/group
grpconv
fi
# if user doesn't exist
if id "$USER_ID" &>/dev/null; then
echo User already exists
else
echo www-kimai:x:"$USER_ID":"$GROUP_ID":www-kimai:/var/www:/usr/sbin/nologin >> /etc/passwd
pwconv
fi
if [ -e /use_apache ]; then
export APACHE_RUN_USER=$(id -nu "$USER_ID")
# This doesn't _exactly_ run as the specified GID, it runs as the GID of the specified user but WTF
export APACHE_RUN_GROUP=$(id -ng "$USER_ID")
export APACHE_PID_FILE=/var/run/apache2/apache2.pid
export APACHE_RUN_DIR=/var/run/apache2
export APACHE_LOCK_DIR=/var/lock/apache2
export APACHE_LOG_DIR=/var/log/apache2
export LANG=C
elif [ -e /use_fpm ]; then
sed -i "s/user = .*/user = $USER_ID/g" /usr/local/etc/php-fpm.d/www.conf
sed -i "s/group = .*/group = $GROUP_ID/g" /usr/local/etc/php-fpm.d/www.conf
echo "Setting fpm to run as ${USER_ID}:${GROUP_ID}"
else
echo "Error, unknown server type"
fi
}
function prepareKimai() {
# These are idempotent, so we can run them on every start-up
/opt/kimai/bin/console -n kimai:install
if [ ! -z "$ADMINPASS" ] && [ ! -a "$ADMINMAIL" ]; then
/opt/kimai/bin/console kimai:user:create admin "$ADMINMAIL" ROLE_SUPER_ADMIN "$ADMINPASS"
fi
echo "$KIMAI" > /opt/kimai/var/installed
echo "Kimai is ready"
}
function runServer() {
# Just while I'm fixing things
/opt/kimai/bin/console kimai:reload --env="$APP_ENV"
chown -R $USER_ID:$GROUP_ID /opt/kimai/var
if [ -e /use_apache ]; then
exec /usr/sbin/apache2 -D FOREGROUND
elif [ -e /use_fpm ]; then
exec php-fpm
else
echo "Error, unknown server type"
fi
}
waitForDB
handleStartup
prepareKimai
runServer

48
.docker/monolog.yaml Normal file
View File

@@ -0,0 +1,48 @@
when@prod:
monolog:
channels: ["deprecation"]
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [403, 404]
nested:
type: stream
level: info
path: php://stderr
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: stream
channels: ["deprecation"]
path: php://stderr
when@dev:
monolog:
channels: ["deprecation"]
handlers:
main:
type: stream
path: php://stderr
level: info
channels: ["!event"]
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
deprecation:
type: stream
channels: ["deprecation"]
path: php://stderr
when@test:
monolog:
handlers:
main:
type: stream
path: php://stderr
level: info
channels: ["!event"]

View File

@@ -10,6 +10,10 @@ charset = utf-8
indent_style = space
indent_size = 4
[*.twig]
indent_style = space
indent_size = 4
[*.yaml]
indent_style = space
indent_size = 4

View File

@@ -1,30 +1,47 @@
### DATABASE CONFIGURATION
# Replace "user", "password" and "database" with your database connection.
# Configure the server version, MariaDB requires the "mariadb-" prefix, eg:
# for MySQL "serverVersion=5.7" and for MariaDB "serverVersion=mariadb-10.5.8"
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8&serverVersion=5.7
#
# You should NOT use this file in production, but instead move the environment variables to your webserver configuration.
# The .env file is only existing to simplify the initial setup!
#
### EMAIL CONFIGURATION
# Emails will be sent "from":
#================================================================================
# Configure your database connection and set the correct server version.
#
# You have to replace the following values with your defaults:
# - the version "5.7" (the database server version, examples for MySQL and MariaDB below)
# - the database username "user"
# - the password "password" for "user"
# - the database schema "database"
# - you might have to adapt port "3306" and server IP "127.0.0.1" as well
#
# For MySQL that would be "serverVersion=5.7" as in:
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=5.7
#
# For MariaDB it would be "serverVersion=10.5.8-MariaDB":
# DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
#
DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB
#================================================================================
# The full documentation can be found at https://www.kimai.org/documentation/emails.html
#
# Email will be sent with this address as sender:
MAILER_FROM=kimai@example.com
# Email connection (disabled by default with MAILER_URL=null://null)
# SMTP: smtp://localhost:25?encryption=&auth_mode=
# Google: gmail://username:password@default
# Amazon: ses://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1
# Mailchimp: mandrill://KEY@default
# Mailgun: mailgun://KEY:DOMAIN@default
# Postmark: postmark://ID@default
# Sendgrid: sendgrid://KEY@default
# Disable emails: null://null
# Email connection (disabled by default) - see documentation for the format
MAILER_URL=null://null
### APPLICATION CONFIGURATION
#================================================================================
# do not change, unless you are developing for Kimai
APP_ENV=prod
#================================================================================
# should be changed to a unique character sequence, used for hashing cookies
APP_SECRET=change_this_to_something_unique
# Running in a "special" environment, eg. behind reverse proxies?
# Check those:
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
# TRUSTED_HOSTS=localhost,example.com
#================================================================================
# Running behind reverse proxies? Try these:
# TRUSTED_PROXIES=127.0.0.1,127.0.0.2
# TRUSTED_HOSTS=localhost|example.com
#================================================================================
# unlikely, that you need to change this one
CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$

20
.gitattributes vendored Normal file
View File

@@ -0,0 +1,20 @@
.docker export-ignore
.github export-ignore
assets export-ignore
tests export-ignore
.codecov.yml export-ignore
.editorconfig export-ignore
eslint.config.js export-ignore
eslint.config.mjs export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.php-cs-fixer.dist.php export-ignore
php-cs-fixer.dist.php export-ignore
babel.config.js export-ignore
package.json export-ignore
php-cs-fixer.sh export-ignore
phpstan.neon export-ignore
phpstan.sh export-ignore
phpunit.xml.dist export-ignore
webpack.config.js export-ignore
yarn.lock export-ignore

3
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,3 @@
github: [kevinpapst]
custom: ["https://www.kimai.org/donate/", "https://www.paypal.me/kevinpapst"]
open_collective: kimai
custom: ["https://www.kimai.org/", "https://www.kimai.cloud/", "https://www.kevinpapst.de/"]

View File

@@ -1,27 +0,0 @@
---
name: Bug report
about: Found a problem? Create a report to help us improve Kimai
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
1. Go to '...'
2. Click on '....'
3. See error
**Logfile**
```
Add the last lines from your logfile at "var/log/prod.log" or "Doctor > Logs", around the time when the problem happened.
```
**Additional context**
- Kimai version: ?.?
- PHP version: ?.?
- Device: [Ubuntu Laptop 16 inch, Windows Desktop 27 inch, iPhone 6s]
- Browser [e.g. Firefox 81, Chrome 85, Safari 14]

75
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: Bug report
description: Create a report to help us improve Kimai
labels: [ "bug" ]
body:
- type: markdown
attributes:
value: |
Thank you for reporting an issue on Kimai! This form will guide you to create a useful issue report.
- type: textarea
id: what-happened
attributes:
label: Describe the issue
description: "A clear and concise description of the problem you are facing. Is that a browser issue? Then add device information: Ubuntu 20.04 with Brave 1.46, Windows 10 with Chrome 85, iPhone 10, Mac with Safari 16"
placeholder: Tell us what you see! Include the steps necessary to reproduce the behavior (e.g. go to, scroll down and click here ...)
validations:
required: true
- type: checkboxes
id: tried
attributes:
label: I already tried
description: If you didnt try already, try to search the documentation and existing issues what you wrote above.
options:
- label: I've read and searched [the documentation](https://www.kimai.org/documentation/).
required: true
- label: I've searched for similar issues in this repository.
required: true
- label: I've searched for similar issues in [the discussions](https://github.com/kimai/kimai/discussions).
required: true
- type: input
id: version
attributes:
label: Kimai version
description: Which version of Kimai are you running (see Doctor/About/Kimai-Cloud screen)
placeholder: 1.xx.x or 2.xx.x
validations:
required: true
- type: dropdown
id: installation
attributes:
label: How do you run Kimai?
options:
- Virtual Server or alike
- KIMAI CLOUD
- Docker
- Synology
- Plesk
- Shared-Hosting
- Other (please mention below)
validations:
required: true
- type: dropdown
id: php
attributes:
label: Which PHP version are you using?
options:
- "8.5"
- "8.4"
- "8.2"
- "8.3"
- "8.1"
- Unknown
- Other (please mention below)
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logfile
description: Please paste the last lines from your logfile at "var/log/prod.log" or "Doctor > Logs", around the time when the problem happened.
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to better explain your problem.

View File

@@ -1,5 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: "Support / Questions / Discussions"
url: https://github.com/kevinpapst/kimai2/discussions
about: "This forum is for support questions: eg. if you need help with your Kimai installation, want to know how to use a Kimai feature or wonder where to start creating a new Kimai plugin."
- name: Read our documentation
url: https://www.kimai.org/documentation/
about: Save your time! There is an instant solution for many issues in the documentation.
- name: Ask a Question
url: https://github.com/kimai/kimai/discussions
about: Want to discuss something with a community? Do it in discussions!
- name: Get Professional support
url: https://www.kimai.org/en/support.html
about: As a customer, you will always receive fast replies from the developer.

40
.github/ISSUE_TEMPLATE/docker.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Docker issue
description: Experiencing an issue with your Docker Setup?
labels: [ "docker" ]
body:
- type: markdown
attributes:
value: |
Thank you for reporting an issue with your docker setup! This form will guide you to create a useful issue report.
- type: textarea
id: describe
attributes:
label: Describe the problem
description: >
Please provide a clear and concise description of what the issue is and the steps to reproduce the behaviour:
1. Start the container '...'
2. Click on '....'
3. See error xyz
validations:
required: true
- type: textarea
id: environment
attributes:
label: Describe your setup and add your Docker compose file (redact your credentials)
description: >
Your working environment:
- OS: [e.g. Linux, Windows, Mac]
- Docker version: [e.g. 19.03.2]
- Docker compose version: [e.g. 1.21.0]
Docker compose file:
version: '3.5'
services:
image: ...
validations:
required: true
- type: input
id: command
attributes:
label: Command used to run the container
description: e.g. docker run -v ....

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea to make Kimai better
title: ''
labels: feature request
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Screenshot**
Add a mockup / screenshot of your idea here.

View File

@@ -0,0 +1,34 @@
name: Feature request
description: Suggest an idea to make Kimai better
labels: [ "feature request" ]
body:
- type: markdown
attributes:
value: |
Thank you for reporting an issue on Kimai! This form will guide you to create a useful issue report.
- type: textarea
id: describe
attributes:
label: Describe the problem
description: >
Is your feature request related to a problem? If so, please provide
a clear and concise description of what the issue is.
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what the new feature should do.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to better explain your problem.

View File

@@ -9,4 +9,4 @@ A clear and concise description of what this pull request adds or changes.
## Checklist
- [ ] I verified that my code applies to the guidelines (`composer code-check`)
- [ ] I updated the documentation (see [here](https://github.com/kimai/www.kimai.org/tree/master/_documentation))
- [ ] I agree that this code is used in Kimai and will be published under the [MIT license](https://github.com/kevinpapst/kimai2/blob/master/LICENSE)
- [ ] I agree that this code is used in Kimai (see [license](https://github.com/kimai/kimai/blob/main/LICENSE))

24
.github/lock.yml vendored
View File

@@ -1,24 +0,0 @@
# Configuration for lock-threads - https://github.com/dessant/lock-threads
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 60
# Issues and pull requests with these labels will not be locked. Set to `[]` to disable
exemptLabels: []
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: >
This thread has been automatically locked since there has not been
any recent activity after it was closed. Please open a new issue for
related bugs.
If you use Kimai on a daily basis, please [consider donating](https://www.kimai.org/donate/) to
support further development of Kimai.
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: false
# Limit to only `issues` or `pulls`
# only: pulls

29
.github/release-drafter.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name-template: '$RESOLVED_VERSION'
tag-template: '$RESOLVED_VERSION'
exclude-labels:
- 'duplicate'
- 'invalid'
- 'wontfix'
- 'release'
exclude-contributors:
- 'dependabot'
- 'weblate'
change-template: '- $TITLE (#$NUMBER)'
change-title-escapes: '\<*_&`#@'
version-resolver:
minor:
labels:
- 'feature request'
- 'release'
patch:
labels:
- 'technical debt'
- 'bug'
- 'translation'
default: patch
template: |
**Compatible with PHP 8.1 to 8.5**
$CHANGES
Involved in this release: $CONTRIBUTORS

View File

@@ -1,48 +0,0 @@
name: CI
on:
pull_request: null
push:
branches:
- master
jobs:
tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_ALLOW_EMPTY_PASSWORD: false
MYSQL_ROOT_PASSWORD: kimai
MYSQL_DATABASE: kimai
ports:
- 3306/tcp
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
matrix:
php: ['7.3']
name: Coverage - PHP ${{ matrix.php }}
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: pcov
extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap
- run: |
composer install --no-progress
composer require laminas/laminas-ldap
- name: Setup problem matchers for PHPUnit
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Run tests
run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml
env:
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7
APP_ENV: dev
MAILER_URL: null://localhost
TEST_WITH_BUNDLES: 1
- name: Upload code coverage
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml

93
.github/workflows/docker.yaml vendored Normal file
View File

@@ -0,0 +1,93 @@
name: 'Docker Build'
on:
workflow_dispatch:
inputs:
kimai_tag:
description: 'Kimai tag to build'
required: true
release:
types: [released]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install buildx
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v4
with:
username: ${{secrets.DOCKERHUB_USERNAME}}
password: ${{secrets.DOCKERHUB_PASSWORD}}
- name: Determine version
run: |
input="${{ github.event.inputs.kimai_tag }}"
# Determine between manual trigger and release event
if [ -z "$input" ]; then
echo "Using release tag: ${{ github.event.release.tag_name }}"
version="${{ github.event.release.tag_name }}"
else
echo "Using tag provided: $input"
version="$input"
fi
if [[ ! $version =~ ^2\.(0|[1-9]*)(0?)\.(0|[0-9]*)(0?)$ ]]; then
echo "Invalid version number: $version"
exit 1
fi
echo "kimai_version=$version" >> $GITHUB_ENV
- name: FPM image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
build-args: |
KIMAI=${{ env.kimai_version }}
BASE=fpm
target: prod
platforms: linux/amd64,linux/arm64
tags: |
kimai/kimai2:latest
kimai/kimai2:fpm
push: true
- name: Apache image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
build-args: |
KIMAI=${{ env.kimai_version }}
BASE=apache
target: prod
platforms: linux/amd64,linux/arm64
tags: |
kimai/kimai2:stable
kimai/kimai2:apache
kimai/kimai2:apache-${{ env.kimai_version }}
push: true
- name: Development image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
build-args: |
KIMAI=${{ env.kimai_version }}
BASE=apache
target: dev
platforms: linux/amd64,linux/arm64
tags: |
kimai/kimai2:dev
push: true

View File

@@ -1,28 +0,0 @@
name: CI
on:
pull_request: null
push:
branches:
- master
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.4']
name: Linting - PHP ${{ matrix.php }}
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap
tools: cs2pr:1.1.0
- run: composer install --no-progress
- run: composer validate --strict
- run: vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php_cs.dist --using-cache=no --show-progress=none --format=checkstyle | cs2pr
- run: vendor/bin/phpstan analyse src -c phpstan.neon --level=5 --no-progress --error-format=checkstyle | cs2pr
- run: vendor/bin/phpstan analyse tests -c tests/phpstan.neon --level=5 --no-progress --error-format=checkstyle | cs2pr
- run: composer kimai:code-lint

29
.github/workflows/lock-threads.yaml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: 'Lock Threads'
on:
schedule:
- cron: '17 1 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock-threads
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v6
with:
process-only: 'issues, prs'
github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '90'
issue-comment: >
This thread has been automatically locked since there has not been any recent activity after it was closed.
Please share your experience with the community and [leave a testimonial](https://love.kimai.org/) to support Kimai.
issue-lock-reason: 'resolved'
pr-inactive-days: '180'
log-output: true

21
.github/workflows/lockfiles.yaml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Check .lock files
on:
pull_request_target: null
push:
branches:
- main
permissions:
pull-requests: read
jobs:
lockfiles:
runs-on: ubuntu-latest
name: Verify lock file integrity
steps:
- name: Prevent file change
uses: xalvarez/prevent-file-change-action@v3
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
pattern: .*\.lock$|^\.github\/.*$
trustedAuthors: kevinpapst, dependabot

30
.github/workflows/release-drafter.yaml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Release Drafter
on:
push:
branches:
- main
permissions:
contents: read
jobs:
correct_repository:
permissions:
contents: none
runs-on: ubuntu-latest
steps:
- name: fail on fork
if: github.repository_owner != 'kimai'
run: exit 1
update_release_draft:
permissions:
contents: write # for release-drafter/release-drafter to create a github release
pull-requests: read
needs: correct_repository
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v7
env:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,15 +1,15 @@
name: CI
name: Tests
on:
pull_request: null
push:
branches:
- master
- main
jobs:
tests:
integration:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
image: mysql:latest
env:
MYSQL_ALLOW_EMPTY_PASSWORD: false
MYSQL_ROOT_PASSWORD: kimai
@@ -19,39 +19,107 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
matrix:
php: ['7.2', '7.3', '7.4']
php: ['8.1', '8.2', '8.3', '8.4', '8.5']
name: Tests - PHP ${{ matrix.php }}
name: Integration (${{ matrix.php }})
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
- name: Clone Kimai
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
extensions: mbstring, xml, ctype, iconv, intl, mysql, zip, gd, ldap
- run: |
composer install --no-progress
composer require laminas/laminas-ldap
- name: Setup problem matchers for PHPUnit
coverage: pcov
extensions: ctype, gd, iconv, intl, ldap, mbstring, mysql, xml, zip
tools: cs2pr, symfony-cli
env:
fail-fast: true
- name: Determine software versions
run: mysql --version && php --version
- name: Determine composer cache directory
id: composer-cache
run: echo "composer_cache_directory=$(composer config cache-dir)" >> $GITHUB_ENV
- name: Cache Composer dependencies
uses: actions/cache@v5
with:
path: "${{ env.composer_cache_directory }}"
key: ${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install
- name: Validate Composer
run: composer validate --strict --no-check-all
- name: Warmup cache
run: APP_ENV=dev bin/console kimai:reload -n
- name: Check codestyles
run: vendor/bin/php-cs-fixer fix --dry-run --verbose --config=.php-cs-fixer.dist.php --using-cache=no --show-progress=none --format=checkstyle | cs2pr
- name: Run PHPStan for application
run: vendor/bin/phpstan analyse -c phpstan.neon --no-progress --error-format=checkstyle | cs2pr
- name: Run PHPStan for tests
run: vendor/bin/phpstan analyse -c tests/phpstan.neon --no-progress --error-format=checkstyle | cs2pr
- name: Lint codebase
run: composer linting
- name: Install LDAP package (for tests)
run: composer require laminas/laminas-ldap
- name: Setup problem matchers (for PHPUnit)
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Run unit tests
run: composer kimai:tests-unit
- name: Run quick unit-tests
run: composer tests-unit
env:
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7
APP_ENV: dev
MAILER_URL: null://localhost
- name: Run integration tests
run: composer kimai:tests-integration
env:
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
APP_ENV: test
MAILER_URL: null://localhost
- name: Full test-suite
if: matrix.php != '8.5'
run: vendor/bin/phpunit tests/
env:
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
APP_ENV: test
MAILER_URL: null://localhost
- name: Full test-suite with coverage
if: matrix.php == '8.5'
run: vendor/bin/phpunit tests/ --coverage-clover=coverage.xml
env:
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
APP_ENV: dev
MAILER_URL: null://localhost
- name: Upload code coverage
if: matrix.php == '8.5'
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
fail_ci_if_error: true
- name: Run migrations on MySQL
run: |
bin/console doctrine:database:drop --if-exists --force -n
bin/console doctrine:database:create --if-not-exists -n
bin/console doctrine:migrations:migrate -n
bin/console doctrine:migrations:migrate first -n
env:
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?serverVersion=5.7
DATABASE_URL: mysql://root:kimai@127.0.0.1:${{ job.services.mysql.ports['3306'] }}/kimai?charset=utf8mb4&serverVersion=8.0.35
APP_ENV: dev
MAILER_URL: null://localhost
- name: Check for security issues in packages
run: symfony security:check

42
.github/workflows/website.yaml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: 'Website update'
on:
workflow_dispatch:
inputs:
kimai_version:
description: 'Kimai version for the website'
required: true
release:
types: [released]
jobs:
build:
name: Trigger version update for website
runs-on: ubuntu-latest
steps:
- name: "Determine Version"
run: |
input="${{ github.event.inputs.kimai_version }}"
# Determine between manual trigger and release event
if [ -z "$input" ]; then
echo "No input provided, using release tag"
version="${{ github.event.release.tag_name }}"
else
echo "Using input provided: $input"
version="$input"
fi
echo "kimai_version=$version" >> $GITHUB_ENV
if [[ ! $version =~ ^2\.(0|[1-9]*)(0?)\.(0|[0-9]*)(0?)$ ]]; then
echo "Invalid version number: $version"
exit 1
fi
- name: Emit repository_dispatch
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.WEBSITE_ACCESS_TOKEN }}
repository: kimai/www.kimai.org
event-type: kimai_release
client-payload: '{"kimai_version": "${{ env.kimai_version }}"}'

View File

@@ -1,5 +0,0 @@
unreleased=true
future-release=1.14
exclude-labels=duplicate,support,question,invalid,wontfix,release,waiting for feedback
enhancement_labels=>enhancement,Enhancement,feature request,translation i18n,technical debt,documentation
issues-wo-labels=false

64
.gitignore vendored
View File

@@ -1,48 +1,42 @@
# ========== KIMAI ==========
# some hosters require a htaccess to change the PHP version
.htaccess
.idea/
/public/.htaccess
/.env-*
/.idea/
.DS_Store
var/templates/
# custom apache rules e.g. to deactivate ioncube loader
/public/.user.ini
# for symfony local webserver
php.ini
/php.ini
/.php-version
/bin/*
!bin/console
# YARN 2
/.yarnrc.yml
/.yarn
/.pnp.*
# for keeping empty directories
/config/packages/local.yaml
/config/packages/*-local.yaml
/config/packages/*/local.yaml
/config/packages/*/*-local.yaml
/config/bundles-local.php
public/avatars/*.png
templates/invoice/renderer/.~lock*
translations/branding.en.xlf
/var/dev/*
/var/data/*
!/var/data/.gitkeep
/var/coverage/
/var/cache/*
!var/cache/.gitkeep
/var/invoices/*
/var/export/*
/var/packages/*
!var/packages/.gitkeep
/var/plugins/*
!var/plugins/.gitkeep
/var/invoices*
/var/export*
/var/log/*
!var/log/.gitkeep
/var/sessions/*
!var/sessions/.gitkeep
# ========== KIMAI ==========
/var/packages/*
/var/plugins*
/var/plugins/*/*.disabled
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
.env
/public/bundles/
/vendor/
@@ -56,11 +50,11 @@ translations/branding.en.xlf
###> friendsofphp/php-cs-fixer ###
.php_cs
.php_cs.cache
.php-cs-fixer.cache
###< friendsofphp/php-cs-fixer ###
###> symfony/phpunit-bridge ###
.phpunit
/phpunit.xml
###< symfony/phpunit-bridge ###
###> symfony/webpack-encore-bundle ###
@@ -68,4 +62,6 @@ translations/branding.en.xlf
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###
/nbproject/*
/nbproject/*
.dockerhub.secrets

View File

@@ -7,28 +7,35 @@ For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
COMMENT;
return PhpCsFixer\Config::create()
$fixer = new PhpCsFixer\Config();
$fixer->setUnsupportedPhpVersionAllowed(true);
$fixer
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
->setRiskyAllowed(true)
->setRules([
'encoding' => true,
'full_opening_tag' => true,
'blank_line_after_namespace' => true,
'braces' => true,
'control_structure_braces' => true,
'control_structure_continuation_position' => ['position' => 'same_line'],
'declare_parentheses' => true,
'no_multiple_statements_per_line' => true,
'statement_indentation' => true,
'class_definition' => true,
'elseif' => true,
'function_declaration' => true,
'indentation_type' => true,
'line_ending' => true,
'lowercase_constants' => true,
'constant_case' => ['case' => 'lower'],
'lowercase_keywords' => true,
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline', 'after_heredoc' => true],
'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'],
'no_php4_constructor' => true,
'ordered_imports' => true,
'no_break_comment' => true,
'no_closing_tag' => true,
'no_spaces_after_function_name' => true,
'no_spaces_inside_parenthesis' => true,
'spaces_inside_parentheses' => ['space' => 'none'],
'no_trailing_whitespace' => true,
'no_trailing_whitespace_in_comment' => true,
'single_blank_line_at_eof' => true,
@@ -37,6 +44,7 @@ return PhpCsFixer\Config::create()
'single_line_after_imports' => true,
'switch_case_semicolon_to_colon' => true,
'switch_case_space' => true,
'php_unit_method_casing' => true,
'array_syntax' => [
'syntax' => 'short'
],
@@ -46,16 +54,16 @@ return PhpCsFixer\Config::create()
'statements' => ['return'],
],
'cast_spaces' => true,
'class_attributes_separation' => ['elements' => ['method']],
'class_attributes_separation' => ['elements' => ['method' => 'one']],
'concat_space' => ['spacing' => 'one'],
'declare_equal_normalize' => true,
'function_typehint_space' => true,
'type_declaration_spaces' => ['elements' => ['function', 'property']],
'include' => true,
'lowercase_cast' => true,
'lowercase_static_reference' => true,
'magic_constant_casing' => true,
'native_function_casing' => true,
'new_with_braces' => true,
'new_with_parentheses' => true,
'no_blank_lines_after_class_opening' => true,
'no_blank_lines_after_phpdoc' => true,
'no_empty_comment' => true,
@@ -76,9 +84,8 @@ return PhpCsFixer\Config::create()
'no_short_bool_cast' => true,
'no_singleline_whitespace_before_semicolons' => true,
'no_spaces_around_offset' => true,
'no_trailing_comma_in_list_call' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_unneeded_curly_braces' => true,
'no_trailing_comma_in_singleline' => true,
'no_unneeded_braces' => true,
'no_unneeded_final_method' => true,
'no_unused_imports' => true,
'no_whitespace_before_comma_in_array' => true,
@@ -100,7 +107,7 @@ return PhpCsFixer\Config::create()
],
'phpdoc_annotation_without_dot' => true,
'phpdoc_indent' => true,
'phpdoc_inline_tag' => true,
'phpdoc_inline_tag_normalizer' => true,
'phpdoc_no_access' => true,
'phpdoc_no_alias_tag' => true,
'phpdoc_no_empty_return' => false,
@@ -119,7 +126,7 @@ return PhpCsFixer\Config::create()
'return_type_declaration' => true,
'semicolon_after_instruction' => true,
'short_scalar_cast' => true,
'single_blank_line_before_namespace' => true,
'blank_lines_before_namespace' => ['min_line_breaks' => 2, 'max_line_breaks' => 2],
'single_line_comment_style' => [
'comment_types' => ['hash'],
],
@@ -130,13 +137,13 @@ return PhpCsFixer\Config::create()
'standardize_increment' => true,
'standardize_not_equals' => true,
'ternary_operator_spaces' => true,
'trailing_comma_in_multiline_array' => false,
'trailing_comma_in_multiline' => false,
'trim_array_spaces' => true,
'unary_operator_spaces' => true,
'whitespace_after_comma_in_array' => true,
'yoda_style' => false,
'ternary_to_null_coalescing' => true,
'visibility_required' => ['elements' => [
'modifier_keywords' => ['elements' => [
'const',
'method',
'property',
@@ -147,19 +154,31 @@ return PhpCsFixer\Config::create()
],
'scope' => 'namespaced'
],
'native_function_type_declaration_casing' => true,
'native_type_declaration_casing' => true,
'no_alias_functions' => [
'sets' => [
'@internal'
]
],
'list_syntax' => ['syntax' => 'short'],
'assign_null_coalescing_to_coalesce_equal' => true,
'non_printable_character' => true,
'octal_notation' => true,
'heredoc_indentation' => ['indentation' => 'start_plus_one'],
'no_space_around_double_colon' => true,
'no_useless_nullsafe_operator' => true,
'clean_namespace' => true,
'no_unset_cast' => true,
])
->setFinder(
PhpCsFixer\Finder::create()
->in([
__DIR__ . '/migrations/',
__DIR__ . '/src/',
__DIR__ . '/tests/',
])
)
->setFormat('checkstyle')
;
return $fixer;

View File

@@ -1,166 +1,3 @@
# Changelog
## [1.14](https://github.com/kevinpapst/kimai2/tree/1.14)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.13...1.14)
## [1.13](https://github.com/kevinpapst/kimai2/tree/1.13)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.12...1.13)
## [1.12](https://github.com/kevinpapst/kimai2/tree/1.12)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.11.1...1.12)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.12)
## [1.11.1](https://github.com/kevinpapst/kimai2/tree/1.11.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.11...1.11.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.11.1)
## [1.11](https://github.com/kevinpapst/kimai2/tree/1.11)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.10.2...1.11)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.11)
## [1.10.2](https://github.com/kevinpapst/kimai2/tree/1.10.2)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.9...1.10.2)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.10.2)
## [1.10.1](https://github.com/kevinpapst/kimai2/tree/1.10.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.9...1.10.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.10.1)
## [1.10](https://github.com/kevinpapst/kimai2/tree/1.10)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.9...1.10)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.10)
## [1.9](https://github.com/kevinpapst/kimai2/tree/1.9)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.8...1.9)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.9)
## [1.8](https://github.com/kevinpapst/kimai2/tree/1.8)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.7...1.8)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.8)
## [1.7](https://github.com/kevinpapst/kimai2/tree/1.7)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.6.2...1.7)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.7)
## [1.6.2](https://github.com/kevinpapst/kimai2/tree/1.6.2)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.6.1...1.6.2)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.6.2)
## [1.6.1](https://github.com/kevinpapst/kimai2/tree/1.6.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.6...1.6.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.6.1)
## [1.6](https://github.com/kevinpapst/kimai2/tree/1.6)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.5...1.6)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.6)
## [1.5](https://github.com/kevinpapst/kimai2/tree/1.5)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.4.2...1.5)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.5)
## [1.4.2](https://github.com/kevinpapst/kimai2/tree/1.4)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.4.1...1.4.2)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.4.2)
## [1.4.1](https://github.com/kevinpapst/kimai2/tree/1.4)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.4...1.4.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.4.1)
## [1.4](https://github.com/kevinpapst/kimai2/tree/1.4)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.3...1.4)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.4)
## [1.3](https://github.com/kevinpapst/kimai2/tree/1.3)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.2...1.3)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.3)
## [1.2](https://github.com/kevinpapst/kimai2/tree/1.2)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.1...1.2)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.2)
## [1.1](https://github.com/kevinpapst/kimai2/tree/1.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.0.1...1.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.1)
## [1.0.1](https://github.com/kevinpapst/kimai2/tree/1.0.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/1.0...1.0.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.0.1)
## [1.0](https://github.com/kevinpapst/kimai2/tree/1.0)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.9...1.0)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/1.0)
## [0.9](https://github.com/kevinpapst/kimai2/tree/0.9)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.8.1...0.9)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.9)
## [0.8.1](https://github.com/kevinpapst/kimai2/tree/0.8.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.8...0.8.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.8.1)
## [0.8](https://github.com/kevinpapst/kimai2/tree/0.8)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.7...0.8)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.8)
## [0.7](https://github.com/kevinpapst/kimai2/tree/0.7)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.6.1...0.7)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.7)
## [0.6.1](https://github.com/kevinpapst/kimai2/tree/0.6.1)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.6...0.6.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.6.1)
## [0.6](https://github.com/kevinpapst/kimai2/tree/0.6)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.5...0.6)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.6)
## [0.5](https://github.com/kevinpapst/kimai2/tree/0.5)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.4...0.5)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.5)
## [0.4](https://github.com/kevinpapst/kimai2/tree/0.4)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.3...0.4)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.4)
## [0.3](https://github.com/kevinpapst/kimai2/tree/0.3)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.2...0.3)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.3)
## [0.2](https://github.com/kevinpapst/kimai2/tree/0.2)
[Full Changelog](https://github.com/kevinpapst/kimai2/compare/0.1...0.2)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.2)
## [0.1](https://github.com/kevinpapst/kimai2/tree/0.1)
Release notes including the changelog can be found [here](https://github.com/kevinpapst/kimai2/releases/tag/0.1)
See [GitHub release page](https://github.com/kimai/kimai/releases).

View File

@@ -1,26 +1,16 @@
# Contributing
Kimai is an open source project, contributions made by the community are welcome.
Send your ideas, code reviews, pull requests and feature requests to help improving this project.
Kimai is an open source project, contributions made by the community are welcome.
But we can only accept contributions with a signed CLA (Contributor License Agreement) to prevent issues in the future (you will see a link when opening a PR).
Send your ideas, code reviews, pull requests and feature requests to help to improve this project.
## Pull request rules
- Use the [pre-configured codesniffer](.php_cs.dist)) to check for `composer kimai:codestyle` and fix `composer kimai:codestyle-fix` violations
- Add PHPUnit tests for your changes!
- Verify everything still works with `composer kimai:tests-unit` and `composer kimai:tests-integration`
- If you contribute new files, add them with the file-header template from below (the code-style fixer can do that for you)
- When sending in a PR, you must accept that your contributions/code will be published under MIT license (see the [LICENSE](LICENSE) file as well), otherwise your PR will be closed
- If one of the PR checks/builds fails, fix it before asking for a review
- Make your changes in a new git branch, based on the latest code in `main`
- Apply our code-style by running `composer codestyle-fix`
- Run the static code analysis with `composer phpstan`
- Verify everything still works with `composer tests`
- Add tests for your changes
Further documentation can be found in the [developer documentation](https://www.kimai.org/documentation/developers.html).
### File-header template
```
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
```

330
Dockerfile Normal file
View File

@@ -0,0 +1,330 @@
# _ ___ _
# | |/ (_)_ __ ___ __ _(_)
# | ' /| | '_ ` _ \ / _` | |
# | . \| | | | | | | (_| | |
# |_|\_\_|_| |_| |_|\__,_|_|
#
# Kimai images for:
# - plain PHP FPM (kimai/kimai2:fpm)
# - Apache with PHP (kimai/kimai2:apache)
# - Development (kimai/kimai2:dev)
# ---------------------------------------------------------------------
# For local testing by maintainer:
#
# docker build --no-cache -t kimai-fpm --build-arg BASE=fpm .
# docker build --no-cache -t kimai-apache --build-arg BASE=apache .
# docker run -d --name kimai-apache-app kimai-apache
# docker exec -ti kimai-apache-app /bin/bash
# ---------------------------------------------------------------------
# Official PHP images: https://hub.docker.com/_/php/
# https://github.com/docker-library/docs/blob/master/php/README.md#supported-tags-and-respective-dockerfile-links
# Pass-through Arguments: https://benkyriakou.com/posts/docker-args-empty
# Best practices: https://docs.docker.com/build/building/best-practices/
# ---------------------------------------------------------------------
# Source base, one of: fpm, apache
ARG BASE="fpm"
# Kimai branch/tag to run
ARG KIMAI="main"
# Timezone for images
ARG TIMEZONE="Europe/Berlin"
###########################
# Shared tools
###########################
# composer base image
FROM composer:latest AS composer
###########################
# PHP extensions
###########################
# fpm alpine php extension base
FROM php:8.3-fpm-alpine AS fpm-php-ext-base
RUN apk add --no-cache \
# build-tools
autoconf \
dpkg \
dpkg-dev \
file \
g++ \
gcc \
icu-dev \
libatomic \
libc-dev \
libgomp \
libmagic \
m4 \
make \
mpc1 \
mpfr4 \
musl-dev \
perl \
re2c \
# gd
freetype-dev \
libpng-dev \
# icu
icu-dev \
icu-data-full \
# ldap
openldap-dev \
libldap \
# zip
libzip-dev \
# xsl
libxslt-dev
# apache debian php extension base
FROM php:8.3-apache-bookworm AS apache-php-ext-base
RUN apt-get update && \
apt-get install -y \
libldap2-dev \
libicu-dev \
libpng-dev \
libzip-dev \
libxslt1-dev \
libfreetype6-dev
# php extension gd - 13.86s
FROM ${BASE}-php-ext-base AS php-ext-gd
RUN docker-php-ext-configure gd \
--with-freetype && \
docker-php-ext-install -j$(nproc) gd
# php extension intl : 15.26s
FROM ${BASE}-php-ext-base AS php-ext-intl
RUN docker-php-ext-install -j$(nproc) intl
# php extension ldap : 8.45s
FROM ${BASE}-php-ext-base AS php-ext-ldap
RUN docker-php-ext-configure ldap && \
docker-php-ext-install -j$(nproc) ldap
# php extension pdo_mysql : 6.14s
FROM ${BASE}-php-ext-base AS php-ext-pdo_mysql
RUN docker-php-ext-install -j$(nproc) pdo_mysql
# php extension zip : 8.18s
FROM ${BASE}-php-ext-base AS php-ext-zip
RUN docker-php-ext-install -j$(nproc) zip
# php extension xsl : ?.?? s
FROM ${BASE}-php-ext-base AS php-ext-xsl
RUN docker-php-ext-install -j$(nproc) xsl
# php extension opcache
FROM ${BASE}-php-ext-base AS php-ext-opcache
RUN docker-php-ext-install -j$(nproc) opcache
###########################
# fpm base build
###########################
FROM php:8.3-fpm-alpine AS fpm-base
ARG TIMEZONE
RUN apk add --no-cache \
bash \
coreutils \
freetype \
haveged \
icu \
icu-data-full \
libldap \
libpng \
libzip \
libxslt-dev \
fcgi \
tzdata && \
touch /use_fpm && \
sed -i "s/;ping.path/ping.path/g" /usr/local/etc/php-fpm.d/www.conf && \
sed -i "s/;access.suppress_path\[\] = \/ping/access.suppress_path\[\] = \/ping/g" /usr/local/etc/php-fpm.d/www.conf
EXPOSE 9000
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
CMD \
SCRIPT_NAME=/ping \
SCRIPT_FILENAME=/ping \
REQUEST_METHOD=GET \
cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1
###########################
# apache base build
###########################
FROM php:8.3-apache-bookworm AS apache-base
ARG TIMEZONE
RUN apt-get update && \
apt-get install -y \
bash \
haveged \
libicu72 \
libldap-common \
libpng16-16 \
libzip4 \
libxslt1.1 \
libfreetype6 \
unzip && \
echo "Listen 8001" > /etc/apache2/ports.conf && \
a2enmod rewrite && \
touch /use_apache
COPY .docker/000-default.conf /etc/apache2/sites-available/000-default.conf
EXPOSE 8001
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
CMD curl -f http://127.0.0.1:8001 || exit 1
###########################
# global base build
###########################
FROM ${BASE}-base AS php-base
ARG TIMEZONE
ENV TIMEZONE=${TIMEZONE}
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone && \
# make composer home dir
mkdir /composer && \
chown -R www-data:www-data /composer
# copy composer
COPY --from=composer /usr/bin/composer /usr/bin/composer
# copy php extensions
# PHP extension xsl
COPY --from=php-ext-xsl /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini /usr/local/etc/php/conf.d/docker-php-ext-xsl.ini
COPY --from=php-ext-xsl /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xsl.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/xsl.so
# PHP extension pdo_mysql
COPY --from=php-ext-pdo_mysql /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini
COPY --from=php-ext-pdo_mysql /usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so
# PHP extension zip
COPY --from=php-ext-zip /usr/local/etc/php/conf.d/docker-php-ext-zip.ini /usr/local/etc/php/conf.d/docker-php-ext-zip.ini
COPY --from=php-ext-zip /usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so
# PHP extension ldap
COPY --from=php-ext-ldap /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini /usr/local/etc/php/conf.d/docker-php-ext-ldap.ini
COPY --from=php-ext-ldap /usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so
# PHP extension gd
COPY --from=php-ext-gd /usr/local/etc/php/conf.d/docker-php-ext-gd.ini /usr/local/etc/php/conf.d/docker-php-ext-gd.ini
COPY --from=php-ext-gd /usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so
# PHP extension intl
COPY --from=php-ext-intl /usr/local/etc/php/conf.d/docker-php-ext-intl.ini /usr/local/etc/php/conf.d/docker-php-ext-intl.ini
COPY --from=php-ext-intl /usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so /usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so
# PHP extension opcache
COPY --from=php-ext-opcache /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
###########################
# fetch Kimai sources
###########################
FROM alpine:latest AS git-prod
ARG KIMAI
ARG TIMEZONE
# the convention in the Kimai repository is: tags are always version numbers, branch names always start with a letter
# if the KIMAI variable starts with a number (e.g. 2.24.0) we assume its a tag, otherwise its a branch
RUN [[ $KIMAI =~ ^[0-9] ]] && export REF='tags' || export REF='heads' && \
wget -O "/opt/kimai.tar.gz" "https://github.com/kimai/kimai/archive/refs/${REF}/${KIMAI}.tar.gz" && \
tar -xpzf /opt/kimai.tar.gz -C /opt/ && \
mv /opt/kimai-${KIMAI} /opt/kimai
###########################
# global base build
###########################
FROM php-base AS base
ARG KIMAI
ARG TIMEZONE
LABEL org.opencontainers.image.title="Kimai" \
org.opencontainers.image.description="Kimai is a time-tracking application." \
org.opencontainers.image.authors="Kimai Community" \
org.opencontainers.image.url="https://www.kimai.org/" \
org.opencontainers.image.documentation="https://www.kimai.org/documentation/" \
org.opencontainers.image.source="https://github.com/kimai/kimai" \
org.opencontainers.image.version="${KIMAI}" \
org.opencontainers.image.vendor="Kevin Papst" \
org.opencontainers.image.licenses="AGPL-3.0"
ENV KIMAI=${KIMAI}
ENV TIMEZONE=${TIMEZONE}
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone && \
mkdir -p /composer && \
chown -R www-data:www-data /composer
# copy startup script & DB checking script
COPY .docker/dbtest.php /dbtest.php
COPY .docker/entrypoint.sh /entrypoint.sh
ENV DATABASE_URL="mysql://kimai:kimai@127.0.0.1:3306/kimai?charset=utf8mb4&serverVersion=8.3"
ENV APP_SECRET=change_this_to_something_unique
# The default container name for nginx is nginx
ENV TRUSTED_PROXIES=nginx,localhost,127.0.0.1
ENV MAILER_FROM=kimai@example.com
ENV MAILER_URL=null://localhost
ENV ADMINPASS=
ENV ADMINMAIL=
ENV USER_ID=
ENV GROUP_ID=
# default values to configure composer behavior
ENV COMPOSER_MEMORY_LIMIT=-1
ENV COMPOSER_ALLOW_SUPERUSER=1
VOLUME [ "/opt/kimai/var" ]
CMD [ "/entrypoint.sh" ]
###########################
# final builds
###########################
# development build
FROM base AS dev
# copy kimai develop source
COPY --from=git-prod --chown=www-data:www-data /opt/kimai /opt/kimai
COPY .docker /assets
# do the composer deps installation
RUN \
export COMPOSER_HOME=/composer && \
composer --no-ansi install --working-dir=/opt/kimai --optimize-autoloader && \
composer --no-ansi clearcache && \
composer --no-ansi require --working-dir=/opt/kimai laminas/laminas-ldap && \
cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini && \
chown -R www-data:www-data /opt/kimai /usr/local/etc/php/php.ini && \
mkdir -p /opt/kimai/var/logs && chmod 777 /opt/kimai/var/logs && \
sed "s/128M/-1/g" /usr/local/etc/php/php.ini-development > /opt/kimai/php-cli.ini && \
sed -i "s/env php/env -S php -c \/opt\/kimai\/php-cli.ini/g" /opt/kimai/bin/console && \
/opt/kimai/bin/console kimai:version | awk '{print $2}' > /opt/kimai/version.txt
ENV APP_ENV=dev
ENV DATABASE_URL=
ENV memory_limit=512M
# the "prod" stage (production build) is configured as last stage in the file, as this is the default target in BuildKit
FROM base AS prod
# copy kimai production source
COPY --from=git-prod --chown=www-data:www-data /opt/kimai /opt/kimai
COPY .docker /assets
# do the composer deps installation
RUN \
export COMPOSER_HOME=/composer && \
composer --no-ansi install --working-dir=/opt/kimai --no-dev --optimize-autoloader && \
composer --no-ansi clearcache && \
composer --no-ansi require --update-no-dev --working-dir=/opt/kimai laminas/laminas-ldap && \
cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini && \
sed -i "s/expose_php = On/expose_php = Off/g" /usr/local/etc/php/php.ini && \
sed -i "s/;opcache.enable=1/opcache.enable=1/g" /usr/local/etc/php/php.ini && \
sed -i "s/;opcache.memory_consumption=128/opcache.memory_consumption=256/g" /usr/local/etc/php/php.ini && \
sed -i "s/;opcache.interned_strings_buffer=8/opcache.interned_strings_buffer=24/g" /usr/local/etc/php/php.ini && \
sed -i "s/;opcache.max_accelerated_files=10000/opcache.max_accelerated_files=100000/g" /usr/local/etc/php/php.ini && \
sed -i "s/opcache.validate_timestamps=1/opcache.validate_timestamps=0/g" /usr/local/etc/php/php.ini && \
sed -i "s/session.gc_maxlifetime = 1440/session.gc_maxlifetime = 604800/g" /usr/local/etc/php/php.ini && \
mkdir -p /opt/kimai/var/logs && chmod 777 /opt/kimai/var/logs && \
sed "s/128M/-1/g" /usr/local/etc/php/php.ini-development > /opt/kimai/php-cli.ini && \
chown -R www-data:www-data /opt/kimai /usr/local/etc/php/php.ini && \
/opt/kimai/bin/console kimai:version | awk '{print $2}' > /opt/kimai/version.txt
ENV APP_ENV=prod
ENV DATABASE_URL=
ENV memory_limit=512M

674
LICENSE
View File

@@ -1,21 +1,661 @@
MIT License
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (c) 2017-2019 Kevin Papst @ https://www.kevinpapst.de
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Preamble
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

116
README.md
View File

@@ -1,79 +1,93 @@
<p align="center">
<img src="https://raw.githubusercontent.com/kimai/images/master/repository-header.png" alt="Kimai logo">
<img src="https://raw.githubusercontent.com/kimai/images/main/repository-header.png" alt="Kimai logo">
</p>
<p align="center">
<a href="https://github.com/kevinpapst/kimai2/actions"><img alt="CI Status" src="https://github.com/kevinpapst/kimai2/workflows/CI/badge.svg"></a>
<a href="https://codecov.io/gh/kevinpapst/kimai2"><img alt="Code Coverage" src="https://codecov.io/gh/kevinpapst/kimai2/branch/master/graph/badge.svg"></a>
<a href="https://packagist.org/packages/kevinpapst/kimai2"><img alt="Latest stable version" src="https://poser.pugx.org/kevinpapst/kimai2/v/stable"></a>
<a href="https://packagist.org/packages/kevinpapst/kimai2"><img alt="License" src="https://poser.pugx.org/kevinpapst/kimai2/license"></a>
<a href="https://gitter.im/kimai2/support"><img alt="Gitter" src="https://badges.gitter.im/kimai2/support.svg"></a>
<a href="https://www.bountysource.com/teams/kimai2"><img alt="Bountysource" src="https://img.shields.io/bountysource/team/kimai2/activity"></a>
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/actions/workflows/testing.yaml/badge.svg"></a>
<a href="https://codecov.io/gh/kimai/kimai"><img alt="Code Coverage" src="https://codecov.io/gh/kimai/kimai/branch/main/graph/badge.svg"></a>
<a href="https://packagist.org/packages/kimai/kimai"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a>
</p>
<h1 align="center">Kimai - time-tracker</h1>
<h1 align="center">Kimai<br>#1 Open-Source Time-Tracker</h1>
Kimai is a free, open source and online time-tracking software designed for small businesses and freelancers.
It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Doctrine, AdminLTE, Webpack, ES6 etc.
Kimai is a professional grade time-tracking application, free and open-source.
It handles use-cases of freelancers as well as companies with dozens or hundreds of users.
Kimai was build to track your project times and ships with many advanced features, including but not limited to:
## Introduction
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user - multi-timezones - multi-language ([over 30 translations existing](https://hosted.weblate.org/projects/kimai/)!),
authentication via SAML/LDAP/Database, two-factor authentication (2FA) with TOTP, customizable role and team permissions, responsive design,
user/customer/project specific rates, advanced search & filtering, money and time budgets, advanced reporting, support for [plugins](https://www.kimai.org/store/)
and so much more.
- [Home](https://www.kimai.org) - Kimai project homepage
- [Blog](https://www.kimai.org/blog/) - Read the latest news
- [Documentation](https://www.kimai.org/documentation/) - Learn how to use Kimai
- [Translations](https://hosted.weblate.org/projects/kimai/#languages) - Kimai in your language
- [Migration](https://www.kimai.org/documentation/migration-v1.html) - Import data from Kimai 1
### Links
- [Home](https://www.kimai.org) — Kimai project homepage
- [Blog](https://www.kimai.org/blog/) — Read the latest news
- [Documentation](https://www.kimai.org/documentation/) — Learn how to use Kimai
### Requirements
- PHP 7.2.9 or higher ([PHP 8 not yet](https://github.com/kevinpapst/kimai2/pull/2158))
- MariaDB or MySQL
- Webserver (nginx, Apache)
- Some PHP extensions, see [composer.json](composer.json) or [here](https://www.kimai.org/download/)
### About
This is the new version of the open source time tracker Kimai. It is stable and production ready, ships
with many advanced features, including but not limited to:
JSON API, invoicing, data exports, multi-timer and punch-in punch-out mode, tagging, multi-user and multi-timezones,
authentication via SAML/LDAP/Database, customizable role and team permissions, responsive and ready for your mobile device,
user specific rates, advanced search & filtering, money and time budgets, multiple reports, support for plugins and many more.
- PHP 8.1.3 minimum with support for 8.2, 8.3, 8.4, 8.5
- MariaDB / MySQL: oldest maintained LTS release (MariaDB >= [10.6](https://endoflife.date/mariadb) or MySQL >= [8.4](https://endoflife.date/mysql)) or newer
- A webserver and subdomain (subdirectory is not supported)
- PHP extensions: `gd`, `intl`, `json`, `mbstring`, `pdo`, `tokenizer`, `xml`, `xsl`, `zip`
## Installation
- [Recommended setup](https://www.kimai.org/documentation/installation.html#recommended-setup) - with Git and Composer
- [Docker](https://www.kimai.org/documentation/docker.html) - containerized
- [Development](https://www.kimai.org/documentation/installation.html#development-installation) - on your local machine
- [1-click installer](https://www.kimai.org/documentation/installation.html#hosting-and-1-click-installations) - hosted environments
- Caddy with Docker-Compose at [Hetzner](https://www.kimai.org/documentation/hosting-hetzner-cloud.html) and [DigitalOcean](https://www.kimai.org/documentation/hosting-digital-ocean.html)
- [SSH setup](https://www.kimai.org/documentation/installation.html) with Git and Composer
- [Docker images](https://hub.docker.com/r/kimai/kimai2) with FPM only or incl. Apache
- [Synology](https://www.kimai.org/documentation/synology.html) user can host the Docker version
- [Developer setups](https://www.kimai.org/documentation/developers.html) if you want to create Kimai integrations
There are more documented ways for [on-premise hosting](https://www.kimai.org/documentation/chapter-on-premise.html).
And if you don't want to host Kimai, you can use the [Cloud version](https://www.kimai.cloud/) of it.
### Updating Kimai
- [Update Kimai](https://www.kimai.org/documentation/updates.html) - get the latest version
- [UPGRADING guide](UPGRADING.md) - version specific steps
- [Update Kimai](https://www.kimai.org/documentation/updates.html) get the latest version
- [UPGRADING guide](UPGRADING.md) version specific steps
### Plugins
- [Plugin marketplace](https://www.kimai.org/store/) - find existing plugins here
- [Developer documentation](https://www.kimai.org/documentation/developers.html) - how to create a plugin
- [Plugins](https://www.kimai.org/store/) — paid and free plugin marketplace
- [Developer documentation](https://www.kimai.org/documentation/developers.html) how to create a plugin
## Roadmap and releases
You can see a rough development roadmap in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections.
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome.
You can see a rough development [roadmap](https://github.com/orgs/kimai/projects/2), which is open for changes and input from the community, your [ideas](https://github.com/kimai/kimai/issues) are welcome.
> Kimai 2 uses a rolling release concept for delivering updates.
> You can upgrade Kimai at any time, you don't need to wait for the next official release.
> The master branch is always deployable, release tags are only snapshots of the current development version.
Release versions will be created on a regular basis, every couple of weeks latest.
Every code change, whether it's a new feature or a bugfix, will be done on the `main` branch.
Release versions will be created on a regular base (approx. one release every 4-8 weeks).
Every code change, whether it's a new feature or a bugfix, will be done on the master branch.
Kimai is actively developed in my spare time, I put my effort into the software instead of backporting changes.
## Contributing
## Credits
You want to contribute to this repository? This is so great!
The best way to start is to [open a new issue](https://github.com/kimai/kimai/issues) for bugs or feature requests or a [discussion](https://github.com/kimai/kimai/discussions) for questions, support and such.
In case you want to contribute, but you wouldn't know how, here are some suggestions:
- Spread the word: Please [write a testimonial for our Wall of love](https://love.kimai.org), vote for Kimai on any software platform, you can toot or tweet about it, share it on LinkedIn, Reddit and any other social media platform!
- [Translate Kimai into your language](https://hosted.weblate.org/engage/kimai/), or help to improve the existing translations, many languages look for a contributor
- Answer questions: You know the answer to another user's problem? Share your knowledge.
- Something can be done better? An essential feature is missing? Create a feature request.
- Report bugs makes Kimai better for everyone.
- You don't have to be programmer, the documentation and translation could always use some attention.
- Sponsor the project: free software costs money to create!
There is one simple rule in our "Code of conduct": Don't be an ass!
## Follow Kimai
- Mastodon: [@kimai](https://phpc.social/@kimai)
- Youtube: [@kimai_org](https://www.youtube.com/@kimai_org)
- LinkedIn: [@kimai-org](https://www.linkedin.com/company/kimai-org/)
### Credits
Kimai is based on modern technologies and frameworks such as [PHP](https://www.php.net/),
[Symfony](https://github.com/symfony/symfony) and [Doctrine](https://github.com/doctrine/),
[Bootstrap](https://github.com/twbs/bootstrap) and [Tabler](https://tabler.io/),
and [countless](composer.json) [others](package.json).
Kimai 2 is developed with modern frameworks like
[Symfony v4](https://github.com/symfony/symfony),
[Doctrine](https://github.com/doctrine/),
[AdminLTEBundle](https://github.com/kevinpapst/AdminLTEBundle/) (based on [AdminLTE theme](https://github.com/almasaeed2010/AdminLTE)) and
[many](composer.json) [more](package.json).

View File

@@ -1,23 +1,10 @@
# Security Policy
## Supported Versions
As announced in the [README](README.md) security fixes will only be added to the `main` branch.
As announced in the [README](README.md) I only support the latest available release and master.
| Version | Supported |
|----------------------|--------------------|
| main branch | :white_check_mark: |
| older releases | :x: |
| Version | Supported |
| ------- | ------------------ |
| master | :white_check_mark: |
| 1.14 | :white_check_mark: |
| < 1.14 | :x: |
## Reporting a Vulnerability
Please report any security related vulnerability in the [advisories section at GitHub](https://github.com/kevinpapst/kimai2/security/advisories) or via email to info@keleo.de or kpapst@gmx.net.
I will work as fast as I can to fix the problem and publish a bugfix release / security update.
Depending on the size of the required fixes, this might take a couple of hours or a couple of days.
You can expect that your message will be answered ASAP, but please take into account that I am living in the timezone CET.
If your issue is valid and after I verified and fixed it, you will be mentioned in the release notes.
I am grateful for any (discrete) disclosure of vulnerabilities!
You find all information in our [Bughunter documentation](https://www.kimai.org/documentation/bughunter.html).

368
UPGRADING-1.md Normal file
View File

@@ -0,0 +1,368 @@
# Upgrading Kimai - Version 1.x
_Make sure to create a backup before you start!_
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how
you can upgrade your Kimai installation to the latest stable release.
Check below if there are more version specific steps required, which need to be executed after the normal update process.
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
## [1.16](https://github.com/kimai/kimai/releases/tag/1.16)
If you are using MariaDB, it must be at least version 10.1, older versions are not supported any longer.
**DEVELOPER**
- Removed `formDateTime` field from API model `I18nConfig`
- Upgraded to Doctrine DBAL 3, see [docs](https://github.com/doctrine/dbal/blob/3.1.x/UPGRADE.md) - you might have to update your bundle
## [1.15](https://github.com/kimai/kimai/releases/tag/1.15)
**Many database changes: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
Updating the database might take quite a while, depending on the amount of timesheet entries and speed of your database server (~1 minute per 100k records).
**ATTENTION**
- This release bumps the minimum required [PHP version to 7.3](https://www.kimai.org/blog/2021/php8-support-php72-dropped/)
- Self-registration is disabled by default
- Self-registration now always requires email confirmation
- All plugins that use own databases need to be updated as well
- Removed the YearChart widget and the related configs named `userRecapThisYear`, `userRecapLastYear`, `userRecapTwoYears`, `userRecapThreeYears`
**LDAP & SAML**
Please verify your config with the [LDAP](https://www.kimai.org/documentation/ldap.html) and [SAML](https://www.kimai.org/documentation/saml.html) documentation, especially:
- SAML users: activate it by setting the `kimai.saml.activate: true` config key
- LDAP users: activate it by setting the `kimai.ldap.activate: true` config key
- LDAP and SAML users need to remove the complete `security` section from their `local.yaml`
**DEVELOPER**
PHP 8 compatibility forced to upgrade MANY libraries, including but not limited to:
- Removed FOSUserBundle and hslavich/oneloginsaml
- Doctrine Migrations, whose new major version forces plugin updates
- Gedmo v3 (which include BC breaks in definitions)
- Doctrine DBAL and others, which required PHP 7.3 as well
**API BC break**: Due to team structure changes, it was impossible to keep the (writing) API structure. Please adjust your code accordingly!
## [1.14](https://github.com/kimai/kimai/releases/tag/1.14)
**CRITICAL BC break**: SQLite support was removed. If you are using SQLite, you have to [read this blog post](https://www.kimai.org/blog/2021/sqlite-and-ftp-support-removed/) and migrate to MySQL/MariaDB first!
**New database tables and fields: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
Permission changes:
- `history_invoice` - removed permission entirely
## [1.13](https://github.com/kimai/kimai/releases/tag/1.13)
- Deprecated `now` variable in export templates: create it yourself with `{% set now = create_date('now', app.user) %}`
- Changed invoice filename generation (check if you use cronjob for invoices)
- **BC break**: duration entered as plain numbers will now be treated as decimal duration in hours instead of seconds
## [1.12](https://github.com/kimai/kimai/releases/tag/1.12)
- Export templates can now include items from plugins (eg. Expenses).
## [1.10](https://github.com/kimai/kimai/releases/tag/1.10)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
- Invoice renderer `CSV` was removed
- Sessions are now stored in the database (all users have to re-login after upgrade)
- New permissions: `lockdown_grace_timesheet`, `lockdown_override_timesheet`, `view_all_data`
- Fixed team permissions on user queries: depending on your previous team & permission setup your users might see less data (SUPER_ADMINS see all data, but new: ADMINS only see all data if they own the `view_all_data` permission)
- Markdown does not support headings anymore, text like `# foo` is not converted to `<h1 id="foo">foo</h1>` anymore
### Developer
- **BC break**: removed registration of `.env` with `putenv()` - do not rely on `getenv()` as it is not thread-safe
- **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf()` changed
- **BC break**: the macros `badge` and `label` do not apply the `|trans` filter any more
- **BC Break**: removed `getVisible()` (deprecated since 1.4) method on Customer, Project and Activity (use `isVisible()` instead, templates are still working)
- **BC Break**: API changes
- some representation names changed (eg. from `ActivityMetaField` to `ActivityMeta`, `TimesheetSubCollection` vs `TimesheetCollectionExpanded`), you could use `class_alias()` if you use auto-generated code from Swagger-Gen or alike
- new result types were introduced
- result data changed in some areas to smooth out inconsistencies (eg. TeamEntity fields changed in nested results)
## [1.9](https://github.com/kimai/kimai/releases/tag/1.9)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
- The directory `var/data/invoices/` will be used to store archived invoice files (check file permissions)
- The default invoice number format changed. If you want to use the old one: configure `{date}` as format - see [invoice documentation](https://www.kimai.org/documentation/invoices.html)
- HTML invoice templates are now treated like other files and offered as download. If you are using relative URLs for including assets (CSS, images) you need to either inline them (see the default templates) or use absolute URLs
- Invoice templates that use the templates variables `${activity.X}` or `${project.X}` should be checked and possibly adapted, as multi-select is now possible for filtering
- Invoice templates have access to all meta-fields as variables, not only the ones marked as visible
- Rates configuration/structure changed for customer, project and activity
- **Invoice templates**: rates variables were removed
Permission changes:
- `history_invoice` - NEW: grants all features for the new invoice archive (by default for all admins)
### Developer
- **BC break**: `InvoiceItemInterface` has new methods `getType()` and `getCategory()`
- **BC break**: API fields changed - see new `/rates` endpoints
## [1.8](https://github.com/kimai/kimai/releases/tag/1.8)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
- New PHP requirement: `ext-xsl` - which should be pre-installed in most environments when `ext-xml` is loaded
- New mailer library: check if emails are still working (eg. by using the "password forgotten" function) or if you need to adjust your configuration, [see docs at symfony.com](https://symfony.com/doc/current/components/mailer.html#transport)
- Support for line breaks in multiline invoice fields for spreadsheets (check your invoice templates after the update)
Permission changes:
- `comments_create_customer` - NEW: permission that allows to add new comments for customers
- `comments_create_team_customer` - NEW: permission that allows to add new comments for team members of the current customer
- `comments_create_teamlead_customer` - NEW: permission that allows to add new comments for a teamlead of the current customer
- `comments_create_project` - NEW: permission that allows to add new comments for project
- `comments_create_team_project` - NEW: permission that allows to add new comments for team members of the current project
- `comments_create_teamlead_project` - NEW: permission that allows to add new comments for a teamlead of the current project
- `edit_teamlead_project` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
- `edit_teamlead_customer` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
- `upload_invoice_template` - NEW: permission that allows to upload invoice documents from the UI
## [1.7](https://github.com/kimai/kimai/releases/tag/1.7)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
New permissions:
- `comments_customer` - show comment list on customer detail page (new feature)
- `details_customer` - show detail information for customers (customer number, vat, rates, meta-fields, assigned teams ...)
- `comments_project` - show comment list on project detail page (new feature)
- `details_project` - show detail information for projects (rates, meta-fields, assigned teams ...)
If you are using teams, please read on: The following list of permissions are now also available in the UI and they (can) replace the `X_project` and `X_customer` permissions.
They are more strict, as they allow only access to team specific items, the older permissions without `_teamlead_`/`_team_` work on a global level instead.
- `view_teamlead_customer`, `edit_teamlead_customer`, `budget_teamlead_customer`, `permissions_teamlead_customer`, `comments_teamlead_customer`, `details_teamlead_customer` - allows access to customer data when user is teamlead of a team assigned to the customer (replaces more global permission like `view_customer` for teamleads)
- `view_team_customer`, `edit_team_customer`, `budget_team_customer`, `comments_team_customer`, `details_team_customer` - allows access to customer data when user is member of a team assigned to the customer (replaces more global permission like `view_customer` for users)
- `view_teamlead_project`, `edit_teamlead_project`, `budget_teamlead_project`, `permissions_teamlead_project`, `comments_teamlead_project`, `details_teamlead_project` - allows access to customer data when user is teamlead of a team assigned to the project (replaces more global permission like `view_project` for teamleads)
- `view_team_project`, `edit_team_project`, `budget_team_project`, `comments_team_project`, `details_team_project` - allows access to customer data when user is member of a team assigned to the project (replaces more global permission like `view_project` for users)
### ExpenseBundle
**ATTENTION** due to incompatibilities in the underlying frameworks users of the ExpenseBundle need to do one more step:
You need to delete the bundle before updating: `rm -r var/plugins/ExpenseBundle`, otherwise you will run into errors during the update.
After the Kimai update was successful, you have to re-install the latest bundle version, which is compatible with Kimai 1.7 only.
### Developer
- Projects now have a start and end date and the API will only return those, which are either unconfigured or currently active, you might want to reload the list of projects once the user entered begin and end datetime OR use the new `ignoreDates` parameter.
- Doctrine bundle was updated to v2, check your code for [the usage of RegistryInterface and ObjectManager](https://github.com/doctrine/DoctrineBundle/blob/master/UPGRADE-2.0.md)
- Removed the webserver bundle and the command `server:run` - see [docs](https://www.kimai.org/documentation/developers.html)
## [1.6](https://github.com/kimai/kimai/releases/tag/1.6), [1.6.1](https://github.com/kimai/kimai/releases/tag/1.6.1), [1.6.2](https://github.com/kimai/kimai/releases/tag/1.6.2)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
- Invoice changes:
- Moved CSV, ODS and XSLX invoice templates to [another repository](https://github.com/Keleo/kimai2-invoice-templates). Using them? Install them manually (see [invoice documentation](https://www.kimai.org/documentation/invoices.html)).
- Added new invoice fields (VAT, contact, payment details) and customer field (VAT). Used the twig settings before? Move them to the respective invoice template settings.
- Permissions can be managed via Admin UI. Please move your permission settings from [local.yaml to your database](https://www.kimai.org/documentation/permissions.html).
- Important permission change: regular users with the `view_other_timesheet` permission could see all timesheets. This was a legacy from the time before team permissions were introduced. If you rely on this behavior, you need to create a team with all users and the teamlead being the user who needs access to all timesheets.
### Developer
Please add default permissions to your [plugin](https://www.kimai.org/documentation/plugins.html).
## [1.5](https://github.com/kimai/kimai/releases/tag/1.5)
[Update as usual](https://www.kimai.org/documentation/updates.html)
## [1.4](https://github.com/kimai/kimai/releases/tag/1.4)
**There is a new directory, which needs to be writable by the webserver: `public/avatars/`.**
New permission (used in new dashboard widget):
- `view_team_member` - display team assignments (names, teamleads and members) for the current user
Activated Javascript select component by default (check mobile devices).
### Developer: BC breaks
- Dashboard widgets and rows need to define their `type` by FQCN
- Switched to Symfony 4.3 event types, this could fail in plugins, but only if they didn't use the official constants for event names
## [1.3](https://github.com/kimai/kimai/releases/tag/1.3)
Added `manage_tag` permission for new tag features
### Developer: BC breaks
- Refactored toolbars and search, plugins needs to be checked
- Invoices now supports multiple repositories, some method signatures had to be changed (eg. `calculateSumIdentifier()`)
## [1.2](https://github.com/kimai/kimai/releases/tag/1.2)
**If you are still using 0.7 or below, you need to upgrade to 1.1 before upgrading to this version.**
- Deleted timezone conversion command.
- Minimum password length raised from 5 to 8 character (applies only for password changes and new users)
- Maximum customer name length lowered to 150 character
- Maximum project name length lowered to 150 character
- Maximum activity name length lowered to 150 character
- Added new permission: `manage_invoice_template`
- Removed permissions: `view_invoice_template`, `create_invoice_template`, `edit_invoice_template`, `delete_invoice_template`
- Removed permission: `view_export` (using `create_export` only)
### Developer: BC breaks
- Custom export renderer need to check for usage of `Timesheet::getEnd()` as running entries can now be exported as well
## [1.1](https://github.com/kimai/kimai/releases/tag/1.1)
[Update as usual](https://www.kimai.org/documentation/updates.html), nothing special for this release if you upgrade from 1.0 / 1.0.1.
## [1.0](https://github.com/kimai/kimai/releases/tag/1.0)
This release contains several changes, as I still have the goal to stabilize the code base to prevent
such "challenges" after 1.0 for a while.
### Changes for your local.yaml
New permissions are available. You have to add them to your `local.yaml` ONLY if you use a custom permission structure,
otherwise you can't use the new features:
- `view_tag` - view all tags
- `delete_tag` - delete tags
- `edit_exported_timesheet` - allows to edit records which were exported
- `role_permissions` - view calculated permissions for user roles
- `budget_activity` - view and edit budgets for activities
- `budget_project` - view and edit budgets for projects
- `budget_customer` - view and edit budgets for customers
Removed permission:
- `system_actions` - removed experimental feature to flush app cache from the about screen
### BC BREAKS
- API: Format for queries including a datetime object fixed to use HTML5 format (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00`)
- **Permission config**: the `permissions` definition in your `local.yaml` needs to be verified/changed, as the internal structure was highly optimized to simplify the definition.
Thanks to the new structure, you should be able to remove almost everything from your `local.yaml` (tip: start over from scratch!). Please read [the updated permission docu](https://www.kimai.org/documentation/permissions.html).
- default widgets were removed from `kimai.yaml`, that shouldn't cause any issues ... but if something is odd: [look here for help](https://www.kimai.org/documentation/dashboard.html)
## [0.9](https://github.com/kimai/kimai/releases/tag/0.9)
Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
### BC BREAKS
This release contains some BC breaks which were necessary before 1.0 will be released (_now or never_), to prevent those BC breaks after 1.0.
- **Kimai requires PHP 7.2 now => [PHP 7.1 expired 4 month ago](https://www.php.net/supported-versions.php)**
- The `.env` variable `DATABASE_PREFIX` was removed and the table prefix is now hardcoded to `kimai2_`. If you used another prefix,
you have to rename your tables manually before starting the update process. You can delete the row `DATABASE_PREFIX` from your `.env` file.
- API: Format for DateTime objects changed, now including timezone identifier (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00+00:00`), see [#718](https://github.com/kimai/kimai/pull/718)
- API: changed from snake_case to camelCase (affected fields: hourlyRate vs hourly_rate / fixedRate vs fixed_rate / orderNumber vs order_number / i18n config object)
- Plugin mechanism changed: existing Plugins have to be deleted or updated
### Apply necessary changes to your `local.yaml`:
New permissions are available:
- `system_configuration` - for accessing the new system configuration screen
- `system_actions` - for the experimental feature to flush your cache from the about screen
- `plugins` - for accessing the new plugins screen
The setting `kimai.timesheet.mode` replaces the setting `kimai.timesheet.duration_only`. If you used the duration_only mode, you need to change your config:
```yaml
# Before
kimai:
timesheet:
duration_only: true
# After
kimai:
timesheet:
mode: duration_only
```
Or switch the mode directly in the new System configuration screen within Kimai.
## [0.8.1](https://github.com/kimai/kimai/releases/tag/0.8.1)
A bug fixing release. Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
## [0.8](https://github.com/kimai/kimai/releases/tag/0.8)
After you followed the normal update and database migration process (see above), you need to execute a bash command to convert your timesheet data for timezone support:
- Read this [pull request](https://github.com/kimai/kimai/pull/372) BEFORE you follow the instructions to convert the
timezones in your existing time records with `bin/console kimai:convert-timezone`. Without that, you will end up with wrong times in your database.
### Apply necessary changes to your `local.yaml`:
- A new boolean setting `kimai.timesheet.rules.allow_future_times` was introduced
- New permissions are available:
- `view_export` - for the new export feature
- `create_export` - for the new export feature
- `edit_export_own_timesheet` - for the new export feature
- `edit_export_other_timesheet` - for the new export feature
- `system_information` - to see the new about screen
## [0.7](https://github.com/kimai/kimai/releases/tag/0.7)
The configuration `kimai.theme.active_warning` was deprecated and should be replaced in your local.yaml,
[read config docs for more information](https://www.kimai.org/documentation/timesheet.html#limit-active-entries).
## [0.6.1](https://github.com/kimai/kimai/releases/tag/0.6.1)
A bugfix release to address database compatibility issues with older MySQL/MariaDB versions.
## [0.6](https://github.com/kimai/kimai/releases/tag/0.6)
The API has some minor BC breaks: some fields were renamed and entities have a larger attribute set than collections.
Be aware that the API is still is development mode and shouldn't be considered stable for now.
## [0.5](https://github.com/kimai/kimai/releases/tag/0.5)
Some configuration nodes were removed, if you have one of them in your `local.yaml` you need to delete them before you start the update:
- `kimai.invoice.calculator`
- `kimai.invoice.renderer`
- `kimai.invoice.number_generator`
The new config `kimai.invoice.documents` was introduced, holding a list of directories ([read more](https://www.kimai.org/documentation/invoices.html)).
**BC break:** InvoiceTemplate name was changed from 255 characters to 60. If you used longer invoice-template names, they will be truncated when upgrading the database.
Please make sure that they are unique in the first 60 character before you upgrade your database with `doctrine:migrations:migrate`.
## [0.4](https://github.com/kimai/kimai/releases/tag/0.4)
In the time between 0.3 and 0.4 there was a release of composer that introduced a BC break,
which leads to problems between Composer and Symfony Flex, resulting in an error like this when running it:
```
[ErrorException]
Declaration of Symfony\Flex\ParallelDownloader::getRemoteContents($originUrl, $fileUrl, $context) should be compatible with Composer\Util\RemoteFilesystem::getRemoteContents($originUrl, $fileUrl, $context, ?array &$responseHeaders = NULL)
```
This can be fixed by updating Composer and Flex before executing the Kimai update:
```
sudo composer self-update
sudo -u www-data composer update symfony/flex --no-plugins --no-scripts
```
## [0.3](https://github.com/kimai/kimai/releases/tag/0.3)
You need to adjust your `.env` file and add your `from` address for [all emails](https://www.kimai.org/documentation/emails.html) generated by Kimai 2:
```
MAILER_FROM=kimai@example.com
```
Create a file and database backup before executing the following steps:
```bash
git pull origin master
sudo -u www-data composer install --no-dev --optimize-autoloader
sudo -u www-data bin/console cache:clear --env=prod
sudo -u www-data bin/console cache:warmup --env=prod
bin/console doctrine:migrations:version --add 20180701120000
bin/console doctrine:migrations:migrate
```

26
UPGRADING-3.md Normal file
View File

@@ -0,0 +1,26 @@
# Upgrading Kimai - Version 3.x
_Make sure to create a backup before you start!_
Read the [updates documentation](https://www.kimai.org/documentation/updates.html) to find out how you can upgrade your Kimai installation to the latest stable release.
Check below if there are more version specific steps required, which need to be executed after the normal update process.
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
## 3.0
**!! This release requires minimum PHP version 8.4 !!**
### Developer
Do not use method chaining: all fluent interface, especially in Entities, are no longer supported.
Removed translations:
- `action.edit`: use `edit` instead
- `my.profile`: use `user_profile` instead
- `stats.userAmountToday`: use `` instead
- `stats.userAmountWeek`: use `` instead
- `stats.userAmountMonth`: use `` instead
- `stats.userAmountYear`: use `` instead
- `stats.userAmountTotal`: use `` instead
- `update_multiple`

View File

@@ -1,4 +1,4 @@
# Upgrading Kimai 2
# Upgrading Kimai - Version 2.x
_Make sure to create a backup before you start!_
@@ -8,319 +8,43 @@ you can upgrade your Kimai installation to the latest stable release.
Check below if there are more version specific steps required, which need to be executed after the normal update process.
Perform EACH version specific task between your version and the new one, otherwise you risk data inconsistency or a broken installation.
## [1.14](https://github.com/kevinpapst/kimai2/releases/tag/1.13)
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
**CRITICAL BC break**: SQLite support was removed. If you are using SQLite, you have to [read this blog post](https://www.kimai.org/blog/2021/sqlite-and-ftp-support-removed/) and migrate to MySQL/MariaDB first!
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
**New database tables and fields: don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
Permission changes:
- `history_invoice` - removed permission entirely
**!! This release requires minimum PHP version to 8.1 !!**
## [1.13](https://github.com/kevinpapst/kimai2/releases/tag/1.13)
### Breaking changes
- Deprecated `now` variable in export templates: create it yourself with `{% set now = create_date('now', app.user) %}`
- Changed invoice filename generation (check if you use cronjob for invoices)
- **BC break**: duration entered as plain numbers will now be treated as decimal duration in hours instead of seconds
## [1.12](https://github.com/kevinpapst/kimai2/releases/tag/1.12)
- Export templates can now include items from plugins (eg. Expenses).
## [1.10](https://github.com/kevinpapst/kimai2/releases/tag/1.10)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
- Invoice renderer `CSV` was removed
- Sessions are now stored in the database (all users have to re-login after upgrade)
- New permissions: `lockdown_grace_timesheet`, `lockdown_override_timesheet`, `view_all_data`
- Fixed team permissions on user queries: depending on your previous team & permission setup your users might see less data (SUPER_ADMINS see all data, but new: ADMINS only see all data if they own the `view_all_data` permission)
- Markdown does not support headings anymore, text like `# foo` is not converted to `<h1 id="foo">foo</h1>` anymore
- All plugins need to be updated: delete all previous version from your installation (`rm -r var/plugins/*`) before updating!
- The `local.yaml` is not compatible with old version, remove it before the update and then re-create it after everything works
- removed: configuring the `dashboard` is not supported any longer
- removed: custom translation files via `theme.branding.translation`
- removed: changing the plugin directory via `kimai.plugin_dir`
### Developer
- **BC break**: removed registration of `.env` with `putenv()` - do not rely on `getenv()` as it is not thread-safe
- **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf()` changed
- **BC break**: the macros `badge` and `label` do not apply the `|trans` filter any more
- **BC Break**: removed `getVisible()` (deprecated since 1.4) method on Customer, Project and Activity (use `isVisible()` instead, templates are still working)
- **BC Break**: API changes
- some representation names changed (eg. from `ActivityMetaField` to `ActivityMeta`, `TimesheetSubCollection` vs `TimesheetCollectionExpanded`), you could use `class_alias()` if you use auto-generated code from Swagger-Gen or alike
- new result types were introduced
- result data changed in some areas to smooth out inconsistencies (eg. TeamEntity fields changed in nested results)
## [1.9](https://github.com/kevinpapst/kimai2/releases/tag/1.9)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
- The directory `var/data/invoices/` will be used to store archived invoice files (check file permissions)
- The default invoice number format changed. If you want to use the old one: configure `{date}` as format - see [invoice documentation](https://www.kimai.org/documentation/invoices.html)
- HTML invoice templates are now treated like other files and offered as download. If you are using relative URLs for including assets (CSS, images) you need to either inline them (see the default templates) or use absolute URLs
- Invoice templates that use the templates variables `${activity.X}` or `${project.X}` should be checked and possibly adapted, as multi-select is now possible for filtering
- Invoice templates have access to all meta-fields as variables, not only the ones marked as visible
- Rates configuration/structure changed for customer, project and activity
- **Invoice templates**: rates variables were removed
Permission changes:
- `history_invoice` - NEW: grants all features for the new invoice archive (by default for all admins)
### Developer
- **BC break**: `InvoiceItemInterface` has new methods `getType()` and `getCategory()`
- **BC break**: API fields changed - see new `/rates` endpoints
## [1.8](https://github.com/kevinpapst/kimai2/releases/tag/1.8)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
- New PHP requirement: `ext-xsl` - which should be pre-installed in most environments when `ext-xml` is loaded
- New mailer library: check if emails are still working (eg. by using the "password forgotten" function) or if you need to adjust your configuration, [see docs at symfony.com](https://symfony.com/doc/current/components/mailer.html#transport)
- Support for line breaks in multiline invoice fields for spreadsheets (check your invoice templates after the update)
Permission changes:
- `comments_create_customer` - NEW: permission that allows to add new comments for customers
- `comments_create_team_customer` - NEW: permission that allows to add new comments for team members of the current customer
- `comments_create_teamlead_customer` - NEW: permission that allows to add new comments for a teamlead of the current customer
- `comments_create_project` - NEW: permission that allows to add new comments for project
- `comments_create_team_project` - NEW: permission that allows to add new comments for team members of the current project
- `comments_create_teamlead_project` - NEW: permission that allows to add new comments for a teamlead of the current project
- `edit_teamlead_project` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
- `edit_teamlead_customer` - removed default permission from ROLE_TEAMLEAD (if you use it: change it in the Role & Permission UI)
- `upload_invoice_template` - NEW: permission that allows to upload invoice documents from the UI
## [1.7](https://github.com/kevinpapst/kimai2/releases/tag/1.7)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
New permissions:
- `comments_customer` - show comment list on customer detail page (new feature)
- `details_customer` - show detail information for customers (customer number, vat, rates, meta-fields, assigned teams ...)
- `comments_project` - show comment list on project detail page (new feature)
- `details_project` - show detail information for projects (rates, meta-fields, assigned teams ...)
If you are using teams, please read on: The following list of permissions are now also available in the UI and they (can) replace the `X_project` and `X_customer` permissions.
They are more strict, as they allow only access to team specific items, the older permissions without `_teamlead_`/`_team_` work on a global level instead.
- `view_teamlead_customer`, `edit_teamlead_customer`, `budget_teamlead_customer`, `permissions_teamlead_customer`, `comments_teamlead_customer`, `details_teamlead_customer` - allows access to customer data when user is teamlead of a team assigned to the customer (replaces more global permission like `view_customer` for teamleads)
- `view_team_customer`, `edit_team_customer`, `budget_team_customer`, `comments_team_customer`, `details_team_customer` - allows access to customer data when user is member of a team assigned to the customer (replaces more global permission like `view_customer` for users)
- `view_teamlead_project`, `edit_teamlead_project`, `budget_teamlead_project`, `permissions_teamlead_project`, `comments_teamlead_project`, `details_teamlead_project` - allows access to customer data when user is teamlead of a team assigned to the project (replaces more global permission like `view_project` for teamleads)
- `view_team_project`, `edit_team_project`, `budget_team_project`, `comments_team_project`, `details_team_project` - allows access to customer data when user is member of a team assigned to the project (replaces more global permission like `view_project` for users)
### ExpenseBundle
**ATTENTION** due to incompatibilities in the underlying frameworks users of the ExpenseBundle need to do one more step:
You need to delete the bundle before updating: `rm -r var/plugins/ExpenseBundle`, otherwise you will run into errors during the update.
After the Kimai update was successful, you have to re-install the latest bundle version, which is compatible with Kimai 1.7 only.
### Developer
- Projects now have a start and end date and the API will only return those, which are either unconfigured or currently active, you might want to reload the list of projects once the user entered begin and end datetime OR use the new `ignoreDates` parameter.
- Doctrine bundle was updated to v2, check your code for [the usage of RegistryInterface and ObjectManager](https://github.com/doctrine/DoctrineBundle/blob/master/UPGRADE-2.0.md)
- Removed the webserver bundle and the command `server:run` - see [docs](https://www.kimai.org/documentation/developers.html)
## [1.6](https://github.com/kevinpapst/kimai2/releases/tag/1.6), [1.6.1](https://github.com/kevinpapst/kimai2/releases/tag/1.6.1), [1.6.2](https://github.com/kevinpapst/kimai2/releases/tag/1.6.2)
**New database tables and fields were created, don't forget to [run the updater](https://www.kimai.org/documentation/updates.html).**
- Invoice changes:
- Moved CSV, ODS and XSLX invoice templates to [another repository](https://github.com/Keleo/kimai2-invoice-templates). Using them? Install them manually (see [invoice documentation](https://www.kimai.org/documentation/invoices.html)).
- Added new invoice fields (VAT, contact, payment details) and customer field (VAT). Used the twig settings before? Move them to the respective invoice template settings.
- Permissions can be managed via Admin UI. Please move your permission settings from [local.yaml to your database](https://www.kimai.org/documentation/permissions.html).
- Important permission change: regular users with the `view_other_timesheet` permission could see all timesheets. This was a legacy from the time before team permissions were introduced. If you rely on this behavior, you need to create a team with all users and the teamlead being the user who needs access to all timesheets.
### Developer
Please add default permissions to your [plugin](https://www.kimai.org/documentation/plugins.html).
## [1.5](https://github.com/kevinpapst/kimai2/releases/tag/1.5)
[Update as usual](https://www.kimai.org/documentation/updates.html)
## [1.4](https://github.com/kevinpapst/kimai2/releases/tag/1.4)
**There is a new directory, which needs to be writable by the webserver: `public/avatars/`.**
New permission (used in new dashboard widget):
- `view_team_member` - display team assignments (names, teamleads and members) for the current user
Activated Javascript select component by default (check mobile devices).
### Developer: BC breaks
- Dashboard widgets and rows need to define their `type` by FQCN
- Switched to Symfony 4.3 event types, this could fail in plugins, but only if they didn't use the official constants for event names
## [1.3](https://github.com/kevinpapst/kimai2/releases/tag/1.3)
Added `manage_tag` permission for new tag features
### Developer: BC breaks
- Refactored toolbars and search, plugins needs to be checked
- Invoices now supports multiple repositories, some method signatures had to be changed (eg. `calculateSumIdentifier()`)
## [1.2](https://github.com/kevinpapst/kimai2/releases/tag/1.2)
**If you are still using 0.7 or below, you need to upgrade to 1.1 before upgrading to this version.**
- Deleted timezone conversion command.
- Minimum password length raised from 5 to 8 character (applies only for password changes and new users)
- Maximum customer name length lowered to 150 character
- Maximum project name length lowered to 150 character
- Maximum activity name length lowered to 150 character
- Added new permission: `manage_invoice_template`
- Removed permissions: `view_invoice_template`, `create_invoice_template`, `edit_invoice_template`, `delete_invoice_template`
- Removed permission: `view_export` (using `create_export` only)
### Developer: BC breaks
- Custom export renderer need to check for usage of `Timesheet::getEnd()` as running entries can now be exported as well
## [1.1](https://github.com/kevinpapst/kimai2/releases/tag/1.1)
[Update as usual](https://www.kimai.org/documentation/updates.html), nothing special for this release if you upgrade from 1.0 / 1.0.1.
## [1.0](https://github.com/kevinpapst/kimai2/releases/tag/1.0)
This release contains several changes, as I still have the goal to stabilize the code base to prevent
such "challenges" after 1.0 for a while.
### Changes for your local.yaml
New permissions are available. You have to add them to your `local.yaml` ONLY if you use a custom permission structure,
otherwise you can't use the new features:
- `view_tag` - view all tags
- `delete_tag` - delete tags
- `edit_exported_timesheet` - allows to edit records which were exported
- `role_permissions` - view calculated permissions for user roles
- `budget_activity` - view and edit budgets for activities
- `budget_project` - view and edit budgets for projects
- `budget_customer` - view and edit budgets for customers
Removed permission:
- `system_actions` - removed experimental feature to flush app cache from the about screen
### BC BREAKS
- API: Format for queries including a datetime object fixed to use HTML5 format (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00`)
- **Permission config**: the `permissions` definition in your `local.yaml` needs to be verified/changed, as the internal structure was highly optimized to simplify the definition.
Thanks to the new structure, you should be able to remove almost everything from your `local.yaml` (tip: start over from scratch!). Please read [the updated permission docu](https://www.kimai.org/documentation/permissions.html).
- default widgets were removed from `kimai.yaml`, that shouldn't cause any issues ... but if something is odd: [look here for help](https://www.kimai.org/documentation/dashboard.html)
## [0.9](https://github.com/kevinpapst/kimai2/releases/tag/0.9)
Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
### BC BREAKS
This release contains some BC breaks which were necessary before 1.0 will be released (_now or never_), to prevent those BC breaks after 1.0.
- **Kimai requires PHP 7.2 now => [PHP 7.1 expired 4 month ago](https://www.php.net/supported-versions.php)**
- The `.env` variable `DATABASE_PREFIX` was removed and the table prefix is now hardcoded to `kimai2_`. If you used another prefix,
you have to rename your tables manually before starting the update process. You can delete the row `DATABASE_PREFIX` from your `.env` file.
- API: Format for DateTime objects changed, now including timezone identifier (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00+00:00`), see [#718](https://github.com/kevinpapst/kimai2/pull/718)
- API: changed from snake_case to camelCase (affected fields: hourlyRate vs hourly_rate / fixedRate vs fixed_rate / orderNumber vs order_number / i18n config object)
- Plugin mechanism changed: existing Plugins have to be deleted or updated
### Apply necessary changes to your `local.yaml`:
New permissions are available:
- `system_configuration` - for accessing the new system configuration screen
- `system_actions` - for the experimental feature to flush your cache from the about screen
- `plugins` - for accessing the new plugins screen
The setting `kimai.timesheet.mode` replaces the setting `kimai.timesheet.duration_only`. If you used the duration_only mode, you need to change your config:
```yaml
# Before
kimai:
timesheet:
duration_only: true
# After
kimai:
timesheet:
mode: duration_only
```
Or switch the mode directly in the new System configuration screen within Kimai.
## [0.8.1](https://github.com/kevinpapst/kimai2/releases/tag/0.8.1)
A bug fixing release. Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
## [0.8](https://github.com/kevinpapst/kimai2/releases/tag/0.8)
After you followed the normal update and database migration process (see above), you need to execute a bash command to convert your timesheet data for timezone support:
- Read this [pull request](https://github.com/kevinpapst/kimai2/pull/372) BEFORE you follow the instructions to convert the
timezones in your existing time records with `bin/console kimai:convert-timezone`. Without that, you will end up with wrong times in your database.
### Apply necessary changes to your `local.yaml`:
- A new boolean setting `kimai.timesheet.rules.allow_future_times` was introduced
- New permissions are available:
- `view_export` - for the new export feature
- `create_export` - for the new export feature
- `edit_export_own_timesheet` - for the new export feature
- `edit_export_other_timesheet` - for the new export feature
- `system_information` - to see the new about screen
## [0.7](https://github.com/kevinpapst/kimai2/releases/tag/0.7)
The configuration `kimai.theme.active_warning` was deprecated and should be replaced in your local.yaml,
[read config docs for more information](https://www.kimai.org/documentation/timesheet.html#limit-active-entries).
## [0.6.1](https://github.com/kevinpapst/kimai2/releases/tag/0.6.1)
A bugfix release to address database compatibility issues with older MySQL/MariaDB versions.
## [0.6](https://github.com/kevinpapst/kimai2/releases/tag/0.6)
The API has some minor BC breaks: some fields were renamed and entities have a larger attribute set than collections.
Be aware that the API is still is development mode and shouldn't be considered stable for now.
## [0.5](https://github.com/kevinpapst/kimai2/releases/tag/0.5)
Some configuration nodes were removed, if you have one of them in your `local.yaml` you need to delete them before you start the update:
- `kimai.invoice.calculator`
- `kimai.invoice.renderer`
- `kimai.invoice.number_generator`
The new config `kimai.invoice.documents` was introduced, holding a list of directories ([read more](https://www.kimai.org/documentation/invoices.html)).
**BC break:** InvoiceTemplate name was changed from 255 characters to 60. If you used longer invoice-template names, they will be truncated when upgrading the database.
Please make sure that they are unique in the first 60 character before you upgrade your database with `doctrine:migrations:migrate`.
## [0.4](https://github.com/kevinpapst/kimai2/releases/tag/0.4)
In the time between 0.3 and 0.4 there was a release of composer that introduced a BC break,
which leads to problems between Composer and Symfony Flex, resulting in an error like this when running it:
```
[ErrorException]
Declaration of Symfony\Flex\ParallelDownloader::getRemoteContents($originUrl, $fileUrl, $context) should be compatible with Composer\Util\RemoteFilesystem::getRemoteContents($originUrl, $fileUrl, $context, ?array &$responseHeaders = NULL)
```
This can be fixed by updating Composer and Flex before executing the Kimai update:
```
sudo composer self-update
sudo -u www-data composer update symfony/flex --no-plugins --no-scripts
```
## [0.3](https://github.com/kevinpapst/kimai2/releases/tag/0.3)
You need to adjust your `.env` file and add your `from` address for [all emails](https://www.kimai.org/documentation/emails.html) generated by Kimai 2:
```
MAILER_FROM=kimai@example.com
```
Create a file and database backup before executing the following steps:
```bash
git pull origin master
sudo -u www-data composer install --no-dev --optimize-autoloader
sudo -u www-data bin/console cache:clear --env=prod
sudo -u www-data bin/console cache:warmup --env=prod
bin/console doctrine:migrations:version --add 20180701120000
bin/console doctrine:migrations:migrate
```
Developer read the full documentation at [https://www.kimai.org/documentation/migration-v2.html](https://www.kimai.org/documentation/migration-v2.html).
- Invoice renderer and templates for XML, JSON and TEXT were moved to the [Extended invoicing plugin](https://www.kimai.org/store/invoice-bundle.html) (install if you use one of those)
- Moved `company.docx` to [external repo](https://github.com/kimai/invoice-templates/tree/main/docx-company) (needs to be re-uploaded if you want to keep on using it!)
- Role names are forced to be uppercase
- Removed unused `public/avatars/` directory
- Time-tracking mode `duration_only` was removed, existing installations will be switched to `duration_fixed_begin`
- Removed Twig filters. You might have to replace them in your custom export/invoice templates:
- `date_full` => `date_time`
- `duration_decimal` => `duration(true)`
- `currency` => `currency_name`
- `country` => `country_name`
- `language` => `language_name`
- Removed support for custom translation files (use [TranslationBundle](https://www.kimai.org/store/translation-bundle.html) instead or write your own plugin)
- Removed all 3rd party mailer packages, you need to install them manually (ONLY if you used a short syntax to configure the `MAILER_URL` in `.env`):
- `composer require symfony/amazon-mailer`
- `composer require symfony/google-mailer`
- `composer require symfony/mailchimp-mailer`
- `composer require symfony/mailgun-mailer`
- `composer require symfony/postmark-mailer`
- `composer require symfony/sendgrid-mailer`

0
assets/.gitignore vendored
View File

1
assets/app-rtl.js Normal file
View File

@@ -0,0 +1 @@
require('./sass/_app-rtl.scss');

View File

@@ -1,102 +1,9 @@
// ------------------- INLINED ADMIN-LTE DEFINITIONS -------------------
// require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte');
// this was replaced to save around 300kb by:
// - removing moment locales which are not used
// - removing fullcalendar locales which are not used
// - removing icheck which is not used
// - removing jquery-ui which is not used
const $ = require('jquery');
global.$ = global.jQuery = $;
require('bootstrap-sass');
require('jquery-slimscroll');
require('select2');
require('select2/dist/js/i18n/ar');
require('select2/dist/js/i18n/cs');
require('select2/dist/js/i18n/da');
require('select2/dist/js/i18n/de');
require('select2/dist/js/i18n/es');
require('select2/dist/js/i18n/eu');
require('select2/dist/js/i18n/fi');
require('select2/dist/js/i18n/fr');
require('select2/dist/js/i18n/he');
require('select2/dist/js/i18n/hu');
require('select2/dist/js/i18n/it');
require('select2/dist/js/i18n/ja');
require('select2/dist/js/i18n/ko');
require('select2/dist/js/i18n/nl');
require('select2/dist/js/i18n/pl');
require('select2/dist/js/i18n/pt');
require('select2/dist/js/i18n/pt-BR');
require('select2/dist/js/i18n/ro');
require('select2/dist/js/i18n/ru');
require('select2/dist/js/i18n/sk');
require('select2/dist/js/i18n/sv');
require('select2/dist/js/i18n/tr');
require('select2/dist/js/i18n/vi');
require('select2/dist/js/i18n/zh-CN');
const Moment = require('moment');
global.moment = Moment;
require('moment/locale/ar');
require('moment/locale/cs');
require('moment/locale/da');
require('moment/locale/de');
require('moment/locale/de-at');
require('moment/locale/de-ch');
require('moment/locale/eo');
require('moment/locale/es');
require('moment/locale/eu');
require('moment/locale/fi');
require('moment/locale/fo');
require('moment/locale/fr');
require('moment/locale/he');
require('moment/locale/hu');
require('moment/locale/it');
require('moment/locale/ja');
require('moment/locale/ko');
require('moment/locale/nl');
require('moment/locale/pl');
require('moment/locale/pt');
require('moment/locale/pt-br');
require('moment/locale/ro');
require('moment/locale/ru');
require('moment/locale/sk');
require('moment/locale/sv');
require('moment/locale/tr');
require('moment/locale/vi');
require('moment/locale/zh-cn');
require('daterangepicker');
/*
"sortablejs": "^1.10",
const Sortable = require('sortablejs/Sortable.min');
global.Sortable = Sortable;
*/
// ------ AdminLTE framework ------
require('./sass/bootstrap.scss');
require('./sass/fontawesome.scss');
require('admin-lte/dist/css/AdminLTE.min.css');
require('admin-lte/dist/css/skins/_all-skins.css');
require('../vendor/kevinpapst/adminlte-bundle/Resources/assets/admin-lte-extensions.scss');
global.$.AdminLTE = {};
global.$.AdminLTE.options = {};
require('admin-lte/dist/js/adminlte.min');
// ------------------- INLINED ADMIN-LTE DEFINITIONS -------------------
// ---------------------------------------------------------------------
require('./sass/app.scss');
require('./sass/_app.scss');
// ------ Kimai itself ------
require('./js/KimaiWebLoader.js');
global.KimaiPaginatedBoxWidget = require('./js/widgets/KimaiPaginatedBoxWidget').default;
global.KimaiReloadPageWidget = require('./js/widgets/KimaiReloadPageWidget').default;
global.KimaiCookies = require('./js/widgets/KimaiCookies').default;
// ------ Autocomplete for tags only ------
require('jquery-ui/ui/widgets/autocomplete');
global.KimaiColor = require('./js/widgets/KimaiColor').default;
global.KimaiStorage = require('./js/widgets/KimaiStorage').default;

View File

@@ -1,36 +1,2 @@
// can be removed, once fullcalendar was updated and works without draggable jquery ui objects
require('jquery-ui/ui/widgets/draggable');
require('fullcalendar');
require('fullcalendar/dist/gcal.min');
require('fullcalendar/dist/locale/ar');
require('fullcalendar/dist/locale/cs');
require('fullcalendar/dist/locale/da');
require('fullcalendar/dist/locale/de');
require('fullcalendar/dist/locale/de-at');
require('fullcalendar/dist/locale/de-ch');
require('fullcalendar/dist/locale/es');
require('fullcalendar/dist/locale/eu');
require('fullcalendar/dist/locale/fi');
require('fullcalendar/dist/locale/fr');
require('fullcalendar/dist/locale/he');
require('fullcalendar/dist/locale/hu');
require('fullcalendar/dist/locale/it');
require('fullcalendar/dist/locale/ja');
require('fullcalendar/dist/locale/ko');
require('fullcalendar/dist/locale/nl');
require('fullcalendar/dist/locale/pl');
require('fullcalendar/dist/locale/pt');
require('fullcalendar/dist/locale/pt-br');
require('fullcalendar/dist/locale/ro');
require('fullcalendar/dist/locale/ru');
require('fullcalendar/dist/locale/sk');
require('fullcalendar/dist/locale/sv');
require('fullcalendar/dist/locale/tr');
require('fullcalendar/dist/locale/zh-cn');
require('fullcalendar/dist/locale/vi');
require('fullcalendar/dist/locale/en-gb'); // the last imported file is used as fallback for locales that do not exist (like EO)
require('fullcalendar/dist/fullcalendar.min.css');
global.KimaiCalendar = require('./js/widgets/KimaiCalendar').default;

View File

@@ -1,2 +1,58 @@
import {
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
// BubbleController,
DoughnutController,
LineController,
PieController,
// PolarAreaController,
// RadarController,
// ScatterController,
CategoryScale,
LinearScale,
// LogarithmicScale,
// RadialLinearScale,
// TimeScale,
// TimeSeriesScale,
// Decimation,
// Filler,
Legend,
Title,
Tooltip,
Colors,
// SubTitle
} from 'chart.js';
require('chart.js/dist/Chart.min');
Chart.register(
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
// BubbleController,
DoughnutController,
LineController,
PieController,
// PolarAreaController,
// RadarController,
// ScatterController,
CategoryScale,
LinearScale,
// LogarithmicScale,
// RadialLinearScale,
// TimeScale,
// TimeSeriesScale,
// Decimation,
// Filler,
Legend,
Title,
Tooltip,
Colors
// SubTitle
);
global.Chart = Chart;

8
assets/dashboard.js Normal file
View File

@@ -0,0 +1,8 @@
/**
* https://gridstackjs.com
* https://github.com/gridstack/gridstack.js/tree/master/doc
*/
require('gridstack/dist/gridstack.min.css');
require('gridstack/dist/gridstack-extra.min.css');
import { GridStack } from 'gridstack';
global.GridStack = GridStack;

2
assets/export-pdf.js Normal file
View File

@@ -0,0 +1,2 @@
require('./sass/_export-pdf.scss');

5
assets/highlight.js Normal file
View File

@@ -0,0 +1,5 @@
require('highlight.js/styles/github-dark.css');
const hljs = require('highlight.js/lib/common');
global.hljs = hljs;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,2 +1,2 @@
require('./sass/invoice-pdf.scss');
require('./sass/_invoice-pdf.scss');

View File

@@ -1,2 +1,4 @@
require('./sass/invoice.scss');
/**
* @deprecated use invoice-pdf instead
*/
require('./sass/_invoice.scss');

View File

@@ -23,4 +23,31 @@ export default class KimaiConfiguration {
return name in this._configurations;
}
isRTL() {
return this.get('direction') === 'rtl';
}
getLanguage() {
return this.get('locale').replace('_', '-');
}
is24Hours() {
return !!this.get('twentyFourHours');
}
/**
* @param {boolean} iso
* @return {number}
*/
getFirstDayOfWeek(iso = true) {
if (iso === undefined) {
iso = true;
}
let config = this.get('first_dow_iso');
if (!iso) {
config = config % 7;
}
return config;
}
}

View File

@@ -20,8 +20,8 @@ export default class KimaiContainer {
/**
* Create a new Container with the given configurations and translations.
*
* @param {Object} configuration
* @param {Object} translation
* @param {KimaiConfiguration} configuration
* @param {KimaiTranslation} translation
*/
constructor(configuration, translation) {
if (!(configuration instanceof KimaiConfiguration)) {
@@ -88,4 +88,11 @@ export default class KimaiContainer {
return this._configuration;
}
/**
* @returns {KimaiUser}
*/
getUser() {
return this.getPlugin('user');
}
}

View File

@@ -9,78 +9,97 @@
* [KIMAI] KimaiLoader: bootstrap the application and all plugins
*/
import moment from 'moment';
import { Settings } from 'luxon';
import KimaiTranslation from "./KimaiTranslation";
import KimaiConfiguration from "./KimaiConfiguration";
import KimaiContainer from "./KimaiContainer";
import KimaiActiveRecordsDuration from './plugins/KimaiActiveRecordsDuration.js';
import KimaiDatatableColumnView from './plugins/KimaiDatatableColumnView.js';
import KimaiThemeInitializer from "./plugins/KimaiThemeInitializer";
import KimaiDateRangePicker from "./plugins/KimaiDateRangePicker";
import KimaiDateRangePicker from "./forms/KimaiDateRangePicker";
import KimaiDatatable from "./plugins/KimaiDatatable";
import KimaiToolbar from "./plugins/KimaiToolbar";
import KimaiAPI from "./plugins/KimaiAPI";
import KimaiSelectDataAPI from "./plugins/KimaiSelectDataAPI";
import KimaiDateTimePicker from "./plugins/KimaiDateTimePicker";
import KimaiAlternativeLinks from "./plugins/KimaiAlternativeLinks";
import KimaiAjaxModalForm from "./plugins/KimaiAjaxModalForm";
import KimaiActiveRecords from "./plugins/KimaiActiveRecords";
import KimaiRecentActivities from "./plugins/KimaiRecentActivities";
import KimaiEvent from "./plugins/KimaiEvent";
import KimaiAPILink from "./plugins/KimaiAPILink";
import KimaiAlert from "./plugins/KimaiAlert";
import KimaiAutocomplete from "./plugins/KimaiAutocomplete";
import KimaiFormSelect from "./plugins/KimaiFormSelect";
import KimaiAutocomplete from "./forms/KimaiAutocomplete";
import KimaiFormSelect from "./forms/KimaiFormSelect";
import KimaiForm from "./plugins/KimaiForm";
import KimaiDatePicker from "./plugins/KimaiDatePicker";
import KimaiDatePicker from "./forms/KimaiDatePicker";
import KimaiConfirmationLink from "./plugins/KimaiConfirmationLink";
import KimaiMultiUpdateTable from "./plugins/KimaiMultiUpdateTable";
import KimaiDateUtils from "./plugins/KimaiDateUtils";
import KimaiEscape from "./plugins/KimaiEscape";
import KimaiFetch from "./plugins/KimaiFetch";
import KimaiTimesheetForm from "./forms/KimaiTimesheetForm";
import KimaiTeamForm from "./forms/KimaiTeamForm";
import KimaiCopyDataForm from "./forms/KimaiCopyDataForm";
import KimaiDateNowForm from "./forms/KimaiDateNowForm";
import KimaiNotification from "./plugins/KimaiNotification";
import KimaiHotkeys from "./plugins/KimaiHotkeys";
import KimaiRemoteModal from "./plugins/KimaiRemoteModal";
import KimaiUser from "./plugins/KimaiUser";
import KimaiAutocompleteTags from "./forms/KimaiAutocompleteTags";
export default class KimaiLoader {
constructor(configurations, translations) {
// set the current locale for all javascript components
moment.locale(configurations['locale'].replace('_', '-').toLowerCase());
Settings.defaultLocale = configurations['locale'].replace('_', '-').toLowerCase();
Settings.defaultZone = configurations['timezone'];
const kimai = new KimaiContainer(
new KimaiConfiguration(configurations),
new KimaiTranslation(translations)
);
// GLOBAL HELPER PLUGINS
kimai.registerPlugin(new KimaiUser());
kimai.registerPlugin(new KimaiEscape());
kimai.registerPlugin(new KimaiEvent());
kimai.registerPlugin(new KimaiAPI());
kimai.registerPlugin(new KimaiAlert());
kimai.registerPlugin(new KimaiFetch());
kimai.registerPlugin(new KimaiDateUtils());
kimai.registerPlugin(new KimaiFormSelect('.selectpicker'));
kimai.registerPlugin(new KimaiNotification());
// FORM PLUGINS
kimai.registerPlugin(new KimaiFormSelect('.selectpicker', 'select[data-related-select]'));
kimai.registerPlugin(new KimaiDateRangePicker('input[data-daterangepicker="on"]'));
kimai.registerPlugin(new KimaiDatePicker('input[data-datepicker="on"]'));
kimai.registerPlugin(new KimaiAutocomplete());
kimai.registerPlugin(new KimaiAutocompleteTags());
kimai.registerPlugin(new KimaiTimesheetForm());
kimai.registerPlugin(new KimaiTeamForm());
kimai.registerPlugin(new KimaiCopyDataForm());
kimai.registerPlugin(new KimaiDateNowForm());
kimai.registerPlugin(new KimaiForm());
kimai.registerPlugin(new KimaiHotkeys());
// SPECIAL FEATURES
kimai.registerPlugin(new KimaiConfirmationLink('confirmation-link'));
kimai.registerPlugin(new KimaiActiveRecordsDuration('[data-since]'));
kimai.registerPlugin(new KimaiDatatableColumnView('data-column-visibility'));
kimai.registerPlugin(new KimaiDateRangePicker('input[data-daterangepickerenable="on"]'));
kimai.registerPlugin(new KimaiDateTimePicker('input[data-datetimepicker="on"]'));
kimai.registerPlugin(new KimaiDatePicker('input[data-datepickerenable="on"]'));
kimai.registerPlugin(new KimaiDatatable('section.content', 'table.dataTable'));
kimai.registerPlugin(new KimaiToolbar('form.searchform', 'toolbar-action'));
kimai.registerPlugin(new KimaiSelectDataAPI('select[data-related-select]'));
kimai.registerPlugin(new KimaiAlternativeLinks('.alternative-link'));
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form'));
kimai.registerPlugin(new KimaiRecentActivities('li.notifications-menu'));
kimai.registerPlugin(new KimaiActiveRecords('li.messages-menu', 'li.messages-menu-empty'));
kimai.registerPlugin(new KimaiAjaxModalForm('.modal-ajax-form', ['td.multiCheckbox', 'td.actions']));
kimai.registerPlugin(new KimaiRemoteModal());
kimai.registerPlugin(new KimaiActiveRecords());
kimai.registerPlugin(new KimaiAPILink('api-link'));
kimai.registerPlugin(new KimaiAutocomplete('.js-autocomplete'));
kimai.registerPlugin(new KimaiForm());
kimai.registerPlugin(new KimaiThemeInitializer());
kimai.registerPlugin(new KimaiMultiUpdateTable());
//kimai.registerPlugin(new KimaiPauseRecord('li.messages-menu ul.menu li'));
kimai.registerPlugin(new KimaiThemeInitializer());
// notify all listeners that Kimai plugins can now be registered
kimai.getPlugin('event').trigger('kimai.pluginRegister', {'kimai': kimai});
document.dispatchEvent(new CustomEvent('kimai.pluginRegister', {detail: {'kimai': kimai}}));
// initialize all plugins
kimai.getPlugins().map(plugin => { plugin.init(); });
// notify all listeners that Kimai is now ready to be used
kimai.getPlugin('event').trigger('kimai.initialized', {'kimai': kimai});
document.dispatchEvent(new CustomEvent('kimai.initialized', {detail: {'kimai': kimai}}));
this.kimai = kimai;
}

View File

@@ -42,7 +42,7 @@ export default class KimaiPlugin {
}
/**
* This function returns null, if xou call it BEFORE init().
* This function returns null, if you call it BEFORE init().
*
* @returns {KimaiContainer}
*/
@@ -58,6 +58,20 @@ export default class KimaiPlugin {
return this.getContainer().getConfiguration().get(name);
}
/**
* @return {KimaiConfiguration}
*/
getConfigurations() {
return this.getContainer().getConfiguration();
}
/**
* @returns {KimaiDateUtils}
*/
getDateUtils() {
return this.getPlugin('date');
}
/**
* @param {string} name
* @returns {KimaiPlugin}
@@ -66,4 +80,83 @@ export default class KimaiPlugin {
return this.getContainer().getPlugin(name);
}
/**
* @returns {KimaiTranslation}
*/
getTranslation() {
return this.getContainer().getTranslation();
}
/**
* @param {string} name
* @returns {string}
*/
translate(name) {
return this.getTranslation().get(name);
}
/**
* @deprecated use the plugin directly
* @param {string} title
* @returns {string}
*/
escape(title) {
return this.getPlugin('escape').escapeForHtml(title);
}
/**
* @param {string} name
* @param {string|null} details
*/
trigger(name, details = null) {
this.getPlugin('event').trigger(name, details);
}
/**
* @param {string} url
* @param {object} options
* @returns {Promise<Response>}
*/
fetch(url, options = {}) {
return this.getPlugin('fetch').fetch(url, options);
}
/**
* @param {HTMLFormElement} form
* @param {object} options
* @param {string|null} url
* @returns {Promise<Response>}
*/
fetchForm(form, options = {}, url = null) {
url = url || form.getAttribute('action');
const method = form.getAttribute('method').toUpperCase();
if (method === 'GET') {
const data = this.getPlugin('form').convertFormDataToQueryString(form, {}, true);
// TODO const data = new URLSearchParams(new FormData(form)).toString();
url = url + (url.includes('?') ? '&' : '?') + data;
options = {...{method: 'GET'}, ...options};
} else if (method === 'POST') {
options = {...{
method: 'POST',
body: new FormData(form)
}, ...options};
}
return this.fetch(url, options);
}
/**
* Check if the current device is a mobile device (targeting the bootstrip xs breakpoint size).
*
* @returns {boolean}
*/
isMobile() {
const width = Math.max(
document.documentElement.clientWidth,
window.innerWidth || 0
);
return width < 576;
}
}

View File

@@ -0,0 +1,98 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import TomSelect from 'tom-select';
import KimaiFormTomselectPlugin from "./KimaiFormTomselectPlugin";
/**
* Supporting auto-complete fields via API.
*/
export default class KimaiAutocomplete extends KimaiFormTomselectPlugin {
init()
{
this.selector = '[data-form-widget="autocomplete"]';
}
/**
* @param {HTMLFormElement} form
* @return boolean
*/
supportsForm(form) // eslint-disable-line no-unused-vars
{
return true;
}
loadData(apiUrl, query, callback) {
/** @type {KimaiAPI} API */
const API = this.getContainer().getPlugin('api');
API.get(apiUrl, {'name': query}, (data) => {
let results = [];
for (let item of data) {
results.push({text: item.name, value: item.name});
}
callback(results);
}, () => {
callback();
});
}
activateForm(form)
{
[].slice.call(form.querySelectorAll(this.selector)).map((node) => {
const apiUrl = node.dataset['autocompleteUrl'];
let minChars = 3;
if (node.dataset['minimumCharacter'] !== undefined) {
minChars = parseInt(node.dataset['minimumCharacter']);
}
let options = {
// see https://github.com/orchidjs/tom-select/issues/543#issuecomment-1664342257
onItemAdd: function(){
// remove remaining characters from input after selecting an item
this.setTextboxValue('');
},
// if there are more than 500, they need to be found by "typing"
maxOptions: 500,
// the autocomplete is ONLY used, when the user can create tags
create: node.dataset['create'] !== undefined,
onOptionAdd: (value) => {
node.dispatchEvent(new CustomEvent('create', {detail: {'value': value}}));
},
plugins: ['remove_button'],
shouldLoad: function(query) {
return query.length >= minChars;
},
load: (query, callback) => {
this.loadData(apiUrl, query, callback);
},
};
let render = {
// eslint-disable-next-line
not_loading: (data, escape) => {
// no default content
},
};
const rendererType = (node.dataset['renderer'] !== undefined) ? node.dataset['renderer'] : 'default';
options.render = {...render, ...this.getRenderer(rendererType)};
new TomSelect(node, options);
});
}
destroyForm(form) {
[].slice.call(form.querySelectorAll(this.selector)).map((node) => {
if (node.tomselect) {
node.tomselect.destroy();
}
});
}
}

View File

@@ -0,0 +1,34 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import KimaiAutocomplete from "./KimaiAutocomplete";
/**
* Used for timesheet tagging in toolbar and edit dialogs.
*/
export default class KimaiAutocompleteTags extends KimaiAutocomplete {
init()
{
this.selector = '[data-form-widget="tags"]';
}
loadData(apiUrl, query, callback) {
/** @type {KimaiAPI} API */
const API = this.getContainer().getPlugin('api');
API.get(apiUrl, {'name': query}, (data) => {
let results = [];
for (let item of data) {
results.push({text: item.name, value: item.name, color: item['color-safe']});
}
callback(results);
}, () => {
callback();
});
}
}

View File

@@ -0,0 +1,71 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiEditTimesheetForm: responsible for the most important form in the application
*/
import KimaiFormPlugin from "./KimaiFormPlugin";
/**
* Used for simple copy from link to input action, e.g. the time and duration dropdowns
* copy the selected values into their corresponding input.
*/
export default class KimaiCopyDataForm extends KimaiFormPlugin {
/**
* @param {HTMLFormElement} form
* @return boolean
*/
supportsForm(form) // eslint-disable-line no-unused-vars
{
return true;
}
/**
* @param {HTMLFormElement} form
*/
activateForm(form)
{
if (this._eventHandler === undefined) {
this._eventHandler = (event) => {
let element = event.target;
if (!element.matches('a[data-form-widget="copy-data"]')) {
element = element.parentNode; // mostly for icons
}
if (!element.matches('a[data-form-widget="copy-data"]') || element.dataset.target === undefined) {
return;
}
const target = document.querySelector(element.dataset.target);
if (target === null) {
return;
}
target.value = element.dataset.value;
if (element.dataset.event !== undefined) {
for (const event of element.dataset.event.split(' ')) {
target.dispatchEvent(new Event(event));
const form = target.closest('form');
if (form !== null) {
form.dispatchEvent(new Event(event));
}
}
}
event.preventDefault();
};
}
form.addEventListener('click', this._eventHandler);
}
/**
* @param {HTMLFormElement} form
*/
destroyForm(form)
{
form.removeEventListener('click', this._eventHandler);
}
}

View File

@@ -0,0 +1,73 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiEditTimesheetForm: responsible for the most important form in the application
*/
import KimaiFormPlugin from "./KimaiFormPlugin";
/**
*/
export default class KimaiDateNowForm extends KimaiFormPlugin {
init()
{
this.selector = 'a[data-form-widget="date-now"]';
}
/**
* @param {HTMLFormElement} form
* @return boolean
*/
supportsForm(form) // eslint-disable-line no-unused-vars
{
return true;
}
/**
* @param {HTMLFormElement} form
*/
activateForm(form)
{
[].slice.call(form.querySelectorAll(this.selector)).map((element) => {
if (element.dataset.format !== undefined && element.dataset.target !== undefined) {
if (this._eventHandler === undefined) {
this._eventHandler = (event) => {
const linkTarget = event.currentTarget;
const formElement = document.getElementById(linkTarget.dataset.target);
if (!formElement.disabled) {
formElement.value = this.getDateUtils().format(linkTarget.dataset.format, null);
// this should usually work
formElement.dispatchEvent(new Event('change', {bubbles: true}));
// this is required for Litepicker to pick the new date (with autoRefresh option)
formElement.dispatchEvent(new Event('keyup', {bubbles: true}));
}
event.preventDefault();
};
}
element.addEventListener('click', this._eventHandler);
}
});
}
/**
* @param {HTMLFormElement} form
*/
destroyForm(form)
{
[].slice.call(form.querySelectorAll(this.selector)).map((element) => {
if (element.dataset.format !== undefined && element.dataset.target !== undefined) {
element.removeEventListener('click', this._eventHandler);
}
});
}
}

View File

@@ -0,0 +1,129 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDatePicker: single date selects (currently unused)
*/
import { Litepicker } from 'litepicker';
import 'litepicker/dist/plugins/mobilefriendly';
import KimaiFormPlugin from "./KimaiFormPlugin";
export default class KimaiDatePicker extends KimaiFormPlugin {
constructor(selector)
{
super();
this._selector = selector;
}
init()
{
window.disableLitepickerStyles = true;
this._pickers = [];
}
/**
* @param {HTMLFormElement} form
* @return boolean
*/
supportsForm(form) // eslint-disable-line no-unused-vars
{
return true;
}
/**
* @param {HTMLFormElement} form
*/
activateForm(form)
{
const FIRST_DOW = this.getConfigurations().getFirstDayOfWeek(false);
const LANGUAGE = this.getConfigurations().getLanguage();
let options = {
buttonText: {
previousMonth: `<i class="fas fa-chevron-left"></i>`,
nextMonth: `<i class="fas fa-chevron-right"></i>`,
apply: this.translate('confirm'),
cancel: this.translate('cancel'),
},
};
const newPickers = [].slice.call(form.querySelectorAll(this._selector)).map((element) => {
if (element.dataset.format === undefined) {
console.log('Trying to bind litepicker to an element without data-format attribute');
}
if (element.hasAttribute('min') !== undefined) {
options = {...options, ...{
'minDate': element.getAttribute('min'),
}};
}
if (element.hasAttribute('max') !== undefined) {
options = {...options, ...{
'maxDate': element.getAttribute('max'),
}};
}
options = {...options, ...{
format: element.dataset.format,
showTooltip: false,
element: element,
lang: LANGUAGE,
autoRefresh: true,
firstDay: FIRST_DOW, // Litepicker: 0 = Sunday, 1 = Monday
setup: (picker) => {
// nasty hack, because litepicker does not trigger change event on the input and the available
// event "selected" is triggered way to often, even when moving the cursor inside the input
// element (not even typing is necessary) and so we have to make sure that the manual "click" event
// (works for touch as well) happened before we actually dispatch the change event manually ...
// what? report forms would be submitted upon cursor move without the "preselect” check
picker.on('preselect', (date1, date2) => { // eslint-disable-line no-unused-vars
picker._wasPreselected = true;
});
picker.on('selected', (date1, date2) => { // eslint-disable-line no-unused-vars
if (picker._wasPreselected !== undefined) {
element.dispatchEvent(new Event('change', {bubbles: true}));
delete picker._wasPreselected;
}
});
// only if mobile.friendly plugin is activated
if (picker.backdrop !== undefined) {
// the node needs to be moved, so the flat form layout works properly (e.g. for date types)
document.body.appendChild(picker.backdrop);
}
},
}};
return [element, new Litepicker(this.prepareOptions(options))];
});
this._pickers = this._pickers.concat(newPickers);
}
prepareOptions(options)
{
return {...options, ...{
plugins: ['mobilefriendly'],
}};
}
/**
* @param {HTMLFormElement} form
*/
destroyForm(form)
{
[].slice.call(form.querySelectorAll(this._selector)).map((element) => {
for (let i = 0; i < this._pickers.length; i++) {
if (this._pickers[i][0] === element) {
this._pickers[i][1].destroy();
this._pickers.splice(i, 1);
}
}
});
}
}

View File

@@ -0,0 +1,25 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDateRangePicker: activate the (daterange picker) compound field in toolbar
*/
import KimaiDatePicker from "./KimaiDatePicker";
export default class KimaiDateRangePicker extends KimaiDatePicker {
prepareOptions(options)
{
return {...options, ...{
plugins: ['mobilefriendly'],
singleMode: false,
autoRefresh: true,
}};
}
}

View File

@@ -0,0 +1,39 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiFormPlugin: base class for all none ID plugin that handle forms
*/
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiFormPlugin extends KimaiPlugin {
/**
* @param {HTMLFormElement} form
* @return boolean
*/
supportsForm(form) // eslint-disable-line no-unused-vars
{
return false;
}
/**
* @param {HTMLFormElement} form
*/
activateForm(form) // eslint-disable-line no-unused-vars
{
}
/**
* @param {HTMLFormElement} form
*/
destroyForm(form) // eslint-disable-line no-unused-vars
{
}
}

View File

@@ -0,0 +1,564 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiFormSelect: enhanced functionality for HTMLSelectElement
*/
import TomSelect from 'tom-select';
import KimaiFormTomselectPlugin from "./KimaiFormTomselectPlugin";
export default class KimaiFormSelect extends KimaiFormTomselectPlugin {
constructor(selector, apiSelects)
{
super();
this._selector = selector;
this._apiSelects = apiSelects;
}
getId()
{
return 'form-select';
}
init()
{
// selects the original value inside dropdowns, as the "reset" event (the updated option)
// is not automatically propagated to the JS element
document.addEventListener('reset', (event) => {
if (event.target.tagName.toUpperCase() === 'FORM') {
setTimeout(() => {
const fields = event.target.querySelectorAll(this._selector);
for (let field of fields) {
if (field.tagName.toUpperCase() === 'SELECT') {
field.dispatchEvent(new Event('data-reloaded'));
}
}
}, 10);
}
});
}
/**
* @param {HTMLFormElement} node
*/
activateSelectPickerByElement(node)
{
let plugins = ['change_listener'];
const isMultiple = node.multiple !== undefined && node.multiple === true;
const isRequired = node.required !== undefined && node.required === true;
if (isRequired) {
plugins.push('no_backspace_delete');
}
if (isMultiple) {
plugins.push('remove_button');
}
if (node.dataset['order'] !== undefined && node.dataset['order'] === '1') {
//plugins.push('caret_position');
plugins.push('drag_drop');
}
let options = {
// see https://github.com/orchidjs/tom-select/issues/543#issuecomment-1664342257
onItemAdd: function(){
// remove remaining characters from input after selecting an item
this.setTextboxValue('');
},
lockOptgroupOrder: true,
allowEmptyOption: !isRequired,
hidePlaceholder: false,
plugins: plugins,
// if there are more than X entries, the other ones are hidden and can only be found
// by typing some characters to trigger the internal option search
// see App\Form\Type\TagsType::MAX_AMOUNT_SELECT
// TODO make this value configurable with a data attribute
maxOptions: 500,
sortField:[{field: '$order'}, {field: '$score'}],
// required so it works in table.responsive, but requires z-index 1056, because bootstrap modal would otherwise hide it
dropdownParent: 'body',
};
let render = {
onOptionAdd: (value) => {
node.dispatchEvent(new CustomEvent('create', {detail: {'value': value}}));
},
};
const rendererType = (node.dataset['renderer'] !== undefined) ? node.dataset['renderer'] : 'default';
options.render = {...render, ...this.getRenderer(rendererType)};
if (node.dataset['create'] !== undefined) {
options = {...options, ...{
persist: true,
create: true,
}};
} else {
options = {...options, ...{
persist: false,
create: false,
}};
}
if (node.dataset.disableSearch !== undefined) {
options = {...options, ...{
controlInput: null,
}};
}
const select = new TomSelect(node, options);
node.addEventListener('data-reloaded', (event) => {
select.clear(true);
select.clearOptionGroups();
select.clearOptions();
select.sync();
select.setValue(event.detail);
select.refreshItems();
select.refreshOptions(false);
});
// support reloading the list upon external event
if (node.dataset['reload'] !== undefined) {
node.addEventListener('reload', () => {
select.disable();
node.disabled = true;
/** @type {KimaiAPI} API */
const API = this.getContainer().getPlugin('api');
API.get(node.dataset['reload'], {}, (data) => {
this._updateSelect(node, data);
select.enable();
node.disabled = false;
});
node.dispatchEvent(new Event('change'));
});
}
}
/**
* @param {HTMLFormElement} form
* @return boolean
*/
supportsForm(form) // eslint-disable-line no-unused-vars
{
return true;
}
/**
* @param {HTMLFormElement} form
*/
activateForm(form)
{
[].slice.call(form.querySelectorAll(this._selector)).map((node) => {
this.activateSelectPickerByElement(node);
});
this._activateApiSelects(this._apiSelects);
}
/**
* @param {HTMLFormElement} form
*/
destroyForm(form)
{
[].slice.call(form.querySelectorAll(this._selector)).map((node) => {
if (node.tomselect) {
node.tomselect.destroy();
}
});
}
/**
* @param {string|Element} selectIdentifier
* @param {object} data
* @private
*/
_updateOptions(selectIdentifier, data)
{
let emptyOption = null;
let node = null;
if (selectIdentifier instanceof Element) {
node = selectIdentifier;
} else {
node = document.querySelector(selectIdentifier);
}
if (node === null) {
console.log('Missing select: ' + selectIdentifier);
return;
}
const selectedValue = node.value;
for (let i = 0; i < node.options.length; i++) {
if (node.options[i].value === '') {
emptyOption = node.options[i];
}
}
node.options.length = 0;
if (emptyOption !== null) {
node.appendChild(this._createOption(emptyOption.text, ''));
}
let emptyOpts = [];
let options = [];
/** @type {string|null} titlePattern */
let titlePattern = null;
if (node.dataset !== undefined && node.dataset['optionPattern'] !== undefined) {
titlePattern = node.dataset['optionPattern'];
}
if (titlePattern === null || titlePattern === '') {
titlePattern = '{name}';
}
for (const [key, value] of Object.entries(data)) {
if (key === '__empty__') {
for (const entity of value) {
emptyOpts.push(this._createOption(this._getTitleFromPattern(titlePattern, entity), entity.id));
}
continue;
}
let optGroup = this._createOptgroup(key);
for (const entity of value) {
optGroup.appendChild(this._createOption(this._getTitleFromPattern(titlePattern, entity), entity.id));
}
options.push(optGroup);
}
// log the one with a group name first (e.g. non-global activities)
options.forEach(child => node.appendChild(child));
// append the ones with no parent at the end (e.g. global activities)
const optGroupEmpty = this._createOptgroup('');
emptyOpts.forEach(child => optGroupEmpty.appendChild(child));
node.appendChild(optGroupEmpty);
// if available, re-select the previous selected option (mostly usable for global activities)
node.value = selectedValue;
// pre-select an option if it is the only available one
if (node.value === '' || node.value === null) {
const allOptions = node.options;
const optionLength = allOptions.length;
let selectOption = '';
if (optionLength === 1 && node.dataset['autoselect'] === undefined) {
selectOption = allOptions[0].value;
} else if (optionLength === 2 && emptyOption !== null) {
selectOption = allOptions[1].value;
}
if (selectOption !== '') {
node.value = selectOption;
}
}
// this will update the attached javascript component
node.dispatchEvent(new CustomEvent('data-reloaded', {detail: node.value}));
// if we don't trigger the change, the other selects won't reset
node.dispatchEvent(new Event('change'));
}
/**
* @param {string} pattern
* @param {array} entity
* @private
*/
_getTitleFromPattern(pattern, entity)
{
const DATE_UTILS = this.getDateUtils();
const regexp = new RegExp('{[^}]*?}','g');
let title = pattern;
let match = null;
while ((match = regexp.exec(pattern)) !== null) {
// cutting a string like "{name}" into "name"
const field = match[0].slice(1, -1);
let value = entity[field] === undefined ? null : entity[field];
if ((field === 'start' || field === 'end')) {
if (value === null) {
value = '?';
} else {
value = DATE_UTILS.getFormattedDate(value);
}
}
title = title.replace(new RegExp('{' + field + '}', 'g'), value ?? '');
}
title = title.replace(/- \?-\?/, '');
title = title.replace(/\r\n|\r|\n/g, ' ');
title = title.substring(0, 110);
const chars = '- ';
let start = 0, end = title.length;
while (start < end && chars.indexOf(title[start]) >= 0) {
++start;
}
while (end > start && chars.indexOf(title[end - 1]) >= 0) {
--end;
}
let result = (start > 0 || end < title.length) ? title.substring(start, end) : title;
if (result === '' && entity['name'] !== undefined) {
return entity['name'];
}
return result;
}
/**
* @param {HTMLSelectElement} select
* @param {string} label
* @param {string} value
* @param {object} dataset
*/
addOption(select, label, value, dataset)
{
const option = this._createOption(label, value);
for (const key in dataset) {
option.dataset[key] = dataset[key];
}
select.options.add(option);
if (select.tomselect !== undefined) {
select.tomselect.sync();
}
}
/**
*
* @param {HTMLSelectElement} select
* @param {HTMLOptionElement} option
*/
removeOption(select, option)
{
option.remove();
if (select.tomselect !== undefined) {
select.tomselect.removeOption(option.value, true);
select.tomselect.clear(true);
}
}
/**
* @param {string} label
* @param {string} value
* @returns {HTMLElement}
* @private
*/
_createOption(label, value)
{
let option = document.createElement('option');
option.innerText = label;
option.value = value;
return option;
}
/**
* @param {string} label
* @returns {HTMLElement}
* @private
*/
_createOptgroup(label)
{
let optGroup = document.createElement('optgroup');
optGroup.label = label;
return optGroup;
}
/**
* @param {string} selector
* @private
*/
_activateApiSelects(selector)
{
if (this._eventHandlerApiSelects === undefined) {
this._eventHandlerApiSelects = (event) => {
if (event.target === null || !event.target.matches(selector)) {
return;
}
const apiSelect = event.target;
const targetSelectId = '#' + apiSelect.dataset['relatedSelect'];
/** @type {HTMLSelectElement} targetSelect */
const targetSelect = document.getElementById(apiSelect.dataset['relatedSelect']);
// if the related target select does not exist, we do not need to load the related data
if (targetSelect === null || targetSelect.dataset['reloading'] === '1') {
return;
}
targetSelect.dataset['reloading'] = '1';
if (targetSelect.tomselect !== undefined) {
targetSelect.tomselect.disable();
}
targetSelect.disabled = true;
let formPrefix = apiSelect.dataset['formPrefix'];
if (formPrefix === undefined || formPrefix === null) {
formPrefix = '';
} else if (formPrefix.length > 0) {
formPrefix += '_';
}
let newApiUrl = this._buildUrlWithFormFields(apiSelect.dataset['apiUrl'], formPrefix);
const selectValue = apiSelect.value;
// Problem: select a project with activities and then select a customer that has no project
// results in a wrong URL, it triggers "activities?project=" instead of using the "emptyUrl"
if (selectValue === undefined || selectValue === null || selectValue === '' || (Array.isArray(selectValue) && selectValue.length === 0)) {
if (apiSelect.dataset['emptyUrl'] === undefined) {
this._updateSelect(targetSelectId, {});
targetSelect.dataset['reloading'] = '0';
return;
}
newApiUrl = this._buildUrlWithFormFields(apiSelect.dataset['emptyUrl'], formPrefix);
}
/** @type {KimaiAPI} API */
const API = this.getContainer().getPlugin('api');
API.get(newApiUrl, {}, (data) => {
this._updateSelect(targetSelectId, data);
if (targetSelect.tomselect !== undefined) {
targetSelect.tomselect.enable();
}
targetSelect.dataset['reloading'] = '0';
targetSelect.disabled = false;
});
};
document.addEventListener('change', this._eventHandlerApiSelects);
}
}
/**
* @param {string} apiUrl
* @param {string} formPrefix
* @return {string}
* @private
*/
_buildUrlWithFormFields(apiUrl, formPrefix)
{
let newApiUrl = apiUrl;
apiUrl.split('?')[1].split('&').forEach(item => {
const [key, value] = item.split('='); // eslint-disable-line no-unused-vars
const decoded = decodeURIComponent(value);
const test = decoded.match(/%(.*)%/);
if (test !== null) {
const originalFieldName = test[1];
const targetFieldName = (formPrefix + originalFieldName).replace(/\[/, '').replace(/]/, '');
const targetField = document.getElementById(targetFieldName);
let newValue = '';
if (targetField === null) {
// happens for example:
// - in duration only mode, when the end field is not found
// console.log('ERROR: Cannot find field with name "' + test[1] + '" by selector: #' + formPrefix + test[1]);
} else {
if (targetField.value !== null) {
newValue = targetField.value;
if (targetField.tagName === 'SELECT' && targetField.multiple) {
newValue = [...targetField.selectedOptions].map(o => o.value);
} else if (newValue !== '') {
if (targetField.type === 'date') {
const timeId = targetField.id.replace('_date', '_time');
const timeElement = document.getElementById(timeId);
const time = timeElement === null ? '12:00:00' : timeElement.value;
// using 12:00 as fallback, because timezone handling might change the date if we use 00:00
const newDate = this.getDateUtils().fromHtml5Input(newValue, time);
newValue = this.getDateUtils().formatForAPI(newDate, false);
} else if (targetField.type === 'text' && targetField.name.includes('date')) {
const timeId = targetField.id.replace('_date', '_time');
const timeElement = document.getElementById(timeId);
// using 12:00 as fallback, because timezone handling might change the date if we use 00:00
let time = '12:00:00';
let timeFormat = 'HH:mm';
if (timeElement !== null) {
time = timeElement.value;
timeFormat = timeElement.dataset['format'];
}
const newDate = this.getDateUtils().fromFormat(newValue.trim() + ' ' + time.trim(), targetField.dataset['format'] + ' ' + timeFormat);
newValue = this.getDateUtils().formatForAPI(newDate, false);
} else if (targetField.dataset['format'] !== undefined) {
// find out when this else branch is triggered and document!
if (this.getDateUtils().isValidDateTime(newValue, targetField.dataset['format'])) {
newValue = this.getDateUtils().format(targetField.dataset['format'], newValue);
}
}
} else {
// happens for example:
// - when the end date is not set on a timesheet record and the project list is loaded (as the URL contains the %end% replacer)
// console.log('Empty value found for field with name "' + test[1] + '" by selector: #' + formPrefix + test[1]);
}
} else {
// happens for example:
// - when a customer without projects is selected
// console.log('ERROR: Empty field with name "' + test[1] + '" by selector: #' + formPrefix + test[1]);
}
}
if (Array.isArray(newValue)) {
let urlParams = [];
for (let tmpValue of newValue) {
if (tmpValue === null) {
tmpValue = '';
}
urlParams.push(originalFieldName + '=' + tmpValue);
}
newApiUrl = newApiUrl.replace(item, urlParams.join('&'));
} else {
if (newValue === null) {
newValue = '';
}
newApiUrl = newApiUrl.replace(value, newValue);
}
}
});
return newApiUrl;
}
/**
* @param {string|Element} select
* @param {object} data
* @private
*/
_updateSelect(select, data)
{
const options = {};
for (const apiData of data) {
let title = '__empty__';
if (apiData['parentTitle'] !== undefined && apiData['parentTitle'] !== null) {
title = apiData['parentTitle'];
}
if (options[title] === undefined) {
options[title] = [];
}
options[title].push(apiData);
}
const ordered = {};
Object.keys(options).sort().forEach(function(key) {
ordered[key] = options[key];
});
this._updateOptions(select, ordered);
}
}

View File

@@ -0,0 +1,80 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiFormTomselectPlugin: base form plugin for everyone using tomselect
*/
import KimaiFormPlugin from './KimaiFormPlugin';
export default class KimaiFormTomselectPlugin extends KimaiFormPlugin {
/**
* @param {string} rendererType
* @return array
*/
getRenderer(rendererType)
{
// default renderer
let render = {
option_create: (data, escape) => {
const name = escape(data.input);
if (name.length < 3) {
return null;
}
const tpl = this.translate('select.search.create');
const tplReplaced = tpl.replace('%input%', '<strong>' + name + '</strong>');
return '<div class="create">' + tplReplaced + '</div>';
},
no_results: (data, escape) => {
const tpl = this.translate('select.search.notfound');
const tplReplaced = tpl.replace('%input%', '<strong>' + escape(data.input) + '</strong>');
return '<div class="no-results">' + tplReplaced + '</div>';
},
};
if (rendererType === 'color') {
render = {...render, ...{
option: function(data, escape) {
let item = '<div class="list-group-item border-0 p-1 ps-2 text-nowrap">';
// if no color is set, do NOT add an empty placeholder
if (data.color !== undefined) {
item += '<span style="background-color:' + data.color + '" class="color-choice-item me-2">&nbsp;</span>';
}
item += escape(data.text) + '</div>';
return item;
},
item: function(data, escape) {
let item = '<div class="text-nowrap">';
// if no color is set, do NOT add an empty placeholder
if (data.color !== undefined) {
item += '<span style="background-color:' + data.color + '" class="color-choice-item me-2">&nbsp;</span>';
}
item += escape(data.text) + '</div>';
return item;
}
}};
} else {
render = {...render, ...{
// the empty entry would collapse and only show as a tiny 5px line if there is no content inside
option: function(data, escape) {
let text = data.text;
if (text === null || text.trim() === '') {
text = '&nbsp;';
} else {
text = escape(text);
}
return '<div>' + text + '</div>';
}
}};
}
return render;
}
}

View File

@@ -0,0 +1,147 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiEditTimesheetForm: responsible for the most important form in the application
*/
import KimaiFormPlugin from "./KimaiFormPlugin";
import KimaiColor from "../widgets/KimaiColor";
export default class KimaiTeamForm extends KimaiFormPlugin {
init()
{
this.usersId = 'team_edit_form_users';
}
/**
* @param {HTMLFormElement} form
* @return boolean
*/
supportsForm(form)
{
return form.name === 'team_edit_form';
}
/**
* @return {HTMLElement}
* @private
*/
_getPrototype()
{
return document.getElementById('team_edit_form_members');
}
/**
* @param {HTMLFormElement} form
*/
activateForm(form)
{
if (!this.supportsForm(form)) {
return;
}
// must be attached to the form, because the button is added dynamically
form.addEventListener('click', event => this._removeMember(event));
document.getElementById(this.usersId).addEventListener('change', event => {
const select = event.target;
const option = select.options[select.selectedIndex];
const member = this._createMember(option);
this._getPrototype().append(member);
this.getPlugin('form-select').removeOption(select, option);
});
}
/**
* @param {HTMLOptionElement} option
* @returns {Element}
* @private
*/
_createMember(option)
{
/** @type {KimaiEscape} ESCAPER */
const ESCAPER = this.getPlugin('escape');
const prototype = this._getPrototype();
let counter = prototype.dataset['widgetCounter'] || prototype.childNodes.length;
let newWidget = prototype.dataset['prototype'];
newWidget = newWidget.replace(/__name__/g, counter);
newWidget = newWidget.replace(/#000000/g, KimaiColor.calculateContrastColor(option.dataset.color));
newWidget = newWidget.replace(/__DISPLAY__/g, ESCAPER.escapeForHtml(option.dataset.display));
newWidget = newWidget.replace(/__COLOR__/g, option.dataset.color);
newWidget = newWidget.replace(/__INITIALS__/g, ESCAPER.escapeForHtml(option.dataset.initials));
newWidget = newWidget.replace(/__TITLE__/g, ESCAPER.escapeForHtml(option.dataset.title));
newWidget = newWidget.replace(/__USERNAME__/g, ESCAPER.escapeForHtml(option.text));
prototype.dataset['widgetCounter'] = (++counter).toString();
const temp = document.createElement('div');
temp.innerHTML = ESCAPER.sanitize(newWidget);
temp.querySelector('input[type=hidden]').value = option.value;
const newNode = temp.firstElementChild;
// copy over all initial settings, so we are able to rebuild the original option if the
// member is removed from the list later on
for (const key in option.dataset) {
newNode.dataset[key] = option.dataset[key];
}
return newNode;
}
/**
* @param {Event} event
* @private
*/
_removeMember(event)
{
let button = event.target;
if (button.parentNode.matches('.remove-member')) {
button = button.parentNode;
}
if (button.matches('.remove-member')) {
// see blocks.html.twig => block team_member_widget
const element = button.parentNode.parentNode.parentNode.parentNode.parentNode;
// re-adding the option to the select makes up for form validation errors
// because the list would have to be re-ordered and indices need to be changed ...
/*
this.getPlugin('form-select').addOption(
document.getElementById(this.usersId),
element.dataset['display'],
element.dataset['id'],
element.dataset
);
const prototype = this._getPrototype();
prototype.dataset['widgetCounter'] = (prototype.dataset['widgetCounter'] - 1).toString();
*/
element.remove();
event.stopPropagation();
event.preventDefault();
}
}
/**
* @param {HTMLFormElement} form
*/
destroyForm(form)
{
if (!this.supportsForm(form)) {
return;
}
form.removeEventListener('click', this._removeMember);
}
}

View File

@@ -0,0 +1,698 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiEditTimesheetForm: responsible for the most important form in the application
*/
import { DateTime } from 'luxon';
import KimaiFormPlugin from "./KimaiFormPlugin";
export default class KimaiTimesheetForm extends KimaiFormPlugin {
/**
* @param {HTMLFormElement} form
* @return boolean
*/
supportsForm(form)
{
return (form.name === 'timesheet_edit_form' || form.name ==='timesheet_admin_edit_form' || form.name ==='timesheet_multi_user_edit_form');
}
/**
* @param {HTMLFormElement} form
*/
destroyForm(form)
{
if (!this.supportsForm(form)) {
return;
}
if (this._beginDate !== undefined) {
this._beginDate.removeEventListener('change', this._beginListener);
delete this._beginListener;
delete this._beginDate;
}
if (this._beginTime !== undefined) {
this._beginTime.removeEventListener('change', this._beginListener);
delete this._beginListener;
this._beginTime.removeEventListener('blur', this._beginBlurListener);
delete this._beginBlurListener;
delete this._beginTime;
}
if (this._endTime !== undefined) {
this._endTime.removeEventListener('change', this._endListener);
delete this._endListener;
this._endTime.removeEventListener('blur', this._endBlurListener);
delete this._endBlurListener;
delete this._endTime;
}
if (this._duration !== undefined) {
this._duration.removeEventListener('change', this._durationListener);
delete this._durationListener;
this._duration.removeEventListener('keydown', this._durationKeyListener);
delete this._durationKeyListener;
this._duration.removeEventListener('blur', this._durationBlurListener);
delete this._durationBlurListener;
delete this._duration;
}
if (this._durationToggle !== undefined && this._durationToggle !== null) {
this._durationToggle.removeEventListener('change', this._durationToggleListener);
delete this._durationToggleListener;
delete this._durationToggle;
}
if (this._activity !== undefined) {
this._activity.removeEventListener('create', this._activityListener);
delete this._activityListener;
delete this._activity;
}
if (this._project !== undefined) {
delete this._project;
}
}
activateForm(form)
{
if (!this.supportsForm(form)) {
return;
}
const formPrefix = form.name;
this._activity = document.getElementById(formPrefix + '_activity');
this._project = document.getElementById(formPrefix + '_project');
/** @param {CustomEvent} event */
this._activityListener = (event) => {
const project = this._project.value;
/** @type {KimaiAPI} API */
const API = this.getContainer().getPlugin('api');
API.post(this._activity.dataset['create'], {
name: event.detail.value,
project: (project === '' ? null : project),
visible: true,
}, () => {
this._project.dispatchEvent(new Event('change'));
});
};
this._activity.addEventListener('create', this._activityListener);
this._beginDate = document.getElementById(formPrefix + '_begin_date');
this._beginTime = document.getElementById(formPrefix + '_begin_time');
this._endTime = document.getElementById(formPrefix + '_end_time');
this._duration = document.getElementById(formPrefix + '_duration');
this._durationToggle = document.getElementById(formPrefix + '_duration_toggle');
if (this._beginDate === null || this._beginTime === null || this._endTime === null || this._duration === null) {
return;
}
this._beginListener = () => this._changedBegin();
this._beginBlurListener = () => this._parseBeginTime();
this._endListener = () => this._changedEnd();
this._endBlurListener = () => this._parseEndTime();
this._durationListener = () => this._changedDuration();
this._durationKeyListener = (event) => this._changeDurationOnKeypress(event);
this._durationBlurListener = () => this._parseDuration();
this._beginDate.addEventListener('change', this._beginListener);
this._beginTime.addEventListener('change', this._beginListener);
this._beginTime.addEventListener('blur', this._beginBlurListener);
this._endTime.addEventListener('change', this._endListener);
this._endTime.addEventListener('blur', this._endBlurListener);
this._duration.addEventListener('change', this._durationListener);
this._duration.addEventListener('keydown', this._durationKeyListener);
this._duration.addEventListener('blur', this._durationBlurListener);
if (this._duration !== null && this._durationToggle !== null) {
this._durationToggleListener = () => {
this._durationToggle.classList.toggle('text-success');
};
this._durationToggle.addEventListener('click', this._durationToggleListener);
}
}
_parseBeginTime()
{
if (this._beginTime.value === '') {
return;
}
let newBeginTime = this._formatTimeForParsing(this._beginTime.value, this._beginTime.dataset['format']);
if (newBeginTime !== this._beginTime.value) {
this._beginTime.value = newBeginTime;
this._changedBegin();
}
}
_parseEndTime()
{
if (this._endTime.value === '') {
return;
}
let newEndTime = this._formatTimeForParsing(this._endTime.value, this._endTime.dataset['format']);
if (newEndTime !== this._endTime.value) {
this._endTime.value = newEndTime;
this._changedEnd();
}
}
_parseDuration()
{
if (this._duration.value === '') {
return;
}
this._setDurationAsString(this._getParsedDuration());
}
/**
* Receives a time, written by a human, probably in an invalid format.
* This method supports 12-hour or 24-hour format, the format string contains an uppercase "A" in case of the 12-hour format.
* If it is 12-hour format, then always en-US locallized with AM/PM.
*
* Ruleset:
* - Some locales use a dot instead of a colon, always replace the dot in HH.mm with a colon as in HH:mm
* - If there is an "am" or "pm", always uppercase them
* - Split the string into time and prefix: if AM/PM is included remove it and remember for later
* - If the time is a 1 or 2 character long number: use as hours
* - If the time now is 3 character long: use the 1 char as hour and the 2 and 3 char as minute
* - If the time now is 4 character long: use the 1 and 2 char as hour and the 3 and 4 char as minutes
* - If the format is 12-hour: try to identify the correct time and suffix
* - If the format is 12-hour and misses the AM/PM: try to detect whether it
* - If the time contains AM or PM, make sure that it is always prefixed by a space character
*
* @param {string} time
* @param {string} format
* @returns {string}
* @private
*/
_formatTimeForParsing(time, format)
{
let formatted = time.trim();
// replace invalid separators with colon
formatted = formatted.replace(/\.|;|,/g, ':');
// uppercase 12-hour format
formatted = formatted.replace(/am/i, 'AM');
formatted = formatted.replace(/pm/i, 'PM');
// Split time and AM/PM suffix if present
let suffix = '';
let hour = 0;
let minute = 0;
let timePart = formatted;
const ampmMatch = formatted.match(/\s*(AM|PM)$/i);
if (ampmMatch) {
suffix = ampmMatch[1].toUpperCase();
timePart = formatted.replace(/\s*(AM|PM)$/i, '').trim();
}
if (timePart.indexOf(':') !== -1) {
const match = timePart.match(/(?:(\d+):)?(\d+)/);
hour = parseInt(match?.[1] || 0, 10);
minute = parseInt(match?.[2] || 0, 10);
} else {
timePart = timePart.replace(/:/, '');
if (/^\d{1,2}$/.test(timePart)) {
hour = timePart;
}
if (/^\d{3}$/.test(timePart)) {
hour = timePart.slice(0, 1);
minute = timePart.slice(1);
}
if (/^\d{4}$/.test(timePart)) {
hour = timePart.slice(0, 2);
minute = timePart.slice(2);
}
}
hour = parseInt(hour);
minute = parseInt(minute);
// just in case a person entered a wrong time like 35 hours
hour = hour % 24;
minute = minute % 60;
// format is 12-hour
if (format.toUpperCase().indexOf('A') !== -1) {
// time entered in 24-hour: convert to 12-hour format
if (hour > 12 && hour < 24) {
hour = hour - 12;
suffix = 'PM';
}
// if the person forgot to add a suffix, calculate it and convert time
if (suffix === '') {
if (hour === 0) {
hour = 12;
suffix = 'AM';
} else if (hour === 12) {
suffix = 'PM';
} else {
suffix = 'AM';
}
}
if (suffix === 'PM' && hour === 0) {
hour = 12;
}
} else {
// this is the 34-hour format branch
// check if the person entered time in 12-hour format and convert it
if (suffix === 'AM' && hour === 12) {
hour = 0;
} else if (suffix === 'PM' && hour !== 12) {
hour = (hour + 12) % 24;
}
// make sure we have no suffix
suffix = '';
}
formatted = hour + ':' + minute.toString().padStart(2, '0');
if (suffix !== '') {
formatted = formatted + ' ' + suffix.trim();
}
return formatted;
}
_isDurationConnected()
{
if (this._duration === null && this._durationToggle === null) {
return false;
}
if (this._durationToggle === null) {
return true;
}
return this._durationToggle.classList.contains('text-success');
}
/**
* @returns {DateTime|null}
* @private
*/
_getBegin()
{
if (this._beginDate.value === '' || this._beginTime.value === '') {
return null;
}
let date = this._parseBegin(this._beginTime.dataset['format']);
if (date.invalid) {
date = this._parseBegin(this._fixTimeFormat(this._beginTime.dataset['format']));
if (date.invalid) {
return null;
}
}
return date;
}
_parseBegin(timeFormat)
{
return this.getDateUtils().fromFormat(
this._beginDate.value + ' ' + this._beginTime.value,
this._beginDate.dataset['format'] + ' ' + timeFormat,
);
}
_parseEnd(endDate, timeFormat)
{
let date = this.getDateUtils().fromFormat(
endDate.toFormat('yyyy-LL-dd') + ' ' + this._endTime.value,
'yyyy-LL-dd ' + timeFormat,
);
if (date.invalid) {
date = this.getDateUtils().fromFormat(
endDate.toFormat('yyyy-LL-dd') + ' ' + this._endTime.value,
'yyyy-LL-dd ' + this._fixTimeFormat(timeFormat),
);
}
return date;
}
_fixTimeFormat(format)
{
return format.replace('HH', 'H').replace('hh', 'h');
}
/**
* @returns {DateTime|null}
* @private
*/
_getEnd()
{
if (this._endTime.value === '') {
return null;
}
let date = this._parseEnd(DateTime.now(), this._endTime.dataset['format']);
const begin = this._getBegin();
if (begin !== null) {
date = this._parseEnd(begin, this._endTime.dataset['format']);
if (date < begin) {
date = date.plus({days: 1});
}
}
if (date.invalid) {
return null;
}
return date;
}
/**
* Ruleset:
* - invalid begin => skip
* - empty end => set end to begin (only if duration > 0 = running record)
* - invalid end => skip
* - calculate duration
*/
_changedBegin()
{
const begin = this._getBegin();
if (begin === null) {
return;
}
const duration = this._getParsedDuration();
const hasDuration = duration.as('seconds') > 0;
const end = this._getEnd();
if (end === null && hasDuration) {
this._applyDateToField(begin.plus(duration), null, this._endTime);
} else {
this._updateDuration();
}
}
/**
* Ruleset:
* - invalid end => skip
* - empty begin => set begin to end
* - invalid begin => skip
* - calculate duration
*/
_changedEnd()
{
const end = this._getEnd();
// empty or invalid date => reset duration and stop progress
if (end === null) {
return;
}
const duration = this._getParsedDuration();
const hasDuration = duration.as('seconds') > 0;
const begin = this._getBegin();
if (begin === null && hasDuration) {
this._applyDateToField(end.minus(duration), this._beginDate, this._beginTime);
} else {
this._updateDuration();
}
}
/**
* @private
*/
_updateDuration()
{
const begin = this._getBegin();
const end = this._getEnd();
let newDuration = null;
if (begin !== null && end !== null) {
newDuration = end.diff(begin);
}
this._setDurationAsString(newDuration);
}
/**
* Ruleset:
* - invalid or empty duration => skip
* - if begin and end are empty: set begin to now and end to duration
* - if begin is empty and end is not empty: set begin to end minus duration
* - if begin is not empty and end is empty and duration is > 0 (running records = 0): set end to begin plus duration
*/
_changedDuration()
{
if (!this._isDurationConnected() || this._duration.value === '') {
return;
}
const duration = this._getParsedDuration();
if (!duration.isValid) {
this._setDurationAsString(null);
return;
}
const begin = this._getBegin();
let end = this._getEnd();
const seconds = duration.as('seconds');
if (seconds < 0) {
end = null;
}
if (begin === null && end === null) {
const newBegin = DateTime.now();
this._applyDateToField(newBegin, this._beginDate, this._beginTime);
this._addSecondsToEndDate(newBegin, seconds);
} else if (begin === null && end !== null) {
this._applyDateToField(end.minus({seconds: seconds}), this._beginDate, this._beginTime);
} else if (begin !== null && seconds >= 0) {
this._addSecondsToEndDate(begin, seconds);
}
}
/**
* Writes the value of a duration object as human-readable string into the duration field
*
* @param {Duration|null} duration
*/
_setDurationAsString(duration)
{
if (!this._isDurationConnected()) {
return;
}
if (duration === null) {
this._duration.value = '';
return;
}
if (!duration.isValid) {
return;
}
const seconds = duration.as('seconds');
if (seconds < 0) {
this._duration.value = '';
return;
}
const hours = Math.floor(seconds / 3600);
let minutes = Math.floor((seconds - (hours * 3600)) / 60);
if (minutes < 10) {
minutes = '0' + minutes;
}
this._duration.value = hours + ':' + minutes;
}
/**
* Returns a duration object from the duration input field.
*
* @private
* @return {Duration}
*/
_getParsedDuration()
{
return this.getDateUtils().parseDuration(this._duration.value);
}
/**
* @param {DateTime} dateTime
* @param {int} seconds
* @private
*/
_addSecondsToEndDate(dateTime, seconds)
{
// if the duration is longer than one day, the end field should be empty
// so kimai can calculate it after submitting the data from start + duration
if (seconds < 86400) {
this._applyDateToField(dateTime.plus({seconds: seconds}), null, this._endTime);
} else {
this._endTime.value = '';
}
}
/**
* @param {DateTime|null} dateTime
* @param {HTMLElement|null} dateField
* @param {HTMLElement} timeField
* @private
*/
_applyDateToField(dateTime, dateField, timeField)
{
if (dateTime === null || dateTime.invalid) {
dateField.value = '';
timeField.value = '';
return;
}
if (dateField !== null) {
dateField.value = this.getDateUtils().format(dateField.dataset['format'], dateTime);
}
timeField.value = this.getDateUtils().format(timeField.dataset['format'], dateTime);
}
/**
* @param {KeyboardEvent} event
* @private
*/
_changeDurationOnKeypress(event)
{
switch (event.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'PageUp':
case 'PageDown':
case 'Home':
case 'End':
this._setDurationAsString(this._getParsedDuration());
break;
default:
return; // Ignore other keys
}
this._changeTimeOnKeypress(event, this._duration, 99999, this._durationListener);
}
/**
* This method helps the user to change a duration field with simple keyboard interaction:
* - Read the current duration from the given timeField input in format HH:MM (no seconds)
* - Change the duration based on the rules below
* - Write the new duration back to the field
* - If the field is empty or invalid it uses 00:00 as start-time
* - Duration cannot exceed maxtime (which is given in minutes)
* - Duration cannot drop below 00:00
* - Read the position of the cursor and decide whether to increase minutes or hours: if the cursor is in the hour section (before the colon) change hours, if the cursor is in the minute section (after the colon) change minutes
* - It reads the pressed key from the given KeyboardEvent and changes the duration accordingly to the rules below
*
* Rules to apply when a key is pressed:
* - ArrowUp key to increase the duration (either 5 minutes or 1 hour, depending on the cursor position)
* - ArrowDown key to decrease the duration (either 5 minutes or 1 hour, depending on the cursor position)
* - PageUp key to increase the duration by 1 hour
* - PageDown key to decrease the duration by 1 hour
* - Home key to set the duration to 08:00
* - End key to set the duration to 00:00
* - all other keys are ignored
*
* @param {KeyboardEvent} event
* @param {HTMLElement} timeField
* @param {int} maxTime
* @param {function} changeCallback
* @private
*/
_changeTimeOnKeypress(event, timeField, maxTime, changeCallback)
{
// Parse current value or default to 00:00
let value = timeField.value || '00:00';
let [hours, minutes] = value.split(':').map(Number);
if (isNaN(hours)) { hours = 0; }
if (isNaN(minutes)) { minutes = 0; }
// Cursor position: before or after colon
const cursorPos = timeField.selectionStart || 0;
const colonPos = value.indexOf(':');
const inHour = cursorPos <= colonPos;
// Helper to clamp values
const clamp = (h, m) => {
let total = h * 60 + m;
if (total < 0) { total = 0; }
if (total > maxTime) { total = maxTime; }
h = Math.floor(total / 60);
m = total % 60;
return [h, m];
};
switch (event.key) {
case 'ArrowUp':
if (inHour) {
[hours, minutes] = clamp(hours + 1, minutes);
} else {
[hours, minutes] = clamp(hours, minutes + 5);
}
break;
case 'ArrowDown':
if (inHour) {
[hours, minutes] = clamp(hours - 1, minutes);
} else {
[hours, minutes] = clamp(hours, minutes - 5);
}
break;
case 'PageUp':
[hours, minutes] = clamp(hours + 1, minutes);
event.preventDefault();
break;
case 'PageDown':
[hours, minutes] = clamp(hours - 1, minutes);
event.preventDefault();
break;
case 'Home':
// TODO this should use the configured working time for today
hours = 8;
minutes = 0;
event.preventDefault();
break;
case 'End':
hours = 0;
minutes = 0;
event.preventDefault();
break;
default:
return; // Ignore other keys
}
// Format and set value
timeField.value = `${hours}:${minutes.toString().padStart(2, '0')}`;
// trigger update of linked fields
changeCallback(timeField);
// Move cursor to original position if possible
setTimeout(() => {
timeField.setSelectionRange(cursorPos, cursorPos);
}, 0);
}
}

View File

@@ -9,7 +9,6 @@
* [KIMAI] KimaiAPI: easy access to API methods
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiAPI extends KimaiPlugin {
@@ -18,135 +17,161 @@ export default class KimaiAPI extends KimaiPlugin {
return 'api';
}
_headers() {
const headers = new Headers();
headers.append('Content-Type', 'application/json');
return headers;
}
get(url, data, callbackSuccess, callbackError) {
jQuery.ajax({
url: url,
headers: {
'X-AUTH-SESSION': true,
'Content-Type':'application/json'
},
if (data !== undefined) {
const params = (new URLSearchParams(data)).toString();
if (params !== '') {
url = url + (url.includes('?') ? '&' : '?') + params;
}
}
if (callbackError === undefined) {
callbackError = (error) => {
this.handleError('An error occurred', error);
};
}
this.fetch(url, {
method: 'GET',
data: data,
dataType: 'json',
success: callbackSuccess,
error: callbackError
headers: this._headers()
}).then((response) => {
response.json().then((json) => {
callbackSuccess(json);
});
}).catch((error) => {
callbackError(error);
});
}
post(url, data, callbackSuccess, callbackError) {
if (callbackError === null || callbackError === undefined) {
callbackError = this.getPostErrorHandler();
if (callbackError === undefined) {
callbackError = (error) => {
this.handleError('action.update.error', error);
};
}
jQuery.ajax({
url: url,
headers: {
'X-AUTH-SESSION': true,
'Content-Type':'application/json'
},
this.fetch(url, {
method: 'POST',
data: data,
dataType: 'json',
success: callbackSuccess,
error: callbackError
body: this._parseData(data),
headers: this._headers()
}).then((response) => {
response.json().then((json) => {
callbackSuccess(json);
});
}).catch((error) => {
callbackError(error);
});
}
patch(url, data, callbackSuccess, callbackError) {
if (callbackError === null || callbackError === undefined) {
callbackError = this.getPatchErrorHandler();
if (callbackError === undefined) {
callbackError = (error) => {
this.handleError('action.update.error', error);
};
}
jQuery.ajax({
url: url,
headers: {
'X-AUTH-SESSION': true,
'Content-Type':'application/json'
},
this.fetch(url, {
method: 'PATCH',
data: data,
dataType: 'json',
success: callbackSuccess,
error: callbackError
body: this._parseData(data),
headers: this._headers()
}).then((response) => {
if (response.statusCode === 204) {
callbackSuccess();
} else {
response.json().then((json) => {
callbackSuccess(json);
});
}
}).catch((error) => {
callbackError(error);
});
}
delete(url, callbackSuccess, callbackError) {
if (callbackError === null || callbackError === undefined) {
callbackError = this.getDeleteErrorHandler();
if (callbackError === undefined) {
callbackError = (error) => {
this.handleError('action.delete.error', error);
};
}
jQuery.ajax({
url: url,
headers: {
'X-AUTH-SESSION': true,
'Content-Type':'application/json'
},
this.fetch(url, {
method: 'DELETE',
dataType: 'json',
success: callbackSuccess,
error: callbackError
headers: this._headers()
}).then(() => {
callbackSuccess();
}).catch((error) => {
callbackError(error);
});
}
getDeleteErrorHandler() {
const self = this;
return function(xhr, err) {
self.handleError('action.delete.error', xhr, err);
};
}
/**
* @param {string|object} data
* @returns {string}
* @private
*/
_parseData(data) {
if (typeof data === 'object') {
return JSON.stringify(data);
}
getPatchErrorHandler() {
const self = this;
return function(xhr, err) {
self.handleError('action.update.error', xhr, err);
};
}
getPostErrorHandler() {
const self = this;
return function(xhr, err) {
self.handleError('action.update.error', xhr, err);
};
return data;
}
/**
* @param {string} message
* @param {jqXHR} xhr
* @param {string} err
* @param {Response} response
*/
handleError(message, xhr, err) {
let resultError = err;
if (xhr.responseJSON && xhr.responseJSON.message) {
resultError = xhr.responseJSON.message;
// find validation errors
if (xhr.status === 400 && xhr.responseJSON.errors) {
let collected = ['<u>' + resultError + '</u>'];
// form errors that are not attached to a field (like extra fields)
if (xhr.responseJSON.errors.errors) {
for (let error of xhr.responseJSON.errors.errors) {
collected.push(error);
handleError(message, response) {
if (response.headers === undefined) {
// this can happen if someone clicks to fast and auto running
// requests (e.g. active records) are aborted
return;
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
response.json().then(data => {
let resultError = data.message;
// find validation errors
if (response.status === 400 && data.errors) {
let collected = ['<u>' + resultError + '</u>'];
// form errors that are not attached to a field (like extra fields)
if (data.errors.errors) {
for (let error of data.errors.errors) {
collected.push(error);
}
}
}
if (xhr.responseJSON.errors.children) {
for (let field in xhr.responseJSON.errors.children) {
let tmpField = xhr.responseJSON.errors.children[field];
if (tmpField.hasOwnProperty('errors') && tmpField.errors.length > 0) {
for (let error of tmpField.errors) {
collected.push(error);
if (data.errors.children) {
for (let field in data.errors.children) {
let tmpField = data.errors.children[field];
if (tmpField.errors !== undefined && tmpField.errors.length > 0) {
for (let error of tmpField.errors) {
collected.push(error);
}
}
}
}
if (collected.length > 0) {
resultError = collected;
}
}
if (collected.length > 0) {
resultError = collected;
}
}
} else if (xhr.status && xhr.statusText) {
resultError = '[' + xhr.status + '] ' + xhr.statusText;
}
this.getPlugin('alert').error(message, resultError);
this.getPlugin('alert').error(message, resultError);
});
} else {
response.text().then(() => {
const resultError = '[' + response.statusCode + '] ' + response.statusText;
this.getPlugin('alert').error(message, resultError);
});
}
}
}

View File

@@ -22,15 +22,14 @@ export default class KimaiAPILink extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
this._selector = selector;
}
init() {
const self = this;
document.addEventListener('click', function(event) {
document.addEventListener('click', (event) => {
let target = event.target;
while (target !== null && !target.matches('body')) {
if (target.classList.contains(self.selector)) {
while (target !== null && typeof target.matches === "function" && !target.matches('body')) {
if (target.classList.contains(this._selector)) {
const attributes = target.dataset;
let url = attributes['href'];
@@ -39,13 +38,13 @@ export default class KimaiAPILink extends KimaiPlugin {
}
if (attributes.question !== undefined) {
self.getContainer().getPlugin('alert').question(attributes.question, function(value) {
this.getContainer().getPlugin('alert').question(attributes.question, (value) => {
if (value) {
self._callApi(url, attributes);
this._callApi(url, attributes);
}
});
} else {
self._callApi(url, attributes);
this._callApi(url, attributes);
}
event.preventDefault();
@@ -57,41 +56,50 @@ export default class KimaiAPILink extends KimaiPlugin {
});
}
/**
* @param {string} url
* @param {DOMStringMap} attributes
* @private
*/
_callApi(url, attributes)
{
const method = attributes['method'];
const eventName = attributes['event'];
/** @type {KimaiAPI} API */
const API = this.getContainer().getPlugin('api');
const eventing = this.getContainer().getPlugin('event');
const alert = this.getContainer().getPlugin('alert');
const successHandle = function(result) {
eventing.trigger(eventName);
if (attributes.msgSuccess) {
alert.success(attributes.msgSuccess);
}
/** @type {KimaiEvent} EVENTS */
const EVENTS = this.getContainer().getPlugin('event');
/** @type {KimaiAlert} ALERT */
const ALERT = this.getContainer().getPlugin('alert');
const successHandle = () => {
EVENTS.trigger(eventName);
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
};
const errorHandle = function(xhr, err) {
const errorHandle = (error) => {
let message = 'action.update.error';
if (attributes.msgError) {
message = attributes.msgError;
if (attributes['msgError'] !== undefined) {
message = attributes['msgError'];
}
API.handleError(message, xhr, err);
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
API.handleError(message, error);
};
let data = {};
if (attributes['payload'] !== undefined) {
data = attributes['payload'];
}
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
if (method === 'PATCH') {
let data = {};
if (attributes.payload) {
data = attributes.payload;
}
API.patch(url, data, successHandle, errorHandle);
} else if (method === 'POST') {
let data = {};
if (attributes.payload) {
data = attributes.payload;
}
API.post(url, data, successHandle, errorHandle);
} else if (method === 'DELETE') {
API.delete(url, successHandle, errorHandle);
} else if (method === 'GET') {
API.get(url, data, successHandle, errorHandle);
}
}

View File

@@ -13,105 +13,221 @@ import KimaiPlugin from '../KimaiPlugin';
export default class KimaiActiveRecords extends KimaiPlugin {
constructor(selector, selectorEmpty) {
constructor()
{
super();
this.selector = selector;
this.selectorEmpty = selectorEmpty;
this._selector = '.ticktac-menu';
this._selectorEmpty = '.ticktac-menu-empty';
this._favIconUrl = null;
}
getId() {
/**
* @returns {string}
*/
getId()
{
return 'active-records';
}
init() {
const menu = document.querySelector(this.selector);
init()
{
// the menu can be hidden if user has no permissions to see it
if (menu === null) {
if (document.querySelector(this._selector) === null) {
return;
}
const dropdown = menu.querySelector('ul.dropdown-menu');
const handleUpdate = () => {
this.reloadActiveRecords();
};
this.attributes = dropdown.dataset;
this.itemList = dropdown.querySelector('li > ul.menu');
this.label = menu.querySelector('a > span.label');
document.addEventListener('kimai.timesheetUpdate', handleUpdate);
document.addEventListener('kimai.timesheetDelete', handleUpdate);
document.addEventListener('kimai.activityUpdate', handleUpdate);
document.addEventListener('kimai.activityDelete', handleUpdate);
document.addEventListener('kimai.projectUpdate', handleUpdate);
document.addEventListener('kimai.projectDelete', handleUpdate);
document.addEventListener('kimai.customerUpdate', handleUpdate);
document.addEventListener('kimai.customerDelete', handleUpdate);
const self = this;
const handle = function() { self.reloadActiveRecords(); };
// -----------------------------------------------------------------------
// handle duration in the visible UI
this._updateBrowserTitle = !!this.getConfiguration('updateBrowserTitle');
const handle = () => {
this._updateDuration();
};
this._updatesHandler = setInterval(handle, 10000);
document.addEventListener('kimai.timesheetUpdate', handle);
document.addEventListener('kimai.timesheetDelete', handle);
document.addEventListener('kimai.activityUpdate', handle);
document.addEventListener('kimai.activityDelete', handle);
document.addEventListener('kimai.projectUpdate', handle);
document.addEventListener('kimai.projectDelete', handle);
document.addEventListener('kimai.customerUpdate', handle);
document.addEventListener('kimai.customerDelete', handle);
document.addEventListener('kimai.reloadedContent', handle);
}
emptyList() {
this.itemList.innerHTML = '';
// TODO we could unregister all handler and listener
// _unregisterHandler() {
// clearInterval(this._updatesHandler);
// }
/**
* Updates the duration of all running entries, both in the ticktac menus and in the listing pages.
*
* @private
*/
_updateDuration()
{
// needs to search in document, to find all running entries, both in "ticktac" and listing pages
const activeRecords = document.querySelectorAll('[data-since]:not([data-since=""])');
if (this._updateBrowserTitle) {
this._changeFavicon(activeRecords.length > 0);
}
if (activeRecords.length === 0) {
if (this._updateBrowserTitle) {
if (document.body.dataset['title'] === undefined) {
this._updateBrowserTitle = false;
} else {
document.title = document.body.dataset['title'];
}
}
return;
}
const DATE = this.getDateUtils();
let durations = [];
for (const record of activeRecords) {
const duration = DATE.formatDuration(record.dataset['since']);
// only use the ones from the menu for the title
if (record.dataset['replacer'] !== undefined && record.dataset['title'] !== null && duration !== '?') {
durations.push(duration);
}
// but update all on the page (running entries in list pages)
record.textContent = duration;
}
if (durations.length === 0) {
return;
}
if (this._updateBrowserTitle) {
// only show the first found record, even if we have more
document.title = durations.shift();
}
}
_toggleMenu(hasEntries) {
const menu = document.querySelector(this.selector);
const menuEmpty = document.querySelector(this.selectorEmpty);
/**
* Adapts the ticktac menus according to the given entries (amount and duration).
* Does not influence listing pages, as those refresh themselves.
*
* @param {array} entries
* @private
*/
_setEntries(entries)
{
const hasEntries = entries.length > 0;
menu.style.display = hasEntries ? 'inline-block' : 'none';
if (menuEmpty !== null) {
// these contain the "start" button
for (let menuEmpty of document.querySelectorAll(this._selectorEmpty)) {
menuEmpty.style.display = !hasEntries ? 'inline-block' : 'none';
}
// and they contain the "stop" button
for (let menu of document.querySelectorAll(this._selector)) {
menu.style.display = hasEntries ? 'inline-block' : 'none';
if (!hasEntries) {
// make sure that template entries in the menu are removed, otherwise they
// might still be shown in the browsers title
for (let record of menu.querySelectorAll('[data-since]')) {
record.dataset['since'] = '';
}
}
const stop = menu.querySelector('.ticktac-stop');
if (!hasEntries) {
if (stop) {
stop.accesskey = null;
}
continue;
}
if (stop) {
stop.accesskey = 's';
}
this._replaceInNode(menu, entries[0]);
}
this._updateDuration();
}
setEntries(entries) {
this._toggleMenu(entries.length > 0);
if (entries.length === 0) {
this.label.innerText = '';
this.emptyList();
return;
/**
* @param {HTMLElement} node
* @param {object} timesheet
* @private
*/
_replaceInNode(node, timesheet)
{
const date = this.getDateUtils();
const allReplacer = node.querySelectorAll('[data-replacer]');
for (let link of allReplacer) {
const replacerName = link.dataset['replacer'];
if (replacerName === 'url') {
link.dataset['href'] = node.dataset['href'].replace('000', timesheet.id);
} else if (replacerName === 'activity') {
link.innerText = timesheet.activity.name;
} else if (replacerName === 'project') {
link.innerText = timesheet.project.name;
} else if (replacerName === 'customer') {
link.innerText = timesheet.project.customer.name;
} else if (replacerName === 'duration') {
link.dataset['since'] = timesheet.begin;
link.innerText = date.formatDuration(timesheet.duration);
}
}
let htmlToInsert = '';
const durations = this.getContainer().getPlugin('timesheet-duration');
for (let timesheet of entries) {
htmlToInsert +=
`<li>` +
`<a href="${ this.attributes['href'].replace('000', timesheet.id) }" data-event="kimai.timesheetStop kimai.timesheetUpdate" class="api-link" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">` +
`<div class="pull-left">` +
`<i class="${ this.attributes['icon'] } fa-2x"></i>` +
`</div>` +
`<h4>` +
`<span>${ timesheet.activity.name }</span>` +
`<small>` +
`<span data-title="true" data-since="${ timesheet.begin }">${ durations.formatDuration(timesheet.duration) }</span>` +
`</small>` +
`</h4>` +
`<p>${ timesheet.project.name } (${ timesheet.project.customer.name })</p>` +
`</a>` +
`</li>`;
}
if (this.label.dataset.warning < entries.length) {
this.label.classList = 'label label-danger';
} else {
this.label.classList = 'label label-warning';
}
this.label.innerText = entries.length;
this.itemList.innerHTML = htmlToInsert;
durations.updateRecords();
}
reloadActiveRecords() {
const self = this;
const API= this.getContainer().getPlugin('api');
reloadActiveRecords()
{
/** @type {KimaiAPI} API */
const API = this.getContainer().getPlugin('api');
API.get(this.attributes['api'], {}, function(result) {
self.setEntries(result);
// TODO using the first found "ticktac" menu is working, but can be done better
const apiUrl = document.querySelector(this._selector).dataset['api'];
API.get(apiUrl, {}, (result) => {
this._setEntries(result);
});
}
/**
* @param {boolean} running
* @private
*/
_changeFavicon(running)
{
const canvas = document.createElement('canvas');
const orig = document.getElementById('favicon');
if (this._favIconUrl === null) {
this._favIconUrl = orig.href;
}
const link = orig.cloneNode(true);
if (canvas.getContext && link) {
const ratio = window.devicePixelRatio;
const img = document.createElement('img');
canvas.height = canvas.width = 16 * ratio;
img.onload = function () {
const ctx = canvas.getContext('2d');
ctx.drawImage(this, 0, 0, canvas.width, canvas.height);
if (running) {
const width = 5.5 * ratio;
ctx.fillStyle = 'rgb(182,57,57)';
ctx.fillRect((canvas.width / 2) - (width / 2), (canvas.height / 2) - (width / 2), width, width);
}
link.href = canvas.toDataURL('image/png');
orig.remove();
document.head.appendChild(link);
};
img.src = this._favIconUrl;
}
}
}

View File

@@ -1,80 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiActiveRecordsDuration: activate the updates for all active timesheet records on this page
*/
import moment from 'moment';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiActiveRecordsDuration extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'timesheet-duration';
}
init() {
this.updateBrowserTitle = !!this.getConfiguration('updateBrowserTitle');
this.updateRecords();
const self = this;
const handle = function() { self.updateRecords(); };
this._updatesHandler = setInterval(handle, 10000);
// this will probably not work as expected, as other event-handler might need longer to update the DOM
document.addEventListener('kimai.timesheetUpdate', handle);
}
unregisterUpdates() {
clearInterval(this._updatesHandler);
}
updateRecords() {
let durations = [];
const activeRecords = document.querySelectorAll(this.selector);
if (activeRecords.length === 0) {
if (this.updateBrowserTitle) {
document.title = document.querySelector('body').dataset['title'];
}
return;
}
for(let record of activeRecords) {
const since = record.getAttribute('data-since');
const duration = this.formatDuration(since);
if (record.getAttribute('data-title') !== null && duration !== '?') {
durations.push(duration);
}
record.textContent = duration;
}
if (durations.length === 0) {
return;
}
if (!this.updateBrowserTitle) {
return;
}
let title = durations.shift();
let prefix = ' | ';
for (let duration of durations.slice(0, 2)) {
title += prefix + duration;
}
document.title = title;
}
formatDuration(since) {
return this.getPlugin('date').formatDuration(since);
}
}

View File

@@ -12,78 +12,96 @@
* opening a modal with the content from the URL given in the elements 'data-href' or 'href' attribute
*/
import jQuery from 'jquery';
import KimaiReducedClickHandler from "./KimaiReducedClickHandler";
import { Modal } from 'bootstrap';
export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
constructor(selector) {
constructor(selector, stopSelector) {
super();
this.selector = selector;
this._selector = selector;
this._stopSelector = stopSelector;
}
getId() {
getId()
{
return 'modal';
}
init() {
const self = this;
this.isDirty = false;
init()
{
this._isDirty = false;
this.modal = jQuery('#remote_form_modal');
this.modal.on('hide.bs.modal', function (e) {
if (self.isDirty) {
if (jQuery('#remote_form_modal .modal-body .remote_modal_is_dirty_warning').length === 0) {
const msg = self.getContainer().getTranslation().get('modal.dirty');
jQuery('#remote_form_modal .modal-body').prepend('<p class="'+(self.modal.hasClass('modal-danger') ? 'well well-sm ' : '') + 'text-danger small remote_modal_is_dirty_warning">' + msg + '</p>');
}
e.preventDefault();
return;
}
jQuery(self._getFormIdentifier()).off('change', self._isDirtyHandler);
self.isDirty = false;
self.getContainer().getPlugin('event').trigger('modal-hide');
});
this.modal.on('hidden.bs.modal', function () {
// kill all references, so GC can kick in
self.getContainer().getPlugin('form').destroyForm(self._getFormIdentifier());
jQuery('#remote_form_modal .modal-body').replaceWith('');
});
this.modal.on('show.bs.modal', function () {
self.getContainer().getPlugin('event').trigger('modal-show');
});
this.modal.on('shown.bs.modal', function () {
// workaround for autofocus attribute, as the modal "steals" it
let formAutofocus = jQuery(self._getFormIdentifier()).find('[autofocus]');
if (formAutofocus.length < 1) {
formAutofocus = jQuery(self._getFormIdentifier()).find('input[type=text],textarea,select');
}
formAutofocus.filter(':not("[data-datetimepicker=on]")').filter(':visible:first').focus().delay(1000).focus();
});
this._addClickHandler(this.selector, function(href) {
self.openUrlInModal(href);
});
}
openUrlInModal(url, errorHandler) {
const self = this;
if (errorHandler === undefined) {
errorHandler = function(xhr, err) {
if (xhr.status === undefined || xhr.status !== 403) {
window.location = url;
}
};
const modalElement = this._getModalElement();
if (modalElement === null) {
return;
}
jQuery.ajax({
url: url,
success: function(html) {
self._openFormInModal(html);
},
error: errorHandler
modalElement.addEventListener('hide.bs.modal', (event) => {
if (this._isDirty) {
if (modalElement.querySelector('.modal-body .remote_modal_is_dirty_warning') === null) {
const msg = this.translate('modal.dirty');
const temp = document.createElement('div');
temp.innerHTML = '<p class="text-danger small remote_modal_is_dirty_warning">' + msg + '</p>';
modalElement.querySelector('.modal-body').prepend(temp.firstElementChild);
}
event.preventDefault();
return;
}
this._isDirty = false;
document.dispatchEvent(new Event('modal-hide'));
});
modalElement.addEventListener('hidden.bs.modal', () => {
// kill all references, so GC can kick in
this.getContainer().getPlugin('form').destroyForm(this._getFormIdentifier());
modalElement.querySelector('.modal-body').replaceWith('');
});
modalElement.addEventListener('show.bs.modal', () => {
document.dispatchEvent(new Event('modal-show'));
});
this.addClickHandler(this._selector, (href) => {
this.openUrlInModal(href);
}, this._stopSelector);
}
_getModal()
{
return Modal.getOrCreateInstance(this._getModalElement());
}
/**
* @param {string} url
* @param {function(Response)} error the callback to execute if the fetch failed
*/
openUrlInModal(url, error)
{
const headers = new Headers();
headers.append('X-Requested-With', 'Kimai-Modal');
this.fetch(url, {
method: 'GET',
redirect: 'follow',
headers: headers
})
.then(response => {
if (!response.ok) {
window.location = url;
return;
}
return response.text().then(html => {
this._openFormInModal(html);
});
})
.catch((reason) => {
if (error === undefined || error === null) {
window.location = url;
} else {
error(reason);
}
});
}
@@ -93,141 +111,178 @@ export default class KimaiAjaxModalForm extends KimaiReducedClickHandler {
* @returns {string}
* @private
*/
_getFormIdentifier() {
_getFormIdentifier()
{
return '#remote_form_modal .modal-content form';
}
_openFormInModal(html) {
const self = this;
/**
* @returns {HTMLElement|null}
* @private
*/
_getModalElement()
{
return document.getElementById('remote_form_modal');
}
let formIdentifier = this._getFormIdentifier();
// if any of these is found in a response, the form will be re-displayed
let flashErrorIdentifier = 'div.alert-error';
// messages to show above the form
let flashMessageIdentifier = 'div.alert';
let form = jQuery(formIdentifier);
let remoteModal = this.modal;
/**
* @param {Element|ChildNode} node
* @returns {Element}
* @private
*/
_makeScriptExecutable(node) {
if (node.tagName !== undefined && node.tagName === 'SCRIPT') {
const script = document.createElement('script');
script.text = node.innerHTML;
node.parentNode.replaceChild(script, node);
} else {
for (const child of node.childNodes) {
this._makeScriptExecutable(child);
}
}
// will be (re-)activated later
form.off('submit');
return node;
}
_openFormInModal(html)
{
const formIdentifier = this._getFormIdentifier();
let remoteModal = this._getModalElement();
const newFormHtml = document.createElement('div');
newFormHtml.innerHTML = html;
const newModalContent = this._makeScriptExecutable(newFormHtml.querySelector('#form_modal .modal-content'));
// load new form from given content
if (jQuery(html).find('#form_modal .modal-content').length > 0) {
// switch classes, in case the modal type changed
remoteModal.on('hidden.bs.modal', function () {
if (remoteModal.hasClass('modal-danger')) {
remoteModal.removeClass('modal-danger');
}
});
if (newModalContent !== null) {
// Support changing modal sizes
let modalDialog = remoteModal.querySelector('.modal-dialog');
let largeModal = newFormHtml.querySelector('.modal-dialog').classList.contains('modal-lg');
if (jQuery(html).find('#form_modal').hasClass('modal-danger')) {
remoteModal.addClass('modal-danger');
if (largeModal && !modalDialog.classList.contains('modal-lg')) {
modalDialog.classList.toggle('modal-lg');
}
jQuery('#remote_form_modal .modal-content').replaceWith(
jQuery(html).find('#form_modal .modal-content')
);
if (!largeModal && modalDialog.classList.contains('modal-lg')) {
modalDialog.classList.toggle('modal-lg');
}
jQuery('#remote_form_modal [data-dismiss=modal]').on('click', function() {
self.isDirty = false;
remoteModal.querySelector('.modal-content').replaceWith(newModalContent);
[].slice.call(remoteModal.querySelectorAll('[data-bs-dismiss="modal"]')).map((element) => {
element.addEventListener('click', () => {
this._isDirty = false;
this._getModal().hide();
});
});
// activate new loaded widgets
self.getContainer().getPlugin('form').activateForm(formIdentifier);
this.getContainer().getPlugin('form').activateForm(formIdentifier);
}
// show error flash messages
let flashMessages = jQuery(html).find(flashMessageIdentifier);
if (flashMessages.length > 0) {
jQuery('#remote_form_modal .modal-body').prepend(flashMessages);
let flashMessages = newFormHtml.querySelector('div.alert');
if (flashMessages !== null) {
remoteModal.querySelector('.modal-body').prepend(flashMessages);
}
// -----------------------------------------------------------------------
// a fix for firefox focus problems with datepicker in modal
// see https://github.com/kevinpapst/kimai2/issues/618
let enforceModalFocusFn = jQuery.fn.modal.Constructor.prototype.enforceFocus;
jQuery.fn.modal.Constructor.prototype.enforceFocus = function() {};
remoteModal.on('hidden.bs.modal', function () {
jQuery.fn.modal.Constructor.prototype.enforceFocus = enforceModalFocusFn;
});
// -----------------------------------------------------------------------
remoteModal.modal('show');
// the new form that was loaded via ajax
form = jQuery(formIdentifier);
const form = document.querySelector(formIdentifier);
this._isDirtyHandler = function(e) {
self.isDirty = true;
}
form.on('change', this._isDirtyHandler);
form.addEventListener('change', () => {
this._isDirty = true;
});
// click handler for modal save button, to send forms via ajax
form.on('submit', function(event){
const btn = jQuery(formIdentifier + ' button[type=submit]').button('loading');
const eventName = form.attr('data-form-event');
const events = self.getContainer().getPlugin('event');
const alert = self.getContainer().getPlugin('alert');
form.addEventListener('submit', this._getEventHandler());
event.preventDefault();
event.stopPropagation();
this._getModal().show();
}
jQuery.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: form.serialize(),
success: function(html) {
btn.button('reset');
let hasFieldError = jQuery(html).find('#form_modal .modal-content .has-error').length > 0;
let hasFormError = jQuery(html).find('#form_modal .modal-content ul.list-unstyled li.text-danger').length > 0;
let hasFlashError = jQuery(html).find(flashErrorIdentifier).length > 0;
_getEventHandler()
{
if (this.eventHandler === undefined) {
this.eventHandler = (event) => {
const form = event.target;
if (hasFieldError || hasFormError || hasFlashError) {
self._openFormInModal(html);
} else {
events.trigger(eventName);
// try to find form defined messages first ...
let msg = form.attr('data-msg-success');
if (msg === null || msg === undefined) {
// ... but if none was available, check the response to find server rendered flash-message
let flashMessage = jQuery(html).find('section.content div.row div.alert.alert-success');
if (flashMessage.length > 0) {
let flashContent = flashMessage.contents();
if (flashContent.length === 3) {
msg = flashContent[2].textContent;
}
}
}
// ... and if even that is not available, we use a generic fallback message
if (msg === null || msg === undefined) {
msg = 'action.update.success';
}
self.isDirty = false;
remoteModal.modal('hide');
alert.success(msg);
}
return false;
},
error: function(xhr, err) {
let message = form.attr('data-msg-error');
if (message === null || message === undefined) {
message = 'action.update.error';
}
if (xhr.responseJSON && xhr.responseJSON.message) {
err = xhr.responseJSON.message;
} else if (xhr.status && xhr.statusText) {
err = '[' + xhr.status +'] ' + xhr.statusText;
}
alert.error(message, err);
// this is useful for changing form fields and retrying to save (and in development to test form changes)
setTimeout(function() {
btn.button('reset');
}, 1500);
// if the form has a target, we let the normal HTML flow happen
if (form.target !== undefined && form.target !== '') {
return true;
}
});
});
// otherwise we do some AJAX magic to process the form in the background
/** @type {HTMLButtonElement} btn */
const btn = document.querySelector(this._getFormIdentifier() + ' button[type=submit]');
btn.textContent = btn.textContent + ' …';
btn.disabled = true;
const eventName = form.dataset['formEvent'];
/** @type {KimaiEvent} alert */
const events = this.getContainer().getPlugin('event');
event.preventDefault();
event.stopPropagation();
const headers = new Headers();
headers.append('X-Requested-With', 'Kimai-Modal');
const options = {headers: headers};
this.fetchForm(form, options)
.then(response => {
response.text().then((html) => {
/** @type {HTMLDivElement} responseHtml */
const responseHtml = document.createElement('div');
responseHtml.innerHTML = html;
let hasFieldError = false;
let hasFormError = false;
let hasFlashError = false;
// button must be re-enabled anyway
btn.textContent = btn.textContent.replace(' …', '');
btn.disabled = false;
// if the request was successful, there will be no form
/** @type {Element} modalContent */
const modalContent = responseHtml.querySelector('#form_modal .modal-content');
if (modalContent !== null) {
hasFieldError = modalContent.querySelector('.is-invalid') !== null;
if (!hasFieldError) {
// happens when an error occurs for a "hidden or non-classical" form element e.g. creating team without users
hasFieldError = modalContent.querySelector('.invalid-feedback') !== null;
}
hasFormError = modalContent.querySelector('ul.list-unstyled li.text-danger') !== null;
hasFlashError = responseHtml.querySelector('div.alert-danger') !== null;
}
if (hasFieldError || hasFormError || hasFlashError) {
this._openFormInModal(html);
} else {
events.trigger(eventName);
this._isDirty = false;
this._getModal().hide();
}
});
})
.catch(error => {
let message = form.dataset['msgError'];
if (message === null || message === undefined || message === '') {
message = 'action.update.error';
}
/** @type {KimaiAlert} alert */
const alert = this.getContainer().getPlugin('alert');
alert.error(message, error.message);
// this is useful for changing form fields and retrying to save (and in development to test form changes)
setTimeout(() =>{
// critical error, allow to re-submit?
btn.textContent = btn.textContent.replace(' …', '');
btn.disabled = false;
}, 1500);
});
};
}
return this.eventHandler;
}
}

View File

@@ -9,108 +9,261 @@
* [KIMAI] KimaiAlert: notifications for Kimai
*/
import Swal from 'sweetalert2'
import KimaiPlugin from "../KimaiPlugin";
import {Modal, Toast} from "bootstrap";
export default class KimaiAlert extends KimaiPlugin {
/**
* @return {string}
*/
getId() {
return 'alert';
}
/**
* @param {string} title
* @param {string|array} message
* @param {string|array|undefined} message
*/
error(title, message) {
const translation = this.getContainer().getTranslation();
const translation = this.getTranslation();
if (translation.has(title)) {
title = translation.get(title);
}
if (translation.has(message)) {
message = translation.get(message);
title = title.replace('%reason%', '');
if (message === undefined) {
message = null;
}
if (Array.isArray(message)) {
Swal.fire({
icon: 'error',
title: title.replace('%reason%', ''),
html: message.join('<br>'),
});
} else {
Swal.fire({
icon: 'error',
title: title.replace('%reason%', ''),
text: message,
});
if (message !== null) {
if (translation.has(message)) {
message = translation.get(message);
}
if (Array.isArray(message)) {
message = message.join('<br>');
}
}
const id = 'alert_global_error';
const oldModalElement = document.getElementById(id);
if (oldModalElement !== null) {
Modal.getOrCreateInstance(oldModalElement).hide();
}
const html = `
<div class="modal" id="` + id + `" tabindex="-1" role="dialog">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-status bg-` + this._mapClass('danger') + `"></div>
<div class="modal-body text-center py-4">
<i class="fas fa-exclamation-circle fa-3x mb-3 text-danger"></i>
<h2>` + title + `</h2>
` + (message !== null ? '<div class="text-muted">' + message + '</div>' : '') + `
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col text-center"><a href="#" class="btn btn-primary" data-bs-dismiss="modal">` + translation.get('close') + `</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
this._showModal(html);
}
/**
* @param {string} message
*/
warning(message) {
this._show('warning', message);
}
/**
* @param {string} message
*/
success(message) {
this._toast('success', message);
}
/**
* @param {string} message
*/
info(message) {
this._show('info', message);
}
_show(type, message) {
const translation = this.getContainer().getTranslation();
/**
* @param {string} html
* @private
*/
_showModal(html) {
const container = document.body;
const template = document.createElement('template');
template.innerHTML = html.trim();
const element = template.content.firstChild;
container.appendChild(element);
if (translation.has(message)) {
message = translation.get(message);
}
Swal.fire({
icon: type,
title: message,
});
}
_toast(type, message) {
const translation = this.getContainer().getTranslation();
if (translation.has(message)) {
message = translation.get(message);
}
Swal.fire({
timer: 2000,
timerProgressBar: true,
toast: true,
position: 'top',
showConfirmButton: false,
icon: type,
title: message,
const modal = new Modal(element);
element.addEventListener('hidden.bs.modal', function () {
container.removeChild(element);
});
modal.show();
}
/**
* Callback receives a value and needs to decide what should happen with it
* @param {string} type
* @param {string} message
* @private
*/
_show(type, message) {
const translation = this.getTranslation();
if (translation.has(message)) {
message = translation.get(message);
}
const html = `
<div class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-status bg-` + this._mapClass(type) + `"></div>
<div class="modal-body text-center py-4">
<i class="fas fa-exclamation-circle fa-3x mb-3 text-` + this._mapClass(type) + `"></i>
<h2>` + message + `</h2>
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col text-center"><a href="#" class="btn btn-primary" data-bs-dismiss="modal">` + translation.get('close') + `</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
this._showModal(html);
}
/**
* @param {string} type
* @return {string}
* @private
*/
_mapClass(type) {
if (type === 'info' || type === 'success' || type === 'warning' || type === 'danger') {
return type;
} else if (type === 'error') {
return 'danger';
}
return 'primary';
}
/**
* @param type
* @param message
* @private
*/
_toast(type, message) {
const translation = this.getTranslation();
if (translation.has(message)) {
message = translation.get(message);
}
let icon = '<i class="fas fa-info me-2"></i>';
if (type === 'success') {
icon = '<i class="fas fa-check me-2"></i>';
} else if (type === 'warning') {
icon = '<i class="fas fa-exclamation me-2"></i>';
} else if (type === 'danger' || type === 'error') {
icon = '<i class="fas fa-exclamation-circle me-2"></i>';
}
const html =
`<div class="toast align-items-center text-white bg-` + this._mapClass(type) + ` border-0" data-bs-delay="2000" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
` + icon + ' ' + message + `
</div>
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="` + translation.get('close') + `"></button>
</div>
</div>`;
const container = document.getElementById('toast-container');
const template = document.createElement('template');
template.innerHTML = html.trim();
const element = template.content.firstChild;
container.appendChild(element);
const toast = new Toast(element);
element.addEventListener('hidden.bs.toast', function () {
container.removeChild(element);
});
toast.show();
}
/**
* Callback receives a bool value (true = confirm, false = cancel / close without action).
*
* @param message
* @param callback
*/
question(message, callback) {
const translation = this.getContainer().getTranslation();
const translation = this.getTranslation();
if (translation.has(message)) {
message = translation.get(message);
}
Swal.fire({
title: message,
icon: 'question',
showCancelButton: true,
confirmButtonText: translation.get('confirm'),
cancelButtonText: translation.get('cancel')
}).then((result) => {
callback(result.value);
});
}
const css = this._mapClass('info');
const html = `
<div class="modal fade" tabindex="-1" role="dialog" data-bs-backdrop="static">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-status bg-` + css + `"></div>
<div class="modal-body text-center py-4">
<i class="fas fa-question fa-3x mb-3 text-` + css + `"></i>
<h2>` + message + `</h2>
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col"><a href="#" class="question-confirm btn btn-primary w-100" data-bs-dismiss="modal">` + translation.get('confirm') + `</a></div>
<div class="col"><a href="#" class="question-cancel btn w-100" data-bs-dismiss="modal">` + translation.get('cancel') + `</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
const container = document.body;
const template = document.createElement('template');
template.innerHTML = html.trim();
const element = template.content.firstChild;
container.appendChild(element);
element.querySelector('.question-confirm').addEventListener('click', () => {
callback(true);
});
element.querySelector('.question-cancel').addEventListener('click', () => {
callback(false);
});
const modal = new Modal(element);
element.addEventListener('hidden.bs.modal', () => {
container.removeChild(element);
});
modal.show();
}
}

View File

@@ -12,20 +12,19 @@
* redirecting to the URL given in the elements 'data-href' or 'href' attribute
*/
import jQuery from 'jquery';
import KimaiReducedClickHandler from "./KimaiReducedClickHandler";
export default class KimaiAlternativeLinks extends KimaiReducedClickHandler {
constructor(selector) {
super();
this.selector = selector;
this._selector = selector;
}
init() {
this._addClickHandler(this.selector, function(href) {
this.addClickHandler(this._selector, function(href) {
window.location = href;
});
}, []);
}
}

View File

@@ -1,106 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
/**
* Supporting auto-complete fields via API.
* Currently used for timesheet tagging in toolbar and edit dialogs.
*/
export default class KimaiAutocomplete extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
init() {
this.minChars = this.getConfiguration('autoComplete');
}
getId() {
return 'autocomplete';
}
splitTagList(val) {
return val.split(/,\s*/);
}
extractLastTag(term) {
return this.splitTagList(term).pop();
}
activateAutocomplete(selector) {
const self = this;
jQuery(selector + ' ' + this.selector).each(function(index) {
const currentField = jQuery(this);
const apiUrl = currentField.attr('data-autocomplete-url');
const API = self.getContainer().getPlugin('api');
currentField
// don't navigate away from the field on tab when selecting an item
.on("keydown", function (event) {
if (event.keyCode === jQuery.ui.keyCode.TAB &&
jQuery(this).autocomplete("instance").menu.active) {
event.preventDefault();
}
})
.autocomplete({
source: function (request, response) {
const lastEntry = self.extractLastTag(request.term);
API.get(apiUrl, {'name': lastEntry}, function(data){
response(data);
});
},
search: function () {
// custom minLength
var term = self.extractLastTag(this.value);
if (term.length < self.minChars) {
return false;
}
},
focus: function () {
// prevent value inserted on focus
return false;
},
select: function (event, ui) {
var terms = self.splitTagList(this.value);
// remove the current input
terms.pop();
// check if selected tag is already in list
if (!terms.includes(ui.item.value)) {
// add the selected item
terms.push(ui.item.value);
}
// add placeholder to get the comma-and-space at the end
terms.push("");
this.value = terms.join(", ");
$(this).trigger('change');
return false;
}
}
)
;
});
}
destroyAutocomplete(selector) {
jQuery(selector + ' ' + this.selector).each(function(index) {
const currentField = jQuery(this);
currentField.autocomplete("destroy");
currentField.removeData('autocomplete');
});
}
}

View File

@@ -19,15 +19,14 @@ export default class KimaiConfirmationLink extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
this._selector = selector;
}
init() {
const self = this;
document.addEventListener('click', function(event) {
document.addEventListener('click', (event) => {
let target = event.target;
while (target !== null && !target.matches('body')) {
if (target.classList.contains(self.selector)) {
while (target !== null && typeof target.matches === "function" && !target.matches('body')) {
if (target.classList.contains(this._selector)) {
const attributes = target.dataset;
// is this a link?
@@ -44,7 +43,7 @@ export default class KimaiConfirmationLink extends KimaiPlugin {
}
if (attributes.question !== undefined) {
self.getContainer().getPlugin('alert').question(attributes.question, function(value) {
this.getContainer().getPlugin('alert').question(attributes.question, function(value) {
if (value) {
if (form === null) {
document.location = url;

View File

@@ -9,15 +9,15 @@
* [KIMAI] KimaiDatatable: handles functionality for the datatable
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
import KimaiContextMenu from "../widgets/KimaiContextMenu";
export default class KimaiDatatable extends KimaiPlugin {
constructor(contentAreaSelector, tableSelector) {
super();
this.contentArea = contentAreaSelector;
this.selector = tableSelector;
this._contentArea = contentAreaSelector;
this._selector = tableSelector;
}
getId() {
@@ -25,24 +25,21 @@ export default class KimaiDatatable extends KimaiPlugin {
}
init() {
const dataTable = document.querySelector(this.selector);
const dataTable = document.querySelector(this._selector);
// not every page contains a dataTable
if (dataTable === null) {
return;
}
const attributes = dataTable.dataset;
const events = attributes['reloadEvent'];
this.fixDropdowns();
this.registerContextMenu(this._selector);
const events = dataTable.dataset['reloadEvent'];
if (events === undefined) {
return;
}
const self = this;
const handle = function() { self.reloadDatatable(); };
const handle = () => { this.reloadDatatable(); };
for (let eventName of events.split(' ')) {
document.addEventListener(eventName, handle);
@@ -52,55 +49,49 @@ export default class KimaiDatatable extends KimaiPlugin {
document.addEventListener('filter-change', handle);
}
reloadDatatable() {
const self = this;
const contentArea = this.contentArea;
const durations = this.getContainer().getPlugin('timesheet-duration');
const toolbarSelector = this.getContainer().getPlugin('toolbar').getSelector();
const form = jQuery(toolbarSelector);
let loading = '<div class="overlay"><i class="fas fa-sync fa-spin"></i></div>';
jQuery(contentArea).append(loading);
// remove the empty fields to prevent errors
let formData = jQuery(toolbarSelector + ' :input')
.filter(function(index, element) {
return jQuery(element).val() !== '';
})
.serialize();
jQuery.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: formData,
success: function(html) {
jQuery(contentArea).replaceWith(
jQuery(html).find(contentArea)
);
durations.updateRecords();
self.fixDropdowns();
},
error: function(xhr, err) {
form.submit();
}
});
/**
* @param {string} selector
* @private
*/
registerContextMenu(selector)
{
KimaiContextMenu.createForDataTable(selector);
}
/**
* show dropdown menu upwards, if it is outside the visible viewport
*/
fixDropdowns() {
const docHeight = jQuery(document).height();
jQuery(this.selector + ' [data-toggle=dropdown]').each(function() {
const parent = jQuery(this).parent();
const menu = parent.find('.dropdown-menu');
reloadDatatable()
{
const toolbarSelector = this.getContainer().getPlugin('toolbar').getSelector();
if (parent && menu) {
if ((parent.offset().top + parent.outerHeight() + menu.outerHeight()) > docHeight) {
parent.addClass('dropup').removeClass('dropdown');
}
}
/** @type {HTMLFormElement} form */
const form = document.querySelector(toolbarSelector);
const callback = (text) => {
const temp = document.createElement('div');
temp.innerHTML = text;
const newContent = temp.querySelector(this._contentArea);
document.querySelector(this._contentArea).replaceWith(newContent);
this.registerContextMenu(this._selector);
document.dispatchEvent(new Event('kimai.reloadedContent'));
};
document.dispatchEvent(new CustomEvent('kimai.reloadContent', {detail: this._contentArea}));
if (form === null) {
this.fetch(document.location)
.then(response => {
response.text().then(callback);
})
.catch(() => {
document.location.reload();
});
return;
}
this.fetchForm(form)
.then(response => {
response.text().then(callback);
})
.catch(() => {
form.submit();
});
}
}

View File

@@ -9,8 +9,6 @@
* [KIMAI] KimaiDatatableColumnView: manages the visibility of data-table columns in cookies
*/
import Cookies from 'js-cookie';
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiDatatableColumnView extends KimaiPlugin {
@@ -29,80 +27,110 @@ export default class KimaiDatatableColumnView extends KimaiPlugin {
if (dataTable === null) {
return;
}
this.id = dataTable.getAttribute(this.dataAttribute);
this.modal = document.getElementById('modal_' + this.id);
this.bindButtons();
}
bindButtons() {
let self = this;
this.modal.querySelector('button[data-type=save]').addEventListener('click', function() {
self.saveVisibility();
this._id = dataTable.getAttribute(this.dataAttribute);
this._modal = document.getElementById('modal_' + this._id);
this._modal.addEventListener('show.bs.modal', () => {
this._evaluateCheckboxes();
});
this.modal.querySelector('button[data-type=reset]').addEventListener('click', function() {
self.resetVisibility();
this._modal.querySelector('button[data-type=save]').addEventListener('click', () => {
this._saveVisibility();
});
for (let checkbox of this.modal.querySelectorAll('form input[type=checkbox]')) {
checkbox.addEventListener('click', function () {
self.changeVisibility(checkbox.getAttribute('name'), checkbox.checked);
this._modal.querySelector('button[data-type=reset]').addEventListener('click', (event) => {
this._resetVisibility(event.currentTarget);
});
this._modal.querySelectorAll('input[name=datatable_profile]').forEach(element => {
element.addEventListener('change', () => {
const form = this._modal.getElementsByTagName('form')[0];
this.fetchForm(form, {}, element.getAttribute('data-href'))
.then(() => {
// the local storage is read in the login screen to set a cookie,
// which triggers the session switch in ProfileSubscriber
localStorage.setItem('kimai_profile', element.getAttribute('value'));
document.location.reload();
})
.catch(() => {
form.setAttribute('action', element.getAttribute('data-href'));
form.submit();
});
});
});
for (let checkbox of this._modal.querySelectorAll('form input[type=checkbox]')) {
checkbox.addEventListener('change', () => {
this._changeVisibility(checkbox.getAttribute('name'), checkbox.checked);
});
}
}
saveVisibility() {
const form = this.modal.getElementsByTagName('form')[0];
let settings = {};
for (let checkbox of form.querySelectorAll('input[type=checkbox]')) {
settings[checkbox.getAttribute('name')] = checkbox.checked;
_evaluateCheckboxes() {
const form = this._modal.getElementsByTagName('form')[0];
const table = document.getElementsByClassName('datatable_' + this._id)[0];
for (let columnElement of table.getElementsByTagName('th')) {
const fieldName = columnElement.getAttribute('data-field');
if (fieldName === null) {
continue;
}
const checkbox = form.querySelector('input[name=' + fieldName + ']');
if (checkbox === null) {
continue;
}
checkbox.checked = window.getComputedStyle(columnElement).display !== 'none';
}
Cookies.set(form.getAttribute('name'), JSON.stringify(settings), {expires: 365, SameSite: 'Strict'});
jQuery(this.modal).modal('toggle');
}
resetVisibility() {
const form = this.modal.getElementsByTagName('form')[0];
Cookies.remove(form.getAttribute('name'));
for (let checkbox of form.querySelectorAll('input[type=checkbox]')) {
if (!checkbox.checked) {
checkbox.click();
}
}
jQuery(this.modal).modal('toggle');
_saveVisibility() {
const form = this._modal.getElementsByTagName('form')[0];
this.fetchForm(form)
.then(() => {
document.location.reload();
})
.catch(() => {
form.submit();
});
}
changeVisibility(columnName, checked) {
const tables = document.getElementsByClassName('datatable_' + this.id);
for (let tableBox of tables) {
let column = 0;
let foundColumn = false;
let table = tableBox.getElementsByClassName('dataTable')[0];
for (let columnElement of table.getElementsByTagName('th')) {
if (columnElement.getAttribute('data-field') === columnName) {
foundColumn = true;
break;
_resetVisibility(button) {
const form = this._modal.getElementsByTagName('form')[0];
this.fetchForm(form, {}, button.getAttribute('formaction'))
.then(() => {
document.location.reload();
})
.catch(() => {
form.setAttribute('action', button.getAttribute('formaction'));
form.submit();
});
}
_changeVisibility(columnName, checked) {
for (const tableBox of document.getElementsByClassName('datatable_' + this._id)) {
let targetClasses = null;
for (let element of tableBox.getElementsByClassName('col_' + columnName)) {
// only calculate that once and re-use the cached class list
if (targetClasses === null) {
let removeClass = '-none';
let addClass = 'd-table-cell';
if (!checked) {
removeClass = '-table-cell';
addClass = 'd-none';
}
targetClasses = '';
element.classList.forEach(
function (name, index, listObj) { // eslint-disable-line no-unused-vars
if (name.indexOf(removeClass) === -1) {
targetClasses += ' ' + name;
}
}
);
if (targetClasses.indexOf(addClass) === -1) {
targetClasses += ' ' + addClass;
}
}
if (columnElement.getAttribute('colspan') !== null) {
console.log('Tables with colspans are not supported!');
}
column++;
}
if (!foundColumn) {
return;
}
for (let rowElement of table.getElementsByTagName('tr')) {
if (rowElement.children[column] === undefined) {
continue;
}
if (checked) {
rowElement.children[column].classList.remove('hidden');
} else {
rowElement.children[column].classList.add('hidden');
}
element.className = targetClasses;
}
}
}

View File

@@ -1,65 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDatePicker: single date selects (currently unused)
*/
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
import moment from "moment";
export default class KimaiDatePicker extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'date-picker';
}
activateDatePicker(selector) {
const TRANSLATE = this.getContainer().getTranslation();
const DATE_UTILS = this.getContainer().getPlugin('date');
const firstDow = this.getConfiguration('first_dow_iso') % 7;
jQuery(selector + ' ' + this.selector).each(function(index) {
let localeFormat = jQuery(this).data('format');
jQuery(this).daterangepicker({
singleDatePicker: true,
showDropdowns: true,
autoUpdateInput: false,
locale: {
format: localeFormat,
firstDay: firstDow,
applyLabel: TRANSLATE.get('confirm'),
cancelLabel: TRANSLATE.get('cancel'),
customRangeLabel: TRANSLATE.get('customRange'),
daysOfWeek: DATE_UTILS.getWeekDaysShort(),
monthNames: DATE_UTILS.getMonthNames(),
}
});
jQuery(this).on('apply.daterangepicker', function(ev, picker) {
jQuery(this).val(picker.startDate.format(localeFormat));
jQuery(this).trigger("change");
});
});
}
destroyDatePicker(selector) {
jQuery(selector + ' ' + this.selector).each(function(index) {
if (jQuery(this).data('daterangepicker') !== undefined) {
jQuery(this).daterangepicker('destroy');
jQuery(this).data('daterangepicker').remove();
}
});
}
}

View File

@@ -1,87 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDateRangePicker: activate the (daterange picker) compound field in toolbar
*/
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
import moment from 'moment';
export default class KimaiDateRangePicker extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'date-range-picker';
}
activateDateRangePicker(selector) {
const TRANSLATE = this.getContainer().getTranslation();
const DATE_UTILS = this.getContainer().getPlugin('date');
const firstDow = this.getConfiguration('first_dow_iso') % 7;
jQuery(selector + ' ' + this.selector).each(function(index) {
let localeFormat = jQuery(this).data('format');
let separator = jQuery(this).data('separator');
let rangesList = {};
rangesList[TRANSLATE.get('today')] = [moment(), moment()];
rangesList[TRANSLATE.get('yesterday')] = [moment().subtract(1, 'days'), moment().subtract(1, 'days')];
rangesList[TRANSLATE.get('thisWeek')] = [moment().startOf('isoWeek'), moment().endOf('isoWeek')];
rangesList[TRANSLATE.get('lastWeek')] = [moment().subtract(1, 'week').startOf('isoWeek'), moment().subtract(1, 'week').endOf('isoWeek')];
if (firstDow === 0) { // sunday = 0
rangesList[TRANSLATE.get('thisWeek')] = [moment().startOf('isoWeek').subtract(1, 'day'), moment().endOf('isoWeek').subtract(1, 'day')];
rangesList[TRANSLATE.get('lastWeek')] = [moment().subtract(1, 'week').startOf('isoWeek').subtract(1, 'day'), moment().subtract(1, 'week').endOf('isoWeek').subtract(1, 'day')];
}
rangesList[TRANSLATE.get('thisMonth')] = [moment().startOf('month'), moment().endOf('month')];
rangesList[TRANSLATE.get('lastMonth')] = [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')];
rangesList[TRANSLATE.get('thisYear')] = [moment().startOf('year'), moment().endOf('year')];
rangesList[TRANSLATE.get('lastYear')] = [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')];
jQuery(this).daterangepicker({
showDropdowns: true,
autoUpdateInput: false,
autoApply: false,
linkedCalendars: true,
locale: {
separator: separator,
format: localeFormat,
firstDay: firstDow,
applyLabel: TRANSLATE.get('confirm'),
cancelLabel: TRANSLATE.get('cancel'),
customRangeLabel: TRANSLATE.get('customRange'),
daysOfWeek: DATE_UTILS.getWeekDaysShort(),
monthNames: DATE_UTILS.getMonthNames(),
},
ranges: rangesList,
alwaysShowCalendars: true
});
jQuery(this).on('apply.daterangepicker', function(ev, picker) {
jQuery(this).val(picker.startDate.format(localeFormat) + ' - ' + picker.endDate.format(localeFormat));
jQuery(this).data('begin', picker.startDate.format(localeFormat));
jQuery(this).data('end', picker.endDate.format(localeFormat));
jQuery(this).trigger("change");
});
});
}
destroyDateRangePicker(selector) {
jQuery(selector + ' ' + this.selector).each(function(index) {
if (jQuery(this).data('daterangepicker') !== undefined) {
jQuery(this).daterangepicker('destroy');
jQuery(this).data('daterangepicker').remove();
}
});
}
}

View File

@@ -1,67 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiDateTimePicker: activate the (datetime picker) field in timesheet edit dialog
*/
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiDateTimePicker extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'date-time-picker';
}
activateDateTimePicker(selector) {
const TRANSLATE = this.getContainer().getTranslation();
const DATE_UTILS = this.getContainer().getPlugin('date');
const firstDow = this.getConfiguration('first_dow_iso') % 7;
const is24hours = this.getConfiguration('twentyFourHours');
jQuery(selector + ' ' + this.selector).each(function(index) {
let localeFormat = jQuery(this).data('format');
jQuery(this).daterangepicker({
singleDatePicker: true,
timePicker: true,
timePicker24Hour: is24hours,
showDropdowns: true,
autoUpdateInput: false,
locale: {
format: localeFormat,
firstDay: firstDow,
applyLabel: TRANSLATE.get('confirm'),
cancelLabel: TRANSLATE.get('cancel'),
customRangeLabel: TRANSLATE.get('customRange'),
daysOfWeek: DATE_UTILS.getWeekDaysShort(),
monthNames: DATE_UTILS.getMonthNames(),
}
});
jQuery(this).on('apply.daterangepicker', function(ev, picker) {
jQuery(this).val(picker.startDate.format(localeFormat));
jQuery(this).trigger("change");
});
});
}
destroyDateTimePicker(selector) {
jQuery(selector + ' ' + this.selector).each(function(index) {
if (jQuery(this).data('daterangepicker') !== undefined) {
jQuery(this).daterangepicker('destroy');
jQuery(this).data('daterangepicker').remove();
}
});
}
}

View File

@@ -10,55 +10,300 @@
*/
import KimaiPlugin from '../KimaiPlugin';
import moment from 'moment';
import { DateTime, Duration } from 'luxon';
export default class KimaiDateUtils extends KimaiPlugin {
getId() {
getId()
{
return 'date';
}
getWeekDaysShort() {
return moment.localeData().weekdaysShort();
init()
{
if (this.getConfigurations().is24Hours()) {
this.timeFormat = 'HH:mm';
} else {
this.timeFormat = 'hh:mm a';
}
this.durationFormat = this.getConfiguration('formatDuration');
this.dateFormat = this.getConfiguration('formatDate');
}
getMonthNames() {
return moment.localeData().months();
/**
* @see https://moment.github.io/luxon/#/formatting?id=table-of-tokens
* @param {string} format
* @returns {string}
* @private
*/
_parseFormat(format)
{
format = format.replace('DD', 'dd');
format = format.replace('D', 'd');
format = format.replace('MM', 'LL');
format = format.replace('M', 'L');
format = format.replace('YYYY', 'yyyy');
format = format.replace('YY', 'yy');
format = format.replace('A', 'a');
return format;
}
formatDuration(since) {
const duration = moment.duration(moment(new Date()).diff(moment(since)));
/**
* @param {string} format
* @param {string|Date|null|undefined} dateTime
* @returns {string}
*/
format(format, dateTime)
{
let newDate = null;
return this.formatMomentDuration(duration);
}
formatSeconds(seconds) {
const duration = moment.duration('PT' + seconds + 'S');
return this.formatMomentDuration(duration);
}
formatMomentDuration(duration) {
const hours = parseInt(duration.asHours()).toString();
const minutes = duration.minutes();
const seconds = duration.seconds();
return this.formatTime(hours, minutes, seconds);
}
formatTime(hours, minutes, seconds) {
if (hours < 0 || minutes < 0 || seconds < 0) {
return '?';
if (dateTime === null || dateTime === undefined) {
newDate = DateTime.now();
} else if (dateTime instanceof Date) {
newDate = DateTime.fromJSDate(dateTime);
} else {
newDate = DateTime.fromISO(dateTime);
}
// special case for hours, as they can overflow the 24h barrier - Kimai does not support days as duration unit
if (hours.length === 1) {
hours = '0' + hours;
// using locale english here prevents that that AM/PM is translated to the
// locale variant: e.g. "ko" translates it to 오후 / 오전
return newDate.toFormat(this._parseFormat(format), { locale: 'en-us' });
}
/**
* @param {string|Date} dateTime
* @returns {string}
*/
getFormattedDate(dateTime)
{
return this.format(this._parseFormat(this.dateFormat), dateTime);
}
/**
* Returns a "YYYY-MM-DDTHH:mm:ss" formatted string in local time.
* This can take Date objects (e.g. from FullCalendar) and turn them into the correct format.
*
* @param {Date|DateTime} date
* @param {boolean|undefined} isUtc
* @return {string}
*/
formatForAPI(date, isUtc = false)
{
if (date instanceof Date) {
date = DateTime.fromJSDate(date);
}
const format = this.getConfiguration('formatDuration');
if (isUtc === undefined || !isUtc) {
date = date.toUTC();
}
return format.replace('%h', hours).replace('%m', ('0'+minutes).substr(-2)).replace('%s', ('0'+seconds).substr(-2));
return date.toISO({ includeOffset: false, suppressMilliseconds: true });
}
/**
* @param {string} date
* @param {string} format
* @return {DateTime}
*/
fromFormat(date, format)
{
// using locale en-us here prevents that Luxon expects the localized
// version of AM/PM (e.g. 오후 / 오전 for locale "ko")
return DateTime.fromFormat(date, this._parseFormat(format), { locale: 'en-us' });
}
/**
* @param {string|null} date
* @param {string|null} time
* @return {DateTime}
*/
fromHtml5Input(date, time)
{
date = date ?? '';
time = time ?? '';
if (date === '' && time === '') {
return DateTime.invalid('Empty date and time given');
}
if (date !== '' && time !== '') {
date = date + 'T' + time;
}
return DateTime.fromISO(date);
}
/**
* @param {string} date
* @param {string} format
* @return {boolean}
*/
isValidDateTime(date, format)
{
return this.fromFormat(date, format).isValid;
}
/**
* Adds a string like "00:30:00" or "01:15" to a given date.
*
* @param {Date} date
* @param {string} duration
* @return {Date}
*/
addHumanDuration(date, duration)
{
/** @type {DateTime} newDate */
let newDate = null;
if (date instanceof Date) {
newDate = DateTime.fromJSDate(date);
} else if (date instanceof DateTime) {
newDate = date;
} else {
throw 'addHumanDuration() needs a JS Date';
}
const parsed = DateTime.fromISO(duration);
const today = DateTime.now().startOf('day');
const timeOfDay = parsed.diff(today);
return newDate.plus(timeOfDay).toJSDate();
}
/**
* @param {string|integer|null} since
* @return {string}
*/
formatDuration(since)
{
let duration = null;
if (typeof since === 'string') {
duration = DateTime.now().diff(DateTime.fromISO(since));
} else {
duration = Duration.fromISO('PT' + (since === null ? 0 : since) + 'S');
}
return this.formatLuxonDuration(duration);
}
/**
* @param {integer} seconds
* @return {string}
*/
formatSeconds(seconds)
{
return this.formatLuxonDuration(Duration.fromObject({seconds: seconds}));
}
/**
* @param {Duration} duration
* @returns {string}
* @private
*/
formatLuxonDuration(duration)
{
duration = duration.shiftTo('hours', 'minutes', 'seconds');
return this.formatAsDuration(duration.hours, duration.minutes);
}
/**
* @param {Date} date
* @param {boolean|undefined} isUtc
* @return {string}
*/
formatTime(date, isUtc = false)
{
let newDate = DateTime.fromJSDate(date);
if (isUtc === undefined || !isUtc) {
newDate = newDate.toUTC();
}
// .utc() is required for calendar
return newDate.toFormat(this.timeFormat);
}
/**
* @param {int} hours
* @param {int} minutes
* @return {string}
*/
formatAsDuration(hours, minutes)
{
let format = this.durationFormat;
if (hours < 0 || minutes < 0) {
hours = Math.abs(hours);
minutes = Math.abs(minutes);
format = '-' + format;
}
return format.replace('%h', hours.toString()).replace('%m', ('0' + minutes).slice(-2));
}
/**
* @param {string} duration
* @returns {int}
*/
getSecondsFromDurationString(duration)
{
const luxonDuration = this.parseDuration(duration);
if (luxonDuration === null || !luxonDuration.isValid) {
return 0;
}
return luxonDuration.as('seconds');
}
/**
* @param {string} duration
* @returns {Duration}
*/
parseDuration(duration)
{
if (duration === undefined || duration === null || duration === '') {
return new Duration({seconds: 0});
}
duration = duration.trim().toUpperCase();
let luxonDuration = null;
if (duration.indexOf(':') !== -1) {
const match = duration.match(/(?:(\d+):)?(\d+)(?::(\d+))?/);
const hours = parseInt(match?.[1] || 0, 10);
const minutes = parseInt(match?.[2] || 0, 10);
const seconds = parseInt(match?.[3] || 0, 10);
luxonDuration = Duration.fromObject({hours: hours, minutes: minutes, seconds: seconds});
} else if (duration.indexOf('.') !== -1 || duration.indexOf(',') !== -1) {
duration = duration.replace(/,/, '.');
duration = (parseFloat(duration) * 3600).toString();
luxonDuration = Duration.fromISO('PT' + duration + 'S');
} else if (duration.indexOf('H') !== -1 || duration.indexOf('M') !== -1 || duration.indexOf('S') !== -1) {
/* D for days does not work, because 'PT1H' but with days 'P1D' is used */
luxonDuration = Duration.fromISO('PT' + duration);
} else {
let c = parseInt(duration);
const d = parseInt(duration).toFixed();
if (!isNaN(c) && duration === d) {
duration = (c * 3600).toString();
luxonDuration = Duration.fromISO('PT' + duration + 'S');
}
}
if (luxonDuration === null || !luxonDuration.isValid) {
return new Duration({seconds: 0});
}
// actually, the parsing above should be improved, but that works as well
if (duration[0] === '-' && luxonDuration.valueOf() > 0) {
return luxonDuration.negate();
}
return luxonDuration;
}
}

View File

@@ -0,0 +1,49 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiEscape: sanitize strings
*/
import KimaiPlugin from "../KimaiPlugin";
import DOMPurify from "dompurify";
export default class KimaiEscape extends KimaiPlugin {
getId() {
return 'escape';
}
/**
* @param {string} title
* @returns {string}
*/
escapeForHtml(title) {
if (title === undefined || title === null) {
return '';
}
const charToReplace = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
};
return title.replace(/[&<>"]/g, function(tag) {
return charToReplace[tag] || tag;
});
}
/**
* @param {string} html
* @returns {string}
*/
sanitize(html) {
return DOMPurify.sanitize(html);
}
}

View File

@@ -13,18 +13,24 @@ import KimaiPlugin from "../KimaiPlugin";
export default class KimaiEvent extends KimaiPlugin {
getId() {
getId()
{
return 'event';
}
trigger(name, details) {
if (name === null || name === undefined) {
/**
* @param {string} name
* @param {string|array|object|null} details
*/
trigger(name, details = null)
{
if (name === '') {
return;
}
for(let event of name.split(' ')) {
for (const event of name.split(' ')) {
let triggerEvent = new Event(event);
if (details !== undefined) {
if (details !== null) {
triggerEvent = new CustomEvent(event, {detail: details});
}
document.dispatchEvent(triggerEvent);

View File

@@ -0,0 +1,80 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiEscape: sanitize strings
*/
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiFetch extends KimaiPlugin {
getId() {
return 'fetch';
}
/**
* @param {string} url
* @param {object} options
* @returns {Promise<Response>}
*/
fetch(url, options = {}) {
if (options.headers === undefined) {
options.headers = new Headers();
}
options.headers.append('X-Requested-With', 'Kimai');
options = {...{
redirect: 'follow',
}, ...options};
return new Promise((resolve, reject) => {
fetch(url, options).then(response => {
if (response.ok) {
if (response.status === 201 && response.headers.has('x-modal-redirect')) {
window.location = response.headers.get('x-modal-redirect');
return;
}
// "ok" is only in status code range of 2xx
resolve(response);
return;
}
let stopPropagation = false;
switch (response.status) {
case 403: {
if (response.headers.has('login-required')) {
const loginUrl = this.getConfiguration('login').toString();
/** @type {KimaiAlert} alert */
const alert = this.getContainer().getPlugin('alert');
alert.question(this.translate('login.required'), (result) => {
if (result === true) {
window.location.replace(loginUrl);
}
});
stopPropagation = true;
}
break;
}
default: {
console.log('Some error occurred');
break;
}
}
if (!stopPropagation) {
reject(response);
}
})
.catch(error => {
console.log('Error occurred while talking to Kimai backend', error);
reject(error);
});
});
}
}

View File

@@ -10,67 +10,56 @@
*/
import KimaiPlugin from "../KimaiPlugin";
import KimaiFormPlugin from "../forms/KimaiFormPlugin";
export default class KimaiForm extends KimaiPlugin {
getId() {
getId()
{
return 'form';
}
activateForm(formSelector, container) {
this.getContainer().getPlugin('date-range-picker').activateDateRangePicker(formSelector);
this.getContainer().getPlugin('date-time-picker').activateDateTimePicker(formSelector);
this.getContainer().getPlugin('date-picker').activateDatePicker(formSelector);
this.getContainer().getPlugin('autocomplete').activateAutocomplete(formSelector);
this.getContainer().getPlugin('form-select').activateSelectPicker(formSelector, container);
}
destroyForm(formSelector) {
this.getContainer().getPlugin('form-select').destroySelectPicker(formSelector);
this.getContainer().getPlugin('autocomplete').destroyAutocomplete(formSelector);
this.getContainer().getPlugin('date-picker').destroyDatePicker(formSelector);
this.getContainer().getPlugin('date-time-picker').destroyDateTimePicker(formSelector);
this.getContainer().getPlugin('date-range-picker').destroyDateRangePicker(formSelector);
}
getFormData(form) {
let serialized = [];
// Loop through each field in the form
for (let i = 0; i < form.elements.length; i++) {
let field = form.elements[i];
// Don't serialize a couple of field types (button and submit are important to exclude, eg. invoice preview would fail otherwise)
if (!field.name || field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') {
continue;
}
// If a multi-select, get all selections
if (field.type === 'select-multiple') {
for (var n = 0; n < field.options.length; n++) {
if (!field.options[n].selected) continue;
serialized.push({
name: field.name,
value: field.options[n].value
});
activateForm(formSelector)
{
[].slice.call(document.querySelectorAll(formSelector)).map((form) => {
for (const plugin of this.getContainer().getPlugins()) {
if (plugin instanceof KimaiFormPlugin && plugin.supportsForm(form)) {
plugin.activateForm(form);
}
} else if ((field.type !== 'checkbox' && field.type !== 'radio') || field.checked) {
serialized.push({
name: field.name,
value: field.value
});
}
});
}
destroyForm(formSelector)
{
[].slice.call(document.querySelectorAll(formSelector)).map((form) => {
for (const plugin of this.getContainer().getPlugins()) {
if (plugin instanceof KimaiFormPlugin && plugin.supportsForm(form)) {
plugin.destroyForm(form);
}
}
});
}
/**
* @param {HTMLFormElement} form
* @param {Object} overwrites
* @param {boolean} removeEmpty
* @returns {string}
*/
convertFormDataToQueryString(form, overwrites = {}, removeEmpty = false)
{
let serialized = [];
let data = new FormData(form);
for (const key in overwrites) {
data.set(key, overwrites[key]);
}
return serialized;
}
convertFormDataToQueryString(formData) {
let serialized = [];
for (let row of formData) {
serialized.push(encodeURIComponent(row.name) + "=" + encodeURIComponent(row.value));
for (let row of data) {
if (!removeEmpty || row[1] !== '') {
serialized.push(encodeURIComponent(row[0]) + "=" + encodeURIComponent(row[1]));
}
}
return serialized.join('&');

View File

@@ -1,204 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiFormSelect: enhanced functionality for HTML select's
*/
import KimaiPlugin from "../KimaiPlugin";
import jQuery from "jquery";
export default class KimaiFormSelect extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'form-select';
}
activateSelectPicker(selector, container) {
const elementSelector = this.selector;
let options = {};
if (container !== undefined) {
options = {
dropdownParent: $(container),
};
}
// Function to match the name of the parent and not only the names of the children
// Based on the original matcher function of Select2: https://github.com/select2/select2/blob/5765090318c4d382ae56463cfa25ba8ca7bdd495/src/js/select2/defaults.js#L272
// More information: https://select2.org/searching | https://github.com/select2/docs/blob/develop/pages/11.searching/docs.md
function matcher(params, data) {
// Always return the object if there is nothing to compare
if (jQuery.trim(params.term) === '') {
return data;
}
// Check whether options has children
let hasChildren = data.children && data.children.length > 0;
// Split search param by space to search for all terms and convert all to uppercase
let terms = params.term.toUpperCase().split(' ');
let original = data.text.toUpperCase();
// Always return the parent option including its children, when the name matches one of the params
// Check if the text contains all or at least one of the terms
let foundAll = true;
let foundOne = false;
let missingTerms = [];
terms.forEach(function(item, index) {
if (original.indexOf(item) > -1) {
foundOne = true;
} else {
foundAll = false;
missingTerms.push(item);
}
});
// If the option element contains all terms, return it
if (foundAll) {
return data;
}
// Do a recursive check for options with children
if (hasChildren) {
// If the parent already contains one or more search terms, proceed only with the missing ones
// First: Clone the original params object...
let newParams = jQuery.extend(true, {}, params);
if (foundOne) {
newParams.term = missingTerms.join(' ');
} else {
newParams.term = params.term;
}
// Clone the data object if there are children
// This is required as we modify the object to remove any non-matches
let match = jQuery.extend(true, {}, data);
// Check each child of the option
for (let c = data.children.length - 1; c >= 0; c--) {
let child = data.children[c];
let matches = matcher(newParams, child);
// If there wasn't a match, remove the object in the array
if (matches === null) {
match.children.splice(c, 1);
}
}
// If any children matched, return the new object
if (match.children.length > 0) {
return match;
}
}
// If the option or its children do not contain the term, don't return anything
return null;
}
options = {...options, ...{
language: this.getConfiguration('locale').replace('_', '-'),
theme: "bootstrap",
matcher: matcher
}};
const templateResultFunc = function (state) {
console.log(state);
return jQuery('<span><span style="background-color:'+state.id+'; width: 20px; height: 20px; display: inline-block; margin-right: 10px;">&nbsp;</span>' + state.text + '</span>');
};
let optionsColor = {...options, ...{
templateSelection: templateResultFunc,
templateResult: templateResultFunc
}};
jQuery(selector + ' ' + elementSelector + ':not([data-renderer=color])').select2(options);
jQuery(selector + ' ' + elementSelector + '[data-renderer=color]').select2(optionsColor);
jQuery('body').on('reset', 'form', function(event){
setTimeout(function() {
jQuery(event.target).find(elementSelector).trigger('change');
}, 10);
});
}
destroySelectPicker(selector) {
jQuery(selector + ' ' + this.selector).select2('destroy');
}
updateOptions(selectIdentifier, data) {
let select = jQuery(selectIdentifier);
let emptyOption = jQuery(selectIdentifier + ' option[value=""]');
const selectedValue = select.val();
select.find('option').remove().end().find('optgroup').remove().end();
if (emptyOption.length !== 0) {
select.append(this._createOption(emptyOption.text(), ''));
}
let emptyOpts = [];
let options = [];
for (const [key, value] of Object.entries(data)) {
if (key === '__empty__') {
for (const entity of value) {
emptyOpts.push(this._createOption(entity.name, entity.id));
}
continue;
}
let optGroup = this._createOptgroup(key);
for (const entity of value) {
optGroup.appendChild(this._createOption(entity.name, entity.id));
}
options.push(optGroup);
}
select.append(options);
select.append(emptyOpts);
// if available, re-select the previous selected option (mostly usable for global activities)
select.val(selectedValue);
// if we don't trigger the change, the other selects won't reset
select.trigger('change');
// if select2 is active, this will tell the select to refresh
if (select.hasClass('selectpicker')) {
select.trigger('change.select2');
}
}
/**
* @param {string} label
* @param {string} value
* @returns {HTMLElement}
* @private
*/
_createOption(label, value) {
let option = document.createElement('option');
option.innerText = label;
option.value = value;
return option;
}
/**
* @param {string} label
* @returns {HTMLElement}
* @private
*/
_createOptgroup(label) {
let optGroup = document.createElement('optgroup');
optGroup.label = label;
return optGroup;
}
}

View File

@@ -0,0 +1,51 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiHotkeys extends KimaiPlugin {
getId()
{
return 'hotkeys';
}
init()
{
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
const selector = '[data-hotkey="ctrl+Enter"]';
window.addEventListener('keyup', (ev) => {
if (ev.ctrlKey && ev.key === 'Enter') {
const elements = [...document.querySelectorAll(selector)].filter(element => this.isVisible(element));
if (elements.length > 1) {
console.warn('KimaiHotkeys: More than one visible element matches ${selector}. No action triggered.');
}
if (elements.length === 1) {
ev.stopPropagation();
ev.preventDefault();
elements[0].click();
}
}
});
}
// adopted from Bootstrap 5.1.1, MIT
isVisible (element)
{
if (!element || element.getClientRects().length === 0) {
return false;
}
return getComputedStyle(element).getPropertyValue('visibility') === 'visible';
}
}

View File

@@ -10,63 +10,121 @@
*/
import KimaiPlugin from '../KimaiPlugin';
import jQuery from "jquery";
export default class KimaiMultiUpdateTable extends KimaiPlugin {
init() {
const self = this;
jQuery('body').
on('change', '#multi_update_all', function(event) {
jQuery('.multi_update_single').prop('checked', jQuery(event.target).prop('checked'));
self.toggleForm();
})
.on('change', '.multi_update_single', function(event) {
self.toggleForm();
})
.on('change', '#multi_update_table_action', function(event) {
const selectedItem = jQuery('#multi_update_table_action option:selected');
const selectedVal = selectedItem.val();
if (selectedVal === '') {
return;
}
const form = jQuery('#multi_update_form form');
const selectedText = selectedItem.text();
const ids = self.getSelectedIds();
const question = form.attr('data-question').replace(/%action%/, selectedText).replace(/%count%/, ids.length);
self.getContainer().getPlugin('alert').question(question, function(value) {
if (value) {
form.attr('action', selectedVal).submit();
} else {
jQuery('#multi_update_table_action').val('').trigger('change');
}
});
});
}
getSelectedIds()
getId()
{
let ids = [];
jQuery('.multi_update_single:checked').each(function(i){
ids[i] = $(this).val();
return 'datatable-batch-action';
}
init()
{
if (document.getElementsByClassName('multi_update_all').length === 0) {
return;
}
// we have to attach it to the "page-body" div, because section.content can be replaced
// via KimaiDatable and everything inside will be removed, including event listeners
const element = document.querySelector('div.page-body');
element.addEventListener('change', (event) => {
if (event.target.matches('.multi_update_all')) {
this.toggle(event.target.checked, event.target.closest('table'));
event.stopPropagation();
} else if (event.target.matches('.multi_update_single')) {
// single checkboxes in front of each row
this._toggleDatatable(event.target.closest('table'));
event.stopPropagation();
}
});
return ids;
element.addEventListener('click', (event) => {
if (event.target.matches('.multi_update_table_action')) {
const selectedButton = event.target;
const form = selectedButton.form;
const ids = form.querySelector('.multi_update_ids').value.split(',');
const question = form.dataset['question'].replace(/%action%/, selectedButton.textContent).replace(/%count%/, ids.length.toString());
/** @type {KimaiAlert} ALERT */
const ALERT = this.getPlugin('alert');
ALERT.question(question, function(value) {
if (value) {
form.action = selectedButton.dataset['href'];
form.submit();
}
});
}
});
}
toggleForm()
/**
* @param {boolean} checked
* @param {HTMLTableElement} table
*/
toggle(checked, table)
{
const ids = this.getSelectedIds();
jQuery('#multi_update_table_entities').val(ids.join(','));
for (const element of table.querySelectorAll('.multi_update_single')) {
element.checked = checked;
}
this._toggleDatatable(table);
}
/**
* @param {boolean} checked
*/
toggleAll(checked)
{
for (const element of document.querySelectorAll('.multi_update_all')) {
this._toggleAll(checked, element);
}
}
/**
* @param {boolean} checked
* @param {string} name
*/
toggleByName(checked, name)
{
for (const element of document.querySelectorAll('#multi_update_all_' + name)) {
this._toggleAll(checked, element);
}
}
/**
* @param {boolean} checked
* @param {Element} name
*/
_toggleAll(checked, element)
{
element.checked = checked;
this.toggle(checked, element.closest('table'));
}
/**
* @param {HTMLTableElement} table
* @private
*/
_toggleDatatable(table)
{
const card = table.closest('div.card.data_table');
let ids = [];
for (const box of table.querySelectorAll('input.multi_update_single:checked')) {
ids.push(box.value);
}
card.querySelector('.multi_update_ids').value = ids.join(',');
if (ids.length > 0) {
jQuery('#multi_update_form').show();
for (const element of card.querySelectorAll('.multi_update_form_hide')) {
element.style.setProperty('display', 'none', 'important');
}
card.querySelector('form.multi_update_form').style.display = null;//'block';
} else {
jQuery('#multi_update_form').hide();
card.querySelector('form.multi_update_form').style.setProperty('display', 'none', 'important');
for (const element of card.querySelectorAll('.multi_update_form_hide')) {
element.style.display = null;
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] Notification: notifications for Kimai
*/
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiNotification extends KimaiPlugin {
getId()
{
return 'notification';
}
isSupported()
{
if (!window.Notification) {
return false;
}
if (Notification.permission === 'denied') {
return false;
}
return Notification.permission === "granted";
}
request(callback)
{
try {
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
callback(true);
} else if (permission === "default") {
callback(null);
} else {
callback(false);
}
});
} catch (e) { // eslint-disable-line no-unused-vars
Notification.requestPermission((permission) => {
if (permission === "granted") {
callback(true);
} else if (permission === "default") {
callback(null);
} else {
callback(false);
}
});
}
}
notify(title, message, icon, options)
{
this.request((permission) => {
if (permission !== true) {
/** @type KimaiAlert */
const ALERT = this.getPlugin('alert');
ALERT.info(message);
}
let opts = {
body: message,
dir: this.getConfigurations().isRTL() ? 'rtl' : 'ltr',
};
//opts.requireInteraction = true;
//opts.renotify = true;
/*
if (options.tag === undefined) {
opts.tag = 'kimai';
}
*/
if (icon !== undefined && icon !== null) {
opts.icon = icon;
}
let nTitle = 'Kimai';
if (title !== null) {
nTitle = nTitle + ': ' + title;
}
if (options !== undefined && options !== null) {
opts = { ...opts, ...options};
}
const notification = new window.Notification(nTitle, opts);
notification.onclick = function () {
window.focus();
notification.close();
};
});
}
}

View File

@@ -1,41 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiPauseRecord
*
* allows to pause records
* THIS IS JUST A DRAFT FOR THE DOM, IT IS NOT SUPPORTED IN KIMAI ITSELF!
*/
import jQuery from 'jquery';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiPauseRecord extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
init() {
this.activate(this.selector);
}
activate(selector) {
jQuery(selector + ' .pull-left i').hover(function () {
let link = jQuery(this).parents('a');
link.attr('href', link.attr('href').replace('/stop', '/pause'));
jQuery(this).removeClass('fa-stop-circle').addClass('fa-pause-circle').addClass('text-orange');
},function () {
let link = jQuery(this).parents('a');
link.attr('href', link.attr('href').replace('/pause', '/stop'));
jQuery(this).removeClass('fa-pause-circle').removeClass('text-orange').addClass('fa-stop-circle');
});
}
}

View File

@@ -1,92 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiRecentActivities: responsible to reload the users recent activities
*/
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiRecentActivities extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'recent-activities';
}
init() {
const menu = document.querySelector(this.selector);
// the menu can be hidden if user has no permissions to see it
if (menu === null) {
return;
}
const dropdown = menu.querySelector('ul.dropdown-menu');
this.attributes = dropdown.dataset;
this.itemList = dropdown.querySelector('li > ul.menu');
const self = this;
const handle = function() { self.reloadRecentActivities(); };
// don't block initial browser rendering
setTimeout(handle, 500);
document.addEventListener('kimai.recentActivities', handle);
document.addEventListener('kimai.timesheetUpdate', handle);
document.addEventListener('kimai.timesheetDelete', handle);
document.addEventListener('kimai.activityUpdate', handle);
document.addEventListener('kimai.activityDelete', handle);
document.addEventListener('kimai.projectUpdate', handle);
document.addEventListener('kimai.projectDelete', handle);
document.addEventListener('kimai.customerUpdate', handle);
document.addEventListener('kimai.customerDelete', handle);
}
emptyList() {
this.itemList.innerHTML = '';
}
setEntries(entries) {
if (entries.length === 0) {
this.emptyList();
return;
}
let htmlToInsert = '';
for (let timesheet of entries) {
let label = this.attributes['template']
.replace('%customer%', timesheet.project.customer.name)
.replace('%project%', timesheet.project.name)
.replace('%activity%', timesheet.activity.name);
htmlToInsert +=
`<li>` +
`<a href="${ this.attributes['href'].replace('000', timesheet.id) }" data-event="kimai.timesheetStart kimai.timesheetUpdate" class="api-link" data-method="PATCH" data-msg-error="timesheet.start.error" data-msg-success="timesheet.start.success">` +
`<i class="${ this.attributes['icon'] }"></i> ${ label }` +
`</a>` +
`</li>`;
}
this.itemList.innerHTML = htmlToInsert;
}
reloadRecentActivities() {
const self = this;
const API = this.getContainer().getPlugin('api');
API.get(this.attributes['api'], {}, function(result) {
self.setEntries(result);
});
}
}

View File

@@ -9,37 +9,70 @@
* [KIMAI] KimaiReducedClickHandler: abstract class
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiReducedClickHandler extends KimaiPlugin {
_addClickHandler(selector, callback) {
jQuery('body').on('click', selector, function(event) {
// just in case an inner element is editable, than this should not be triggered
if (event.target.parentNode.isContentEditable || event.target.isContentEditable) {
/**
* No _underscore naming for now, as it would be mangled otherwise
* @param {string} selector
* @param {callback} callback
* @param {array<string>} stopSelector
*/
addClickHandler(selector, callback, stopSelector) {
document.body.addEventListener('click', (event) => {
// event.currentTarget is ALWAYS the body
let target = event.target;
while (target !== null) {
const tagName = target.tagName.toUpperCase();
if (tagName === 'BODY') {
return;
}
if (target.matches(selector)) {
break;
}
// when an element is clicked, which can trigger stuff itself, we don't want the event to be processed
if (tagName === 'A' || tagName === 'BUTTON' || tagName === 'INPUT' || tagName === 'LABEL') {
return;
}
for (let x of stopSelector) {
if (target.matches(x)) {
return;
}
}
target = target.parentNode;
}
if (target === null) {
return;
}
// handles the "click" on table rows or list elements
let target = event.target;
if (event.currentTarget.matches('tr') || event.currentTarget.matches('li')) {
while (target !== null && !target.matches('body')) {
// when an element within the row is clicked, that can trigger stuff itself, we don't want the event to be processed
// don't act if a link, button or form element was clicked
if (target.matches('a') || target.matches ('button') || target.matches ('input')) {
return;
}
target = target.parentNode;
// just in case an inner element is editable, then this should not be triggered
if (target.isContentEditable || target.parentNode.isContentEditable) {
return;
}
if (!target.matches(selector)) {
return;
}
for (let x of stopSelector) {
if (target.matches(x)) {
return;
}
}
event.preventDefault();
event.stopPropagation();
let href = jQuery(this).attr('data-href');
if (!href) {
href = jQuery(this).attr('href');
let href = target.dataset['href'];
if (href === undefined || href === null) {
href = target.href;
}
if (href === undefined || href === null || href === '') {

View File

@@ -0,0 +1,94 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiRemoteModal: load remote content (without forms) into a modal
*/
import KimaiPlugin from '../KimaiPlugin';
import { Modal } from 'bootstrap';
/**
* Use like this:
* <a href="{{ path('your-route') }}" class="remote-modal-load" data-modal-id="remote_modal" data-modal-class="p-0" data-modal-title="Some title" title="Some title">Modal</a>
* <a href="{{ path('your-route') }}" class="remote-modal-load" data-modal-title="Some title" title="Some title">Modal</a>
*/
export default class KimaiRemoteModal extends KimaiPlugin {
constructor()
{
super();
this._selector = 'a.remote-modal-load';
}
/**
* @returns {string}
*/
getId()
{
return 'remote-modal';
}
init()
{
this.handle = (event) => {
this._showModal(event.currentTarget);
event.stopPropagation();
event.preventDefault();
};
for (let link of document.querySelectorAll(this._selector)) {
link.addEventListener('click', this.handle);
}
}
/**
* @param {HTMLLinkElement} element
* @private
*/
_showModal(element)
{
this.fetch(element.href, {method: 'GET'})
.then(response => {
if (!response.ok) {
return;
}
let modalSelector = 'remote_modal';
if (element.dataset['modalId'] !== undefined) {
modalSelector = element.dataset['modalId'];
}
const modalElement = document.getElementById(modalSelector);
if (modalElement === null) {
console.log('Could not find modal with ID: ' + modalSelector);
}
return response.text().then(html => {
const modalBody = document.createElement('div');
modalBody.classList.add('modal-body');
if (element.dataset['modalClass'] !== undefined) {
modalBody.classList.add(element.dataset['modalClass']);
}
modalBody.innerHTML = html;
for (let link of modalBody.querySelectorAll('a.remote-modal-reload')) {
link.addEventListener('click', this.handle);
}
modalElement.querySelector('.modal-body').replaceWith(modalBody);
if (element.dataset['modalTitle'] !== undefined) {
modalElement.querySelector('.modal-title').textContent = element.dataset['modalTitle'];
}
Modal.getOrCreateInstance(modalElement).show();
});
})
.catch((reason) => {
console.log('Failed to load remote modal', reason);
});
}
}

View File

@@ -1,148 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiSelectDataAPI: <select> boxes with dynamic data from API
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
import moment from 'moment';
export default class KimaiSelectDataAPI extends KimaiPlugin {
constructor(selector) {
super();
this.selector = selector;
}
getId() {
return 'select-data-api';
}
init() {
this.activateApiSelects(this.selector);
}
activateApiSelects(selector) {
const self = this;
const API = this.getContainer().getPlugin('api');
jQuery('body').on('change', selector, function(event) {
const targetSelect = '#' + this.dataset['relatedSelect'];
// if the related target select does not exist, we do not need to load the related data
if (jQuery(targetSelect).length === 0) {
return;
}
let formPrefix = jQuery(this).parents('form').first().attr('name');
if (formPrefix === undefined || formPrefix === null) {
formPrefix = '';
} else {
formPrefix += '_';
}
let newApiUrl = self._buildUrlWithFormFields(this.dataset['apiUrl'], formPrefix);
const selectValue = jQuery(this).val();
// Problem: select a project with activities and then select a customer that has no project
// results in a wrong URL, it triggers "activities?project=" instead of using the "emptyUrl"
if (selectValue === undefined || selectValue === null || selectValue === '' || (Array.isArray(selectValue) && selectValue.length === 0)) {
if (this.dataset['emptyUrl'] === undefined) {
self._updateSelect(targetSelect, {});
jQuery(targetSelect).attr('disabled', 'disabled');
return;
}
newApiUrl = self._buildUrlWithFormFields(this.dataset['emptyUrl'], formPrefix);
}
jQuery(targetSelect).removeAttr('disabled');
API.get(newApiUrl, {}, function(data){
self._updateSelect(targetSelect, data);
});
});
}
_buildUrlWithFormFields(apiUrl, formPrefix) {
let newApiUrl = apiUrl;
apiUrl.split('?')[1].split('&').forEach(item => {
let [key, value] = item.split('=');
let decoded = decodeURIComponent(value);
let test = decoded.match(/%(.*)%/);
if (test !== null) {
let targetField = jQuery('#' + formPrefix + test[1]);
let newValue = '';
if (targetField.length === 0) {
// happens for example:
// - in duration only mode, when the end field is not found
// console.log('ERROR: Cannot find field with name "' + test[1] + '" by selector: #' + formPrefix + test[1]);
} else {
if (targetField.val() !== null) {
newValue = targetField.val();
if (newValue !== '') {
// having that special case here is far from being perfect... but for now it works ;-)
if (targetField.data('daterangepicker') !== undefined) {
if (key === 'begin' || key === 'start' || targetField.data('daterangepicker').singleDatePicker) {
newValue = targetField.data('daterangepicker').startDate.format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
} else if (key === 'end') {
newValue = targetField.data('daterangepicker').endDate.format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
}
} else if (targetField.data('format') !== undefined) {
if (moment(newValue, targetField.data('format')).isValid()) {
newValue = moment(newValue, targetField.data('format')).format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
}
}
} else {
// happens for example:
// - when the end date is not set on a timesheet record and the project list is loaded (as the URL contains the %end% replacer)
// console.log('Empty value found for field with name "' + test[1] + '" by selector: #' + formPrefix + test[1]);
}
} else {
// happens for example:
// - when a customer without projects is selected
// console.log('ERROR: Empty field with name "' + test[1] + '" by selector: #' + formPrefix + test[1]);
}
}
if (Array.isArray(newValue)) {
newValue = newValue.join(',');
}
newApiUrl = newApiUrl.replace(value, newValue);
}
});
return newApiUrl;
}
_updateSelect(selectName, data) {
const options = {};
for (const apiData of data) {
let title = '__empty__';
if (apiData.hasOwnProperty('parentTitle') && apiData.parentTitle !== null) {
title = apiData.parentTitle;
}
if (!options.hasOwnProperty(title)) {
options[title] = [];
}
options[title].push(apiData);
}
const ordered = {};
Object.keys(options).sort().forEach(function(key) {
ordered[key] = options[key];
});
this.getContainer().getPlugin('form-select').updateOptions(selectName, ordered);
}
}

View File

@@ -9,69 +9,84 @@
* [KIMAI] KimaiThemeInitializer: initialize theme functionality
*/
import jQuery from 'jquery';
import { Tooltip, Offcanvas } from 'bootstrap';
import KimaiPlugin from '../KimaiPlugin';
export default class KimaiThemeInitializer extends KimaiPlugin {
init() {
this.registerGlobalAjaxErrorHandler();
this.registerAutomaticAlertRemove('div.alert-success', 5000);
// activate the dropdown functionality
jQuery('.dropdown-toggle').dropdown();
// activate the tooltip functionality
jQuery('[data-toggle="tooltip"]').tooltip();
// activate all form plugins
this.getContainer().getPlugin('form').activateForm('.content-wrapper form', 'body');
this.getContainer().getPlugin('form').activateForm('form.searchform', 'body');
}
init()
{
// the tooltip do not use data-bs-toggle="tooltip" so they can be mixed with data-toggle="modal"
[].slice.call(document.querySelectorAll('[data-toggle="tooltip"]')).map(function (tooltipTriggerEl) {
return new Tooltip(tooltipTriggerEl);
});
/**
* redirect access denied / session timeouts to login page
*/
registerGlobalAjaxErrorHandler() {
const loginUrl = this.getConfiguration('login');
const alert = this.getContainer().getPlugin('alert');
const translation = this.getContainer().getTranslation().get('login.required');
jQuery(document).ajaxError(function(event, jqxhr, settings, thrownError) {
if (jqxhr.status !== undefined && jqxhr.status === 403) {
const loginRequired = jqxhr.getResponseHeader('login-required');
if (loginRequired !== null) {
alert.question(translation, function (result) {
if (result === true) {
window.location.replace(loginUrl);
}
});
}
// support for offcanvas elements
const offcanvasElementList = document.querySelectorAll('.offcanvas');
[...offcanvasElementList].map(offcanvasEl => new Offcanvas(offcanvasEl));
// activate all form plugins
/** @type {KimaiForm} FORMS */
const FORMS = this.getContainer().getPlugin('form');
FORMS.activateForm('div.page-wrapper form');
this._registerModalAutofocus('#remote_form_modal');
this.overlay = null;
// register a global event listener, which displays an overlays upon notification
document.addEventListener('kimai.reloadContent', (event) => {
// do not allow more than one loading screen at a time
if (this.overlay !== null) {
return;
}
// at which element we append the loading screen
let container = 'div.page-wrapper';
if (event.detail !== undefined && event.detail !== null) {
container = event.detail;
}
const temp = document.createElement('div');
temp.innerHTML = '<div class="overlay"><div class="progress progress-sm"><div class="progress-bar progress-bar-indeterminate"></div></div></div>';
this.overlay = temp.firstElementChild;
document.querySelector(container).append(this.overlay);
});
// register a global event listener, which hides an overlay upon notification
document.addEventListener('kimai.reloadedContent', () => {
if (this.overlay !== null) {
this.overlay.remove();
this.overlay = null;
}
});
}
/**
* auto hide success messages, as they are just meant as user feedback and not as a permanent information
* Helps to set the autofocus on modals.
*
* @param {string} selector
* @param {integer} interval
*/
registerAutomaticAlertRemove(selector, interval) {
const self = this;
this._alertRemoveHandler = setInterval(
function() {
self.hideAlert(selector);
},
interval
);
}
_registerModalAutofocus(selector) {
// on mobile you do not want to trigger the virtual keyboard upon modal open
if (this.isMobile()) {
return;
}
unregisterAutomaticAlertRemove() {
clearInterval(this._alertRemoveHandler);
}
const modal = document.querySelector(selector);
if (modal === null) {
return;
}
/**
* @param {string} selector
*/
hideAlert(selector) {
jQuery(selector).alert('close');
modal.addEventListener('shown.bs.modal', () => {
const form = modal.querySelector('form');
let formAutofocus = form.querySelectorAll('[autofocus]');
if (formAutofocus.length < 1) {
formAutofocus = form.querySelectorAll('input[type=text],input[type=date],textarea,select');
}
if (formAutofocus.length > 0) {
formAutofocus[0].focus();
}
});
}
}

View File

@@ -9,15 +9,14 @@
* [KIMAI] KimaiToolbar: some event listener to handle the toolbar/data-table filter, toolbar and navigation
*/
import jQuery from 'jquery';
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiToolbar extends KimaiPlugin {
constructor(formSelector, formSubmitActionClass) {
super();
this.formSelector = formSelector;
this.actionClass = formSubmitActionClass;
this._formSelector = formSelector;
this._actionClass = formSubmitActionClass;
}
getId() {
@@ -26,50 +25,52 @@ export default class KimaiToolbar extends KimaiPlugin {
init() {
const formSelector = this.getSelector();
const self = this;
const EVENT = self.getContainer().getPlugin('event');
this._registerPagination(formSelector, EVENT);
this._registerSortableTables(formSelector, EVENT);
this._registerAlternativeSubmitActions(formSelector, this.actionClass);
this._registerPagination(formSelector);
this._registerSortableTables(formSelector);
this._registerAlternativeSubmitActions(formSelector, this._actionClass);
// Reset the page if filter values are changed, otherwise we might end up with a limited set of data,
// which does not support the given page - and it would be just wrong to stay in the same page
jQuery(formSelector +' input').change(function (event) {
switch (event.target.id) {
case 'order':
case 'orderBy':
case 'page':
break;
default:
jQuery(formSelector + ' input#page').val(1);
break;
}
self.triggerChange();
[].slice.call(document.querySelectorAll(formSelector + ' input')).map((element) => {
element.addEventListener('change', (event) => {
switch (event.target.id) {
case 'order':
case 'orderBy':
case 'page':
break;
default:
document.querySelector(formSelector + ' input#page').value = 1;
break;
}
});
this.triggerChange();
});
// when user selected a new customer or project, reset the pagination back to 1
// and then find out if the results should be reloaded
jQuery(formSelector + ' select').change(function (event) {
let reload = true;
switch (event.target.id) {
case 'customer':
if (jQuery(formSelector + ' select#project').length > 0) {
reload = false;
}
break;
[].slice.call(document.querySelectorAll(formSelector + ' select')).map((element) => {
element.addEventListener('change', (event) => {
let reload = true;
switch (event.target.id) {
case 'customer':
if (document.querySelector(formSelector + ' select#project') !== null) {
reload = false;
}
break;
case 'project':
if (jQuery(formSelector + ' select#activity').length > 0) {
reload = false;
}
break;
}
jQuery(formSelector + ' input#page').val(1);
case 'project':
if (document.querySelector(formSelector + ' select#activity') !== null) {
reload = false;
}
break;
}
document.querySelector(formSelector + ' input#page').value = 1;
if (reload) {
self.triggerChange();
}
if (reload) {
this.triggerChange();
}
});
});
}
@@ -80,15 +81,17 @@ export default class KimaiToolbar extends KimaiPlugin {
_registerAlternativeSubmitActions(toolbarSelector, actionBtnClass) {
document.addEventListener('click', function(event) {
let target = event.target;
while (target !== null && !target.matches('body')) {
while (target !== null && typeof target.matches === "function" && !target.matches('body')) {
if (target.classList.contains(actionBtnClass)) {
const form = document.querySelector(toolbarSelector);
if (form === null) {
return;
}
const prevAction = form.action;
const prevMethod = form.method;
form.target = '_blank';
const prevAction = form.getAttribute('action');
const prevMethod = form.getAttribute('method');
if (target.dataset.target !== undefined) {
form.target = target.dataset.target;
}
form.action = target.href;
if (target.dataset.method !== undefined) {
form.method = target.dataset.method;
@@ -104,51 +107,70 @@ export default class KimaiToolbar extends KimaiPlugin {
target = target.parentNode;
}
});
});
}
/**
* Sortable datatables use hidden fields in the toolbar filter/search form
* @private
*/
_registerSortableTables(formSelector, EVENT) {
jQuery('body').on('click', 'th.sortable', function(event){
var $header = jQuery(event.target);
var order = 'DESC';
var orderBy = $header.data('order');
if ($header.hasClass('sorting_desc')) {
_registerSortableTables(formSelector) {
document.body.addEventListener('click', (event) => {
if (!event.target.matches('th.sortable')) {
return;
}
let order = 'DESC';
let orderBy = event.target.dataset['order'];
if (event.target.classList.contains('sorting_desc')) {
order = 'ASC';
}
jQuery(formSelector + ' #orderBy').val(orderBy);
jQuery(formSelector + ' #order').val(order);
document.querySelector(formSelector + ' #orderBy').value = orderBy;
document.querySelector(formSelector + ' #order').value = order;
// re-render the selectboxes
jQuery(formSelector + ' #orderBy').trigger('change');
jQuery(formSelector + ' #order').trigger('change');
// re-render the selectbox
document.querySelector(formSelector + ' #orderBy').dispatchEvent(new Event('change'));
document.querySelector(formSelector + ' #order').dispatchEvent(new Event('change'));
// triggers the datatable reload - search for the event name
EVENT.trigger('filter-change');
document.dispatchEvent(new Event('filter-change'));
});
}
/**
* This catches all clicks on the pagination and prevents the default action, as we want to reload the page via JS
* This catches all clicks on the pagination and prevents the default action,
* as we want to reload the page via JS.
*
* @private
*/
_registerPagination(formSelector, EVENT) {
jQuery('body').on('click', 'div.navigation ul.pagination li a', function(event) {
let pager = jQuery(formSelector + " input#page");
if (pager.length === 0) {
_registerPagination(formSelector) {
document.body.addEventListener('click', (event) => {
if (!event.target.matches('ul.pagination li a') && (event.target.parentNode === null || !event.target.parentNode.matches('ul.pagination li a'))) {
return;
}
let pager = document.querySelector(formSelector + " input#page");
if (pager === null) {
return;
}
let target = event.target;
// this happens for the arrows, which can be an icon <i> element
if (!target.matches('a')) {
target = target.parentNode;
}
event.preventDefault();
event.stopPropagation();
let urlParts = jQuery(this).attr('href').split('/');
let page = urlParts[urlParts.length-1];
pager.val(page);
pager.trigger('change');
EVENT.trigger('pagination-change');
let urlParts = target.href.split('/');
let pageNumber = urlParts[urlParts.length - 1];
// page number usually is the default value and is therefor missing from the URL
if (!/\d/.test(pageNumber)) {
pageNumber = 1;
}
pager.value = pageNumber;
pager.dispatchEvent(new Event('change'));
document.dispatchEvent(new Event('pagination-change'));
return false;
});
@@ -158,7 +180,7 @@ export default class KimaiToolbar extends KimaiPlugin {
* Triggers an event, that everyone can listen for.
*/
triggerChange() {
this.getContainer().getPlugin('event').trigger('toolbar-change');
document.dispatchEvent(new Event('toolbar-change'));
}
/**
@@ -167,7 +189,7 @@ export default class KimaiToolbar extends KimaiPlugin {
* @returns {string}
*/
getSelector() {
return this.formSelector;
return this._formSelector;
}
}

View File

@@ -0,0 +1,59 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiUser: information about the current user
*/
import KimaiPlugin from "../KimaiPlugin";
export default class KimaiUser extends KimaiPlugin {
getId() {
return 'user';
}
init() {
this.user = this.getConfigurations().get('user');
}
/**
* @returns {string}
*/
getUserId() {
return this.user.id;
}
/**
* @returns {string}
*/
getName() {
return this.user.name;
}
/**
* @returns {boolean}
*/
isAdmin() {
return this.user.admin;
}
/**
* @returns {boolean}
*/
isSuperAdmin() {
return this.user.superAdmin;
}
/**
* @returns {array}
*/
getRoles() {
return this.user.roles;
}
}

View File

@@ -0,0 +1,797 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiCalendar: wrapping Fullcalendar.io
*/
import { Popover } from 'bootstrap';
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import bootstrap5Plugin, { BootstrapTheme } from '@fullcalendar/bootstrap5';
import googlePlugin from '@fullcalendar/google-calendar';
import iCalendarPlugin from '@fullcalendar/icalendar';
import interactionPlugin, { Draggable } from '@fullcalendar/interaction';
import arLocale from '@fullcalendar/core/locales/ar';
import csLocale from '@fullcalendar/core/locales/cs';
import daLocale from '@fullcalendar/core/locales/da';
import deLocale from '@fullcalendar/core/locales/de';
import deAtLocale from '@fullcalendar/core/locales/de-at';
import elLocale from '@fullcalendar/core/locales/el';
import esLocale from '@fullcalendar/core/locales/es';
import euLocale from '@fullcalendar/core/locales/eu';
import faLocale from '@fullcalendar/core/locales/fa';
import fiLocale from '@fullcalendar/core/locales/fi';
import frLocale from '@fullcalendar/core/locales/fr';
import heLocale from '@fullcalendar/core/locales/he';
import hrLocale from '@fullcalendar/core/locales/hr';
import huLocale from '@fullcalendar/core/locales/hu';
import itLocale from '@fullcalendar/core/locales/it';
import jaLocale from '@fullcalendar/core/locales/ja';
import koLocale from '@fullcalendar/core/locales/ko';
import nbLocale from '@fullcalendar/core/locales/nb';
import nlLocale from '@fullcalendar/core/locales/nl';
import plLocale from '@fullcalendar/core/locales/pl';
import ptLocale from '@fullcalendar/core/locales/pt';
import ptBrLocale from '@fullcalendar/core/locales/pt-br';
import roLocale from '@fullcalendar/core/locales/ro';
import ruLocale from '@fullcalendar/core/locales/ru';
import skLocale from '@fullcalendar/core/locales/sk';
import svLocale from '@fullcalendar/core/locales/sv';
import trLocale from '@fullcalendar/core/locales/tr';
import zhLocale from '@fullcalendar/core/locales/zh-cn';
import viLocale from '@fullcalendar/core/locales/vi';
import enGbLocale from '@fullcalendar/core/locales/en-gb';
import enUsLocale from '@fullcalendar/core/locales/en-gb';
import KimaiColor from './KimaiColor';
import KimaiContextMenu from "./KimaiContextMenu";
import { DateTime } from 'luxon';
export default class KimaiCalendar {
/**
* Options is a huge JSON object.
*
* @param {KimaiContainer} kimai
* @param {HTMLElement} element
* @param {Object} options
*/
constructor(kimai, element, options) {
this.kimai = kimai;
this.options = options;
/** @type {KimaiAPI} API */
const API = this.kimai.getPlugin('api');
/** @type {KimaiDateUtils} DATES */
const DATES = this.kimai.getPlugin('date');
/** @type {KimaiAjaxModalForm} MODAL */
const MODAL = this.kimai.getPlugin('modal');
// Instead of using "buttonIcons" the theme needs to be adjusted directly
// https://fullcalendar.io/docs/buttonIcons
BootstrapTheme.prototype.classes = {
root: 'fc-theme-bootstrap5',
tableCellShaded: 'fc-theme-bootstrap5-shaded',
buttonGroup: 'btn-group',
button: 'btn btn-primary btn-icon', // required for Tabler
buttonActive: 'active',
popover: 'popover',
popoverHeader: 'popover-header',
popoverContent: 'popover-body',
};
BootstrapTheme.prototype.baseIconClass = ''; // required for Fontawesome
BootstrapTheme.prototype.iconOverridePrefix = ''; // required for Fontawesome
BootstrapTheme.prototype.iconClasses = {
close: 'fa-times',
prev: this.options['icons']['previous'],
next: this.options['icons']['next'],
prevYear: this.options['icons']['previousYear'],
nextYear: this.options['icons']['nextYear'],
};
BootstrapTheme.prototype.rtlIconClasses = {
prev: this.options['icons']['previous'],
next: this.options['icons']['next'],
prevYear: this.options['icons']['previousYear'],
nextYear: this.options['icons']['nextYear'],
};
let calendarOptions = {
locales: [ enGbLocale, enUsLocale, arLocale, csLocale, daLocale, deLocale, deAtLocale, elLocale,
esLocale, euLocale, faLocale, fiLocale, frLocale, heLocale, hrLocale, huLocale, itLocale, jaLocale, koLocale,
nbLocale, nlLocale, plLocale, ptLocale, ptBrLocale, roLocale, ruLocale, skLocale, svLocale, trLocale, zhLocale, viLocale ],
plugins: [ bootstrap5Plugin, dayGridPlugin, timeGridPlugin, googlePlugin, iCalendarPlugin, interactionPlugin ],
initialView: this.toInternalViewName(this.options['initialView']),
initialDate: this.options['initialDate'],
// https://fullcalendar.io/docs/theming
themeSystem: 'bootstrap5',
// https://fullcalendar.io/docs/headerToolbar
headerToolbar: {
start: 'title',
center: 'dayGridMonth,timeGridWeek,timeGridDay',
end: 'today prev,next'
},
direction: this.kimai.getConfiguration().get('direction'),
locale: this.kimai.getConfiguration().getLanguage().toLowerCase(),
// https://fullcalendar.io/docs/height
// auto makes the calendar too small
// height: 'auto',
height: '80vh',
// allow clicking e.g. week-numbers to change the view to this week
navLinks: true,
nowIndicator: true,
weekends: this.options['showWeekends'],
weekNumbers: this.options['showWeekNumbers'],
weekNumberCalculation: 'ISO',
firstDay: this.kimai.getConfiguration().getFirstDayOfWeek(true),
now: this.options['now'],
businessHours: {
daysOfWeek: [0, 1, 2, 3, 4, 5, 6],
startTime: this.options['businessTimeBegin'],
endTime: this.options['businessTimeEnd']
},
slotDuration: this.options['slotDuration'],
slotMinTime: this.options['timeframeBegin'] + ':00',
slotMaxTime: this.options['timeframeEnd'] === '23:59' ? '24:00:00' : (this.options['timeframeEnd'] + ':59'),
// deactivate for auto calculation, which does a good job.
// but 1h seems to be a "normal distance" for calendar apps (like Google and Apple)
slotLabelInterval: '1:00',
// how long should entries look like when they don't have an end
defaultTimedEventDuration: this.options['slotDuration'],
// https://fullcalendar.io/docs/timeZone
timeZone: this.options['timezone'],
// TODO implement me later on
// https://fullcalendar.io/docs/validRange
// limit to the users registration date or a configuration for the first day in job
// https://fullcalendar.io/docs/hiddenDays
// once we can configure working days
// hiddenDays: [ 2, 4 ]
// dropAccept
dayMaxEventRows: true,
eventMaxStack: this.options['dayLimit'],
dayMaxEvents: this.options['dayLimit'],
// the callbacks "viewDidMount" and "viewWillUnmount" are only called when switching between month and others, not between week and day
datesSet: (dateInfo) => {
document.dispatchEvent(new CustomEvent('kimai.calendar.changeDate', {detail: {
view: this.toExternalViewName(dateInfo.view.type),
date: dateInfo.start.toISOString().split('T')[0],
}}));
},
views: {
dayGrid: {
dayMaxEventRows: this.options['dayLimit']
}
},
// ============= POPOVER =============
viewClassNames: () => {
document.querySelector('.fc-dayGridMonth-button').classList.remove('btn-icon');
document.querySelector('.fc-timeGridWeek-button').classList.remove('btn-icon');
document.querySelector('.fc-timeGridDay-button').classList.remove('btn-icon');
},
// DESTROY TO PREVENT MEMORY LEAKS
eventWillUnmount: (unmountInfo) => {
// this happens when a user drags an external event to the calendar (view: week and day) and moves it around
// for some reason the "eventWillUnmount" is triggered for this "potential but not yet existing event"
if (unmountInfo.event.source === null) {
return;
}
if (!this.isKimaiSource(unmountInfo.event)) {
return;
}
const popover = Popover.getInstance(unmountInfo.el);
if (popover !== null) {
popover.dispose();
}
},
// SHOW POPOVER FOR TIMESHEETS
eventMouseEnter: (mouseEnterInfo) => {
const event = mouseEnterInfo.event;
if (!this.isKimaiSource(event)) {
// TODO allow to copy into kimai
return;
}
const element = mouseEnterInfo.el;
const popoverTitle = DATES.getFormattedDate(event.start) + ' | ' + DATES.formatTime(event.start) + ' - ' + (event.end ? DATES.formatTime(event.end) : '');
const popoverContent = this.renderEventPopoverContent(event);
let popover = Popover.getInstance(element);
if (popover !== null) {
// see https://github.com/kimai/kimai/issues/4043
popover.setContent({
'.popover-header': popoverTitle,
'.popover-body': popoverContent
});
} else {
// https://getbootstrap.com/docs/5.0/components/popovers/#options
popover = new Popover(element, {
title: popoverTitle,
placement: 'top',
html: true,
content: popoverContent,
trigger: 'focus',
});
}
popover.show();
},
// HIDE POPOVER
eventMouseLeave: (mouseLeaveInfo) => {
if (!this.isKimaiSource(mouseLeaveInfo.event)) {
return;
}
this.hidePopover(mouseLeaveInfo.el);
},
// ContextMenu
eventDidMount: (arg) => {
arg.el.addEventListener('contextmenu', (jsEvent) => {
jsEvent.preventDefault();
const event = arg.event;
if (!event.allDay) {
const url = this.options.url.actions(event.extendedProps.timesheet);
API.get(url, {}, result => {
const contextMenu = new KimaiContextMenu('calendar_contextMenu');
contextMenu.createFromApi(jsEvent, result);
}, (e) => { console.log('Failed to load actions for context menu', e); });
}
});
},
// called after all events of one source were set, so this can
// and will be called multiple times before the calendar is initialized
eventsSet: (events) => {
this._renderDayAndWeekSum(this.getCalendar().getCurrentData().viewSpec.type, events);
}
};
// ============= DRAG & DROP =============
if (!this.hasPermission('punch') && this.hasPermission('create') && this.options.dragdrop !== undefined) {
const draggableList = [].slice.call(document.querySelectorAll(this.options.dragdrop.container));
draggableList.map((containerEl) => {
return new Draggable(containerEl, {
itemSelector: this.options.dragdrop.items
});
});
calendarOptions = {...calendarOptions, ...{
droppable: true,
// drop function handles external draggable events
drop: (dropInfo) => {
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
const entry = dropInfo.draggedEl;
const source = entry.parentElement;
let data = JSON.parse(entry.dataset.entry);
const urlReplacer = JSON.parse(source.dataset.routeReplacer);
let apiUrl = source.dataset.route;
for (const [key, value] of Object.entries(urlReplacer)) {
apiUrl = apiUrl.replace(key, data[value]);
}
let begin = dropInfo.date;
if (dropInfo.view.type === 'dayGridMonth') {
let defaultStartTime = this.options.defaultStartTime;
if (defaultStartTime === null) {
const now = new Date();
defaultStartTime = (now.getHours() < 10 ? '0' : '') + now.getHours() + ':' + (now.getMinutes() < 10 ? '0' : '') + now.getMinutes();
}
begin = DATES.addHumanDuration(begin, defaultStartTime);
}
let end = DATES.addHumanDuration(begin, this.options['slotDuration']);
if (!this.hasPermission('punch')) {
if (this.hasPermission('edit_begin')) {
data.begin = DATES.formatForAPI(begin);
}
if (this.hasPermission('edit_end')) {
data.end = DATES.formatForAPI(end);
}
}
data = this.options.preparePayloadForUpdate(data);
if (source.dataset.method === 'PATCH') {
API.patch(
apiUrl,
JSON.stringify(data),
(result) => {
const newItem = this.convertSourceForCalendar(result);
this.getCalendar().addEvent(newItem, true);
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
}
);
} else {
API.post(
apiUrl,
JSON.stringify(data),
(result) => {
const newItem = this.convertSourceForCalendar(result);
this.getCalendar().addEvent(newItem, true);
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
}
);
}
},
}};
}
// ============= CREATE NEW RECORDS =============
// After click or selection, not allowed for everyone
if (!this.hasPermission('punch') && this.hasPermission('create')) {
calendarOptions = {...calendarOptions, ...{
dateClick: (dateClickInfo) => {
// Day-clicks are always triggered, unless a selection was created.
// So clicking in a day (month view) or any slot (week and day view) will trigger a dayClick
// BEFORE triggering a select - make sure not two create dialogs are requested
if (dateClickInfo.view.type !== 'dayGridMonth') {
return;
}
const createUrl = this.options.url.create(dateClickInfo.dateStr);
MODAL.openUrlInModal(createUrl);
},
selectable: true,
select: (selectionInfo) => {
if(selectionInfo.view.type === 'dayGridMonth') {
// Multi-day clicks are NOT allowed in the month view, as simple day clicks would also trigger
// a select - there is no way to distinguish a simple click and a two-day selection
return;
}
const createUrl = this.options.url.create(selectionInfo.startStr, selectionInfo.endStr);
MODAL.openUrlInModal(createUrl);
},
}};
}
// ============= EDIT TIMESHEET =============
if (this.hasPermission('edit')) {
calendarOptions = {...calendarOptions, ...{
eventClick: (eventClickInfo) => {
const event = eventClickInfo.event;
if (!this.isKimaiSource(event)) {
eventClickInfo.jsEvent.preventDefault();
return;
}
this.hidePopover(eventClickInfo.el);
if (!event.extendedProps.exported || this.hasPermission('edit_exported')) {
MODAL.openUrlInModal(
this.options.url.edit(event.id), (reason) => {
// 403 = user is not allowed to edit the entry (e.g. lockdown mode)
if (reason.status !== 403) {
// keep the log, it might help with debugging
console.log(reason);
}
}
);
}
},
}};
// UPDATE TIMESHEET - MOVE THEM OR EXTEND THEM
if (!this.hasPermission('punch')) {
calendarOptions = {...calendarOptions, ...{
// https://fullcalendar.io/docs/event-dragging-resizing
dragRevertDuration: 0,
eventStartEditable: this.hasPermission('edit_begin'),
eventDurationEditable: this.hasPermission('edit_end') || this.hasPermission('edit_duration'),
eventDragStart: (info) => {
this.hidePopover(info.el);
},
eventDrop: (eventDropInfo) => {
this.changeHandler(eventDropInfo);
},
eventResizeStart: (info) => {
this.hidePopover(info.el);
},
eventResize: (eventResizeInfo) => {
this.changeHandler(eventResizeInfo);
},
}};
}
}
// ============= GOOGLE CALENDAR =============
if (this.options['googleCalendarApiKey'] !== undefined) {
calendarOptions = {...calendarOptions, ...{
// https://fullcalendar.io/docs/google-calendar
googleCalendarApiKey: this.options['googleCalendarApiKey'],
}};
}
// ============= EVENT SOURCES =============
let eventSources = [];
for (const source of this.options['eventSources']) {
let calendarSource = {};
if (source.type === 'timesheet') {
calendarSource = {...calendarSource, ...{
id: 'kimai-' + source.id,
events: (fetchInfo, successCallback, failureCallback) => {
const targetFrom = DATES.formatForAPI(fetchInfo.start);
const targetTo = DATES.formatForAPI(fetchInfo.end);
let url = source.url;
url = url.replace('{from}', targetFrom);
url = url.replace('__FROM__', targetFrom);
url = url.replace('{to}', targetTo);
url = url.replace('__TO__', targetTo);
API.get(url, {}, result => {
let apiEvents = [];
for (const record of result) {
apiEvents.push(this.convertSourceForCalendar(record));
}
successCallback(apiEvents);
}, failureCallback);
},
}};
} else if (source.type === 'google') {
calendarSource = {...calendarSource, ...{
id: 'google-' + source.id,
name: 'google',
editable: false,
}};
} else if (source.type === 'json') {
calendarSource = {...calendarSource, ...{
id: 'json-' + source.id,
editable: false,
events: (fetchInfo, successCallback, failureCallback) => {
const targetFrom = DATES.formatForAPI(fetchInfo.start);
const targetTo = DATES.formatForAPI(fetchInfo.end);
let url = source.url;
url = url.replace('{from}', targetFrom);
url = url.replace('__FROM__', targetFrom);
url = url.replace('{to}', targetTo);
url = url.replace('__TO__', targetTo);
API.get(url, {}, result => {
let apiEvents = [];
for (const record of result) {
apiEvents.push(record);
}
successCallback(apiEvents);
}, failureCallback);
},
}};
} else if (source.type === 'ical') {
calendarSource = {...calendarSource, ...{
id: 'ical-' + source.id,
url: source.url,
format: 'ics',
editable: false,
}};
} else {
console.log('Unknown source type given, skipping to load events from: ' + source.id);
continue;
}
if (source.options !== undefined) {
calendarSource = {...calendarSource, ...source.options};
}
eventSources.push(calendarSource);
}
if (eventSources.length > 0) {
calendarOptions = {...calendarOptions, ...{
eventSources: eventSources,
}};
}
// INITIALIZE CALENDAR
this.calendar = new Calendar(element, calendarOptions);
}
/**
* @param {EventApi} event
* @return {boolean}
* @private
*/
isKimaiSource(event) {
if (event === null) {
return false;
}
if (event.source === null) {
return false;
}
return (event.source.id.indexOf('kimai-') === 0);
}
/**
* @param {string} viewName
* @returns {string}
*/
toExternalViewName(viewName) {
switch(viewName) {
case 'timeGridDay':
return 'day';
case 'timeGridWeek':
return 'week';
case 'dayGridMonth':
default:
return 'month';
}
}
/**
* @param {string} viewName
* @returns {string}
*/
toInternalViewName(viewName) {
switch(viewName) {
case 'day':
case 'agendaDay':
case 'timeGridDay':
return 'timeGridDay';
case 'week':
case 'agendaWeek':
case 'timeGridWeek':
return 'timeGridWeek';
case 'month':
case 'agendaMonth':
case 'dayGridMonth':
default:
return 'dayGridMonth';
}
}
/**
* @param {string} name
* @return {boolean}
* @private
*/
hasPermission(name) {
return this.options['permissions'][name];
}
/**
* @return {Calendar}
*/
getCalendar() {
return this.calendar;
}
render() {
this.calendar.render();
}
reloadEvents() {
this.calendar.getEventSources().forEach(source => source.refetch());
}
/**
* Only used on manipulated timesheets!
*
* @param {object} apiItem
* @return {{activity, color: *, start, description, project, end, id, title: *, textColor: *, customer, tags: ([number,number,[],string,string]|*)}}
* @private
*/
convertSourceForCalendar(apiItem) {
const defaultColor = this.kimai.getConfiguration().get('defaultColor');
let color = apiItem.activity.color;
if (color === null || color === defaultColor) {
color = apiItem.project.color;
if (color === null || color === defaultColor) {
color = apiItem.project.customer.color;
}
}
if (color === null) {
color = defaultColor;
}
/** @type {KimaiDateUtils} DATES */
const DATES = this.kimai.getPlugin('date');
let title = this.options['patterns']['title'];
title = title.replace('{project}', apiItem.project.name);
title = title.replace('{customer}', apiItem.project.customer.name);
title = title.replace('{description}', apiItem.description ?? '');
title = title.replace('{activity}', apiItem.activity.name ?? '');
if (apiItem.end === null) {
// duration = 0 and end = null => is a running entry
title = title.replace('{duration}', '');
} else {
title = title.replace('{duration}', DATES.formatDuration(apiItem.duration));
}
if (title === '' || title === null) {
title = apiItem.activity.name;
}
return {
id: apiItem.id,
timesheet: apiItem.id,
title: title,
description: apiItem.description,
exported: apiItem.exported,
start: apiItem.begin,
end: apiItem.end,
activity: apiItem.activity.name,
project: apiItem.project.name,
customer: apiItem.project.customer.name,
tags: apiItem.tags,
color: color,
textColor: KimaiColor.calculateContrastColor(color),
};
}
/**
* @param {EventApi} event
* @return {string}
* @private
*/
renderEventPopoverContent(event) {
const eventObj = event.extendedProps;
/** @type {KimaiEscape} escaper */
const escaper = this.kimai.getPlugin('escape');
let tags = '';
if (eventObj.tags !== null && eventObj.tags.length > 0) {
for (let tag of eventObj.tags) {
tags += '<span class="badge bg-green">' + escaper.escapeForHtml(tag) + '</span>';
}
}
return escaper.sanitize(`
<div class="calendar-entry">
<ul>
<li>` + this.options['translations']['customer'] + `: ` + escaper.escapeForHtml(eventObj.customer) + `</li>
<li>` + this.options['translations']['project'] + `: ` + escaper.escapeForHtml(eventObj.project) + `</li>
<li>` + this.options['translations']['activity'] + `: ` + escaper.escapeForHtml(eventObj.activity) + `</li>
</ul>` +
(eventObj.description !== null || eventObj.tags.length > 0 ? '<hr>' : '') +
(eventObj.description ? '<div>' + escaper.escapeForHtml(eventObj.description) + '</div>' : '') + tags + `
</div>`);
}
/**
* @param {HTMLElement} element
* @private
*/
hidePopover(element) {
let popover = Popover.getInstance(element);
if (popover !== null) {
popover.hide();
}
}
/**
* @param {EventDropArg} eventArg
* @private
*/
changeHandler(eventArg) {
/** @type {EventApi} event */
const event = eventArg.event;
if (event.extendedProps.exported && !this.hasPermission('edit_exported')) {
eventArg.revert();
return;
}
/** @type {KimaiAPI} API */
const API = this.kimai.getPlugin('api');
/** @type {KimaiDateUtils} DATE */
const DATES = this.kimai.getPlugin('date');
let payload = {'begin': DATES.formatForAPI(event.start)};
if (event.end !== null && event.end !== undefined) {
payload.end = DATES.formatForAPI(event.end);
} else {
payload.end = null;
}
document.dispatchEvent(new CustomEvent('kimai.reloadContent'));
const updateUrl = this.options.url.update(event.id);
API.patch(updateUrl, JSON.stringify(payload), () => {
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
}, (error) => {
eventArg.revert();
document.dispatchEvent(new CustomEvent('kimai.reloadedContent'));
API.handleError('action.update.error', error);
});
}
/**
* @param {string} view
* @param {EventApi[]} events
* @private
*/
_renderDayAndWeekSum(view, events) {
if (view === 'dayGridMonth') {
// currently we do not display totals in month view
return;
}
/** @type {KimaiDateUtils} DATES */
const DATES = this.kimai.getPlugin('date');
const durations = {};
if (view === 'timeGridWeek') {
// make sure we have an entry for every day of the week, even days without timesheets
document.querySelectorAll(`th.fc-col-header-cell[data-date]`).forEach(cell => {
durations[cell.dataset.date] = 0;
});
}
events.forEach(item => {
const start = DateTime.fromJSDate(item.start).toUTC();
const dateStr = start.toFormat('yyyy-MM-dd');
if (!durations[dateStr]) {
durations[dateStr] = 0;
}
// absences or public holidays are all day
if (item.end !== null) {
const end = DateTime.fromJSDate(item.end).toUTC();
const duration = end.diff(start, 'hours').as('seconds');
durations[dateStr] += duration;
}
});
const dailyTotals = document.querySelectorAll('.fc-dailytotal');
dailyTotals.forEach(element => element.remove());
for (const dateValue in durations) {
const durationValue = durations[dateValue];
if (view === 'timeGridWeek') { // this is the week view
const headerCells = document.querySelectorAll(`th.fc-col-header-cell[data-date="${dateValue}"]`);
headerCells.forEach(cell => {
const newElement = document.createElement('div');
newElement.classList.add('fc-dailytotal');
newElement.textContent = DATES.formatSeconds(durationValue);
cell.appendChild(newElement);
});
}
}
// this is the day view
if (view === 'timeGridDay') {
const dayEl = document.querySelector('th.fc-day');
const dayDate = dayEl.dataset.date;
const dayTotal = document.querySelectorAll('.fc-dailytotal');
dayTotal.forEach(element => element.remove());
const newElement = document.createElement('div');
newElement.classList.add('fc-dailytotal');
newElement.textContent = DATES.formatSeconds(durations[dayDate]);
dayEl.appendChild(newElement);
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiColor: handle colors
*/
export default class KimaiColor {
/**
* @param {string} hexcolor
* @return {string}
*/
static calculateContrastColor(hexcolor)
{
if (hexcolor.slice(0, 1) === '#') {
hexcolor = hexcolor.slice(1);
}
if (hexcolor.length === 3) {
hexcolor = hexcolor.split('').map(function (hex) { return hex + hex; }).join('');
}
const r = parseInt(hexcolor.substring(0,2),16);
const g = parseInt(hexcolor.substring(2,4),16);
const b = parseInt(hexcolor.substring(4,6),16);
// https://gomakethings.com/dynamically-changing-the-text-color-based-on-background-color-contrast-with-vanilla-js/
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
return (yiq >= 128) ? '#000000' : '#ffffff';
}
}

View File

@@ -0,0 +1,145 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiContextMenu: help to create, position and display context menus
*/
export default class KimaiContextMenu {
/**
* @param {string} id
*/
constructor(id)
{
this.id = id;
}
/**
* @returns {HTMLElement}
*/
getContextMenuElement()
{
if (document.getElementById(this.id) === null) {
const temp = document.createElement('div');
temp.id = this.id;
temp.classList.add('dropdown-menu', 'd-none');
document.body.appendChild(temp);
}
return document.getElementById(this.id);
}
/**
* @param {MouseEvent} event
* @param {object} json
*/
createFromApi(event, json)
{
let html = '';
for (const options of json) {
if (options['divider'] === true) {
html += '<div class="dropdown-divider"></div>';
}
if (options['url'] !== null) {
html += '<a class="dropdown-item ' + (options['class'] !== null ? options['class'] : '') + '" href="' + options['url'] + '"';
if (options['attr'] !== undefined) {
for (const attrName in options['attr']) {
html += ' ' + attrName + '="' + options['attr'][attrName].replaceAll('"', '&quot;') + '"';
}
}
html += '>' + options['title'] + '</a>';
}
}
this.createFromClickEvent(event, html);
}
/**
* @param {MouseEvent} event
* @param {string} html
*/
createFromClickEvent(event, html)
{
const dropdownElement = this.getContextMenuElement();
if (!dropdownElement.classList.contains('action-dropdown')) {
dropdownElement.classList.add('action-dropdown');
}
dropdownElement.innerHTML = html;
dropdownElement.style.position = 'fixed';
dropdownElement.style.top = (event.clientY) + 'px';
dropdownElement.style.left = (event.clientX) + 'px';
const dropdownListener = (event) => {
if (event.target.classList.contains('dropdown-toggle') || event.target.classList.contains('dropdown-divider')) {
return;
}
dropdownElement.classList.remove('d-block');
if (!dropdownElement.classList.contains('d-none')) {
dropdownElement.classList.add('d-none');
}
dropdownElement.removeEventListener('click', dropdownListener);
document.removeEventListener('click', dropdownListener);
};
dropdownElement.addEventListener('click', dropdownListener);
document.addEventListener('click', dropdownListener);
dropdownElement.classList.remove('d-none');
if (!dropdownElement.classList.contains('d-block')) {
dropdownElement.classList.add('d-block');
}
}
/**
* @param {string} selector
*/
static createForDataTable(selector)
{
[].slice.call(document.querySelectorAll(selector)).map((dataTable) => {
const actions = dataTable.querySelector('td.actions div.dropdown-menu');
if (actions === null) {
return;
}
dataTable.addEventListener('contextmenu', (jsEvent) => {
let target = jsEvent.target;
while (target !== null) {
const tagName = target.tagName.toUpperCase();
if (tagName === 'TH' || tagName === 'TABLE' || tagName === 'BODY') {
return;
}
if (tagName === 'TR') {
break;
}
target = target.parentNode;
}
if (target === null || !target.matches('table.dataTable tbody tr')) {
return;
}
const actions = target.querySelector('td.actions div.dropdown-menu');
if (actions === null) {
return;
}
jsEvent.preventDefault();
const contextMenu = new KimaiContextMenu(dataTable.dataset['contextMenu']);
contextMenu.createFromClickEvent(jsEvent, actions.innerHTML);
});
});
}
}

View File

@@ -1,28 +0,0 @@
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*!
* [KIMAI] KimaiCookies: simple wrapper to handle cookies
*/
import Cookies from 'js-cookie';
export default class KimaiCookies {
static set(name, values, options) {
Cookies.set(name, values, options);
}
static get(name) {
return Cookies.getJSON(name);
}
static remove(name) {
Cookies.remove(name);
}
}

Some files were not shown because too many files have changed in this diff Show More